diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 004add68de..2f6f88d0df 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,11 +1,4 @@ -# [Choice] PHP version: 7, 7.4, 7.3 -ARG VARIANT=7 -FROM mcr.microsoft.com/vscode/devcontainers/php:${VARIANT} - -# [Option] Install Node.js -ARG INSTALL_NODE="true" -ARG NODE_VERSION="lts/*" -RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi +FROM mcr.microsoft.com/vscode/devcontainers/php:8.1 # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ @@ -17,11 +10,4 @@ RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/shar # PHP memory limit RUN echo "memory_limit=768M" > /usr/local/etc/php/php.ini -# Composer v2 -RUN EXPECTED_CHECKSUM="$(wget -q -O - https://composer.github.io/installer.sig)" \ - && php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \ - && ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" \ - && if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then >&2 echo 'ERROR: Invalid installer checksum'; rm composer-setup.php; exit 1; fi \ - && php composer-setup.php --version=2.0.0-RC1 \ - && php -r "unlink('composer-setup.php');" \ - && mv composer.phar /usr/local/bin/composer +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer diff --git a/.devcontainer/base.Dockerfile b/.devcontainer/base.Dockerfile deleted file mode 100644 index 5c57924435..0000000000 --- a/.devcontainer/base.Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -# [Choice] PHP version: 7, 7.4, 7.3 -ARG VARIANT=7 -FROM php:${VARIANT}-apache - -# [Option] Install zsh -ARG INSTALL_ZSH="true" -# [Option] Upgrade OS packages to their latest versions -ARG UPGRADE_PACKAGES="true" - -# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID -COPY library-scripts/common-debian.sh /tmp/library-scripts/ -RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ - && bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \ - && apt-get -y install --no-install-recommends lynx \ - && usermod -aG www-data ${USERNAME} \ - && sed -i -e "s/Listen 80/Listen 80\\nListen 8080/g" /etc/apache2/ports.conf \ - && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts - -# Install xdebug -RUN yes | pecl install xdebug \ - && echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > /usr/local/etc/php/conf.d/xdebug.ini \ - && echo "xdebug.remote_enable=on" >> /usr/local/etc/php/conf.d/xdebug.ini \ - && echo "xdebug.remote_autostart=on" >> /usr/local/etc/php/conf.d/xdebug.ini \ - && rm -rf /tmp/pear - -# Install composer -RUN curl -sSL https://getcomposer.org/installer | php \ - && chmod +x composer.phar \ - && mv composer.phar /usr/local/bin/composer - -# [Option] Install Node.js -ARG INSTALL_NODE="true" -ARG NODE_VERSION="none" -ENV NVM_DIR=/usr/local/share/nvm -ENV NVM_SYMLINK_CURRENT=true \ - PATH=${NVM_DIR}/current/bin:${PATH} -COPY library-scripts/node-debian.sh /tmp/library-scripts/ -RUN if [ "$INSTALL_NODE" = "true" ]; then /bin/bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}"; fi \ - && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts - -# [Optional] Uncomment this section to install additional packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e2f0ccb650..ff13dd64a3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,13 +1,7 @@ { "name": "PHP", "build": { - "dockerfile": "Dockerfile", - "args": { - // Update VARIANT to pick a PHP version: 7, 7.4, 7.3 - "VARIANT": "7.4", - "INSTALL_NODE": "false", - "NODE_VERSION": "lts/*" - } + "dockerfile": "Dockerfile" }, // Set *default* container specific settings.json values on container create. diff --git a/.devcontainer/library-scripts/node-debian.sh b/.devcontainer/library-scripts/node-debian.sh index f35e77fe1b..d230a14e82 100644 --- a/.devcontainer/library-scripts/node-debian.sh +++ b/.devcontainer/library-scripts/node-debian.sh @@ -18,7 +18,7 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi -# Treat a user name of "none" or non-existant user as root +# Treat a user name of "none" or non-existent user as root if [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then USERNAME=root fi diff --git a/.editorconfig b/.editorconfig index 92ecb85c25..0dc4814a91 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,10 @@ trim_trailing_whitespace = true indent_style = tab indent_size = 4 +[bin/phpstan] +indent_style = tab +indent_size = 4 + [*.xml] indent_style = tab indent_size = 4 diff --git a/.github/renovate.json b/.github/renovate.json index e23901f133..59522a2dbc 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -3,7 +3,10 @@ "config:base", "schedule:weekly" ], + "dependencyDashboard": true, "rangeStrategy": "update-lockfile", + "rebaseWhen": "conflicted", + "baseBranches": ["2.1.x"], "packageRules": [ { "matchPackagePatterns": ["*"], @@ -12,13 +15,28 @@ { "matchPaths": ["+(composer.json)"], "enabled": true, - "groupName": "root-composer" + "matchBaseBranches": ["2.1.x"] }, { "matchPaths": ["build-cs/**"], "enabled": true, "groupName": "build-cs" }, + { + "matchPaths": ["apigen/**"], + "enabled": true, + "groupName": "apigen" + }, + { + "matchPaths": ["issue-bot/**"], + "enabled": true, + "groupName": "issue-bot" + }, + { + "matchPaths": ["changelog-generator/**"], + "enabled": true, + "groupName": "changelog-generator" + }, { "matchPaths": ["compiler/**"], "enabled": true, diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml new file mode 100644 index 0000000000..24ba44284b --- /dev/null +++ b/.github/workflows/apiref.yml @@ -0,0 +1,108 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "API Reference" + +on: + workflow_dispatch: + push: + branches: + - "2.1.x" + paths: + - 'src/**' + - 'composer.lock' + - 'apigen/**' + - '.github/workflows/apiref.yml' + +env: + COMPOSER_ROOT_VERSION: "2.1.x-dev" + +concurrency: + group: apigen-${{ github.ref }} # will be canceled on subsequent pushes in branch + cancel-in-progress: true + +jobs: + apigen: + name: "Run ApiGen" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Install ApiGen dependencies" + working-directory: "apigen" + run: "composer install --no-interaction --no-progress" + + - name: "Run ApiGen" + run: "apigen/vendor/bin/apigen -c apigen/apigen.neon --output docs -- src vendor/nikic/php-parser vendor/ondrejmirtes/better-reflection vendor/phpstan/phpdoc-parser" + + - name: "Upload docs" + uses: actions/upload-artifact@v4 + with: + name: docs + path: docs + + deploy: + name: "Deploy" + needs: + - apigen + if: github.repository_owner == 'phpstan' + runs-on: "ubuntu-latest" + steps: + - name: "Install Node" + uses: actions/setup-node@v4 + with: + node-version: "16" + + - name: "Download docs" + uses: actions/download-artifact@v4 + with: + name: docs + path: docs + + - name: "Sync with S3" + uses: jakejarvis/s3-sync-action@v0.5.1 + with: + args: --exclude '.git*/*' --follow-symlinks + env: + SOURCE_DIR: './docs' + DEST_DIR: ${{ github.ref_name }} + AWS_REGION: 'eu-west-1' + AWS_S3_BUCKET: "web-apiref.phpstan.org" + AWS_ACCESS_KEY_ID: ${{ secrets.APIREF_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.APIREF_AWS_SECRET_ACCESS_KEY }} + + - name: "Invalidate CloudFront" + uses: chetan/invalidate-cloudfront-action@v2 + env: + DISTRIBUTION: "E37G1C2KWNAPBD" + PATHS: '/${{ github.ref_name }}/*' + AWS_REGION: 'eu-west-1' + AWS_ACCESS_KEY_ID: ${{ secrets.APIREF_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.APIREF_AWS_SECRET_ACCESS_KEY }} + + - uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + repository: "phpstan/phpstan" + event-type: check_website_links + + - name: "Check for broken links" + uses: ScholliYT/Broken-Links-Crawler-Action@v3 + with: + website_url: 'https://apiref.phpstan.org/$%7B%7B%20github.ref_name%20%7D%7D/index.html' + resolve_before_filtering: 'true' + verbose: 'warning' + max_retry_time: 30 + max_retries: 5 diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index c0bdcb9480..541d15addc 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,10 +6,14 @@ on: pull_request: push: branches: - - "master" + - "2.1.x" + paths: + - 'src/**' + - '.github/workflows/backward-compatibility.yml' -env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" +concurrency: + group: bc-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: backward-compatibility: @@ -20,7 +24,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -28,7 +32,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.1" - name: "Install dependencies" run: "composer install --no-dev --no-interaction --no-progress" @@ -37,7 +41,7 @@ jobs: run: | composer global config minimum-stability dev composer global config prefer-stable true - composer global require --dev ondrejmirtes/backward-compatibility-check:^5.0.8 + composer global require --dev ondrejmirtes/backward-compatibility-check:^7.3.0.1 - name: "Check" run: "$(composer global config bin-dir --absolute)/roave-backward-compatibility-check" diff --git a/.github/workflows/block-merge-commits.yml b/.github/workflows/block-merge-commits.yml new file mode 100644 index 0000000000..2399d07570 --- /dev/null +++ b/.github/workflows/block-merge-commits.yml @@ -0,0 +1,15 @@ +on: pull_request + +name: Block merge commits + +jobs: + message-check: + name: Block Merge Commits + + runs-on: ubuntu-latest + + steps: + - name: Block Merge Commits + uses: Morishiri/block-merge-commits-action@v1.0.1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-issue-bot.yml b/.github/workflows/build-issue-bot.yml new file mode 100644 index 0000000000..0c6904033b --- /dev/null +++ b/.github/workflows/build-issue-bot.yml @@ -0,0 +1,54 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Build Issue Bot" + +on: + pull_request: + paths: + - 'issue-bot/**' + - '.github/workflows/build-issue-bot.yml' + push: + branches: + - "2.1.x" + paths: + - 'issue-bot/**' + - '.github/workflows/build-issue-bot.yml' + +concurrency: + group: build-issue-bot-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +jobs: + build-issue-bot: + name: "Build Issue Bot" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + script: + - "../bin/phpstan" + - "vendor/bin/phpunit" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Install Issue Bot dependencies" + working-directory: "issue-bot" + run: "composer install --no-interaction --no-progress" + + - name: "Tests" + working-directory: "issue-bot" + run: ${{ matrix.script }} diff --git a/.github/workflows/changelog-generator.yml b/.github/workflows/changelog-generator.yml new file mode 100644 index 0000000000..aaa121a682 --- /dev/null +++ b/.github/workflows/changelog-generator.yml @@ -0,0 +1,47 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Build Changelog Generator" + +on: + pull_request: + paths: + - 'changelog-generator/**' + - '.github/workflows/changelog-generator.yml' + push: + branches: + - "2.1.x" + paths: + - 'changelog-generator/**' + - '.github/workflows/changelog-generator.yml' + +concurrency: + group: changelog-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +jobs: + changelog-generator: + name: "Build Changelog Generator" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Install Changelog Generator dependencies" + working-directory: "changelog-generator" + run: "composer install --no-interaction --no-progress" + + - name: "PHPStan" + working-directory: "changelog-generator" + run: "../bin/phpstan" diff --git a/.github/workflows/checksum-phar.yml b/.github/workflows/checksum-phar.yml new file mode 100644 index 0000000000..185fc779b4 --- /dev/null +++ b/.github/workflows/checksum-phar.yml @@ -0,0 +1,124 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +# This workflow checks that PHAR checksum changes only when it's supposed to +# It should stay the same when the PHAR contents do not change + +name: "Check PHAR checksum" + +on: + pull_request: + paths: + - 'compiler/**' + - '.github/workflows/checksum-phar.yml' + push: + branches: + - "2.1.x" + paths: + - 'compiler/**' + - '.github/workflows/checksum-phar.yml' + +concurrency: + group: checksum-phar-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +jobs: + check-phar-checksum: + name: "Check PHAR checksum" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + steps: + - name: "Checkout phpstan-dist" + uses: actions/checkout@v4 + with: + repository: phpstan/phpstan + path: phpstan-dist + ref: 2.1.x + + - name: "Get info" + id: info + working-directory: phpstan-dist + run: | + echo "checksum=$(head -n 1 .phar-checksum)" >> $GITHUB_OUTPUT + echo "commit=$(tail -n 1 .phar-checksum)" >> $GITHUB_OUTPUT + + - name: "Delete phpstan-dist" + run: "rm -r phpstan-dist" + + - name: "Checkout" + uses: actions/checkout@v4 + with: + ref: ${{ steps.info.outputs.commit }} + + - name: "Checkout latest PHAR compiler" + uses: actions/checkout@v4 + with: + path: phpstan-src + ref: ${{ github.sha }} + + - name: "Delete old compiler" + run: "rm -r compiler" + + - name: "Move new compiler" + run: "mv phpstan-src/compiler/ ." + + - name: "Delete phpstan-src" + run: "rm -r phpstan-src" + + - name: "Change and commit README.md" + run: | + echo Testing > README.md + git config --global user.name "phpstan-bot" + git config --global user.email "ondrej+phpstanbot@mirtes.cz" + git commit -a -m 'Changed README' + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + extensions: mbstring, intl + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Install compiler dependencies" + run: "composer install --no-interaction --no-progress --working-dir=compiler" + + # same steps as in phar.yml + + - name: "Prepare for PHAR compilation" + working-directory: "compiler" + run: "php bin/prepare" + + - name: "Set autoloader suffix" + run: "composer config autoloader-suffix PHPStanChecksum" + + - name: "Composer dump" + run: "composer install --no-interaction --no-progress" + env: + COMPOSER_ROOT_VERSION: "2.1.x-dev" + + - name: "Compile PHAR for checksum" + working-directory: "compiler/build" + run: "php box.phar compile --no-parallel" + env: + PHAR_CHECKSUM: "1" + COMPOSER_ROOT_VERSION: "2.1.x-dev" + + - name: "Re-sign PHAR" + run: "php compiler/build/resign.php tmp/phpstan.phar" + + - name: "Unset autoloader suffix" + run: "composer config autoloader-suffix --unset" + + - name: "Save checksum" + id: "new_checksum" + run: echo "md5=$(md5sum tmp/phpstan.phar | cut -d' ' -f1)" >> $GITHUB_OUTPUT + + - name: "Assert checksum" + run: | + checksum=${{ steps.info.outputs.checksum }} + new_checksum=${{ steps.new_checksum.outputs.md5 }} + [[ "$checksum" == "$new_checksum" ]]; diff --git a/.github/workflows/compiler-tests.yml b/.github/workflows/compiler-tests.yml deleted file mode 100644 index 65e1c2e58d..0000000000 --- a/.github/workflows/compiler-tests.yml +++ /dev/null @@ -1,67 +0,0 @@ -# https://help.github.com/en/categories/automating-your-workflow-with-github-actions - -name: "Compiler tests" - -on: - pull_request: - push: - branches: - - "master" - -env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" - -jobs: - compiler-tests: - name: "Compiler Tests" - - runs-on: "ubuntu-latest" - timeout-minutes: 60 - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - 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: "Tests" - run: | - cd compiler && \ - composer install --no-interaction && \ - vendor/bin/phpunit -c tests/phpunit.xml tests && \ - ../bin/phpstan analyse -l 8 src tests && \ - php bin/compile && \ - ../tmp/phpstan.phar list - - - uses: actions/upload-artifact@v2 - with: - 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@master - with: - ref: master - - extension-tests: - if: github.event_name == 'pull_request' - needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/extension-tests.yml@master - with: - ref: master - - other-tests: - if: github.event_name == 'pull_request' - needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/other-tests.yml@master - with: - ref: master diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml new file mode 100644 index 0000000000..a853501487 --- /dev/null +++ b/.github/workflows/create-tag.yml @@ -0,0 +1,53 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Create tag" + +on: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch + workflow_dispatch: + inputs: + version: + description: 'Next version' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + +jobs: + create-tag: + name: "Create tag" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + + - name: 'Get Previous tag' + id: previoustag + uses: "WyriHaximus/github-action-get-previous-tag@v1" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: 'Get next versions' + id: semvers + uses: "WyriHaximus/github-action-next-semvers@v1" + with: + version: ${{ steps.previoustag.outputs.tag }} + + - name: "Create new minor tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'minor' + with: + tag: ${{ steps.semvers.outputs.minor }} + message: ${{ steps.semvers.outputs.minor }} + + - name: "Create new patch tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'patch' + with: + tag: ${{ steps.semvers.outputs.patch }} + message: ${{ steps.semvers.outputs.patch }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 136ef4969e..e02de443a5 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -6,47 +6,275 @@ on: pull_request: paths-ignore: - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' push: branches: - - "master" + - "2.1.x" paths-ignore: - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' -env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" +concurrency: + group: e2e-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: result-cache-e2e-tests: name: "Result cache E2E tests" - - runs-on: ${{ matrix.operating-system }} - timeout-minutes: 60 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - php-version: - - "8.0" - operating-system: [ubuntu-latest, windows-latest] + include: + - script: | + cd e2e/result-cache-1 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Bar.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Bar.php.orig src/Bar.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-2 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Bar.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Bar.php.orig src/Bar.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-3 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Baz.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Baz.php.orig src/Baz.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-4 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Bar.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Bar.php.orig src/Bar.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-5 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Baz.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Baz.php.orig src/Baz.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-6 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Baz.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Baz.php.orig src/Baz.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-7 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Bar.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Bar.php.orig src/Bar.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/bug10449 + ../../bin/phpstan analyze + git apply patch.diff + rm phpstan-baseline.neon + mv after-phpstan-baseline.neon phpstan-baseline.neon + ../../bin/phpstan analyze -vvv + - script: | + cd e2e/bug10449b + ../../bin/phpstan analyze + git apply patch.diff + rm phpstan-baseline.neon + mv after-phpstan-baseline.neon phpstan-baseline.neon + ../../bin/phpstan analyze -vvv + - script: | + cd e2e/bug-9622 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Foo.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Foo.php.orig src/Foo.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/bug-9622-trait + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Foo.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Foo.php.orig src/Foo.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/env-parameter + export PHPSTAN_RESULT_CACHE_PATH=/some/path + ACTUAL=$(../../bin/phpstan dump-parameters -c phpstan.neon --json -l 9 | jq --raw-output '.resultCachePath') + [[ "$ACTUAL" == "/some/path" ]]; + - script: | + cd e2e/result-cache-8 + composer install + ../../bin/phpstan + echo -en '\n' >> build/CustomRule.php + OUTPUT=$(../../bin/phpstan analyze 2>&1 || true) + echo "$OUTPUT" + ../bashunit -a contains 'Result cache might not behave correctly' "$OUTPUT" + ../bashunit -a contains 'ResultCache8E2E\CustomRule' "$OUTPUT" + - script: | + cd e2e/env-int-key + env 1=1 ../../bin/phpstan analyse test.php + - script: | + cd e2e/trait-caching + ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ + ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ + - script: | + cd e2e/trait-caching + ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ + patch -b data/TraitOne.php < TraitOne.patch + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/") + echo "$OUTPUT" + ../bashunit -a line_count 2 "$OUTPUT" + ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" + ../bashunit -a contains 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.' "$OUTPUT" + - script: | + cd e2e/trait-caching + ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ + patch -b data/TraitTwo.php < TraitTwo.patch + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/") + echo "$OUTPUT" + ../bashunit -a line_count 2 "$OUTPUT" + ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" + ../bashunit -a contains 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.' "$OUTPUT" + - script: | + cd e2e/trait-caching + ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ + patch -b data/TraitOne.php < TraitOne.patch + patch -b data/TraitTwo.php < TraitTwo.patch + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/") + echo "$OUTPUT" + ../bashunit -a line_count 3 "$OUTPUT" + ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" + ../bashunit -a contains 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.' "$OUTPUT" + ../bashunit -a contains 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.' "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c ignore.neon") + echo "$OUTPUT" + ../bashunit -a contains 'Invalid entry in ignoreErrors' "$OUTPUT" + ../bashunit -a contains 'tests" is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c phpneon.php") + echo "$OUTPUT" + ../bashunit -a contains 'Invalid entry in ignoreErrors' "$OUTPUT" + ../bashunit -a contains '"src/test.php" is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c excludePaths.neon") + echo "$OUTPUT" + ../bashunit -a contains 'Invalid entry in excludePaths' "$OUTPUT" + ../bashunit -a contains 'tests" is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c phpneon2.php") + echo "$OUTPUT" + ../bashunit -a contains 'Invalid entry in excludePaths' "$OUTPUT" + ../bashunit -a contains '"src/test.php" is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../../bin/phpstan analyse -c ignoreNonexistentExcludePath.neon) + echo "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + cp -r tmp-node-modules node_modules + OUTPUT=$(../../bin/phpstan analyse -c ignoreNonexistentExcludePath.neon) + echo "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../../bin/phpstan analyse -c ignoreReportUnmatchedFalse.neon) + echo "$OUTPUT" + - script: | + cd e2e/bug-11826 + composer install + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan") + echo "$OUTPUT" + ../bashunit -a contains 'Child process error (exit code 255): PHP Fatal error' "$OUTPUT" + ../bashunit -a contains 'Result is incomplete because of severe errors.' "$OUTPUT" + - script: | + cd e2e/bug-11857 + composer install + ../../bin/phpstan + - script: | + cd e2e/result-cache-meta-extension + composer install + ../../bin/phpstan -vvv + ../../bin/phpstan -vvv --fail-without-result-cache + echo 'modified-hash' > hash.txt + OUTPUT=$(../bashunit -a exit_code "2" "../../bin/phpstan -vvv --fail-without-result-cache") + echo "$OUTPUT" + ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" + ../bashunit -a contains 'Result cache not used because the metadata do not match: metaExtensions' "$OUTPUT" + - script: | + cd e2e/bug-12606 + export CONFIGTEST=test + ../../bin/phpstan + - script: | + cd e2e/ignore-error-extension + composer install + ../../bin/phpstan steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.1" extensions: mbstring ini-values: memory_limit=256M - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Tests" - run: | - git clone https://github.com/nikic/PHP-Parser.git tests/e2e/PHP-Parser && git -C tests/e2e/PHP-Parser checkout v3.1.5 && composer install --working-dir tests/e2e/PHP-Parser && vendor/bin/phpunit tests/e2e/ResultCacheEndToEndTest.php + - name: "Patch PHPStan" + run: "patch src/Analyser/Error.php < e2e/PHPStanErrorPatch.patch" + + - name: "Install bashunit" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.18.0" + + - name: "Test" + run: "${{ matrix.script }}" e2e-tests: name: "E2E tests" @@ -56,30 +284,100 @@ jobs: strategy: matrix: include: - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/timecop.php tests/e2e/data/timecop.php" + - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/timecop.php -c tests/e2e/data/empty.neon tests/e2e/data/timecop.php" tools: "pecl" extensions: "timecop-beta" - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php tests/e2e/data/soap.php" + - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php -c tests/e2e/data/empty.neon tests/e2e/data/soap.php" extensions: "soap" - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php tests/e2e/data/soap.php" + - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php -c tests/e2e/data/empty.neon tests/e2e/data/soap.php" extensions: "" - script: "bin/phpstan analyse -l 8 tests/e2e/anon-class/Granularity.php" extensions: "" + - script: "bin/phpstan analyse -l 8 e2e/phpstan-phpunit-190/test.php -c e2e/phpstan-phpunit-190/test.neon" + extensions: "" + - script: "bin/phpstan analyse e2e/only-files-not-analysed-trait/src -c e2e/only-files-not-analysed-trait/ignore.neon" + extensions: "" + - script: "bin/phpstan analyse e2e/only-files-not-analysed-trait/src/Foo.php e2e/only-files-not-analysed-trait/src/BarTrait.php -c e2e/only-files-not-analysed-trait/no-ignore.neon" + extensions: "" + - script: | + cd e2e/baseline-uninit-prop-trait + ../../bin/phpstan analyse --debug --configuration test-no-baseline.neon --generate-baseline test-baseline.neon + ../../bin/phpstan analyse --debug --configuration test.neon + - script: | + cd e2e/baseline-uninit-prop-trait + ../../bin/phpstan analyse --configuration test-no-baseline.neon --generate-baseline test-baseline.neon + ../../bin/phpstan analyse --configuration test.neon + - script: | + cd e2e/discussion-11362 + composer install + ../../bin/phpstan + - script: | + cd e2e/bug-11819 + ../../bin/phpstan + - script: | + cd e2e/composer-and-phpstan-version-config + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-max-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-max-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-open-end-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version-v5 + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version-v7 + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-no-versions + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-version-config-invalid + OUTPUT=$(../bashunit -a exit_code "1" ../../bin/phpstan) + echo "$OUTPUT" + ../bashunit -a contains 'Invalid configuration' "$OUTPUT" + ../bashunit -a contains 'Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.' "$OUTPUT" + - script: | + cd e2e/composer-version-config-patch + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-version-config + composer install + ../../bin/phpstan analyze test.php --level=0 steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.1" tools: ${{ matrix.tools }} extensions: ${{ matrix.extensions }} - name: "Install dependencies" run: "composer install --no-interaction --no-progress" + - name: "Install bashunit" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.18.0" + - name: "Test" run: ${{ matrix.script }} diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml new file mode 100644 index 0000000000..2366222087 --- /dev/null +++ b/.github/workflows/issue-bot.yml @@ -0,0 +1,172 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Issue bot" + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + push: + branches: + - "2.1.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + +concurrency: + group: run-issue-bot-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +jobs: + download: + name: "Download data" + + runs-on: "ubuntu-latest" + + outputs: + matrix: ${{ steps.download-data.outputs.matrix }} + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - name: "Install Issue Bot dependencies" + working-directory: "issue-bot" + run: "composer install --no-interaction --no-progress" + + - name: "Cache downloads" + uses: actions/cache@v4 + with: + path: ./issue-bot/tmp + key: "issue-bot-download-v7-${{ github.run_id }}" + restore-keys: | + issue-bot-download-v7- + + - name: "Download data" + working-directory: "issue-bot" + id: download-data + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + run: echo "matrix=$(./console.php download)" >> $GITHUB_OUTPUT + + + - uses: actions/upload-artifact@v4 + with: + name: playground-cache + path: issue-bot/tmp/playgroundCache.tmp + + - uses: actions/upload-artifact@v4 + with: + name: issue-cache + path: issue-bot/tmp/issueCache.tmp + + analyse: + name: "Analyse" + needs: download + + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.download.outputs.matrix) }} + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress --no-dev" + + - name: "Install Issue Bot dependencies" + working-directory: "issue-bot" + run: "composer install --no-interaction --no-progress" + + - uses: Wandalen/wretry.action@v3.8.0 + with: + action: actions/download-artifact@v4 + with: | + name: playground-cache + path: issue-bot/tmp + attempt_limit: 5 + attempt_delay: 1000 + + - name: "Run PHPStan" + working-directory: "issue-bot" + timeout-minutes: 5 + run: ./console.php run ${{ matrix.phpVersion }} ${{ matrix.playgroundExamples }} + + - uses: actions/upload-artifact@v4 + with: + name: results-${{ matrix.phpVersion }}-${{ matrix.chunkNumber }} + path: issue-bot/tmp/results-${{ matrix.phpVersion }}-*.tmp + + evaluate: + name: "Evaluate results" + needs: analyse + + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - name: "Install Issue Bot dependencies" + working-directory: "issue-bot" + run: "composer install --no-interaction --no-progress" + + - uses: actions/download-artifact@v4 + with: + name: playground-cache + path: issue-bot/tmp + + - uses: actions/download-artifact@v4 + with: + name: issue-cache + path: issue-bot/tmp + + - uses: actions/download-artifact@v4 + with: + pattern: results-* + merge-multiple: true + path: issue-bot/tmp + + - name: "List tmp" + run: "ls -lA issue-bot/tmp" + + - name: "Evaluate results - pull request" + working-directory: "issue-bot" + if: github.event_name == 'pull_request' + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + run: ./console.php evaluate >> $GITHUB_STEP_SUMMARY + + - name: "Evaluate results - push" + working-directory: "issue-bot" + if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/2.1.x'" + env: + GITHUB_PAT: ${{ secrets.PHPSTAN_BOT_TOKEN }} + PHPSTAN_SRC_COMMIT_BEFORE: ${{ github.event.before }} + PHPSTAN_SRC_COMMIT_AFTER: ${{ github.event.after }} + run: ./console.php evaluate --post-comments >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6c7afe5f5a..d93e59843f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,10 +6,11 @@ on: pull_request: push: branches: - - "master" + - "2.1.x" -env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" +concurrency: + group: lint-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: lint: @@ -21,16 +22,16 @@ jobs: fail-fast: false matrix: php-version: - - "7.1" - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" + - "8.2" + - "8.3" + - "8.4" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -44,23 +45,9 @@ jobs: - name: "Install dependencies" 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 != '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 }}" + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' && matrix.php-version != '8.4' + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - name: "Lint" run: "make lint" @@ -71,20 +58,15 @@ jobs: runs-on: "ubuntu-latest" timeout-minutes: 60 - strategy: - matrix: - php-version: - - "8.0" - steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.1" - name: "Validate Composer" run: "composer validate" @@ -104,23 +86,40 @@ jobs: runs-on: "ubuntu-latest" timeout-minutes: 60 - strategy: - matrix: - php-version: - - "8.0" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Composer Dependency Analyser" + run: "make composer-dependency-analyser" + + name-collision: + name: "Name Collision Detector" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.4" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Composer Require Checker" - run: "make composer-require-checker" + - name: "Name Collision Detector" + run: "make name-collision" diff --git a/.github/workflows/merge-bot-pr.yml b/.github/workflows/merge-bot-pr.yml index aa8c91d70f..6d34bb3d80 100644 --- a/.github/workflows/merge-bot-pr.yml +++ b/.github/workflows/merge-bot-pr.yml @@ -16,7 +16,7 @@ jobs: id: waitforstatuschecks uses: "WyriHaximus/github-action-wait-for-status@v1" with: - ignoreActions: automerge + ignoreActions: "automerge,Automerge PRs" checkInterval: 13 env: GITHUB_TOKEN: "${{ secrets.PHPSTAN_BOT_TOKEN }}" diff --git a/.github/workflows/merge-maintained-branch.yml b/.github/workflows/merge-maintained-branch.yml new file mode 100644 index 0000000000..0ac13c5f68 --- /dev/null +++ b/.github/workflows/merge-maintained-branch.yml @@ -0,0 +1,24 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: Merge maintained branch + +on: + push: + branches: + - "1.12.x" + +jobs: + merge: + name: Merge branch + if: github.repository_owner == 'phpstan' + runs-on: ubuntu-latest + steps: + - name: "Checkout" + uses: actions/checkout@v4 + - name: "Merge branch" + uses: everlytic/branch-merge@1.1.5 + with: + github_token: "${{ secrets.PHPSTAN_BOT_TOKEN }}" + source_ref: ${{ github.ref }} + target_branch: '2.1.x' + commit_message_template: 'Merge branch {source_ref} into {target_branch}' diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index 678701860a..0cf91034a3 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -3,23 +3,30 @@ name: "Compile PHAR" on: + pull_request: push: branches: - - "master" + - "2.1.x" tags: - - '1.*' + - '2.1.*' -concurrency: phar +concurrency: + group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests + cancel-in-progress: true jobs: - compile: - name: "Compile PHAR" + compiler-tests: + name: "Compiler Tests" + runs-on: "ubuntu-latest" timeout-minutes: 60 + outputs: + checksum: ${{ steps.checksum.outputs.md5 }} + steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -27,7 +34,8 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.1" + extensions: mbstring, intl - name: "Install dependencies" run: "composer install --no-interaction --no-progress" @@ -35,64 +43,205 @@ jobs: - name: "Install compiler dependencies" run: "composer install --no-interaction --no-progress --working-dir=compiler" + - name: "Compiler tests" + working-directory: "compiler" + run: "vendor/bin/phpunit -c tests/phpunit.xml tests" + + - name: "Compiler PHPStan" + working-directory: "compiler" + run: "../bin/phpstan analyse -l 8 src tests" + + - name: "Prepare for PHAR compilation" + working-directory: "compiler" + run: "php bin/prepare" + - name: "Compile PHAR" - run: php compiler/bin/compile + working-directory: "compiler/build" + run: "php box.phar compile --no-parallel" + + - uses: actions/upload-artifact@v4 + with: + name: phar-file + path: tmp/phpstan.phar - - name: "Configure GPG signing key" - run: echo "$GPG_SIGNING_KEY" | base64 --decode | gpg --import --no-tty --batch --yes + - name: "Run PHAR" + working-directory: "compiler" + run: "../tmp/phpstan.phar list" + + - name: "Delete PHAR" + run: "rm tmp/phpstan.phar" + + - name: "Set autoloader suffix" + run: "composer config autoloader-suffix PHPStanChecksum" + + - name: "Composer dump" + run: "composer install --no-interaction --no-progress" env: - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + COMPOSER_ROOT_VERSION: "2.1.x-dev" - - name: "Get Git log" - id: git-log - run: echo ::set-output name=log::$(git log ${{ github.event.before }}..${{ github.event.after }} --reverse --pretty='%H %s' | sed -e 's/^/https:\/\/github.com\/phpstan\/phpstan-src\/commit\//') + - name: "Compile PHAR for checksum" + working-directory: "compiler/build" + run: "php box.phar compile --no-parallel" + env: + PHAR_CHECKSUM: "1" + COMPOSER_ROOT_VERSION: "2.1.x-dev" + + - name: "Re-sign PHAR" + run: "php compiler/build/resign.php tmp/phpstan.phar" + + - name: "Unset autoloader suffix" + run: "composer config autoloader-suffix --unset" + + - name: "Save checksum" + id: "checksum" + run: echo "md5=$(md5sum tmp/phpstan.phar | cut -d' ' -f1)" >> $GITHUB_OUTPUT + + - uses: actions/upload-artifact@v4 + with: + name: phar-file-checksum + path: tmp/phpstan.phar + + - name: "Delete checksum PHAR" + run: "rm tmp/phpstan.phar" + + integration-tests: + if: github.event_name == 'pull_request' + needs: compiler-tests + uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.1.x + with: + ref: 2.1.x + phar-checksum: ${{needs.compiler-tests.outputs.checksum}} + + extension-tests: + if: github.event_name == 'pull_request' + needs: compiler-tests + uses: phpstan/phpstan/.github/workflows/extension-tests.yml@2.1.x + with: + ref: 2.1.x + phar-checksum: ${{needs.compiler-tests.outputs.checksum}} + + other-tests: + if: github.event_name == 'pull_request' + needs: compiler-tests + uses: phpstan/phpstan/.github/workflows/other-tests.yml@2.1.x + with: + ref: 2.1.x + phar-checksum: ${{needs.compiler-tests.outputs.checksum}} + + commit: + name: "Commit PHAR" + if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.1.x' || startsWith(github.ref, 'refs/tags/'))" + needs: compiler-tests + runs-on: "ubuntu-latest" + timeout-minutes: 60 + steps: + - + name: Import GPG key + id: import-gpg + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PHPSTANBOT_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PHPSTANBOT_KEY_PASSPHRASE }} + git_config_global: true + git_user_signingkey: true + git_commit_gpgsign: true - name: "Checkout phpstan-dist" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 with: repository: phpstan/phpstan path: phpstan-dist - token: ${{ secrets.PAT }} + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + ref: 2.1.x + + - name: "Get previous pushed dist commit" + id: previous-commit + working-directory: phpstan-dist + run: echo "sha=$(sed -n '2p' .phar-checksum)" >> $GITHUB_OUTPUT - - name: "cp PHAR" - run: cp tmp/phpstan.phar phpstan-dist/phpstan.phar + - name: "Checkout phpstan-src" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + path: phpstan-src + + - name: "Get Git log" + id: git-log + working-directory: phpstan-src + run: | + echo "log<> $GITHUB_OUTPUT + echo "$(git log ${{ steps.previous-commit.outputs.sha }}..${{ github.event.after }} --reverse --pretty='https://github.com/phpstan/phpstan-src/commit/%H%20%s')" >> $GITHUB_OUTPUT + echo 'MESSAGE' >> $GITHUB_OUTPUT + + - name: "Get short phpstan-src SHA" + id: short-src-sha + working-directory: phpstan-src + run: echo "sha=$(git rev-parse --short=7 HEAD)" >> $GITHUB_OUTPUT + + - name: "Check PHAR checksum" + id: checksum-difference + working-directory: phpstan-dist + run: | + checksum=${{needs.compiler-tests.outputs.checksum}} + if [[ $(head -n 1 .phar-checksum) != "$checksum" ]]; then + echo "result=different" >> $GITHUB_OUTPUT + else + echo "result=same" >> $GITHUB_OUTPUT + fi + + - name: "Download phpstan.phar" + uses: actions/download-artifact@v4 + with: + name: phar-file + + - name: "mv PHAR" + run: mv phpstan.phar phpstan-dist/phpstan.phar + + - name: "chmod PHAR" + run: chmod 755 phpstan-dist/phpstan.phar + + - name: "Update checksum" + run: | + echo ${{needs.compiler-tests.outputs.checksum}} > phpstan-dist/.phar-checksum + echo ${{ github.event.head_commit.id }} >> phpstan-dist/.phar-checksum - name: "Sign PHAR" working-directory: phpstan-dist run: rm phpstan.phar.asc && gpg --command-fd 0 --pinentry-mode loopback -u "$GPG_ID" --batch --detach-sign --armor --output phpstan.phar.asc phpstan.phar env: - GPG_ID: ${{ secrets.GPG_ID }} + GPG_ID: ${{ steps.import-gpg.outputs.fingerprint }} - name: "Verify PHAR" working-directory: phpstan-dist run: "gpg --verify phpstan.phar.asc" - - name: "Set Git signing key" - working-directory: phpstan-dist - run: git config user.signingkey "$GPG_ID" - env: - GPG_ID: ${{ secrets.GPG_ID }} - - - name: "Configure Git" - working-directory: phpstan-dist - run: | - git config user.email "ondrej@mirtes.cz" && \ - git config user.name "Ondrej Mirtes" + - name: "Install lucky_commit" + uses: baptiste0928/cargo-install@v3 + with: + crate: lucky_commit + args: --no-default-features - - name: "Commit PHAR - master" + - name: "Commit PHAR - development" + if: "!startsWith(github.ref, 'refs/tags/') && steps.checksum-difference.outputs.result == 'different'" working-directory: phpstan-dist - if: "!startsWith(github.ref, 'refs/tags/')" + env: + INPUT_LOG: ${{ steps.git-log.outputs.log }} 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 config --global user.name "phpstan-bot" + git config --global user.email "ondrej+phpstanbot@mirtes.cz" + git add . + git commit --gpg-sign -m "Updated PHPStan to commit ${{ github.event.after }}" -m "$INPUT_LOG" --author "phpstan-bot " + lucky_commit ${{ steps.short-src-sha.outputs.sha }} + git push - name: "Commit PHAR - tag" - working-directory: phpstan-dist if: "startsWith(github.ref, 'refs/tags/')" - 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 tag -s ${GITHUB_REF#refs/tags/} -m "${GITHUB_REF#refs/tags/}" && \ - git push --quiet origin ${GITHUB_REF#refs/tags/} + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_user_name: "phpstan-bot" + commit_user_email: "ondrej+phpstanbot@mirtes.cz" + commit_author: "phpstan-bot " + commit_options: "--gpg-sign" + repository: phpstan-dist + commit_message: "PHPStan ${{github.ref_name}}" + tagging_message: ${{github.ref_name}} diff --git a/.github/workflows/pr-base-on-previous-branch.yml b/.github/workflows/pr-base-on-previous-branch.yml new file mode 100644 index 0000000000..34ef71bb83 --- /dev/null +++ b/.github/workflows/pr-base-on-previous-branch.yml @@ -0,0 +1,24 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Base PR on previous branch" + +on: + pull_request_target: + types: + - opened + branches: + - '2.2.x' + + +jobs: + comment: + name: "Comment on pull request" + runs-on: 'ubuntu-latest' + + steps: + - name: Comment PR + uses: peter-evans/create-or-update-comment@v4 + with: + body: "You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x." + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/pr-marked-as-ready.yml b/.github/workflows/pr-marked-as-ready.yml new file mode 100644 index 0000000000..b9785a2a3c --- /dev/null +++ b/.github/workflows/pr-marked-as-ready.yml @@ -0,0 +1,21 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Pull request ready for review" + +on: + pull_request_target: + types: + - ready_for_review + +jobs: + comment: + name: "Comment on pull request" + runs-on: 'ubuntu-latest' + + steps: + - name: Comment PR + uses: peter-evans/create-or-update-comment@v4 + with: + body: "This pull request has been marked as ready for review." + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml new file mode 100644 index 0000000000..8d0050cfd3 --- /dev/null +++ b/.github/workflows/reflection-golden-test.yml @@ -0,0 +1,127 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Reflection golden test" + +on: + pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' + push: + branches: + - "2.1.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' + +env: + REFLECTION_GOLDEN_TEST_FILE: "/tmp/reflection-golden.test" + REFLECTION_GOLDEN_SYMBOLS_FILE: "/tmp/reflection-golden-symbols.txt" + +concurrency: + group: reflection-golden-test-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +jobs: + dump-php-symbols: + name: "Dump PHP symbols" + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + # Include exotic extensions to discover more symbols + extensions: ds,mbstring,runkit7,scoutapm,seaslog,simdjson,var_representation,yac + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Dump phpSymbols.txt" + run: "php tests/dump-reflection-test-symbols.php" + + - uses: actions/upload-artifact@v4 + with: + name: phpSymbols + path: ${{ env.REFLECTION_GOLDEN_SYMBOLS_FILE }} + + reflection-golden-test: + name: "Reflection golden test" + needs: dump-php-symbols + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + + steps: + - uses: Wandalen/wretry.action@v3.8.0 + with: + action: actions/download-artifact@v4 + with: | + name: phpSymbols + path: /tmp + attempt_limit: 5 + attempt_delay: 1000 + + - name: "Checkout base commit" + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha || github.event.before }} + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=2G + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Transform source code" + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' && matrix.php-version != '8.4' + shell: bash + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" + + - name: "Dump previous reflection data" + run: "php tests/generate-reflection-test.php" + + - uses: actions/upload-artifact@v4 + with: + name: reflection-${{ matrix.php-version }}.test + path: ${{ env.REFLECTION_GOLDEN_TEST_FILE }} + + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Transform source code" + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' && matrix.php-version != '8.4' + shell: bash + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" + + - name: "Reflection golden test" + run: "make tests-golden-reflection || true" diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml new file mode 100644 index 0000000000..b2f810732c --- /dev/null +++ b/.github/workflows/spelling.yml @@ -0,0 +1,23 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Spelling" + +on: + pull_request: + push: + branches: + - "2.1.x" + +jobs: + typos: + name: "Check for typos" + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Check for typos" + uses: "crate-ci/typos@v1" + with: + files: "README.md src/" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 543254e867..602152e12f 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -6,14 +6,17 @@ on: pull_request: paths-ignore: - 'compiler/**' + - 'apigen/**' push: branches: - - "master" + - "2.1.x" paths-ignore: - 'compiler/**' + - 'apigen/**' -env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" +concurrency: + group: sa-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: static-analysis: @@ -25,59 +28,36 @@ jobs: fail-fast: false matrix: php-version: - - "7.1" - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" + - "8.2" + - "8.3" + - "8.4" operating-system: [ubuntu-latest, windows-latest] - script: - - "make phpstan" - - "make phpstan-static-reflection" - - "make phpstan-validate-stub-files" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" php-version: "${{ matrix.php-version }}" + ini-file: development extensions: mbstring - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Install PHP for code transform" - if: matrix.php-version != '8.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 != '8.0' && matrix.php-version != '8.1' + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' && matrix.php-version != '8.4' 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" + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - name: "PHPStan" - run: ${{ matrix.script }} + run: "make phpstan" static-analysis-with-result-cache: name: "PHPStan with result cache" @@ -86,29 +66,36 @@ jobs: timeout-minutes: 60 strategy: + fail-fast: false matrix: php-version: - - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" php-version: "${{ matrix.php-version }}" + ini-file: development extensions: mbstring - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - name: "Cache Result cache" - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ./tmp - key: "result-cache-v4" + key: "result-cache-v14-${{ matrix.php-version }}-${{ github.run_id }}" + restore-keys: | + result-cache-v14-${{ matrix.php-version }}- - name: "PHPStan with result cache" run: | @@ -119,32 +106,22 @@ jobs: make phpstan-result-cache make phpstan-result-cache - - name: "Upload result cache artifact" - uses: actions/upload-artifact@v2 - with: - name: resultCache-ubuntu-latest.php - path: tmp/resultCache.php - generate-baseline: name: "Generate baseline" runs-on: "ubuntu-latest" timeout-minutes: 60 - strategy: - matrix: - php-version: - - "8.0" - steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.1" + ini-file: development - name: "Install dependencies" run: "composer install --no-interaction --no-progress" @@ -154,3 +131,29 @@ jobs: cp phpstan-baseline.neon phpstan-baseline-orig.neon && \ make phpstan-generate-baseline && \ diff phpstan-baseline.neon phpstan-baseline-orig.neon + + generate-baseline-php: + name: "Generate PHP baseline" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + ini-file: development + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Generate baseline" + run: | + > phpstan-baseline.neon && \ + make phpstan-generate-baseline-php && \ + make phpstan-result-cache diff --git a/.github/workflows/tests-levels-matrix.php b/.github/workflows/tests-levels-matrix.php new file mode 100644 index 0000000000..1344e8e81e --- /dev/null +++ b/.github/workflows/tests-levels-matrix.php @@ -0,0 +1,40 @@ +testCaseClass as $testCaseClass) { + foreach($testCaseClass->testCaseMethod as $testCaseMethod) { + if ((string) $testCaseMethod['groups'] !== 'levels') { + continue; + } + + $testCaseName = (string) $testCaseMethod['id']; + + [$className, $testName] = explode('::', $testCaseName, 2); + $fileName = 'tests/'. str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; + + $filter = str_replace('\\', '\\\\', $testCaseName); + + $testFilters[] = sprintf("%s --filter %s", escapeshellarg($fileName), escapeshellarg($filter)); + } +} + +if ($testFilters === []) { + throw new RuntimeException('No tests found'); +} + +$chunkSize = (int) ceil(count($testFilters) / 10); +$chunks = array_chunk($testFilters, $chunkSize); + +$commands = []; +foreach ($chunks as $chunk) { + $commands[] = implode("\n", array_map(fn (string $ch) => sprintf('php vendor/bin/phpunit %s --group levels', $ch), $chunk)); +} + +echo json_encode($commands); diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f9f0e8b07f..d7c4673b40 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,14 +6,21 @@ on: pull_request: paths-ignore: - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' push: branches: - - "master" + - "2.1.x" paths-ignore: - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' -env: - COMPOSER_ROOT_VERSION: "1.4.x-dev" +concurrency: + group: tests-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true jobs: tests: @@ -25,19 +32,17 @@ jobs: fail-fast: false matrix: php-version: - - "7.3" - "7.4" - "8.0" - "8.1" + - "8.2" + - "8.3" + - "8.4" operating-system: [ ubuntu-latest, windows-latest ] - script: - - "make tests" - - "make tests-static-reflection" - - "make tests-integration" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -46,91 +51,106 @@ jobs: php-version: "${{ matrix.php-version }}" tools: pecl extensions: ds,mbstring - ini-values: memory_limit=640M + ini-file: development + ini-values: memory_limit=2G - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Install PHP for code transform" - if: matrix.php-version != '8.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 != '8.0' && matrix.php-version != '8.1' + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' && matrix.php-version != '8.4' 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 + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - name: "Tests" - run: "${{ matrix.script }}" + run: "make tests" - tests-old-phpunit: - name: "Tests with old PHPUnit" + tests-integration: + name: "Integration tests" runs-on: ${{ matrix.operating-system }} timeout-minutes: 60 strategy: fail-fast: false matrix: - php-version: - - "7.1" - - "7.2" operating-system: [ ubuntu-latest, windows-latest ] - script: - - "make tests-coverage" - - "make tests-static-reflection-coverage" - - "make tests-integration-coverage" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.1" tools: pecl extensions: ds,mbstring - ini-values: memory_limit=640M + ini-file: development + ini-values: memory_limit=1G - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Install PHP for code transform" + - name: "Tests" + run: "make tests-integration" + + tests-levels-matrix: + name: "Determine levels tests matrix" + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.3" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=1G - - name: "Transform source code" - shell: bash - run: "build/transform-source ${{ matrix.php-version }}" + - name: "Install PHPUnit 10.x" + run: "composer remove --dev brianium/paratest && composer require --dev --with-all-dependencies phpunit/phpunit:^10" + + - id: set-matrix + run: echo "matrix=$(php .github/workflows/tests-levels-matrix.php)" >> $GITHUB_OUTPUT - - name: "Reinstall matrix PHP version" + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + + tests-levels: + needs: tests-levels-matrix + + name: "Levels tests" + runs-on: ubuntu-latest + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + script: "${{fromJson(needs.tests-levels-matrix.outputs.matrix)}}" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.3" tools: pecl extensions: ds,mbstring - ini-values: memory_limit=640M + ini-file: development + ini-values: memory_limit=1G - - name: "Downgrade PHPUnit" - run: "composer require --dev phpunit/phpunit:^7.5.20 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" - name: "Tests" run: "${{ matrix.script }}" diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml index b1618055b0..396be1c0be 100644 --- a/.github/workflows/update-phpstorm-stubs.yml +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -14,20 +14,20 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 with: - ref: ${{ github.head_ref }} + ref: 2.1.x fetch-depth: '0' token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.1" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - name: "Checkout stubs" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 with: path: "phpstorm-stubs" repository: "jetbrains/phpstorm-stubs" @@ -39,7 +39,7 @@ jobs: run: "./bin/generate-function-metadata.php" - name: "Create Pull Request" id: create-pr - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} branch-suffix: random diff --git a/.gitignore b/.gitignore index 65cd70f464..47f19ba656 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ !.idea/icon.png /tests/tmp /tests/.phpunit.result.cache +/tests/PHPStan/Reflection/data/golden/ tmp/.memory_limit +e2e/bashunit diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000000..78c99b1d76 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,14 @@ +[files] +extend-exclude = [ + ".git/", +] +ignore-hidden = false + +[default.extend-identifiers] +# Known typos +NonRemoveableTypeTrait = "NonRemoveableTypeTrait" +supportsLessOverridenParametersWithVariadic = "supportsLessOverridenParametersWithVariadic" + +[default.extend-words] +# override false-positives +Excluder = "Excluder" diff --git a/LICENSE b/LICENSE index 7c0f2b7b69..e5f34e607a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2016 Ondřej Mirtes +Copyright (c) 2025 PHPStan s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index fa08492748..9e007ae2cc 100644 --- a/Makefile +++ b/Makefile @@ -3,28 +3,27 @@ build: cs tests phpstan tests: - php vendor/bin/paratest --no-coverage + XDEBUG_MODE=off php vendor/bin/paratest --runner WrapperRunner --no-coverage tests-integration: - php vendor/bin/paratest --no-coverage --group exec + php vendor/bin/paratest --runner WrapperRunner --no-coverage --group exec -tests-static-reflection: - php vendor/bin/paratest --no-coverage --bootstrap tests/bootstrap-static-reflection.php +tests-levels: + php vendor/bin/paratest --runner WrapperRunner --no-coverage --group levels tests-coverage: - php vendor/bin/paratest + php vendor/bin/paratest --runner WrapperRunner -tests-integration-coverage: - php vendor/bin/paratest --group exec - -tests-static-reflection-coverage: - php vendor/bin/paratest --bootstrap tests/bootstrap-static-reflection.php +tests-golden-reflection: + php vendor/bin/paratest --runner WrapperRunner --no-coverage tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php lint: - php vendor/bin/parallel-lint --colors \ + XDEBUG_MODE=off php vendor/bin/parallel-lint --colors \ --exclude tests/PHPStan/Analyser/data \ + --exclude tests/PHPStan/Analyser/nsrt \ --exclude tests/PHPStan/Rules/Methods/data \ --exclude tests/PHPStan/Rules/Functions/data \ + --exclude tests/PHPStan/Rules/Names/data \ --exclude tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php \ --exclude tests/PHPStan/Rules/Arrays/data/offset-access-without-dim-for-reading.php \ --exclude tests/PHPStan/Rules/Classes/data/duplicate-declarations.php \ @@ -34,6 +33,7 @@ lint: --exclude tests/PHPStan/Rules/Classes/data/implements-error.php \ --exclude tests/PHPStan/Rules/Classes/data/interface-extends-error.php \ --exclude tests/PHPStan/Rules/Classes/data/trait-use-error.php \ + --exclude tests/PHPStan/Rules/Methods/data/method-in-enum-without-body.php \ --exclude tests/PHPStan/Rules/Properties/data/default-value-for-native-property-type.php \ --exclude tests/PHPStan/Rules/Arrays/data/empty-array-item.php \ --exclude tests/PHPStan/Rules/Classes/data/invalid-promoted-properties.php \ @@ -43,37 +43,98 @@ lint: --exclude tests/PHPStan/Rules/Functions/data/arrow-function-nullsafe-by-ref.php \ --exclude tests/PHPStan/Levels/data/namedArguments.php \ --exclude tests/PHPStan/Rules/Keywords/data/continue-break.php \ + --exclude tests/PHPStan/Rules/Keywords/data/continue-break-property-hook.php \ + --exclude tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php \ + --exclude tests/PHPStan/Rules/Properties/data/properties-in-interface.php \ --exclude tests/PHPStan/Rules/Properties/data/read-only-property.php \ + --exclude tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php \ + --exclude tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php \ --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 \ + --exclude tests/PHPStan/Rules/Classes/data/bug-9402.php \ + --exclude tests/PHPStan/Rules/Constants/data/class-as-class-constant.php \ + --exclude tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-native-type.php \ + --exclude tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-10043.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-7859.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-8081.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-9014.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-10101.php \ + --exclude tests/PHPStan/Rules/Methods/data/final-method-by-phpdoc.php \ + --exclude tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php \ + --exclude tests/PHPStan/Rules/Types/data/invalid-union-with-mixed.php \ + --exclude tests/PHPStan/Rules/Types/data/invalid-union-with-never.php \ + --exclude tests/PHPStan/Rules/Types/data/invalid-union-with-void.php \ + --exclude tests/PHPStan/Rules/Constants/data/dynamic-class-constant-fetch.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-position.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-position2.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-position-nested.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-strict-nonsense.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-strict-nonsense-bool.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-inline-html.php \ + --exclude tests/PHPStan/Rules/Classes/data/extends-readonly-class.php \ + --exclude tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php \ + --exclude tests/PHPStan/Rules/Classes/data/bug-11592.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-with-bodies.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-abstract-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/readonly-property-hooks.php \ + --exclude tests/PHPStan/Rules/Properties/data/readonly-property-hooks-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/static-hooked-properties.php \ + --exclude tests/PHPStan/Rules/Properties/data/static-hooked-property-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/virtual-hooked-properties.php \ + --exclude tests/PHPStan/Rules/Classes/data/bug-12281.php \ + --exclude tests/PHPStan/Rules/Traits/data/bug-12281.php \ + --exclude tests/PHPStan/Rules/Classes/data/invalid-hooked-properties.php \ + --exclude tests/PHPStan/Parser/data/cleaning-property-hooks-before.php \ + --exclude tests/PHPStan/Parser/data/cleaning-property-hooks-after.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-private-property-hook.php \ + --exclude tests/PHPStan/Rules/Properties/data/existing-classes-property-hooks.php \ + --exclude tests/PHPStan/Rules/Properties/data/set-property-hook-parameter.php \ + --exclude tests/PHPStan/Rules/Properties/data/overriding-final-property.php \ + --exclude tests/PHPStan/Rules/Properties/data/private-final-property-hooks.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php \ + --exclude tests/PHPStan/Rules/Properties/data/final-property-hooks-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/final-property-hooks.php \ + --exclude tests/PHPStan/Rules/Properties/data/final-properties.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-in-interface-explicit-abstract.php \ + --exclude tests/PHPStan/Rules/Constants/data/final-private-const.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php \ src tests cs: - composer install --working-dir build-cs && php build-cs/vendor/bin/phpcs + composer install --working-dir build-cs && XDEBUG_MODE=off php build-cs/vendor/bin/phpcs cs-fix: - php build-cs/vendor/bin/phpcbf + XDEBUG_MODE=off php build-cs/vendor/bin/phpcbf phpstan: - php bin/phpstan clear-result-cache -q && php -d memory_limit=768M bin/phpstan - -phpstan-static-reflection: - php bin/phpstan clear-result-cache -q && php -d memory_limit=768M bin/phpstan analyse -c phpstan-static-reflection.neon + php bin/phpstan clear-result-cache -q && php -d memory_limit=448M bin/phpstan phpstan-result-cache: - php -d memory_limit=768M bin/phpstan + php -d memory_limit=448M bin/phpstan phpstan-generate-baseline: - php -d memory_limit=768M bin/phpstan --generate-baseline + php -d memory_limit=448M bin/phpstan --generate-baseline -phpstan-validate-stub-files: - php bin/phpstan analyse -c conf/config.stubFiles.neon -l 8 tests/notAutoloaded/empty.php +phpstan-generate-baseline-php: + php -d memory_limit=448M bin/phpstan analyse --generate-baseline phpstan-baseline.php phpstan-pro: - php -d memory_limit=768M bin/phpstan --pro + php -d memory_limit=448M bin/phpstan --pro + +name-collision: + php vendor/bin/detect-collisions --configuration build/collision-detector.json -composer-require-checker: - php build/composer-require-checker.phar check --config-file $(CURDIR)/build/composer-require-checker.json +composer-dependency-analyser: + php vendor/bin/composer-dependency-analyser --config build/composer-dependency-analyser.php diff --git a/README.md b/README.md index b64d557a42..e817604542 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,17 @@ This repository (`phpstan/phpstan-src`) is for PHPStan's development only. Head Any contributions are welcome. +### Installation + +```bash +composer install +``` + +If you are using macOS and are using an older version of `patch`, you may have problems with patch application failure during `composer install`. Try using `brew install gpatch` to install a newer and supported `patch` version. + ### Building -PHPStan's source code is developed on PHP 8.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. +PHPStan's source code is developed on PHP 8.1. For distribution in `phpstan/phpstan` package and as a PHAR file, the source code is transformed to run on PHP 7.2 and higher. Initially you need to run `composer install` in case you aren't working in a directory which was built before. @@ -54,7 +62,7 @@ make tests ### Debugging -1. Make sure XDebug is installed and configured. +1. Make sure Xdebug is installed and configured. 2. Add `--xdebug` option when running PHPStan. Without it PHPStan turns the debugger off at runtime. 3. If you're not debugging the [result cache](https://phpstan.org/user-guide/result-cache), also add the `--debug` option. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000000..1b76768ec4 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,338 @@ +Upgrading from PHPStan 1.x to 2.0 +================================= + +## PHP version requirements + +PHPStan now requires PHP 7.4 or newer to run. + +## Upgrading guide for end users + +The best way to get ready for upgrade to PHPStan 2.0 is to update to the **latest PHPStan 1.12 release** +and enable [**Bleeding Edge**](https://phpstan.org/blog/what-is-bleeding-edge). This will enable the new rules and behaviours that 2.0 turns on for all users. + +Also make sure to install and enable [`phpstan/phpstan-deprecation-rules`](https://github.com/phpstan/phpstan-deprecation-rules). + +Once you get to a green build with no deprecations showed on latest PHPStan 1.12.x with Bleeding Edge enabled, you can update all your related PHPStan dependencies to 2.0 in `composer.json`: + +```json +"require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-doctrine": "^2.0", + "phpstan/phpstan-nette": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpstan/phpstan-webmozart-assert": "^2.0", + ... +} +``` + +Don't forget to update [3rd party PHPStan extensions](https://phpstan.org/user-guide/extension-library) as well. + +After changing your `composer.json`, run `composer update 'phpstan/*' -W`. + +It's up to you whether you go through the new reported errors or if you just put them all to the [baseline](https://phpstan.org/user-guide/baseline) ;) Everyone who's on PHPStan 1.12 should be able to upgrade to PHPStan 2.0. + +### Noteworthy changes to code analysis + +* [**Enhancements in handling parameters passed by reference**](https://phpstan.org/blog/enhancements-in-handling-parameters-passed-by-reference) +* [**Validate inline PHPDoc `@var` tag type**](https://phpstan.org/blog/phpstan-1-10-comes-with-lie-detector#validate-inline-phpdoc-%40var-tag-type) +* [**List type enforced**](https://phpstan.org/blog/phpstan-1-9-0-with-phpdoc-asserts-list-type#list-type) +* **Always `true` conditions always reported**: previously reported only with phpstan-strict-rules, this is now always reported. + +### Removed option `checkMissingIterableValueType` + +It's strongly recommended to add the missing array typehints. + +If you want to continue ignoring missing typehints from arrays, add `missingType.iterableValue` error identifier to your `ignoreErrors`: + +```neon +parameters: + ignoreErrors: + - + identifier: missingType.iterableValue +``` + +### Removed option `checkGenericClassInNonGenericObjectType` + +It's strongly recommended to add the missing generic typehints. + +If you want to continue ignoring missing typehints from generics, add `missingType.generics` error identifier to your `ignoreErrors`: + +```neon +parameters: + ignoreErrors: + - + identifier: missingType.generics +``` + +### Removed `checkAlwaysTrue*` options + +These options have been removed because PHPStan now always behaves as if these were set to `true`: + +* `checkAlwaysTrueCheckTypeFunctionCall` +* `checkAlwaysTrueInstanceof` +* `checkAlwaysTrueStrictComparison` +* `checkAlwaysTrueLooseComparison` + +### Removed option `excludes_analyse` + +It has been replaced with [`excludePaths`](https://phpstan.org/user-guide/ignoring-errors#excluding-whole-files). + +### Paths in `excludePaths` and `ignoreErrors` have to be a valid file path or a fnmatch pattern + +If you are excluding a file path that might not exist but you still want to have it in `excludePaths`, append `(?)`: + +```neon +parameters: + excludePaths: + - tests/*/data/* + - src/broken + - node_modules (?) # optional path, might not exist +``` + +If you have the same situation in `ignoreErrors` (ignoring an error in a path that might not exist), use `reportUnmatchedIgnoredErrors: false`. + +```neon +parameters: + reportUnmatchedIgnoredErrors: false +``` + +Appending `(?)` in `ignoreErrors` is not supported. + +### Changes in 1st party PHPStan extensions + +* [phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine) + * Removed config parameter `searchOtherMethodsForQueryBuilderBeginning` (extension now behaves as when this was set to `true`) + * Removed config parameter `queryBuilderFastAlgorithm` (extension now behaves as when this was set to `false`) +* [phpstan-symfony](https://github.com/phpstan/phpstan-symfony) + * Removed legacy options with `_` in the name + * `container_xml_path` -> use `containerXmlPath` + * `constant_hassers` -> use `constantHassers` + * `console_application_loader` -> use `consoleApplicationLoader` + +### Minor backward compatibility breaks + +* Removed unused config parameter `cache.nodesByFileCountMax` +* Removed unused config parameter `memoryLimitFile` +* Removed unused feature toggle `disableRuntimeReflectionProvider` +* Removed unused config parameter `staticReflectionClassNamePatterns` +* Remove `fixerTmpDir` config parameter, use `pro.tmpDir` instead +* Remove `tempResultCachePath` config parameter, use `resultCachePath` instead +* `additionalConfigFiles` config parameter must be a list + +## Upgrading guide for extension developers + +> [!NOTE] +> Please switch to PHPStan 2.0 in a new major version of your extension. It's not feasible to try to support both PHPStan 1.x and PHPStan 2.x with the same extension code. +> +> You can definitely get closer to supporting PHPStan 2.0 without increasing major version by solving reported deprecations and other issues by analysing your extension code with PHPStan & phpstan-deprecation-rules & Bleeding Edge, but the final leap and solving backward incompatibilities should be done by requiring `"phpstan/phpstan": "^2.0"` in your `composer.json`, and releasing a new major version. + +### PHPStan now uses nikic/php-parser v5 + +See [UPGRADING](https://github.com/nikic/PHP-Parser/blob/master/UPGRADE-5.0.md) guide for PHP-Parser. + +The most notable change is how `throw` statement is represented. Previously, `throw` statements like `throw $e;` were represented using the `Stmt\Throw_` class, while uses inside other expressions (such as `$x ?? throw $e`) used the `Expr\Throw_` class. + +Now, `throw $e;` is represented as a `Stmt\Expression` that contains an `Expr\Throw_`. The +`Stmt\Throw_` class has been removed. + +### PHPStan now uses phpstan/phpdoc-parser v2 + +See [UPGRADING](https://github.com/phpstan/phpdoc-parser/blob/2.0.x/UPGRADING.md) guide for phpstan/phpdoc-parser. + +### Returning plain strings as errors no longer supported, use RuleErrorBuilder + +Identifiers are also required in custom rules. + +Learn more: [Using RuleErrorBuilder to enrich reported errors in custom rules](https://phpstan.org/blog/using-rule-error-builder) + +**Before**: + +```php +return ['My error']; +``` + +**After**: + +```php +return [ + RuleErrorBuilder::message('My error') + ->identifier('my.error') + ->build(), +]; +``` + +### Deprecate various `instanceof *Type` in favour of new methods on `Type` interface + +Learn more: [Why Is instanceof *Type Wrong and Getting Deprecated?](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) + +### Removed deprecated `ParametersAcceptorSelector::selectSingle()` + +Use [`ParametersAcceptorSelector::selectFromArgs()`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ParametersAcceptorSelector.html#_selectFromArgs) instead. It should be used in most places where `selectSingle()` was previously used, like dynamic return type extensions. + +**Before**: + +```php +$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); +``` + +**After**: + +```php +$defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants() +)->getReturnType(); +``` + +If you're analysing function or method body itself and you're using one of the following methods, ask for `getParameters()` and `getReturnType()` directly on the reflection object: + +* [InClassMethodNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InClassMethodNode.html) +* [InFunctionNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InFunctionNode.html) +* [FunctionReturnStatementsNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.FunctionReturnStatementsNode.html) +* [MethodReturnStatementsNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.MethodReturnStatementsNode.html) +* [Scope::getFunction()](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.Scope.html#_getFunction) + +**Before**: + +```php +$function = $node->getFunctionReflection(); +$returnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); +``` + +**After**: + +```php +$returnType = $node->getFunctionReflection()->getReturnType(); +``` + +### Changed `TypeSpecifier::create()` and `SpecifiedTypes` constructor parameters + +[`PHPStan\Analyser\TypeSpecifier::create()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.TypeSpecifier.html#_create) now accepts (all parameters are required): + +* `Expr $expr` +* `Type $type` +* `TypeSpecifierContext $context` +* `Scope $scope` + +If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by this method), call `setAlwaysOverwriteTypes()` and `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::create()`). These methods return a new object (SpecifiedTypes is immutable). + +[`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) constructor now accepts: + +* `array $sureTypes` +* `array $sureNotTypes` + +If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by the constructor), call `setAlwaysOverwriteTypes()` and `setRootExpr()`. These methods return a new object (SpecifiedTypes is immutable). + +### `ConstantArrayType` no longer extends `ArrayType` + +`Type::getArrays()` now returns `list`. + +Using `$type instanceof ArrayType` is [being deprecated anyway](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) so the impact of this change should be minimal. + +### Changed `TypeSpecifier::specifyTypesInCondition()` + +This method now longer accepts `Expr $rootExpr`. If you want to change it, call `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::specifyTypesInCondition()`). `setRootExpr()` method returns a new object (SpecifiedTypes is immutable). + +### Node attributes `parent`, `previous`, `next` are no longer available + +Learn more: https://phpstan.org/blog/preprocessing-ast-for-custom-rules + +### Removed config parameter `scopeClass` + +As a replacement you can implement [`PHPStan\Type\ExpressionTypeResolverExtension`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.ExpressionTypeResolverExtension.html) interface instead and register it as a service. + +### Removed `PHPStan\Broker\Broker` + +Use [`PHPStan\Reflection\ReflectionProvider`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ReflectionProvider.html) instead. + +`BrokerAwareExtension` was also removed. Ask for `ReflectionProvider` in the extension constructor instead. + +Instead of `PHPStanTestCase::createBroker()`, call `PHPStanTestCase::createReflectionProvider()`. + +### List type is enabled for everyone + +Removed static methods from `AccessoryArrayListType` class: + +* `isListTypeEnabled()` +* `setListTypeEnabled()` +* `intersectWith()` + +Instead of `AccessoryArrayListType::intersectWith($type)`, do `TypeCombinator::intersect($type, new AccessoryArrayListType())`. + +### Minor backward compatibility breaks + +* Classes that were previously `@final` were made `final` +* Parameter `$callableParameters` of [`MutatingScope::enterAnonymousFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterAnonymousFunction) and [`enterArrowFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterArrowFunction) made required +* Parameter `StatementContext $context` of [`NodeScopeResolver::processStmtNodes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.NodeScopeResolver.html#_processStmtNodes) made required +* ClassPropertiesNode - remove `$extensions` parameter from [`getUninitializedProperties()`](https://apiref.phpstan.org/2.0.x/PHPStan.Node.ClassPropertiesNode.html#_getUninitializedProperties) +* `Type::getSmallerType()`, `Type::getSmallerOrEqualType()`, `Type::getGreaterType()`, `Type::getGreaterOrEqualType()`, `Type::isSmallerThan()`, `Type::isSmallerThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. +* `CompoundType::isGreaterThan()`, `CompoundType::isGreaterThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. +* Removed `ReflectionProvider::supportsAnonymousClasses()` (all reflection providers support anonymous classes) +* Remove `ArrayType::generalizeKeys()` +* Remove `ArrayType::count()`, use `Type::getArraySize()` instead +* Remove `ArrayType::castToArrayKeyType()`, `Type::toArrayKey()` instead +* Remove `UnionType::pickTypes()`, use `pickFromTypes()` instead +* Remove `RegexArrayShapeMatcher::matchType()`, use `matchExpr()` instead +* Remove unused `PHPStanTestCase::$useStaticReflectionProvider` +* Remove `PHPStanTestCase::getReflectors()`, use `getReflector()` instead +* Remove `ClassReflection::getFileNameWithPhpDocs()`, use `getFileName()` instead +* Remove `AnalysisResult::getInternalErrors()`, use `getInternalErrorObjects()` instead +* Remove `ConstantReflection::getValue()`, use `getValueExpr()` instead. To get `Type` from `Expr`, use `Scope::getType()` or `InitializerExprTypeResolver::getType()` +* Remove `PropertyTag::getType()`, use `getReadableType()` / `getWritableType()` instead +* Remove `GenericTypeVariableResolver`, use [`Type::getTemplateType()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getTemplateType) instead +* Rename `Type::isClassStringType()` to `Type::isClassString()` +* Remove `Scope::isSpecified()`, use `hasExpressionType()` instead +* Remove `ConstantArrayType::isEmpty()`, use `isIterableAtLeastOnce()->no()` instead +* Remove `ConstantArrayType::getNextAutoIndex()` +* Removed methods from `ConstantArrayType` - `getFirst*Type` and `getLast*Type` + * Use `getFirstIterable*Type` and `getLastIterable*Type` instead +* Remove `ConstantArrayType::generalizeToArray()` +* Remove `ConstantArrayType::findTypeAndMethodName()`, use `findTypeAndMethodNames()` instead +* Remove `ConstantArrayType::removeLast()`, use [`Type::popArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_popArray) instead +* Remove `ConstantArrayType::removeFirst()`, use [`Type::shiftArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_shiftArray) instead +* Remove `ConstantArrayType::reverse()`, use [`Type::reverseArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_reverseArray) instead +* Remove `ConstantArrayType::chunk()`, use [`Type::chunkArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_chunkArray) instead +* Remove `ConstantArrayType::slice()`, use [`Type::sliceArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_sliceArray) instead +* Made `TypeUtils` thinner by removing methods: + * Remove `TypeUtils::getArrays()` and `getAnyArrays()`, use [`Type::getArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getArrays) instead + * Remove `TypeUtils::getConstantArrays()` and `getOldConstantArrays()`, use [`Type::getConstantArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantArrays) instead + * Remove `TypeUtils::getConstantStrings()`, use [`Type::getConstantStrings()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantStrings) instead + * Remove `TypeUtils::getConstantTypes()` and `getAnyConstantTypes()`, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) or [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) + * Remove `TypeUtils::generalizeType()`, use [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) instead + * Remove `TypeUtils::getDirectClassNames()`, use [`Type::getObjectClassNames()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getObjectClassNames) instead + * Remove `TypeUtils::getConstantScalars()`, use [`Type::isConstantScalarValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantScalarValue) or [`Type::getConstantScalarTypes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantScalarTypes) instead + * Remove `TypeUtils::getEnumCaseObjects()`, use [`Type::getEnumCases()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getEnumCases) instead + * Remove `TypeUtils::containsCallable()`, use [`Type::isCallable()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isCallable) instead +* Removed `Scope::doNotTreatPhpDocTypesAsCertain()`, use `getNativeType()` instead +* Parameter `$isList` in `ConstantArrayType` constructor can only be `TrinaryLogic`, no longer `bool` +* Parameter `$nextAutoIndexes` in `ConstantArrayType` constructor can only be `non-empty-list`, no longer `int` +* Remove `ConstantType` interface, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) instead +* `acceptsNamedArguments()` in `FunctionReflection`, `ExtendedMethodReflection` and `CallableParametersAcceptor` interfaces returns `TrinaryLogic` instead of `bool` +* Remove `FunctionReflection::isFinal()` +* [`Type::getProperty()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getProperty) now returns [`ExtendedPropertyReflection`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ExtendedPropertyReflection.html) +* Remove `__set_state()` on objects that should not be serialized in cache +* Parameter `$selfClass` of [`TypehintHelper::decideTypeFromReflection()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.TypehintHelper.html#_decideTypeFromReflection) no longer accepts `string` +* `LevelsTestCase::dataTopics()` data provider made static +* `PHPStan\Node\Printer\Printer` no longer autowired as `PhpParser\PrettyPrinter\Standard`, use `PHPStan\Node\Printer\Printer` in the typehint +* Remove `Type::acceptsWithReason()`, `Type:accepts()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +* Remove `CompoundType::isAcceptedWithReasonBy()`, `CompoundType::isAcceptedBy()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +Remove `Type::isSuperTypeOfWithReason()`, `Type:isSuperTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* Remove `CompoundType::isSubTypeOfWithReasonBy()`, `CompoundType::isSubTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* Remove `TemplateType::isValidVarianceWithReason()`, changed `TemplateType::isValidVariance()` return type to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* `RuleLevelHelper::accepts()` return type changed from `bool` to [`RuleLevelHelperAcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +* Changes around `ClassConstantReflection` + * Class `ClassConstantReflection` removed from BC promise, renamed to `RealClassConstantReflection` + * Interface `ConstantReflection` renamed to `ClassConstantReflection` + * Added more methods around PHPDoc types and native types to the (new) `ClassConstantReflection` + * Interface `GlobalConstantReflection` renamed to `ConstantReflection` +* Renamed interfaces and classes from `*WithPhpDocs` to `Extended*` + * `ParametersAcceptorWithPhpDocs` -> `ExtendedParametersAcceptor` + * `ParameterReflectionWithPhpDocs` -> `ExtendedParameterReflection` + * `FunctionVariantWithPhpDocs` -> `ExtendedFunctionVariant` +* `ClassPropertyNode::getNativeType()` return type changed from AST node to `Type|null` +* Class `PHPStan\Node\ClassMethod` (accessible from `ClassMethodsNode`) is no longer an AST node + * Call `PHPStan\Node\ClassMethod::getNode()` to access the original AST node diff --git a/apigen/.gitignore b/apigen/.gitignore new file mode 100644 index 0000000000..61ead86667 --- /dev/null +++ b/apigen/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/apigen/apigen.neon b/apigen/apigen.neon new file mode 100644 index 0000000000..16d87b17a4 --- /dev/null +++ b/apigen/apigen.neon @@ -0,0 +1,10 @@ +parameters: + title: PHPStan + themeDir: theme + +services: + analyzer.filter: + factory: PHPStan\ApiGen\Filter(excludeProtected: %excludeProtected%, excludePrivate: %excludePrivate%, excludeTagged: %excludeTagged%) + + renderer.filter: + factory: PHPStan\ApiGen\RendererFilter diff --git a/apigen/composer.json b/apigen/composer.json new file mode 100644 index 0000000000..cac161def9 --- /dev/null +++ b/apigen/composer.json @@ -0,0 +1,13 @@ +{ + "require": { + "php": "^8.1" + }, + "require-dev": { + "apigen/apigen": "dev-master#52f74870943620f96c669d730596fc1091117441" + }, + "autoload": { + "psr-4": { + "PHPStan\\ApiGen\\": "src" + } + } +} diff --git a/apigen/composer.lock b/apigen/composer.lock new file mode 100644 index 0000000000..5f296fa17b --- /dev/null +++ b/apigen/composer.lock @@ -0,0 +1,1970 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "cca72dc10f8d1df2104049e381d62fcb", + "packages": [], + "packages-dev": [ + { + "name": "apigen/apigen", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/ApiGen/ApiGen.git", + "reference": "52f74870943620f96c669d730596fc1091117441" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ApiGen/ApiGen/zipball/52f74870943620f96c669d730596fc1091117441", + "reference": "52f74870943620f96c669d730596fc1091117441", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "jetbrains/phpstorm-stubs": "^2022.1", + "latte/latte": "^3.0", + "league/commonmark": "^2.3", + "nette/di": "^3.0", + "nette/finder": "^2.5", + "nette/schema": "^1.2", + "nette/utils": "^3.2", + "nikic/php-parser": "^4.14", + "php": "^8.1", + "phpstan/php-8-stubs": "^0.3.9", + "phpstan/phpdoc-parser": "^1.5", + "symfony/console": "^6.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.7", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-pcntl": "for multiprocess rendering" + }, + "default-branch": true, + "bin": [ + "bin/apigen" + ], + "type": "library", + "autoload": { + "psr-4": { + "ApiGen\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "ApiGen Contributors", + "homepage": "https://github.com/apigen/apigen/graphs/contributors" + }, + { + "name": "Jaroslav Hanslík", + "homepage": "https://github.com/kukulich" + }, + { + "name": "Ondřej Nešpor", + "homepage": "https://github.com/andrewsville" + }, + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + } + ], + "description": "PHP source code API generator.", + "support": { + "issues": "https://github.com/ApiGen/ApiGen/issues", + "source": "https://github.com/ApiGen/ApiGen/tree/master" + }, + "time": "2022-07-10T16:43:45+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "0992cc19268b259a39e86f296da5f0677841f42c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/0992cc19268b259a39e86f296da5f0677841f42c", + "reference": "0992cc19268b259a39e86f296da5f0677841f42c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^3.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com/" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com/" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com/" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.1" + }, + "time": "2021-08-13T13:06:58+00:00" + }, + { + "name": "jetbrains/phpstorm-stubs", + "version": "v2022.1", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "066fa5b3cd989b9c4fb1793d5ad20af5ab0e2b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/066fa5b3cd989b9c4fb1793d5ad20af5ab0e2b3c", + "reference": "066fa5b3cd989b9c4fb1793d5ad20af5ab0e2b3c", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "@stable", + "nikic/php-parser": "@stable", + "php": "^8.0", + "phpdocumentor/reflection-docblock": "@stable", + "phpunit/phpunit": "@stable" + }, + "type": "library", + "autoload": { + "files": [ + "PhpStormStubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "PHP runtime & extensions header files for PhpStorm", + "homepage": "https://www.jetbrains.com/phpstorm", + "keywords": [ + "autocomplete", + "code", + "inference", + "inspection", + "jetbrains", + "phpstorm", + "stubs", + "type" + ], + "support": { + "source": "https://github.com/JetBrains/phpstorm-stubs/tree/v2022.1" + }, + "time": "2022-03-08T07:40:50+00:00" + }, + { + "name": "latte/latte", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/nette/latte.git", + "reference": "c3eee2e4e2c21cdf9f9c158c4bfa6150625457e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/latte/zipball/c3eee2e4e2c21cdf9f9c158c4bfa6150625457e1", + "reference": "c3eee2e4e2c21cdf9f9c158c4bfa6150625457e1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=8.0 <8.2" + }, + "conflict": { + "nette/application": "<2.4.1" + }, + "require-dev": { + "nette/php-generator": "^3.3.4", + "nette/tester": "^2.0", + "nette/utils": "^3.0", + "phpstan/phpstan": "^1", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-fileinfo": "to use filter |datastream", + "ext-iconv": "to use filters |reverse, |substring", + "ext-mbstring": "to use filters like lower, upper, capitalize, ...", + "nette/php-generator": "to use tag {templatePrint}", + "nette/utils": "to use filter |webalize" + }, + "bin": [ + "bin/latte-lint" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "☕ Latte: the intuitive and fast template engine for those who want the most secure PHP sites. Introduces context-sensitive escaping.", + "homepage": "https://latte.nette.org/", + "keywords": [ + "context-sensitive", + "engine", + "escaping", + "html", + "nette", + "security", + "template", + "twig" + ], + "support": { + "issues": "https://github.com/nette/latte/issues", + "source": "https://github.com/nette/latte/tree/v3.0.2" + }, + "time": "2022-06-15T13:42:57+00:00" + }, + { + "name": "league/commonmark", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "0da1dca5781dd3cfddbe328224d9a7a62571addc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/0da1dca5781dd3cfddbe328224d9a7a62571addc", + "reference": "0da1dca5781dd3cfddbe328224d9a7a62571addc", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.30.0", + "commonmark/commonmark.js": "0.30.0", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com/", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com/", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2022-06-07T21:28:26+00:00" + }, + { + "name": "league/config", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e", + "reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.90", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com/", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com/", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2021-08-14T12:15:32+00:00" + }, + { + "name": "nette/di", + "version": "v3.0.13", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "9878f2958a0a804b08430dbc719a52e493022739" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/9878f2958a0a804b08430dbc719a52e493022739", + "reference": "9878f2958a0a804b08430dbc719a52e493022739", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/neon": "^3.3 || ^4.0", + "nette/php-generator": "^3.5.4 || ^4.0", + "nette/robot-loader": "^3.2", + "nette/schema": "^1.1", + "nette/utils": "^3.1.6", + "php": ">=7.1 <8.2" + }, + "conflict": { + "nette/bootstrap": "<3.0" + }, + "require-dev": { + "nette/tester": "^2.2", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "💎 Nette 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", + "di", + "dic", + "factory", + "ioc", + "nette", + "static" + ], + "support": { + "issues": "https://github.com/nette/di/issues", + "source": "https://github.com/nette/di/tree/v3.0.13" + }, + "time": "2022-03-10T02:43:04+00:00" + }, + { + "name": "nette/finder", + "version": "v2.5.3", + "source": { + "type": "git", + "url": "https://github.com/nette/finder.git", + "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/finder/zipball/64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", + "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", + "shasum": "" + }, + "require": { + "nette/utils": "^2.4 || ^3.0", + "php": ">=7.1" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🔍 Nette Finder: find files and directories with an intuitive API.", + "homepage": "https://nette.org/", + "keywords": [ + "filesystem", + "glob", + "iterator", + "nette" + ], + "support": { + "issues": "https://github.com/nette/finder/issues", + "source": "https://github.com/nette/finder/tree/v2.5.3" + }, + "time": "2021-12-12T17:43:24+00:00" + }, + { + "name": "nette/neon", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "22e384da162fab42961d48eb06c06d3ad0c11b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/22e384da162fab42961d48eb06c06d3ad0c11b95", + "reference": "22e384da162fab42961d48eb06c06d3ad0c11b95", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.0", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.7" + }, + "bin": [ + "bin/neon-lint" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🍸 Nette NEON: encodes and decodes NEON file format.", + "homepage": "https://ne-on.org/", + "keywords": [ + "export", + "import", + "neon", + "nette", + "yaml" + ], + "support": { + "issues": "https://github.com/nette/neon/issues", + "source": "https://github.com/nette/neon/tree/v3.3.3" + }, + "time": "2022-03-10T02:04:26+00:00" + }, + { + "name": "nette/php-generator", + "version": "v4.0.2", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "f19b7975c7c4d729be5b64fce7eb72f0d4aac6fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/f19b7975c7c4d729be5b64fce7eb72f0d4aac6fc", + "reference": "f19b7975c7c4d729be5b64fce7eb72f0d4aac6fc", + "shasum": "" + }, + "require": { + "nette/utils": "^3.2.7 || ^4.0", + "php": ">=8.0 <8.2" + }, + "require-dev": { + "nette/tester": "^2.4", + "nikic/php-parser": "^4.13", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.8" + }, + "suggest": { + "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.1 features.", + "homepage": "https://nette.org/", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "support": { + "issues": "https://github.com/nette/php-generator/issues", + "source": "https://github.com/nette/php-generator/tree/v4.0.2" + }, + "time": "2022-06-17T12:20:08+00:00" + }, + { + "name": "nette/robot-loader", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/nette/robot-loader.git", + "reference": "e2adc334cb958164c050f485d99c44c430f51fe2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/e2adc334cb958164c050f485d99c44c430f51fe2", + "reference": "e2adc334cb958164c050f485d99c44c430f51fe2", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/finder": "^2.5 || ^3.0", + "nette/utils": "^3.0", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.0", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.", + "homepage": "https://nette.org/", + "keywords": [ + "autoload", + "class", + "interface", + "nette", + "trait" + ], + "support": { + "issues": "https://github.com/nette/robot-loader/issues", + "source": "https://github.com/nette/robot-loader/tree/v3.4.1" + }, + "time": "2021-08-25T15:53:54+00:00" + }, + { + "name": "nette/schema", + "version": "v1.2.2", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/9a39cef03a5b34c7de64f551538cbba05c2be5df", + "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df", + "shasum": "" + }, + "require": { + "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", + "php": ">=7.1 <8.2" + }, + "require-dev": { + "nette/tester": "^2.3 || ^2.4", + "phpstan/phpstan-nette": "^0.12", + "tracy/tracy": "^2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org/", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.2.2" + }, + "time": "2021-10-15T11:40:02+00:00" + }, + { + "name": "nette/utils", + "version": "v3.2.7", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/0af4e3de4df9f1543534beab255ccf459e7a2c99", + "reference": "0af4e3de4df9f1543534beab255ccf459e7a2c99", + "shasum": "" + }, + "require": { + "php": ">=7.2 <8.2" + }, + "conflict": { + "nette/di": "<3.0.6" + }, + "require-dev": { + "nette/tester": "~2.0", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org/", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v3.2.7" + }, + "time": "2022-01-24T11:29:14+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.14.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + }, + "time": "2022-05-31T20:59:12+00:00" + }, + { + "name": "phpstan/php-8-stubs", + "version": "0.3.17", + "source": { + "type": "git", + "url": "https://github.com/phpstan/php-8-stubs.git", + "reference": "2be8adf73c7034f78ca109bb5bb6cb14ea04a49f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/2be8adf73c7034f78ca109bb5bb6cb14ea04a49f", + "reference": "2be8adf73c7034f78ca109bb5bb6cb14ea04a49f", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "Php8StubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "PHP-3.01" + ], + "description": "PHP stubs extracted from php-src", + "support": { + "issues": "https://github.com/phpstan/php-8-stubs/issues", + "source": "https://github.com/phpstan/php-8-stubs/tree/0.3.17" + }, + "time": "2022-06-23T00:14:01+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.6.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "135607f9ccc297d6923d49c2bcf309f509413215" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/135607f9ccc297d6923d49c2bcf309f509413215", + "reference": "135607f9ccc297d6923d49c2bcf309f509413215", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.6.4" + }, + "time": "2022-06-26T13:09:08+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "symfony/console", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/7a86c1c42fbcb69b59768504c7bca1d3767760b7", + "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "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", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com/", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-26T13:01:30+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com/", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.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": "2022-02-25T11:15:52+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.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": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "433d05519ce6990bf3530fba6957499d327395c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", + "reference": "433d05519ce6990bf3530fba6957499d327395c2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-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": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.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": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.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": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.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": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-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.26.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": "2022-05-10T07:21:04+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com/", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.1.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": "2022-05-30T19:18:58+00:00" + }, + { + "name": "symfony/string", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "1903f2879875280c5af944625e8246d81c2f0604" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/1903f2879875280c5af944625e8246d81c2f0604", + "reference": "1903f2879875280c5af944625e8246d81c2f0604", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" + }, + "require-dev": { + "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": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com/", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-26T16:35:04+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "apigen/apigen": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/apigen/src/Filter.php b/apigen/src/Filter.php new file mode 100644 index 0000000000..66f39c3bfb --- /dev/null +++ b/apigen/src/Filter.php @@ -0,0 +1,170 @@ +namespacedName->toString(); + if (Strings::startsWith($name, 'PhpParser\\')) { + return true; + } + + if (!Strings::startsWith($name, 'PHPStan\\')) { + return false; + } + + if (Strings::startsWith($name, 'PHPStan\\PhpDocParser\\')) { + return true; + } + + if (Strings::startsWith($name, 'PHPStan\\BetterReflection\\')) { + return true; + } + + if ($this->hasApiTag($node)) { + return true; + } + + foreach ($node->getMethods() as $method) { + if ($this->hasApiTag($method)) { + return true; + } + } + + return false; + } + + public function filterClassLikeTags(array $tags): bool + { + return parent::filterClassLikeTags($tags); + } + + public function filterClassLikeInfo(ClassLikeInfo $info): bool + { + return parent::filterClassLikeInfo($info); + } + + public function filterFunctionNode(Node\Stmt\Function_ $node): bool + { + $name = $node->namespacedName->toString(); + if (!Strings::startsWith($name, 'PHPStan\\')) { + return false; + } + + return $this->hasApiTag($node); + } + + public function filterFunctionTags(array $tags): bool + { + return parent::filterFunctionTags($tags); + } + + public function filterFunctionInfo(FunctionInfo $info): bool + { + return parent::filterFunctionInfo($info); + } + + public function filterConstantNode(Node\Stmt\ClassConst $node): bool + { + return parent::filterConstantNode($node); + } + + public function filterPropertyNode(Node\Stmt\Property $node): bool + { + return parent::filterPropertyNode($node); + } + + public function filterPromotedPropertyNode(Node\Param $node): bool + { + return parent::filterPromotedPropertyNode($node); + } + + public function filterMethodNode(Node\Stmt\ClassMethod $node): bool + { + return parent::filterMethodNode($node); + } + + public function filterEnumCaseNode(Node\Stmt\EnumCase $node): bool + { + return parent::filterEnumCaseNode($node); + } + + public function filterMemberTags(array $tags): bool + { + return parent::filterMemberTags($tags); + } + + public function filterMemberInfo(ClassLikeInfo $classLike, MemberInfo $member): bool + { + $className = $classLike->name->full; + if (Strings::startsWith($className, 'PhpParser\\')) { + return true; + } + if (Strings::startsWith($className, 'PHPStan\\PhpDocParser\\')) { + return true; + } + + if (Strings::startsWith($className, 'PHPStan\\BetterReflection\\')) { + return true; + } + if (!$member instanceof MethodInfo) { + return !Strings::startsWith($className, 'PHPStan\\'); + } + + if (!Strings::startsWith($className, 'PHPStan\\')) { + return false; + } + + if (isset($classLike->tags['api'])) { + return true; + } + + return isset($member->tags['api']); + } + + private function hasApiTag(Node $node): bool + { + $classDoc = $this->extractPhpDoc($node); + $tags = $this->extractTags($classDoc); + + return isset($tags['api']); + } + + private function extractPhpDoc(Node $node): PhpDocNode + { + return $node->getAttribute('phpDoc') ?? new PhpDocNode([]); + } + + /** + * @return PhpDocTagValueNode[][] indexed by [tagName][] + */ + private function extractTags(PhpDocNode $node): array + { + $tags = []; + + foreach ($node->getTags() as $tag) { + if ($tag->value instanceof InvalidTagValueNode) { + continue; + } + + $tags[substr($tag->name, 1)][] = $tag->value; + } + + return $tags; + } + +} diff --git a/apigen/src/RendererFilter.php b/apigen/src/RendererFilter.php new file mode 100644 index 0000000000..9f25bbd5d8 --- /dev/null +++ b/apigen/src/RendererFilter.php @@ -0,0 +1,113 @@ +children as $child) { + if ($this->filterNamespacePage($child)) { + return true; + } + } + + foreach ($namespace->class as $class) { + if ($this->filterClassLikePage($class)) { + return true; + } + } + + foreach ($namespace->interface as $interface) { + if ($this->filterClassLikePage($interface)) { + return true; + } + } + + foreach ($namespace->trait as $trait) { + if ($this->filterClassLikePage($trait)) { + return true; + } + } + + foreach ($namespace->enum as $enum) { + if ($this->filterClassLikePage($enum)) { + return true; + } + } + + foreach ($namespace->exception as $exception) { + if ($this->filterClassLikePage($exception)) { + return true; + } + } + + foreach ($namespace->function as $function) { + if ($this->filterFunctionPage($function)) { + return true; + } + } + + return false; + } + + public function filterClassLikePage(ClassLikeInfo $classLike): bool + { + return $this->isClassRendered($classLike); + } + + private function isClassRendered(ClassLikeInfo $classLike): bool + { + $className = $classLike->name->full; + if (Strings::startsWith($className, 'PhpParser\\')) { + return true; + } + if (Strings::startsWith($className, 'PHPStan\\PhpDocParser\\')) { + return true; + } + + if (Strings::startsWith($className, 'PHPStan\\BetterReflection\\')) { + return true; + } + + if (!Strings::startsWith($className, 'PHPStan\\')) { + return false; + } + + if (isset($classLike->tags['api'])) { + return true; + } + + foreach ($classLike->methods as $method) { + if (isset($method->tags['api'])) { + return true; + } + } + + return false; + } + + public function filterFunctionPage(FunctionInfo $function): bool + { + return parent::filterFunctionPage($function); // todo + } + + public function filterSourcePage(FileIndex $file): bool + { + return parent::filterSourcePage($file); + } + +} diff --git a/apigen/theme/blocks/head.latte b/apigen/theme/blocks/head.latte new file mode 100644 index 0000000000..16f25417e3 --- /dev/null +++ b/apigen/theme/blocks/head.latte @@ -0,0 +1,8 @@ +{define head} + + +{/define} +{define menu} + + {include #parent} +{/define} diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 8004c6dcda..f4c9cd19fc 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -31,11 +31,14 @@ 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], + 'array_pop' => ['hasSideEffects' => true], 'array_product' => ['hasSideEffects' => false], + 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], + 'array_shift' => ['hasSideEffects' => true], 'array_slice' => ['hasSideEffects' => false], 'array_sum' => ['hasSideEffects' => false], 'array_udiff' => ['hasSideEffects' => false], @@ -45,6 +48,7 @@ 'array_uintersect_assoc' => ['hasSideEffects' => false], 'array_uintersect_uassoc' => ['hasSideEffects' => false], 'array_unique' => ['hasSideEffects' => false], + 'array_unshift' => ['hasSideEffects' => true], 'array_values' => ['hasSideEffects' => false], 'asin' => ['hasSideEffects' => false], 'asinh' => ['hasSideEffects' => false], @@ -66,6 +70,7 @@ 'chown' => ['hasSideEffects' => true], 'copy' => ['hasSideEffects' => true], 'count' => ['hasSideEffects' => false], + 'error_log' => ['hasSideEffects' => true], 'fclose' => ['hasSideEffects' => true], 'fflush' => ['hasSideEffects' => true], 'fgetc' => ['hasSideEffects' => true], @@ -83,11 +88,25 @@ 'fseek' => ['hasSideEffects' => true], 'ftruncate' => ['hasSideEffects' => true], 'fwrite' => ['hasSideEffects' => true], + 'json_validate' => ['hasSideEffects' => false], 'lchgrp' => ['hasSideEffects' => true], 'lchown' => ['hasSideEffects' => true], 'link' => ['hasSideEffects' => true], + 'mb_str_pad' => ['hasSideEffects' => false], 'mkdir' => ['hasSideEffects' => true], 'move_uploaded_file' => ['hasSideEffects' => true], + 'ob_clean' => ['hasSideEffects' => true], + 'ob_end_clean' => ['hasSideEffects' => true], + 'ob_end_flush' => ['hasSideEffects' => true], + 'ob_flush' => ['hasSideEffects' => true], + 'ob_get_clean' => ['hasSideEffects' => true], + 'ob_get_contents' => ['hasSideEffects' => true], + 'ob_get_length' => ['hasSideEffects' => true], + 'ob_get_level' => ['hasSideEffects' => true], + 'ob_get_status' => ['hasSideEffects' => true], + 'ob_list_handlers' => ['hasSideEffects' => true], + 'output_add_rewrite_var' => ['hasSideEffects' => true], + 'output_reset_rewrite_vars' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], 'popen' => ['hasSideEffects' => true], 'readfile' => ['hasSideEffects' => true], @@ -95,6 +114,8 @@ 'rewind' => ['hasSideEffects' => true], 'rmdir' => ['hasSideEffects' => true], 'sprintf' => ['hasSideEffects' => false], + 'str_decrement' => ['hasSideEffects' => false], + 'str_increment' => ['hasSideEffects' => false], 'symlink' => ['hasSideEffects' => true], 'tempnam' => ['hasSideEffects' => true], 'tmpfile' => ['hasSideEffects' => true], @@ -142,4 +163,20 @@ 'DateTimeImmutable::getOffset' => ['hasSideEffects' => false], 'DateTimeImmutable::getTimestamp' => ['hasSideEffects' => false], 'DateTimeImmutable::getTimezone' => ['hasSideEffects' => false], + + 'SplFileObject::fflush' => ['hasSideEffects' => true], + 'SplFileObject::fgetc' => ['hasSideEffects' => true], + 'SplFileObject::fgetcsv' => ['hasSideEffects' => true], + 'SplFileObject::fgets' => ['hasSideEffects' => true], + 'SplFileObject::fgetss' => ['hasSideEffects' => true], + 'SplFileObject::fpassthru' => ['hasSideEffects' => true], + 'SplFileObject::fputcsv' => ['hasSideEffects' => true], + 'SplFileObject::fread' => ['hasSideEffects' => true], + 'SplFileObject::fscanf' => ['hasSideEffects' => true], + 'SplFileObject::fseek' => ['hasSideEffects' => true], + 'SplFileObject::ftruncate' => ['hasSideEffects' => true], + 'SplFileObject::fwrite' => ['hasSideEffects' => true], + + 'XmlReader::next' => ['hasSideEffects' => true], + 'XmlReader::read' => ['hasSideEffects' => true], ]; diff --git a/bin/generate-changelog.php b/bin/generate-changelog.php deleted file mode 100755 index 5119d62564..0000000000 --- a/bin/generate-changelog.php +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env php -setName('run'); - $this->addArgument('fromCommit', InputArgument::REQUIRED); - $this->addArgument('toCommit', InputArgument::REQUIRED); - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $commitLines = $this->exec(['git', 'log', sprintf('%s..%s', $input->getArgument('fromCommit'), $input->getArgument('toCommit')), '--reverse', '--pretty=%H %s']); - $commits = array_map(function (string $line): array { - [$hash, $message] = explode(' ', $line, 2); - - return [ - 'hash' => $hash, - 'message' => $message - ]; - }, explode("\n", $commitLines)); - - $i = 0; - - foreach ($commits as $commit) { - $searchPullRequestsResponse = Request::get(sprintf('https://api.github.com/search/issues?q=repo:phpstan/phpstan-src+%s', $commit['hash'])) - ->sendsAndExpectsType('application/json') - ->basicAuth('ondrejmirtes', getenv('GITHUB_TOKEN')) - ->send(); - if ($searchPullRequestsResponse->code !== 200) { - $output->writeln(var_export($searchPullRequestsResponse->body, true)); - throw new \InvalidArgumentException((string) $searchPullRequestsResponse->code); - } - $searchPullRequestsResponse = $searchPullRequestsResponse->body; - - $searchIssuesResponse = Request::get(sprintf('https://api.github.com/search/issues?q=repo:phpstan/phpstan+%s', $commit['hash'])) - ->sendsAndExpectsType('application/json') - ->basicAuth('ondrejmirtes', getenv('GITHUB_TOKEN')) - ->send(); - if ($searchIssuesResponse->code !== 200) { - $output->writeln(var_export($searchIssuesResponse->body, true)); - throw new \InvalidArgumentException((string) $searchIssuesResponse->code); - } - $searchIssuesResponse = $searchIssuesResponse->body; - $items = array_merge($searchPullRequestsResponse->items, $searchIssuesResponse->items); - $parenthesis = 'https://github.com/phpstan/phpstan-src/commit/' . $commit['hash']; - $thanks = null; - $issuesToReference = []; - foreach ($items as $responseItem) { - if (isset($responseItem->pull_request)) { - $parenthesis = sprintf('[#%d](%s)', $responseItem->number, 'https://github.com/phpstan/phpstan-src/pull/' . $responseItem->number); - $thanks = $responseItem->user->login; - } else { - $issuesToReference[] = sprintf('#%d', $responseItem->number); - } - } - - $output->writeln(sprintf('* %s (%s)%s%s', $commit['message'], $parenthesis, count($issuesToReference) > 0 ? ', ' . implode(', ', $issuesToReference) : '', $thanks !== null ? sprintf(', thanks @%s!', $thanks) : '')); - - if ($i > 0 && $i % 8 === 0) { - sleep(60); - } - - $i++; - } - - return 0; - } - - /** - * @param string[] $commandParts - * @return string - */ - private function exec(array $commandParts): string - { - $command = implode(' ', array_map(function (string $part): string { - return escapeshellarg($part); - }, $commandParts)); - - exec($command, $outputLines, $statusCode); - $output = implode("\n", $outputLines); - if ($statusCode !== 0) { - throw new \InvalidArgumentException(sprintf('Command %s failed: %s', $command, $output)); - } - - return $output; - } - - }; - - $application = new \Symfony\Component\Console\Application(); - $application->add($command); - $application->setDefaultCommand('run', true); - $application->run(); - -})(); diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index a3779dda53..d161d374e4 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -1,36 +1,87 @@ #!/usr/bin/env php -create(ParserFactory::ONLY_PHP7); - $finder = new Symfony\Component\Finder\Finder(); + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $finder = new Finder(); $finder->in(__DIR__ . '/../vendor/jetbrains/phpstorm-stubs')->files()->name('*.php'); - $visitor = new class() extends \PhpParser\NodeVisitorAbstract { + $visitor = new class() extends NodeVisitorAbstract { /** @var string[] */ - public $functions = []; + public array $functions = []; + + /** @var list */ + public array $impureFunctions = []; /** @var string[] */ - public $methods = []; + public array $methods = []; public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Function_) { + assert(isset($node->namespacedName)); + $functionName = $node->namespacedName->toLowerString(); + foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { - if ($attr->name->toString() === \JetBrains\PhpStorm\Pure::class) { - $this->functions[] = $node->namespacedName->toLowerString(); + if ($attr->name->toString() !== Pure::class) { + continue; + } + + // The following functions have side effects, but their state is managed within the PHPStan scope: + if (in_array($functionName, [ + 'stat', + 'lstat', + 'file_exists', + 'is_writable', + 'is_writeable', + 'is_readable', + 'is_executable', + 'is_file', + 'is_dir', + 'is_link', + 'filectime', + 'fileatime', + 'filemtime', + 'fileinode', + 'filegroup', + 'fileowner', + 'filesize', + 'filetype', + 'fileperms', + 'ftell', + 'ini_get', + 'function_exists', + 'json_last_error', + 'json_last_error_msg', + ], true)) { + $this->functions[] = $functionName; break 2; } + + // PhpStorm stub's #[Pure(true)] means the function has side effects but its return value is important. + // In PHPStan's criteria, these functions are simply considered as ['hasSideEffect' => true]. + if (isset($attr->args[0]->value->name->name) && $attr->args[0]->value->name->name === 'true') { + $this->impureFunctions[] = $functionName; + } else { + $this->functions[] = $functionName; + } + break 2; } } } @@ -38,12 +89,12 @@ public function enterNode(Node $node) if ($node instanceof Node\Stmt\ClassMethod) { $class = $node->getAttribute('parent'); if (!$class instanceof Node\Stmt\ClassLike) { - throw new \PHPStan\ShouldNotHappenException($node->name->toString()); + throw new ShouldNotHappenException($node->name->toString()); } $className = $class->namespacedName->toString(); foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { - if ($attr->name->toString() === \JetBrains\PhpStorm\Pure::class) { + if ($attr->name->toString() === Pure::class) { $this->methods[] = sprintf('%s::%s', $className, $node->name->toString()); break 2; } @@ -53,6 +104,7 @@ public function enterNode(Node $node) return null; } + }; foreach ($finder as $stubFile) { @@ -63,32 +115,46 @@ public function enterNode(Node $node) $traverser->addVisitor($visitor); $traverser->traverse( - $parser->parse(\PHPStan\File\FileReader::read($path)) + $parser->parse(FileReader::read($path)), ); } + /** @var array $metadata */ $metadata = require __DIR__ . '/functionMetadata_original.php'; 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); + throw new ShouldNotHappenException($functionName); } } $metadata[$functionName] = ['hasSideEffects' => false]; } + foreach ($visitor->impureFunctions as $functionName) { + if (in_array($functionName, [ + 'class_exists', + 'enum_exists', + 'interface_exists', + 'trait_exists', + ], true)) { + continue; + } + if (array_key_exists($functionName, $metadata)) { + if (in_array($functionName, [ + 'ob_get_contents', + ], true)) { + continue; + } + if (!$metadata[$functionName]['hasSideEffects']) { + throw new ShouldNotHappenException($functionName); + } + } + $metadata[$functionName] = ['hasSideEffects' => true]; + } foreach ($visitor->methods as $methodName) { if (array_key_exists($methodName, $metadata)) { if ($metadata[$methodName]['hasSideEffects']) { - throw new \PHPStan\ShouldNotHappenException($methodName); + throw new ShouldNotHappenException($methodName); } } $metadata[$methodName] = ['hasSideEffects' => false]; @@ -99,6 +165,20 @@ public function enterNode(Node $node) $template = <<<'php' true as a modification to bin/functionMetadata_original.php. + * 3) Contribute the #[Pure] functions without side effects to https://github.com/JetBrains/phpstorm-stubs + * 4) Once the PR from 3) is merged, please update the package here and run ./bin/generate-function-metadata.php. + */ + return [ %s ]; @@ -113,6 +193,5 @@ public function enterNode(Node $node) ); } - \PHPStan\File\FileWriter::write(__DIR__ . '/../resources/functionMetadata.php', sprintf($template, $content)); - + FileWriter::write(__DIR__ . '/../resources/functionMetadata.php', sprintf($template, $content)); })(); diff --git a/bin/generate-rule-error-classes.php b/bin/generate-rule-error-classes.php index 116910ee86..633b1824b4 100755 --- a/bin/generate-rule-error-classes.php +++ b/bin/generate-rule-error-classes.php @@ -1,7 +1,9 @@ #!/usr/bin/env php - [$interface, $propertyName, $nativePropertyType, $phpDocPropertyType]) { - if (($typeCombination & $typeNumber) === $typeNumber) { - $interfaces[] = '\\' . $interface; - if ($propertyName !== null && $nativePropertyType !== null && $phpDocPropertyType !== null) { - $properties[] = [$propertyName, $nativePropertyType, $phpDocPropertyType]; - } + foreach ($ruleErrorTypes as $typeNumber => [$interface, $typeProperties]) { + if (!(($typeCombination & $typeNumber) === $typeNumber)) { + continue; } + + $interfaces[] = '\\' . $interface; + $properties = array_merge($properties, $typeProperties); } $phpClass = sprintf( $template, $typeCombination, implode(', ', $interfaces), - implode("\n\n\t", array_map(function (array $property): string { - return sprintf("%spublic %s $%s;", $property[2] !== $property[1] ? sprintf("/** @var %s */\n\t", $property[2]) : '', $property[1], $property[0]); - }, $properties)), - implode("\n\n\t", array_map(function (array $property): string { - return sprintf("%spublic function get%s(): %s\n\t{\n\t\treturn \$this->%s;\n\t}", $property[2] !== $property[1] ? sprintf("/**\n\t * @return %s\n\t */\n\t", $property[2]) : '', ucfirst($property[0]), $property[1], $property[0]); - }, $properties)) + implode("\n\n\t", array_map(static fn (array $property): string => sprintf('%spublic %s $%s;', $property[2] !== $property[1] ? sprintf("/** @var %s */\n\t", $property[2]) : '', $property[1], $property[0]), $properties)), + implode("\n\n\t", array_map(static fn (array $property): string => sprintf("%spublic function get%s(): %s\n\t{\n\t\treturn \$this->%s;\n\t}", $property[2] !== $property[1] ? sprintf("/**\n\t * @return %s\n\t */\n\t", $property[2]) : '', ucfirst($property[0]), $property[1], $property[0]), $properties)), ); file_put_contents(__DIR__ . '/../src/Rules/RuleErrors/RuleError' . $typeCombination . '.php', $phpClass); diff --git a/bin/make-optional-parameters-required.php b/bin/make-optional-parameters-required.php new file mode 100755 index 0000000000..5d2dd308c0 --- /dev/null +++ b/bin/make-optional-parameters-required.php @@ -0,0 +1,51 @@ +#!/usr/bin/env php +createForHostVersion(); + $traverser = new NodeTraverser(new CloningVisitor()); + $printer = new Standard(); + $finder = new Finder(); + $finder->followLinks(); + + $removeParamDefaultTraverser = new NodeTraverser(new class () extends NodeVisitorAbstract { + + public function enterNode(Node $node) + { + if (!$node instanceof Node\Param) { + return null; + } + + $node->default = null; + + return $node; + } + + }); + foreach ($finder->files()->name('*.php')->in($dir) as $fileInfo) { + $oldStmts = $parser->parse(file_get_contents($fileInfo->getPathname())); + $oldTokens = $parser->getTokens(); + + $newStmts = $traverser->traverse($oldStmts); + $newStmts = $removeParamDefaultTraverser->traverse($newStmts); + + $newCode = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens); + file_put_contents($fileInfo->getPathname(), $newCode); + } +})(); diff --git a/bin/phpstan b/bin/phpstan index 2c06a78841..119af4377c 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -3,24 +3,30 @@ use PHPStan\Command\AnalyseCommand; use PHPStan\Command\ClearResultCacheCommand; +use PHPStan\Command\DiagnoseCommand; +use PHPStan\Command\DumpParametersCommand; use PHPStan\Command\FixerWorkerCommand; use PHPStan\Command\WorkerCommand; +use PHPStan\Internal\ComposerHelper; use Symfony\Component\Console\Helper\ProgressBar; (function () { - error_reporting(E_ALL); + error_reporting(E_ALL & ~E_DEPRECATED); ini_set('display_errors', 'stderr'); - if (version_compare(PHP_VERSION, '7.4.0', '<')) { - // PHP earlier than 7.4.x with OpCache triggers a bug when we intercept - // custom autoloaders' reads to discover file paths. See PHPStan #4881. - ini_set('opcache.enable', 'Off'); - } - gc_disable(); // performance boost define('__PHPSTAN_RUNNING__', true); + $analysisStartTime = microtime(true); + $devOrPharLoader = require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../preload.php'; + $composer = ComposerHelper::getComposerConfig(getcwd()); + + if ($composer !== null) { + $vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig(getcwd(), $composer); + } else { + $vendorDirectory = getcwd() . '/' . 'vendor'; + } $devOrPharLoader->unregister(); $composerAutoloadFiles = $GLOBALS['__composer_autoload_files']; @@ -30,10 +36,8 @@ use Symfony\Component\Console\Helper\ProgressBar; || !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) + || !array_key_exists('23c18046f52bef3eea034657bafda50f', $composerAutoloadFiles) ) { echo "Composer autoloader changed\n"; exit(1); @@ -54,76 +58,100 @@ use Symfony\Component\Console\Helper\ProgressBar; // 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, + + // vendor/symfony/polyfill-php81/bootstrap.php + '23c18046f52bef3eea034657bafda50f' => true, ]; - $autoloaderInWorkingDirectory = getcwd() . '/vendor/autoload.php'; + $autoloaderInWorkingDirectory = $vendorDirectory . '/autoload.php'; $composerAutoloaderProjectPaths = []; - if (is_file($autoloaderInWorkingDirectory)) { + + /** @var array|false $autoloadFunctionsBefore */ + $autoloadFunctionsBefore = spl_autoload_functions(); + + if (@is_file($autoloaderInWorkingDirectory)) { $composerAutoloaderProjectPaths[] = dirname($autoloaderInWorkingDirectory, 2); require_once $autoloaderInWorkingDirectory; } - $autoloadProjectAutoloaderFile = function (string $file) use (&$composerAutoloaderProjectPaths): void { - $path = dirname(__DIR__) . $file; - if (!extension_loaded('phar')) { - if (is_file($path)) { + $path = dirname(__DIR__, 3) . '/autoload.php'; + if (!extension_loaded('phar')) { + if (@is_file($path)) { + $composerAutoloaderProjectPaths[] = dirname($path, 2); + + require_once $path; + } + } else { + $pharPath = \Phar::running(false); + if ($pharPath === '') { + if (@is_file($path)) { $composerAutoloaderProjectPaths[] = dirname($path, 2); require_once $path; } } else { - $pharPath = \Phar::running(false); - if ($pharPath === '') { - if (is_file($path)) { - $composerAutoloaderProjectPaths[] = dirname($path, 2); + $path = dirname($pharPath, 3) . '/autoload.php'; + if (@is_file($path)) { + $composerAutoloaderProjectPaths[] = dirname($path, 2); - require_once $path; - } - } else { - $path = dirname($pharPath) . $file; - if (is_file($path)) { - $composerAutoloaderProjectPaths[] = dirname($path, 2); + require_once $path; + } + } + } - require_once $path; + /** @var array|false $autoloadFunctionsAfter */ + $autoloadFunctionsAfter = spl_autoload_functions(); + + if ($autoloadFunctionsBefore !== false && $autoloadFunctionsAfter !== false) { + $newAutoloadFunctions = []; + foreach ($autoloadFunctionsAfter as $after) { + if ( + is_array($after) + && count($after) > 0 + ) { + if (is_object($after[0]) + && get_class($after[0]) === \Composer\Autoload\ClassLoader::class + ) { + continue; + } + if ($after[0] === 'PHPStan\\PharAutoloader') { + continue; + } + } + foreach ($autoloadFunctionsBefore as $before) { + if ($after === $before) { + continue 2; } } + + $newAutoloadFunctions[] = $after; } - }; - $autoloadProjectAutoloaderFile('/../../autoload.php'); + $GLOBALS['__phpstanAutoloadFunctions'] = $newAutoloadFunctions; + } $devOrPharLoader->register(true); - $version = 'Version unknown'; - try { - $version = \Jean85\PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion() ?: $version; - } catch (\OutOfBoundsException $e) { - - } - $application = new \Symfony\Component\Console\Application( 'PHPStan - PHP Static Analysis Tool', - $version + ComposerHelper::getPhpStanVersion() ); $application->setDefaultCommand('analyse'); ProgressBar::setFormatDefinition('file_download', ' [%bar%] %percent:3s%% %fileSize%'); - $reversedComposerAutoloaderProjectPaths = array_reverse($composerAutoloaderProjectPaths); - $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths)); + $composerAutoloaderProjectPaths = array_map(function(string $s): string { + return str_replace(DIRECTORY_SEPARATOR, '/', $s); + }, $composerAutoloaderProjectPaths); + $reversedComposerAutoloaderProjectPaths = array_values(array_unique(array_reverse($composerAutoloaderProjectPaths))); + + $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths, $analysisStartTime)); $application->add(new WorkerCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new ClearResultCacheCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new FixerWorkerCommand($reversedComposerAutoloaderProjectPaths)); + $application->add(new DumpParametersCommand($reversedComposerAutoloaderProjectPaths)); + $application->add(new DiagnoseCommand($reversedComposerAutoloaderProjectPaths)); $application->run(); })(); diff --git a/build-cs/composer.json b/build-cs/composer.json index 7912119afa..16a240bc97 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -1,8 +1,8 @@ { "require-dev": { "consistence-community/coding-standard": "^3.11.0", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^7.0.18", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "slevomat/coding-standard": "^8.8.0", "squizlabs/php_codesniffer": "^3.5.3" }, "config": { diff --git a/build-cs/composer.lock b/build-cs/composer.lock index b787a5858a..e0bfbd4cae 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": "98d65cadfe6f694505bce09b80004cae", + "content-hash": "e69c1916405a7e3c8001c1b609a0ee61", "packages": [], "packages-dev": [ { "name": "consistence-community/coding-standard", - "version": "3.11.1", + "version": "3.11.3", "source": { "type": "git", "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df" + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/4632fead8c9ee8f50044fcbce9f66c797b34c0df", - "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", "shasum": "" }, "require": { - "php": ">=7.4", - "slevomat/coding-standard": "~7.0", - "squizlabs/php_codesniffer": "~3.6.0" + "php": "~8.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "~3.7.0" }, "replace": { "consistence/coding-standard": "3.10.*" }, "require-dev": { - "phing/phing": "2.16.4", - "php-parallel-lint/php-parallel-lint": "1.3.0", - "phpunit/phpunit": "9.5.4" + "phing/phing": "2.17.0", + "php-parallel-lint/php-parallel-lint": "1.3.1", + "phpunit/phpunit": "9.5.10" }, "type": "library", "autoload": { @@ -70,41 +70,44 @@ ], "support": { "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.1" + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.3" }, - "time": "2021-05-03T18:13:22+00:00" + "time": "2023-03-27T14:55:41+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.2", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", + "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -120,7 +123,7 @@ }, { "name": "Contributors", - "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -144,42 +147,40 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2022-02-04T12:51:07+00:00" + "time": "2023-01-05T11:28:13+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.2.0", + "version": "1.24.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" + "reference": "bcad8d995980440892759db0c32acae7c8e79442" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442", + "reference": "bcad8d995980440892759db0c32acae7c8e79442", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { "psr-4": { "PHPStan\\PhpDocParser\\": [ @@ -194,48 +195,48 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.2.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.2" }, - "time": "2021-09-16T20:46:02+00:00" + "time": "2023-09-26T12:28:12+00:00" }, { "name": "slevomat/coding-standard", - "version": "7.0.18", + "version": "8.14.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc" + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", - "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", - "php": "^7.1 || ^8.0", - "phpstan/phpdoc-parser": "^1.0.0", - "squizlabs/php_codesniffer": "^3.6.1" + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "php": "^7.2 || ^8.0", + "phpstan/phpdoc-parser": "^1.23.1", + "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { - "phing/phing": "2.17.0", - "php-parallel-lint/php-parallel-lint": "1.3.1", - "phpstan/phpstan": "1.2.0", - "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.10" + "phing/phing": "2.17.4", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.10.37", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.14", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "8.5.21|9.6.8|10.3.5" }, "type": "phpcodesniffer-standard", "extra": { "branch-alias": { - "dev-master": "7.x-dev" + "dev-master": "8.x-dev" } }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", @@ -243,9 +244,13 @@ "MIT" ], "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.0.18" + "source": "https://github.com/slevomat/coding-standard/tree/8.14.1" }, "funding": [ { @@ -257,20 +262,20 @@ "type": "tidelift" } ], - "time": "2021-12-07T17:19:06+00:00" + "time": "2023-10-08T07:28:08+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -306,14 +311,15 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2021-12-12T21:44:58+00:00" + "time": "2023-02-22T23:07:41+00:00" } ], "aliases": [], @@ -323,5 +329,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } diff --git a/build.xml b/build.xml deleted file mode 100644 index ba53184e43..0000000000 --- a/build.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/PHPStan/Build/ContainerDynamicReturnTypeExtension.php b/build/PHPStan/Build/ContainerDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..8e43bd2d47 --- /dev/null +++ b/build/PHPStan/Build/ContainerDynamicReturnTypeExtension.php @@ -0,0 +1,62 @@ +getName(), [ + 'getByType', + ], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + if (count($methodCall->getArgs()) === 0) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + $argType = $scope->getType($methodCall->getArgs()[0]->value); + if (!$argType instanceof ConstantStringType) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + $type = new ObjectType($argType->getValue()); + if ($methodReflection->getName() === 'getByType' && count($methodCall->getArgs()) >= 2) { + $argType = $scope->getType($methodCall->getArgs()[1]->value); + if ($argType->isTrue()->yes()) { + $type = TypeCombinator::addNull($type); + } + } + + return $type; + } + +} diff --git a/build/PHPStan/Build/FinalClassRule.php b/build/PHPStan/Build/FinalClassRule.php new file mode 100644 index 0000000000..018e1da4f8 --- /dev/null +++ b/build/PHPStan/Build/FinalClassRule.php @@ -0,0 +1,74 @@ + + */ +final class FinalClassRule implements Rule +{ + + public function __construct(private FileHelper $fileHelper) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$classReflection->isClass()) { + return []; + } + if ($classReflection->isAbstract()) { + return []; + } + if ($classReflection->isFinal()) { + return []; + } + if ($classReflection->is(Type::class)) { + return []; + } + + // exceptions + if (in_array($classReflection->getName(), [ + FunctionVariant::class, + ExtendedFunctionVariant::class, + DummyParameter::class, + PhpFunctionFromParserNodeReflection::class, + ], true)) { + return []; + } + + if (str_starts_with($this->fileHelper->normalizePath($scope->getFile()), $this->fileHelper->normalizePath(dirname(__DIR__, 3) . '/tests'))) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Class %s must be abstract or final.', $classReflection->getDisplayName()), + ) + ->identifier('phpstan.finalClass') + ->build(), + ]; + } + +} diff --git a/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php b/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php index da67c961e6..22812240f4 100644 --- a/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php +++ b/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php @@ -6,11 +6,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ServiceLocatorDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +final class ServiceLocatorDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension { public function getClass(): string @@ -34,10 +33,14 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); - if ($methodReflection->getName() === 'getByType' && count($methodCall->getArgs()) >= 2) { - $argType = $scope->getType($methodCall->getArgs()[1]->value); - if ($argType instanceof ConstantBooleanType && $argType->getValue()) { - $returnType = TypeCombinator::addNull($returnType); + if ($methodReflection->getName() === 'getByType') { + if (count($methodCall->getArgs()) < 2) { + $returnType = TypeCombinator::removeNull($returnType); + } else { + $argType = $scope->getType($methodCall->getArgs()[1]->value); + if ($argType->isTrue()->yes()) { + $returnType = TypeCombinator::removeNull($returnType); + } } } diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon index f1efeccdf6..82e6b89e0c 100644 --- a/build/baseline-7.4.neon +++ b/build/baseline-7.4.neon @@ -4,10 +4,6 @@ parameters: message: "#^Class PHPStan\\\\Command\\\\ErrorsConsoleStyle has an uninitialized property \\$progressBar\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/Command/ErrorsConsoleStyle.php - - - message: "#^Class PHPStan\\\\DependencyInjection\\\\Reflection\\\\DirectClassReflectionExtensionRegistryProvider has an uninitialized property \\$broker\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php - message: "#^Class PHPStan\\\\Parallel\\\\ParallelAnalyser has an uninitialized property \\$processPool\\. Give it default value or assign it in the constructor\\.$#" @@ -18,10 +14,6 @@ parameters: message: "#^Class PHPStan\\\\Parallel\\\\Process has an uninitialized property \\$process\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/Parallel/Process.php - - - message: "#^Class PHPStan\\\\Parallel\\\\Process has an uninitialized property \\$in\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/Parallel/Process.php - message: "#^Class PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$phpDocNodes\\. Give it default value or assign it in the constructor\\.$#" count: 1 @@ -55,6 +47,12 @@ parameters: message: "#^Class PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$phpDocNodeResolver\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/PhpDoc/ResolvedPhpDocBlock.php + + - + message: "#^Class PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/PhpDoc/ResolvedPhpDocBlock.php + - message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$fileName\\. Give it default value or assign it in the constructor\\.$#" count: 1 diff --git a/build/baseline-8.0.neon b/build/baseline-8.0.neon index 7d6f91d1c8..95dfa6cf8a 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -1,42 +1,36 @@ parameters: ignoreErrors: - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" + message: "#^Strict comparison using \\=\\=\\= between list and false will always evaluate to false\\.$#" count: 1 path: ../src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" + message: "#^Strict comparison using \\=\\=\\= between list\\ and false will always evaluate to false\\.$#" count: 1 path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php - - message: "#^Strict comparison using \\=\\=\\= between non-empty-array and false will always evaluate to false\\.$#" + message: "#^Strict comparison using \\=\\=\\= between int<0, max> and false will always evaluate to false\\.$#" count: 1 - path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - - - message: "#^Call to function method_exists\\(\\) with ReflectionProperty and 'isPromoted' will always evaluate to true\\.$#" - paths: - - ../src/Reflection/Php/PhpClassReflectionExtension.php - - ../src/Reflection/Php/PhpPropertyReflection.php + path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - message: "#^Call to function method_exists\\(\\) with ReflectionProperty and 'getDefaultValue' will always evaluate to true\\.$#" - paths: - - ../tests/PHPStan/Analyser/AnalyserIntegrationTest.php + message: "#^Strict comparison using \\=\\=\\= between list\\ and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - message: "#^Call to function method_exists\\(\\) with ReflectionParameter and 'isPromoted' will always evaluate to true\\.$#" - paths: - - ../src/Reflection/Php/PhpClassReflectionExtension.php + message: "#^Strict comparison using \\=\\=\\= between list\\ and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - message: "#^Call to function method_exists\\(\\) with ReflectionClass and 'getAttributes' will always evaluate to true\\.$#" + message: "#^Strict comparison using \\=\\=\\= between list and false will always evaluate to false\\.$#" count: 1 - path: ../src/Reflection/ClassReflection.php + path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" + message: "#^Call to function is_bool\\(\\) with string will always evaluate to false\\.$#" count: 1 - path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php + path: ../src/Type/Php/SubstrDynamicReturnTypeExtension.php diff --git a/build/baseline-8.1.neon b/build/baseline-8.1.neon index fdd42f9d3d..aab4991158 100644 --- a/build/baseline-8.1.neon +++ b/build/baseline-8.1.neon @@ -1,22 +1,2 @@ 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 - + ignoreErrors: [] diff --git a/build/baseline-lt-7.3.neon b/build/baseline-lt-7.3.neon deleted file mode 100644 index 8d088b9056..0000000000 --- a/build/baseline-lt-7.3.neon +++ /dev/null @@ -1,6 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Call to an undefined static method PHPUnit\\\\Framework\\\\TestCase\\:\\:assertFileDoesNotExist\\(\\)\\.$#" - count: 1 - path: ../src/Testing/LevelsTestCase.php diff --git a/build/collision-detector.json b/build/collision-detector.json new file mode 100644 index 0000000000..a687cd3ea4 --- /dev/null +++ b/build/collision-detector.json @@ -0,0 +1,20 @@ +{ + "scanPaths": ["../src", "../build", "../tests"], + "excludePaths": [ + "../tests/PHPStan/Analyser/data/parse-error.php", + "../tests/PHPStan/Analyser/data/multipleParseErrors.php", + "../tests/PHPStan/Parser/data/cleaning-1-before.php", + "../tests/PHPStan/Parser/data/cleaning-1-after.php", + "../tests/PHPStan/Parser/data/cleaning-property-hooks-before.php", + "../tests/PHPStan/Parser/data/cleaning-property-hooks-after.php", + "../tests/PHPStan/Rules/Functions/data/duplicate-function.php", + "../tests/PHPStan/Rules/Classes/data/duplicate-class.php", + "../tests/PHPStan/Rules/Names/data/multiple-namespaces.php", + "../tests/PHPStan/Rules/Names/data/no-namespace.php", + "../tests/notAutoloaded", + "../tests/PHPStan/Rules/Functions/data/define-bug-3349.php", + "../tests/PHPStan/Levels/data/stubs/function.php", + "../tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php", + "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php" + ] +} diff --git a/build/composer-dependency-analyser.php b/build/composer-dependency-analyser.php new file mode 100644 index 0000000000..79dc694253 --- /dev/null +++ b/build/composer-dependency-analyser.php @@ -0,0 +1,38 @@ +addPathToScan(__DIR__ . '/../bin', true) + ->ignoreErrorsOnPackages( + [ + ...$pinnedToSupportPhp72, // those are unused, but we need to pin them to support PHP 7.2 + ...$polyfills, // not detected by composer-dependency-analyser + ], + [ErrorType::UNUSED_DEPENDENCY], + ) + ->ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // prepared test tooling + ->ignoreErrorsOnPackage('jetbrains/phpstorm-stubs', [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]) // there is no direct usage, but we need newer version then required by ondrejmirtes/BetterReflection + ->ignoreErrorsOnPath(__DIR__ . '/../tests', [ErrorType::UNKNOWN_CLASS, ErrorType::UNKNOWN_FUNCTION, ErrorType::SHADOW_DEPENDENCY]) // to be able to test invalid symbols + ->ignoreUnknownClasses([ + 'JetBrains\PhpStorm\Pure', // not present on composer's classmap + 'PHPStan\ExtensionInstaller\GeneratedConfig', // generated + ]); diff --git a/build/composer-require-checker.json b/build/composer-require-checker.json deleted file mode 100644 index 75cb6d099e..0000000000 --- a/build/composer-require-checker.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "symbol-whitelist" : [ - "null", "true", "false", - "static", "self", "parent", - "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", - "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", "ReflectionIntersectionType", "Attribute", "ReflectionEnum", "ReflectionEnumBackedCase", "enum_exists", - "Clue\\React\\Block\\await", "Hoa\\File\\Read" - ], - "php-core-extensions" : [ - "Core", - "date", - "pcre", - "Phar", - "Reflection", - "SPL", - "standard", - "pcntl", - "mbstring", - "hash", - "tokenizer", - "dom" - ] -} diff --git a/build/composer-require-checker.phar b/build/composer-require-checker.phar deleted file mode 100755 index 59c029ef5c..0000000000 Binary files a/build/composer-require-checker.phar and /dev/null differ diff --git a/build/datetime-php-83.neon b/build/datetime-php-83.neon new file mode 100644 index 0000000000..953379bf4f --- /dev/null +++ b/build/datetime-php-83.neon @@ -0,0 +1,11 @@ +parameters: + ignoreErrors: + - + message: "#^If condition is always false\\.$#" + count: 1 + path: ../src/Type/Php/DateTimeModifyReturnTypeExtension.php + + - + message: "#^Strict comparison using \\=\\=\\= between DateTime and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/DateTimeModifyReturnTypeExtension.php diff --git a/build/deprecated-8.4.neon b/build/deprecated-8.4.neon new file mode 100644 index 0000000000..6b3bd6db5e --- /dev/null +++ b/build/deprecated-8.4.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: '#^Use of constant E_STRICT is deprecated\.$#' + identifier: constant.deprecated + count: 1 + path: ../src/Analyser/FileAnalyser.php diff --git a/build/downgrade.php b/build/downgrade.php new file mode 100644 index 0000000000..7c117d13f5 --- /dev/null +++ b/build/downgrade.php @@ -0,0 +1,20 @@ + [ + __DIR__ . '/../build/PHPStan', + __DIR__ . '/../src', + __DIR__ . '/../tests/PHPStan', + __DIR__ . '/../tests/e2e', + ], + 'excludePaths' => [ + 'tests/*/data/*', + 'tests/*/Fixture/*', + 'tests/PHPStan/Analyser/traits/*', + 'tests/PHPStan/Analyser/nsrt/*', + 'tests/PHPStan/Generics/functions.php', + 'tests/e2e/resultCache_1.php', + 'tests/e2e/resultCache_2.php', + 'tests/e2e/resultCache_3.php', + ], +]; diff --git a/build/enum-adapter-errors.neon b/build/enum-adapter-errors.neon deleted file mode 100644 index 7516959bee..0000000000 --- a/build/enum-adapter-errors.neon +++ /dev/null @@ -1,42 +0,0 @@ -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 index 6c61194dd9..44eaccbbd1 100644 --- a/build/enums.neon +++ b/build/enums.neon @@ -2,3 +2,18 @@ parameters: excludePaths: - ../tests/PHPStan/Fixture/TestEnum.php - ../tests/PHPStan/Fixture/AnotherTestEnum.php + - ../tests/PHPStan/Fixture/ManyCasesTestEnum.php + + ignoreErrors: + - + message: '#^Access to constant ONE on an unknown class EnumTypeAssertions\\Foo\.$#' + path: ../tests/PHPStan/Analyser/NodeScopeResolverTest.php + - + message: '#^Class ObjectTypeEnums\\FooEnum not found\.$#' + paths: + - ../tests/PHPStan/Type/ObjectTypeTest.php + - ../tests/PHPStan/Type/IntersectionTypeTest.php + - + message: '#^Class CustomDeprecations\\MyDeprecatedEnum not found\.$#' + paths: + - ../tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php diff --git a/build/even-more-enum-adapter-errors.neon b/build/even-more-enum-adapter-errors.neon new file mode 100644 index 0000000000..364905f714 --- /dev/null +++ b/build/even-more-enum-adapter-errors.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: diff --git a/build/ignore-by-php-version.neon.php b/build/ignore-by-php-version.neon.php index 518cc5a40a..c250ea9eec 100644 --- a/build/ignore-by-php-version.neon.php +++ b/build/ignore-by-php-version.neon.php @@ -1,11 +1,6 @@ = 80000) { $includes[] = __DIR__ . '/baseline-8.0.neon'; } @@ -13,6 +8,7 @@ $includes[] = __DIR__ . '/baseline-8.1.neon'; } else { $includes[] = __DIR__ . '/enums.neon'; + $includes[] = __DIR__ . '/readonly-property.neon'; } if (PHP_VERSION_ID >= 70400) { @@ -20,11 +16,27 @@ } if (PHP_VERSION_ID < 80000) { - $includes[] = __DIR__ . '/enum-adapter-errors.neon'; + $includes[] = __DIR__ . '/more-enum-adapter-errors.neon'; +} + +if (PHP_VERSION_ID < 80000) { + $includes[] = __DIR__ . '/spl-autoload-functions-pre-php-7.neon'; +} else { + $includes[] = __DIR__ . '/spl-autoload-functions-php-8.neon'; +} + +if (PHP_VERSION_ID >= 80300) { + $includes[] = __DIR__ . '/datetime-php-83.neon'; +} + +if (PHP_VERSION_ID >= 80400) { + $includes[] = __DIR__ . '/deprecated-8.4.neon'; } $config = []; $config['includes'] = $includes; + +// overrides config.platform.php in composer.json $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 54b5cb905d..d5ae8bada3 100644 --- a/build/ignore-gte-php7.4-errors.neon +++ b/build/ignore-gte-php7.4-errors.neon @@ -3,9 +3,6 @@ includes: parameters: ignoreErrors: - - - message: "#^Call to function method_exists\\(\\) with ReflectionProperty and '(?:hasType|getType)' will always evaluate to true\\.$#" - 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)#' - diff --git a/build/more-enum-adapter-errors.neon b/build/more-enum-adapter-errors.neon new file mode 100644 index 0000000000..4e7e2ee083 --- /dev/null +++ b/build/more-enum-adapter-errors.neon @@ -0,0 +1,24 @@ +parameters: + ignoreErrors: + - + message: "#^Strict comparison using \\!\\=\\= between class\\-string and 'UnitEnum' will always evaluate to true\\.$#" + count: 1 + path: ../src/Reflection/Php/PhpClassReflectionExtension.php + + - + message: "#^Access to property \\$name on an unknown class UnitEnum\\.$#" + count: 1 + path: ../src/Type/ConstantTypeHelper.php + + - + message: "#^PHPDoc tag @var for variable \\$value contains unknown class UnitEnum\\.$#" + count: 1 + path: ../src/Type/ConstantTypeHelper.php + + - + message: "#^Class BackedEnum not found\\.$#" + count: 1 + path: ../src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php + + - + message: "#^Call to method PHPStan\\\\Reflection\\\\ClassReflection::isEnum\\(\\) will always evaluate to false\\.$#" diff --git a/build/phpstan.neon b/build/phpstan.neon index aea35d06cc..b285f56e12 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -1,14 +1,15 @@ includes: - ../vendor/phpstan/phpstan-deprecation-rules/rules.neon - ../vendor/phpstan/phpstan-nette/rules.neon - - ../vendor/phpstan/phpstan-php-parser/extension.neon - ../vendor/phpstan/phpstan-phpunit/extension.neon - ../vendor/phpstan/phpstan-phpunit/rules.neon - ../vendor/phpstan/phpstan-strict-rules/rules.neon - ../conf/bleedingEdge.neon - ../phpstan-baseline.neon + - ../phpstan-baseline.php - ignore-by-php-version.neon.php - ignore-by-architecture.neon.php + parameters: level: 8 paths: @@ -17,19 +18,18 @@ parameters: - ../tests bootstrapFiles: - ../tests/phpstan-bootstrap.php + cache: + nodesByStringCountMax: 128 checkUninitializedProperties: true checkMissingCallableSignature: true excludePaths: - - ../src/Reflection/SignatureMap/functionMap.php - - ../src/Reflection/SignatureMap/functionMetadata.php - ../tests/*/data/* - ../tests/tmp/* + - ../tests/PHPStan/Analyser/nsrt/* - ../tests/PHPStan/Analyser/traits/* - ../tests/notAutoloaded/* - - ../tests/PHPStan/Generics/functions.php - ../tests/PHPStan/Reflection/UnionTypesTest.php - ../tests/PHPStan/Reflection/MixedTypeTest.php - - ../tests/PHPStan/Reflection/StaticTypeTest.php - ../tests/e2e/magic-setter/* - ../tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php - ../tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -43,6 +43,7 @@ parameters: - 'Symfony\Component\Finder\Exception\DirectoryNotFoundException' - 'InvalidArgumentException' - 'PHPStan\DependencyInjection\ParameterNotFoundException' + - 'PHPStan\DependencyInjection\DuplicateIncludedFilesException' - 'PHPStan\Analyser\UndefinedVariableException' - 'RuntimeException' - 'Nette\Neon\Exception' @@ -62,7 +63,7 @@ parameters: - 'PHPStan\Type\CircularTypeAliasDefinitionException' - 'PHPStan\Broker\ClassAutoloadingException' - 'LogicException' - - 'TypeError' + - 'Error' check: missingCheckedExceptionInThrows: true tooWideThrowType: true @@ -78,45 +79,35 @@ parameters: message: '#Fetching class constant class of deprecated class DeprecatedAnnotations\\DeprecatedWithMultipleTags.#' path: ../tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php - - message: '#^Variable property access on PHPStan\\Rules\\RuleError\.$#' + message: '#^Variable property access on T of PHPStan\\Rules\\RuleError\.$#' path: ../src/Rules/RuleErrorBuilder.php - message: "#^Parameter \\#1 (?:\\$argument|\\$objectOrClass) of class ReflectionClass constructor expects class\\-string\\\\|PHPStan\\\\ExtensionInstaller\\\\GeneratedConfig, string given\\.$#" count: 1 path: ../src/Command/CommandHelper.php + - + message: "#^Parameter \\#1 (?:\\$argument|\\$objectOrClass) of class ReflectionClass constructor expects class\\-string\\\\|PHPStan\\\\ExtensionInstaller\\\\GeneratedConfig, string given\\.$#" + count: 1 + path: ../src/Diagnose/PHPStanDiagnoseExtension.php + - '#^Short ternary operator is not allowed#' reportStaticMethodSignatures: true tmpDir: %rootDir%/tmp stubFiles: - stubs/ReactChildProcess.stub - stubs/ReactStreams.stub - stubs/NetteDIContainer.stub + - stubs/PhpParserName.stub + - stubs/Identifier.stub + +rules: + - PHPStan\Build\FinalClassRule + services: - class: PHPStan\Build\ServiceLocatorDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStan\Internal\ContainerDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Internal\UnionTypeGetInternalDynamicReturnTypeExtension + class: PHPStan\Build\ContainerDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - scopeIsInClass: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInClass - removeNullMethodName: getClassReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - scopeIsInTrait: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInTrait - removeNullMethodName: getTraitReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension diff --git a/build/readonly-property.neon b/build/readonly-property.neon new file mode 100644 index 0000000000..96657fb795 --- /dev/null +++ b/build/readonly-property.neon @@ -0,0 +1,3 @@ +parameters: + excludePaths: + - ../tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php diff --git a/build/rector-downgrade-vendor.php b/build/rector-downgrade-vendor.php deleted file mode 100644 index ac77234538..0000000000 --- a/build/rector-downgrade-vendor.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index afdb07beef..0000000000 --- a/build/rector-downgrade.php +++ /dev/null @@ -1,63 +0,0 @@ -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/spl-autoload-functions-php-8.neon b/build/spl-autoload-functions-php-8.neon new file mode 100644 index 0000000000..3669321889 --- /dev/null +++ b/build/spl-autoload-functions-php-8.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^PHPDoc tag @var with type list\\\\|false is not subtype of native type list\\\\.$#" + count: 2 + path: ../src/Command/CommandHelper.php diff --git a/build/spl-autoload-functions-pre-php-7.neon b/build/spl-autoload-functions-pre-php-7.neon new file mode 100644 index 0000000000..42cd820e71 --- /dev/null +++ b/build/spl-autoload-functions-pre-php-7.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + message: '#^Parameter \#1 \$array \(list\) of array_values is already a list, call has no effect\.$#' + path: ../src/Type/TypeCombinator.php diff --git a/build/stubs/Identifier.stub b/build/stubs/Identifier.stub new file mode 100644 index 0000000000..301d034b2d --- /dev/null +++ b/build/stubs/Identifier.stub @@ -0,0 +1,15 @@ + $attributes + */ + public function __construct(string $name, array $attributes = []) { } + +} diff --git a/build/stubs/PhpParserName.stub b/build/stubs/PhpParserName.stub new file mode 100644 index 0000000000..a044fbf684 --- /dev/null +++ b/build/stubs/PhpParserName.stub @@ -0,0 +1,25 @@ +|self $name Name as string, part array or Name instance (copy ctor) + * @param array $attributes Additional attributes + */ + public function __construct($name, array $attributes = []) { + } + + /** @return non-empty-string */ + public function toString() : string { + } + + /** @return non-empty-string */ + public function toCodeString() : string { + } +} diff --git a/build/transform-source b/build/transform-source deleted file mode 100755 index 71773edfbe..0000000000 --- a/build/transform-source +++ /dev/null @@ -1,11 +0,0 @@ -#!/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/changelog-generator/.gitignore b/changelog-generator/.gitignore new file mode 100644 index 0000000000..61ead86667 --- /dev/null +++ b/changelog-generator/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/changelog-generator/composer.json b/changelog-generator/composer.json new file mode 100644 index 0000000000..d4527f1c45 --- /dev/null +++ b/changelog-generator/composer.json @@ -0,0 +1,22 @@ +{ + "name": "phpstan/changelog-generator", + "require": { + "php": "^8.1", + "php-http/client-common": "^2.5", + "php-http/discovery": "^1.14", + "guzzlehttp/guzzle": "^7.4", + "http-interop/http-factory-guzzle": "^1.2", + "knplabs/github-api": "^3.7", + "symfony/console": "^6.1" + }, + "autoload": { + "psr-4": { + "PHPStan\\ChangelogGenerator\\": "src" + } + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } + } +} diff --git a/changelog-generator/composer.lock b/changelog-generator/composer.lock new file mode 100644 index 0000000000..3b830c7c5d --- /dev/null +++ b/changelog-generator/composer.lock @@ -0,0 +1,2159 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "3e1d902170abb95f02293ffeb1aa1b2b", + "packages": [ + { + "name": "clue/stream-filter", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "d6169430c7731d8509da7aecd0af756a5747b78e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/d6169430c7731d8509da7aecd0af756a5747b78e", + "reference": "d6169430c7731d8509da7aecd0af756a5747b78e", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/php-stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-02-21T13:15:14+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-08-27T10:20:53+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-08-03T15:11:55+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu/" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-08-27T10:13:57+00:00" + }, + { + "name": "http-interop/http-factory-guzzle", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/http-interop/http-factory-guzzle.git", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7||^2.0", + "php": ">=7.3", + "psr/http-factory": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^9.5" + }, + "suggest": { + "guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Factory\\Guzzle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "An HTTP Factory using Guzzle PSR7", + "keywords": [ + "factory", + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/http-interop/http-factory-guzzle/issues", + "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0" + }, + "time": "2021-07-21T13:50:14+00:00" + }, + { + "name": "knplabs/github-api", + "version": "v3.13.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/php-github-api.git", + "reference": "47024f3483520c0fafdfc5c10d2a20d87b4c7ceb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/47024f3483520c0fafdfc5c10d2a20d87b4c7ceb", + "reference": "47024f3483520c0fafdfc5c10d2a20d87b4c7ceb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2.5 || ^8.0", + "php-http/cache-plugin": "^1.7.1", + "php-http/client-common": "^2.3", + "php-http/discovery": "^1.12", + "php-http/httplug": "^2.2", + "php-http/multipart-stream-builder": "^1.1.2", + "psr/cache": "^1.0|^2.0|^3.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0|^2.0", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.2", + "guzzlehttp/psr7": "^1.7", + "http-interop/http-factory-guzzle": "^1.0", + "php-http/mock-client": "^1.4.1", + "phpstan/extension-installer": "^1.0.5", + "phpstan/phpstan": "^0.12.57", + "phpstan/phpstan-deprecation-rules": "^0.12.5", + "phpunit/phpunit": "^8.5 || ^9.4", + "symfony/cache": "^5.1.8", + "symfony/phpunit-bridge": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.20.x-dev", + "dev-master": "3.12-dev" + } + }, + "autoload": { + "psr-4": { + "Github\\": "lib/Github/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "http://knplabs.com/" + }, + { + "name": "Thibault Duplessis", + "email": "thibault.duplessis@gmail.com", + "homepage": "http://ornicar.github.com/" + } + ], + "description": "GitHub API v3 client", + "homepage": "https://github.com/KnpLabs/php-github-api", + "keywords": [ + "api", + "gh", + "gist", + "github" + ], + "support": { + "issues": "https://github.com/KnpLabs/php-github-api/issues", + "source": "https://github.com/KnpLabs/php-github-api/tree/v3.13.0" + }, + "funding": [ + { + "url": "https://github.com/acrobat", + "type": "github" + } + ], + "time": "2023-11-19T21:08:19+00:00" + }, + { + "name": "php-http/cache-plugin", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/cache-plugin.git", + "reference": "6bf9fbf66193f61d90c2381b75eb1fa0202fd314" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/cache-plugin/zipball/6bf9fbf66193f61d90c2381b75eb1fa0202fd314", + "reference": "6bf9fbf66193f61d90c2381b75eb1fa0202fd314", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/client-common": "^1.9 || ^2.0", + "php-http/message-factory": "^1.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "PSR-6 Cache plugin for HTTPlug", + "homepage": "http://httplug.io/", + "keywords": [ + "cache", + "http", + "httplug", + "plugin" + ], + "support": { + "issues": "https://github.com/php-http/cache-plugin/issues", + "source": "https://github.com/php-http/cache-plugin/tree/1.8.0" + }, + "time": "2023-04-28T10:56:55+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "880509727a447474d2a71b7d7fa5d268ddd3db4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/880509727a447474d2a71b7d7fa5d268ddd3db4b", + "reference": "880509727a447474d2a71b7d7fa5d268ddd3db4b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io/", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.0" + }, + "time": "2023-05-17T06:46:59+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.19.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/57f3de01d32085fea20865f9b16fb0e69347c39e", + "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "symfony/phpunit-bridge": "^6.2" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org/", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.19.1" + }, + "time": "2023-07-11T07:02:26+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu/" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io/", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.0" + }, + "time": "2023-04-14T15:10:03+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/47a14338bf4ebd67d317bf1144253d7db4ab55fd", + "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org/", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.0" + }, + "time": "2023-05-17T06:43:38+00:00" + }, + { + "name": "php-http/message-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org/", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/1.1.0" + }, + "abandoned": "psr/http-factory", + "time": "2023-04-14T14:16:17+00:00" + }, + { + "name": "php-http/multipart-stream-builder", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/multipart-stream-builder.git", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/f5938fd135d9fa442cc297dc98481805acfe2b6a", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Message\\MultipartStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "A builder class that help you create a multipart stream", + "homepage": "http://php-http.org/", + "keywords": [ + "factory", + "http", + "message", + "multipart stream", + "stream" + ], + "support": { + "issues": "https://github.com/php-http/multipart-stream-builder/issues", + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.0" + }, + "time": "2023-04-28T14:10:22+00:00" + }, + { + "name": "php-http/promise", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "44a67cb59f708f826f3bec35f22030b3edb90119" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/44a67cb59f708f826f3bec35f22030b3edb90119", + "reference": "44a67cb59f708f826f3bec35f22030b3edb90119", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io/", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.2.1" + }, + "time": "2023-11-08T12:57:08+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/console", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "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" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com/", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T08:09:35+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com/", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a10f19f5198d589d5c33333cffe98dc9820332dd", + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com/", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-12T14:21:09+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-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": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com/", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/string", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "13880a87790c76ef994c91e87efb96134522577a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/13880a87790c76ef994c91e87efb96134522577a", + "reference": "13880a87790c76ef994c91e87efb96134522577a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/intl": "^6.2", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com/", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-09T08:28:21+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/changelog-generator/phpstan.neon b/changelog-generator/phpstan.neon new file mode 100644 index 0000000000..a1fee9d3a7 --- /dev/null +++ b/changelog-generator/phpstan.neon @@ -0,0 +1,16 @@ +includes: + - ../vendor/phpstan/phpstan-deprecation-rules/rules.neon + - ../vendor/phpstan/phpstan-nette/rules.neon + - ../vendor/phpstan/phpstan-phpunit/extension.neon + - ../vendor/phpstan/phpstan-phpunit/rules.neon + - ../vendor/phpstan/phpstan-strict-rules/rules.neon + - ../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - src + - run.php + ignoreErrors: + - + identifier: missingType.generics diff --git a/changelog-generator/run.php b/changelog-generator/run.php new file mode 100755 index 0000000000..fc709b521a --- /dev/null +++ b/changelog-generator/run.php @@ -0,0 +1,157 @@ +#!/usr/bin/env php +setName('run'); + $this->addArgument('fromCommit', InputArgument::REQUIRED); + $this->addArgument('toCommit', InputArgument::REQUIRED); + $this->addOption('exclude-branch', null, InputOption::VALUE_REQUIRED); + $this->addOption('include-headings', null, InputOption::VALUE_NONE); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $token = $_SERVER['GITHUB_TOKEN']; + + $rateLimitPlugin = new RateLimitPlugin(); + $httpBuilder = new Builder(); + $httpBuilder->addPlugin($rateLimitPlugin); + + $gitHubClient = new Client($httpBuilder); + $gitHubClient->authenticate($token, AuthMethod::ACCESS_TOKEN); + $rateLimitPlugin->setClient($gitHubClient); + + /** @var Search $searchApi */ + $searchApi = $gitHubClient->api('search'); + + $command = ['git', 'log', sprintf('%s..%s', $input->getArgument('fromCommit'), $input->getArgument('toCommit'))]; + $excludeBranch = $input->getOption('exclude-branch'); + if ($excludeBranch !== null) { + $command[] = '--not'; + $command[] = $excludeBranch; + $command[] = '--no-merges'; + } + $command[] = '--reverse'; + $command[] = '--pretty=%H %s'; + + $commitLines = $this->exec($command); + $commits = array_map(static function (string $line): array { + [$hash, $message] = explode(' ', $line, 2); + + return [ + 'hash' => $hash, + 'message' => $message, + ]; + }, explode("\n", $commitLines)); + + if ($input->getOption('include-headings') === true) { + $output->writeln(<<<'MARKDOWN' + Major new features 🚀 + ===================== + + Bleeding edge 🔪 + ===================== + + * + + *If you want to see the shape of things to come and adopt bleeding edge features early, you can include this config file in your project's `phpstan.neon`:* + + ``` + includes: + - vendor/phpstan/phpstan/conf/bleedingEdge.neon + ``` + + *Of course, there are no backwards compatibility guarantees when you include this file. The behaviour and reported errors can change in minor versions with this file included. [Learn more](https://phpstan.org/blog/what-is-bleeding-edge)* + + Improvements 🔧 + ===================== + + Bugfixes 🐛 + ===================== + + Function signature fixes 🤖 + ======================= + + Internals 🔍 + ===================== + + + MARKDOWN); + } + + foreach ($commits as $commit) { + $pullRequests = $searchApi->issues(sprintf('repo:phpstan/phpstan-src %s', $commit['hash'])); + $issues = $searchApi->issues(sprintf('repo:phpstan/phpstan %s', $commit['hash']), 'created'); + $items = array_merge($pullRequests['items'], $issues['items']); + $parenthesis = 'https://github.com/phpstan/phpstan-src/commit/' . $commit['hash']; + $thanks = null; + $issuesToReference = []; + foreach ($items as $responseItem) { + if (isset($responseItem['pull_request'])) { + $parenthesis = sprintf('[#%d](%s)', $responseItem['number'], 'https://github.com/phpstan/phpstan-src/pull/' . $responseItem['number']); + $thanks = $responseItem['user']['login']; + } else { + $issuesToReference[] = sprintf('#%d', $responseItem['number']); + } + } + + $output->writeln(sprintf('* %s (%s)%s%s', $commit['message'], $parenthesis, count($issuesToReference) > 0 ? ', ' . implode(', ', $issuesToReference) : '', $thanks !== null ? sprintf(', thanks @%s!', $thanks) : '')); + } + + return 0; + } + + /** + * @param string[] $commandParts + */ + private function exec(array $commandParts): string + { + $command = implode(' ', array_map(static fn (string $part): string => escapeshellarg($part), $commandParts)); + + exec($command, $outputLines, $statusCode); + $output = implode("\n", $outputLines); + if ($statusCode !== 0) { + throw new InvalidArgumentException(sprintf('Command %s failed: %s', $command, $output)); + } + + return $output; + } + + }; + + $application = new Application(); + $application->add($command); + $application->setDefaultCommand('run', true); + $application->setCatchExceptions(false); + $application->run(); +})(); diff --git a/changelog-generator/src/RateLimitPlugin.php b/changelog-generator/src/RateLimitPlugin.php new file mode 100644 index 0000000000..fd724ab31f --- /dev/null +++ b/changelog-generator/src/RateLimitPlugin.php @@ -0,0 +1,47 @@ +client = $client; + } + + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $path = $request->getUri()->getPath(); + if ($path === '/rate_limit') { + return $next($request); + } + + /** @var RateLimit $api */ + $api = $this->client->api('rate_limit'); + + /** @var RateLimitResource $resource */ + $resource = $api->getResource('search'); + if ($resource->getRemaining() < 10) { + $reset = $resource->getReset(); + $sleepFor = $reset - time(); + if ($sleepFor > 0) { + sleep($sleepFor); + } + } + + return $next($request); + } + +} diff --git a/compiler/bin/compile b/compiler/bin/compile deleted file mode 100755 index e5f1ef6bb4..0000000000 --- a/compiler/bin/compile +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env php -add($compileCommand); -$application->setDefaultCommand($compileCommand->getName(), true); -$application->run(); diff --git a/compiler/bin/prepare b/compiler/bin/prepare new file mode 100755 index 0000000000..b72543c452 --- /dev/null +++ b/compiler/bin/prepare @@ -0,0 +1,18 @@ +#!/usr/bin/env php +add($prepareCommand); +$application->setDefaultCommand($prepareCommand->getName(), true); +$application->run(); diff --git a/compiler/build/box.phar b/compiler/build/box.phar old mode 100644 new mode 100755 index d0476cfc4c..402ceabdc4 Binary files a/compiler/build/box.phar and b/compiler/build/box.phar differ diff --git a/compiler/build/resign.php b/compiler/build/resign.php new file mode 100644 index 0000000000..01b7228962 --- /dev/null +++ b/compiler/build/resign.php @@ -0,0 +1,15 @@ +updateTimestamps(new \DateTimeImmutable('2017-10-11 08:58:00')); +$util->save($file, \Phar::SHA512); diff --git a/compiler/build/scoper.inc.php b/compiler/build/scoper.inc.php index c2b758dab3..0d0008d341 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -17,11 +17,9 @@ '../../vendor/jetbrains/phpstorm-stubs', '../../vendor/phpstan/php-8-stubs/stubs', '../../vendor/symfony/polyfill-php80', + '../../vendor/symfony/polyfill-php81', '../../vendor/symfony/polyfill-mbstring', '../../vendor/symfony/polyfill-intl-normalizer', - '../../vendor/symfony/polyfill-php73', - '../../vendor/symfony/polyfill-php74', - '../../vendor/symfony/polyfill-php72', '../../vendor/symfony/polyfill-intl-grapheme', ]) as $file) { if ($file->getPathName() === '../../vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php') { @@ -30,15 +28,21 @@ $stubs[] = $file->getPathName(); } -exec('git rev-parse --short HEAD', $gitCommitOutputLines, $gitExitCode); -if ($gitExitCode !== 0) { - die('Could not get Git commit'); +if ($_SERVER['PHAR_CHECKSUM'] ?? false) { + $prefix = '_PHPStan_checksum'; +} else { + exec('git rev-parse --short HEAD', $gitCommitOutputLines, $gitExitCode); + if ($gitExitCode !== 0) { + die('Could not get Git commit'); + } + + $prefix = sprintf('_PHPStan_%s', $gitCommitOutputLines[0]); } return [ - 'prefix' => sprintf('_PHPStan_%s', $gitCommitOutputLines[0]), + 'prefix' => $prefix, 'finders' => [], - 'files-whitelist' => $stubs, + 'exclude-files' => $stubs, 'patchers' => [ function (string $filePath, string $prefix, string $content): string { if ($filePath !== 'bin/phpstan') { @@ -46,6 +50,12 @@ function (string $filePath, string $prefix, string $content): string { } return str_replace('__DIR__ . \'/..', '\'phar://phpstan.phar', $content); }, + function (string $filePath, string $prefix, string $content): string { + if ($filePath !== 'bin/phpstan') { + return $content; + } + return str_replace(sprintf('%s\\\\__PHPSTAN_RUNNING__', $prefix), '__PHPSTAN_RUNNING__', $content); + }, function (string $filePath, string $prefix, string $content): string { if ($filePath !== 'vendor/nette/di/src/DI/Compiler.php') { return $content; @@ -53,7 +63,7 @@ function (string $filePath, string $prefix, string $content): string { return str_replace('|Nette\\\\DI\\\\Statement', sprintf('|\\\\%s\\\\Nette\\\\DI\\\\Statement', $prefix), $content); }, function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'vendor/nette/di/src/DI/Config/DefinitionSchema.php') { + if ($filePath !== 'vendor/nette/di/src/DI/Extensions/DefinitionSchema.php') { return $content; } $content = str_replace( @@ -86,22 +96,6 @@ function (string $filePath, string $prefix, string $content): string { return $content; }, - function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'src/Testing/PHPStanTestCase.php') { - return $content; - } - return str_replace(sprintf('\\%s\\PHPUnit\\Framework\\TestCase', $prefix), '\\PHPUnit\\Framework\\TestCase', $content); - }, - function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'src/Testing/LevelsTestCase.php') { - return $content; - } - return str_replace( - [sprintf('\\%s\\PHPUnit\\Framework\\AssertionFailedError', $prefix), sprintf('\\%s\\PHPUnit\\Framework\\TestCase', $prefix)], - ['\\PHPUnit\\Framework\\AssertionFailedError', '\\PHPUnit\\Framework\\TestCase'], - $content - ); - }, function (string $filePath, string $prefix, string $content): string { if (strpos($filePath, 'src/') !== 0) { return $content; @@ -166,6 +160,7 @@ function (string $filePath, string $prefix, string $content): string { }, function (string $filePath, string $prefix, string $content): string { if (!in_array($filePath, [ + 'bin/phpstan', 'src/Testing/TestCaseSourceLocatorFactory.php', 'src/Testing/PHPStanTestCase.php', 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/Type/ComposerSourceLocator.php', @@ -175,6 +170,13 @@ function (string $filePath, string $prefix, string $content): string { return str_replace(sprintf('%s\\Composer\\Autoload\\ClassLoader', $prefix), 'Composer\\Autoload\\ClassLoader', $content); }, + function (string $filePath, string $prefix, string $content): string { + if ($filePath !== 'src/Internal/ComposerHelper.php') { + return $content; + } + + return str_replace(sprintf('%s\\Composer\\InstalledVersions', $prefix), 'Composer\\InstalledVersions', $content); + }, function (string $filePath, string $prefix, string $content): string { if ($filePath !== 'vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php') { return $content; @@ -194,82 +196,71 @@ function (string $filePath, string $prefix, string $content): string { return $content; }, function (string $filePath, string $prefix, string $content): string { - if (!in_array($filePath, [ - 'src/Type/TypehintHelper.php', - 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionUnionType.php', - ], true)) { + if ($filePath !== 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php') { return $content; } - return str_replace(sprintf('%s\\ReflectionUnionType', $prefix), 'ReflectionUnionType', $content); + return str_replace('Core/Core_d.php', 'Core/Core_d.stub', $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)) { + if ($filePath !== 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php') { return $content; } - return str_replace(sprintf('%s\\ReturnTypeWillChange', $prefix), 'ReturnTypeWillChange', $content); + return str_replace(sprintf('\'%s\\\\JetBrains\\\\', $prefix), '\'JetBrains\\\\', $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)) { + if (!str_starts_with($filePath, 'vendor/nikic/php-parser/lib')) { return $content; } - return str_replace(sprintf('%s\\ReflectionIntersectionType', $prefix), 'ReflectionIntersectionType', $content); + return str_replace(sprintf('use %s\\PhpParser;', $prefix), 'use PhpParser;', $content); }, function (string $filePath, string $prefix, string $content): string { - if (strpos($filePath, 'src/') !== 0) { + if (!str_starts_with($filePath, 'vendor/nikic/php-parser/lib')) { return $content; } - return str_replace(sprintf('%s\\Attribute', $prefix), 'Attribute', $content); + return str_replace([ + sprintf('\\%s', $prefix), + sprintf('\\\\%s', $prefix), + ], '', $content); }, function (string $filePath, string $prefix, string $content): string { - if (strpos($filePath, 'src/') !== 0) { + if (!str_starts_with($filePath, 'vendor/ondrejmirtes/better-reflection')) { return $content; } - return str_replace(sprintf('%s\\ReturnTypeWillChange', $prefix), 'ReturnTypeWillChange', $content); + return str_replace(sprintf('%s\\PropertyHookType', $prefix), 'PropertyHookType', $content); }, function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php') { + if ( + $filePath !== 'vendor/nette/utils/src/Utils/Strings.php' + && $filePath !== 'vendor/nette/utils/src/Utils/Arrays.php' + ) { return $content; } - return str_replace('Core/Core_d.php', 'Core/Core_d.stub', $content); + return str_replace('#[\\JetBrains\\PhpStorm\\Language(\'RegExp\')] ', '', $content); }, function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php') { + if ($filePath !== 'vendor/fidry/cpu-core-counter/src/Finder/WindowsRegistryLogicalFinder.php') { return $content; } - - return str_replace(sprintf('\'%s\\\\JetBrains\\\\', $prefix), '\'JetBrains\\\\', $content); - } + return str_replace(sprintf('%s\\\\reg query', $prefix), 'reg query', $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\*', + 'exclude-namespaces' => [ + 'PHPStan', + 'PHPUnit', + 'PhpParser', + 'Hoa', + 'Symfony\Polyfill\Php80', + 'Symfony\Polyfill\Php81', + 'Symfony\Polyfill\Mbstring', + 'Symfony\Polyfill\Intl\Normalizer', + 'Symfony\Polyfill\Intl\Grapheme', ], - 'whitelist-global-functions' => false, - 'whitelist-global-classes' => false, + 'expose-global-functions' => false, + 'expose-global-classes' => false, ]; diff --git a/compiler/composer.json b/compiler/composer.json index 89b5e05bf1..4da1b08076 100644 --- a/compiler/composer.json +++ b/compiler/composer.json @@ -6,10 +6,11 @@ "require": { "php": "^8.0", "nette/neon": "^3.0.0", + "seld/phar-utils": "^1.2", "symfony/console": "^6.0.0", - "symfony/process": "^6.0.0", "symfony/filesystem": "^6.0.0", - "symfony/finder": "^6.0.0" + "symfony/finder": "^6.0.0", + "symfony/process": "^6.0.0" }, "autoload": { "psr-4": { diff --git a/compiler/composer.lock b/compiler/composer.lock index 399d42395f..d970a7b6f7 100644 --- a/compiler/composer.lock +++ b/compiler/composer.lock @@ -4,29 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fed40217d551b1c8bdc627086b633e9b", + "content-hash": "7caab5611acadb20806eaeca4e294e98", "packages": [ { "name": "nette/neon", - "version": "v3.3.2", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "54b287d8c2cdbe577b02e28ca1713e275b05ece2" + "reference": "372d945c156ee7f35c953339fb164538339e6283" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/54b287d8c2cdbe577b02e28ca1713e275b05ece2", - "reference": "54b287d8c2cdbe577b02e28ca1713e275b05ece2", + "url": "https://api.github.com/repos/nette/neon/zipball/372d945c156ee7f35c953339fb164538339e6283", + "reference": "372d945c156ee7f35c953339fb164538339e6283", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.1" + "php": ">=8.0 <8.3" }, "require-dev": { - "nette/tester": "^2.0", - "phpstan/phpstan": "^0.12", + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.7" }, "bin": [ @@ -35,7 +35,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -70,9 +70,9 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/v3.3.2" + "source": "https://github.com/nette/neon/tree/v3.4.0" }, - "time": "2021-11-25T15:57:41+00:00" + "time": "2023-01-13T03:08:29+00:00" }, { "name": "psr/container", @@ -127,18 +127,66 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "seld/phar-utils", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + }, + "time": "2022-08-31T10:31:18+00:00" + }, { "name": "symfony/console", - "version": "v6.0.3", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "22e8efd019c3270c4f79376234a3f8752cd25490" + "reference": "c3ebc83d031b71c39da318ca8b7a07ecc67507ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/22e8efd019c3270c4f79376234a3f8752cd25490", - "reference": "22e8efd019c3270c4f79376234a3f8752cd25490", + "url": "https://api.github.com/repos/symfony/console/zipball/c3ebc83d031b71c39da318ca8b7a07ecc67507ed", + "reference": "c3ebc83d031b71c39da318ca8b7a07ecc67507ed", "shasum": "" }, "require": { @@ -204,7 +252,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.0.3" + "source": "https://github.com/symfony/console/tree/v6.0.19" }, "funding": [ { @@ -220,20 +268,20 @@ "type": "tidelift" } ], - "time": "2022-01-26T17:23:29+00:00" + "time": "2023-01-01T08:36:10+00:00" }, { "name": "symfony/filesystem", - "version": "v6.0.3", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "6ae49c4fda17322171a2b8dc5f70bc6edbc498e1" + "reference": "3d49eec03fda1f0fc19b7349fbbe55ebc1004214" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/6ae49c4fda17322171a2b8dc5f70bc6edbc498e1", - "reference": "6ae49c4fda17322171a2b8dc5f70bc6edbc498e1", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3d49eec03fda1f0fc19b7349fbbe55ebc1004214", + "reference": "3d49eec03fda1f0fc19b7349fbbe55ebc1004214", "shasum": "" }, "require": { @@ -267,7 +315,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com/", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.0.3" + "source": "https://github.com/symfony/filesystem/tree/v6.0.19" }, "funding": [ { @@ -283,20 +331,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2023-01-20T17:44:14+00:00" }, { "name": "symfony/finder", - "version": "v6.0.3", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8661b74dbabc23223f38c9b99d3f8ade71170430" + "reference": "5cc9cac6586fc0c28cd173780ca696e419fefa11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8661b74dbabc23223f38c9b99d3f8ade71170430", - "reference": "8661b74dbabc23223f38c9b99d3f8ade71170430", + "url": "https://api.github.com/repos/symfony/finder/zipball/5cc9cac6586fc0c28cd173780ca696e419fefa11", + "reference": "5cc9cac6586fc0c28cd173780ca696e419fefa11", "shasum": "" }, "require": { @@ -328,7 +376,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com/", "support": { - "source": "https://github.com/symfony/finder/tree/v6.0.3" + "source": "https://github.com/symfony/finder/tree/v6.0.19" }, "funding": [ { @@ -344,20 +392,20 @@ "type": "tidelift" } ], - "time": "2022-01-26T17:23:29+00:00" + "time": "2023-01-20T17:44:14+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.24.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", "shasum": "" }, "require": { @@ -372,7 +420,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -380,12 +428,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -410,7 +458,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" }, "funding": [ { @@ -426,20 +474,20 @@ "type": "tidelift" } ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.24.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + "reference": "511a08c03c1960e08a883f4cffcacd219b758354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354", "shasum": "" }, "require": { @@ -451,7 +499,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -459,12 +507,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -491,7 +539,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" }, "funding": [ { @@ -507,20 +555,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T21:10:46+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.24.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", "shasum": "" }, "require": { @@ -532,7 +580,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -540,12 +588,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -575,7 +623,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" }, "funding": [ { @@ -591,20 +639,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.24.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "shasum": "" }, "require": { @@ -619,7 +667,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -627,12 +675,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -658,7 +706,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" }, "funding": [ { @@ -674,20 +722,20 @@ "type": "tidelift" } ], - "time": "2021-11-30T18:21:41+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/process", - "version": "v6.0.3", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "298ed357274c1868c20a0061df256a1250a6c4af" + "reference": "2114fd60f26a296cc403a7939ab91478475a33d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/298ed357274c1868c20a0061df256a1250a6c4af", - "reference": "298ed357274c1868c20a0061df256a1250a6c4af", + "url": "https://api.github.com/repos/symfony/process/zipball/2114fd60f26a296cc403a7939ab91478475a33d4", + "reference": "2114fd60f26a296cc403a7939ab91478475a33d4", "shasum": "" }, "require": { @@ -719,7 +767,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com/", "support": { - "source": "https://github.com/symfony/process/tree/v6.0.3" + "source": "https://github.com/symfony/process/tree/v6.0.19" }, "funding": [ { @@ -735,20 +783,20 @@ "type": "tidelift" } ], - "time": "2022-01-26T17:23:29+00:00" + "time": "2023-01-01T08:36:10+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.0.0", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603" + "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/36715ebf9fb9db73db0cb24263c79077c6fe8603", - "reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66", "shasum": "" }, "require": { @@ -801,7 +849,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.0.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.0.2" }, "funding": [ { @@ -817,20 +865,20 @@ "type": "tidelift" } ], - "time": "2021-11-04T17:53:12+00:00" + "time": "2022-05-30T19:17:58+00:00" }, { "name": "symfony/string", - "version": "v6.0.3", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2" + "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/522144f0c4c004c80d56fa47e40e17028e2eefc2", - "reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2", + "url": "https://api.github.com/repos/symfony/string/zipball/d9e72497367c23e08bf94176d2be45b00a9d232a", + "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a", "shasum": "" }, "require": { @@ -851,12 +899,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -886,7 +934,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.0.3" + "source": "https://github.com/symfony/string/tree/v6.0.19" }, "funding": [ { @@ -902,35 +950,36 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2023-01-01T08:36:10+00:00" } ], "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^8.0", + "doctrine/coding-standard": "^9 || ^11", "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.30 || ^5.4" }, "type": "library", "autoload": { @@ -957,7 +1006,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.5.0" }, "funding": [ { @@ -973,38 +1022,42 @@ "type": "tidelift" } ], - "time": "2020-11-10T18:47:58+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.10.2", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, "require-dev": { - "doctrine/collections": "^1.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": [ @@ -1020,7 +1073,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.1" }, "funding": [ { @@ -1028,20 +1081,20 @@ "type": "tidelift" } ], - "time": "2020-11-13T09:40:50+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nikic/php-parser", - "version": "v4.13.2", + "version": "v4.17.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", "shasum": "" }, "require": { @@ -1082,9 +1135,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" }, - "time": "2021-11-30T19:35:32+00:00" + "time": "2023-08-13T19:53:39+00:00" }, { "name": "phar-io/manifest", @@ -1148,16 +1201,16 @@ }, { "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": { @@ -1193,253 +1246,26 @@ "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" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org/", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "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/5.3.0" - }, - "time": "2021-10-19T17:43:47+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.6.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "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.6.0" - }, - "time": "2022-01-04T19:58:01+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "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 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com/" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" - }, - "time": "2021-12-08T12:19:24+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpstan/phpstan", - "version": "1.2.0", + "version": "1.10.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "cbe085f9fdead5b6d62e4c022ca52dc9427a10ee" + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cbe085f9fdead5b6d62e4c022ca52dc9427a10ee", - "reference": "cbe085f9fdead5b6d62e4c022ca52dc9427a10ee", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/762c4dac4da6f8756eebb80e528c3a47855da9bd", + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd", "shasum": "" }, "require": { - "php": "^7.1|^8.0" + "php": "^7.2|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -1449,11 +1275,6 @@ "phpstan.phar" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2-dev" - } - }, "autoload": { "files": [ "bootstrap.php" @@ -1464,9 +1285,16 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.2.0" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -1477,34 +1305,30 @@ "url": "https://github.com/phpstan", "type": "github" }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, { "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", "type": "tidelift" } ], - "time": "2021-11-18T14:09:01+00:00" + "time": "2023-05-09T15:28:01+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.0.0", + "version": "1.3.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3" + "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", - "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d8bdab0218c5eb0964338d24a8511b65e9c94fa5", + "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -1517,9 +1341,6 @@ }, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon", @@ -1539,29 +1360,29 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.0.0" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.13" }, - "time": "2021-10-14T08:03:54+00:00" + "time": "2023-05-26T11:05:59+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.10", + "version": "9.2.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687" + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d5850aaf931743067f4bfc1ae4cbd06468400687", - "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1", + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.15", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -1576,8 +1397,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { @@ -1610,7 +1431,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.10" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27" }, "funding": [ { @@ -1618,7 +1440,7 @@ "type": "github" } ], - "time": "2021-12-05T09:12:13+00:00" + "time": "2023-07-26T13:44:30+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1863,20 +1685,20 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.13", + "version": "9.6.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "597cb647654ede35e43b137926dfdfef0fb11743" + "reference": "810500e92855eba8a7a5319ae913be2da6f957b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/597cb647654ede35e43b137926dfdfef0fb11743", - "reference": "597cb647654ede35e43b137926dfdfef0fb11743", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/810500e92855eba8a7a5319ae913be2da6f957b0", + "reference": "810500e92855eba8a7a5319ae913be2da6f957b0", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -1887,31 +1709,26 @@ "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.7", + "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", "phpunit/php-timer": "^5.0.2", "sebastian/cli-parser": "^1.0.1", "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", + "sebastian/comparator": "^4.0.8", "sebastian/diff": "^4.0.3", "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", + "sebastian/exporter": "^4.0.5", "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3.4", + "sebastian/type": "^3.2", "sebastian/version": "^3.0.2" }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -1919,15 +1736,15 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { - "classmap": [ - "src/" - ], "files": [ "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1950,7 +1767,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.13" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.11" }, "funding": [ { @@ -1960,9 +1778,13 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-01-24T07:33:35+00:00" + "time": "2023-08-19T07:10:56+00:00" }, { "name": "sebastian/cli-parser", @@ -2133,16 +1955,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -2195,7 +2017,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -2203,7 +2025,7 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", @@ -2264,16 +2086,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -2318,7 +2140,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -2326,20 +2148,20 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -2381,7 +2203,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -2389,20 +2211,20 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -2458,7 +2280,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -2466,20 +2288,20 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.3", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49" + "reference": "bde739e7565280bda77be70044ac1047bc007e34" }, "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/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", "shasum": "" }, "require": { @@ -2522,7 +2344,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.6" }, "funding": [ { @@ -2530,7 +2352,7 @@ "type": "github" } ], - "time": "2021-06-11T13:31:12+00:00" + "time": "2023-08-02T09:26:13+00:00" }, { "name": "sebastian/lines-of-code", @@ -2703,16 +2525,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -2751,10 +2573,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -2762,7 +2584,7 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", @@ -2821,28 +2643,28 @@ }, { "name": "sebastian/type", - "version": "2.3.4", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2865,7 +2687,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -2873,7 +2695,7 @@ "type": "github" } ], - "time": "2021-06-15T12:49:02+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", @@ -2977,64 +2799,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" } ], "aliases": [], @@ -3049,5 +2813,5 @@ "platform-overrides": { "php": "8.0.99" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/compiler/src/Console/CompileCommand.php b/compiler/src/Console/CompileCommand.php deleted file mode 100644 index 16834d69e2..0000000000 --- a/compiler/src/Console/CompileCommand.php +++ /dev/null @@ -1,224 +0,0 @@ -setName('phpstan:compile') - ->setDescription('Compile PHAR'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $this->processFactory->setOutput($output); - - $this->buildPreloadScript(); - $this->deleteUnnecessaryVendorCode(); - $this->fixComposerJson($this->buildDir); - $this->renamePhpStormStubs(); - $this->renamePhp8Stubs(); - $this->transformSource(); - - $this->processFactory->create(['php', 'box.phar', 'compile', '--no-parallel'], $this->dataDir); - - return 0; - } - - private function fixComposerJson(string $buildDir): void - { - $json = json_decode($this->filesystem->read($buildDir . '/composer.json'), true); - - unset($json['replace']); - $json['name'] = 'phpstan/phpstan'; - $json['require']['php'] = '^7.1'; - - // simplify autoload (remove not packed build directory] - $json['autoload']['psr-4']['PHPStan\\'] = 'src/'; - - $encodedJson = json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); - if ($encodedJson === false) { - throw new Exception('json_encode() was not successful.'); - } - - $this->filesystem->write($buildDir . '/composer.json', $encodedJson); - } - - private function renamePhpStormStubs(): void - { - $directory = $this->buildDir . '/vendor/jetbrains/phpstorm-stubs'; - if (!is_dir($directory)) { - return; - } - - $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) { - continue; - } - - $renameSuccess = rename( - $path, - dirname($path) . '/' . $stubFile->getBasename('.php') . '.stub', - ); - if ($renameSuccess === false) { - throw new ShouldNotHappenException(sprintf('Could not rename %s', $path)); - } - } - - $stubsMapContents = file_get_contents($stubsMapPath); - if ($stubsMapContents === false) { - 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 ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); - } - } - - private function renamePhp8Stubs(): void - { - $directory = $this->buildDir . '/vendor/phpstan/php-8-stubs/stubs'; - if (!is_dir($directory)) { - return; - } - - $stubFinder = Finder::create(); - $stubsMapPath = $directory . '/../Php8StubsMap.php'; - foreach ($stubFinder->files()->name('*.php')->in($directory) as $stubFile) { - $path = $stubFile->getPathname(); - if ($path === $stubsMapPath) { - continue; - } - - $renameSuccess = rename( - $path, - dirname($path) . '/' . $stubFile->getBasename('.php') . '.stub', - ); - if ($renameSuccess === false) { - throw new ShouldNotHappenException(sprintf('Could not rename %s', $path)); - } - } - - $stubsMapContents = file_get_contents($stubsMapPath); - if ($stubsMapContents === false) { - 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 ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); - } - } - - private function buildPreloadScript(): void - { - $vendorDir = $this->buildDir . '/vendor'; - if (!is_dir($vendorDir . '/nikic/php-parser/lib/PhpParser')) { - return; - } - - $preloadScript = $this->buildDir . '/preload.php'; - $template = <<<'php' -files()->name('*.php')->in([ - $this->buildDir . '/src', - $vendorDir . '/nikic/php-parser/lib/PhpParser', - $vendorDir . '/phpstan/phpdoc-parser/src', - ])->exclude([ - 'Testing', - ]) as $phpFile) { - $realPath = $phpFile->getRealPath(); - if ($realPath === false) { - return; - } - $path = substr($realPath, strlen($root)); - $output .= 'require_once __DIR__ . ' . var_export($path, true) . ';' . "\n"; - } - - file_put_contents($preloadScript, sprintf($template, $output)); - } - - private function deleteUnnecessaryVendorCode(): void - { - $vendorDir = $this->buildDir . '/vendor'; - if (!is_dir($vendorDir . '/nikic/php-parser')) { - return; - } - - @unlink($vendorDir . '/nikic/php-parser/grammar/rebuildParsers.php'); - @unlink($vendorDir . '/nikic/php-parser/bin/php-parse'); - } - - private function transformSource(): void - { - chdir(__DIR__ . '/../../..'); - exec(escapeshellarg(__DIR__ . '/../../../build/transform-source') . ' 7.1', $outputLines, $exitCode); - if ($exitCode === 0) { - return; - } - - throw new ShouldNotHappenException(implode("\n", $outputLines)); - } - -} diff --git a/compiler/src/Console/PrepareCommand.php b/compiler/src/Console/PrepareCommand.php new file mode 100644 index 0000000000..9dbc3e3192 --- /dev/null +++ b/compiler/src/Console/PrepareCommand.php @@ -0,0 +1,231 @@ +setName('prepare') + ->setDescription('Prepare PHAR'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->buildPreloadScript(); + $this->deleteUnnecessaryVendorCode(); + $this->fixComposerJson($this->buildDir); + $this->renamePhpStormStubs(); + $this->renamePhp8Stubs(); + $this->transformSource(); + return 0; + } + + private function fixComposerJson(string $buildDir): void + { + $json = json_decode($this->filesystem->read($buildDir . '/composer.json'), true); + + unset($json['replace']['phpstan/phpstan']); + $json['name'] = 'phpstan/phpstan'; + $json['require']['php'] = '^7.4|^8.0'; + + // simplify autoload (remove not packed build directory] + $json['autoload']['psr-4']['PHPStan\\'] = 'src/'; + + $encodedJson = json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + if ($encodedJson === false) { + throw new Exception('json_encode() was not successful.'); + } + + $this->filesystem->write($buildDir . '/composer.json', $encodedJson); + } + + private function renamePhpStormStubs(): void + { + $directory = $this->buildDir . '/vendor/jetbrains/phpstorm-stubs'; + if (!is_dir($directory)) { + return; + } + + $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) { + continue; + } + + $renameSuccess = rename( + $path, + dirname($path) . '/' . $stubFile->getBasename('.php') . '.stub', + ); + if ($renameSuccess === false) { + throw new ShouldNotHappenException(sprintf('Could not rename %s', $path)); + } + } + + $stubsMapContents = file_get_contents($stubsMapPath); + if ($stubsMapContents === false) { + 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 ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); + } + } + + private function renamePhp8Stubs(): void + { + $directory = $this->buildDir . '/vendor/phpstan/php-8-stubs/stubs'; + if (!is_dir($directory)) { + return; + } + + $stubFinder = Finder::create(); + $stubsMapPath = $directory . '/../Php8StubsMap.php'; + foreach ($stubFinder->files()->name('*.php')->in($directory) as $stubFile) { + $path = $stubFile->getPathname(); + if ($path === $stubsMapPath) { + continue; + } + + $renameSuccess = rename( + $path, + dirname($path) . '/' . $stubFile->getBasename('.php') . '.stub', + ); + if ($renameSuccess === false) { + throw new ShouldNotHappenException(sprintf('Could not rename %s', $path)); + } + } + + $stubsMapContents = file_get_contents($stubsMapPath); + if ($stubsMapContents === false) { + 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 ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); + } + } + + private function buildPreloadScript(): void + { + $vendorDir = $this->buildDir . '/vendor'; + if (!is_dir($vendorDir . '/nikic/php-parser/lib/PhpParser')) { + return; + } + + $preloadScript = $this->buildDir . '/preload.php'; + $template = <<<'php' +files()->name('*.php')->in([ + $this->buildDir . '/src', + $vendorDir . '/nikic/php-parser/lib/PhpParser', + $vendorDir . '/phpstan/phpdoc-parser/src', + ])->exclude([ + 'Testing', + ])->sortByName() as $phpFile) { + $realPath = $phpFile->getRealPath(); + if ($realPath === false) { + return; + } + if (in_array($realPath, [ + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Expr/ArrayItem.php', + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Expr/ClosureUse.php', + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Stmt/DeclareDeclare.php', + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Scalar/DNumber.php', + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Scalar/Encapsed.php', + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Scalar/EncapsedStringPart.php', + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Scalar/LNumber.php', + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Stmt/PropertyProperty.php', + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Stmt/StaticVar.php', + $vendorDir . '/nikic/php-parser/lib/PhpParser/Node/Stmt/UseUse.php', + ], true)) { + continue; + } + $path = substr($realPath, strlen($root)); + $output .= 'require_once __DIR__ . ' . var_export($path, true) . ';' . "\n"; + } + + file_put_contents($preloadScript, sprintf($template, $output)); + } + + private function deleteUnnecessaryVendorCode(): void + { + $vendorDir = $this->buildDir . '/vendor'; + if (!is_dir($vendorDir . '/nikic/php-parser')) { + return; + } + + @unlink($vendorDir . '/nikic/php-parser/grammar/rebuildParsers.php'); + @unlink($vendorDir . '/nikic/php-parser/bin/php-parse'); + } + + private function transformSource(): void + { + chdir(__DIR__ . '/../../..'); + exec(escapeshellarg(__DIR__ . '/../../../vendor/bin/simple-downgrade') . ' downgrade -c ' . escapeshellarg('build/downgrade.php') . ' 7.2', $outputLines, $exitCode); + if ($exitCode === 0) { + return; + } + + throw new ShouldNotHappenException(implode("\n", $outputLines)); + } + +} diff --git a/compiler/src/Process/DefaultProcessFactory.php b/compiler/src/Process/DefaultProcessFactory.php deleted file mode 100644 index 3df3ce50da..0000000000 --- a/compiler/src/Process/DefaultProcessFactory.php +++ /dev/null @@ -1,31 +0,0 @@ -output = new NullOutput(); - } - - /** - * @param string[] $command - */ - public function create(array $command, string $cwd): Process - { - return new SymfonyProcess($command, $cwd, $this->output); - } - - public function setOutput(OutputInterface $output): void - { - $this->output = $output; - } - -} diff --git a/compiler/src/Process/Process.php b/compiler/src/Process/Process.php deleted file mode 100644 index 1bf5d1f25d..0000000000 --- a/compiler/src/Process/Process.php +++ /dev/null @@ -1,13 +0,0 @@ - - */ - public function getProcess(): \Symfony\Component\Process\Process; - -} diff --git a/compiler/src/Process/ProcessFactory.php b/compiler/src/Process/ProcessFactory.php deleted file mode 100644 index b2225f5343..0000000000 --- a/compiler/src/Process/ProcessFactory.php +++ /dev/null @@ -1,17 +0,0 @@ - */ - private \Symfony\Component\Process\Process $process; - - /** - * @param string[] $command - */ - public function __construct(array $command, string $cwd, OutputInterface $output) - { - $this->process = (new \Symfony\Component\Process\Process($command, $cwd, null, null, null)) - ->mustRun(static function (string $type, string $buffer) use ($output): void { - $output->write($buffer); - }); - } - - /** - * @return \Symfony\Component\Process\Process - */ - public function getProcess(): \Symfony\Component\Process\Process - { - return $this->process; - } - -} diff --git a/compiler/tests/.phpunit.result.cache b/compiler/tests/.phpunit.result.cache index 4cf5b2c855..136029df2b 100644 --- a/compiler/tests/.phpunit.result.cache +++ b/compiler/tests/.phpunit.result.cache @@ -1 +1 @@ -C:37:"PHPUnit\Runner\DefaultTestResultCache":636:{a:2:{s:7:"defects";a:0:{}s:5:"times";a:8:{s:56:"PHPStan\Compiler\Console\CompileCommandTest::testCommand";d:0.053;s:61:"PHPStan\Compiler\Filesystem\SymfonyFilesystemTest::testExists";d:0.01;s:61:"PHPStan\Compiler\Filesystem\SymfonyFilesystemTest::testRemove";d:0;s:60:"PHPStan\Compiler\Filesystem\SymfonyFilesystemTest::testMkdir";d:0;s:59:"PHPStan\Compiler\Filesystem\SymfonyFilesystemTest::testRead";d:0;s:60:"PHPStan\Compiler\Filesystem\SymfonyFilesystemTest::testWrite";d:0.008;s:62:"PHPStan\Compiler\Process\DefaultProcessFactoryTest::testCreate";d:0.058;s:59:"PHPStan\Compiler\Process\SymfonyProcessTest::testGetProcess";d:0.01;}}} \ No newline at end of file +{"version":1,"defects":[],"times":{"PHPStan\\Compiler\\Filesystem\\SymfonyFilesystemTest::testExists":0.014,"PHPStan\\Compiler\\Filesystem\\SymfonyFilesystemTest::testRemove":0,"PHPStan\\Compiler\\Filesystem\\SymfonyFilesystemTest::testMkdir":0,"PHPStan\\Compiler\\Filesystem\\SymfonyFilesystemTest::testRead":0,"PHPStan\\Compiler\\Filesystem\\SymfonyFilesystemTest::testWrite":0}} \ No newline at end of file diff --git a/compiler/tests/Console/CompileCommandTest.php b/compiler/tests/Console/CompileCommandTest.php deleted file mode 100644 index 000b738533..0000000000 --- a/compiler/tests/Console/CompileCommandTest.php +++ /dev/null @@ -1,53 +0,0 @@ -createMock(Filesystem::class); - $filesystem->expects(self::once())->method('read')->with('bar/composer.json')->willReturn('{"name":"phpstan/phpstan-src","replace":{"phpstan/phpstan": "self.version"},"require":{"php":"^7.4"},"require-dev":1,"autoload-dev":2,"autoload":{"psr-4":{"PHPStan\\\\":[3]}}}'); - $filesystem->expects(self::once())->method('write')->with('bar/composer.json', <<createMock(Process::class); - - $processFactory = $this->createMock(ProcessFactory::class); - $processFactory->method('setOutput'); - $processFactory->method('create')->with(['php', 'box.phar', 'compile', '--no-parallel'], 'foo')->willReturn($process); - - $application = new Application(); - $application->add(new CompileCommand($filesystem, $processFactory, 'foo', 'bar')); - - $command = $application->find('phpstan:compile'); - $commandTester = new CommandTester($command); - $exitCode = $commandTester->execute([ - 'command' => $command->getName(), - ]); - - self::assertSame(0, $exitCode); - } - -} diff --git a/compiler/tests/Process/DefaultProcessFactoryTest.php b/compiler/tests/Process/DefaultProcessFactoryTest.php deleted file mode 100644 index 31756e8f18..0000000000 --- a/compiler/tests/Process/DefaultProcessFactoryTest.php +++ /dev/null @@ -1,24 +0,0 @@ -createMock(OutputInterface::class); - $output->expects(self::once())->method('write'); - - $factory = new DefaultProcessFactory(); - $factory->setOutput($output); - - $process = $factory->create(['ls'], __DIR__)->getProcess(); - self::assertSame('\'ls\'', $process->getCommandLine()); - self::assertSame(__DIR__, $process->getWorkingDirectory()); - } - -} diff --git a/compiler/tests/Process/SymfonyProcessTest.php b/compiler/tests/Process/SymfonyProcessTest.php deleted file mode 100644 index 8124ba1c96..0000000000 --- a/compiler/tests/Process/SymfonyProcessTest.php +++ /dev/null @@ -1,21 +0,0 @@ -createMock(OutputInterface::class); - $output->expects(self::once())->method('write'); - - $process = (new SymfonyProcess(['ls'], __DIR__, $output))->getProcess(); - self::assertSame('\'ls\'', $process->getCommandLine()); - self::assertSame(__DIR__, $process->getWorkingDirectory()); - } - -} diff --git a/composer.json b/composer.json index f9943dc0ee..3a585da23c 100644 --- a/composer.json +++ b/composer.json @@ -5,31 +5,35 @@ "MIT" ], "require": { - "php": "^8.0", - "clue/block-react": "^1.4", + "php": "^8.1", + "composer-runtime-api": "^2.0", "clue/ndjson-react": "^1.0", "composer/ca-bundle": "^1.2", - "composer/xdebug-handler": "^2.0.1", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "fidry/cpu-core-counter": "^1.2", "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", - "hoa/regex": "1.17.01.13", - "jean85/pretty-package-versions": "^1.0.3", - "jetbrains/phpstorm-stubs": "dev-master#248e61f4d4a8bee1348facc98fecbd4bc41984f1", + "hoa/file": "1.17.07.11", + "jetbrains/phpstorm-stubs": "dev-master#b22fb017543bb7147e3bcc53f08fb13a48aff994", "nette/bootstrap": "^3.0", - "nette/di": "^3.0.11", - "nette/finder": "^2.5", - "nette/neon": "^3.3.1", + "nette/di": "^3.1.4", + "nette/neon": "3.3.4", + "nette/php-generator": "3.6.9", "nette/schema": "^1.2.2", "nette/utils": "^3.2.5", - "nikic/php-parser": "^4.13.2", + "nikic/php-parser": "^5.4.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "5.0.7.2", - "phpstan/php-8-stubs": "0.1.48", - "phpstan/phpdoc-parser": "^1.2.0", - "react/child-process": "^0.6.4", + "ondrejmirtes/better-reflection": "6.57.0.0", + "phpstan/php-8-stubs": "0.4.12", + "phpstan/phpdoc-parser": "2.1.0", + "psr/http-message": "^1.1", + "react/async": "^3", + "react/child-process": "^0.7", + "react/dns": "^1.10", "react/event-loop": "^1.2", "react/http": "^1.1", - "react/promise": "^2.8", + "react/promise": "^3.2", "react/socket": "^1.3", "react/stream": "^1.1", "symfony/console": "^5.4.3", @@ -37,47 +41,86 @@ "symfony/polyfill-intl-grapheme": "^1.23", "symfony/polyfill-intl-normalizer": "^1.23", "symfony/polyfill-mbstring": "^1.23", - "symfony/polyfill-php72": "^1.23", - "symfony/polyfill-php73": "^1.23", - "symfony/polyfill-php74": "^1.23", "symfony/polyfill-php80": "^1.23", + "symfony/polyfill-php81": "^1.27", "symfony/process": "^5.4.3", "symfony/service-contracts": "^2.5.0", "symfony/string": "^5.4.3" }, "replace": { - "phpstan/phpstan": "self.version" + "phpstan/phpstan": "2.1.x", + "symfony/polyfill-php73": "*" }, "require-dev": { - "brianium/paratest": "^6.2.0", - "nategood/httpful": "^0.2.20", + "brianium/paratest": "^6.5", + "cweagans/composer-patches": "^1.7.3", + "ondrejmirtes/simple-downgrader": "^2.0", "php-parallel-lint/php-parallel-lint": "^1.2.0", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-nette": "^1.0", - "phpstan/phpstan-php-parser": "^1.1", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5.4", - "rector/rector": "^0.12.15", - "vaimo/composer-patches": "^4.22" + "phpstan/phpstan-deprecation-rules": "^2.0.2", + "phpstan/phpstan-nette": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "shipmonk/composer-dependency-analyser": "^1.5", + "shipmonk/name-collision-detector": "^2.0" }, "config": { "platform": { - "php": "8.0.99" + "php": "8.1.99" }, "platform-check": false, "sort-packages": true, "allow-plugins": { - "composer/package-versions-deprecated": true, - "vaimo/composer-patches": true + "cweagans/composer-patches": true } }, "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - }, - "patcher": { - "search": "patches" + "composer-exit-on-patch-failure": true, + "patches": { + "composer/ca-bundle": [ + "patches/cloudflare-ca.patch" + ], + "hoa/exception": [ + "patches/Idle.patch" + ], + "hoa/file": [ + "patches/File.patch", + "patches/Read.patch" + ], + "hoa/iterator": [ + "patches/Buffer.patch", + "patches/Lookahead.patch" + ], + "hoa/compiler": [ + "patches/HoaException.patch", + "patches/Invocation.patch", + "patches/Rule.patch", + "patches/Lexer.patch", + "patches/TreeNode.patch" + ], + "hoa/consistency": [ + "patches/Consistency.patch" + ], + "hoa/protocol": [ + "patches/Node.patch", + "patches/Wrapper.patch" + ], + "hoa/stream": [ + "patches/Stream.patch" + ], + "jetbrains/phpstorm-stubs": [ + "patches/PDO.patch", + "patches/ReflectionProperty.patch", + "patches/SessionHandler.patch", + "patches/xmlreader.patch", + "patches/dom_c.patch" + ], + "nette/di": [ + "patches/DependencyChecker.patch" + ], + "react/http": [ + "patches/Sender.patch" + ] } }, "autoload": { @@ -86,7 +129,7 @@ "src/" ] }, - "files": ["src/dumpType.php","src/Testing/functions.php"] + "files": ["src/debugScope.php", "src/dumpType.php", "src/autoloadFunctions.php", "src/Testing/functions.php"] }, "autoload-dev": { "psr-4": { @@ -99,6 +142,83 @@ "tests/PHPStan" ] }, + "repositories": [ + { + "type": "package", + "package": { + "name": "nette/di", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/00ea0afa643b3b4383a5cd1a322656c989ade498", + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/neon": "^3.3 || ^4.0", + "nette/php-generator": "^3.5.4 || ^4.0", + "nette/robot-loader": "^3.2 || ~4.0.0", + "nette/schema": "^1.2", + "nette/utils": "^3.2.5 || ~4.0.0", + "php": "7.2 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "💎 Nette 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", + "di", + "dic", + "factory", + "ioc", + "nette", + "static" + ], + "support": { + "issues": "https://github.com/nette/di/issues", + "source": "https://github.com/nette/di/tree/v3.1.5" + }, + "time": "2023-10-02T19:58:38+00:00" + } + } + ], "minimum-stability": "dev", "prefer-stable": true, "bin": [ diff --git a/composer.lock b/composer.lock index 3497da7b42..b304bec2cb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,97 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ede0704e050d946882afd2613bdbb349", + "content-hash": "a5aee6235dc8ddeac7b42ed53ce87902", "packages": [ - { - "name": "clue/block-react", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/clue/reactphp-block.git", - "reference": "718b0571a94aa693c6fffc72182e87257ac900f3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-block/zipball/718b0571a94aa693c6fffc72182e87257ac900f3", - "reference": "718b0571a94aa693c6fffc72182e87257ac900f3", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "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.4" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - } - ], - "description": "Lightweight library that eases integrating async components built for ReactPHP in a traditional, blocking environment.", - "homepage": "https://github.com/clue/reactphp-block", - "keywords": [ - "async", - "await", - "blocking", - "event loop", - "promise", - "reactphp", - "sleep", - "synchronous" - ], - "support": { - "issues": "https://github.com/clue/reactphp-block/issues", - "source": "https://github.com/clue/reactphp-block/tree/v1.5.0" - }, - "funding": [ - { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2021-10-20T14:07:33+00:00" - }, { "name": "clue/ndjson-react", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/clue/reactphp-ndjson.git", - "reference": "708411c7e45ac85371a99d50f52284971494bede" + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/708411c7e45ac85371a99d50f52284971494bede", - "reference": "708411c7e45ac85371a99d50f52284971494bede", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", "shasum": "" }, "require": { "php": ">=5.3", - "react/stream": "^1.0 || ^0.7 || ^0.6" + "react/stream": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" }, "type": "library", "autoload": { @@ -124,7 +56,7 @@ ], "support": { "issues": "https://github.com/clue/reactphp-ndjson/issues", - "source": "https://github.com/clue/reactphp-ndjson/tree/v1.2.0" + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" }, "funding": [ { @@ -136,32 +68,32 @@ "type": "github" } ], - "time": "2020-12-09T13:09:07+00:00" + "time": "2022-12-23T10:58:28+00:00" }, { "name": "composer/ca-bundle", - "version": "1.3.1", + "version": "1.5.6", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b" + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", - "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -196,7 +128,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.3.1" + "source": "https://github.com/composer/ca-bundle/tree/1.5.6" }, "funding": [ { @@ -212,44 +144,47 @@ "type": "tidelift" } ], - "time": "2021-10-28T20:44:15+00:00" + "time": "2025-03-06T14:30:56+00:00" }, { - "name": "composer/package-versions-deprecated", - "version": "1.11.99.4", + "name": "composer/pcre", + "version": "3.3.1", "source": { "type": "git", - "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "b174585d1fe49ceed21928a945138948cb394600" + "url": "https://github.com/composer/pcre.git", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600", - "reference": "b174585d1fe49ceed21928a945138948cb394600", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1.0 || ^2.0", - "php": "^7 || ^8" + "php": "^7.4 || ^8.0" }, - "replace": { - "ocramius/package-versions": "1.11.99" + "conflict": { + "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "composer/composer": "^1.9.3 || ^2.0@dev", - "ext-zip": "^1.13", - "phpunit/phpunit": "^6.5 || ^7" + "phpstan/phpstan": "^1.11.10", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8 || ^9" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "PackageVersions\\Installer", "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] } }, "autoload": { "psr-4": { - "PackageVersions\\": "src/PackageVersions" + "Composer\\Pcre\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -257,19 +192,22 @@ "MIT" ], "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - }, { "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be/" } ], - "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], "support": { - "issues": "https://github.com/composer/package-versions-deprecated/issues", - "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.1" }, "funding": [ { @@ -285,39 +223,38 @@ "type": "tidelift" } ], - "time": "2021-09-13T08:41:34+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { - "name": "composer/pcre", - "version": "1.0.0", + "name": "composer/semver", + "version": "3.4.3", "source": { "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2" + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/3d322d715c43a1ac36c7fe215fa59336265500f2", - "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "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" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { "psr-4": { - "Composer\\Pcre\\": "src" + "Composer\\Semver\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -325,22 +262,33 @@ "MIT" ], "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de/" + }, { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", "homepage": "http://seld.be/" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl/" } ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "description": "Semver library that offers utilities, version constraint parsing and validation.", "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" + "semantic", + "semver", + "validation", + "versioning" ], "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/1.0.0" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -356,31 +304,31 @@ "type": "tidelift" } ], - "time": "2021-12-06T15:17:27+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/xdebug-handler", - "version": "2.0.3", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6555461e76962fd0379c444c46fd558a0fcfb65e", - "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "composer/pcre": "^1", - "php": "^5.3.2 || ^7.0 || ^8.0", + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", "psr/log": "^1 || ^2 || ^3" }, "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -404,9 +352,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.3" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -422,32 +370,32 @@ "type": "tidelift" } ], - "time": "2021-12-08T13:07:32+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "evenement/evenement", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { - "psr-0": { - "Evenement": "src" + "psr-4": { + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -467,9 +415,126 @@ ], "support": { "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" }, - "time": "2017-07-23T21:35:13+00:00" + "time": "2020-11-24T22:02:12+00:00" }, { "name": "hoa/compiler", @@ -591,12 +656,12 @@ } }, "autoload": { - "psr-4": { - "Hoa\\Consistency\\": "." - }, "files": [ "Prelude.php" - ] + ], + "psr-4": { + "Hoa\\Consistency\\": "." + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -998,12 +1063,12 @@ } }, "autoload": { - "psr-4": { - "Hoa\\Protocol\\": "." - }, "files": [ "Wrapper.php" - ] + ], + "psr-4": { + "Hoa\\Protocol\\": "." + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1371,81 +1436,25 @@ "abandoned": true, "time": "2017-01-10T10:39:54+00:00" }, - { - "name": "jean85/pretty-package-versions", - "version": "1.5.1", - "source": { - "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "a917488320c20057da87f67d0d40543dd9427f7a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/a917488320c20057da87f67d0d40543dd9427f7a", - "reference": "a917488320c20057da87f67d0d40543dd9427f7a", - "shasum": "" - }, - "require": { - "composer/package-versions-deprecated": "^1.8.0", - "php": "^7.0|^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0|^8.5|^9.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Jean85\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" - } - ], - "description": "A wrapper for ocramius/package-versions to get pretty versions strings", - "keywords": [ - "composer", - "package", - "release", - "versions" - ], - "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/1.5.1" - }, - "time": "2020-09-14T08:43:34+00:00" - }, { "name": "jetbrains/phpstorm-stubs", "version": "dev-master", "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "248e61f4d4a8bee1348facc98fecbd4bc41984f1" + "reference": "b22fb017543bb7147e3bcc53f08fb13a48aff994" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/248e61f4d4a8bee1348facc98fecbd4bc41984f1", - "reference": "248e61f4d4a8bee1348facc98fecbd4bc41984f1", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/b22fb017543bb7147e3bcc53f08fb13a48aff994", + "reference": "b22fb017543bb7147e3bcc53f08fb13a48aff994", "shasum": "" }, "require-dev": { - "friendsofphp/php-cs-fixer": "@stable", - "nikic/php-parser": "@stable", - "php": "^8.0", - "phpdocumentor/reflection-docblock": "@stable", - "phpunit/phpunit": "@stable" + "friendsofphp/php-cs-fixer": "^v3.64.0", + "nikic/php-parser": "^v5.3.1", + "phpdocumentor/reflection-docblock": "^5.6.0", + "phpunit/phpunit": "^11.4.3" }, "default-branch": true, "type": "library", @@ -1473,26 +1482,26 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2022-03-07T17:51:23+00:00" + "time": "2025-04-22T16:22:26+00:00" }, { "name": "nette/bootstrap", - "version": "v3.1.2", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/nette/bootstrap.git", - "reference": "3ab4912a08af0c16d541c3709935c3478b5ee090" + "reference": "1a7965b4ee401ad0e3f673b9c016d2481afdc280" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/bootstrap/zipball/3ab4912a08af0c16d541c3709935c3478b5ee090", - "reference": "3ab4912a08af0c16d541c3709935c3478b5ee090", + "url": "https://api.github.com/repos/nette/bootstrap/zipball/1a7965b4ee401ad0e3f673b9c016d2481afdc280", + "reference": "1a7965b4ee401ad0e3f673b9c016d2481afdc280", "shasum": "" }, "require": { "nette/di": "^3.0.5", - "nette/utils": "^3.2.1", - "php": ">=7.2 <8.2" + "nette/utils": "^3.2.1 || ^4.0", + "php": ">=7.2 <8.3" }, "conflict": { "tracy/tracy": "<2.6" @@ -1552,45 +1561,42 @@ ], "support": { "issues": "https://github.com/nette/bootstrap/issues", - "source": "https://github.com/nette/bootstrap/tree/v3.1.2" + "source": "https://github.com/nette/bootstrap/tree/v3.1.4" }, - "time": "2021-11-24T16:51:46+00:00" + "time": "2022-12-14T15:23:02+00:00" }, { "name": "nette/di", - "version": "v3.0.11", + "version": "v3.1.5", "source": { "type": "git", "url": "https://github.com/nette/di.git", - "reference": "942e406f63b88b57cb4e095ae0fd95c103d12c5b" + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/di/zipball/942e406f63b88b57cb4e095ae0fd95c103d12c5b", - "reference": "942e406f63b88b57cb4e095ae0fd95c103d12c5b", + "url": "https://api.github.com/repos/nette/di/zipball/00ea0afa643b3b4383a5cd1a322656c989ade498", + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498", "shasum": "" }, "require": { "ext-tokenizer": "*", - "nette/neon": "^3.3", - "nette/php-generator": "^3.3.3", - "nette/robot-loader": "^3.2", - "nette/schema": "^1.1", - "nette/utils": "^3.1.6", - "php": ">=7.1 <8.2" - }, - "conflict": { - "nette/bootstrap": "<3.0" + "nette/neon": "^3.3 || ^4.0", + "nette/php-generator": "^3.5.4 || ^4.0", + "nette/robot-loader": "^3.2 || ~4.0.0", + "nette/schema": "^1.2", + "nette/utils": "^3.2.5 || ~4.0.0", + "php": "7.2 - 8.3" }, "require-dev": { - "nette/tester": "^2.2", - "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -1627,22 +1633,22 @@ ], "support": { "issues": "https://github.com/nette/di/issues", - "source": "https://github.com/nette/di/tree/v3.0.11" + "source": "https://github.com/nette/di/tree/v3.1.5" }, - "time": "2021-10-26T11:44:44+00:00" + "time": "2023-10-02T19:58:38+00:00" }, { "name": "nette/finder", - "version": "v2.5.3", + "version": "v2.6.0", "source": { "type": "git", "url": "https://github.com/nette/finder.git", - "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8" + "reference": "991aefb42860abeab8e003970c3809a9d83cb932" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/finder/zipball/64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", - "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", + "url": "https://api.github.com/repos/nette/finder/zipball/991aefb42860abeab8e003970c3809a9d83cb932", + "reference": "991aefb42860abeab8e003970c3809a9d83cb932", "shasum": "" }, "require": { @@ -1660,7 +1666,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -1694,27 +1700,27 @@ ], "support": { "issues": "https://github.com/nette/finder/issues", - "source": "https://github.com/nette/finder/tree/v2.5.3" + "source": "https://github.com/nette/finder/tree/v2.6.0" }, - "time": "2021-12-12T17:43:24+00:00" + "time": "2022-10-13T01:31:15+00:00" }, { "name": "nette/neon", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "54b287d8c2cdbe577b02e28ca1713e275b05ece2" + "reference": "bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/54b287d8c2cdbe577b02e28ca1713e275b05ece2", - "reference": "54b287d8c2cdbe577b02e28ca1713e275b05ece2", + "url": "https://api.github.com/repos/nette/neon/zipball/bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda", + "reference": "bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.1" + "php": "7.1 - 8.4" }, "require-dev": { "nette/tester": "^2.0", @@ -1762,27 +1768,27 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/v3.3.2" + "source": "https://github.com/nette/neon/tree/v3.3.4" }, - "time": "2021-11-25T15:57:41+00:00" + "time": "2024-10-04T22:17:24+00:00" }, { "name": "nette/php-generator", - "version": "v3.6.5", + "version": "v3.6.9", "source": { "type": "git", "url": "https://github.com/nette/php-generator.git", - "reference": "9370403f9d9c25b51c4596ded1fbfe70347f7c82" + "reference": "d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/php-generator/zipball/9370403f9d9c25b51c4596ded1fbfe70347f7c82", - "reference": "9370403f9d9c25b51c4596ded1fbfe70347f7c82", + "url": "https://api.github.com/repos/nette/php-generator/zipball/d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6", + "reference": "d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6", "shasum": "" }, "require": { "nette/utils": "^3.1.2", - "php": ">=7.2 <8.2" + "php": ">=7.2 <8.3" }, "require-dev": { "nette/tester": "^2.4", @@ -1830,9 +1836,9 @@ ], "support": { "issues": "https://github.com/nette/php-generator/issues", - "source": "https://github.com/nette/php-generator/tree/v3.6.5" + "source": "https://github.com/nette/php-generator/tree/v3.6.9" }, - "time": "2021-11-24T16:23:44+00:00" + "time": "2022-10-04T11:49:47+00:00" }, { "name": "nette/robot-loader", @@ -1903,25 +1909,25 @@ }, { "name": "nette/schema", - "version": "v1.2.2", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df" + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/9a39cef03a5b34c7de64f551538cbba05c2be5df", - "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df", + "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", "shasum": "" }, "require": { "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": ">=7.1 <8.2" + "php": "7.1 - 8.3" }, "require-dev": { "nette/tester": "^2.3 || ^2.4", - "phpstan/phpstan-nette": "^0.12", + "phpstan/phpstan-nette": "^1.0", "tracy/tracy": "^2.7" }, "type": "library", @@ -1959,31 +1965,32 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.2" + "source": "https://github.com/nette/schema/tree/v1.2.5" }, - "time": "2021-10-15T11:40:02+00:00" + "time": "2023-10-05T20:37:59+00:00" }, { "name": "nette/utils", - "version": "v3.2.6", + "version": "v3.2.10", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "2f261e55bd6a12057442045bf2c249806abc1d02" + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/2f261e55bd6a12057442045bf2c249806abc1d02", - "reference": "2f261e55bd6a12057442045bf2c249806abc1d02", + "url": "https://api.github.com/repos/nette/utils/zipball/a4175c62652f2300c8017fb7e640f9ccb11648d2", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2", "shasum": "" }, "require": { - "php": ">=7.2 <8.2" + "php": ">=7.2 <8.4" }, "conflict": { "nette/di": "<3.0.6" }, "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", "nette/tester": "~2.0", "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.3" @@ -2044,31 +2051,33 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v3.2.6" + "source": "https://github.com/nette/utils/tree/v3.2.10" }, - "time": "2021-11-24T15:47:23+00:00" + "time": "2023-07-30T15:38:18+00:00" }, { "name": "nikic/php-parser", - "version": "v4.13.2", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -2076,7 +2085,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -2100,9 +2109,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2021-11-30T19:35:32+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "ondram/ci-detector", @@ -2178,32 +2187,33 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "5.0.7.2", + "version": "6.57.0.0", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "62e6d33070d670cf7385b248371179cb3b552c2d" + "reference": "dcc22b90a63497f3450dd5eed62197bc46937297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/62e6d33070d670cf7385b248371179cb3b552c2d", - "reference": "62e6d33070d670cf7385b248371179cb3b552c2d", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/dcc22b90a63497f3450dd5eed62197bc46937297", + "reference": "dcc22b90a63497f3450dd5eed62197bc46937297", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#3a6d6053bcc6c9a154827b2624e10b5c428b7eb0", - "nikic/php-parser": "^4.13.2", - "php": "^7.1 || ^8.0" + "jetbrains/phpstorm-stubs": "dev-master#dfcad4524db603bd20bdec3aab1a31c5f5128ea3", + "nikic/php-parser": "^5.4.0", + "php": "^7.4 || ^8.0" }, "conflict": { "thecodingmachine/safe": "<1.1.3" }, "require-dev": { - "doctrine/coding-standard": "^9.0.0", - "phpstan/phpstan": "^1.3.0", - "phpunit/phpunit": "^9.5.11", - "rector/rector": "0.12.5" + "doctrine/coding-standard": "^12.0.0", + "phpstan/phpstan": "^1.10.60", + "phpstan/phpstan-phpunit": "^1.3.16", + "phpunit/phpunit": "^11.5.7", + "rector/rector": "1.2.10" }, "suggest": { "composer/composer": "Required to use the ComposerSourceLocator" @@ -2242,27 +2252,27 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/5.0.7.2" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.57.0.0" }, - "time": "2022-02-25T19:46:30+00:00" + "time": "2025-02-12T21:16:38+00:00" }, { "name": "phpstan/php-8-stubs", - "version": "0.1.48", + "version": "0.4.12", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "6fc5082cf5a2d629c54543c4067d3fe6e68c6f8e" + "reference": "d8f8290313e4fd1b4840c553a8492eff31ad54eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/6fc5082cf5a2d629c54543c4067d3fe6e68c6f8e", - "reference": "6fc5082cf5a2d629c54543c4067d3fe6e68c6f8e", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/d8f8290313e4fd1b4840c553a8492eff31ad54eb", + "reference": "d8f8290313e4fd1b4840c553a8492eff31ad54eb", "shasum": "" }, "type": "library", "autoload": { - "files": [ + "classmap": [ "Php8StubsMap.php" ] }, @@ -2274,41 +2284,39 @@ "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.48" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.12" }, - "time": "2022-03-10T00:11:46+00:00" + "time": "2025-04-15T00:22:00+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.2.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { "psr-4": { "PHPStan\\PhpDocParser\\": [ @@ -2323,26 +2331,26 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.2.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2021-09-16T20:46:02+00:00" + "time": "2025-02-19T13:28:12+00:00" }, { "name": "psr/container", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "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/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", "autoload": { @@ -2371,31 +2379,31 @@ ], "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/1.1.2" }, - "time": "2021-03-05T17:36:06+00:00" + "time": "2021-11-05T16:50:12+00:00" }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -2424,36 +2432,36 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/log", - "version": "1.1.3", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2463,7 +2471,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -2474,22 +2482,94 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" + "source": "https://github.com/php-fig/log/tree/2.0.0" + }, + "time": "2021-07-14T16:41:46+00:00" + }, + { + "name": "react/async", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/async.git", + "reference": "bc3ef672b33e95bf814fe8377731e46888ed4b54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/async/zipball/bc3ef672b33e95bf814fe8377731e46888ed4b54", + "reference": "bc3ef672b33e95bf814fe8377731e46888ed4b54", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "react/event-loop": "^1.2", + "react/promise": "^3.0 || ^2.8 || ^1.2.1" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async utilities for ReactPHP", + "keywords": [ + "async", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/async/issues", + "source": "https://github.com/reactphp/async/tree/v3.2.0" }, - "time": "2020-03-23T09:12:05+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-22T16:21:11+00:00" }, { "name": "react/cache", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/cache.git", - "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e" + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/4bf736a2cccec7298bdf745db77585966fc2ca7e", - "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { @@ -2497,7 +2577,7 @@ "react/promise": "^3.0 || ^2.0 || ^1.1" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, "type": "library", "autoload": { @@ -2540,49 +2620,46 @@ ], "support": { "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.1.1" + "source": "https://github.com/reactphp/cache/tree/v1.2.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-02-02T06:47:52+00:00" + "time": "2022-11-30T15:59:55+00:00" }, { "name": "react/child-process", - "version": "v0.6.4", + "version": "0.7.x-dev", "source": { "type": "git", "url": "https://github.com/reactphp/child-process.git", - "reference": "a778f3fb828d68caf8a9ab6567fd8342a86f12fe" + "reference": "ce2654d21d2a749e0a6142d00432e65ba003a2d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/a778f3fb828d68caf8a9ab6567fd8342a86f12fe", - "reference": "a778f3fb828d68caf8a9ab6567fd8342a86f12fe", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/ce2654d21d2a749e0a6142d00432e65ba003a2d9", + "reference": "ce2654d21d2a749e0a6142d00432e65ba003a2d9", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", "react/event-loop": "^1.2", - "react/stream": "^1.2" + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/socket": "^1.8", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { - "React\\ChildProcess\\": "src" + "React\\ChildProcess\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2619,49 +2696,45 @@ ], "support": { "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.4" + "source": "https://github.com/reactphp/child-process/tree/0.7.x" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-10-12T10:37:07+00:00" + "time": "2024-08-04T20:30:51+00:00" }, { "name": "react/dns", - "version": "v1.8.0", + "version": "v1.13.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "2a5a74ab751e53863b45fb87e1d3913884f88248" + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/2a5a74ab751e53863b45fb87e1d3913884f88248", - "reference": "2a5a74ab751e53863b45fb87e1d3913884f88248", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", "shasum": "" }, "require": { "php": ">=5.3.0", "react/cache": "^1.0 || ^0.6 || ^0.5", "react/event-loop": "^1.2", - "react/promise": "^3.0 || ^2.7 || ^1.2.1", - "react/promise-timer": "^1.2" + "react/promise": "^3.2 || ^2.7 || ^1.2.1" }, "require-dev": { - "clue/block-react": "^1.2", - "phpunit/phpunit": "^9.3 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { "psr-4": { - "React\\Dns\\": "src" + "React\\Dns\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2699,49 +2772,43 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.8.0" + "source": "https://github.com/reactphp/dns/tree/v1.13.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-07-11T12:40:34+00:00" + "time": "2024-06-13T14:18:03+00:00" }, { "name": "react/event-loop", - "version": "v1.2.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2" + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/be6dee480fc4692cec0504e65eb486e3be1aa6f2", - "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { - "ext-event": "~1.0 for ExtEventLoop", - "ext-pcntl": "For signal handling support when using the StreamSelectLoop", - "ext-uv": "* for ExtUvLoop" + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" }, "type": "library", "autoload": { "psr-4": { - "React\\EventLoop\\": "src" + "React\\EventLoop\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2777,56 +2844,53 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.2.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-07-11T12:31:24+00:00" + "time": "2023-11-13T13:48:05+00:00" }, { "name": "react/http", - "version": "v1.5.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/reactphp/http.git", - "reference": "8a0fd7c0aa74f0db3008b1e47ca86c613cbb040e" + "reference": "8111281ee57f22b7194f5dba225e609ba7ce4d20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/http/zipball/8a0fd7c0aa74f0db3008b1e47ca86c613cbb040e", - "reference": "8a0fd7c0aa74f0db3008b1e47ca86c613cbb040e", + "url": "https://api.github.com/repos/reactphp/http/zipball/8111281ee57f22b7194f5dba225e609ba7ce4d20", + "reference": "8111281ee57f22b7194f5dba225e609ba7ce4d20", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "fig/http-message-util": "^1.1", "php": ">=5.3.0", "psr/http-message": "^1.0", "react/event-loop": "^1.2", - "react/promise": "^2.3 || ^1.2.1", - "react/promise-stream": "^1.1", - "react/socket": "^1.9", - "react/stream": "^1.2", - "ringcentral/psr7": "^1.2" + "react/promise": "^3 || ^2.3 || ^1.2.1", + "react/socket": "^1.12", + "react/stream": "^1.2" }, "require-dev": { - "clue/block-react": "^1.1", - "clue/http-proxy-react": "^1.3", - "clue/reactphp-ssh-proxy": "^1.0", - "clue/socks-react": "^1.0", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.9" }, "type": "library", "autoload": { "psr-4": { - "React\\Http\\": "src" + "React\\Http\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2871,192 +2935,56 @@ ], "support": { "issues": "https://github.com/reactphp/http/issues", - "source": "https://github.com/reactphp/http/tree/v1.5.0" + "source": "https://github.com/reactphp/http/tree/v1.10.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-08-04T12:24:55+00:00" + "time": "2024-03-27T17:20:46+00:00" }, { "name": "react/promise", - "version": "v2.8.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4" + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4", - "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.8.0" - }, - "time": "2020-05-12T15:16:56+00:00" - }, - { - "name": "react/promise-stream", - "version": "v1.3.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise-stream.git", - "reference": "3ebd94fe0d8edbf44937948af28d02d5437e9949" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-stream/zipball/3ebd94fe0d8edbf44937948af28d02d5437e9949", - "reference": "3ebd94fe0d8edbf44937948af28d02d5437e9949", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/promise": "^2.1 || ^1.2", - "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6" - }, - "require-dev": { - "clue/block-react": "^1.0", - "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" - ] + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, { "name": "Jan Sorgalla", "email": "jsorgalla@gmail.com", "homepage": "https://sorgalla.com/" }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "The missing link between Promise-land and Stream-land for ReactPHP", - "homepage": "https://github.com/reactphp/promise-stream", - "keywords": [ - "Buffer", - "async", - "promise", - "reactphp", - "stream", - "unwrap" - ], - "support": { - "issues": "https://github.com/reactphp/promise-stream/issues", - "source": "https://github.com/reactphp/promise-stream/tree/v1.3.0" - }, - "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.8.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise-timer.git", - "reference": "0bbbcc79589e5bfdddba68a287f1cb805581a479" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/0bbbcc79589e5bfdddba68a287f1cb805581a479", - "reference": "0bbbcc79589e5bfdddba68a287f1cb805581a479", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/event-loop": "^1.2", - "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Promise\\Timer\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ { "name": "Christian Lück", "email": "christian@clue.engineering", @@ -3067,75 +2995,61 @@ "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.", - "homepage": "https://github.com/reactphp/promise-timer", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "async", - "event-loop", "promise", - "reactphp", - "timeout", - "timer" + "promises" ], "support": { - "issues": "https://github.com/reactphp/promise-timer/issues", - "source": "https://github.com/reactphp/promise-timer/tree/v1.8.0" + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-12-06T11:08:48+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { "name": "react/socket", - "version": "v1.10.0", + "version": "v1.16.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "d132fde589ea97f4165f2d94b5296499eac125ec" + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/d132fde589ea97f4165f2d94b5296499eac125ec", - "reference": "d132fde589ea97f4165f2d94b5296499eac125ec", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", - "react/dns": "^1.8", + "react/dns": "^1.13", "react/event-loop": "^1.2", - "react/promise": "^2.6.0 || ^1.2.1", - "react/promise-timer": "^1.4.0", - "react/stream": "^1.2" + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" }, "require-dev": { - "clue/block-react": "^1.2", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/promise-stream": "^1.2" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { "psr-4": { - "React\\Socket\\": "src" + "React\\Socket\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3174,32 +3088,28 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.10.0" + "source": "https://github.com/reactphp/socket/tree/v1.16.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-11-29T10:08:24+00:00" + "time": "2024-07-26T10:38:09+00:00" }, { "name": "react/stream", - "version": "v1.2.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/reactphp/stream.git", - "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9" + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/7a423506ee1903e89f1e08ec5f0ed430ff784ae9", - "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", "shasum": "" }, "require": { @@ -3209,12 +3119,12 @@ }, "require-dev": { "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { "psr-4": { - "React\\Stream\\": "src" + "React\\Stream\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3256,103 +3166,38 @@ ], "support": { "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.2.0" + "source": "https://github.com/reactphp/stream/tree/v1.4.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-07-11T12:37:55+00:00" + "time": "2024-06-11T12:45:25+00:00" }, { - "name": "ringcentral/psr7", - "version": "1.3.0", + "name": "symfony/console", + "version": "v5.4.47", "source": { "type": "git", - "url": "https://github.com/ringcentral/psr7.git", - "reference": "360faaec4b563958b673fb52bbe94e37f14bc686" + "url": "https://github.com/symfony/console.git", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ringcentral/psr7/zipball/360faaec4b563958b673fb52bbe94e37f14bc686", - "reference": "360faaec4b563958b673fb52bbe94e37f14bc686", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", "shasum": "" }, "require": { - "php": ">=5.3", - "psr/http-message": "~1.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "RingCentral\\Psr7\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "PSR-7 message implementation", - "keywords": [ - "http", - "message", - "stream", - "uri" - ], - "support": { - "source": "https://github.com/ringcentral/psr7/tree/master" - }, - "time": "2018-05-29T20:21:04+00:00" - }, - { - "name": "symfony/console", - "version": "v5.4.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "a2a86ec353d825c75856c6fd14fac416a7bdb6b8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a2a86ec353d825c75856c6fd14fac416a7bdb6b8", - "reference": "a2a86ec353d825c75856c6fd14fac416a7bdb6b8", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" }, "conflict": { "psr/log": ">=3", @@ -3407,12 +3252,12 @@ "homepage": "https://symfony.com/", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.3" + "source": "https://github.com/symfony/console/tree/v5.4.47" }, "funding": [ { @@ -3428,33 +3273,33 @@ "type": "tidelift" } ], - "time": "2022-01-26T16:28:35+00:00" + "time": "2024-11-06T11:30:55+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -3479,7 +3324,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com/", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -3495,20 +3340,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/finder", - "version": "v5.4.3", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d" + "reference": "63741784cd7b9967975eec610b256eed3ede022b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/231313534dded84c7ecaa79d14bc5da4ccb69b7d", - "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", "shasum": "" }, "require": { @@ -3542,7 +3387,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com/", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.3" + "source": "https://github.com/symfony/finder/tree/v5.4.45" }, "funding": [ { @@ -3558,24 +3403,24 @@ "type": "tidelift" } ], - "time": "2022-01-26T16:34:36+00:00" + "time": "2024-09-28T13:32:08+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.24.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -3585,21 +3430,18 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3624,7 +3466,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -3640,45 +3482,42 @@ "type": "tidelift" } ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.24.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "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" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3705,7 +3544,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -3721,45 +3560,42 @@ "type": "tidelift" } ], - "time": "2021-11-23T21:10:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.24.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "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" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -3789,7 +3625,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -3805,24 +3641,24 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.24.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -3832,21 +3668,18 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3872,7 +3705,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -3888,118 +3721,39 @@ "type": "tidelift" } ], - "time": "2021-11-30T18:21:41+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.24.0", + "name": "symfony/polyfill-php80", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "9a142215a36a3888e30d0a9eeea9766764e96976" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976", - "reference": "9a142215a36a3888e30d0a9eeea9766764e96976", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - }, "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 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": { + ], "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, - "files": [ - "bootstrap.php" - ], "classmap": [ "Resources/stubs" ] @@ -4008,82 +3762,6 @@ "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": { - "psr-4": { - "Symfony\\Polyfill\\Php74\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], "authors": [ { "name": "Ion Bazan", @@ -4098,7 +3776,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.4+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com/", "keywords": [ "compatibility", @@ -4107,7 +3785,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php74/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -4123,42 +3801,39 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.24.0", + "name": "symfony/polyfill-php81", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", - "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4168,10 +3843,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -4181,7 +3852,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", "homepage": "https://symfony.com/", "keywords": [ "compatibility", @@ -4190,7 +3861,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" }, "funding": [ { @@ -4206,20 +3877,20 @@ "type": "tidelift" } ], - "time": "2021-09-13T13:58:33+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v5.4.3", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "553f50487389a977eb31cf6b37faae56da00f753" + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/553f50487389a977eb31cf6b37faae56da00f753", - "reference": "553f50487389a977eb31cf6b37faae56da00f753", + "url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046", + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046", "shasum": "" }, "require": { @@ -4252,7 +3923,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com/", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.3" + "source": "https://github.com/symfony/process/tree/v5.4.40" }, "funding": [ { @@ -4268,26 +3939,26 @@ "type": "tidelift" } ], - "time": "2022-01-26T16:28:35+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.0", + "version": "v2.5.4", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -4297,12 +3968,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -4335,7 +4006,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" }, "funding": [ { @@ -4351,20 +4022,20 @@ "type": "tidelift" } ], - "time": "2021-11-04T16:48:04+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/string", - "version": "v5.4.3", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10" + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/92043b7d8383e48104e411bc9434b260dbeb5a10", - "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", "shasum": "" }, "require": { @@ -4386,13 +4057,13 @@ }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], - "exclude-from-classmap": [ + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ "/Tests/" ] }, @@ -4421,7 +4092,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.3" + "source": "https://github.com/symfony/string/tree/v5.4.47" }, "funding": [ { @@ -4437,22 +4108,22 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2024-11-10T20:33:58+00:00" } ], "packages-dev": [ { "name": "brianium/paratest", - "version": "v6.2.0", + "version": "v6.6.3", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "9a94366983ce32c7724fc92e3b544327d4adb9be" + "reference": "f2d781bb9136cda2f5e73ee778049e80ba681cf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/9a94366983ce32c7724fc92e3b544327d4adb9be", - "reference": "9a94366983ce32c7724fc92e3b544327d4adb9be", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f2d781bb9136cda2f5e73ee778049e80ba681cf6", + "reference": "f2d781bb9136cda2f5e73ee778049e80ba681cf6", "shasum": "" }, "require": { @@ -4460,32 +4131,31 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", + "jean85/pretty-package-versions": "^2.0.5", "php": "^7.3 || ^8.0", - "phpunit/php-code-coverage": "^9.2.5", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.16", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-timer": "^5.0.3", - "phpunit/phpunit": "^9.5.1", - "sebastian/environment": "^5.1.3", - "symfony/console": "^4.4 || ^5.2", - "symfony/process": "^4.4 || ^5.2" + "phpunit/phpunit": "^9.5.23", + "sebastian/environment": "^5.1.4", + "symfony/console": "^5.4.9 || ^6.1.2", + "symfony/polyfill-php80": "^v1.26.0", + "symfony/process": "^5.4.8 || ^6.1.0" }, "require-dev": { - "doctrine/coding-standard": "^8.2.0", - "ekino/phpstan-banned-code": "^0.3.1", - "ergebnis/phpstan-rules": "^0.15.3", + "doctrine/coding-standard": "^9.0.0", + "ext-pcov": "*", "ext-posix": "*", - "infection/infection": "^0.20.2", - "phpstan/phpstan": "^0.12.70", - "phpstan/phpstan-deprecation-rules": "^0.12.6", - "phpstan/phpstan-phpunit": "^0.12.17", - "phpstan/phpstan-strict-rules": "^0.12.9", - "squizlabs/php_codesniffer": "^3.5.8", - "symfony/filesystem": "^5.2.2", - "thecodingmachine/phpstan-strict-rules": "^0.12.1", - "vimeo/psalm": "^4.4.1" + "infection/infection": "^0.26.13", + "malukenho/mcbumpface": "^1.1.5", + "squizlabs/php_codesniffer": "^3.7.1", + "symfony/filesystem": "^5.4.9 || ^6.1.0", + "vimeo/psalm": "^4.26.0" }, "bin": [ - "bin/paratest" + "bin/paratest", + "bin/paratest.bat", + "bin/paratest_for_phpstorm" ], "type": "library", "autoload": { @@ -4503,8 +4173,12 @@ { "name": "Brian Scaturro", "email": "scaturrob@gmail.com", - "homepage": "http://brianscaturro.com/", - "role": "Lead" + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" } ], "description": "Parallel testing for PHP", @@ -4517,35 +4191,94 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.2.0" + "source": "https://github.com/paratestphp/paratest/tree/v6.6.3" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2022-08-25T05:44:14+00:00" + }, + { + "name": "cweagans/composer-patches", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "composer/composer": "~1.0 || ~2.0", + "phpunit/phpunit": "~4.6" + }, + "type": "composer-plugin", + "extra": { + "class": "cweagans\\Composer\\Patches" + }, + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } }, - "time": "2021-01-29T15:25:31+00:00" + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a way to patch Composer packages.", + "support": { + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" + }, + "time": "2022-12-20T22:53:13+00:00" }, { "name": "doctrine/instantiator", - "version": "1.4.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^8.0", + "doctrine/coding-standard": "^11", "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": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -4572,7 +4305,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/2.0.0" }, "funding": [ { @@ -4588,35 +4321,42 @@ "type": "tidelift" } ], - "time": "2020-11-10T18:47:58+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { - "name": "drupol/phposinfo", - "version": "1.6.5", + "name": "jean85/pretty-package-versions", + "version": "2.0.5", "source": { "type": "git", - "url": "https://github.com/drupol/phposinfo.git", - "reference": "36b0250d38279c8a131a1898a31e359606024507" + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupol/phposinfo/zipball/36b0250d38279c8a131a1898a31e359606024507", - "reference": "36b0250d38279c8a131a1898a31e359606024507", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", "shasum": "" }, "require": { - "php": ">= 7.1.3" + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" }, "require-dev": { - "drupol/php-conventions": "^1.7.1", - "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", - "infection/infection": "^0.13.6 || ^0.15.0", - "phpspec/phpspec": "^5.1.2 || ^6.1.1" + "friendsofphp/php-cs-fixer": "^2.17", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^0.12.66", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { "psr-4": { - "drupol\\phposinfo\\": "src/" + "Jean85\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4625,57 +4365,58 @@ ], "authors": [ { - "name": "Pol Dellaiera", - "email": "pol.dellaiera@protonmail.com" + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" } ], - "description": "Try to guess the host operating system.", + "description": "A library to get pretty versions strings of installed dependencies", "keywords": [ - "operating system detection" + "composer", + "package", + "release", + "versions" ], "support": { - "issues": "https://github.com/drupol/phposinfo/issues", - "source": "https://github.com/drupol/phposinfo/tree/master" + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" }, - "funding": [ - { - "url": "https://github.com/drupol", - "type": "github" - } - ], - "abandoned": "loophp/phposinfo", - "time": "2020-05-19T14:14:28+00:00" + "time": "2021-10-08T21:21:46+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.10.2", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "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": [ @@ -4691,7 +4432,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.13.1" }, "funding": [ { @@ -4699,78 +4440,74 @@ "type": "tidelift" } ], - "time": "2020-11-13T09:40:50+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { - "name": "nategood/httpful", - "version": "0.2.20", + "name": "ondrejmirtes/simple-downgrader", + "version": "2.0.0", "source": { "type": "git", - "url": "https://github.com/nategood/httpful.git", - "reference": "c1cd4d46a4b281229032cf39d4dd852f9887c0f6" + "url": "https://github.com/ondrejmirtes/simple-downgrader.git", + "reference": "fb8b7833034f0396d5e4518ed090e3d099b7d9bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nategood/httpful/zipball/c1cd4d46a4b281229032cf39d4dd852f9887c0f6", - "reference": "c1cd4d46a4b281229032cf39d4dd852f9887c0f6", + "url": "https://api.github.com/repos/ondrejmirtes/simple-downgrader/zipball/fb8b7833034f0396d5e4518ed090e3d099b7d9bc", + "reference": "fb8b7833034f0396d5e4518ed090e3d099b7d9bc", "shasum": "" }, "require": { - "ext-curl": "*", - "php": ">=5.3" + "nette/utils": "^3.2.5", + "nikic/php-parser": "^5.3.0", + "php": "^7.4|^8.0", + "phpstan/phpdoc-parser": "^2.0", + "symfony/console": "^5.4", + "symfony/finder": "^5.4" }, "require-dev": { - "phpunit/phpunit": "*" + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^9.6" }, + "bin": [ + "bin/simple-downgrade" + ], "type": "library", "autoload": { - "psr-0": { - "Httpful": "src/" + "psr-4": { + "SimpleDowngrader\\": [ + "src/" + ] } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Nate Good", - "email": "me@nategood.com", - "homepage": "http://nategood.com/" - } - ], - "description": "A Readable, Chainable, REST friendly, PHP HTTP Client", - "homepage": "http://github.com/nategood/httpful", - "keywords": [ - "api", - "curl", - "http", - "requests", - "rest", - "restful" - ], + "description": "Simple Downgrader", "support": { - "issues": "https://github.com/nategood/httpful/issues", - "source": "https://github.com/nategood/httpful/tree/v0.2.20" + "issues": "https://github.com/ondrejmirtes/simple-downgrader/issues", + "source": "https://github.com/ondrejmirtes/simple-downgrader/tree/2.0.0" }, - "time": "2015-10-26T16:11:30+00:00" + "time": "2024-10-09T14:55:47+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.1", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "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/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -4811,22 +4548,28 @@ "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.4" }, - "time": "2020-06-27T14:33:11+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+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": { @@ -4862,27 +4605,27 @@ "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": "php-parallel-lint/php-parallel-lint", - "version": "v1.2.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git", - "reference": "474f18bc6cc6aca61ca40bfab55139de614e51ca" + "reference": "6db563514f27e19595a19f45a4bf757b6401194e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/474f18bc6cc6aca61ca40bfab55139de614e51ca", - "reference": "474f18bc6cc6aca61ca40bfab55139de614e51ca", + "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6db563514f27e19595a19f45a4bf757b6401194e", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=5.4.0" + "php": ">=5.3.0" }, "replace": { "grogy/php-parallel-lint": "*", @@ -4890,8 +4633,8 @@ }, "require-dev": { "nette/tester": "^1.3 || ^2.0", - "php-parallel-lint/php-console-highlighter": "~0.3", - "squizlabs/php_codesniffer": "~3.0" + "php-parallel-lint/php-console-highlighter": "0.* || ^1.0", + "squizlabs/php_codesniffer": "^3.6" }, "suggest": { "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet" @@ -4902,7 +4645,7 @@ "type": "library", "autoload": { "classmap": [ - "./" + "./src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4915,333 +4658,46 @@ "email": "ahoj@jakubonderka.cz" } ], - "description": "This tool check syntax of PHP files about 20x faster than serial check.", + "description": "This tool checks the syntax of PHP files about 20x faster than serial check.", "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint", - "support": { - "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues", - "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/master" - }, - "time": "2020-04-04T12:18:32+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org/", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", + "lint", "static analysis" ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "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" - }, - "time": "2020-09-03T19:13:55+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "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" - }, - "time": "2020-09-17T18:55:26+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "1.13.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com/" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues", + "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.4.0" }, - "time": "2021-03-17T13:42:18+00:00" + "time": "2024-03-27T12:14:49+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "1.0.0", + "version": "2.0.x-dev", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "e5ccafb0dd8d835dd65d8d7a1a0d2b1b75414682" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/e5ccafb0dd8d835dd65d8d7a1a0d2b1b75414682", - "reference": "e5ccafb0dd8d835dd65d8d7a1a0d2b1b75414682", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" - }, - "require-dev": { - "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": "1.0-dev" - }, - "phpstan": { - "includes": [ - "rules.neon" - ] - } - }, - "autoload": { - "psr-4": { - "PHPStan\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", - "support": { - "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.0.0" - }, - "time": "2021-09-23T11:02:21+00:00" - }, - { - "name": "phpstan/phpstan-nette", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan-nette.git", - "reference": "f4654b27b107241e052755ec187a0b1964541ba6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/f4654b27b107241e052755ec187a0b1964541ba6", - "reference": "f4654b27b107241e052755ec187a0b1964541ba6", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" - }, - "conflict": { - "nette/application": "<2.3.0", - "nette/component-model": "<2.3.0", - "nette/di": "<2.3.0", - "nette/forms": "<2.3.0", - "nette/http": "<2.3.0", - "nette/utils": "<2.3.0" + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "15f1d89bd70d9d05c9c99f7698ab8724e5a8431b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/15f1d89bd70d9d05c9c99f7698ab8724e5a8431b", + "reference": "15f1d89bd70d9d05c9c99f7698ab8724e5a8431b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.13" }, "require-dev": { - "nette/forms": "^3.0", - "nette/utils": "^2.3.0 || ^3.0.0", - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-php-parser": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" }, + "default-branch": true, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ - "extension.neon", "rules.neon" ] } @@ -5255,45 +4711,55 @@ "license": [ "MIT" ], - "description": "Nette Framework class reflection extension for PHPStan", + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { - "issues": "https://github.com/phpstan/phpstan-nette/issues", - "source": "https://github.com/phpstan/phpstan-nette/tree/1.0.0" + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.x" }, - "time": "2021-09-20T16:12:57+00:00" + "time": "2025-04-19T16:06:02+00:00" }, { - "name": "phpstan/phpstan-php-parser", - "version": "1.1.0", + "name": "phpstan/phpstan-nette", + "version": "2.0.0", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan-php-parser.git", - "reference": "1c7670dd92da864b5d019f22d9f512a6ae18b78e" + "url": "https://github.com/phpstan/phpstan-nette.git", + "reference": "cacb6983bbdf44d5c3a7222e5ca74f61f8531806" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-php-parser/zipball/1c7670dd92da864b5d019f22d9f512a6ae18b78e", - "reference": "1c7670dd92da864b5d019f22d9f512a6ae18b78e", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/cacb6983bbdf44d5c3a7222e5ca74f61f8531806", + "reference": "cacb6983bbdf44d5c3a7222e5ca74f61f8531806", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.3" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "conflict": { + "nette/application": "<2.3.0", + "nette/component-model": "<2.3.0", + "nette/di": "<2.3.0", + "nette/forms": "<2.3.0", + "nette/http": "<2.3.0", + "nette/utils": "<2.3.0" }, "require-dev": { + "nette/application": "^3.0", + "nette/di": "^3.1.10", + "nette/forms": "^3.0", + "nette/utils": "^2.3.0 || ^3.0.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - }, "phpstan": { "includes": [ - "extension.neon" + "extension.neon", + "rules.neon" ] } }, @@ -5306,45 +4772,41 @@ "license": [ "MIT" ], - "description": "PHP-Parser extensions for PHPStan", + "description": "Nette Framework class reflection extension for PHPStan", "support": { - "issues": "https://github.com/phpstan/phpstan-php-parser/issues", - "source": "https://github.com/phpstan/phpstan-php-parser/tree/1.1.0" + "issues": "https://github.com/phpstan/phpstan-nette/issues", + "source": "https://github.com/phpstan/phpstan-nette/tree/2.0.0" }, - "time": "2021-12-16T19:43:32+00:00" + "time": "2024-10-26T16:03:48+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.0.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3" + "reference": "3cc855474263ad6220dfa49167cbea34ca1dd300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", - "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/3cc855474263ad6220dfa49167cbea34ca1dd300", + "reference": "3cc855474263ad6220dfa49167cbea34ca1dd300", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.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": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon", @@ -5364,39 +4826,36 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.0.0" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.0" }, - "time": "2021-10-14T08:03:54+00:00" + "time": "2024-10-14T03:16:27+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.1.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "e12d55f74a8cca18c6e684c6450767e055ba7717" + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/e12d55f74a8cca18c6e684c6450767e055ba7717", - "reference": "e12d55f74a8cca18c6e684c6450767e055ba7717", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.2.0" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "rules.neon" @@ -5415,50 +4874,50 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.1.0" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.0" }, - "time": "2021-11-18T09:30:29+00:00" + "time": "2024-10-26T16:04:33+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.10", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d5850aaf931743067f4bfc1ae4cbd06468400687", - "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -5486,7 +4945,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.10" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -5494,7 +4954,7 @@ "type": "github" } ], - "time": "2021-12-05T09:12:13+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5739,55 +5199,50 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.7", + "version": "9.6.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d0dc8b6999c937616df4fb046792004b33fd31c5" + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0dc8b6999c937616df4fb046792004b33fd31c5", - "reference": "d0dc8b6999c937616df4fb046792004b33fd31c5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.1", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.13.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.3", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3.4", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -5795,15 +5250,15 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { - "classmap": [ - "src/" - ], "files": [ "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5826,92 +5281,45 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.7" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" }, "funding": [ { - "url": "https://phpunit.de/donate.html", + "url": "https://phpunit.de/sponsors.html", "type": "custom" }, { "url": "https://github.com/sebastianbergmann", "type": "github" - } - ], - "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" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-01-26T12:02:35+00:00" + "time": "2025-05-02T06:40:34+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -5946,7 +5354,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -5954,7 +5362,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -6069,16 +5477,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -6131,7 +5539,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -6139,24 +5547,24 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -6188,7 +5596,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -6196,20 +5604,20 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -6254,7 +5662,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -6262,20 +5670,20 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -6317,7 +5725,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -6325,20 +5733,20 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -6394,7 +5802,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -6402,20 +5810,20 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.3", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "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/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -6458,7 +5866,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.7" }, "funding": [ { @@ -6466,24 +5874,24 @@ "type": "github" } ], - "time": "2021-06-11T13:31:12+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -6515,7 +5923,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -6523,7 +5931,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -6639,16 +6047,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -6687,10 +6095,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -6698,20 +6106,20 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -6723,7 +6131,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -6744,8 +6152,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -6753,32 +6160,32 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", - "version": "2.3.4", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -6801,7 +6208,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -6809,7 +6216,7 @@ "type": "github" } ], - "time": "2021-06-15T12:49:02+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", @@ -6865,335 +6272,178 @@ "time": "2020-09-28T06:39:44+00:00" }, { - "name": "seld/jsonlint", - "version": "1.8.3", + "name": "shipmonk/composer-dependency-analyser", + "version": "1.7.0", "source": { "type": "git", - "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57" + "url": "https://github.com/shipmonk-rnd/composer-dependency-analyser.git", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57", + "url": "https://api.github.com/repos/shipmonk-rnd/composer-dependency-analyser/zipball/bca862b2830a453734aee048eb0cdab82e5c9da3", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3", "shasum": "" }, "require": { - "php": "^5.3 || ^7.0 || ^8.0" + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "editorconfig-checker/editorconfig-checker": "^10.3.0", + "ergebnis/composer-normalize": "^2.19", + "ext-dom": "*", + "ext-libxml": "*", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.10.63", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.2.3", + "phpunit/phpunit": "^8.5.28 || ^9.5.20", + "shipmonk/name-collision-detector": "^2.0.0", + "slevomat/coding-standard": "^8.0.1" }, "bin": [ - "bin/jsonlint" + "bin/composer-dependency-analyser" ], "type": "library", "autoload": { "psr-4": { - "Seld\\JsonLint\\": "src/Seld/JsonLint/" + "ShipMonk\\ComposerDependencyAnalyser\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be/" - } - ], - "description": "JSON Linter", + "description": "Fast detection of composer dependency issues (dead dependencies, shadow dependencies, misplaced dependencies)", "keywords": [ - "json", - "linter", - "parser", - "validator" + "analyser", + "composer", + "composer dependency", + "dead code", + "dead dependency", + "detector", + "dev", + "misplaced dependency", + "shadow dependency", + "static analysis", + "unused code", + "unused dependency" ], "support": { - "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3" + "issues": "https://github.com/shipmonk-rnd/composer-dependency-analyser/issues", + "source": "https://github.com/shipmonk-rnd/composer-dependency-analyser/tree/1.7.0" }, - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", - "type": "tidelift" - } - ], - "time": "2020-11-11T09:19:24+00:00" + "time": "2024-08-08T08:12:32+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.2.0", + "name": "shipmonk/name-collision-detector", + "version": "2.1.1", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" + "url": "https://github.com/shipmonk-rnd/name-collision-detector.git", + "reference": "e8c8267a9a3774450b64f4cbf0bb035108e78f07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "url": "https://api.github.com/repos/shipmonk-rnd/name-collision-detector/zipball/e8c8267a9a3774450b64f4cbf0bb035108e78f07", + "reference": "e8c8267a9a3774450b64f4cbf0bb035108e78f07", "shasum": "" }, "require": { - "ext-dom": "*", + "ext-json": "*", "ext-tokenizer": "*", - "ext-xmlwriter": "*", + "nette/schema": "^1.1.0", "php": "^7.2 || ^8.0" }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/master" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2020-07-12T23:59:07+00:00" - }, - { - "name": "vaimo/composer-patches", - "version": "4.22.4", - "source": { - "type": "git", - "url": "https://github.com/vaimo/composer-patches.git", - "reference": "3da4cdf03fb4dc8d92b3d435de183f6044d679d6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vaimo/composer-patches/zipball/3da4cdf03fb4dc8d92b3d435de183f6044d679d6", - "reference": "3da4cdf03fb4dc8d92b3d435de183f6044d679d6", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "drupol/phposinfo": "^1.6", - "ext-json": "*", - "php": ">=5.3.0", - "seld/jsonlint": "^1.7.1", - "vaimo/topological-sort": "^1.0" - }, "require-dev": { - "composer/composer": "^1.0 || ^2.0", - "phpcompatibility/php-compatibility": ">=9.1.1", - "phpmd/phpmd": ">=2.6.0", - "sebastian/phpcpd": ">=1.4.3", - "squizlabs/php_codesniffer": ">=2.9.2", - "vaimo/composer-changelogs": "^0.17.0", - "vaimo/composer-patches-proxy": "1.0.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Vaimo\\ComposerPatches\\Plugin", - "changelog": { - "source": "changelog.json", - "output": { - "md": "CHANGELOG.md" - } - } + "editorconfig-checker/editorconfig-checker": "^10.3.0", + "ergebnis/composer-normalize": "^2.19", + "phpstan/phpstan": "^1.8.7", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.2.3", + "phpunit/phpunit": "^8.5.28 || ^9.5.20", + "shipmonk/composer-dependency-analyser": "^1.0.0", + "slevomat/coding-standard": "^8.0.1" }, + "bin": [ + "bin/detect-collisions" + ], + "type": "library", "autoload": { "psr-4": { - "Vaimo\\ComposerPatches\\": "src" + "ShipMonk\\NameCollision\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Allan Paiste", - "email": "allan.paiste@vaimo.com" - } - ], - "description": "Applies a patch from a local or remote file to any package that is part of a given composer project. Patches can be defined both on project and on package level. Optional support for patch versioning, sequencing, custom patch applier configuration and patch command for testing/troubleshooting added patches.", + "description": "Simple tool to find ambiguous classes or any other name duplicates within your project.", "keywords": [ - "Fixes", - "back-ports", - "backports", - "bulk patches", - "bundled patches", - "composer command", - "composer plugin", - "configurable patch applier", - "development patches", - "downloaded patches", - "environment flags", - "hot-fixes", - "hotfixes", - "indirect restrictions", - "maintenance", - "maintenance tools", - "multi-version patches", - "multiple formats", - "os-specific config", - "package bug-fix", - "package patches", - "patch branching", - "patch command", - "patch description", - "patch exclusion", - "patch header", - "patch meta-data", - "patch resolve", - "patch search", - "patch skipping", - "patcher", - "patching", - "plugin", - "remote patch files", - "resolve patches", - "skipped packages", - "tools", - "utilities", - "utility", - "utils", - "version restriction" + "ambiguous", + "autoload", + "autoloading", + "classname", + "collision", + "namespace" ], "support": { - "docs": "https://github.com/vaimo/composer-patches", - "issues": "https://github.com/vaimo/composer-patches/issues", - "source": "https://github.com/vaimo/composer-patches" + "issues": "https://github.com/shipmonk-rnd/name-collision-detector/issues", + "source": "https://github.com/shipmonk-rnd/name-collision-detector/tree/2.1.1" }, - "time": "2021-02-25T11:24:50+00:00" + "time": "2024-03-01T13:26:32+00:00" }, { - "name": "vaimo/topological-sort", - "version": "1.0.0", + "name": "theseer/tokenizer", + "version": "1.2.3", "source": { "type": "git", - "url": "https://github.com/vaimo/topological-sort.git", - "reference": "e19b93df2bac0e995ecd4b982ec4ea2fb1131e64" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vaimo/topological-sort/zipball/e19b93df2bac0e995ecd4b982ec4ea2fb1131e64", - "reference": "e19b93df2bac0e995ecd4b982ec4ea2fb1131e64", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { - "php": ">=5.3" - }, - "require-dev": { - "codeclimate/php-test-reporter": "dev-master", - "phpcompatibility/php-compatibility": "^9.1.1", - "phpmd/phpmd": "^2.6.0", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "^2.9.2", - "symfony/console": "~2.5 || ~3.0 || ~4.0" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { - "psr-4": { - "Vaimo\\TopSort\\": "src/", - "Vaimo\\TopSort\\Tests\\": "tests/Tests/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Marc J. Schmidt", - "email": "marc@marcjschmidt.de" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "High-Performance TopSort/Dependency resolving algorithm (compatibility version to work with 5.3)", - "keywords": [ - "dependency resolving", - "topological sort", - "topsort" - ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "source": "https://github.com/vaimo/topological-sort/tree/1.0.0" - }, - "time": "2019-04-13T14:15:06+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "url": "https://github.com/theseer", + "type": "github" } ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], @@ -7204,11 +6454,12 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.0" + "php": "^8.1", + "composer-runtime-api": "^2.0" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { - "php": "8.0.99" + "php": "8.1.99" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 5af3a763db..22487e357c 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,7 +1,8 @@ parameters: featureToggles: bleedingEdge: true - skipCheckGenericClasses: [] - explicitMixedInUnknownGenericNew: true - stubFiles: - - ../stubs/bleedingEdge/Countable.stub + checkParameterCastableToNumberFunctions: true + skipCheckGenericClasses!: [] + stricterFunctionMap: true + reportPreciseLineForUnusedFunctionParameter: true + internalTag: true diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 9b911c7b1f..24b19d99bf 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -1,76 +1,137 @@ parameters: customRulesetUsed: false -conditionalTags: - PHPStan\Rules\Properties\UninitializedPropertyRule: - phpstan.rules.rule: %checkUninitializedProperties% - rules: + - PHPStan\Rules\Api\ApiInstanceofRule + - PHPStan\Rules\Api\ApiInstanceofTypeRule - PHPStan\Rules\Api\ApiInstantiationRule + - PHPStan\Rules\Api\ApiClassConstFetchRule - 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\GetTemplateTypeRule + - PHPStan\Rules\Api\NodeConnectingVisitorAttributesRule + - PHPStan\Rules\Api\OldPhpParser4ClassRule - PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule + - PHPStan\Rules\Api\RuntimeReflectionInstantiationRule + - PHPStan\Rules\Api\RuntimeReflectionFunctionRule - PHPStan\Rules\Arrays\DuplicateKeysInLiteralArraysRule - - PHPStan\Rules\Arrays\EmptyArrayItemRule - PHPStan\Rules\Arrays\OffsetAccessWithoutDimForReadingRule - PHPStan\Rules\Cast\UnsetCastRule + - PHPStan\Rules\Classes\AllowedSubTypesRule - PHPStan\Rules\Classes\ClassAttributesRule - PHPStan\Rules\Classes\ClassConstantAttributesRule - PHPStan\Rules\Classes\ClassConstantRule - PHPStan\Rules\Classes\DuplicateDeclarationRule - PHPStan\Rules\Classes\EnumSanityRule - - PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule - - PHPStan\Rules\Classes\ExistingClassesInEnumImplementsRule - - PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule - - PHPStan\Rules\Classes\ExistingClassInTraitUseRule - - PHPStan\Rules\Classes\InstantiationRule - PHPStan\Rules\Classes\InstantiationCallableRule - PHPStan\Rules\Classes\InvalidPromotedPropertiesRule + - PHPStan\Rules\Classes\LocalTypeAliasesRule + - PHPStan\Rules\Classes\LocalTypeTraitUseAliasesRule + - PHPStan\Rules\Classes\LocalTypeTraitAliasesRule - PHPStan\Rules\Classes\NewStaticRule - PHPStan\Rules\Classes\NonClassAttributeClassRule + - PHPStan\Rules\Classes\ReadOnlyClassRule - PHPStan\Rules\Classes\TraitAttributeClassRule + - PHPStan\Rules\Constants\ClassAsClassConstantRule + - PHPStan\Rules\Constants\DynamicClassConstantFetchRule - PHPStan\Rules\Constants\FinalConstantRule + - PHPStan\Rules\Constants\MagicConstantContextRule + - PHPStan\Rules\Constants\NativeTypedClassConstantRule + - PHPStan\Rules\Constants\FinalPrivateConstantRule - PHPStan\Rules\EnumCases\EnumCaseAttributesRule + - PHPStan\Rules\Exceptions\NoncapturingCatchRule - PHPStan\Rules\Exceptions\ThrowExpressionRule - PHPStan\Rules\Functions\ArrowFunctionAttributesRule - PHPStan\Rules\Functions\ArrowFunctionReturnNullsafeByRefRule - - PHPStan\Rules\Functions\CallToFunctionParametersRule - PHPStan\Rules\Functions\ClosureAttributesRule - PHPStan\Rules\Functions\DefineParametersRule - PHPStan\Rules\Functions\ExistingClassesInArrowFunctionTypehintsRule + - PHPStan\Rules\Functions\CallToFunctionParametersRule - PHPStan\Rules\Functions\ExistingClassesInClosureTypehintsRule - PHPStan\Rules\Functions\ExistingClassesInTypehintsRule - PHPStan\Rules\Functions\FunctionAttributesRule - PHPStan\Rules\Functions\InnerFunctionRule + - PHPStan\Rules\Functions\InvalidLexicalVariablesInClosureUseRule - PHPStan\Rules\Functions\ParamAttributesRule + - PHPStan\Rules\Functions\PrintfArrayParametersRule - PHPStan\Rules\Functions\PrintfParametersRule + - PHPStan\Rules\Functions\RedefinedParametersRule - PHPStan\Rules\Functions\ReturnNullsafeByRefRule + - PHPStan\Rules\Ignore\IgnoreParseErrorRule + - PHPStan\Rules\Functions\VariadicParametersDeclarationRule - PHPStan\Rules\Keywords\ContinueBreakInLoopRule + - PHPStan\Rules\Keywords\DeclareStrictTypesRule - PHPStan\Rules\Methods\AbstractMethodInNonAbstractClassRule + - PHPStan\Rules\Methods\AbstractPrivateMethodRule - PHPStan\Rules\Methods\CallMethodsRule - PHPStan\Rules\Methods\CallStaticMethodsRule + - PHPStan\Rules\Methods\ConsistentConstructorRule + - PHPStan\Rules\Methods\ConstructorReturnTypeRule - PHPStan\Rules\Methods\ExistingClassesInTypehintsRule + - PHPStan\Rules\Methods\FinalPrivateMethodRule - PHPStan\Rules\Methods\MethodCallableRule + - PHPStan\Rules\Methods\MethodVisibilityInInterfaceRule + - PHPStan\Rules\Methods\MissingMagicSerializationMethodsRule - PHPStan\Rules\Methods\MissingMethodImplementationRule - PHPStan\Rules\Methods\MethodAttributesRule - PHPStan\Rules\Methods\StaticMethodCallableRule + - PHPStan\Rules\Names\UsedNamesRule - PHPStan\Rules\Operators\InvalidAssignVarRule + - PHPStan\Rules\Operators\InvalidIncDecOperationRule - PHPStan\Rules\Properties\AccessPropertiesInAssignRule - PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule + - PHPStan\Rules\Properties\ExistingClassesInPropertyHookTypehintsRule + - PHPStan\Rules\Properties\InvalidCallablePropertyTypeRule + - PHPStan\Rules\Properties\MissingReadOnlyPropertyAssignRule + - PHPStan\Rules\Properties\MissingReadOnlyByPhpDocPropertyAssignRule + - PHPStan\Rules\Properties\PropertiesInInterfaceRule + - PHPStan\Rules\Properties\PropertyAssignRefRule - PHPStan\Rules\Properties\PropertyAttributesRule + - PHPStan\Rules\Properties\PropertyHookAttributesRule + - PHPStan\Rules\Properties\PropertyInClassRule - PHPStan\Rules\Properties\ReadOnlyPropertyRule + - PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyRule + - PHPStan\Rules\Regexp\RegularExpressionPatternRule + - PHPStan\Rules\Traits\ConflictingTraitConstantsRule + - PHPStan\Rules\Traits\ConstantsInTraitsRule + - PHPStan\Rules\Traits\TraitAttributesRule + - PHPStan\Rules\Types\InvalidTypesInUnionRule - PHPStan\Rules\Variables\UnsetRule - PHPStan\Rules\Whitespace\FileWhitespaceRule +conditionalTags: + PHPStan\Rules\InternalTag\RestrictedInternalClassConstantUsageExtension: + phpstan.restrictedClassConstantUsageExtension: %featureToggles.internalTag% + PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension: + phpstan.restrictedClassNameUsageExtension: %featureToggles.internalTag% + PHPStan\Rules\InternalTag\RestrictedInternalFunctionUsageExtension: + phpstan.restrictedFunctionUsageExtension: %featureToggles.internalTag% + services: - class: PHPStan\Rules\Classes\ExistingClassInClassExtendsRule tags: - phpstan.rules.rule + arguments: + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule + tags: + - phpstan.rules.rule + arguments: + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\Classes\ExistingClassesInEnumImplementsRule + tags: + - phpstan.rules.rule + arguments: + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\Classes\ExistingClassInInstanceOfRule @@ -78,6 +139,28 @@ services: - phpstan.rules.rule arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule + tags: + - phpstan.rules.rule + arguments: + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\Classes\ExistingClassInTraitUseRule + tags: + - phpstan.rules.rule + arguments: + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\Classes\InstantiationRule + tags: + - phpstan.rules.rule + arguments: + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\Exceptions\CaughtExceptionExistenceRule @@ -85,6 +168,7 @@ services: - phpstan.rules.rule arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\Functions\CallToNonExistentFunctionRule @@ -92,6 +176,7 @@ services: - phpstan.rules.rule arguments: checkFunctionNameCase: %checkFunctionNameCase% + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\Constants\OverridingConstantRule @@ -104,9 +189,11 @@ services: class: PHPStan\Rules\Methods\OverridingMethodRule arguments: checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% + checkMissingOverrideMethodAttribute: %checkMissingOverrideMethodAttribute% tags: - phpstan.rules.rule + - class: PHPStan\Rules\Missing\MissingReturnRule arguments: @@ -121,6 +208,7 @@ services: - phpstan.rules.rule arguments: checkFunctionNameCase: %checkFunctionNameCase% + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\Namespaces\ExistingNamesInUseRule @@ -128,25 +216,19 @@ services: - phpstan.rules.rule arguments: checkFunctionNameCase: %checkFunctionNameCase% - - - - class: PHPStan\Rules\Operators\InvalidIncDecOperationRule - tags: - - phpstan.rules.rule - arguments: - checkThisOnly: %checkThisOnly% + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\Properties\AccessPropertiesRule tags: - phpstan.rules.rule - arguments: - reportMagicProperties: %reportMagicProperties% - class: PHPStan\Rules\Properties\AccessStaticPropertiesRule tags: - phpstan.rules.rule + arguments: + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\Properties\ExistingClassesInPropertiesRule @@ -155,6 +237,7 @@ services: arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% checkThisOnly: %checkThisOnly% + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\Functions\FunctionCallableRule @@ -164,13 +247,6 @@ services: tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Properties\MissingReadOnlyPropertyAssignRule - arguments: - additionalConstructors: %additionalConstructors% - tags: - - phpstan.rules.rule - - class: PHPStan\Rules\Properties\OverridingPropertyRule arguments: @@ -180,9 +256,12 @@ services: - phpstan.rules.rule - - class: PHPStan\Rules\Properties\UninitializedPropertyRule + class: PHPStan\Rules\Properties\SetPropertyHookParameterRule arguments: - additionalConstructors: %additionalConstructors% + checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% + checkMissingTypehints: %checkMissingTypehints% + tags: + - phpstan.rules.rule - class: PHPStan\Rules\Properties\WritingToReadOnlyPropertiesRule @@ -214,13 +293,17 @@ services: - phpstan.rules.rule - - class: PHPStan\Rules\Regexp\RegularExpressionPatternRule + class: PHPStan\Rules\Keywords\RequireFileExistsRule + arguments: + currentWorkingDirectory: %currentWorkingDirectory% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Classes\LocalTypeAliasesRule - arguments: - globalTypeAliases: %typeAliases% - tags: - - phpstan.rules.rule + class: PHPStan\Rules\InternalTag\RestrictedInternalClassConstantUsageExtension + + - + class: PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension + + - + class: PHPStan\Rules\InternalTag\RestrictedInternalFunctionUsageExtension diff --git a/conf/config.level1.neon b/conf/config.level1.neon index 3b5f68d64a..a1e3872d6b 100644 --- a/conf/config.level1.neon +++ b/conf/config.level1.neon @@ -9,8 +9,15 @@ parameters: rules: - PHPStan\Rules\Classes\UnusedConstructorParametersRule - - PHPStan\Rules\Constants\ConstantRule - PHPStan\Rules\Functions\UnusedClosureUsesRule - PHPStan\Rules\Variables\EmptyRule - PHPStan\Rules\Variables\IssetRule - PHPStan\Rules\Variables\NullCoalesceRule + +services: + - + class: PHPStan\Rules\Constants\ConstantRule + tags: + - phpstan.rules.rule + arguments: + discoveringSymbolsTip: %tips.discoveringSymbols% diff --git a/conf/config.level10.neon b/conf/config.level10.neon new file mode 100644 index 0000000000..5d052692c9 --- /dev/null +++ b/conf/config.level10.neon @@ -0,0 +1,5 @@ +includes: + - config.level9.neon + +parameters: + checkImplicitMixed: true diff --git a/conf/config.level2.neon b/conf/config.level2.neon index becc7b5b02..51214ea554 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -12,8 +12,19 @@ rules: - PHPStan\Rules\Cast\InvalidPartOfEncapsedStringRule - PHPStan\Rules\Cast\PrintRule - PHPStan\Rules\Classes\AccessPrivateConstantThroughStaticRule + - PHPStan\Rules\Classes\MethodTagRule + - PHPStan\Rules\Classes\MethodTagTraitRule + - PHPStan\Rules\Classes\MethodTagTraitUseRule + - PHPStan\Rules\Classes\PropertyTagRule + - PHPStan\Rules\Classes\PropertyTagTraitRule + - PHPStan\Rules\Classes\PropertyTagTraitUseRule + - PHPStan\Rules\Classes\MixinTraitRule + - PHPStan\Rules\Classes\MixinTraitUseRule - PHPStan\Rules\Comparison\UsageOfVoidMatchExpressionRule + - PHPStan\Rules\Constants\ValueAssignedToClassConstantRule - PHPStan\Rules\Functions\IncompatibleDefaultParameterTypeRule + - PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule + - PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule - PHPStan\Rules\Generics\ClassAncestorsRule - PHPStan\Rules\Generics\ClassTemplateTypeRule - PHPStan\Rules\Generics\EnumAncestorsRule @@ -23,40 +34,91 @@ rules: - PHPStan\Rules\Generics\InterfaceAncestorsRule - PHPStan\Rules\Generics\InterfaceTemplateTypeRule - PHPStan\Rules\Generics\MethodTemplateTypeRule + - PHPStan\Rules\Generics\MethodTagTemplateTypeRule + - PHPStan\Rules\Generics\MethodTagTemplateTypeTraitRule - PHPStan\Rules\Generics\MethodSignatureVarianceRule + - PHPStan\Rules\Generics\PropertyVarianceRule - 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\Operators\InvalidUnaryOperationRule + - PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule + - PHPStan\Rules\PhpDoc\MethodConditionalReturnTypeRule + - PHPStan\Rules\PhpDoc\FunctionAssertRule + - PHPStan\Rules\PhpDoc\MethodAssertRule + - PHPStan\Rules\PhpDoc\IncompatibleSelfOutTypeRule - PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule + - PHPStan\Rules\PhpDoc\IncompatiblePropertyHookPhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule - - PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule - - PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule - PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule + - PHPStan\Rules\PhpDoc\IncompatibleParamImmediatelyInvokedCallableRule + - PHPStan\Rules\PhpDoc\VarTagChangedExpressionTypeRule - PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule - PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule + - PHPStan\Rules\Classes\RequireImplementsRule + - PHPStan\Rules\Classes\RequireExtendsRule + - PHPStan\Rules\PhpDoc\RequireImplementsDefinitionClassRule + - PHPStan\Rules\PhpDoc\RequireExtendsDefinitionClassRule + - PHPStan\Rules\PhpDoc\RequireExtendsDefinitionTraitRule + - PHPStan\Rules\Pure\PureFunctionRule + - PHPStan\Rules\Pure\PureMethodRule + +conditionalTags: + PHPStan\Rules\InternalTag\RestrictedInternalPropertyUsageExtension: + phpstan.restrictedPropertyUsageExtension: %featureToggles.internalTag% + PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension: + phpstan.restrictedMethodUsageExtension: %featureToggles.internalTag% services: - class: PHPStan\Rules\Classes\MixinRule + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\PhpDoc\RequireExtendsCheck + arguments: + checkClassCaseSensitivity: %checkClassCaseSensitivity% + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\PhpDoc\RequireImplementsDefinitionTraitRule arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% + discoveringSymbolsTip: %tips.discoveringSymbols% tags: - phpstan.rules.rule + - class: PHPStan\Rules\Functions\CallCallablesRule arguments: reportMaybes: %reportMaybes% tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule + tags: + - phpstan.rules.rule - class: PHPStan\Rules\PhpDoc\InvalidPhpDocVarTagTypeRule arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% checkMissingVarTagTypehint: %checkMissingVarTagTypehint% + discoveringSymbolsTip: %tips.discoveringSymbols% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\InternalTag\RestrictedInternalPropertyUsageExtension + + - + class: PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension diff --git a/conf/config.level3.neon b/conf/config.level3.neon index 51450cddef..4e5f80c5ef 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -3,21 +3,28 @@ includes: rules: - PHPStan\Rules\Arrays\ArrayDestructuringRule + - PHPStan\Rules\Arrays\ArrayUnpackingRule - PHPStan\Rules\Arrays\IterableInForeachRule - PHPStan\Rules\Arrays\OffsetAccessAssignmentRule - PHPStan\Rules\Arrays\OffsetAccessAssignOpRule - PHPStan\Rules\Arrays\OffsetAccessValueAssignmentRule - PHPStan\Rules\Arrays\UnpackIterableInArrayRule + - PHPStan\Rules\Exceptions\ThrowExprTypeRule - PHPStan\Rules\Functions\ArrowFunctionReturnTypeRule - PHPStan\Rules\Functions\ClosureReturnTypeRule - PHPStan\Rules\Functions\ReturnTypeRule - PHPStan\Rules\Generators\YieldTypeRule - PHPStan\Rules\Methods\ReturnTypeRule - PHPStan\Rules\Properties\DefaultValueTypesAssignedToPropertiesRule + - PHPStan\Rules\Properties\GetNonVirtualPropertyHookReadRule - PHPStan\Rules\Properties\ReadOnlyPropertyAssignRule + - PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRule - PHPStan\Rules\Properties\ReadOnlyPropertyAssignRefRule + - PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRefRule + - PHPStan\Rules\Properties\SetNonVirtualPropertyHookAssignRule - PHPStan\Rules\Properties\TypesAssignedToPropertiesRule - - PHPStan\Rules\Variables\ThrowTypeRule + - PHPStan\Rules\Variables\ParameterOutAssignedTypeRule + - PHPStan\Rules\Variables\ParameterOutExecutionEndTypeRule - PHPStan\Rules\Variables\VariableCloningRule parameters: @@ -61,6 +68,14 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Exceptions\ThrowsVoidPropertyHookWithExplicitThrowPointRule + arguments: + exceptionTypeResolver: @exceptionTypeResolver + missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows% + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\Generators\YieldFromTypeRule arguments: diff --git a/conf/config.level4.neon b/conf/config.level4.neon index 61f32bd2f7..b026238cfb 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -3,22 +3,36 @@ includes: rules: - PHPStan\Rules\Arrays\DeadForeachRule - - PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule + - PHPStan\Rules\DeadCode\CallToConstructorStatementWithoutImpurePointsRule + - PHPStan\Rules\DeadCode\CallToFunctionStatementWithoutImpurePointsRule + - PHPStan\Rules\DeadCode\CallToMethodStatementWithoutImpurePointsRule + - PHPStan\Rules\DeadCode\CallToStaticMethodStatementWithoutImpurePointsRule - PHPStan\Rules\DeadCode\NoopRule - PHPStan\Rules\DeadCode\UnreachableStatementRule - PHPStan\Rules\DeadCode\UnusedPrivateConstantRule - PHPStan\Rules\DeadCode\UnusedPrivateMethodRule - - PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule - PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule - PHPStan\Rules\Functions\CallToFunctionStatementWithoutSideEffectsRule + - PHPStan\Rules\Functions\UselessFunctionReturnValueRule - PHPStan\Rules\Methods\CallToConstructorStatementWithoutSideEffectsRule - 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 + - PHPStan\Rules\TooWideTypehints\TooWideFunctionParameterOutTypeRule + - PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule + - PHPStan\Rules\TooWideTypehints\TooWidePropertyTypeRule + - PHPStan\Rules\Traits\NotAnalysedTraitRule + +conditionalTags: + PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule: + phpstan.rules.rule: %exceptions.check.tooWideThrowType% + PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule: + phpstan.rules.rule: %exceptions.check.tooWideThrowType% + PHPStan\Rules\Exceptions\TooWidePropertyHookThrowTypeRule: + phpstan.rules.rule: %exceptions.check.tooWideThrowType% parameters: checkAdvancedIsset: true @@ -27,8 +41,9 @@ services: - class: PHPStan\Rules\Classes\ImpossibleInstanceOfRule arguments: - checkAlwaysTrueInstanceof: %checkAlwaysTrueInstanceof% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -36,6 +51,8 @@ services: class: PHPStan\Rules\Comparison\BooleanAndConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -43,6 +60,8 @@ services: class: PHPStan\Rules\Comparison\BooleanOrConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -50,9 +69,46 @@ services: class: PHPStan\Rules\Comparison\BooleanNotConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector + tags: + - phpstan.collector + + - + class: PHPStan\Rules\DeadCode\PossiblyPureNewCollector + tags: + - phpstan.collector + + - + class: PHPStan\Rules\DeadCode\FunctionWithoutImpurePointsCollector + tags: + - phpstan.collector + + - + class: PHPStan\Rules\DeadCode\PossiblyPureFuncCallCollector + tags: + - phpstan.collector + + - + class: PHPStan\Rules\DeadCode\MethodWithoutImpurePointsCollector + tags: + - phpstan.collector + + - + class: PHPStan\Rules\DeadCode\PossiblyPureMethodCallCollector + tags: + - phpstan.collector + + - + class: PHPStan\Rules\DeadCode\PossiblyPureStaticCallCollector + tags: + - phpstan.collector + - class: PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule arguments: @@ -66,6 +122,7 @@ services: class: PHPStan\Rules\Comparison\DoWhileLoopConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -73,6 +130,8 @@ services: class: PHPStan\Rules\Comparison\ElseIfConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -80,65 +139,85 @@ services: class: PHPStan\Rules\Comparison\IfConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeFunctionCallRule arguments: - checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule arguments: - checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule arguments: - checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Comparison\LogicalXorConstantConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - class: PHPStan\Rules\Comparison\MatchExpressionRule arguments: - checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule + class: PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule arguments: - checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule + class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Comparison\UnreachableIfBranchesRule + class: PHPStan\Rules\Comparison\ConstantLooseComparisonRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Comparison\UnreachableTernaryElseBranchRule + class: PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -146,6 +225,7 @@ services: class: PHPStan\Rules\Comparison\WhileLoopAlwaysFalseConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -153,12 +233,45 @@ services: class: PHPStan\Rules\Comparison\WhileLoopAlwaysTrueConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule + + - + class: PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule + + - + class: PHPStan\Rules\Exceptions\TooWidePropertyHookThrowTypeRule + - class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule arguments: checkProtectedAndPublicMethods: %checkTooWideReturnTypesInProtectedAndPublicMethods% tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\Properties\NullsafePropertyFetchRule + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Traits\TraitDeclarationCollector + tags: + - phpstan.collector + + - + class: PHPStan\Rules\Traits\TraitUseCollector + tags: + - phpstan.collector + + - + class: PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule + arguments: + exceptionTypeResolver: @exceptionTypeResolver + reportUncheckedExceptionDeadCatch: %exceptions.reportUncheckedExceptionDeadCatch% + tags: + - phpstan.rules.rule diff --git a/conf/config.level5.neon b/conf/config.level5.neon index c890be88ec..fd3835fbf1 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -5,9 +5,17 @@ parameters: checkFunctionArgumentTypes: true checkArgumentsPassedByReference: true +conditionalTags: + PHPStan\Rules\Functions\ParameterCastableToNumberRule: + phpstan.rules.rule: %featureToggles.checkParameterCastableToNumberFunctions% + rules: - PHPStan\Rules\DateTimeInstantiationRule - - PHPStan\Rules\Functions\ImplodeFunctionRule + - PHPStan\Rules\Functions\CallUserFuncRule + - PHPStan\Rules\Functions\ParameterCastableToStringRule + - PHPStan\Rules\Functions\ImplodeParameterCastableToStringRule + - PHPStan\Rules\Functions\SortParameterCastableToStringRule + - PHPStan\Rules\Regexp\RegularExpressionQuotingRule services: - @@ -16,3 +24,21 @@ services: reportMaybes: %reportMaybes% tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\Functions\ArrayFilterRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Functions\ArrayValuesRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule + - + class: PHPStan\Rules\Functions\ParameterCastableToNumberRule diff --git a/conf/config.level6.neon b/conf/config.level6.neon index 05f3616832..25214e97dd 100644 --- a/conf/config.level6.neon +++ b/conf/config.level6.neon @@ -2,8 +2,6 @@ includes: - config.level5.neon parameters: - checkGenericClassInNonGenericObjectType: true - checkMissingIterableValueType: true checkMissingVarTagTypehint: true checkMissingTypehints: true @@ -13,4 +11,5 @@ rules: - PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule - PHPStan\Rules\Methods\MissingMethodParameterTypehintRule - PHPStan\Rules\Methods\MissingMethodReturnTypehintRule + - PHPStan\Rules\Methods\MissingMethodSelfOutTypeRule - PHPStan\Rules\Properties\MissingPropertyTypehintRule diff --git a/conf/config.levelmax.neon b/conf/config.levelmax.neon index da48578fe3..ce4c43f2f7 100644 --- a/conf/config.levelmax.neon +++ b/conf/config.levelmax.neon @@ -1,2 +1,2 @@ includes: - - config.level9.neon + - config.level10.neon diff --git a/conf/config.neon b/conf/config.neon index 0b86120409..7a4a43a4de 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1,46 +1,42 @@ includes: - - config.stubFiles.neon + - parametersSchema.neon + parameters: bootstrapFiles: - ../stubs/runtime/ReflectionUnionType.php - ../stubs/runtime/ReflectionAttribute.php - ../stubs/runtime/Attribute.php - ../stubs/runtime/ReflectionIntersectionType.php - excludes_analyse: [] - excludePaths: null + excludePaths: [] level: null paths: [] exceptions: implicitThrows: true + reportUncheckedExceptionDeadCatch: true uncheckedExceptionRegexes: [] uncheckedExceptionClasses: [] checkedExceptionRegexes: [] checkedExceptionClasses: [] check: missingCheckedExceptionInThrows: false - tooWideThrowType: false + tooWideThrowType: true featureToggles: bleedingEdge: false - disableRuntimeReflectionProvider: false - skipCheckGenericClasses: - - DatePeriod - - CallbackFilterIterator - - FilterIterator - - RecursiveCallbackFilterIterator - explicitMixedInUnknownGenericNew: false + checkParameterCastableToNumberFunctions: false + skipCheckGenericClasses: [] + stricterFunctionMap: false + reportPreciseLineForUnusedFunctionParameter: false + internalTag: false fileExtensions: - php checkAdvancedIsset: false - checkAlwaysTrueCheckTypeFunctionCall: false - checkAlwaysTrueInstanceof: false - checkAlwaysTrueStrictComparison: false + reportAlwaysTrueInLastCondition: false checkClassCaseSensitivity: false checkExplicitMixed: false + checkImplicitMixed: false checkFunctionArgumentTypes: false checkFunctionNameCase: false - checkGenericClassInNonGenericObjectType: false checkInternalClassCaseSensitivity: false - checkMissingIterableValueType: false checkMissingCallableSignature: false checkMissingVarTagTypehint: false checkArgumentsPassedByReference: false @@ -48,6 +44,7 @@ parameters: checkNullables: false checkThisOnly: true checkUnionTypes: false + checkBenevolentUnionTypes: false checkExplicitMixedMissingReturn: false checkPhpDocMissingReturn: false checkPhpDocMethodSignatures: false @@ -55,11 +52,19 @@ parameters: checkMissingTypehints: false checkTooWideReturnTypesInProtectedAndPublicMethods: false checkUninitializedProperties: false + checkDynamicProperties: false + strictRulesInstalled: false + deprecationRulesInstalled: false inferPrivatePropertyTypeFromConstructor: false reportMaybes: false reportMaybesInMethodSignatures: false reportMaybesInPropertyPhpDocTypes: false reportStaticMethodSignatures: false + reportWrongPhpDocTypeInVarTag: false + reportAnyTypeWideningInVarTag: false + reportPossiblyNonexistentGeneralArrayOffset: false + reportPossiblyNonexistentConstantArrayOffset: false + checkMissingOverrideMethodAttribute: false mixinExcludeClasses: [] scanFiles: [] scanDirectories: [] @@ -72,54 +77,67 @@ parameters: phpVersion: null polluteScopeWithLoopInitialAssignments: true polluteScopeWithAlwaysIterableForeach: true + polluteScopeWithBlock: true propertyAlwaysWrittenTags: [] propertyAlwaysReadTags: [] additionalConstructors: [] treatPhpDocTypesAsCertain: true + usePathConstantsAsConstantString: false + rememberPossiblyImpureFunctionValues: true + tips: + discoveringSymbols: true + treatPhpDocTypesAsCertain: true tipsOfTheDay: true reportMagicMethods: false reportMagicProperties: false ignoreErrors: [] internalErrorsCountLimit: 50 cache: - nodesByFileCountMax: 1024 - nodesByStringCountMax: 1024 + nodesByStringCountMax: 256 reportUnmatchedIgnoredErrors: true - scopeClass: PHPStan\Analyser\MutatingScope typeAliases: [] universalObjectCratesClasses: - stdClass + stubFiles: + - ../stubs/ReflectionAttribute.stub + - ../stubs/ReflectionClassConstant.stub + - ../stubs/ReflectionFunctionAbstract.stub + - ../stubs/ReflectionMethod.stub + - ../stubs/ReflectionParameter.stub + - ../stubs/ReflectionProperty.stub + - ../stubs/iterable.stub + - ../stubs/ArrayObject.stub + - ../stubs/WeakReference.stub + - ../stubs/ext-ds.stub + - ../stubs/ImagickPixel.stub + - ../stubs/PDOStatement.stub + - ../stubs/date.stub + - ../stubs/ibm_db2.stub + - ../stubs/mysqli.stub + - ../stubs/zip.stub + - ../stubs/dom.stub + - ../stubs/spl.stub + - ../stubs/SplObjectStorage.stub + - ../stubs/Exception.stub + - ../stubs/arrayFunctions.stub + - ../stubs/core.stub + - ../stubs/typeCheckingFunctions.stub + - ../stubs/Countable.stub earlyTerminatingMethodCalls: [] earlyTerminatingFunctionCalls: [] - memoryLimitFile: %tmpDir%/.memory_limit - tempResultCachePath: %tmpDir%/resultCaches resultCachePath: %tmpDir%/resultCache.php + resultCacheSkipIfOlderThanDays: 7 resultCacheChecksProjectExtensionFilesDependencies: false - staticReflectionClassNamePatterns: - - '#^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 - LIBXML_DOTTED_VERSION + - Memcached::HAVE_ENCODING + - Memcached::HAVE_IGBINARY + - Memcached::HAVE_JSON + - Memcached::HAVE_MSGPACK + - Memcached::HAVE_SASL + - Memcached::HAVE_SESSION - PHP_VERSION - PHP_MAJOR_VERSION - PHP_MINOR_VERSION @@ -162,178 +180,63 @@ parameters: - OPENSSL_VERSION_NUMBER - ZEND_DEBUG_BUILD - ZEND_THREAD_SAFE + - E_ALL # different on PHP 8.4 customRulesetUsed: null editorUrl: null + editorUrlTitle: null + errorFormat: null + sysGetTempDir: ::sys_get_temp_dir() + sourceLocatorPlaygroundMode: false + pro: + dnsServers: + - '1.1.1.2' + tmpDir: %sysGetTempDir%/phpstan-fixer __validate: true + parametersNotInvalidatingCache: + - [parameters, editorUrl] + - [parameters, editorUrlTitle] + - [parameters, errorFormat] + - [parameters, ignoreErrors] + - [parameters, reportUnmatchedIgnoredErrors] + - [parameters, tipsOfTheDay] + - [parameters, parallel] + - [parameters, internalErrorsCountLimit] + - [parameters, cache] + - [parameters, memoryLimitFile] + - [parameters, pro] + - parametersSchema extensions: rules: PHPStan\DependencyInjection\RulesExtension conditionalTags: PHPStan\DependencyInjection\ConditionalTagsExtension parametersSchema: PHPStan\DependencyInjection\ParametersSchemaExtension validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension - -parametersSchema: - bootstrapFiles: listOf(string()) - excludes_analyse: listOf(string()) - excludePaths: schema(anyOf( - structure([ - analyse: listOf(string()), - ]), - structure([ - analyseAndScan: listOf(string()), - ]) - structure([ - analyse: listOf(string()), - analyseAndScan: listOf(string()) - ]) - ), nullable()) - level: schema(anyOf(int(), string()), nullable()) - paths: listOf(string()) - exceptions: structure([ - implicitThrows: bool(), - uncheckedExceptionRegexes: listOf(string()), - uncheckedExceptionClasses: listOf(string()), - checkedExceptionRegexes: listOf(string()), - checkedExceptionClasses: listOf(string()), - check: structure([ - missingCheckedExceptionInThrows: bool(), - tooWideThrowType: bool() - ]) - ]) - featureToggles: structure([ - bleedingEdge: bool(), - disableRuntimeReflectionProvider: bool(), - skipCheckGenericClasses: listOf(string()), - explicitMixedInUnknownGenericNew: bool(), - ]) - fileExtensions: listOf(string()) - checkAdvancedIsset: bool() - checkAlwaysTrueCheckTypeFunctionCall: bool() - checkAlwaysTrueInstanceof: bool() - checkAlwaysTrueStrictComparison: bool() - checkClassCaseSensitivity: bool() - checkExplicitMixed: bool() - checkFunctionArgumentTypes: bool() - checkFunctionNameCase: bool() - checkGenericClassInNonGenericObjectType: bool() - checkInternalClassCaseSensitivity: bool() - checkMissingIterableValueType: bool() - checkMissingCallableSignature: bool() - checkMissingVarTagTypehint: bool() - checkArgumentsPassedByReference: bool() - checkMaybeUndefinedVariables: bool() - checkNullables: bool() - checkThisOnly: bool() - checkUnionTypes: bool() - checkExplicitMixedMissingReturn: bool() - checkPhpDocMissingReturn: bool() - checkPhpDocMethodSignatures: bool() - checkExtraArguments: bool() - checkMissingTypehints: bool() - checkTooWideReturnTypesInProtectedAndPublicMethods: bool() - checkUninitializedProperties: bool() - inferPrivatePropertyTypeFromConstructor: bool() - - tipsOfTheDay: bool() - reportMaybes: bool() - reportMaybesInMethodSignatures: bool() - reportMaybesInPropertyPhpDocTypes: bool() - reportStaticMethodSignatures: bool() - parallel: structure([ - jobSize: int(), - processTimeout: float(), - maximumNumberOfProcesses: int(), - minimumNumberOfJobsPerProcess: int(), - buffer: int() - ]) - phpVersion: schema(anyOf(schema(int(), min(70100), max(80199))), nullable()) - polluteScopeWithLoopInitialAssignments: bool() - polluteScopeWithAlwaysIterableForeach: bool() - propertyAlwaysWrittenTags: listOf(string()) - propertyAlwaysReadTags: listOf(string()) - additionalConstructors: listOf(string()) - treatPhpDocTypesAsCertain: bool() - reportMagicMethods: bool() - reportMagicProperties: bool() - ignoreErrors: listOf( - anyOf( - string(), - structure([ - message: string() - path: string() - ]), - structure([ - message: string() - count: int() - path: string() - ]), - structure([ - message: string() - paths: listOf(string()) - ]) - ) - ) - internalErrorsCountLimit: int() - cache: structure([ - nodesByFileCountMax: int() - nodesByStringCountMax: int() - ]) - reportUnmatchedIgnoredErrors: bool() - scopeClass: string() - typeAliases: arrayOf(string()) - universalObjectCratesClasses: listOf(string()) - stubFiles: listOf(string()) - earlyTerminatingMethodCalls: arrayOf(listOf(string())) - earlyTerminatingFunctionCalls: listOf(string()) - memoryLimitFile: string() - tempResultCachePath: string() - resultCachePath: string() - resultCacheChecksProjectExtensionFilesDependencies: bool() - staticReflectionClassNamePatterns: listOf(string()) - dynamicConstantNames: listOf(string()) - customRulesetUsed: schema(bool(), nullable()) - rootDir: string() - tmpDir: string() - currentWorkingDirectory: string() - cliArgumentsVariablesRegistered: bool() - mixinExcludeClasses: listOf(string()) - scanFiles: listOf(string()) - scanDirectories: listOf(string()) - fixerTmpDir: string() - editorUrl: schema(string(), nullable()) - - # irrelevant Nette parameters - debugMode: bool() - productionMode: bool() - tempDir: string() - __validate: bool() - - # internal parameters only for DerivativeContainerFactory - additionalConfigFiles: arrayOf(string()) - generateBaselineFile: schema(string(), nullable()) - analysedPaths: listOf(string()) - composerAutoloaderProjectPaths: listOf(string()) - analysedPathsFromConfig: listOf(string()) - usedLevel: string() - cliAutoloadFile: schema(string(), nullable()) - - # internal - static reflection - singleReflectionFile: schema(string(), nullable()) - singleReflectionInsteadOfFile: schema(string(), nullable()) + validateExcludePaths: PHPStan\DependencyInjection\ValidateExcludePathsExtension rules: + - PHPStan\Rules\Debug\DebugScopeRule + - PHPStan\Rules\Debug\DumpPhpDocTypeRule - PHPStan\Rules\Debug\DumpTypeRule - PHPStan\Rules\Debug\FileAssertRule + - PHPStan\Rules\RestrictedUsage\RestrictedClassConstantUsageRule + - PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageRule + - PHPStan\Rules\RestrictedUsage\RestrictedFunctionCallableUsageRule + - PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageRule + - PHPStan\Rules\RestrictedUsage\RestrictedMethodCallableUsageRule + - PHPStan\Rules\RestrictedUsage\RestrictedPropertyUsageRule + - PHPStan\Rules\RestrictedUsage\RestrictedStaticMethodUsageRule + - PHPStan\Rules\RestrictedUsage\RestrictedStaticMethodCallableUsageRule + - PHPStan\Rules\RestrictedUsage\RestrictedStaticPropertyUsageRule conditionalTags: PHPStan\Rules\Exceptions\MissingCheckedExceptionInFunctionThrowsRule: phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows% PHPStan\Rules\Exceptions\MissingCheckedExceptionInMethodThrowsRule: phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows% - PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule: - phpstan.rules.rule: %exceptions.check.tooWideThrowType% - PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule: - phpstan.rules.rule: %exceptions.check.tooWideThrowType% + PHPStan\Rules\Exceptions\MissingCheckedExceptionInPropertyHookThrowsRule: + phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows% + PHPStan\Rules\Properties\UninitializedPropertyRule: + phpstan.rules.rule: %checkUninitializedProperties% services: - @@ -349,10 +252,107 @@ services: preserveOriginalNames: true - - class: PhpParser\NodeVisitor\NodeConnectingVisitor + class: PHPStan\Parser\AnonymousClassVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ArrayFilterArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ArrayFindArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ArrayMapArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ArrayWalkArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ClosureArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ClosureBindToVarVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ClosureBindArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\CurlSetOptArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ArrowFunctionArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\MagicConstantParamDefaultVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\NewAssignedToPropertyVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ParentStmtTypesVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\StandaloneThrowExprVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\TryCatchTypeVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\LastConditionVisitor + tags: + - phpstan.parser.richParserNodeVisitor - - class: PhpParser\PrettyPrinter\Standard + class: PHPStan\Parser\TypeTraverserInstanceofVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\VariadicMethodsVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\VariadicFunctionsVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Node\Printer\ExprPrinter + + - + class: PHPStan\Node\Printer\Printer + autowired: + - PHPStan\Node\Printer\Printer - class: PHPStan\Broker\AnonymousClassNameHelper @@ -370,9 +370,20 @@ services: - class: PHPStan\Php\PhpVersionFactoryFactory arguments: - versionId: %phpVersion% + phpVersion: %phpVersion% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + - + class: PHPStan\Php\ComposerPhpVersionFactory + arguments: + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + + - + class: PHPStan\PhpDocParser\ParserConfig + arguments: + usedAttributes: + lines: true + - class: PHPStan\PhpDocParser\Lexer\Lexer @@ -385,6 +396,9 @@ services: - class: PHPStan\PhpDocParser\Parser\PhpDocParser + - + class: PHPStan\PhpDocParser\Printer\Printer + - class: PHPStan\PhpDoc\PhpDocInheritanceResolver @@ -410,29 +424,74 @@ services: - class: PHPStan\PhpDoc\StubValidator + - + class: PHPStan\PhpDoc\SocketSelectStubFilesExtension + tags: + - phpstan.stubFilesExtension + + - + class: PHPStan\PhpDoc\DefaultStubFilesProvider + arguments: + stubFiles: %stubFiles% + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + autowired: + - PHPStan\PhpDoc\StubFilesProvider + + - + class: PHPStan\PhpDoc\JsonValidateStubFilesExtension + tags: + - phpstan.stubFilesExtension + + - + class: PHPStan\PhpDoc\ReflectionClassStubFilesExtension + tags: + - phpstan.stubFilesExtension + + - + class: PHPStan\PhpDoc\ReflectionEnumStubFilesExtension + tags: + - phpstan.stubFilesExtension + - class: PHPStan\Analyser\Analyser arguments: internalErrorsCountLimit: %internalErrorsCountLimit% + - + class: PHPStan\Analyser\AnalyserResultFinalizer + arguments: + reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% + - class: PHPStan\Analyser\FileAnalyser arguments: parser: @defaultAnalysisParser - reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% - - class: PHPStan\Analyser\IgnoredErrorHelper + class: PHPStan\Analyser\IgnoreErrorExtensionProvider + + - + class: PHPStan\Analyser\LocalIgnoresProcessor + + - + class: PHPStan\Analyser\RuleErrorTransformer + + - + class: PHPStan\Analyser\Ignore\IgnoredErrorHelper arguments: ignoreErrors: %ignoreErrors% reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% - - class: PHPStan\Analyser\LazyScopeFactory - arguments: - scopeClass: %scopeClass% + class: PHPStan\Analyser\Ignore\IgnoreLexer + + - + class: PHPStan\Analyser\LazyInternalScopeFactory autowired: - - PHPStan\Analyser\ScopeFactory + - PHPStan\Analyser\InternalScopeFactory + + - + class: PHPStan\Analyser\ScopeFactory - class: PHPStan\Analyser\NodeScopeResolver @@ -441,41 +500,59 @@ services: reflector: @nodeScopeResolverReflector polluteScopeWithLoopInitialAssignments: %polluteScopeWithLoopInitialAssignments% polluteScopeWithAlwaysIterableForeach: %polluteScopeWithAlwaysIterableForeach% + polluteScopeWithBlock: %polluteScopeWithBlock% earlyTerminatingMethodCalls: %earlyTerminatingMethodCalls% earlyTerminatingFunctionCalls: %earlyTerminatingFunctionCalls% implicitThrows: %exceptions.implicitThrows% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + universalObjectCratesClasses: %universalObjectCratesClasses% + narrowMethodScopeFromConstructor: true + + - + class: PHPStan\Analyser\ConstantResolver + factory: @PHPStan\Analyser\ConstantResolverFactory::create() + + - + class: PHPStan\Analyser\ConstantResolverFactory - implement: PHPStan\Analyser\ResultCache\ResultCacheManagerFactory arguments: scanFileFinder: @fileFinderScan cacheFilePath: %resultCachePath% - tempResultCachePath: %tempResultCachePath% analysedPaths: %analysedPaths% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - stubFiles: %stubFiles% usedLevel: %usedLevel% cliAutoloadFile: %cliAutoloadFile% bootstrapFiles: %bootstrapFiles% scanFiles: %scanFiles% scanDirectories: %scanDirectories% checkDependenciesOfProjectExtensionFiles: %resultCacheChecksProjectExtensionFilesDependencies% + parametersNotInvalidatingCache: %parametersNotInvalidatingCache% + skipResultCacheIfOlderThanDays: %resultCacheSkipIfOlderThanDays% - class: PHPStan\Analyser\ResultCache\ResultCacheClearer arguments: cacheFilePath: %resultCachePath% - tempResultCachePath: %tempResultCachePath% + + - + class: PHPStan\Analyser\RicherScopeGetTypeHelper - class: PHPStan\Cache\Cache arguments: storage: @cacheStorage + - + class: PHPStan\Collectors\Registry + factory: @PHPStan\Collectors\RegistryFactory::create + + - + class: PHPStan\Collectors\RegistryFactory + - class: PHPStan\Command\AnalyseApplication - arguments: - internalErrorsCountLimit: %internalErrorsCountLimit% - class: PHPStan\Command\AnalyserRunner @@ -485,8 +562,14 @@ services: arguments: analysedPaths: %analysedPaths% currentWorkingDirectory: %currentWorkingDirectory% - fixerTmpDir: %fixerTmpDir% - maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% + proTmpDir: %pro.tmpDir% + dnsServers: %pro.dnsServers% + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + allConfigFiles: %allConfigFiles% + cliAutoloadFile: %cliAutoloadFile% + bootstrapFiles: %bootstrapFiles% + editorUrl: %editorUrl% + usedLevel: %usedLevel% - class: PHPStan\Dependency\DependencyResolver @@ -525,8 +608,6 @@ services: usedLevel: %usedLevel% generateBaselineFile: %generateBaselineFile% cliAutoloadFile: %cliAutoloadFile% - singleReflectionFile: %singleReflectionFile% - singleReflectionInsteadOfFile: %singleReflectionInsteadOfFile% - class: PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider @@ -536,6 +617,14 @@ services: class: PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider factory: PHPStan\DependencyInjection\Type\LazyDynamicReturnTypeExtensionRegistryProvider + - + class: PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider + factory: PHPStan\DependencyInjection\Type\LazyParameterOutTypeExtensionProvider + + - + class: PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider + factory: PHPStan\DependencyInjection\Type\LazyExpressionTypeResolverExtensionRegistryProvider + - class: PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider factory: PHPStan\DependencyInjection\Type\LazyOperatorTypeSpecifyingExtensionRegistryProvider @@ -544,6 +633,10 @@ services: class: PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider factory: PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider + - + class: PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider + factory: PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider + - class: PHPStan\File\FileHelper arguments: @@ -552,13 +645,10 @@ services: - class: PHPStan\File\FileExcluderFactory arguments: - obsoleteExcludesAnalyse: %excludes_analyse% excludePaths: %excludePaths% - implement: PHPStan\File\FileExcluderRawFactory - arguments: - stubFiles: %stubFiles% fileExcluderAnalyse: class: PHPStan\File\FileExcluder @@ -590,7 +680,14 @@ services: fileFinder: @fileFinderAnalyse - - class: PHPStan\NodeVisitor\StatementOrderVisitor + class: PHPStan\Parser\DeclarePositionVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ImmediatelyInvokedClosureVisitor + tags: + - phpstan.parser.richParserNodeVisitor - class: PHPStan\Parallel\ParallelAnalyser @@ -605,18 +702,25 @@ services: jobSize: %parallel.jobSize% maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% minimumNumberOfJobsPerProcess: %parallel.minimumNumberOfJobsPerProcess% + tags: + - phpstan.diagnoseExtension - - class: PHPStan\Parser\FunctionCallStatementFinder + class: PHPStan\Process\CpuCoreCounter - - class: PHPStan\Process\CpuCoreCounter + class: PHPStan\Reflection\AttributeReflectionFactory - implement: PHPStan\Reflection\FunctionReflectionFactory arguments: parser: @defaultAnalysisParser + - + class: PHPStan\Reflection\InitializerExprTypeResolver + arguments: + usePathConstantsAsConstantString: %usePathConstantsAsConstantString% + - class: PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension @@ -631,11 +735,6 @@ services: arguments: parser: @defaultAnalysisParser - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator - arguments: - disableRuntimeReflectionProvider: %featureToggles.disableRuntimeReflectionProvider% - - class: PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker @@ -657,16 +756,42 @@ services: class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository - - class: PHPStan\Reflection\Mixin\MixinMethodsClassReflectionExtension + class: PHPStan\Reflection\BetterReflection\Type\AdapterReflectionEnumCaseDynamicReturnTypeExtension + arguments: + class: PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase tags: - - phpstan.broker.methodsClassReflectionExtension + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Reflection\BetterReflection\Type\AdapterReflectionEnumCaseDynamicReturnTypeExtension + arguments: + class: PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumUnitCase + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Reflection\BetterReflection\Type\AdapterReflectionEnumDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Reflection\ConstructorsHelper + arguments: + additionalConstructors: %additionalConstructors% + + - + class: PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension + + - + class: PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension + + - + class: PHPStan\Reflection\Mixin\MixinMethodsClassReflectionExtension arguments: mixinExcludeClasses: %mixinExcludeClasses% - class: PHPStan\Reflection\Mixin\MixinPropertiesClassReflectionExtension - tags: - - phpstan.broker.propertiesClassReflectionExtension arguments: mixinExcludeClasses: %mixinExcludeClasses% @@ -675,7 +800,6 @@ services: arguments: parser: @defaultAnalysisParser inferPrivatePropertyTypeFromConstructor: %inferPrivatePropertyTypeFromConstructor% - universalObjectCratesClasses: %universalObjectCratesClasses% - implement: PHPStan\Reflection\Php\PhpMethodReflectionFactory @@ -687,6 +811,11 @@ services: tags: - phpstan.broker.methodsClassReflectionExtension + - + class: PHPStan\Reflection\Php\EnumAllowedSubTypesClassReflectionExtension + tags: + - phpstan.broker.allowedSubTypesClassReflectionExtension + - class: PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension tags: @@ -694,6 +823,22 @@ services: arguments: classes: %universalObjectCratesClasses% + - + class: PHPStan\Reflection\PHPStan\NativeReflectionEnumReturnDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + arguments: + className: PHPStan\Reflection\ClassReflection + methodName: getNativeReflection + + - + class: PHPStan\Reflection\PHPStan\NativeReflectionEnumReturnDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + arguments: + className: PHPStan\Reflection\Php\BuiltinMethodReflection + methodName: getDeclaringClass + - class: PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider factory: PHPStan\Reflection\ReflectionProvider\LazyReflectionProviderProvider @@ -708,6 +853,8 @@ services: - class: PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider + arguments: + stricterFunctionMap: %featureToggles.stricterFunctionMap% autowired: - PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider @@ -728,17 +875,56 @@ services: - class: PHPStan\Rules\AttributesCheck + arguments: + deprecationRulesInstalled: %deprecationRulesInstalled% - class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchCheck arguments: reportMaybes: %reportMaybes% + reportPossiblyNonexistentGeneralArrayOffset: %reportPossiblyNonexistentGeneralArrayOffset% + reportPossiblyNonexistentConstantArrayOffset: %reportPossiblyNonexistentConstantArrayOffset% + + - + class: PHPStan\Rules\ClassNameCheck - class: PHPStan\Rules\ClassCaseSensitivityCheck arguments: checkInternalClassCaseSensitivity: %checkInternalClassCaseSensitivity% + - + class: PHPStan\Rules\ClassForbiddenNameCheck + + - + class: PHPStan\Rules\Classes\LocalTypeAliasesCheck + arguments: + globalTypeAliases: %typeAliases% + checkMissingTypehints: %checkMissingTypehints% + checkClassCaseSensitivity: %checkClassCaseSensitivity% + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\Classes\MethodTagCheck + arguments: + checkClassCaseSensitivity: %checkClassCaseSensitivity% + checkMissingTypehints: %checkMissingTypehints% + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\Classes\MixinCheck + arguments: + checkClassCaseSensitivity: %checkClassCaseSensitivity% + checkMissingTypehints: %checkMissingTypehints% + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\Classes\PropertyTagCheck + arguments: + checkClassCaseSensitivity: %checkClassCaseSensitivity% + checkMissingTypehints: %checkMissingTypehints% + discoveringSymbolsTip: %tips.discoveringSymbols% + - class: PHPStan\Rules\Comparison\ConstantConditionRuleHelper arguments: @@ -766,19 +952,18 @@ services: - class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInMethodThrowsRule + - + class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInPropertyHookThrowsRule + - class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInThrowsCheck arguments: exceptionTypeResolver: @exceptionTypeResolver - - - class: PHPStan\Rules\Exceptions\TooWideFunctionThrowTypeRule - - - - class: PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule - - class: PHPStan\Rules\Exceptions\TooWideThrowTypeCheck + arguments: + implicitThrows: %exceptions.implicitThrows% - class: PHPStan\Rules\FunctionCallParametersCheck @@ -796,6 +981,8 @@ services: - class: PHPStan\Rules\FunctionReturnTypeCheck + - + class: PHPStan\Rules\ParameterCastableToStringCheck - class: PHPStan\Rules\Generics\CrossCheckInterfacesHelper @@ -803,12 +990,15 @@ services: - class: PHPStan\Rules\Generics\GenericAncestorsCheck arguments: - checkGenericClassInNonGenericObjectType: %checkGenericClassInNonGenericObjectType% skipCheckGenericClasses: %featureToggles.skipCheckGenericClasses% + checkMissingTypehints: %checkMissingTypehints% - class: PHPStan\Rules\Generics\GenericObjectTypeCheck + - + class: PHPStan\Rules\Generics\MethodTagTemplateTypeCheck + - class: PHPStan\Rules\Generics\TemplateTypeCheck arguments: @@ -817,6 +1007,9 @@ services: - class: PHPStan\Rules\Generics\VarianceCheck + - + class: PHPStan\Rules\InternalTag\RestrictedInternalUsageHelper + - class: PHPStan\Rules\IssetCheck arguments: @@ -833,6 +1026,7 @@ services: class: PHPStan\Rules\Methods\StaticMethodCallCheck arguments: checkFunctionNameCase: %checkFunctionNameCase% + discoveringSymbolsTip: %tips.discoveringSymbols% reportMagicMethods: %reportMagicMethods% - @@ -842,11 +1036,15 @@ services: reportMaybes: %reportMaybesInMethodSignatures% reportStatic: %reportStaticMethodSignatures% + - + class: PHPStan\Rules\Methods\MethodParameterComparisonHelper + + - + class: PHPStan\Rules\Methods\MethodVisibilityComparisonHelper + - class: PHPStan\Rules\MissingTypehintCheck arguments: - checkMissingIterableValueType: %checkMissingIterableValueType% - checkGenericClassInNonGenericObjectType: %checkGenericClassInNonGenericObjectType% checkMissingCallableSignature: %checkMissingCallableSignature% skipCheckGenericClasses: %featureToggles.skipCheckGenericClasses% @@ -856,9 +1054,49 @@ services: - class: PHPStan\Rules\Constants\LazyAlwaysUsedClassConstantsExtensionProvider + - + class: PHPStan\Rules\Methods\LazyAlwaysUsedMethodExtensionProvider + + - + class: PHPStan\Rules\PhpDoc\ConditionalReturnTypeRuleHelper + + - + class: PHPStan\Rules\PhpDoc\AssertRuleHelper + arguments: + checkMissingTypehints: %checkMissingTypehints% + checkClassCaseSensitivity: %checkClassCaseSensitivity% + - class: PHPStan\Rules\PhpDoc\UnresolvableTypeHelper + - + class: PHPStan\Rules\PhpDoc\GenericCallableRuleHelper + + - + class: PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeCheck + + - + class: PHPStan\Rules\PhpDoc\VarTagTypeRuleHelper + arguments: + checkTypeAgainstPhpDocType: %reportWrongPhpDocTypeInVarTag% + strictWideningCheck: %reportAnyTypeWideningInVarTag% + + - + class: PHPStan\Rules\Playground\NeverRuleHelper + + - + class: PHPStan\Rules\Properties\AccessPropertiesCheck + arguments: + reportMagicProperties: %reportMagicProperties% + checkDynamicProperties: %checkDynamicProperties% + - + class: PHPStan\Type\Php\BcMathNumberOperatorTypeSpecifyingExtension + tags: + - phpstan.broker.operatorTypeSpecifyingExtension + + - + class: PHPStan\Rules\Properties\UninitializedPropertyRule + - class: PHPStan\Rules\Properties\LazyReadWritePropertiesExtensionProvider @@ -869,7 +1107,7 @@ services: class: PHPStan\Rules\Properties\PropertyReflectionFinder - - class: PHPStan\Rules\RegistryFactory + class: PHPStan\Rules\Pure\FunctionPurityCheck - class: PHPStan\Rules\RuleLevelHelper @@ -878,15 +1116,29 @@ services: checkThisOnly: %checkThisOnly% checkUnionTypes: %checkUnionTypes% checkExplicitMixed: %checkExplicitMixed% + checkImplicitMixed: %checkImplicitMixed% + checkBenevolentUnionTypes: %checkBenevolentUnionTypes% + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\UnusedFunctionParametersCheck + arguments: + reportExactLine: %featureToggles.reportPreciseLineForUnusedFunctionParameter% + + - + class: PHPStan\Rules\TooWideTypehints\TooWideParameterOutTypeCheck - class: PHPStan\Type\FileTypeMapper arguments: phpParser: @defaultAnalysisParser + stubFileTypeMapper: + class: PHPStan\Type\FileTypeMapper + arguments: + phpParser: @stubParser + autowired: false + - class: PHPStan\Type\TypeAliasResolver factory: PHPStan\Type\UsefulTypeAliasResolver @@ -898,7 +1150,30 @@ services: factory: PHPStan\Type\LazyTypeAliasResolverProvider - - class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension + class: PHPStan\Type\BitwiseFlagHelper + + - + class: PHPStan\Type\Php\AbsFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayChangeKeyCaseFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayChunkFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension @@ -928,7 +1203,10 @@ services: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeReturnTypeExtension + class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeHelper + + - + class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension @@ -937,6 +1215,16 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayFindFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayFindKeyFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayKeyDynamicReturnTypeExtension tags: @@ -992,6 +1280,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayReplaceFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayReverseFunctionReturnTypeExtension tags: @@ -1017,6 +1310,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArraySearchFunctionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\ArrayValuesFunctionDynamicReturnTypeExtension tags: @@ -1027,6 +1325,16 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\AssertThrowTypeExtension + tags: + - phpstan.dynamicFunctionThrowTypeExtension + + - + class: PHPStan\Type\Php\BackedEnumFromMethodDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - class: PHPStan\Type\Php\Base64DecodeDynamicFunctionReturnTypeExtension tags: @@ -1059,21 +1367,37 @@ services: arguments: checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables% + - + class: PHPStan\Type\Php\ConstantFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ConstantHelper + - class: PHPStan\Type\Php\CountFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\CountCharsFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\CountFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\CurlInitReturnTypeExtension + class: PHPStan\Type\Php\CurlGetinfoFunctionDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\DateFunctionReturnTypeHelper + - class: PHPStan\Type\Php\DateFormatFunctionReturnTypeExtension tags: @@ -1094,21 +1418,65 @@ services: tags: - phpstan.dynamicStaticMethodThrowTypeExtension + - + class: PHPStan\Type\Php\DateIntervalDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\DateTimeCreateDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\DateTimeDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\DateTimeModifyReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + arguments: + dateTimeClass: DateTime + + - + class: PHPStan\Type\Php\DateTimeModifyReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + arguments: + dateTimeClass: DateTimeImmutable + - class: PHPStan\Type\Php\DateTimeConstructorThrowTypeExtension tags: - phpstan.dynamicStaticMethodThrowTypeExtension + - + class: PHPStan\Type\Php\DateTimeModifyMethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + + - + class: PHPStan\Type\Php\DateTimeSubMethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + + - + class: PHPStan\Type\Php\DateTimeZoneConstructorThrowTypeExtension + tags: + - phpstan.dynamicStaticMethodThrowTypeExtension + - class: PHPStan\Type\Php\DsMapDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Php\DsMapDynamicMethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + - class: PHPStan\Type\Php\DioStatDynamicFunctionReturnTypeExtension tags: @@ -1119,11 +1487,24 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\FilterFunctionReturnTypeHelper + + - + class: PHPStan\Type\Php\FilterInputDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\FilterVarArrayDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GetCalledClassDynamicReturnTypeExtension tags: @@ -1134,11 +1515,26 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\GetDebugTypeFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\GetDefinedVarsFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\GettypeFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GettimeofdayDynamicFunctionReturnTypeExtension tags: @@ -1148,16 +1544,60 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\HighlightStringDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\IntdivThrowTypeExtension tags: - phpstan.dynamicFunctionThrowTypeExtension + - + class: PHPStan\Type\Php\IniGetReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\JsonThrowTypeExtension tags: - phpstan.dynamicFunctionThrowTypeExtension + - + class: PHPStan\Type\Php\OpenSslEncryptParameterOutTypeExtension + tags: + - phpstan.functionParameterOutTypeExtension + + - + class: PHPStan\Type\Php\ParseStrParameterOutTypeExtension + tags: + - phpstan.functionParameterOutTypeExtension + + - + class: PHPStan\Type\Php\PregMatchTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + + - + class: PHPStan\Type\Php\PregMatchParameterOutTypeExtension + tags: + - phpstan.functionParameterOutTypeExtension + + - + class: PHPStan\Type\Php\PregReplaceCallbackClosureTypeExtension + tags: + - phpstan.functionParameterClosureTypeExtension + + - + class: PHPStan\Type\Php\RegexArrayShapeMatcher + + - + class: PHPStan\Type\Regex\RegexGroupParser + + - + class: PHPStan\Type\Regex\RegexExpressionHelper + - class: PHPStan\Type\Php\ReflectionClassConstructorThrowTypeExtension tags: @@ -1178,6 +1618,11 @@ services: tags: - phpstan.dynamicStaticMethodThrowTypeExtension + - + class: PHPStan\Type\Php\StrContainingTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension tags: @@ -1245,7 +1690,7 @@ services: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\VarExportFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\LtrimFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension @@ -1264,6 +1709,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\MbStrlenFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\MicrotimeFunctionReturnTypeExtension tags: @@ -1284,11 +1734,26 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\SetTypeFunctionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + + - + class: PHPStan\Type\Php\StrCaseFunctionsReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\StrlenFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\StrIncrementDecrementFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\StrPadFunctionReturnTypeExtension tags: @@ -1319,8 +1784,15 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\TrimFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\VersionCompareFunctionDynamicReturnTypeExtension + arguments: + configPhpVersion: %phpVersion% tags: - phpstan.broker.dynamicFunctionReturnTypeExtension @@ -1359,6 +1831,11 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: PHPStan\Type\Php\ClassImplementsFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\DefineConstantTypeSpecifyingExtension tags: @@ -1379,56 +1856,21 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - class: PHPStan\Type\Php\IsIntFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsFloatFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsNullFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsArrayFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - class: PHPStan\Type\Php\IsBoolFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsCallableFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - class: PHPStan\Type\Php\IsCountableFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsResourceFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsIterableFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - class: PHPStan\Type\Php\IsStringFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsSubclassOfFunctionTypeSpecifyingExtension tags: @@ -1439,21 +1881,6 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - - class: PHPStan\Type\Php\IsObjectFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsNumericFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsScalarFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingExtension tags: @@ -1463,7 +1890,7 @@ services: class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingHelper - - class: PHPStan\Type\Php\ArrayIsListFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\CtypeDigitFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension @@ -1505,6 +1932,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\SscanfFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\StrvalFamilyFunctionReturnTypeExtension tags: @@ -1561,6 +1993,21 @@ services: tags: - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Type\PHPStan\ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\ClosureTypeFactory + arguments: + parser: @currentPhpVersionPhpParser + + - + class: PHPStan\Type\Constant\OversizedArrayBuilder + + - + class: PHPStan\Rules\Functions\PrintfHelper + exceptionTypeResolver: class: PHPStan\Rules\Exceptions\ExceptionTypeResolver factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver @@ -1593,15 +2040,6 @@ services: parentDirectory: %currentWorkingDirectory% autowired: false - broker: - class: PHPStan\Broker\Broker - factory: @brokerFactory::create - autowired: - - PHPStan\Broker\Broker - - brokerFactory: - class: PHPStan\Broker\BrokerFactory - cacheStorage: class: PHPStan\Cache\FileCacheStorage arguments: @@ -1612,7 +2050,6 @@ services: class: PHPStan\Parser\RichParser arguments: parser: @currentPhpVersionPhpParser - lexer: @currentPhpVersionLexer autowired: no currentPhpVersionSimpleParser: @@ -1646,29 +2083,32 @@ services: autowired: false currentPhpVersionPhpParser: - class: PhpParser\Parser\Php7 + factory: @currentPhpVersionPhpParserFactory::create() + autowired: false + + currentPhpVersionPhpParserFactory: + class: PHPStan\Parser\PhpParserFactory arguments: lexer: @currentPhpVersionLexer autowired: false registry: - class: PHPStan\Rules\Registry - factory: @PHPStan\Rules\RegistryFactory::create + class: PHPStan\Rules\LazyRegistry + autowired: + - PHPStan\Rules\Registry stubPhpDocProvider: class: PHPStan\PhpDoc\StubPhpDocProvider arguments: - parser: @defaultAnalysisParser - stubFiles: %stubFiles% + parser: @stubParser + fileTypeMapper: @stubFileTypeMapper # Reflection providers reflectionProviderFactory: class: PHPStan\Reflection\ReflectionProvider\ReflectionProviderFactory arguments: - runtimeReflectionProvider: @runtimeReflectionProvider staticReflectionProvider: @betterReflectionProvider - disableRuntimeReflectionProvider: %featureToggles.disableRuntimeReflectionProvider% reflectionProvider: factory: @PHPStan\Reflection\ReflectionProvider\ReflectionProviderFactory::create @@ -1691,27 +2131,6 @@ services: reflector: @originalBetterReflectionReflector autowired: false - # deprecated - betterReflectionClassReflector: - class: PHPStan\BetterReflection\Reflector\ClassReflector - arguments: - sourceLocator: @betterReflectionSourceLocator - autowired: false - - # deprecated - betterReflectionFunctionReflector: - class: PHPStan\BetterReflection\Reflector\FunctionReflector - arguments: - sourceLocator: @betterReflectionSourceLocator - autowired: false - - # deprecated - betterReflectionConstantReflector: - class: PHPStan\BetterReflection\Reflector\ConstantReflector - arguments: - sourceLocator: @betterReflectionSourceLocator - autowired: false - nodeScopeResolverReflector: factory: @betterReflectionReflector autowired: false @@ -1720,19 +2139,9 @@ services: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: reflector: @betterReflectionReflector + universalObjectCratesClasses: %universalObjectCratesClasses% autowired: false - runtimeReflectionProvider: - class: PHPStan\Reflection\ReflectionProvider\ClassBlacklistReflectionProvider - arguments: - reflectionProvider: @innerRuntimeReflectionProvider - patterns: %staticReflectionClassNamePatterns% - singleReflectionInsteadOfFile: %singleReflectionInsteadOfFile% - autowired: false - - innerRuntimeReflectionProvider: - class: PHPStan\Reflection\Runtime\RuntimeReflectionProvider - - class: PHPStan\Reflection\BetterReflection\BetterReflectionSourceLocatorFactory arguments: @@ -1743,11 +2152,12 @@ services: analysedPaths: %analysedPaths% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% analysedPathsFromConfig: %analysedPathsFromConfig% - singleReflectionFile: %singleReflectionFile% - staticReflectionClassNamePatterns: %staticReflectionClassNamePatterns% + playgroundMode: %sourceLocatorPlaygroundMode% - implement: PHPStan\Reflection\BetterReflection\BetterReflectionProviderFactory + arguments: + universalObjectCratesClasses: %universalObjectCratesClasses% - class: PHPStan\Reflection\BetterReflection\SourceStubber\PhpStormStubsSourceStubberFactory @@ -1760,16 +2170,23 @@ services: - PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber - - class: PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber + factory: @PHPStan\Reflection\BetterReflection\SourceStubber\ReflectionSourceStubberFactory::create() autowired: - PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber + - + class: PHPStan\Reflection\BetterReflection\SourceStubber\ReflectionSourceStubberFactory + + - + class: PHPStan\Reflection\Deprecation\DeprecationProvider + php8Lexer: class: PhpParser\Lexer\Emulative + factory: @PHPStan\Parser\LexerFactory::createEmulative() autowired: false php8PhpParser: - class: PhpParser\Parser\Php7 + class: PhpParser\Parser\Php8 arguments: lexer: @php8Lexer autowired: false @@ -1788,16 +2205,44 @@ services: php8Parser: @php8Parser autowired: false + freshStubParser: + class: PHPStan\Parser\StubParser + arguments: + parser: @php8PhpParser + autowired: false + + stubParser: + class: PHPStan\Parser\CachedParser + arguments: + originalParser: @freshStubParser + cachedNodesByStringCountMax: %cache.nodesByStringCountMax% + autowired: false + + phpstanDiagnoseExtension: + class: PHPStan\Diagnose\PHPStanDiagnoseExtension + arguments: + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + allConfigFiles: %allConfigFiles% + configPhpVersion: %phpVersion% + autowired: false + # Error formatters + - + class: PHPStan\Command\ErrorFormatter\CiDetectedErrorFormatter + autowired: + - PHPStan\Command\ErrorFormatter\CiDetectedErrorFormatter + errorFormatter.raw: class: PHPStan\Command\ErrorFormatter\RawErrorFormatter errorFormatter.table: class: PHPStan\Command\ErrorFormatter\TableErrorFormatter arguments: + simpleRelativePathHelper: @simpleRelativePathHelper showTipsOfTheDay: %tipsOfTheDay% editorUrl: %editorUrl% + editorUrlTitle: %editorUrlTitle% errorFormatter.checkstyle: class: PHPStan\Command\ErrorFormatter\CheckstyleErrorFormatter @@ -1828,7 +2273,6 @@ 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 deleted file mode 100644 index 7f8c6e81d6..0000000000 --- a/conf/config.stubFiles.neon +++ /dev/null @@ -1,21 +0,0 @@ -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/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 d08129c1d2..52eac72312 100644 --- a/conf/config.stubValidator.neon +++ b/conf/config.stubValidator.neon @@ -1,8 +1,6 @@ parameters: checkThisOnly: false checkClassCaseSensitivity: true - checkGenericClassInNonGenericObjectType: true - checkMissingIterableValueType: true checkMissingTypehints: true checkMissingCallableSignature: false __validate: false @@ -12,15 +10,18 @@ services: class: PHPStan\PhpDoc\StubSourceLocatorFactory arguments: php8Parser: @php8PhpParser - stubFiles: %stubFiles% - nodeScopeResolverClassReflector: + defaultAnalysisParser!: + factory: @stubParser + + nodeScopeResolverReflector: factory: @stubReflector stubBetterReflectionProvider: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: reflector: @stubReflector + universalObjectCratesClasses: %universalObjectCratesClasses% autowired: false stubReflector: @@ -38,3 +39,11 @@ services: factory: @stubBetterReflectionProvider autowired: - PHPStan\Reflection\ReflectionProvider + + currentPhpVersionLexer: + factory: @php8Lexer + autowired: false + + currentPhpVersionPhpParser: + factory: @php8PhpParser + autowired: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon new file mode 100644 index 0000000000..d18df776e3 --- /dev/null +++ b/conf/parametersSchema.neon @@ -0,0 +1,194 @@ +parametersSchema: + bootstrapFiles: listOf(string()) + excludePaths: anyOf( + structure([ + analyse: listOf(string()), + ]), + structure([ + analyseAndScan: listOf(string()), + ]) + structure([ + analyse: listOf(string()), + analyseAndScan: listOf(string()) + ]) + ) + level: schema(anyOf(int(), string()), nullable()) + paths: listOf(string()) + exceptions: structure([ + implicitThrows: bool(), + reportUncheckedExceptionDeadCatch: bool(), + uncheckedExceptionRegexes: listOf(string()), + uncheckedExceptionClasses: listOf(string()), + checkedExceptionRegexes: listOf(string()), + checkedExceptionClasses: listOf(string()), + check: structure([ + missingCheckedExceptionInThrows: bool(), + tooWideThrowType: bool() + ]) + ]) + featureToggles: structure([ + bleedingEdge: bool(), + checkParameterCastableToNumberFunctions: bool(), + skipCheckGenericClasses: listOf(string()), + stricterFunctionMap: bool() + reportPreciseLineForUnusedFunctionParameter: bool() + internalTag: bool() + ]) + fileExtensions: listOf(string()) + checkAdvancedIsset: bool() + reportAlwaysTrueInLastCondition: bool() + checkClassCaseSensitivity: bool() + checkExplicitMixed: bool() + checkImplicitMixed: bool() + checkFunctionArgumentTypes: bool() + checkFunctionNameCase: bool() + checkInternalClassCaseSensitivity: bool() + checkMissingCallableSignature: bool() + checkMissingVarTagTypehint: bool() + checkArgumentsPassedByReference: bool() + checkMaybeUndefinedVariables: bool() + checkNullables: bool() + checkThisOnly: bool() + checkUnionTypes: bool() + checkBenevolentUnionTypes: bool() + checkExplicitMixedMissingReturn: bool() + checkPhpDocMissingReturn: bool() + checkPhpDocMethodSignatures: bool() + checkExtraArguments: bool() + checkMissingTypehints: bool() + checkTooWideReturnTypesInProtectedAndPublicMethods: bool() + checkUninitializedProperties: bool() + checkDynamicProperties: bool() + strictRulesInstalled: bool() + deprecationRulesInstalled: bool() + inferPrivatePropertyTypeFromConstructor: bool() + + tips: structure([ + discoveringSymbols: bool() + treatPhpDocTypesAsCertain: bool() + ]) + tipsOfTheDay: bool() + reportMaybes: bool() + reportMaybesInMethodSignatures: bool() + reportMaybesInPropertyPhpDocTypes: bool() + reportStaticMethodSignatures: bool() + reportWrongPhpDocTypeInVarTag: bool() + reportAnyTypeWideningInVarTag: bool() + reportPossiblyNonexistentGeneralArrayOffset: bool() + reportPossiblyNonexistentConstantArrayOffset: bool() + checkMissingOverrideMethodAttribute: bool() + parallel: structure([ + jobSize: int(), + processTimeout: float(), + maximumNumberOfProcesses: int(), + minimumNumberOfJobsPerProcess: int(), + buffer: int() + ]) + phpVersion: schema(anyOf( + schema(int(), min(70100), max(80499)), + structure([ + min: schema(int(), min(70100), max(80499)), + max: schema(int(), min(70100), max(80499)) + ]) + ), nullable()) + polluteScopeWithLoopInitialAssignments: bool() + polluteScopeWithAlwaysIterableForeach: bool() + polluteScopeWithBlock: bool() + propertyAlwaysWrittenTags: listOf(string()) + propertyAlwaysReadTags: listOf(string()) + additionalConstructors: listOf(string()) + treatPhpDocTypesAsCertain: bool() + usePathConstantsAsConstantString: bool() + rememberPossiblyImpureFunctionValues: bool() + reportMagicMethods: bool() + reportMagicProperties: bool() + ignoreErrors: listOf( + anyOf( + string(), + structure([ + ?messages: listOf(string()) + ?identifier: string() + ?path: string() + ?reportUnmatched: bool() + ]), + structure([ + ?message: string() + ?identifier: string() + ?path: string() + ?reportUnmatched: bool() + ]), + structure([ + ?message: string() + count: int() + path: string() + ?identifier: string() + ?reportUnmatched: bool() + ]), + structure([ + ?message: string() + paths: listOf(string()) + ?identifier: string() + ?reportUnmatched: bool() + ]), + structure([ + ?messages: listOf(string()) + paths: listOf(string()) + ?identifier: string() + ?reportUnmatched: bool() + ]) + ) + ) + internalErrorsCountLimit: int() + cache: structure([ + nodesByStringCountMax: int() + ]) + reportUnmatchedIgnoredErrors: bool() + typeAliases: arrayOf(string()) + universalObjectCratesClasses: listOf(string()) + stubFiles: listOf(string()) + earlyTerminatingMethodCalls: arrayOf(listOf(string())) + earlyTerminatingFunctionCalls: listOf(string()) + resultCachePath: string() + resultCacheSkipIfOlderThanDays: int() + resultCacheChecksProjectExtensionFilesDependencies: bool() + dynamicConstantNames: listOf(string()) + customRulesetUsed: schema(bool(), nullable()) + rootDir: string() + tmpDir: string() + currentWorkingDirectory: string() + cliArgumentsVariablesRegistered: bool() + mixinExcludeClasses: listOf(string()) + scanFiles: listOf(string()) + scanDirectories: listOf(string()) + editorUrl: schema(string(), nullable()) + editorUrlTitle: schema(string(), nullable()) + errorFormat: schema(string(), nullable()) + pro: structure([ + dnsServers: schema(listOf(string()), min(1)), + tmpDir: string() + ]) + env: arrayOf(string(), anyOf(int(), string())) + sysGetTempDir: string() + parametersNotInvalidatingCache: listOf(schema(anyOf( + string(), + listOf(string()), + ))) + + # playground mode + sourceLocatorPlaygroundMode: bool() + + # irrelevant Nette parameters + debugMode: bool() + productionMode: bool() + tempDir: string() + __validate: bool() + + # internal parameters only for DerivativeContainerFactory + additionalConfigFiles: listOf(string()) + generateBaselineFile: schema(string(), nullable()) + analysedPaths: listOf(string()) + allConfigFiles: listOf(string()) + composerAutoloaderProjectPaths: listOf(string()) + analysedPathsFromConfig: listOf(string()) + usedLevel: string() + cliAutoloadFile: schema(string(), nullable()) diff --git a/conf/staticReflection.neon b/conf/staticReflection.neon deleted file mode 100644 index 7fe9268c0b..0000000000 --- a/conf/staticReflection.neon +++ /dev/null @@ -1,5 +0,0 @@ -# WARNING - This isn't ready for use - -parameters: - featureToggles: - disableRuntimeReflectionProvider: true diff --git a/e2e/PHPStanErrorPatch.patch b/e2e/PHPStanErrorPatch.patch new file mode 100644 index 0000000000..3d23608791 --- /dev/null +++ b/e2e/PHPStanErrorPatch.patch @@ -0,0 +1,11 @@ +--- Error.php 2022-09-28 15:43:03.000000000 +0200 ++++ Error.php 2022-10-17 20:47:54.000000000 +0200 +@@ -105,7 +105,7 @@ + + public function canBeIgnored(): bool + { +- return $this->canBeIgnored === true; ++ return !$this->canBeIgnored instanceof Throwable; + } + + public function hasNonIgnorableException(): bool diff --git a/e2e/bad-exclude-paths/excludePaths.neon b/e2e/bad-exclude-paths/excludePaths.neon new file mode 100644 index 0000000000..14ed59cad4 --- /dev/null +++ b/e2e/bad-exclude-paths/excludePaths.neon @@ -0,0 +1,9 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - src + excludePaths: + - tests diff --git a/e2e/bad-exclude-paths/ignore.neon b/e2e/bad-exclude-paths/ignore.neon new file mode 100644 index 0000000000..26d56b4b57 --- /dev/null +++ b/e2e/bad-exclude-paths/ignore.neon @@ -0,0 +1,11 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - src + ignoreErrors: + - + message: '#aaa#' + path: tests diff --git a/e2e/bad-exclude-paths/ignoreNonexistentExcludePath.neon b/e2e/bad-exclude-paths/ignoreNonexistentExcludePath.neon new file mode 100644 index 0000000000..b78c536f97 --- /dev/null +++ b/e2e/bad-exclude-paths/ignoreNonexistentExcludePath.neon @@ -0,0 +1,10 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - . + excludePaths: + - node_modules (?) + - tmp-node-modules diff --git a/e2e/bad-exclude-paths/ignoreReportUnmatchedFalse.neon b/e2e/bad-exclude-paths/ignoreReportUnmatchedFalse.neon new file mode 100644 index 0000000000..2206595e61 --- /dev/null +++ b/e2e/bad-exclude-paths/ignoreReportUnmatchedFalse.neon @@ -0,0 +1,12 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - src + reportUnmatchedIgnoredErrors: false + ignoreErrors: + - + message: '#aaa#' + path: tests diff --git a/e2e/bad-exclude-paths/phpneon.php b/e2e/bad-exclude-paths/phpneon.php new file mode 100644 index 0000000000..92ebd989a1 --- /dev/null +++ b/e2e/bad-exclude-paths/phpneon.php @@ -0,0 +1,17 @@ + [ + __DIR__ . '/../../conf/bleedingEdge.neon', + ], + 'parameters' => [ + 'level' => '8', + 'paths' => [__DIR__ . '/src'], + 'ignoreErrors' => [ + [ + 'message' => '#aaa#', + 'path' => 'src/test.php', // not absolute path - invalid in .php config + ], + ], + ], +]; diff --git a/e2e/bad-exclude-paths/phpneon2.php b/e2e/bad-exclude-paths/phpneon2.php new file mode 100644 index 0000000000..4c06f1f310 --- /dev/null +++ b/e2e/bad-exclude-paths/phpneon2.php @@ -0,0 +1,16 @@ + [ + __DIR__ . '/../../conf/bleedingEdge.neon', + ], + 'parameters' => [ + 'level' => '8', + 'paths' => [__DIR__ . '/src'], + 'excludePaths' => [ + 'analyse' => [ + 'src/test.php', // not absolute path - invalid in .php config + ], + ], + ], +]; diff --git a/e2e/bad-exclude-paths/src/test.php b/e2e/bad-exclude-paths/src/test.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/bad-exclude-paths/tmp-node-modules/test.php b/e2e/bad-exclude-paths/tmp-node-modules/test.php new file mode 100644 index 0000000000..c24b518fb0 --- /dev/null +++ b/e2e/bad-exclude-paths/tmp-node-modules/test.php @@ -0,0 +1,3 @@ +x; + } + + public function init(): void + { + $this->x = rand(); + } +} diff --git a/e2e/baseline-uninit-prop-trait/src/HelloWorld.php b/e2e/baseline-uninit-prop-trait/src/HelloWorld.php new file mode 100644 index 0000000000..f7ad689a59 --- /dev/null +++ b/e2e/baseline-uninit-prop-trait/src/HelloWorld.php @@ -0,0 +1,14 @@ +init(); + $this->foo(); + } +} diff --git a/e2e/baseline-uninit-prop-trait/test-no-baseline.neon b/e2e/baseline-uninit-prop-trait/test-no-baseline.neon new file mode 100644 index 0000000000..3e639cd0d2 --- /dev/null +++ b/e2e/baseline-uninit-prop-trait/test-no-baseline.neon @@ -0,0 +1,4 @@ +parameters: + level: 9 + paths: + - src diff --git a/e2e/baseline-uninit-prop-trait/test.neon b/e2e/baseline-uninit-prop-trait/test.neon new file mode 100644 index 0000000000..9b21d7b642 --- /dev/null +++ b/e2e/baseline-uninit-prop-trait/test.neon @@ -0,0 +1,3 @@ +includes: + - test-baseline.neon + - test-no-baseline.neon diff --git a/e2e/bug-11819/phpstan.neon b/e2e/bug-11819/phpstan.neon new file mode 100644 index 0000000000..5fbd64483a --- /dev/null +++ b/e2e/bug-11819/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 5 + paths: + - test.php diff --git a/e2e/bug-11819/test.php b/e2e/bug-11819/test.php new file mode 100644 index 0000000000..267e1647ad --- /dev/null +++ b/e2e/bug-11819/test.php @@ -0,0 +1,11 @@ + + */ +class DummyRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + /** + * @param InClassNode $node + * @return list + */ + public function processNode( + Node $node, + Scope $scope, + ): array + { + return [FatalErrorWhenAutoloaded::AUTOLOAD]; + } + +} diff --git a/e2e/bug-11826/src/FatalErrorWhenAutoloaded.php b/e2e/bug-11826/src/FatalErrorWhenAutoloaded.php new file mode 100644 index 0000000000..a75127a356 --- /dev/null +++ b/e2e/bug-11826/src/FatalErrorWhenAutoloaded.php @@ -0,0 +1,11 @@ +getName() === 'belongsTo'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { + $returnType = $methodReflection->getVariants()[0]->getReturnType(); + $argType = $scope->getType($methodCall->getArgs()[0]->value); + $modelClass = $argType->getClassStringObjectType()->getObjectClassNames()[0]; + + return new GenericObjectType($returnType->getObjectClassNames()[0], [ + new ObjectType($modelClass), + $scope->getType($methodCall->var), + ]); + } +} + diff --git a/e2e/bug-11857/src/test.php b/e2e/bug-11857/src/test.php new file mode 100644 index 0000000000..5c237f25e8 --- /dev/null +++ b/e2e/bug-11857/src/test.php @@ -0,0 +1,70 @@ + */ + public function belongsTo(string $related): BelongsTo + { + return new BelongsTo(); + } +} + +/** + * @template TRelatedModel of Model + * @template TDeclaringModel of Model + */ +class BelongsTo {} + +class User extends Model {} + +class Post extends Model +{ + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** @return BelongsTo */ + public function userSelf(): BelongsTo + { + /** @phpstan-ignore return.type */ + return $this->belongsTo(User::class); + } +} + +class ChildPost extends Post {} + +final class Comment extends Model +{ + // This model is final, so either of these + // two methods would work. It seems that + // PHPStan is automatically converting the + // `$this` to a `self` type in the user docblock, + // but it is not doing so likewise for the `$this` + // that is returned by the dynamic return extension. + + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** @return BelongsTo */ + public function user2(): BelongsTo + { + /** @phpstan-ignore return.type */ + return $this->belongsTo(User::class); + } +} + +function test(ChildPost $child): void +{ + assertType('Bug11857\BelongsTo', $child->user()); + // This demonstrates why `$this` is needed in non-final models + assertType('Bug11857\BelongsTo', $child->userSelf()); // should be: Bug11857\BelongsTo +} diff --git a/e2e/bug-12606/phpstan.neon b/e2e/bug-12606/phpstan.neon new file mode 100644 index 0000000000..1557144b26 --- /dev/null +++ b/e2e/bug-12606/phpstan.neon @@ -0,0 +1,2 @@ +includes: + - %env.CONFIGTEST%.neon diff --git a/e2e/bug-12606/src/empty.php b/e2e/bug-12606/src/empty.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/bug-12606/test.neon b/e2e/bug-12606/test.neon new file mode 100644 index 0000000000..c308dcf542 --- /dev/null +++ b/e2e/bug-12606/test.neon @@ -0,0 +1,4 @@ +parameters: + level: 8 + paths: + - src diff --git a/e2e/bug-12606/test.php b/e2e/bug-12606/test.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/bug-9622-trait/baseline-1.neon b/e2e/bug-9622-trait/baseline-1.neon new file mode 100644 index 0000000000..caa24d89e8 --- /dev/null +++ b/e2e/bug-9622-trait/baseline-1.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Offset 'foo' might not exist on array\\{foo\\?\\: int\\}\\.$#" + count: 1 + path: src/UsesBar.php diff --git a/e2e/bug-9622-trait/patch-1.patch b/e2e/bug-9622-trait/patch-1.patch new file mode 100644 index 0000000000..cc2a6a622d --- /dev/null +++ b/e2e/bug-9622-trait/patch-1.patch @@ -0,0 +1,11 @@ +--- src/Foo.php 2023-07-13 09:01:37 ++++ src/Foo.php 2023-07-13 09:02:20 +@@ -3,7 +3,7 @@ + namespace Bug9622Trait; + + /** +- * @phpstan-type AnArray array{foo: int} ++ * @phpstan-type AnArray array{foo?: int} + */ + class Foo + { diff --git a/e2e/bug-9622-trait/phpstan-baseline.neon b/e2e/bug-9622-trait/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/bug-9622-trait/phpstan.neon b/e2e/bug-9622-trait/phpstan.neon new file mode 100644 index 0000000000..ddbf4c2114 --- /dev/null +++ b/e2e/bug-9622-trait/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src diff --git a/e2e/bug-9622-trait/src/Bar.php b/e2e/bug-9622-trait/src/Bar.php new file mode 100644 index 0000000000..082a948bd5 --- /dev/null +++ b/e2e/bug-9622-trait/src/Bar.php @@ -0,0 +1,19 @@ + $query + * + * @return T + */ + public function handle(QueryInterface $query); +} \ No newline at end of file diff --git a/e2e/bug10449/src/Bus/QueryHandlerInterface.php b/e2e/bug10449/src/Bus/QueryHandlerInterface.php new file mode 100644 index 0000000000..88faa0164a --- /dev/null +++ b/e2e/bug10449/src/Bus/QueryHandlerInterface.php @@ -0,0 +1,9 @@ +queryBus->handle(new Query\ExampleQuery()); + $this->needsString($value); + return $value; + } + + private function needsString(string $s):void {} +} \ No newline at end of file diff --git a/e2e/bug10449/src/Query/ExampleQuery.php b/e2e/bug10449/src/Query/ExampleQuery.php new file mode 100644 index 0000000000..a5c7637793 --- /dev/null +++ b/e2e/bug10449/src/Query/ExampleQuery.php @@ -0,0 +1,15 @@ + + */ +final class ExampleQuery implements QueryInterface +{ +} \ No newline at end of file diff --git a/e2e/bug10449/src/Query/ExampleQueryHandler.php b/e2e/bug10449/src/Query/ExampleQueryHandler.php new file mode 100644 index 0000000000..e9bc1d59f0 --- /dev/null +++ b/e2e/bug10449/src/Query/ExampleQueryHandler.php @@ -0,0 +1,19 @@ + $query + * + * @return T + */ + public function handle(QueryInterface $query); +} \ No newline at end of file diff --git a/e2e/bug10449b/src/Bus/QueryHandlerInterface.php b/e2e/bug10449b/src/Bus/QueryHandlerInterface.php new file mode 100644 index 0000000000..88faa0164a --- /dev/null +++ b/e2e/bug10449b/src/Bus/QueryHandlerInterface.php @@ -0,0 +1,9 @@ +queryBus->handle($x); + $this->needsString($value); + return $value; + } + + return 'hello'; + } + + private function needsString(string $s):void {} +} \ No newline at end of file diff --git a/e2e/bug10449b/src/Query/ExampleQuery.php b/e2e/bug10449b/src/Query/ExampleQuery.php new file mode 100644 index 0000000000..a5c7637793 --- /dev/null +++ b/e2e/bug10449b/src/Query/ExampleQuery.php @@ -0,0 +1,15 @@ + + */ +final class ExampleQuery implements QueryInterface +{ +} \ No newline at end of file diff --git a/e2e/bug10449b/src/Query/ExampleQueryHandler.php b/e2e/bug10449b/src/Query/ExampleQueryHandler.php new file mode 100644 index 0000000000..e9bc1d59f0 --- /dev/null +++ b/e2e/bug10449b/src/Query/ExampleQueryHandler.php @@ -0,0 +1,19 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 3>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); + +\PHPStan\Testing\assertType('1', version_compare(PHP_VERSION, '7.0.0')); +\PHPStan\Testing\assertType('false', version_compare(PHP_VERSION, '7.0.0', '<')); +\PHPStan\Testing\assertType('true', version_compare(PHP_VERSION, '7.0.0', '>')); diff --git a/e2e/composer-max-version/.gitignore b/e2e/composer-max-version/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-max-version/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-max-version/composer.json b/e2e/composer-max-version/composer.json new file mode 100644 index 0000000000..4d4ca141ef --- /dev/null +++ b/e2e/composer-max-version/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "<=8.3" + } +} diff --git a/e2e/composer-max-version/test.php b/e2e/composer-max-version/test.php new file mode 100644 index 0000000000..038f559122 --- /dev/null +++ b/e2e/composer-max-version/test.php @@ -0,0 +1,10 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('int<5, 8>', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); + +\PHPStan\Testing\assertType('-1|0|1', version_compare(PHP_VERSION, '7.0.0')); +\PHPStan\Testing\assertType('bool', version_compare(PHP_VERSION, '7.0.0', '<')); +\PHPStan\Testing\assertType('bool', version_compare(PHP_VERSION, '7.0.0', '>')); diff --git a/e2e/composer-min-max-version/.gitignore b/e2e/composer-min-max-version/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-max-version/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-max-version/composer.json b/e2e/composer-min-max-version/composer.json new file mode 100644 index 0000000000..869fd2ce42 --- /dev/null +++ b/e2e/composer-min-max-version/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": ">=8.1, <=8.2.99" + } +} diff --git a/e2e/composer-min-max-version/test.php b/e2e/composer-min-max-version/test.php new file mode 100644 index 0000000000..28d770f3bb --- /dev/null +++ b/e2e/composer-min-max-version/test.php @@ -0,0 +1,10 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 2>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); + +\PHPStan\Testing\assertType('1', version_compare(PHP_VERSION, '7.0.0')); +\PHPStan\Testing\assertType('false', version_compare(PHP_VERSION, '7.0.0', '<')); +\PHPStan\Testing\assertType('true', version_compare(PHP_VERSION, '7.0.0', '>')); diff --git a/e2e/composer-min-open-end-version/.gitignore b/e2e/composer-min-open-end-version/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-open-end-version/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-open-end-version/composer.json b/e2e/composer-min-open-end-version/composer.json new file mode 100644 index 0000000000..b6303c6b77 --- /dev/null +++ b/e2e/composer-min-open-end-version/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": ">= 8.1" + } +} diff --git a/e2e/composer-min-open-end-version/test.php b/e2e/composer-min-open-end-version/test.php new file mode 100644 index 0000000000..d4d06a34eb --- /dev/null +++ b/e2e/composer-min-open-end-version/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 4>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-min-version-v5/.gitignore b/e2e/composer-min-version-v5/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-version-v5/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-version-v5/composer.json b/e2e/composer-min-version-v5/composer.json new file mode 100644 index 0000000000..b73464d219 --- /dev/null +++ b/e2e/composer-min-version-v5/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^5.6" + } +} diff --git a/e2e/composer-min-version-v5/test.php b/e2e/composer-min-version-v5/test.php new file mode 100644 index 0000000000..2f652079a6 --- /dev/null +++ b/e2e/composer-min-version-v5/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('5', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('6', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, 99>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-min-version-v7/.gitignore b/e2e/composer-min-version-v7/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-version-v7/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-version-v7/composer.json b/e2e/composer-min-version-v7/composer.json new file mode 100644 index 0000000000..9f9b263871 --- /dev/null +++ b/e2e/composer-min-version-v7/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^7" + } +} diff --git a/e2e/composer-min-version-v7/test.php b/e2e/composer-min-version-v7/test.php new file mode 100644 index 0000000000..e8876bd78f --- /dev/null +++ b/e2e/composer-min-version-v7/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('7', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<0, 4>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-min-version/.gitignore b/e2e/composer-min-version/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-version/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-version/composer.json b/e2e/composer-min-version/composer.json new file mode 100644 index 0000000000..9be64619f1 --- /dev/null +++ b/e2e/composer-min-version/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^8.1" + } +} diff --git a/e2e/composer-min-version/test.php b/e2e/composer-min-version/test.php new file mode 100644 index 0000000000..d4d06a34eb --- /dev/null +++ b/e2e/composer-min-version/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 4>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-no-versions/.gitignore b/e2e/composer-no-versions/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-no-versions/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-no-versions/composer.json b/e2e/composer-no-versions/composer.json new file mode 100644 index 0000000000..2c63c08510 --- /dev/null +++ b/e2e/composer-no-versions/composer.json @@ -0,0 +1,2 @@ +{ +} diff --git a/e2e/composer-no-versions/test.php b/e2e/composer-no-versions/test.php new file mode 100644 index 0000000000..28c8a3183b --- /dev/null +++ b/e2e/composer-no-versions/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('int<5, 8>', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-version-config-invalid/phpstan.neon b/e2e/composer-version-config-invalid/phpstan.neon new file mode 100644 index 0000000000..96977def5f --- /dev/null +++ b/e2e/composer-version-config-invalid/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + phpVersion: + min: 80303 + max: 80104 + diff --git a/e2e/composer-version-config-patch/.gitignore b/e2e/composer-version-config-patch/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-version-config-patch/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-version-config-patch/composer.json b/e2e/composer-version-config-patch/composer.json new file mode 100644 index 0000000000..d6103988c8 --- /dev/null +++ b/e2e/composer-version-config-patch/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": ">=8.0.2, <8.0.15" + } +} diff --git a/e2e/composer-version-config-patch/test.php b/e2e/composer-version-config-patch/test.php new file mode 100644 index 0000000000..3f201eadab --- /dev/null +++ b/e2e/composer-version-config-patch/test.php @@ -0,0 +1,7 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('0', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<2, 15>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-version-config/.gitignore b/e2e/composer-version-config/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-version-config/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-version-config/composer.json b/e2e/composer-version-config/composer.json new file mode 100644 index 0000000000..2da0adaf1c --- /dev/null +++ b/e2e/composer-version-config/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^8.0" + } +} diff --git a/e2e/composer-version-config/phpstan.neon b/e2e/composer-version-config/phpstan.neon new file mode 100644 index 0000000000..003e5e1484 --- /dev/null +++ b/e2e/composer-version-config/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + phpVersion: + min: 80103 + max: 80304 diff --git a/e2e/composer-version-config/test.php b/e2e/composer-version-config/test.php new file mode 100644 index 0000000000..a9afaa4b65 --- /dev/null +++ b/e2e/composer-version-config/test.php @@ -0,0 +1,11 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 3>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); + +\PHPStan\Testing\assertType('1', version_compare(PHP_VERSION, '7.0.0')); +\PHPStan\Testing\assertType('false', version_compare(PHP_VERSION, '7.0.0', '<')); +\PHPStan\Testing\assertType('true', version_compare(PHP_VERSION, '7.0.0', '>')); diff --git a/e2e/discussion-11362/.gitignore b/e2e/discussion-11362/.gitignore new file mode 100644 index 0000000000..61ead86667 --- /dev/null +++ b/e2e/discussion-11362/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/e2e/discussion-11362/composer.json b/e2e/discussion-11362/composer.json new file mode 100644 index 0000000000..114bec3040 --- /dev/null +++ b/e2e/discussion-11362/composer.json @@ -0,0 +1,24 @@ +{ + "config": { + "preferred-install": { + "*": "dist", + "repro/*": "source" + }, + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true, + "repositories": [ + { + "type": "path", + "url": "./packages/*/" + } + ], + "require": { + "repro/site": "@dev", + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "1.11.7" + } +} diff --git a/e2e/discussion-11362/composer.lock b/e2e/discussion-11362/composer.lock new file mode 100644 index 0000000000..b893bfb8cc --- /dev/null +++ b/e2e/discussion-11362/composer.lock @@ -0,0 +1,103 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "49a30c2374c218ffcab0e58fd0628fab", + "packages": [ + { + "name": "repro/site", + "version": "dev-main", + "dist": { + "type": "path", + "url": "./packages/site", + "reference": "0f4b564a39e8ff3758c1d96a2a5d4b72dea08b23" + }, + "require": { + "php": "^8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Repro\\Site\\": "Classes" + } + }, + "transport-options": { + "relative": true + } + } + ], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "1.11.7", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/52d2bbfdcae7f895915629e4694e9497d0f8e28d", + "reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-07-06T11:17:41+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "repro/site": 20 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/e2e/discussion-11362/packages/site/Classes/Domain/Model/ContentPage.php b/e2e/discussion-11362/packages/site/Classes/Domain/Model/ContentPage.php new file mode 100644 index 0000000000..4ca5833958 --- /dev/null +++ b/e2e/discussion-11362/packages/site/Classes/Domain/Model/ContentPage.php @@ -0,0 +1,59 @@ +parentIssue; + } + + public function getParentLesson(): ?Lesson + { + return $this->parentLesson; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getType(): string + { + return $this->type; + } + + public function getNavigationVisible(): bool + { + return $this->navigationVisible; + } + + public function getNavigationColor(): string + { + return $this->navigationColor; + } +} + diff --git a/e2e/discussion-11362/packages/site/Classes/Domain/Model/Issue.php b/e2e/discussion-11362/packages/site/Classes/Domain/Model/Issue.php new file mode 100644 index 0000000000..8343551215 --- /dev/null +++ b/e2e/discussion-11362/packages/site/Classes/Domain/Model/Issue.php @@ -0,0 +1,45 @@ +parentSchoolYear; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getStartDate(): int + { + return $this->startDate; + } + + public function getHolidayTitle(): string + { + return $this->holidayTitle; + } +} diff --git a/e2e/discussion-11362/packages/site/Classes/Domain/Model/Lesson.php b/e2e/discussion-11362/packages/site/Classes/Domain/Model/Lesson.php new file mode 100644 index 0000000000..e00f2f0efb --- /dev/null +++ b/e2e/discussion-11362/packages/site/Classes/Domain/Model/Lesson.php @@ -0,0 +1,29 @@ +schoolLevel; + } + + public function getParentIssue(): ?Issue + { + return $this->parentIssue; + } + + public function getLessonNumber(): int + { + return $this->lessonNumber; + } +} diff --git a/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolLevel.php b/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolLevel.php new file mode 100644 index 0000000000..5c326ca596 --- /dev/null +++ b/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolLevel.php @@ -0,0 +1,15 @@ +title; + } +} diff --git a/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolYear.php b/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolYear.php new file mode 100644 index 0000000000..a86f5bf575 --- /dev/null +++ b/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolYear.php @@ -0,0 +1,40 @@ +startDate; + } + + public function getEndDate(): int + { + return $this->endDate; + } + + public function getIntroStartDate(): int + { + return $this->introStartDate; + } + + public function getIntroEndDate(): int + { + return $this->introEndDate; + } +} diff --git a/e2e/discussion-11362/packages/site/composer.json b/e2e/discussion-11362/packages/site/composer.json new file mode 100644 index 0000000000..ca413c1c8d --- /dev/null +++ b/e2e/discussion-11362/packages/site/composer.json @@ -0,0 +1,11 @@ +{ + "autoload": { + "psr-4": { + "Repro\\Site\\": "Classes" + } + }, + "name": "repro/site", + "require": { + "php": "^8.1" + } +} diff --git a/e2e/discussion-11362/phpstan.neon b/e2e/discussion-11362/phpstan.neon new file mode 100644 index 0000000000..2e6178c1a7 --- /dev/null +++ b/e2e/discussion-11362/phpstan.neon @@ -0,0 +1,9 @@ +parameters: + excludePaths: + analyse: + - vendor + + level: 1 + + paths: + - . diff --git a/e2e/env-int-key/test.php b/e2e/env-int-key/test.php new file mode 100644 index 0000000000..b3d9bbc7f3 --- /dev/null +++ b/e2e/env-int-key/test.php @@ -0,0 +1 @@ + + */ +final class ClassCollector implements Collector +{ + public function getNodeType(): string + { + return Node\Stmt\Class_::class; + } + + public function processNode(Node $node, Scope $scope) : ?array + { + if ($node->name === null) { + return null; + } + + return [$node->name->name, $node->getStartLine()]; + } +} diff --git a/e2e/ignore-error-extension/src/ClassRule.php b/e2e/ignore-error-extension/src/ClassRule.php new file mode 100644 index 0000000000..17283bafe5 --- /dev/null +++ b/e2e/ignore-error-extension/src/ClassRule.php @@ -0,0 +1,43 @@ + + */ +final class ClassRule implements Rule +{ + #[Override] + public function getNodeType() : string + { + return CollectedDataNode::class; + } + + #[Override] + public function processNode(Node $node, Scope $scope) : array + { + $errors = []; + + foreach ($node->get(ClassCollector::class) as $file => $data) { + foreach ($data as [$className, $line]) { + $errors[] = RuleErrorBuilder::message('This is an error from a rule that uses a collector') + ->file($file) + ->line($line) + ->identifier('class.name') + ->build(); + } + } + + return $errors; + } + +} diff --git a/e2e/ignore-error-extension/src/ControllerActionReturnTypeIgnoreExtension.php b/e2e/ignore-error-extension/src/ControllerActionReturnTypeIgnoreExtension.php new file mode 100644 index 0000000000..dc7b0dab5a --- /dev/null +++ b/e2e/ignore-error-extension/src/ControllerActionReturnTypeIgnoreExtension.php @@ -0,0 +1,41 @@ +getIdentifier() !== 'missingType.iterableValue') { + return false; + } + + // @phpstan-ignore phpstanApi.instanceofAssumption + if (! $node instanceof InClassMethodNode) { + return false; + } + + if (! str_ends_with($node->getClassReflection()->getName(), 'Controller')) { + return false; + } + + if (! str_ends_with($node->getMethodReflection()->getName(), 'Action')) { + return false; + } + + if (! $node->getMethodReflection()->isPublic()) { + return false; + } + + return true; + } +} diff --git a/e2e/ignore-error-extension/src/ControllerClassNameIgnoreExtension.php b/e2e/ignore-error-extension/src/ControllerClassNameIgnoreExtension.php new file mode 100644 index 0000000000..b52b4f7ef1 --- /dev/null +++ b/e2e/ignore-error-extension/src/ControllerClassNameIgnoreExtension.php @@ -0,0 +1,34 @@ +getIdentifier() !== 'class.name') { + return false; + } + + // @phpstan-ignore phpstanApi.instanceofAssumption + if (!$node instanceof CollectedDataNode) { + return false; + } + + if (!str_ends_with($error->getFile(), 'Controller.php')) { + return false; + } + + return true; + } +} diff --git a/e2e/ignore-error-extension/src/HomepageController.php b/e2e/ignore-error-extension/src/HomepageController.php new file mode 100644 index 0000000000..d55c955157 --- /dev/null +++ b/e2e/ignore-error-extension/src/HomepageController.php @@ -0,0 +1,29 @@ + 'Homepage', + 'something' => $this->getSomething(), + ]; + } + + public function contactAction($someUnrelatedError): array + { + return [ + 'title' => 'Contact', + 'something' => $this->getSomething(), + ]; + } + + private function getSomething(): array + { + return []; + } +} diff --git a/e2e/only-files-not-analysed-trait/ignore.neon b/e2e/only-files-not-analysed-trait/ignore.neon new file mode 100644 index 0000000000..e0257ac498 --- /dev/null +++ b/e2e/only-files-not-analysed-trait/ignore.neon @@ -0,0 +1,10 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 + ignoreErrors: + - + message: "#^Trait OnlyFilesNotAnalysedTrait\\\\BarTrait is used zero times and is not analysed\\.$#" + count: 1 + path: src/BarTrait.php diff --git a/e2e/only-files-not-analysed-trait/no-ignore.neon b/e2e/only-files-not-analysed-trait/no-ignore.neon new file mode 100644 index 0000000000..899fee922c --- /dev/null +++ b/e2e/only-files-not-analysed-trait/no-ignore.neon @@ -0,0 +1,5 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 diff --git a/e2e/only-files-not-analysed-trait/src/BarTrait.php b/e2e/only-files-not-analysed-trait/src/BarTrait.php new file mode 100644 index 0000000000..efb6e5abb5 --- /dev/null +++ b/e2e/only-files-not-analysed-trait/src/BarTrait.php @@ -0,0 +1,8 @@ += 8.1 + +namespace PhpstanPhpUnit190; + +class FoobarTest +{ + public function testBaz(): int + { + $matcher = new self(); + $this->acceptCallback(static function (string $test) use ($matcher): string { + match ($matcher->testBaz()) { + 1 => 1, + 2 => 2, + default => new \LogicException() + }; + + return $test; + }); + + return 1; + } + + public function acceptCallback(callable $cb): void + { + + } +} diff --git a/e2e/result-cache-1/baseline-1.neon b/e2e/result-cache-1/baseline-1.neon new file mode 100644 index 0000000000..ff4bda03bd --- /dev/null +++ b/e2e/result-cache-1/baseline-1.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Call to an undefined method TestResultCache1\\\\Foo\\:\\:doFoo\\(\\)\\.$#" + count: 1 + path: src/Baz.php diff --git a/e2e/result-cache-1/patch-1.patch b/e2e/result-cache-1/patch-1.patch new file mode 100644 index 0000000000..55f8d3c2be --- /dev/null +++ b/e2e/result-cache-1/patch-1.patch @@ -0,0 +1,11 @@ +--- src/Bar.php 2022-10-17 20:57:35.000000000 +0200 ++++ src/Bar2.php 2022-10-17 20:57:47.000000000 +0200 +@@ -5,7 +5,7 @@ + class Bar + { + +- public function doFoo(): void ++ public function doFooo(): void + { + + } diff --git a/e2e/result-cache-1/phpstan-baseline.neon b/e2e/result-cache-1/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/result-cache-1/phpstan.neon b/e2e/result-cache-1/phpstan.neon new file mode 100644 index 0000000000..ddbf4c2114 --- /dev/null +++ b/e2e/result-cache-1/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src diff --git a/e2e/result-cache-1/src/Bar.php b/e2e/result-cache-1/src/Bar.php new file mode 100644 index 0000000000..713500e65d --- /dev/null +++ b/e2e/result-cache-1/src/Bar.php @@ -0,0 +1,13 @@ +doFoo(); + } + +} diff --git a/e2e/result-cache-1/src/Foo.php b/e2e/result-cache-1/src/Foo.php new file mode 100644 index 0000000000..f51ae8be5a --- /dev/null +++ b/e2e/result-cache-1/src/Foo.php @@ -0,0 +1,11 @@ + in PHPDoc tag @use is not subtype of template type T of Exception of trait TestResultCache3\\\\BarTrait\\.$#" + count: 1 + path: src/Foo.php diff --git a/e2e/result-cache-3/patch-1.patch b/e2e/result-cache-3/patch-1.patch new file mode 100644 index 0000000000..6507669199 --- /dev/null +++ b/e2e/result-cache-3/patch-1.patch @@ -0,0 +1,11 @@ +--- src/Baz.php 2022-10-24 14:28:45.000000000 +0200 ++++ src/Baz.php 2022-10-24 14:30:02.000000000 +0200 +@@ -2,7 +2,7 @@ + + namespace TestResultCache3; + +-class Baz extends \Exception ++class Baz extends \stdClass + { + + } diff --git a/e2e/result-cache-3/phpstan-baseline.neon b/e2e/result-cache-3/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/result-cache-3/phpstan.neon b/e2e/result-cache-3/phpstan.neon new file mode 100644 index 0000000000..ddbf4c2114 --- /dev/null +++ b/e2e/result-cache-3/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src diff --git a/e2e/result-cache-3/src/BarTrait.php b/e2e/result-cache-3/src/BarTrait.php new file mode 100644 index 0000000000..7529085522 --- /dev/null +++ b/e2e/result-cache-3/src/BarTrait.php @@ -0,0 +1,11 @@ + */ + use BarTrait; + +} diff --git a/e2e/result-cache-4/baseline-1.neon b/e2e/result-cache-4/baseline-1.neon new file mode 100644 index 0000000000..d54e5899ce --- /dev/null +++ b/e2e/result-cache-4/baseline-1.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^PHPDoc tag @var for property TestResultCache4\\\\Foo\\:\\:\\$foo with type TestResultCache4\\\\Bar is incompatible with native type Exception\\.$#" + count: 1 + path: src/Foo.php diff --git a/e2e/result-cache-4/patch-1.patch b/e2e/result-cache-4/patch-1.patch new file mode 100644 index 0000000000..d8f6d0aa67 --- /dev/null +++ b/e2e/result-cache-4/patch-1.patch @@ -0,0 +1,11 @@ +--- src/Bar.php 2022-10-24 14:28:45.000000000 +0200 ++++ src/Bar.php 2022-10-24 14:30:02.000000000 +0200 +@@ -2,7 +2,7 @@ + + namespace TestResultCache3; + +-class Bar extends \Exception ++class Bar extends \stdClass + { + + } diff --git a/e2e/result-cache-4/phpstan-baseline.neon b/e2e/result-cache-4/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/result-cache-4/phpstan.neon b/e2e/result-cache-4/phpstan.neon new file mode 100644 index 0000000000..ddbf4c2114 --- /dev/null +++ b/e2e/result-cache-4/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src diff --git a/e2e/result-cache-4/src/Bar.php b/e2e/result-cache-4/src/Bar.php new file mode 100644 index 0000000000..d07bf2d9c9 --- /dev/null +++ b/e2e/result-cache-4/src/Bar.php @@ -0,0 +1,8 @@ +foo; + } + +} diff --git a/e2e/result-cache-5/baseline-1.neon b/e2e/result-cache-5/baseline-1.neon new file mode 100644 index 0000000000..f0db0c2a00 --- /dev/null +++ b/e2e/result-cache-5/baseline-1.neon @@ -0,0 +1,11 @@ +parameters: + ignoreErrors: + - + message: "#^Expected type true, actual\\: false$#" + count: 1 + path: src/Foo.php + + - + message: "#^Instanceof between TestResultCache5\\\\Baz and Exception will always evaluate to false\\.$#" + count: 1 + path: src/Foo.php diff --git a/e2e/result-cache-5/patch-1.patch b/e2e/result-cache-5/patch-1.patch new file mode 100644 index 0000000000..69791cca29 --- /dev/null +++ b/e2e/result-cache-5/patch-1.patch @@ -0,0 +1,11 @@ +--- src/Baz.php 2022-10-24 14:28:45.000000000 +0200 ++++ src/Baz.php 2022-10-24 14:30:02.000000000 +0200 +@@ -2,7 +2,7 @@ + + namespace TestResultCache5; + +-class Baz extends \Exception ++class Baz extends \stdClass + { + + } diff --git a/e2e/result-cache-5/phpstan-baseline.neon b/e2e/result-cache-5/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/result-cache-5/phpstan.neon b/e2e/result-cache-5/phpstan.neon new file mode 100644 index 0000000000..7c3f71ae98 --- /dev/null +++ b/e2e/result-cache-5/phpstan.neon @@ -0,0 +1,11 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src + ignoreErrors: + - + identifier: instanceof.alwaysTrue + reportUnmatched: false diff --git a/e2e/result-cache-5/src/Bar.php b/e2e/result-cache-5/src/Bar.php new file mode 100644 index 0000000000..6df1b7ee81 --- /dev/null +++ b/e2e/result-cache-5/src/Bar.php @@ -0,0 +1,16 @@ +doBar($var); + assertType('true', $var instanceof \Exception); + } + +} diff --git a/e2e/result-cache-6/baseline-1.neon b/e2e/result-cache-6/baseline-1.neon new file mode 100644 index 0000000000..a9ebd9fb61 --- /dev/null +++ b/e2e/result-cache-6/baseline-1.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Access to an undefined property TestResultCache6\\\\Bar\\:\\:\\$s\\.$#" + count: 1 + path: src/Foo.php diff --git a/e2e/result-cache-6/patch-1.patch b/e2e/result-cache-6/patch-1.patch new file mode 100644 index 0000000000..e2d7e84db0 --- /dev/null +++ b/e2e/result-cache-6/patch-1.patch @@ -0,0 +1,10 @@ +diff --git b/e2e/result-cache-6/src/Baz.php a/e2e/result-cache-6/src/Baz.php +index 4a94eb3ae..6fed0b9ec 100644 +--- b/e2e/result-cache-6/src/Baz.php ++++ a/e2e/result-cache-6/src/Baz.php +@@ -4,5 +4,4 @@ namespace TestResultCache6; + + class Baz + { +- public string $s; + } diff --git a/e2e/result-cache-6/phpstan-baseline.neon b/e2e/result-cache-6/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/result-cache-6/phpstan.neon b/e2e/result-cache-6/phpstan.neon new file mode 100644 index 0000000000..ddbf4c2114 --- /dev/null +++ b/e2e/result-cache-6/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src diff --git a/e2e/result-cache-6/src/Bar.php b/e2e/result-cache-6/src/Bar.php new file mode 100644 index 0000000000..a9d6848bfe --- /dev/null +++ b/e2e/result-cache-6/src/Bar.php @@ -0,0 +1,10 @@ +s; + } + +} diff --git a/e2e/result-cache-7/baseline-1.neon b/e2e/result-cache-7/baseline-1.neon new file mode 100644 index 0000000000..6f1062520d --- /dev/null +++ b/e2e/result-cache-7/baseline-1.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^PHPDoc tag @phpstan\\-require\\-implements cannot contain non\\-interface type TestResultCache7\\\\Bar\\.$#" + count: 1 + path: src/Foo.php diff --git a/e2e/result-cache-7/patch-1.patch b/e2e/result-cache-7/patch-1.patch new file mode 100644 index 0000000000..a381f8b428 --- /dev/null +++ b/e2e/result-cache-7/patch-1.patch @@ -0,0 +1,12 @@ +diff --git a/e2e/result-cache-7/src/Bar.php b/e2e/result-cache-7/src/Bar.php +index b698e695d..0bbcc3093 100644 +--- a/e2e/result-cache-7/src/Bar.php ++++ b/e2e/result-cache-7/src/Bar.php +@@ -2,6 +2,6 @@ + + namespace TestResultCache7; + +-interface Bar ++class Bar + { + } diff --git a/e2e/result-cache-7/phpstan-baseline.neon b/e2e/result-cache-7/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/result-cache-7/phpstan.neon b/e2e/result-cache-7/phpstan.neon new file mode 100644 index 0000000000..66c19c7166 --- /dev/null +++ b/e2e/result-cache-7/phpstan.neon @@ -0,0 +1,10 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src + ignoreErrors: + - + identifier: trait.unused diff --git a/e2e/result-cache-7/src/Bar.php b/e2e/result-cache-7/src/Bar.php new file mode 100644 index 0000000000..b698e695dd --- /dev/null +++ b/e2e/result-cache-7/src/Bar.php @@ -0,0 +1,7 @@ + + */ +class CustomRule implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/e2e/result-cache-8/build/CustomRule2.php b/e2e/result-cache-8/build/CustomRule2.php new file mode 100644 index 0000000000..413d37b54d --- /dev/null +++ b/e2e/result-cache-8/build/CustomRule2.php @@ -0,0 +1,24 @@ + + */ +class CustomRule2 implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/e2e/result-cache-8/composer.json b/e2e/result-cache-8/composer.json new file mode 100644 index 0000000000..d29ab86e75 --- /dev/null +++ b/e2e/result-cache-8/composer.json @@ -0,0 +1,14 @@ +{ + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-webmozart-assert": "^1.2" + }, + "autoload": { + "classmap": ["src"] + }, + "autoload-dev": { + "classmap": [ + "build" + ] + } +} diff --git a/e2e/result-cache-8/composer.lock b/e2e/result-cache-8/composer.lock new file mode 100644 index 0000000000..23a7f311ea --- /dev/null +++ b/e2e/result-cache-8/composer.lock @@ -0,0 +1,132 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "964b13a11680dbf7fa5291f0baa6d10c", + "packages": [], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "1.10.63", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "ad12836d9ca227301f5fb9960979574ed8628339" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ad12836d9ca227301f5fb9960979574ed8628339", + "reference": "ad12836d9ca227301f5fb9960979574ed8628339", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2024-03-18T16:53:53+00:00" + }, + { + "name": "phpstan/phpstan-webmozart-assert", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-webmozart-assert.git", + "reference": "d1ff28697bd4e1c9ef5d3f871367ce9092871fec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-webmozart-assert/zipball/d1ff28697bd4e1c9ef5d3f871367ce9092871fec", + "reference": "d1ff28697bd4e1c9ef5d3f871367ce9092871fec", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10" + }, + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "webmozart/assert": "^1.11.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan webmozart/assert extension", + "support": { + "issues": "https://github.com/phpstan/phpstan-webmozart-assert/issues", + "source": "https://github.com/phpstan/phpstan-webmozart-assert/tree/1.2.4" + }, + "time": "2023-02-21T20:34:19+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/e2e/result-cache-8/phpstan.neon b/e2e/result-cache-8/phpstan.neon new file mode 100644 index 0000000000..c7fd83c756 --- /dev/null +++ b/e2e/result-cache-8/phpstan.neon @@ -0,0 +1,17 @@ +includes: + - vendor/phpstan/phpstan-webmozart-assert/extension.neon + +parameters: + paths: + - src + level: 8 + +rules: + - ResultCache8E2E\CustomRule + - ResultCache8E2E\CustomRule3 + +services: + - + class: ResultCache8E2E\CustomRule2 + tags: + - phpstan.rules.rule diff --git a/e2e/result-cache-8/src/CustomRule3.php b/e2e/result-cache-8/src/CustomRule3.php new file mode 100644 index 0000000000..1f0ca326e1 --- /dev/null +++ b/e2e/result-cache-8/src/CustomRule3.php @@ -0,0 +1,24 @@ + + */ +class CustomRule3 implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/e2e/result-cache-8/src/Foo.php b/e2e/result-cache-8/src/Foo.php new file mode 100644 index 0000000000..0082675fde --- /dev/null +++ b/e2e/result-cache-8/src/Foo.php @@ -0,0 +1,8 @@ +=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com/" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com/" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com/" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-07-18T10:29:17+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu/" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, + { + "name": "knplabs/github-api", + "version": "v3.15.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/php-github-api.git", + "reference": "d4b7a1c00e22c1ca32408ecdd4e33c674196b1bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/d4b7a1c00e22c1ca32408ecdd4e33c674196b1bc", + "reference": "d4b7a1c00e22c1ca32408ecdd4e33c674196b1bc", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2.5 || ^8.0", + "php-http/cache-plugin": "^1.7.1|^2.0", + "php-http/client-common": "^2.3", + "php-http/discovery": "^1.12", + "php-http/httplug": "^2.2", + "php-http/multipart-stream-builder": "^1.1.2", + "psr/cache": "^1.0|^2.0|^3.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0|^2.0", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.2", + "guzzlehttp/psr7": "^2.7", + "http-interop/http-factory-guzzle": "^1.0", + "php-http/mock-client": "^1.4.1", + "phpstan/extension-installer": "^1.0.5", + "phpstan/phpstan": "^0.12.57", + "phpstan/phpstan-deprecation-rules": "^0.12.5", + "phpunit/phpunit": "^8.5 || ^9.4", + "symfony/cache": "^5.1.8", + "symfony/phpunit-bridge": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.20.x-dev", + "dev-master": "3.14-dev" + } + }, + "autoload": { + "psr-4": { + "Github\\": "lib/Github/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "http://knplabs.com/" + }, + { + "name": "Thibault Duplessis", + "email": "thibault.duplessis@gmail.com", + "homepage": "http://ornicar.github.com/" + } + ], + "description": "GitHub API v3 client", + "homepage": "https://github.com/KnpLabs/php-github-api", + "keywords": [ + "api", + "gh", + "gist", + "github" + ], + "support": { + "issues": "https://github.com/KnpLabs/php-github-api/issues", + "source": "https://github.com/KnpLabs/php-github-api/tree/v3.15.0" + }, + "funding": [ + { + "url": "https://github.com/acrobat", + "type": "github" + } + ], + "time": "2024-09-23T19:00:43+00:00" + }, + { + "name": "league/commonmark", + "version": "2.5.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "b650144166dfa7703e62a22e493b853b58d874b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b650144166dfa7703e62a22e493b853b58d874b0", + "reference": "b650144166dfa7703e62a22e493b853b58d874b0", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 || ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com/", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com/", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2024-08-16T11:46:16+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com/", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com/", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "nette/neon", + "version": "v3.4.3", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "c8481c104431c8d94cc88424a1e21f47f8c93280" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/c8481c104431c8d94cc88424a1e21f47f8c93280", + "reference": "c8481c104431c8d94cc88424a1e21f47f8c93280", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "8.0 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.7" + }, + "bin": [ + "bin/neon-lint" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🍸 Nette NEON: encodes and decodes NEON file format.", + "homepage": "https://ne-on.org/", + "keywords": [ + "export", + "import", + "neon", + "nette", + "yaml" + ], + "support": { + "issues": "https://github.com/nette/neon/issues", + "source": "https://github.com/nette/neon/tree/v3.4.3" + }, + "time": "2024-06-26T14:53:59+00:00" + }, + { + "name": "nette/schema", + "version": "v1.2.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", + "shasum": "" + }, + "require": { + "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", + "php": "7.1 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.3 || ^2.4", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org/", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.2.5" + }, + "time": "2023-10-05T20:37:59+00:00" + }, + { + "name": "nette/utils", + "version": "v3.2.10", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/a4175c62652f2300c8017fb7e640f9ccb11648d2", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2", + "shasum": "" + }, + "require": { + "php": ">=7.2 <8.4" + }, + "conflict": { + "nette/di": "<3.0.6" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "~2.0", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org/", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v3.2.10" + }, + "time": "2023-07-30T15:38:18+00:00" + }, + { + "name": "php-http/cache-plugin", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/cache-plugin.git", + "reference": "539b2d1ea0dc1c2f141c8155f888197d4ac5635b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/cache-plugin/zipball/539b2d1ea0dc1c2f141c8155f888197d4ac5635b", + "reference": "539b2d1ea0dc1c2f141c8155f888197d4ac5635b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/client-common": "^1.9 || ^2.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/http-factory-implementation": "^1.0", + "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "require-dev": { + "nyholm/psr7": "^1.6.1", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "PSR-6 Cache plugin for HTTPlug", + "homepage": "http://httplug.io/", + "keywords": [ + "cache", + "http", + "httplug", + "plugin" + ], + "support": { + "issues": "https://github.com/php-http/cache-plugin/issues", + "source": "https://github.com/php-http/cache-plugin/tree/2.0.0" + }, + "time": "2024-02-19T17:02:14+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.7.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io/", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.2" + }, + "time": "2024-09-24T06:21:48+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.19.4", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "0700efda8d7526335132360167315fdab3aeb599" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599", + "reference": "0700efda8d7526335132360167315fdab3aeb599", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org/", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.19.4" + }, + "time": "2024-03-29T13:00:05+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu/" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io/", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.1" + }, + "time": "2024-09-23T11:39:58+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "5997f3289332c699fa2545c427826272498a2088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", + "reference": "5997f3289332c699fa2545c427826272498a2088", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org/", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.1" + }, + "time": "2024-03-07T13:22:09+00:00" + }, + { + "name": "php-http/multipart-stream-builder", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/multipart-stream-builder.git", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Message\\MultipartStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "A builder class that help you create a multipart stream", + "homepage": "http://php-http.org/", + "keywords": [ + "factory", + "http", + "message", + "multipart stream", + "stream" + ], + "support": { + "issues": "https://github.com/php-http/multipart-stream-builder/issues", + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2" + }, + "time": "2024-09-04T13:22:54+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io/", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "72115ab2bf1e40af1f9b238938d493ba7f3221e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/72115ab2bf1e40af1f9b238938d493ba7f3221e7", + "reference": "72115ab2bf1e40af1f9b238938d493ba7f3221e7", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-11-11T07:06:55+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.0" + }, + "time": "2024-10-26T16:04:33+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765", + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com/", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-20T08:15:52+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.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/deprecation-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/d7eb6daf8cd7e9ac4976e9576b32042ef7253453", + "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com/", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-08-13T14:27:37+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com/", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.1.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": "2024-05-31T14:57:53+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com/", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/string", + "version": "v7.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com/", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-20T08:28:38+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-06-12T14:39:25+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" + }, + "time": "2024-09-15T16:40:33+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.21", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-09-19T10:50:18+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/issue-bot/console.php b/issue-bot/console.php new file mode 100755 index 0000000000..fd1dfa73a8 --- /dev/null +++ b/issue-bot/console.php @@ -0,0 +1,73 @@ +#!/usr/bin/env php +addPlugin($rateLimitPlugin); + $httpBuilder->addPlugin($requestCounter); + + $client = new Client($httpBuilder); + $client->authenticate($token, AuthMethod::ACCESS_TOKEN); + $rateLimitPlugin->setClient($client); + + $markdownEnvironment = new Environment(); + $markdownEnvironment->addExtension(new CommonMarkCoreExtension()); + $markdownEnvironment->addExtension(new GithubFlavoredMarkdownExtension()); + $botCommentParser = new BotCommentParser(new MarkdownParser($markdownEnvironment)); + $issueCommentDownloader = new IssueCommentDownloader($client, $botCommentParser); + + $issueCachePath = __DIR__ . '/tmp/issueCache.tmp'; + $playgroundCachePath = __DIR__ . '/tmp/playgroundCache.tmp'; + $tmpDir = __DIR__ . '/tmp'; + + exec('git branch --show-current', $gitBranchLines, $exitCode); + if ($exitCode === 0) { + $gitBranch = implode("\n", $gitBranchLines); + } else { + $gitBranch = 'dev-master'; + } + + $postGenerator = new PostGenerator(new Differ(new UnifiedDiffOutputBuilder(''))); + + $application = new Application(); + $application->add(new DownloadCommand($client, new PlaygroundClient(new \GuzzleHttp\Client()), $issueCommentDownloader, $issueCachePath, $playgroundCachePath)); + $application->add(new RunCommand($playgroundCachePath, $tmpDir)); + $application->add(new EvaluateCommand(new TabCreator(), $postGenerator, $client, $issueCommentDownloader, $issueCachePath, $playgroundCachePath, $tmpDir, $gitBranch, $phpstanSrcCommitBefore, $phpstanSrcCommitAfter)); + + $application->setCatchExceptions(false); + $application->run(); +})(); diff --git a/issue-bot/phpstan.neon b/issue-bot/phpstan.neon new file mode 100644 index 0000000000..7ff756707c --- /dev/null +++ b/issue-bot/phpstan.neon @@ -0,0 +1,13 @@ +includes: + - ../vendor/phpstan/phpstan-deprecation-rules/rules.neon + - ../vendor/phpstan/phpstan-phpunit/extension.neon + - ../vendor/phpstan/phpstan-phpunit/rules.neon + - ../vendor/phpstan/phpstan-strict-rules/rules.neon + - ../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - src + - tests + - console.php diff --git a/issue-bot/phpunit.xml b/issue-bot/phpunit.xml new file mode 100644 index 0000000000..0946e9b886 --- /dev/null +++ b/issue-bot/phpunit.xml @@ -0,0 +1,28 @@ + + + + + src + + + + + + + + + tests + + + + diff --git a/issue-bot/playground.neon b/issue-bot/playground.neon new file mode 100644 index 0000000000..d4072a4170 --- /dev/null +++ b/issue-bot/playground.neon @@ -0,0 +1,13 @@ +rules: + - PHPStan\Rules\Playground\FunctionNeverRule + - PHPStan\Rules\Playground\MethodNeverRule + - PHPStan\Rules\Playground\NotAnalysedTraitRule + - PHPStan\Rules\Playground\NoPhpCodeRule + +conditionalTags: + PHPStan\Rules\Playground\StaticVarWithoutTypeRule: + phpstan.rules.rule: %checkImplicitMixed% + +services: + - + class: PHPStan\Rules\Playground\StaticVarWithoutTypeRule diff --git a/issue-bot/src/.gitkeep b/issue-bot/src/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/issue-bot/src/Comment/BotComment.php b/issue-bot/src/Comment/BotComment.php new file mode 100644 index 0000000000..3fbc351a6e --- /dev/null +++ b/issue-bot/src/Comment/BotComment.php @@ -0,0 +1,32 @@ +resultHash = $playgroundExample->getHash(); + } + + public function getResultHash(): string + { + return $this->resultHash; + } + + public function getDiff(): string + { + return $this->diff; + } + +} diff --git a/issue-bot/src/Comment/BotCommentParser.php b/issue-bot/src/Comment/BotCommentParser.php new file mode 100644 index 0000000000..c09d09263d --- /dev/null +++ b/issue-bot/src/Comment/BotCommentParser.php @@ -0,0 +1,63 @@ +docParser->parse($text); + $walker = $document->walker(); + $hashes = []; + $diffs = []; + while ($event = $walker->next()) { + if (!$event->isEntering()) { + continue; + } + + $node = $event->getNode(); + if ($node instanceof Link) { + $url = $node->getUrl(); + $match = Strings::match($url, '/^https:\/\/phpstan\.org\/r\/([0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12})$/i'); + if ($match === null) { + continue; + } + + $hashes[] = $match[1]; + continue; + } + + if (!($node instanceof FencedCode)) { + continue; + } + + if ($node->getInfo() !== 'diff') { + continue; + } + + $diffs[] = $node->getLiteral(); + } + + if (count($hashes) !== 1) { + throw new BotCommentParserException(); + } + + if (count($diffs) !== 1) { + throw new BotCommentParserException(); + } + + return new BotCommentParserResult($hashes[0], $diffs[0]); + } + +} diff --git a/issue-bot/src/Comment/BotCommentParserException.php b/issue-bot/src/Comment/BotCommentParserException.php new file mode 100644 index 0000000000..8b4a7d7794 --- /dev/null +++ b/issue-bot/src/Comment/BotCommentParserException.php @@ -0,0 +1,10 @@ +hash; + } + + public function getDiff(): string + { + return $this->diff; + } + +} diff --git a/issue-bot/src/Comment/Comment.php b/issue-bot/src/Comment/Comment.php new file mode 100644 index 0000000000..e29701b08d --- /dev/null +++ b/issue-bot/src/Comment/Comment.php @@ -0,0 +1,39 @@ + $playgroundExamples + */ + public function __construct( + private string $author, + private string $text, + private array $playgroundExamples, + ) + { + } + + public function getAuthor(): string + { + return $this->author; + } + + public function getText(): string + { + return $this->text; + } + + /** + * @return non-empty-list + */ + public function getPlaygroundExamples(): array + { + return $this->playgroundExamples; + } + +} diff --git a/issue-bot/src/Comment/IssueCommentDownloader.php b/issue-bot/src/Comment/IssueCommentDownloader.php new file mode 100644 index 0000000000..f676390289 --- /dev/null +++ b/issue-bot/src/Comment/IssueCommentDownloader.php @@ -0,0 +1,92 @@ + + */ + public function getComments(int $issueNumber): array + { + $comments = []; + foreach ($this->downloadComments($issueNumber) as $issueComment) { + $commentExamples = $this->searchBody($issueComment['body']); + if (count($commentExamples) === 0) { + continue; + } + + if ($issueComment['user']['login'] === 'phpstan-bot') { + $parserResult = $this->botCommentParser->parse($issueComment['body']); + if (count($commentExamples) !== 1 || $commentExamples[0]->getHash() !== $parserResult->getHash()) { + throw new BotCommentParserException(); + } + + $comments[] = new BotComment($issueComment['body'], $commentExamples[0], $parserResult->getDiff()); + continue; + } + + $comments[] = new Comment($issueComment['user']['login'], $issueComment['body'], $commentExamples); + } + + return $comments; + } + + /** + * @return mixed[] + */ + private function downloadComments(int $issueNumber): array + { + $page = 1; + + /** @var Issue $api */ + $api = $this->githubClient->api('issue'); + + $comments = []; + while (true) { + $newComments = $api->comments()->all('phpstan', 'phpstan', $issueNumber, [ + 'page' => $page, + 'per_page' => 100, + ]); + $comments = array_merge($comments, $newComments); + if (count($newComments) < 100) { + break; + } + $page++; + } + + return $comments; + } + + /** + * @return list + */ + public function searchBody(string $text): array + { + $matches = Strings::matchAll($text, '/https:\/\/phpstan\.org\/r\/([0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12})/i'); + + $examples = []; + + foreach ($matches as [$url, $hash]) { + $examples[] = new PlaygroundExample($url, $hash); + } + + return $examples; + } + +} diff --git a/issue-bot/src/Console/DownloadCommand.php b/issue-bot/src/Console/DownloadCommand.php new file mode 100644 index 0000000000..fb0855b792 --- /dev/null +++ b/issue-bot/src/Console/DownloadCommand.php @@ -0,0 +1,258 @@ +setName('download'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $issues = $this->getIssues(); + + $playgroundCache = $this->loadPlaygroundCache(); + if ($playgroundCache === null) { + $cachedResults = []; + } else { + $cachedResults = $playgroundCache->getResults(); + } + + $unusedCachedResults = $cachedResults; + + $deduplicatedExamples = []; + foreach ($issues as $issue) { + foreach ($issue->getComments() as $comment) { + if ($comment instanceof BotComment) { + continue; + } + foreach ($comment->getPlaygroundExamples() as $example) { + $deduplicatedExamples[$example->getHash()] = $example; + } + } + } + + $hashes = array_keys($deduplicatedExamples); + foreach ($hashes as $hash) { + if (array_key_exists($hash, $cachedResults)) { + unset($unusedCachedResults[$hash]); + continue; + } + + $cachedResults[$hash] = $this->playgroundClient->getResult($hash); + } + + foreach (array_keys($unusedCachedResults) as $hash) { + unset($cachedResults[$hash]); + } + + $this->savePlaygroundCache(new PlaygroundCache($cachedResults)); + + $chunkSize = (int) ceil(count($hashes) / 20); + if ($chunkSize < 1) { + throw new Exception('Chunk size less than 1'); + } + + $matrix = []; + foreach ([70300, 70400, 80000, 80100, 80200, 80300, 80400] as $phpVersion) { + $phpVersionHashes = []; + foreach ($cachedResults as $hash => $result) { + $resultPhpVersions = array_keys($result->getVersionedErrors()); + if ($resultPhpVersions === [70400]) { + $resultPhpVersions = [70300, 70400, 80000]; + } + + if (!in_array(80100, $resultPhpVersions, true)) { + $resultPhpVersions[] = 80100; + } + if (!in_array(80200, $resultPhpVersions, true)) { + $resultPhpVersions[] = 80200; + } + if (!in_array(80300, $resultPhpVersions, true)) { + $resultPhpVersions[] = 80300; + } + if (!in_array(80400, $resultPhpVersions, true)) { + $resultPhpVersions[] = 80400; + } + + if (!in_array($phpVersion, $resultPhpVersions, true)) { + continue; + } + $phpVersionHashes[] = $hash; + } + $chunkSize = (int) ceil(count($phpVersionHashes) / 18); + if ($chunkSize < 1) { + throw new Exception('Chunk size less than 1'); + } + $chunks = array_chunk($phpVersionHashes, $chunkSize); + $i = 1; + foreach ($chunks as $chunk) { + $matrix[] = [ + 'phpVersion' => $phpVersion, + 'chunkNumber' => $i, + 'playgroundExamples' => implode(',', $chunk), + ]; + $i++; + } + } + + $output->writeln(Json::encode(['include' => $matrix])); + + return 0; + } + + /** + * @return Issue[] + */ + private function getIssues(): array + { + /** @var \Github\Api\Issue $api */ + $api = $this->githubClient->api('issue'); + + $cache = $this->loadIssueCache(); + $newDate = new DateTimeImmutable(); + + $issues = []; + foreach (['feature-request', 'bug'] as $label) { + $page = 1; + while (true) { + $parameters = [ + 'labels' => $label, + 'page' => $page, + 'per_page' => 100, + 'sort' => 'created', + 'direction' => 'desc', + ]; + if ($cache !== null) { + $parameters['state'] = 'all'; + $parameters['since'] = $cache->getDate()->format(DateTimeImmutable::ATOM); + } else { + $parameters['state'] = 'open'; + } + $newIssues = $api->all('phpstan', 'phpstan', $parameters); + $issues = array_merge($issues, $newIssues); + if (count($newIssues) < 100) { + break; + } + + $page++; + } + } + + $issueObjects = []; + if ($cache !== null) { + $issueObjects = $cache->getIssues(); + } + foreach ($issues as $issue) { + if ($issue['state'] === 'closed') { + unset($issueObjects[$issue['number']]); + continue; + } + $comments = []; + $issueExamples = $this->issueCommentDownloader->searchBody($issue['body']); + if (count($issueExamples) > 0) { + $comments[] = new Comment($issue['user']['login'], $issue['body'], $issueExamples); + } + + foreach ($this->issueCommentDownloader->getComments($issue['number']) as $issueComment) { + $comments[] = $issueComment; + } + + $issueObjects[(int) $issue['number']] = new Issue( + $issue['number'], + $comments, + ); + } + + $this->saveIssueCache(new IssueCache($newDate, $issueObjects)); + + return $issueObjects; + } + + private function loadIssueCache(): ?IssueCache + { + if (!is_file($this->issueCachePath)) { + return null; + } + + $contents = file_get_contents($this->issueCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + + private function saveIssueCache(IssueCache $cache): void + { + $result = file_put_contents($this->issueCachePath, serialize($cache)); + if ($result === false) { + throw new Exception('Write unsuccessful'); + } + } + + private function loadPlaygroundCache(): ?PlaygroundCache + { + if (!is_file($this->playgroundCachePath)) { + return null; + } + + $contents = file_get_contents($this->playgroundCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + + private function savePlaygroundCache(PlaygroundCache $cache): void + { + $result = file_put_contents($this->playgroundCachePath, serialize($cache)); + if ($result === false) { + throw new Exception('Write unsuccessful'); + } + } + +} diff --git a/issue-bot/src/Console/EvaluateCommand.php b/issue-bot/src/Console/EvaluateCommand.php new file mode 100644 index 0000000000..9836d85bb0 --- /dev/null +++ b/issue-bot/src/Console/EvaluateCommand.php @@ -0,0 +1,319 @@ +setName('evaluate'); + $this->addOption('post-comments', null, InputOption::VALUE_NONE); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $issueCache = $this->loadIssueCache(); + $originalResults = $this->loadPlaygroundCache()->getResults(); + $newResults = $this->loadResults(); + $toPost = []; + $totalCodeSnippets = 0; + + foreach ($issueCache->getIssues() as $issue) { + $botComments = []; + $deduplicatedExamples = []; + foreach ($issue->getComments() as $comment) { + if ($comment instanceof BotComment) { + $botComments[] = $comment; + continue; + } + foreach ($comment->getPlaygroundExamples() as $example) { + if (isset($deduplicatedExamples[$example->getHash()])) { + $deduplicatedExamples[$example->getHash()]['users'][] = $comment->getAuthor(); + $deduplicatedExamples[$example->getHash()]['users'] = array_values(array_unique($deduplicatedExamples[$example->getHash()]['users'])); + continue; + } + $deduplicatedExamples[$example->getHash()] = [ + 'example' => $example, + 'users' => [$comment->getAuthor()], + ]; + } + } + + $totalCodeSnippets += count($deduplicatedExamples); + foreach ($deduplicatedExamples as ['example' => $example, 'users' => $users]) { + $hash = $example->getHash(); + if (!array_key_exists($hash, $originalResults)) { + throw new Exception(sprintf('Hash %s does not exist in original results.', $hash)); + } + + $originalErrors = $originalResults[$hash]->getVersionedErrors(); + $originalTabs = $this->tabCreator->create($originalErrors); + + if (!array_key_exists($hash, $newResults)) { + throw new Exception(sprintf('Hash %s does not exist in new results.', $hash)); + } + + $originalPhpVersions = array_keys($originalErrors); + $newResult = $newResults[$hash]; + if (array_key_exists(70100, $originalErrors) || $originalPhpVersions === [70400]) { + $newResult[70100] = $newResult[70300]; + } + if (array_key_exists(70200, $originalErrors)) { + $newResult[70200] = $newResult[70300]; + } + + $newTabs = $this->tabCreator->create($this->filterErrors($originalErrors, $newResult)); + $text = $this->postGenerator->createText($hash, $originalTabs, $newTabs, $botComments); + if ($text === null) { + continue; + } + + if ($this->isIssueClosed($issue->getNumber())) { + continue; + } + + $freshBotComments = $this->getFreshBotComments($issue->getNumber()); + $textAgain = $this->postGenerator->createText($hash, $originalTabs, $newTabs, $freshBotComments); + if ($textAgain === null) { + continue; + } + + $toPost[] = [ + 'issue' => $issue->getNumber(), + 'hash' => $hash, + 'users' => $users, + 'diff' => $text['diff'], + 'details' => $text['details'], + ]; + } + } + + if (count($toPost) === 0) { + $output->writeln(sprintf('No changes in results in %d code snippets from %d GitHub issues. :tada:', $totalCodeSnippets, count($issueCache->getIssues()))); + } + + foreach ($toPost as ['issue' => $issue, 'hash' => $hash, 'users' => $users, 'diff' => $diff, 'details' => $details]) { + $text = sprintf( + "Result of the [code snippet](https://phpstan.org/r/%s) from %s in [#%d](https://github.com/phpstan/phpstan/issues/%d) changed:\n\n```diff\n%s```", + $hash, + implode(' ', array_map(static fn (string $user): string => sprintf('@%s', $user), $users)), + $issue, + $issue, + $diff, + ); + if ($details !== null) { + $text .= "\n\n" . sprintf('
+ Full report + +%s +
', $details); + } + + $text .= "\n\n---\n"; + + $output->writeln($text); + } + + $postComments = (bool) $input->getOption('post-comments'); + if ($postComments) { + if (count($toPost) > 20) { + $output->writeln('Too many comments to post, something is probably wrong.'); + return 1; + } + foreach ($toPost as ['issue' => $issue, 'hash' => $hash, 'users' => $users, 'diff' => $diff, 'details' => $details]) { + $text = sprintf( + "%s After [the latest push in %s](https://github.com/phpstan/phpstan-src/compare/%s...%s), PHPStan now reports different result with your [code snippet](https://phpstan.org/r/%s):\n\n```diff\n%s```", + implode(' ', array_map(static fn (string $user): string => sprintf('@%s', $user), $users)), + $this->gitBranch, + $this->phpstanSrcCommitBefore, + $this->phpstanSrcCommitAfter, + $hash, + $diff, + ); + if ($details !== null) { + $text .= "\n\n" . sprintf('
+ Full report + +%s +
', $details); + } + + /** @var GitHubIssueApi $issueApi */ + $issueApi = $this->githubClient->api('issue'); + $issueApi->comments()->create('phpstan', 'phpstan', $issue, [ + 'body' => $text, + ]); + } + } + + return 0; + } + + /** + * @return list + */ + private function getFreshBotComments(int $issueNumber): array + { + $comments = []; + foreach ($this->issueCommentDownloader->getComments($issueNumber) as $issueComment) { + if (!$issueComment instanceof BotComment) { + continue; + } + + $comments[] = $issueComment; + } + + return $comments; + } + + private function isIssueClosed(int $issueNumber): bool + { + /** @var GitHubIssueApi $issueApi */ + $issueApi = $this->githubClient->api('issue'); + $issue = $issueApi->show('phpstan', 'phpstan', $issueNumber); + + return $issue['state'] === 'closed'; + } + + private function loadIssueCache(): IssueCache + { + if (!is_file($this->issueCachePath)) { + throw new Exception('Issue cache must exist'); + } + + $contents = file_get_contents($this->issueCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + + private function loadPlaygroundCache(): PlaygroundCache + { + if (!is_file($this->playgroundCachePath)) { + throw new Exception('Playground cache must exist'); + } + + $contents = file_get_contents($this->playgroundCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + + /** + * @return array>> + */ + private function loadResults(): array + { + $finder = new Finder(); + $tmpResults = []; + foreach ($finder->files()->name('results-*.tmp')->in($this->tmpDir) as $resultFile) { + $contents = file_get_contents($resultFile->getPathname()); + if ($contents === false) { + throw new Exception('Result read unsuccessful'); + } + $result = unserialize($contents); + $phpVersion = (int) $result['phpVersion']; + foreach ($result['errors'] as $hash => $errors) { + $tmpResults[(string) $hash][$phpVersion] = array_values($errors); + } + } + + return $tmpResults; + } + + /** + * @param array> $originalErrors + * @param array> $newErrors + * @return array> + */ + private function filterErrors( + array $originalErrors, + array $newErrors, + ): array + { + $originalPhpVersions = array_keys($originalErrors); + $filteredNewErrors = []; + foreach ($newErrors as $phpVersion => $errors) { + if (!in_array($phpVersion, $originalPhpVersions, true)) { + continue; + } + + $filteredNewErrors[$phpVersion] = $errors; + } + + $newTabs = $this->tabCreator->create($newErrors); + $filteredNewTabs = $this->tabCreator->create($filteredNewErrors); + if (count($newTabs) !== count($filteredNewTabs)) { + return $newErrors; + } + + $firstFilteredNewTab = $filteredNewTabs[0]; + $firstNewTab = $newTabs[0]; + + if (count($firstFilteredNewTab->getErrors()) !== count($firstNewTab->getErrors())) { + return $newErrors; + } + + foreach ($firstFilteredNewTab->getErrors() as $i => $error) { + $otherError = $firstNewTab->getErrors()[$i]; + if ($error->getLine() !== $otherError->getLine()) { + return $newErrors; + } + if ($error->getMessage() !== $otherError->getMessage()) { + return $newErrors; + } + } + + return $filteredNewErrors; + } + +} diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php new file mode 100644 index 0000000000..763db57cd0 --- /dev/null +++ b/issue-bot/src/Console/RunCommand.php @@ -0,0 +1,150 @@ +setName('run'); + $this->addArgument('phpVersion', InputArgument::REQUIRED); + $this->addArgument('playgroundHashes', InputArgument::REQUIRED); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $phpVersion = (int) $input->getArgument('phpVersion'); + $commaSeparatedPlaygroundHashes = $input->getArgument('playgroundHashes'); + $playgroundHashes = explode(',', $commaSeparatedPlaygroundHashes); + $playgroundCache = $this->loadPlaygroundCache(); + $errors = []; + foreach ($playgroundHashes as $hash) { + if (!array_key_exists($hash, $playgroundCache->getResults())) { + throw new Exception(sprintf('Hash %s must exist', $hash)); + } + $errors[$hash] = $this->analyseHash($output, $phpVersion, $playgroundCache->getResults()[$hash]); + } + + $data = ['phpVersion' => $phpVersion, 'errors' => $errors]; + + $writeSuccess = file_put_contents(sprintf($this->tmpDir . '/results-%d-%s.tmp', $phpVersion, sha1($commaSeparatedPlaygroundHashes)), serialize($data)); + if ($writeSuccess === false) { + throw new Exception('Result write unsuccessful'); + } + + return 0; + } + + /** + * @return list + */ + private function analyseHash(OutputInterface $output, int $phpVersion, PlaygroundResult $result): array + { + $configFiles = [ + __DIR__ . '/../../playground.neon', + ]; + if ($result->isBleedingEdge()) { + $configFiles[] = __DIR__ . '/../../../conf/bleedingEdge.neon'; + } + if ($result->isStrictRules()) { + $configFiles[] = __DIR__ . '/../../vendor/phpstan/phpstan-strict-rules/rules.neon'; + } + $neon = Neon::encode([ + 'includes' => $configFiles, + 'parameters' => [ + 'level' => $result->getLevel(), + 'inferPrivatePropertyTypeFromConstructor' => true, + 'treatPhpDocTypesAsCertain' => $result->isTreatPhpDocTypesAsCertain(), + 'phpVersion' => $phpVersion, + ], + ]); + + $hash = $result->getHash(); + $neonPath = sprintf($this->tmpDir . '/%s.neon', $hash); + $codePath = sprintf($this->tmpDir . '/%s.php', $hash); + file_put_contents($neonPath, $neon); + file_put_contents($codePath, $result->getCode()); + + $commandArray = [ + __DIR__ . '/../../../bin/phpstan', + 'analyse', + '--error-format', + 'json', + '--no-progress', + '-c', + $neonPath, + $codePath, + ]; + + $output->writeln(sprintf('Starting analysis of %s', $hash)); + + $startTime = microtime(true); + exec(implode(' ', $commandArray), $outputLines, $exitCode); + $elapsedTime = microtime(true) - $startTime; + $output->writeln(sprintf('Analysis of %s took %.2f s', $hash, $elapsedTime)); + + if ($exitCode !== 0 && $exitCode !== 1) { + throw new Exception(sprintf('PHPStan exited with code %d during analysis of %s', $exitCode, $hash)); + } + + $json = Json::decode(implode("\n", $outputLines), Json::FORCE_ARRAY); + $errors = []; + foreach ($json['files'] as ['messages' => $messages]) { + foreach ($messages as $message) { + $messageText = str_replace(sprintf('/%s.php', $hash), '/tmp.php', $message['message']); + if (strpos($messageText, 'Internal error') !== false) { + throw new Exception(sprintf('While analysing %s: %s', $hash, $messageText)); + } + $errors[] = new PlaygroundError($message['line'] ?? -1, $messageText, $message['identifier'] ?? null); + } + } + + return $errors; + } + + private function loadPlaygroundCache(): PlaygroundCache + { + if (!is_file($this->playgroundCachePath)) { + throw new Exception('Playground cache must exist'); + } + + $contents = file_get_contents($this->playgroundCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + +} diff --git a/issue-bot/src/GitHub/RateLimitPlugin.php b/issue-bot/src/GitHub/RateLimitPlugin.php new file mode 100644 index 0000000000..ce0bbf8caa --- /dev/null +++ b/issue-bot/src/GitHub/RateLimitPlugin.php @@ -0,0 +1,47 @@ +client = $client; + } + + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $path = $request->getUri()->getPath(); + if ($path === '/rate_limit') { + return $next($request); + } + + /** @var RateLimit $api */ + $api = $this->client->api('rate_limit'); + + /** @var RateLimitResource $resource */ + $resource = $api->getResource('core'); + if ($resource->getRemaining() < 10) { + $reset = $resource->getReset(); + $sleepFor = $reset - time(); + if ($sleepFor > 0) { + sleep($sleepFor); + } + } + + return $next($request); + } + +} diff --git a/issue-bot/src/GitHub/RequestCounterPlugin.php b/issue-bot/src/GitHub/RequestCounterPlugin.php new file mode 100644 index 0000000000..042cb53202 --- /dev/null +++ b/issue-bot/src/GitHub/RequestCounterPlugin.php @@ -0,0 +1,30 @@ +getUri()->getPath(); + if ($path === '/rate_limit') { + return $next($request); + } + + $this->totalCount++; + return $next($request); + } + + public function getTotalCount(): int + { + return $this->totalCount; + } + +} diff --git a/issue-bot/src/Issue/Issue.php b/issue-bot/src/Issue/Issue.php new file mode 100644 index 0000000000..a9feaf01d8 --- /dev/null +++ b/issue-bot/src/Issue/Issue.php @@ -0,0 +1,30 @@ +number; + } + + /** + * @return Comment[] + */ + public function getComments(): array + { + return $this->comments; + } + +} diff --git a/issue-bot/src/Issue/IssueCache.php b/issue-bot/src/Issue/IssueCache.php new file mode 100644 index 0000000000..6b5ed53fec --- /dev/null +++ b/issue-bot/src/Issue/IssueCache.php @@ -0,0 +1,30 @@ + $issues + */ + public function __construct(private DateTimeImmutable $date, private array $issues) + { + } + + public function getDate(): DateTimeImmutable + { + return $this->date; + } + + /** + * @return array + */ + public function getIssues(): array + { + return $this->issues; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundCache.php b/issue-bot/src/Playground/PlaygroundCache.php new file mode 100644 index 0000000000..cda495a831 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundCache.php @@ -0,0 +1,23 @@ + $results + */ + public function __construct(private array $results) + { + } + + /** + * @return array + */ + public function getResults(): array + { + return $this->results; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundClient.php b/issue-bot/src/Playground/PlaygroundClient.php new file mode 100644 index 0000000000..43cd6ea9d3 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundClient.php @@ -0,0 +1,42 @@ +client->get(sprintf('https://api.phpstan.org/sample?id=%s', $hash)); + + $body = (string) $response->getBody(); + $json = Json::decode($body, Json::FORCE_ARRAY); + + $versionedErrors = []; + foreach ($json['versionedErrors'] as ['phpVersion' => $phpVersion, 'errors' => $errors]) { + $versionedErrors[(int) $phpVersion] = array_map(static fn (array $error) => new PlaygroundError($error['line'] ?? -1, $error['message'], $error['identifier'] ?? null), array_values($errors)); + } + + return new PlaygroundResult( + sprintf('https://phpstan.org/r/%s', $hash), + $hash, + $json['code'], + $json['level'], + $json['config']['strictRules'], + $json['config']['bleedingEdge'], + $json['config']['treatPhpDocTypesAsCertain'], + $versionedErrors, + ); + } + +} diff --git a/issue-bot/src/Playground/PlaygroundError.php b/issue-bot/src/Playground/PlaygroundError.php new file mode 100644 index 0000000000..1e55ac88b3 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundError.php @@ -0,0 +1,27 @@ +line; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getIdentifier(): ?string + { + return $this->identifier; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundExample.php b/issue-bot/src/Playground/PlaygroundExample.php new file mode 100644 index 0000000000..0d25f7bfbb --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundExample.php @@ -0,0 +1,25 @@ +url; + } + + public function getHash(): string + { + return $this->hash; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundResult.php b/issue-bot/src/Playground/PlaygroundResult.php new file mode 100644 index 0000000000..faddc6c078 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundResult.php @@ -0,0 +1,67 @@ +> $versionedErrors + */ + public function __construct( + private string $url, + private string $hash, + private string $code, + private string $level, + private bool $strictRules, + private bool $bleedingEdge, + private bool $treatPhpDocTypesAsCertain, + private array $versionedErrors, + ) + { + } + + public function getUrl(): string + { + return $this->url; + } + + public function getHash(): string + { + return $this->hash; + } + + public function getCode(): string + { + return $this->code; + } + + public function getLevel(): string + { + return $this->level; + } + + public function isStrictRules(): bool + { + return $this->strictRules; + } + + public function isBleedingEdge(): bool + { + return $this->bleedingEdge; + } + + public function isTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + /** + * @return array> + */ + public function getVersionedErrors(): array + { + return $this->versionedErrors; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundResultTab.php b/issue-bot/src/Playground/PlaygroundResultTab.php new file mode 100644 index 0000000000..03d33ac7d4 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundResultTab.php @@ -0,0 +1,28 @@ + $errors + */ + public function __construct(private string $title, private array $errors) + { + } + + public function getTitle(): string + { + return $this->title; + } + + /** + * @return list + */ + public function getErrors(): array + { + return $this->errors; + } + +} diff --git a/issue-bot/src/Playground/TabCreator.php b/issue-bot/src/Playground/TabCreator.php new file mode 100644 index 0000000000..a6254957b2 --- /dev/null +++ b/issue-bot/src/Playground/TabCreator.php @@ -0,0 +1,130 @@ +> $versionedErrors + * @return list + */ + public function create(array $versionedErrors): array + { + ksort($versionedErrors, SORT_NUMERIC); + + $versions = []; + $last = null; + + foreach ($versionedErrors as $phpVersion => $errors) { + $errors = array_values(array_filter($errors, static fn (PlaygroundError $error) => $error->getIdentifier() !== 'phpstanPlayground.configParameter')); + $errors = array_map(static function (PlaygroundError $error): PlaygroundError { + if ($error->getIdentifier() === null) { + return $error; + } + + if (!str_starts_with($error->getIdentifier(), 'phpstanPlayground.')) { + return $error; + } + + return new PlaygroundError( + $error->getLine(), + sprintf('Tip: %s', $error->getMessage()), + $error->getIdentifier(), + ); + }, $errors); + $current = [ + 'versions' => [$phpVersion], + 'errors' => $errors, + ]; + if ($last === null) { + $last = $current; + continue; + } + + if (count($errors) !== count($last['errors'])) { + $versions[] = $last; + $last = $current; + continue; + } + + $merge = true; + foreach ($errors as $i => $error) { + $lastError = $last['errors'][$i]; + if ($error->getLine() !== $lastError->getLine()) { + $versions[] = $last; + $last = $current; + $merge = false; + break; + } + if ($error->getMessage() !== $lastError->getMessage()) { + $versions[] = $last; + $last = $current; + $merge = false; + break; + } + } + + if (!$merge) { + continue; + } + + $last['versions'][] = $phpVersion; + } + + if ($last !== null) { + $versions[] = $last; + } + + usort($versions, static function ($a, $b): int { + $aVersion = $a['versions'][count($a['versions']) - 1]; + $bVersion = $b['versions'][count($b['versions']) - 1]; + + return $bVersion - $aVersion; + }); + + $tabs = []; + + foreach ($versions as $version) { + $title = 'PHP '; + if (count($version['versions']) > 1) { + $title .= $this->versionNumberToString($version['versions'][0]); + $title .= ' – '; + $title .= $this->versionNumberToString($version['versions'][count($version['versions']) - 1]); + } else { + $title .= $this->versionNumberToString($version['versions'][0]); + } + + if (count($version['errors']) === 1) { + $title .= ' (1 error)'; + } elseif (count($version['errors']) > 0) { + $title .= ' (' . count($version['errors']) . ' errors)'; + } + + $tabs[] = new PlaygroundResultTab($title, $version['errors']); + } + + return $tabs; + } + + private function versionNumberToString(int $versionId): string + { + $first = (int) floor($versionId / 10000); + $second = (int) floor(($versionId % 10000) / 100); + $third = (int) floor($versionId % 100); + + return $first . '.' . $second . ($third !== 0 ? '.' . $third : ''); + } + +} diff --git a/issue-bot/src/PostGenerator.php b/issue-bot/src/PostGenerator.php new file mode 100644 index 0000000000..db62d448a8 --- /dev/null +++ b/issue-bot/src/PostGenerator.php @@ -0,0 +1,138 @@ + $originalTabs + * @param list $currentTabs + * @param BotComment[] $botComments + * @return array{diff: string, details: string|null}|null + */ + public function createText( + string $hash, + array $originalTabs, + array $currentTabs, + array $botComments, + ): ?array + { + foreach ($currentTabs as $tab) { + foreach ($tab->getErrors() as $error) { + if (strpos($error->getMessage(), 'Internal error') === false) { + continue; + } + + return null; + } + } + + $maxDigit = 1; + foreach (array_merge($originalTabs, $currentTabs) as $tab) { + foreach ($tab->getErrors() as $error) { + $length = strlen((string) $error->getLine()); + if ($length <= $maxDigit) { + continue; + } + + $maxDigit = $length; + } + } + $originalErrorsText = $this->generateTextFromTabs($originalTabs, $maxDigit); + $currentErrorsText = $this->generateTextFromTabs($currentTabs, $maxDigit); + if ($originalErrorsText === $currentErrorsText) { + return null; + } + + $diff = $this->differ->diff($originalErrorsText, $currentErrorsText); + foreach ($botComments as $botComment) { + if ($botComment->getResultHash() !== $hash) { + continue; + } + + if ($botComment->getDiff() === $diff) { + return null; + } + } + + if (count($currentTabs) === 1 && count($currentTabs[0]->getErrors()) === 0) { + return ['diff' => $diff, 'details' => null]; + } + + $details = []; + foreach ($currentTabs as $tab) { + $detail = ''; + if (count($currentTabs) > 1) { + $detail .= sprintf("%s\n-----------\n\n", $tab->getTitle()); + } + + if (count($tab->getErrors()) === 0) { + $detail .= "No errors\n"; + $details[] = $detail; + continue; + } + + $detail .= "| Line | Error |\n"; + $detail .= "|---|---|\n"; + + foreach ($tab->getErrors() as $error) { + $errorText = Strings::replace($error->getMessage(), "/\r|\n/", ''); + $detail .= sprintf("| %d | `%s` |\n", $error->getLine(), $errorText); + } + + $details[] = $detail; + } + + return ['diff' => $diff, 'details' => implode("\n", $details)]; + } + + /** + * @param PlaygroundResultTab[] $tabs + */ + private function generateTextFromTabs(array $tabs, int $maxDigit): string + { + $parts = []; + foreach ($tabs as $tab) { + $text = ''; + if (count($tabs) > 1) { + $text .= sprintf("%s\n==========\n\n", $tab->getTitle()); + } + + if (count($tab->getErrors()) === 0) { + $text .= 'No errors'; + $parts[] = $text; + continue; + } + + $errorLines = []; + foreach ($tab->getErrors() as $error) { + $errorLines[] = sprintf('%s: %s', str_pad((string) $error->getLine(), $maxDigit, ' ', STR_PAD_LEFT), $error->getMessage()); + } + + $text .= implode("\n", $errorLines); + + $parts[] = $text; + } + + return implode("\n\n", $parts); + } + +} diff --git a/issue-bot/tests/Comment/BotCommentParserResultTest.php b/issue-bot/tests/Comment/BotCommentParserResultTest.php new file mode 100644 index 0000000000..123c8034f1 --- /dev/null +++ b/issue-bot/tests/Comment/BotCommentParserResultTest.php @@ -0,0 +1,49 @@ + + */ + public function dataParse(): iterable + { + yield [ + '@foobar After [the latest commit to dev-master](https://github.com/phpstan/phpstan-src/commit/abc123), PHPStan now reports different result with your [code snippet](https://phpstan.org/r/74c3b0af-5a87-47e7-907a-9ea6fbb1c396): + +```diff +@@ @@ +-1: abc ++1: def +```', + '74c3b0af-5a87-47e7-907a-9ea6fbb1c396', + '@@ @@ +-1: abc ++1: def +', + ]; + } + + /** + * @dataProvider dataParse + */ + public function testParse(string $text, string $expectedHash, string $expectedDiff): void + { + $markdownEnvironment = new Environment(); + $markdownEnvironment->addExtension(new CommonMarkCoreExtension()); + $markdownEnvironment->addExtension(new GithubFlavoredMarkdownExtension()); + $parser = new BotCommentParser(new MarkdownParser($markdownEnvironment)); + $result = $parser->parse($text); + self::assertSame($expectedHash, $result->getHash()); + self::assertSame($expectedDiff, $result->getDiff()); + } + +} diff --git a/issue-bot/tests/Playground/TabCreatorTest.php b/issue-bot/tests/Playground/TabCreatorTest.php new file mode 100644 index 0000000000..47bf147308 --- /dev/null +++ b/issue-bot/tests/Playground/TabCreatorTest.php @@ -0,0 +1,134 @@ +>, list}> + */ + public function dataCreate(): array + { + return [ + [ + [ + 70100 => [ + + ], + 70200 => [ + + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 – 7.2', []), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', null), + ], + 70200 => [ + new PlaygroundError(2, 'Foo', null), + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 – 7.2 (1 error)', [ + new PlaygroundError(2, 'Foo', null), + ]), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ], + 70200 => [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 – 7.2 (2 errors)', [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ]), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ], + 70200 => [ + new PlaygroundError(3, 'Foo', null), + ], + ], + [ + new PlaygroundResultTab('PHP 7.2 (1 error)', [ + new PlaygroundError(3, 'Foo', null), + ]), + new PlaygroundResultTab('PHP 7.1 (2 errors)', [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ]), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', 'attribute.notFound'), + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 (1 error)', [ + new PlaygroundError(2, 'Foo', null), + ]), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', 'phpstanPlayground.never'), + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 (1 error)', [ + new PlaygroundError(2, 'Tip: Foo', null), + ]), + ], + ], + ]; + } + + /** + * @dataProvider dataCreate + * @param array> $versionedErrors + * @param list $expectedTabs + * @return void + */ + public function testCreate(array $versionedErrors, array $expectedTabs): void + { + $tabCreator = new TabCreator(); + $tabs = $tabCreator->create($versionedErrors); + self::assertCount(count($expectedTabs), $tabs); + + foreach ($tabs as $i => $tab) { + $expectedTab = $expectedTabs[$i]; + self::assertSame($expectedTab->getTitle(), $tab->getTitle()); + self::assertCount(count($expectedTab->getErrors()), $tab->getErrors()); + foreach ($tab->getErrors() as $j => $error) { + $expectedError = $expectedTab->getErrors()[$j]; + self::assertSame($expectedError->getMessage(), $error->getMessage()); + self::assertSame($expectedError->getLine(), $error->getLine()); + } + } + } + +} diff --git a/issue-bot/tests/PostGeneratorTest.php b/issue-bot/tests/PostGeneratorTest.php new file mode 100644 index 0000000000..06c30811c9 --- /dev/null +++ b/issue-bot/tests/PostGeneratorTest.php @@ -0,0 +1,133 @@ +, list, BotComment[], string|null}> + */ + public function dataGeneratePosts(): iterable + { + $diff = '@@ @@ +-1: abc ++1: def +'; + + $details = "| Line | Error | +|---|---| +| 1 | `def` | +"; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [], + null, + null, + ]; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'def', null), + ])], + [], + $diff, + $details, + ]; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'def', null), + ])], + [ + new BotComment('', new PlaygroundExample('', 'abc-def'), 'some diff'), + ], + $diff, + $details, + ]; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'def', null), + ])], + [ + new BotComment('', new PlaygroundExample('', 'abc-def'), $diff), + ], + null, + null, + ]; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'Internal error', null), + ])], + [], + null, + null, + ]; + } + + /** + * @dataProvider dataGeneratePosts + * @param list $originalTabs + * @param list $currentTabs + * @param BotComment[] $botComments + */ + public function testGeneratePosts( + string $hash, + array $originalTabs, + array $currentTabs, + array $botComments, + ?string $expectedDiff, + ?string $expectedDetails + ): void + { + $generator = new PostGenerator(new Differ(new UnifiedDiffOutputBuilder(''))); + $text = $generator->createText( + $hash, + $originalTabs, + $currentTabs, + $botComments, + ); + if ($text === null) { + self::assertNull($expectedDiff); + self::assertNull($expectedDetails); + return; + } + + self::assertSame($expectedDiff, $text['diff']); + self::assertSame($expectedDetails, $text['details']); + } + +} diff --git a/issue-bot/tmp/.gitignore b/issue-bot/tmp/.gitignore new file mode 100644 index 0000000000..125e34294b --- /dev/null +++ b/issue-bot/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.* diff --git a/patches/BooleanTypeMapper.patch b/patches/BooleanTypeMapper.patch deleted file mode 100644 index dfb247bb93..0000000000 --- a/patches/BooleanTypeMapper.patch +++ /dev/null @@ -1,13 +0,0 @@ -@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 index 9b2e2c7f86..1e50ecf112 100644 --- a/patches/Buffer.patch +++ b/patches/Buffer.patch @@ -1,5 +1,3 @@ -@package hoa/iterator - --- Buffer.php 2017-01-10 11:34:47.000000000 +0100 +++ Buffer.php 2021-10-30 16:36:22.000000000 +0200 @@ -103,7 +103,7 @@ diff --git a/patches/Consistency.patch b/patches/Consistency.patch index 73926b901a..4409109b36 100644 --- a/patches/Consistency.patch +++ b/patches/Consistency.patch @@ -1,5 +1,3 @@ -@package hoa/consistency - --- Consistency.php 2017-05-02 14:18:12.000000000 +0200 +++ Consistency.php 2020-05-05 08:28:35.000000000 +0200 @@ -319,42 +319,6 @@ diff --git a/patches/DependencyChecker.patch b/patches/DependencyChecker.patch new file mode 100644 index 0000000000..4902922537 --- /dev/null +++ b/patches/DependencyChecker.patch @@ -0,0 +1,13 @@ +--- src/DI/DependencyChecker.php 2023-10-02 21:58:38 ++++ src/DI/DependencyChecker.php 2024-07-07 09:24:35 +@@ -147,7 +147,9 @@ + $flip = array_flip($classes); + foreach ($functions as $name) { + if (strpos($name, '::')) { +- $method = new ReflectionMethod($name); ++ $method = PHP_VERSION_ID < 80300 ++ ? new ReflectionMethod($name) ++ : ReflectionMethod::createFromMethodName($name); + $class = $method->getDeclaringClass(); + if (isset($flip[$class->name])) { + continue; diff --git a/patches/File.patch b/patches/File.patch new file mode 100644 index 0000000000..0732eb0e55 --- /dev/null +++ b/patches/File.patch @@ -0,0 +1,11 @@ +--- File.php 2017-07-11 09:42:15 ++++ File.php 2024-08-26 23:13:27 +@@ -192,7 +192,7 @@ + * @throws \Hoa\File\Exception\FileDoesNotExist + * @throws \Hoa\File\Exception + */ +- protected function &_open($streamName, Stream\Context $context = null) ++ protected function &_open($streamName, ?Stream\Context $context = null) + { + if (substr($streamName, 0, 4) == 'file' && + false === is_dir(dirname($streamName))) { diff --git a/patches/HoaException.patch b/patches/HoaException.patch new file mode 100644 index 0000000000..bada616504 --- /dev/null +++ b/patches/HoaException.patch @@ -0,0 +1,11 @@ +--- Exception/Exception.php 2024-06-24 15:17:26 ++++ Exception/Exception.php 2024-06-24 15:17:51 +@@ -37,7 +37,7 @@ + namespace Hoa\Compiler\Exception; + + use Hoa\Consistency; +-use Hoa\Exception as HoaException; ++use Hoa\Exception\Exception as HoaException; + + /** + * Class \Hoa\Compiler\Exception. diff --git a/patches/Idle.patch b/patches/Idle.patch new file mode 100644 index 0000000000..6f6f0dbe29 --- /dev/null +++ b/patches/Idle.patch @@ -0,0 +1,11 @@ +--- Idle.php 2017-01-16 08:53:27 ++++ Idle.php 2024-08-26 23:18:04 +@@ -100,7 +100,7 @@ + $message, + $code = 0, + $arguments = [], +- \Exception $previous = null ++ ?\Exception $previous = null + ) { + $this->_tmpArguments = $arguments; + parent::__construct($message, $code, $previous); diff --git a/patches/Invocation.patch b/patches/Invocation.patch new file mode 100644 index 0000000000..a3e9e10965 --- /dev/null +++ b/patches/Invocation.patch @@ -0,0 +1,11 @@ +--- Llk/Rule/Invocation.php 2017-08-08 09:44:07 ++++ Llk/Rule/Invocation.php 2024-08-26 23:11:25 +@@ -95,7 +95,7 @@ + public function __construct( + $rule, + $data, +- array $todo = null, ++ ?array $todo = null, + $depth = -1 + ) { + $this->_rule = $rule; diff --git a/patches/Lexer.patch b/patches/Lexer.patch new file mode 100644 index 0000000000..b2d84ed6e9 --- /dev/null +++ b/patches/Lexer.patch @@ -0,0 +1,12 @@ +diff --git a/Llk/Lexer.php b/Llk/Lexer.php +index 6851367..b8acf98 100644 +--- a/Llk/Lexer.php ++++ b/Llk/Lexer.php +@@ -281,7 +281,7 @@ class Lexer + $offset + ); + +- if (0 === $preg) { ++ if (0 === $preg || $preg === false) { + return null; + } diff --git a/patches/Lookahead.patch b/patches/Lookahead.patch index b6c283492c..d17a378444 100644 --- a/patches/Lookahead.patch +++ b/patches/Lookahead.patch @@ -1,5 +1,3 @@ -@package hoa/iterator - --- Lookahead.php 2017-01-10 11:34:47.000000000 +0100 +++ Lookahead.php 2021-10-30 16:35:30.000000000 +0200 @@ -93,7 +93,7 @@ diff --git a/patches/NameNodeMapper.patch b/patches/NameNodeMapper.patch deleted file mode 100644 index 6ca3b0a748..0000000000 --- a/patches/NameNodeMapper.patch +++ /dev/null @@ -1,13 +0,0 @@ -@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 index 303ab36e75..d289251ebd 100644 --- a/patches/Node.patch +++ b/patches/Node.patch @@ -1,5 +1,3 @@ -@package hoa/protocol - --- Node/Node.php 2017-01-14 13:26:10.000000000 +0100 +++ Node/Node.php 2021-10-30 16:32:43.000000000 +0200 @@ -108,7 +108,7 @@ diff --git a/patches/PDO.patch b/patches/PDO.patch index 51d125c9d0..607a23fda2 100644 --- a/patches/PDO.patch +++ b/patches/PDO.patch @@ -1,14 +1,11 @@ -@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 {} +@@ -1476,7 +1476,7 @@ namespace { + * @return array|false if one or more notifications is pending, returns a single row, + * with fields message and pid, otherwise FALSE. + */ +- public function pgsqlGetNotify($fetchMode = PDO::FETCH_DEFAULT, $timeoutMilliseconds = 0) {} ++ public function pgsqlGetNotify($fetchMode = 1, $timeoutMilliseconds = 0) {} - /** - * (PHP 5 >= 5.6.0, PHP 7, PHP 8)
+ /** + * (PHP 5 >= 5.6.0, PHP 7, PHP 8)
diff --git a/patches/Read.patch b/patches/Read.patch new file mode 100644 index 0000000000..ad9b64c445 --- /dev/null +++ b/patches/Read.patch @@ -0,0 +1,11 @@ +--- Read.php 2017-07-11 09:42:15 ++++ Read.php 2024-08-26 23:09:54 +@@ -77,7 +77,7 @@ + * @throws \Hoa\File\Exception\FileDoesNotExist + * @throws \Hoa\File\Exception + */ +- protected function &_open($streamName, Stream\Context $context = null) ++ protected function &_open($streamName, ?Stream\Context $context = null) + { + static $createModes = [ + parent::MODE_READ diff --git a/patches/ReflectionProperty.patch b/patches/ReflectionProperty.patch new file mode 100644 index 0000000000..38d60b1bd4 --- /dev/null +++ b/patches/ReflectionProperty.patch @@ -0,0 +1,11 @@ +--- Reflection/ReflectionProperty.php 2023-09-07 12:59:56.000000000 +0200 ++++ Reflection/ReflectionProperty.php 2023-09-15 13:24:07.900736741 +0200 +@@ -248,7 +248,7 @@ + * Gets property type + * + * @link https://php.net/manual/en/reflectionproperty.gettype.php +- * @return ReflectionNamedType|ReflectionUnionType|null Returns a {@see ReflectionType} if the ++ * @return ReflectionType|null Returns a {@see ReflectionType} if the + * property has a type, and {@see null} otherwise. + * @since 7.4 + */ diff --git a/patches/Rule.patch b/patches/Rule.patch index 2efb475fe8..faf698a5ff 100644 --- a/patches/Rule.patch +++ b/patches/Rule.patch @@ -1,5 +1,3 @@ -@package hoa/compiler - --- Llk/Rule/Rule.php 2017-08-08 09:44:07.000000000 +0200 +++ Llk/Rule/Rule.php 2021-10-29 16:42:12.000000000 +0200 @@ -118,7 +118,10 @@ diff --git a/patches/Sender.patch b/patches/Sender.patch new file mode 100644 index 0000000000..e01d1ff81c --- /dev/null +++ b/patches/Sender.patch @@ -0,0 +1,11 @@ +--- src/Io/Sender.php 2024-03-27 18:20:46 ++++ src/Io/Sender.php 2024-10-14 10:19:28 +@@ -48,7 +48,7 @@ + * @param ConnectorInterface|null $connector + * @return self + */ +- public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) ++ public static function createFromLoop(LoopInterface $loop, ?ConnectorInterface $connector = null) + { + if ($connector === null) { + $connector = new Connector(array(), $loop); diff --git a/patches/SessionHandler.patch b/patches/SessionHandler.patch index 598c201951..ba45ffc1b5 100644 --- a/patches/SessionHandler.patch +++ b/patches/SessionHandler.patch @@ -1,6 +1,3 @@ -@package jetbrains/phpstorm-stubs -@version dev-master - --- session/SessionHandler.php 2021-11-04 14:27:30.000000000 +0100 +++ session/SessionHandler.php 2021-11-05 11:26:14.000000000 +0100 @@ -147,7 +147,7 @@ diff --git a/patches/Stream.patch b/patches/Stream.patch index 5abd708df1..8dbb2e108e 100644 --- a/patches/Stream.patch +++ b/patches/Stream.patch @@ -1,7 +1,5 @@ -@package hoa/stream - ---- Stream.php 2017-02-21 17:01:06.000000000 +0100 -+++ Stream.php 2021-04-19 17:10:20.000000000 +0200 +--- Stream.php 2024-08-26 23:05:49 ++++ Stream.php 2024-08-26 23:01:08 @@ -192,7 +192,7 @@ * @return array * @throws \Hoa\Stream\Exception @@ -11,6 +9,15 @@ $streamName, Stream $handler, $context = null +@@ -250,7 +250,7 @@ + * @return resource + * @throws \Hoa\Exception\Exception + */ +- abstract protected function &_open($streamName, Context $context = null); ++ abstract protected function &_open($streamName, ?Context $context = null); + + /** + * Close the current stream. @@ -687,11 +687,6 @@ Consistency::flexEntity('Hoa\Stream\Stream'); diff --git a/patches/TreeNode.patch b/patches/TreeNode.patch new file mode 100644 index 0000000000..39e7917def --- /dev/null +++ b/patches/TreeNode.patch @@ -0,0 +1,14 @@ +--- Llk/TreeNode.php 2017-08-08 09:44:07 ++++ Llk/TreeNode.php 2024-08-26 23:07:29 +@@ -95,9 +95,9 @@ + */ + public function __construct( + $id, +- array $value = null, ++ ?array $value = null, + array $children = [], +- self $parent = null ++ ?self $parent = null + ) { + $this->setId($id); + diff --git a/patches/Wrapper.patch b/patches/Wrapper.patch index ade4d0a2fe..b8282376bd 100644 --- a/patches/Wrapper.patch +++ b/patches/Wrapper.patch @@ -1,5 +1,3 @@ -@package hoa/protocol - --- Wrapper.php 2017-01-14 13:26:10.000000000 +0100 +++ Wrapper.php 2020-05-05 08:39:18.000000000 +0200 @@ -582,24 +582,3 @@ diff --git a/patches/cloudflare-ca.patch b/patches/cloudflare-ca.patch new file mode 100644 index 0000000000..57f6cf5a77 --- /dev/null +++ b/patches/cloudflare-ca.patch @@ -0,0 +1,27 @@ +--- ca-bundle/res/cacert.pem 2024-06-18 13:50:00 ++++ ca-bundle/res/cacert.pem 2024-06-18 13:50:29 +@@ -3579,3 +3579,24 @@ + HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0 + o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A= + -----END CERTIFICATE----- ++ ++Cloudflare CA ++================================== ++-----BEGIN CERTIFICATE----- ++MIIC6zCCAkygAwIBAgIUI7b68p0pPrCBoW4ptlyvVcPItscwCgYIKoZIzj0EAwQw ++gY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T ++YW4gRnJhbmNpc2NvMRgwFgYDVQQKEw9DbG91ZGZsYXJlLCBJbmMxNzA1BgNVBAMT ++LkNsb3VkZmxhcmUgZm9yIFRlYW1zIEVDQyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw ++HhcNMjAwMjA0MTYwNTAwWhcNMjUwMjAyMTYwNTAwWjCBjTELMAkGA1UEBhMCVVMx ++EzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGDAW ++BgNVBAoTD0Nsb3VkZmxhcmUsIEluYzE3MDUGA1UEAxMuQ2xvdWRmbGFyZSBmb3Ig ++VGVhbXMgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0eTCBmzAQBgcqhkjOPQIBBgUr ++gQQAIwOBhgAEAVdXsX8tpA9NAQeEQalvUIcVaFNDvGsR69ysZxOraRWNGHLfq1mi ++P6o3wtmtx/C2OXG01Cw7UFJbKl5MEDxnT2KoAdFSynSJOF2NDoe5LoZHbUW+yR3X ++FDl+MF6JzZ590VLGo6dPBf06UsXbH7PvHH2XKtFt8bBXVNMa5a21RdmpD0Pho0Uw ++QzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU ++YBcQng1AEMMNteuRDAMG0/vgFe0wCgYIKoZIzj0EAwQDgYwAMIGIAkIBQU5OTA2h ++YqmFk8paan5ezHVLcmcucsfYw4L/wmeEjCkczRmCVNm6L86LjhWU0v0wER0e+lHO ++3efvjbsu8gIGSagCQgEBnyYMP9gwg8l96QnQ1khFA1ljFlnqc2XgJHDSaAJC0gdz +++NV3JMeWaD2Rb32jc9r6/a7xY0u0ByqxBQ1OQ0dt7A== ++-----END CERTIFICATE----- diff --git a/patches/dom_c.patch b/patches/dom_c.patch new file mode 100644 index 0000000000..9e542c826d --- /dev/null +++ b/patches/dom_c.patch @@ -0,0 +1,17 @@ +--- dom/dom_c.php 2024-01-02 12:04:54 ++++ dom/dom_c.php 2024-01-21 10:41:56 +@@ -1347,6 +1347,14 @@ + */ + class DOMNamedNodeMap implements IteratorAggregate, Countable + { ++ ++ /** ++ * The number of nodes in the map. The range of valid child node indices is 0 to length - 1 inclusive. ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $length; + /** + * Retrieves a node specified by name + * @link https://php.net/manual/en/domnamednodemap.getnameditem.php diff --git a/patches/paratest.patch b/patches/paratest.patch new file mode 100644 index 0000000000..f2091828ff --- /dev/null +++ b/patches/paratest.patch @@ -0,0 +1,35 @@ +--- src/Runners/PHPUnit/Worker/BaseWorker.php 2020-02-07 23:07:07.000000000 +0100 ++++ src/Runners/PHPUnit/Worker/BaseWorker.php 2022-03-27 17:35:45.000000000 +0200 +@@ -28,17 +28,18 @@ + array $parameters = [], + ?Options $options = null + ) { +- $bin = 'PARATEST=1 '; ++ $env = getenv(); ++ $env['PARATEST'] = 1; + if (\is_numeric($token)) { +- $bin .= 'XDEBUG_CONFIG="true" '; +- $bin .= "TEST_TOKEN=$token "; ++ $env['XDEBUG_CONFIG'] = 'true'; ++ $env['TEST_TOKEN'] = $token; + } + if ($uniqueToken) { +- $bin .= "UNIQUE_TEST_TOKEN=$uniqueToken "; ++ $env['UNIQUE_TEST_TOKEN'] = $uniqueToken; + } + $finder = new PhpExecutableFinder(); + $phpExecutable = $finder->find(); +- $bin .= "$phpExecutable "; ++ $bin = "$phpExecutable "; + if ($options && $options->passthruPhp) { + $bin .= $options->passthruPhp . ' '; + } +@@ -50,7 +51,7 @@ + if ($options && $options->verbose) { + echo "Starting WrapperWorker via: $bin\n"; + } +- $process = \proc_open($bin, self::$descriptorspec, $pipes); ++ $process = \proc_open($bin, self::$descriptorspec, $pipes, null, $env); + $this->proc = \is_resource($process) ? $process : null; + $this->pipes = $pipes; + } diff --git a/patches/xmlreader.patch b/patches/xmlreader.patch new file mode 100644 index 0000000000..7be168133f --- /dev/null +++ b/patches/xmlreader.patch @@ -0,0 +1,122 @@ +--- xmlreader/xmlreader.php 2024-01-21 10:44:31 ++++ xmlreader/xmlreader.php 2024-01-21 10:48:24 +@@ -28,7 +28,119 @@ + */ + class XMLReader + { ++ /** ++ * The number of attributes on the node ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $attributeCount; ++ ++ /** ++ * The base URI of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $baseURI; ++ ++ /** ++ * Depth of the node in the tree, starting at 0 ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $depth; ++ ++ /** ++ * Indicates if node has attributes ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $hasAttributes; ++ ++ /** ++ * Indicates if node has a text value ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $hasValue; ++ ++ /** ++ * Indicates if attribute is defaulted from DTD ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $isDefault; ++ ++ /** ++ * Indicates if node is an empty element tag ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $isEmptyElement; ++ ++ /** ++ * The local name of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $localName; ++ + /** ++ * The qualified name of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $name; ++ ++ /** ++ * The URI of the namespace associated with the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $namespaceURI; ++ ++ /** ++ * The node type for the node ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $nodeType; ++ ++ /** ++ * The prefix of the namespace associated with the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $prefix; ++ ++ /** ++ * The text value of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $value; ++ ++ /** ++ * The xml:lang scope which the node resides ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $xmlLang; ++ ++ /** + * No node type + */ + public const NONE = 0; diff --git a/phpcs.xml b/phpcs.xml index a8579080da..fa9198745f 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,17 +1,23 @@ - + + - + bin src tests compiler/src compiler/tests + apigen/src + changelog-generator/src + changelog-generator/run.php + issue-bot/src + issue-bot/console.php @@ -22,6 +28,7 @@ + @@ -37,17 +44,20 @@ - + + + src/Rules/Whitespace/FileWhitespaceRule.php + 10 - + @@ -57,7 +67,7 @@ - + @@ -70,9 +80,6 @@ tests - - src/Command/AnalyseApplication.php - @@ -93,9 +100,23 @@ + + + + + + + + + + + + + + @@ -124,24 +145,37 @@ - - - - - - - src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php + + + + + + + + + + + + + + + + + + + + @@ -169,16 +203,19 @@ - - tests/*/data - tests/*/Fixture + src/Type/TypeResult.php + + compiler/tests/*/data/ + tests/*/Fixture/ + tests/*/cache/ + tests/*/data/ + tests/*/traits/ + tests/PHPStan/Analyser/nsrt/ + tests/e2e/anon-class/ + tests/e2e/magic-setter/ tests/e2e/resultCache_1.php tests/e2e/resultCache_2.php tests/e2e/resultCache_3.php - tests/*/traits - tests/tmp - tests/notAutoloaded - src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php - src/Reflection/SignatureMap/functionMap.php - src/Reflection/SignatureMap/functionMetadata.php + tests/notAutoloaded/ + tests/tmp/ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2d8e472cdb..dc8fcfbcc1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,412 +1,2053 @@ parameters: ignoreErrors: - - message: "#^Only numeric types are allowed in pre\\-decrement, bool\\|float\\|int\\|string\\|null given\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Analyser/MutatingScope.php + path: build/PHPStan/Build/ContainerDynamicReturnTypeExtension.php + + - + message: '#^Method PHPStan\\Analyser\\AnalyserResultFinalizer\:\:finalize\(\) throws checked exception Throwable but it''s missing from the PHPDoc @throws tag\.$#' + identifier: missingType.checkedException + count: 1 + path: src/Analyser/AnalyserResultFinalizer.php - - message: "#^Only numeric types are allowed in pre\\-increment, bool\\|float\\|int\\|string\\|null given\\.$#" + message: '#^Cannot assign offset ''realCount'' to array\\|string\.$#' + identifier: offsetAssign.dimType count: 1 + path: src/Analyser/Ignore/IgnoredErrorHelperResult.php + + - + message: '#^Casting to string something that''s already string\.$#' + identifier: cast.useless + count: 3 + path: src/Analyser/MutatingScope.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Analyser/MutatingScope.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 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\\.$#" + message: '#^Only numeric types are allowed in pre\-increment, float\|int\|string\|null given\.$#' + identifier: preInc.nonNumeric count: 1 + path: src/Analyser/MutatingScope.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 path: src/Analyser/NodeScopeResolver.php - - message: "#^Parameter \\#9 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects ReflectionClass, object 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\.$#' + identifier: argument.type count: 1 path: src/Analyser/NodeScopeResolver.php - - message: "#^Anonymous function has an unused use \\$container\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Analyser/RicherScopeGetTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Analyser/TypeSpecifier.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 5 + path: src/Analyser/TypeSpecifier.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Analyser/TypeSpecifier.php + + - + message: '#^Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\\Collectors\\Collector\:\:processNode\(\)\.$#' + identifier: generics.variance + count: 1 + path: src/Collectors/Collector.php + + - + message: '#^Method PHPStan\\Collectors\\Registry\:\:__construct\(\) has parameter \$collectors with generic interface PHPStan\\Collectors\\Collector but does not specify its types\: TNodeType, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Collectors/Registry.php + + - + message: '#^Property PHPStan\\Collectors\\Registry\:\:\$cache with generic interface PHPStan\\Collectors\\Collector does not specify its types\: TNodeType, TValue$#' + identifier: missingType.generics count: 1 + path: src/Collectors/Registry.php + + - + message: '#^Property PHPStan\\Collectors\\Registry\:\:\$collectors with generic interface PHPStan\\Collectors\\Collector does not specify its types\: TNodeType, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Collectors/Registry.php + + - + message: '#^Anonymous function has an unused use \$container\.$#' + identifier: closure.unusedUse + count: 1 + path: src/Command/CommandHelper.php + + - + message: '#^Call to static method expand\(\) of internal class Nette\\DI\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass + count: 2 path: src/Command/CommandHelper.php - - message: "#^Parameter \\#1 \\$path of function dirname expects string, string\\|false given\\.$#" + message: '#^Parameter \#1 \$path of function dirname expects string, string\|false given\.$#' + identifier: argument.type count: 1 path: src/Command/CommandHelper.php - - message: "#^Static property PHPStan\\\\Command\\\\CommandHelper\\:\\:\\$reservedMemory is never read, only written\\.$#" + message: '#^Static property PHPStan\\Command\\CommandHelper\:\:\$reservedMemory is never read, only written\.$#' + identifier: property.onlyWritten 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\\(\\)$#" + message: '#^Call to static method escape\(\) of internal class Nette\\DI\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass + count: 4 + path: src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php + + - + message: '#^Call to static method escape\(\) of internal class Nette\\DI\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass + count: 5 + path: src/Command/ErrorFormatter/BaselinePhpErrorFormatter.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\(\)$#' + identifier: method.childParameterType count: 1 path: src/Command/ErrorsConsoleStyle.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\\\\SymfonyStyle\\:\\:table\\(\\)$#" + message: '#^Parameter \#1 \$headers \(array\\) of method PHPStan\\Command\\ErrorsConsoleStyle\:\:table\(\) should be contravariant with parameter \$headers \(array\) of method Symfony\\Component\\Console\\Style\\SymfonyStyle\:\:table\(\)$#' + identifier: method.childParameterType count: 1 path: src/Command/ErrorsConsoleStyle.php - - message: "#^Parameter \\#2 \\$rows \\(array\\\\>\\) of method PHPStan\\\\Command\\\\ErrorsConsoleStyle\\:\\:table\\(\\) should be contravariant with parameter \\$rows \\(array\\) of method Symfony\\\\Component\\\\Console\\\\Style\\\\StyleInterface\\:\\:table\\(\\)$#" + message: '#^Parameter \#2 \$rows \(array\\>\) of method PHPStan\\Command\\ErrorsConsoleStyle\:\:table\(\) should be contravariant with parameter \$rows \(array\) of method Symfony\\Component\\Console\\Style\\StyleInterface\:\:table\(\)$#' + identifier: method.childParameterType count: 1 path: src/Command/ErrorsConsoleStyle.php - - message: "#^Parameter \\#2 \\$rows \\(array\\\\>\\) of method PHPStan\\\\Command\\\\ErrorsConsoleStyle\\:\\:table\\(\\) should be contravariant with parameter \\$rows \\(array\\) of method Symfony\\\\Component\\\\Console\\\\Style\\\\SymfonyStyle\\:\\:table\\(\\)$#" + message: '#^Parameter \#2 \$rows \(array\\>\) of method PHPStan\\Command\\ErrorsConsoleStyle\:\:table\(\) should be contravariant with parameter \$rows \(array\) of method Symfony\\Component\\Console\\Style\\SymfonyStyle\:\:table\(\)$#' + identifier: method.childParameterType count: 1 path: src/Command/ErrorsConsoleStyle.php - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\:\\:done\\(\\)\\.$#" + message: '#^Call to static method expand\(\) of internal class Nette\\DI\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass + count: 1 + path: src/DependencyInjection/ContainerFactory.php + + - + message: '#^Call to static method merge\(\) of internal class Nette\\Schema\\Helpers from outside its root namespace Nette\.$#' + identifier: staticMethod.internalClass count: 2 - path: src/Command/FixerApplication.php + path: src/DependencyInjection/ContainerFactory.php - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\\\:\\:done\\(\\)\\.$#" + message: '#^Variable method call on Nette\\Schema\\Elements\\AnyOf\|Nette\\Schema\\Elements\\Structure\|Nette\\Schema\\Elements\\Type\.$#' + identifier: method.dynamicName count: 1 - path: src/Command/FixerApplication.php + path: src/DependencyInjection/ContainerFactory.php - - message: "#^Parameter \\#1 \\$arg of function escapeshellarg expects string, string\\|false given\\.$#" + message: '#^Variable static method call on Nette\\Schema\\Expect\.$#' + identifier: staticMethod.dynamicName count: 1 - path: src/Command/FixerApplication.php + path: src/DependencyInjection/ContainerFactory.php - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\:\\:done\\(\\)\\.$#" + message: '#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\DI\\Config\\Helpers\.$#' + identifier: classConstant.deprecatedClass count: 1 - path: src/Command/WorkerCommand.php + path: src/DependencyInjection/NeonAdapter.php - - message: "#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\\\DI\\\\Config\\\\Helpers\\.$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/DependencyInjection/NeonAdapter.php + path: src/Diagnose/PHPStanDiagnoseExtension.php - - message: "#^Variable method call on Nette\\\\Schema\\\\Elements\\\\AnyOf\\|Nette\\\\Schema\\\\Elements\\\\Structure\\|Nette\\\\Schema\\\\Elements\\\\Type\\.$#" + message: '#^Parameter \#1 \$path of function dirname expects string, string\|false given\.$#' + identifier: argument.type count: 1 - path: src/DependencyInjection/ParametersSchemaExtension.php + path: src/Diagnose/PHPStanDiagnoseExtension.php - - message: "#^Variable static method call on Nette\\\\Schema\\\\Expect\\.$#" + message: '#^Access to property \$id of internal class Symfony\\Polyfill\\Php80\\PhpToken from outside its root namespace Symfony\.$#' + identifier: property.internalClass count: 1 - path: src/DependencyInjection/ParametersSchemaExtension.php + path: src/Parser/RichParser.php - - message: "#^Variable method call on PHPStan\\\\Reflection\\\\ClassReflection\\.$#" - count: 2 - path: src/PhpDoc/PhpDocBlock.php + message: '#^Access to property \$line of internal class Symfony\\Polyfill\\Php80\\PhpToken from outside its root namespace Symfony\.$#' + identifier: property.internalClass + count: 4 + path: src/Parser/RichParser.php + + - + message: '#^Access to property \$text of internal class Symfony\\Polyfill\\Php80\\PhpToken from outside its root namespace Symfony\.$#' + identifier: property.internalClass + count: 3 + path: src/Parser/RichParser.php + + - + message: '#^Call to function method_exists\(\) with PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocNode and ''getParamOutTypeTagV…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: src/PhpDoc/PhpDocNodeResolver.php - - message: "#^Variable static method call on PHPStan\\\\PhpDoc\\\\PhpDocBlock\\.$#" + message: '#^Call to function method_exists\(\) with PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocNode and ''getSelfOutTypeTagVa…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: src/PhpDoc/PhpDocBlock.php + path: src/PhpDoc/PhpDocNodeResolver.php - - message: "#^Method PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock\\:\\:getNameScope\\(\\) should return PHPStan\\\\Analyser\\\\NameScope but returns PHPStan\\\\Analyser\\\\NameScope\\|null\\.$#" + message: '#^Method PHPStan\\PhpDoc\\ResolvedPhpDocBlock\:\:getNameScope\(\) should return PHPStan\\Analyser\\NameScope but returns PHPStan\\Analyser\\NameScope\|null\.$#' + identifier: return.type count: 1 path: src/PhpDoc/ResolvedPhpDocBlock.php - - message: """ - #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# - """ + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/StubValidator.php + path: src/PhpDoc/TypeNodeResolver.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\\(\\)$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/Tag/ParamTag.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ReturnTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ReturnTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/Tag/ReturnTag.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/Tag/VarTag.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 2 + path: src/PhpDoc/TypeNodeResolver.php + + - + message: '#^Dead catch \- PHPStan\\BetterReflection\\Identifier\\Exception\\InvalidIdentifierName is never thrown in the try block\.$#' + identifier: catch.neverThrown + count: 3 path: src/Reflection/BetterReflection/BetterReflectionProvider.php - - message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\NodeCompiler\\\\Exception\\\\UnableToCompileNode\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAClassReflection\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAnInterfaceReflection is never thrown in the try block\\.$#" + message: '#^Dead catch \- PHPStan\\BetterReflection\\NodeCompiler\\Exception\\UnableToCompileNode is never thrown in the try block\.$#' + identifier: catch.neverThrown count: 1 path: src/Reflection/BetterReflection/BetterReflectionProvider.php - - message: "#^Parameter \\#9 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects ReflectionClass, object given\\.$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/Reflection/BetterReflection/BetterReflectionProvider.php + path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.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 + message: '#^Creating new ReflectionFunction is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection + count: 1 path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" + 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\.$#' + identifier: argument.type count: 1 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.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\\|PhpParser\\\\Node\\\\Stmt\\\\Function_ given\\.$#" + message: '#^Method PHPStan\\Reflection\\BetterReflection\\SourceLocator\\FileReadTrapStreamWrapper\:\:invokeWithRealFileStreamWrapper\(\) has parameter \$cb with no signature specified for callable\.$#' + identifier: missingType.callable count: 1 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php - - message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Stmt\\\\ClassLike 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\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\ClassLike\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Function_ given\.$#' + identifier: argument.type 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\.$#' + identifier: argument.type + count: 2 path: src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php - - message: "#^Only booleans are allowed in &&, int\\|false given on the right side\\.$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php + path: src/Reflection/BetterReflection/SourceLocator/ReflectionClassSourceLocator.php - - message: "#^Only booleans are allowed in an if condition, int\\|false given\\.$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php + path: src/Reflection/BetterReflection/SourceLocator/RewriteClassAliasSourceLocator.php - - message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:__construct\\(\\) has parameter \\$reflection with generic class ReflectionClass but does not specify its types\\: T$#" + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/Reflection/ClassReflection.php + path: src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php - - message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getCacheKey\\(\\) should return string but returns string\\|null\\.$#" + message: ''' + #^Call to deprecated method isSubclassOf\(\) of class PHPStan\\Reflection\\ClassReflection\: + Use isSubclassOfClass instead\.$# + ''' + identifier: method.deprecated count: 1 path: src/Reflection/ClassReflection.php - - message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getNativeReflection\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Reflection/ClassReflection.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Reflection/ClassReflection.php - - message: "#^Property PHPStan\\\\Reflection\\\\ClassReflection\\:\\:\\$reflection with generic class ReflectionClass does not specify its types\\: T$#" + message: '#^Method PHPStan\\Reflection\\ClassReflection\:\:getCacheKey\(\) should return string but returns string\|null\.$#' + identifier: return.type count: 1 path: src/Reflection/ClassReflection.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\BuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + message: '#^Binary operation "&" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Binary operation "\*" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Binary operation "\+" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Binary operation "\-" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Binary operation "\^" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Binary operation "\|" between bool\|float\|int\|string\|null and bool\|float\|int\|string\|null results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 22 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 10 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^PHPDoc tag @var with type float\|int is not subtype of native type int\.$#' + identifier: varTag.nativeType + count: 1 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^PHPDoc tag @var with type float\|int is not subtype of type int\.$#' + identifier: varTag.type + count: 4 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^PHPDoc tag @var with type float\|int\|null is not subtype of type int\|null\.$#' + identifier: varTag.type + count: 6 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: '#^Creating new PHPStan\\Php8StubsMap is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.constructor + count: 1 + path: src/Reflection/SignatureMap/Php8SignatureMapProvider.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/Api/NodeConnectingVisitorAttributesRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Classes/ImpossibleInstanceOfRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Classes/RequireExtendsRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Reflection/Php/BuiltinMethodReflection.php + path: src/Rules/Classes/RequireImplementsRule.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:__construct\\(\\) has parameter \\$declaringClass with generic class ReflectionClass but does not specify its types\\: T$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 6 + path: src/Rules/Comparison/BooleanAndConstantConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/BooleanNotConstantConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 6 + path: src/Rules/Comparison/BooleanOrConstantConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/ConstantLooseComparisonRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/DoWhileLoopConstantConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/ElseIfConstantConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/IfConstantConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\TypeWithClassName is error\-prone and deprecated\. Use Type\:\:getObjectClassNames\(\) or Type\:\:getObjectClassReflections\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Rules/Comparison/LogicalXorConstantConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Rules/Comparison/MatchExpressionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/Comparison/TernaryOperatorConstantConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php + + - + message: '#^Function class_implements\(\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection + count: 1 + path: src/Rules/DirectRegistry.php + + - + message: '#^Function class_parents\(\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection + count: 1 + path: src/Rules/DirectRegistry.php + + - + message: '#^Method PHPStan\\Rules\\DirectRegistry\:\:__construct\(\) has parameter \$rules with generic interface PHPStan\\Rules\\Rule but does not specify its types\: TNodeType$#' + identifier: missingType.generics + count: 1 + path: src/Rules/DirectRegistry.php + + - + message: '#^Property PHPStan\\Rules\\DirectRegistry\:\:\$cache with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics + count: 1 + path: src/Rules/DirectRegistry.php + + - + message: '#^Property PHPStan\\Rules\\DirectRegistry\:\:\$rules with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics + count: 1 + path: src/Rules/DirectRegistry.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Reflection/Php/FakeBuiltinMethodReflection.php + path: src/Rules/Generics/GenericAncestorsCheck.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Reflection/Php/FakeBuiltinMethodReflection.php + path: src/Rules/Generics/TemplateTypeCheck.php - - message: "#^Property PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:\\$declaringClass with generic class ReflectionClass does not specify its types\\: T$#" + message: '#^Function class_implements\(\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/Reflection/Php/FakeBuiltinMethodReflection.php + path: src/Rules/LazyRegistry.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\NativeBuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + message: '#^Function class_parents\(\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection count: 1 - path: src/Reflection/Php/NativeBuiltinMethodReflection.php + path: src/Rules/LazyRegistry.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\PhpClassReflectionExtension\\:\\:collectTraits\\(\\) has parameter \\$class with generic class ReflectionClass but does not specify its types\\: T$#" + message: '#^Method PHPStan\\Rules\\LazyRegistry\:\:getRulesFromContainer\(\) return type with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 - path: src/Reflection/Php/PhpClassReflectionExtension.php + path: src/Rules/LazyRegistry.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\PhpClassReflectionExtension\\:\\:collectTraits\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + message: '#^Property PHPStan\\Rules\\LazyRegistry\:\:\$cache with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 - path: src/Reflection/Php/PhpClassReflectionExtension.php + path: src/Rules/LazyRegistry.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: '#^Property PHPStan\\Rules\\LazyRegistry\:\:\$rules with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics count: 1 - path: src/Rules/Api/ApiClassExtendsRule.php + path: src/Rules/LazyRegistry.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Api/ApiClassImplementsRule.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Api/ApiInstantiationRule.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Api/ApiInterfaceExtendsRule.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Api/ApiMethodCallRule.php + path: src/Rules/Methods/StaticMethodCallCheck.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Api/ApiStaticCallRule.php + path: src/Rules/PhpDoc/RequireExtendsCheck.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Api/ApiTraitUseRule.php + path: src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php - - message: "#^Binary operation \"\\+\" between array\\{class\\-string\\\\} and array\\\\|false results in an error\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Registry.php + path: src/Rules/PhpDoc/VarTagTypeRuleHelper.php - - message: "#^Method PHPStan\\\\Rules\\\\Registry\\:\\:__construct\\(\\) has parameter \\$rules with generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + message: '#^Access to an undefined property T of PHPStan\\Rules\\RuleError\:\:\$tip\.$#' + identifier: property.notFound + count: 2 + path: src/Rules/RuleErrorBuilder.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/RuleLevelHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Rules/RuleLevelHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/UnusedFunctionParametersCheck.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/Variables/CompactVariablesRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/Variables/CompactVariablesRule.php + + - + message: ''' + #^Call to deprecated method assertFileNotExists\(\) of class PHPUnit\\Framework\\Assert\: + https\://github\.com/sebastianbergmann/phpunit/issues/4077$# + ''' + identifier: staticMethod.deprecated count: 1 - path: src/Rules/Registry.php + path: src/Testing/LevelsTestCase.php - - message: "#^Property PHPStan\\\\Rules\\\\Registry\\:\\:\\$cache with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Call to function method_exists\(\) with ''PHPUnit\\\\Framework\\\\TestCase'' and ''assertFileDoesNotEx…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: src/Rules/Registry.php + path: src/Testing/LevelsTestCase.php + + - + message: '#^Catching internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: catch.internalClass + count: 2 + path: src/Testing/LevelsTestCase.php - - message: "#^Property PHPStan\\\\Rules\\\\Registry\\:\\:\\$rules with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Return type of method PHPStan\\Testing\\LevelsTestCase\:\:compareFiles\(\) has typehint with internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: return.internalClass count: 1 - path: src/Rules/Registry.php + path: src/Testing/LevelsTestCase.php - - message: "#^Anonymous function has an unused use \\$container\\.$#" + message: '#^Anonymous function has an unused use \$container\.$#' + identifier: closure.unusedUse count: 1 path: src/Testing/PHPStanTestCase.php - - message: """ - #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# - """ + message: '#^Catching internal class PHPUnit\\Framework\\ExpectationFailedException\.$#' + identifier: catch.internalClass count: 1 - path: src/Type/ObjectType.php + path: src/Testing/PHPStanTestCase.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Testing/TypeInferenceTestCase.php - - message: """ - #^Call to deprecated method getUniversalObjectCratesClasses\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Inject %%universalObjectCratesClasses%% parameter instead\\.$# - """ + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Type/ObjectType.php + path: src/Type/Accessory/AccessoryArrayListType.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/AnalyserTest.php + path: src/Type/Accessory/AccessoryLiteralStringType.php - - message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRule implements generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRule.php + path: src/Type/Accessory/AccessoryLowercaseStringType.php - - message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + path: src/Type/Accessory/AccessoryNonEmptyStringType.php - - message: "#^Method PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + path: src/Type/Accessory/AccessoryNonEmptyStringType.php - - message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderRule implements generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderRule.php + path: src/Type/Accessory/AccessoryNonFalsyStringType.php - - message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderTest.php + path: src/Type/Accessory/AccessoryNumericStringType.php - - message: "#^Method PHPStan\\\\Analyser\\\\EvaluationOrderTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderTest.php + path: src/Type/Accessory/AccessoryNumericStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/AccessoryUppercaseStringType.php - - message: """ - #^Call to deprecated method getClass\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProvider instead$# - """ + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Broker/BrokerTest.php + path: src/Type/Accessory/HasMethodType.php - - message: """ - #^Call to deprecated method getFunction\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProvider instead$# - """ + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Broker/BrokerTest.php + path: src/Type/Accessory/HasOffsetType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Accessory/HasOffsetValueType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Accessory/HasOffsetValueType.php - - message: """ - #^Call to deprecated method hasClass\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProvider instead$# - """ + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Broker/BrokerTest.php + path: src/Type/Accessory/HasOffsetValueType.php - - message: "#^Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Command/AnalyseCommandTest.php + path: src/Type/Accessory/HasPropertyType.php - - message: "#^Class PHPStan\\\\Node\\\\FileNodeTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Node/FileNodeTest.php + path: src/Type/Accessory/NonEmptyArrayType.php - - message: "#^Method PHPStan\\\\Node\\\\FileNodeTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Node/FileNodeTest.php + path: src/Type/Accessory/OversizedArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/ArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/ArrayType.php - - message: """ - #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/ArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + path: src/Type/ArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/BooleanType.php - - message: """ - #^Return type of method PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + path: src/Type/BooleanType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/CallableType.php - - message: """ - #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayKeyTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/CallableType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php + path: src/Type/ClosureType.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$# - """ + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php + path: src/Type/Constant/ConstantArrayType.php + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Constant/ConstantArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 5 + path: src/Type/Constant/ConstantArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Constant/ConstantArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Constant/ConstantArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Constant/ConstantArrayTypeBuilder.php + + - + message: '#^PHPDoc tag @var assumes the expression with type PHPStan\\Type\\Type is always PHPStan\\Type\\Constant\\ConstantIntegerType\|PHPStan\\Type\\Constant\\ConstantStringType but it''s error\-prone and dangerous\.$#' + identifier: phpstanApi.varTagAssumption + count: 2 + path: src/Type/Constant/ConstantArrayTypeBuilder.php + + - + message: '#^PHPDoc tag @var with type float\|int is not subtype of native type int\.$#' + identifier: varTag.nativeType + count: 2 + path: src/Type/Constant/ConstantArrayTypeBuilder.php + + - + message: '#^PHPDoc tag @var with type float\|int is not subtype of type int\.$#' + identifier: varTag.type + count: 1 + path: src/Type/Constant/ConstantArrayTypeBuilder.php + + - + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Constant/ConstantBooleanType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/Constant/ConstantBooleanType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Constant/ConstantBooleanType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/Constant/ConstantFloatType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Constant/ConstantFloatType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/Constant/ConstantIntegerType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Constant/ConstantIntegerType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Constant/ConstantStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/Constant/ConstantStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/Constant/ConstantStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Constant/ConstantStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Constant/ConstantStringType.php + + - + message: '#^PHPDoc tag @var with type int\|string is not subtype of type string\.$#' + identifier: varTag.type + count: 1 + path: src/Type/Constant/ConstantStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Constant/OversizedArrayBuilder.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Enum\\EnumCaseObjectType is error\-prone and deprecated\. Use Type\:\:getEnumCases\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Enum/EnumCaseObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/ExponentiateHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/FileTypeMapper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/FloatType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericClassStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/Generic/GenericClassStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/Generic/GenericClassStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericClassStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Generic/GenericClassStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/GenericObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\TypeWithClassName is error\-prone and deprecated\. Use Type\:\:getObjectClassNames\(\) or Type\:\:getObjectClassReflections\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Generic/GenericObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericStaticType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\TypeWithClassName is error\-prone and deprecated\. Use Type\:\:getObjectClassNames\(\) or Type\:\:getObjectClassReflections\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/GenericStaticType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateBenevolentUnionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateBooleanType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateConstantArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateConstantIntegerType.php + + - + message: '#^Method PHPStan\\Type\\Generic\\TemplateConstantIntegerType\:\:toPhpDocNode\(\) should return PHPStan\\PhpDocParser\\Ast\\Type\\ConstTypeNode but returns PHPStan\\PhpDocParser\\Ast\\Type\\IdentifierTypeNode\.$#' + identifier: return.type + count: 1 + path: src/Type/Generic/TemplateConstantIntegerType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateConstantStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateFloatType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateGenericObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateIntegerType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateIntersectionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateKeyOfType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Generic/TemplateMixedType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateObjectShapeType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateObjectWithoutClassType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Generic/TemplateStrictMixedType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateUnionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/IntegerRangeType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/IntegerRangeType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/IntegerRangeType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/IntegerType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/IntersectionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/IntersectionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/IntersectionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/IntersectionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/IntersectionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/IntersectionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/IterableType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/IterableType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/NullType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\NullType is error\-prone and deprecated\. Use Type\:\:isNull\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/NullType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/ObjectShapeType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/ObjectShapeType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/ObjectShapeType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Enum\\EnumCaseObjectType is error\-prone and deprecated\. Use Type\:\:getEnumCases\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/ObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/ObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/ObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/ObjectType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/ObjectWithoutClassType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/ObjectWithoutClassType.php + + - + message: '#^Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection + count: 1 + path: src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 16 + path: src/Type/Php/BcMathStringOrNullReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/CompactFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/CompactFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/DefineConstantTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/DefinedConstantTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\TypeWithClassName is error\-prone and deprecated\. Use Type\:\:getObjectClassNames\(\) or Type\:\:getObjectClassReflections\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/DsMapDynamicReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Php/FilterFunctionReturnTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/FilterFunctionReturnTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/IsAFunctionTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Php/LtrimFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/MethodExistsTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/Php/RangeFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php + + - + message: '#^Cannot access offset int\<0, max\> on \(float\|int\)\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Php/StrRepeatFunctionReturnTypeExtension.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/StaticType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectWithoutClassType is error\-prone and deprecated\. Use Type\:\:isObject\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/StaticType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/StringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/StringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 5 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\BooleanType is error\-prone and deprecated\. Use Type\:\:isBoolean\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 16 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 5 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 8 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\NullType is error\-prone and deprecated\. Use Type\:\:isNull\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\StringType is error\-prone and deprecated\. Use Type\:\:isString\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypeCombinator.php + + - + message: '#^Instanceof between PHPStan\\Type\\Constant\\ConstantIntegerType and PHPStan\\Type\\Constant\\ConstantIntegerType will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 1 + path: src/Type/TypeCombinator.php + + - + message: '#^Result of \|\| is always true\.$#' + identifier: booleanOr.alwaysTrue + count: 1 + path: src/Type/TypeCombinator.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/TypeUtils.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/TypeUtils.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/TypehintHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/TypehintHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypehintHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericClassStringType is error\-prone and deprecated\. Use Type\:\:isClassStringType\(\) and Type\:\:getClassStringObjectType\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/UnionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/UnionType.php + + - + message: '#^PHPDoc tag @var assumes the expression with type PHPStan\\Type\\Type is always PHPStan\\Type\\BooleanType but it''s error\-prone and dangerous\.$#' + identifier: phpstanApi.varTagAssumption + count: 1 + path: src/Type/UnionType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 4 + path: src/Type/UnionTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntegerType is error\-prone and deprecated\. Use Type\:\:isInteger\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\NullType is error\-prone and deprecated\. Use Type\:\:isNull\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/UnionTypeHelper.php + + - + message: '#^Doing instanceof PHPStan\\Type\\VoidType is error\-prone and deprecated\. Use Type\:\:isVoid\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/VoidType.php + + - + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable + count: 1 + path: tests/PHPStan/Analyser/AnalyserTest.php + + - + message: '#^Class PHPStan\\Analyser\\AnonymousClassNameRuleTest extends generic class PHPStan\\Testing\\RuleTestCase but does not specify its types\: TRule$#' + identifier: missingType.generics + count: 1 + path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + + - + message: '#^Method PHPStan\\Analyser\\AnonymousClassNameRuleTest\:\:getRule\(\) return type with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics + count: 1 + path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + + - + message: '#^Class PHPStan\\Analyser\\EvaluationOrderTest extends generic class PHPStan\\Testing\\RuleTestCase but does not specify its types\: TRule$#' + identifier: missingType.generics + count: 1 + path: tests/PHPStan/Analyser/EvaluationOrderTest.php + + - + message: '#^Method PHPStan\\Analyser\\EvaluationOrderTest\:\:getRule\(\) return type with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics + count: 1 + path: tests/PHPStan/Analyser/EvaluationOrderTest.php + + - + message: '#^Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found\.$#' + identifier: constant.notFound + count: 1 + path: tests/PHPStan/Command/AnalyseCommandTest.php + + - + message: '#^Class PHPStan\\Node\\FileNodeTest extends generic class PHPStan\\Testing\\RuleTestCase but does not specify its types\: TRule$#' + identifier: missingType.generics + count: 1 + path: tests/PHPStan/Node/FileNodeTest.php + + - + message: '#^Method PHPStan\\Node\\FileNodeTest\:\:getRule\(\) return type with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics + count: 1 + path: tests/PHPStan/Node/FileNodeTest.php + + - + message: '#^Access to constant on internal class InternalAnnotations\\InternalFoo\.$#' + identifier: classConstant.internalClass + count: 1 + path: tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php + + - + message: '#^Access to constant on internal interface InternalAnnotations\\InternalFooInterface\.$#' + identifier: classConstant.internalInterface + count: 1 + path: tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php + + - + message: '#^Access to constant on internal trait InternalAnnotations\\InternalFooTrait\.$#' + identifier: classConstant.internalTrait + count: 1 + path: tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php + + - + message: '#^PHPDoc tag @var with type string is not subtype of type class\-string\.$#' + identifier: varTag.type + count: 1 + path: tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php + + - + message: '#^Creating new PHPStan\\Php8StubsMap is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.constructor + count: 1 + path: tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php + + - + message: '#^Instanceof references internal interface PHPUnit\\Exception\.$#' + identifier: instanceof.internalInterface + count: 1 + path: tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php + + - + message: '#^Creating new PHPStan\\Php8StubsMap is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.constructor + count: 1 + path: tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php + + - + message: '#^Catching internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: catch.internalClass + count: 1 + path: tests/PHPStan/Rules/WarningEmittingRuleTest.php + + - + message: '#^Call to method getComparisonFailure\(\) of internal class PHPUnit\\Framework\\ExpectationFailedException from outside its root namespace PHPUnit\.$#' + identifier: method.internalClass + count: 2 + path: tests/PHPStan/Testing/NonexistentAnalysedClassRuleTest.php + + - + message: '#^Catching internal class PHPUnit\\Framework\\ExpectationFailedException\.$#' + identifier: catch.internalClass + count: 1 + path: tests/PHPStan/Testing/NonexistentAnalysedClassRuleTest.php + + - + message: '#^Access to constant on internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: classConstant.internalClass + count: 1 + path: tests/PHPStan/Testing/TypeInferenceTestCaseTest.php + + - + message: '#^Catching internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: catch.internalClass + count: 1 + path: tests/PHPStan/Testing/TypeInferenceTestCaseTest.php + + - + message: '#^PHPDoc tag @var assumes the expression with type PHPStan\\Type\\Generic\\TemplateType is always PHPStan\\Type\\Generic\\TemplateMixedType but it''s error\-prone and dangerous\.$#' + identifier: phpstanApi.varTagAssumption + count: 1 + path: tests/PHPStan/Type/IterableTypeTest.php diff --git a/phpstan-baseline.php b/phpstan-baseline.php new file mode 100644 index 0000000000..646cbdbef6 --- /dev/null +++ b/phpstan-baseline.php @@ -0,0 +1,3 @@ + + verbose="false" + convertDeprecationsToExceptions="true"> src @@ -24,12 +27,14 @@ tests/PHPStan + tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php exec + levels diff --git a/resources/RegexGrammar.pp b/resources/RegexGrammar.pp new file mode 100644 index 0000000000..3f49912a36 --- /dev/null +++ b/resources/RegexGrammar.pp @@ -0,0 +1,224 @@ +// +// Hoa +// +// +// @license +// +// New BSD License +// +// Copyright © 2007-2017, Hoa community. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the Hoa nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Grammar \Hoa\Regex\Grammar. +// +// Provide grammar of PCRE (Perl Compatible Regular Expression)for the LL(k) +// parser. More informations at http://pcre.org/pcre.txt, sections pcrepattern & +// pcresyntax. +// +// @copyright Copyright © 2007-2017 Hoa community. +// @license New BSD License +// + +// Character classes. +// tokens suffixed with "fc_" are the same as without such suffix but followed by "class:_class" +%token negative_class_fc_ \[\^(?=\]) -> class_fc +%token class_fc_ \[(?=\]) -> class_fc +%token class_fc:_class \] -> class +%token negative_class_ \[\^ -> class +%token class_ \[ -> class +%token class:posix_class \[:\^?[a-z]+:\] +%token class:class_ \[ +%token class:_class \] -> default +%token class:range \- +// taken over from literals but class:character has \b support on top (backspace in character classes) +%token class:character \\([aefnrtb]|c[\x00-\x7f]) +%token class:dynamic_character \\([0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}) +%token class:character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+}) +%token class:literal \\.|.|\n + +// Internal options. +// See https://www.regular-expressions.info/refmodifiers.html +// and https://www.php.net/manual/en/regexp.reference.internal-options.php +%token internal_option \(\?[imsxnJUX^]*-?[imsxnJUX^]+\) + +// Lookahead and lookbehind assertions. +%token lookahead_ \(\?= +%token negative_lookahead_ \(\?! +%token lookbehind_ \(\?<= +%token negative_lookbehind_ \(\? nc +%token absolute_reference_ \(\?\((?=\d) -> c +%token relative_reference_ \(\?\((?=[\+\-]) -> c +%token c:index [\+\-]?\d+ -> default +%token assertion_reference_ \(\?\( + +// Comments. +%token comment_ \(\?# -> co +%token co:_comment \) -> default +%token co:comment .*?(?=(? mark +%token mark:name [^)]+ +%token mark:_marker \) -> default + +// Capturing group. +%token named_capturing_ \(\?P?< -> nc +%token nc:_named_capturing > -> default +%token nc:capturing_name .+?(?=(?) +%token non_capturing_ \(\?: +%token non_capturing_internal_option \(\?[imsxnJUX^]*-?[imsxnJUX^]+: +%token non_capturing_reset_ \(\?\| +%token atomic_group_ \(\?> +%token capturing_ \( +%token _capturing \) + +// Quantifiers (by default, greedy). +%token zero_or_one_possessive \?\+ +%token zero_or_one_lazy \?\? +%token zero_or_one \? +%token zero_or_more_possessive \*\+ +%token zero_or_more_lazy \*\? +%token zero_or_more \* +%token one_or_more_possessive \+\+ +%token one_or_more_lazy \+\? +%token one_or_more \+ +%token exactly_n \{[0-9]+\} +%token n_to_m_possessive \{[0-9]+,[0-9]+\}\+ +%token n_to_m_lazy \{[0-9]+,[0-9]+\}\? +%token n_to_m \{[0-9]+,[0-9]+\} +%token n_or_more_possessive \{[0-9]+,\}\+ +%token n_or_more_lazy \{[0-9]+,\}\? +%token n_or_more \{[0-9]+,\} + +// Alternation. +%token alternation \| + +// Literal. +%token character \\([aefnrt]|c[\x00-\x7f]) +%token dynamic_character \\([0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}) +// Please, see PCRESYNTAX(3), General Category properties, PCRE special category +// properties and script names for \p{} and \P{}. +%token character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+}) +%token anchor \\([bBAZzG])|\^|\$ +%token match_point_reset \\K +%token literal \\.|.|\n + + +// Rules. + +#expression: + alternation() + +alternation: + concatenation()? ( concatenation()? #alternation )* + +concatenation: + ( internal_options() | assertion() | quantification() | condition() ) + ( ( internal_options() | assertion() | quantification() | condition() ) #concatenation )* + +#internal_options: + + +#condition: + ( + ::named_reference_:: ::_named_capturing:: #namedcondition + | ( + ::relative_reference_:: #relativecondition + | ::absolute_reference_:: #absolutecondition + ) + + | ::assertion_reference_:: alternation() #assertioncondition + ) + ::_capturing:: + alternation() + ::_capturing:: + +assertion: + ( + ::lookahead_:: #lookahead + | ::negative_lookahead_:: #negativelookahead + | ::lookbehind_:: #lookbehind + | ::negative_lookbehind_:: #negativelookbehind + ) + alternation() + ::_capturing:: + +quantification: + ( class() | simple() ) ( quantifier() #quantification )? + +quantifier: + | | + | | | + | | | + | + | | | + | | | + +#class: + ( + ::negative_class_fc_:: #negativeclass + <_class> + | ::class_fc_:: + <_class> + | ::negative_class_:: #negativeclass + | ::class_:: + ) + ? ( | | range() ? | literal() )* ? + ::_class:: + +#range: + literal() ::range:: literal() + +simple: + capturing() + | literal() + +#capturing: + ::marker_:: ::_marker:: #mark + | ::comment_:: ? ::_comment:: #comment + | ( + ::named_capturing_:: ::_named_capturing:: #namedcapturing + | ::non_capturing_:: #noncapturing + | non_capturing_internal_options() #noncapturing + | ::non_capturing_reset_:: #noncapturingreset + | ::atomic_group_:: #atomicgroup + | ::capturing_:: + ) + alternation() + ::_capturing:: + +non_capturing_internal_options: + + +literal: + + | + | + | + | + | diff --git a/resources/functionMap.php b/resources/functionMap.php index 215649b625..aea642904a 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -57,9 +57,7 @@ return [ '_' => ['string', 'message'=>'string'], -'abs' => ['0|positive-int', 'number'=>'int'], -'abs\'1' => ['float', 'number'=>'float'], -'abs\'2' => ['float|0|positive-int', 'number'=>'string'], +'abs' => ['float|0|positive-int', 'num'=>'int|float'], 'accelerator_get_configuration' => ['array'], 'accelerator_get_scripts' => ['array'], 'accelerator_get_status' => ['array', 'fetch_scripts'=>'bool'], @@ -69,105 +67,105 @@ 'acosh' => ['float', 'number'=>'float'], 'addcslashes' => ['string', 'str'=>'string', 'charlist'=>'string'], 'addslashes' => ['string', 'str'=>'string'], -'AMQPChannel::__construct' => ['void', 'amqp_connection'=>'AMQPConnection'], -'AMQPChannel::basicRecover' => ['', 'requeue='=>'bool|true'], -'AMQPChannel::commitTransaction' => ['bool'], -'AMQPChannel::getChannelId' => ['int'], +'AMQPChannel::__construct' => ['void', 'connection'=>'AMQPConnection'], +'AMQPChannel::basicRecover' => ['void', 'requeue='=>'bool'], +'AMQPChannel::commitTransaction' => ['void'], +'AMQPChannel::getChannelId' => ['int<1, 65535>'], 'AMQPChannel::getConnection' => ['AMQPConnection'], -'AMQPChannel::getPrefetchCount' => ['int'], -'AMQPChannel::getPrefetchSize' => ['int'], +'AMQPChannel::getPrefetchCount' => ['int<0, 65535>'], +'AMQPChannel::getPrefetchSize' => ['int<0, max>'], 'AMQPChannel::isConnected' => ['bool'], -'AMQPChannel::qos' => ['bool', 'size'=>'int', 'count'=>'int'], -'AMQPChannel::rollbackTransaction' => ['bool'], -'AMQPChannel::setPrefetchCount' => ['bool', 'count'=>'int'], -'AMQPChannel::setPrefetchSize' => ['bool', 'size'=>'int'], -'AMQPChannel::startTransaction' => ['bool'], +'AMQPChannel::qos' => ['void', 'size'=>'int', 'count'=>'int', 'global='=>'bool'], +'AMQPChannel::rollbackTransaction' => ['void'], +'AMQPChannel::setPrefetchCount' => ['void', 'count'=>'int'], +'AMQPChannel::setPrefetchSize' => ['void', 'size'=>'int'], +'AMQPChannel::startTransaction' => ['void'], 'AMQPConnection::__construct' => ['void', 'credentials='=>'array'], -'AMQPConnection::connect' => ['bool'], -'AMQPConnection::disconnect' => ['bool'], +'AMQPConnection::connect' => ['void'], +'AMQPConnection::disconnect' => ['void'], 'AMQPConnection::getHost' => ['string'], 'AMQPConnection::getLogin' => ['string'], -'AMQPConnection::getMaxChannels' => ['int|null'], +'AMQPConnection::getMaxChannels' => ['int<1, 65535>'], 'AMQPConnection::getPassword' => ['string'], -'AMQPConnection::getPort' => ['int'], +'AMQPConnection::getPort' => ['int<1, 65535>'], 'AMQPConnection::getReadTimeout' => ['float'], 'AMQPConnection::getTimeout' => ['float'], -'AMQPConnection::getUsedChannels' => ['int'], +'AMQPConnection::getUsedChannels' => ['int<1, 65535>'], 'AMQPConnection::getVhost' => ['string'], 'AMQPConnection::getWriteTimeout' => ['float'], 'AMQPConnection::isConnected' => ['bool'], -'AMQPConnection::isPersistent' => ['bool|null'], -'AMQPConnection::pconnect' => ['bool'], -'AMQPConnection::pdisconnect' => ['bool'], -'AMQPConnection::preconnect' => ['bool'], -'AMQPConnection::reconnect' => ['bool'], -'AMQPConnection::setHost' => ['bool', 'host'=>'string'], -'AMQPConnection::setLogin' => ['bool', 'login'=>'string'], -'AMQPConnection::setPassword' => ['bool', 'password'=>'string'], -'AMQPConnection::setPort' => ['bool', 'port'=>'int'], -'AMQPConnection::setReadTimeout' => ['bool', 'timeout'=>'int'], -'AMQPConnection::setTimeout' => ['bool', 'timeout'=>'int'], -'AMQPConnection::setVhost' => ['bool', 'vhost'=>'string'], -'AMQPConnection::setWriteTimeout' => ['bool', 'timeout'=>'int'], -'AMQPEnvelope::getAppId' => ['string'], +'AMQPConnection::isPersistent' => ['bool'], +'AMQPConnection::pconnect' => ['void'], +'AMQPConnection::pdisconnect' => ['void'], +'AMQPConnection::preconnect' => ['void'], +'AMQPConnection::reconnect' => ['void'], +'AMQPConnection::setHost' => ['void', 'host'=>'string'], +'AMQPConnection::setLogin' => ['void', 'login'=>'string'], +'AMQPConnection::setPassword' => ['void', 'password'=>'string'], +'AMQPConnection::setPort' => ['void', 'port'=>'int'], +'AMQPConnection::setReadTimeout' => ['void', 'timeout'=>'float'], +'AMQPConnection::setTimeout' => ['void', 'timeout'=>'float'], +'AMQPConnection::setVhost' => ['void', 'vhost'=>'string'], +'AMQPConnection::setWriteTimeout' => ['void', 'timeout'=>'float'], +'AMQPEnvelope::getAppId' => ['string|null'], 'AMQPEnvelope::getBody' => ['string'], -'AMQPEnvelope::getContentEncoding' => ['string'], -'AMQPEnvelope::getContentType' => ['string'], -'AMQPEnvelope::getCorrelationId' => ['string'], +'AMQPEnvelope::getContentEncoding' => ['string|null'], +'AMQPEnvelope::getContentType' => ['string|null'], +'AMQPEnvelope::getCorrelationId' => ['string|null'], 'AMQPEnvelope::getDeliveryMode' => ['int'], -'AMQPEnvelope::getDeliveryTag' => ['string'], -'AMQPEnvelope::getExchangeName' => ['string'], -'AMQPEnvelope::getExpiration' => ['string'], -'AMQPEnvelope::getHeader' => ['bool|string', 'header_key'=>'string'], -'AMQPEnvelope::getHeaders' => ['array'], -'AMQPEnvelope::getMessageId' => ['string'], -'AMQPEnvelope::getPriority' => ['int'], -'AMQPEnvelope::getReplyTo' => ['string'], +'AMQPEnvelope::getDeliveryTag' => ['int|null'], +'AMQPEnvelope::getExchangeName' => ['string|null'], +'AMQPEnvelope::getExpiration' => ['string|null'], +'AMQPEnvelope::getHeader' => ['mixed', 'headerName'=>'string'], +'AMQPEnvelope::getHeaders' => ['array'], +'AMQPEnvelope::getMessageId' => ['string|null'], +'AMQPEnvelope::getPriority' => ['int<0, max>'], +'AMQPEnvelope::getReplyTo' => ['string|null'], 'AMQPEnvelope::getRoutingKey' => ['string'], -'AMQPEnvelope::getTimeStamp' => ['string'], -'AMQPEnvelope::getType' => ['string'], -'AMQPEnvelope::getUserId' => ['string'], +'AMQPEnvelope::getTimestamp' => ['int|null'], +'AMQPEnvelope::getType' => ['string|null'], +'AMQPEnvelope::getUserId' => ['string|null'], 'AMQPEnvelope::isRedelivery' => ['bool'], -'AMQPExchange::__construct' => ['void', 'amqp_channel'=>'AMQPChannel'], -'AMQPExchange::bind' => ['bool', 'exchange_name'=>'string', 'routing_key='=>'string', 'arguments='=>'array'], -'AMQPExchange::declareExchange' => ['bool'], -'AMQPExchange::delete' => ['bool', 'exchangeName='=>'string', 'flags='=>'int'], -'AMQPExchange::getArgument' => ['bool|int|string', 'key'=>'string'], -'AMQPExchange::getArguments' => ['array'], +'AMQPExchange::__construct' => ['void', 'channel'=>'AMQPChannel'], +'AMQPExchange::bind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], +'AMQPExchange::declareExchange' => ['void'], +'AMQPExchange::delete' => ['void', 'exchangeName='=>'string', 'flags='=>'int'], +'AMQPExchange::getArgument' => ['scalar|null', 'argumentName'=>'string'], +'AMQPExchange::getArguments' => ['array'], 'AMQPExchange::getChannel' => ['AMQPChannel'], 'AMQPExchange::getConnection' => ['AMQPConnection'], 'AMQPExchange::getFlags' => ['int'], -'AMQPExchange::getName' => ['string'], -'AMQPExchange::getType' => ['string'], -'AMQPExchange::publish' => ['bool', 'message'=>'string', 'routing_key='=>'string', 'flags='=>'int', 'attributes='=>'array'], -'AMQPExchange::setArgument' => ['bool', 'key'=>'string', 'value'=>'int|string'], -'AMQPExchange::setArguments' => ['bool', 'arguments'=>'array'], -'AMQPExchange::setFlags' => ['bool', 'flags'=>'int'], -'AMQPExchange::setName' => ['bool', 'exchange_name'=>'string'], -'AMQPExchange::setType' => ['bool', 'exchange_type'=>'string'], -'AMQPExchange::unbind' => ['bool', 'exchange_name'=>'string', 'routing_key='=>'string', 'arguments='=>'array'], -'AMQPQueue::__construct' => ['void', 'amqp_channel'=>'AMQPChannel'], -'AMQPQueue::ack' => ['bool', 'delivery_tag'=>'string', 'flags='=>'int'], -'AMQPQueue::bind' => ['bool', 'exchange_name'=>'string', 'routing_key='=>'string', 'arguments='=>'array'], -'AMQPQueue::cancel' => ['bool', 'consumer_tag='=>'string'], -'AMQPQueue::consume' => ['void', 'callback='=>'?callable', 'flags='=>'int', 'consumerTag='=>'string'], +'AMQPExchange::getName' => ['string|null'], +'AMQPExchange::getType' => ['string|null'], +'AMQPExchange::publish' => ['void', 'message'=>'string', 'routingKey='=>'string|null', 'flags='=>'int|null', 'header='=>'array'], +'AMQPExchange::setArgument' => ['void', 'argumentName'=>'string', 'argumentValue'=>'scalar|null'], +'AMQPExchange::setArguments' => ['void', 'arguments'=>'array'], +'AMQPExchange::setFlags' => ['void', 'flags'=>'int|null'], +'AMQPExchange::setName' => ['void', 'exchangeName'=>'string|null'], +'AMQPExchange::setType' => ['void', 'exchangeType'=>'string|null'], +'AMQPExchange::unbind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], +'AMQPQueue::__construct' => ['void', 'channel'=>'AMQPChannel'], +'AMQPQueue::ack' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], +'AMQPQueue::bind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], +'AMQPQueue::cancel' => ['void', 'consumerTag='=>'string'], +'AMQPQueue::consume' => ['void', 'callback='=>'null|callable(AMQPEnvelope, AMQPQueue): mixed', 'flags='=>'int|null', 'consumerTag='=>'string|null'], 'AMQPQueue::declareQueue' => ['int'], -'AMQPQueue::delete' => ['int', 'flags='=>'int'], -'AMQPQueue::get' => ['AMQPEnvelope|bool', 'flags='=>'int'], -'AMQPQueue::getArgument' => ['bool|int|string', 'key'=>'string'], +'AMQPQueue::delete' => ['int', 'flags='=>'int|null'], +'AMQPQueue::get' => ['AMQPEnvelope|null', 'flags='=>'int|null'], +'AMQPQueue::getArgument' => ['scalar|null|array|AMQPValue|AMQPDecimal|AMQPTimestamp', 'argumentName'=>'string'], 'AMQPQueue::getArguments' => ['array'], 'AMQPQueue::getChannel' => ['AMQPChannel'], 'AMQPQueue::getConnection' => ['AMQPConnection'], 'AMQPQueue::getFlags' => ['int'], -'AMQPQueue::getName' => ['string'], -'AMQPQueue::nack' => ['bool', 'delivery_tag'=>'string', 'flags='=>'int'], -'AMQPQueue::purge' => ['bool'], -'AMQPQueue::reject' => ['bool', 'delivery_tag'=>'string', 'flags='=>'int'], -'AMQPQueue::setArgument' => ['bool', 'key'=>'string', 'value'=>'mixed'], -'AMQPQueue::setArguments' => ['bool', 'arguments'=>'array'], -'AMQPQueue::setFlags' => ['bool', 'flags'=>'int'], -'AMQPQueue::setName' => ['bool', 'queue_name'=>'string'], -'AMQPQueue::unbind' => ['bool', 'exchange_name'=>'string', 'routing_key='=>'string', 'arguments='=>'array'], +'AMQPQueue::getName' => ['string|null'], +'AMQPQueue::nack' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], +'AMQPQueue::purge' => ['int'], +'AMQPQueue::reject' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], +'AMQPQueue::setArgument' => ['void', 'argumentName'=>'string', 'argumentValue'=>'scalar|null|array|AMQPValue|AMQPDecimal|AMQPTimestamp'], +'AMQPQueue::setArguments' => ['void', 'arguments'=>'array'], +'AMQPQueue::setFlags' => ['void', 'flags'=>'int|null'], +'AMQPQueue::setName' => ['void', 'name'=>'string'], +'AMQPQueue::unbind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], 'apache_child_terminate' => ['bool'], 'apache_get_modules' => ['array'], 'apache_get_version' => ['string|false'], @@ -211,18 +209,18 @@ 'APCIterator::valid' => ['bool'], 'apcu_add' => ['bool', 'key'=>'string', 'var'=>'', 'ttl='=>'int'], 'apcu_add\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], -'apcu_cache_info' => ['array', 'limited='=>'bool'], +'apcu_cache_info' => ['__benevolent|false>', 'limited='=>'bool'], 'apcu_cas' => ['bool', 'key'=>'string', 'old'=>'int', 'new'=>'int'], 'apcu_clear_cache' => ['bool'], 'apcu_dec' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], 'apcu_delete' => ['bool', 'key'=>'string|APCuIterator'], -'apcu_delete\'1' => ['array', 'key'=>'string[]'], +'apcu_delete\'1' => ['list', 'key'=>'string[]'], 'apcu_entry' => ['mixed', 'key'=>'string', 'generator'=>'callable', 'ttl='=>'int'], 'apcu_exists' => ['bool', 'keys'=>'string'], 'apcu_exists\'1' => ['array', 'keys'=>'string[]'], 'apcu_fetch' => ['mixed', 'key'=>'string|string[]', '&w_success='=>'bool'], 'apcu_inc' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], -'apcu_sma_info' => ['array', 'limited='=>'bool'], +'apcu_sma_info' => ['__benevolent', 'limited='=>'bool'], 'apcu_store' => ['bool', 'key'=>'string', 'var='=>'', 'ttl='=>'int'], 'apcu_store\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], 'APCuIterator::__construct' => ['void', 'search='=>'string|string[]|null', 'format='=>'int', 'chunk_size='=>'int', 'list='=>'int'], @@ -259,7 +257,7 @@ 'AppendIterator::rewind' => ['void'], 'AppendIterator::valid' => ['bool'], 'array_change_key_case' => ['array', 'input'=>'array', 'case='=>'int'], -'array_chunk' => ['array[]', 'input'=>'array', 'size'=>'positive-int', 'preserve_keys='=>'bool'], +'array_chunk' => ['list', 'input'=>'array', 'size'=>'positive-int', 'preserve_keys='=>'bool'], 'array_column' => ['array', 'array'=>'array', 'column_key'=>'mixed', 'index_key='=>'mixed'], 'array_combine' => ['array|false', 'keys'=>'array', 'values'=>'array'], 'array_count_values' => ['array', 'input'=>'array'], @@ -282,13 +280,13 @@ 'array_intersect_ukey' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'], 'array_intersect_ukey\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest'=>'array|callable(mixed,mixed):int'], 'array_key_exists' => ['bool', 'key'=>'string|int', 'search'=>'array'], -'array_key_first' => ['int|string|null', 'array' => 'array'], -'array_key_last' => ['int|string|null', 'array' => 'array'], -'array_keys' => ['array', 'input'=>'array', 'search_value='=>'mixed', 'strict='=>'bool'], +'array_key_first' => ['int|string|null', 'array'=>'array'], +'array_key_last' => ['int|string|null', 'array'=>'array'], +'array_keys' => ['list', 'input'=>'array', 'search_value='=>'mixed', 'strict='=>'bool'], 'array_map' => ['array', 'callback'=>'?callable', 'array'=>'array', '...args='=>'array'], 'array_merge' => ['array', 'arr1'=>'array', '...args='=>'array'], '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'], +'array_multisort' => ['bool', 'array1'=>'array', 'array1_sort_order='=>'array|int', 'array1_sort_flags='=>'array|int', '...args='=>'array|int'], 'array_pad' => ['array', 'input'=>'array', 'pad_size'=>'int', 'pad_value'=>'mixed'], 'array_pop' => ['mixed', '&rw_stack'=>'array'], 'array_product' => ['int|float', 'input'=>'array'], @@ -318,7 +316,7 @@ 'array_uintersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', 'arg5'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_unique' => ['array', 'array'=>'array', 'flags='=>'int'], 'array_unshift' => ['positive-int', '&rw_stack'=>'array', 'var'=>'mixed', '...vars='=>'mixed'], -'array_values' => ['array', 'input'=>'array'], +'array_values' => ['list', 'input'=>'array'], 'array_walk' => ['bool', '&rw_input'=>'array|object', 'callback'=>'callable', 'userdata='=>'mixed'], 'array_walk_recursive' => ['bool', '&rw_input'=>'array|object', 'callback'=>'callable', 'userdata='=>'mixed'], 'ArrayAccess::offsetExists' => ['bool', 'offset'=>'mixed'], @@ -349,7 +347,7 @@ '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'], +'ArrayObject::__construct' => ['void', 'input='=>'array|object', 'flags='=>'int', 'iterator_class='=>'class-string'], 'ArrayObject::append' => ['void', 'value'=>'mixed'], 'ArrayObject::asort' => ['void'], 'ArrayObject::count' => ['0|positive-int'], @@ -395,7 +393,7 @@ 'BadFunctionCallException::getLine' => ['int'], 'BadFunctionCallException::getMessage' => ['string'], 'BadFunctionCallException::getPrevious' => ['(?Throwable)|(?BadFunctionCallException)'], -'BadFunctionCallException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'BadFunctionCallException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'BadFunctionCallException::getTraceAsString' => ['string'], 'BadMethodCallException::__clone' => ['void'], 'BadMethodCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?BadMethodCallException)'], @@ -405,9 +403,10 @@ 'BadMethodCallException::getLine' => ['int'], 'BadMethodCallException::getMessage' => ['string'], 'BadMethodCallException::getPrevious' => ['(?Throwable)|(?BadMethodCallException)'], -'BadMethodCallException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'BadMethodCallException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'BadMethodCallException::getTraceAsString' => ['string'], -'base64_decode' => ['string|false', 'str'=>'string', 'strict='=>'bool'], +'base64_decode' => ['string', 'str'=>'string', 'strict='=>'false'], +'base64_decode\'1' => ['string|false', 'str'=>'string', 'strict='=>'true'], 'base64_encode' => ['string', 'str'=>'string'], 'base_convert' => ['string', 'number'=>'string', 'frombase'=>'int', 'tobase'=>'int'], 'basename' => ['string', 'path'=>'string', 'suffix='=>'string'], @@ -418,11 +417,11 @@ 'bbcode_parse' => ['string', 'bbcode_container'=>'resource', 'to_parse'=>'string'], 'bbcode_set_arg_parser' => ['bool', 'bbcode_container'=>'resource', 'bbcode_arg_parser'=>'resource'], 'bbcode_set_flags' => ['bool', 'bbcode_container'=>'resource', 'flags'=>'int', 'mode='=>'int'], -'bcadd' => ['numeric-string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bccomp' => ['int', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bcdiv' => ['numeric-string|null', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bcmod' => ['numeric-string|null', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bcmul' => ['numeric-string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], +'bcadd' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], +'bccomp' => ['0|1|-1', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], +'bcdiv' => ['numeric-string|null', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], +'bcmod' => ['numeric-string|null', 'left_operand'=>'string', 'right_operand'=>'numeric-string', 'scale='=>'int'], +'bcmul' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], 'bcompiler_load' => ['bool', 'filename'=>'string'], 'bcompiler_load_exe' => ['bool', 'filename'=>'string'], 'bcompiler_parse_class' => ['bool', 'class'=>'string', 'callback'=>'string'], @@ -436,11 +435,11 @@ 'bcompiler_write_functions_from_file' => ['bool', 'filehandle'=>'resource', 'filename'=>'string'], 'bcompiler_write_header' => ['bool', 'filehandle'=>'resource', 'write_ver='=>'string'], 'bcompiler_write_included_filename' => ['bool', 'filehandle'=>'resource', 'filename'=>'string'], -'bcpow' => ['numeric-string', 'base'=>'string', 'exponent'=>'string', 'scale='=>'int'], -'bcpowmod' => ['numeric-string|null', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], +'bcpow' => ['numeric-string', 'base'=>'numeric-string', 'exponent'=>'numeric-string', 'scale='=>'int'], +'bcpowmod' => ['numeric-string|null', 'base'=>'numeric-string', 'exponent'=>'numeric-string', 'modulus'=>'string', 'scale='=>'int'], 'bcscale' => ['int', 'scale='=>'int'], -'bcsqrt' => ['numeric-string', 'operand'=>'string', 'scale='=>'int'], -'bcsub' => ['numeric-string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], +'bcsqrt' => ['numeric-string', 'operand'=>'numeric-string', 'scale='=>'int'], +'bcsub' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], 'bin2hex' => ['string', 'data'=>'string'], 'bind_textdomain_codeset' => ['string|false', 'domain'=>'string', 'codeset'=>'string'], 'bindec' => ['float|int', 'binary_number'=>'string'], @@ -932,7 +931,7 @@ 'CallbackFilterIterator::next' => ['void'], 'CallbackFilterIterator::rewind' => ['void'], 'CallbackFilterIterator::valid' => ['bool'], -'ceil' => ['float|false', 'number'=>'float'], +'ceil' => ['__benevolent', 'number'=>'float'], 'chdb::__construct' => ['void', 'pathname'=>'string'], 'chdb::get' => ['string', 'key'=>'string'], 'chdb_create' => ['bool', 'pathname'=>'string', 'data'=>'array'], @@ -948,9 +947,9 @@ 'chunk_split' => ['string', 'str'=>'string', 'chunklen='=>'positive-int', 'ending='=>'string'], 'class_alias' => ['bool', 'user_class_name'=>'string', 'alias_name'=>'string', 'autoload='=>'bool'], 'class_exists' => ['bool', 'classname'=>'string', 'autoload='=>'bool'], -'class_implements' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], +'class_implements' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], 'class_parents' => ['array|false', 'instance'=>'object|string', 'autoload='=>'bool'], -'class_uses' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], +'class_uses' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], 'classkit_import' => ['array', 'filename'=>'string'], 'classkit_method_add' => ['bool', 'classname'=>'string', 'methodname'=>'string', 'args'=>'string', 'code'=>'string', 'flags='=>'int'], 'classkit_method_copy' => ['bool', 'dclass'=>'string', 'dmethod'=>'string', 'sclass'=>'string', 'smethod='=>'string'], @@ -989,14 +988,14 @@ 'ClosedGeneratorException::getLine' => ['int'], 'ClosedGeneratorException::getMessage' => ['string'], 'ClosedGeneratorException::getPrevious' => ['Throwable|ClosedGeneratorException|null'], -'ClosedGeneratorException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'ClosedGeneratorException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'ClosedGeneratorException::getTraceAsString' => ['string'], 'closedir' => ['void', 'dir_handle='=>'resource'], 'closelog' => ['bool'], 'Closure::__construct' => ['void'], 'Closure::__invoke' => ['', '...args='=>''], -'Closure::bind' => ['Closure', 'old'=>'Closure', 'to'=>'?object', 'scope='=>'object|string'], -'Closure::bindTo' => ['Closure', 'new'=>'?object', 'newscope='=>'object|string'], +'Closure::bind' => ['__benevolent', 'old'=>'Closure', 'to'=>'?object', 'scope='=>'object|class-string|\'static\'|null'], +'Closure::bindTo' => ['__benevolent', 'new'=>'?object', 'newscope='=>'object|class-string|\'static\'|null'], 'Closure::call' => ['', 'to'=>'object', '...parameters='=>''], 'Closure::fromCallable' => ['Closure', 'callable'=>'callable'], 'clusterObj::convertToString' => ['string'], @@ -1007,7 +1006,7 @@ 'Collator::__construct' => ['void', 'locale'=>'string'], 'Collator::asort' => ['bool', '&rw_arr'=>'array', 'sort_flag='=>'int'], 'Collator::compare' => ['int|false', 'str1'=>'string', 'str2'=>'string'], -'Collator::create' => ['Collator', 'locale'=>'string'], +'Collator::create' => ['?Collator', 'locale'=>'string'], 'Collator::getAttribute' => ['int', 'attr'=>'int'], 'Collator::getErrorCode' => ['int'], 'Collator::getErrorMessage' => ['string'], @@ -1020,7 +1019,7 @@ 'Collator::sortWithSortKeys' => ['bool', '&rw_arr'=>'array'], 'collator_asort' => ['bool', 'coll'=>'collator', '&rw_arr'=>'array', 'sort_flag='=>'int'], 'collator_compare' => ['int|false', 'coll'=>'collator', 'str1'=>'string', 'str2'=>'string'], -'collator_create' => ['Collator', 'locale'=>'string'], +'collator_create' => ['?Collator', 'locale'=>'string'], 'collator_get_attribute' => ['int|false', 'coll'=>'collator', 'attr'=>'int'], 'collator_get_error_code' => ['int|false', 'coll'=>'collator'], 'collator_get_error_message' => ['string|false', 'coll'=>'collator'], @@ -1060,7 +1059,7 @@ 'componere\cast_by_ref' => ['Type', 'arg1'=>'', 'object'=>''], 'confirm_pdo_ibm_compiled' => [''], 'connection_aborted' => ['0|1'], -'connection_status' => ['int'], +'connection_status' => ['int-mask'], 'connection_timeout' => ['int'], 'constant' => ['mixed', 'const_name'=>'string'], 'convert_cyr_string' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], @@ -1364,7 +1363,7 @@ 'Couchbase\WildcardSearchQuery::jsonSerialize' => ['array'], 'Couchbase\zlibCompress' => ['string', 'data'=>'string'], 'Couchbase\zlibDecompress' => ['string', 'data'=>'string'], -'count' => ['0|positive-int', 'var'=>'Countable|array', 'mode='=>'int'], +'count' => ['0|positive-int', 'var'=>'Countable|array', 'mode='=>'0|1'], 'count_chars' => ['mixed', 'input'=>'string', 'mode='=>'0|1|2|3|4'], 'Countable::count' => ['0|positive-int'], 'crack_check' => ['bool', 'dictionary'=>'', 'password'=>'string'], @@ -1374,7 +1373,7 @@ 'crash' => [''], 'crc32' => ['int', 'str'=>'string'], 'create_function' => ['string', 'args'=>'string', 'code'=>'string'], -'crypt' => ['string', 'str'=>'string', 'salt='=>'string'], +'crypt' => ['non-empty-string', 'str'=>'string', 'salt='=>'string'], 'ctype_alnum' => ['bool', 'c'=>'mixed'], 'ctype_alpha' => ['bool', 'c'=>'mixed'], 'ctype_cntrl' => ['bool', 'c'=>'mixed'], @@ -1493,14 +1492,14 @@ 'curl_exec' => ['bool|string', 'ch'=>'resource'], 'curl_file_create' => ['CURLFile', 'filename'=>'string', 'mimetype='=>'string', 'postfilename='=>'string'], 'curl_getinfo' => ['mixed', 'ch'=>'resource', 'option='=>'int'], -'curl_init' => ['resource|false', 'url='=>'string'], +'curl_init' => ['__benevolent', 'url='=>'string'], 'curl_multi_add_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], 'curl_multi_close' => ['void', 'mh'=>'resource'], 'curl_multi_errno' => ['int', 'mh'=>'resource'], 'curl_multi_exec' => ['int', 'mh'=>'resource', '&w_still_running'=>'int'], -'curl_multi_getcontent' => ['string', 'ch'=>'resource'], +'curl_multi_getcontent' => ['string|null', 'ch'=>'resource'], 'curl_multi_info_read' => ['array|false', 'mh'=>'resource', '&w_msgs_in_queue='=>'int'], -'curl_multi_init' => ['resource|false'], +'curl_multi_init' => ['resource'], 'curl_multi_remove_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], 'curl_multi_select' => ['int', 'mh'=>'resource', 'timeout='=>'float'], 'curl_multi_setopt' => ['bool', 'mh'=>'resource', 'option'=>'int', 'value'=>'mixed'], @@ -1533,16 +1532,16 @@ 'cyrus_unbind' => ['bool', 'connection'=>'resource', 'trigger_name'=>'string'], 'date' => ['string', 'format'=>'string', 'timestamp='=>'int'], 'date_add' => ['DateTime|false', 'object'=>'', 'interval'=>''], -'date_create' => ['DateTime|false', 'time='=>'string|null', 'timezone='=>'?\DateTimeZone'], -'date_create_from_format' => ['DateTime|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?\DateTimeZone'], -'date_create_immutable' => ['DateTimeImmutable|false', 'time='=>'string', 'timezone='=>'?\DateTimeZone'], -'date_create_immutable_from_format' => ['DateTimeImmutable|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?\DateTimeZone'], +'date_create' => ['DateTime|false', 'time='=>'string|null', 'timezone='=>'?DateTimeZone'], +'date_create_from_format' => ['DateTime|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], +'date_create_immutable' => ['DateTimeImmutable|false', 'time='=>'string', 'timezone='=>'?DateTimeZone'], +'date_create_immutable_from_format' => ['DateTimeImmutable|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], 'date_date_set' => ['DateTime|false', 'object'=>'', 'year'=>'', 'month'=>'', 'day'=>''], 'date_default_timezone_get' => ['string'], 'date_default_timezone_set' => ['bool', 'timezone_identifier'=>'string'], 'date_diff' => ['DateInterval', 'obj1'=>'DateTimeInterface', 'obj2'=>'DateTimeInterface', 'absolute='=>'bool'], 'date_format' => ['string', 'obj'=>'DateTimeInterface', 'format'=>'string'], -'date_get_last_errors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], +'date_get_last_errors' => ['array{warning_count: 0|positive-int, warnings: list, error_count: 0|positive-int, errors: list}|false'], 'date_interval_create_from_date_string' => ['DateInterval|false', 'time'=>'string'], 'date_interval_format' => ['string', 'object'=>'DateInterval', 'format'=>'string'], 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], @@ -1551,7 +1550,7 @@ 'date_parse' => ['array{year: int|false, month: int|false, day: int|false, hour: int|false, minute: int|false, second: int|false, fraction: float|false, warning_count: int, warnings: string[], error_count: int, errors: string[], is_localtime: bool, zone_type?: int|bool, zone?: int|bool, is_dst?: bool, tz_abbr?: string, tz_id?: string, relative?: array{year: int, month: int, day: int, hour: int, minute: int, second: int, weekday?: int, weekdays?: int, first_day_of_month?: bool, last_day_of_month?: bool}}', 'date'=>'string'], 'date_parse_from_format' => ['array{year: int|false, month: int|false, day: int|false, hour: int|false, minute: int|false, second: int|false, fraction: float|false, warning_count: int, warnings: string[], error_count: int, errors: string[], is_localtime: bool, zone_type?: int|bool, zone?: int|bool, is_dst?: bool, tz_abbr?: string, tz_id?: string, relative?: array{year: int, month: int, day: int, hour: int, minute: int, second: int, weekday?: int, weekdays?: int, first_day_of_month?: bool, last_day_of_month?: bool}}', 'format'=>'string', 'date'=>'string'], 'date_sub' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], -'date_sun_info' => ['array', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], +'date_sun_info' => ['__benevolent', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_sunrise' => ['mixed', 'time'=>'int', 'format='=>'int', 'latitude='=>'float', 'longitude='=>'float', 'zenith='=>'float', 'gmt_offset='=>'float'], 'date_sunset' => ['mixed', 'time'=>'int', 'format='=>'int', 'latitude='=>'float', 'longitude='=>'float', 'zenith='=>'float', 'gmt_offset='=>'float'], 'date_time_set' => ['DateTime|false', 'object'=>'', 'hour'=>'', 'minute'=>'', 'second='=>'', 'microseconds='=>''], @@ -1563,7 +1562,7 @@ 'datefmt_format' => ['string|false', 'fmt'=>'IntlDateFormatter', 'value'=>'DateTime|IntlCalendar|array|int'], 'datefmt_format_object' => ['string|false', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], 'datefmt_get_calendar' => ['int|false', 'fmt'=>'IntlDateFormatter'], -'datefmt_get_calendar_object' => ['IntlCalendar|false', 'fmt'=>'IntlDateFormatter'], +'datefmt_get_calendar_object' => ['IntlCalendar|false|null', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_datetype' => ['int|false', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_error_code' => ['int', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_error_message' => ['string', 'fmt'=>'IntlDateFormatter'], @@ -1573,17 +1572,17 @@ 'datefmt_get_timezone' => ['IntlTimeZone|false'], 'datefmt_get_timezone_id' => ['string|false', 'fmt'=>'IntlDateFormatter'], 'datefmt_is_lenient' => ['bool', 'fmt'=>'IntlDateFormatter'], -'datefmt_localtime' => ['array|bool|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], -'datefmt_parse' => ['int|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], +'datefmt_localtime' => ['array|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], +'datefmt_parse' => ['int|float|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], 'datefmt_set_calendar' => ['bool', 'fmt'=>'IntlDateFormatter', 'which'=>'int'], -'datefmt_set_lenient' => ['?bool', 'fmt'=>'IntlDateFormatter', 'lenient'=>'bool'], +'datefmt_set_lenient' => ['void', 'fmt'=>'IntlDateFormatter', 'lenient'=>'bool'], 'datefmt_set_pattern' => ['bool', 'fmt'=>'IntlDateFormatter', 'pattern'=>'string'], 'datefmt_set_timezone' => ['bool', 'zone'=>'mixed'], 'datefmt_set_timezone_id' => ['bool', 'fmt'=>'IntlDateFormatter', 'zone'=>'string'], 'DateInterval::__construct' => ['void', 'spec'=>'string'], 'DateInterval::__set_state' => ['DateInterval', 'array'=>'array'], 'DateInterval::__wakeup' => ['void'], -'DateInterval::createFromDateString' => ['DateInterval', 'time'=>'string'], +'DateInterval::createFromDateString' => ['DateInterval|false', 'time'=>'string'], 'DateInterval::format' => ['string', 'format'=>'string'], 'DatePeriod::__construct' => ['void', 'start'=>'DateTimeInterface', 'interval'=>'DateInterval', 'recur'=>'int', 'options='=>'int'], 'DatePeriod::__construct\'1' => ['void', 'start'=>'DateTimeInterface', 'interval'=>'DateInterval', 'end'=>'DateTimeInterface', 'options='=>'int'], @@ -1600,11 +1599,11 @@ 'DateTime::createFromImmutable' => ['static', 'object'=>'DateTimeImmutable'], 'DateTime::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTime::format' => ['string', 'format'=>'string'], -'DateTime::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], +'DateTime::getLastErrors' => ['array{warning_count: 0|positive-int, warnings: list, error_count: 0|positive-int, errors: list}|false'], 'DateTime::getOffset' => ['int'], 'DateTime::getTimestamp' => ['int'], 'DateTime::getTimezone' => ['DateTimeZone'], -'DateTime::modify' => ['static', 'modify'=>'string'], +'DateTime::modify' => ['__benevolent', 'modify'=>'string'], 'DateTime::setDate' => ['static', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTime::setISODate' => ['static', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTime::setTime' => ['static', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], @@ -1619,11 +1618,11 @@ 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::format' => ['string', 'format'=>'string'], -'DateTimeImmutable::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], +'DateTimeImmutable::getLastErrors' => ['array{warning_count: 0|positive-int, warnings: list, error_count: 0|positive-int, errors: list}|false'], 'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int'], 'DateTimeImmutable::getTimezone' => ['DateTimeZone'], -'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], +'DateTimeImmutable::modify' => ['__benevolent', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTime' => ['static', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], @@ -1641,12 +1640,12 @@ 'DateTimeZone::getLocation' => ['array{country_code: string, latitude: float, longitude: float, comments: string}|false'], 'DateTimeZone::getName' => ['string'], 'DateTimeZone::getOffset' => ['int', 'datetime'=>'DateTimeInterface'], -'DateTimeZone::getTransitions' => ['array', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], -'DateTimeZone::listAbbreviations' => ['array'], -'DateTimeZone::listIdentifiers' => ['array', 'what='=>'int', 'country='=>'string'], -'db2_autocommit' => ['mixed', 'connection'=>'resource', 'value='=>'int'], +'DateTimeZone::getTransitions' => ['list', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], +'DateTimeZone::listAbbreviations' => ['array>'], +'DateTimeZone::listIdentifiers' => ['list', 'what='=>'int', 'country='=>'string'], +'db2_autocommit' => ['DB2_AUTOCOMMIT_OFF|DB2_AUTOCOMMIT_ON|bool', 'connection'=>'resource', 'value='=>'DB2_AUTOCOMMIT_OFF|DB2_AUTOCOMMIT_ON'], 'db2_bind_param' => ['bool', 'stmt'=>'resource', 'parameter_number'=>'int', 'variable_name'=>'string', 'parameter_type='=>'int', 'data_type='=>'int', 'precision='=>'int', 'scale='=>'int'], -'db2_client_info' => ['object|false', 'connection'=>'resource'], +'db2_client_info' => ['stdClass|false', 'connection'=>'resource'], 'db2_close' => ['bool', 'connection'=>'resource'], 'db2_column_privileges' => ['resource|false', 'connection'=>'resource', 'qualifier='=>'string', 'schema='=>'string', 'table_name='=>'string', 'column_name='=>'string'], 'db2_columns' => ['resource|false', 'connection'=>'resource', 'qualifier='=>'string', 'schema='=>'string', 'table_name='=>'string', 'column_name='=>'string'], @@ -1658,10 +1657,10 @@ 'db2_escape_string' => ['string', 'string_literal'=>'string'], 'db2_exec' => ['resource|false', 'connection'=>'resource', 'statement'=>'string', 'options='=>'array'], 'db2_execute' => ['bool', 'stmt'=>'resource', 'parameters='=>'array'], -'db2_fetch_array' => ['array|false', 'stmt'=>'resource', 'row_number='=>'int'], -'db2_fetch_assoc' => ['array|false', 'stmt'=>'resource', 'row_number='=>'int'], +'db2_fetch_array' => ['non-empty-list|false', 'stmt'=>'resource', 'row_number='=>'int'], +'db2_fetch_assoc' => ['non-empty-array|false', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_fetch_both' => ['array|false', 'stmt'=>'resource', 'row_number='=>'int'], -'db2_fetch_object' => ['object|false', 'stmt'=>'resource', 'row_number='=>'int'], +'db2_fetch_object' => ['stdClass|false', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_fetch_row' => ['bool', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_field_display_size' => ['int|false', 'stmt'=>'resource', 'column'=>'mixed'], 'db2_field_name' => ['string|false', 'stmt'=>'resource', 'column'=>'mixed'], @@ -1674,11 +1673,11 @@ 'db2_free_result' => ['bool', 'stmt'=>'resource'], 'db2_free_stmt' => ['bool', 'stmt'=>'resource'], 'db2_get_option' => ['string|false', 'resource'=>'resource', 'option'=>'string'], -'db2_last_insert_id' => ['string', 'resource'=>'resource'], +'db2_last_insert_id' => ['string|null', 'resource'=>'resource'], 'db2_lob_read' => ['string|false', 'stmt'=>'resource', 'colnum'=>'int', 'length'=>'int'], 'db2_next_result' => ['resource|false', 'stmt'=>'resource'], -'db2_num_fields' => ['int|false', 'stmt'=>'resource'], -'db2_num_rows' => ['int', 'stmt'=>'resource'], +'db2_num_fields' => ['0|positive-int|false', 'stmt'=>'resource'], +'db2_num_rows' => ['0|positive-int|false', 'stmt'=>'resource'], 'db2_pclose' => ['bool', 'resource'=>'resource'], 'db2_pconnect' => ['resource|false', 'database'=>'string', 'username'=>'string', 'password'=>'string', 'options='=>'array'], 'db2_prepare' => ['resource|false', 'connection'=>'resource', 'statement'=>'string', 'options='=>'array'], @@ -1689,7 +1688,7 @@ 'db2_procedures' => ['resource|false', 'connection'=>'resource', 'qualifier'=>'string', 'schema'=>'string', 'procedure'=>'string'], 'db2_result' => ['mixed', 'stmt'=>'resource', 'column'=>'mixed'], 'db2_rollback' => ['bool', 'connection'=>'resource'], -'db2_server_info' => ['object|false', 'connection'=>'resource'], +'db2_server_info' => ['stdClass|false', 'connection'=>'resource'], 'db2_set_option' => ['bool', 'resource'=>'resource', 'options'=>'array', 'type'=>'int'], 'db2_setoption' => [''], 'db2_special_columns' => ['resource|false', 'connection'=>'resource', 'qualifier'=>'string', 'schema'=>'string', 'table_name'=>'string', 'scope'=>'int'], @@ -1786,7 +1785,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' => ['list', 'options='=>'int|bool', 'limit='=>'int'], +'debug_backtrace' => ['list\',args?:mixed[],object?:object}>', 'options='=>'int|bool', 'limit='=>'int'], 'debug_print_backtrace' => ['void', 'options='=>'int|bool', 'limit='=>'int'], 'debug_zval_dump' => ['void', '...var'=>'mixed'], 'debugger_connect' => [''], @@ -1863,7 +1862,7 @@ 'dngettext' => ['string', 'domain'=>'string', 'msgid1'=>'string', 'msgid2'=>'string', 'count'=>'int'], 'dns_check_record' => ['bool', 'host'=>'string', 'type='=>'string'], 'dns_get_mx' => ['bool', 'hostname'=>'string', '&w_mxhosts'=>'array', '&w_weight'=>'array'], -'dns_get_record' => ['array|false', 'hostname'=>'string', 'type='=>'int', '&w_authns='=>'array', '&w_addtl='=>'array', 'raw='=>'bool'], +'dns_get_record' => ['list|false', 'hostname'=>'string', 'type='=>'int', '&w_authns='=>'array', '&w_addtl='=>'array', 'raw='=>'bool'], 'dom_document_relaxNG_validate_file' => ['bool', 'filename'=>'string'], 'dom_document_relaxNG_validate_xml' => ['bool', 'source'=>'string'], 'dom_document_schema_validate' => ['bool', 'source'=>'string', 'flags'=>'int'], @@ -1883,7 +1882,7 @@ 'DomainException::getLine' => ['int'], 'DomainException::getMessage' => ['string'], 'DomainException::getPrevious' => ['Throwable|DomainException|null'], -'DomainException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'DomainException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'DomainException::getTraceAsString' => ['string'], 'DOMAttr::__construct' => ['void', 'name'=>'string', 'value='=>'string'], 'DOMAttr::isId' => ['bool'], @@ -1899,24 +1898,24 @@ 'DOMCharacterData::substringData' => ['string', 'offset'=>'int', 'count'=>'int'], 'DOMComment::__construct' => ['void', 'value='=>'string'], 'DOMDocument::__construct' => ['void', 'version='=>'string', 'encoding='=>'string'], -'DOMDocument::createAttribute' => ['DOMAttr', 'name'=>'string'], -'DOMDocument::createAttributeNS' => ['DOMAttr', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], -'DOMDocument::createCDATASection' => ['DOMCDATASection', 'data'=>'string'], +'DOMDocument::createAttribute' => ['__benevolent', 'name'=>'string'], +'DOMDocument::createAttributeNS' => ['__benevolent', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], +'DOMDocument::createCDATASection' => ['__benevolent', 'data'=>'string'], 'DOMDocument::createComment' => ['DOMComment', 'data'=>'string'], 'DOMDocument::createDocumentFragment' => ['DOMDocumentFragment'], -'DOMDocument::createElement' => ['DOMElement', 'name'=>'string', 'value='=>'string'], -'DOMDocument::createElementNS' => ['DOMElement', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], -'DOMDocument::createEntityReference' => ['DOMEntityReference', 'name'=>'string'], -'DOMDocument::createProcessingInstruction' => ['DOMProcessingInstruction', 'target'=>'string', 'data='=>'string'], +'DOMDocument::createElement' => ['__benevolent', 'name'=>'string', 'value='=>'string'], +'DOMDocument::createElementNS' => ['__benevolent', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], +'DOMDocument::createEntityReference' => ['__benevolent', 'name'=>'string'], +'DOMDocument::createProcessingInstruction' => ['__benevolent', 'target'=>'string', 'data='=>'string'], 'DOMDocument::createTextNode' => ['DOMText', 'content'=>'string'], 'DOMDocument::getElementById' => ['DOMElement|null', 'elementid'=>'string'], 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], 'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], 'DOMDocument::importNode' => ['DOMNode', 'importednode'=>'DOMNode', 'deep='=>'bool'], -'DOMDocument::load' => ['mixed', 'filename'=>'string', 'options='=>'int'], +'DOMDocument::load' => ['bool', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::loadHTML' => ['bool', 'source'=>'string', 'options='=>'int'], 'DOMDocument::loadHTMLFile' => ['bool', 'filename'=>'string', 'options='=>'int'], -'DOMDocument::loadXML' => ['mixed', 'source'=>'string', 'options='=>'int'], +'DOMDocument::loadXML' => ['bool', 'source'=>'string', 'options='=>'int'], 'DOMDocument::normalizeDocument' => ['void'], 'DOMDocument::registerNodeClass' => ['bool', 'baseclass'=>'string', 'extendedclass'=>'string'], 'DOMDocument::relaxNGValidate' => ['bool', 'filename'=>'string'], @@ -1987,7 +1986,7 @@ 'DOMNode::isDefaultNamespace' => ['bool', 'namespaceuri'=>'string'], 'DOMNode::isSameNode' => ['bool', 'node'=>'DOMNode'], 'DOMNode::isSupported' => ['bool', 'feature'=>'string', 'version'=>'string'], -'DOMNode::lookupNamespaceURI' => ['string', 'prefix'=>'string'], +'DOMNode::lookupNamespaceURI' => ['?string', 'prefix'=>'?string'], 'DOMNode::lookupPrefix' => ['string', 'namespaceuri'=>'string'], 'DOMNode::normalize' => ['void'], 'DOMNode::removeChild' => ['DOMNode', 'oldnode'=>'DOMNode'], @@ -2000,7 +1999,7 @@ 'DOMText::__construct' => ['void', 'value='=>'string'], 'DOMText::isElementContentWhitespace' => ['bool'], 'DOMText::isWhitespaceInElementContent' => ['bool'], -'DOMText::splitText' => ['DOMText', 'offset'=>'int'], +'DOMText::splitText' => ['DOMText|false', 'offset'=>'int'], 'domxml_new_doc' => ['DomDocument', 'version'=>'string'], 'domxml_open_file' => ['DomDocument', 'filename'=>'string', 'mode='=>'int', 'error='=>'array'], 'domxml_open_mem' => ['DomDocument', 'str'=>'string', 'mode='=>'int', 'error='=>'array'], @@ -2020,7 +2019,7 @@ 'DomXsltStylesheet::result_dump_mem' => ['string', 'xmldoc'=>'DOMDocument'], 'DOTNET::__construct' => ['void', 'assembly_name'=>'string', 'class_name'=>'string', 'codepage='=>'int'], 'dotnet_load' => ['int', 'assembly_name'=>'string', 'datatype_name='=>'string', 'codepage='=>'int'], -'doubleval' => ['float', 'var'=>'mixed'], +'doubleval' => ['float', 'var'=>'scalar|array|resource|null'], 'Ds\Collection::clear' => ['void'], 'Ds\Collection::copy' => ['Ds\Collection'], 'Ds\Collection::isEmpty' => ['bool'], @@ -2331,11 +2330,11 @@ 'Error::getLine' => ['int'], 'Error::getMessage' => ['string'], 'Error::getPrevious' => ['Throwable|Error|null'], -'Error::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'Error::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'Error::getTraceAsString' => ['string'], 'error_clear_last' => ['void'], 'error_get_last' => ['?array{type:int,message:string,file:string,line:int}'], -'error_log' => ['bool', 'message'=>'string', 'message_type='=>'int', 'destination='=>'string', 'extra_headers='=>'string'], +'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|2|3|4', 'destination='=>'string', 'extra_headers='=>'string'], 'error_reporting' => ['int', 'new_error_level='=>'int'], 'ErrorException::__clone' => ['void'], 'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'lineno='=>'int', 'previous='=>'(?Throwable)|(?ErrorException)'], @@ -2346,7 +2345,7 @@ 'ErrorException::getMessage' => ['string'], 'ErrorException::getPrevious' => ['Throwable|ErrorException|null'], 'ErrorException::getSeverity' => ['int'], -'ErrorException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'ErrorException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'ErrorException::getTraceAsString' => ['string'], 'escapeshellarg' => ['string', 'arg'=>'string'], 'escapeshellcmd' => ['string', 'command'=>'string'], @@ -2625,9 +2624,9 @@ 'Exception::getLine' => ['int'], 'Exception::getMessage' => ['string'], 'Exception::getPrevious' => ['(?Throwable)|(?Exception)'], -'Exception::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'Exception::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'Exception::getTraceAsString' => ['string'], -'exec' => ['string', 'command'=>'string', '&w_output='=>'array', '&w_return_value='=>'int'], +'exec' => ['string|false', 'command'=>'string', '&w_output='=>'array', '&w_return_value='=>'int'], 'exif_imagetype' => ['int|false', 'imagefile'=>'string'], 'exif_read_data' => ['array|false', 'filename'=>'string|resource', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], 'exif_tagname' => ['string|false', 'index'=>'int'], @@ -2635,10 +2634,10 @@ 'exp' => ['float', 'number'=>'float'], 'expect_expectl' => ['int', 'expect'=>'resource', 'cases'=>'array', 'match='=>'array'], 'expect_popen' => ['resource|false', 'command'=>'string'], -'explode' => ['non-empty-array|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], +'explode' => ['list|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], 'expm1' => ['float', 'number'=>'float'], 'extension_loaded' => ['bool', 'extension_name'=>'string'], -'extract' => ['int', '&rw_var_array'=>'array', 'extract_type='=>'int', 'prefix='=>'string|null'], +'extract' => ['0|positive-int', 'array'=>'array', 'flags='=>'EXTR_OVERWRITE|EXTR_SKIP|EXTR_PREFIX_SAME|EXTR_PREFIX_ALL|EXTR_PREFIX_INVALID|EXTR_IF_EXISTS|EXTR_PREFIX_IF_EXISTS|EXTR_REFS', 'prefix='=>'string|null'], 'ezmlm_hash' => ['int', 'addr'=>'string'], 'fam_cancel_monitor' => ['bool', 'fam'=>'resource', 'fam_monitor'=>'resource'], 'fam_close' => ['void', 'fam'=>'resource'], @@ -2933,10 +2932,10 @@ 'ffmpeg_movie::hasAudio' => ['bool'], 'ffmpeg_movie::hasVideo' => ['bool'], 'fgetc' => ['string|false', 'fp'=>'resource'], -'fgetcsv' => ['(?array)|(?false)', 'fp'=>'resource', 'length='=>'0|positive-int', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'fgetcsv' => ['list|array{0: null}|false|null', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'fgets' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int'], 'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], -'file' => ['array|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], +'file' => ['list|false', 'filename'=>'string', 'flags='=>'int-mask', 'context='=>'resource'], 'file_exists' => ['bool', 'filename'=>'string'], 'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'maxlen='=>'0|positive-int'], 'file_put_contents' => ['0|positive-int|false', 'file'=>'string', 'data'=>'mixed', 'flags='=>'int', 'context='=>'?resource'], @@ -2967,7 +2966,7 @@ 'filter_id' => ['int|false', 'filtername'=>'string'], 'filter_input' => ['mixed', 'type'=>'int', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], 'filter_input_array' => ['array|false|null', 'type'=>'int', 'definition='=>'int|array', 'add_empty='=>'bool'], -'filter_list' => ['array'], +'filter_list' => ['non-empty-list'], 'filter_var' => ['mixed', 'variable'=>'mixed', 'filter='=>'int', 'options='=>'mixed'], 'filter_var_array' => ['array|false|null', 'data'=>'array', 'definition='=>'mixed', 'add_empty='=>'bool'], 'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'], @@ -2988,31 +2987,31 @@ 'finfo_file' => ['string|false', 'finfo'=>'resource', 'file_name'=>'string', 'options='=>'int', 'context='=>'resource'], 'finfo_open' => ['resource|false', 'options='=>'int', 'arg='=>'string'], 'finfo_set_flags' => ['bool', 'finfo'=>'resource', 'options'=>'int'], -'floatval' => ['float', 'var'=>'mixed'], -'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int', '&w_wouldblock='=>'int'], -'floor' => ['float|false', 'number'=>'float'], +'floatval' => ['float', 'var'=>'scalar|array|resource|null'], +'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int-mask', '&w_wouldblock='=>'0|1'], +'floor' => ['__benevolent', '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'], +'fopen' => ['resource|false', 'filename'=>'string', 'mode'=>'string', 'use_include_path='=>'bool', 'context='=>'resource|null'], 'forward_static_call' => ['mixed', 'function'=>'callable', '...parameters='=>'mixed'], 'forward_static_call_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], 'fpassthru' => ['0|positive-int|false', 'fp'=>'resource'], -'fpm_get_status' => ['array|false'], -'fprintf' => ['int', 'stream'=>'resource', 'format'=>'string', '...values='=>'string|int|float'], -'fputcsv' => ['0|positive-int|false', 'fp'=>'resource', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape_char='=>'string'], +'fpm_get_status' => ['array{pool: string, process-manager: \'dynamic\'|\'ondemand\'|\'static\', start-time: int<0, max>, start-since: int<0, max>, accepted-conn: int<0, max>, listen-queue: int<0, max>, max-listen-queue: int<0, max>, listen-queue-len: int<0, max>, idle-processes: int<0, max>, active-processes: int<1, max>, total-processes: int<1, max>, max-active-processes: int<1, max>, max-children-reached: 0|1, slow-requests: int<0, max>, procs: array, state: \'Idle\'|\'Running\', start-time: int<0, max>, start-since: int<0, max>, requests: int<0, max>, request-duration: int<0, max>, request-method: string, request-uri: string, query-string: string, request-length: int<0, max>, user: string, script: string, last-request-cpu: float, last-request-memory: int<0, max>}>}|false'], +'fprintf' => ['int', 'stream'=>'resource', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], +'fputcsv' => ['0|positive-int|false', 'fp'=>'resource', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape_char='=>'string'], 'fputs' => ['0|positive-int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'0|positive-int'], -'fread' => ['string|false', 'fp'=>'resource', 'length'=>'0|positive-int'], +'fread' => ['string', 'fp'=>'resource', 'length'=>'positive-int'], 'frenchtojd' => ['int', 'month'=>'int', 'day'=>'int', 'year'=>'int'], 'fribidi_log2vis' => ['string', 'str'=>'string', 'direction'=>'string', 'charset'=>'int'], -'fscanf' => ['array|int|false', 'stream'=>'resource', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], +'fscanf' => ['list|int|false', 'stream'=>'resource', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], 'fseek' => ['0|-1', 'fp'=>'resource', 'offset'=>'int', 'whence='=>'int'], 'fsockopen' => ['resource|false', 'hostname'=>'string', 'port='=>'int', '&w_errno='=>'int', '&w_errstr='=>'string', 'timeout='=>'float'], 'fstat' => ['array|false', 'fp'=>'resource'], 'ftell' => ['int|false', 'fp'=>'resource'], 'ftok' => ['int', 'pathname'=>'string', 'proj'=>'string'], 'ftp_alloc' => ['bool', 'stream'=>'resource', 'size'=>'int', '&w_response='=>'string'], -'ftp_append' => ['bool', 'ftp'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'int'], +'ftp_append' => ['bool', 'ftp'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY'], 'ftp_cdup' => ['bool', 'stream'=>'resource'], 'ftp_chdir' => ['bool', 'stream'=>'resource', 'directory'=>'string'], 'ftp_chmod' => ['int|false', 'stream'=>'resource', 'mode'=>'int', 'filename'=>'string'], @@ -3020,22 +3019,22 @@ 'ftp_connect' => ['resource|false', 'host'=>'string', 'port='=>'int', 'timeout='=>'int'], 'ftp_delete' => ['bool', 'stream'=>'resource', 'file'=>'string'], 'ftp_exec' => ['bool', 'stream'=>'resource', 'command'=>'string'], -'ftp_fget' => ['bool', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'int', 'resumepos='=>'int'], -'ftp_fput' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'int', 'startpos='=>'int'], -'ftp_get' => ['bool', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'int', 'resume_pos='=>'int'], +'ftp_fget' => ['bool', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resumepos='=>'int'], +'ftp_fput' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], +'ftp_get' => ['bool', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resume_pos='=>'int'], 'ftp_get_option' => ['mixed', 'stream'=>'resource', 'option'=>'int'], 'ftp_login' => ['bool', 'stream'=>'resource', 'username'=>'string', 'password'=>'string'], 'ftp_mdtm' => ['int', 'stream'=>'resource', 'filename'=>'string'], 'ftp_mkdir' => ['string|false', 'stream'=>'resource', 'directory'=>'string'], 'ftp_mlsd' => ['array|false', 'ftp_stream'=>'resource', 'directory'=>'string'], 'ftp_nb_continue' => ['int', 'stream'=>'resource'], -'ftp_nb_fget' => ['int', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode'=>'int', 'resumepos='=>'int'], -'ftp_nb_fput' => ['int', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode'=>'int', 'startpos='=>'int'], -'ftp_nb_get' => ['int', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode'=>'int', 'resume_pos='=>'int'], -'ftp_nb_put' => ['int|false', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode'=>'int', 'startpos='=>'int'], +'ftp_nb_fget' => ['int', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resumepos='=>'int'], +'ftp_nb_fput' => ['int', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], +'ftp_nb_get' => ['int|false', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resume_pos='=>'int'], +'ftp_nb_put' => ['int|false', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], 'ftp_nlist' => ['array|false', 'stream'=>'resource', 'directory'=>'string'], 'ftp_pasv' => ['bool', 'stream'=>'resource', 'pasv'=>'bool'], -'ftp_put' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode'=>'int', 'startpos='=>'int'], +'ftp_put' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], 'ftp_pwd' => ['string|false', 'stream'=>'resource'], 'ftp_raw' => ['array', 'stream'=>'resource', 'command'=>'string'], 'ftp_rawlist' => ['array|false', 'stream'=>'resource', 'directory'=>'string', 'recursive='=>'bool'], @@ -3048,7 +3047,7 @@ 'ftp_systype' => ['string|false', 'stream'=>'resource'], 'ftruncate' => ['bool', 'fp'=>'resource', 'size'=>'0|positive-int'], 'func_get_arg' => ['mixed', 'arg_num'=>'0|positive-int'], -'func_get_args' => ['array'], +'func_get_args' => ['list'], 'func_num_args' => ['0|positive-int'], 'function_exists' => ['bool', 'function_name'=>'string'], 'fwrite' => ['0|positive-int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'0|positive-int'], @@ -3298,19 +3297,19 @@ 'get_called_class' => ['class-string'], 'get_cfg_var' => ['mixed', 'option_name'=>'string'], 'get_class' => ['class-string', 'object='=>'object'], -'get_class_methods' => ['array', 'class'=>'mixed'], +'get_class_methods' => ['list', 'class'=>'mixed'], 'get_class_vars' => ['array', 'class_name'=>'string'], 'get_current_user' => ['string'], -'get_declared_classes' => ['array'], -'get_declared_interfaces' => ['array'], -'get_declared_traits' => ['array'], -'get_defined_constants' => ['array', 'categorize='=>'bool'], -'get_defined_functions' => ['array>', 'exclude_disabled='=>'bool'], -'get_defined_vars' => ['array'], -'get_extension_funcs' => ['list|false', 'extension_name'=>'string'], +'get_declared_classes' => ['list'], +'get_declared_interfaces' => ['list'], +'get_declared_traits' => ['list'], +'get_defined_constants' => ['array', 'categorize='=>'bool'], +'get_defined_functions' => ['array{internal:non-empty-list,user:list}', 'exclude_disabled='=>'bool'], +'get_defined_vars' => ['array'], +'get_extension_funcs' => ['list|false', 'extension_name'=>'string'], 'get_headers' => ['array|false', 'url'=>'string', 'format='=>'int', 'context='=>'resource'], 'get_html_translation_table' => ['array', 'table='=>'int', 'flags='=>'int', 'encoding='=>'string'], -'get_include_path' => ['string|false'], +'get_include_path' => ['__benevolent'], 'get_included_files' => ['list'], 'get_loaded_extensions' => ['list', 'zend_extensions='=>'bool'], 'get_magic_quotes_gpc' => ['false'], @@ -3322,23 +3321,23 @@ 'get_resource_type' => ['string', 'res'=>'resource'], 'get_resources' => ['array', 'type='=>'string'], 'getallheaders' => ['array'], -'getcwd' => ['string|false'], +'getcwd' => ['non-empty-string|false'], '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[]'], +'getenv\'1' => ['array'], 'gethostbyaddr' => ['string|false', 'ip_address'=>'string'], 'gethostbyname' => ['string', 'hostname'=>'string'], -'gethostbynamel' => ['array|false', 'hostname'=>'string'], +'gethostbynamel' => ['list|false', 'hostname'=>'string'], 'gethostname' => ['string|false'], -'getimagesize' => ['array|false', 'imagefile'=>'string', '&w_info='=>'array'], -'getimagesizefromstring' => ['array|false', 'data'=>'string', '&w_info='=>'array'], +'getimagesize' => ['array{0: 0|positive-int, 1: 0|positive-int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'imagefile'=>'string', '&w_info='=>'array'], +'getimagesizefromstring' => ['array{0: 0|positive-int, 1: 0|positive-int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'data'=>'string', '&w_info='=>'array'], 'getlastmod' => ['int|false'], 'getmxrr' => ['bool', 'hostname'=>'string', '&w_mxhosts'=>'array', '&w_weight='=>'array'], 'getmygid' => ['int|false'], 'getmyinode' => ['int|false'], 'getmypid' => ['int|false'], 'getmyuid' => ['int|false'], -'getopt' => ['__benevolent|array|array>|false>', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], +'getopt' => ['__benevolent|array|array>|false>', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], 'getprotobyname' => ['int|false', 'name'=>'string'], 'getprotobynumber' => ['string|false', 'proto'=>'int'], 'getrandmax' => ['int'], @@ -3348,7 +3347,7 @@ 'gettext' => ['string', 'msgid'=>'string'], 'gettimeofday' => ['array|float', 'get_as_float='=>'bool'], 'gettype' => ['string', 'var'=>'mixed'], -'glob' => ['array|false', 'pattern'=>'string', 'flags='=>'int'], +'glob' => ['list|false', 'pattern'=>'string', 'flags='=>'int'], 'GlobIterator::__construct' => ['void', 'path'=>'string', 'flags='=>'int'], 'GlobIterator::cont' => ['int'], 'GlobIterator::count' => ['0|positive-int'], @@ -3598,42 +3597,50 @@ 'gnupg::cleardecryptkeys' => ['bool'], 'gnupg::clearencryptkeys' => ['bool'], 'gnupg::clearsignkeys' => ['bool'], -'gnupg::decrypt' => ['string', 'text'=>'string'], -'gnupg::decryptverify' => ['array', 'text'=>'string', '&plaintext'=>'string'], -'gnupg::encrypt' => ['string', 'plaintext'=>'string'], -'gnupg::encryptsign' => ['string', 'plaintext'=>'string'], -'gnupg::export' => ['string', 'fingerprint'=>'string'], -'gnupg::geterror' => ['string'], +'gnupg::decrypt' => ['string|false', 'text'=>'string'], +'gnupg::deletekey' => ['bool', 'key'=>'string', 'allow_secret'=>'bool'], +'gnupg::decryptverify' => ['array|false', 'text'=>'string', '&plaintext'=>'string'], +'gnupg::encrypt' => ['string|false', 'plaintext'=>'string'], +'gnupg::encryptsign' => ['string|false', 'plaintext'=>'string'], +'gnupg::export' => ['string|false', 'fingerprint'=>'string'], +'gnupg::getengineinfo' => ['array'], +'gnupg::geterror' => ['string|false'], 'gnupg::getprotocol' => ['int'], -'gnupg::import' => ['array', 'keydata'=>'string'], -'gnupg::init' => ['resource'], -'gnupg::keyinfo' => ['array', 'pattern'=>'string'], +'gnupg::gettrustlist' => ['array', 'pattern'=>'string'], +'gnupg::import' => ['array|false', 'keydata'=>'string'], +'gnupg::init' => ['resource', 'options'=>'?array{file_name?:string,home_dir?:string}'], +'gnupg::keyinfo' => ['array|false', 'pattern'=>'string'], +'gnupg::listsignatures' => ['?array', 'keyid'=>'string'], 'gnupg::setarmor' => ['bool', 'armor'=>'int'], 'gnupg::seterrormode' => ['void', 'errormode'=>'int'], 'gnupg::setsignmode' => ['bool', 'signmode'=>'int'], -'gnupg::sign' => ['string', 'plaintext'=>'string'], -'gnupg::verify' => ['array', 'signed_text'=>'string', 'signature'=>'string|false', '&plaintext='=>'string'], +'gnupg::sign' => ['string|false', 'plaintext'=>'string'], +'gnupg::verify' => ['array|false', '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'], 'gnupg_cleardecryptkeys' => ['bool', 'identifier'=>'resource'], 'gnupg_clearencryptkeys' => ['bool', 'identifier'=>'resource'], 'gnupg_clearsignkeys' => ['bool', 'identifier'=>'resource'], -'gnupg_decrypt' => ['string', 'identifier'=>'resource', 'text'=>'string'], +'gnupg_decrypt' => ['string|false', 'identifier'=>'resource', 'text'=>'string'], 'gnupg_decryptverify' => ['array', 'identifier'=>'resource', 'text'=>'string', 'plaintext'=>'string'], -'gnupg_encrypt' => ['string', 'identifier'=>'resource', 'plaintext'=>'string'], -'gnupg_encryptsign' => ['string', 'identifier'=>'resource', 'plaintext'=>'string'], -'gnupg_export' => ['string', 'identifier'=>'resource', 'fingerprint'=>'string'], -'gnupg_geterror' => ['string', 'identifier'=>'resource'], +'gnupg_deletekey' => ['bool', 'identifier'=>'resource', 'key'=>'string', 'allow_secret'=>'bool'], +'gnupg_encrypt' => ['string|false', 'identifier'=>'resource', 'plaintext'=>'string'], +'gnupg_encryptsign' => ['string|false', 'identifier'=>'resource', 'plaintext'=>'string'], +'gnupg_export' => ['string|false', 'identifier'=>'resource', 'fingerprint'=>'string'], +'gnupg_getengineinfo' => ['array', 'identifier'=>'resource'], +'gnupg_geterror' => ['string|false', 'identifier'=>'resource'], 'gnupg_getprotocol' => ['int', 'identifier'=>'resource'], -'gnupg_import' => ['array', 'identifier'=>'resource', 'keydata'=>'string'], -'gnupg_init' => ['resource'], -'gnupg_keyinfo' => ['array', 'identifier'=>'resource', 'pattern'=>'string'], +'gnupg_gettrustlist' => ['array', 'identifier'=>'resource', 'pattern'=>'string'], +'gnupg_import' => ['array|false', 'identifier'=>'resource', 'keydata'=>'string'], +'gnupg_init' => ['resource', 'options='=>'?array{file_name?:string,home_dir?:string}'], +'gnupg_keyinfo' => ['array|false', 'identifier'=>'resource', 'pattern'=>'string'], +'gnupg_listsignatures' => ['?array', 'identifier'=>'resource', 'keyid'=>'string'], 'gnupg_setarmor' => ['bool', 'identifier'=>'resource', 'armor'=>'int'], '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|false', '&plaintext='=>'string'], +'gnupg_sign' => ['string|false', 'identifier'=>'resource', 'plaintext'=>'string'], +'gnupg_verify' => ['array|false', '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'], @@ -3902,18 +3909,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' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], -'hash_algos' => ['array'], +'hash' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], +'hash_algos' => ['non-empty-list'], 'hash_copy' => ['HashContext', 'context'=>'HashContext'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], -'hash_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], -'hash_final' => ['non-empty-string', 'context'=>'HashContext', 'raw_output='=>'bool'], -'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], -'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], -'hash_hmac_algos' => ['array'], -'hash_hmac_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_file' => ['non-falsy-string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], +'hash_final' => ['non-falsy-string', 'context'=>'HashContext', 'raw_output='=>'bool'], +'hash_hkdf' => ['non-falsy-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], +'hash_hmac' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_hmac_algos' => ['non-empty-list'], +'hash_hmac_file' => ['non-falsy-string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_init' => ['HashContext', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], -'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], +'hash_pbkdf2' => ['(non-falsy-string&lowercase-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'], @@ -3925,7 +3932,7 @@ 'header' => ['void', 'header'=>'string', 'replace='=>'bool', 'http_response_code='=>'int'], 'header_register_callback' => ['bool', 'callback'=>'callable'], 'header_remove' => ['void', 'name='=>'string'], -'headers_list' => ['array'], +'headers_list' => ['list'], 'headers_sent' => ['bool', '&w_file='=>'string', '&w_line='=>'int'], 'hebrev' => ['string', 'str'=>'string', 'max_chars_per_line='=>'int'], 'hebrevc' => ['string', 'str'=>'string', 'max_chars_per_line='=>'int'], @@ -3950,8 +3957,8 @@ 'HRTime\StopWatch::start' => ['void'], 'HRTime\StopWatch::stop' => ['void'], 'html_entity_decode' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string'], -'htmlentities' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string', 'double_encode='=>'bool'], -'htmlspecialchars' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string', 'double_encode='=>'bool'], +'htmlentities' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string|null', 'double_encode='=>'bool'], +'htmlspecialchars' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string|null', 'double_encode='=>'bool'], 'htmlspecialchars_decode' => ['string', 'string'=>'string', 'quote_style='=>'int'], 'http\Env\Request::__construct' => ['void'], 'http\Env\Request::getCookie' => ['mixed', 'name='=>'string', 'type='=>'mixed', 'defval='=>'mixed', 'delete='=>'bool|false'], @@ -4505,7 +4512,7 @@ 'ifxus_write_slob' => ['int', 'bid'=>'int', 'content'=>'string'], 'igbinary_serialize' => ['string|null', 'value'=>'mixed'], 'igbinary_unserialize' => ['mixed', 'str'=>'string'], -'ignore_user_abort' => ['int', 'value='=>'bool'], +'ignore_user_abort' => ['0|1', 'value='=>'bool'], 'iis_add_server' => ['int', 'path'=>'string', 'comment'=>'string', 'server_ip'=>'string', 'port'=>'int', 'host_name'=>'string', 'rights'=>'int', 'start_server'=>'int'], 'iis_get_dir_security' => ['int', 'server_instance'=>'int', 'virtual_path'=>'string'], 'iis_get_script_map' => ['string', 'server_instance'=>'int', 'virtual_path'=>'string', 'script_extension'=>'string'], @@ -4535,21 +4542,21 @@ 'imagebmp' => ['bool', 'image'=>'resource', 'to='=>'string|resource|null', 'compressed='=>'bool'], 'imagechar' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'c'=>'string', 'col'=>'int'], 'imagecharup' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'c'=>'string', 'col'=>'int'], -'imagecolorallocate' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorallocatealpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorat' => ['int|false', 'im'=>'resource', 'x'=>'int', 'y'=>'int'], -'imagecolorclosest' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorclosestalpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorclosesthwb' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], +'imagecolorallocate' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], +'imagecolorallocatealpha' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha'=>'int<0, 127>'], +'imagecolorat' => ['int<0, max>|false', 'im'=>'resource', 'x'=>'int', 'y'=>'int'], +'imagecolorclosest' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], +'imagecolorclosestalpha' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha'=>'int<0, 127>'], +'imagecolorclosesthwb' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], 'imagecolordeallocate' => ['bool', 'im'=>'resource', 'index'=>'int'], -'imagecolorexact' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorexactalpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], +'imagecolorexact' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], +'imagecolorexactalpha' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha'=>'int<0, 127>'], 'imagecolormatch' => ['bool', 'im1'=>'resource', 'im2'=>'resource'], -'imagecolorresolve' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorresolvealpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorset' => ['void', 'im'=>'resource', 'col'=>'int', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha='=>'int'], -'imagecolorsforindex' => ['array|false', 'im'=>'resource', 'col'=>'int'], -'imagecolorstotal' => ['int', 'im'=>'resource'], +'imagecolorresolve' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>'], +'imagecolorresolvealpha' => ['int<0, max>', 'im'=>'resource', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha'=>'int<0, 127>'], +'imagecolorset' => ['void', 'im'=>'resource', 'col'=>'int', 'red'=>'int<0, 255>', 'green'=>'int<0, 255>', 'blue'=>'int<0, 255>', 'alpha='=>'int<0, 127>'], +'imagecolorsforindex' => ['array{red: int<0, 255>, green: int<0, 255>, blue: int<0, 255>, alpha: int<0, 127>}', 'im'=>'resource', 'col'=>'int'], +'imagecolorstotal' => ['int<0, 256>', 'im'=>'resource'], 'imagecolortransparent' => ['int', 'im'=>'resource', 'col='=>'int'], 'imageconvolution' => ['bool', 'src_im'=>'resource', 'matrix3x3'=>'array', 'div'=>'float', 'offset'=>'float'], 'imagecopy' => ['bool', 'dst_im'=>'resource', 'src_im'=>'resource', 'dst_x'=>'int', 'dst_y'=>'int', 'src_x'=>'int', 'src_y'=>'int', 'src_w'=>'int', 'src_h'=>'int'], @@ -4557,7 +4564,7 @@ 'imagecopymergegray' => ['bool', 'src_im'=>'resource', 'dst_im'=>'resource', 'dst_x'=>'int', 'dst_y'=>'int', 'src_x'=>'int', 'src_y'=>'int', 'src_w'=>'int', 'src_h'=>'int', 'pct'=>'int'], 'imagecopyresampled' => ['bool', 'dst_im'=>'resource', 'src_im'=>'resource', 'dst_x'=>'int', 'dst_y'=>'int', 'src_x'=>'int', 'src_y'=>'int', 'dst_w'=>'int', 'dst_h'=>'int', 'src_w'=>'int', 'src_h'=>'int'], 'imagecopyresized' => ['bool', 'dst_im'=>'resource', 'src_im'=>'resource', 'dst_x'=>'int', 'dst_y'=>'int', 'src_x'=>'int', 'src_y'=>'int', 'dst_w'=>'int', 'dst_h'=>'int', 'src_w'=>'int', 'src_h'=>'int'], -'imagecreate' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], +'imagecreate' => ['__benevolent', 'x_size'=>'int<1, max>', 'y_size'=>'int<1, max>'], 'imagecreatefrombmp' => ['resource|false', 'filename'=>'string'], 'imagecreatefromgd' => ['resource|false', 'filename'=>'string'], 'imagecreatefromgd2' => ['resource|false', 'filename'=>'string'], @@ -4570,7 +4577,7 @@ 'imagecreatefromwebp' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxbm' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxpm' => ['resource|false', 'filename'=>'string'], -'imagecreatetruecolor' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], +'imagecreatetruecolor' => ['__benevolent', 'x_size'=>'int<1, max>', 'y_size'=>'int<1, max>'], 'imagecrop' => ['resource|false', 'im'=>'resource', 'rect'=>'array'], 'imagecropauto' => ['resource|false', 'im'=>'resource', 'mode='=>'int', 'threshold='=>'float', 'color='=>'int'], 'imagedashedline' => ['bool', 'im'=>'resource', 'x1'=>'int', 'y1'=>'int', 'x2'=>'int', 'y2'=>'int', 'col'=>'int'], @@ -4630,8 +4637,8 @@ 'imagesettile' => ['bool', 'image'=>'resource', 'tile'=>'resource'], 'imagestring' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'str'=>'string', 'col'=>'int'], 'imagestringup' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'str'=>'string', 'col'=>'int'], -'imagesx' => ['int', 'im'=>'resource'], -'imagesy' => ['int', 'im'=>'resource'], +'imagesx' => ['int<1, max>', 'im'=>'resource'], +'imagesy' => ['int<1, max>', 'im'=>'resource'], 'imagetruecolortopalette' => ['bool', 'im'=>'resource', 'ditherflag'=>'bool', 'colorswanted'=>'int'], 'imagettfbbox' => ['array|false', 'size'=>'float', 'angle'=>'float', 'font_file'=>'string', 'text'=>'string'], 'imagettftext' => ['array|false', 'im'=>'resource', 'size'=>'float', 'angle'=>'float', 'x'=>'int', 'y'=>'int', 'col'=>'int', 'font_file'=>'string', 'text'=>'string'], @@ -4641,25 +4648,25 @@ 'imagexbm' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'foreground='=>'int'], 'Imagick::__construct' => ['void', 'files='=>''], 'Imagick::__toString' => ['string'], -'Imagick::adaptiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], +'Imagick::adaptiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::adaptiveResizeImage' => ['bool', 'columns'=>'int', 'rows'=>'int', 'bestfit='=>'bool'], -'Imagick::adaptiveSharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], +'Imagick::adaptiveSharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::adaptiveThresholdImage' => ['bool', 'width'=>'int', 'height'=>'int', 'offset'=>'int'], 'Imagick::addImage' => ['bool', 'source'=>'imagick'], -'Imagick::addNoiseImage' => ['bool', 'noise_type'=>'int', 'channel='=>'int'], +'Imagick::addNoiseImage' => ['bool', 'noise_type'=>'Imagick::NOISE_*', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::affineTransformImage' => ['bool', 'matrix'=>'imagickdraw'], 'Imagick::animateImages' => ['bool', 'x_server'=>'string'], 'Imagick::annotateImage' => ['bool', 'draw_settings'=>'imagickdraw', 'x'=>'float', 'y'=>'float', 'angle'=>'float', 'text'=>'string'], 'Imagick::appendImages' => ['Imagick', 'stack'=>'bool'], -'Imagick::autoGammaImage' => ['bool', 'channel='=>'int'], -'Imagick::autoLevelImage' => ['bool', 'CHANNEL='=>'string'], +'Imagick::autoGammaImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::autoLevelImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::autoOrient' => ['bool'], 'Imagick::averageImages' => ['Imagick'], 'Imagick::blackThresholdImage' => ['bool', 'threshold'=>'mixed'], 'Imagick::blueShiftImage' => ['bool', 'factor='=>'float'], -'Imagick::blurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], +'Imagick::blurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::borderImage' => ['bool', 'bordercolor'=>'mixed', 'width'=>'int', 'height'=>'int'], -'Imagick::brightnessContrastImage' => ['bool', 'brightness'=>'float', 'contrast'=>'float', 'channel='=>'int'], +'Imagick::brightnessContrastImage' => ['bool', 'brightness'=>'float', 'contrast'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::charcoalImage' => ['bool', 'radius'=>'float', 'sigma'=>'float'], 'Imagick::chopImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::clampImage' => ['bool', 'channel='=>'int'], @@ -4673,16 +4680,16 @@ 'Imagick::colorFloodfillImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int'], 'Imagick::colorizeImage' => ['bool', 'colorize'=>'mixed', 'opacity'=>'mixed'], 'Imagick::colorMatrixImage' => ['bool', 'color_matrix'=>'array'], -'Imagick::combineImages' => ['Imagick', 'channeltype'=>'int'], +'Imagick::combineImages' => ['Imagick', 'channeltype'=>'Imagick::CHANNEL_*'], 'Imagick::commentImage' => ['bool', 'comment'=>'string'], -'Imagick::compareImageChannels' => ['array', 'image'=>'imagick', 'channeltype'=>'int', 'metrictype'=>'int'], -'Imagick::compareImageLayers' => ['Imagick', 'method'=>'int'], -'Imagick::compareImages' => ['array', 'compare'=>'imagick', 'metric'=>'int'], -'Imagick::compositeImage' => ['bool', 'composite_object'=>'imagick', 'composite'=>'int', 'x'=>'int', 'y'=>'int', 'channel='=>'int'], +'Imagick::compareImageChannels' => ['array{Imagick,float}', 'image'=>'imagick', 'channeltype'=>'Imagick::CHANNEL_*', 'metrictype'=>'Imagick::METRIC_*'], +'Imagick::compareImageLayers' => ['Imagick', 'method'=>'Imagick::LAYERMETHOD_*'], +'Imagick::compareImages' => ['array{Imagick,float}', 'compare'=>'imagick', 'metric'=>'Imagick::METRIC_*'], +'Imagick::compositeImage' => ['bool', 'composite_object'=>'imagick', 'composite'=>'Imagick::COMPOSITE_*', 'x'=>'int', 'y'=>'int', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::compositeImageGravity' => ['bool', 'imagick'=>'Imagick', 'COMPOSITE_CONSTANT'=>'int', 'GRAVITY_CONSTANT'=>'int'], 'Imagick::contrastImage' => ['bool', 'sharpen'=>'bool'], -'Imagick::contrastStretchImage' => ['bool', 'black_point'=>'float', 'white_point'=>'float', 'channel='=>'int'], -'Imagick::convolveImage' => ['bool', 'kernel'=>'array', 'channel='=>'int'], +'Imagick::contrastStretchImage' => ['bool', 'black_point'=>'float', 'white_point'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::convolveImage' => ['bool', 'kernel'=>'array', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::count' => ['0|positive-int', 'mode='=>'int'], 'Imagick::cropImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::cropThumbnailImage' => ['bool', 'width'=>'int', 'height'=>'int', 'legacy='=>'bool'], @@ -4697,167 +4704,167 @@ 'Imagick::destroy' => ['bool'], 'Imagick::displayImage' => ['bool', 'servername'=>'string'], 'Imagick::displayImages' => ['bool', 'servername'=>'string'], -'Imagick::distortImage' => ['bool', 'method'=>'int', 'arguments'=>'array', 'bestfit'=>'bool'], +'Imagick::distortImage' => ['bool', 'method'=>'Imagick::DISTORTION_*', 'arguments'=>'array', 'bestfit'=>'bool'], 'Imagick::drawImage' => ['bool', 'draw'=>'imagickdraw'], 'Imagick::edgeImage' => ['bool', 'radius'=>'float'], 'Imagick::embossImage' => ['bool', 'radius'=>'float', 'sigma'=>'float'], 'Imagick::encipherImage' => ['bool', 'passphrase'=>'string'], 'Imagick::enhanceImage' => ['bool'], 'Imagick::equalizeImage' => ['bool'], -'Imagick::evaluateImage' => ['bool', 'op'=>'int', 'constant'=>'float', 'channel='=>'int'], +'Imagick::evaluateImage' => ['bool', 'op'=>'Imagick::EVALUATE_*', 'constant'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::evaluateImages' => ['bool', 'EVALUATE_CONSTANT'=>'int'], -'Imagick::exportImagePixels' => ['array', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'int'], +'Imagick::exportImagePixels' => ['list', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'Imagick::PIXEL_*'], 'Imagick::extentImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::filter' => ['bool', 'ImagickKernel'=>'ImagickKernel', 'CHANNEL='=>'int'], 'Imagick::flattenImages' => ['Imagick'], 'Imagick::flipImage' => ['bool'], -'Imagick::floodFillPaintImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'target'=>'mixed', 'x'=>'int', 'y'=>'int', 'invert'=>'bool', 'channel='=>'int'], +'Imagick::floodFillPaintImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'target'=>'mixed', 'x'=>'int', 'y'=>'int', 'invert'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::flopImage' => ['bool'], 'Imagick::forwardFourierTransformimage' => ['bool', 'magnitude'=>'bool'], 'Imagick::frameImage' => ['bool', 'matte_color'=>'mixed', 'width'=>'int', 'height'=>'int', 'inner_bevel'=>'int', 'outer_bevel'=>'int'], -'Imagick::functionImage' => ['bool', 'function'=>'int', 'arguments'=>'array', 'channel='=>'int'], -'Imagick::fxImage' => ['Imagick', 'expression'=>'string', 'channel='=>'int'], -'Imagick::gammaImage' => ['bool', 'gamma'=>'float', 'channel='=>'int'], -'Imagick::gaussianBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], -'Imagick::getColorspace' => ['int'], -'Imagick::getCompression' => ['int'], +'Imagick::functionImage' => ['bool', 'function'=>'Imagick::FUNCTION_*', 'arguments'=>'array', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::fxImage' => ['Imagick', 'expression'=>'string', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::gammaImage' => ['bool', 'gamma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::gaussianBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::getColorspace' => ['Imagick::COLORSPACE_*'], +'Imagick::getCompression' => ['Imagick::COMPRESSION_*'], 'Imagick::getCompressionQuality' => ['int'], -'Imagick::getConfigureOptions' => ['string'], +'Imagick::getConfigureOptions' => ['array'], 'Imagick::getCopyright' => ['string'], 'Imagick::getFeatures' => ['string'], 'Imagick::getFilename' => ['string'], 'Imagick::getFont' => ['string'], 'Imagick::getFormat' => ['string'], -'Imagick::getGravity' => ['int'], +'Imagick::getGravity' => ['Imagick::GRAVITY_*'], 'Imagick::getHDRIEnabled' => ['int'], 'Imagick::getHomeURL' => ['string'], 'Imagick::getImage' => ['Imagick'], -'Imagick::getImageAlphaChannel' => ['int'], +'Imagick::getImageAlphaChannel' => ['bool'], 'Imagick::getImageArtifact' => ['string', 'artifact'=>'string'], 'Imagick::getImageAttribute' => ['string', 'key'=>'string'], 'Imagick::getImageBackgroundColor' => ['ImagickPixel'], 'Imagick::getImageBlob' => ['string'], -'Imagick::getImageBluePrimary' => ['array'], +'Imagick::getImageBluePrimary' => ['array{x:float,y:float}'], 'Imagick::getImageBorderColor' => ['ImagickPixel'], -'Imagick::getImageChannelDepth' => ['int', 'channel'=>'int'], -'Imagick::getImageChannelDistortion' => ['float', 'reference'=>'imagick', 'channel'=>'int', 'metric'=>'int'], -'Imagick::getImageChannelDistortions' => ['float', 'reference'=>'imagick', 'metric'=>'int', 'channel='=>'int'], -'Imagick::getImageChannelExtrema' => ['array', 'channel'=>'int'], -'Imagick::getImageChannelKurtosis' => ['array', 'channel='=>'int'], -'Imagick::getImageChannelMean' => ['array', 'channel'=>'int'], -'Imagick::getImageChannelRange' => ['array', 'channel'=>'int'], -'Imagick::getImageChannelStatistics' => ['array'], +'Imagick::getImageChannelDepth' => ['int', 'channel'=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelDistortion' => ['float', 'reference'=>'imagick', 'channel'=>'Imagick::CHANNEL_*', 'metric'=>'Imagick::METRIC_*'], +'Imagick::getImageChannelDistortions' => ['float', 'reference'=>'imagick', 'metric'=>'Imagick::METRIC_*', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelExtrema' => ['array{minima:0|positive-int,maxima:0|positive-int}', 'channel'=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelKurtosis' => ['array{kurtosis:float,skewness:float}', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelMean' => ['array{mean:float,standardDeviation:float}', 'channel'=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelRange' => ['array{minima:float,maxima:float}', 'channel'=>'Imagick::CHANNEL_*'], +'Imagick::getImageChannelStatistics' => ['array{mean:float,minima:float,maxima:float,standardDeviation:float,depth:int}'], 'Imagick::getImageClipMask' => ['Imagick'], 'Imagick::getImageColormapColor' => ['ImagickPixel', 'index'=>'int'], 'Imagick::getImageColors' => ['int'], -'Imagick::getImageColorspace' => ['int'], -'Imagick::getImageCompose' => ['int'], -'Imagick::getImageCompression' => ['int'], +'Imagick::getImageColorspace' => ['Imagick::COLORSPACE_*'], +'Imagick::getImageCompose' => ['Imagick::COMPOSITE_*'], +'Imagick::getImageCompression' => ['Imagick::COMPRESSION_*'], 'Imagick::getImageCompressionQuality' => ['int'], 'Imagick::getImageDelay' => ['int'], 'Imagick::getImageDepth' => ['int'], -'Imagick::getImageDispose' => ['int'], -'Imagick::getImageDistortion' => ['float', 'reference'=>'magickwand', 'metric'=>'int'], -'Imagick::getImageExtrema' => ['array'], +'Imagick::getImageDispose' => ['Imagick::DISPOSE_*'], +'Imagick::getImageDistortion' => ['float', 'reference'=>'magickwand', 'metric'=>'Imagick::METRIC_*'], +'Imagick::getImageExtrema' => ['array{min:0|positive-int,max:0|positive-int}'], 'Imagick::getImageFilename' => ['string'], 'Imagick::getImageFormat' => ['string'], 'Imagick::getImageGamma' => ['float'], -'Imagick::getImageGeometry' => ['array'], -'Imagick::getImageGravity' => ['int'], -'Imagick::getImageGreenPrimary' => ['array'], +'Imagick::getImageGeometry' => ['array{width:int,height:int}'], +'Imagick::getImageGravity' => ['Imagick::GRAVITY_*'], +'Imagick::getImageGreenPrimary' => ['array{x:float,y:float}'], 'Imagick::getImageHeight' => ['int'], -'Imagick::getImageHistogram' => ['array'], +'Imagick::getImageHistogram' => ['list'], 'Imagick::getImageIndex' => ['int'], -'Imagick::getImageInterlaceScheme' => ['int'], -'Imagick::getImageInterpolateMethod' => ['int'], +'Imagick::getImageInterlaceScheme' => ['Imagick::INTERLACE_*'], +'Imagick::getImageInterpolateMethod' => ['Imagick::INTERPOLATE_*'], 'Imagick::getImageIterations' => ['int'], -'Imagick::getImageLength' => ['int'], +'Imagick::getImageLength' => ['0|positive-int'], 'Imagick::getImageMagickLicense' => ['string'], 'Imagick::getImageMatte' => ['bool'], 'Imagick::getImageMatteColor' => ['ImagickPixel'], -'Imagick::getImageMimeType' => ['string'], -'Imagick::getImageOrientation' => ['int'], -'Imagick::getImagePage' => ['array'], +'Imagick::getImageMimeType' => ['non-empty-string'], +'Imagick::getImageOrientation' => ['Imagick::ORIENTATION_*'], +'Imagick::getImagePage' => ['array{width:int,height:int,x:int,y:int}'], 'Imagick::getImagePixelColor' => ['ImagickPixel', 'x'=>'int', 'y'=>'int'], 'Imagick::getImageProfile' => ['string', 'name'=>'string'], 'Imagick::getImageProfiles' => ['array', 'pattern='=>'string', 'only_names='=>'bool'], 'Imagick::getImageProperties' => ['array', 'pattern='=>'string', 'only_names='=>'bool'], 'Imagick::getImageProperty' => ['string', 'name'=>'string'], -'Imagick::getImageRedPrimary' => ['array'], +'Imagick::getImageRedPrimary' => ['array{x:float,y:float}'], 'Imagick::getImageRegion' => ['Imagick', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], -'Imagick::getImageRenderingIntent' => ['int'], -'Imagick::getImageResolution' => ['array'], +'Imagick::getImageRenderingIntent' => ['Imagick::RENDERINGINTENT_*'], +'Imagick::getImageResolution' => ['array{x:float,y:float}'], 'Imagick::getImagesBlob' => ['string'], -'Imagick::getImageScene' => ['int'], +'Imagick::getImageScene' => ['0|positive-int'], 'Imagick::getImageSignature' => ['string'], -'Imagick::getImageSize' => ['int'], -'Imagick::getImageTicksPerSecond' => ['int'], +'Imagick::getImageSize' => ['0|positive-int'], +'Imagick::getImageTicksPerSecond' => ['0|positive-int'], 'Imagick::getImageTotalInkDensity' => ['float'], -'Imagick::getImageType' => ['int'], +'Imagick::getImageType' => ['Imagick::IMGTYPE_*'], 'Imagick::getImageUnits' => ['int'], 'Imagick::getImageVirtualPixelMethod' => ['int'], -'Imagick::getImageWhitePoint' => ['array'], -'Imagick::getImageWidth' => ['int'], -'Imagick::getInterlaceScheme' => ['int'], +'Imagick::getImageWhitePoint' => ['array{x:float,y:float}'], +'Imagick::getImageWidth' => ['0|positive-int'], +'Imagick::getInterlaceScheme' => ['Imagick::INTERLACE_*'], 'Imagick::getIteratorIndex' => ['int'], -'Imagick::getNumberImages' => ['int'], +'Imagick::getNumberImages' => ['0|positive-int'], 'Imagick::getOption' => ['string', 'key'=>'string'], 'Imagick::getPackageName' => ['string'], -'Imagick::getPage' => ['array'], +'Imagick::getPage' => ['array{width:int,height:int,x:int,y:int}'], 'Imagick::getPixelIterator' => ['ImagickPixelIterator'], 'Imagick::getPixelRegionIterator' => ['ImagickPixelIterator', 'x'=>'int', 'y'=>'int', 'columns'=>'int', 'rows'=>'int'], 'Imagick::getPointSize' => ['float'], -'Imagick::getQuantum' => ['int'], -'Imagick::getQuantumDepth' => ['array'], -'Imagick::getQuantumRange' => ['array'], +'Imagick::getQuantum' => ['0|positive-int'], +'Imagick::getQuantumDepth' => ['array{quantumDepthLong:0|positive-int,quantumDepthString:numeric-string}'], +'Imagick::getQuantumRange' => ['array{quantumRangeLong:0|positive-int,quantumRangeString:numeric-string}'], 'Imagick::getRegistry' => ['string', 'key'=>'string'], 'Imagick::getReleaseDate' => ['string'], -'Imagick::getResource' => ['int', 'type'=>'int'], -'Imagick::getResourceLimit' => ['int', 'type'=>'int'], -'Imagick::getSamplingFactors' => ['array'], -'Imagick::getSize' => ['array'], +'Imagick::getResource' => ['int', 'type'=>'Imagick::RESOURCETYPE_*'], +'Imagick::getResourceLimit' => ['int', 'type'=>'Imagick::RESOURCETYPE_*'], +'Imagick::getSamplingFactors' => ['list'], +'Imagick::getSize' => ['array{columns:0|positive-int,rows:0|positive-int}'], 'Imagick::getSizeOffset' => ['int'], -'Imagick::getVersion' => ['array'], +'Imagick::getVersion' => ['array{versionNumber:0|positive-int,versionString:non-falsy-string}'], 'Imagick::haldClutImage' => ['bool', 'clut'=>'imagick', 'channel='=>'int'], 'Imagick::hasNextImage' => ['bool'], 'Imagick::hasPreviousImage' => ['bool'], 'Imagick::identifyFormat' => ['string|false', 'embedText'=>'string'], -'Imagick::identifyImage' => ['array', 'appendrawoutput='=>'bool'], +'Imagick::identifyImage' => ['array{imageName:string,mimetype:string,format:string,units:string,colorSpace:string,type:string,compression:string,fileSize:string,geometry:array{width:0|positive-int,height:0|positive-int},resolution:array{x:float,y:float},signature:string}', 'appendrawoutput='=>'bool'], 'Imagick::identifyImageType' => ['int'], 'Imagick::implodeImage' => ['bool', 'radius'=>'float'], -'Imagick::importImagePixels' => ['bool', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'int', 'pixels'=>'array'], +'Imagick::importImagePixels' => ['bool', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'Imagick::PIXEL_*', 'pixels'=>'array'], 'Imagick::inverseFourierTransformImage' => ['bool', 'complement'=>'Imagick', 'magnitude'=>'bool'], 'Imagick::key' => ['int|string'], 'Imagick::labelImage' => ['bool', 'label'=>'string'], -'Imagick::levelImage' => ['bool', 'blackpoint'=>'float', 'gamma'=>'float', 'whitepoint'=>'float', 'channel='=>'int'], +'Imagick::levelImage' => ['bool', 'blackpoint'=>'float', 'gamma'=>'float', 'whitepoint'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::linearStretchImage' => ['bool', 'blackpoint'=>'float', 'whitepoint'=>'float'], 'Imagick::liquidRescaleImage' => ['bool', 'width'=>'int', 'height'=>'int', 'delta_x'=>'float', 'rigidity'=>'float'], -'Imagick::listRegistry' => ['array'], +'Imagick::listRegistry' => ['array'], 'Imagick::localContrastImage' => ['bool', 'radius'=>'float', 'strength'=>'float'], 'Imagick::magnifyImage' => ['bool'], 'Imagick::mapImage' => ['bool', 'map'=>'imagick', 'dither'=>'bool'], 'Imagick::matteFloodfillImage' => ['bool', 'alpha'=>'float', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int'], 'Imagick::medianFilterImage' => ['bool', 'radius'=>'float'], -'Imagick::mergeImageLayers' => ['Imagick', 'layer_method'=>'int'], +'Imagick::mergeImageLayers' => ['Imagick', 'layer_method'=>'Imagick::LAYERMETHOD_*'], 'Imagick::minifyImage' => ['bool'], 'Imagick::modulateImage' => ['bool', 'brightness'=>'float', 'saturation'=>'float', 'hue'=>'float'], -'Imagick::montageImage' => ['Imagick', 'draw'=>'imagickdraw', 'tile_geometry'=>'string', 'thumbnail_geometry'=>'string', 'mode'=>'int', 'frame'=>'string'], +'Imagick::montageImage' => ['Imagick', 'draw'=>'imagickdraw', 'tile_geometry'=>'string', 'thumbnail_geometry'=>'string', 'mode'=>'Imagick::MONTAGEMODE_*', 'frame'=>'string'], 'Imagick::morphImages' => ['Imagick', 'number_frames'=>'int'], -'Imagick::morphology' => ['bool', 'morphologyMethod'=>'int', 'iterations'=>'int', 'ImagickKernel'=>'ImagickKernel', 'channel='=>'int'], +'Imagick::morphology' => ['bool', 'morphologyMethod'=>'Imagick::MORPHOLOGY_*', 'iterations'=>'int', 'ImagickKernel'=>'ImagickKernel', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::mosaicImages' => ['Imagick'], -'Imagick::motionBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'angle'=>'float', 'channel='=>'int'], -'Imagick::negateImage' => ['bool', 'gray'=>'bool', 'channel='=>'int'], +'Imagick::motionBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'angle'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::negateImage' => ['bool', 'gray'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::newImage' => ['bool', 'cols'=>'int', 'rows'=>'int', 'background'=>'mixed', 'format='=>'string'], 'Imagick::newPseudoImage' => ['bool', 'columns'=>'int', 'rows'=>'int', 'pseudostring'=>'string'], 'Imagick::next' => ['void'], 'Imagick::nextImage' => ['bool'], -'Imagick::normalizeImage' => ['bool', 'channel='=>'int'], +'Imagick::normalizeImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::oilPaintImage' => ['bool', 'radius'=>'float'], -'Imagick::opaquePaintImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'invert'=>'bool', 'channel='=>'int'], +'Imagick::opaquePaintImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'invert'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::optimizeImageLayers' => ['bool'], -'Imagick::orderedPosterizeImage' => ['bool', 'threshold_map'=>'string', 'channel='=>'int'], -'Imagick::paintFloodfillImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int', 'channel='=>'int'], -'Imagick::paintOpaqueImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'channel='=>'int'], +'Imagick::orderedPosterizeImage' => ['bool', 'threshold_map'=>'string', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::paintFloodfillImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::paintOpaqueImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::paintTransparentImage' => ['bool', 'target'=>'mixed', 'alpha'=>'float', 'fuzz'=>'float'], 'Imagick::pingImage' => ['bool', 'filename'=>'string'], 'Imagick::pingImageBlob' => ['bool', 'image'=>'string'], @@ -4866,22 +4873,22 @@ 'Imagick::posterizeImage' => ['bool', 'levels'=>'int', 'dither'=>'bool'], 'Imagick::previewImages' => ['bool', 'preview'=>'int'], 'Imagick::previousImage' => ['bool'], -'Imagick::profileImage' => ['bool', 'name'=>'string', 'profile'=>'string'], +'Imagick::profileImage' => ['bool', 'name'=>'string', 'profile'=>'?string'], 'Imagick::quantizeImage' => ['bool', 'numbercolors'=>'int', 'colorspace'=>'int', 'treedepth'=>'int', 'dither'=>'bool', 'measureerror'=>'bool'], 'Imagick::quantizeImages' => ['bool', 'numbercolors'=>'int', 'colorspace'=>'int', 'treedepth'=>'int', 'dither'=>'bool', 'measureerror'=>'bool'], -'Imagick::queryFontMetrics' => ['array', 'properties'=>'imagickdraw', 'text'=>'string', 'multiline='=>'bool'], -'Imagick::queryFonts' => ['array', 'pattern='=>'string'], -'Imagick::queryFormats' => ['array', 'pattern='=>'string'], -'Imagick::radialBlurImage' => ['bool', 'angle'=>'float', 'channel='=>'int'], +'Imagick::queryFontMetrics' => ['array{characterWidth:float,characterHeight:float,ascender:float,descender:float,textWidth:float,textHeight:float,maxHorizontalAdvance:float,boundingBox:array{x1:float,x2:float,y1:float,y2:float},originX:float,originY:float}', 'properties'=>'imagickdraw', 'text'=>'string', 'multiline='=>'bool'], +'Imagick::queryFonts' => ['list', 'pattern='=>'string'], +'Imagick::queryFormats' => ['list', 'pattern='=>'string'], +'Imagick::radialBlurImage' => ['bool', 'angle'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::raiseImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int', 'raise'=>'bool'], -'Imagick::randomThresholdImage' => ['bool', 'low'=>'float', 'high'=>'float', 'channel='=>'int'], +'Imagick::randomThresholdImage' => ['bool', 'low'=>'float', 'high'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::readImage' => ['bool', 'filename'=>'string'], 'Imagick::readImageBlob' => ['bool', 'image'=>'string', 'filename='=>'string'], 'Imagick::readImageFile' => ['bool', 'filehandle'=>'resource', 'filename='=>'string'], 'Imagick::readImages' => ['Imagick', 'filenames'=>'string'], 'Imagick::recolorImage' => ['bool', 'matrix'=>'array'], 'Imagick::reduceNoiseImage' => ['bool', 'radius'=>'float'], -'Imagick::remapImage' => ['bool', 'replacement'=>'imagick', 'dither'=>'int'], +'Imagick::remapImage' => ['bool', 'replacement'=>'imagick', 'dither'=>'Imagick::DITHERMETHOD_*'], 'Imagick::removeImage' => ['bool'], 'Imagick::removeImageProfile' => ['string', 'name'=>'string'], 'Imagick::render' => ['bool'], @@ -4892,28 +4899,28 @@ 'Imagick::rewind' => ['void'], 'Imagick::rollImage' => ['bool', 'x'=>'int', 'y'=>'int'], 'Imagick::rotateImage' => ['bool', 'background'=>'mixed', 'degrees'=>'float'], -'Imagick::rotationalBlurImage' => ['bool', 'float'=>'string', 'channel='=>'int'], +'Imagick::rotationalBlurImage' => ['bool', 'float'=>'string', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::roundCorners' => ['bool', 'x_rounding'=>'float', 'y_rounding'=>'float', 'stroke_width='=>'float', 'displace='=>'float', 'size_correction='=>'float'], -'Imagick::roundCornersImage' => ['', 'xRounding'=>'', 'yRounding'=>'', 'strokeWidth'=>'', 'displace'=>'', 'sizeCorrection'=>''], +'Imagick::roundCornersImage' => ['bool', 'x_rounding'=>'', 'y_rounding'=>'', 'stroke_width='=>'', 'displace='=>'', 'size_correction='=>''], 'Imagick::sampleImage' => ['bool', 'columns'=>'int', 'rows'=>'int'], -'Imagick::scaleImage' => ['bool', 'cols'=>'int', 'rows'=>'int', 'bestfit='=>'bool'], -'Imagick::segmentImage' => ['bool', 'colorspace'=>'int', 'cluster_threshold'=>'float', 'smooth_threshold'=>'float', 'verbose='=>'bool'], -'Imagick::selectiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'threshold'=>'float', 'channel='=>'int'], -'Imagick::separateImageChannel' => ['bool', 'channel'=>'int'], +'Imagick::scaleImage' => ['bool', 'cols'=>'int', 'rows'=>'int', 'bestfit='=>'bool', 'legacy='=>'bool'], +'Imagick::segmentImage' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*', 'cluster_threshold'=>'float', 'smooth_threshold'=>'float', 'verbose='=>'bool'], +'Imagick::selectiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::separateImageChannel' => ['bool', 'channel'=>'Imagick::CHANNEL_*'], 'Imagick::sepiaToneImage' => ['bool', 'threshold'=>'float'], 'Imagick::setAntiAlias' => ['int', 'antialias'=>'bool'], 'Imagick::setBackgroundColor' => ['bool', 'background'=>'mixed'], -'Imagick::setColorspace' => ['bool', 'colorspace'=>'int'], -'Imagick::setCompression' => ['bool', 'compression'=>'int'], +'Imagick::setColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], +'Imagick::setCompression' => ['bool', 'compression'=>'Imagick::COMPRESSION_*'], 'Imagick::setCompressionQuality' => ['bool', 'quality'=>'int'], 'Imagick::setFilename' => ['bool', 'filename'=>'string'], 'Imagick::setFirstIterator' => ['bool'], 'Imagick::setFont' => ['bool', 'font'=>'string'], 'Imagick::setFormat' => ['bool', 'format'=>'string'], -'Imagick::setGravity' => ['bool', 'gravity'=>'int'], +'Imagick::setGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], 'Imagick::setImage' => ['bool', 'replace'=>'imagick'], 'Imagick::setImageAlpha' => ['bool', 'alpha'=>'float'], -'Imagick::setImageAlphaChannel' => ['bool', 'mode'=>'int'], +'Imagick::setImageAlphaChannel' => ['bool', 'mode'=>'Imagick::ALPHACHANNEL_*'], 'Imagick::setImageArtifact' => ['bool', 'artifact'=>'string', 'value'=>'string'], 'Imagick::setImageAttribute' => ['bool', 'key'=>'string', 'value'=>'string'], 'Imagick::setImageBackgroundColor' => ['bool', 'background'=>'mixed'], @@ -4921,41 +4928,41 @@ 'Imagick::setImageBiasQuantum' => ['void', 'bias'=>'string'], 'Imagick::setImageBluePrimary' => ['bool', 'x'=>'float', 'y'=>'float'], 'Imagick::setImageBorderColor' => ['bool', 'border'=>'mixed'], -'Imagick::setImageChannelDepth' => ['bool', 'channel'=>'int', 'depth'=>'int'], -'Imagick::setImageChannelMask' => ['', 'channel'=>'int'], +'Imagick::setImageChannelDepth' => ['bool', 'channel'=>'Imagick::CHANNEL_*', 'depth'=>'int'], +'Imagick::setImageChannelMask' => ['', 'channel'=>'Imagick::CHANNEL_*'], 'Imagick::setImageClipMask' => ['bool', 'clip_mask'=>'imagick'], 'Imagick::setImageColormapColor' => ['bool', 'index'=>'int', 'color'=>'imagickpixel'], -'Imagick::setImageColorspace' => ['bool', 'colorspace'=>'int'], -'Imagick::setImageCompose' => ['bool', 'compose'=>'int'], -'Imagick::setImageCompression' => ['bool', 'compression'=>'int'], +'Imagick::setImageColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], +'Imagick::setImageCompose' => ['bool', 'compose'=>'Imagick::COMPOSITE_*'], +'Imagick::setImageCompression' => ['bool', 'compression'=>'Imagick::COMPRESSION_*'], 'Imagick::setImageCompressionQuality' => ['bool', 'quality'=>'int'], 'Imagick::setImageDelay' => ['bool', 'delay'=>'int'], 'Imagick::setImageDepth' => ['bool', 'depth'=>'int'], -'Imagick::setImageDispose' => ['bool', 'dispose'=>'int'], +'Imagick::setImageDispose' => ['bool', 'dispose'=>'Imagick::DISPOSE_*'], 'Imagick::setImageExtent' => ['bool', 'columns'=>'int', 'rows'=>'int'], 'Imagick::setImageFilename' => ['bool', 'filename'=>'string'], 'Imagick::setImageFormat' => ['bool', 'format'=>'string'], 'Imagick::setImageGamma' => ['bool', 'gamma'=>'float'], -'Imagick::setImageGravity' => ['bool', 'gravity'=>'int'], +'Imagick::setImageGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], 'Imagick::setImageGreenPrimary' => ['bool', 'x'=>'float', 'y'=>'float'], 'Imagick::setImageIndex' => ['bool', 'index'=>'int'], -'Imagick::setImageInterlaceScheme' => ['bool', 'interlace_scheme'=>'int'], -'Imagick::setImageInterpolateMethod' => ['bool', 'method'=>'int'], +'Imagick::setImageInterlaceScheme' => ['bool', 'interlace_scheme'=>'Imagick::INTERLACE_*'], +'Imagick::setImageInterpolateMethod' => ['bool', 'method'=>'Imagick::INTERPOLATE_*'], 'Imagick::setImageIterations' => ['bool', 'iterations'=>'int'], 'Imagick::setImageMatte' => ['bool', 'matte'=>'bool'], 'Imagick::setImageMatteColor' => ['bool', 'matte'=>'mixed'], 'Imagick::setImageOpacity' => ['bool', 'opacity'=>'float'], -'Imagick::setImageOrientation' => ['bool', 'orientation'=>'int'], +'Imagick::setImageOrientation' => ['bool', 'orientation'=>'Imagick::ORIENTATION_*'], 'Imagick::setImagePage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::setImageProfile' => ['bool', 'name'=>'string', 'profile'=>'string'], 'Imagick::setImageProgressMonitor' => ['', 'filename'=>''], 'Imagick::setImageProperty' => ['bool', 'name'=>'string', 'value'=>'string'], 'Imagick::setImageRedPrimary' => ['bool', 'x'=>'float', 'y'=>'float'], -'Imagick::setImageRenderingIntent' => ['bool', 'rendering_intent'=>'int'], +'Imagick::setImageRenderingIntent' => ['bool', 'rendering_intent'=>'Imagick::RENDERINGINTENT_*'], 'Imagick::setImageResolution' => ['bool', 'x_resolution'=>'float', 'y_resolution'=>'float'], 'Imagick::setImageScene' => ['bool', 'scene'=>'int'], 'Imagick::setImageTicksPerSecond' => ['bool', 'ticks_per_second'=>'int'], -'Imagick::setImageType' => ['bool', 'image_type'=>'int'], +'Imagick::setImageType' => ['bool', 'image_type'=>'Imagick::IMGTYPE_*'], 'Imagick::setImageUnits' => ['bool', 'units'=>'int'], 'Imagick::setImageVirtualPixelMethod' => ['bool', 'method'=>'int'], 'Imagick::setImageWhitePoint' => ['bool', 'x'=>'float', 'y'=>'float'], @@ -4972,46 +4979,46 @@ 'Imagick::setSamplingFactors' => ['bool', 'factors'=>'array'], 'Imagick::setSize' => ['bool', 'columns'=>'int', 'rows'=>'int'], 'Imagick::setSizeOffset' => ['bool', 'columns'=>'int', 'rows'=>'int', 'offset'=>'int'], -'Imagick::setType' => ['bool', 'image_type'=>'int'], +'Imagick::setType' => ['bool', 'image_type'=>'Imagick::IMGTYPE_*'], 'Imagick::shadeImage' => ['bool', 'gray'=>'bool', 'azimuth'=>'float', 'elevation'=>'float'], 'Imagick::shadowImage' => ['bool', 'opacity'=>'float', 'sigma'=>'float', 'x'=>'int', 'y'=>'int'], -'Imagick::sharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], +'Imagick::sharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::shaveImage' => ['bool', 'columns'=>'int', 'rows'=>'int'], 'Imagick::shearImage' => ['bool', 'background'=>'mixed', 'x_shear'=>'float', 'y_shear'=>'float'], -'Imagick::sigmoidalContrastImage' => ['bool', 'sharpen'=>'bool', 'alpha'=>'float', 'beta'=>'float', 'channel='=>'int'], -'Imagick::similarityImage' => ['Imagick', 'imagick'=>'Imagick', '&bestMatch'=>'array', '&similarity'=>'float', 'similarity_threshold'=>'float', 'metric'=>'int'], +'Imagick::sigmoidalContrastImage' => ['bool', 'sharpen'=>'bool', 'alpha'=>'float', 'beta'=>'float', 'channel='=>'Imagick::CHANNEL_*'], +'Imagick::similarityImage' => ['Imagick', 'imagick'=>'Imagick', '&bestMatch'=>'array', '&similarity'=>'float', 'similarity_threshold'=>'float', 'metric'=>'Imagick::METRIC_*'], 'Imagick::sketchImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'angle'=>'float'], 'Imagick::smushImages' => ['Imagick', 'stack'=>'bool', 'offset'=>'int'], -'Imagick::solarizeImage' => ['bool', 'threshold'=>'int'], -'Imagick::sparseColorImage' => ['bool', 'sparse_method'=>'int', 'arguments'=>'array', 'channel='=>'int'], +'Imagick::solarizeImage' => ['bool', 'threshold'=>'0|positive-int'], +'Imagick::sparseColorImage' => ['bool', 'sparse_method'=>'Imagick::SPARSECOLORMETHOD_*', 'arguments'=>'array', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::spliceImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::spreadImage' => ['bool', 'radius'=>'float'], -'Imagick::statisticImage' => ['bool', 'type'=>'int', 'width'=>'int', 'height'=>'int', 'channel='=>'int'], +'Imagick::statisticImage' => ['bool', 'type'=>'Imagick::STATISTIC_*', 'width'=>'int', 'height'=>'int', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::steganoImage' => ['Imagick', 'watermark_wand'=>'imagick', 'offset'=>'int'], 'Imagick::stereoImage' => ['bool', 'offset_wand'=>'imagick'], 'Imagick::stripImage' => ['bool'], 'Imagick::subImageMatch' => ['Imagick', 'Imagick'=>'Imagick', '&w_offset='=>'array', '&w_similarity='=>'float'], 'Imagick::swirlImage' => ['bool', 'degrees'=>'float'], 'Imagick::textureImage' => ['Imagick', 'texture_wand'=>'imagick'], -'Imagick::thresholdImage' => ['bool', 'threshold'=>'float', 'channel='=>'int'], +'Imagick::thresholdImage' => ['bool', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::thumbnailImage' => ['bool', 'columns'=>'int', 'rows'=>'int', 'bestfit='=>'bool', 'fill='=>'bool', 'legacy='=>'bool'], 'Imagick::tintImage' => ['bool', 'tint'=>'mixed', 'opacity'=>'mixed'], 'Imagick::transformImage' => ['Imagick', 'crop'=>'string', 'geometry'=>'string'], -'Imagick::transformImageColorspace' => ['bool', 'colorspace'=>'int'], +'Imagick::transformImageColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], 'Imagick::transparentPaintImage' => ['bool', 'target'=>'mixed', 'alpha'=>'float', 'fuzz'=>'float', 'invert'=>'bool'], 'Imagick::transposeImage' => ['bool'], 'Imagick::transverseImage' => ['bool'], 'Imagick::trimImage' => ['bool', 'fuzz'=>'float'], 'Imagick::uniqueImageColors' => ['bool'], -'Imagick::unsharpMaskImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'amount'=>'float', 'threshold'=>'float', 'channel='=>'int'], +'Imagick::unsharpMaskImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'amount'=>'float', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::valid' => ['bool'], 'Imagick::vignetteImage' => ['bool', 'blackpoint'=>'float', 'whitepoint'=>'float', 'x'=>'int', 'y'=>'int'], 'Imagick::waveImage' => ['bool', 'amplitude'=>'float', 'length'=>'float'], 'Imagick::whiteThresholdImage' => ['bool', 'threshold'=>'mixed'], 'Imagick::writeImage' => ['bool', 'filename='=>'string'], -'Imagick::writeImageFile' => ['bool', 'filehandle'=>'resource'], +'Imagick::writeImageFile' => ['bool', 'filehandle'=>'resource', 'format='=>'?string'], 'Imagick::writeImages' => ['bool', 'filename'=>'string', 'adjoin'=>'bool'], -'Imagick::writeImagesFile' => ['bool', 'filehandle'=>'resource'], +'Imagick::writeImagesFile' => ['bool', 'filehandle'=>'resource', 'format='=>'?string'], 'ImagickDraw::__construct' => ['void'], 'ImagickDraw::affine' => ['bool', 'affine'=>'array'], 'ImagickDraw::annotation' => ['bool', 'x'=>'float', 'y'=>'float', 'text'=>'string'], @@ -5020,9 +5027,9 @@ 'ImagickDraw::circle' => ['bool', 'ox'=>'float', 'oy'=>'float', 'px'=>'float', 'py'=>'float'], 'ImagickDraw::clear' => ['bool'], 'ImagickDraw::clone' => ['ImagickDraw'], -'ImagickDraw::color' => ['bool', 'x'=>'float', 'y'=>'float', 'paintmethod'=>'int'], +'ImagickDraw::color' => ['bool', 'x'=>'float', 'y'=>'float', 'paintmethod'=>'Imagick::PAINT_*'], 'ImagickDraw::comment' => ['bool', 'comment'=>'string'], -'ImagickDraw::composite' => ['bool', 'compose'=>'int', 'x'=>'float', 'y'=>'float', 'width'=>'float', 'height'=>'float', 'compositewand'=>'imagick'], +'ImagickDraw::composite' => ['bool', 'compose'=>'Imagick::COMPOSITE_*', 'x'=>'float', 'y'=>'float', 'width'=>'float', 'height'=>'float', 'compositewand'=>'imagick'], 'ImagickDraw::destroy' => ['bool'], 'ImagickDraw::ellipse' => ['bool', 'ox'=>'float', 'oy'=>'float', 'rx'=>'float', 'ry'=>'float', 'start'=>'float', 'end'=>'float'], 'ImagickDraw::getBorderColor' => ['ImagickPixel'], @@ -5032,28 +5039,28 @@ 'ImagickDraw::getDensity' => ['null|string'], 'ImagickDraw::getFillColor' => ['ImagickPixel'], 'ImagickDraw::getFillOpacity' => ['float'], -'ImagickDraw::getFillRule' => ['int'], +'ImagickDraw::getFillRule' => ['Imagick::FILLRULE_*'], 'ImagickDraw::getFont' => ['string'], 'ImagickDraw::getFontFamily' => ['string'], 'ImagickDraw::getFontResolution' => ['array'], 'ImagickDraw::getFontSize' => ['float'], -'ImagickDraw::getFontStretch' => ['int'], -'ImagickDraw::getFontStyle' => ['int'], +'ImagickDraw::getFontStretch' => ['Imagick::STRETCH_*'], +'ImagickDraw::getFontStyle' => ['Imagick::STYLE_*'], 'ImagickDraw::getFontWeight' => ['int'], -'ImagickDraw::getGravity' => ['int'], +'ImagickDraw::getGravity' => ['Imagick::GRAVITY_*'], 'ImagickDraw::getOpacity' => ['float'], 'ImagickDraw::getStrokeAntialias' => ['bool'], 'ImagickDraw::getStrokeColor' => ['ImagickPixel'], 'ImagickDraw::getStrokeDashArray' => ['array'], 'ImagickDraw::getStrokeDashOffset' => ['float'], -'ImagickDraw::getStrokeLineCap' => ['int'], -'ImagickDraw::getStrokeLineJoin' => ['int'], +'ImagickDraw::getStrokeLineCap' => ['Imagick::LINECAP_*'], +'ImagickDraw::getStrokeLineJoin' => ['Imagick::LINEJOIN_*'], 'ImagickDraw::getStrokeMiterLimit' => ['int'], 'ImagickDraw::getStrokeOpacity' => ['float'], 'ImagickDraw::getStrokeWidth' => ['float'], -'ImagickDraw::getTextAlignment' => ['int'], +'ImagickDraw::getTextAlignment' => ['Imagick::ALIGN_*'], 'ImagickDraw::getTextAntialias' => ['bool'], -'ImagickDraw::getTextDecoration' => ['int'], +'ImagickDraw::getTextDecoration' => ['Imagick::DECORATION_*'], 'ImagickDraw::getTextDirection' => ['bool'], 'ImagickDraw::getTextEncoding' => ['string'], 'ImagickDraw::getTextInterlineSpacing' => ['float'], @@ -5062,7 +5069,7 @@ 'ImagickDraw::getTextUnderColor' => ['ImagickPixel'], 'ImagickDraw::getVectorGraphics' => ['string'], 'ImagickDraw::line' => ['bool', 'sx'=>'float', 'sy'=>'float', 'ex'=>'float', 'ey'=>'float'], -'ImagickDraw::matte' => ['bool', 'x'=>'float', 'y'=>'float', 'paintmethod'=>'int'], +'ImagickDraw::matte' => ['bool', 'x'=>'float', 'y'=>'float', 'paintmethod'=>'Imagick::PAINT_*'], 'ImagickDraw::pathClose' => ['bool'], 'ImagickDraw::pathCurveToAbsolute' => ['bool', 'x1'=>'float', 'y1'=>'float', 'x2'=>'float', 'y2'=>'float', 'x'=>'float', 'y'=>'float'], 'ImagickDraw::pathCurveToQuadraticBezierAbsolute' => ['bool', 'x1'=>'float', 'y1'=>'float', 'x'=>'float', 'y'=>'float'], @@ -5103,22 +5110,22 @@ 'ImagickDraw::scale' => ['bool', 'x'=>'float', 'y'=>'float'], 'ImagickDraw::setBorderColor' => ['bool', 'color'=>'ImagickPixel|string'], 'ImagickDraw::setClipPath' => ['bool', 'clip_mask'=>'string'], -'ImagickDraw::setClipRule' => ['bool', 'fill_rule'=>'int'], +'ImagickDraw::setClipRule' => ['bool', 'fill_rule'=>'Imagick::FILLRULE_*'], 'ImagickDraw::setClipUnits' => ['bool', 'clip_units'=>'int'], 'ImagickDraw::setDensity' => ['bool', 'density_string'=>'string'], 'ImagickDraw::setFillAlpha' => ['bool', 'opacity'=>'float'], 'ImagickDraw::setFillColor' => ['bool', 'fill_pixel'=>'ImagickPixel|string'], 'ImagickDraw::setFillOpacity' => ['bool', 'fillopacity'=>'float'], 'ImagickDraw::setFillPatternURL' => ['bool', 'fill_url'=>'string'], -'ImagickDraw::setFillRule' => ['bool', 'fill_rule'=>'int'], +'ImagickDraw::setFillRule' => ['bool', 'fill_rule'=>'Imagick::FILLRULE_*'], 'ImagickDraw::setFont' => ['bool', 'font_name'=>'string'], 'ImagickDraw::setFontFamily' => ['bool', 'font_family'=>'string'], 'ImagickDraw::setFontResolution' => ['bool', 'x'=>'float', 'y'=>'float'], 'ImagickDraw::setFontSize' => ['bool', 'pointsize'=>'float'], -'ImagickDraw::setFontStretch' => ['bool', 'fontstretch'=>'int'], -'ImagickDraw::setFontStyle' => ['bool', 'style'=>'int'], +'ImagickDraw::setFontStretch' => ['bool', 'fontstretch'=>'Imagick::STRETCH_*'], +'ImagickDraw::setFontStyle' => ['bool', 'style'=>'Imagick::STYLE_*'], 'ImagickDraw::setFontWeight' => ['bool', 'font_weight'=>'int'], -'ImagickDraw::setGravity' => ['bool', 'gravity'=>'int'], +'ImagickDraw::setGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], 'ImagickDraw::setOpacity' => ['void', 'opacity'=>'float'], 'ImagickDraw::setResolution' => ['void', 'x_resolution'=>'float', 'y_resolution'=>'float'], 'ImagickDraw::setStrokeAlpha' => ['bool', 'opacity'=>'float'], @@ -5126,15 +5133,15 @@ 'ImagickDraw::setStrokeColor' => ['bool', 'stroke_pixel'=>'ImagickPixel|string'], 'ImagickDraw::setStrokeDashArray' => ['bool', 'dasharray'=>'array'], 'ImagickDraw::setStrokeDashOffset' => ['bool', 'dash_offset'=>'float'], -'ImagickDraw::setStrokeLineCap' => ['bool', 'linecap'=>'int'], -'ImagickDraw::setStrokeLineJoin' => ['bool', 'linejoin'=>'int'], +'ImagickDraw::setStrokeLineCap' => ['bool', 'linecap'=>'Imagick::LINECAP_*'], +'ImagickDraw::setStrokeLineJoin' => ['bool', 'linejoin'=>'Imagick::LINEJOIN_*'], 'ImagickDraw::setStrokeMiterLimit' => ['bool', 'miterlimit'=>'int'], 'ImagickDraw::setStrokeOpacity' => ['bool', 'stroke_opacity'=>'float'], 'ImagickDraw::setStrokePatternURL' => ['bool', 'stroke_url'=>'string'], 'ImagickDraw::setStrokeWidth' => ['bool', 'stroke_width'=>'float'], -'ImagickDraw::setTextAlignment' => ['bool', 'alignment'=>'int'], +'ImagickDraw::setTextAlignment' => ['bool', 'alignment'=>'Imagick::ALIGN_*'], 'ImagickDraw::setTextAntialias' => ['bool', 'antialias'=>'bool'], -'ImagickDraw::setTextDecoration' => ['bool', 'decoration'=>'int'], +'ImagickDraw::setTextDecoration' => ['bool', 'decoration'=>'Imagick::DECORATION_*'], 'ImagickDraw::setTextDirection' => ['bool', 'direction'=>'int'], 'ImagickDraw::setTextEncoding' => ['bool', 'encoding'=>'string'], 'ImagickDraw::setTextInterlineSpacing' => ['void', 'spacing'=>'float'], @@ -5148,17 +5155,16 @@ 'ImagickDraw::translate' => ['bool', 'x'=>'float', 'y'=>'float'], 'ImagickKernel::addKernel' => ['void', 'ImagickKernel'=>'ImagickKernel'], 'ImagickKernel::addUnityKernel' => ['void'], -'ImagickKernel::fromBuiltin' => ['ImagickKernel', 'kernelType'=>'string', 'kernelString'=>'string'], +'ImagickKernel::fromBuiltin' => ['ImagickKernel', 'kernelType'=>'Imagick::KERNEL_*', 'kernelString'=>'string'], 'ImagickKernel::fromMatrix' => ['ImagickKernel', 'matrix'=>'array', 'origin='=>'array'], -'ImagickKernel::getMatrix' => ['array'], -'ImagickKernel::scale' => ['void'], +'ImagickKernel::getMatrix' => ['list>'], +'ImagickKernel::scale' => ['void', 'scale'=>'float', 'normalizeFlag'=>'Imagick::NORMALIZE_KERNEL_*'], 'ImagickKernel::separate' => ['array'], -'ImagickKernel::seperate' => ['void'], 'ImagickPixel::__construct' => ['void', 'color='=>'string'], 'ImagickPixel::clear' => ['bool'], 'ImagickPixel::clone' => ['void'], 'ImagickPixel::destroy' => ['bool'], -'ImagickPixel::getColor' => ['array', 'normalized='=>'bool'], +'ImagickPixel::getColor' => ['array{r: int|float, g: int|float, b: int|float, a: int|float}', 'normalized='=>'0|1|2'], 'ImagickPixel::getColorAsString' => ['string'], 'ImagickPixel::getColorCount' => ['int'], 'ImagickPixel::getColorQuantum' => ['mixed'], @@ -5244,7 +5250,7 @@ 'imap_mutf7_to_utf8' => ['string|false', 'in'=>'string'], 'imap_num_msg' => ['int|false', 'stream_id'=>'resource'], 'imap_num_recent' => ['int|false', 'stream_id'=>'resource'], -'imap_open' => ['resource|false', 'mailbox'=>'string', 'user'=>'string', 'password'=>'string', 'options='=>'int', 'n_retries='=>'int', 'params=' => 'array|null'], +'imap_open' => ['resource|false', 'mailbox'=>'string', 'user'=>'string', 'password'=>'string', 'options='=>'int', 'n_retries='=>'int', 'params='=>'array|null'], 'imap_ping' => ['bool', 'stream_id'=>'resource'], 'imap_qprint' => ['string|false', 'text'=>'string'], 'imap_rename' => ['bool', 'stream_id'=>'resource', 'old_name'=>'string', 'new_name'=>'string'], @@ -5325,10 +5331,10 @@ 'ini_get_all' => ['array|false', 'extension='=>'?string', 'details='=>'bool'], 'ini_restore' => ['void', 'varname'=>'string'], 'ini_set' => ['string|false', 'varname'=>'string', 'newvalue'=>'string'], -'inotify_add_watch' => ['int', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], +'inotify_add_watch' => ['int<1,max>|false', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource'], -'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], -'inotify_read' => ['array', 'inotify_instance'=>'resource'], +'inotify_queue_len' => ['int<0,max>', 'inotify_instance'=>'resource'], +'inotify_read' => ['list,mask:int<0,max>,cookie:int<0,max>,name:string}>|false', 'inotify_instance'=>'resource'], 'inotify_rm_watch' => ['bool', 'inotify_instance'=>'resource', 'watch_descriptor'=>'int'], 'intdiv' => ['int', 'numerator'=>'int', 'divisor'=>'int'], 'interface_exists' => ['bool', 'classname'=>'string', 'autoload='=>'bool'], @@ -5349,7 +5355,7 @@ 'IntlBreakIterator::getErrorCode' => ['int'], 'IntlBreakIterator::getErrorMessage' => ['string'], 'IntlBreakIterator::getLocale' => ['string', 'locale_type'=>'string'], -'IntlBreakIterator::getPartsIterator' => ['IntlPartsIterator', 'key_type='=>'int'], +'IntlBreakIterator::getPartsIterator' => ['IntlPartsIterator', 'key_type='=>'IntlPartsIterator::KEY_*'], 'IntlBreakIterator::getText' => ['string'], 'IntlBreakIterator::isBoundary' => ['bool', 'offset'=>'int'], 'IntlBreakIterator::last' => ['int'], @@ -5509,24 +5515,24 @@ 'IntlChar::toupper' => ['mixed', 'codepoint'=>'mixed'], 'IntlCodePointBreakIterator::getLastCodePoint' => ['int'], 'IntlDateFormatter::__construct' => ['void', 'locale'=>'?string', 'datetype'=>'?int', 'timetype'=>'?int', 'timezone='=>'null|string|IntlTimeZone|DateTimeZone', 'calendar='=>'null|int|IntlCalendar', 'pattern='=>'string'], -'IntlDateFormatter::create' => ['IntlDateFormatter|false', 'locale'=>'?string', 'datetype'=>'?int', 'timetype'=>'?int', 'timezone='=>'null|string|IntlTimeZone|DateTimeZone', 'calendar='=>'int|IntlCalendar', 'pattern='=>'string'], +'IntlDateFormatter::create' => ['IntlDateFormatter|null', 'locale'=>'?string', 'datetype'=>'?int', 'timetype'=>'?int', 'timezone='=>'null|string|IntlTimeZone|DateTimeZone', 'calendar='=>'int|IntlCalendar', 'pattern='=>'string'], 'IntlDateFormatter::format' => ['string|false', 'args'=>''], -'IntlDateFormatter::formatObject' => ['string', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], -'IntlDateFormatter::getCalendar' => ['int'], -'IntlDateFormatter::getCalendarObject' => ['IntlCalendar'], -'IntlDateFormatter::getDateType' => ['int'], +'IntlDateFormatter::formatObject' => ['string|false', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], +'IntlDateFormatter::getCalendar' => ['int|false'], +'IntlDateFormatter::getCalendarObject' => ['IntlCalendar|false|null'], +'IntlDateFormatter::getDateType' => ['int|false'], 'IntlDateFormatter::getErrorCode' => ['int'], 'IntlDateFormatter::getErrorMessage' => ['string'], -'IntlDateFormatter::getLocale' => ['string'], -'IntlDateFormatter::getPattern' => ['string'], -'IntlDateFormatter::getTimeType' => ['int'], -'IntlDateFormatter::getTimeZone' => ['IntlTimeZone'], -'IntlDateFormatter::getTimeZoneId' => ['string'], +'IntlDateFormatter::getLocale' => ['string|false'], +'IntlDateFormatter::getPattern' => ['string|false'], +'IntlDateFormatter::getTimeType' => ['int|false'], +'IntlDateFormatter::getTimeZone' => ['IntlTimeZone|false'], +'IntlDateFormatter::getTimeZoneId' => ['string|false'], 'IntlDateFormatter::isLenient' => ['bool'], -'IntlDateFormatter::localtime' => ['array', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], -'IntlDateFormatter::parse' => ['int|false', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], +'IntlDateFormatter::localtime' => ['array|false', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], +'IntlDateFormatter::parse' => ['int|float|false', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], 'IntlDateFormatter::setCalendar' => ['bool', 'calendar'=>''], -'IntlDateFormatter::setLenient' => ['bool', 'lenient'=>'bool'], +'IntlDateFormatter::setLenient' => ['void', 'lenient'=>'bool'], 'IntlDateFormatter::setPattern' => ['bool', 'pattern'=>'string'], 'IntlDateFormatter::setTimeZone' => ['bool', 'timezone'=>''], 'IntlDateFormatter::setTimeZoneId' => ['bool', 'zone'=>'string', 'fmt='=>'IntlDateFormatter'], @@ -5538,6 +5544,7 @@ 'IntlIterator::next' => ['void'], 'IntlIterator::rewind' => ['void'], 'IntlIterator::valid' => ['bool'], +'IntlPartsIterator::current' => ['non-empty-string'], 'IntlPartsIterator::getBreakIterator' => ['IntlBreakIterator'], 'IntlRuleBasedBreakIterator::__construct' => ['void', 'rules'=>'string', 'areCompiled='=>'string'], 'IntlRuleBasedBreakIterator::createCharacterInstance' => ['IntlRuleBasedBreakIterator', 'locale'=>'string'], @@ -5607,7 +5614,7 @@ 'intltz_to_date_time_zone' => ['DateTimeZone|false', 'obj'=>''], 'intltz_use_daylight_time' => ['bool', 'obj'=>''], 'intlz_create_default' => ['IntlTimeZone'], -'intval' => ['int', 'var'=>'mixed', 'base='=>'int'], +'intval' => ['int', 'var'=>'scalar|array|resource|null', 'base='=>'int'], 'InvalidArgumentException::__clone' => ['void'], 'InvalidArgumentException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?InvalidArgumentException)'], 'InvalidArgumentException::__toString' => ['string'], @@ -5616,11 +5623,11 @@ 'InvalidArgumentException::getLine' => ['int'], 'InvalidArgumentException::getMessage' => ['string'], 'InvalidArgumentException::getPrevious' => ['Throwable|InvalidArgumentException|null'], -'InvalidArgumentException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'InvalidArgumentException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'InvalidArgumentException::getTraceAsString' => ['string'], 'ip2long' => ['int|false', 'ip_address'=>'string'], 'iptcembed' => ['string|bool', 'iptcdata'=>'string', 'jpeg_file_name'=>'string', 'spool='=>'int'], -'iptcparse' => ['array|false', 'iptcdata'=>'string'], +'iptcparse' => ['array>|false', 'iptcdata'=>'string'], 'is_a' => ['bool', 'object_or_string'=>'object|string', 'class_name'=>'string', 'allow_string='=>'bool'], 'is_array' => ['bool', 'var'=>'mixed'], 'is_bool' => ['bool', 'var'=>'mixed'], @@ -5689,9 +5696,9 @@ '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='=>'positive-int', 'options='=>'int'], -'json_encode' => ['string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'positive-int'], -'json_last_error' => ['int'], +'json_decode' => ['mixed', 'json'=>'string', 'assoc='=>'bool|null', 'depth='=>'positive-int', 'options='=>'int'], +'json_encode' => ['non-empty-string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'positive-int'], +'json_last_error' => ['JSON_ERROR_NONE|JSON_ERROR_DEPTH|JSON_ERROR_STATE_MISMATCH|JSON_ERROR_CTRL_CHAR|JSON_ERROR_SYNTAX|JSON_ERROR_UTF8|JSON_ERROR_RECURSION|JSON_ERROR_INF_OR_NAN|JSON_ERROR_UNSUPPORTED_TYPE|JSON_ERROR_INVALID_PROPERTY_NAME|JSON_ERROR_UTF16'], 'json_last_error_msg' => ['string'], 'JsonIncrementalParser::__construct' => ['void', 'depth'=>'', 'options'=>''], 'JsonIncrementalParser::get' => ['', 'options'=>''], @@ -5843,8 +5850,8 @@ 'ldap_8859_to_t61' => ['string|false', 'value'=>'string'], 'ldap_add' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], 'ldap_add_ext' => ['resource|false', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], -'ldap_bind' => ['bool', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls=' => 'array'], -'ldap_bind_ext' => ['resource|false', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls=' => 'array'], +'ldap_bind' => ['bool', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls='=>'array'], +'ldap_bind_ext' => ['resource|false', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls='=>'array'], 'ldap_close' => ['bool', 'link_identifier'=>'resource'], 'ldap_compare' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'attr'=>'string', 'value'=>'string', 'servercontrols='=>'array'], 'ldap_connect' => ['resource|false', 'host='=>'string', 'port='=>'int', 'wallet='=>'string', 'wallet_passwd='=>'string', 'authmode='=>'int'], @@ -5913,7 +5920,7 @@ 'LengthException::getLine' => ['int'], 'LengthException::getMessage' => ['string'], 'LengthException::getPrevious' => ['Throwable|LengthException|null'], -'LengthException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'LengthException::getTrace' => ['list\',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'], @@ -5944,41 +5951,41 @@ 'linkinfo' => ['int|false', 'filename'=>'string'], 'litespeed_request_headers' => ['array'], 'litespeed_response_headers' => ['array|false'], -'Locale::acceptFromHttp' => ['string|false', 'header'=>'string'], -'Locale::canonicalize' => ['string', 'locale'=>'string'], -'Locale::composeLocale' => ['string', 'subtags'=>'array'], -'Locale::filterMatches' => ['bool', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], -'Locale::getAllVariants' => ['array', 'locale'=>'string'], -'Locale::getDefault' => ['string'], -'Locale::getDisplayLanguage' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayName' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayRegion' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayScript' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayVariant' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getKeywords' => ['array|false', 'locale'=>'string'], -'Locale::getPrimaryLanguage' => ['string', 'locale'=>'string'], -'Locale::getRegion' => ['string', 'locale'=>'string'], -'Locale::getScript' => ['string', 'locale'=>'string'], -'Locale::lookup' => ['string', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'default='=>'string'], -'Locale::parseLocale' => ['array', 'locale'=>'string'], +'Locale::acceptFromHttp' => ['non-empty-string|false', 'header'=>'string'], +'Locale::canonicalize' => ['non-empty-string|null', 'locale'=>'string'], +'Locale::composeLocale' => ['string|false', 'subtags'=>'array{language:string, script?:string, region?:string, variant?:array, private?:array, extlang?:array, variant0?:string, variant1?:string, variant2?:string, variant3?:string, variant4?:string, variant5?:string, variant6?:string, variant7?:string, variant8?:string, variant9?:string, variant10?:string, variant11?:string, variant12?:string, variant13?:string, variant14?:string, private0?:string, private1?:string, private2?:string, private3?:string, private4?:string, private5?:string, private6?:string, private7?:string, private8?:string, private9?:string, private10?:string, private11?:string, private12?:string, private13?:string, private14?:string, extlang0?:string, extlang1?:string, extlang2?:string}'], +'Locale::filterMatches' => ['bool|null', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], +'Locale::getAllVariants' => ['array|null', 'locale'=>'string'], +'Locale::getDefault' => ['non-empty-string'], +'Locale::getDisplayLanguage' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayName' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayRegion' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayScript' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayVariant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getKeywords' => ['array|null', 'locale'=>'string'], +'Locale::getPrimaryLanguage' => ['non-empty-string|null', 'locale'=>'string'], +'Locale::getRegion' => ['string|null', 'locale'=>'string'], +'Locale::getScript' => ['string|null', 'locale'=>'string'], +'Locale::lookup' => ['string|null', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], +'Locale::parseLocale' => ['array|null', 'locale'=>'string'], 'Locale::setDefault' => ['bool', 'locale'=>'string'], -'locale_accept_from_http' => ['string|false', 'header'=>'string'], -'locale_canonicalize' => ['', 'arg1'=>''], -'locale_compose' => ['string|false', 'subtags'=>'array'], -'locale_filter_matches' => ['bool', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], -'locale_get_all_variants' => ['array', 'locale'=>'string'], -'locale_get_default' => ['string'], -'locale_get_display_language' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_name' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_region' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_script' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_variant' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_keywords' => ['array|false', 'locale'=>'string'], -'locale_get_primary_language' => ['string', 'locale'=>'string'], -'locale_get_region' => ['string', 'locale'=>'string'], -'locale_get_script' => ['string', 'locale'=>'string'], -'locale_lookup' => ['string', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'default='=>'string'], -'locale_parse' => ['array', 'locale'=>'string'], +'locale_accept_from_http' => ['non-empty-string|false', 'header'=>'string'], +'locale_canonicalize' => ['non-empty-string|null', 'locale'=>'string'], +'locale_compose' => ['string|false', 'subtags'=>'array{language:string, script?:string, region?:string, variant?:array, private?:array, extlang?:array, variant0?:string, variant1?:string, variant2?:string, variant3?:string, variant4?:string, variant5?:string, variant6?:string, variant7?:string, variant8?:string, variant9?:string, variant10?:string, variant11?:string, variant12?:string, variant13?:string, variant14?:string, private0?:string, private1?:string, private2?:string, private3?:string, private4?:string, private5?:string, private6?:string, private7?:string, private8?:string, private9?:string, private10?:string, private11?:string, private12?:string, private13?:string, private14?:string, extlang0?:string, extlang1?:string, extlang2?:string}'], +'locale_filter_matches' => ['bool|null', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], +'locale_get_all_variants' => ['array|null', 'locale'=>'string'], +'locale_get_default' => ['non-empty-string'], +'locale_get_display_language' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_name' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_region' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_script' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_variant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_keywords' => ['array|null', 'locale'=>'string'], +'locale_get_primary_language' => ['non-empty-string|null', 'locale'=>'string'], +'locale_get_region' => ['string|null', 'locale'=>'string'], +'locale_get_script' => ['string|null', 'locale'=>'string'], +'locale_lookup' => ['string|null', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], +'locale_parse' => ['array|null', 'locale'=>'string'], 'locale_set_default' => ['bool', 'locale'=>'string'], 'localeconv' => ['array'], 'localtime' => ['array', 'timestamp='=>'int', 'associative_array='=>'bool'], @@ -5993,7 +6000,7 @@ 'LogicException::getLine' => ['int'], 'LogicException::getMessage' => ['string'], 'LogicException::getPrevious' => ['Throwable|LogicException|null'], -'LogicException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'LogicException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'LogicException::getTraceAsString' => ['string'], 'long2ip' => ['string|false', 'proper_address'=>'int'], 'lstat' => ['array|false', 'filename'=>'string'], @@ -6057,7 +6064,7 @@ 'mailparse_msg_extract_part_file' => ['string', 'mimemail'=>'resource', 'filename'=>'mixed', 'callbackfunc='=>'callable'], 'mailparse_msg_extract_whole_part_file' => ['string', 'mimemail'=>'resource', 'filename'=>'string', 'callbackfunc='=>'callable'], 'mailparse_msg_free' => ['bool', 'mimemail'=>'resource'], -'mailparse_msg_get_part' => ['resource', 'mimemail'=>'resource', 'mimesection'=>'string'], +'mailparse_msg_get_part' => ['resource|false', 'mimemail'=>'resource', 'mimesection'=>'string'], 'mailparse_msg_get_part_data' => ['array', 'mimemail'=>'resource'], 'mailparse_msg_get_structure' => ['array', 'mimemail'=>'resource'], 'mailparse_msg_parse' => ['bool', 'mimemail'=>'resource', 'data'=>'string'], @@ -6135,7 +6142,7 @@ 'mapObj::zoomPoint' => ['int', 'nZoomFactor'=>'int', 'oPixelPos'=>'pointObj', 'nImageWidth'=>'int', 'nImageHeight'=>'int', 'oGeorefExt'=>'rectObj'], 'mapObj::zoomRectangle' => ['int', 'oPixelExt'=>'rectObj', 'nImageWidth'=>'int', 'nImageHeight'=>'int', 'oGeorefExt'=>'rectObj'], 'mapObj::zoomScale' => ['int', 'nScaleDenom'=>'float', 'oPixelPos'=>'pointObj', 'nImageWidth'=>'int', 'nImageHeight'=>'int', 'oGeorefExt'=>'rectObj', 'oMaxGeorefExt'=>'rectObj'], -'max' => ['', '...arg1'=>'array'], +'max' => ['', '...arg1'=>'non-empty-array'], 'max\'1' => ['', 'arg1'=>'', 'arg2'=>'', '...args='=>''], 'maxdb::__construct' => ['void', 'host='=>'string', 'username='=>'string', 'passwd='=>'string', 'dbname='=>'string', 'port='=>'int', 'socket='=>'string'], 'maxdb::affected_rows' => ['int', 'link'=>''], @@ -6302,7 +6309,7 @@ 'maxdb_thread_safe' => ['bool'], 'maxdb_use_result' => ['resource', 'link'=>''], 'maxdb_warning_count' => ['int', 'link'=>'resource'], -'mb_check_encoding' => ['bool', 'var='=>'string', 'encoding='=>'string'], +'mb_check_encoding' => ['bool', 'var='=>'string|array', 'encoding='=>'string'], 'mb_chr' => ['string|false', 'cp'=>'int', 'encoding='=>'string'], 'mb_convert_case' => ['string', 'sourcestring'=>'string', 'mode'=>'int', 'encoding='=>'string'], 'mb_convert_encoding' => ['string|array|false', 'val'=>'string|array', 'to_encoding'=>'string', 'from_encoding='=>'mixed'], @@ -6311,14 +6318,14 @@ 'mb_decode_mimeheader' => ['string', 'string'=>'string'], 'mb_decode_numericentity' => ['string', 'string'=>'string', 'convmap'=>'array', 'encoding'=>'string'], 'mb_detect_encoding' => ['string|false', 'str'=>'string', 'encoding_list='=>'mixed', 'strict='=>'bool'], -'mb_detect_order' => ['bool|array', 'encoding_list='=>'mixed'], +'mb_detect_order' => ['bool|list', 'encoding_list='=>'non-empty-list|non-falsy-string'], 'mb_encode_mimeheader' => ['string', 'str'=>'string', 'charset='=>'string', 'transfer_encoding='=>'string', 'linefeed='=>'string', 'indent='=>'int'], 'mb_encode_numericentity' => ['string', 'string'=>'string', 'convmap'=>'array', 'encoding='=>'string', 'is_hex='=>'bool'], -'mb_encoding_aliases' => ['array|false', 'encoding'=>'string'], +'mb_encoding_aliases' => ['list|false', 'encoding'=>'string'], 'mb_ereg' => ['int|false', 'pattern'=>'string', 'string'=>'string', '&w_registers='=>'array'], 'mb_ereg_match' => ['bool', 'pattern'=>'string', 'string'=>'string', 'option='=>'string'], 'mb_ereg_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'option='=>'string'], -'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'option='=>'string'], +'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable(array):string', 'string'=>'string', 'option='=>'string'], 'mb_ereg_search' => ['bool', 'pattern='=>'string', 'option='=>'string'], 'mb_ereg_search_getpos' => ['int'], 'mb_ereg_search_getregs' => ['array|false'], @@ -6333,7 +6340,7 @@ 'mb_http_output' => ['string|bool', 'encoding='=>'string'], 'mb_internal_encoding' => ['string|bool', 'encoding='=>'string'], 'mb_language' => ['string|bool', 'language='=>'string'], -'mb_list_encodings' => ['array'], +'mb_list_encodings' => ['non-empty-list'], 'mb_ord' => ['int|false', 'str'=>'string', 'enc='=>'string'], 'mb_output_handler' => ['string', 'contents'=>'string', 'status'=>'int'], 'mb_parse_str' => ['bool', 'encoded_string'=>'string', '&w_result='=>'array'], @@ -6354,8 +6361,8 @@ '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_strtolower' => ['lowercase-string', 'str'=>'string', 'encoding='=>'string'], +'mb_strtoupper' => ['uppercase-string', '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'], @@ -6396,8 +6403,8 @@ 'mcrypt_module_open' => ['resource|false', 'cipher'=>'string', 'cipher_directory'=>'string', 'mode'=>'string', 'mode_directory'=>'string'], 'mcrypt_module_self_test' => ['bool', 'algorithm'=>'string', 'lib_dir='=>'string'], 'mcrypt_ofb' => ['string', 'cipher'=>'string', 'key'=>'string', 'data'=>'string', 'mode'=>'int', 'iv='=>'string'], -'md5' => ['non-empty-string', 'str'=>'string', 'raw_output='=>'bool'], -'md5_file' => ['non-empty-string|false', 'filename'=>'string', 'raw_output='=>'bool'], +'md5' => ['non-falsy-string&lowercase-string', 'str'=>'string', 'raw_output='=>'bool'], +'md5_file' => ['(non-falsy-string&lowercase-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'], @@ -6406,7 +6413,8 @@ 'Memcache::decrement' => ['int', 'key'=>'string', 'value='=>'int'], 'Memcache::delete' => ['bool', 'key'=>'string', 'timeout='=>'int'], 'Memcache::flush' => ['bool'], -'Memcache::get' => ['string|array|false', 'key'=>'string', '&flags='=>'array', '&keys='=>'array'], +'Memcache::get' => ['mixed', 'key'=>'string', '&flags='=>'int'], +'Memcache::get\'1' => ['mixed[]|false', 'keys'=>'string[]', '&flags='=>'int[]'], 'Memcache::getExtendedStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], 'Memcache::getServerStatus' => ['int', 'host'=>'string', 'port='=>'int'], 'Memcache::getStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], @@ -6473,7 +6481,8 @@ 'MemcachePool::decrement' => ['int', 'key'=>'string', 'value='=>'int'], 'MemcachePool::delete' => ['bool', 'key'=>'string', 'timeout='=>'int'], 'MemcachePool::flush' => ['bool'], -'MemcachePool::get' => ['string|array|false', 'key'=>'string', '&flags='=>'array', '&keys='=>'array'], +'MemcachePool::get' => ['mixed', 'key'=>'string', '&flags='=>'int'], +'MemcachePool::get\'1' => ['mixed[]|false', 'keys'=>'string[]', '&flags='=>'int[]'], 'MemcachePool::getExtendedStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], 'MemcachePool::getServerStatus' => ['int', 'host'=>'string', 'port='=>'int'], 'MemcachePool::getStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], @@ -6483,8 +6492,8 @@ 'MemcachePool::set' => ['bool', 'key'=>'string', 'var'=>'mixed', 'flag='=>'int', 'expire='=>'int'], 'MemcachePool::setCompressThreshold' => ['bool', 'threshold'=>'int', 'min_savings='=>'float'], 'MemcachePool::setServerParams' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'int', 'retry_interval='=>'int', 'status='=>'bool', 'failure_callback='=>'callable'], -'memory_get_peak_usage' => ['int', 'real_usage='=>'bool'], -'memory_get_usage' => ['int', 'real_usage='=>'bool'], +'memory_get_peak_usage' => ['positive-int', 'real_usage='=>'bool'], +'memory_get_usage' => ['positive-int', 'real_usage='=>'bool'], 'MessageFormatter::__construct' => ['void', 'locale'=>'string', 'pattern'=>'string'], 'MessageFormatter::create' => ['MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], 'MessageFormatter::format' => ['false|string', 'args'=>'array'], @@ -6505,7 +6514,7 @@ 'mhash_keygen_s2k' => ['string|false', 'hash'=>'int', 'input_password'=>'string', 'salt'=>'string', 'bytes'=>'int'], 'microtime' => ['mixed', 'get_as_float='=>'bool'], 'mime_content_type' => ['string|false', 'filename_or_stream'=>'string|resource'], -'min' => ['', '...arg1'=>'array'], +'min' => ['', '...arg1'=>'non-empty-array'], 'min\'1' => ['', 'arg1'=>'', 'arg2'=>'', '...args='=>''], 'ming_keypress' => ['int', 'char'=>'string'], 'ming_setcubicthreshold' => ['void', 'threshold'=>'int'], @@ -6514,7 +6523,7 @@ 'ming_useconstants' => ['void', 'use'=>'int'], 'ming_useswfversion' => ['void', 'version'=>'int'], 'mkdir' => ['bool', 'pathname'=>'string', 'mode='=>'int', 'recursive='=>'bool', 'context='=>'resource'], -'mktime' => ['int|false', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], +'mktime' => ['__benevolent', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], 'money_format' => ['string', 'format'=>'string', 'value'=>'float'], 'Mongo::__construct' => ['void', 'server='=>'string', 'options='=>'array', 'driver_options='=>'array'], 'Mongo::__get' => ['MongoDB', 'dbname'=>'string'], @@ -6584,7 +6593,7 @@ 'MongoCollection::ensureIndex' => ['bool', 'keys'=>'array', 'options='=>'array'], 'MongoCollection::find' => ['MongoCursor', 'query='=>'array', 'fields='=>'array'], 'MongoCollection::findAndModify' => ['array', 'query'=>'array', 'update='=>'array', 'fields='=>'array', 'options='=>'array'], -'MongoCollection::findOne' => ['array', 'query='=>'array', 'fields='=>'array'], +'MongoCollection::findOne' => ['array|null', 'query='=>'array', 'fields='=>'array'], 'MongoCollection::getDBRef' => ['array', 'ref'=>'array'], 'MongoCollection::getIndexInfo' => ['array'], 'MongoCollection::getName' => ['string'], @@ -6595,7 +6604,7 @@ 'MongoCollection::insert' => ['bool|array', 'a'=>'array', 'options='=>'array'], 'MongoCollection::parallelCollectionScan' => ['MongoCommandCursor[]', 'num_cursors'=>'int'], 'MongoCollection::remove' => ['bool|array', 'criteria='=>'array', 'options='=>'array'], -'MongoCollection::save' => ['mixed', 'a'=>'array', 'options='=>'array'], +'MongoCollection::save' => ['mixed', 'a'=>'array|object', 'options='=>'array'], 'MongoCollection::setReadPreference' => ['bool', 'read_preference'=>'string', 'tags='=>'array'], 'MongoCollection::setSlaveOkay' => ['bool', 'ok='=>'bool'], 'MongoCollection::setWriteConcern' => ['bool', 'w'=>'mixed', 'wtimeout='=>'int'], @@ -6657,7 +6666,7 @@ 'MongoCursorException::getLine' => ['int'], 'MongoCursorException::getMessage' => ['string'], 'MongoCursorException::getPrevious' => ['Exception|Throwable'], -'MongoCursorException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'MongoCursorException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoCursorException::getTraceAsString' => ['string'], 'MongoCursorInterface::__construct' => ['void'], 'MongoCursorInterface::batchSize' => ['MongoCursorInterface', 'batchSize'=>'int'], @@ -6703,125 +6712,360 @@ 'MongoDB::setReadPreference' => ['bool', 'read_preference'=>'string', 'tags='=>'array'], 'MongoDB::setSlaveOkay' => ['bool', 'ok='=>'bool'], 'MongoDB::setWriteConcern' => ['bool', 'w'=>'mixed', 'wtimeout='=>'int'], -'MongoDB\BSON\Binary::__construct' => ['void', 'data'=>'string', 'type'=>'int'], +'MongoDB\BSON\fromJSON' => ['string', 'json'=>'string'], +'MongoDB\BSON\fromPHP' => ['string', 'value'=>'object|array'], +'MongoDB\BSON\toCanonicalExtendedJSON' => ['string', 'bson'=>'string'], +'MongoDB\BSON\toJSON' => ['string', 'bson'=>'string'], +'MongoDB\BSON\toPHP' => ['object|array', 'bson'=>'string', 'typemap='=>'?array'], +'MongoDB\BSON\toRelaxedExtendedJSON' => ['string', 'bson'=>'string'], +'MongoDB\Driver\Monitoring\addSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Monitoring\removeSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\BSON\Binary::__construct' => ['void', 'data'=>'string', 'type='=>'int'], 'MongoDB\BSON\Binary::getData' => ['string'], 'MongoDB\BSON\Binary::getType' => ['int'], -'MongoDB\BSON\Decimal128::__construct' => ['void', 'value='=>'string'], +'MongoDB\BSON\Binary::__toString' => ['string'], +'MongoDB\BSON\Binary::serialize' => ['string'], +'MongoDB\BSON\Binary::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], +'MongoDB\BSON\BinaryInterface::getData' => ['string'], +'MongoDB\BSON\BinaryInterface::getType' => ['int'], +'MongoDB\BSON\BinaryInterface::__toString' => ['string'], +'MongoDB\BSON\DBPointer::__toString' => ['string'], +'MongoDB\BSON\DBPointer::serialize' => ['string'], +'MongoDB\BSON\DBPointer::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Decimal128::__construct' => ['void', 'value'=>'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], -'MongoDB\BSON\fromJSON' => ['string', 'json'=>'string'], -'MongoDB\BSON\fromPHP' => ['string', 'value'=>'array|object'], -'MongoDB\BSON\Javascript::__construct' => ['void', 'code'=>'string', 'scope='=>'array|object'], -'MongoDB\BSON\ObjectId::__construct' => ['void', 'id='=>'string'], +'MongoDB\BSON\Decimal128::serialize' => ['string'], +'MongoDB\BSON\Decimal128::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], +'MongoDB\BSON\Document::fromBSON' => ['MongoDB\BSON\Document', 'bson'=>'string'], +'MongoDB\BSON\Document::fromJSON' => ['MongoDB\BSON\Document', 'json'=>'string'], +'MongoDB\BSON\Document::fromPHP' => ['MongoDB\BSON\Document', 'value'=>'object|array'], +'MongoDB\BSON\Document::get' => ['mixed', 'key'=>'string'], +'MongoDB\BSON\Document::getIterator' => ['MongoDB\BSON\Iterator'], +'MongoDB\BSON\Document::has' => ['bool', 'key'=>'string'], +'MongoDB\BSON\Document::toPHP' => ['object|array', 'typeMap='=>'?array'], +'MongoDB\BSON\Document::toCanonicalExtendedJSON' => ['string'], +'MongoDB\BSON\Document::toRelaxedExtendedJSON' => ['string'], +'MongoDB\BSON\Document::offsetExists' => ['bool', 'offset'=>'mixed'], +'MongoDB\BSON\Document::offsetGet' => ['mixed', 'offset'=>'mixed'], +'MongoDB\BSON\Document::offsetSet' => ['void', 'offset'=>'mixed', 'value'=>'mixed'], +'MongoDB\BSON\Document::offsetUnset' => ['void', 'offset'=>'mixed'], +'MongoDB\BSON\Document::__toString' => ['string'], +'MongoDB\BSON\Document::serialize' => ['string'], +'MongoDB\BSON\Document::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Int64::__construct' => ['void', 'value'=>'string|int'], +'MongoDB\BSON\Int64::__toString' => ['string'], +'MongoDB\BSON\Int64::serialize' => ['string'], +'MongoDB\BSON\Int64::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Iterator::current' => ['mixed'], +'MongoDB\BSON\Iterator::key' => ['string|int'], +'MongoDB\BSON\Iterator::next' => ['void'], +'MongoDB\BSON\Iterator::rewind' => ['void'], +'MongoDB\BSON\Iterator::valid' => ['bool'], +'MongoDB\BSON\Javascript::__construct' => ['void', 'code'=>'string', 'scope='=>'object|array|null'], +'MongoDB\BSON\Javascript::getCode' => ['string'], +'MongoDB\BSON\Javascript::getScope' => ['?object'], +'MongoDB\BSON\Javascript::__toString' => ['string'], +'MongoDB\BSON\Javascript::serialize' => ['string'], +'MongoDB\BSON\Javascript::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], +'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], +'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], +'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], +'MongoDB\BSON\MaxKey::serialize' => ['string'], +'MongoDB\BSON\MaxKey::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], +'MongoDB\BSON\MinKey::serialize' => ['string'], +'MongoDB\BSON\MinKey::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], +'MongoDB\BSON\ObjectId::__construct' => ['void', 'id='=>'?string'], +'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], +'MongoDB\BSON\ObjectId::serialize' => ['string'], +'MongoDB\BSON\ObjectId::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], +'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], +'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], +'MongoDB\BSON\PackedArray::fromPHP' => ['MongoDB\BSON\PackedArray', 'value'=>'array'], +'MongoDB\BSON\PackedArray::get' => ['mixed', 'index'=>'int'], +'MongoDB\BSON\PackedArray::getIterator' => ['MongoDB\BSON\Iterator'], +'MongoDB\BSON\PackedArray::has' => ['bool', 'index'=>'int'], +'MongoDB\BSON\PackedArray::toPHP' => ['object|array', 'typeMap='=>'?array'], +'MongoDB\BSON\PackedArray::offsetExists' => ['bool', 'offset'=>'mixed'], +'MongoDB\BSON\PackedArray::offsetGet' => ['mixed', 'offset'=>'mixed'], +'MongoDB\BSON\PackedArray::offsetSet' => ['void', 'offset'=>'mixed', 'value'=>'mixed'], +'MongoDB\BSON\PackedArray::offsetUnset' => ['void', 'offset'=>'mixed'], +'MongoDB\BSON\PackedArray::__toString' => ['string'], +'MongoDB\BSON\PackedArray::serialize' => ['string'], +'MongoDB\BSON\PackedArray::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Persistable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|array'], 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern'=>'string', 'flags='=>'string'], -'MongoDB\BSON\Regex::__toString' => ['string'], -'MongoDB\BSON\Regex::getFlags' => [''], 'MongoDB\BSON\Regex::getPattern' => ['string'], -'MongoDB\BSON\Serializable::bsonSerialize' => ['array|object'], -'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment'=>'int', 'timestamp'=>'int'], +'MongoDB\BSON\Regex::getFlags' => ['string'], +'MongoDB\BSON\Regex::__toString' => ['string'], +'MongoDB\BSON\Regex::serialize' => ['string'], +'MongoDB\BSON\Regex::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], +'MongoDB\BSON\RegexInterface::getPattern' => ['string'], +'MongoDB\BSON\RegexInterface::getFlags' => ['string'], +'MongoDB\BSON\RegexInterface::__toString' => ['string'], +'MongoDB\BSON\Serializable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray|array'], +'MongoDB\BSON\Symbol::__toString' => ['string'], +'MongoDB\BSON\Symbol::serialize' => ['string'], +'MongoDB\BSON\Symbol::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment'=>'string|int', 'timestamp'=>'string|int'], +'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], +'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], -'MongoDB\BSON\toJSON' => ['string', 'bson'=>'string'], -'MongoDB\BSON\toPHP' => ['object', 'bson'=>'string', 'typeMap='=>'array'], -'MongoDB\BSON\Unserializable::bsonUnserialize' => ['', 'data'=>'array'], -'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds='=>'int|DateTimeInterface'], -'MongoDB\BSON\UTCDateTime::__toString' => ['string'], +'MongoDB\BSON\Timestamp::serialize' => ['string'], +'MongoDB\BSON\Timestamp::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], +'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], +'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], +'MongoDB\BSON\TimestampInterface::__toString' => ['string'], +'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds='=>'DateTimeInterface|string|int|float|null'], 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], -'MongoDB\Driver\BulkWrite::__construct' => ['void', 'ordered='=>'bool'], -'MongoDB\Driver\BulkWrite::count' => ['0|positive-int'], -'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'array|object', 'deleteOptions='=>'array'], -'MongoDB\Driver\BulkWrite::insert' => ['MongoDB\Driver\ObjectID', 'document'=>'array|object'], -'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array'], -'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'array|object'], -'MongoDB\Driver\Cursor::__construct' => ['void', 'server'=>'Server', 'responseDocument'=>'string'], +'MongoDB\BSON\UTCDateTime::__toString' => ['string'], +'MongoDB\BSON\UTCDateTime::serialize' => ['string'], +'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], +'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], +'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], +'MongoDB\BSON\Undefined::__toString' => ['string'], +'MongoDB\BSON\Undefined::serialize' => ['string'], +'MongoDB\BSON\Undefined::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data'=>'array'], +'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options='=>'?array'], +'MongoDB\Driver\BulkWrite::count' => ['int'], +'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'object|array', 'deleteOptions='=>'?array'], +'MongoDB\Driver\BulkWrite::insert' => ['mixed', 'document'=>'object|array'], +'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'object|array', 'newObj'=>'object|array', 'updateOptions='=>'?array'], +'MongoDB\Driver\ClientEncryption::__construct' => ['void', 'options'=>'array'], +'MongoDB\Driver\ClientEncryption::addKeyAltName' => ['?object', 'keyId'=>'MongoDB\BSON\Binary', 'keyAltName'=>'string'], +'MongoDB\Driver\ClientEncryption::createDataKey' => ['MongoDB\BSON\Binary', 'kmsProvider'=>'string', 'options='=>'?array'], +'MongoDB\Driver\ClientEncryption::decrypt' => ['mixed', 'value'=>'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::deleteKey' => ['object', 'keyId'=>'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::encrypt' => ['MongoDB\BSON\Binary', 'value'=>'mixed', 'options='=>'?array'], +'MongoDB\Driver\ClientEncryption::encryptExpression' => ['object', 'expr'=>'object|array', 'options='=>'?array'], +'MongoDB\Driver\ClientEncryption::getKey' => ['?object', 'keyId'=>'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::getKeyByAltName' => ['?object', 'keyAltName'=>'string'], +'MongoDB\Driver\ClientEncryption::getKeys' => ['MongoDB\Driver\Cursor'], +'MongoDB\Driver\ClientEncryption::removeKeyAltName' => ['?object', 'keyId'=>'MongoDB\BSON\Binary', 'keyAltName'=>'string'], +'MongoDB\Driver\ClientEncryption::rewrapManyDataKey' => ['object', 'filter'=>'object|array', 'options='=>'?array'], +'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'object|array', 'commandOptions='=>'?array'], +'MongoDB\Driver\Cursor::current' => ['object|array|null'], 'MongoDB\Driver\Cursor::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\Cursor::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\Cursor::isDead' => ['bool'], +'MongoDB\Driver\Cursor::key' => ['?int'], +'MongoDB\Driver\Cursor::next' => ['void'], +'MongoDB\Driver\Cursor::rewind' => ['void'], 'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap'=>'array'], 'MongoDB\Driver\Cursor::toArray' => ['array'], -'MongoDB\Driver\CursorId::__construct' => ['void', 'id'=>'string'], +'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::__clone' => ['void'], -'MongoDB\Driver\Exception\RuntimeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?RuntimeException)|(?Throwable)'], +'MongoDB\Driver\CursorId::serialize' => ['string'], +'MongoDB\Driver\CursorId::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], +'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\CursorInterface::isDead' => ['bool'], +'MongoDB\Driver\CursorInterface::setTypeMap' => ['void', 'typemap'=>'array'], +'MongoDB\Driver\CursorInterface::toArray' => ['array'], +'MongoDB\Driver\Exception\AuthenticationException::__toString' => ['string'], +'MongoDB\Driver\Exception\BulkWriteException::__toString' => ['string'], +'MongoDB\Driver\Exception\CommandException::getResultDocument' => ['object'], +'MongoDB\Driver\Exception\CommandException::__toString' => ['string'], +'MongoDB\Driver\Exception\ConnectionException::__toString' => ['string'], +'MongoDB\Driver\Exception\ConnectionTimeoutException::__toString' => ['string'], +'MongoDB\Driver\Exception\EncryptionException::__toString' => ['string'], +'MongoDB\Driver\Exception\Exception::__toString' => ['string'], +'MongoDB\Driver\Exception\ExecutionTimeoutException::__toString' => ['string'], +'MongoDB\Driver\Exception\InvalidArgumentException::__toString' => ['string'], +'MongoDB\Driver\Exception\LogicException::__toString' => ['string'], +'MongoDB\Driver\Exception\RuntimeException::hasErrorLabel' => ['bool', 'errorLabel'=>'string'], 'MongoDB\Driver\Exception\RuntimeException::__toString' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::__wakeup' => ['void'], -'MongoDB\Driver\Exception\RuntimeException::getCode' => ['int'], -'MongoDB\Driver\Exception\RuntimeException::getFile' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::getLine' => ['int'], -'MongoDB\Driver\Exception\RuntimeException::getMessage' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::getPrevious' => ['RuntimeException|Throwable'], -'MongoDB\Driver\Exception\RuntimeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?: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)'], -'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], -'MongoDB\Driver\Exception\WriteException::__wakeup' => ['void'], -'MongoDB\Driver\Exception\WriteException::getCode' => ['int'], -'MongoDB\Driver\Exception\WriteException::getFile' => ['string'], -'MongoDB\Driver\Exception\WriteException::getLine' => ['int'], -'MongoDB\Driver\Exception\WriteException::getMessage' => ['string'], -'MongoDB\Driver\Exception\WriteException::getPrevious' => ['RuntimeException|Throwable'], -'MongoDB\Driver\Exception\WriteException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], -'MongoDB\Driver\Exception\WriteException::getTraceAsString' => ['string'], +'MongoDB\Driver\Exception\SSLConnectionException::__toString' => ['string'], +'MongoDB\Driver\Exception\ServerException::__toString' => ['string'], +'MongoDB\Driver\Exception\UnexpectedValueException::__toString' => ['string'], 'MongoDB\Driver\Exception\WriteException::getWriteResult' => ['MongoDB\Driver\WriteResult'], -'MongoDB\Driver\Manager::__construct' => ['void', 'uri'=>'string', 'options='=>'array', 'driverOptions='=>'array'], -'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulk'=>'MongoDB\Driver\BulkWrite', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::executeDelete' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'deleteOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeInsert' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'document'=>'array|object', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::executeUpdate' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], +'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], +'MongoDB\Driver\Manager::__construct' => ['void', 'uri='=>'?string', 'uriOptions='=>'?array', 'driverOptions='=>'?array'], +'MongoDB\Driver\Manager::addSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Manager::createClientEncryption' => ['MongoDB\Driver\ClientEncryption', 'options'=>'array'], +'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulk'=>'MongoDB\Driver\BulkWrite', 'options='=>'MongoDB\Driver\WriteConcern|array|null'], +'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Manager::getEncryptedFieldsMap' => ['object|array|null'], 'MongoDB\Driver\Manager::getReadConcern' => ['MongoDB\Driver\ReadConcern'], 'MongoDB\Driver\Manager::getReadPreference' => ['MongoDB\Driver\ReadPreference'], 'MongoDB\Driver\Manager::getServers' => ['array'], 'MongoDB\Driver\Manager::getWriteConcern' => ['MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference'=>'MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Query::__construct' => ['void', 'filter'=>'array|object', 'queryOptions='=>'array'], -'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level='=>'string'], -'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object'], -'MongoDB\Driver\ReadConcern::getLevel' => ['null|string'], -'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode'=>'string|int', 'tagSets='=>'array', 'options='=>'array'], -'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object'], +'MongoDB\Driver\Manager::removeSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference='=>'?MongoDB\Driver\ReadPreference'], +'MongoDB\Driver\Manager::startSession' => ['MongoDB\Driver\Session', 'options='=>'?array'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getError' => ['Exception'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommand' => ['object'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getDatabaseName' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandStartedEvent'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandSucceededEvent'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandFailedEvent'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\LogSubscriber::log' => ['void', 'level'=>'int', 'domain'=>'string', 'message'=>'string'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverChanged' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerChangedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverClosed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerClosedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverOpening' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerOpeningEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyChanged' => ['void', 'event'=>'MongoDB\Driver\Monitoring\TopologyChangedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyClosed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\TopologyClosedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyOpening' => ['void', 'event'=>'MongoDB\Driver\Monitoring\TopologyOpeningEvent'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getNewDescription' => ['MongoDB\Driver\ServerDescription'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getPreviousDescription' => ['MongoDB\Driver\ServerDescription'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getError' => ['Exception'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getNewDescription' => ['MongoDB\Driver\TopologyDescription'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getPreviousDescription' => ['MongoDB\Driver\TopologyDescription'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Query::__construct' => ['void', 'filter'=>'object|array', 'queryOptions='=>'?array'], +'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level='=>'?string'], +'MongoDB\Driver\ReadConcern::getLevel' => ['?string'], +'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], +'MongoDB\Driver\ReadConcern::bsonSerialize' => ['stdClass'], +'MongoDB\Driver\ReadConcern::serialize' => ['string'], +'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode'=>'string|int', 'tagSets='=>'?array', 'options='=>'?array'], +'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], +'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], 'MongoDB\Driver\ReadPreference::getMode' => ['int'], +'MongoDB\Driver\ReadPreference::getModeString' => ['string'], 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], -'MongoDB\Driver\Server::__construct' => ['void', 'host'=>'string', 'port'=>'string', 'options='=>'array', 'driverOptions='=>'array'], -'MongoDB\Driver\Server::executeBulkWrite' => ['', 'namespace'=>'string', 'zwrite'=>'BulkWrite'], -'MongoDB\Driver\Server::executeCommand' => ['', 'db'=>'string', 'command'=>'Command'], -'MongoDB\Driver\Server::executeQuery' => ['', 'namespace'=>'string', 'zquery'=>'Query'], -'MongoDB\Driver\Server::getHost' => [''], -'MongoDB\Driver\Server::getInfo' => [''], -'MongoDB\Driver\Server::getLatency' => [''], -'MongoDB\Driver\Server::getPort' => [''], -'MongoDB\Driver\Server::getState' => [''], +'MongoDB\Driver\ReadPreference::bsonSerialize' => ['stdClass'], +'MongoDB\Driver\ReadPreference::serialize' => ['string'], +'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulkWrite'=>'MongoDB\Driver\BulkWrite', 'options='=>'MongoDB\Driver\WriteConcern|array|null'], +'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Server::getHost' => ['string'], +'MongoDB\Driver\Server::getInfo' => ['array'], +'MongoDB\Driver\Server::getLatency' => ['?int'], +'MongoDB\Driver\Server::getPort' => ['int'], +'MongoDB\Driver\Server::getServerDescription' => ['MongoDB\Driver\ServerDescription'], 'MongoDB\Driver\Server::getTags' => ['array'], -'MongoDB\Driver\Server::getType' => [''], +'MongoDB\Driver\Server::getType' => ['int'], 'MongoDB\Driver\Server::isArbiter' => ['bool'], -'MongoDB\Driver\Server::isDelayed' => [''], 'MongoDB\Driver\Server::isHidden' => ['bool'], -'MongoDB\Driver\Server::isPassive' => [''], +'MongoDB\Driver\Server::isPassive' => ['bool'], 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], -'MongoDB\Driver\WriteConcern::__construct' => ['void', 'w'=>'string|int', 'wtimeout='=>'int', 'journal='=>'bool', 'fsync='=>'bool'], -'MongoDB\Driver\WriteConcern::getJurnal' => ['bool|null'], -'MongoDB\Driver\WriteConcern::getW' => ['int|null|string'], +'MongoDB\Driver\ServerApi::__construct' => ['void', 'version'=>'string', 'strict='=>'?bool', 'deprecationErrors='=>'?bool'], +'MongoDB\Driver\ServerApi::bsonSerialize' => ['stdClass'], +'MongoDB\Driver\ServerApi::serialize' => ['string'], +'MongoDB\Driver\ServerApi::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], +'MongoDB\Driver\ServerDescription::getHost' => ['string'], +'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], +'MongoDB\Driver\ServerDescription::getPort' => ['int'], +'MongoDB\Driver\ServerDescription::getRoundTripTime' => ['?int'], +'MongoDB\Driver\ServerDescription::getType' => ['string'], +'MongoDB\Driver\Session::abortTransaction' => ['void'], +'MongoDB\Driver\Session::advanceClusterTime' => ['void', 'clusterTime'=>'object|array'], +'MongoDB\Driver\Session::advanceOperationTime' => ['void', 'operationTime'=>'MongoDB\BSON\TimestampInterface'], +'MongoDB\Driver\Session::commitTransaction' => ['void'], +'MongoDB\Driver\Session::endSession' => ['void'], +'MongoDB\Driver\Session::getClusterTime' => ['?object'], +'MongoDB\Driver\Session::getLogicalSessionId' => ['object'], +'MongoDB\Driver\Session::getOperationTime' => ['?MongoDB\BSON\Timestamp'], +'MongoDB\Driver\Session::getServer' => ['?MongoDB\Driver\Server'], +'MongoDB\Driver\Session::getTransactionOptions' => ['?array'], +'MongoDB\Driver\Session::getTransactionState' => ['string'], +'MongoDB\Driver\Session::isDirty' => ['bool'], +'MongoDB\Driver\Session::isInTransaction' => ['bool'], +'MongoDB\Driver\Session::startTransaction' => ['void', 'options='=>'?array'], +'MongoDB\Driver\TopologyDescription::getServers' => ['array'], +'MongoDB\Driver\TopologyDescription::getType' => ['string'], +'MongoDB\Driver\TopologyDescription::hasReadableServer' => ['bool', 'readPreference='=>'?MongoDB\Driver\ReadPreference'], +'MongoDB\Driver\TopologyDescription::hasWritableServer' => ['bool'], +'MongoDB\Driver\WriteConcern::__construct' => ['void', 'w'=>'string|int', 'wtimeout='=>'?int', 'journal='=>'?bool'], +'MongoDB\Driver\WriteConcern::getJournal' => ['?bool'], +'MongoDB\Driver\WriteConcern::getW' => ['string|int|null'], 'MongoDB\Driver\WriteConcern::getWtimeout' => ['int'], -'MongoDB\Driver\WriteConcernError::getCode' => [''], -'MongoDB\Driver\WriteConcernError::getInfo' => [''], -'MongoDB\Driver\WriteConcernError::getMessage' => [''], -'MongoDB\Driver\WriteError::getCode' => [''], -'MongoDB\Driver\WriteError::getIndex' => [''], -'MongoDB\Driver\WriteError::getInfo' => ['mixed'], -'MongoDB\Driver\WriteError::getMessage' => [''], -'MongoDB\Driver\WriteException::getWriteResult' => [''], -'MongoDB\Driver\WriteResult::getDeletedCount' => ['int'], -'MongoDB\Driver\WriteResult::getInfo' => [''], -'MongoDB\Driver\WriteResult::getInsertedCount' => ['int'], -'MongoDB\Driver\WriteResult::getMatchedCount' => ['int'], -'MongoDB\Driver\WriteResult::getModifiedCount' => ['int'], -'MongoDB\Driver\WriteResult::getServer' => [''], -'MongoDB\Driver\WriteResult::getUpsertedCount' => ['int'], -'MongoDB\Driver\WriteResult::getUpsertedIds' => [''], -'MongoDB\Driver\WriteResult::getWriteConcernError' => [''], -'MongoDB\Driver\WriteResult::getWriteErrors' => [''], +'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], +'MongoDB\Driver\WriteConcern::bsonSerialize' => ['stdClass'], +'MongoDB\Driver\WriteConcern::serialize' => ['string'], +'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\WriteConcernError::getCode' => ['int'], +'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], +'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], +'MongoDB\Driver\WriteError::getCode' => ['int'], +'MongoDB\Driver\WriteError::getIndex' => ['int'], +'MongoDB\Driver\WriteError::getInfo' => ['?object'], +'MongoDB\Driver\WriteError::getMessage' => ['string'], +'MongoDB\Driver\WriteResult::getInsertedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getMatchedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getModifiedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getUpsertedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\WriteResult::getUpsertedIds' => ['array'], +'MongoDB\Driver\WriteResult::getWriteConcernError' => ['?MongoDB\Driver\WriteConcernError'], +'MongoDB\Driver\WriteResult::getWriteErrors' => ['array'], +'MongoDB\Driver\WriteResult::getErrorReplies' => ['array'], 'MongoDB\Driver\WriteResult::isAcknowledged' => ['bool'], 'MongoDBRef::create' => ['array', 'collection'=>'string', 'id'=>'mixed', 'database='=>'string'], 'MongoDBRef::get' => ['array', 'db'=>'mongodb', 'ref'=>'array'], @@ -6836,7 +7080,7 @@ 'MongoException::getLine' => ['int'], 'MongoException::getMessage' => ['string'], 'MongoException::getPrevious' => ['Exception|Throwable'], -'MongoException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'MongoException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoException::getTraceAsString' => ['string'], 'MongoGridFS::__construct' => ['void', 'db'=>'MongoDB', 'prefix='=>'string', 'chunks='=>'mixed'], 'MongoGridFS::__get' => ['MongoCollection', 'name'=>'string'], @@ -6929,7 +7173,7 @@ 'MongoLog::getCallback' => ['callable'], 'MongoLog::getLevel' => ['int'], 'MongoLog::getModule' => ['int'], -'MongoLog::setCallback' => ['void', 'log_function'=>'callable'], +'MongoLog::setCallback' => ['bool', 'log_function'=>'callable'], 'MongoLog::setLevel' => ['void', 'level'=>'int'], 'MongoLog::setModule' => ['void', 'module'=>'int'], 'MongoPool::getSize' => ['int'], @@ -6947,7 +7191,7 @@ 'MongoResultException::getLine' => ['int'], 'MongoResultException::getMessage' => ['string'], 'MongoResultException::getPrevious' => ['Exception|Throwable'], -'MongoResultException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'MongoResultException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoResultException::getTraceAsString' => ['string'], 'MongoTimestamp::__construct' => ['void', 'sec='=>'int', 'inc='=>'int'], 'MongoTimestamp::__toString' => ['string'], @@ -6967,7 +7211,7 @@ 'MongoWriteConcernException::getLine' => ['int'], 'MongoWriteConcernException::getMessage' => ['string'], 'MongoWriteConcernException::getPrevious' => ['Exception|Throwable'], -'MongoWriteConcernException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'MongoWriteConcernException::getTrace' => ['list\',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'], @@ -7174,7 +7418,7 @@ 'mysqli::get_charset' => ['object'], 'mysqli::get_client_info' => ['string'], 'mysqli::get_connection_stats' => ['array|false'], -'mysqli::get_warnings' => ['mysqli_warning'], +'mysqli::get_warnings' => ['mysqli_warning|false'], 'mysqli::init' => ['mysqli'], 'mysqli::kill' => ['bool', 'processid'=>'int'], 'mysqli::more_results' => ['bool'], @@ -7185,7 +7429,7 @@ 'mysqli::poll' => ['int|false', '&w_read'=>'array', '&w_error'=>'array', '&w_reject'=>'array', 'sec'=>'int', 'usec='=>'int'], 'mysqli::prepare' => ['mysqli_stmt|false', 'query'=>'string'], 'mysqli::query' => ['bool|mysqli_result', 'query'=>'string', 'resultmode='=>'int'], -'mysqli::real_connect' => ['bool', 'host='=>'string', 'username='=>'string', 'passwd='=>'string', 'dbname='=>'string', 'port='=>'int', 'socket='=>'string', 'flags='=>'int'], +'mysqli::real_connect' => ['bool', 'host='=>'?string', 'username='=>'?string', 'passwd='=>'?string', 'dbname='=>'?string', 'port='=>'?int', 'socket='=>'?string', 'flags='=>'int'], 'mysqli::real_escape_string' => ['string', 'escapestr'=>'string'], 'mysqli::real_query' => ['bool', 'query'=>'string'], 'mysqli::reap_async_query' => ['mysqli_result|false'], @@ -7205,7 +7449,7 @@ 'mysqli::store_result' => ['mysqli_result|false', 'option='=>'int'], 'mysqli::thread_safe' => ['bool'], 'mysqli::use_result' => ['mysqli_result|false'], -'mysqli_affected_rows' => ['int', 'link'=>'mysqli'], +'mysqli_affected_rows' => ['int<-1,max>|numeric-string', 'link'=>'mysqli'], 'mysqli_autocommit' => ['bool', 'link'=>'mysqli', 'mode'=>'bool'], 'mysqli_begin_transaction' => ['bool', 'link'=>'mysqli', 'flags='=>'int', 'name='=>'string'], 'mysqli_change_user' => ['bool', 'link'=>'mysqli', 'user'=>'string', 'password'=>'string', 'database'=>'string'], @@ -7229,14 +7473,15 @@ 'mysqli_errno' => ['int', 'link'=>'mysqli'], 'mysqli_error' => ['string|null', 'link'=>'mysqli'], 'mysqli_error_list' => ['array', 'connection'=>'mysqli'], -'mysqli_fetch_all' => ['array|false', 'result'=>'mysqli_result', 'resulttype='=>'int'], +'mysqli_fetch_all' => ['list', 'result'=>'mysqli_result', 'resulttype='=>'int'], 'mysqli_fetch_array' => ['array|null|false', 'result'=>'mysqli_result', 'resulttype='=>'int'], -'mysqli_fetch_assoc' => ['array|null', 'result'=>'mysqli_result'], -'mysqli_fetch_field' => ['object|false', 'result'=>'mysqli_result'], -'mysqli_fetch_field_direct' => ['object|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], -'mysqli_fetch_fields' => ['array', 'result'=>'mysqli_result'], +'mysqli_fetch_assoc' => ['array|null|false', 'result'=>'mysqli_result'], +'mysqli_fetch_column' => ['null|int|float|string|false', 'result' => 'mysqli_result', 'column'=>'int'], +'mysqli_fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result'], +'mysqli_fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], +'mysqli_fetch_fields' => ['list', 'result'=>'mysqli_result'], 'mysqli_fetch_lengths' => ['array|false', 'result'=>'mysqli_result'], -'mysqli_fetch_object' => ['object|null', 'result'=>'mysqli_result', 'class_name='=>'string', 'params='=>'?array'], +'mysqli_fetch_object' => ['object|false|null', 'result'=>'mysqli_result', 'class_name='=>'string', 'params='=>'?array'], 'mysqli_fetch_row' => ['array|null', 'result'=>'mysqli_result'], 'mysqli_field_count' => ['int', 'link'=>'mysqli'], 'mysqli_field_seek' => ['bool', 'result'=>'mysqli_result', 'fieldnr'=>'int'], @@ -7264,13 +7509,13 @@ 'mysqli_multi_query' => ['bool', 'link'=>'mysqli', 'query'=>'string'], 'mysqli_next_result' => ['bool', 'link'=>'mysqli'], 'mysqli_num_fields' => ['int', 'link'=>'mysqli_result'], -'mysqli_num_rows' => ['int', 'link'=>'mysqli_result'], +'mysqli_num_rows' => ['int<0,max>|numeric-string', 'link'=>'mysqli_result'], 'mysqli_options' => ['bool', 'link'=>'mysqli', 'option'=>'int', 'value'=>'mixed'], 'mysqli_ping' => ['bool', 'link'=>'mysqli'], 'mysqli_poll' => ['int|false', 'read'=>'array', 'error'=>'array', 'reject'=>'array', 'sec'=>'int', 'usec='=>'int'], 'mysqli_prepare' => ['mysqli_stmt|false', 'link'=>'mysqli', 'query'=>'string'], 'mysqli_query' => ['mysqli_result|bool', 'link'=>'mysqli', 'query'=>'string', 'resultmode='=>'int'], -'mysqli_real_connect' => ['bool', 'link='=>'mysqli', 'host='=>'string', 'username='=>'string', 'passwd='=>'string', 'dbname='=>'string', 'port='=>'int', 'socket='=>'string', 'flags='=>'int'], +'mysqli_real_connect' => ['bool', 'link='=>'mysqli', 'host='=>'?string', 'username='=>'?string', 'passwd='=>'?string', 'dbname='=>'?string', 'port='=>'?int', 'socket='=>'?string', 'flags='=>'int'], 'mysqli_real_escape_string' => ['string', 'link'=>'mysqli', 'escapestr'=>'string'], 'mysqli_real_query' => ['bool', 'link'=>'mysqli', 'query'=>'string'], 'mysqli_reap_async_query' => ['mysqli_result|false', 'link'=>'mysqli'], @@ -7280,12 +7525,13 @@ 'mysqli_result::__construct' => ['void', 'link'=>'mysqli', 'resultmode='=>'int'], 'mysqli_result::close' => ['void'], 'mysqli_result::data_seek' => ['bool', 'offset'=>'int'], -'mysqli_result::fetch_all' => ['array', 'resulttype='=>'int'], -'mysqli_result::fetch_array' => ['array|null', 'resulttype='=>'int'], -'mysqli_result::fetch_assoc' => ['array|null'], -'mysqli_result::fetch_field' => ['object|false'], -'mysqli_result::fetch_field_direct' => ['object|false', 'fieldnr'=>'int'], -'mysqli_result::fetch_fields' => ['array'], +'mysqli_result::fetch_all' => ['list', 'resulttype='=>'int'], +'mysqli_result::fetch_array' => ['array|null|false', 'resulttype='=>'int'], +'mysqli_result::fetch_assoc' => ['array|null|false'], +'mysqli_result::fetch_column' => ['null|int|float|string|false', 'column'=>'int'], +'mysqli_result::fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false'], +'mysqli_result::fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'fieldnr'=>'int'], +'mysqli_result::fetch_fields' => ['list'], 'mysqli_result::fetch_object' => ['object|null', 'class_name='=>'string', 'params='=>'array'], 'mysqli_result::fetch_row' => ['array|null'], 'mysqli_result::field_seek' => ['bool', 'fieldnr'=>'int'], @@ -7317,16 +7563,16 @@ 'mysqli_stmt::fetch' => ['bool|null'], 'mysqli_stmt::free_result' => ['void'], 'mysqli_stmt::get_result' => ['mysqli_result|false'], -'mysqli_stmt::get_warnings' => ['object'], +'mysqli_stmt::get_warnings' => ['mysqli_warning|false'], 'mysqli_stmt::more_results' => ['bool'], 'mysqli_stmt::next_result' => ['bool'], -'mysqli_stmt::num_rows' => ['int'], +'mysqli_stmt::num_rows' => ['int<0,max>|numeric-string'], 'mysqli_stmt::prepare' => ['bool', 'query'=>'string'], 'mysqli_stmt::reset' => ['bool'], 'mysqli_stmt::result_metadata' => ['mysqli_result|false'], 'mysqli_stmt::send_long_data' => ['bool', 'param_nr'=>'int', 'data'=>'string'], 'mysqli_stmt::store_result' => ['bool'], -'mysqli_stmt_affected_rows' => ['int|string', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_affected_rows' => ['int<-1,max>|numeric-string', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_attr_get' => ['int|false', 'stmt'=>'mysqli_stmt', 'attr'=>'int'], 'mysqli_stmt_attr_set' => ['bool', 'stmt'=>'mysqli_stmt', 'attr'=>'int', 'mode'=>'int'], 'mysqli_stmt_bind_param' => ['bool', 'stmt'=>'mysqli_stmt', 'types'=>'string', 'var1'=>'mixed', '...args='=>'mixed'], @@ -7341,7 +7587,7 @@ 'mysqli_stmt_field_count' => ['0|positive-int', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_free_result' => ['void', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_get_result' => ['mysqli_result|false', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_get_warnings' => ['object|false', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_get_warnings' => ['mysqli_warning|false', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_init' => ['mysqli_stmt|false', 'link'=>'mysqli'], 'mysqli_stmt_insert_id' => ['', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_more_results' => ['bool', 'stmt'=>'mysqli_stmt'], @@ -7780,11 +8026,11 @@ 'nsapi_response_headers' => ['array'], 'nsapi_virtual' => ['bool', 'uri'=>'string'], 'nthmac' => ['string', 'clent'=>'string', 'data'=>'string'], -'number_format' => ['string', 'number'=>'float', 'num_decimal_places='=>'int', 'dec_separator='=>'string|null', 'thousands_separator='=>'string|null'], +'number_format' => ['non-empty-string', 'number'=>'float', 'num_decimal_places='=>'int', 'dec_separator='=>'string|null', 'thousands_separator='=>'string|null'], 'NumberFormatter::__construct' => ['void', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::create' => ['NumberFormatter', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], -'NumberFormatter::format' => ['string', 'num'=>'', 'type='=>'int'], -'NumberFormatter::formatCurrency' => ['string', 'num'=>'float', 'currency'=>'string'], +'NumberFormatter::format' => ['string|false', 'num'=>'', 'type='=>'int'], +'NumberFormatter::formatCurrency' => ['string|false', 'num'=>'float', 'currency'=>'string'], 'NumberFormatter::getAttribute' => ['int', 'attr'=>'int'], 'NumberFormatter::getErrorCode' => ['int'], 'NumberFormatter::getErrorMessage' => ['string'], @@ -7793,7 +8039,7 @@ 'NumberFormatter::getSymbol' => ['string', 'attr'=>'int'], 'NumberFormatter::getTextAttribute' => ['string', 'attr'=>'int'], 'NumberFormatter::parse' => ['float|false', 'str'=>'string', 'type='=>'int', '&rw_position='=>'int'], -'NumberFormatter::parseCurrency' => ['float', 'str'=>'string', '&w_currency'=>'string', '&rw_position='=>'int'], +'NumberFormatter::parseCurrency' => ['float|false', 'str'=>'string', '&w_currency'=>'string', '&rw_position='=>'int'], 'NumberFormatter::setAttribute' => ['bool', 'attr'=>'int', 'value'=>''], 'NumberFormatter::setPattern' => ['bool', 'pattern'=>'string'], 'NumberFormatter::setSymbol' => ['bool', 'attr'=>'int', 'symbol'=>'string'], @@ -7874,7 +8120,7 @@ 'ob_iconv_handler' => ['string', 'contents'=>'string', 'status'=>'int'], 'ob_implicit_flush' => ['void', 'flag='=>'int'], 'ob_inflatehandler' => ['string', 'data'=>'string', 'mode'=>'int'], -'ob_list_handlers' => ['false|array'], +'ob_list_handlers' => ['false|list'], 'ob_start' => ['bool', 'user_function='=>'string|array|callable|null', 'chunk_size='=>'int', 'flags='=>'int'], 'ob_tidyhandler' => ['string', 'input'=>'string', 'mode='=>'int'], 'OCI-Collection::append' => ['bool', 'value'=>'mixed'], @@ -7971,8 +8217,8 @@ 'oci_new_connect' => ['resource|false', 'user'=>'string', 'pass'=>'string', 'db='=>'string', 'charset='=>'string', 'session_mode='=>'int'], 'oci_new_cursor' => ['resource|false', 'connection'=>'resource'], 'oci_new_descriptor' => ['OCI-Lob|false', 'connection'=>'resource', 'type='=>'int'], -'oci_num_fields' => ['int|false', 'stmt'=>'resource'], -'oci_num_rows' => ['int|false', 'stmt'=>'resource'], +'oci_num_fields' => ['0|positive-int|false', 'stmt'=>'resource'], +'oci_num_rows' => ['0|positive-int|false', 'stmt'=>'resource'], 'oci_parse' => ['resource|false', 'connection'=>'resource', 'statement'=>'string'], 'oci_password_change' => ['bool', 'connection'=>'', 'username'=>'string', 'old_password'=>'string', 'new_password'=>'string'], 'oci_pconnect' => ['resource|false', 'user'=>'string', 'pass'=>'string', 'db='=>'string', 'charset='=>'string', 'session_mode='=>'int'], @@ -7992,7 +8238,7 @@ 'ocifetchinto' => ['int|false', 'stmt'=>'', '&w_output'=>'array', 'mode='=>'int'], 'ocigetbufferinglob' => ['bool'], 'ocisetbufferinglob' => ['bool', 'flag'=>'bool'], -'octdec' => ['int', 'octal_number'=>'string'], +'octdec' => ['int|float', 'octal_number'=>'string'], 'odbc_autocommit' => ['mixed', 'connection_id'=>'resource', 'onoff='=>'bool'], 'odbc_binmode' => ['bool', 'result_id'=>'int', 'mode'=>'int'], 'odbc_close' => ['void', 'connection_id'=>'resource'], @@ -8082,10 +8328,10 @@ 'openssl_encrypt' => ['string|false', 'data'=>'string', 'method'=>'string', 'key'=>'string', 'options='=>'int', 'iv='=>'string', '&w_tag='=>'string', 'aad='=>'string', 'tag_length='=>'int'], 'openssl_error_string' => ['string|false'], 'openssl_free_key' => ['void', 'key_identifier'=>'resource'], -'openssl_get_cert_locations' => ['array'], -'openssl_get_cipher_methods' => ['array', 'aliases='=>'bool'], -'openssl_get_curve_names' => ['array|false'], -'openssl_get_md_methods' => ['array', 'aliases='=>'bool'], +'openssl_get_cert_locations' => ['array'], +'openssl_get_cipher_methods' => ['list', 'aliases='=>'bool'], +'openssl_get_curve_names' => ['list|false'], +'openssl_get_md_methods' => ['list', 'aliases='=>'bool'], 'openssl_get_privatekey' => ['resource|false', 'key'=>'string', 'passphrase='=>'string'], 'openssl_get_publickey' => ['resource|false', 'cert'=>'resource|string'], 'openssl_open' => ['bool', 'sealed_data'=>'string', '&w_open_data'=>'string', 'env_key'=>'string', 'priv_key_id'=>'string|array|resource', 'method='=>'string', 'iv='=>'string'], @@ -8126,7 +8372,7 @@ 'openssl_x509_free' => ['void', 'x509'=>'resource'], 'openssl_x509_parse' => ['array|false', 'x509cert'=>'string|resource', 'shortnames='=>'bool'], 'openssl_x509_read' => ['resource|false', 'x509certdata'=>'string|resource'], -'ord' => ['int', 'character'=>'string'], +'ord' => ['int<0, 255>', 'character'=>'string'], 'OuterIterator::getInnerIterator' => ['Iterator'], 'OutOfBoundsException::__clone' => ['void'], 'OutOfBoundsException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?OutOfBoundsException)'], @@ -8136,7 +8382,7 @@ 'OutOfBoundsException::getLine' => ['int'], 'OutOfBoundsException::getMessage' => ['string'], 'OutOfBoundsException::getPrevious' => ['Throwable|OutOfBoundsException|null'], -'OutOfBoundsException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'OutOfBoundsException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'OutOfBoundsException::getTraceAsString' => ['string'], 'OutOfRangeException::__clone' => ['void'], 'OutOfRangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?OutOfRangeException)'], @@ -8146,7 +8392,7 @@ 'OutOfRangeException::getLine' => ['int'], 'OutOfRangeException::getMessage' => ['string'], 'OutOfRangeException::getPrevious' => ['Throwable|OutOfRangeException|null'], -'OutOfRangeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'OutOfRangeException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'OutOfRangeException::getTraceAsString' => ['string'], 'output_add_rewrite_var' => ['bool', 'name'=>'string', 'value'=>'string'], 'output_reset_rewrite_vars' => ['bool'], @@ -8158,7 +8404,7 @@ 'OverflowException::getLine' => ['int'], 'OverflowException::getMessage' => ['string'], 'OverflowException::getPrevious' => ['Throwable|OverflowException|null'], -'OverflowException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'OverflowException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'OverflowException::getTraceAsString' => ['string'], 'overload' => ['', 'class_name'=>'string'], 'override_function' => ['bool', 'function_name'=>'string', 'function_args'=>'string', 'function_code'=>'string'], @@ -8234,14 +8480,14 @@ 'ParseError::getLine' => ['int'], 'ParseError::getMessage' => ['string'], 'ParseError::getPrevious' => ['Throwable|ParseError|null'], -'ParseError::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'ParseError::getTrace' => ['list\',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'], 'parsekit_func_arginfo' => ['array', 'function'=>'mixed'], 'passthru' => ['void', 'command'=>'string', '&w_return_value='=>'int'], 'password_get_info' => ['array', 'hash'=>'string'], -'password_hash' => ['string|false', 'password'=>'string', 'algo'=>'int', 'options='=>'array'], +'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|int', 'options='=>'array'], 'password_make_salt' => ['bool', 'password'=>'string', 'hash'=>'string'], 'password_needs_rehash' => ['bool', 'hash'=>'string', 'algo'=>'int', 'options='=>'array'], 'password_verify' => ['bool', 'password'=>'string', 'hash'=>'string'], @@ -8427,7 +8673,7 @@ 'PDF_utf32_to_utf16' => ['string', 'pdfdoc'=>'resource', 'utf32string'=>'string', 'ordering'=>'string'], 'PDF_utf8_to_utf16' => ['string', 'pdfdoc'=>'resource', 'utf8string'=>'string', 'ordering'=>'string'], 'PDO::__construct' => ['void', 'dsn'=>'string', 'username='=>'?string', 'passwd='=>'?string', 'options='=>'?array'], -'PDO::__sleep' => ['array'], +'PDO::__sleep' => ['list'], 'PDO::__wakeup' => ['void'], 'PDO::beginTransaction' => ['bool'], 'PDO::commit' => ['bool'], @@ -8439,10 +8685,10 @@ 'PDO::getAvailableDrivers' => ['array'], 'PDO::inTransaction' => ['bool'], '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::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::pgsqlGetPid' => ['int'], 'PDO::pgsqlLOBCreate' => ['string'], @@ -8458,22 +8704,22 @@ 'PDO::setAttribute' => ['bool', 'attribute'=>'int', 'value'=>''], 'PDO::sqliteCreateAggregate' => ['bool', 'function_name'=>'string', 'step_func'=>'callable', 'finalize_func'=>'callable', 'num_args='=>'int'], 'PDO::sqliteCreateCollation' => ['bool', 'name'=>'string', 'callback'=>'callable'], -'PDO::sqliteCreateFunction' => ['bool', 'function_name'=>'string', 'callback'=>'callable', 'num_args='=>'int'], +'PDO::sqliteCreateFunction' => ['bool', 'function_name'=>'string', 'callback'=>'callable', 'num_args='=>'int', 'flags='=>'int'], 'pdo_drivers' => ['array'], 'PDOException::getCode' => [''], 'PDOException::getFile' => [''], 'PDOException::getLine' => [''], 'PDOException::getMessage' => [''], 'PDOException::getPrevious' => [''], -'PDOException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'PDOException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'PDOException::getTraceAsString' => [''], -'PDOStatement::__sleep' => ['array'], +'PDOStatement::__sleep' => ['list'], 'PDOStatement::__wakeup' => ['void'], 'PDOStatement::bindColumn' => ['bool', 'column'=>'mixed', '&w_param'=>'mixed', 'type='=>'int', 'maxlen='=>'int', 'driverdata='=>'mixed'], 'PDOStatement::bindParam' => ['bool', 'parameter'=>'mixed', '&w_variable'=>'mixed', 'data_type='=>'int', 'length='=>'int', 'driver_options='=>'mixed'], 'PDOStatement::bindValue' => ['bool', 'parameter'=>'mixed', 'value'=>'mixed', 'data_type='=>'int'], 'PDOStatement::closeCursor' => ['bool'], -'PDOStatement::columnCount' => ['int'], +'PDOStatement::columnCount' => ['0|positive-int'], 'PDOStatement::debugDumpParams' => ['void'], 'PDOStatement::errorCode' => ['string'], 'PDOStatement::errorInfo' => ['array'], @@ -8485,7 +8731,7 @@ 'PDOStatement::getAttribute' => ['mixed', 'attribute'=>'int'], 'PDOStatement::getColumnMeta' => ['array|false', 'column'=>'int'], 'PDOStatement::nextRowset' => ['bool'], -'PDOStatement::rowCount' => ['int'], +'PDOStatement::rowCount' => ['0|positive-int'], 'PDOStatement::setAttribute' => ['bool', 'attribute'=>'int', 'value'=>'mixed'], 'PDOStatement::setFetchMode' => ['bool', 'mode'=>'int'], 'PDOStatement::setFetchMode\'1' => ['bool', 'fetch_column'=>'int', 'colno'=>'int'], @@ -8518,15 +8764,15 @@ 'pg_escape_string\'1' => ['string', 'data'=>'string'], 'pg_execute' => ['resource|false', 'connection'=>'resource', 'stmtname'=>'string', 'params'=>'array'], 'pg_execute\'1' => ['resource|false', 'stmtname'=>'string', 'params'=>'array'], -'pg_fetch_all' => ['array|false', 'result'=>'resource', 'result_type='=>'int'], +'pg_fetch_all' => ['array>', 'result'=>'resource', 'result_type='=>'int'], 'pg_fetch_all_columns' => ['array|false', 'result'=>'resource', 'column_number='=>'int'], 'pg_fetch_array' => ['array|false', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], -'pg_fetch_assoc' => ['array|false', 'result'=>'resource', 'row='=>'?int'], +'pg_fetch_assoc' => ['non-empty-array|false', 'result'=>'resource', 'row='=>'?int'], 'pg_fetch_object' => ['object|false', 'result'=>'', 'row='=>'?int', 'result_type='=>'int'], 'pg_fetch_object\'1' => ['object', 'result'=>'', 'row='=>'?int', 'class_name='=>'string', 'ctor_params='=>'array'], 'pg_fetch_result' => ['', 'result'=>'', 'field_name'=>'string|int'], 'pg_fetch_result\'1' => ['', 'result'=>'', 'row_number'=>'int', 'field_name'=>'string|int'], -'pg_fetch_row' => ['array|false', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], +'pg_fetch_row' => ['non-empty-list|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|false', 'result'=>'resource', 'field_number'=>'int'], @@ -8567,7 +8813,7 @@ 'pg_options' => ['string', 'connection='=>'resource'], 'pg_parameter_status' => ['string|false', 'connection'=>'resource', 'param_name'=>'string'], 'pg_parameter_status\'1' => ['string|false', 'param_name'=>'string'], -'pg_pconnect' => ['resource|false', 'connection_string'=>'string', 'host='=>'string', 'port='=>'string|int', 'options='=>'string', 'tty='=>'string', 'database='=>'string'], +'pg_pconnect' => ['resource|false', 'connection_string'=>'string', 'connect_type='=>'int'], 'pg_ping' => ['bool', 'connection='=>'resource'], 'pg_port' => ['int', 'connection='=>'resource'], 'pg_prepare' => ['resource|false', 'connection'=>'resource', 'stmtname'=>'string', 'query'=>'string'], @@ -8716,10 +8962,10 @@ 'phdfs::tell' => ['int', 'path'=>'string'], 'phdfs::write' => ['bool', 'path'=>'string', 'buffer'=>'string', 'mode='=>'string'], 'php_check_syntax' => ['bool', 'filename'=>'string', 'error_message='=>'string'], -'php_ini_loaded_file' => ['string|false'], +'php_ini_loaded_file' => ['non-empty-string|false'], 'php_ini_scanned_files' => ['string|false'], 'php_logo_guid' => ['string'], -'php_sapi_name' => ['string|false'], +'php_sapi_name' => ['__benevolent'], 'php_strip_whitespace' => ['string', 'file_name'=>'string'], 'php_uname' => ['string', 'mode='=>'string'], 'php_user_filter::filter' => ['int', 'in'=>'resource', 'out'=>'resource', '&rw_consumed'=>'int', 'closing'=>'bool'], @@ -8794,16 +9040,16 @@ 'posix_getegid' => ['int'], 'posix_geteuid' => ['int'], 'posix_getgid' => ['int'], -'posix_getgrgid' => ['array|false', 'gid'=>'int'], -'posix_getgrnam' => ['array|false', 'groupname'=>'string'], -'posix_getgroups' => ['array|false'], +'posix_getgrgid' => ['array{name: string, passwd: string, gid: int, members: list}|false', 'gid'=>'int'], +'posix_getgrnam' => ['array{name: string, passwd: string, gid: int, members: list}|false', 'groupname'=>'string'], +'posix_getgroups' => ['list|false'], 'posix_getlogin' => ['string|false'], 'posix_getpgid' => ['int|false', 'pid'=>'int'], 'posix_getpgrp' => ['int'], 'posix_getpid' => ['int'], 'posix_getppid' => ['int'], 'posix_getpwnam' => ['array|false', 'groupname'=>'string'], -'posix_getpwuid' => ['array|false', 'uid'=>'int'], +'posix_getpwuid' => ['array{name: string, passwd: string, uid: int, gid: int, gecos: string, dir: string, shell: string}|false', 'uid'=>'int'], 'posix_getrlimit' => ['array|false'], 'posix_getsid' => ['int|false', 'pid'=>'int'], 'posix_getuid' => ['int'], @@ -8835,10 +9081,10 @@ 'preg_replace' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_split' => ['array|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], +'preg_split' => ['list|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], 'prev' => ['mixed', '&rw_array_arg'=>'array|object'], 'print_r' => ['string|true', 'var'=>'mixed', 'return='=>'bool'], -'printf' => ['int', 'format'=>'string', '...values='=>'string|int|float'], +'printf' => ['int', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], 'proc_close' => ['int', 'process'=>'resource'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}|false', 'process'=>'resource'], 'proc_nice' => ['bool', 'priority'=>'int'], @@ -9067,7 +9313,7 @@ 'RangeException::getLine' => ['int'], 'RangeException::getMessage' => ['string'], 'RangeException::getPrevious' => ['Throwable|RangeException|null'], -'RangeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'RangeException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'RangeException::getTraceAsString' => ['string'], 'rar_allow_broken_set' => ['bool', 'rarfile'=>'RarArchive', 'allow_broken'=>'bool'], 'rar_broken_is' => ['bool', 'rarfile'=>'RarArchive'], @@ -9110,14 +9356,14 @@ 'RarException::getLine' => ['int'], 'RarException::getMessage' => ['string'], 'RarException::getPrevious' => ['Exception|Throwable'], -'RarException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'RarException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'RarException::getTraceAsString' => ['string'], 'RarException::isUsingExceptions' => ['bool'], 'RarException::setUsingExceptions' => ['void', 'using_exceptions'=>'bool'], 'rawurldecode' => ['string', 'str'=>'string'], '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'], +'readdir' => ['non-empty-string|false', 'dir_handle='=>'resource'], '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'], @@ -9134,7 +9380,7 @@ 'readline_redisplay' => ['void'], 'readline_write_history' => ['bool', 'filename='=>'string'], 'readlink' => ['string|false', 'filename'=>'string'], -'realpath' => ['string|false', 'path'=>'string'], +'realpath' => ['non-empty-string|false', 'path'=>'string'], 'realpath_cache_get' => ['array'], 'realpath_cache_size' => ['int'], 'recode' => ['string', 'request'=>'string', 'str'=>'string'], @@ -9192,7 +9438,7 @@ 'RecursiveFilterIterator::hasChildren' => ['bool'], 'RecursiveIterator::getChildren' => ['RecursiveIterator'], 'RecursiveIterator::hasChildren' => ['bool'], -'RecursiveIteratorIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'mode='=>'int', 'flags='=>'int'], +'RecursiveIteratorIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'mode='=>'RecursiveIteratorIterator::LEAVES_ONLY|RecursiveIteratorIterator::SELF_FIRST|RecursiveIteratorIterator::CHILD_FIRST', 'flags='=>'0|RecursiveIteratorIterator::CATCH_GET_CHILD'], 'RecursiveIteratorIterator::beginChildren' => ['void'], 'RecursiveIteratorIterator::beginIteration' => ['RecursiveIterator'], 'RecursiveIteratorIterator::callGetChildren' => ['RecursiveIterator'], @@ -9231,229 +9477,276 @@ 'RecursiveTreeIterator::setPostfix' => ['void', 'prefix'=>'string'], 'RecursiveTreeIterator::setPrefixPart' => ['void', 'part'=>'int', 'prefix'=>'string'], 'RecursiveTreeIterator::valid' => ['bool'], -'Redis::__construct' => ['void'], -'Redis::_prefix' => ['string', 'value'=>'mixed'], -'Redis::_serialize' => ['mixed', 'value'=>'mixed'], -'Redis::_unserialize' => ['mixed', 'value'=>'string'], -'Redis::append' => ['int', 'key'=>'string', 'value'=>'string'], -'Redis::auth' => ['bool', 'password'=>'string|string[]'], -'Redis::bgRewriteAOF' => ['bool'], -'Redis::bgSave' => ['bool'], -'Redis::bitCount' => ['int', 'key'=>'string'], -'Redis::bitOp' => ['int', 'operation'=>'string', '...args'=>'string'], -'Redis::bitpos' => ['int', 'key'=>'string', 'bit'=>'int', 'start='=>'int', 'end='=>'int'], -'Redis::blPop' => ['array', 'keys'=>'string[]', 'timeout'=>'int'], +'Redis::__construct' => ['void', 'options='=>'?array{host?:string,port?:int,connectTimeout?:float,auth?:list{string|null|false,string}|list{string},ssl?:array,backoff?:array}'], +'Redis::_compress' => ['string', 'value'=>'string'], +'Redis::_uncompress' => ['string', 'value'=>'string'], +'Redis::_prefix' => ['string', 'key'=>'mixed'], +'Redis::_serialize' => ['mixed', 'value'=>'string'], +'Redis::_unserialize' => ['string', 'value'=>'mixed'], +'Redis::_pack' => ['mixed', 'value'=>'string'], +'Redis::_unpack' => ['string', 'value'=>'mixed'], +'Redis::acl' => ['mixed', 'subcmd'=>'string', '...args='=>'string'], +'Redis::append' => ['__benevolent', 'key'=>'string', 'value'=>'string'], +'Redis::auth' => ['__benevolent', 'credentials'=>'string|string[]'], +'Redis::bgrewriteaof' => ['__benevolent'], +'Redis::bgSave' => ['__benevolent'], +'Redis::bitcount' => ['__benevolent', 'key'=>'string', 'start='=>'int', 'end='=>'int', 'bybit='=>'bool'], +'Redis::bitop' => ['__benevolent', 'operation'=>'string', 'deskey'=>'string', 'srckey'=>'string', '...other_keys'=>'string'], +'Redis::bitpos' => ['__benevolent', 'key'=>'string', 'bit'=>'bool', 'start='=>'int', 'end='=>'int', 'bybit='=>'bool'], +'Redis::blmove' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'wherefrom'=>'string', 'whereto'=>'string', 'timeout'=>'float'], +'Redis::blmpop' => ['__benevolent', 'timeout'=>'float', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], +'Redis::blPop' => ['__benevolent', 'key_or_keys'=>'string|string[]', 'timeout_or_key'=>'string|float|int', '...extra_args'=>'mixed'], 'Redis::blPop\'1' => ['array', 'key'=>'string', 'timeout_or_key'=>'int|string', '...extra_args'=>'int|string'], -'Redis::brPop' => ['array', 'keys'=>'string[]', 'timeout'=>'int'], +'Redis::brPop' => ['__benevolent', 'key_or_keys'=>'string|string[]', 'timeout_or_key'=>'string|float|int', '...extra_args'=>'mixed'], 'Redis::brPop\'1' => ['array', 'key'=>'string', 'timeout_or_key'=>'int|string', '...extra_args'=>'int|string'], -'Redis::brpoplpush' => ['string|false', 'srcKey'=>'string', 'dstKey'=>'string', 'timeout'=>'int'], +'Redis::brpoplpush' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'timeout'=>'int|float'], +'Redis::bzPopMax' => ['__benevolent', 'key'=>'string|string[]', 'timeout_or_key'=>'string|int', '...extra_args'=>'mixed'], +'Redis::bzPopMin' => ['__benevolent', 'key'=>'string|string[]', 'timeout_or_key'=>'string|int', '...extra_args'=>'mixed'], +'Redis::bzmpop' => ['__benevolent', 'timeout'=>'float', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], 'Redis::clearLastError' => ['bool'], -'Redis::client' => ['mixed', 'command'=>'string', 'arg='=>'string'], +'Redis::clearTransferredBytes' => ['void'], +'Redis::client' => ['mixed', 'opt'=>'string', '...args='=>'mixed'], 'Redis::close' => ['bool'], -'Redis::config' => ['string', 'operation'=>'string', 'key'=>'string', 'value='=>'string'], -'Redis::connect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'reserved='=>'null', 'retry_interval='=>'?int', 'read_timeout='=>'float'], -'Redis::dbSize' => ['int'], -'Redis::decr' => ['int', 'key'=>'string'], -'Redis::decrBy' => ['int', 'key'=>'string', 'value'=>'int'], +'Redis::command' => ['mixed', 'opt='=>'?string', '...args'=>'mixed'], +'Redis::config' => ['mixed', 'operation'=>'string', 'key_or_settings='=>'array|string[]|string|null', 'value='=>'?string'], +'Redis::connect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::copy' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'options='=>'?array'], +'Redis::dbSize' => ['__benevolent'], +'Redis::debug' => ['__benevolent', 'key'=>'string'], +'Redis::decr' => ['__benevolent', 'key'=>'string', 'by='=>'int'], +'Redis::decrBy' => ['__benevolent', 'key'=>'string', 'value'=>'int'], 'Redis::decrByFloat' => ['float', 'key'=>'string', 'value'=>'float'], -'Redis::del' => ['int', 'key'=>'string', '...args'=>'string'], +'Redis::del' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], 'Redis::del\'1' => ['int', 'key'=>'string[]'], -'Redis::delete' => ['int', 'key'=>'string', '...args'=>'string'], +'Redis::delete' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], 'Redis::delete\'1' => ['int', 'key'=>'string[]'], -'Redis::discard' => [''], -'Redis::dump' => ['string|false', 'key'=>'string'], -'Redis::echo' => ['string', 'message'=>'string'], -'Redis::eval' => ['mixed', 'script'=>'', 'args='=>'', 'numKeys='=>''], -'Redis::evalSha' => ['mixed', 'scriptSha'=>'string', 'args='=>'array', 'numKeys='=>'int'], +'Redis::discard' => ['__benevolent'], +'Redis::dump' => ['__benevolent', 'key'=>'string'], +'Redis::echo' => ['__benevolent', 'str'=>'string'], +'Redis::eval' => ['mixed', 'script'=>'string', 'args='=>'array', 'num_keys='=>'int'], +'Redis::eval_ro' => ['mixed', 'script'=>'string', 'args='=>'array', 'num_keys='=>'int'], +'Redis::evalsha' => ['mixed', 'sha1'=>'string', 'args='=>'array', 'num_keys='=>'int'], +'Redis::evalsha_ro' => ['mixed', 'sha1'=>'string', 'args='=>'array', 'num_keys='=>'int'], 'Redis::evaluate' => ['mixed', 'script'=>'string', 'args='=>'array', 'numKeys='=>'int'], -'Redis::evaluateSha' => ['', 'scriptSha'=>'string', 'args='=>'array', 'numKeys='=>'int'], -'Redis::exec' => ['array'], -'Redis::exists' => ['int', 'keys'=>'string|string[]'], +'Redis::exec' => ['__benevolent'], +'Redis::exists' => ['__benevolent', 'keys'=>'string|string[]', '...other_keys='=>'string'], 'Redis::exists\'1' => ['int', '...keys'=>'string'], -'Redis::expire' => ['bool', 'key'=>'string', 'ttl'=>'int'], -'Redis::expireAt' => ['bool', 'key'=>'string', 'expiry'=>'int'], -'Redis::flushAll' => ['bool', 'async='=>'bool'], -'Redis::flushDb' => ['bool', 'async='=>'bool'], -'Redis::geoAdd' => ['int', 'key'=>'string', 'longitude'=>'float', 'latitude'=>'float', 'member'=>'string', '...other_triples='=>'string|int|float'], -'Redis::geoDist' => ['float', 'key'=>'string', 'member1'=>'string', 'member2'=>'string', 'unit='=>'string'], -'Redis::geoHash' => ['array', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], -'Redis::geoPos' => ['array', 'key'=>'string', 'member'=>'string', '...members'=>'string'], -'Redis::geoRadius' => ['array|int', 'key'=>'string', 'longitude'=>'float', 'latitude'=>'float', 'radius'=>'float', 'unit'=>'float', 'options='=>'array'], -'Redis::geoRadiusByMember' => ['array|int', 'key'=>'string', 'member'=>'string', 'radius'=>'float', 'units'=>'string', 'options='=>'array'], -'Redis::get' => ['string|false', 'key'=>'string'], +'Redis::expire' => ['__benevolent', 'key'=>'string', 'timeout'=>'int', 'mode='=>'?string'], +'Redis::expireAt' => ['__benevolent', 'key'=>'string', 'timeout'=>'int', 'mode='=>'?string'], +'Redis::expiretime' => ['__benevolent', 'key'=>'string'], +'Redis::failover' => ['__benevolent', 'to='=>'?array', 'abort='=>'bool', 'timeout='=>'int'], +'Redis::fcall' => ['mixed', 'fn'=>'string', 'keys='=>'string[]', 'args='=>'array'], +'Redis::fcall_ro' => ['mixed', 'fn'=>'string', 'keys='=>'string[]', 'args='=>'array'], +'Redis::flushAll' => ['__benevolent', 'sync='=>'?bool'], +'Redis::flushDb' => ['__benevolent', 'sync='=>'?bool'], +'Redis::function' => ['__benevolent', 'operation'=>'string', '...args='=>'mixed'], +'Redis::geoadd' => ['__benevolent', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'member'=>'string', '...other_triples_and_options='=>'mixed'], +'Redis::geodist' => ['__benevolent', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], +'Redis::geohash' => ['__benevolent|false>', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], +'Redis::geopos' => ['__benevolent|false>', 'key'=>'string', 'member'=>'string', '...other_members'=>'string'], +'Redis::georadius' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], +'Redis::georadiusbymember' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], +'Redis::georadiusbymember_ro' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], +'Redis::geosearch' => ['__benevolent>', 'key'=>'string', 'position'=>'array|string', 'shape'=>'array|int|float', 'unit'=>'string', 'options='=>'array'], +'Redis::geosearchstore' => ['__benevolent|int|false>', 'dst'=>'string', 'src'=>'string', 'position'=>'array|string', 'shape'=>'array|int|float', 'unit'=>'string', 'options='=>'array'], +'Redis::get' => ['mixed', 'key'=>'string'], 'Redis::getAuth' => ['string|false|null'], -'Redis::getBit' => ['int', 'key'=>'string', 'offset'=>'int'], +'Redis::getBit' => ['__benevolent', 'key'=>'string', 'idx'=>'int'], +'Redis::getEx' => ['__benevolent', 'key'=>'string', 'options'=>'?array{EX?:int,PX?:int,EXAT?:int,PXAT?:int,PERSIST?:bool}'], +'Redis::getDBNum' => ['int'], +'Redis::getDel' => ['__benevolent', 'key'=>'string'], +'Redis::getHost' => ['string'], 'Redis::getKeys' => ['array', 'pattern'=>'string'], -'Redis::getLastError' => ['null|string'], +'Redis::getLastError' => ['?string'], 'Redis::getMode' => ['int'], 'Redis::getMultiple' => ['array', 'keys'=>'string[]'], 'Redis::getOption' => ['int', 'name'=>'int'], -'Redis::getRange' => ['int', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::getSet' => ['string', 'key'=>'string', 'string'=>'string'], -'Redis::hDel' => ['int|false', 'key'=>'string', 'hashKey1'=>'string', '...otherHashKeys='=>'string'], -'Redis::hExists' => ['bool', 'key'=>'string', 'hashKey'=>'string'], -'Redis::hGet' => ['string|false', 'key'=>'string', 'hashKey'=>'string'], -'Redis::hGetAll' => ['array', 'key'=>'string'], -'Redis::hIncrBy' => ['int', 'key'=>'string', 'hashKey'=>'string', 'value'=>'int'], -'Redis::hIncrByFloat' => ['float', 'key'=>'string', 'field'=>'string', 'increment'=>'float'], -'Redis::hKeys' => ['array', 'key'=>'string'], -'Redis::hLen' => ['int|false', 'key'=>'string'], -'Redis::hMGet' => ['array', 'key'=>'string', 'hashKeys'=>'array'], -'Redis::hMSet' => ['bool', 'key'=>'string', 'hashKeys'=>'array'], -'Redis::hScan' => ['array', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], -'Redis::hSet' => ['int|false', 'key'=>'string', 'hashKey'=>'string', 'value'=>'string'], -'Redis::hSetNx' => ['bool', 'key'=>'string', 'hashKey'=>'string', 'value'=>'string'], -'Redis::hVals' => ['array', 'key'=>'string'], -'Redis::incr' => ['int', 'key'=>'string'], -'Redis::incrBy' => ['int', 'key'=>'string', 'value'=>'int'], -'Redis::incrByFloat' => ['float', 'key'=>'string', 'value'=>'float'], -'Redis::info' => ['array', 'option='=>'string'], -'Redis::keys' => ['array', 'pattern'=>'string'], +'Redis::getPersistentID' => ['?string'], +'Redis::getPort' => ['int'], +'Redis::getRange' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::getReadTimeout' => ['float'], +'Redis::getset' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::getTimeout' => ['float|false'], +'Redis::getTransferredBytes' => ['array'], +'Redis::hDel' => ['__benevolent', 'key'=>'string', 'field'=>'string', '...other_fields='=>'string'], +'Redis::hExists' => ['__benevolent', 'key'=>'string', 'field'=>'string'], +'Redis::hGet' => ['__benevolent', 'key'=>'string', 'member'=>'string'], +'Redis::hGetAll' => ['__benevolent', 'key'=>'string'], +'Redis::hIncrBy' => ['__benevolent', 'key'=>'string', 'field'=>'string', 'value'=>'int'], +'Redis::hIncrByFloat' => ['__benevolent', 'key'=>'string', 'field'=>'string', 'value'=>'float'], +'Redis::hKeys' => ['__benevolent', 'key'=>'string'], +'Redis::hLen' => ['__benevolent', 'key'=>'string'], +'Redis::hMget' => ['__benevolent|false>', 'key'=>'string', 'fields'=>'string[]'], +'Redis::hMset' => ['__benevolent', 'key'=>'string', 'fieldvals'=>'array'], +'Redis::hRandField' => ['__benevolent>', 'key'=>'string', 'options'=>'?array{COUNT?:int,WITHVALUES?:bool}'], +'Redis::hscan' => ['__benevolent|bool>', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'Redis::hSet' => ['__benevolent', 'key'=>'string', 'member'=>'string', 'value'=>'mixed'], +'Redis::hSetNx' => ['__benevolent', 'key'=>'string', 'field'=>'string', 'value'=>'string'], +'Redis::hStrLen' => ['__benevolent', 'key'=>'string', 'field'=>'string'], +'Redis::hVals' => ['__benevolent', 'key'=>'string'], +'Redis::incr' => ['__benevolent', 'key'=>'string', 'by='=>'int'], +'Redis::incrBy' => ['__benevolent', 'key'=>'string', 'value'=>'int'], +'Redis::incrByFloat' => ['__benevolent', 'key'=>'string', 'value'=>'float'], +'Redis::info' => ['__benevolent|false>', '...sections='=>'string'], +'Redis::isConnected' => ['bool'], +'Redis::keys' => ['__benevolent|false>', 'pattern'=>'string'], 'Redis::lastSave' => ['int'], +'Redis::lcs' => ['__benevolent', 'key1'=>'string', 'key2'=>'string', 'options'=>'?array{MINMATCHLEN?:int,WITHMATCHLEN?:bool,LEN?:bool,IDX?:bool}'], 'Redis::lGet' => ['', 'key'=>'string', 'index'=>'int'], 'Redis::lGetRange' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::lIndex' => ['string|false', 'key'=>'string', 'index'=>'int'], -'Redis::lInsert' => ['int', 'key'=>'string', 'position'=>'int', 'pivot'=>'string', 'value'=>'string'], +'Redis::lindex' => ['null|string|false', 'key'=>'string', 'index'=>'int'], +'Redis::lInsert' => ['__benevolent', 'key'=>'string', 'pos'=>'int', 'pivot'=>'mixed', 'value'=>'mixed'], 'Redis::listTrim' => ['', 'key'=>'string', 'start'=>'int', 'stop'=>'int'], -'Redis::lLen' => ['int|false', 'key'=>'string'], -'Redis::lPop' => ['string', 'key'=>'string'], -'Redis::lPush' => ['int|false', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::lPushx' => ['int|false', 'key'=>'string', 'value'=>'string'], -'Redis::lRange' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::lRem' => ['int|false', 'key'=>'string', 'value'=>'string', 'count'=>'int'], -'Redis::lRemove' => ['', 'key'=>'string', 'value'=>'string', 'count'=>'int'], -'Redis::lSet' => ['bool', 'key'=>'string', 'index'=>'int', 'value'=>'string'], +'Redis::lLen' => ['__benevolent', 'key'=>'string'], +'Redis::lMove' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'wherefrom'=>'string', 'whereto'=>'string'], +'Redis::lmpop' => ['__benevolent|null|false>', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], +'Redis::lPop' => ['__benevolent', 'key'=>'string', 'count='=>'int'], +'Redis::lPos' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', 'options'=>'?array{COUNT?:int,RANK?:int,MAXLEN?:int}'], +'Redis::lPush' => ['__benevolent', 'key'=>'string', '...elements='=>'mixed'], +'Redis::lPushx' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::lrange' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::lrem' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', 'count='=>'int'], +'Redis::lSet' => ['__benevolent', 'key'=>'string', 'index'=>'int', 'value'=>'mixed'], 'Redis::lSize' => ['', 'key'=>'string'], -'Redis::lTrim' => ['array|false', 'key'=>'string', 'start'=>'int', 'stop'=>'int'], -'Redis::mGet' => ['array', 'keys'=>'string[]'], -'Redis::migrate' => ['bool', 'host'=>'string', 'port'=>'int', 'key'=>'string|string[]', 'db'=>'int', 'timeout'=>'int', 'copy='=>'bool', 'replace='=>'bool'], -'Redis::move' => ['bool', 'key'=>'string', 'dbindex'=>'int'], -'Redis::mSet' => ['bool', 'pairs'=>'array'], -'Redis::mSetNx' => ['bool', 'pairs'=>'array'], -'Redis::multi' => ['Redis', 'mode='=>'int'], -'Redis::object' => ['string|long|false', 'info'=>'string', 'key'=>'string'], -'Redis::open' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'reserved='=>'null', 'retry_interval='=>'?int', 'read_timeout='=>'float'], -'Redis::pconnect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'string', 'retry_interval='=>'?int'], -'Redis::persist' => ['bool', 'key'=>'string'], -'Redis::pExpire' => ['bool', 'key'=>'string', 'ttl'=>'int'], -'Redis::pexpireAt' => ['bool', 'key'=>'string', 'expiry'=>'int'], -'Redis::pfAdd' => ['bool', 'key'=>'string', 'elements'=>'array'], -'Redis::pfCount' => ['int', 'key'=>'array|string'], -'Redis::pfMerge' => ['bool', 'destkey'=>'string', 'sourcekeys'=>'array'], -'Redis::ping' => ['string'], -'Redis::popen' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'string', 'retry_interval='=>'?int'], -'Redis::psetex' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::psubscribe' => ['', 'patterns'=>'array', 'callback'=>'array|string'], -'Redis::pttl' => ['int|false', 'key'=>'string'], -'Redis::publish' => ['int', 'channel'=>'string', 'message'=>'string'], -'Redis::pubsub' => ['array|int', 'keyword'=>'string', 'argument'=>'array|string'], -'Redis::punsubscribe' => ['', 'pattern'=>'string', '...other_patterns='=>'string'], -'Redis::randomKey' => ['string'], -'Redis::rawCommand' => ['mixed', 'command'=>'string', '...arguments='=>'mixed'], -'Redis::rename' => ['bool', 'srckey'=>'string', 'dstkey'=>'string'], +'Redis::ltrim' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::mget' => ['__benevolent>', 'keys'=>'string[]'], +'Redis::migrate' => ['__benevolent', 'host'=>'string', 'port'=>'int', 'key'=>'string|string[]', 'dstdb'=>'int', 'timeout'=>'int', 'copy='=>'bool', 'replace='=>'bool', 'credentials='=>'mixed'], +'Redis::move' => ['__benevolent', 'key'=>'string', 'index'=>'int'], +'Redis::mset' => ['__benevolent', 'key_values'=>'array'], +'Redis::msetnx' => ['__benevolent', 'key_values'=>'array'], +'Redis::multi' => ['__benevolent', 'value='=>'int'], +'Redis::object' => ['__benevolent', 'subcommand'=>'string', 'key'=>'string'], +'Redis::open' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::pconnect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::persist' => ['__benevolent', 'key'=>'string'], +'Redis::pexpire' => ['bool', 'key'=>'string', 'timeout'=>'int', 'mode='=>'?string'], +'Redis::pexpireAt' => ['__benevolent', 'key'=>'string', 'timestamp'=>'int', 'mode='=>'?string'], +'Redis::pexpiretime' => ['__benevolent', 'key'=>'string'], +'Redis::pfadd' => ['__benevolent', 'key'=>'string', 'elements'=>'array'], +'Redis::pfcount' => ['__benevolent', 'key_or_keys'=>'string[]|string'], +'Redis::pfmerge' => ['__benevolent', 'dst'=>'string', 'srckeys'=>'string[]'], +'Redis::ping' => ['__benevolent', 'message='=>'?string'], +'Redis::pipeline' => ['__benevolent'], +'Redis::popen' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::psetex' => ['__benevolent', 'key'=>'string', 'expire'=>'int', 'value'=>'mixed'], +'Redis::psubscribe' => ['bool', 'patterns'=>'string[]', 'cb'=>'callable'], +'Redis::pttl' => ['__benevolent', 'key'=>'string'], +'Redis::publish' => ['__benevolent', 'channel'=>'string', 'message'=>'string'], +'Redis::pubsub' => ['array|int', 'command'=>'string', 'arg'=>'array|string'], +'Redis::punsubscribe' => ['__benevolent', 'patterns='=>'string[]'], +'Redis::randomKey' => ['__benevolent'], +'Redis::rawcommand' => ['mixed', 'command'=>'string', '...args='=>'mixed'], +'Redis::rename' => ['__benevolent', 'old_name'=>'string', 'new_name'=>'string'], 'Redis::renameKey' => ['bool', 'srckey'=>'string', 'dstkey'=>'string'], -'Redis::renameNx' => ['bool', 'srckey'=>'string', 'dstkey'=>'string'], +'Redis::renameNx' => ['__benevolent', 'old_name'=>'string', 'new_name'=>'string'], +'Redis::reset' => ['__benevolent'], 'Redis::resetStat' => ['bool'], -'Redis::restore' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::rPop' => ['string', 'key'=>'string'], -'Redis::rpoplpush' => ['string', 'srcKey'=>'string', 'dstKey'=>'string'], -'Redis::rPush' => ['int|false', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::rPushx' => ['int|false', 'key'=>'string', 'value'=>'string'], -'Redis::sAdd' => ['int|false', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::sAddArray' => ['bool', 'key'=>'string', 'values'=>'array'], -'Redis::save' => ['bool'], -'Redis::scan' => ['array|false', '&w_iterator'=>'?int', 'pattern='=>'?string', 'count='=>'?int'], -'Redis::sCard' => ['int', 'key'=>'string'], +'Redis::restore' => ['__benevolent', 'key'=>'string', 'ttl'=>'int', 'value'=>'string', 'options='=>'?array{ABSTTL?:bool,REPLACE?:bool,IDLETIME?:int,FREQ?:int}'], +'Redis::role' => ['mixed'], +'Redis::rPop' => ['__benevolent|string|bool>', 'key'=>'string', 'count='=>'int'], +'Redis::rpoplpush' => ['__benevolent', 'srckey'=>'string', 'dstkey'=>'string'], +'Redis::rPush' => ['__benevolent', 'key'=>'string', '...elements='=>'mixed'], +'Redis::rPushx' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::sAdd' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', '...other_values='=>'string'], +'Redis::sAddArray' => ['int', 'key'=>'string', 'values'=>'array'], +'Redis::save' => ['__benevolent'], +'Redis::scan' => ['array|false', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'?int', 'type='=>'?string'], +'Redis::scard' => ['__benevolent', 'key'=>'string'], 'Redis::sContains' => ['', 'key'=>'string', 'value'=>'string'], 'Redis::script' => ['mixed', 'command'=>'string', '...args='=>'mixed'], -'Redis::sDiff' => ['array', 'key1'=>'string', '...other_keys='=>'string'], -'Redis::sDiffStore' => ['int|false', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], -'Redis::select' => ['bool', 'dbindex'=>'int'], -'Redis::set' => ['bool', 'key'=>'string', 'value'=>'mixed', 'options='=>'array'], +'Redis::sDiff' => ['__benevolent|false>', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sDiffStore' => ['__benevolent', 'dst'=>'string', 'key'=>'string', '...other_keys='=>'string'], +'Redis::select' => ['__benevolent', 'db'=>'int'], +'Redis::set' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', 'options='=>'array'], 'Redis::set\'1' => ['bool', 'key'=>'string', 'value'=>'mixed', 'timeout='=>'int'], -'Redis::setBit' => ['int', 'key'=>'string', 'offset'=>'int', 'value'=>'int'], -'Redis::setEx' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::setex' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::setNx' => ['bool', 'key'=>'string', 'value'=>'string'], -'Redis::setnx' => ['bool', 'key'=>'string', 'value'=>'string'], -'Redis::setOption' => ['bool', 'name'=>'int', 'value'=>'mixed'], -'Redis::setRange' => ['int', 'key'=>'string', 'offset'=>'int', 'end'=>'string'], +'Redis::setBit' => ['__benevolent', 'key'=>'string', 'idx'=>'int', 'value'=>'bool'], +'Redis::setex' => ['__benevolent', 'key'=>'string', 'expire'=>'int', 'value'=>'mixed'], +'Redis::setnx' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::setOption' => ['bool', 'option'=>'int', 'value'=>'mixed'], +'Redis::setRange' => ['__benevolent', 'key'=>'string', 'index'=>'int', 'value'=>'string'], 'Redis::setTimeout' => ['bool', 'key'=>'string', 'ttl'=>'int'], 'Redis::sGetMembers' => ['array', 'key'=>'string'], -'Redis::sInter' => ['array|false', 'key'=>'string', '...other_keys='=>'string'], -'Redis::sInterStore' => ['int|false', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], -'Redis::sIsMember' => ['bool', 'key'=>'string', 'value'=>'string'], +'Redis::sInter' => ['__benevolent|false>', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sintercard' => ['__benevolent', 'keys'=>'string[]', 'limit='=>'int'], +'Redis::sInterStore' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], +'Redis::sismember' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], 'Redis::slave' => ['bool', 'host'=>'string', 'port'=>'int'], 'Redis::slave\'1' => ['bool', 'host'=>'string', 'port'=>'int'], -'Redis::slaveof' => ['bool', 'host='=>'string', 'port='=>'int'], -'Redis::slowLog' => ['mixed', 'operation'=>'string', 'length='=>'int'], -'Redis::sMembers' => ['array', 'key'=>'string'], -'Redis::sMove' => ['bool', 'srcKey'=>'string', 'dstKey'=>'string', 'member'=>'string'], -'Redis::sort' => ['array|int', 'key'=>'string', 'options='=>'array'], -'Redis::sPop' => ['string|false', 'key'=>'string'], -'Redis::sRandMember' => ['array|string|false', 'key'=>'string', 'count='=>'int'], -'Redis::sRem' => ['int', 'key'=>'string', 'member1'=>'string', '...other_members='=>'string'], +'Redis::slaveof' => ['__benevolent', 'host='=>'?string', 'port='=>'int'], +'Redis::slowlog' => ['mixed', 'operation'=>'string', 'length='=>'int'], +'Redis::sMembers' => ['__benevolent|false>', 'key'=>'string'], +'Redis::sMisMember' => ['__benevolent', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], +'Redis::sMove' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'value'=>'mixed'], +'Redis::sort' => ['array|int', 'key'=>'string', 'options='=>'?array{SORT?:string,ALPHA?:bool,LIMIT?:array{0:int,1:int},BY?:string,GET?:string}'], +'Redis::sort_ro' => ['array|int', 'key'=>'string', 'options='=>'?array{SORT?:string,ALPHA?:bool,LIMIT?:array{0:int,1:int},BY?:string,GET?:string}'], +'Redis::sPop' => ['__benevolent', 'key'=>'string', 'count='=>'int'], +'Redis::sRandMember' => ['__benevolent', 'key'=>'string', 'count='=>'int'], +'Redis::srem' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', '...other_values='=>'mixed'], 'Redis::sRemove' => ['int', 'key'=>'string', 'member1'=>'string', '...other_members='=>'string'], -'Redis::sScan' => ['array|bool', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], -'Redis::strLen' => ['0|positive-int', 'key'=>'string'], -'Redis::subscribe' => ['mixed|null', 'channels'=>'array', 'callback'=>'string|array'], +'Redis::sscan' => ['__benevolent', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'Redis::ssubscribe' => ['bool', 'channels'=>'string[]', 'cb'=>'callable'], +'Redis::strlen' => ['__benevolent', 'key'=>'string'], +'Redis::subscribe' => ['bool', 'channels'=>'string[]', 'cb'=>'callable'], 'Redis::substr' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::sUnion' => ['array', 'key'=>'string', '...other_keys='=>'string'], -'Redis::sUnionStore' => ['int', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], -'Redis::time' => ['array'], -'Redis::ttl' => ['int|false', 'key'=>'string'], -'Redis::type' => ['int', 'key'=>'string'], -'Redis::unlink' => ['int', 'key'=>'string', '...args'=>'string'], 'Redis::unlink\'1' => ['int', 'key'=>'string[]'], -'Redis::unsubscribe' => ['', 'channel'=>'string', '...other_channels='=>'string'], -'Redis::unwatch' => [''], -'Redis::wait' => ['int', 'numSlaves'=>'int', 'timeout'=>'int'], -'Redis::watch' => ['void', 'key'=>'string', '...other_keys='=>'string'], -'Redis::xack' => ['', 'str_key'=>'string', 'str_group'=>'string', 'arr_ids'=>'array'], -'Redis::xadd' => ['', 'str_key'=>'string', 'str_id'=>'string', 'arr_fields'=>'array', 'i_maxlen='=>'', 'boo_approximate='=>''], -'Redis::xclaim' => ['', 'str_key'=>'string', 'str_group'=>'string', 'str_consumer'=>'string', 'i_min_idle'=>'', 'arr_ids'=>'array', 'arr_opts='=>'array'], -'Redis::xdel' => ['', 'str_key'=>'string', 'arr_ids'=>'array'], -'Redis::xgroup' => ['', 'str_operation'=>'string', 'str_key='=>'string', 'str_arg1='=>'', 'str_arg2='=>'', 'str_arg3='=>''], -'Redis::xinfo' => ['', 'str_cmd'=>'string', 'str_key='=>'string', 'str_group='=>'string'], -'Redis::xlen' => ['', 'key'=>''], -'Redis::xpending' => ['', 'str_key'=>'string', 'str_group'=>'string', 'str_start='=>'', 'str_end='=>'', 'i_count='=>'', 'str_consumer='=>'string'], -'Redis::xrange' => ['', 'str_key'=>'string', 'str_start'=>'', 'str_end'=>'', 'i_count='=>''], -'Redis::xread' => ['', 'arr_streams'=>'array', 'i_count='=>'', 'i_block='=>''], -'Redis::xreadgroup' => ['', 'str_group'=>'string', 'str_consumer'=>'string', 'arr_streams'=>'array', 'i_count='=>'', 'i_block='=>''], -'Redis::xrevrange' => ['', 'str_key'=>'string', 'str_start'=>'', 'str_end'=>'', 'i_count='=>''], -'Redis::xtrim' => ['', 'str_key'=>'string', 'i_maxlen'=>'', 'boo_approximate='=>''], -'Redis::zAdd' => ['int', 'key'=>'string', 'score1'=>'float', 'value1'=>'string', 'score2='=>'float', 'value2='=>'string', 'scoreN='=>'float', 'valueN='=>'string'], -'Redis::zAdd\'1' => ['int', 'options'=>'array', 'key'=>'string', 'score1'=>'float', 'value1'=>'string', 'score2='=>'float', 'value2='=>'string', 'scoreN='=>'float', 'valueN='=>'string'], -'Redis::zCard' => ['int', 'key'=>'string'], -'Redis::zCount' => ['int', 'key'=>'string', 'start'=>'string', 'end'=>'string'], +'Redis::sUnion' => ['__benevolent|false>', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sUnionStore' => ['__benevolent', 'dst'=>'string', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sunsubscribe' => ['__benevolent', 'channels'=>'string[]'], +'Redis::swapdb' => ['__benevolent', 'src'=>'int', 'dst'=>'int'], +'Redis::time' => ['__benevolent'], +'Redis::ttl' => ['__benevolent', 'key'=>'string'], +'Redis::type' => ['__benevolent', 'key'=>'string'], +'Redis::unlink' => ['__benevolent', 'key'=>'string[]|string', '...other_keys'=>'string'], +'Redis::unsubscribe' => ['__benevolent', 'channels'=>'string[]'], +'Redis::unwatch' => ['__benevolent'], +'Redis::wait' => ['int|false', 'numreplicas'=>'int', 'timeout'=>'int'], +'Redis::watch' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], +'Redis::xack' => ['int|false', 'key'=>'string', 'group'=>'string', 'ids'=>'array'], +'Redis::xadd' => ['__benevolent', 'key'=>'string', 'id'=>'string', 'values'=>'array', 'maxlen='=>'int', 'approx='=>'bool', 'nomkstream='=>'bool'], +'Redis::xclaim' => ['__benevolent', 'key'=>'string', 'group'=>'string', 'consumer'=>'string', 'min_idle'=>'int', 'ids'=>'array', 'options='=>'array'], +'Redis::xdel' => ['__benevolent', 'key'=>'string', 'ids'=>'array'], +'Redis::xgroup' => ['mixed', 'operation'=>'string', 'key='=>'?string', 'group='=>'?string', 'id_or_consumer='=>'?string', 'mkstream='=>'bool', 'entries_read='=>'int'], +'Redis::xinfo' => ['mixed', 'operation'=>'string', 'arg1='=>'?string', 'arg2='=>'?string', 'count='=>'int'], +'Redis::xlen' => ['__benevolent', 'key'=>'string'], +'Redis::xpending' => ['__benevolent', 'key'=>'string', 'group'=>'string', 'start='=>'?string', 'end='=>'?string', 'count='=>'int', 'consumer='=>'?string'], +'Redis::xrange' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string', 'count='=>'int'], +'Redis::xread' => ['__benevolent', 'streams'=>'array', 'count='=>'int', 'block='=>'int'], +'Redis::xreadgroup' => ['__benevolent', 'group'=>'string', 'consumer'=>'string', 'streams'=>'array', 'count='=>'int', 'block='=>'int'], +'Redis::xrevrange' => ['__benevolent', 'key'=>'string', 'end'=>'string', 'start'=>'string', 'count='=>'int'], +'Redis::xtrim' => ['__benevolent', 'key'=>'string', 'threshold'=>'string', 'approx='=>'bool', 'minid='=>'bool', 'limit='=>'int'], +'Redis::zAdd' => ['__benevolent', 'key'=>'string', 'score_or_options'=>'array|float', '...more_scores_and_mems='=>'mixed'], +'Redis::zAdd\'1' => ['int', 'key'=>'string', 'options'=>'array', 'score1'=>'float', 'value1'=>'string', 'score2='=>'float', 'value2='=>'string', 'scoreN='=>'float', 'valueN='=>'string'], +'Redis::zCard' => ['__benevolent', 'key'=>'string'], +'Redis::zCount' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string'], 'Redis::zDelete' => ['int', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], 'Redis::zDeleteRangeByRank' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], 'Redis::zDeleteRangeByScore' => ['', 'key'=>'string', 'start'=>'float', 'end'=>'float'], -'Redis::zIncrBy' => ['float', 'key'=>'string', 'value'=>'float', 'member'=>'string'], -'Redis::zInter' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], -'Redis::zRange' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'withscores='=>'bool'], -'Redis::zRangeByLex' => ['array|false', 'key'=>'string', 'min'=>'int', 'max'=>'int', 'offset='=>'int', 'limit='=>'int'], -'Redis::zRangeByScore' => ['array', 'key'=>'string', 'start'=>'int|string', 'end'=>'int|string', 'options='=>'array'], -'Redis::zRank' => ['int', 'key'=>'string', 'member'=>'string'], -'Redis::zRem' => ['int', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], +'Redis::zIncrBy' => ['__benevolent', 'key'=>'string', 'value'=>'float', 'member'=>'mixed'], +'Redis::zInter' => ['__benevolent', 'keys'=>'string[]', 'weights='=>'?array', 'options='=>'?array'], +'Redis::zmpop' => ['__benevolent', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], +'Redis::zRange' => ['__benevolent', 'key'=>'string', 'start'=>'string|int', 'end'=>'string|int', 'options='=>'array|bool|null'], +'Redis::zRangeByLex' => ['__benevolent', 'key'=>'string', 'min'=>'string', 'max'=>'string', 'offset='=>'int', 'limit='=>'int'], +'Redis::zRangeByScore' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string', 'options='=>'array'], +'Redis::zRank' => ['__benevolent', 'key'=>'string', 'member'=>'mixed'], +'Redis::zRem' => ['__benevolent', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], 'Redis::zRemove' => ['int', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], -'Redis::zRemRangeByRank' => ['int', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::zRemRangeByScore' => ['int', 'key'=>'string', 'start'=>'float|string', 'end'=>'float|string'], -'Redis::zRevRange' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'withscore='=>'bool'], -'Redis::zRevRangeByLex' => ['array', 'key'=>'string', 'min'=>'int', 'max'=>'int', 'offset='=>'int', 'limit='=>'int'], -'Redis::zRevRangeByScore' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'options='=>'array'], -'Redis::zRevRank' => ['int', 'key'=>'string', 'member'=>'string'], -'Redis::zScan' => ['array|bool', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], -'Redis::zScore' => ['float|false', 'key'=>'string', 'member'=>'string'], +'Redis::zRemRangeByRank' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::zRemRangeByScore' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string'], +'Redis::zRevRange' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'scores='=>'bool|array{withscores:bool}|null'], +'Redis::zRevRangeByLex' => ['__benevolent', 'key'=>'string', 'max'=>'string', 'min'=>'string', 'offset='=>'int', 'limit='=>'int'], +'Redis::zRevRangeByScore' => ['__benevolent', 'key'=>'string', 'max'=>'string', 'min'=>'string', 'options='=>'array|bool'], +'Redis::zRevRank' => ['__benevolent', 'key'=>'string', 'member'=>'mixed'], +'Redis::zscan' => ['__benevolent', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'Redis::zScore' => ['__benevolent', 'key'=>'string', 'member'=>'mixed'], 'Redis::zSize' => ['', 'key'=>'string'], -'Redis::zUnion' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], +'Redis::zUnion' => ['__benevolent', 'keys'=>'string[]', 'weights'=>'?array', 'options='=>'?array'], 'RedisArray::__construct' => ['void', 'name'=>'string'], 'RedisArray::__construct\'1' => ['void', 'hosts'=>'array', 'opts='=>'array'], 'RedisArray::_function' => ['string'], 'RedisArray::_hosts' => ['array'], 'RedisArray::_rehash' => ['', 'callable='=>'callable'], 'RedisArray::_target' => ['string', 'key'=>'string'], -'RedisCluster::__construct' => ['void', 'name'=>'string|null', 'seeds'=>'array', 'timeout='=>'float', 'readTimeout='=>'float', 'persistent='=>'bool|false', 'auth='=>'string|array|null'], +'RedisCluster::__construct' => ['void', 'name'=>'string|null', 'seeds='=>'string[]|null', 'timeout='=>'int|float', 'read_timeout='=>'int|float', 'persistent='=>'bool', 'auth='=>'mixed', 'context='=>'array|null'], 'RedisCluster::_masters' => ['array'], 'RedisCluster::_prefix' => ['string', 'value'=>'mixed'], 'RedisCluster::_serialize' => ['mixed', 'value'=>'mixed'], @@ -9476,7 +9769,7 @@ 'RedisCluster::dbSize' => ['int', 'nodeParams'=>'string'], 'RedisCluster::decr' => ['int', 'key'=>'string'], 'RedisCluster::decrBy' => ['int', 'key'=>'string', 'value'=>'int'], -'RedisCluster::del' => ['int', 'key1'=>'int', 'key2='=>'string', 'key3='=>'string'], +'RedisCluster::del' => ['int', 'key1'=>'int|string', 'key2='=>'int|string', 'key3='=>'int|string'], 'RedisCluster::discard' => [''], 'RedisCluster::dump' => ['string', 'key'=>'string'], 'RedisCluster::echo' => ['mixed', 'nodeParams'=>'string', 'msg'=>'string'], @@ -9498,7 +9791,7 @@ 'RedisCluster::getBit' => ['int', 'key'=>'string', 'offset'=>'int'], 'RedisCluster::getLastError' => ['string'], 'RedisCluster::getMode' => ['int'], -'RedisCluster::getOption' => ['int', 'name'=>'string'], +'RedisCluster::getOption' => ['int', 'name'=>'int'], 'RedisCluster::getRange' => ['string', 'key'=>'string', 'start'=>'int', 'end'=>'int'], 'RedisCluster::getSet' => ['string', 'key'=>'string', 'value'=>'string'], 'RedisCluster::hDel' => ['int', 'key'=>'string', 'hashKey1'=>'string', 'hashKey2='=>'string', 'hashKeyN='=>'string'], @@ -9566,19 +9859,19 @@ 'RedisCluster::scan' => ['array', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], 'RedisCluster::sCard' => ['int', 'key'=>'string'], 'RedisCluster::script' => ['mixed', 'nodeParams'=>'string', 'command'=>'string', 'script'=>'string'], -'RedisCluster::sDiff' => ['array', 'key1'=>'string', 'key2'=>'string', '...other_keys='=>'string'], +'RedisCluster::sDiff' => ['list', 'key1'=>'string', 'key2'=>'string', '...other_keys='=>'string'], 'RedisCluster::sDiffStore' => ['int', 'dstKey'=>'string', 'key1'=>'string', '...other_keys='=>'string'], 'RedisCluster::set' => ['bool', 'key'=>'string', 'value'=>'string', 'timeout='=>'array|int'], 'RedisCluster::setBit' => ['int', 'key'=>'string', 'offset'=>'int', 'value'=>'bool|int'], 'RedisCluster::setex' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], 'RedisCluster::setnx' => ['bool', 'key'=>'string', 'value'=>'string'], -'RedisCluster::setOption' => ['bool', 'name'=>'string', 'value'=>'string'], +'RedisCluster::setOption' => ['bool', 'name'=>'int', 'value'=>'mixed'], 'RedisCluster::setRange' => ['string', 'key'=>'string', 'offset'=>'int', 'value'=>'string'], -'RedisCluster::sInter' => ['array', 'key'=>'string', '...other_keys='=>'string'], +'RedisCluster::sInter' => ['list', 'key'=>'string', '...other_keys='=>'string'], 'RedisCluster::sInterStore' => ['int', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], 'RedisCluster::sIsMember' => ['bool', 'key'=>'string', 'value'=>'string'], 'RedisCluster::slowLog' => ['', 'nodeParams'=>'string', 'command'=>'string', 'argument'=>'mixed', '...other_arguments='=>'mixed'], -'RedisCluster::sMembers' => ['array', 'key'=>'string'], +'RedisCluster::sMembers' => ['list', 'key'=>'string'], 'RedisCluster::sMove' => ['bool', 'srcKey'=>'string', 'dstKey'=>'string', 'member'=>'string'], 'RedisCluster::sort' => ['array', 'key'=>'string', 'option='=>'array'], 'RedisCluster::sPop' => ['string', 'key'=>'string'], @@ -9641,28 +9934,28 @@ 'ReflectionClass::getConstructor' => ['ReflectionMethod|null'], 'ReflectionClass::getDefaultProperties' => ['array'], 'ReflectionClass::getDocComment' => ['string|false'], -'ReflectionClass::getEndLine' => ['int|false'], +'ReflectionClass::getEndLine' => ['positive-int|false'], 'ReflectionClass::getExtension' => ['ReflectionExtension|null'], 'ReflectionClass::getExtensionName' => ['string|false'], -'ReflectionClass::getFileName' => ['string|false'], -'ReflectionClass::getInterfaceNames' => ['array'], +'ReflectionClass::getFileName' => ['non-empty-string|false'], +'ReflectionClass::getInterfaceNames' => ['list'], 'ReflectionClass::getInterfaces' => ['array'], 'ReflectionClass::getMethod' => ['ReflectionMethod', 'name'=>'string'], -'ReflectionClass::getMethods' => ['array', 'filter='=>'int'], +'ReflectionClass::getMethods' => ['list', 'filter='=>'int'], 'ReflectionClass::getModifiers' => ['int'], 'ReflectionClass::getName' => ['class-string'], 'ReflectionClass::getNamespaceName' => ['string'], 'ReflectionClass::getParentClass' => ['ReflectionClass|false'], -'ReflectionClass::getProperties' => ['array', 'filter='=>'int'], +'ReflectionClass::getProperties' => ['list', 'filter='=>'int'], 'ReflectionClass::getProperty' => ['ReflectionProperty', 'name'=>'string'], 'ReflectionClass::getReflectionConstant' => ['ReflectionClassConstant|false', 'name'=>'string'], -'ReflectionClass::getReflectionConstants' => ['array'], +'ReflectionClass::getReflectionConstants' => ['list'], 'ReflectionClass::getShortName' => ['string'], -'ReflectionClass::getStartLine' => ['int|false'], +'ReflectionClass::getStartLine' => ['positive-int|false'], 'ReflectionClass::getStaticProperties' => ['array'], 'ReflectionClass::getStaticPropertyValue' => ['mixed', 'name'=>'string', 'default='=>'mixed'], 'ReflectionClass::getTraitAliases' => ['array'], -'ReflectionClass::getTraitNames' => ['array'], +'ReflectionClass::getTraitNames' => ['list'], 'ReflectionClass::getTraits' => ['array'], 'ReflectionClass::hasConstant' => ['bool', 'name'=>'string'], 'ReflectionClass::hasMethod' => ['bool', 'name'=>'string'], @@ -9702,7 +9995,7 @@ 'ReflectionExtension::__toString' => ['string'], 'ReflectionExtension::export' => ['string|null', 'name'=>'string', 'return='=>'bool'], 'ReflectionExtension::getClasses' => ['array'], -'ReflectionExtension::getClassNames' => ['array'], +'ReflectionExtension::getClassNames' => ['list'], 'ReflectionExtension::getConstants' => ['array'], 'ReflectionExtension::getDependencies' => ['array'], 'ReflectionExtension::getFunctions' => ['array'], @@ -9715,22 +10008,22 @@ 'ReflectionFunction::__construct' => ['void', 'name'=>'string|Closure'], 'ReflectionFunction::__toString' => ['string'], 'ReflectionFunction::export' => ['string|null', 'name'=>'string', 'return='=>'bool'], -'ReflectionFunction::getClosure' => ['?Closure'], +'ReflectionFunction::getClosure' => ['Closure'], 'ReflectionFunction::getClosureScopeClass' => ['ReflectionClass'], 'ReflectionFunction::getClosureThis' => ['bool'], 'ReflectionFunction::getDocComment' => ['string|false'], -'ReflectionFunction::getEndLine' => ['int|false'], +'ReflectionFunction::getEndLine' => ['positive-int|false'], 'ReflectionFunction::getExtension' => ['ReflectionExtension|null'], 'ReflectionFunction::getExtensionName' => ['string|false'], -'ReflectionFunction::getFileName' => ['string|false'], -'ReflectionFunction::getName' => ['string'], +'ReflectionFunction::getFileName' => ['non-empty-string|false'], +'ReflectionFunction::getName' => ['non-empty-string'], 'ReflectionFunction::getNamespaceName' => ['string'], 'ReflectionFunction::getNumberOfParameters' => ['int'], 'ReflectionFunction::getNumberOfRequiredParameters' => ['int'], -'ReflectionFunction::getParameters' => ['array'], +'ReflectionFunction::getParameters' => ['list'], 'ReflectionFunction::getReturnType' => ['?ReflectionType'], 'ReflectionFunction::getShortName' => ['string'], -'ReflectionFunction::getStartLine' => ['int|false'], +'ReflectionFunction::getStartLine' => ['positive-int|false'], 'ReflectionFunction::getStaticVariables' => ['array'], 'ReflectionFunction::inNamespace' => ['bool'], 'ReflectionFunction::invoke' => ['mixed', '...args='=>'mixed'], @@ -9748,18 +10041,18 @@ 'ReflectionFunctionAbstract::getClosureScopeClass' => ['ReflectionClass|null'], 'ReflectionFunctionAbstract::getClosureThis' => ['object|null'], 'ReflectionFunctionAbstract::getDocComment' => ['string|false'], -'ReflectionFunctionAbstract::getEndLine' => ['int|false'], -'ReflectionFunctionAbstract::getExtension' => ['ReflectionExtension'], -'ReflectionFunctionAbstract::getExtensionName' => ['string'], -'ReflectionFunctionAbstract::getFileName' => ['string|false'], -'ReflectionFunctionAbstract::getName' => ['string'], +'ReflectionFunctionAbstract::getEndLine' => ['positive-int|false'], +'ReflectionFunctionAbstract::getExtension' => ['ReflectionExtension|null'], +'ReflectionFunctionAbstract::getExtensionName' => ['string|false'], +'ReflectionFunctionAbstract::getFileName' => ['non-empty-string|false'], +'ReflectionFunctionAbstract::getName' => ['non-empty-string'], 'ReflectionFunctionAbstract::getNamespaceName' => ['string'], 'ReflectionFunctionAbstract::getNumberOfParameters' => ['int'], 'ReflectionFunctionAbstract::getNumberOfRequiredParameters' => ['int'], -'ReflectionFunctionAbstract::getParameters' => ['array'], +'ReflectionFunctionAbstract::getParameters' => ['list'], 'ReflectionFunctionAbstract::getReturnType' => ['?ReflectionType'], 'ReflectionFunctionAbstract::getShortName' => ['string'], -'ReflectionFunctionAbstract::getStartLine' => ['int|false'], +'ReflectionFunctionAbstract::getStartLine' => ['positive-int|false'], 'ReflectionFunctionAbstract::getStaticVariables' => ['array'], 'ReflectionFunctionAbstract::hasReturnType' => ['bool'], 'ReflectionFunctionAbstract::inNamespace' => ['bool'], @@ -9776,12 +10069,12 @@ 'ReflectionGenerator::getExecutingLine' => ['int'], 'ReflectionGenerator::getFunction' => ['ReflectionFunctionAbstract'], 'ReflectionGenerator::getThis' => ['object'], -'ReflectionGenerator::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}', 'options'=>'int'], +'ReflectionGenerator::getTrace' => ['list\',args?:mixed[],object?:object}>', 'options'=>'int'], 'ReflectionMethod::__construct' => ['void', 'class'=>'string|object', 'name'=>'string'], 'ReflectionMethod::__construct\'1' => ['void', 'class_method'=>'string'], 'ReflectionMethod::__toString' => ['string'], 'ReflectionMethod::export' => ['string|null', 'class'=>'string', 'name'=>'string', 'return='=>'bool'], -'ReflectionMethod::getClosure' => ['?Closure', 'object'=>'?object'], +'ReflectionMethod::getClosure' => ['Closure', 'object'=>'?object'], 'ReflectionMethod::getDeclaringClass' => ['ReflectionClass'], 'ReflectionMethod::getModifiers' => ['int'], 'ReflectionMethod::getPrototype' => ['ReflectionMethod'], @@ -9813,7 +10106,7 @@ 'ReflectionParameter::getDeclaringFunction' => ['ReflectionFunctionAbstract'], 'ReflectionParameter::getDefaultValue' => ['mixed'], 'ReflectionParameter::getDefaultValueConstantName' => ['?string'], -'ReflectionParameter::getName' => ['string'], +'ReflectionParameter::getName' => ['non-empty-string'], 'ReflectionParameter::getPosition' => ['int'], 'ReflectionParameter::getType' => ['ReflectionType|null'], 'ReflectionParameter::hasType' => ['bool'], @@ -9831,7 +10124,7 @@ 'ReflectionProperty::getDeclaringClass' => ['ReflectionClass'], 'ReflectionProperty::getDocComment' => ['string|false'], 'ReflectionProperty::getModifiers' => ['int'], -'ReflectionProperty::getName' => ['string'], +'ReflectionProperty::getName' => ['non-empty-string'], 'ReflectionProperty::getValue' => ['mixed', 'object='=>'object'], 'ReflectionProperty::isDefault' => ['bool'], 'ReflectionProperty::isPrivate' => ['bool'], @@ -9876,7 +10169,7 @@ 'ResourceBundle::get' => ['', 'index'=>'string|int', 'fallback='=>'bool'], 'ResourceBundle::getErrorCode' => ['int'], 'ResourceBundle::getErrorMessage' => ['string'], -'ResourceBundle::getLocales' => ['array', 'bundlename'=>'string'], +'ResourceBundle::getLocales' => ['array|false', 'bundlename'=>'string'], 'resourcebundle_count' => ['int', 'r'=>'resourcebundle'], 'resourcebundle_create' => ['?ResourceBundle', 'locale'=>'string', 'bundlename'=>'string', 'fallback='=>'bool'], 'resourcebundle_get' => ['', 'r'=>'resourcebundle', 'index'=>'string|int', 'fallback='=>'bool'], @@ -9889,7 +10182,7 @@ 'rewind' => ['bool', 'fp'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'dirname'=>'string', 'context='=>'resource'], -'round' => ['float|false', 'number'=>'float', 'precision='=>'int', 'mode='=>'int'], +'round' => ['__benevolent', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], @@ -9957,7 +10250,7 @@ 'RuntimeException::getLine' => ['int'], 'RuntimeException::getMessage' => ['string'], 'RuntimeException::getPrevious' => ['Throwable|RuntimeException|null'], -'RuntimeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'RuntimeException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'RuntimeException::getTraceAsString' => ['string'], 'SAMConnection::commit' => ['bool'], 'SAMConnection::connect' => ['bool', 'protocol'=>'string', 'properties='=>'array'], @@ -9990,7 +10283,7 @@ 'scalebarObj::set' => ['int', 'property_name'=>'string', 'new_value'=>''], 'scalebarObj::setImageColor' => ['int', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], 'scalebarObj::updateFromString' => ['int', 'snippet'=>'string'], -'scandir' => ['array|false', 'dir'=>'string', 'sorting_order='=>'int', 'context='=>'resource'], +'scandir' => ['__benevolent|false>', 'dir'=>'string', 'sorting_order='=>'SCANDIR_SORT_ASCENDING|SCANDIR_SORT_DESCENDING|SCANDIR_SORT_NONE', 'context='=>'resource'], 'SDO_DAS_ChangeSummary::beginLogging' => [''], 'SDO_DAS_ChangeSummary::endLogging' => [''], 'SDO_DAS_ChangeSummary::getChangedDataObjects' => ['SDO_List'], @@ -10090,11 +10383,11 @@ 'session_destroy' => ['bool'], 'session_encode' => ['string|false'], 'session_gc' => ['int|false'], -'session_get_cookie_params' => ['array'], +'session_get_cookie_params' => ['array{lifetime:0|positive-int,path:non-falsy-string,domain:string,secure:bool,httponly:bool,samesite:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'session_id' => ['string|false', 'newid='=>'string'], 'session_is_registered' => ['bool', 'name'=>'string'], 'session_module_name' => ['string|false', 'newname='=>'string'], -'session_name' => ['string|false', 'newname='=>'string'], +'session_name' => ['non-falsy-string|false', 'newname='=>'string'], 'session_pgsql_add_error' => ['bool', 'error_level'=>'int', 'error_message='=>'string'], 'session_pgsql_get_error' => ['array', 'with_error_message='=>'bool'], 'session_pgsql_get_field' => ['string'], @@ -10107,11 +10400,11 @@ 'session_reset' => ['bool'], 'session_save_path' => ['string|false', 'newname='=>'string'], 'session_set_cookie_params' => ['bool', 'lifetime'=>'int', 'path='=>'string', 'domain='=>'?string', 'secure='=>'bool', 'httponly='=>'bool'], -'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:int,path?:string,domain?:?string,secure?:bool,httponly?:bool}'], +'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:int,path?:string,domain?:?string,secure?:bool,httponly?:bool,samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'session_set_save_handler' => ['bool', 'open'=>'callable(string,string):bool', 'close'=>'callable():bool', 'read'=>'callable(string):string', 'write'=>'callable(string,string):bool', 'destroy'=>'callable(string):bool', 'gc'=>'callable(string):bool', 'create_sid='=>'callable():string', 'validate_sid='=>'callable(string):bool', 'update_timestamp='=>'callable(string):bool'], 'session_set_save_handler\'1' => ['bool', 'sessionhandler'=>'SessionHandlerInterface', 'register_shutdown='=>'bool'], 'session_start' => ['bool', 'options='=>'array'], -'session_status' => ['int'], +'session_status' => ['PHP_SESSION_NONE|PHP_SESSION_DISABLED|PHP_SESSION_ACTIVE'], 'session_unregister' => ['bool', 'name'=>'string'], 'session_unset' => ['bool'], 'session_write_close' => ['bool'], @@ -10128,7 +10421,7 @@ 'SessionHandlerInterface::destroy' => ['bool', 'session_id'=>'string'], 'SessionHandlerInterface::gc' => ['int|false', 'maxlifetime'=>'int'], 'SessionHandlerInterface::open' => ['bool', 'save_path'=>'string', 'name'=>'string'], -'SessionHandlerInterface::read' => ['string', 'session_id'=>'string'], +'SessionHandlerInterface::read' => ['string|false', 'session_id'=>'string'], 'SessionHandlerInterface::write' => ['bool', 'session_id'=>'string', 'session_data'=>'string'], 'SessionIdInterface::create_sid' => ['string'], 'SessionUpdateTimestampHandler::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'], @@ -10141,22 +10434,19 @@ 'set_include_path' => ['string|false', 'new_include_path'=>'string'], 'set_magic_quotes_runtime' => ['bool', 'new_setting'=>'bool'], 'set_time_limit' => ['bool', 'seconds'=>'int'], -'setcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool', 'samesite='=>'\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'', 'url_encode='=>'int'], -'setcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\', url_encode?:int}'], +'setcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool'], +'setcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'setLeftFill' => ['void', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setLine' => ['void', 'width'=>'int', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setlocale' => ['string|false', 'category'=>'int', 'locale'=>'string|null', '...args='=>'string'], 'setlocale\'1' => ['string|false', 'category'=>'int', 'locale'=>'?array'], -'setproctitle' => ['void', 'title'=>'string'], -'setrawcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool', 'samesite='=>'\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'', 'url_encode='=>'int'], -'setrawcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\', url_encode?:int}'], +'setrawcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool'], +'setrawcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'setRightFill' => ['void', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setthreadtitle' => ['bool', 'title'=>'string'], 'settype' => ['bool', '&rw_var'=>'mixed', 'type'=>'string'], -'sha1' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'sha1_file' => ['string|false', 'filename'=>'string', 'raw_output='=>'bool'], -'sha256' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'sha256_file' => ['string', 'filename'=>'string', 'raw_output='=>'bool'], +'sha1' => ['non-falsy-string&lowercase-string', 'str'=>'string', 'raw_output='=>'bool'], +'sha1_file' => ['(non-falsy-string&lowercase-string)|false', 'filename'=>'string', 'raw_output='=>'bool'], 'shapefileObj::__construct' => ['void', 'filename'=>'string', 'type'=>'int'], 'shapefileObj::addPoint' => ['int', 'point'=>'pointObj'], 'shapefileObj::addShape' => ['int', 'shape'=>'shapeObj'], @@ -10199,7 +10489,7 @@ 'shapeObj::toWkt' => ['string'], 'shapeObj::union' => ['shapeObj', 'shape'=>'shapeObj'], 'shapeObj::within' => ['int', 'shape2'=>'shapeObj'], -'shell_exec' => ['?string', 'cmd'=>'string'], +'shell_exec' => ['string|false|null', 'cmd'=>'string'], 'shm_attach' => ['resource|false', 'key'=>'int', 'memsize='=>'int', 'perm='=>'int'], 'shm_detach' => ['bool', 'shm_identifier'=>'resource'], 'shm_get_var' => ['mixed', 'id'=>'resource', 'variable_key'=>'int'], @@ -10224,16 +10514,16 @@ 'SimpleXMLElement::__get' => ['static', 'name'=>'string'], 'SimpleXMLElement::__toString' => ['string'], 'SimpleXMLElement::addAttribute' => ['void', 'name'=>'string', 'value='=>'string', 'ns='=>'string'], -'SimpleXMLElement::addChild' => ['static', 'name'=>'string', 'value='=>'string', 'ns='=>'string'], +'SimpleXMLElement::addChild' => ['__benevolent', 'name'=>'string', 'value='=>'string|null', 'ns='=>'string|null'], 'SimpleXMLElement::asXML' => ['string|bool', 'filename='=>'string'], -'SimpleXMLElement::attributes' => ['static|null', 'ns='=>'string', 'is_prefix='=>'bool'], -'SimpleXMLElement::children' => ['static', 'namespaceOrPrefix='=>'string|null', 'is_prefix='=>'bool'], +'SimpleXMLElement::attributes' => ['__benevolent', 'ns='=>'string', 'is_prefix='=>'bool'], +'SimpleXMLElement::children' => ['__benevolent', 'namespaceOrPrefix='=>'string|null', 'is_prefix='=>'bool'], 'SimpleXMLElement::count' => ['0|positive-int'], -'SimpleXMLElement::getDocNamespaces' => ['string[]', 'recursive='=>'bool', 'from_root='=>'bool'], +'SimpleXMLElement::getDocNamespaces' => ['string[]|false', 'recursive='=>'bool', 'from_root='=>'bool'], 'SimpleXMLElement::getName' => ['string'], 'SimpleXMLElement::getNamespaces' => ['string[]', 'recursive='=>'bool'], 'SimpleXMLElement::registerXPathNamespace' => ['bool', 'prefix'=>'string', 'ns'=>'string'], -'SimpleXMLElement::xpath' => ['static[]|false', 'path'=>'string'], +'SimpleXMLElement::xpath' => ['static[]|false|null', 'path'=>'string'], 'SimpleXMLIterator::current' => ['SimpleXMLIterator'], 'SimpleXMLIterator::getChildren' => ['SimpleXMLIterator'], 'SimpleXMLIterator::hasChildren' => ['bool'], @@ -10293,7 +10583,7 @@ 'SoapClient::__setSoapHeaders' => ['bool', 'soapheaders='=>''], '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::__construct' => ['void', 'faultcode'=>'string', 'string'=>'string', 'faultactor='=>'string', 'detail='=>'mixed', 'faultname='=>'string', 'headerfault='=>'mixed'], 'SoapFault::__toString' => ['string'], 'SoapFault::SoapFault' => ['object', 'faultcode'=>'string', 'string'=>'string', 'faultactor='=>'string', 'detail='=>'string', 'faultname='=>'string', 'headerfault='=>'string'], 'SoapHeader::__construct' => ['void', 'namespace'=>'string', 'name'=>'string', 'data='=>'mixed', 'mustunderstand='=>'bool', 'actor='=>'string'], @@ -10337,7 +10627,7 @@ 'socket_recv' => ['int|false', 'socket'=>'resource', '&w_buf'=>'string', 'len'=>'int', 'flags'=>'int'], 'socket_recvfrom' => ['int|false', 'socket'=>'resource', '&w_buf'=>'string', 'len'=>'int', 'flags'=>'int', '&w_name'=>'string', '&w_port='=>'int'], 'socket_recvmsg' => ['int|false', 'socket'=>'resource', '&w_message'=>'string', 'flags='=>'int'], -'socket_select' => ['int|false', '&rw_read_fds'=>'resource[]|null', '&rw_write_fds'=>'resource[]|null', '&rw_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], +'socket_select' => ['int|false', '&w_read_fds'=>'resource[]|null', '&w_write_fds'=>'resource[]|null', '&w_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], 'socket_send' => ['int|false', 'socket'=>'resource', 'buf'=>'string', 'len'=>'int', 'flags'=>'int'], 'socket_sendmsg' => ['int|false', 'socket'=>'resource', 'message'=>'array', 'flags'=>'int'], 'socket_sendto' => ['int|false', 'socket'=>'resource', 'buf'=>'string', 'len'=>'int', 'flags'=>'int', 'addr'=>'string', 'port='=>'int'], @@ -10441,11 +10731,11 @@ 'sodium_crypto_box_seal_open' => ['string|false', 'message'=>'string', 'recipient_keypair'=>'string'], 'sodium_crypto_box_secretkey' => ['string', 'keypair'=>'string'], 'sodium_crypto_box_seed_keypair' => ['string', 'seed'=>'string'], -'sodium_crypto_generichash' => ['string', 'msg'=>'string', 'key='=>'?string', 'length='=>'?int'], -'sodium_crypto_generichash_final' => ['string', 'state'=>'string', 'length='=>'?int'], -'sodium_crypto_generichash_init' => ['string', 'key='=>'?string', 'length='=>'?int'], -'sodium_crypto_generichash_keygen' => ['string'], -'sodium_crypto_generichash_update' => ['bool', 'state'=>'string', 'string'=>'string'], +'sodium_crypto_generichash' => ['non-empty-string', 'msg'=>'string', 'key='=>'?string', 'length='=>'?int'], +'sodium_crypto_generichash_final' => ['non-empty-string', 'state'=>'non-empty-string', 'length='=>'?int'], +'sodium_crypto_generichash_init' => ['non-empty-string', 'key='=>'?string', 'length='=>'?int'], +'sodium_crypto_generichash_keygen' => ['non-empty-string'], +'sodium_crypto_generichash_update' => ['bool', 'state'=>'non-empty-string', 'string'=>'string'], 'sodium_crypto_kdf_derive_from_key' => ['string', 'subkey_len'=>'int', 'subkey_id'=>'int', 'context'=>'string', 'key'=>'string'], 'sodium_crypto_kdf_keygen' => ['string'], 'sodium_crypto_kx' => ['string', 'secretkey'=>'string', 'publickey'=>'string', 'client_publickey'=>'string', 'server_publickey'=>'string'], @@ -10475,18 +10765,18 @@ 'sodium_crypto_secretstream_xchacha20poly1305_rekey' => ['void', 'state'=>'string'], 'sodium_crypto_shorthash' => ['string', 'message'=>'string', 'key'=>'string'], 'sodium_crypto_shorthash_keygen' => ['string'], -'sodium_crypto_sign' => ['string', 'message'=>'string', 'secretkey'=>'string'], -'sodium_crypto_sign_detached' => ['string', 'message'=>'string', 'secretkey'=>'string'], -'sodium_crypto_sign_ed25519_pk_to_curve25519' => ['string', 'ed25519pk'=>'string'], -'sodium_crypto_sign_ed25519_sk_to_curve25519' => ['string', 'ed25519sk'=>'string'], -'sodium_crypto_sign_keypair' => ['string'], -'sodium_crypto_sign_keypair_from_secretkey_and_publickey' => ['string', 'secret_key'=>'string', 'public_key'=>'string'], -'sodium_crypto_sign_open' => ['string|false', 'message'=>'string', 'publickey'=>'string'], -'sodium_crypto_sign_publickey' => ['string', 'keypair'=>'string'], -'sodium_crypto_sign_publickey_from_secretkey' => ['string', 'secretkey'=>'string'], -'sodium_crypto_sign_secretkey' => ['string', 'keypair'=>'string'], -'sodium_crypto_sign_seed_keypair' => ['string', 'seed'=>'string'], -'sodium_crypto_sign_verify_detached' => ['bool', 'signature'=>'string', 'message'=>'string', 'publickey'=>'string'], +'sodium_crypto_sign' => ['non-empty-string', 'message'=>'string', 'secretkey'=>'non-empty-string'], +'sodium_crypto_sign_detached' => ['non-empty-string', 'message'=>'string', 'secretkey'=>'non-empty-string'], +'sodium_crypto_sign_ed25519_pk_to_curve25519' => ['non-empty-string', 'ed25519pk'=>'non-empty-string'], +'sodium_crypto_sign_ed25519_sk_to_curve25519' => ['non-empty-string', 'ed25519sk'=>'non-empty-string'], +'sodium_crypto_sign_keypair' => ['non-empty-string'], +'sodium_crypto_sign_keypair_from_secretkey_and_publickey' => ['non-empty-string', 'secret_key'=>'non-empty-string', 'public_key'=>'non-empty-string'], +'sodium_crypto_sign_open' => ['string|false', 'message'=>'string', 'publickey'=>'non-empty-string'], +'sodium_crypto_sign_publickey' => ['non-empty-string', 'keypair'=>'non-empty-string'], +'sodium_crypto_sign_publickey_from_secretkey' => ['non-empty-string', 'secretkey'=>'non-empty-string'], +'sodium_crypto_sign_secretkey' => ['non-empty-string', 'keypair'=>'non-empty-string'], +'sodium_crypto_sign_seed_keypair' => ['non-empty-string', 'seed'=>'non-empty-string'], +'sodium_crypto_sign_verify_detached' => ['bool', 'signature'=>'non-empty-string', 'message'=>'string', 'publickey'=>'non-empty-string'], 'sodium_crypto_stream' => ['string', 'length'=>'int', 'nonce'=>'string', 'key'=>'string'], 'sodium_crypto_stream_keygen' => ['string'], 'sodium_crypto_stream_xor' => ['string', 'message'=>'string', 'nonce'=>'string', 'key'=>'string'], @@ -10814,7 +11104,7 @@ 'SolrException::getLine' => ['int'], 'SolrException::getMessage' => ['string'], 'SolrException::getPrevious' => ['Exception|Throwable'], -'SolrException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'SolrException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrException::getTraceAsString' => ['string'], 'SolrGenericResponse::__construct' => ['void'], 'SolrGenericResponse::__destruct' => [''], @@ -10839,7 +11129,7 @@ 'SolrIllegalArgumentException::getLine' => ['int'], 'SolrIllegalArgumentException::getMessage' => ['string'], 'SolrIllegalArgumentException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalArgumentException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'SolrIllegalArgumentException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrIllegalArgumentException::getTraceAsString' => ['string'], 'SolrIllegalOperationException::__clone' => ['void'], 'SolrIllegalOperationException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Exception)|(?Throwable)'], @@ -10851,7 +11141,7 @@ 'SolrIllegalOperationException::getLine' => ['int'], 'SolrIllegalOperationException::getMessage' => ['string'], 'SolrIllegalOperationException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalOperationException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'SolrIllegalOperationException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrIllegalOperationException::getTraceAsString' => ['string'], 'SolrInputDocument::__clone' => ['void'], 'SolrInputDocument::__construct' => ['void'], @@ -11159,7 +11449,7 @@ 'SolrServerException::getLine' => ['int'], 'SolrServerException::getMessage' => ['string'], 'SolrServerException::getPrevious' => ['Exception|Throwable'], -'SolrServerException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'SolrServerException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrServerException::getTraceAsString' => ['string'], 'SolrUpdateResponse::__construct' => ['void'], 'SolrUpdateResponse::__destruct' => [''], @@ -11218,7 +11508,7 @@ 'spl_autoload' => ['void', 'class_name'=>'string', 'file_extensions='=>'string'], 'spl_autoload_call' => ['void', 'class_name'=>'string'], 'spl_autoload_extensions' => ['string', 'file_extensions='=>'string'], -'spl_autoload_functions' => ['false|array'], +'spl_autoload_functions' => ['false|list'], 'spl_autoload_register' => ['bool', 'autoload_function='=>'callable(string):void', 'throw='=>'bool', 'prepend='=>'bool'], 'spl_autoload_unregister' => ['bool', 'autoload_function'=>'mixed'], 'spl_classes' => ['array'], @@ -11263,7 +11553,7 @@ 'SplFileInfo::getMTime' => ['__benevolent'], 'SplFileInfo::getOwner' => ['__benevolent'], 'SplFileInfo::getPath' => ['string'], -'SplFileInfo::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], +'SplFileInfo::getPathInfo' => ['__benevolent', 'class_name='=>'string'], 'SplFileInfo::getPathname' => ['string'], 'SplFileInfo::getPerms' => ['__benevolent'], 'SplFileInfo::getRealPath' => ['__benevolent'], @@ -11285,10 +11575,10 @@ 'SplFileObject::fflush' => ['bool'], 'SplFileObject::fgetc' => ['string|false'], // Do not believe https://www.php.net/manual/en/splfileobject.fgetcsv#refsect1-splfileobject.fgetcsv-returnvalues -'SplFileObject::fgetcsv' => ['array|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], -'SplFileObject::fgets' => ['string|false'], +'SplFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'SplFileObject::fgets' => ['string'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], -'SplFileObject::flock' => ['bool', 'operation'=>'int', '&w_wouldblock='=>'int'], +'SplFileObject::flock' => ['bool', 'operation'=>'int-mask', '&w_wouldblock='=>'0|1'], 'SplFileObject::fpassthru' => ['int'], 'SplFileObject::fputcsv' => ['int|false', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplFileObject::fread' => ['string|false', 'length'=>'int'], @@ -11300,7 +11590,7 @@ 'SplFileObject::fwrite' => ['int', 'str'=>'string', 'length='=>'int'], 'SplFileObject::getChildren' => ['null'], 'SplFileObject::getCsvControl' => ['array'], -'SplFileObject::getCurrentLine' => ['string|false'], +'SplFileObject::getCurrentLine' => ['string'], 'SplFileObject::getFlags' => ['int'], 'SplFileObject::getMaxLineLen' => ['int'], 'SplFileObject::hasChildren' => ['false'], @@ -11345,7 +11635,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' => ['0|positive-int', 'os'=>'SplObjectStorage'], 'SplObjectStorage::attach' => ['void', 'obj'=>'object', 'inf='=>'mixed'], 'SplObjectStorage::contains' => ['bool', 'obj'=>'object'], 'SplObjectStorage::count' => ['0|positive-int'], @@ -11359,8 +11649,8 @@ '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' => ['0|positive-int', 'os'=>'SplObjectStorage'], +'SplObjectStorage::removeAllExcept' => ['0|positive-int', 'os'=>'SplObjectStorage'], 'SplObjectStorage::rewind' => ['void'], 'SplObjectStorage::serialize' => ['string'], 'SplObjectStorage::setInfo' => ['void', 'inf'=>'mixed'], @@ -11396,7 +11686,7 @@ 'Spoofchecker::setAllowedLocales' => ['void', 'locale_list'=>'string'], 'Spoofchecker::setChecks' => ['void', 'checks'=>'long'], 'Spoofchecker::setRestrictionLevel' => ['void', 'restriction_level'=>'int'], -'sprintf' => ['string', 'format'=>'string', '...values='=>'string|int|float|bool'], +'sprintf' => ['string', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], 'sql_regcase' => ['string', 'string'=>'string'], 'SQLite3::__construct' => ['void', 'filename'=>'string', 'flags='=>'int', 'encryption_key='=>'string|null'], 'SQLite3::busyTimeout' => ['bool', 'msecs'=>'int'], @@ -11413,7 +11703,7 @@ 'SQLite3::lastInsertRowID' => ['int'], 'SQLite3::loadExtension' => ['bool', 'shared_library'=>'string'], 'SQLite3::open' => ['void', 'filename'=>'string', 'flags='=>'int', 'encryption_key='=>'string|null'], -'SQLite3::openBlob' => ['resource', 'table'=>'string', 'column'=>'string', 'rowid'=>'int', 'dbname'=>'string', 'flags='=>'int'], +'SQLite3::openBlob' => ['resource|false', 'table'=>'string', 'column'=>'string', 'rowid'=>'int', 'dbname='=>'string', 'flags='=>'int'], 'SQLite3::prepare' => ['SQLite3Stmt|false', 'query'=>'string'], 'SQLite3::query' => ['SQLite3Result|false', 'query'=>'string'], 'SQLite3::querySingle' => ['array|int|string|bool|float|null|false', 'query'=>'string', 'entire_row='=>'bool'], @@ -11535,7 +11825,7 @@ 'sqlsrv_prepare' => ['resource|false', 'conn'=>'resource', 'sql'=>'string', 'params='=>'array', 'options='=>'array'], 'sqlsrv_query' => ['resource|false', 'conn'=>'resource', 'sql'=>'string', 'params='=>'array', 'options='=>'array'], 'sqlsrv_rollback' => ['bool', 'conn'=>'resource'], -'sqlsrv_rows_affected' => ['int|false', 'stmt'=>'resource'], +'sqlsrv_rows_affected' => ['int<-1,max>|false', 'stmt'=>'resource'], 'sqlsrv_send_stream_data' => ['bool', 'stmt'=>'resource'], 'sqlsrv_server_info' => ['array', 'conn'=>'resource'], 'sqrt' => ['float', 'number'=>'float'], @@ -11684,28 +11974,28 @@ 'stomp_version' => ['string'], 'StompException::getDetails' => ['string'], 'StompFrame::__construct' => ['void', 'command='=>'string', 'headers='=>'array', 'body='=>'string'], -'str_getcsv' => ['array', 'input'=>'string', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'str_getcsv' => ['non-empty-list', 'input'=>'string', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'str_ireplace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_replace_count='=>'int'], 'str_pad' => ['string', 'input'=>'string', 'pad_length'=>'int', 'pad_string='=>'string', 'pad_type='=>'int'], 'str_repeat' => ['string', 'input'=>'string', 'multiplier'=>'int'], 'str_replace' => ['string|array', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_replace_count='=>'int'], 'str_rot13' => ['string', 'str'=>'string'], 'str_shuffle' => ['string', 'str'=>'string'], -'str_split' => ['non-empty-array|false', 'str'=>'string', 'split_length='=>'positive-int'], +'str_split' => ['non-empty-list|false', 'str'=>'string', 'split_length='=>'positive-int'], 'str_word_count' => ['array|int|false', 'string'=>'string', 'format='=>'int', 'charlist='=>'string'], -'strcasecmp' => ['int', 'str1'=>'string', 'str2'=>'string'], +'strcasecmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], -'strcmp' => ['int', 'str1'=>'string', 'str2'=>'string'], -'strcoll' => ['int', 'str1'=>'string', 'str2'=>'string'], -'strcspn' => ['int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'length='=>'int'], +'strcmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string'], +'strcoll' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string'], +'strcspn' => ['non-negative-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_make_writeable' => ['stdClass|null', 'brigade'=>'resource'], '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'], 'stream_context_get_options' => ['array', 'context'=>'resource'], -'stream_context_get_params' => ['array', 'context'=>'resource'], +'stream_context_get_params' => ['array{notification:string, options:array}', 'context'=>'resource'], 'stream_context_set_default' => ['resource', 'options'=>'array'], 'stream_context_set_option' => ['bool', 'context'=>'', 'wrappername'=>'string', 'optionname'=>'string', 'value'=>''], 'stream_context_set_option\'1' => ['bool', 'context'=>'', 'options'=>'array'], @@ -11717,11 +12007,11 @@ 'stream_filter_register' => ['bool', 'filtername'=>'string', 'classname'=>'string'], 'stream_filter_remove' => ['bool', 'stream_filter'=>'resource'], 'stream_get_contents' => ['string|false', 'source'=>'resource', 'maxlen='=>'int', 'offset='=>'int'], -'stream_get_filters' => ['array'], +'stream_get_filters' => ['list'], 'stream_get_line' => ['string|false', 'stream'=>'resource', 'maxlen'=>'int', 'ending='=>'string'], -'stream_get_meta_data' => ['array{timed_out:bool,blocked:bool,eof:bool,unread_bytes:int,stream_type:string,wrapper_type:string,wrapper_data:mixed,mode:string,seekable:bool,uri:string,mediatype?:string,base64?:bool}', 'fp'=>'resource'], -'stream_get_transports' => ['array'], -'stream_get_wrappers' => ['array'], +'stream_get_meta_data' => ['array{timed_out:bool,blocked:bool,eof:bool,unread_bytes:int,stream_type:string,wrapper_type:string,wrapper_data:mixed,mode:string,seekable:bool,uri?:string,mediatype?:string,base64?:bool}', 'fp'=>'resource'], +'stream_get_transports' => ['list'], +'stream_get_wrappers' => ['list'], 'stream_is_local' => ['bool', 'stream'=>'resource|string'], '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'], @@ -11733,8 +12023,8 @@ 'stream_set_timeout' => ['bool', 'stream'=>'resource', 'seconds'=>'int', 'microseconds='=>'int'], 'stream_set_write_buffer' => ['int', 'fp'=>'resource', 'buffer'=>'int'], 'stream_socket_accept' => ['resource|false', 'serverstream'=>'resource', 'timeout='=>'float', '&w_peername='=>'string'], -'stream_socket_client' => ['resource|false', 'remoteaddress'=>'string', '&w_errcode='=>'int', '&w_errstring='=>'string', 'timeout='=>'float', 'flags='=>'int', 'context='=>'resource'], -'stream_socket_enable_crypto' => ['int|bool', 'stream'=>'resource', 'enable'=>'bool', 'cryptokind='=>'int', 'sessionstream='=>'resource'], +'stream_socket_client' => ['resource|false', 'remoteaddress'=>'string', '&w_errcode='=>'int', '&w_errstring='=>'string', 'timeout='=>'float', 'flags='=>'int-mask', 'context='=>'resource'], +'stream_socket_enable_crypto' => ['0|bool', 'stream'=>'resource', 'enable'=>'bool', 'crypto_method='=>'STREAM_CRYPTO_METHOD_SSLv2_CLIENT|STREAM_CRYPTO_METHOD_SSLv3_CLIENT|STREAM_CRYPTO_METHOD_SSLv23_CLIENT|STREAM_CRYPTO_METHOD_ANY_CLIENT|STREAM_CRYPTO_METHOD_TLS_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT|STREAM_CRYPTO_METHOD_SSLv2_SERVER|STREAM_CRYPTO_METHOD_SSLv3_SERVER|STREAM_CRYPTO_METHOD_SSLv23_SERVER|STREAM_CRYPTO_METHOD_ANY_SERVER|STREAM_CRYPTO_METHOD_TLS_SERVER|STREAM_CRYPTO_METHOD_TLSv1_0_SERVER|STREAM_CRYPTO_METHOD_TLSv1_1_SERVER|STREAM_CRYPTO_METHOD_TLSv1_2_SERVER|STREAM_CRYPTO_METHOD_TLSv1_3_SERVER', 'session_stream='=>'resource'], 'stream_socket_get_name' => ['string|false', 'stream'=>'resource', 'want_peer'=>'bool'], 'stream_socket_pair' => ['resource[]|false', 'domain'=>'int', 'type'=>'int', 'protocol'=>'int'], 'stream_socket_recvfrom' => ['string|false', 'stream'=>'resource', 'amount'=>'int', 'flags='=>'int', '&w_remote_addr='=>'string'], @@ -11777,10 +12067,10 @@ 'stripslashes' => ['string', 'str'=>'string'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'mixed', 'before_needle='=>'bool'], 'strlen' => ['0|positive-int', 'string'=>'string'], -'strnatcasecmp' => ['int', 's1'=>'string', 's2'=>'string'], -'strnatcmp' => ['int', 's1'=>'string', 's2'=>'string'], -'strncasecmp' => ['int', 'str1'=>'string', 'str2'=>'string', 'len'=>'int'], -'strncmp' => ['int', 'str1'=>'string', 'str2'=>'string', 'len'=>'int'], +'strnatcasecmp' => ['int<-1, 1>', 's1'=>'string', 's2'=>'string'], +'strnatcmp' => ['int<-1, 1>', 's1'=>'string', 's2'=>'string'], +'strncasecmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string', 'len'=>'int'], +'strncmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string', 'len'=>'int'], 'strpbrk' => ['string|false', 'haystack'=>'string', 'char_list'=>'string'], 'strpos' => ['positive-int|0|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strptime' => ['array|false', 'datestr'=>'string', 'format'=>'string'], @@ -11788,18 +12078,18 @@ 'strrev' => ['string', 'str'=>'string'], 'strripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strrpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], -'strspn' => ['int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'len='=>'int'], +'strspn' => ['non-negative-int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'len='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'mixed', 'before_needle='=>'bool'], -'strtok' => ['string|false', 'str'=>'string', 'token'=>'string'], -'strtok\'1' => ['string|false', 'token'=>'string'], -'strtolower' => ['string', 'str'=>'string'], +'strtok' => ['non-empty-string|false', 'str'=>'string', 'token'=>'string'], +'strtok\'1' => ['non-empty-string|false', 'token'=>'string'], +'strtolower' => ['lowercase-string', 'str'=>'string'], 'strtotime' => ['int|false', 'time'=>'string', 'now='=>'int'], -'strtoupper' => ['string', 'str'=>'string'], +'strtoupper' => ['uppercase-string', 'str'=>'string'], 'strtr' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], 'strtr\'1' => ['string', 'str'=>'string', 'replace_pairs'=>'array'], -'strval' => ['string', 'var'=>'mixed'], +'strval' => ['string', 'var'=>'__stringAndStringable|int|float|bool|resource|null'], 'substr' => ['__benevolent', 'string'=>'string', 'start'=>'int', 'length='=>'int'], -'substr_compare' => ['int|false', 'main_str'=>'string', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'case_sensitivity='=>'bool'], +'substr_compare' => ['int<-1, 1>|false', 'main_str'=>'string', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'case_sensitivity='=>'bool'], 'substr_count' => ['0|positive-int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'int'], 'substr_replace' => ['string|array', 'str'=>'string|array', 'repl'=>'mixed', 'start'=>'mixed', 'length='=>'mixed'], 'suhosin_encrypt_cookie' => ['string', 'name'=>'string', 'value'=>'string'], @@ -12187,7 +12477,7 @@ 'tan' => ['float', 'number'=>'float'], 'tanh' => ['float', 'number'=>'float'], 'tcpwrap_check' => ['bool', 'daemon'=>'string', 'address'=>'string', 'user='=>'string', 'nodns='=>'bool'], -'tempnam' => ['string|false', 'dir'=>'string', 'prefix'=>'string'], +'tempnam' => ['__benevolent', 'dir'=>'string', 'prefix'=>'string'], 'textdomain' => ['string', 'domain'=>'string'], 'Thread::__construct' => ['void'], 'Thread::chunk' => ['array', 'size'=>'int', 'preserve'=>'bool'], @@ -12238,7 +12528,7 @@ 'Throwable::getLine' => ['int'], 'Throwable::getMessage' => ['string'], 'Throwable::getPrevious' => ['Throwable|null'], -'Throwable::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'Throwable::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'Throwable::getTraceAsString' => ['string'], 'tidy::__construct' => ['void', 'filename='=>'string', 'config='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], 'tidy::body' => ['tidyNode'], @@ -12300,20 +12590,20 @@ 'tidyNode::isPhp' => ['bool'], 'tidyNode::isText' => ['bool'], 'time' => ['positive-int'], -'time_nanosleep' => ['array{0:0|positive-int,1:0|positive-int}|bool', 'seconds'=>'int', 'nanoseconds'=>'int'], +'time_nanosleep' => ['array{seconds:0|positive-int,nanoseconds:0|positive-int}|bool', 'seconds'=>'int', 'nanoseconds'=>'int'], 'time_sleep_until' => ['bool', 'timestamp'=>'float'], -'timezone_abbreviations_list' => ['array'], -'timezone_identifiers_list' => ['array', 'what='=>'int', 'country='=>'?string'], +'timezone_abbreviations_list' => ['array>'], +'timezone_identifiers_list' => ['list', 'what='=>'int', 'country='=>'?string'], 'timezone_location_get' => ['array{country_code: string, latitude: float, longitude: float, comments: string}|false', 'object'=>'DateTimeZone'], 'timezone_name_from_abbr' => ['string|false', 'abbr'=>'string', 'gmtoffset='=>'int', 'isdst='=>'int'], 'timezone_name_get' => ['string', 'object'=>'DateTimeZone'], 'timezone_offset_get' => ['int', 'object'=>'DateTimeZone', 'datetime'=>'DateTime'], 'timezone_open' => ['DateTimeZone|false', 'timezone'=>'string'], -'timezone_transitions_get' => ['array|false', 'object'=>'DateTimeZone', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], +'timezone_transitions_get' => ['list|false', 'object'=>'DateTimeZone', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], 'timezone_version_get' => ['string'], -'tmpfile' => ['resource|false'], -'token_get_all' => ['array', 'source'=>'string', 'flags='=>'int'], -'token_name' => ['string', 'type'=>'int'], +'tmpfile' => ['__benevolent'], +'token_get_all' => ['list', 'source'=>'string', 'flags='=>'int'], +'token_name' => ['non-falsy-string', 'type'=>'int'], 'TokyoTyrant::__construct' => ['void', 'host='=>'string', 'port='=>'int', 'options='=>'array'], 'TokyoTyrant::add' => ['int|float', 'key'=>'string', 'increment'=>'float', 'type='=>'int'], 'TokyoTyrant::connect' => ['TokyoTyrant', 'host'=>'string', 'port='=>'int', 'options='=>'array'], @@ -12370,183 +12660,183 @@ 'TokyoTyrantTable::putShl' => ['void', 'key'=>'string', 'value'=>'string', 'width'=>'int'], 'TokyoTyrantTable::setIndex' => ['mixed', 'column'=>'string', 'type'=>'int'], 'touch' => ['bool', 'filename'=>'string', 'time='=>'int', 'atime='=>'int'], -'trader_acos' => ['array', 'real'=>'array'], -'trader_ad' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array'], -'trader_add' => ['array', 'real0'=>'array', 'real1'=>'array'], -'trader_adosc' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int'], -'trader_adx' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_adxr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_apo' => ['array', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'mAType='=>'int'], -'trader_aroon' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_aroonosc' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_asin' => ['array', 'real'=>'array'], -'trader_atan' => ['array', 'real'=>'array'], -'trader_atr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_avgprice' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_bbands' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'nbDevUp='=>'float', 'nbDevDn='=>'float', 'mAType='=>'int'], -'trader_beta' => ['array', 'real0'=>'array', 'real1'=>'array', 'timePeriod='=>'int'], -'trader_bop' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cci' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_cdl2crows' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3blackcrows' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3inside' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3linestrike' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3outside' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3starsinsouth' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdl3whitesoldiers' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlabandonedbaby' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdladvanceblock' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlbelthold' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlbreakaway' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlclosingmarubozu' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlconcealbabyswall' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlcounterattack' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdldarkcloudcover' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdldoji' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdldojistar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdldragonflydoji' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlengulfing' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdleveningdojistar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdleveningstar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdlgapsidesidewhite' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlgravestonedoji' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhammer' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhangingman' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlharami' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlharamicross' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhighwave' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhikkake' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhikkakemod' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlhomingpigeon' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlidentical3crows' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlinneck' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlinvertedhammer' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlkicking' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlkickingbylength' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlladderbottom' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdllongleggeddoji' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdllongline' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlmarubozu' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlmatchinglow' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlmathold' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdlmorningdojistar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdlmorningstar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], -'trader_cdlonneck' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlpiercing' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlrickshawman' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlrisefall3methods' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlseparatinglines' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlshootingstar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlshortline' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlspinningtop' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlstalledpattern' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlsticksandwich' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdltakuri' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdltasukigap' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlthrusting' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdltristar' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlunique3river' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlupsidegap2crows' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_cdlxsidegap3methods' => ['array', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_ceil' => ['array', 'real'=>'array'], -'trader_cmo' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_correl' => ['array', 'real0'=>'array', 'real1'=>'array', 'timePeriod='=>'int'], -'trader_cos' => ['array', 'real'=>'array'], -'trader_cosh' => ['array', 'real'=>'array'], -'trader_dema' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_div' => ['array', 'real0'=>'array', 'real1'=>'array'], -'trader_dx' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_ema' => ['array', 'real'=>'array', 'timePeriod='=>'int'], +'trader_acos' => ['array|false', 'real'=>'array'], +'trader_ad' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array'], +'trader_add' => ['array|false', 'real0'=>'array', 'real1'=>'array'], +'trader_adosc' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int'], +'trader_adx' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_adxr' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_apo' => ['array|false', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'mAType='=>'int'], +'trader_aroon' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_aroonosc' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_asin' => ['array|false', 'real'=>'array'], +'trader_atan' => ['array|false', 'real'=>'array'], +'trader_atr' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_avgprice' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_bbands' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'nbDevUp='=>'float', 'nbDevDn='=>'float', 'mAType='=>'int'], +'trader_beta' => ['array|false', 'real0'=>'array', 'real1'=>'array', 'timePeriod='=>'int'], +'trader_bop' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cci' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_cdl2crows' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3blackcrows' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3inside' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3linestrike' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3outside' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3starsinsouth' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdl3whitesoldiers' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlabandonedbaby' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdladvanceblock' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlbelthold' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlbreakaway' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlclosingmarubozu' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlconcealbabyswall' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlcounterattack' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdldarkcloudcover' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdldoji' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdldojistar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdldragonflydoji' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlengulfing' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdleveningdojistar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdleveningstar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdlgapsidesidewhite' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlgravestonedoji' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhammer' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhangingman' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlharami' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlharamicross' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhighwave' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhikkake' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhikkakemod' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlhomingpigeon' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlidentical3crows' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlinneck' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlinvertedhammer' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlkicking' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlkickingbylength' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlladderbottom' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdllongleggeddoji' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdllongline' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlmarubozu' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlmatchinglow' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlmathold' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdlmorningdojistar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdlmorningstar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'penetration='=>'float'], +'trader_cdlonneck' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlpiercing' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlrickshawman' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlrisefall3methods' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlseparatinglines' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlshootingstar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlshortline' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlspinningtop' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlstalledpattern' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlsticksandwich' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdltakuri' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdltasukigap' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlthrusting' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdltristar' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlunique3river' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlupsidegap2crows' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_cdlxsidegap3methods' => ['array|false', 'open'=>'array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_ceil' => ['array|false', 'real'=>'array'], +'trader_cmo' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_correl' => ['array|false', 'real0'=>'array', 'real1'=>'array', 'timePeriod='=>'int'], +'trader_cos' => ['array|false', 'real'=>'array'], +'trader_cosh' => ['array|false', 'real'=>'array'], +'trader_dema' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_div' => ['array|false', 'real0'=>'array', 'real1'=>'array'], +'trader_dx' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_ema' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], 'trader_errno' => ['int'], -'trader_exp' => ['array', 'real'=>'array'], -'trader_floor' => ['array', 'real'=>'array'], +'trader_exp' => ['array|false', 'real'=>'array'], +'trader_floor' => ['array|false', 'real'=>'array'], 'trader_get_compat' => ['int'], 'trader_get_unstable_period' => ['int', 'functionId'=>'int'], -'trader_ht_dcperiod' => ['array', 'real'=>'array'], -'trader_ht_dcphase' => ['array', 'real'=>'array'], -'trader_ht_phasor' => ['array', 'real'=>'array'], -'trader_ht_sine' => ['array', 'real'=>'array'], -'trader_ht_trendline' => ['array', 'real'=>'array'], -'trader_ht_trendmode' => ['array', 'real'=>'array'], -'trader_kama' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_linearreg' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_linearreg_angle' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_linearreg_intercept' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_linearreg_slope' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_ln' => ['array', 'real'=>'array'], -'trader_log10' => ['array', 'real'=>'array'], -'trader_ma' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'mAType='=>'int'], -'trader_macd' => ['array', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'signalPeriod='=>'int'], -'trader_macdext' => ['array', 'real'=>'array', 'fastPeriod='=>'int', 'fastMAType='=>'int', 'slowPeriod='=>'int', 'slowMAType='=>'int', 'signalPeriod='=>'int', 'signalMAType='=>'int'], -'trader_macdfix' => ['array', 'real'=>'array', 'signalPeriod='=>'int'], -'trader_mama' => ['array', 'real'=>'array', 'fastLimit='=>'float', 'slowLimit='=>'float'], -'trader_mavp' => ['array', 'real'=>'array', 'periods'=>'array', 'minPeriod='=>'int', 'maxPeriod='=>'int', 'mAType='=>'int'], -'trader_max' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_maxindex' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_medprice' => ['array', 'high'=>'array', 'low'=>'array'], -'trader_mfi' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array', 'timePeriod='=>'int'], -'trader_midpoint' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_midprice' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_min' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_minindex' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_minmax' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_minmaxindex' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_minus_di' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_minus_dm' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_mom' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_mult' => ['array', 'real0'=>'array', 'real1'=>'array'], -'trader_natr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_obv' => ['array', 'real'=>'array', 'volume'=>'array'], -'trader_plus_di' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_plus_dm' => ['array', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], -'trader_ppo' => ['array', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'mAType='=>'int'], -'trader_roc' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_rocp' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_rocr' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_rocr100' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_rsi' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_sar' => ['array', 'high'=>'array', 'low'=>'array', 'acceleration='=>'float', 'maximum='=>'float'], -'trader_sarext' => ['array', 'high'=>'array', 'low'=>'array', 'startValue='=>'float', 'offsetOnReverse='=>'float', 'accelerationInitLong='=>'float', 'accelerationLong='=>'float', 'accelerationMaxLong='=>'float', 'accelerationInitShort='=>'float', 'accelerationShort='=>'float', 'accelerationMaxShort='=>'float'], +'trader_ht_dcperiod' => ['array|false', 'real'=>'array'], +'trader_ht_dcphase' => ['array|false', 'real'=>'array'], +'trader_ht_phasor' => ['array|false', 'real'=>'array'], +'trader_ht_sine' => ['array|false', 'real'=>'array'], +'trader_ht_trendline' => ['array|false', 'real'=>'array'], +'trader_ht_trendmode' => ['array|false', 'real'=>'array'], +'trader_kama' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_linearreg' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_linearreg_angle' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_linearreg_intercept' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_linearreg_slope' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_ln' => ['array|false', 'real'=>'array'], +'trader_log10' => ['array|false', 'real'=>'array'], +'trader_ma' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'mAType='=>'int'], +'trader_macd' => ['array|false', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'signalPeriod='=>'int'], +'trader_macdext' => ['array|false', 'real'=>'array', 'fastPeriod='=>'int', 'fastMAType='=>'int', 'slowPeriod='=>'int', 'slowMAType='=>'int', 'signalPeriod='=>'int', 'signalMAType='=>'int'], +'trader_macdfix' => ['array|false', 'real'=>'array', 'signalPeriod='=>'int'], +'trader_mama' => ['array|false', 'real'=>'array', 'fastLimit='=>'float', 'slowLimit='=>'float'], +'trader_mavp' => ['array|false', 'real'=>'array', 'periods'=>'array', 'minPeriod='=>'int', 'maxPeriod='=>'int', 'mAType='=>'int'], +'trader_max' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_maxindex' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_medprice' => ['array|false', 'high'=>'array', 'low'=>'array'], +'trader_mfi' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'volume'=>'array', 'timePeriod='=>'int'], +'trader_midpoint' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_midprice' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_min' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_minindex' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_minmax' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_minmaxindex' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_minus_di' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_minus_dm' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_mom' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_mult' => ['array|false', 'real0'=>'array', 'real1'=>'array'], +'trader_natr' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_obv' => ['array|false', 'real'=>'array', 'volume'=>'array'], +'trader_plus_di' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_plus_dm' => ['array|false', 'high'=>'array', 'low'=>'array', 'timePeriod='=>'int'], +'trader_ppo' => ['array|false', 'real'=>'array', 'fastPeriod='=>'int', 'slowPeriod='=>'int', 'mAType='=>'int'], +'trader_roc' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_rocp' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_rocr' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_rocr100' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_rsi' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_sar' => ['array|false', 'high'=>'array', 'low'=>'array', 'acceleration='=>'float', 'maximum='=>'float'], +'trader_sarext' => ['array|false', 'high'=>'array', 'low'=>'array', 'startValue='=>'float', 'offsetOnReverse='=>'float', 'accelerationInitLong='=>'float', 'accelerationLong='=>'float', 'accelerationMaxLong='=>'float', 'accelerationInitShort='=>'float', 'accelerationShort='=>'float', 'accelerationMaxShort='=>'float'], 'trader_set_compat' => ['void', 'compatId'=>'int'], 'trader_set_unstable_period' => ['void', 'functionId'=>'int', 'timePeriod'=>'int'], -'trader_sin' => ['array', 'real'=>'array'], -'trader_sinh' => ['array', 'real'=>'array'], -'trader_sma' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_sqrt' => ['array', 'real'=>'array'], -'trader_stddev' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'nbDev='=>'float'], -'trader_stoch' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'fastK_Period='=>'int', 'slowK_Period='=>'int', 'slowK_MAType='=>'int', 'slowD_Period='=>'int', 'slowD_MAType='=>'int'], -'trader_stochf' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'fastK_Period='=>'int', 'fastD_Period='=>'int', 'fastD_MAType='=>'int'], -'trader_stochrsi' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'fastK_Period='=>'int', 'fastD_Period='=>'int', 'fastD_MAType='=>'int'], -'trader_sub' => ['array', 'real0'=>'array', 'real1'=>'array'], -'trader_sum' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_t3' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'vFactor='=>'float'], -'trader_tan' => ['array', 'real'=>'array'], -'trader_tanh' => ['array', 'real'=>'array'], -'trader_tema' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_trange' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_trima' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_trix' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_tsf' => ['array', 'real'=>'array', 'timePeriod='=>'int'], -'trader_typprice' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_ultosc' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod1='=>'int', 'timePeriod2='=>'int', 'timePeriod3='=>'int'], -'trader_var' => ['array', 'real'=>'array', 'timePeriod='=>'int', 'nbDev='=>'float'], -'trader_wclprice' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array'], -'trader_willr' => ['array', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], -'trader_wma' => ['array', 'real'=>'array', 'timePeriod='=>'int'], +'trader_sin' => ['array|false', 'real'=>'array'], +'trader_sinh' => ['array|false', 'real'=>'array'], +'trader_sma' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_sqrt' => ['array|false', 'real'=>'array'], +'trader_stddev' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'nbDev='=>'float'], +'trader_stoch' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'fastK_Period='=>'int', 'slowK_Period='=>'int', 'slowK_MAType='=>'int', 'slowD_Period='=>'int', 'slowD_MAType='=>'int'], +'trader_stochf' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'fastK_Period='=>'int', 'fastD_Period='=>'int', 'fastD_MAType='=>'int'], +'trader_stochrsi' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'fastK_Period='=>'int', 'fastD_Period='=>'int', 'fastD_MAType='=>'int'], +'trader_sub' => ['array|false', 'real0'=>'array', 'real1'=>'array'], +'trader_sum' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_t3' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'vFactor='=>'float'], +'trader_tan' => ['array|false', 'real'=>'array'], +'trader_tanh' => ['array|false', 'real'=>'array'], +'trader_tema' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_trange' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_trima' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_trix' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_tsf' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], +'trader_typprice' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_ultosc' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod1='=>'int', 'timePeriod2='=>'int', 'timePeriod3='=>'int'], +'trader_var' => ['array|false', 'real'=>'array', 'timePeriod='=>'int', 'nbDev='=>'float'], +'trader_wclprice' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array'], +'trader_willr' => ['array|false', 'high'=>'array', 'low'=>'array', 'close'=>'array', 'timePeriod='=>'int'], +'trader_wma' => ['array|false', 'real'=>'array', 'timePeriod='=>'int'], 'trait_exists' => ['bool', 'traitname'=>'string', 'autoload='=>'bool'], 'Transliterator::create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'Transliterator::createFromRules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], -'Transliterator::createInverse' => ['Transliterator'], -'Transliterator::getErrorCode' => ['int'], -'Transliterator::getErrorMessage' => ['string'], -'Transliterator::listIDs' => ['array'], +'Transliterator::createInverse' => ['?Transliterator'], +'Transliterator::getErrorCode' => ['int|false'], +'Transliterator::getErrorMessage' => ['string|false'], +'Transliterator::listIDs' => ['list|false'], 'Transliterator::transliterate' => ['string|false', 'subject'=>'string', 'start='=>'int', 'end='=>'int'], 'transliterator_create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'transliterator_create_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], -'transliterator_create_inverse' => ['Transliterator', 'obj'=>'Transliterator'], +'transliterator_create_inverse' => ['?Transliterator', 'obj'=>'Transliterator'], 'transliterator_get_error_code' => ['int|false', 'obj'=>'Transliterator'], 'transliterator_get_error_message' => ['string|false', 'obj'=>'Transliterator'], -'transliterator_list_ids' => ['array|false'], +'transliterator_list_ids' => ['list|false'], 'transliterator_transliterate' => ['string|false', 'obj'=>'Transliterator|string', 'subject'=>'string', 'start='=>'int', 'end='=>'int'], 'trigger_error' => ['bool', 'message'=>'string', 'error_type='=>'int'], 'trim' => ['string', 'str'=>'string', 'character_mask='=>'string'], @@ -12558,7 +12848,7 @@ 'TypeError::getLine' => ['int'], 'TypeError::getMessage' => ['string'], 'TypeError::getPrevious' => ['Throwable|TypeError|null'], -'TypeError::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'TypeError::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'TypeError::getTraceAsString' => ['string'], 'uasort' => ['bool', '&rw_array_arg'=>'array', 'callback'=>'callable(mixed,mixed):int'], 'ucfirst' => ['string', 'str'=>'string'], @@ -12619,7 +12909,7 @@ 'UnderflowException::getLine' => ['int'], 'UnderflowException::getMessage' => ['string'], 'UnderflowException::getPrevious' => ['Throwable|UnderflowException|null'], -'UnderflowException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'UnderflowException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'UnderflowException::getTraceAsString' => ['string'], 'UnexpectedValueException::__clone' => ['void'], 'UnexpectedValueException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?UnexpectedValueException)'], @@ -12629,7 +12919,7 @@ 'UnexpectedValueException::getLine' => ['int'], 'UnexpectedValueException::getMessage' => ['string'], 'UnexpectedValueException::getPrevious' => ['Throwable|UnexpectedValueException|null'], -'UnexpectedValueException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'UnexpectedValueException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'UnexpectedValueException::getTraceAsString' => ['string'], 'uniqid' => ['non-empty-string', 'prefix='=>'string', 'more_entropy='=>'bool'], 'unixtojd' => ['int|false', 'timestamp='=>'int'], @@ -12639,7 +12929,7 @@ 'unserialize' => ['mixed', 'variable_representation'=>'string', 'allowed_classes='=>'array{allowed_classes?:string[]|bool}'], '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'], +'uopz_add_function\'1' => ['bool', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool'], 'uopz_allow_exit' => ['void', 'allow'=>'bool'], 'uopz_backup' => ['void', 'class'=>'string', 'function'=>'string'], 'uopz_backup\'1' => ['void', 'function'=>'string'], @@ -12647,23 +12937,23 @@ 'uopz_copy' => ['Closure', 'class'=>'string', 'function'=>'string'], 'uopz_copy\'1' => ['Closure', 'function'=>'string'], 'uopz_del_function' => ['bool', 'class'=>'string', 'function'=>'string', '$all'=>'bool'], -'uopz_del_function\1' => ['bool', 'function'=>'string'], +'uopz_del_function\'1' => ['bool', 'function'=>'string'], 'uopz_delete' => ['void', 'class'=>'string', 'function'=>'string'], 'uopz_delete\'1' => ['void', 'function'=>'string'], 'uopz_extend' => ['void', 'class'=>'string', 'parent'=>'string'], -'uopz_flags' => ['int', 'class'=>'string', 'function'=>'string', 'flags'=>'int'], -'uopz_flags\'1' => ['int', 'function'=>'string', 'flags'=>'int'], +'uopz_flags' => ['int', 'class'=>'string', 'function'=>'string', 'flags='=>'int'], +'uopz_flags\'1' => ['int', 'function'=>'string', 'flags='=>'int'], 'uopz_function' => ['void', 'class'=>'string', 'function'=>'string', 'handler'=>'Closure', 'modifiers='=>'int'], 'uopz_function\'1' => ['void', 'function'=>'string', 'handler'=>'Closure', 'modifiers='=>'int'], 'uopz_get_exit_status' => ['mixed'], 'uopz_get_hook' => ['Closure', 'class'=>'string', 'function'=>'string'], -'uopz_get_hook\1' => ['Closure', 'function'=>'string'], +'uopz_get_hook\'1' => ['Closure', 'function'=>'string'], 'uopz_get_mock' => ['mixed', 'class'=>'string'], -'uopz_get_property' => ['void', 'class'=>'string', 'property'=>'string'], -'uopz_get_property\1' => ['void', 'instance'=>'object', 'property'=>'string'], +'uopz_get_property' => ['mixed', 'class'=>'string', 'property'=>'string'], +'uopz_get_property\'1' => ['mixed', 'instance'=>'object', 'property'=>'string'], 'uopz_get_return' => ['mixed', 'class='=>'string', 'function='=>'string'], 'uopz_get_static' => ['array', 'class='=>'string', 'function='=>'string'], -'uopz_get_static\1' => ['array', 'function='=>'string'], +'uopz_get_static\'1' => ['array', 'function='=>'string'], 'uopz_implement' => ['void', 'class'=>'string', 'interface'=>'string'], 'uopz_overload' => ['void', 'opcode'=>'int', 'callable'=>'Callable'], 'uopz_redefine' => ['void', 'class'=>'string', 'constant'=>'string', 'value'=>'mixed'], @@ -12674,13 +12964,13 @@ 'uopz_restore\'1' => ['void', 'function'=>'string'], 'uopz_set_mock' => ['void', 'class'=>'string', 'mock'=>'object|string'], 'uopz_set_property' => ['void', 'class'=>'string', 'property'=>'string', 'value'=>'mixed'], -'uopz_set_property\1' => ['void', 'instance'=>'object', 'property'=>'string', 'value'=>'mixed'], +'uopz_set_property\'1' => ['void', 'instance'=>'object', 'property'=>'string', 'value'=>'mixed'], 'uopz_set_return' => ['bool', 'class'=>'string', 'function'=>'string', 'value'=>'mixed', 'execute='=>'bool'], 'uopz_set_return\'1' => ['bool', 'function'=>'string', 'value'=>'mixed', 'execute='=>'bool'], 'uopz_undefine' => ['void', 'class'=>'string', 'constant'=>'string'], 'uopz_undefine\'1' => ['void', 'constant'=>'string'], 'uopz_set_hook' => ['bool', 'class'=>'string', 'function'=>'string', 'hook'=>'Closure'], -'uopz_set_hook\1' => ['bool', 'function'=>'string', 'hook'=>'Closure'], +'uopz_set_hook\'1' => ['bool', 'function'=>'string', 'hook'=>'Closure'], 'uopz_unset_mock' => ['void', 'class'=>'string'], 'uopz_unset_return' => ['bool', 'class='=>'string', 'function='=>'string'], 'uopz_unset_return\'1' => ['bool', 'function'=>'string'], @@ -12724,7 +13014,7 @@ 'V8JsScriptException::getLine' => ['int'], 'V8JsScriptException::getMessage' => ['string'], 'V8JsScriptException::getPrevious' => ['Exception|Throwable'], -'V8JsScriptException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], +'V8JsScriptException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'V8JsScriptException::getTraceAsString' => ['string'], 'var_dump' => ['void', 'var'=>'mixed', '...args='=>'mixed'], 'var_export' => ['string|null', 'var'=>'mixed', 'return='=>'bool'], @@ -12780,8 +13070,8 @@ 'VarnishStat::__construct' => ['void', 'args='=>'array'], 'VarnishStat::getSnapshot' => ['array'], 'version_compare' => ['int', 'version1'=>'string', 'version2'=>'string'], -'version_compare\'1' => ['bool', 'version1'=>'string', 'version2'=>'string', 'operator'=>'string'], -'vfprintf' => ['int', 'stream'=>'resource', 'format'=>'string', 'args'=>'array'], +'version_compare\'1' => ['bool', 'version1'=>'string', 'version2'=>'string', 'operator'=>'string|null'], +'vfprintf' => ['int', 'stream'=>'resource', 'format'=>'string', 'args'=>'array<__stringAndStringable|int|float|null|bool>'], 'virtual' => ['bool', 'uri'=>'string'], 'Volatile::__construct' => ['void'], 'Volatile::chunk' => ['array', 'size'=>'int', 'preserve'=>'bool'], @@ -12818,8 +13108,8 @@ 'vpopmail_error' => ['string'], 'vpopmail_passwd' => ['bool', 'user'=>'string', 'domain'=>'string', 'password'=>'string', 'apop='=>'bool'], 'vpopmail_set_user_quota' => ['bool', 'user'=>'string', 'domain'=>'string', 'quota'=>'string'], -'vprintf' => ['int', 'format'=>'string', 'args'=>'array'], -'vsprintf' => ['string', 'format'=>'string', 'args'=>'array'], +'vprintf' => ['int', 'format'=>'string', 'args'=>'array<__stringAndStringable|int|float|null|bool>'], +'vsprintf' => ['string', 'format'=>'string', 'args'=>'array<__stringAndStringable|int|float|null|bool>'], 'w32api_deftype' => ['bool', 'typename'=>'string', 'member1_type'=>'string', 'member1_name'=>'string', '...args='=>'string'], 'w32api_init_dtype' => ['resource', 'typename'=>'string', 'value'=>'', '...args='=>''], 'w32api_invoke_function' => ['', 'funcname'=>'string', 'argument'=>'', '...args='=>''], @@ -12961,10 +13251,10 @@ 'Xcom::send' => ['int', 'topic'=>'string', 'data'=>'mixed', 'json_schema='=>'string', 'http_headers='=>'array'], 'Xcom::sendAsync' => ['int', 'topic'=>'string', 'data'=>'mixed', 'json_schema='=>'string', 'http_headers='=>'array'], 'xdebug_break' => ['bool'], -'xdebug_call_class' => ['string', 'depth=' => 'int'], -'xdebug_call_file' => ['string', 'depth=' => 'int'], -'xdebug_call_function' => ['string', 'depth=' => 'int'], -'xdebug_call_line' => ['int', 'depth=' => 'int'], +'xdebug_call_class' => ['string', 'depth='=>'int'], +'xdebug_call_file' => ['string', 'depth='=>'int'], +'xdebug_call_function' => ['string', 'depth='=>'int'], +'xdebug_call_line' => ['int', 'depth='=>'int'], 'xdebug_clear_aggr_profiling_data' => ['bool'], 'xdebug_code_coverage_started' => ['bool'], 'xdebug_connect_to_client' => ['bool'], @@ -12979,7 +13269,7 @@ 'xdebug_get_declared_vars' => ['array'], 'xdebug_get_formatted_function_stack' => [''], 'xdebug_get_function_count' => ['int'], -'xdebug_get_function_stack' => ['array', 'message='=>'string', 'options='=>'int'], +'xdebug_get_function_stack' => ['array', 'options='=>'array{local_vars?: bool, params_as_values?: bool, from_exception?: Throwable}'], 'xdebug_get_headers' => ['array'], 'xdebug_get_monitored_functions' => ['array'], 'xdebug_get_profiler_filename' => ['string'], @@ -12988,10 +13278,10 @@ 'xdebug_is_debugger_active' => ['bool'], 'xdebug_is_enabled' => ['bool'], 'xdebug_memory_usage' => ['int'], -'xdebug_notify' => ['bool', 'data' => 'mixed'], +'xdebug_notify' => ['bool', 'data'=>'mixed'], 'xdebug_peak_memory_usage' => ['int'], -'xdebug_print_function_stack' => ['array', 'message='=>'string', 'options=' => 'int'], -'xdebug_set_filter' => ['void', 'group' => 'int', 'list_type' => 'int', 'configuration' => 'array'], +'xdebug_print_function_stack' => ['array', 'message='=>'string', 'options='=>'int'], +'xdebug_set_filter' => ['void', 'group'=>'int', 'list_type'=>'int', 'configuration'=>'array'], 'xdebug_start_code_coverage' => ['void', 'options='=>'int'], 'xdebug_start_error_collection' => ['void'], 'xdebug_start_function_monitor' => ['void', 'list_of_functions_to_monitor'=>'string[]'], @@ -13022,7 +13312,7 @@ 'xdiff_string_rabdiff' => ['string', 'old_data'=>'string', 'new_data'=>'string'], 'xhprof_disable' => ['array'], 'xhprof_enable' => ['void', 'flags='=>'int', 'options='=>'array'], -'xhprof_sample_disable' => ['array'], +'xhprof_sample_disable' => ['array'], 'xhprof_sample_enable' => ['void'], 'xml_error_string' => ['string', 'code'=>'int'], 'xml_get_current_byte_index' => ['int', 'parser'=>'resource'], @@ -13079,7 +13369,7 @@ 'XMLReader::setRelaxNGSchema' => ['bool', 'filename'=>'string'], 'XMLReader::setRelaxNGSchemaSource' => ['bool', 'source'=>'string'], 'XMLReader::setSchema' => ['bool', 'filename'=>'string'], -'XMLReader::XML' => ['bool', 'source'=>'string', 'encoding='=>'?string', 'options='=>'int'], +'XMLReader::XML' => ['bool|XMLReader', 'source'=>'string', 'encoding='=>'?string', 'options='=>'int'], 'xmlrpc_decode' => ['?array', 'xml'=>'string', 'encoding='=>'string'], 'xmlrpc_decode_request' => ['?array', 'xml'=>'string', '&w_method'=>'string', 'encoding='=>'string'], 'xmlrpc_encode' => ['string', 'value'=>'mixed'], @@ -13121,7 +13411,7 @@ 'XMLWriter::startDTDElement' => ['bool', 'qualifiedname'=>'string'], 'XMLWriter::startDTDEntity' => ['bool', 'name'=>'string', 'isparam'=>'bool'], 'XMLWriter::startElement' => ['bool', 'name'=>'string'], -'XMLWriter::startElementNS' => ['bool', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string'], +'XMLWriter::startElementNS' => ['bool', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string|null'], 'XMLWriter::startPI' => ['bool', 'target'=>'string'], 'XMLWriter::text' => ['bool', 'content'=>'string'], 'XMLWriter::writeAttribute' => ['bool', 'name'=>'string', 'value'=>'string'], @@ -13163,7 +13453,7 @@ 'xmlwriter_start_dtd_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], 'xmlwriter_start_dtd_entity' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'isparam'=>'bool'], 'xmlwriter_start_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], -'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string'], +'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string|null'], 'xmlwriter_start_pi' => ['bool', 'xmlwriter'=>'resource', 'target'=>'string'], 'xmlwriter_text' => ['bool', 'xmlwriter'=>'resource', 'content'=>'string'], 'xmlwriter_write_attribute' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string'], @@ -13246,7 +13536,7 @@ 'Yaf_Application::__clone' => ['void'], 'Yaf_Application::__construct' => ['void', 'config'=>'mixed', 'envrion='=>'string'], 'Yaf_Application::__destruct' => ['void'], -'Yaf_Application::__sleep' => ['void'], +'Yaf_Application::__sleep' => ['list'], 'Yaf_Application::__wakeup' => ['void'], 'Yaf_Application::app' => ['void'], 'Yaf_Application::bootstrap' => ['void', 'bootstrap='=>'Yaf_Bootstrap_Abstract'], @@ -13261,93 +13551,113 @@ 'Yaf_Application::getModules' => ['array'], 'Yaf_Application::run' => ['void'], 'Yaf_Application::setAppDirectory' => ['Yaf_Application', 'directory'=>'string'], -'Yaf_Config_Abstract::get' => ['mixed', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Abstract::__get' => ['mixed', 'name'=>'string'], +'Yaf_Config_Abstract::__isset' => ['bool', 'name'=>'string'], +'Yaf_Config_Abstract::count' => ['0|positive-int'], +'Yaf_Config_Abstract::current' => ['mixed'], +'Yaf_Config_Abstract::get' => ['mixed', 'name'=>'?string'], +'Yaf_Config_Abstract::key' => ['int|string|null|bool'], +'Yaf_Config_Abstract::next' => ['void'], +'Yaf_Config_Abstract::offsetExists' => ['bool', 'name'=>'mixed'], +'Yaf_Config_Abstract::offsetGet' => ['mixed', 'name'=>'mixed'], +'Yaf_Config_Abstract::offsetSet' => ['void', 'name'=>'mixed', 'value'=>'mixed'], +'Yaf_Config_Abstract::offsetUnset' => ['void', 'name'=>'mixed'], 'Yaf_Config_Abstract::readonly' => ['bool'], -'Yaf_Config_Abstract::set' => ['Yaf_Config_Abstract'], +'Yaf_Config_Abstract::rewind' => ['void'], +'Yaf_Config_Abstract::set' => ['bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Abstract::toArray' => ['array'], -'Yaf_Config_Ini::__construct' => ['void', 'config_file'=>'string', 'section='=>'string'], -'Yaf_Config_Ini::__get' => ['void', 'name='=>'string'], -'Yaf_Config_Ini::__isset' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::__set' => ['void', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Abstract::valid' => ['bool'], +'Yaf_Config_Ini::__construct' => ['void', 'config_file'=>'array|string', 'section='=>'?string'], +'Yaf_Config_Ini::__isset' => ['bool', 'name'=>'string'], +'Yaf_Config_Ini::__set' => ['void', 'name'=>'mixed', 'value'=>'mixed'], 'Yaf_Config_Ini::count' => ['0|positive-int'], -'Yaf_Config_Ini::current' => ['void'], -'Yaf_Config_Ini::get' => ['mixed', 'name='=>'mixed'], -'Yaf_Config_Ini::key' => ['void'], +'Yaf_Config_Ini::current' => ['mixed'], +'Yaf_Config_Ini::get' => ['mixed', 'name='=>'?string'], +'Yaf_Config_Ini::key' => ['int|string|null|bool'], 'Yaf_Config_Ini::next' => ['void'], -'Yaf_Config_Ini::offsetExists' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::offsetGet' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::offsetSet' => ['void', 'name'=>'string', 'value'=>'string'], -'Yaf_Config_Ini::offsetUnset' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::readonly' => ['void'], +'Yaf_Config_Ini::offsetExists' => ['bool', 'name'=>'mixed'], +'Yaf_Config_Ini::offsetGet' => ['mixed', 'name'=>'mixed'], +'Yaf_Config_Ini::offsetSet' => ['void', 'name'=>'mixed', 'value'=>'mixed'], +'Yaf_Config_Ini::offsetUnset' => ['void', 'name'=>'mixed'], +'Yaf_Config_Ini::readonly' => ['bool'], 'Yaf_Config_Ini::rewind' => ['void'], -'Yaf_Config_Ini::set' => ['Yaf_Config_Abstract', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Ini::set' => ['bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Ini::toArray' => ['array'], -'Yaf_Config_Ini::valid' => ['void'], -'Yaf_Config_Simple::__construct' => ['void', 'config_file'=>'string', 'section='=>'string'], +'Yaf_Config_Ini::valid' => ['bool'], +'Yaf_Config_Simple::__construct' => ['void', 'config_file'=>'array|string', 'section='=>'string'], 'Yaf_Config_Simple::__get' => ['void', 'name='=>'string'], -'Yaf_Config_Simple::__isset' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::__set' => ['void', 'name'=>'string', 'value'=>'string'], +'Yaf_Config_Simple::__isset' => ['bool', 'name'=>'string'], +'Yaf_Config_Simple::__set' => ['void', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Simple::count' => ['0|positive-int'], -'Yaf_Config_Simple::current' => ['void'], -'Yaf_Config_Simple::get' => ['mixed', 'name='=>'mixed'], -'Yaf_Config_Simple::key' => ['void'], +'Yaf_Config_Simple::current' => ['mixed'], +'Yaf_Config_Simple::get' => ['mixed', 'name='=>'?string'], +'Yaf_Config_Simple::key' => ['int|string|null|bool'], 'Yaf_Config_Simple::next' => ['void'], -'Yaf_Config_Simple::offsetExists' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::offsetGet' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::offsetSet' => ['void', 'name'=>'string', 'value'=>'string'], -'Yaf_Config_Simple::offsetUnset' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::readonly' => ['void'], +'Yaf_Config_Simple::offsetExists' => ['bool', 'name'=>'mixed'], +'Yaf_Config_Simple::offsetGet' => ['mixed', 'name'=>'mixed'], +'Yaf_Config_Simple::offsetSet' => ['void', 'name'=>'mixed', 'value'=>'mixed'], +'Yaf_Config_Simple::offsetUnset' => ['void', 'name'=>'mixed'], +'Yaf_Config_Simple::readonly' => ['bool'], 'Yaf_Config_Simple::rewind' => ['void'], -'Yaf_Config_Simple::set' => ['Yaf_Config_Abstract', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Simple::set' => ['bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Simple::toArray' => ['array'], -'Yaf_Config_Simple::valid' => ['void'], +'Yaf_Config_Simple::valid' => ['bool'], 'Yaf_Controller_Abstract::__clone' => ['void'], 'Yaf_Controller_Abstract::__construct' => ['void'], -'Yaf_Controller_Abstract::display' => ['bool', 'tpl'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::forward' => ['void', 'action'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::forward\'1' => ['void', 'controller'=>'string', 'action'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::forward\'2' => ['void', 'module'=>'string', 'controller'=>'string', 'action'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::getInvokeArg' => ['void', 'name'=>'string'], -'Yaf_Controller_Abstract::getInvokeArgs' => ['void'], -'Yaf_Controller_Abstract::getModuleName' => ['string'], -'Yaf_Controller_Abstract::getRequest' => ['Yaf_Request_Abstract'], -'Yaf_Controller_Abstract::getResponse' => ['Yaf_Response_Abstract'], -'Yaf_Controller_Abstract::getView' => ['Yaf_View_Interface'], -'Yaf_Controller_Abstract::getViewpath' => ['void'], +'Yaf_Controller_Abstract::display' => ['?bool', 'tpl'=>'string', 'parameters='=>'?array'], +'Yaf_Controller_Abstract::forward' => ['?bool', 'action'=>'string'], +'Yaf_Controller_Abstract::forward\'1' => ['?bool', 'controller'=>'string', 'action'=>'string'], +'Yaf_Controller_Abstract::forward\'2' => ['?bool', 'action'=>'string', 'invoke_args'=>'array'], +'Yaf_Controller_Abstract::forward\'3' => ['?bool', 'module'=>'string', 'controller'=>'string', 'action'=>'string'], +'Yaf_Controller_Abstract::forward\'4' => ['?bool', 'controller'=>'string', 'action'=>'string', 'invoke_args'=>'array'], +'Yaf_Controller_Abstract::forward\'5' => ['?bool', 'module'=>'string', 'controller'=>'string', 'action'=>'string', 'invoke_args'=>'array'], +'Yaf_Controller_Abstract::getInvokeArg' => ['?string', 'name'=>'string'], +'Yaf_Controller_Abstract::getInvokeArgs' => ['?array'], +'Yaf_Controller_Abstract::getModuleName' => ['?string'], +'Yaf_Controller_Abstract::getName' => ['?string'], +'Yaf_Controller_Abstract::getRequest' => ['?Yaf_Request_Abstract'], +'Yaf_Controller_Abstract::getResponse' => ['?Yaf_Response_Abstract'], +'Yaf_Controller_Abstract::getView' => ['?Yaf_View_Interface'], +'Yaf_Controller_Abstract::getViewpath' => ['?string'], 'Yaf_Controller_Abstract::init' => ['void'], -'Yaf_Controller_Abstract::initView' => ['void', 'options='=>'array'], -'Yaf_Controller_Abstract::redirect' => ['bool', 'url'=>'string'], -'Yaf_Controller_Abstract::render' => ['string', 'tpl'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::setViewpath' => ['void', 'view_directory'=>'string'], +'Yaf_Controller_Abstract::initView' => ['?Yaf_View_Interface', 'options='=>'?array'], +'Yaf_Controller_Abstract::redirect' => ['?bool', 'url'=>'string'], +'Yaf_Controller_Abstract::render' => ['string|null|bool', 'tpl'=>'string', 'parameters='=>'?array'], +'Yaf_Controller_Abstract::setViewpath' => ['?bool', 'view_directory'=>'string'], 'Yaf_Dispatcher::__clone' => ['void'], 'Yaf_Dispatcher::__construct' => ['void'], -'Yaf_Dispatcher::__sleep' => ['void'], +'Yaf_Dispatcher::__sleep' => ['list'], 'Yaf_Dispatcher::__wakeup' => ['void'], -'Yaf_Dispatcher::autoRender' => ['Yaf_Dispatcher', 'flag='=>'bool'], -'Yaf_Dispatcher::catchException' => ['Yaf_Dispatcher', 'flag='=>'bool'], -'Yaf_Dispatcher::disableView' => ['bool'], -'Yaf_Dispatcher::dispatch' => ['Yaf_Response_Abstract', 'request'=>'Yaf_Request_Abstract'], -'Yaf_Dispatcher::enableView' => ['Yaf_Dispatcher'], -'Yaf_Dispatcher::flushInstantly' => ['Yaf_Dispatcher', 'flag='=>'bool'], -'Yaf_Dispatcher::getApplication' => ['Yaf_Application'], -'Yaf_Dispatcher::getInstance' => ['Yaf_Dispatcher'], -'Yaf_Dispatcher::getRequest' => ['Yaf_Request_Abstract'], -'Yaf_Dispatcher::getRouter' => ['Yaf_Router'], -'Yaf_Dispatcher::initView' => ['Yaf_View_Interface', 'templates_dir'=>'string', 'options='=>'array'], -'Yaf_Dispatcher::registerPlugin' => ['Yaf_Dispatcher', 'plugin'=>'Yaf_Plugin_Abstract'], -'Yaf_Dispatcher::returnResponse' => ['Yaf_Dispatcher', 'flag'=>'bool'], -'Yaf_Dispatcher::setDefaultAction' => ['Yaf_Dispatcher', 'action'=>'string'], -'Yaf_Dispatcher::setDefaultController' => ['Yaf_Dispatcher', 'controller'=>'string'], -'Yaf_Dispatcher::setDefaultModule' => ['Yaf_Dispatcher', 'module'=>'string'], -'Yaf_Dispatcher::setErrorHandler' => ['Yaf_Dispatcher', 'callback'=>'call', 'error_types'=>'int'], -'Yaf_Dispatcher::setRequest' => ['Yaf_Dispatcher', 'request'=>'Yaf_Request_Abstract'], -'Yaf_Dispatcher::setView' => ['Yaf_Dispatcher', 'view'=>'Yaf_View_Interface'], -'Yaf_Dispatcher::throwException' => ['Yaf_Dispatcher', 'flag='=>'bool'], +'Yaf_Dispatcher::autoRender' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], +'Yaf_Dispatcher::catchException' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], +'Yaf_Dispatcher::disableView' => ['?Yaf_Dispatcher'], +'Yaf_Dispatcher::dispatch' => ['Yaf_Response_Abstract|false|null', 'request'=>'Yaf_Request_Abstract'], +'Yaf_Dispatcher::enableView' => ['?Yaf_Dispatcher'], +'Yaf_Dispatcher::flushInstantly' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], +'Yaf_Dispatcher::getApplication' => ['?Yaf_Application'], +'Yaf_Dispatcher::getDefaultAction' => ['?string'], +'Yaf_Dispatcher::getDefaultController' => ['?string'], +'Yaf_Dispatcher::getDefaultModule' => ['?string'], +'Yaf_Dispatcher::getInstance' => ['?Yaf_Dispatcher'], +'Yaf_Dispatcher::getRequest' => ['?Yaf_Request_Abstract'], +'Yaf_Dispatcher::getResponse' => ['?Yaf_Response_Abstract'], +'Yaf_Dispatcher::getRouter' => ['?Yaf_Router'], +'Yaf_Dispatcher::initView' => ['Yaf_View_Interface|null|false', 'templates_dir'=>'string', 'options='=>'?array'], +'Yaf_Dispatcher::registerPlugin' => ['Yaf_Dispatcher|false|null', 'plugin'=>'Yaf_Plugin_Abstract'], +'Yaf_Dispatcher::returnResponse' => ['Yaf_Dispatcher|false|null', 'flag='=>'bool'], +'Yaf_Dispatcher::setDefaultAction' => ['Yaf_Dispatcher|false|null', 'action'=>'string'], +'Yaf_Dispatcher::setDefaultController' => ['Yaf_Dispatcher|false|null', 'controller'=>'string'], +'Yaf_Dispatcher::setDefaultModule' => ['Yaf_Dispatcher|false|null', 'module'=>'string'], +'Yaf_Dispatcher::setErrorHandler' => ['Yaf_Dispatcher|false|null', 'callback'=>'mixed', 'error_types'=>'int'], +'Yaf_Dispatcher::setRequest' => ['?Yaf_Dispatcher', 'request'=>'Yaf_Request_Abstract'], +'Yaf_Dispatcher::setResponse' => ['?Yaf_Dispatcher', 'response'=>'Yaf_Response_Abstract'], +'Yaf_Dispatcher::setView' => ['?Yaf_Dispatcher', 'view'=>'Yaf_View_Interface'], +'Yaf_Dispatcher::throwException' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], 'Yaf_Exception::__construct' => ['void'], 'Yaf_Exception::getPrevious' => ['void'], 'Yaf_Loader::__clone' => ['void'], 'Yaf_Loader::__construct' => ['void'], -'Yaf_Loader::__sleep' => ['void'], +'Yaf_Loader::__sleep' => ['list'], 'Yaf_Loader::__wakeup' => ['void'], 'Yaf_Loader::autoload' => ['void'], 'Yaf_Loader::clearLocalNamespace' => ['void'], @@ -13371,149 +13681,163 @@ 'Yaf_Registry::get' => ['mixed', 'name'=>'string'], 'Yaf_Registry::has' => ['bool', 'name'=>'string'], 'Yaf_Registry::set' => ['bool', 'name'=>'string', 'value'=>'string'], -'Yaf_Request_Abstract::getActionName' => ['void'], -'Yaf_Request_Abstract::getBaseUri' => ['void'], -'Yaf_Request_Abstract::getControllerName' => ['void'], -'Yaf_Request_Abstract::getEnv' => ['void', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Abstract::getException' => ['void'], -'Yaf_Request_Abstract::getLanguage' => ['void'], -'Yaf_Request_Abstract::getMethod' => ['void'], -'Yaf_Request_Abstract::getModuleName' => ['void'], -'Yaf_Request_Abstract::getParam' => ['void', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Abstract::getParams' => ['void'], -'Yaf_Request_Abstract::getRequestUri' => ['void'], -'Yaf_Request_Abstract::getServer' => ['void', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Abstract::isCli' => ['void'], -'Yaf_Request_Abstract::isDispatched' => ['void'], -'Yaf_Request_Abstract::isGet' => ['void'], -'Yaf_Request_Abstract::isHead' => ['void'], -'Yaf_Request_Abstract::isOptions' => ['void'], -'Yaf_Request_Abstract::isPost' => ['void'], -'Yaf_Request_Abstract::isPut' => ['void'], -'Yaf_Request_Abstract::isRouted' => ['void'], -'Yaf_Request_Abstract::isXmlHttpRequest' => ['void'], -'Yaf_Request_Abstract::setActionName' => ['void', 'action'=>'string'], -'Yaf_Request_Abstract::setBaseUri' => ['bool', 'uir'=>'string'], -'Yaf_Request_Abstract::setControllerName' => ['void', 'controller'=>'string'], -'Yaf_Request_Abstract::setDispatched' => ['void'], -'Yaf_Request_Abstract::setModuleName' => ['void', 'module'=>'string'], -'Yaf_Request_Abstract::setParam' => ['void', 'name'=>'string', 'value='=>'string'], -'Yaf_Request_Abstract::setRequestUri' => ['void', 'uir'=>'string'], -'Yaf_Request_Abstract::setRouted' => ['void', 'flag='=>'string'], +'Yaf_Request_Abstract::get' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getActionName' => ['?string'], +'Yaf_Request_Abstract::getBaseUri' => ['?string'], +'Yaf_Request_Abstract::getCookie' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getControllerName' => ['?string'], +'Yaf_Request_Abstract::getEnv' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getException' => ['?Exception'], +'Yaf_Request_Abstract::getFiles' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getLanguage' => ['?string'], +'Yaf_Request_Abstract::getMethod' => ['?string'], +'Yaf_Request_Abstract::getModuleName' => ['?string'], +'Yaf_Request_Abstract::getParam' => ['mixed', 'name'=>'string', 'default='=>'mixed'], +'Yaf_Request_Abstract::getParams' => ['?array'], +'Yaf_Request_Abstract::getPost' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getQuery' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getRaw' => ['?string'], +'Yaf_Request_Abstract::getRequest' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getRequestUri' => ['?string'], +'Yaf_Request_Abstract::getServer' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::cleanParams' => ['?Yaf_Request_Abstract'], +'Yaf_Request_Abstract::isCli' => ['bool'], +'Yaf_Request_Abstract::isDelete' => ['bool'], +'Yaf_Request_Abstract::isDispatched' => ['bool'], +'Yaf_Request_Abstract::isGet' => ['bool'], +'Yaf_Request_Abstract::isHead' => ['bool'], +'Yaf_Request_Abstract::isOptions' => ['bool'], +'Yaf_Request_Abstract::isPatch' => ['bool'], +'Yaf_Request_Abstract::isPost' => ['bool'], +'Yaf_Request_Abstract::isPut' => ['bool'], +'Yaf_Request_Abstract::isRouted' => ['bool'], +'Yaf_Request_Abstract::isXmlHttpRequest' => ['bool'], +'Yaf_Request_Abstract::setActionName' => ['?Yaf_Request_Abstract', 'action'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Abstract::setBaseUri' => ['Yaf_Request_Abstract|false', 'uir'=>'string'], +'Yaf_Request_Abstract::setControllerName' => ['?Yaf_Request_Abstract', 'controller'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Abstract::setDispatched' => ['?Yaf_Request_Abstract', 'flag='=>'bool|true'], +'Yaf_Request_Abstract::setModuleName' => ['?Yaf_Request_Abstract', 'module'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Abstract::setParam' => ['Yaf_Request_Abstract|false|null', 'name'=>'mixed', 'value='=>'?mixed'], +'Yaf_Request_Abstract::setRequestUri' => ['?Yaf_Request_Abstract', 'uir'=>'string'], +'Yaf_Request_Abstract::setRouted' => ['?Yaf_Request_Abstract', 'flag='=>'bool|true'], 'Yaf_Request_Http::__clone' => ['void'], -'Yaf_Request_Http::__construct' => ['void'], -'Yaf_Request_Http::get' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getActionName' => ['string'], -'Yaf_Request_Http::getBaseUri' => ['string'], -'Yaf_Request_Http::getControllerName' => ['string'], -'Yaf_Request_Http::getCookie' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getEnv' => ['mixed', 'name='=>'string', 'default='=>'mixed'], -'Yaf_Request_Http::getException' => ['Yaf_Exception'], -'Yaf_Request_Http::getFiles' => ['void'], -'Yaf_Request_Http::getLanguage' => ['string'], -'Yaf_Request_Http::getMethod' => ['string'], -'Yaf_Request_Http::getModuleName' => ['string'], +'Yaf_Request_Http::__construct' => ['void', 'requestUri='=>'?string', 'baseUri='=>'?string'], +'Yaf_Request_Http::get' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getActionName' => ['?string'], +'Yaf_Request_Http::getBaseUri' => ['?string'], +'Yaf_Request_Http::getCookie' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getControllerName' => ['?string'], +'Yaf_Request_Http::getEnv' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getException' => ['?Exception'], +'Yaf_Request_Http::getFiles' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getLanguage' => ['?string'], +'Yaf_Request_Http::getMethod' => ['?string'], +'Yaf_Request_Http::getModuleName' => ['?string'], 'Yaf_Request_Http::getParam' => ['mixed', 'name'=>'string', 'default='=>'mixed'], -'Yaf_Request_Http::getParams' => ['array'], -'Yaf_Request_Http::getPost' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getQuery' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getRaw' => ['mixed'], -'Yaf_Request_Http::getRequest' => ['void'], -'Yaf_Request_Http::getRequestUri' => ['string'], -'Yaf_Request_Http::getServer' => ['mixed', 'name='=>'string', 'default='=>'mixed'], +'Yaf_Request_Http::getParams' => ['?array'], +'Yaf_Request_Http::getPost' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getQuery' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getRaw' => ['?string'], +'Yaf_Request_Http::getRequest' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getRequestUri' => ['?string'], +'Yaf_Request_Http::getServer' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::cleanParams' => ['?Yaf_Request_Http'], 'Yaf_Request_Http::isCli' => ['bool'], +'Yaf_Request_Http::isDelete' => ['bool'], 'Yaf_Request_Http::isDispatched' => ['bool'], 'Yaf_Request_Http::isGet' => ['bool'], 'Yaf_Request_Http::isHead' => ['bool'], 'Yaf_Request_Http::isOptions' => ['bool'], +'Yaf_Request_Http::isPatch' => ['bool'], 'Yaf_Request_Http::isPost' => ['bool'], 'Yaf_Request_Http::isPut' => ['bool'], 'Yaf_Request_Http::isRouted' => ['bool'], 'Yaf_Request_Http::isXmlHttpRequest' => ['bool'], -'Yaf_Request_Http::setActionName' => ['Yaf_Request_Abstract|bool', 'action'=>'string'], -'Yaf_Request_Http::setBaseUri' => ['bool', 'uri'=>'string'], -'Yaf_Request_Http::setControllerName' => ['Yaf_Request_Abstract|bool', 'controller'=>'string'], -'Yaf_Request_Http::setDispatched' => ['bool'], -'Yaf_Request_Http::setModuleName' => ['Yaf_Request_Abstract|bool', 'module'=>'string'], -'Yaf_Request_Http::setParam' => ['Yaf_Request_Abstract|bool', 'name'=>'array|string', 'value='=>'string'], -'Yaf_Request_Http::setRequestUri' => ['', 'uri'=>'string'], -'Yaf_Request_Http::setRouted' => ['Yaf_Request_Abstract|bool'], -'Yaf_Request_Simple::__clone' => ['void'], -'Yaf_Request_Simple::__construct' => ['void'], -'Yaf_Request_Simple::get' => ['void'], -'Yaf_Request_Simple::getActionName' => ['string'], -'Yaf_Request_Simple::getBaseUri' => ['string'], -'Yaf_Request_Simple::getControllerName' => ['string'], -'Yaf_Request_Simple::getCookie' => ['void'], -'Yaf_Request_Simple::getEnv' => ['mixed', 'name='=>'string', 'default='=>'mixed'], -'Yaf_Request_Simple::getException' => ['Yaf_Exception'], -'Yaf_Request_Simple::getFiles' => ['void'], -'Yaf_Request_Simple::getLanguage' => ['string'], -'Yaf_Request_Simple::getMethod' => ['string'], -'Yaf_Request_Simple::getModuleName' => ['string'], +'Yaf_Request_Http::setActionName' => ['?Yaf_Request_Http', 'action'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Http::setBaseUri' => ['Yaf_Request_Http|false', 'uir'=>'string'], +'Yaf_Request_Http::setControllerName' => ['?Yaf_Request_Http', 'controller'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Http::setDispatched' => ['?Yaf_Request_Http', 'flag='=>'bool|true'], +'Yaf_Request_Http::setModuleName' => ['?Yaf_Request_Http', 'module'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Http::setParam' => ['Yaf_Request_Http|false|null', 'name'=>'mixed', 'value='=>'?mixed'], +'Yaf_Request_Http::setRequestUri' => ['?Yaf_Request_Http', 'uir'=>'string'], +'Yaf_Request_Http::setRouted' => ['?Yaf_Request_Http', 'flag='=>'bool|true'], +'Yaf_Request_Simple::__construct' => ['void', 'method='=>'?string', 'module='=>'?string', 'controller='=>'?string', 'action='http://github.com/=%3E'?string', 'params='=>'?array'], +'Yaf_Request_Simple::get' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getActionName' => ['?string'], +'Yaf_Request_Simple::getBaseUri' => ['?string'], +'Yaf_Request_Simple::getCookie' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getControllerName' => ['?string'], +'Yaf_Request_Simple::getEnv' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getException' => ['?Exception'], +'Yaf_Request_Simple::getFiles' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getLanguage' => ['?string'], +'Yaf_Request_Simple::getMethod' => ['?string'], +'Yaf_Request_Simple::getModuleName' => ['?string'], 'Yaf_Request_Simple::getParam' => ['mixed', 'name'=>'string', 'default='=>'mixed'], -'Yaf_Request_Simple::getParams' => ['array'], -'Yaf_Request_Simple::getPost' => ['void'], -'Yaf_Request_Simple::getQuery' => ['void'], -'Yaf_Request_Simple::getRequest' => ['void'], -'Yaf_Request_Simple::getRequestUri' => ['string'], -'Yaf_Request_Simple::getServer' => ['mixed', 'name='=>'string', 'default='=>'mixed'], +'Yaf_Request_Simple::getParams' => ['?array'], +'Yaf_Request_Simple::getPost' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getQuery' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getRaw' => ['?string'], +'Yaf_Request_Simple::getRequest' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getRequestUri' => ['?string'], +'Yaf_Request_Simple::getServer' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::cleanParams' => ['?Yaf_Request_Simple'], 'Yaf_Request_Simple::isCli' => ['bool'], +'Yaf_Request_Simple::isDelete' => ['bool'], 'Yaf_Request_Simple::isDispatched' => ['bool'], 'Yaf_Request_Simple::isGet' => ['bool'], 'Yaf_Request_Simple::isHead' => ['bool'], 'Yaf_Request_Simple::isOptions' => ['bool'], +'Yaf_Request_Simple::isPatch' => ['bool'], 'Yaf_Request_Simple::isPost' => ['bool'], 'Yaf_Request_Simple::isPut' => ['bool'], 'Yaf_Request_Simple::isRouted' => ['bool'], -'Yaf_Request_Simple::isXmlHttpRequest' => ['void'], -'Yaf_Request_Simple::setActionName' => ['Yaf_Request_Abstract|bool', 'action'=>'string'], -'Yaf_Request_Simple::setBaseUri' => ['bool', 'uri'=>'string'], -'Yaf_Request_Simple::setControllerName' => ['Yaf_Request_Abstract|bool', 'controller'=>'string'], -'Yaf_Request_Simple::setDispatched' => ['bool'], -'Yaf_Request_Simple::setModuleName' => ['Yaf_Request_Abstract|bool', 'module'=>'string'], -'Yaf_Request_Simple::setParam' => ['Yaf_Request_Abstract|bool', 'name'=>'array|string', 'value='=>'string'], -'Yaf_Request_Simple::setRequestUri' => ['', 'uri'=>'string'], -'Yaf_Request_Simple::setRouted' => ['Yaf_Request_Abstract|bool'], +'Yaf_Request_Simple::isXmlHttpRequest' => ['bool'], +'Yaf_Request_Simple::setActionName' => ['?Yaf_Request_Simple', 'action'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Simple::setBaseUri' => ['Yaf_Request_Simple|false', 'uir'=>'string'], +'Yaf_Request_Simple::setControllerName' => ['?Yaf_Request_Simple', 'controller'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Simple::setDispatched' => ['?Yaf_Request_Simple', 'flag='=>'bool|true'], +'Yaf_Request_Simple::setModuleName' => ['?Yaf_Request_Simple', 'module'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Simple::setParam' => ['Yaf_Request_Simple|bool|null', 'name'=>'mixed', 'value='=>'?mixed'], +'Yaf_Request_Simple::setRequestUri' => ['?Yaf_Request_Simple', 'uir'=>'string'], +'Yaf_Request_Simple::setRouted' => ['?Yaf_Request_Simple', 'flag='=>'bool|true'], 'Yaf_Response_Abstract::__clone' => ['void'], 'Yaf_Response_Abstract::__construct' => ['void'], 'Yaf_Response_Abstract::__destruct' => ['void'], 'Yaf_Response_Abstract::__toString' => ['string'], -'Yaf_Response_Abstract::appendBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Abstract::clearBody' => ['bool', 'key='=>'string'], -'Yaf_Response_Abstract::clearHeaders' => ['void'], -'Yaf_Response_Abstract::getBody' => ['mixed', 'key='=>'string'], -'Yaf_Response_Abstract::getHeader' => ['void'], -'Yaf_Response_Abstract::prependBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Abstract::response' => ['void'], -'Yaf_Response_Abstract::setAllHeaders' => ['void'], -'Yaf_Response_Abstract::setBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Abstract::setHeader' => ['void'], -'Yaf_Response_Abstract::setRedirect' => ['void'], -'Yaf_Response_Cli::__clone' => [''], +'Yaf_Response_Abstract::appendBody' => ['Yaf_Response_Abstract|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Abstract::clearBody' => ['?Yaf_Response_Abstract', 'name='=>'?string'], +'Yaf_Response_Abstract::getBody' => ['mixed', 'name='=>'string'], +'Yaf_Response_Abstract::prependBody' => ['Yaf_Response_Abstract|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Abstract::response' => ['bool'], +'Yaf_Response_Abstract::setBody' => ['Yaf_Response_Abstract|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Abstract::setRedirect' => ['?bool', 'url'=>'string'], +'Yaf_Response_Cli::__clone' => ['void'], 'Yaf_Response_Cli::__construct' => ['void'], -'Yaf_Response_Cli::__destruct' => [''], +'Yaf_Response_Cli::__destruct' => ['void'], 'Yaf_Response_Cli::__toString' => ['string'], -'Yaf_Response_Cli::appendBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Cli::clearBody' => ['bool', 'key='=>'string'], -'Yaf_Response_Cli::getBody' => ['mixed', 'key='=>'null|string'], -'Yaf_Response_Cli::prependBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Cli::setBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::__clone' => [''], +'Yaf_Response_Cli::appendBody' => ['Yaf_Response_Cli|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Cli::clearBody' => ['?Yaf_Response_Cli', 'name='=>'?string'], +'Yaf_Response_Cli::getBody' => ['mixed', 'name='=>'string'], +'Yaf_Response_Cli::prependBody' => ['Yaf_Response_Cli|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Cli::response' => ['bool'], +'Yaf_Response_Cli::setBody' => ['Yaf_Response_Cli|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Cli::setRedirect' => ['?bool', 'url'=>'string'], +'Yaf_Response_Http::__clone' => ['void'], 'Yaf_Response_Http::__construct' => ['void'], -'Yaf_Response_Http::__destruct' => [''], +'Yaf_Response_Http::__destruct' => ['void'], 'Yaf_Response_Http::__toString' => ['string'], -'Yaf_Response_Http::appendBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::clearBody' => ['bool', 'key='=>'string'], -'Yaf_Response_Http::clearHeaders' => ['Yaf_Response_Abstract|false', 'name='=>'string'], -'Yaf_Response_Http::getBody' => ['mixed', 'key='=>'null|string'], +'Yaf_Response_Http::appendBody' => ['Yaf_Response_Http|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Http::clearHeaders' => ['Yaf_Response_Http|false|null'], +'Yaf_Response_Http::clearBody' => ['?Yaf_Response_Http', 'name='=>'?string'], +'Yaf_Response_Http::getBody' => ['mixed', 'name='=>'string'], 'Yaf_Response_Http::getHeader' => ['mixed', 'name='=>'string'], -'Yaf_Response_Http::prependBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::response' => ['bool'], +'Yaf_Response_Http::prependBody' => ['Yaf_Response_Http|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Http::response' => ['?bool'], 'Yaf_Response_Http::setAllHeaders' => ['bool', 'headers'=>'array'], -'Yaf_Response_Http::setBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::setHeader' => ['bool', 'name'=>'string', 'value'=>'string', 'replace='=>'bool|false', 'response_code='=>'int'], -'Yaf_Response_Http::setRedirect' => ['bool', 'url'=>'string'], +'Yaf_Response_Http::setBody' => ['Yaf_Response_Http|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Http::setHeader' => ['?bool', 'name'=>'string', 'value'=>'string', 'replace='=>'bool|false', 'response_code='=>'int'], +'Yaf_Response_Http::setRedirect' => ['?bool', 'url'=>'string'], 'Yaf_Route_Interface::__construct' => ['void'], 'Yaf_Route_Interface::assemble' => ['string', 'info'=>'array', 'query='=>'array'], 'Yaf_Route_Interface::route' => ['bool', 'request'=>'Yaf_Request_Abstract'], @@ -13557,7 +13881,7 @@ 'Yaf_Session::__get' => ['void', 'name'=>'string'], 'Yaf_Session::__isset' => ['void', 'name'=>'string'], 'Yaf_Session::__set' => ['void', 'name'=>'string', 'value'=>'string'], -'Yaf_Session::__sleep' => ['void'], +'Yaf_Session::__sleep' => ['list'], 'Yaf_Session::__unset' => ['void', 'name'=>'string'], 'Yaf_Session::__wakeup' => ['void'], 'Yaf_Session::count' => ['0|positive-int'], @@ -13576,23 +13900,24 @@ 'Yaf_Session::set' => ['Yaf_Session|bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Session::start' => ['void'], 'Yaf_Session::valid' => ['void'], -'Yaf_View_Interface::assign' => ['bool', 'name'=>'string', 'value='=>'string'], -'Yaf_View_Interface::display' => ['bool', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Interface::getScriptPath' => ['void'], -'Yaf_View_Interface::render' => ['string', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Interface::setScriptPath' => ['void', 'template_dir'=>'string'], -'Yaf_View_Simple::__construct' => ['void', 'tempalte_dir'=>'string', 'options='=>'array'], -'Yaf_View_Simple::__get' => ['void', 'name='=>'string'], -'Yaf_View_Simple::__isset' => ['void', 'name'=>'string'], +'Yaf_View_Interface::assign' => ['Yaf_View_Interface|bool', 'name'=>'string', 'value='=>'?mixed'], +'Yaf_View_Interface::display' => ['Yaf_View_Interface|bool', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Interface::getScriptPath' => ['string'], +'Yaf_View_Interface::render' => ['string|bool', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Interface::setScriptPath' => ['bool', 'template_dir'=>'string'], +'Yaf_View_Simple::__construct' => ['void', 'tempalte_dir'=>'string', 'options='=>'?array'], +'Yaf_View_Simple::__get' => ['mixed', 'name='=>'?string'], +'Yaf_View_Simple::__isset' => ['bool', 'name'=>'string'], 'Yaf_View_Simple::__set' => ['void', 'name'=>'string', 'value'=>'mixed'], -'Yaf_View_Simple::assign' => ['bool', 'name'=>'string', 'value='=>'mixed'], -'Yaf_View_Simple::assignRef' => ['bool', 'name'=>'string', '&rw_value'=>'mixed'], -'Yaf_View_Simple::clear' => ['bool', 'name='=>'string'], -'Yaf_View_Simple::display' => ['bool', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Simple::eval' => ['string', 'tpl_content'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Simple::getScriptPath' => ['string'], -'Yaf_View_Simple::render' => ['string', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Simple::setScriptPath' => ['bool', 'template_dir'=>'string'], +'Yaf_View_Simple::assign' => ['Yaf_View_Simple|false|null', 'name='=>'?mixed', 'default='=>'?mixed'], +'Yaf_View_Simple::assignRef' => ['?Yaf_View_Simple', 'name'=>'string', '&value'=>'mixed'], +'Yaf_View_Simple::clear' => ['?Yaf_View_Simple', 'name='=>'string'], +'Yaf_View_Simple::display' => ['?bool', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Simple::eval' => ['string|null|false', 'tpl_str'=>'string', 'vars='=>'?array'], +'Yaf_View_Simple::get' => ['mixed', 'name='=>'?string'], +'Yaf_View_Simple::getScriptPath' => ['?string'], +'Yaf_View_Simple::render' => ['string|null|false', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Simple::setScriptPath' => ['Yaf_View_Simple|false|null', 'template_dir'=>'string'], 'yaml_emit' => ['string', 'data'=>'mixed', 'encoding='=>'int', 'linebreak='=>'int'], 'yaml_emit_file' => ['bool', 'filename'=>'string', 'data'=>'mixed', 'encoding='=>'int', 'linebreak='=>'int'], 'yaml_parse' => ['mixed', 'input'=>'string', 'pos='=>'int', '&w_ndocs='=>'int', 'callbacks='=>'array'], diff --git a/resources/functionMap_bleedingEdge.php b/resources/functionMap_bleedingEdge.php new file mode 100644 index 0000000000..92f41e9db0 --- /dev/null +++ b/resources/functionMap_bleedingEdge.php @@ -0,0 +1,8 @@ + [ + ], + 'old' => [ + ], +]; diff --git a/resources/functionMap_php74delta.php b/resources/functionMap_php74delta.php index ff3f99ba0d..bb05fe2b6a 100644 --- a/resources/functionMap_php74delta.php +++ b/resources/functionMap_php74delta.php @@ -22,26 +22,26 @@ */ return [ 'new' => [ - 'FFI::addr' => ['FFI\CData', '&ptr'=>'FFI\CData'], - 'FFI::alignof' => ['int', '&ptr'=>'mixed'], + 'FFI::addr' => ['FFI\CData', 'ptr'=>'FFI\CData'], + 'FFI::alignof' => ['int', 'ptr'=>'mixed'], 'FFI::arrayType' => ['FFI\CType', 'type'=>'string|FFI\CType', 'dims'=>'array'], - 'FFI::cast' => ['FFI\CData', 'type'=>'string|FFI\CType', '&ptr'=>''], + 'FFI::cast' => ['FFI\CData', 'type'=>'string|FFI\CType', 'ptr'=>''], 'FFI::cdef' => ['FFI', 'code='=>'string', 'lib='=>'?string'], - 'FFI::free' => ['void', '&ptr'=>'FFI\CData'], + 'FFI::free' => ['void', 'ptr'=>'FFI\CData'], 'FFI::load' => ['FFI', 'filename'=>'string'], - 'FFI::memcmp' => ['int', '&ptr1'=>'FFI\CData|string', '&ptr2'=>'FFI\CData|string', 'size'=>'int'], - 'FFI::memcpy' => ['void', '&dst'=>'FFI\CData', '&src'=>'string|FFI\CData', 'size'=>'int'], - 'FFI::memset' => ['void', '&ptr'=>'FFI\CData', 'ch'=>'int', 'size'=>'int'], + 'FFI::memcmp' => ['int', 'ptr1'=>'FFI\CData|string', 'ptr2'=>'FFI\CData|string', 'size'=>'int'], + 'FFI::memcpy' => ['void', 'dst'=>'FFI\CData', 'src'=>'string|FFI\CData', 'size'=>'int'], + 'FFI::memset' => ['void', 'ptr'=>'FFI\CData', 'ch'=>'int', 'size'=>'int'], 'FFI::new' => ['FFI\CData', 'type'=>'string|FFI\CType', 'owned='=>'bool', 'persistent='=>'bool'], 'FFI::scope' => ['FFI', 'scope_name'=>'string'], - 'FFI::sizeof' => ['int', '&ptr'=>'FFI\CData|FFI\CType'], - 'FFI::string' => ['string', '&ptr'=>'FFI\CData', 'size='=>'int'], - 'FFI::typeof' => ['FFI\CType', '&ptr'=>'FFI\CData'], + 'FFI::sizeof' => ['int', 'ptr'=>'FFI\CData|FFI\CType'], + 'FFI::string' => ['string', 'ptr'=>'FFI\CData', 'size='=>'int'], + 'FFI::typeof' => ['FFI\CType', 'ptr'=>'FFI\CData'], 'FFI::type' => ['FFI\CType', 'type'=>'string'], + 'fread' => ['string|false', 'fp'=>'resource', 'length'=>'positive-int'], 'get_mangled_object_vars' => ['array', 'obj'=>'object'], - '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'], + 'mb_str_split' => ['list|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], + 'password_algos' => ['list'], 'password_needs_rehash' => ['bool', 'hash'=>'string', 'algo'=>'string|null', 'options='=>'array'], 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index fb557075d8..bca5fef36b 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -22,12 +22,16 @@ return [ 'new' => [ 'array_combine' => ['associative-array', 'keys'=>'string[]|int[]', 'values'=>'array'], + 'base64_decode' => ['string', 'string'=>'string', 'strict='=>'false'], + 'base64_decode\'1' => ['string|false', 'string'=>'string', 'strict='=>'true'], 'bcdiv' => ['string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcmod' => ['string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcpowmod' => ['string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], 'call_user_func_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], + 'ceil' => ['float', 'number'=>'float'], 'com_load_typelib' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'true'], 'count_chars' => ['array|string', 'input'=>'string', 'mode='=>'int'], + 'curl_init' => ['__benevolent', 'url='=>'string'], 'date_add' => ['DateTime', 'object'=>'DateTime', 'interval'=>'DateInterval'], 'date_date_set' => ['DateTime', 'object'=>'DateTime', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'date_diff' => ['DateInterval', 'obj1'=>'DateTimeInterface', 'obj2'=>'DateTimeInterface', 'absolute='=>'bool'], @@ -35,22 +39,28 @@ 'date_isodate_set' => ['DateTime', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], 'date_parse' => ['array', 'date'=>'string'], 'date_sub' => ['DateTime', 'object'=>'DateTime', 'interval'=>'DateInterval'], - 'date_sun_info' => ['array', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], + 'date_sun_info' => ['array{sunrise: int|bool,sunset: int|bool,transit: int|bool,civil_twilight_begin: int|bool,civil_twilight_end: int|bool,nautical_twilight_begin: int|bool,nautical_twilight_end: int|bool,astronomical_twilight_begin: int|bool,astronomical_twilight_end: int|bool}', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_time_set' => ['DateTime', 'object'=>'DateTime', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'date_timestamp_set' => ['DateTime', 'object'=>'DateTime', 'unixtimestamp'=>'int'], 'date_timezone_set' => ['DateTime', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], - 'explode' => ['non-empty-array', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], + 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|3|4', 'destination='=>'string', 'extra_headers='=>'string'], + 'explode' => ['list', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], 'fdiv' => ['float', 'dividend'=>'float', 'divisor'=>'float'], + 'fgetcsv' => ['list|array{0: null}|false', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], + 'filter_input' => ['mixed', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], + 'filter_input_array' => ['array|false|null', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'definition='=>'int|array', 'add_empty='=>'bool'], + 'floor' => ['float', 'number'=>'float'], + 'forward_static_call_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], 'get_debug_type' => ['string', 'var'=>'mixed'], 'get_resource_id' => ['int', 'res'=>'resource'], 'gmdate' => ['string', 'format'=>'string', 'timestamp='=>'int'], 'gmmktime' => ['int|false', 'hour'=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], - 'hash' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], - 'hash_hkdf' => ['non-empty-string', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], - 'hash_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'], + 'hash' => ['non-falsy-string', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], + 'hash_hkdf' => ['non-falsy-string', 'algo'=>'non-falsy-string', 'key'=>'string', 'length='=>'0|positive-int', 'info='=>'string', 'salt='=>'string'], + 'hash_hmac' => ['non-falsy-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], + 'hash_pbkdf2' => ['non-falsy-string', 'algo'=>'non-falsy-string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'positive-int', 'length='=>'0|positive-int', 'raw_output='=>'bool'], 'imageaffine' => ['false|object', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], - 'imagecreate' => ['false|object', 'x_size'=>'int', 'y_size'=>'int'], + 'imagecreate' => ['__benevolent', 'width'=>'int<1, max>', 'height'=>'int<1, max>'], 'imagecreatefrombmp' => ['false|object', 'filename'=>'string'], 'imagecreatefromgd' => ['false|object', 'filename'=>'string'], 'imagecreatefromgd2' => ['false|object', 'filename'=>'string'], @@ -63,7 +73,7 @@ 'imagecreatefromwebp' => ['false|object', 'filename'=>'string'], 'imagecreatefromxbm' => ['false|object', 'filename'=>'string'], 'imagecreatefromxpm' => ['false|object', 'filename'=>'string'], - 'imagecreatetruecolor' => ['false|object', 'x_size'=>'int', 'y_size'=>'int'], + 'imagecreatetruecolor' => ['__benevolent', 'width'=>'int<1, max>', 'height'=>'int<1, max>'], 'imagecrop' => ['false|object', 'im'=>'resource', 'rect'=>'array'], 'imagecropauto' => ['false|object', 'im'=>'resource', 'mode'=>'int', 'threshold'=>'float', 'color'=>'int'], 'imagegetclip' => ['array', 'im'=>'resource'], @@ -74,29 +84,33 @@ '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' => ['non-empty-array', 'str'=>'string', 'split_length='=>'positive-int', 'encoding='=>'string'], + 'mb_detect_order' => ['bool|list', 'encoding_list='=>'non-empty-list|non-falsy-string|null'], + 'mb_encoding_aliases' => ['list', 'encoding'=>'string'], + 'mb_str_split' => ['list', 'str'=>'string', 'split_length='=>'positive-int', 'encoding='=>'string'], 'mb_strlen' => ['0|positive-int', 'str'=>'string', 'encoding='=>'string'], 'mktime' => ['int|false', 'hour'=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'odbc_exec' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result'=>'array'], - 'password_hash' => ['string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], + 'password_hash' => ['non-empty-string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], 'PDOStatement::fetchAll' => ['array', 'how='=>'int', 'fetch_argument='=>'int|string|callable', 'ctor_args='=>'?array'], 'PhpToken::tokenize' => ['list', 'code'=>'string', 'flags='=>'int'], 'PhpToken::is' => ['bool', 'kind'=>'string|int|string[]|int[]'], 'PhpToken::isIgnorable' => ['bool'], - 'PhpToken::getTokenName' => ['string'], + 'PhpToken::getTokenName' => ['non-falsy-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'], + 'scandir' => ['__benevolent|false>', 'dir'=>'string', 'sorting_order='=>'SCANDIR_SORT_ASCENDING|SCANDIR_SORT_DESCENDING|SCANDIR_SORT_NONE', 'context='=>'?resource'], 'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int):bool', 'error_types='=>'int'], 'socket_addrinfo_lookup' => ['AddressInfo[]', 'node'=>'string', 'service='=>'mixed', 'hints='=>'array'], - 'socket_select' => ['int|false', '&rw_read'=>'Socket[]|null', '&rw_write'=>'Socket[]|null', '&rw_except'=>'Socket[]|null', 'seconds'=>'int|null', 'microseconds='=>'int'], + 'socket_select' => ['int|false', '&w_read'=>'Socket[]|null', '&w_write'=>'Socket[]|null', '&w_except'=>'Socket[]|null', 'seconds'=>'int|null', 'microseconds='=>'int'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['string|false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'spl_autoload_functions' => ['list'], 'str_contains' => ['bool', 'haystack'=>'string', 'needle'=>'string'], - 'str_split' => ['non-empty-array', 'str'=>'string', 'split_length='=>'positive-int'], + 'str_split' => ['non-empty-list', 'str'=>'string', 'split_length='=>'positive-int'], 'str_ends_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_starts_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], - 'stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], + 'stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'strpos' => ['positive-int|0|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], @@ -104,7 +118,8 @@ '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'], + 'round' => ['float', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], + 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string|null'], 'xml_parser_create' => ['XMLParser', 'encoding='=>'string'], 'xml_parser_create_ns' => ['XMLParser', 'encoding='=>'string', 'sep='=>'string'], 'xml_parser_free' => ['bool', 'parser'=>'XMLParser'], @@ -137,7 +152,7 @@ 'xmlwriter_start_dtd_element' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string'], 'xmlwriter_start_dtd_entity' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'isparam'=>'bool'], 'xmlwriter_start_element' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string'], - 'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'XMLWriter', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string'], + 'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'XMLWriter', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string|null'], 'xmlwriter_start_pi' => ['bool', 'xmlwriter'=>'XMLWriter', 'target'=>'string'], 'xmlwriter_text' => ['bool', 'xmlwriter'=>'XMLWriter', 'content'=>'string'], 'xmlwriter_write_attribute' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'content'=>'string'], @@ -154,13 +169,15 @@ 'xmlwriter_write_raw' => ['bool', 'xmlwriter'=>'XMLWriter', 'content'=>'string'], ], 'old' => [ - 'array_combine' => ['associative-array|false', 'keys'=>'string[]|int[]', 'values'=>'array'], 'bcdiv' => ['?string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcmod' => ['?string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcpowmod' => ['?string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], + 'ceil' => ['__benevolent', 'number'=>'float'], + 'convert_cyr_string' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], 'com_load_typelib' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'bool'], 'count_chars' => ['array|false|string', 'input'=>'string', 'mode='=>'int'], + 'curl_init' => ['__benevolent', 'url='=>'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,19 +185,24 @@ 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], 'date_parse' => ['array|false', 'date'=>'string'], 'date_sub' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], - 'date_sun_info' => ['array|false', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], + 'date_sun_info' => ['__benevolent', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_time_set' => ['DateTime|false', 'object'=>'DateTime', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'date_timestamp_set' => ['DateTime|false', 'object'=>'DateTime', 'unixtimestamp'=>'int'], 'date_timezone_set' => ['DateTime|false', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], 'each' => ['array{0:int|string,key:int|string,1:mixed,value:mixed}', '&r_arr'=>'array'], + 'ezmlm_hash' => ['int', 'addr'=>'string'], + 'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], + 'floor' => ['__benevolent', 'number'=>'float'], + 'get_magic_quotes_gpc' => ['false'], 'gmdate' => ['string|false', 'format'=>'string', 'timestamp='=>'int'], 'gmmktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'gmp_random' => ['GMP', 'limiter='=>'int'], 'gzgetss' => ['string|false', 'zp'=>'resource', 'length'=>'int', 'allowable_tags='=>'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'], + 'hash' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], + 'hash_hkdf' => ['non-falsy-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash_hmac' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], + 'hash_pbkdf2' => ['non-falsy-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], + 'hebrevc' => ['string', 'str'=>'string', 'max_chars_per_line='=>'int'], 'image2wbmp' => ['bool', 'im'=>'resource', 'filename='=>'?string', 'threshold='=>'int'], 'imageaffine' => ['resource|false', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], 'imagecreate' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], @@ -205,24 +227,30 @@ 'imagejpeg' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'quality='=>'int'], 'imagerotate' => ['resource|false', 'src_im'=>'resource', 'angle'=>'float', 'bgdcolor'=>'int', 'ignoretransparent='=>'int'], 'imagescale' => ['resource|false', 'im'=>'resource', 'new_width'=>'int', 'new_height='=>'int', 'method='=>'int'], + 'imap_header' => ['stdClass|false', 'stream_id'=>'resource', 'msg_no'=>'int', 'from_length='=>'int', 'subject_length='=>'int', 'default_host='=>'string'], 'implode\'1' => ['string', 'pieces'=>'array'], 'jpeg2wbmp' => ['bool', 'jpegname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], + 'ldap_control_paged_result' => ['bool', 'link_identifier'=>'resource', 'pagesize'=>'int', 'iscritical='=>'bool', 'cookie='=>'string'], + 'ldap_control_paged_result_response' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', '&w_cookie='=>'string', '&w_estimated='=>'int'], 'ldap_set_rebind_proc' => ['bool', 'link_identifier'=>'resource', 'callback'=>'callable'], 'ldap_sort' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', 'sortfilter'=>'string'], 'mb_decode_numericentity' => ['string|false', 'string'=>'string', 'convmap'=>'array', 'encoding='=>'string', 'is_hex='=>'bool'], - 'mktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'mb_strlen' => ['0|positive-int', 'str'=>'string', 'encoding='=>'string'], + 'mktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], + 'money_format' => ['string', 'format'=>'string', 'value'=>'float'], 'odbc_exec' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string', 'flags='=>'int'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result='=>'array'], - 'password_hash' => ['string|false|null', 'password'=>'string', 'algo'=>'?string|?int', 'options='=>'array'], + 'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|int', 'options='=>'array'], 'png2wbmp' => ['bool', 'pngname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}|false', 'process'=>'resource'], 'read_exif_data' => ['array', 'filename'=>'string', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], - 'socket_select' => ['int|false', '&rw_read_fds'=>'resource[]|null', '&rw_write_fds'=>'resource[]|null', '&rw_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], + 'restore_include_path' => ['void'], + 'round' => ['__benevolent', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], + 'socket_select' => ['int|false', '&w_read_fds'=>'resource[]|null', '&w_write_fds'=>'resource[]|null', '&w_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['?string|?false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], - 'stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], + 'stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], 'strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int'], @@ -230,7 +258,7 @@ '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'], + 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string|null'], 'xml_parser_create' => ['resource', 'encoding='=>'string'], 'xml_parser_create_ns' => ['resource', 'encoding='=>'string', 'sep='=>'string'], 'xml_parser_free' => ['bool', 'parser'=>'resource'], @@ -263,7 +291,7 @@ 'xmlwriter_start_dtd_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], 'xmlwriter_start_dtd_entity' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'isparam'=>'bool'], 'xmlwriter_start_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], - 'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string'], + 'xmlwriter_start_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string|null'], 'xmlwriter_start_pi' => ['bool', 'xmlwriter'=>'resource', 'target'=>'string'], 'xmlwriter_text' => ['bool', 'xmlwriter'=>'resource', 'content'=>'string'], 'xmlwriter_write_attribute' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string'], diff --git a/resources/functionMap_php80delta_bleedingEdge.php b/resources/functionMap_php80delta_bleedingEdge.php new file mode 100644 index 0000000000..92f41e9db0 --- /dev/null +++ b/resources/functionMap_php80delta_bleedingEdge.php @@ -0,0 +1,8 @@ + [ + ], + 'old' => [ + ], +]; diff --git a/resources/functionMap_php81delta.php b/resources/functionMap_php81delta.php new file mode 100644 index 0000000000..54a3199934 --- /dev/null +++ b/resources/functionMap_php81delta.php @@ -0,0 +1,74 @@ + [ + 'mysqli_fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result'], + 'mysqli_fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], + 'mysqli_fetch_fields' => ['list', 'result'=>'mysqli_result'], + 'mysqli_result::fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false'], + 'mysqli_result::fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'fieldnr'=>'int'], + 'mysqli_result::fetch_fields' => ['list'], + 'UnitEnum::cases' => ['list'], + ], + 'old' => [ + 'pg_escape_bytea' => ['string', 'connection'=>'resource', 'data'=>'string'], + 'pg_escape_bytea\'1' => ['string', 'data'=>'string'], + 'pg_escape_identifier' => ['string|false', 'connection'=>'resource', 'data'=>'string'], + 'pg_escape_identifier\'1' => ['string', 'data'=>'string'], + 'pg_escape_literal' => ['string|false', 'connection'=>'resource', 'data'=>'string'], + 'pg_escape_literal\'1' => ['string', 'data'=>'string'], + 'pg_escape_string' => ['string', 'connection'=>'resource', 'data'=>'string'], + 'pg_escape_string\'1' => ['string', 'data'=>'string'], + 'pg_execute' => ['resource|false', 'connection'=>'resource', 'stmtname'=>'string', 'params'=>'array'], + 'pg_execute\'1' => ['resource|false', 'stmtname'=>'string', 'params'=>'array'], + 'pg_fetch_object' => ['object|false', 'result'=>'', 'row='=>'?int', 'result_type='=>'int'], + 'pg_fetch_object\'1' => ['object', 'result'=>'', 'row='=>'?int', 'class_name='=>'string', 'ctor_params='=>'array'], + 'pg_fetch_result' => ['', 'result'=>'', 'field_name'=>'string|int'], + 'pg_fetch_result\'1' => ['', 'result'=>'', 'row_number'=>'int', 'field_name'=>'string|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_prtlen' => ['int|false', 'result'=>'', 'field_name_or_number'=>''], + 'pg_field_prtlen\'1' => ['int', 'result'=>'', 'row'=>'int', 'field_name_or_number'=>'string|int'], + 'pg_lo_export' => ['bool', 'connection'=>'resource', 'oid'=>'int', 'filename'=>'string'], + 'pg_lo_export\'1' => ['bool', 'oid'=>'int', 'pathname'=>'string'], + 'pg_lo_import' => ['int|false', 'connection'=>'resource', 'pathname'=>'string', 'oid'=>''], + 'pg_lo_import\'1' => ['int', 'pathname'=>'string', 'oid'=>''], + 'pg_parameter_status' => ['string|false', 'connection'=>'resource', 'param_name'=>'string'], + 'pg_parameter_status\'1' => ['string|false', 'param_name'=>'string'], + 'pg_prepare' => ['resource|false', 'connection'=>'resource', 'stmtname'=>'string', 'query'=>'string'], + 'pg_prepare\'1' => ['resource|false', 'stmtname'=>'string', 'query'=>'string'], + 'pg_put_line' => ['bool', 'connection'=>'resource', 'data'=>'string'], + 'pg_put_line\'1' => ['bool', 'data'=>'string'], + 'pg_query' => ['resource|false', 'connection'=>'resource', 'query'=>'string'], + 'pg_query\'1' => ['resource|false', 'query'=>'string'], + 'pg_query_params' => ['resource|false', 'connection'=>'resource', 'query'=>'string', 'params'=>'array'], + 'pg_query_params\'1' => ['resource|false', 'query'=>'string', 'params'=>'array'], + 'pg_set_client_encoding' => ['int', 'connection'=>'resource', 'encoding'=>'string'], + 'pg_set_client_encoding\'1' => ['int', 'encoding'=>'string'], + 'pg_set_error_verbosity' => ['int|false', 'connection'=>'resource', 'verbosity'=>'int'], + 'pg_set_error_verbosity\'1' => ['int', 'verbosity'=>'int'], + 'pg_tty' => ['string', 'connection='=>'resource'], + 'pg_tty\'1' => ['string'], + 'pg_untrace' => ['bool', 'connection='=>'resource'], + 'pg_untrace\'1' => ['bool'], + ] +]; diff --git a/resources/functionMap_php82delta.php b/resources/functionMap_php82delta.php new file mode 100644 index 0000000000..6054b7a9ce --- /dev/null +++ b/resources/functionMap_php82delta.php @@ -0,0 +1,31 @@ + [ + 'iterator_count' => ['0|positive-int', 'iterator'=>'iterable'], + 'iterator_to_array' => ['array', 'iterator'=>'iterable', 'use_keys='=>'bool'], + 'str_split' => ['list', 'str'=>'string', 'split_length='=>'positive-int'], + ], + 'old' => [ + + ] +]; diff --git a/resources/functionMap_php83delta.php b/resources/functionMap_php83delta.php new file mode 100644 index 0000000000..bcc70d4b0f --- /dev/null +++ b/resources/functionMap_php83delta.php @@ -0,0 +1,34 @@ + [ + 'DateTime::modify' => ['static', 'modify'=>'string'], + 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], + 'str_decrement' => ['non-empty-string', 'string'=>'non-empty-string'], + 'str_increment' => ['non-falsy-string', 'string'=>'non-empty-string'], + 'gc_status' => ['array{running:bool,protected:bool,full:bool,runs:int,collected:int,threshold:int,buffer_size:int,roots:int,application_time:float,collector_time:float,destructor_time:float,free_time:float}'], + 'stream_get_meta_data' => ['array{timed_out:bool,blocked:bool,eof:bool,unread_bytes:int,stream_type:string,wrapper_type:string,wrapper_data:mixed,mode:string,seekable:bool,uri:string,mediatype?:string,base64?:bool}', 'fp'=>'resource'], + ], + 'old' => [ + + ] +]; diff --git a/resources/functionMap_php84delta.php b/resources/functionMap_php84delta.php new file mode 100644 index 0000000000..fe719fce15 --- /dev/null +++ b/resources/functionMap_php84delta.php @@ -0,0 +1,26 @@ + [ + 'http_get_last_response_headers' => ['list|null'], + 'http_clear_last_response_headers' => ['void'], + 'mb_lcfirst' => ['string', 'string'=>'string', 'encoding='=>'string'], + 'mb_ucfirst' => ['string', 'string'=>'string', 'encoding='=>'string'], + ], + 'old' => [ + + ] +]; diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index fb6a45857c..6136b1c068 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -1,6 +1,22 @@ true as a modification to bin/functionMetadata_original.php. + * 3) Contribute the #[Pure] functions without side effects to https://github.com/JetBrains/phpstorm-stubs + * 4) Once the PR from 3) is merged, please update the package here and run ./bin/generate-function-metadata.php. + */ + return [ + 'BackedEnum::from' => ['hasSideEffects' => false], + 'BackedEnum::tryFrom' => ['hasSideEffects' => false], 'CURLFile::getFilename' => ['hasSideEffects' => false], 'CURLFile::getMimeType' => ['hasSideEffects' => false], 'CURLFile::getPostFilename' => ['hasSideEffects' => false], @@ -28,6 +44,8 @@ 'Cassandra\\Exception\\UnpreparedException::__construct' => ['hasSideEffects' => false], 'Cassandra\\Exception\\ValidationException::__construct' => ['hasSideEffects' => false], 'Cassandra\\Exception\\WriteTimeoutException::__construct' => ['hasSideEffects' => false], + 'Closure::bind' => ['hasSideEffects' => false], + 'Closure::bindTo' => ['hasSideEffects' => false], 'Collator::__construct' => ['hasSideEffects' => false], 'Collator::compare' => ['hasSideEffects' => false], 'Collator::getAttribute' => ['hasSideEffects' => false], @@ -340,6 +358,8 @@ 'ImagickPixelIterator::getIteratorRow' => ['hasSideEffects' => false], 'ImagickPixelIterator::getNextIteratorRow' => ['hasSideEffects' => false], 'ImagickPixelIterator::getPreviousIteratorRow' => ['hasSideEffects' => false], + 'IntBackedEnum::from' => ['hasSideEffects' => false], + 'IntBackedEnum::tryFrom' => ['hasSideEffects' => false], 'IntlBreakIterator::current' => ['hasSideEffects' => false], 'IntlBreakIterator::getErrorCode' => ['hasSideEffects' => false], 'IntlBreakIterator::getErrorMessage' => ['hasSideEffects' => false], @@ -470,6 +490,7 @@ 'ReflectionClass::isInternal' => ['hasSideEffects' => false], 'ReflectionClass::isIterable' => ['hasSideEffects' => false], 'ReflectionClass::isIterateable' => ['hasSideEffects' => false], + 'ReflectionClass::isReadOnly' => ['hasSideEffects' => false], 'ReflectionClass::isSubclassOf' => ['hasSideEffects' => false], 'ReflectionClass::isTrait' => ['hasSideEffects' => false], 'ReflectionClass::isUserDefined' => ['hasSideEffects' => false], @@ -482,6 +503,9 @@ 'ReflectionClassConstant::isPrivate' => ['hasSideEffects' => false], 'ReflectionClassConstant::isProtected' => ['hasSideEffects' => false], 'ReflectionClassConstant::isPublic' => ['hasSideEffects' => false], + 'ReflectionEnumBackedCase::getBackingValue' => ['hasSideEffects' => false], + 'ReflectionEnumUnitCase::getEnum' => ['hasSideEffects' => false], + 'ReflectionEnumUnitCase::getValue' => ['hasSideEffects' => false], 'ReflectionExtension::getClassNames' => ['hasSideEffects' => false], 'ReflectionExtension::getClasses' => ['hasSideEffects' => false], 'ReflectionExtension::getConstants' => ['hasSideEffects' => false], @@ -495,6 +519,7 @@ 'ReflectionFunction::getClosure' => ['hasSideEffects' => false], 'ReflectionFunction::isDisabled' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getAttributes' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getClosureCalledClass' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getClosureScopeClass' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getClosureThis' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getClosureUsedVariables' => ['hasSideEffects' => false], @@ -609,10 +634,23 @@ 'SimpleXMLIterator::hasChildren' => ['hasSideEffects' => false], 'SimpleXMLIterator::valid' => ['hasSideEffects' => false], 'SoapFault::__construct' => ['hasSideEffects' => false], + 'SplFileObject::fflush' => ['hasSideEffects' => true], + 'SplFileObject::fgetc' => ['hasSideEffects' => true], + 'SplFileObject::fgetcsv' => ['hasSideEffects' => true], + 'SplFileObject::fgets' => ['hasSideEffects' => true], + 'SplFileObject::fgetss' => ['hasSideEffects' => true], + 'SplFileObject::fpassthru' => ['hasSideEffects' => true], + 'SplFileObject::fputcsv' => ['hasSideEffects' => true], + 'SplFileObject::fread' => ['hasSideEffects' => true], + 'SplFileObject::fscanf' => ['hasSideEffects' => true], + 'SplFileObject::fseek' => ['hasSideEffects' => true], + 'SplFileObject::ftruncate' => ['hasSideEffects' => true], + 'SplFileObject::fwrite' => ['hasSideEffects' => true], 'Spoofchecker::__construct' => ['hasSideEffects' => false], + 'StringBackedEnum::from' => ['hasSideEffects' => false], + 'StringBackedEnum::tryFrom' => ['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], 'Transliterator::createInverse' => ['hasSideEffects' => false], @@ -630,6 +668,15 @@ 'UConverter::getStandards' => ['hasSideEffects' => false], 'UConverter::getSubstChars' => ['hasSideEffects' => false], 'UConverter::reasonText' => ['hasSideEffects' => false], + 'UnitEnum::cases' => ['hasSideEffects' => false], + 'WeakMap::count' => ['hasSideEffects' => false], + 'WeakMap::getIterator' => ['hasSideEffects' => false], + 'WeakMap::offsetExists' => ['hasSideEffects' => false], + 'WeakMap::offsetGet' => ['hasSideEffects' => false], + 'WeakReference::create' => ['hasSideEffects' => false], + 'WeakReference::get' => ['hasSideEffects' => false], + 'XmlReader::next' => ['hasSideEffects' => true], + 'XmlReader::read' => ['hasSideEffects' => true], 'Zookeeper::getAcl' => ['hasSideEffects' => false], 'Zookeeper::getChildren' => ['hasSideEffects' => false], 'Zookeeper::getClientId' => ['hasSideEffects' => false], @@ -671,12 +718,15 @@ 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], + 'array_pop' => ['hasSideEffects' => true], 'array_product' => ['hasSideEffects' => false], + 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], 'array_search' => ['hasSideEffects' => false], + 'array_shift' => ['hasSideEffects' => true], 'array_slice' => ['hasSideEffects' => false], 'array_sum' => ['hasSideEffects' => false], 'array_udiff' => ['hasSideEffects' => false], @@ -686,6 +736,7 @@ 'array_uintersect_assoc' => ['hasSideEffects' => false], 'array_uintersect_uassoc' => ['hasSideEffects' => false], 'array_unique' => ['hasSideEffects' => false], + 'array_unshift' => ['hasSideEffects' => true], 'array_values' => ['hasSideEffects' => false], 'asin' => ['hasSideEffects' => false], 'asinh' => ['hasSideEffects' => false], @@ -725,19 +776,19 @@ 'chunk_split' => ['hasSideEffects' => false], 'class_implements' => ['hasSideEffects' => false], 'class_parents' => ['hasSideEffects' => false], - 'cli_get_process_title' => ['hasSideEffects' => false], + 'cli_get_process_title' => ['hasSideEffects' => true], 'collator_compare' => ['hasSideEffects' => false], 'collator_create' => ['hasSideEffects' => false], 'collator_get_attribute' => ['hasSideEffects' => false], - 'collator_get_error_code' => ['hasSideEffects' => false], + 'collator_get_error_code' => ['hasSideEffects' => true], 'collator_get_error_message' => ['hasSideEffects' => false], 'collator_get_locale' => ['hasSideEffects' => false], 'collator_get_sort_key' => ['hasSideEffects' => false], 'collator_get_strength' => ['hasSideEffects' => false], 'compact' => ['hasSideEffects' => false], - 'connection_aborted' => ['hasSideEffects' => false], - 'connection_status' => ['hasSideEffects' => false], - 'constant' => ['hasSideEffects' => false], + 'connection_aborted' => ['hasSideEffects' => true], + 'connection_status' => ['hasSideEffects' => true], + 'constant' => ['hasSideEffects' => true], 'convert_cyr_string' => ['hasSideEffects' => false], 'convert_uudecode' => ['hasSideEffects' => false], 'convert_uuencode' => ['hasSideEffects' => false], @@ -760,47 +811,47 @@ 'ctype_upper' => ['hasSideEffects' => false], 'ctype_xdigit' => ['hasSideEffects' => false], 'curl_copy_handle' => ['hasSideEffects' => false], - 'curl_errno' => ['hasSideEffects' => false], - 'curl_error' => ['hasSideEffects' => false], + 'curl_errno' => ['hasSideEffects' => true], + 'curl_error' => ['hasSideEffects' => true], 'curl_escape' => ['hasSideEffects' => false], 'curl_file_create' => ['hasSideEffects' => false], - 'curl_getinfo' => ['hasSideEffects' => false], - 'curl_multi_errno' => ['hasSideEffects' => false], + 'curl_getinfo' => ['hasSideEffects' => true], + 'curl_multi_errno' => ['hasSideEffects' => true], 'curl_multi_getcontent' => ['hasSideEffects' => false], 'curl_multi_info_read' => ['hasSideEffects' => false], - 'curl_share_errno' => ['hasSideEffects' => false], + 'curl_share_errno' => ['hasSideEffects' => true], 'curl_share_strerror' => ['hasSideEffects' => false], 'curl_strerror' => ['hasSideEffects' => false], 'curl_unescape' => ['hasSideEffects' => false], 'curl_version' => ['hasSideEffects' => false], 'current' => ['hasSideEffects' => false], - 'date' => ['hasSideEffects' => false], - 'date_create' => ['hasSideEffects' => false], - 'date_create_from_format' => ['hasSideEffects' => false], - 'date_create_immutable' => ['hasSideEffects' => false], - 'date_create_immutable_from_format' => ['hasSideEffects' => false], + 'date' => ['hasSideEffects' => true], + 'date_create' => ['hasSideEffects' => true], + 'date_create_from_format' => ['hasSideEffects' => true], + 'date_create_immutable' => ['hasSideEffects' => true], + 'date_create_immutable_from_format' => ['hasSideEffects' => true], 'date_default_timezone_get' => ['hasSideEffects' => false], - 'date_diff' => ['hasSideEffects' => false], - 'date_format' => ['hasSideEffects' => false], - 'date_get_last_errors' => ['hasSideEffects' => false], - 'date_interval_create_from_date_string' => ['hasSideEffects' => false], - 'date_interval_format' => ['hasSideEffects' => false], - 'date_offset_get' => ['hasSideEffects' => false], - 'date_parse' => ['hasSideEffects' => false], - 'date_parse_from_format' => ['hasSideEffects' => false], - 'date_sun_info' => ['hasSideEffects' => false], - 'date_sunrise' => ['hasSideEffects' => false], - 'date_sunset' => ['hasSideEffects' => false], - 'date_timestamp_get' => ['hasSideEffects' => false], - 'date_timezone_get' => ['hasSideEffects' => false], + 'date_diff' => ['hasSideEffects' => true], + 'date_format' => ['hasSideEffects' => true], + 'date_get_last_errors' => ['hasSideEffects' => true], + 'date_interval_create_from_date_string' => ['hasSideEffects' => true], + 'date_interval_format' => ['hasSideEffects' => true], + 'date_offset_get' => ['hasSideEffects' => true], + 'date_parse' => ['hasSideEffects' => true], + 'date_parse_from_format' => ['hasSideEffects' => true], + 'date_sun_info' => ['hasSideEffects' => true], + 'date_sunrise' => ['hasSideEffects' => true], + 'date_sunset' => ['hasSideEffects' => true], + 'date_timestamp_get' => ['hasSideEffects' => true], + 'date_timezone_get' => ['hasSideEffects' => true], 'datefmt_create' => ['hasSideEffects' => false], 'datefmt_format' => ['hasSideEffects' => false], 'datefmt_format_object' => ['hasSideEffects' => false], 'datefmt_get_calendar' => ['hasSideEffects' => false], 'datefmt_get_calendar_object' => ['hasSideEffects' => false], 'datefmt_get_datetype' => ['hasSideEffects' => false], - 'datefmt_get_error_code' => ['hasSideEffects' => false], - 'datefmt_get_error_message' => ['hasSideEffects' => false], + 'datefmt_get_error_code' => ['hasSideEffects' => true], + 'datefmt_get_error_message' => ['hasSideEffects' => true], 'datefmt_get_locale' => ['hasSideEffects' => false], 'datefmt_get_pattern' => ['hasSideEffects' => false], 'datefmt_get_timetype' => ['hasSideEffects' => false], @@ -808,19 +859,21 @@ 'datefmt_get_timezone_id' => ['hasSideEffects' => false], 'datefmt_is_lenient' => ['hasSideEffects' => false], 'dcngettext' => ['hasSideEffects' => false], + 'debug_backtrace' => ['hasSideEffects' => true], 'decbin' => ['hasSideEffects' => false], 'dechex' => ['hasSideEffects' => false], 'decoct' => ['hasSideEffects' => false], - 'defined' => ['hasSideEffects' => false], + 'defined' => ['hasSideEffects' => true], 'deflate_init' => ['hasSideEffects' => false], 'deg2rad' => ['hasSideEffects' => false], 'dirname' => ['hasSideEffects' => false], - 'disk_free_space' => ['hasSideEffects' => false], - 'disk_total_space' => ['hasSideEffects' => false], - 'diskfreespace' => ['hasSideEffects' => false], + 'disk_free_space' => ['hasSideEffects' => true], + 'disk_total_space' => ['hasSideEffects' => true], + 'diskfreespace' => ['hasSideEffects' => true], 'dngettext' => ['hasSideEffects' => false], 'doubleval' => ['hasSideEffects' => false], - 'error_get_last' => ['hasSideEffects' => false], + 'error_get_last' => ['hasSideEffects' => true], + 'error_log' => ['hasSideEffects' => true], 'escapeshellarg' => ['hasSideEffects' => false], 'escapeshellcmd' => ['hasSideEffects' => false], 'exp' => ['hasSideEffects' => false], @@ -829,15 +882,15 @@ 'extension_loaded' => ['hasSideEffects' => false], 'fclose' => ['hasSideEffects' => true], 'fdiv' => ['hasSideEffects' => false], - 'feof' => ['hasSideEffects' => false], + 'feof' => ['hasSideEffects' => true], 'fflush' => ['hasSideEffects' => true], 'fgetc' => ['hasSideEffects' => true], 'fgetcsv' => ['hasSideEffects' => true], 'fgets' => ['hasSideEffects' => true], 'fgetss' => ['hasSideEffects' => true], - 'file' => ['hasSideEffects' => false], + 'file' => ['hasSideEffects' => true], 'file_exists' => ['hasSideEffects' => false], - 'file_get_contents' => ['hasSideEffects' => false], + 'file_get_contents' => ['hasSideEffects' => true], 'file_put_contents' => ['hasSideEffects' => true], 'fileatime' => ['hasSideEffects' => false], 'filectime' => ['hasSideEffects' => false], @@ -861,7 +914,7 @@ 'flock' => ['hasSideEffects' => true], 'floor' => ['hasSideEffects' => false], 'fmod' => ['hasSideEffects' => false], - 'fnmatch' => ['hasSideEffects' => false], + 'fnmatch' => ['hasSideEffects' => true], 'fopen' => ['hasSideEffects' => true], 'fpassthru' => ['hasSideEffects' => true], 'fputcsv' => ['hasSideEffects' => true], @@ -869,17 +922,17 @@ 'fread' => ['hasSideEffects' => true], 'fscanf' => ['hasSideEffects' => true], 'fseek' => ['hasSideEffects' => true], - 'fstat' => ['hasSideEffects' => false], + 'fstat' => ['hasSideEffects' => true], 'ftell' => ['hasSideEffects' => false], - 'ftok' => ['hasSideEffects' => false], + 'ftok' => ['hasSideEffects' => true], '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], + 'gc_enabled' => ['hasSideEffects' => true], + 'gc_status' => ['hasSideEffects' => true], 'gd_info' => ['hasSideEffects' => false], 'geoip_continent_code_by_name' => ['hasSideEffects' => false], 'geoip_country_code3_by_name' => ['hasSideEffects' => false], @@ -896,57 +949,59 @@ 'geoip_region_by_name' => ['hasSideEffects' => false], 'geoip_region_name_by_code' => ['hasSideEffects' => false], 'geoip_time_zone_by_country_and_region' => ['hasSideEffects' => false], - 'get_browser' => ['hasSideEffects' => false], + 'get_browser' => ['hasSideEffects' => true], 'get_called_class' => ['hasSideEffects' => false], 'get_cfg_var' => ['hasSideEffects' => false], 'get_class' => ['hasSideEffects' => false], 'get_class_methods' => ['hasSideEffects' => false], 'get_class_vars' => ['hasSideEffects' => false], - 'get_current_user' => ['hasSideEffects' => false], + 'get_current_user' => ['hasSideEffects' => true], 'get_debug_type' => ['hasSideEffects' => false], - 'get_declared_classes' => ['hasSideEffects' => false], - 'get_declared_interfaces' => ['hasSideEffects' => false], - 'get_declared_traits' => ['hasSideEffects' => false], - 'get_defined_constants' => ['hasSideEffects' => false], - 'get_defined_functions' => ['hasSideEffects' => false], - 'get_defined_vars' => ['hasSideEffects' => false], + 'get_declared_classes' => ['hasSideEffects' => true], + 'get_declared_interfaces' => ['hasSideEffects' => true], + 'get_declared_traits' => ['hasSideEffects' => true], + 'get_defined_constants' => ['hasSideEffects' => true], + 'get_defined_functions' => ['hasSideEffects' => true], + 'get_defined_vars' => ['hasSideEffects' => true], 'get_extension_funcs' => ['hasSideEffects' => false], - 'get_headers' => ['hasSideEffects' => false], + 'get_headers' => ['hasSideEffects' => true], 'get_html_translation_table' => ['hasSideEffects' => false], - 'get_include_path' => ['hasSideEffects' => false], - 'get_included_files' => ['hasSideEffects' => false], + 'get_include_path' => ['hasSideEffects' => true], + 'get_included_files' => ['hasSideEffects' => true], 'get_loaded_extensions' => ['hasSideEffects' => false], - 'get_meta_tags' => ['hasSideEffects' => false], - 'get_object_vars' => ['hasSideEffects' => false], + 'get_meta_tags' => ['hasSideEffects' => true], + 'get_object_vars' => ['hasSideEffects' => true], 'get_parent_class' => ['hasSideEffects' => false], - 'get_required_files' => ['hasSideEffects' => false], + 'get_required_files' => ['hasSideEffects' => true], 'get_resource_id' => ['hasSideEffects' => false], - 'get_resources' => ['hasSideEffects' => false], + 'get_resource_type' => ['hasSideEffects' => true], + 'get_resources' => ['hasSideEffects' => true], 'getallheaders' => ['hasSideEffects' => false], - 'getcwd' => ['hasSideEffects' => false], - 'getdate' => ['hasSideEffects' => false], - 'getenv' => ['hasSideEffects' => false], + 'getcwd' => ['hasSideEffects' => true], + 'getdate' => ['hasSideEffects' => true], + 'getenv' => ['hasSideEffects' => true], 'gethostbyaddr' => ['hasSideEffects' => false], 'gethostbyname' => ['hasSideEffects' => false], 'gethostbynamel' => ['hasSideEffects' => false], 'gethostname' => ['hasSideEffects' => false], - 'getlastmod' => ['hasSideEffects' => false], + 'getlastmod' => ['hasSideEffects' => true], 'getmygid' => ['hasSideEffects' => false], 'getmyinode' => ['hasSideEffects' => false], 'getmypid' => ['hasSideEffects' => false], 'getmyuid' => ['hasSideEffects' => false], + 'getopt' => ['hasSideEffects' => true], 'getprotobyname' => ['hasSideEffects' => false], 'getprotobynumber' => ['hasSideEffects' => false], 'getrandmax' => ['hasSideEffects' => false], - 'getrusage' => ['hasSideEffects' => false], + 'getrusage' => ['hasSideEffects' => true], 'getservbyname' => ['hasSideEffects' => false], 'getservbyport' => ['hasSideEffects' => false], 'gettext' => ['hasSideEffects' => false], - 'gettimeofday' => ['hasSideEffects' => false], + 'gettimeofday' => ['hasSideEffects' => true], 'gettype' => ['hasSideEffects' => false], - 'glob' => ['hasSideEffects' => false], - 'gmdate' => ['hasSideEffects' => false], - 'gmmktime' => ['hasSideEffects' => false], + 'glob' => ['hasSideEffects' => true], + 'gmdate' => ['hasSideEffects' => true], + 'gmmktime' => ['hasSideEffects' => true], 'gmp_abs' => ['hasSideEffects' => false], 'gmp_add' => ['hasSideEffects' => false], 'gmp_and' => ['hasSideEffects' => false], @@ -1021,7 +1076,7 @@ 'headers_list' => ['hasSideEffects' => false], 'hebrev' => ['hasSideEffects' => false], 'hexdec' => ['hasSideEffects' => false], - 'hrtime' => ['hasSideEffects' => false], + 'hrtime' => ['hasSideEffects' => true], 'html_entity_decode' => ['hasSideEffects' => false], 'htmlentities' => ['hasSideEffects' => false], 'htmlspecialchars' => ['hasSideEffects' => false], @@ -1059,7 +1114,7 @@ 'iconv_strpos' => ['hasSideEffects' => false], 'iconv_strrpos' => ['hasSideEffects' => false], 'iconv_substr' => ['hasSideEffects' => false], - 'idate' => ['hasSideEffects' => false], + 'idate' => ['hasSideEffects' => true], 'image_type_to_extension' => ['hasSideEffects' => false], 'image_type_to_mime_type' => ['hasSideEffects' => false], 'imagecolorat' => ['hasSideEffects' => false], @@ -1094,13 +1149,13 @@ 'inflate_get_status' => ['hasSideEffects' => false], 'inflate_init' => ['hasSideEffects' => false], 'ini_get' => ['hasSideEffects' => false], - 'ini_get_all' => ['hasSideEffects' => false], + 'ini_get_all' => ['hasSideEffects' => true], 'intcal_get_maximum' => ['hasSideEffects' => false], 'intdiv' => ['hasSideEffects' => false], 'intl_error_name' => ['hasSideEffects' => false], 'intl_get' => ['hasSideEffects' => false], - 'intl_get_error_code' => ['hasSideEffects' => false], - 'intl_get_error_message' => ['hasSideEffects' => false], + 'intl_get_error_code' => ['hasSideEffects' => true], + 'intl_get_error_message' => ['hasSideEffects' => true], 'intl_is_failure' => ['hasSideEffects' => false], 'intlcal_after' => ['hasSideEffects' => false], 'intlcal_before' => ['hasSideEffects' => false], @@ -1113,8 +1168,8 @@ 'intlcal_get_actual_minimum' => ['hasSideEffects' => false], 'intlcal_get_available_locales' => ['hasSideEffects' => false], 'intlcal_get_day_of_week_type' => ['hasSideEffects' => false], - 'intlcal_get_error_code' => ['hasSideEffects' => false], - 'intlcal_get_error_message' => ['hasSideEffects' => false], + 'intlcal_get_error_code' => ['hasSideEffects' => true], + 'intlcal_get_error_message' => ['hasSideEffects' => true], 'intlcal_get_first_day_of_week' => ['hasSideEffects' => false], 'intlcal_get_greatest_minimum' => ['hasSideEffects' => false], 'intlcal_get_keyword_values_for_locale' => ['hasSideEffects' => false], @@ -1123,7 +1178,7 @@ 'intlcal_get_maximum' => ['hasSideEffects' => false], 'intlcal_get_minimal_days_in_first_week' => ['hasSideEffects' => false], 'intlcal_get_minimum' => ['hasSideEffects' => false], - 'intlcal_get_now' => ['hasSideEffects' => false], + 'intlcal_get_now' => ['hasSideEffects' => true], 'intlcal_get_repeated_wall_time_option' => ['hasSideEffects' => false], 'intlcal_get_skipped_wall_time_option' => ['hasSideEffects' => false], 'intlcal_get_time' => ['hasSideEffects' => false], @@ -1150,8 +1205,8 @@ 'intltz_get_display_name' => ['hasSideEffects' => false], 'intltz_get_dst_savings' => ['hasSideEffects' => false], 'intltz_get_equivalent_id' => ['hasSideEffects' => false], - 'intltz_get_error_code' => ['hasSideEffects' => false], - 'intltz_get_error_message' => ['hasSideEffects' => false], + 'intltz_get_error_code' => ['hasSideEffects' => true], + 'intltz_get_error_message' => ['hasSideEffects' => true], 'intltz_get_gmt' => ['hasSideEffects' => false], 'intltz_get_id' => ['hasSideEffects' => false], 'intltz_get_offset' => ['hasSideEffects' => false], @@ -1193,22 +1248,23 @@ 'is_scalar' => ['hasSideEffects' => false], 'is_string' => ['hasSideEffects' => false], 'is_subclass_of' => ['hasSideEffects' => false], - 'is_uploaded_file' => ['hasSideEffects' => false], + 'is_uploaded_file' => ['hasSideEffects' => true], 'is_writable' => ['hasSideEffects' => false], 'is_writeable' => ['hasSideEffects' => false], 'iterator_count' => ['hasSideEffects' => false], 'join' => ['hasSideEffects' => false], 'json_last_error' => ['hasSideEffects' => false], 'json_last_error_msg' => ['hasSideEffects' => false], + 'json_validate' => ['hasSideEffects' => false], 'key' => ['hasSideEffects' => false], 'key_exists' => ['hasSideEffects' => false], 'lcfirst' => ['hasSideEffects' => false], 'lchgrp' => ['hasSideEffects' => true], 'lchown' => ['hasSideEffects' => true], - 'libxml_get_errors' => ['hasSideEffects' => false], - 'libxml_get_last_error' => ['hasSideEffects' => false], + 'libxml_get_errors' => ['hasSideEffects' => true], + 'libxml_get_last_error' => ['hasSideEffects' => true], 'link' => ['hasSideEffects' => true], - 'linkinfo' => ['hasSideEffects' => false], + 'linkinfo' => ['hasSideEffects' => true], 'locale_accept_from_http' => ['hasSideEffects' => false], 'locale_canonicalize' => ['hasSideEffects' => false], 'locale_compose' => ['hasSideEffects' => false], @@ -1226,8 +1282,8 @@ 'locale_get_script' => ['hasSideEffects' => false], 'locale_lookup' => ['hasSideEffects' => false], 'locale_parse' => ['hasSideEffects' => false], - 'localeconv' => ['hasSideEffects' => false], - 'localtime' => ['hasSideEffects' => false], + 'localeconv' => ['hasSideEffects' => true], + 'localtime' => ['hasSideEffects' => true], 'log' => ['hasSideEffects' => false], 'log10' => ['hasSideEffects' => false], 'log1p' => ['hasSideEffects' => false], @@ -1263,6 +1319,7 @@ 'mb_preferred_mime_name' => ['hasSideEffects' => false], 'mb_scrub' => ['hasSideEffects' => false], 'mb_split' => ['hasSideEffects' => false], + 'mb_str_pad' => ['hasSideEffects' => false], 'mb_str_split' => ['hasSideEffects' => false], 'mb_strcut' => ['hasSideEffects' => false], 'mb_strimwidth' => ['hasSideEffects' => false], @@ -1282,9 +1339,9 @@ 'mb_substr_count' => ['hasSideEffects' => false], 'mbereg_search_setpos' => ['hasSideEffects' => false], 'md5' => ['hasSideEffects' => false], - 'md5_file' => ['hasSideEffects' => false], - 'memory_get_peak_usage' => ['hasSideEffects' => false], - 'memory_get_usage' => ['hasSideEffects' => false], + 'md5_file' => ['hasSideEffects' => true], + 'memory_get_peak_usage' => ['hasSideEffects' => true], + 'memory_get_usage' => ['hasSideEffects' => true], 'metaphone' => ['hasSideEffects' => false], 'method_exists' => ['hasSideEffects' => false], 'mhash' => ['hasSideEffects' => false], @@ -1292,16 +1349,16 @@ 'mhash_get_block_size' => ['hasSideEffects' => false], 'mhash_get_hash_name' => ['hasSideEffects' => false], 'mhash_keygen_s2k' => ['hasSideEffects' => false], - 'microtime' => ['hasSideEffects' => false], + 'microtime' => ['hasSideEffects' => true], 'min' => ['hasSideEffects' => false], 'mkdir' => ['hasSideEffects' => true], - 'mktime' => ['hasSideEffects' => false], + 'mktime' => ['hasSideEffects' => true], 'move_uploaded_file' => ['hasSideEffects' => true], 'msgfmt_create' => ['hasSideEffects' => false], 'msgfmt_format' => ['hasSideEffects' => false], 'msgfmt_format_message' => ['hasSideEffects' => false], - 'msgfmt_get_error_code' => ['hasSideEffects' => false], - 'msgfmt_get_error_message' => ['hasSideEffects' => false], + 'msgfmt_get_error_code' => ['hasSideEffects' => true], + 'msgfmt_get_error_message' => ['hasSideEffects' => true], 'msgfmt_get_locale' => ['hasSideEffects' => false], 'msgfmt_get_pattern' => ['hasSideEffects' => false], 'msgfmt_parse' => ['hasSideEffects' => false], @@ -1311,7 +1368,7 @@ 'net_get_interfaces' => ['hasSideEffects' => false], 'ngettext' => ['hasSideEffects' => false], 'nl2br' => ['hasSideEffects' => false], - 'nl_langinfo' => ['hasSideEffects' => false], + 'nl_langinfo' => ['hasSideEffects' => true], 'normalizer_get_raw_decomposition' => ['hasSideEffects' => false], 'normalizer_is_normalized' => ['hasSideEffects' => false], 'normalizer_normalize' => ['hasSideEffects' => false], @@ -1320,26 +1377,40 @@ 'numfmt_format' => ['hasSideEffects' => false], 'numfmt_format_currency' => ['hasSideEffects' => false], 'numfmt_get_attribute' => ['hasSideEffects' => false], - 'numfmt_get_error_code' => ['hasSideEffects' => false], - 'numfmt_get_error_message' => ['hasSideEffects' => false], + 'numfmt_get_error_code' => ['hasSideEffects' => true], + 'numfmt_get_error_message' => ['hasSideEffects' => true], 'numfmt_get_locale' => ['hasSideEffects' => false], 'numfmt_get_pattern' => ['hasSideEffects' => false], 'numfmt_get_symbol' => ['hasSideEffects' => false], 'numfmt_get_text_attribute' => ['hasSideEffects' => false], 'numfmt_parse' => ['hasSideEffects' => false], + 'ob_clean' => ['hasSideEffects' => true], + 'ob_end_clean' => ['hasSideEffects' => true], + 'ob_end_flush' => ['hasSideEffects' => true], 'ob_etaghandler' => ['hasSideEffects' => false], - 'ob_get_contents' => ['hasSideEffects' => false], + 'ob_flush' => ['hasSideEffects' => true], + 'ob_get_clean' => ['hasSideEffects' => true], + 'ob_get_contents' => ['hasSideEffects' => true], + 'ob_get_flush' => ['hasSideEffects' => true], + 'ob_get_length' => ['hasSideEffects' => true], + 'ob_get_level' => ['hasSideEffects' => true], + 'ob_get_status' => ['hasSideEffects' => true], 'ob_iconv_handler' => ['hasSideEffects' => false], + 'ob_list_handlers' => ['hasSideEffects' => true], 'octdec' => ['hasSideEffects' => false], 'ord' => ['hasSideEffects' => false], + 'output_add_rewrite_var' => ['hasSideEffects' => true], + 'output_reset_rewrite_vars' => ['hasSideEffects' => true], 'pack' => ['hasSideEffects' => false], - 'parse_ini_file' => ['hasSideEffects' => false], + 'pam_auth' => ['hasSideEffects' => false], + 'pam_chpass' => ['hasSideEffects' => false], + 'parse_ini_file' => ['hasSideEffects' => true], 'parse_ini_string' => ['hasSideEffects' => false], 'parse_url' => ['hasSideEffects' => false], - 'pathinfo' => ['hasSideEffects' => false], + 'pathinfo' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], - 'pcntl_errno' => ['hasSideEffects' => false], - 'pcntl_get_last_error' => ['hasSideEffects' => false], + 'pcntl_errno' => ['hasSideEffects' => true], + 'pcntl_get_last_error' => ['hasSideEffects' => true], 'pcntl_getpriority' => ['hasSideEffects' => false], 'pcntl_strerror' => ['hasSideEffects' => false], 'pcntl_wexitstatus' => ['hasSideEffects' => false], @@ -1354,16 +1425,16 @@ 'php_ini_scanned_files' => ['hasSideEffects' => false], 'php_logo_guid' => ['hasSideEffects' => false], 'php_sapi_name' => ['hasSideEffects' => false], - 'php_strip_whitespace' => ['hasSideEffects' => false], - 'php_uname' => ['hasSideEffects' => false], + 'php_strip_whitespace' => ['hasSideEffects' => true], + 'php_uname' => ['hasSideEffects' => true], 'phpversion' => ['hasSideEffects' => false], 'pi' => ['hasSideEffects' => false], 'popen' => ['hasSideEffects' => true], 'pos' => ['hasSideEffects' => false], 'posix_ctermid' => ['hasSideEffects' => false], - 'posix_errno' => ['hasSideEffects' => false], - 'posix_get_last_error' => ['hasSideEffects' => false], - 'posix_getcwd' => ['hasSideEffects' => false], + 'posix_errno' => ['hasSideEffects' => true], + 'posix_get_last_error' => ['hasSideEffects' => true], + 'posix_getcwd' => ['hasSideEffects' => true], 'posix_getegid' => ['hasSideEffects' => false], 'posix_geteuid' => ['hasSideEffects' => false], 'posix_getgid' => ['hasSideEffects' => false], @@ -1388,8 +1459,8 @@ 'posix_uname' => ['hasSideEffects' => false], 'pow' => ['hasSideEffects' => false], 'preg_grep' => ['hasSideEffects' => false], - 'preg_last_error' => ['hasSideEffects' => false], - 'preg_last_error_msg' => ['hasSideEffects' => false], + 'preg_last_error' => ['hasSideEffects' => true], + 'preg_last_error_msg' => ['hasSideEffects' => true], 'preg_quote' => ['hasSideEffects' => false], 'preg_split' => ['hasSideEffects' => false], 'property_exists' => ['hasSideEffects' => false], @@ -1404,23 +1475,23 @@ '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], + 'readlink' => ['hasSideEffects' => true], + 'realpath' => ['hasSideEffects' => true], + 'realpath_cache_get' => ['hasSideEffects' => true], + 'realpath_cache_size' => ['hasSideEffects' => true], '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_get_error_code' => ['hasSideEffects' => true], + 'resourcebundle_get_error_message' => ['hasSideEffects' => true], 'resourcebundle_locales' => ['hasSideEffects' => false], 'rewind' => ['hasSideEffects' => true], 'rmdir' => ['hasSideEffects' => true], 'round' => ['hasSideEffects' => false], 'rtrim' => ['hasSideEffects' => false], 'sha1' => ['hasSideEffects' => false], - 'sha1_file' => ['hasSideEffects' => false], + 'sha1_file' => ['hasSideEffects' => true], 'sin' => ['hasSideEffects' => false], 'sinh' => ['hasSideEffects' => false], 'sizeof' => ['hasSideEffects' => false], @@ -1431,8 +1502,10 @@ 'sqrt' => ['hasSideEffects' => false], 'stat' => ['hasSideEffects' => false], 'str_contains' => ['hasSideEffects' => false], + 'str_decrement' => ['hasSideEffects' => false], 'str_ends_with' => ['hasSideEffects' => false], 'str_getcsv' => ['hasSideEffects' => false], + 'str_increment' => ['hasSideEffects' => false], 'str_pad' => ['hasSideEffects' => false], 'str_repeat' => ['hasSideEffects' => false], 'str_rot13' => ['hasSideEffects' => false], @@ -1444,9 +1517,9 @@ 'strcmp' => ['hasSideEffects' => false], 'strcoll' => ['hasSideEffects' => false], 'strcspn' => ['hasSideEffects' => false], - 'stream_get_filters' => ['hasSideEffects' => false], - 'stream_get_transports' => ['hasSideEffects' => false], - 'stream_get_wrappers' => ['hasSideEffects' => false], + 'stream_get_filters' => ['hasSideEffects' => true], + 'stream_get_transports' => ['hasSideEffects' => true], + 'stream_get_wrappers' => ['hasSideEffects' => true], 'stream_is_local' => ['hasSideEffects' => false], 'stream_isatty' => ['hasSideEffects' => false], 'strip_tags' => ['hasSideEffects' => false], @@ -1461,7 +1534,7 @@ 'strncmp' => ['hasSideEffects' => false], 'strpbrk' => ['hasSideEffects' => false], 'strpos' => ['hasSideEffects' => false], - 'strptime' => ['hasSideEffects' => false], + 'strptime' => ['hasSideEffects' => true], 'strrchr' => ['hasSideEffects' => false], 'strrev' => ['hasSideEffects' => false], 'strripos' => ['hasSideEffects' => false], @@ -1469,7 +1542,7 @@ 'strspn' => ['hasSideEffects' => false], 'strstr' => ['hasSideEffects' => false], 'strtolower' => ['hasSideEffects' => false], - 'strtotime' => ['hasSideEffects' => false], + 'strtotime' => ['hasSideEffects' => true], 'strtoupper' => ['hasSideEffects' => false], 'strtr' => ['hasSideEffects' => false], 'strval' => ['hasSideEffects' => false], @@ -1478,18 +1551,18 @@ 'substr_count' => ['hasSideEffects' => false], 'substr_replace' => ['hasSideEffects' => false], 'symlink' => ['hasSideEffects' => true], - 'sys_getloadavg' => ['hasSideEffects' => false], + 'sys_getloadavg' => ['hasSideEffects' => true], 'tan' => ['hasSideEffects' => false], 'tanh' => ['hasSideEffects' => false], 'tempnam' => ['hasSideEffects' => true], 'timezone_abbreviations_list' => ['hasSideEffects' => false], - 'timezone_identifiers_list' => ['hasSideEffects' => false], - 'timezone_location_get' => ['hasSideEffects' => false], - 'timezone_name_from_abbr' => ['hasSideEffects' => false], + 'timezone_identifiers_list' => ['hasSideEffects' => true], + 'timezone_location_get' => ['hasSideEffects' => true], + 'timezone_name_from_abbr' => ['hasSideEffects' => true], 'timezone_name_get' => ['hasSideEffects' => false], - 'timezone_offset_get' => ['hasSideEffects' => false], - 'timezone_open' => ['hasSideEffects' => false], - 'timezone_transitions_get' => ['hasSideEffects' => false], + 'timezone_offset_get' => ['hasSideEffects' => true], + 'timezone_open' => ['hasSideEffects' => true], + 'timezone_transitions_get' => ['hasSideEffects' => true], 'timezone_version_get' => ['hasSideEffects' => false], 'tmpfile' => ['hasSideEffects' => true], 'token_get_all' => ['hasSideEffects' => false], @@ -1498,20 +1571,22 @@ 'transliterator_create' => ['hasSideEffects' => false], 'transliterator_create_from_rules' => ['hasSideEffects' => false], 'transliterator_create_inverse' => ['hasSideEffects' => false], - 'transliterator_get_error_code' => ['hasSideEffects' => false], - 'transliterator_get_error_message' => ['hasSideEffects' => false], + 'transliterator_get_error_code' => ['hasSideEffects' => true], + 'transliterator_get_error_message' => ['hasSideEffects' => true], 'transliterator_list_ids' => ['hasSideEffects' => false], 'transliterator_transliterate' => ['hasSideEffects' => false], 'trim' => ['hasSideEffects' => false], 'ucfirst' => ['hasSideEffects' => false], 'ucwords' => ['hasSideEffects' => false], 'umask' => ['hasSideEffects' => true], + 'uniqid' => ['hasSideEffects' => true], 'unlink' => ['hasSideEffects' => true], 'unpack' => ['hasSideEffects' => false], 'urldecode' => ['hasSideEffects' => false], 'urlencode' => ['hasSideEffects' => false], 'utf8_decode' => ['hasSideEffects' => false], 'utf8_encode' => ['hasSideEffects' => false], + 'version_compare' => ['hasSideEffects' => false], 'vsprintf' => ['hasSideEffects' => false], 'wordwrap' => ['hasSideEffects' => false], 'xml_error_string' => ['hasSideEffects' => false], diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index 0ddf4b727c..31599aaee4 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -3,19 +3,25 @@ namespace PHPStan\Analyser; use Closure; -use PHPStan\Rules\Registry; +use PHPStan\Collectors\CollectedData; +use PHPStan\Collectors\Registry as CollectorRegistry; +use PHPStan\Rules\Registry as RuleRegistry; use Throwable; use function array_fill_keys; use function array_merge; use function count; -use function sprintf; +use function memory_get_peak_usage; -class Analyser +/** + * @phpstan-import-type CollectorData from CollectedData + */ +final class Analyser { public function __construct( private FileAnalyser $fileAnalyser, - private Registry $registry, + private RuleRegistry $ruleRegistry, + private CollectorRegistry $collectorRegistry, private NodeScopeResolver $nodeScopeResolver, private int $internalErrorsCountLimit, ) @@ -43,7 +49,22 @@ public function analyse( $this->nodeScopeResolver->setAnalysedFiles($allAnalysedFiles); $allAnalysedFiles = array_fill_keys($allAnalysedFiles, true); + /** @var list $errors */ $errors = []; + /** @var list $filteredPhpErrors */ + $filteredPhpErrors = []; + /** @var list $allPhpErrors */ + $allPhpErrors = []; + + /** @var list $locallyIgnoredErrors */ + $locallyIgnoredErrors = []; + + $linesToIgnore = []; + $unmatchedLineIgnores = []; + + /** @var CollectorData $collectedData */ + $collectedData = []; + $internalErrorsCount = 0; $reachedInternalErrorsCountLimit = false; $dependencies = []; @@ -57,10 +78,18 @@ public function analyse( $fileAnalyserResult = $this->fileAnalyser->analyseFile( $file, $allAnalysedFiles, - $this->registry, + $this->ruleRegistry, + $this->collectorRegistry, null, ); $errors = array_merge($errors, $fileAnalyserResult->getErrors()); + $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); + $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); + + $locallyIgnoredErrors = array_merge($locallyIgnoredErrors, $fileAnalyserResult->getLocallyIgnoredErrors()); + $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore(); + $unmatchedLineIgnores[$file] = $fileAnalyserResult->getUnmatchedLineIgnores(); + $collectedData = array_merge($collectedData, $fileAnalyserResult->getCollectedData()); $dependencies[$file] = $fileAnalyserResult->getDependencies(); $fileExportedNodes = $fileAnalyserResult->getExportedNodes(); @@ -72,14 +101,12 @@ public function analyse( throw $t; } $internalErrorsCount++; - $internalErrorMessage = sprintf('Internal error: %s', $t->getMessage()); - $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', - ); - $errors[] = new Error($internalErrorMessage, $file, null, $t); + $errors[] = (new Error($t->getMessage(), $file, null, $t)) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($t), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $t->getTraceAsString(), + ]); if ($internalErrorsCount >= $this->internalErrorsCountLimit) { $reachedInternalErrorsCountLimit = true; break; @@ -95,10 +122,17 @@ public function analyse( return new AnalyserResult( $errors, + $filteredPhpErrors, + $allPhpErrors, + $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, [], + $collectedData, $internalErrorsCount === 0 ? $dependencies : null, $exportedNodes, $reachedInternalErrorsCountLimit, + memory_get_peak_usage(true), ); } diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index 3966d008b3..4226e76fbd 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -2,47 +2,51 @@ namespace PHPStan\Analyser; -use PHPStan\Dependency\ExportedNode; +use PHPStan\Collectors\CollectedData; +use PHPStan\Dependency\RootExportedNode; use function usort; -class AnalyserResult +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + * @phpstan-import-type CollectorData from CollectedData + */ +final class AnalyserResult { - /** @var Error[] */ - private array $unorderedErrors; + /** @var list|null */ + private ?array $errors = null; /** - * @param Error[] $errors - * @param string[] $internalErrors + * @param list $unorderedErrors + * @param list $filteredPhpErrors + * @param list $allPhpErrors + * @param list $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores + * @param CollectorData $collectedData + * @param list $internalErrors * @param array>|null $dependencies - * @param array> $exportedNodes + * @param array> $exportedNodes */ public function __construct( - private array $errors, + private array $unorderedErrors, + private array $filteredPhpErrors, + private array $allPhpErrors, + private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, private array $internalErrors, + private array $collectedData, private ?array $dependencies, private array $exportedNodes, private bool $reachedInternalErrorsCountLimit, + private int $peakMemoryUsageBytes, ) { - $this->unorderedErrors = $errors; - - usort( - $this->errors, - static fn (Error $a, Error $b): int => [ - $a->getFile(), - $a->getLine(), - $a->getMessage(), - ] <=> [ - $b->getFile(), - $b->getLine(), - $b->getMessage(), - ], - ); } /** - * @return Error[] + * @return list */ public function getUnorderedErrors(): array { @@ -50,21 +54,85 @@ public function getUnorderedErrors(): array } /** - * @return Error[] + * @return list */ public function getErrors(): array { + if (!isset($this->errors)) { + $this->errors = $this->unorderedErrors; + usort( + $this->errors, + static fn (Error $a, Error $b): int => [ + $a->getFile(), + $a->getLine(), + $a->getMessage(), + ] <=> [ + $b->getFile(), + $b->getLine(), + $b->getMessage(), + ], + ); + } + return $this->errors; } /** - * @return string[] + * @return list + */ + public function getFilteredPhpErrors(): array + { + return $this->filteredPhpErrors; + } + + /** + * @return list + */ + public function getAllPhpErrors(): array + { + return $this->allPhpErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return array + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return array + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + + /** + * @return list */ public function getInternalErrors(): array { return $this->internalErrors; } + /** + * @return CollectorData + */ + public function getCollectedData(): array + { + return $this->collectedData; + } + /** * @return array>|null */ @@ -74,7 +142,7 @@ public function getDependencies(): ?array } /** - * @return array> + * @return array> */ public function getExportedNodes(): array { @@ -86,4 +154,9 @@ public function hasReachedInternalErrorsCountLimit(): bool return $this->reachedInternalErrorsCountLimit; } + public function getPeakMemoryUsageBytes(): int + { + return $this->peakMemoryUsageBytes; + } + } diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php new file mode 100644 index 0000000000..56fde0132e --- /dev/null +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -0,0 +1,233 @@ +getCollectedData()) === 0) { + return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []); + } + + $hasInternalErrors = count($analyserResult->getInternalErrors()) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit(); + if ($hasInternalErrors) { + return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []); + } + + $nodeType = CollectedDataNode::class; + $node = new CollectedDataNode($analyserResult->getCollectedData(), $onlyFiles); + + $file = 'N/A'; + $scope = $this->scopeFactory->create(ScopeContext::create($file)); + $tempCollectorErrors = []; + $internalErrors = $analyserResult->getInternalErrors(); + foreach ($this->ruleRegistry->getRules($nodeType) as $rule) { + try { + $ruleErrors = $rule->processNode($node, $scope); + } catch (AnalysedCodeException $e) { + $tempCollectorErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + continue; + } catch (IdentifierNotFound $e) { + $tempCollectorErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + continue; + } catch (UnableToCompileNode | CircularReference $e) { + $tempCollectorErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + continue; + } catch (Throwable $t) { + if ($debug) { + throw $t; + } + + $internalErrors[] = new InternalError( + $t->getMessage(), + sprintf('running CollectedDataNode rule %s', get_class($rule)), + InternalError::prepareTrace($t), + $t->getTraceAsString(), + true, + ); + continue; + } + + foreach ($ruleErrors as $ruleError) { + $error = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); + + if ($error->canBeIgnored()) { + foreach ($this->ignoreErrorExtensionProvider->getExtensions() as $ignoreErrorExtension) { + if ($ignoreErrorExtension->shouldIgnore($error, $node, $scope)) { + continue 2; + } + } + } + + $tempCollectorErrors[] = $error; + } + } + + $errors = $analyserResult->getUnorderedErrors(); + $locallyIgnoredErrors = $analyserResult->getLocallyIgnoredErrors(); + $allLinesToIgnore = $analyserResult->getLinesToIgnore(); + $allUnmatchedLineIgnores = $analyserResult->getUnmatchedLineIgnores(); + $collectorErrors = []; + $locallyIgnoredCollectorErrors = []; + foreach ($tempCollectorErrors as $tempCollectorError) { + $file = $tempCollectorError->getFilePath(); + $linesToIgnore = $allLinesToIgnore[$file] ?? []; + $unmatchedLineIgnores = $allUnmatchedLineIgnores[$file] ?? []; + $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( + [$tempCollectorError], + $linesToIgnore, + $unmatchedLineIgnores, + ); + foreach ($localIgnoresProcessorResult->getFileErrors() as $error) { + $errors[] = $error; + $collectorErrors[] = $error; + } + foreach ($localIgnoresProcessorResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { + $locallyIgnoredErrors[] = $locallyIgnoredError; + $locallyIgnoredCollectorErrors[] = $locallyIgnoredError; + } + $allLinesToIgnore[$file] = $localIgnoresProcessorResult->getLinesToIgnore(); + $allUnmatchedLineIgnores[$file] = $localIgnoresProcessorResult->getUnmatchedLineIgnores(); + } + + return $this->addUnmatchedIgnoredErrors(new AnalyserResult( + array_merge($errors, $analyserResult->getFilteredPhpErrors()), + [], + $analyserResult->getAllPhpErrors(), + $locallyIgnoredErrors, + $allLinesToIgnore, + $allUnmatchedLineIgnores, + $internalErrors, + $analyserResult->getCollectedData(), + $analyserResult->getDependencies(), + $analyserResult->getExportedNodes(), + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ), $collectorErrors, $locallyIgnoredCollectorErrors); + } + + private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): AnalyserResult + { + return new AnalyserResult( + array_merge($analyserResult->getUnorderedErrors(), $analyserResult->getFilteredPhpErrors()), + [], + $analyserResult->getAllPhpErrors(), + $analyserResult->getLocallyIgnoredErrors(), + $analyserResult->getLinesToIgnore(), + $analyserResult->getUnmatchedLineIgnores(), + $analyserResult->getInternalErrors(), + $analyserResult->getCollectedData(), + $analyserResult->getDependencies(), + $analyserResult->getExportedNodes(), + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ); + } + + /** + * @param list $collectorErrors + * @param list $locallyIgnoredCollectorErrors + */ + private function addUnmatchedIgnoredErrors( + AnalyserResult $analyserResult, + array $collectorErrors, + array $locallyIgnoredCollectorErrors, + ): FinalizerResult + { + if (!$this->reportUnmatchedIgnoredErrors) { + return new FinalizerResult($analyserResult, $collectorErrors, $locallyIgnoredCollectorErrors); + } + + $errors = $analyserResult->getUnorderedErrors(); + foreach ($analyserResult->getUnmatchedLineIgnores() as $file => $data) { + foreach ($data as $ignoredFile => $lines) { + if ($ignoredFile !== $file) { + continue; + } + + foreach ($lines as $line => $identifiers) { + if ($identifiers === null) { + $errors[] = (new Error( + sprintf('No error to ignore is reported on line %d.', $line), + $file, + $line, + false, + $file, + ))->withIdentifier('ignore.unmatchedLine'); + continue; + } + + foreach ($identifiers as $identifier) { + $errors[] = (new Error( + sprintf('No error with identifier %s is reported on line %d.', $identifier, $line), + $file, + $line, + false, + $file, + ))->withIdentifier('ignore.unmatchedIdentifier'); + } + } + } + } + + return new FinalizerResult( + new AnalyserResult( + $errors, + $analyserResult->getFilteredPhpErrors(), + $analyserResult->getAllPhpErrors(), + $analyserResult->getLocallyIgnoredErrors(), + $analyserResult->getLinesToIgnore(), + $analyserResult->getUnmatchedLineIgnores(), + $analyserResult->getInternalErrors(), + $analyserResult->getCollectedData(), + $analyserResult->getDependencies(), + $analyserResult->getExportedNodes(), + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ), + $collectorErrors, + $locallyIgnoredCollectorErrors, + ); + } + +} diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php new file mode 100644 index 0000000000..da37080cfa --- /dev/null +++ b/src/Analyser/ArgumentsNormalizer.php @@ -0,0 +1,299 @@ +getArgs(); + if (count($args) < 1) { + return null; + } + + $passThruArgs = []; + $callbackArg = null; + foreach ($args as $i => $arg) { + if ($callbackArg === null) { + if ($arg->name === null && $i === 0) { + $callbackArg = $arg; + continue; + } + if ($arg->name !== null && $arg->name->toString() === 'callback') { + $callbackArg = $arg; + continue; + } + } + + $passThruArgs[] = $arg; + } + + if ($callbackArg === null) { + return null; + } + + $calledOnType = $scope->getType($callbackArg->value); + if (!$calledOnType->isCallable()->yes()) { + return null; + } + + $callableParametersAcceptors = $calledOnType->getCallableParametersAcceptors($scope); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $passThruArgs, + $callableParametersAcceptors, + null, + ); + + $acceptsNamedArguments = TrinaryLogic::createYes(); + foreach ($callableParametersAcceptors as $callableParametersAcceptor) { + $acceptsNamedArguments = $acceptsNamedArguments->and($callableParametersAcceptor->acceptsNamedArguments()); + } + + return [$parametersAcceptor, new FuncCall( + $callbackArg->value, + $passThruArgs, + $callUserFuncCall->getAttributes(), + ), $acceptsNamedArguments]; + } + + public static function reorderFuncArguments( + ParametersAcceptor $parametersAcceptor, + FuncCall $functionCall, + ): ?FuncCall + { + $reorderedArgs = self::reorderArgs($parametersAcceptor, $functionCall->getArgs()); + + if ($reorderedArgs === null) { + return null; + } + + return new FuncCall( + $functionCall->name, + $reorderedArgs, + $functionCall->getAttributes(), + ); + } + + public static function reorderMethodArguments( + ParametersAcceptor $parametersAcceptor, + MethodCall $methodCall, + ): ?MethodCall + { + $reorderedArgs = self::reorderArgs($parametersAcceptor, $methodCall->getArgs()); + + if ($reorderedArgs === null) { + return null; + } + + return new MethodCall( + $methodCall->var, + $methodCall->name, + $reorderedArgs, + $methodCall->getAttributes(), + ); + } + + public static function reorderStaticCallArguments( + ParametersAcceptor $parametersAcceptor, + StaticCall $staticCall, + ): ?StaticCall + { + $reorderedArgs = self::reorderArgs($parametersAcceptor, $staticCall->getArgs()); + + if ($reorderedArgs === null) { + return null; + } + + return new StaticCall( + $staticCall->class, + $staticCall->name, + $reorderedArgs, + $staticCall->getAttributes(), + ); + } + + public static function reorderNewArguments( + ParametersAcceptor $parametersAcceptor, + New_ $new, + ): ?New_ + { + $reorderedArgs = self::reorderArgs($parametersAcceptor, $new->getArgs()); + + if ($reorderedArgs === null) { + return null; + } + + return new New_( + $new->class, + $reorderedArgs, + $new->getAttributes(), + ); + } + + /** + * @param Arg[] $callArgs + * @return ?array + */ + public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array + { + if (count($callArgs) === 0) { + return []; + } + + $signatureParameters = $parametersAcceptor->getParameters(); + + $hasNamedArgs = false; + foreach ($callArgs as $arg) { + if ($arg->name !== null) { + $hasNamedArgs = true; + break; + } + } + if (!$hasNamedArgs) { + return $callArgs; + } + + $hasVariadic = false; + $argumentPositions = []; + foreach ($signatureParameters as $i => $parameter) { + if ($hasVariadic) { + // variadic parameter must be last + return null; + } + + $hasVariadic = $parameter->isVariadic(); + $argumentPositions[$parameter->getName()] = $i; + } + + $reorderedArgs = []; + $additionalNamedArgs = []; + $appendArgs = []; + foreach ($callArgs as $i => $arg) { + if ($arg->name === null) { + // add regular args as is + $reorderedArgs[$i] = $arg; + } elseif (array_key_exists($arg->name->toString(), $argumentPositions)) { + $argName = $arg->name->toString(); + // order named args into the position the signature expects them + $attributes = $arg->getAttributes(); + $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg; + $reorderedArgs[$argumentPositions[$argName]] = new Arg( + $arg->value, + $arg->byRef, + $arg->unpack, + $attributes, + null, + ); + } else { + if (!$hasVariadic) { + $attributes = $arg->getAttributes(); + $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg; + $appendArgs[] = new Arg( + $arg->value, + $arg->byRef, + $arg->unpack, + $attributes, + null, + ); + continue; + } + + $attributes = $arg->getAttributes(); + $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg; + $additionalNamedArgs[] = new Arg( + $arg->value, + $arg->byRef, + $arg->unpack, + $attributes, + null, + ); + } + } + + // replace variadic parameter with additional named args, except if it is already set + $additionalNamedArgsOffset = count($argumentPositions) - 1; + if (array_key_exists($additionalNamedArgsOffset, $reorderedArgs)) { + $additionalNamedArgsOffset++; + } + + foreach ($additionalNamedArgs as $i => $additionalNamedArg) { + $reorderedArgs[$additionalNamedArgsOffset + $i] = $additionalNamedArg; + } + + if (count($reorderedArgs) === 0) { + foreach ($appendArgs as $arg) { + $reorderedArgs[] = $arg; + } + return $reorderedArgs; + } + + // fill up all holes with default values until the last given argument + for ($j = 0; $j < max(array_keys($reorderedArgs)); $j++) { + if (array_key_exists($j, $reorderedArgs)) { + continue; + } + if (!array_key_exists($j, $signatureParameters)) { + throw new ShouldNotHappenException('Parameter signatures cannot have holes'); + } + + $parameter = $signatureParameters[$j]; + + // we can only fill up optional parameters with default values + if (!$parameter->isOptional()) { + return null; + } + + $defaultValue = $parameter->getDefaultValue(); + if ($defaultValue === null) { + if (!$parameter->isVariadic()) { + throw new ShouldNotHappenException(sprintf('An optional parameter $%s must have a default value', $parameter->getName())); + } + $defaultValue = new ConstantArrayType([], []); + } + + $reorderedArgs[$j] = new Arg( + new TypeExpr($defaultValue), + ); + } + + ksort($reorderedArgs); + + foreach ($appendArgs as $arg) { + $reorderedArgs[] = $arg; + } + + return $reorderedArgs; + } + +} diff --git a/src/Analyser/ConditionalExpressionHolder.php b/src/Analyser/ConditionalExpressionHolder.php index b36e35bc26..6907183966 100644 --- a/src/Analyser/ConditionalExpressionHolder.php +++ b/src/Analyser/ConditionalExpressionHolder.php @@ -3,37 +3,36 @@ 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 +final class ConditionalExpressionHolder { /** - * @param array $conditionExpressionTypes + * @param array $conditionExpressionTypeHolders */ public function __construct( - private array $conditionExpressionTypes, - private VariableTypeHolder $typeHolder, + private array $conditionExpressionTypeHolders, + private ExpressionTypeHolder $typeHolder, ) { - if (count($conditionExpressionTypes) === 0) { + if (count($conditionExpressionTypeHolders) === 0) { throw new ShouldNotHappenException(); } } /** - * @return array + * @return array */ - public function getConditionExpressionTypes(): array + public function getConditionExpressionTypeHolders(): array { - return $this->conditionExpressionTypes; + return $this->conditionExpressionTypeHolders; } - public function getTypeHolder(): VariableTypeHolder + public function getTypeHolder(): ExpressionTypeHolder { return $this->typeHolder; } @@ -41,8 +40,8 @@ public function getTypeHolder(): VariableTypeHolder public function getKey(): string { $parts = []; - foreach ($this->conditionExpressionTypes as $exprString => $type) { - $parts[] = $exprString . '=' . $type->describe(VerbosityLevel::precise()); + foreach ($this->conditionExpressionTypeHolders as $exprString => $typeHolder) { + $parts[] = $exprString . '=' . $typeHolder->getType()->describe(VerbosityLevel::precise()); } return sprintf( diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php new file mode 100644 index 0000000000..846188b985 --- /dev/null +++ b/src/Analyser/ConstantResolver.php @@ -0,0 +1,439 @@ + */ + private array $currentlyResolving = []; + + /** + * @param string[] $dynamicConstantNames + * @param int|array{min: int, max: int}|null $phpVersion + */ + public function __construct( + private ReflectionProviderProvider $reflectionProviderProvider, + private array $dynamicConstantNames, + private int|array|null $phpVersion, + private ComposerPhpVersionFactory $composerPhpVersionFactory, + ) + { + } + + public function resolveConstant(Name $name, ?NamespaceAnswerer $scope): ?Type + { + if (!$this->getReflectionProvider()->hasConstant($name, $scope)) { + return null; + } + + /** @var string $resolvedConstantName */ + $resolvedConstantName = $this->getReflectionProvider()->resolveConstantName($name, $scope); + + $constantType = $this->resolvePredefinedConstant($resolvedConstantName); + if ($constantType !== null) { + return $constantType; + } + + if (array_key_exists($resolvedConstantName, $this->currentlyResolving)) { + return new MixedType(); + } + + $this->currentlyResolving[$resolvedConstantName] = true; + + $constantReflection = $this->getReflectionProvider()->getConstant($name, $scope); + $constantType = $constantReflection->getValueType(); + + $type = $this->resolveConstantType($resolvedConstantName, $constantType); + unset($this->currentlyResolving[$resolvedConstantName]); + + return $type; + } + + public function resolvePredefinedConstant(string $resolvedConstantName): ?Type + { + // core, https://www.php.net/manual/en/reserved.constants.php + if ($resolvedConstantName === 'PHP_VERSION') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + + $minPhpVersion = null; + $maxPhpVersion = null; + if (in_array($resolvedConstantName, ['PHP_VERSION_ID', 'PHP_MAJOR_VERSION', 'PHP_MINOR_VERSION', 'PHP_RELEASE_VERSION'], true)) { + $minPhpVersion = $this->getMinPhpVersion(); + $maxPhpVersion = $this->getMaxPhpVersion(); + } + + if ($resolvedConstantName === 'PHP_MAJOR_VERSION') { + $minMajor = 5; + $maxMajor = null; + + if ($minPhpVersion !== null) { + $minMajor = max($minMajor, $minPhpVersion->getMajorVersionId()); + } + if ($maxPhpVersion !== null) { + $maxMajor = $maxPhpVersion->getMajorVersionId(); + } + + return $this->createInteger($minMajor, $maxMajor); + } + if ($resolvedConstantName === 'PHP_MINOR_VERSION') { + $minMinor = 0; + $maxMinor = null; + + if ( + $minPhpVersion !== null + && $maxPhpVersion !== null + && $maxPhpVersion->getMajorVersionId() === $minPhpVersion->getMajorVersionId() + ) { + $minMinor = $minPhpVersion->getMinorVersionId(); + $maxMinor = $maxPhpVersion->getMinorVersionId(); + } + + return $this->createInteger($minMinor, $maxMinor); + } + if ($resolvedConstantName === 'PHP_RELEASE_VERSION') { + $minRelease = 0; + $maxRelease = null; + + if ( + $minPhpVersion !== null + && $maxPhpVersion !== null + && $maxPhpVersion->getMajorVersionId() === $minPhpVersion->getMajorVersionId() + && $maxPhpVersion->getMinorVersionId() === $minPhpVersion->getMinorVersionId() + ) { + $minRelease = $minPhpVersion->getPatchVersionId(); + $maxRelease = $maxPhpVersion->getPatchVersionId(); + } + + return $this->createInteger($minRelease, $maxRelease); + } + if ($resolvedConstantName === 'PHP_VERSION_ID') { + $minVersion = 50207; + $maxVersion = null; + if ($minPhpVersion !== null) { + $minVersion = max($minVersion, $minPhpVersion->getVersionId()); + } + if ($maxPhpVersion !== null) { + $maxVersion = $maxPhpVersion->getVersionId(); + } + + return $this->createInteger($minVersion, $maxVersion); + } + 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 AccessoryNonFalsyStringType(), + ]); + } + 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 AccessoryNonFalsyStringType(), + ]), + ]); + } + 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 AccessoryNonFalsyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_PREFIX') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_BINDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_BINARY') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_MANDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_LIBDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_DATADIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_SYSCONFDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_LOCALSTATEDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_CONFIG_FILE_PATH') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + 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(1, 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('/'), + new ConstantStringType('\\'), + ]); + } + if ($resolvedConstantName === 'PATH_SEPARATOR') { + return new UnionType([ + new ConstantStringType(':'), + new ConstantStringType(';'), + ]); + } + // iconv, https://www.php.net/manual/en/iconv.constants.php + if ($resolvedConstantName === 'ICONV_IMPL') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + // 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 AccessoryNonFalsyStringType(), + ]); + } + // openssl, https://www.php.net/manual/en/openssl.constants.php + if ($resolvedConstantName === 'OPENSSL_VERSION_NUMBER') { + return IntegerRangeType::fromInterval(1, null); + } + + // pcre, https://www.php.net/manual/en/pcre.constants.php + if ($resolvedConstantName === 'PCRE_VERSION') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + + if (in_array($resolvedConstantName, ['STDIN', 'STDOUT', 'STDERR'], true)) { + return new ResourceType(); + } + if ($resolvedConstantName === 'NAN') { + return new ConstantFloatType(NAN); + } + if ($resolvedConstantName === 'INF') { + return new ConstantFloatType(INF); + } + + return null; + } + + private function getMinPhpVersion(): ?PhpVersion + { + if (is_int($this->phpVersion)) { + return null; + } + + if (is_array($this->phpVersion)) { + if ($this->phpVersion['max'] < $this->phpVersion['min']) { + throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } + + return new PhpVersion($this->phpVersion['min']); + } + + return $this->composerPhpVersionFactory->getMinVersion(); + } + + private function getMaxPhpVersion(): ?PhpVersion + { + if (is_int($this->phpVersion)) { + return null; + } + + if (is_array($this->phpVersion)) { + if ($this->phpVersion['max'] < $this->phpVersion['min']) { + throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } + + return new PhpVersion($this->phpVersion['max']); + } + + return $this->composerPhpVersionFactory->getMaxVersion(); + } + + public function resolveConstantType(string $constantName, Type $constantType): Type + { + if ($constantType->isConstantValue()->yes() && in_array($constantName, $this->dynamicConstantNames, true)) { + return $constantType->generalize(GeneralizePrecision::lessSpecific()); + } + + return $constantType; + } + + public function resolveClassConstantType(string $className, string $constantName, Type $constantType, ?Type $nativeType): Type + { + $lookupConstantName = sprintf('%s::%s', $className, $constantName); + if (in_array($lookupConstantName, $this->dynamicConstantNames, true)) { + if ($nativeType !== null) { + return $nativeType; + } + + if ($constantType->isConstantValue()->yes()) { + return $constantType->generalize(GeneralizePrecision::lessSpecific()); + } + } + + return $constantType; + } + + private function createInteger(?int $min, ?int $max): Type + { + if ($min !== null && $min === $max) { + return new ConstantIntegerType($min); + } + return IntegerRangeType::fromInterval($min, $max); + } + + private function getReflectionProvider(): ReflectionProvider + { + return $this->reflectionProviderProvider->getReflectionProvider(); + } + +} diff --git a/src/Analyser/ConstantResolverFactory.php b/src/Analyser/ConstantResolverFactory.php new file mode 100644 index 0000000000..5ccc516e42 --- /dev/null +++ b/src/Analyser/ConstantResolverFactory.php @@ -0,0 +1,31 @@ +container->getByType(ComposerPhpVersionFactory::class); + + return new ConstantResolver( + $this->reflectionProviderProvider, + $this->container->getParameter('dynamicConstantNames'), + $this->container->getParameter('phpVersion'), + $composerFactory, + ); + } + +} diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php new file mode 100644 index 0000000000..f65a599113 --- /dev/null +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -0,0 +1,96 @@ +reflectionProvider, + $this->initializerExprTypeResolver, + $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry(), + $this->expressionTypeResolverExtensionRegistryProvider->getRegistry(), + $this->exprPrinter, + $this->typeSpecifier, + $this->propertyReflectionFinder, + $this->parser, + $this->nodeScopeResolver, + $this->richerScopeGetTypeHelper, + $this->constantResolver, + $context, + $this->phpVersion, + $this->attributeReflectionFactory, + $this->configPhpVersion, + $declareStrictTypes, + $function, + $namespace, + $expressionTypes, + $nativeExpressionTypes, + $conditionalExpressions, + $inClosureBindScopeClasses, + $anonymousFunctionReflection, + $inFirstLevelStatement, + $currentlyAssignedExpressions, + $currentlyAllowedUndefinedExpressions, + $inFunctionCallsStack, + $afterExtractCall, + $parentScope, + $nativeTypesPromoted, + ); + } + +} diff --git a/src/Analyser/DirectScopeFactory.php b/src/Analyser/DirectScopeFactory.php deleted file mode 100644 index 2299cd7dd2..0000000000 --- a/src/Analyser/DirectScopeFactory.php +++ /dev/null @@ -1,115 +0,0 @@ -dynamicConstantNames = $container->getParameter('dynamicConstantNames'); - } - - /** - * @param array $constantTypes - * @param VariableTypeHolder[] $variablesTypes - * @param VariableTypeHolder[] $moreSpecificTypes - * @param array $conditionalExpressions - * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack - * - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - FunctionReflection|MethodReflection|null $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - bool $afterExtractCall = false, - ?Scope $parentScope = null, - ): MutatingScope - { - $scopeClass = $this->scopeClass; - if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new ShouldNotHappenException(); - } - - return new $scopeClass( - $this, - $this->reflectionProvider, - $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry(), - $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry(), - $this->printer, - $this->typeSpecifier, - $this->propertyReflectionFinder, - $this->parser, - $this->nodeScopeResolver, - $context, - $this->phpVersion, - $declareStrictTypes, - $constantTypes, - $function, - $namespace, - $variablesTypes, - $moreSpecificTypes, - $conditionalExpressions, - $inClosureBindScopeClass, - $anonymousFunctionReflection, - $inFirstLevelStatement, - $currentlyAssignedExpressions, - $nativeExpressionTypes, - $inFunctionCallsStack, - $this->dynamicConstantNames, - $this->treatPhpDocTypesAsCertain, - $afterExtractCall, - $parentScope, - $this->explicitMixedInUnknownGenericNew, - ); - } - -} diff --git a/src/Analyser/EndStatementResult.php b/src/Analyser/EndStatementResult.php new file mode 100644 index 0000000000..18f97ddc50 --- /dev/null +++ b/src/Analyser/EndStatementResult.php @@ -0,0 +1,27 @@ +statement; + } + + public function getResult(): StatementResult + { + return $this->result; + } + +} diff --git a/src/Analyser/EnsuredNonNullabilityResult.php b/src/Analyser/EnsuredNonNullabilityResult.php index 258a16b18b..6a9e539cf1 100644 --- a/src/Analyser/EnsuredNonNullabilityResult.php +++ b/src/Analyser/EnsuredNonNullabilityResult.php @@ -2,7 +2,7 @@ namespace PHPStan\Analyser; -class EnsuredNonNullabilityResult +final class EnsuredNonNullabilityResult { /** diff --git a/src/Analyser/EnsuredNonNullabilityResultExpression.php b/src/Analyser/EnsuredNonNullabilityResultExpression.php index a7ed1572f8..59f5eba2ee 100644 --- a/src/Analyser/EnsuredNonNullabilityResultExpression.php +++ b/src/Analyser/EnsuredNonNullabilityResultExpression.php @@ -3,15 +3,17 @@ namespace PHPStan\Analyser; use PhpParser\Node\Expr; +use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class EnsuredNonNullabilityResultExpression +final class EnsuredNonNullabilityResultExpression { public function __construct( private Expr $expression, private Type $originalType, private Type $originalNativeType, + private TrinaryLogic $certainty, ) { } @@ -31,4 +33,9 @@ public function getOriginalNativeType(): Type return $this->originalNativeType; } + public function getCertainty(): TrinaryLogic + { + return $this->certainty; + } + } diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index f36ba89284..1ad85c60be 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -4,16 +4,22 @@ use Exception; use JsonSerializable; +use Nette\Utils\Strings; use PhpParser\Node; use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; use Throwable; use function is_bool; +use function sprintf; -/** @api */ -class Error implements JsonSerializable +/** + * @api + */ +final class Error implements JsonSerializable { + public const PATTERN_IDENTIFIER = '[a-zA-Z0-9](?:[a-zA-Z0-9\\.]*[a-zA-Z0-9])?'; + /** * Error constructor. * @@ -34,6 +40,9 @@ public function __construct( private array $metadata = [], ) { + if ($this->identifier !== null && !self::validateIdentifier($this->identifier)) { + throw new ShouldNotHappenException(sprintf('Invalid identifier: %s', $this->identifier)); + } } public function getMessage(): string @@ -156,6 +165,51 @@ public function doNotIgnore(): self ); } + public function withIdentifier(string $identifier): self + { + if ($this->identifier !== null) { + throw new ShouldNotHappenException(sprintf('Error already has an identifier: %s', $this->identifier)); + } + + return new self( + $this->message, + $this->file, + $this->line, + $this->canBeIgnored, + $this->filePath, + $this->traitFilePath, + $this->tip, + $this->nodeLine, + $this->nodeType, + $identifier, + $this->metadata, + ); + } + + /** + * @param mixed[] $metadata + */ + public function withMetadata(array $metadata): self + { + if ($this->metadata !== []) { + throw new ShouldNotHappenException('Error already has metadata'); + } + + return new self( + $this->message, + $this->file, + $this->line, + $this->canBeIgnored, + $this->filePath, + $this->traitFilePath, + $this->tip, + $this->nodeLine, + $this->nodeType, + $this->identifier, + $metadata, + ); + } + public function getNodeLine(): ?int { return $this->nodeLine; @@ -169,6 +223,11 @@ public function getNodeType(): ?string return $this->nodeType; } + /** + * Error identifier set via `RuleErrorBuilder::identifier()`. + * + * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers + */ public function getIdentifier(): ?string { return $this->identifier; @@ -243,4 +302,9 @@ public static function __set_state(array $properties): self ); } + public static function validateIdentifier(string $identifier): bool + { + return Strings::match($identifier, '~^' . self::PATTERN_IDENTIFIER . '$~') !== null; + } + } diff --git a/src/Analyser/ExpressionContext.php b/src/Analyser/ExpressionContext.php index df1f55d284..c910b0cc3d 100644 --- a/src/Analyser/ExpressionContext.php +++ b/src/Analyser/ExpressionContext.php @@ -4,25 +4,26 @@ use PHPStan\Type\Type; -class ExpressionContext +final class ExpressionContext { private function __construct( private bool $isDeep, private ?string $inAssignRightSideVariableName, private ?Type $inAssignRightSideType, + private ?Type $inAssignRightSideNativeType, ) { } public static function createTopLevel(): self { - return new self(false, null, null); + return new self(false, null, null, null); } public static function createDeep(): self { - return new self(true, null, null); + return new self(true, null, null, null); } public function enterDeep(): self @@ -31,7 +32,7 @@ public function enterDeep(): self return $this; } - return new self(true, $this->inAssignRightSideVariableName, $this->inAssignRightSideType); + return new self(true, $this->inAssignRightSideVariableName, $this->inAssignRightSideType, $this->inAssignRightSideNativeType); } public function isDeep(): bool @@ -39,9 +40,9 @@ public function isDeep(): bool return $this->isDeep; } - public function enterRightSideAssign(string $variableName, Type $type): self + public function enterRightSideAssign(string $variableName, Type $type, Type $nativeType): self { - return new self($this->isDeep, $variableName, $type); + return new self($this->isDeep, $variableName, $type, $nativeType); } public function getInAssignRightSideVariableName(): ?string @@ -54,4 +55,9 @@ public function getInAssignRightSideType(): ?Type return $this->inAssignRightSideType; } + public function getInAssignRightSideNativeType(): ?Type + { + return $this->inAssignRightSideNativeType; + } + } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 93a3bdca25..0a50465169 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -2,7 +2,7 @@ namespace PHPStan\Analyser; -class ExpressionResult +final class ExpressionResult { /** @var (callable(): MutatingScope)|null */ @@ -17,6 +17,7 @@ class ExpressionResult /** * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ @@ -24,6 +25,7 @@ public function __construct( private MutatingScope $scope, private bool $hasYield, private array $throwPoints, + private array $impurePoints, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ) @@ -50,6 +52,14 @@ public function getThrowPoints(): array return $this->throwPoints; } + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function getTruthyScope(): MutatingScope { if ($this->truthyScopeCallback === null) { diff --git a/src/Analyser/ExpressionTypeHolder.php b/src/Analyser/ExpressionTypeHolder.php new file mode 100644 index 0000000000..bb598d8fb1 --- /dev/null +++ b/src/Analyser/ExpressionTypeHolder.php @@ -0,0 +1,69 @@ +certainty->equals($other->certainty)) { + return false; + } + + return $this->type->equals($other->type); + } + + public function and(self $other): self + { + if ($this->type->equals($other->type)) { + if ($this->certainty->equals($other->certainty)) { + return $this; + } + + $type = $this->type; + } else { + $type = TypeCombinator::union($this->type, $other->type); + } + return new self( + $this->expr, + $type, + $this->certainty->and($other->certainty), + ); + } + + public function getExpr(): Expr + { + return $this->expr; + } + + public function getType(): Type + { + return $this->type; + } + + public function getCertainty(): TrinaryLogic + { + return $this->certainty; + } + +} diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index 4d9cdb9297..80724ea18a 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -2,52 +2,61 @@ namespace PHPStan\Analyser; -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; +use PHPStan\BetterReflection\Reflection\Exception\CircularReference; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\Collectors\CollectedData; +use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Node\FileNode; +use PHPStan\Node\InTraitNode; use PHPStan\Parser\Parser; use PHPStan\Parser\ParserErrorsException; -use PHPStan\Rules\FileRuleError; -use PHPStan\Rules\IdentifierRuleError; -use PHPStan\Rules\LineRuleError; -use PHPStan\Rules\MetadataRuleError; -use PHPStan\Rules\NonIgnorableRuleError; -use PHPStan\Rules\Registry; -use PHPStan\Rules\TipRuleError; -use function array_key_exists; +use PHPStan\Rules\Registry as RuleRegistry; use function array_keys; -use function array_merge; use function array_unique; use function array_values; +use function count; use function error_reporting; use function get_class; use function is_dir; 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 +use const E_ERROR; +use const E_NOTICE; +use const E_PARSE; +use const E_STRICT; +use const E_USER_DEPRECATED; +use const E_USER_ERROR; +use const E_USER_NOTICE; +use const E_USER_WARNING; +use const E_WARNING; + +/** + * @phpstan-import-type CollectorData from CollectedData + */ +final class FileAnalyser { - /** @var Error[] */ - private array $collectedErrors = []; + /** @var list */ + private array $allPhpErrors = []; + + /** @var list */ + private array $filteredPhpErrors = []; public function __construct( private ScopeFactory $scopeFactory, private NodeScopeResolver $nodeScopeResolver, private Parser $parser, private DependencyResolver $dependencyResolver, - private bool $reportUnmatchedIgnoredErrors, + private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider, + private RuleErrorTransformer $ruleErrorTransformer, + private LocalIgnoresProcessor $localIgnoresProcessor, ) { } @@ -59,35 +68,50 @@ public function __construct( public function analyseFile( string $file, array $analysedFiles, - Registry $registry, + RuleRegistry $ruleRegistry, + CollectorRegistry $collectorRegistry, ?callable $outerNodeCallback, ): FileAnalyserResult { + /** @var list $fileErrors */ $fileErrors = []; + + /** @var list $locallyIgnoredErrors */ + $locallyIgnoredErrors = []; + + /** @var CollectorData $fileCollectedData */ + $fileCollectedData = []; + $fileDependencies = []; $exportedNodes = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; if (is_file($file)) { try { $this->collectErrors($analysedFiles); $parserNodes = $this->parser->parseFile($file); - $linesToIgnore = $this->getLinesToIgnoreFromTokens($file, $parserNodes); + $linesToIgnore = $unmatchedLineIgnores = [$file => $this->getLinesToIgnoreFromTokens($parserNodes)]; $temporaryFileErrors = []; - $nodeCallback = function (Node $node, Scope $scope) use (&$fileErrors, &$fileDependencies, &$exportedNodes, $file, $registry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$temporaryFileErrors): void { + $nodeCallback = function (Node $node, Scope $scope) use (&$fileErrors, &$fileCollectedData, &$fileDependencies, &$exportedNodes, $file, $ruleRegistry, $collectorRegistry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$unmatchedLineIgnores, &$temporaryFileErrors): void { if ($node instanceof Node\Stmt\Trait_) { foreach (array_keys($linesToIgnore[$file] ?? []) as $lineToIgnore) { if ($lineToIgnore < $node->getStartLine() || $lineToIgnore > $node->getEndLine()) { continue; } - unset($linesToIgnore[$file][$lineToIgnore]); + unset($unmatchedLineIgnores[$file][$lineToIgnore]); } } + if ($node instanceof InTraitNode) { + $traitNode = $node->getOriginalNode(); + $linesToIgnore[$scope->getFileDescription()] = $this->getLinesToIgnoreFromTokens([$traitNode]); + } if ($outerNodeCallback !== null) { $outerNodeCallback($node, $scope); } $uniquedAnalysedCodeExceptionMessages = []; $nodeType = get_class($node); - foreach ($registry->getRules($nodeType) as $rule) { + foreach ($ruleRegistry->getRules($nodeType) as $rule) { try { $ruleErrors = $rule->processNode($node, $scope); } catch (AnalysedCodeException $e) { @@ -96,93 +120,85 @@ public function analyseFile( } $uniquedAnalysedCodeExceptionMessages[$e->getMessage()] = true; - $fileErrors[] = new Error($e->getMessage(), $file, $node->getLine(), $e, null, null, $e->getTip()); + $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } catch (IdentifierNotFound $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getLine(), $e); + } catch (UnableToCompileNode | CircularReference $e) { + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } foreach ($ruleErrors as $ruleError) { - $nodeLine = $node->getLine(); - $line = $nodeLine; - $canBeIgnored = true; - $fileName = $scope->getFileDescription(); - $filePath = $scope->getFile(); - $traitFilePath = null; - $tip = null; - $identifier = null; - $metadata = []; - if ($scope->isInTrait()) { - $traitReflection = $scope->getTraitReflection(); - if ($traitReflection->getFileName() !== null) { - $traitFilePath = $traitReflection->getFileName(); - } - } - if (is_string($ruleError)) { - $message = $ruleError; - } else { - $message = $ruleError->getMessage(); - if ( - $ruleError instanceof LineRuleError - && $ruleError->getLine() !== -1 - ) { - $line = $ruleError->getLine(); - } - if ( - $ruleError instanceof FileRuleError - && $ruleError->getFile() !== '' - ) { - $fileName = $ruleError->getFile(); - $filePath = $ruleError->getFile(); - $traitFilePath = null; - } - - if ($ruleError instanceof TipRuleError) { - $tip = $ruleError->getTip(); - } - - if ($ruleError instanceof IdentifierRuleError) { - $identifier = $ruleError->getIdentifier(); - } - - if ($ruleError instanceof MetadataRuleError) { - $metadata = $ruleError->getMetadata(); - } + $error = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); - if ($ruleError instanceof NonIgnorableRuleError) { - $canBeIgnored = false; + if ($error->canBeIgnored()) { + foreach ($this->ignoreErrorExtensionProvider->getExtensions() as $ignoreErrorExtension) { + if ($ignoreErrorExtension->shouldIgnore($error, $node, $scope)) { + continue 2; + } } } - $temporaryFileErrors[] = new Error( - $message, - $fileName, - $line, - $canBeIgnored, - $filePath, - $traitFilePath, - $tip, - $nodeLine, - $nodeType, - $identifier, - $metadata, - ); + + $temporaryFileErrors[] = $error; } } - if ($scope->isInTrait()) { - $sameTraitFile = $file === $scope->getTraitReflection()->getFileName(); - foreach ($this->getLinesToIgnore($node) as $lineToIgnore) { - $linesToIgnore[$scope->getFileDescription()][$lineToIgnore] = true; - if (!$sameTraitFile) { + foreach ($collectorRegistry->getCollectors($nodeType) as $collector) { + try { + $collectedData = $collector->processNode($node, $scope); + } catch (AnalysedCodeException $e) { + if (isset($uniquedAnalysedCodeExceptionMessages[$e->getMessage()])) { continue; } - unset($linesToIgnore[$file][$lineToIgnore]); + $uniquedAnalysedCodeExceptionMessages[$e->getMessage()] = true; + $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + continue; + } catch (IdentifierNotFound $e) { + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + continue; + } catch (UnableToCompileNode | CircularReference $e) { + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + continue; + } + + if ($collectedData === null) { + continue; } + + $fileCollectedData[$scope->getFile()][get_class($collector)][] = $collectedData; } try { @@ -197,7 +213,7 @@ public function analyseFile( // pass } catch (IdentifierNotFound) { // pass - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection) { + } catch (UnableToCompileNode) { // pass } }; @@ -209,136 +225,97 @@ public function analyseFile( $scope, $nodeCallback, ); - $unmatchedLineIgnores = $linesToIgnore; - foreach ($temporaryFileErrors as $tmpFileError) { - $line = $tmpFileError->getLine(); - if ( - $line !== null - && $tmpFileError->canBeIgnored() - && array_key_exists($tmpFileError->getFile(), $linesToIgnore) - && array_key_exists($line, $linesToIgnore[$tmpFileError->getFile()]) - ) { - unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); - continue; - } - $fileErrors[] = $tmpFileError; + $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( + $temporaryFileErrors, + $linesToIgnore, + $unmatchedLineIgnores, + ); + foreach ($localIgnoresProcessorResult->getFileErrors() as $fileError) { + $fileErrors[] = $fileError; } - - if ($this->reportUnmatchedIgnoredErrors) { - foreach ($unmatchedLineIgnores as $ignoredFile => $lines) { - if ($ignoredFile !== $file) { - continue; - } - - foreach (array_keys($lines) as $line) { - $fileErrors[] = new Error( - sprintf('No error to ignore is reported on line %d.', $line), - $scope->getFileDescription(), - $line, - false, - $scope->getFile(), - null, - null, - null, - null, - 'ignoredError.unmatchedOnLine', - ); - } - } + foreach ($localIgnoresProcessorResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { + $locallyIgnoredErrors[] = $locallyIgnoredError; } + $linesToIgnore = $localIgnoresProcessorResult->getLinesToIgnore(); + $unmatchedLineIgnores = $localIgnoresProcessorResult->getUnmatchedLineIgnores(); } catch (\PhpParser\Error $e) { - $fileErrors[] = new Error($e->getMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e); + $fileErrors[] = (new Error($e->getRawMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } catch (ParserErrorsException $e) { foreach ($e->getErrors() as $error) { - $fileErrors[] = new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getStartLine() !== -1 ? $error->getStartLine() : null, $e); + $fileErrors[] = (new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getLine() !== -1 ? $error->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } } catch (AnalysedCodeException $e) { - $fileErrors[] = new Error($e->getMessage(), $file, null, $e, null, null, $e->getTip()); + $fileErrors[] = (new Error($e->getMessage(), $file, null, $e, null, null, $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); } catch (IdentifierNotFound $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, null, $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'); - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, null, $e); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, null, $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + } catch (UnableToCompileNode | CircularReference $e) { + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, null, $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); } } elseif (is_dir($file)) { - $fileErrors[] = new Error(sprintf('File %s is a directory.', $file), $file, null, false); + $fileErrors[] = (new Error(sprintf('File %s is a directory.', $file), $file, null, false))->withIdentifier('phpstan.path'); } else { - $fileErrors[] = new Error(sprintf('File %s does not exist.', $file), $file, null, false); + $fileErrors[] = (new Error(sprintf('File %s does not exist.', $file), $file, null, false))->withIdentifier('phpstan.path'); } $this->restoreCollectErrorsHandler(); - $fileErrors = array_merge($fileErrors, $this->collectedErrors); - - return new FileAnalyserResult($fileErrors, array_values(array_unique($fileDependencies)), $exportedNodes); - } - - /** - * @return int[] - */ - private function getLinesToIgnore(Node $node): array - { - $lines = []; - if ($node->getDocComment() !== null) { - $line = $this->findLineToIgnoreComment($node->getDocComment()); - if ($line !== null) { - $lines[] = $line; + foreach ($linesToIgnore as $fileKey => $lines) { + if (count($lines) > 0) { + continue; } + + unset($linesToIgnore[$fileKey]); } - foreach ($node->getComments() as $comment) { - $line = $this->findLineToIgnoreComment($comment); - if ($line === null) { + foreach ($unmatchedLineIgnores as $fileKey => $lines) { + if (count($lines) > 0) { continue; } - $lines[] = $line; + unset($unmatchedLineIgnores[$fileKey]); } - return $lines; + return new FileAnalyserResult( + $fileErrors, + $this->filteredPhpErrors, + $this->allPhpErrors, + $locallyIgnoredErrors, + $fileCollectedData, + array_values(array_unique($fileDependencies)), + $exportedNodes, + $linesToIgnore, + $unmatchedLineIgnores, + ); } /** * @param Node[] $nodes - * @return array> + * @return array|null> */ - private function getLinesToIgnoreFromTokens(string $file, array $nodes): array + private function getLinesToIgnoreFromTokens(array $nodes): array { if (!isset($nodes[0])) { return []; } - /** @var int[] $tokenLines */ - $tokenLines = $nodes[0]->getAttribute('linesToIgnore', []); - $lines = []; - foreach ($tokenLines as $tokenLine) { - $lines[$file][$tokenLine] = true; - } - - return $lines; - } - - private function findLineToIgnoreComment(Comment $comment): ?int - { - $text = $comment->getText(); - if ($comment instanceof Comment\Doc) { - $line = $comment->getEndLine(); - } else { - if (strpos($text, "\n") === false || strpos($text, '//') === 0) { - $line = $comment->getStartLine(); - } else { - $line = $comment->getEndLine(); - } - } - if (strpos($text, '@phpstan-ignore-next-line') !== false) { - return $line + 1; - } - - if (strpos($text, '@phpstan-ignore-line') !== false) { - return $line; - } - - return null; + /** @var array|null> */ + return $nodes[0]->getAttribute('linesToIgnore', []); } /** @@ -346,13 +323,18 @@ private function findLineToIgnoreComment(Comment $comment): ?int */ private function collectErrors(array $analysedFiles): void { - $this->collectedErrors = []; + $this->filteredPhpErrors = []; + $this->allPhpErrors = []; set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($analysedFiles): bool { if ((error_reporting() & $errno) === 0) { // silence @ operator return true; } + $errorMessage = sprintf('%s: %s', $this->getErrorLabel($errno), $errstr); + + $this->allPhpErrors[] = (new Error($errorMessage, $errfile, $errline, false))->withIdentifier('phpstan.php'); + if ($errno === E_DEPRECATED) { return true; } @@ -361,7 +343,7 @@ private function collectErrors(array $analysedFiles): void return true; } - $this->collectedErrors[] = new Error($errstr, $errfile, $errline, true); + $this->filteredPhpErrors[] = (new Error($errorMessage, $errfile, $errline, $errno === E_USER_DEPRECATED))->withIdentifier('phpstan.php'); return true; }); @@ -372,4 +354,32 @@ private function restoreCollectErrorsHandler(): void restore_error_handler(); } + private function getErrorLabel(int $errno): string + { + switch ($errno) { + case E_ERROR: + return 'Fatal error'; + case E_WARNING: + return 'Warning'; + case E_PARSE: + return 'Parse error'; + case E_NOTICE: + return 'Notice'; + case E_DEPRECATED: + return 'Deprecated'; + case E_USER_ERROR: + return 'User error (E_USER_ERROR)'; + case E_USER_WARNING: + return 'User warning (E_USER_WARNING)'; + case E_USER_NOTICE: + return 'User notice (E_USER_NOTICE)'; + case E_USER_DEPRECATED: + return 'Deprecated (E_USER_DEPRECATED)'; + case E_STRICT: + return 'Strict error (E_STRICT)'; + } + + return 'Unknown PHP error'; + } + } diff --git a/src/Analyser/FileAnalyserResult.php b/src/Analyser/FileAnalyserResult.php index 9669866ef8..2aba60730f 100644 --- a/src/Analyser/FileAnalyserResult.php +++ b/src/Analyser/FileAnalyserResult.php @@ -2,22 +2,43 @@ namespace PHPStan\Analyser; -use PHPStan\Dependency\ExportedNode; +use PHPStan\Collectors\CollectedData; +use PHPStan\Dependency\RootExportedNode; -class FileAnalyserResult +/** + * @phpstan-type LinesToIgnore = array|null>> + * @phpstan-import-type CollectorData from CollectedData + */ +final class FileAnalyserResult { /** - * @param Error[] $errors - * @param array $dependencies - * @param array $exportedNodes + * @param list $errors + * @param list $filteredPhpErrors + * @param list $allPhpErrors + * @param list $locallyIgnoredErrors + * @param CollectorData $collectedData + * @param list $dependencies + * @param list $exportedNodes + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores */ - public function __construct(private array $errors, private array $dependencies, private array $exportedNodes) + public function __construct( + private array $errors, + private array $filteredPhpErrors, + private array $allPhpErrors, + private array $locallyIgnoredErrors, + private array $collectedData, + private array $dependencies, + private array $exportedNodes, + private array $linesToIgnore, + private array $unmatchedLineIgnores, + ) { } /** - * @return Error[] + * @return list */ public function getErrors(): array { @@ -25,7 +46,39 @@ public function getErrors(): array } /** - * @return array + * @return list + */ + public function getFilteredPhpErrors(): array + { + return $this->filteredPhpErrors; + } + + /** + * @return list + */ + public function getAllPhpErrors(): array + { + return $this->allPhpErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return CollectorData + */ + public function getCollectedData(): array + { + return $this->collectedData; + } + + /** + * @return list */ public function getDependencies(): array { @@ -33,11 +86,27 @@ public function getDependencies(): array } /** - * @return array + * @return list */ public function getExportedNodes(): array { return $this->exportedNodes; } + /** + * @return LinesToIgnore + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return LinesToIgnore + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + } diff --git a/src/Analyser/FinalizerResult.php b/src/Analyser/FinalizerResult.php new file mode 100644 index 0000000000..c196b25d99 --- /dev/null +++ b/src/Analyser/FinalizerResult.php @@ -0,0 +1,49 @@ + $collectorErrors + * @param list $locallyIgnoredCollectorErrors + */ + public function __construct( + private AnalyserResult $analyserResult, + private array $collectorErrors, + private array $locallyIgnoredCollectorErrors, + ) + { + } + + /** + * @return list + */ + public function getErrors(): array + { + return $this->analyserResult->getErrors(); + } + + public function getAnalyserResult(): AnalyserResult + { + return $this->analyserResult; + } + + /** + * @return list + */ + public function getCollectorErrors(): array + { + return $this->collectorErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredCollectorErrors(): array + { + return $this->locallyIgnoredCollectorErrors; + } + +} diff --git a/src/Analyser/Ignore/IgnoreLexer.php b/src/Analyser/Ignore/IgnoreLexer.php new file mode 100644 index 0000000000..bcaf00ec10 --- /dev/null +++ b/src/Analyser/Ignore/IgnoreLexer.php @@ -0,0 +1,97 @@ + 'T_WHITESPACE', + self::TOKEN_END => 'end', + self::TOKEN_IDENTIFIER => 'identifier', + self::TOKEN_COMMA => 'comma (,)', + self::TOKEN_OPEN_PARENTHESIS => 'T_OPEN_PARENTHESIS', + self::TOKEN_CLOSE_PARENTHESIS => 'T_CLOSE_PARENTHESIS', + self::TOKEN_OTHER => 'T_OTHER', + ]; + + public const VALUE_OFFSET = 0; + public const TYPE_OFFSET = 1; + public const LINE_OFFSET = 2; + + private ?string $regexp = null; + + /** + * @return list + */ + public function tokenize(string $input): array + { + if ($this->regexp === null) { + $this->regexp = $this->generateRegexp(); + } + + $matches = Strings::matchAll($input, $this->regexp, PREG_SET_ORDER); + + $tokens = []; + $line = 1; + foreach ($matches as $match) { + /** @var self::TOKEN_* $type */ + $type = (int) $match['MARK']; + $tokens[] = [$match[0], $type, $line]; + if ($type !== self::TOKEN_END) { + continue; + } + + $line++; + } + + if (($type ?? null) !== self::TOKEN_END) { + $tokens[] = ['', self::TOKEN_END, $line]; // ensure ending token is present + } + + return $tokens; + } + + /** + * @param self::TOKEN_* $type + */ + public function getLabel(int $type): string + { + return self::LABELS[$type]; + } + + private function generateRegexp(): string + { + $patterns = [ + self::TOKEN_WHITESPACE => '[\\x09\\x20]++', + self::TOKEN_END => '(\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?|\\*/)', + self::TOKEN_IDENTIFIER => Error::PATTERN_IDENTIFIER, + self::TOKEN_COMMA => ',', + self::TOKEN_OPEN_PARENTHESIS => '\\(', + self::TOKEN_CLOSE_PARENTHESIS => '\\)', + + // everything except whitespaces and parentheses + self::TOKEN_OTHER => '([^\\s\\)\\(])++', + ]; + + foreach ($patterns as $type => &$pattern) { + $pattern = '(?:' . $pattern . ')(*MARK:' . $type . ')'; + } + + return '~' . implode('|', $patterns) . '~Asi'; + } + +} diff --git a/src/Analyser/Ignore/IgnoreParseException.php b/src/Analyser/Ignore/IgnoreParseException.php new file mode 100644 index 0000000000..c355cb5ad2 --- /dev/null +++ b/src/Analyser/Ignore/IgnoreParseException.php @@ -0,0 +1,20 @@ +phpDocLine; + } + +} diff --git a/src/Analyser/Ignore/IgnoredError.php b/src/Analyser/Ignore/IgnoredError.php new file mode 100644 index 0000000000..b420ae29ce --- /dev/null +++ b/src/Analyser/Ignore/IgnoredError.php @@ -0,0 +1,100 @@ +getIdentifier() !== $identifier) { + return false; + } + } + + if ($ignoredErrorPattern !== null) { + // normalize newlines to allow working with ignore-patterns independent of used OS newline-format + $errorMessage = $error->getMessage(); + $errorMessage = str_replace(['\r\n', '\r'], '\n', $errorMessage); + $ignoredErrorPattern = str_replace([preg_quote('\r\n'), preg_quote('\r')], preg_quote('\n'), $ignoredErrorPattern); + if (Strings::match($errorMessage, $ignoredErrorPattern) === null) { + return false; + } + } + + if ($path !== null) { + $fileExcluder = new FileExcluder($fileHelper, [$path]); + $isExcluded = $fileExcluder->isExcludedFromAnalysing($error->getFilePath()); + if (!$isExcluded && $error->getTraitFilePath() !== null) { + return $fileExcluder->isExcludedFromAnalysing($error->getTraitFilePath()); + } + + return $isExcluded; + } + + return true; + } + +} diff --git a/src/Analyser/Ignore/IgnoredErrorHelper.php b/src/Analyser/Ignore/IgnoredErrorHelper.php new file mode 100644 index 0000000000..d72f73ed82 --- /dev/null +++ b/src/Analyser/Ignore/IgnoredErrorHelper.php @@ -0,0 +1,134 @@ +ignoreErrors as $ignoreError) { + if (is_array($ignoreError)) { + if (!isset($ignoreError['message']) && !isset($ignoreError['messages']) && !isset($ignoreError['identifier'])) { + $errors[] = sprintf( + 'Ignored error %s is missing a message or an identifier.', + Json::encode($ignoreError), + ); + continue; + } + if (isset($ignoreError['messages'])) { + foreach ($ignoreError['messages'] as $message) { + $expandedIgnoreError = $ignoreError; + unset($expandedIgnoreError['messages']); + $expandedIgnoreError['message'] = $message; + $expandedIgnoreErrors[] = $expandedIgnoreError; + } + } else { + $expandedIgnoreErrors[] = $ignoreError; + } + } else { + $expandedIgnoreErrors[] = $ignoreError; + } + } + + $uniquedExpandedIgnoreErrors = []; + foreach ($expandedIgnoreErrors as $ignoreError) { + if (!isset($ignoreError['message']) && !isset($ignoreError['identifier'])) { + $uniquedExpandedIgnoreErrors[] = $ignoreError; + continue; + } + if (!isset($ignoreError['path'])) { + $uniquedExpandedIgnoreErrors[] = $ignoreError; + continue; + } + + $key = $ignoreError['path']; + if (isset($ignoreError['message'])) { + $key = sprintf("%s\n%s", $key, $ignoreError['message']); + } + if (isset($ignoreError['identifier'])) { + $key = sprintf("%s\n%s", $key, $ignoreError['identifier']); + } + if ($key === '') { + throw new ShouldNotHappenException(); + } + + if (!array_key_exists($key, $uniquedExpandedIgnoreErrors)) { + $uniquedExpandedIgnoreErrors[$key] = $ignoreError; + continue; + } + + $uniquedExpandedIgnoreErrors[$key] = [ + 'message' => $ignoreError['message'] ?? null, + 'path' => $ignoreError['path'], + 'identifier' => $ignoreError['identifier'] ?? null, + 'count' => ($uniquedExpandedIgnoreErrors[$key]['count'] ?? 1) + ($ignoreError['count'] ?? 1), + 'reportUnmatched' => ($uniquedExpandedIgnoreErrors[$key]['reportUnmatched'] ?? $this->reportUnmatchedIgnoredErrors) || ($ignoreError['reportUnmatched'] ?? $this->reportUnmatchedIgnoredErrors), + ]; + } + + $expandedIgnoreErrors = array_values($uniquedExpandedIgnoreErrors); + + foreach ($expandedIgnoreErrors as $i => $ignoreError) { + $ignoreErrorEntry = [ + 'index' => $i, + 'ignoreError' => $ignoreError, + ]; + try { + if (is_array($ignoreError)) { + if (!isset($ignoreError['message']) && !isset($ignoreError['identifier'])) { + $errors[] = sprintf( + 'Ignored error %s is missing a message or an identifier.', + Json::encode($ignoreError), + ); + continue; + } + if (!isset($ignoreError['path'])) { + $otherIgnoreErrors[] = $ignoreErrorEntry; + } elseif (@is_file($ignoreError['path'])) { + $normalizedPath = $this->fileHelper->normalizePath($ignoreError['path']); + $ignoreError['path'] = $normalizedPath; + $ignoreErrorsByFile[$normalizedPath][] = $ignoreErrorEntry; + $ignoreError['realPath'] = $normalizedPath; + $expandedIgnoreErrors[$i] = $ignoreError; + } else { + $otherIgnoreErrors[] = $ignoreErrorEntry; + } + } else { + $otherIgnoreErrors[] = $ignoreErrorEntry; + } + } catch (JsonException $e) { + $errors[] = $e->getMessage(); + } + } + + return new IgnoredErrorHelperResult($this->fileHelper, $errors, $otherIgnoreErrors, $ignoreErrorsByFile, $expandedIgnoreErrors, $this->reportUnmatchedIgnoredErrors); + } + +} diff --git a/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php b/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php new file mode 100644 index 0000000000..67bcfa176c --- /dev/null +++ b/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php @@ -0,0 +1,47 @@ + $notIgnoredErrors + * @param list $ignoredErrors + * @param list $otherIgnoreMessages + */ + public function __construct( + private array $notIgnoredErrors, + private array $ignoredErrors, + private array $otherIgnoreMessages, + ) + { + } + + /** + * @return list + */ + public function getNotIgnoredErrors(): array + { + return $this->notIgnoredErrors; + } + + /** + * @return list + */ + public function getIgnoredErrors(): array + { + return $this->ignoredErrors; + } + + /** + * @return list + */ + public function getOtherIgnoreMessages(): array + { + return $this->otherIgnoreMessages; + } + +} diff --git a/src/Analyser/IgnoredErrorHelperResult.php b/src/Analyser/Ignore/IgnoredErrorHelperResult.php similarity index 78% rename from src/Analyser/IgnoredErrorHelperResult.php rename to src/Analyser/Ignore/IgnoredErrorHelperResult.php index e0dc5de4f6..529e1ae4a4 100644 --- a/src/Analyser/IgnoredErrorHelperResult.php +++ b/src/Analyser/Ignore/IgnoredErrorHelperResult.php @@ -1,24 +1,23 @@ $errors * @param array> $otherIgnoreErrors * @param array>> $ignoreErrorsByFile * @param (string|mixed[])[] $ignoreErrors @@ -35,7 +34,7 @@ public function __construct( } /** - * @return string[] + * @return list */ public function getErrors(): array { @@ -45,28 +44,27 @@ public function getErrors(): array /** * @param Error[] $errors * @param string[] $analysedFiles - * @return string[]|Error[] */ public function process( array $errors, bool $onlyFiles, array $analysedFiles, bool $hasInternalErrors, - ): array + ): IgnoredErrorHelperProcessedResult { $unmatchedIgnoredErrors = $this->ignoreErrors; - $addErrors = []; + $stringErrors = []; - $processIgnoreError = function (Error $error, int $i, $ignore) use (&$unmatchedIgnoredErrors, &$addErrors): bool { + $processIgnoreError = function (Error $error, int $i, $ignore) use (&$unmatchedIgnoredErrors, &$stringErrors): bool { $shouldBeIgnored = false; if (is_string($ignore)) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore, null); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore, null, null); if ($shouldBeIgnored) { unset($unmatchedIgnoredErrors[$i]); } } else { if (isset($ignore['path'])) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'], $ignore['path']); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, $ignore['path']); if ($shouldBeIgnored) { if (isset($ignore['count'])) { $realCount = $unmatchedIgnoredErrors[$i]['realCount'] ?? 0; @@ -87,7 +85,7 @@ public function process( } } elseif (isset($ignore['paths'])) { foreach ($ignore['paths'] as $j => $ignorePath) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'], $ignorePath); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, $ignorePath); if (!$shouldBeIgnored) { continue; } @@ -104,13 +102,16 @@ public function process( break; } } else { - throw new ShouldNotHappenException(); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, null); + if ($shouldBeIgnored) { + unset($unmatchedIgnoredErrors[$i]); + } } } if ($shouldBeIgnored) { if (!$error->canBeIgnored()) { - $addErrors[] = sprintf( + $stringErrors[] = sprintf( 'Error message "%s" cannot be ignored, use excludePaths instead.', $error->getMessage(), ); @@ -122,7 +123,8 @@ public function process( return true; }; - $errors = array_values(array_filter($errors, function (Error $error) use ($processIgnoreError): bool { + $ignoredErrors = []; + foreach ($errors as $errorIndex => $error) { $filePath = $this->fileHelper->normalizePath($error->getFilePath()); if (isset($this->ignoreErrorsByFile[$filePath])) { foreach ($this->ignoreErrorsByFile[$filePath] as $ignoreError) { @@ -130,7 +132,9 @@ public function process( $ignore = $ignoreError['ignoreError']; $result = $processIgnoreError($error, $i, $ignore); if (!$result) { - return false; + unset($errors[$errorIndex]); + $ignoredErrors[] = [$error, $ignore]; + continue 2; } } } @@ -144,7 +148,9 @@ public function process( $ignore = $ignoreError['ignoreError']; $result = $processIgnoreError($error, $i, $ignore); if (!$result) { - return false; + unset($errors[$errorIndex]); + $ignoredErrors[] = [$error, $ignore]; + continue 2; } } } @@ -156,12 +162,14 @@ public function process( $result = $processIgnoreError($error, $i, $ignore); if (!$result) { - return false; + unset($errors[$errorIndex]); + $ignoredErrors[] = [$error, $ignore]; + continue 2; } } + } - return true; - })); + $errors = array_values($errors); foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) { if (!isset($unmatchedIgnoredError['count']) || !isset($unmatchedIgnoredError['realCount'])) { @@ -172,43 +180,45 @@ public function process( continue; } - $addErrors[] = new Error(sprintf( + $errors[] = (new Error(sprintf( 'Ignored error pattern %s is expected to occur %d %s, but occurred %d %s.', IgnoredError::stringifyPattern($unmatchedIgnoredError), $unmatchedIgnoredError['count'], $unmatchedIgnoredError['count'] === 1 ? 'time' : 'times', $unmatchedIgnoredError['realCount'], $unmatchedIgnoredError['realCount'] === 1 ? 'time' : 'times', - ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false); + ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false))->withIdentifier('ignore.count'); } - $errors = array_merge($errors, $addErrors); - $analysedFilesKeys = array_fill_keys($analysedFiles, true); - if ($this->reportUnmatchedIgnoredErrors && !$hasInternalErrors) { + if (!$hasInternalErrors) { foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) { + $reportUnmatched = $unmatchedIgnoredError['reportUnmatched'] ?? $this->reportUnmatchedIgnoredErrors; + if ($reportUnmatched === false) { + continue; + } if ( isset($unmatchedIgnoredError['count']) && isset($unmatchedIgnoredError['realCount']) && (isset($unmatchedIgnoredError['realPath']) || !$onlyFiles) ) { if ($unmatchedIgnoredError['realCount'] < $unmatchedIgnoredError['count']) { - $errors[] = new Error(sprintf( + $errors[] = (new Error(sprintf( 'Ignored error pattern %s is expected to occur %d %s, but occurred only %d %s.', IgnoredError::stringifyPattern($unmatchedIgnoredError), $unmatchedIgnoredError['count'], $unmatchedIgnoredError['count'] === 1 ? 'time' : 'times', $unmatchedIgnoredError['realCount'], $unmatchedIgnoredError['realCount'] === 1 ? 'time' : 'times', - ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false); + ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false))->withIdentifier('ignore.count'); } } elseif (isset($unmatchedIgnoredError['realPath'])) { if (!array_key_exists($unmatchedIgnoredError['realPath'], $analysedFilesKeys)) { continue; } - $errors[] = new Error( + $errors[] = (new Error( sprintf( 'Ignored error pattern %s was not matched in reported errors.', IgnoredError::stringifyPattern($unmatchedIgnoredError), @@ -216,9 +226,9 @@ public function process( $unmatchedIgnoredError['realPath'], null, false, - ); + ))->withIdentifier('ignore.unmatched'); } elseif (!$onlyFiles) { - $errors[] = sprintf( + $stringErrors[] = sprintf( 'Ignored error pattern %s was not matched in reported errors.', IgnoredError::stringifyPattern($unmatchedIgnoredError), ); @@ -226,7 +236,7 @@ public function process( } } - return $errors; + return new IgnoredErrorHelperProcessedResult($errors, $ignoredErrors, $stringErrors); } } diff --git a/src/Analyser/IgnoreErrorExtension.php b/src/Analyser/IgnoreErrorExtension.php new file mode 100644 index 0000000000..3f8b11f432 --- /dev/null +++ b/src/Analyser/IgnoreErrorExtension.php @@ -0,0 +1,32 @@ +container->getServicesByTag(IgnoreErrorExtension::EXTENSION_TAG); + } + +} diff --git a/src/Analyser/IgnoredError.php b/src/Analyser/IgnoredError.php deleted file mode 100644 index ab3c1faf6c..0000000000 --- a/src/Analyser/IgnoredError.php +++ /dev/null @@ -1,74 +0,0 @@ -getMessage(); - $errorMessage = str_replace(['\r\n', '\r'], '\n', $errorMessage); - $ignoredErrorPattern = str_replace([preg_quote('\r\n'), preg_quote('\r')], preg_quote('\n'), $ignoredErrorPattern); - - if ($path !== null) { - if (Strings::match($errorMessage, $ignoredErrorPattern) === null) { - return false; - } - - $fileExcluder = new FileExcluder($fileHelper, [$path], []); - $isExcluded = $fileExcluder->isExcludedFromAnalysing($error->getFilePath()); - if (!$isExcluded && $error->getTraitFilePath() !== null) { - return $fileExcluder->isExcludedFromAnalysing($error->getTraitFilePath()); - } - - return $isExcluded; - } - - return Strings::match($errorMessage, $ignoredErrorPattern) !== null; - } - -} diff --git a/src/Analyser/IgnoredErrorHelper.php b/src/Analyser/IgnoredErrorHelper.php deleted file mode 100644 index b683886b97..0000000000 --- a/src/Analyser/IgnoredErrorHelper.php +++ /dev/null @@ -1,82 +0,0 @@ -ignoreErrors as $i => $ignoreError) { - try { - if (is_array($ignoreError)) { - if (!isset($ignoreError['message'])) { - $errors[] = sprintf( - 'Ignored error %s is missing a message.', - Json::encode($ignoreError), - ); - continue; - } - if (!isset($ignoreError['path'])) { - if (!isset($ignoreError['paths'])) { - $errors[] = sprintf( - 'Ignored error %s is missing a path.', - Json::encode($ignoreError), - ); - } - - $otherIgnoreErrors[] = [ - 'index' => $i, - 'ignoreError' => $ignoreError, - ]; - } elseif (@is_file($ignoreError['path'])) { - $normalizedPath = $this->fileHelper->normalizePath($ignoreError['path']); - $ignoreError['path'] = $normalizedPath; - $ignoreErrorsByFile[$normalizedPath][] = [ - 'index' => $i, - 'ignoreError' => $ignoreError, - ]; - $ignoreError['realPath'] = $normalizedPath; - $this->ignoreErrors[$i] = $ignoreError; - } else { - $otherIgnoreErrors[] = [ - 'index' => $i, - 'ignoreError' => $ignoreError, - ]; - } - } else { - $otherIgnoreErrors[] = [ - 'index' => $i, - 'ignoreError' => $ignoreError, - ]; - } - } catch (JsonException $e) { - $errors[] = $e->getMessage(); - } - } - - return new IgnoredErrorHelperResult($this->fileHelper, $errors, $otherIgnoreErrors, $ignoreErrorsByFile, $this->ignoreErrors, $this->reportUnmatchedIgnoredErrors); - } - -} diff --git a/src/Analyser/ImpurePoint.php b/src/Analyser/ImpurePoint.php new file mode 100644 index 0000000000..dc9e9a6091 --- /dev/null +++ b/src/Analyser/ImpurePoint.php @@ -0,0 +1,60 @@ +scope; + } + + /** + * @return Node\Expr|Node\Stmt|VirtualNode + */ + public function getNode() + { + return $this->node; + } + + /** + * @return ImpurePointIdentifier + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getDescription(): string + { + return $this->description; + } + + public function isCertain(): bool + { + return $this->certain; + } + +} diff --git a/src/Analyser/InternalError.php b/src/Analyser/InternalError.php new file mode 100644 index 0000000000..371b64cdc3 --- /dev/null +++ b/src/Analyser/InternalError.php @@ -0,0 +1,104 @@ + + */ +final class InternalError implements JsonSerializable +{ + + public const STACK_TRACE_METADATA_KEY = 'stackTrace'; + + public const STACK_TRACE_AS_STRING_METADATA_KEY = 'stackTraceAsString'; + + /** + * @param Trace $trace + */ + public function __construct( + private string $message, + private string $contextDescription, + private array $trace, + private ?string $traceAsString, + private bool $shouldReportBug, + ) + { + } + + /** + * @return Trace + */ + public static function prepareTrace(Throwable $exception): array + { + $trace = array_map(static fn (array $trace) => [ + 'file' => $trace['file'] ?? null, + 'line' => $trace['line'] ?? null, + ], $exception->getTrace()); + + array_unshift($trace, [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]); + + return $trace; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getContextDescription(): string + { + return $this->contextDescription; + } + + /** + * @return Trace + */ + public function getTrace(): array + { + return $this->trace; + } + + public function getTraceAsString(): ?string + { + return $this->traceAsString; + } + + public function shouldReportBug(): bool + { + return $this->shouldReportBug; + } + + /** + * @param mixed[] $json + */ + public static function decode(array $json): self + { + return new self($json['message'], $json['contextDescription'], $json['trace'], $json['traceAsString'], $json['shouldReportBug']); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'message' => $this->message, + 'contextDescription' => $this->contextDescription, + 'trace' => $this->trace, + 'traceAsString' => $this->traceAsString, + 'shouldReportBug' => $this->shouldReportBug, + ]; + } + +} diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php new file mode 100644 index 0000000000..8d8daa714f --- /dev/null +++ b/src/Analyser/InternalScopeFactory.php @@ -0,0 +1,42 @@ + $expressionTypes + * @param array $nativeExpressionTypes + * @param array $conditionalExpressions + * @param list $inClosureBindScopeClasses + * @param array $currentlyAssignedExpressions + * @param array $currentlyAllowedUndefinedExpressions + * @param list $inFunctionCallsStack + */ + public function create( + ScopeContext $context, + bool $declareStrictTypes = false, + PhpFunctionFromParserNodeReflection|null $function = null, + ?string $namespace = null, + array $expressionTypes = [], + array $nativeExpressionTypes = [], + array $conditionalExpressions = [], + array $inClosureBindScopeClasses = [], + ?ParametersAcceptor $anonymousFunctionReflection = null, + bool $inFirstLevelStatement = true, + array $currentlyAssignedExpressions = [], + array $currentlyAllowedUndefinedExpressions = [], + array $inFunctionCallsStack = [], + bool $afterExtractCall = false, + ?Scope $parentScope = null, + bool $nativeTypesPromoted = false, + ): MutatingScope; + +} diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php new file mode 100644 index 0000000000..9835fb1591 --- /dev/null +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -0,0 +1,84 @@ +phpVersion = $this->container->getParameter('phpVersion'); + } + + public function create( + ScopeContext $context, + bool $declareStrictTypes = false, + PhpFunctionFromParserNodeReflection|null $function = null, + ?string $namespace = null, + array $expressionTypes = [], + array $nativeExpressionTypes = [], + array $conditionalExpressions = [], + array $inClosureBindScopeClasses = [], + ?ParametersAcceptor $anonymousFunctionReflection = null, + bool $inFirstLevelStatement = true, + array $currentlyAssignedExpressions = [], + array $currentlyAllowedUndefinedExpressions = [], + array $inFunctionCallsStack = [], + bool $afterExtractCall = false, + ?Scope $parentScope = null, + bool $nativeTypesPromoted = false, + ): MutatingScope + { + return new MutatingScope( + $this, + $this->container->getByType(ReflectionProvider::class), + $this->container->getByType(InitializerExprTypeResolver::class), + $this->container->getByType(DynamicReturnTypeExtensionRegistryProvider::class)->getRegistry(), + $this->container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class)->getRegistry(), + $this->container->getByType(ExprPrinter::class), + $this->container->getByType(TypeSpecifier::class), + $this->container->getByType(PropertyReflectionFinder::class), + $this->container->getService('currentPhpVersionSimpleParser'), + $this->container->getByType(NodeScopeResolver::class), + $this->container->getByType(RicherScopeGetTypeHelper::class), + $this->container->getByType(ConstantResolver::class), + $context, + $this->container->getByType(PhpVersion::class), + $this->container->getByType(AttributeReflectionFactory::class), + $this->phpVersion, + $declareStrictTypes, + $function, + $namespace, + $expressionTypes, + $nativeExpressionTypes, + $conditionalExpressions, + $inClosureBindScopeClasses, + $anonymousFunctionReflection, + $inFirstLevelStatement, + $currentlyAssignedExpressions, + $currentlyAllowedUndefinedExpressions, + $inFunctionCallsStack, + $afterExtractCall, + $parentScope, + $nativeTypesPromoted, + ); + } + +} diff --git a/src/Analyser/LazyScopeFactory.php b/src/Analyser/LazyScopeFactory.php deleted file mode 100644 index e704d36419..0000000000 --- a/src/Analyser/LazyScopeFactory.php +++ /dev/null @@ -1,106 +0,0 @@ -dynamicConstantNames = $container->getParameter('dynamicConstantNames'); - $this->treatPhpDocTypesAsCertain = $container->getParameter('treatPhpDocTypesAsCertain'); - $this->explicitMixedInUnknownGenericNew = $this->container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew']; - } - - /** - * @param array $constantTypes - * @param VariableTypeHolder[] $variablesTypes - * @param VariableTypeHolder[] $moreSpecificTypes - * @param array $conditionalExpressions - * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack - * - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - FunctionReflection|MethodReflection|null $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - bool $afterExtractCall = false, - ?Scope $parentScope = null, - ): MutatingScope - { - $scopeClass = $this->scopeClass; - if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new ShouldNotHappenException(); - } - - return new $scopeClass( - $this, - $this->container->getByType(ReflectionProvider::class), - $this->container->getByType(DynamicReturnTypeExtensionRegistryProvider::class)->getRegistry(), - $this->container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class)->getRegistry(), - $this->container->getByType(Standard::class), - $this->container->getByType(TypeSpecifier::class), - $this->container->getByType(PropertyReflectionFinder::class), - $this->container->getService('currentPhpVersionSimpleParser'), - $this->container->getByType(NodeScopeResolver::class), - $context, - $this->container->getByType(PhpVersion::class), - $declareStrictTypes, - $constantTypes, - $function, - $namespace, - $variablesTypes, - $moreSpecificTypes, - $conditionalExpressions, - $inClosureBindScopeClass, - $anonymousFunctionReflection, - $inFirstLevelStatement, - $currentlyAssignedExpressions, - $nativeExpressionTypes, - $inFunctionCallsStack, - $this->dynamicConstantNames, - $this->treatPhpDocTypesAsCertain, - $afterExtractCall, - $parentScope, - $this->explicitMixedInUnknownGenericNew, - ); - } - -} diff --git a/src/Analyser/LocalIgnoresProcessor.php b/src/Analyser/LocalIgnoresProcessor.php new file mode 100644 index 0000000000..d5c0197dd6 --- /dev/null +++ b/src/Analyser/LocalIgnoresProcessor.php @@ -0,0 +1,101 @@ + $temporaryFileErrors + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores + */ + public function process( + array $temporaryFileErrors, + array $linesToIgnore, + array $unmatchedLineIgnores, + ): LocalIgnoresProcessorResult + { + $fileErrors = []; + $locallyIgnoredErrors = []; + foreach ($temporaryFileErrors as $tmpFileError) { + $line = $tmpFileError->getLine(); + if ( + $line !== null + && $tmpFileError->canBeIgnored() + && array_key_exists($tmpFileError->getFile(), $linesToIgnore) + && array_key_exists($line, $linesToIgnore[$tmpFileError->getFile()]) + ) { + $identifiers = $linesToIgnore[$tmpFileError->getFile()][$line]; + if ($identifiers === null) { + $locallyIgnoredErrors[] = $tmpFileError; + unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); + continue; + } + + if ($tmpFileError->getIdentifier() === null) { + $fileErrors[] = $tmpFileError; + continue; + } + + foreach ($identifiers as $i => $ignoredIdentifier) { + if ($ignoredIdentifier !== $tmpFileError->getIdentifier()) { + continue; + } + + unset($identifiers[$i]); + + if (count($identifiers) > 0) { + $linesToIgnore[$tmpFileError->getFile()][$line] = array_values($identifiers); + } else { + unset($linesToIgnore[$tmpFileError->getFile()][$line]); + } + + if ( + array_key_exists($tmpFileError->getFile(), $unmatchedLineIgnores) + && array_key_exists($line, $unmatchedLineIgnores[$tmpFileError->getFile()]) + ) { + $unmatchedIgnoredIdentifiers = $unmatchedLineIgnores[$tmpFileError->getFile()][$line]; + if (is_array($unmatchedIgnoredIdentifiers)) { + foreach ($unmatchedIgnoredIdentifiers as $j => $unmatchedIgnoredIdentifier) { + if ($ignoredIdentifier !== $unmatchedIgnoredIdentifier) { + continue; + } + + unset($unmatchedIgnoredIdentifiers[$j]); + + if (count($unmatchedIgnoredIdentifiers) > 0) { + $unmatchedLineIgnores[$tmpFileError->getFile()][$line] = array_values($unmatchedIgnoredIdentifiers); + } else { + unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); + } + break; + } + } + } + + $locallyIgnoredErrors[] = $tmpFileError; + continue 2; + } + } + + $fileErrors[] = $tmpFileError; + } + + return new LocalIgnoresProcessorResult( + $fileErrors, + $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, + ); + } + +} diff --git a/src/Analyser/LocalIgnoresProcessorResult.php b/src/Analyser/LocalIgnoresProcessorResult.php new file mode 100644 index 0000000000..1d44c98f1f --- /dev/null +++ b/src/Analyser/LocalIgnoresProcessorResult.php @@ -0,0 +1,58 @@ + $fileErrors + * @param list $locallyIgnoredErrors + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores + */ + public function __construct( + private array $fileErrors, + private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, + ) + { + } + + /** + * @return list + */ + public function getFileErrors(): array + { + return $this->fileErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return LinesToIgnore + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return LinesToIgnore + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + +} diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 7bdd515031..16a9c15f05 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -5,9 +5,9 @@ use ArrayAccess; use Closure; use Generator; -use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Arg; +use PhpParser\Node\ComplexType; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\BinaryOp; @@ -22,28 +22,54 @@ use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Identifier; +use PhpParser\Node\InterpolatedStringPart; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; -use PhpParser\Node\Scalar\DNumber; -use PhpParser\Node\Scalar\EncapsedStringPart; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\PropertyHook; use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Function_; use PhpParser\NodeFinder; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Node\ExecutionEndNode; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Expr\ExistingArrayDimFetch; +use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; +use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; +use PHPStan\Node\Expr\PropertyInitializationExpr; +use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; +use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; +use PHPStan\Node\InvalidateExprNode; +use PHPStan\Node\IssetExpr; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\PropertyAssignNode; +use PHPStan\Parser\ArrayMapArgVisitor; +use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Parser\Parser; -use PHPStan\Parser\ParserErrorsException; use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; +use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\Dummy\DummyConstructorReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; @@ -59,74 +85,79 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; -use PHPStan\Type\ClassStringType; use PHPStan\Type\ClosureType; -use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\ConstantType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicReturnTypeExtensionRegistry; -use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; +use PHPStan\Type\ExpressionTypeResolverExtensionRegistry; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\GenericTypeVariableResolver; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\StaticType; -use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use stdClass; use Throwable; use function abs; -use function array_column; use function array_filter; use function array_key_exists; +use function array_key_first; use function array_keys; use function array_map; +use function array_merge; use function array_pop; +use function array_slice; +use function array_values; use function count; -use function dirname; +use function explode; use function get_class; +use function implode; use function in_array; -use function is_float; +use function is_array; +use function is_bool; +use function is_numeric; use function is_string; use function ltrim; -use function max; +use function md5; use function sprintf; use function str_starts_with; use function strlen; @@ -135,20 +166,13 @@ use function usort; use const PHP_INT_MAX; use const PHP_INT_MIN; -use const PHP_INT_SIZE; -class MutatingScope implements Scope +final class MutatingScope implements Scope { - public const CALCULATE_SCALARS_LIMIT = 128; + private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; - private const OPERATOR_SIGIL_MAP = [ - Node\Expr\AssignOp\Plus::class => '+', - Node\Expr\AssignOp\Minus::class => '-', - Node\Expr\AssignOp\Mul::class => '*', - Node\Expr\AssignOp\Pow::class => '^', - Node\Expr\AssignOp\Div::class => '/', - ]; + private const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid'; /** @var Type[] */ private array $resolvedTypes = []; @@ -159,48 +183,57 @@ class MutatingScope implements Scope /** @var array */ private array $falseyScopes = []; + /** @var non-empty-string|null */ private ?string $namespace; + private ?self $scopeOutOfFirstLevelStatement = null; + + private ?self $scopeWithPromotedNativeTypes = null; + + private static int $resolveClosureTypeDepth = 0; + /** - * @param array $constantTypes - * @param VariableTypeHolder[] $variableTypes - * @param VariableTypeHolder[] $moreSpecificTypes + * @param int|array{min: int, max: int}|null $configPhpVersion + * @param array $expressionTypes * @param array $conditionalExpressions + * @param list $inClosureBindScopeClasses * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array $inFunctionCallsStack - * @param string[] $dynamicConstantNames + * @param array $currentlyAllowedUndefinedExpressions + * @param array $nativeExpressionTypes + * @param list $inFunctionCallsStack */ public function __construct( - private ScopeFactory $scopeFactory, + private InternalScopeFactory $scopeFactory, private ReflectionProvider $reflectionProvider, + private InitializerExprTypeResolver $initializerExprTypeResolver, private DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry, - private OperatorTypeSpecifyingExtensionRegistry $operatorTypeSpecifyingExtensionRegistry, - private Standard $printer, + private ExpressionTypeResolverExtensionRegistry $expressionTypeResolverExtensionRegistry, + private ExprPrinter $exprPrinter, private TypeSpecifier $typeSpecifier, private PropertyReflectionFinder $propertyReflectionFinder, private Parser $parser, private NodeScopeResolver $nodeScopeResolver, + private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, + private ConstantResolver $constantResolver, private ScopeContext $context, private PhpVersion $phpVersion, + private AttributeReflectionFactory $attributeReflectionFactory, + private int|array|null $configPhpVersion, private bool $declareStrictTypes = false, - private array $constantTypes = [], - private FunctionReflection|MethodReflection|null $function = null, + private PhpFunctionFromParserNodeReflection|null $function = null, ?string $namespace = null, - private array $variableTypes = [], - private array $moreSpecificTypes = [], + private array $expressionTypes = [], + private array $nativeExpressionTypes = [], private array $conditionalExpressions = [], - private ?string $inClosureBindScopeClass = null, + private array $inClosureBindScopeClasses = [], private ?ParametersAcceptor $anonymousFunctionReflection = null, private bool $inFirstLevelStatement = true, private array $currentlyAssignedExpressions = [], - private array $nativeExpressionTypes = [], + private array $currentlyAllowedUndefinedExpressions = [], private array $inFunctionCallsStack = [], - private array $dynamicConstantNames = [], - private bool $treatPhpDocTypesAsCertain = true, private bool $afterExtractCall = false, private ?Scope $parentScope = null, - private bool $explicitMixedInUnknownGenericNew = false, + private bool $nativeTypesPromoted = false, ) { if ($namespace === '') { @@ -254,10 +287,81 @@ public function enterDeclareStrictTypes(): self return $this->scopeFactory->create( $this->context, true, - [], null, null, - $this->variableTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, + ); + } + + /** + * @param array $currentExpressionTypes + * @return array + */ + private function rememberConstructorExpressions(array $currentExpressionTypes): array + { + $expressionTypes = []; + foreach ($currentExpressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + if ($expr instanceof FuncCall) { + if ( + !$expr->name instanceof Name + || !in_array($expr->name->name, ['class_exists', 'function_exists'], true) + ) { + continue; + } + } elseif ($expr instanceof PropertyFetch) { + if ( + !$expr->name instanceof Node\Identifier + || !$expr->var instanceof Variable + || $expr->var->name !== 'this' + || !$this->phpVersion->supportsReadOnlyProperties() + ) { + continue; + } + + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + if ($propertyReflection === null) { + continue; + } + + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) { + continue; + } + } elseif (!$expr instanceof ConstFetch && !$expr instanceof PropertyInitializationExpr) { + continue; + } + + $expressionTypes[$exprString] = $expressionTypeHolder; + } + + if (array_key_exists('$this', $currentExpressionTypes)) { + $expressionTypes['$this'] = $currentExpressionTypes['$this']; + } + + return $expressionTypes; + } + + public function rememberConstructorScope(): self + { + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + null, + $this->getNamespace(), + $this->rememberConstructorExpressions($this->expressionTypes), + $this->rememberConstructorExpressions($this->nativeExpressionTypes), + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + [], + [], + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -287,9 +391,8 @@ public function getTraitReflection(): ?ClassReflection /** * @api - * @return FunctionReflection|MethodReflection|null */ - public function getFunction() + public function getFunction(): ?PhpFunctionFromParserNodeReflection { return $this->function; } @@ -312,14 +415,6 @@ public function getParentScope(): ?Scope return $this->parentScope; } - /** - * @return array - */ - private function getVariableTypes(): array - { - return $this->variableTypes; - } - /** @api */ public function canAnyVariableExist(): bool { @@ -331,27 +426,27 @@ public function afterExtractCall(): self return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, [], - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, true, $this->parentScope, + $this->nativeTypesPromoted, ); } public function afterClearstatcacheCall(): self { - $moreSpecificTypes = $this->moreSpecificTypes; - foreach (array_keys($moreSpecificTypes) as $exprString) { + $expressionTypes = $this->expressionTypes; + foreach (array_keys($expressionTypes) as $exprString) { // list from https://www.php.net/manual/en/function.clearstatcache.php // stat(), lstat(), file_exists(), is_writable(), is_readable(), is_executable(), is_file(), is_dir(), is_link(), filectime(), fileatime(), filemtime(), fileinode(), filegroup(), fileowner(), filesize(), filetype(), and fileperms(). @@ -360,6 +455,7 @@ public function afterClearstatcacheCall(): self 'lstat', 'file_exists', 'is_writable', + 'is_writeable', 'is_readable', 'is_executable', 'is_file', @@ -375,31 +471,113 @@ public function afterClearstatcacheCall(): self 'filetype', 'fileperms', ] as $functionName) { - if (!str_starts_with((string) $exprString, $functionName . '(') && !str_starts_with((string) $exprString, '\\' . $functionName . '(')) { + if (!str_starts_with($exprString, $functionName . '(') && !str_starts_with($exprString, '\\' . $functionName . '(')) { continue; } - unset($moreSpecificTypes[$exprString]); + unset($expressionTypes[$exprString]); continue 2; } } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypes, + $expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + + public function afterOpenSslCall(string $openSslFunctionName): self + { + $expressionTypes = $this->expressionTypes; + + if (in_array($openSslFunctionName, [ + 'openssl_cipher_iv_length', + 'openssl_cms_decrypt', + 'openssl_cms_encrypt', + 'openssl_cms_read', + 'openssl_cms_sign', + 'openssl_cms_verify', + 'openssl_csr_export_to_file', + 'openssl_csr_export', + 'openssl_csr_get_public_key', + 'openssl_csr_get_subject', + 'openssl_csr_new', + 'openssl_csr_sign', + 'openssl_decrypt', + 'openssl_dh_compute_key', + 'openssl_digest', + 'openssl_encrypt', + 'openssl_get_curve_names', + 'openssl_get_privatekey', + 'openssl_get_publickey', + 'openssl_open', + 'openssl_pbkdf2', + 'openssl_pkcs12_export_to_file', + 'openssl_pkcs12_export', + 'openssl_pkcs12_read', + 'openssl_pkcs7_decrypt', + 'openssl_pkcs7_encrypt', + 'openssl_pkcs7_read', + 'openssl_pkcs7_sign', + 'openssl_pkcs7_verify', + 'openssl_pkey_derive', + 'openssl_pkey_export_to_file', + 'openssl_pkey_export', + 'openssl_pkey_get_private', + 'openssl_pkey_get_public', + 'openssl_pkey_new', + 'openssl_private_decrypt', + 'openssl_private_encrypt', + 'openssl_public_decrypt', + 'openssl_public_encrypt', + 'openssl_random_pseudo_bytes', + 'openssl_seal', + 'openssl_sign', + 'openssl_spki_export_challenge', + 'openssl_spki_export', + 'openssl_spki_new', + 'openssl_spki_verify', + 'openssl_verify', + 'openssl_x509_checkpurpose', + 'openssl_x509_export_to_file', + 'openssl_x509_export', + 'openssl_x509_fingerprint', + 'openssl_x509_read', + 'openssl_x509_verify', + ], true)) { + unset($expressionTypes['\openssl_error_string()']); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, $this->nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->isInFirstLevelStatement(), + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -410,7 +588,8 @@ public function hasVariableType(string $variableName): TrinaryLogic return TrinaryLogic::createYes(); } - if (!isset($this->variableTypes[$variableName])) { + $varExprString = '$' . $variableName; + if (!isset($this->expressionTypes[$varExprString])) { if ($this->canAnyVariableExist()) { return TrinaryLogic::createMaybe(); } @@ -418,40 +597,80 @@ public function hasVariableType(string $variableName): TrinaryLogic return TrinaryLogic::createNo(); } - return $this->variableTypes[$variableName]->getCertainty(); + return $this->expressionTypes[$varExprString]->getCertainty(); } /** @api */ public function getVariableType(string $variableName): Type { - if ($this->isGlobalVariable($variableName)) { - return new ArrayType(new StringType(), new MixedType()); + if ($this->hasVariableType($variableName)->maybe()) { + if ($variableName === 'argc') { + return IntegerRangeType::fromInterval(1, null); + } + if ($variableName === 'argv') { + return TypeCombinator::intersect( + new ArrayType(new IntegerType(), new StringType()), + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ); + } + if ($this->canAnyVariableExist()) { + return new MixedType(); + } } if ($this->hasVariableType($variableName)->no()) { throw new UndefinedVariableException($this, $variableName); } - if (!array_key_exists($variableName, $this->variableTypes)) { + $varExprString = '$' . $variableName; + if (!array_key_exists($varExprString, $this->expressionTypes)) { + if ($this->isGlobalVariable($variableName)) { + return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true)); + } return new MixedType(); } - return $this->variableTypes[$variableName]->getType(); + return TypeUtils::resolveLateResolvableTypes($this->expressionTypes[$varExprString]->getType()); } /** * @api - * @return array + * @return list */ public function getDefinedVariables(): array { $variables = []; - foreach ($this->variableTypes as $variableName => $holder) { + foreach ($this->expressionTypes as $exprString => $holder) { + if (!$holder->getExpr() instanceof Variable) { + continue; + } if (!$holder->getCertainty()->yes()) { continue; } - $variables[] = $variableName; + $variables[] = substr($exprString, 1); + } + + return $variables; + } + + /** + * @api + * @return list + */ + public function getMaybeDefinedVariables(): array + { + $variables = []; + foreach ($this->expressionTypes as $exprString => $holder) { + if (!$holder->getExpr() instanceof Variable) { + continue; + } + if (!$holder->getCertainty()->maybe()) { + continue; + } + + $variables[] = substr($exprString, 1); } return $variables; @@ -459,17 +678,7 @@ public function getDefinedVariables(): array private function isGlobalVariable(string $variableName): bool { - return in_array($variableName, [ - 'GLOBALS', - '_SERVER', - '_GET', - '_POST', - '_FILES', - '_COOKIE', - '_SESSION', - '_REQUEST', - '_ENV', - ], true); + return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true); } /** @api */ @@ -479,31 +688,12 @@ public function hasConstant(Name $name): bool if ($isCompilerHaltOffset) { return $this->fileHasCompilerHaltStatementCalls(); } - if ($name->isFullyQualified()) { - if (array_key_exists($name->toCodeString(), $this->constantTypes)) { - return true; - } - } - - if ($this->getNamespace() !== null) { - $constantName = new FullyQualified([$this->getNamespace(), $name->toString()]); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return true; - } - } - $constantName = new FullyQualified($name->toString()); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { + if ($this->getGlobalConstantType($name) !== null) { return true; } - if (!$this->reflectionProvider->hasConstant($name, $this)) { - return false; - } - - $constantReflection = $this->reflectionProvider->getConstant($name, $this); - - return $constantReflection->getFileName() !== $this->getFile(); + return $this->reflectionProvider->hasConstant($name, $this); } private function fileHasCompilerHaltStatementCalls(): bool @@ -543,18 +733,37 @@ public function getAnonymousFunctionReturnType(): ?Type /** @api */ public function getType(Expr $node): Type { + if ($node instanceof GetIterableKeyTypeExpr) { + return $this->getIterableKeyType($this->getType($node->getExpr())); + } if ($node instanceof GetIterableValueTypeExpr) { - return $this->getType($node->getExpr())->getIterableValueType(); + return $this->getIterableValueType($this->getType($node->getExpr())); } if ($node instanceof GetOffsetValueTypeExpr) { return $this->getType($node->getVar())->getOffsetValueType($this->getType($node->getDim())); } + if ($node instanceof ExistingArrayDimFetch) { + return $this->getType(new Expr\ArrayDimFetch($node->getVar(), $node->getDim())); + } + if ($node instanceof UnsetOffsetExpr) { + return $this->getType($node->getVar())->unsetOffset($this->getType($node->getDim())); + } if ($node instanceof SetOffsetValueTypeExpr) { return $this->getType($node->getVar())->setOffsetValueType( $node->getDim() !== null ? $this->getType($node->getDim()) : null, $this->getType($node->getValue()), ); } + if ($node instanceof SetExistingOffsetValueTypeExpr) { + return $this->getType($node->getVar())->setExistingOffsetValueType( + $this->getType($node->getDim()), + $this->getType($node->getValue()), + ); + } + if ($node instanceof TypeExpr) { + return $node->getExprType(); + } + if ($node instanceof OriginalPropertyTypeExpr) { $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node->getPropertyFetch(), $this); if ($propertyReflection === null) { @@ -567,108 +776,117 @@ public function getType(Expr $node): Type $key = $this->getNodeKey($node); if (!array_key_exists($key, $this->resolvedTypes)) { - $this->resolvedTypes[$key] = $this->resolveType($node); + $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node)); } return $this->resolvedTypes[$key]; } private function getNodeKey(Expr $node): string { - /** @var string|null $key */ - $key = $node->getAttribute('phpstan_cache_printer'); - if ($key === null) { - $key = $this->printer->prettyPrintExpr($node); - $node->setAttribute('phpstan_cache_printer', $key); + $key = $this->exprPrinter->printExpr($node); + + $attributes = $node->getAttributes(); + if ( + $node instanceof Node\FunctionLike + && (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null) + && (($attributes['startFilePos'] ?? null) !== null) + ) { + $key .= '/*' . $attributes['startFilePos'] . '*/'; + } + + if (($attributes[self::KEEP_VOID_ATTRIBUTE_NAME] ?? null) === true) { + $key .= '/*' . self::KEEP_VOID_ATTRIBUTE_NAME . '*/'; } return $key; } - private function resolveType(Expr $node): Type + private function getClosureScopeCacheKey(): string + { + $parts = []; + foreach ($this->expressionTypes as $exprString => $expressionTypeHolder) { + $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache())); + } + $parts[] = '---'; + foreach ($this->nativeExpressionTypes as $exprString => $expressionTypeHolder) { + $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache())); + } + + $parts[] = sprintf(':%d', count($this->inFunctionCallsStack)); + foreach ($this->inFunctionCallsStack as [$method, $parameter]) { + if ($parameter === null) { + $parts[] = ',null'; + continue; + } + + $parts[] = sprintf(',%s', $parameter->getType()->describe(VerbosityLevel::cache())); + } + + return md5(implode("\n", $parts)); + } + + private function resolveType(string $exprString, Expr $node): Type { + foreach ($this->expressionTypeResolverExtensionRegistry->getExtensions() as $extension) { + $type = $extension->getType($node, $this); + if ($type !== null) { + return $type; + } + } + if ($node instanceof Expr\Exit_ || $node instanceof Expr\Throw_) { - return new NeverType(true); + return new NonAcceptingNeverType(); + } + + if (!$node instanceof Variable && $this->hasExpressionType($node)->yes()) { + return $this->expressionTypes[$exprString]->getType(); + } + + if ($node instanceof AlwaysRememberedExpr) { + return $node->getExprType(); } if ($node instanceof Expr\BinaryOp\Smaller) { - return $this->getType($node->left)->isSmallerThan($this->getType($node->right))->toBooleanType(); + return $this->getType($node->left)->isSmallerThan($this->getType($node->right), $this->phpVersion)->toBooleanType(); } if ($node instanceof Expr\BinaryOp\SmallerOrEqual) { - return $this->getType($node->left)->isSmallerThanOrEqual($this->getType($node->right))->toBooleanType(); + return $this->getType($node->left)->isSmallerThanOrEqual($this->getType($node->right), $this->phpVersion)->toBooleanType(); } if ($node instanceof Expr\BinaryOp\Greater) { - return $this->getType($node->right)->isSmallerThan($this->getType($node->left))->toBooleanType(); + return $this->getType($node->right)->isSmallerThan($this->getType($node->left), $this->phpVersion)->toBooleanType(); } if ($node instanceof Expr\BinaryOp\GreaterOrEqual) { - return $this->getType($node->right)->isSmallerThanOrEqual($this->getType($node->left))->toBooleanType(); + return $this->getType($node->right)->isSmallerThanOrEqual($this->getType($node->left), $this->phpVersion)->toBooleanType(); } - if ( - $node instanceof Expr\BinaryOp\Equal - || $node instanceof Expr\BinaryOp\NotEqual - ) { - 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\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); } - 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); + $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 $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; + } - return new BooleanType(); + if ($node instanceof Expr\BinaryOp\NotEqual) { + return $this->getType(new Expr\BooleanNot(new BinaryOp\Equal($node->left, $node->right))); } if ($node instanceof Expr\Empty_) { $result = $this->issetCheck($node->expr, static function (Type $type): ?bool { - $isNull = (new NullType())->isSuperTypeOf($type); - $isFalsey = (new ConstantBooleanType(false))->isSuperTypeOf($type->toBoolean()); + $isNull = $type->isNull(); + $isFalsey = $type->toBoolean()->isFalse(); if ($isNull->maybe()) { return null; } @@ -677,14 +895,7 @@ private function resolveType(Expr $node): Type } if ($isNull->yes()) { - if ($isFalsey->yes()) { - return false; - } - if ($isFalsey->no()) { - return true; - } - - return false; + return $isFalsey->no(); } return !$isFalsey->yes(); @@ -696,41 +907,8 @@ private function resolveType(Expr $node): Type 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; - } - - return !$isNull->yes(); - }); - if ($result !== null) { - if (!$result) { - return new ConstantBooleanType($result); - } - - continue; - } - - $issetResult = $result; - } - - if ($issetResult === null) { - return new BooleanType(); - } - - return new ConstantBooleanType($issetResult); - } - if ($node instanceof Node\Expr\BooleanNot) { - if ($this->treatPhpDocTypesAsCertain) { - $exprBooleanType = $this->getType($node->expr)->toBoolean(); - } else { - $exprBooleanType = $this->getNativeType($node->expr)->toBoolean(); - } + $exprBooleanType = $this->getType($node->expr)->toBoolean(); if ($exprBooleanType instanceof ConstantBooleanType) { return new ConstantBooleanType(!$exprBooleanType->getValue()); } @@ -739,59 +917,34 @@ private function resolveType(Expr $node): Type } 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) { - return $traverse($type); - } - if ($type instanceof ConstantStringType) { - return new ConstantStringType(~$type->getValue()); - } - if ($type instanceof StringType) { - return new StringType(); - } - if ($type instanceof IntegerType || $type instanceof FloatType) { - return new IntegerType(); //no const types here, result depends on PHP_INT_SIZE - } - return new ErrorType(); - }); + return $this->initializerExprTypeResolver->getBitwiseNotType($node->expr, fn (Expr $expr): Type => $this->getType($expr)); } if ( $node instanceof Node\Expr\BinaryOp\BooleanAnd || $node instanceof Node\Expr\BinaryOp\LogicalAnd ) { - if ($this->treatPhpDocTypesAsCertain) { - $leftBooleanType = $this->getType($node->left)->toBoolean(); - } else { - $leftBooleanType = $this->getNativeType($node->left)->toBoolean(); - } - - if ( - $leftBooleanType instanceof ConstantBooleanType - && !$leftBooleanType->getValue() - ) { + $leftBooleanType = $this->getType($node->left)->toBoolean(); + if ($leftBooleanType->isFalse()->yes()) { return new ConstantBooleanType(false); } - if ($this->treatPhpDocTypesAsCertain) { - $rightBooleanType = $this->filterByTruthyValue($node->left)->getType($node->right)->toBoolean(); + if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { + $noopCallback = static function (): void { + }; + $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, $noopCallback, ExpressionContext::createDeep()); + $rightBooleanType = $leftResult->getTruthyScope()->getType($node->right)->toBoolean(); } else { - $rightBooleanType = $this->promoteNativeTypes()->filterByTruthyValue($node->left)->getType($node->right)->toBoolean(); + $rightBooleanType = $this->filterByTruthyValue($node->left)->getType($node->right)->toBoolean(); } - if ( - $rightBooleanType instanceof ConstantBooleanType - && !$rightBooleanType->getValue() - ) { + if ($rightBooleanType->isFalse()->yes()) { return new ConstantBooleanType(false); } if ( - $leftBooleanType instanceof ConstantBooleanType - && $leftBooleanType->getValue() - && $rightBooleanType instanceof ConstantBooleanType - && $rightBooleanType->getValue() + $leftBooleanType->isTrue()->yes() + && $rightBooleanType->isTrue()->yes() ) { return new ConstantBooleanType(true); } @@ -803,36 +956,27 @@ private function resolveType(Expr $node): Type $node instanceof Node\Expr\BinaryOp\BooleanOr || $node instanceof Node\Expr\BinaryOp\LogicalOr ) { - if ($this->treatPhpDocTypesAsCertain) { - $leftBooleanType = $this->getType($node->left)->toBoolean(); - } else { - $leftBooleanType = $this->getNativeType($node->left)->toBoolean(); - } - if ( - $leftBooleanType instanceof ConstantBooleanType - && $leftBooleanType->getValue() - ) { + $leftBooleanType = $this->getType($node->left)->toBoolean(); + if ($leftBooleanType->isTrue()->yes()) { return new ConstantBooleanType(true); } - if ($this->treatPhpDocTypesAsCertain) { - $rightBooleanType = $this->filterByFalseyValue($node->left)->getType($node->right)->toBoolean(); + if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { + $noopCallback = static function (): void { + }; + $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, $noopCallback, ExpressionContext::createDeep()); + $rightBooleanType = $leftResult->getFalseyScope()->getType($node->right)->toBoolean(); } else { - $rightBooleanType = $this->promoteNativeTypes()->filterByFalseyValue($node->left)->getType($node->right)->toBoolean(); + $rightBooleanType = $this->filterByFalseyValue($node->left)->getType($node->right)->toBoolean(); } - if ( - $rightBooleanType instanceof ConstantBooleanType - && $rightBooleanType->getValue() - ) { + if ($rightBooleanType->isTrue()->yes()) { return new ConstantBooleanType(true); } if ( - $leftBooleanType instanceof ConstantBooleanType - && !$leftBooleanType->getValue() - && $rightBooleanType instanceof ConstantBooleanType - && !$rightBooleanType->getValue() + $leftBooleanType->isFalse()->yes() + && $rightBooleanType->isFalse()->yes() ) { return new ConstantBooleanType(false); } @@ -841,13 +985,8 @@ private function resolveType(Expr $node): Type } if ($node instanceof Node\Expr\BinaryOp\LogicalXor) { - if ($this->treatPhpDocTypesAsCertain) { - $leftBooleanType = $this->getType($node->left)->toBoolean(); - $rightBooleanType = $this->getType($node->right)->toBoolean(); - } else { - $leftBooleanType = $this->getNativeType($node->left)->toBoolean(); - $rightBooleanType = $this->getNativeType($node->right)->toBoolean(); - } + $leftBooleanType = $this->getType($node->left)->toBoolean(); + $rightBooleanType = $this->getType($node->right)->toBoolean(); if ( $leftBooleanType instanceof ConstantBooleanType @@ -862,122 +1001,15 @@ 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); - } + return $this->richerScopeGetTypeHelper->getIdenticalResult($this, $node)->type; + } - if ($this->treatPhpDocTypesAsCertain) { - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - } else { - $leftType = $this->getNativeType($node->left); - $rightType = $this->getNativeType($node->right); - } + if ($node instanceof Expr\BinaryOp\NotIdentical) { + return $this->richerScopeGetTypeHelper->getNotIdenticalResult($this, $node)->type; + } - if ( - ( - $node->left instanceof Node\Expr\PropertyFetch - || $node->left instanceof Node\Expr\StaticPropertyFetch - ) - && $rightType instanceof NullType - && !$this->hasPropertyNativeType($node->left) - ) { - return new BooleanType(); - } - - if ( - ( - $node->right instanceof Node\Expr\PropertyFetch - || $node->right instanceof Node\Expr\StaticPropertyFetch - ) - && $leftType instanceof NullType - && !$this->hasPropertyNativeType($node->right) - ) { - return new BooleanType(); - } - - $isSuperset = $leftType->isSuperTypeOf($rightType); - if ($isSuperset->no()) { - return new ConstantBooleanType(false); - } elseif ( - $isSuperset->yes() - && $leftType instanceof ConstantScalarType - && $rightType instanceof ConstantScalarType - && $leftType->getValue() === $rightType->getValue() - ) { - return new ConstantBooleanType(true); - } - - return new BooleanType(); - } - - 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); - } else { - $leftType = $this->getNativeType($node->left); - $rightType = $this->getNativeType($node->right); - } - - if ( - ( - $node->left instanceof Node\Expr\PropertyFetch - || $node->left instanceof Node\Expr\StaticPropertyFetch - ) - && $rightType instanceof NullType - && !$this->hasPropertyNativeType($node->left) - ) { - return new BooleanType(); - } - - if ( - ( - $node->right instanceof Node\Expr\PropertyFetch - || $node->right instanceof Node\Expr\StaticPropertyFetch - ) - && $leftType instanceof NullType - && !$this->hasPropertyNativeType($node->right) - ) { - return new BooleanType(); - } - - $isSuperset = $leftType->isSuperTypeOf($rightType); - if ($isSuperset->no()) { - return new ConstantBooleanType(true); - } elseif ( - $isSuperset->yes() - && $leftType instanceof ConstantScalarType - && $rightType instanceof ConstantScalarType - && $leftType->getValue() === $rightType->getValue() - ) { - return new ConstantBooleanType(false); - } - - return new BooleanType(); - } - - if ($node instanceof Expr\Instanceof_) { - if ($this->treatPhpDocTypesAsCertain) { - $expressionType = $this->getType($node->expr); - } else { - $expressionType = $this->getNativeType($node->expr); - } + if ($node instanceof Expr\Instanceof_) { + $expressionType = $this->getType($node->expr); if ( $this->isInTrait() && TypeUtils::findThisType($expressionType) !== null @@ -1007,7 +1039,7 @@ private function resolveType(Expr $node): Type if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } - if ($type instanceof TypeWithClassName) { + if ($type->getObjectClassNames() !== []) { $uncertainty = true; return $type; } @@ -1048,538 +1080,144 @@ 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) { - if ($scalarValue instanceof ConstantIntegerType) { - $newTypes[] = new ConstantIntegerType(-$scalarValue->getValue()); - } elseif ($scalarValue instanceof ConstantFloatType) { - $newTypes[] = new ConstantFloatType(-$scalarValue->getValue()); - } - } - - return TypeCombinator::union(...$newTypes); - } - - if ($type instanceof IntegerRangeType) { - return $this->resolveType(new Node\Expr\BinaryOp\Mul($node->expr, new LNumber(-1))); - } - - return $type; + return $this->initializerExprTypeResolver->getUnaryMinusType($node->expr, fn (Expr $expr): Type => $this->getType($expr)); } - if ($node instanceof Expr\BinaryOp\Concat || $node instanceof Expr\AssignOp\Concat) { - 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); - } - - $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(); + if ($node instanceof Expr\BinaryOp\Concat) { + return $this->initializerExprTypeResolver->getConcatType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ( - $node instanceof Node\Expr\BinaryOp\Mul - || $node instanceof Node\Expr\AssignOp\Mul - ) { - if ($node instanceof Node\Expr\AssignOp) { - $leftType = $this->getType($node->var)->toNumber(); - $rightType = $this->getType($node->expr)->toNumber(); - } else { - $leftType = $this->getType($node->left)->toNumber(); - $rightType = $this->getType($node->right)->toNumber(); - } - - $floatType = new FloatType(); - - if ($leftType instanceof ConstantIntegerType && $leftType->getValue() === 0) { - if ($floatType->isSuperTypeOf($rightType)->yes()) { - return new ConstantFloatType(0.0); - } - return new ConstantIntegerType(0); - } - if ($rightType instanceof ConstantIntegerType && $rightType->getValue() === 0) { - if ($floatType->isSuperTypeOf($leftType)->yes()) { - return new ConstantFloatType(0.0); - } - return new ConstantIntegerType(0); - } + if ($node instanceof Expr\AssignOp\Concat) { + return $this->initializerExprTypeResolver->getConcatType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); } - if ( - $node instanceof Node\Expr\BinaryOp\Div - || $node instanceof Node\Expr\AssignOp\Div - || $node instanceof Node\Expr\BinaryOp\Mod - || $node instanceof Node\Expr\AssignOp\Mod - ) { - if ($node instanceof Node\Expr\AssignOp) { - $right = $node->expr; - } 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) { - - if ( - $scalarType->getValue() === 0 - || $scalarType->getValue() === 0.0 - ) { - return new ErrorType(); - } - } + if ($node instanceof BinaryOp\BitwiseAnd) { + return $this->initializerExprTypeResolver->getBitwiseAndType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ( - ( - $node instanceof Node\Expr\BinaryOp - || $node instanceof Node\Expr\AssignOp - ) && !$node instanceof Expr\BinaryOp\Coalesce && !$node instanceof Expr\AssignOp\Coalesce - ) { - if ($node instanceof Node\Expr\AssignOp) { - $left = $node->var; - $right = $node->expr; - } else { - $left = $node->left; - $right = $node->right; - } - - $leftTypes = TypeUtils::getConstantScalars($this->getType($left)); - $rightTypes = TypeUtils::getConstantScalars($this->getType($right)); - - $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) { - $resultType = $this->calculateFromScalars($node, $leftType, $rightType); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); - } - $resultTypes[] = $resultType; - } - } - return TypeCombinator::union(...$resultTypes); - } + if ($node instanceof Expr\AssignOp\BitwiseAnd) { + return $this->initializerExprTypeResolver->getBitwiseAndType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); } - 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 ($node instanceof BinaryOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($positiveInt->isSuperTypeOf($leftType)->yes()) { - $rangeMin = 0; - } elseif ($rangeMax !== null) { - $rangeMin = $rangeMax * -1; - } + if ($node instanceof Expr\AssignOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - return IntegerRangeType::fromInterval($rangeMin, $rangeMax); - } elseif ($positiveInt->isSuperTypeOf($leftType)->yes()) { - return IntegerRangeType::fromInterval(0, null); - } + if ($node instanceof BinaryOp\BitwiseXor) { + return $this->initializerExprTypeResolver->getBitwiseXorType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - return new IntegerType(); + if ($node instanceof Expr\AssignOp\BitwiseXor) { + return $this->initializerExprTypeResolver->getBitwiseXorType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); } if ($node instanceof Expr\BinaryOp\Spaceship) { - return IntegerRangeType::fromInterval(-1, 1); + return $this->initializerExprTypeResolver->getSpaceshipType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ($node instanceof Expr\Clone_) { - return $this->getType($node->expr); + if ($node instanceof BinaryOp\Div) { + return $this->initializerExprTypeResolver->getDivType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ( - $node instanceof Expr\AssignOp\ShiftLeft - || $node instanceof Expr\BinaryOp\ShiftLeft - || $node instanceof Expr\AssignOp\ShiftRight - || $node instanceof Expr\BinaryOp\ShiftRight - ) { - if ($node instanceof Node\Expr\AssignOp) { - $left = $node->var; - $right = $node->expr; - } else { - $left = $node->left; - $right = $node->right; - } - - if (TypeCombinator::union( - $this->getType($left)->toNumber(), - $this->getType($right)->toNumber(), - ) instanceof ErrorType) { - return new ErrorType(); - } - - return new IntegerType(); + if ($node instanceof Expr\AssignOp\Div) { + return $this->initializerExprTypeResolver->getDivType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); } - if ( - $node instanceof Expr\AssignOp\BitwiseAnd - || $node instanceof Expr\BinaryOp\BitwiseAnd - || $node instanceof Expr\AssignOp\BitwiseOr - || $node instanceof Expr\BinaryOp\BitwiseOr - || $node instanceof Expr\AssignOp\BitwiseXor - || $node instanceof Expr\BinaryOp\BitwiseXor - ) { - 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); - $stringType = new StringType(); - - if ($stringType->isSuperTypeOf($leftType)->yes() && $stringType->isSuperTypeOf($rightType)->yes()) { - return $stringType; - } - - if (TypeCombinator::union($leftType->toNumber(), $rightType->toNumber()) instanceof ErrorType) { - return new ErrorType(); - } - - return new IntegerType(); + if ($node instanceof BinaryOp\Mod) { + return $this->initializerExprTypeResolver->getModType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ( - $node instanceof Node\Expr\BinaryOp\Plus - || $node instanceof Node\Expr\BinaryOp\Minus - || $node instanceof Node\Expr\BinaryOp\Mul - || $node instanceof Node\Expr\BinaryOp\Pow - || $node instanceof Node\Expr\BinaryOp\Div - || $node instanceof Node\Expr\AssignOp\Plus - || $node instanceof Node\Expr\AssignOp\Minus - || $node instanceof Node\Expr\AssignOp\Mul - || $node instanceof Node\Expr\AssignOp\Pow - || $node instanceof Node\Expr\AssignOp\Div - ) { - 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); - - if ($node instanceof Expr\AssignOp\Plus || $node instanceof Expr\BinaryOp\Plus) { - $leftConstantArrays = TypeUtils::getConstantArrays($leftType); - $rightConstantArrays = TypeUtils::getConstantArrays($rightType); - - if (count($leftConstantArrays) > 0 && count($rightConstantArrays) > 0) { - $resultTypes = []; - foreach ($rightConstantArrays as $rightConstantArray) { - foreach ($leftConstantArrays as $leftConstantArray) { - $newArrayBuilder = ConstantArrayTypeBuilder::createFromConstantArray($rightConstantArray); - foreach ($leftConstantArray->getKeyTypes() as $leftKeyType) { - $newArrayBuilder->setOffsetValueType( - $leftKeyType, - $leftConstantArray->getOffsetValueType($leftKeyType), - ); - } - $resultTypes[] = $newArrayBuilder->getArray(); - } - } - - return TypeCombinator::union(...$resultTypes); - } - - $arrayType = new ArrayType(new MixedType(), new MixedType()); - - if ($arrayType->isSuperTypeOf($leftType)->yes() && $arrayType->isSuperTypeOf($rightType)->yes()) { - if ($leftType->getIterableKeyType()->equals($rightType->getIterableKeyType())) { - // to preserve BenevolentUnionType - $keyType = $leftType->getIterableKeyType(); - } else { - $keyTypes = []; - foreach ([ - $leftType->getIterableKeyType(), - $rightType->getIterableKeyType(), - ] as $keyType) { - $keyTypes[] = $keyType; - } - $keyType = TypeCombinator::union(...$keyTypes); - } - - $arrayType = new ArrayType( - $keyType, - 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) { - return new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - new ArrayType(new MixedType(), new MixedType()), - ]); - } - } - - 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 ($node instanceof Expr\AssignOp\Mod) { + return $this->initializerExprTypeResolver->getModType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($operatorSigil === null) { - $operatorSigil = self::OPERATOR_SIGIL_MAP[get_class($node)] ?? null; - } + if ($node instanceof BinaryOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($operatorSigil !== null) { - $operatorTypeSpecifyingExtensions = $this->operatorTypeSpecifyingExtensionRegistry->getOperatorTypeSpecifyingExtensions($operatorSigil, $leftType, $rightType); + if ($node instanceof Expr\AssignOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - /** @var Type[] $extensionTypes */ - $extensionTypes = []; + if ($node instanceof BinaryOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - foreach ($operatorTypeSpecifyingExtensions as $extension) { - $extensionTypes[] = $extension->specifyType($operatorSigil, $leftType, $rightType); - } + if ($node instanceof Expr\AssignOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - if (count($extensionTypes) > 0) { - return TypeCombinator::union(...$extensionTypes); - } - } + if ($node instanceof BinaryOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - $types = TypeCombinator::union($leftType, $rightType); - if ( - $leftType instanceof ArrayType - || $rightType instanceof ArrayType - || $types instanceof ArrayType - ) { - return new ErrorType(); - } + if ($node instanceof Expr\AssignOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - $leftNumberType = $leftType->toNumber(); - $rightNumberType = $rightType->toNumber(); - if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { - return new ErrorType(); - } + if ($node instanceof BinaryOp\Pow) { + return $this->initializerExprTypeResolver->getPowType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ( - (new FloatType())->isSuperTypeOf($leftNumberType)->yes() - || (new FloatType())->isSuperTypeOf($rightNumberType)->yes() - ) { - return new FloatType(); - } + if ($node instanceof Expr\AssignOp\Pow) { + return $this->initializerExprTypeResolver->getPowType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($node instanceof Expr\AssignOp\Pow || $node instanceof Expr\BinaryOp\Pow) { - return new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - ]); - } + if ($node instanceof BinaryOp\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - $resultType = TypeCombinator::union($leftNumberType, $rightNumberType); - if ($node instanceof Expr\AssignOp\Div || $node instanceof Expr\BinaryOp\Div) { - if ($types instanceof MixedType || $resultType instanceof IntegerType) { - return new BenevolentUnionType([new IntegerType(), new FloatType()]); - } + if ($node instanceof Expr\AssignOp\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - return new UnionType([new IntegerType(), new FloatType()]); - } + if ($node instanceof BinaryOp\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($types instanceof MixedType - || $leftType instanceof BenevolentUnionType - || $rightType instanceof BenevolentUnionType - ) { - return TypeUtils::toBenevolentUnion($resultType); - } + if ($node instanceof Expr\AssignOp\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - return $resultType; + if ($node instanceof Expr\Clone_) { + return $this->getType($node->expr); } - if ($node instanceof LNumber) { - return new ConstantIntegerType($node->value); + if ($node instanceof Node\Scalar\Int_) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof String_) { - return new ConstantStringType($node->value); - } elseif ($node instanceof Node\Scalar\Encapsed) { - $parts = []; + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); + } elseif ($node instanceof Node\Scalar\InterpolatedString) { + $resultType = null; foreach ($node->parts as $part) { - if ($part instanceof EncapsedStringPart) { - $parts[] = new ConstantStringType($part->value); - continue; - } - - $partStringType = $this->getType($part)->toString(); - if ($partStringType instanceof ErrorType) { - return new ErrorType(); + if ($part instanceof InterpolatedStringPart) { + $partType = new ConstantStringType($part->value); + } else { + $partType = $this->getType($part)->toString(); } - - $parts[] = $partStringType; - } - - $constantString = new ConstantStringType(''); - foreach ($parts as $part) { - if ($part instanceof ConstantStringType) { - $constantString = $constantString->append($part); + if ($resultType === null) { + $resultType = $partType; continue; } - $isNonEmpty = false; - $isLiteralString = true; - foreach ($parts as $partType) { - if ($partType->isNonEmptyString()->yes()) { - $isNonEmpty = true; - } - 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); - } - - return new StringType(); + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); } - return $constantString; - } elseif ($node instanceof DNumber) { - return new ConstantFloatType($node->value); + return $resultType ?? new ConstantStringType(''); + } elseif ($node instanceof Node\Scalar\Float_) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { if ($node instanceof FuncCall) { if ($node->name instanceof Name) { if ($this->reflectionProvider->hasFunction($node->name, $this)) { + $function = $this->reflectionProvider->getFunction($node->name, $this); return $this->createFirstClassCallable( - $this->reflectionProvider->getFunction($node->name, $this)->getVariants(), + $function, + $function->getVariants(), ); } @@ -1592,6 +1230,7 @@ private function resolveType(Expr $node): Type } return $this->createFirstClassCallable( + null, $callableType->getCallableParametersAcceptors($this), ); } @@ -1607,7 +1246,10 @@ private function resolveType(Expr $node): Type return new ObjectType(Closure::class); } - return $this->createFirstClassCallable($method->getVariants()); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } if ($node instanceof Expr\StaticCall) { @@ -1615,17 +1257,21 @@ private function resolveType(Expr $node): Type return new ObjectType(Closure::class); } - $classType = $this->resolveTypeByName($node->class); if (!$node->name instanceof Node\Identifier) { return new ObjectType(Closure::class); } + $classType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); $methodName = $node->name->toString(); if (!$classType->hasMethod($methodName)->yes()) { return new ObjectType(Closure::class); } - return $this->createFirstClassCallable($classType->getMethod($methodName, $this)->getVariants()); + $method = $classType->getMethod($methodName, $this); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } if ($node instanceof New_) { @@ -1669,79 +1315,196 @@ private function resolveType(Expr $node): Type } $callableParameters = null; - $arg = $node->getAttribute('parent'); - if ($arg instanceof Arg) { - $funcCall = $arg->getAttribute('parent'); - $argOrder = $arg->getAttribute('expressionOrder'); - if ($funcCall instanceof FuncCall && $funcCall->name instanceof Name) { - $functionName = $this->reflectionProvider->resolveFunctionName($funcCall->name, $this); - if ( - $functionName === 'array_map' - && $argOrder === 0 - && isset($funcCall->getArgs()[1]) - ) { - 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); - } - } + $arrayMapArgs = $node->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + if ($arrayMapArgs !== null) { + $callableParameters = []; + foreach ($arrayMapArgs as $funcCallArg) { + $callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null); + } + } else { + $inFunctionCallsStackCount = count($this->inFunctionCallsStack); + if ($inFunctionCallsStackCount > 0) { + [, $inParameter] = $this->inFunctionCallsStack[$inFunctionCallsStackCount - 1]; + if ($inParameter !== null) { + $callableParameters = $this->nodeScopeResolver->createCallableParameters($this, $node, null, $inParameter->getType()); } } } if ($node instanceof Expr\ArrowFunction) { - $returnType = $this->enterArrowFunctionWithoutReflection($node, $callableParameters)->getType($node->expr); - if ($node->returnType !== null) { - $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); + $arrowScope = $this->enterArrowFunctionWithoutReflection($node, $callableParameters); + + if ($node->expr instanceof Expr\Yield_ || $node->expr instanceof Expr\YieldFrom) { + $yieldNode = $node->expr; + + if ($yieldNode instanceof Expr\Yield_) { + if ($yieldNode->key === null) { + $keyType = new IntegerType(); + } else { + $keyType = $arrowScope->getType($yieldNode->key); + } + + if ($yieldNode->value === null) { + $valueType = new NullType(); + } else { + $valueType = $arrowScope->getType($yieldNode->value); + } + } else { + $yieldFromType = $arrowScope->getType($yieldNode->expr); + $keyType = $arrowScope->getIterableKeyType($yieldFromType); + $valueType = $arrowScope->getIterableValueType($yieldFromType); + } + + $returnType = new GenericObjectType(Generator::class, [ + $keyType, + $valueType, + new MixedType(), + new VoidType(), + ]); + } else { + $returnType = $arrowScope->getKeepVoidType($node->expr); + if ($node->returnType !== null) { + $nativeReturnType = $this->getFunctionType($node->returnType, false, false); + $returnType = self::intersectButNotNever($nativeReturnType, $returnType); + } } + + $arrowFunctionImpurePoints = []; + $invalidateExpressions = []; + $arrowFunctionExprResult = $this->nodeScopeResolver->processExprNode( + new Node\Stmt\Expression($node->expr), + $node->expr, + $arrowScope, + static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { + if ($scope->getAnonymousFunctionReflection() !== $arrowScope->getAnonymousFunctionReflection()) { + return; + } + + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + + if (!$node instanceof PropertyAssignNode) { + return; + } + + $arrowFunctionImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + }, + ExpressionContext::createDeep(), + ); + $throwPoints = $arrowFunctionExprResult->getThrowPoints(); + $impurePoints = array_merge($arrowFunctionImpurePoints, $arrowFunctionExprResult->getImpurePoints()); + $usedVariables = []; } else { + $cachedTypes = $node->getAttribute('phpstanCachedTypes', []); + $cacheKey = $this->getClosureScopeCacheKey(); + if (array_key_exists($cacheKey, $cachedTypes)) { + $cachedClosureData = $cachedTypes[$cacheKey]; + + return new ClosureType( + $parameters, + $cachedClosureData['returnType'], + $isVariadic, + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + TemplateTypeVarianceMap::createEmpty(), + [], + $cachedClosureData['throwPoints'], + $cachedClosureData['impurePoints'], + $cachedClosureData['invalidateExpressions'], + $cachedClosureData['usedVariables'], + TrinaryLogic::createYes(), + ); + } + if (self::$resolveClosureTypeDepth >= 2) { + return new ClosureType( + $parameters, + $this->getFunctionType($node->returnType, false, false), + $isVariadic, + ); + } + + self::$resolveClosureTypeDepth++; + $closureScope = $this->enterAnonymousFunctionWithoutReflection($node, $callableParameters); $closureReturnStatements = []; $closureYieldStatements = []; - $closureExecutionEnds = []; - $this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$closureExecutionEnds): void { - if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { - return; - } + $onlyNeverExecutionEnds = null; + $closureImpurePoints = []; + $invalidateExpressions = []; + + try { + $closureStatementResult = $this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$onlyNeverExecutionEnds, &$closureImpurePoints, &$invalidateExpressions): void { + if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { + return; + } + + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } + + if ($node instanceof ExecutionEndNode) { + if ($node->getStatementResult()->isAlwaysTerminating()) { + foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) { + if ($exitPoint->getStatement() instanceof Node\Stmt\Return_) { + $onlyNeverExecutionEnds = false; + continue; + } - if ($node instanceof ExecutionEndNode) { - if ($node->getStatementResult()->isAlwaysTerminating()) { - foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) { - if ($exitPoint->getStatement() instanceof Node\Stmt\Return_) { - continue; + if ($onlyNeverExecutionEnds === null) { + $onlyNeverExecutionEnds = true; + } + + break; } - $closureExecutionEnds[] = $node; - break; + if (count($node->getStatementResult()->getExitPoints()) === 0) { + if ($onlyNeverExecutionEnds === null) { + $onlyNeverExecutionEnds = true; + } + } + } else { + $onlyNeverExecutionEnds = false; } - if (count($node->getStatementResult()->getExitPoints()) === 0) { - $closureExecutionEnds[] = $node; - } + return; } - return; - } + if ($node instanceof Node\Stmt\Return_) { + $closureReturnStatements[] = [$node, $scope]; + } - if ($node instanceof Node\Stmt\Return_) { - $closureReturnStatements[] = [$node, $scope]; - } + if (!$node instanceof Expr\Yield_ && !$node instanceof Expr\YieldFrom) { + return; + } - if (!$node instanceof Expr\Yield_ && !$node instanceof Expr\YieldFrom) { - return; - } + $closureYieldStatements[] = [$node, $scope]; + }, StatementContext::createTopLevel()); + } finally { + self::$resolveClosureTypeDepth--; + } - $closureYieldStatements[] = [$node, $scope]; - }); + $throwPoints = $closureStatementResult->getThrowPoints(); + $impurePoints = array_merge($closureImpurePoints, $closureStatementResult->getImpurePoints()); $returnTypes = []; $hasNull = false; @@ -1755,14 +1518,14 @@ private function resolveType(Expr $node): Type } if (count($returnTypes) === 0) { - if (count($closureExecutionEnds) > 0 && !$hasNull) { - $returnType = new NeverType(true); + if ($onlyNeverExecutionEnds === true && !$hasNull) { + $returnType = new NonAcceptingNeverType(); } else { $returnType = new VoidType(); } } else { - if (count($closureExecutionEnds) > 0) { - $returnTypes[] = new NeverType(true); + if ($onlyNeverExecutionEnds === true) { + $returnTypes[] = new NonAcceptingNeverType(); } if ($hasNull) { $returnTypes[] = new NullType(); @@ -1791,8 +1554,8 @@ private function resolveType(Expr $node): Type } $yieldFromType = $yieldScope->getType($yieldNode->expr); - $keyTypes[] = $yieldFromType->getIterableKeyType(); - $valueTypes[] = $yieldFromType->getIterableValueType(); + $keyTypes[] = $yieldScope->getIterableKeyType($yieldFromType); + $valueTypes[] = $yieldScope->getIterableValueType($yieldFromType); } $returnType = new GenericObjectType(Generator::class, [ @@ -1802,14 +1565,77 @@ private function resolveType(Expr $node): Type $returnType, ]); } else { - $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); + if ($node->returnType !== null) { + $nativeReturnType = $this->getFunctionType($node->returnType, false, false); + $returnType = self::intersectButNotNever($nativeReturnType, $returnType); + } + } + + $usedVariables = []; + foreach ($node->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $usedVariables[] = $use->var->name; + } + + foreach ($node->uses as $use) { + if (!$use->byRef) { + continue; + } + + $impurePoints[] = new ImpurePoint( + $this, + $node, + 'functionCall', + 'call to a Closure with by-ref use', + true, + ); + break; + } + } + + foreach ($parameters as $parameter) { + if ($parameter->passedByReference()->no()) { + continue; } + + $impurePoints[] = new ImpurePoint( + $this, + $node, + 'functionCall', + 'call to a Closure with by-ref parameter', + true, + ); } + $throwPointsForClosureType = array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? SimpleThrowPoint::createExplicit($throwPoint->getType(), $throwPoint->canContainAnyThrowable()) : SimpleThrowPoint::createImplicit(), $throwPoints); + $impurePointsForClosureType = array_map(static fn (ImpurePoint $impurePoint) => new SimpleImpurePoint($impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $impurePoints); + + $cachedTypes = $node->getAttribute('phpstanCachedTypes', []); + $cachedTypes[$this->getClosureScopeCacheKey()] = [ + 'returnType' => $returnType, + 'throwPoints' => $throwPointsForClosureType, + 'impurePoints' => $impurePointsForClosureType, + 'invalidateExpressions' => $invalidateExpressions, + 'usedVariables' => $usedVariables, + ]; + $node->setAttribute('phpstanCachedTypes', $cachedTypes); + return new ClosureType( $parameters, $returnType, $isVariadic, + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + TemplateTypeVarianceMap::createEmpty(), + [], + $throwPointsForClosureType, + $impurePointsForClosureType, + $invalidateExpressions, + $usedVariables, + TrinaryLogic::createYes(), ); } elseif ($node instanceof New_) { if ($node->class instanceof Name) { @@ -1839,54 +1665,10 @@ private function resolveType(Expr $node): Type } $exprType = $this->getType($node->class); - return $this->getTypeToInstantiateForNew($exprType); + return $exprType->getObjectTypeOrClassStringObjectType(); } elseif ($node instanceof Array_) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - if (count($node->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $arrayBuilder->degradeToGeneralArray(); - } - foreach ($node->items as $arrayItem) { - if ($arrayItem === null) { - continue; - } - - $valueType = $this->getType($arrayItem->value); - if ($arrayItem->unpack) { - if ($valueType instanceof ConstantArrayType) { - $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(); - - 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, - ); - } - } - return $arrayBuilder->getArray(); - + return $this->initializerExprTypeResolver->getArrayType($node, fn (Expr $expr): Type => $this->getType($expr)); } elseif ($node instanceof Int_) { return $this->getType($node->expr)->toInteger(); } elseif ($node instanceof Bool_) { @@ -1897,54 +1679,35 @@ private function resolveType(Expr $node): Type return $this->getType($node->expr)->toString(); } 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()); - } elseif ($node instanceof Node\Scalar\MagicConst\Class_) { - if (!$this->isInClass()) { - return new ConstantStringType(''); - } - - return new ConstantStringType($this->getClassReflection()->getName(), true); - } elseif ($node instanceof Node\Scalar\MagicConst\Dir) { - return new ConstantStringType(dirname($this->getFile())); - } elseif ($node instanceof Node\Scalar\MagicConst\File) { - return new ConstantStringType($this->getFile()); - } elseif ($node instanceof Node\Scalar\MagicConst\Namespace_) { - return new ConstantStringType($this->namespace ?? ''); - } elseif ($node instanceof Node\Scalar\MagicConst\Method) { - if ($this->isInAnonymousFunction()) { - return new ConstantStringType('{closure}'); - } - - $function = $this->getFunction(); - if ($function === null) { - return new ConstantStringType(''); - } - if ($function instanceof MethodReflection) { - return new ConstantStringType( - sprintf('%s::%s', $function->getDeclaringClass()->getName(), $function->getName()), - ); - } - - return new ConstantStringType($function->getName()); - } elseif ($node instanceof Node\Scalar\MagicConst\Function_) { - if ($this->isInAnonymousFunction()) { - return new ConstantStringType('{closure}'); - } - $function = $this->getFunction(); - if ($function === null) { - return new ConstantStringType(''); - } - - return new ConstantStringType($function->getName()); - } elseif ($node instanceof Node\Scalar\MagicConst\Trait_) { - if (!$this->isInTrait()) { - return new ConstantStringType(''); - } - return new ConstantStringType($this->getTraitReflection()->getName(), true); + } elseif ($node instanceof Node\Scalar\MagicConst) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Object_) { $castToObject = static function (Type $type): Type { - if ((new ObjectWithoutClassType())->isSuperTypeOf($type)->yes()) { + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $objects = []; + foreach ($constantArrays as $constantArray) { + $properties = []; + $optionalProperties = []; + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + if (!$keyType instanceof ConstantStringType) { + // an object with integer properties is >weird< + continue; + } + $valueType = $constantArray->getValueTypes()[$i]; + $optional = $constantArray->isOptionalKey($i); + if ($optional) { + $optionalProperties[] = $keyType->getValue(); + } + $properties[$keyType->getValue()] = $valueType; + } + + $objects[] = TypeCombinator::intersect(new ObjectShapeType($properties, $optionalProperties), new ObjectType(stdClass::class)); + } + + return TypeCombinator::union(...$objects); + } + if ($type->isObject()->yes()) { return $type; } @@ -1963,71 +1726,163 @@ private function resolveType(Expr $node): Type return $this->getType($node->var); } elseif ($node instanceof Expr\PreInc || $node instanceof Expr\PreDec) { $varType = $this->getType($node->var); - $varScalars = TypeUtils::getConstantScalars($varType); - $stringType = new StringType(); + $varScalars = $varType->getConstantScalarValues(); + if (count($varScalars) > 0) { $newTypes = []; - foreach ($varScalars as $scalar) { - $varValue = $scalar->getValue(); + foreach ($varScalars as $varValue) { if ($node instanceof Expr\PreInc) { - ++$varValue; - } else { + if (!is_bool($varValue)) { + ++$varValue; + } + } elseif (is_numeric($varValue)) { --$varValue; } $newTypes[] = $this->getTypeFromValue($varValue); } return TypeCombinator::union(...$newTypes); - } elseif ($stringType->isSuperTypeOf($varType)->yes()) { + } elseif ($varType->isString()->yes()) { if ($varType->isLiteralString()->yes()) { - return new IntersectionType([$stringType, new AccessoryLiteralStringType()]); + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); + } + + if ($varType->isNumericString()->yes()) { + return new BenevolentUnionType([ + new IntegerType(), + new FloatType(), + ]); } - return $stringType; + + return new BenevolentUnionType([ + new StringType(), + new IntegerType(), + new FloatType(), + ]); } if ($node instanceof Expr\PreInc) { - return $this->getType(new BinaryOp\Plus($node->var, new LNumber(1))); + return $this->getType(new BinaryOp\Plus($node->var, new Node\Scalar\Int_(1))); } - return $this->getType(new BinaryOp\Minus($node->var, new LNumber(1))); + return $this->getType(new BinaryOp\Minus($node->var, new Node\Scalar\Int_(1))); } elseif ($node instanceof Expr\Yield_) { $functionReflection = $this->getFunction(); if ($functionReflection === null) { return new MixedType(); } - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!$returnType instanceof TypeWithClassName) { - return new MixedType(); - } - - $generatorSendType = GenericTypeVariableResolver::getType($returnType, Generator::class, 'TSend'); - if ($generatorSendType === null) { + $returnType = $functionReflection->getReturnType(); + $generatorSendType = $returnType->getTemplateType(Generator::class, 'TSend'); + if ($generatorSendType instanceof ErrorType) { return new MixedType(); } return $generatorSendType; } elseif ($node instanceof Expr\YieldFrom) { $yieldFromType = $this->getType($node->expr); - - if (!$yieldFromType instanceof TypeWithClassName) { - return new MixedType(); - } - - $generatorReturnType = GenericTypeVariableResolver::getType($yieldFromType, Generator::class, 'TReturn'); - if ($generatorReturnType === null) { + $generatorReturnType = $yieldFromType->getTemplateType(Generator::class, 'TReturn'); + if ($generatorReturnType instanceof ErrorType) { return new MixedType(); } return $generatorReturnType; } elseif ($node instanceof Expr\Match_) { $cond = $node->cond; + $condType = $this->getType($cond); $types = []; $matchScope = $this; - foreach ($node->arms as $arm) { + $arms = $node->arms; + if ($condType->isEnum()->yes()) { + // enum match analysis would work even without this if branch + // but would be much slower + // this avoids using ObjectType::$subtractedType which is slow for huge enums + // because of repeated union type normalization + $enumCases = $condType->getEnumCases(); + if (count($enumCases) > 0) { + $indexedEnumCases = []; + foreach ($enumCases as $enumCase) { + $indexedEnumCases[strtolower($enumCase->getClassName())][$enumCase->getEnumCaseName()] = $enumCase; + } + $unusedIndexedEnumCases = $indexedEnumCases; + + foreach ($arms as $i => $arm) { + if ($arm->conds === null) { + continue; + } + + $conditionCases = []; + foreach ($arm->conds as $armCond) { + if (!$armCond instanceof Expr\ClassConstFetch) { + continue 2; + } + if (!$armCond->class instanceof Name) { + continue 2; + } + if (!$armCond->name instanceof Node\Identifier) { + continue 2; + } + $fetchedClassName = $this->resolveName($armCond->class); + $loweredFetchedClassName = strtolower($fetchedClassName); + if (!array_key_exists($loweredFetchedClassName, $indexedEnumCases)) { + continue 2; + } + + $caseName = $armCond->name->toString(); + if (!array_key_exists($caseName, $indexedEnumCases[$loweredFetchedClassName])) { + continue 2; + } + + $conditionCases[] = $indexedEnumCases[$loweredFetchedClassName][$caseName]; + unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]); + } + + $conditionCasesCount = count($conditionCases); + if ($conditionCasesCount === 0) { + throw new ShouldNotHappenException(); + } elseif ($conditionCasesCount === 1) { + $conditionCaseType = $conditionCases[0]; + } else { + $conditionCaseType = new UnionType($conditionCases); + } + + $types[] = $matchScope->addTypeToExpression( + $cond, + $conditionCaseType, + )->getType($arm->body); + unset($arms[$i]); + } + + $remainingCases = []; + foreach ($unusedIndexedEnumCases as $cases) { + foreach ($cases as $case) { + $remainingCases[] = $case; + } + } + + $remainingCasesCount = count($remainingCases); + if ($remainingCasesCount === 0) { + $remainingType = new NeverType(); + } elseif ($remainingCasesCount === 1) { + $remainingType = $remainingCases[0]; + } else { + $remainingType = new UnionType($remainingCases); + } + + $matchScope = $matchScope->addTypeToExpression($cond, $remainingType); + } + } + + foreach ($arms as $arm) { if ($arm->conds === null) { + if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) { + $arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)); + } $types[] = $matchScope->getType($arm->body); continue; } @@ -2036,19 +1891,32 @@ private function resolveType(Expr $node): Type throw new ShouldNotHappenException(); } - $filteringExpr = null; - foreach ($arm->conds as $armCond) { - $armCondExpr = new BinaryOp\Identical($cond, $armCond); - if ($filteringExpr === null) { - $filteringExpr = $armCondExpr; - continue; + if (count($arm->conds) === 1) { + $filteringExpr = new BinaryOp\Identical($cond, $arm->conds[0]); + } else { + $items = []; + foreach ($arm->conds as $filteringExpr) { + $items[] = new Node\ArrayItem($filteringExpr); } - - $filteringExpr = new BinaryOp\BooleanOr($filteringExpr, $armCondExpr); + $filteringExpr = new FuncCall( + new Name\FullyQualified('in_array'), + [ + new Arg($cond), + new Arg(new Array_($items)), + new Arg(new ConstFetch(new Name\FullyQualified('true'))), + ], + ); } - $truthyScope = $matchScope->filterByTruthyValue($filteringExpr); - $types[] = $truthyScope->getType($arm->body); + $filteringExprType = $matchScope->getType($filteringExpr); + + if (!$filteringExprType->isFalse()->yes()) { + $truthyScope = $matchScope->filterByTruthyValue($filteringExpr); + if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) { + $arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)); + } + $types[] = $truthyScope->getType($arm->body); + } $matchScope = $matchScope->filterByFalseyValue($filteringExpr); } @@ -2056,9 +1924,33 @@ private function resolveType(Expr $node): Type return TypeCombinator::union(...$types); } - $exprString = $this->getNodeKey($node); - if (isset($this->moreSpecificTypes[$exprString]) && $this->moreSpecificTypes[$exprString]->getCertainty()->yes()) { - return $this->moreSpecificTypes[$exprString]->getType(); + if ($node instanceof Expr\Isset_) { + $issetResult = true; + foreach ($node->vars as $var) { + $result = $this->issetCheck($var, static function (Type $type): ?bool { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + return !$isNull->yes(); + }); + if ($result !== null) { + if (!$result) { + return new ConstantBooleanType($result); + } + + continue; + } + + $issetResult = $result; + } + + if ($issetResult === null) { + return new BooleanType(); + } + + return new ConstantBooleanType($issetResult); } if ($node instanceof Expr\AssignOp\Coalesce) { @@ -2066,13 +1958,11 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\BinaryOp\Coalesce) { - $leftType = $this->getType($node->left); - $rightType = $this->filterByFalseyValue( - new BinaryOp\NotIdentical($node->left, new ConstFetch(new Name('null'))), - )->getType($node->right); + $issetLeftExpr = new Expr\Isset_([$node->left]); + $leftType = $this->filterByTruthyValue($issetLeftExpr)->getType($node->left); $result = $this->issetCheck($node->left, static function (Type $type): ?bool { - $isNull = (new NullType())->isSuperTypeOf($type); + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } @@ -2080,6 +1970,12 @@ private function resolveType(Expr $node): Type return !$isNull->yes(); }); + if ($result !== null && $result !== false) { + return TypeCombinator::removeNull($leftType); + } + + $rightType = $this->filterByFalseyValue($issetLeftExpr)->getType($node->right); + if ($result === null) { return TypeCombinator::union( TypeCombinator::removeNull($leftType), @@ -2087,10 +1983,6 @@ private function resolveType(Expr $node): Type ); } - if ($result) { - return TypeCombinator::removeNull($leftType); - } - return $rightType; } @@ -2105,392 +1997,106 @@ private function resolveType(Expr $node): Type return new NullType(); } - if ($node->name->isFullyQualified()) { - if (array_key_exists($node->name->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($node->name->toString(), $this->constantTypes[$node->name->toCodeString()]); - } + $namespacedName = null; + if (!$node->name->isFullyQualified() && $this->getNamespace() !== null) { + $namespacedName = new FullyQualified([$this->getNamespace(), $node->name->toString()]); } + $globalName = new FullyQualified($node->name->toString()); - if ($this->getNamespace() !== null) { - $constantName = new FullyQualified([$this->getNamespace(), $constName]); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); + foreach ([$namespacedName, $globalName] as $name) { + if ($name === null) { + continue; + } + $constFetch = new ConstFetch($name); + if ($this->hasExpressionType($constFetch)->yes()) { + return $this->constantResolver->resolveConstantType( + $name->toString(), + $this->expressionTypes[$this->getNodeKey($constFetch)]->getType(), + ); } } - $constantName = new FullyQualified($constName); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); + $constantType = $this->constantResolver->resolveConstant($node->name, $this); + if ($constantType !== null) { + return $constantType; } - 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(), - ]); + return new ErrorType(); + } elseif ($node instanceof Node\Expr\ClassConstFetch && $node->name instanceof Node\Identifier) { + if ($this->hasExpressionType($node)->yes()) { + return $this->expressionTypes[$exprString]->getType(); + } + return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection( + $node->class, + $node->name->name, + $this->isInClass() ? $this->getClassReflection() : null, + fn (Expr $expr): Type => $this->getType($expr), + ); + } + + if ($node instanceof Expr\Ternary) { + $noopCallback = static function (): void { + }; + $condResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->cond), $node->cond, $this, $noopCallback, ExpressionContext::createDeep()); + if ($node->if === null) { + $conditionType = $this->getType($node->cond); + $booleanConditionType = $conditionType->toBoolean(); + if ($booleanConditionType->isTrue()->yes()) { + return $condResult->getTruthyScope()->getType($node->cond); } - if ($resolvedConstantName === 'PHP_MAJOR_VERSION') { - return IntegerRangeType::fromInterval(5, null); + + if ($booleanConditionType->isFalse()->yes()) { + return $condResult->getFalseyScope()->getType($node->else); } - if ($resolvedConstantName === 'PHP_MINOR_VERSION') { - return IntegerRangeType::fromInterval(0, null); + + return TypeCombinator::union( + TypeCombinator::removeFalsey($condResult->getTruthyScope()->getType($node->cond)), + $condResult->getFalseyScope()->getType($node->else), + ); + } + + $booleanConditionType = $this->getType($node->cond)->toBoolean(); + if ($booleanConditionType->isTrue()->yes()) { + return $condResult->getTruthyScope()->getType($node->if); + } + + if ($booleanConditionType->isFalse()->yes()) { + return $condResult->getFalseyScope()->getType($node->else); + } + + return TypeCombinator::union( + $condResult->getTruthyScope()->getType($node->if), + $condResult->getFalseyScope()->getType($node->else), + ); + } + + if ($node instanceof Variable) { + if (is_string($node->name)) { + if ($this->hasVariableType($node->name)->no()) { + return new ErrorType(); } - 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(), - ]); - } - 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('/'), - new ConstantStringType('\\'), - ]); - } - if ($resolvedConstantName === 'PATH_SEPARATOR') { - return new UnionType([ - new ConstantStringType(':'), - new ConstantStringType(';'), - ]); - } - // iconv, https://www.php.net/manual/en/iconv.constants.php - if ($resolvedConstantName === 'ICONV_IMPL') { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); - } - // 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(); - - return $this->resolveConstantType($resolvedConstantName, $constantType); - } - - 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); - $namesToResolve = [ - 'self', - 'parent', - ]; - if ($this->isInClass()) { - if ($this->getClassReflection()->isFinal()) { - $namesToResolve[] = 'static'; - } elseif (strtolower($constantClass) === 'static') { - if (strtolower($constantName) === 'class') { - return new GenericClassStringType(new StaticType($this->getClassReflection())); - } - - $namesToResolve[] = 'static'; - $isObject = true; - } - } - if (in_array(strtolower($constantClass), $namesToResolve, true)) { - $resolvedName = $this->resolveName($node->class); - if ($resolvedName === 'parent' && strtolower($constantName) === 'class') { - return new ClassStringType(); - } - $constantClassType = $this->resolveTypeByName($node->class); - } - - if (strtolower($constantName) === 'class') { - return new ConstantStringType($constantClassType->getClassName(), true); - } - } 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 = []; - foreach ($referencedClasses as $referencedClass) { - $classTypes[] = new GenericClassStringType(new ObjectType($referencedClass)); - } - - return TypeCombinator::union(...$classTypes); - } - $types = []; - foreach ($referencedClasses as $referencedClass) { - if (!$this->reflectionProvider->hasClass($referencedClass)) { - continue; - } - - $constantClassReflection = $this->reflectionProvider->getClass($referencedClass); - if (!$constantClassReflection->hasConstant($constantName)) { - continue; - } - - if ($constantClassReflection->isEnum() && $constantClassReflection->hasEnumCase($constantName)) { - $types[] = new EnumCaseObjectType($constantClassReflection->getName(), $constantName); - continue; + + return $this->getVariableType($node->name); + } + + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + $types = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $variableScope = $this + ->filterByTruthyValue( + new BinaryOp\Identical($node->name, new String_($constantString->getValue())), + ); + if ($variableScope->hasVariableType($constantString->getValue())->no()) { + $types[] = new ErrorType(); + continue; + } + + $types[] = $variableScope->getVariableType($constantString->getValue()); } - $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', $constantClassReflection->getName(), $constantName), $this->dynamicConstantNames, true) - ) { - $constantType = $constantType->generalize(GeneralizePrecision::lessSpecific()); - } - $types[] = $constantType; - } - - if (count($types) > 0) { return TypeCombinator::union(...$types); } - - if (!$constantClassType->hasConstant($constantName)->yes()) { - return new ErrorType(); - } - - return $constantClassType->getConstant($constantName)->getValueType(); - } - - if ($node instanceof Expr\Ternary) { - if ($node->if === null) { - $conditionType = $this->getType($node->cond); - $booleanConditionType = $conditionType->toBoolean(); - if ($booleanConditionType instanceof ConstantBooleanType) { - if ($booleanConditionType->getValue()) { - return $this->filterByTruthyValue($node->cond)->getType($node->cond); - } - - return $this->filterByFalseyValue($node->cond)->getType($node->else); - } - return TypeCombinator::union( - TypeCombinator::remove($this->filterByTruthyValue($node->cond)->getType($node->cond), StaticTypeFactory::falsey()), - $this->filterByFalseyValue($node->cond)->getType($node->else), - ); - } - - $booleanConditionType = $this->getType($node->cond)->toBoolean(); - if ($booleanConditionType instanceof ConstantBooleanType) { - if ($booleanConditionType->getValue()) { - return $this->filterByTruthyValue($node->cond)->getType($node->if); - } - - return $this->filterByFalseyValue($node->cond)->getType($node->else); - } - - return TypeCombinator::union( - $this->filterByTruthyValue($node->cond)->getType($node->if), - $this->filterByFalseyValue($node->cond)->getType($node->else), - ); - } - - if ($node instanceof Variable && is_string($node->name)) { - if ($this->hasVariableType($node->name)->no()) { - return new ErrorType(); - } - - return $this->getVariableType($node->name); } if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) { @@ -2504,24 +2110,48 @@ private function resolveType(Expr $node): Type ); } - if ($node instanceof MethodCall && $node->name instanceof Node\Identifier) { - $typeCallback = function () use ($node): Type { + if ($node instanceof MethodCall) { + if ($node->name instanceof Node\Identifier) { + if ($this->nativeTypesPromoted) { + $methodReflection = $this->getMethodReflection( + $this->getNativeType($node->var), + $node->name->name, + ); + if ($methodReflection === null) { + $returnType = new ErrorType(); + } else { + $returnType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); + } + + return $this->getNullsafeShortCircuitingType($node->var, $returnType); + } + $returnType = $this->methodCallReturnType( $this->getType($node->var), $node->name->name, $node, ); if ($returnType === null) { - return new ErrorType(); + $returnType = new ErrorType(); } - return $returnType; - }; + return $this->getNullsafeShortCircuitingType($node->var, $returnType); + } - return $this->getNullsafeShortCircuitingType($node->var, $typeCallback()); + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType(new MethodCall($node->var, new Identifier($constantString->getValue()), $node->args)), $nameType->getConstantStrings()), + ); + } } if ($node instanceof Expr\NullsafeMethodCall) { $varType = $this->getType($node->var); + if ($varType->isNull()->yes()) { + return new NullType(); + } if (!TypeCombinator::containsNull($varType)) { return $this->getType(new MethodCall($node->var, $node->name, $node->args)); } @@ -2533,65 +2163,109 @@ private function resolveType(Expr $node): Type ); } - if ($node instanceof Expr\StaticCall && $node->name instanceof Node\Identifier) { - $typeCallback = function () use ($node): Type { - if ($node->class instanceof Name) { - $staticMethodCalledOnType = $this->resolveTypeByName($node->class); - } else { - $staticMethodCalledOnType = TypeTraverser::map($this->getType($node->class), static function (Type $type, callable $traverse): Type { - if ($type instanceof UnionType) { - return $traverse($type); - } + if ($node instanceof Expr\StaticCall) { + if ($node->name instanceof Node\Identifier) { + if ($this->nativeTypesPromoted) { + if ($node->class instanceof Name) { + $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); + } else { + $staticMethodCalledOnType = $this->getNativeType($node->class); + } + $methodReflection = $this->getMethodReflection( + $staticMethodCalledOnType, + $node->name->name, + ); + if ($methodReflection === null) { + $callType = new ErrorType(); + } else { + $callType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); + } - if ($type instanceof GenericClassStringType) { - return $type->getGenericType(); - } + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $callType); + } - if ($type instanceof ConstantStringType && $type->isClassString()) { - return new ObjectType($type->getValue()); - } + return $callType; + } - return $type; - }); + if ($node->class instanceof Name) { + $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); + } else { + $staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } - $returnType = $this->methodCallReturnType( + $callType = $this->methodCallReturnType( $staticMethodCalledOnType, $node->name->toString(), $node, ); - if ($returnType === null) { - return new ErrorType(); + if ($callType === null) { + $callType = new ErrorType(); + } + + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $callType); } - return $returnType; - }; - $callType = $typeCallback(); - if ($node->class instanceof Expr) { - return $this->getNullsafeShortCircuitingType($node->class, $callType); + return $callType; } - return $callType; + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType(new Expr\StaticCall($node->class, new Identifier($constantString->getValue()), $node->args)), $nameType->getConstantStrings()), + ); + } } - if ($node instanceof PropertyFetch && $node->name instanceof Node\Identifier) { - $typeCallback = function () use ($node): Type { + if ($node instanceof PropertyFetch) { + if ($node->name instanceof Node\Identifier) { + if ($this->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this); + if ($propertyReflection === null) { + return new ErrorType(); + } + + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + $nativeType = $propertyReflection->getNativeType(); + + return $this->getNullsafeShortCircuitingType($node->var, $nativeType); + } + $returnType = $this->propertyFetchType( $this->getType($node->var), $node->name->name, $node, ); if ($returnType === null) { - return new ErrorType(); + $returnType = new ErrorType(); } - return $returnType; - }; - return $this->getNullsafeShortCircuitingType($node->var, $typeCallback()); + return $this->getNullsafeShortCircuitingType($node->var, $returnType); + } + + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType( + new PropertyFetch($node->var, new Identifier($constantString->getValue())), + ), $nameType->getConstantStrings()), + ); + } } if ($node instanceof Expr\NullsafePropertyFetch) { $varType = $this->getType($node->var); + if ($varType->isNull()->yes()) { + return new NullType(); + } if (!TypeCombinator::containsNull($varType)) { return $this->getType(new PropertyFetch($node->var, $node->name)); } @@ -2603,37 +2277,56 @@ private function resolveType(Expr $node): Type ); } - if ( - $node instanceof Expr\StaticPropertyFetch - && $node->name instanceof Node\VarLikeIdentifier - ) { - $typeCallback = function () use ($node): Type { + if ($node instanceof Expr\StaticPropertyFetch) { + if ($node->name instanceof Node\VarLikeIdentifier) { + if ($this->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this); + if ($propertyReflection === null) { + return new ErrorType(); + } + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + $nativeType = $propertyReflection->getNativeType(); + + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $nativeType); + } + + return $nativeType; + } + if ($node->class instanceof Name) { $staticPropertyFetchedOnType = $this->resolveTypeByName($node->class); } else { - $staticPropertyFetchedOnType = $this->getType($node->class); - if ($staticPropertyFetchedOnType instanceof GenericClassStringType) { - $staticPropertyFetchedOnType = $staticPropertyFetchedOnType->getGenericType(); - } + $staticPropertyFetchedOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } - $returnType = $this->propertyFetchType( + $fetchType = $this->propertyFetchType( $staticPropertyFetchedOnType, $node->name->toString(), $node, ); - if ($returnType === null) { - return new ErrorType(); + if ($fetchType === null) { + $fetchType = new ErrorType(); } - return $returnType; - }; - $fetchType = $typeCallback(); - if ($node->class instanceof Expr) { - return $this->getNullsafeShortCircuitingType($node->class, $fetchType); + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $fetchType); + } + + return $fetchType; } - return $fetchType; + $nameType = $this->getType($node->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this + ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue()))) + ->getType(new Expr\StaticPropertyFetch($node->class, new Node\VarLikeIdentifier($constantString->getValue()))), $nameType->getConstantStrings()), + ); + } } if ($node instanceof FuncCall) { @@ -2647,6 +2340,7 @@ private function resolveType(Expr $node): Type $this, $node->getArgs(), $calledOnType->getCallableParametersAcceptors($this), + null, )->getReturnType(); } @@ -2655,19 +2349,44 @@ private function resolveType(Expr $node): Type } $functionReflection = $this->reflectionProvider->getFunction($node->name, $this); - foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) { - if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { - continue; - } + if ($this->nativeTypesPromoted) { + return ParametersAcceptorSelector::combineAcceptors($functionReflection->getVariants())->getNativeReturnType(); + } + + if ($functionReflection->getName() === 'call_user_func') { + $result = ArgumentsNormalizer::reorderCallUserFuncArguments($node, $this); + if ($result !== null) { + [, $innerFuncCall] = $result; - return $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall($functionReflection, $node, $this); + return $this->getType($innerFuncCall); + } } - return ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, $node->getArgs(), $functionReflection->getVariants(), - )->getReturnType(); + $functionReflection->getNamedArgumentsVariants(), + ); + $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + if ($normalizedNode !== null) { + foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) { + if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { + continue; + } + + $resolvedType = $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall( + $functionReflection, + $normalizedNode, + $this, + ); + if ($resolvedType !== null) { + return $resolvedType; + } + } + } + + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $node); } return new MixedType(); @@ -2707,10 +2426,29 @@ private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type return $type; } + private function transformVoidToNull(Type $type, Node $node): Type + { + if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type->isVoid()->yes()) { + return new NullType(); + } + + return $type; + }); + } + /** * @param callable(Type): ?bool $typeCallback */ - private function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = null): ?bool + public 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)) { @@ -2733,36 +2471,20 @@ private function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = return $result; } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - $type = $this->treatPhpDocTypesAsCertain - ? $this->getType($expr->var) - : $this->getNativeType($expr->var); - $dimType = $this->treatPhpDocTypesAsCertain - ? $this->getType($expr->dim) - : $this->getNativeType($expr->dim); - $hasOffsetValue = $type->hasOffsetValueType($dimType); + $type = $this->getType($expr->var); if (!$type->isOffsetAccessible()->yes()) { return $result ?? $this->issetCheckUndefined($expr->var); } + $dimType = $this->getType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); if ($hasOffsetValue->no()) { - 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. + // If offset cannot be null, store this error message and see if one of the earlier offsets is. // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null. if ($hasOffsetValue->yes()) { - if ($result !== null) { - return $result; - } - $result = $typeCallback($type->getOffsetValueType($dimType)); if ($result !== null) { @@ -2801,9 +2523,8 @@ private function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = return null; } - $nativeType = $propertyReflection->getNativeType(); - if (!$nativeType instanceof MixedType) { - if (!$this->isSpecified($expr)) { + if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { + if (!$this->hasExpressionType($expr)->yes()) { if ($expr instanceof Node\Expr\PropertyFetch) { return $this->issetCheckUndefined($expr->var); } @@ -2854,12 +2575,13 @@ private function issetCheckUndefined(Expr $expr): ?bool if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { $type = $this->getType($expr->var); - $dimType = $this->getType($expr->dim); - $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$type->isOffsetAccessible()->yes()) { return $this->issetCheckUndefined($expr->var); } + $dimType = $this->getType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if (!$hasOffsetValue->no()) { return $this->issetCheckUndefined($expr->var); } @@ -2881,133 +2603,139 @@ private function issetCheckUndefined(Expr $expr): ?bool /** * @param ParametersAcceptor[] $variants */ - private function createFirstClassCallable(array $variants): Type + private function createFirstClassCallable( + FunctionReflection|ExtendedMethodReflection|null $function, + array $variants, + ): Type { $closureTypes = []; + foreach ($variants as $variant) { + $returnType = $variant->getReturnType(); + if ($variant instanceof ExtendedParametersAcceptor) { + $returnType = $this->nativeTypesPromoted ? $variant->getNativeReturnType() : $returnType; + } + + $templateTags = []; + foreach ($variant->getTemplateTypeMap()->getTypes() as $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } + $templateTags[$templateType->getName()] = new TemplateTag( + $templateType->getName(), + $templateType->getBound(), + $templateType->getDefault(), + $templateType->getVariance(), + ); + } + + $throwPoints = []; + $impurePoints = []; + $acceptsNamedArguments = TrinaryLogic::createYes(); + if ($variant instanceof CallableParametersAcceptor) { + $throwPoints = $variant->getThrowPoints(); + $impurePoints = $variant->getImpurePoints(); + $acceptsNamedArguments = $variant->acceptsNamedArguments(); + } elseif ($function !== null) { + $returnTypeForThrow = $variant->getReturnType(); + $throwType = $function->getThrowType(); + if ($throwType === null) { + if ($returnTypeForThrow instanceof NeverType && $returnTypeForThrow->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true); + } + } else { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnTypeForThrow)->yes()) { + $throwPoints[] = SimpleThrowPoint::createImplicit(); + } + } + + $impurePoint = SimpleImpurePoint::createFromVariant($function, $variant); + if ($impurePoint !== null) { + $impurePoints[] = $impurePoint; + } + + $acceptsNamedArguments = $function->acceptsNamedArguments(); + } + $parameters = $variant->getParameters(); $closureTypes[] = new ClosureType( $parameters, - $variant->getReturnType(), + $returnType, $variant->isVariadic(), $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), + $variant instanceof ExtendedParametersAcceptor ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $templateTags, + $throwPoints, + $impurePoints, + [], + [], + $acceptsNamedArguments, ); } 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(GeneralizePrecision::lessSpecific()); - } - - return $constantType; - } - /** @api */ public function getNativeType(Expr $expr): Type { - $key = $this->getNodeKey($expr); - - if (array_key_exists($key, $this->nativeExpressionTypes)) { - return $this->nativeExpressionTypes[$key]; - } + return $this->promoteNativeTypes()->getType($expr); + } - if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - return $this->getNullsafeShortCircuitingType( - $expr->var, - $this->getTypeFromArrayDimFetch( - $expr, - $this->getNativeType($expr->dim), - $this->getNativeType($expr->var), - ), - ); - } + public function getKeepVoidType(Expr $node): Type + { + $clonedNode = clone $node; + $clonedNode->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, true); - return $this->getType($expr); + return $this->getType($clonedNode); } - /** @api */ public function doNotTreatPhpDocTypesAsCertain(): Scope { - if (!$this->treatPhpDocTypesAsCertain) { - return $this; - } - - return new self( - $this->scopeFactory, - $this->reflectionProvider, - $this->dynamicReturnTypeExtensionRegistry, - $this->operatorTypeSpecifyingExtensionRegistry, - $this->printer, - $this->typeSpecifier, - $this->propertyReflectionFinder, - $this->parser, - $this->nodeScopeResolver, - $this->context, - $this->phpVersion, - $this->declareStrictTypes, - $this->constantTypes, - $this->function, - $this->namespace, - $this->variableTypes, - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - $this->dynamicConstantNames, - false, - $this->afterExtractCall, - $this->parentScope, - $this->explicitMixedInUnknownGenericNew, - ); + return $this->promoteNativeTypes(); } private function promoteNativeTypes(): self { - $variableTypes = $this->variableTypes; - foreach ($this->nativeExpressionTypes as $expressionType => $type) { - if (substr($expressionType, 0, 1) !== '$') { - throw new ShouldNotHappenException(); - } - - $variableName = substr($expressionType, 1); - $has = $this->hasVariableType($variableName); - if ($has->no()) { - throw new ShouldNotHappenException(); - } + if ($this->nativeTypesPromoted) { + return $this; + } - $variableTypes[$variableName] = new VariableTypeHolder($type, $has); + if ($this->scopeWithPromotedNativeTypes !== null) { + return $this->scopeWithPromotedNativeTypes; } - return $this->scopeFactory->create( + return $this->scopeWithPromotedNativeTypes = $this->scopeFactory->create( $this->context, $this->declareStrictTypes, - $this->constantTypes, $this->function, $this->namespace, - $variableTypes, - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->nativeExpressionTypes, + [], + [], + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - [], + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + true, ); } /** * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - private function hasPropertyNativeType($propertyFetch): bool + public function hasPropertyNativeType($propertyFetch): bool { $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $this); if ($propertyReflection === null) { @@ -3018,11 +2746,10 @@ private function hasPropertyNativeType($propertyFetch): bool return false; } - return !$propertyReflection->getNativeType() instanceof MixedType; + return $propertyReflection->hasNativeType(); } - /** @api */ - protected function getTypeFromArrayDimFetch( + private function getTypeFromArrayDimFetch( Expr\ArrayDimFetch $arrayDimFetch, Type $offsetType, Type $offsetAccessibleType, @@ -3032,7 +2759,7 @@ protected function getTypeFromArrayDimFetch( throw new ShouldNotHappenException(); } - if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes()) { + if (!$offsetAccessibleType->isArray()->yes() && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes()) { return $this->getType( new MethodCall( $arrayDimFetch->var, @@ -3047,97 +2774,6 @@ protected function getTypeFromArrayDimFetch( return $offsetAccessibleType->getOffsetValueType($offsetType); } - private function calculateFromScalars(Expr $node, ConstantScalarType $leftType, ConstantScalarType $rightType): Type - { - if ($leftType instanceof StringType && $rightType instanceof StringType) { - /** @var string $leftValue */ - $leftValue = $leftType->getValue(); - /** @var string $rightValue */ - $rightValue = $rightType->getValue(); - - if ($node instanceof Expr\BinaryOp\BitwiseAnd || $node instanceof Expr\AssignOp\BitwiseAnd) { - return $this->getTypeFromValue($leftValue & $rightValue); - } - - if ($node instanceof Expr\BinaryOp\BitwiseOr || $node instanceof Expr\AssignOp\BitwiseOr) { - return $this->getTypeFromValue($leftValue | $rightValue); - } - - if ($node instanceof Expr\BinaryOp\BitwiseXor || $node instanceof Expr\AssignOp\BitwiseXor) { - return $this->getTypeFromValue($leftValue ^ $rightValue); - } - } - - $leftValue = $leftType->getValue(); - $rightValue = $rightType->getValue(); - - if ($node instanceof Node\Expr\BinaryOp\Spaceship) { - return $this->getTypeFromValue($leftValue <=> $rightValue); - } - - $leftNumberType = $leftType->toNumber(); - $rightNumberType = $rightType->toNumber(); - if (TypeCombinator::union($leftNumberType, $rightNumberType) instanceof ErrorType) { - return new ErrorType(); - } - - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } - - /** @var float|int $leftNumberValue */ - $leftNumberValue = $leftNumberType->getValue(); - - /** @var float|int $rightNumberValue */ - $rightNumberValue = $rightNumberType->getValue(); - - if ($node instanceof Node\Expr\BinaryOp\Plus || $node instanceof Node\Expr\AssignOp\Plus) { - return $this->getTypeFromValue($leftNumberValue + $rightNumberValue); - } - - if ($node instanceof Node\Expr\BinaryOp\Minus || $node instanceof Node\Expr\AssignOp\Minus) { - return $this->getTypeFromValue($leftNumberValue - $rightNumberValue); - } - - if ($node instanceof Node\Expr\BinaryOp\Mul || $node instanceof Node\Expr\AssignOp\Mul) { - return $this->getTypeFromValue($leftNumberValue * $rightNumberValue); - } - - if ($node instanceof Node\Expr\BinaryOp\Pow || $node instanceof Node\Expr\AssignOp\Pow) { - return $this->getTypeFromValue($leftNumberValue ** $rightNumberValue); - } - - if ($node instanceof Node\Expr\BinaryOp\Div || $node instanceof Node\Expr\AssignOp\Div) { - return $this->getTypeFromValue($leftNumberValue / $rightNumberValue); - } - - if ($node instanceof Node\Expr\BinaryOp\Mod || $node instanceof Node\Expr\AssignOp\Mod) { - return $this->getTypeFromValue(((int) $leftNumberValue) % ((int) $rightNumberValue)); - } - - if ($node instanceof Expr\BinaryOp\ShiftLeft || $node instanceof Expr\AssignOp\ShiftLeft) { - return $this->getTypeFromValue($leftNumberValue << $rightNumberValue); - } - - if ($node instanceof Expr\BinaryOp\ShiftRight || $node instanceof Expr\AssignOp\ShiftRight) { - return $this->getTypeFromValue($leftNumberValue >> $rightNumberValue); - } - - if ($node instanceof Expr\BinaryOp\BitwiseAnd || $node instanceof Expr\AssignOp\BitwiseAnd) { - return $this->getTypeFromValue($leftNumberValue & $rightNumberValue); - } - - if ($node instanceof Expr\BinaryOp\BitwiseOr || $node instanceof Expr\AssignOp\BitwiseOr) { - return $this->getTypeFromValue($leftNumberValue | $rightNumberValue); - } - - if ($node instanceof Expr\BinaryOp\BitwiseXor || $node instanceof Expr\AssignOp\BitwiseXor) { - return $this->getTypeFromValue($leftNumberValue ^ $rightNumberValue); - } - - return new MixedType(); - } - private function resolveExactName(Name $name): ?string { $originalClass = (string) $name; @@ -3169,15 +2805,16 @@ public function resolveName(Name $name): string { $originalClass = (string) $name; if ($this->isInClass()) { - if (in_array(strtolower($originalClass), [ + $lowerClass = strtolower($originalClass); + if (in_array($lowerClass, [ 'self', 'static', ], true)) { - if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static') { - return $this->inClosureBindScopeClass; + if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) { + return $this->inClosureBindScopeClasses[0]; } return $this->getClassReflection()->getName(); - } elseif ($originalClass === 'parent') { + } elseif ($lowerClass === 'parent') { $currentClassReflection = $this->getClassReflection(); if ($currentClassReflection->getParentClass() !== null) { return $currentClassReflection->getParentClass()->getName(); @@ -3192,9 +2829,9 @@ public function resolveName(Name $name): string public function resolveTypeByName(Name $name): TypeWithClassName { if ($name->toLowerString() === 'static' && $this->isInClass()) { - if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static') { - if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { - return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClass)); + if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) { + if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClasses[0])) { + return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClasses[0])); } } @@ -3203,11 +2840,11 @@ public function resolveTypeByName(Name $name): TypeWithClassName $originalClass = $this->resolveName($name); if ($this->isInClass()) { - 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)); + if ($this->inClosureBindScopeClasses === [$originalClass]) { + if ($this->reflectionProvider->hasClass($originalClass)) { + return new ThisType($this->reflectionProvider->getClass($originalClass)); } - return new ObjectType($this->inClosureBindScopeClass); + return new ObjectType($originalClass); } $thisType = new ThisType($this->getClassReflection()); @@ -3220,6 +2857,26 @@ public function resolveTypeByName(Name $name): TypeWithClassName return new ObjectType($originalClass); } + private function resolveTypeByNameWithLateStaticBinding(Name $class, Node\Identifier $name): TypeWithClassName + { + $classType = $this->resolveTypeByName($class); + + if ( + $classType instanceof StaticType + && !in_array($class->toLowerString(), ['self', 'static', 'parent'], true) + ) { + $methodReflectionCandidate = $this->getMethodReflection( + $classType, + $name->name, + ); + if ($methodReflectionCandidate !== null && $methodReflectionCandidate->isStatic()) { + $classType = $classType->getStaticObjectType(); + } + } + + return $classType; + } + /** * @api * @param mixed $value @@ -3230,39 +2887,44 @@ public function getTypeFromValue($value): Type } /** @api */ - public function isSpecified(Expr $node): bool + public function hasExpressionType(Expr $node): TrinaryLogic { - $exprString = $this->getNodeKey($node); + if ($node instanceof Variable && is_string($node->name)) { + return $this->hasVariableType($node->name); + } - return isset($this->moreSpecificTypes[$exprString]) - && $this->moreSpecificTypes[$exprString]->getCertainty()->yes(); + $exprString = $this->getNodeKey($node); + if (!isset($this->expressionTypes[$exprString])) { + return TrinaryLogic::createNo(); + } + return $this->expressionTypes[$exprString]->getCertainty(); } /** - * @param MethodReflection|FunctionReflection $reflection + * @param MethodReflection|FunctionReflection|null $reflection */ - public function pushInFunctionCall($reflection): self + public function pushInFunctionCall($reflection, ?ParameterReflection $parameter): self { $stack = $this->inFunctionCallsStack; - $stack[] = $reflection; + $stack[] = [$reflection, $parameter]; return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $stack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -3274,27 +2936,27 @@ public function popInFunctionCall(): self return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $stack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } /** @api */ public function isInClassExists(string $className): bool { - foreach ($this->inFunctionCallsStack as $inFunctionCall) { + foreach ($this->inFunctionCallsStack as [$inFunctionCall]) { if (!$inFunctionCall instanceof FunctionReflection) { continue; } @@ -3311,7 +2973,23 @@ public function isInClassExists(string $className): bool new Arg(new String_(ltrim($className, '\\'))), ]); - return (new ConstantBooleanType(true))->isSuperTypeOf($this->getType($expr))->yes(); + return $this->getType($expr)->isTrue()->yes(); + } + + public function getFunctionCallStack(): array + { + return array_values(array_filter( + array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack), + static fn (FunctionReflection|MethodReflection|null $reflection) => $reflection !== null, + )); + } + + public function getFunctionCallStackWithParameters(): array + { + return array_values(array_filter( + $this->inFunctionCallsStack, + static fn ($item) => $item[0] !== null, + )); } /** @api */ @@ -3321,36 +2999,54 @@ public function isInFunctionExists(string $functionName): bool new Arg(new String_(ltrim($functionName, '\\'))), ]); - return (new ConstantBooleanType(true))->isSuperTypeOf($this->getType($expr))->yes(); + return $this->getType($expr)->isTrue()->yes(); } /** @api */ public function enterClass(ClassReflection $classReflection): self { + $thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection)); + $constantTypes = $this->getConstantTypes(); + $constantTypes['$this'] = $thisHolder; + $nativeConstantTypes = $this->getNativeConstantTypes(); + $nativeConstantTypes['$this'] = $thisHolder; + return $this->scopeFactory->create( $this->context->enterClass($classReflection), $this->isDeclareStrictTypes(), - $this->constantTypes, null, $this->getNamespace(), - [ - 'this' => VariableTypeHolder::createYes(new ThisType($classReflection)), - ], + $constantTypes, + $nativeConstantTypes, + [], + [], + null, + true, + [], + [], + [], + false, + $classReflection->isAnonymous() ? $this : null, ); } public function enterTrait(ClassReflection $traitReflection): self { + $namespace = null; + $traitName = $traitReflection->getName(); + $traitNameParts = explode('\\', $traitName); + if (count($traitNameParts) > 1) { + $namespace = implode('\\', array_slice($traitNameParts, 0, -1)); + } return $this->scopeFactory->create( $this->context->enterTrait($traitReflection), $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), - $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $namespace, + $this->expressionTypes, + $this->nativeExpressionTypes, [], - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, ); } @@ -3358,6 +3054,9 @@ public function enterTrait(ClassReflection $traitReflection): self /** * @api * @param Type[] $phpDocParameterTypes + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -3370,6 +3069,14 @@ public function enterClassMethod( bool $isInternal, bool $isFinal, ?bool $isPure = null, + bool $acceptsNamedArguments = true, + ?Assertions $asserts = null, + ?Type $selfOutType = null, + ?string $phpDocComment = null, + array $parameterOutTypes = [], + array $immediatelyInvokedCallableParameters = [], + array $phpDocClosureThisTypeParameters = [], + bool $isConstructor = false, ): self { if (!$this->isInClass()) { @@ -3380,24 +3087,122 @@ public function enterClassMethod( new PhpMethodFromParserNodeReflection( $this->getClassReflection(), $classMethod, + null, $this->getFile(), $templateTypeMap, $this->getRealParameterTypes($classMethod), - array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes), + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes), $this->getRealParameterDefaultValues($classMethod), + $this->getParameterAttributes($classMethod), $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)), - $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, + $phpDocReturnType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocReturnType)) : null, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, + $acceptsNamedArguments, + $asserts ?? Assertions::createEmpty(), + $selfOutType, + $phpDocComment, + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $parameterOutTypes), + $immediatelyInvokedCallableParameters, + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters), + $isConstructor, + $this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)), ), !$classMethod->isStatic(), ); } + /** + * @param Type[] $phpDocParameterTypes + */ + public function enterPropertyHook( + Node\PropertyHook $hook, + string $propertyName, + Identifier|Name|ComplexType|null $nativePropertyTypeNode, + ?Type $phpDocPropertyType, + array $phpDocParameterTypes, + ?Type $throwType, + ?string $deprecatedDescription, + bool $isDeprecated, + ?string $phpDocComment, + ): self + { + if (!$this->isInClass()) { + throw new ShouldNotHappenException(); + } + + $phpDocParameterTypes = array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes); + + $hookName = $hook->name->toLowerString(); + if ($hookName === 'set') { + if ($hook->params === []) { + $hook = clone $hook; + $hook->params = [ + new Node\Param(new Variable('value'), null, $nativePropertyTypeNode), + ]; + } + + $firstParam = $hook->params[0] ?? null; + if ( + $firstParam !== null + && $phpDocPropertyType !== null + && $firstParam->var instanceof Variable + && is_string($firstParam->var->name) + ) { + $valueParamPhpDocType = $phpDocParameterTypes[$firstParam->var->name] ?? null; + if ($valueParamPhpDocType === null) { + $phpDocParameterTypes[$firstParam->var->name] = $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType)); + } + } + + $realReturnType = new VoidType(); + $phpDocReturnType = null; + } elseif ($hookName === 'get') { + $realReturnType = $this->getFunctionType($nativePropertyTypeNode, false, false); + $phpDocReturnType = $phpDocPropertyType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType)) : null; + } else { + throw new ShouldNotHappenException(); + } + + $realParameterTypes = $this->getRealParameterTypes($hook); + + return $this->enterFunctionLike( + new PhpMethodFromParserNodeReflection( + $this->getClassReflection(), + $hook, + $propertyName, + $this->getFile(), + TemplateTypeMap::createEmpty(), + $realParameterTypes, + $phpDocParameterTypes, + [], + $this->getParameterAttributes($hook), + $realReturnType, + $phpDocReturnType, + $throwType, + $deprecatedDescription, + $isDeprecated, + false, + false, + false, + true, + Assertions::createEmpty(), + null, + $phpDocComment, + [], + [], + [], + false, + $this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)), + ), + true, + ); + } + private function transformStaticType(Type $type): Type { return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { @@ -3407,7 +3212,7 @@ private function transformStaticType(Type $type): Type if ($type instanceof StaticType) { $classReflection = $this->getClassReflection(); $changedType = $type->changeBaseClass($classReflection); - if ($classReflection->isFinal()) { + if ($classReflection->isFinal() && !$type instanceof ThisType) { $changedType = $changedType->getStaticObjectType(); } return $traverse($changedType); @@ -3429,7 +3234,7 @@ private function getRealParameterTypes(Node\FunctionLike $functionLike): array } $realParameterTypes[$parameter->var->name] = $this->getFunctionType( $parameter->type, - $this->isParameterValueNullable($parameter), + $this->isParameterValueNullable($parameter) && $parameter->flags === 0, false, ); } @@ -3456,9 +3261,33 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): return $realParameterDefaultValues; } + /** + * @return array> + */ + private function getParameterAttributes(ClassMethod|Function_|PropertyHook $functionLike): array + { + $parameterAttributes = []; + $className = null; + if ($this->isInClass()) { + $className = $this->getClassReflection()->getName(); + } + foreach ($functionLike->getParams() as $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + + $parameterAttributes[$parameter->var->name] = $this->attributeReflectionFactory->fromAttrGroups($parameter->attrGroups, InitializerExprContext::fromStubParameter($className, $this->getFile(), $functionLike)); + } + + return $parameterAttributes; + } + /** * @api * @param Type[] $phpDocParameterTypes + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterFunction( Node\Stmt\Function_ $function, @@ -3469,8 +3298,13 @@ public function enterFunction( ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, ?bool $isPure = null, + bool $acceptsNamedArguments = true, + ?Assertions $asserts = null, + ?string $phpDocComment = null, + array $parameterOutTypes = [], + array $immediatelyInvokedCallableParameters = [], + array $phpDocClosureThisTypeParameters = [], ): self { return $this->enterFunctionLike( @@ -3481,14 +3315,21 @@ public function enterFunction( $this->getRealParameterTypes($function), array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes), $this->getRealParameterDefaultValues($function), + $this->getParameterAttributes($function), $this->getFunctionType($function->returnType, $function->returnType === null, false), $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal, $isPure, + $acceptsNamedArguments, + $asserts ?? Assertions::createEmpty(), + $phpDocComment, + array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes), + $immediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, + $this->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)), ), false, ); @@ -3496,131 +3337,230 @@ public function enterFunction( private function enterFunctionLike( PhpFunctionFromParserNodeReflection $functionReflection, - bool $preserveThis, + bool $preserveConstructorScope, ): self { - $variableTypes = []; + $parametersByName = []; + + foreach ($functionReflection->getParameters() as $parameter) { + $parametersByName[$parameter->getName()] = $parameter; + } + + $expressionTypes = []; $nativeExpressionTypes = []; - foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameter) { + $conditionalTypes = []; + + if ($preserveConstructorScope) { + $expressionTypes = $this->rememberConstructorExpressions($this->expressionTypes); + $nativeExpressionTypes = $this->rememberConstructorExpressions($this->nativeExpressionTypes); + } + + foreach ($functionReflection->getParameters() as $parameter) { $parameterType = $parameter->getType(); + + if ($parameterType instanceof ConditionalTypeForParameter) { + $targetParameterName = substr($parameterType->getParameterName(), 1); + if (array_key_exists($targetParameterName, $parametersByName)) { + $targetParameter = $parametersByName[$targetParameterName]; + + $ifType = $parameterType->isNegated() ? $parameterType->getElse() : $parameterType->getIf(); + $elseType = $parameterType->isNegated() ? $parameterType->getIf() : $parameterType->getElse(); + + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget())), + ], new ExpressionTypeHolder(new Variable($parameter->getName()), $ifType, TrinaryLogic::createYes())); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget())), + ], new ExpressionTypeHolder(new Variable($parameter->getName()), $elseType, TrinaryLogic::createYes())); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + } + } + + $paramExprString = '$' . $parameter->getName(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { $parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType); } else { - $parameterType = new ArrayType(new IntegerType(), $parameterType); + $parameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $parameterType), new AccessoryArrayListType()); } } - $variableTypes[$parameter->getName()] = VariableTypeHolder::createYes($parameterType); + $parameterNode = new Variable($parameter->getName()); + $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $parameterType); + + $parameterOriginalValueExpr = new ParameterVariableOriginalValueExpr($parameter->getName()); + $parameterOriginalValueExprString = $this->getNodeKey($parameterOriginalValueExpr); + $expressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $parameterType); $nativeParameterType = $parameter->getNativeType(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { $nativeParameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $nativeParameterType); } else { - $nativeParameterType = new ArrayType(new IntegerType(), $nativeParameterType); + $nativeParameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $nativeParameterType), new AccessoryArrayListType()); } } - $nativeExpressionTypes[sprintf('$%s', $parameter->getName())] = $nativeParameterType; - } - - if ($preserveThis && array_key_exists('this', $this->variableTypes)) { - $variableTypes['this'] = $this->variableTypes['this']; + $nativeExpressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $nativeParameterType); + $nativeExpressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $nativeParameterType); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $functionReflection, $this->getNamespace(), - $variableTypes, - [], - [], - null, - null, - true, - [], - $nativeExpressionTypes, + array_merge($this->getConstantTypes(), $expressionTypes), + array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes), + $conditionalTypes, ); } + /** @api */ public function enterNamespace(string $namespaceName): self { return $this->scopeFactory->create( $this->context->beginFile(), $this->isDeclareStrictTypes(), - $this->constantTypes, null, $namespaceName, ); } - public function enterClosureBind(?Type $thisType, string $scopeClass): self + /** + * @param list $scopeClasses + */ + public function enterClosureBind(?Type $thisType, ?Type $nativeThisType, array $scopeClasses): self { - $variableTypes = $this->getVariableTypes(); - + $expressionTypes = $this->expressionTypes; if ($thisType !== null) { - $variableTypes['this'] = VariableTypeHolder::createYes($thisType); + $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType); + } else { + unset($expressionTypes['$this']); + } + + $nativeExpressionTypes = $this->nativeExpressionTypes; + if ($nativeThisType !== null) { + $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType); } else { - unset($variableTypes['this']); + unset($nativeExpressionTypes['$this']); } - if ($scopeClass === 'static' && $this->isInClass()) { - $scopeClass = $this->getClassReflection()->getName(); + if ($scopeClasses === ['static'] && $this->isInClass()) { + $scopeClasses = [$this->getClassReflection()->getName()]; } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $scopeClass, + $scopeClasses, $this->anonymousFunctionReflection, ); } public function restoreOriginalScopeAfterClosureBind(self $originalScope): self { - $variableTypes = $this->getVariableTypes(); - if (isset($originalScope->variableTypes['this'])) { - $variableTypes['this'] = $originalScope->variableTypes['this']; + $expressionTypes = $this->expressionTypes; + if (isset($originalScope->expressionTypes['$this'])) { + $expressionTypes['$this'] = $originalScope->expressionTypes['$this']; + } else { + unset($expressionTypes['$this']); + } + + $nativeExpressionTypes = $this->nativeExpressionTypes; + if (isset($originalScope->nativeExpressionTypes['$this'])) { + $nativeExpressionTypes['$this'] = $originalScope->nativeExpressionTypes['$this']; + } else { + unset($nativeExpressionTypes['$this']); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $originalScope->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + ); + } + + public function restoreThis(self $restoreThisScope): self + { + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + + if ($restoreThisScope->isInClass()) { + $nodeFinder = new NodeFinder(); + $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; + foreach ($restoreThisScope->expressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + $thisExpr = $nodeFinder->findFirst([$expr], $cb); + if ($thisExpr === null) { + continue; + } + + $expressionTypes[$exprString] = $expressionTypeHolder; + } + + foreach ($restoreThisScope->nativeExpressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + $thisExpr = $nodeFinder->findFirst([$expr], $cb); + if ($thisExpr === null) { + continue; + } + + $nativeExpressionTypes[$exprString] = $expressionTypeHolder; + } } else { - unset($variableTypes['this']); + unset($expressionTypes['$this']); + unset($nativeExpressionTypes['$this']); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $originalScope->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + [], + [], + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, ); } - public function enterClosureCall(Type $thisType): self + public function enterClosureCall(Type $thisType, Type $nativeThisType): self { - $variableTypes = $this->getVariableTypes(); - $variableTypes['this'] = VariableTypeHolder::createYes($thisType); + $expressionTypes = $this->expressionTypes; + $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType); + + $nativeExpressionTypes = $this->nativeExpressionTypes; + $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType); return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $thisType instanceof TypeWithClassName ? $thisType->getClassName() : null, + $thisType->getObjectClassNames(), $this->anonymousFunctionReflection, ); } @@ -3628,7 +3568,7 @@ public function enterClosureCall(Type $thisType): self /** @api */ public function isInClosureBind(): bool { - return $this->inClosureBindScopeClass !== null; + return $this->inClosureBindScopeClasses !== []; } /** @@ -3637,7 +3577,7 @@ public function isInClosureBind(): bool */ public function enterAnonymousFunction( Expr\Closure $closure, - ?array $callableParameters = null, + ?array $callableParameters, ): self { $anonymousFunctionReflection = $this->getType($closure); @@ -3650,20 +3590,20 @@ public function enterAnonymousFunction( return $this->scopeFactory->create( $scope->context, $scope->isDeclareStrictTypes(), - $scope->constantTypes, $scope->getFunction(), $scope->getNamespace(), - $scope->variableTypes, - $scope->moreSpecificTypes, + $scope->expressionTypes, + $scope->nativeExpressionTypes, [], - $scope->inClosureBindScopeClass, + $scope->inClosureBindScopeClasses, $anonymousFunctionReflection, true, [], - $scope->nativeExpressionTypes, [], + $this->inFunctionCallsStack, false, $this, + $this->nativeTypesPromoted, ); } @@ -3672,97 +3612,149 @@ public function enterAnonymousFunction( */ private function enterAnonymousFunctionWithoutReflection( Expr\Closure $closure, - ?array $callableParameters = null, + ?array $callableParameters, ): self { - $variableTypes = []; + $expressionTypes = []; + $nativeTypes = []; foreach ($closure->params as $i => $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + $paramExprString = sprintf('$%s', $parameter->var->name); $isNullable = $this->isParameterValueNullable($parameter); $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); if ($callableParameters !== null) { if (isset($callableParameters[$i])) { - $parameterType = TypehintHelper::decideType($parameterType, $callableParameters[$i]->getType()); + $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); } elseif (count($callableParameters) > 0) { $lastParameter = $callableParameters[count($callableParameters) - 1]; if ($lastParameter->isVariadic()) { - $parameterType = TypehintHelper::decideType($parameterType, $lastParameter->getType()); + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } - - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new ShouldNotHappenException(); - } - $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes( - $parameterType, - ); + $holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType); + $expressionTypes[$paramExprString] = $holder; + $nativeTypes[$paramExprString] = $holder; } - $nativeTypes = []; - $moreSpecificTypes = []; + $nonRefVariableNames = []; foreach ($closure->uses as $use) { if (!is_string($use->var->name)) { throw new ShouldNotHappenException(); } + $variableName = $use->var->name; + $paramExprString = '$' . $use->var->name; if ($use->byRef) { + $holder = ExpressionTypeHolder::createYes($use->var, new MixedType()); + $expressionTypes[$paramExprString] = $holder; + $nativeTypes[$paramExprString] = $holder; continue; } - $variableName = $use->var->name; + $nonRefVariableNames[$variableName] = true; if ($this->hasVariableType($variableName)->no()) { $variableType = new ErrorType(); + $variableNativeType = new ErrorType(); } else { $variableType = $this->getVariableType($variableName); - $nativeTypes[sprintf('$%s', $variableName)] = $this->getNativeType($use->var); + $variableNativeType = $this->getNativeType($use->var); } - $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType); - foreach ($this->moreSpecificTypes as $exprString => $moreSpecificType) { - $matches = Strings::matchAll((string) $exprString, '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); - if ($matches === []) { - continue; - } + $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); + $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableNativeType); + } - $matches = array_column($matches, 1); - if (!in_array($variableName, $matches, true)) { - continue; + foreach ($this->invalidateStaticExpressions($this->expressionTypes) as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + if ($expr instanceof Variable) { + continue; + } + $variables = (new NodeFinder())->findInstanceOf([$expr], Variable::class); + if ($variables === [] && !$this->expressionTypeIsUnchangeable($typeHolder)) { + continue; + } + foreach ($variables as $variable) { + if (!is_string($variable->name)) { + continue 2; + } + if (!array_key_exists($variable->name, $nonRefVariableNames)) { + continue 2; } - - $moreSpecificTypes[$exprString] = $moreSpecificType; } + + $expressionTypes[$exprString] = $typeHolder; } if ($this->hasVariableType('this')->yes() && !$closure->static) { - $variableTypes['this'] = VariableTypeHolder::createYes($this->getVariableType('this')); + $node = new Variable('this'); + $expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getType($node)); + $nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getNativeType($node)); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $moreSpecificTypes, + array_merge($this->getConstantTypes(), $expressionTypes), + array_merge($this->getNativeConstantTypes(), $nativeTypes), [], - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, new TrivialParametersAcceptor(), true, [], - $nativeTypes, + [], [], false, $this, + $this->nativeTypesPromoted, ); } + private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): bool + { + $expr = $typeHolder->getExpr(); + $type = $typeHolder->getType(); + + return $expr instanceof FuncCall + && !$expr->isFirstClassCallable() + && $expr->name instanceof FullyQualified + && $expr->name->toLowerString() === 'function_exists' + && isset($expr->getArgs()[0]) + && count($this->getType($expr->getArgs()[0]->value)->getConstantStrings()) === 1 + && $type->isTrue()->yes(); + } + + /** + * @param array $expressionTypes + * @return array + */ + private function invalidateStaticExpressions(array $expressionTypes): array + { + $filteredExpressionTypes = []; + $nodeFinder = new NodeFinder(); + foreach ($expressionTypes as $exprString => $expressionType) { + $staticExpression = $nodeFinder->findFirst( + [$expressionType->getExpr()], + static fn ($node) => $node instanceof Expr\StaticCall || $node instanceof Expr\StaticPropertyFetch, + ); + if ($staticExpression !== null) { + continue; + } + $filteredExpressionTypes[$exprString] = $expressionType; + } + return $filteredExpressionTypes; + } + /** * @api * @param ParameterReflection[]|null $callableParameters */ - public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters = null): self + public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self { $anonymousFunctionReflection = $this->getType($arrowFunction); if (!$anonymousFunctionReflection instanceof ClosureType) { @@ -3774,20 +3766,20 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $ca return $this->scopeFactory->create( $scope->context, $scope->isDeclareStrictTypes(), - $scope->constantTypes, $scope->getFunction(), $scope->getNamespace(), - $scope->variableTypes, - $scope->moreSpecificTypes, + $scope->expressionTypes, + $scope->nativeExpressionTypes, $scope->conditionalExpressions, - $scope->inClosureBindScopeClass, + $scope->inClosureBindScopeClasses, $anonymousFunctionReflection, true, [], [], - [], + $this->inFunctionCallsStack, $scope->afterExtractCall, $scope->parentScope, + $this->nativeTypesPromoted, ); } @@ -3796,13 +3788,10 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $ca */ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self { - $variableTypes = $this->variableTypes; - $mixed = new MixedType(); - $parameterVariables = []; - $parameterVariableExpressions = []; + $arrowFunctionScope = $this; foreach ($arrowFunction->params as $i => $parameter) { if ($parameter->type === null) { - $parameterType = $mixed; + $parameterType = new MixedType(); } else { $isNullable = $this->isParameterValueNullable($parameter); $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); @@ -3810,111 +3799,47 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu if ($callableParameters !== null) { if (isset($callableParameters[$i])) { - $parameterType = TypehintHelper::decideType($parameterType, $callableParameters[$i]->getType()); + $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); } elseif (count($callableParameters) > 0) { $lastParameter = $callableParameters[count($callableParameters) - 1]; if ($lastParameter->isVariadic()) { - $parameterType = TypehintHelper::decideType($parameterType, $lastParameter->getType()); + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { throw new ShouldNotHappenException(); } - - $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes($parameterType); - $parameterVariables[] = $parameter->var->name; - $parameterVariableExpressions[] = $parameter->var; + $arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $parameterType, TrinaryLogic::createYes()); } if ($arrowFunction->static) { - unset($variableTypes['this']); - } - - $conditionalExpressions = []; - foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { - $newHolders = []; - foreach ($parameterVariables as $parameterVariable) { - $exprString = '$' . $parameterVariable; - if ($exprString === $conditionalExprString) { - continue 2; - } - } - - foreach ($holders as $holder) { - foreach ($parameterVariables as $parameterVariable) { - $exprString = '$' . $parameterVariable; - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionalExprString2) { - if ($exprString === $conditionalExprString2) { - continue 3; - } - } - } - - $newHolders[] = $holder; - } - - if (count($newHolders) === 0) { - continue; - } - - $conditionalExpressions[$conditionalExprString] = $newHolders; - } - foreach ($parameterVariables as $parameterVariable) { - $exprString = '$' . $parameterVariable; - foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { - if ($exprString === $conditionalExprString) { - continue; - } - - $newHolders = []; - foreach ($holders as $holder) { - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionalExprString2) { - if ($exprString === $conditionalExprString2) { - continue 2; - } - } - - $newHolders[] = $holder; - } - - if (count($newHolders) === 0) { - continue; - } - - $conditionalExpressions[$conditionalExprString] = $newHolders; - } + $arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this')); } - $scope = $this->scopeFactory->create( - $this->context, + return $this->scopeFactory->create( + $arrowFunctionScope->context, $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $conditionalExpressions, - $this->inClosureBindScopeClass, - null, + $arrowFunctionScope->getFunction(), + $arrowFunctionScope->getNamespace(), + $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes), + $arrowFunctionScope->nativeExpressionTypes, + $arrowFunctionScope->conditionalExpressions, + $arrowFunctionScope->inClosureBindScopeClasses, + new TrivialParametersAcceptor(), true, [], [], [], - $this->afterExtractCall, - $this->parentScope, + $arrowFunctionScope->afterExtractCall, + $arrowFunctionScope->parentScope, + $this->nativeTypesPromoted, ); - - foreach ($parameterVariableExpressions as $expr) { - $scope = $scope->invalidateExpression($expr); - } - - return $scope; } public function isParameterValueNullable(Node\Param $parameter): bool @@ -3938,7 +3863,7 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type ); } if ($isVariadic) { - if ($this->phpVersion->supportsNamedArguments()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no()) { return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $this->getFunctionType( $type, false, @@ -3946,11 +3871,11 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type )); } - return new ArrayType(new IntegerType(), $this->getFunctionType( + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $this->getFunctionType( $type, false, false, - )); + )), new AccessoryArrayListType()); } if ($type instanceof Name) { @@ -3965,41 +3890,79 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type } } - return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection() : null); + return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection() : null); + } + + private static function intersectButNotNever(Type $nativeType, Type $inferredType): Type + { + if ($nativeType->isSuperTypeOf($inferredType)->no()) { + return $nativeType; + } + + $result = TypeCombinator::intersect($nativeType, $inferredType); + if (TypeCombinator::containsNull($nativeType)) { + return TypeCombinator::addNull($result); + } + + return $result; + } + + public function enterMatch(Expr\Match_ $expr): self + { + if ($expr->cond instanceof Variable) { + return $this; + } + if ($expr->cond instanceof AlwaysRememberedExpr) { + $cond = $expr->cond->expr; + } else { + $cond = $expr->cond; + } + + $type = $this->getType($cond); + $nativeType = $this->getNativeType($cond); + $condExpr = new AlwaysRememberedExpr($cond, $type, $nativeType); + $expr->cond = $condExpr; + + return $this->assignExpression($condExpr, $type, $nativeType); } - public function enterForeach(Expr $iteratee, string $valueName, ?string $keyName): self + public function enterForeach(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName): self { - $iterateeType = $this->getType($iteratee); - $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType()); - $scope->nativeExpressionTypes[sprintf('$%s', $valueName)] = $nativeIterateeType->getIterableValueType(); - + $iterateeType = $originalScope->getType($iteratee); + $nativeIterateeType = $originalScope->getNativeType($iteratee); + $scope = $this->assignVariable( + $valueName, + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($nativeIterateeType), + TrinaryLogic::createYes(), + ); if ($keyName !== null) { - $scope = $scope->enterForeachKey($iteratee, $keyName); + $scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName); } return $scope; } - public function enterForeachKey(Expr $iteratee, string $keyName): self + public function enterForeachKey(self $originalScope, Expr $iteratee, string $keyName): self { - $iterateeType = $this->getType($iteratee); - $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($keyName, $iterateeType->getIterableKeyType()); - $scope->nativeExpressionTypes[sprintf('$%s', $keyName)] = $nativeIterateeType->getIterableKeyType(); - - return $scope; - } + $iterateeType = $originalScope->getType($iteratee); + $nativeIterateeType = $originalScope->getNativeType($iteratee); + $scope = $this->assignVariable( + $keyName, + $originalScope->getIterableKeyType($iterateeType), + $originalScope->getIterableKeyType($nativeIterateeType), + TrinaryLogic::createYes(), + ); - /** - * @param Node\Name[] $classes - */ - public function enterCatch(array $classes, ?string $variableName): self - { - $type = TypeCombinator::union(...array_map(static fn (Node\Name $class): ObjectType => new ObjectType((string) $class), $classes)); + if ($iterateeType->isArray()->yes()) { + $scope = $scope->assignExpression( + new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($nativeIterateeType), + ); + } - return $this->enterCatchType($type, $variableName); + return $scope; } public function enterCatchType(Type $catchType, ?string $variableName): self @@ -4011,6 +3974,8 @@ 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)), + TrinaryLogic::createYes(), ); } @@ -4020,24 +3985,29 @@ public function enterExpressionAssign(Expr $expr): self $currentlyAssignedExpressions = $this->currentlyAssignedExpressions; $currentlyAssignedExpressions[$exprString] = true; - return $this->scopeFactory->create( + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); + $scope->resolvedTypes = $this->resolvedTypes; + $scope->truthyScopes = $this->truthyScopes; + $scope->falseyScopes = $this->falseyScopes; + + return $scope; } public function exitExpressionAssign(Expr $expr): self @@ -4046,24 +4016,29 @@ public function exitExpressionAssign(Expr $expr): self $currentlyAssignedExpressions = $this->currentlyAssignedExpressions; unset($currentlyAssignedExpressions[$exprString]); - return $this->scopeFactory->create( + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); + $scope->resolvedTypes = $this->resolvedTypes; + $scope->truthyScopes = $this->truthyScopes; + $scope->falseyScopes = $this->falseyScopes; + + return $scope; } /** @api */ @@ -4073,206 +4048,146 @@ public function isInExpressionAssign(Expr $expr): bool return array_key_exists($exprString, $this->currentlyAssignedExpressions); } - public function assignVariable(string $variableName, Type $type, ?TrinaryLogic $certainty = null): self + public function setAllowedUndefinedExpression(Expr $expr): self { - if ($certainty === null) { - $certainty = TrinaryLogic::createYes(); - } elseif ($certainty->no()) { - throw new ShouldNotHappenException(); + if ($expr instanceof Expr\StaticPropertyFetch) { + return $this; } - $variableTypes = $this->getVariableTypes(); - $variableTypes[$variableName] = new VariableTypeHolder($type, $certainty); - - $nativeTypes = $this->nativeExpressionTypes; - $nativeTypes[sprintf('$%s', $variableName)] = $type; - - $variableString = $this->printer->prettyPrintExpr(new Variable($variableName)); - $moreSpecificTypeHolders = $this->moreSpecificTypes; - foreach (array_keys($moreSpecificTypeHolders) as $key) { - $matches = Strings::matchAll((string) $key, '#\$[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*#'); - - if ($matches === []) { - continue; - } - - $matches = array_column($matches, 0); - - if (!in_array($variableString, $matches, true)) { - continue; - } - unset($moreSpecificTypeHolders[$key]); - } + $exprString = $this->getNodeKey($expr); + $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions; + $currentlyAllowedUndefinedExpressions[$exprString] = true; - $conditionalExpressions = []; - foreach ($this->conditionalExpressions as $exprString => $holders) { - $exprVariableName = '$' . $variableName; - if ($exprString === $exprVariableName) { - continue; - } + $scope = $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->isInFirstLevelStatement(), + $this->currentlyAssignedExpressions, + $currentlyAllowedUndefinedExpressions, + [], + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + $scope->resolvedTypes = $this->resolvedTypes; + $scope->truthyScopes = $this->truthyScopes; + $scope->falseyScopes = $this->falseyScopes; - foreach ($holders as $holder) { - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionExprString) { - if ($conditionExprString === $exprVariableName) { - continue 3; - } - } - } + return $scope; + } - $conditionalExpressions[$exprString] = $holders; - } + public function unsetAllowedUndefinedExpression(Expr $expr): self + { + $exprString = $this->getNodeKey($expr); + $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions; + unset($currentlyAllowedUndefinedExpressions[$exprString]); - return $this->scopeFactory->create( + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $moreSpecificTypeHolders, - $conditionalExpressions, - $this->inClosureBindScopeClass, + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, + $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, - $nativeTypes, - $this->inFunctionCallsStack, + $currentlyAllowedUndefinedExpressions, + [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); + $scope->resolvedTypes = $this->resolvedTypes; + $scope->truthyScopes = $this->truthyScopes; + $scope->falseyScopes = $this->falseyScopes; + + return $scope; + } + + /** @api */ + public function isUndefinedExpressionAllowed(Expr $expr): bool + { + $exprString = $this->getNodeKey($expr); + return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions); + } + + public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty): self + { + $node = new Variable($variableName); + $scope = $this->assignExpression($node, $type, $nativeType); + if ($certainty->no()) { + throw new ShouldNotHappenException(); + } elseif (!$certainty->yes()) { + $exprString = '$' . $variableName; + $scope->expressionTypes[$exprString] = new ExpressionTypeHolder($node, $type, $certainty); + $scope->nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($node, $nativeType, $certainty); + } + + $parameterOriginalValueExprString = $this->getNodeKey(new ParameterVariableOriginalValueExpr($variableName)); + unset($scope->expressionTypes[$parameterOriginalValueExprString]); + unset($scope->nativeExpressionTypes[$parameterOriginalValueExprString]); + + return $scope; } public function unsetExpression(Expr $expr): self { - if ($expr instanceof Variable && is_string($expr->name)) { - if ($this->hasVariableType($expr->name)->no()) { - return $this; - } - $variableTypes = $this->getVariableTypes(); - unset($variableTypes[$expr->name]); - $nativeTypes = $this->nativeExpressionTypes; - - $exprString = sprintf('$%s', $expr->name); - unset($nativeTypes[$exprString]); - - $conditionalExpressions = $this->conditionalExpressions; - unset($conditionalExpressions[$exprString]); - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - [], - $nativeTypes, - [], - $this->afterExtractCall, - $this->parentScope, - ); - } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - return $this->specifyExpressionType( - $expr->var, - $this->getType($expr->var)->unsetOffset($this->getType($expr->dim)), - )->invalidateExpression( + $scope = $this; + if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { + $exprVarType = $scope->getType($expr->var); + $dimType = $scope->getType($expr->dim); + $unsetType = $exprVarType->unsetOffset($dimType); + $exprVarNativeType = $scope->getNativeType($expr->var); + $dimNativeType = $scope->getNativeType($expr->dim); + $unsetNativeType = $exprVarNativeType->unsetOffset($dimNativeType); + $scope = $scope->assignExpression($expr->var, $unsetType, $unsetNativeType)->invalidateExpression( new FuncCall(new FullyQualified('count'), [new Arg($expr->var)]), )->invalidateExpression( new FuncCall(new FullyQualified('sizeof'), [new Arg($expr->var)]), + )->invalidateExpression( + new FuncCall(new Name('count'), [new Arg($expr->var)]), + )->invalidateExpression( + new FuncCall(new Name('sizeof'), [new Arg($expr->var)]), ); + + if ($expr->var instanceof Expr\ArrayDimFetch && $expr->var->dim !== null) { + $scope = $scope->assignExpression( + $expr->var->var, + $this->getType($expr->var->var)->setOffsetValueType( + $scope->getType($expr->var->dim), + $scope->getType($expr->var), + ), + $this->getNativeType($expr->var->var)->setOffsetValueType( + $scope->getNativeType($expr->var->dim), + $scope->getNativeType($expr->var), + ), + ); + } } - return $this; + return $scope->invalidateExpression($expr); } - public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType = null): self + public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, TrinaryLogic $certainty): self { - if ($expr instanceof Node\Scalar || $expr instanceof Array_) { - return $this; - } - if ($expr instanceof ConstFetch) { - $constantTypes = $this->constantTypes; - $constantName = new FullyQualified($expr->name->toString()); - $constantTypes[$constantName->toCodeString()] = $type; - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $constantTypes, - $this->getFunction(), - $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope, - ); - } - - $exprString = $this->getNodeKey($expr); - - $scope = $this; - - if ($expr instanceof Variable && is_string($expr->name)) { - $variableName = $expr->name; - - $variableTypes = $this->getVariableTypes(); - $variableTypes[$variableName] = VariableTypeHolder::createYes($type); - - if ($nativeType === null) { - $nativeType = $type; - } - - $nativeTypes = $this->nativeExpressionTypes; - $exprString = sprintf('$%s', $variableName); - $nativeTypes[$exprString] = $nativeType; - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $nativeTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope, - ); - } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - $constantArrays = TypeUtils::getConstantArrays($this->getType($expr->var)); - if (count($constantArrays) > 0) { - $setArrays = []; - $dimType = $this->getType($expr->dim); - foreach ($constantArrays as $constantArray) { - $setArrays[] = $constantArray->setOffsetValueType($dimType, $type); - } - $scope = $this->specifyExpressionType( - $expr->var, - TypeCombinator::union(...$setArrays), - ); + $loweredConstName = strtolower($expr->name->toString()); + if (in_array($loweredConstName, ['true', 'false', 'null'], true)) { + return $this; } } - if ($expr instanceof FuncCall && $expr->name instanceof Name && $type instanceof ConstantBooleanType && !$type->getValue()) { + if ($expr instanceof FuncCall && $expr->name instanceof Name && $type->isFalse()->yes()) { $functionName = $this->reflectionProvider->resolveFunctionName($expr->name, $this); if ($functionName !== null && in_array(strtolower($functionName), [ 'is_dir', @@ -4283,12 +4198,71 @@ public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType } } - return $scope->addMoreSpecificTypes([ - $exprString => $type, - ]); + $scope = $this; + if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { + $dimType = $scope->getType($expr->dim)->toArrayKey(); + if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { + $exprVarType = $scope->getType($expr->var); + if (!$exprVarType instanceof MixedType && !$exprVarType->isArray()->no()) { + $types = [ + new ArrayType(new MixedType(), new MixedType()), + new ObjectType(ArrayAccess::class), + new NullType(), + ]; + if ($dimType instanceof ConstantIntegerType) { + $types[] = new StringType(); + } + + $scope = $scope->specifyExpressionType( + $expr->var, + TypeCombinator::intersect( + TypeCombinator::intersect($exprVarType, TypeCombinator::union(...$types)), + new HasOffsetValueType($dimType, $type), + ), + $scope->getNativeType($expr->var), + $certainty, + ); + } + } + } + + if ($certainty->no()) { + throw new ShouldNotHappenException(); + } + + $exprString = $this->getNodeKey($expr); + $expressionTypes = $scope->expressionTypes; + $expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty); + $nativeTypes = $scope->nativeExpressionTypes; + $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); + + $scope = $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + + if ($expr instanceof AlwaysRememberedExpr) { + return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty); + } + + return $scope; } - public function assignExpression(Expr $expr, Type $type): self + public function assignExpression(Expr $expr, Type $type, Type $nativeType): self { $scope = $this; if ($expr instanceof PropertyFetch) { @@ -4296,60 +4270,75 @@ public function assignExpression(Expr $expr, Type $type): self ->invalidateMethodsOnExpression($expr->var); } elseif ($expr instanceof Expr\StaticPropertyFetch) { $scope = $this->invalidateExpression($expr); + } elseif ($expr instanceof Variable) { + $scope = $this->invalidateExpression($expr); + } + + return $scope->specifyExpressionType($expr, $type, $nativeType, TrinaryLogic::createYes()); + } + + public function assignInitializedProperty(Type $fetchedOnType, string $propertyName): self + { + if (!$this->isInClass()) { + return $this; + } + + if (TypeUtils::findThisType($fetchedOnType) === null) { + return $this; + } + + $propertyReflection = $this->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { + return $this; + } + $declaringClass = $propertyReflection->getDeclaringClass(); + if ($this->getClassReflection()->getName() !== $declaringClass->getName()) { + return $this; + } + if (!$declaringClass->hasNativeProperty($propertyName)) { + return $this; } - return $scope->specifyExpressionType($expr, $type); + return $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType()); } public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false): self { - $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - $expressionToInvalidateClass = get_class($expressionToInvalidate); - $moreSpecificTypeHolders = $this->moreSpecificTypes; + $expressionTypes = $this->expressionTypes; $nativeExpressionTypes = $this->nativeExpressionTypes; $invalidated = false; - $nodeFinder = new NodeFinder(); - foreach (array_keys($moreSpecificTypeHolders) as $exprString) { - $exprString = (string) $exprString; + $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - try { - $expr = $this->parser->parseString(' $exprTypeHolder) { + $exprExpr = $exprTypeHolder->getExpr(); + if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $requireMoreCharacters)) { continue; } - if (!$expr instanceof Node\Stmt\Expression) { - throw new ShouldNotHappenException(); - } - - $exprExpr = $expr->expr; - if ($exprExpr instanceof PropertyFetch) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($exprExpr, $this); - if ($propertyReflection !== null) { - $nativePropertyReflection = $propertyReflection->getNativeReflection(); - if ($nativePropertyReflection !== null && $nativePropertyReflection->isReadOnly()) { - continue; - } - } - } - $found = $nodeFinder->findFirst([$exprExpr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool { - if (!$node instanceof $expressionToInvalidateClass) { - return false; - } + unset($expressionTypes[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } - return $this->getNodeKey($node) === $exprStringToInvalidate; - }); - if ($found === null) { + $newConditionalExpressions = []; + foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { + if (count($holders) === 0) { continue; } - - if ($requireMoreCharacters && $exprString === $exprStringToInvalidate) { + if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $holders[array_key_first($holders)]->getTypeHolder()->getExpr())) { + $invalidated = true; continue; } - - unset($moreSpecificTypeHolders[$exprString]); - unset($nativeExpressionTypes[$exprString]); - $invalidated = true; + foreach ($holders as $holder) { + $conditionalTypeHolders = $holder->getConditionExpressionTypeHolders(); + foreach ($conditionalTypeHolders as $conditionalTypeHolder) { + if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $conditionalTypeHolder->getExpr())) { + $invalidated = true; + continue 3; + } + } + } + $newConditionalExpressions[$conditionalExprString] = $holders; } if (!$invalidated) { @@ -4359,42 +4348,84 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $expressionTypes, + $nativeExpressionTypes, + $newConditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - $nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } - public function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self + private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, bool $requireMoreCharacters = false): bool { - $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - $moreSpecificTypeHolders = $this->moreSpecificTypes; - $nativeExpressionTypes = $this->nativeExpressionTypes; - $invalidated = false; + if ($requireMoreCharacters && $exprStringToInvalidate === $this->getNodeKey($expr)) { + return false; + } + + // Variables will not contain traversable expressions. skip the NodeFinder overhead + if ($expr instanceof Variable && is_string($expr->name) && !$requireMoreCharacters) { + return $exprStringToInvalidate === $this->getNodeKey($expr); + } + $nodeFinder = new NodeFinder(); - foreach (array_keys($moreSpecificTypeHolders) as $exprString) { - $exprString = (string) $exprString; + $expressionToInvalidateClass = get_class($exprToInvalidate); + $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool { + if ( + $exprStringToInvalidate === '$this' + && $node instanceof Name + && ( + in_array($node->toLowerString(), ['self', 'static', 'parent'], true) + || ($this->getClassReflection() !== null && $this->getClassReflection()->is($this->resolveName($node))) + ) + ) { + return true; + } - try { - $expr = $this->parser->parseString('getNodeKey($node); + + return $nodeString === $exprStringToInvalidate; + }); + + if ($found === null) { + return false; + } + + if ($this->phpVersion->supportsReadOnlyProperties() && $expr instanceof PropertyFetch && $expr->name instanceof Node\Identifier && $requireMoreCharacters) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + if ($propertyReflection !== null) { + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection !== null && $nativePropertyReflection->isReadOnly()) { + return false; + } } - $found = $nodeFinder->findFirst([$expr->expr], function (Node $node) use ($exprStringToInvalidate): bool { + } + + return true; + } + + private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self + { + $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + $invalidated = false; + $nodeFinder = new NodeFinder(); + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + $expr = $exprTypeHolder->getExpr(); + $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($exprStringToInvalidate): bool { if (!$node instanceof MethodCall) { return false; } @@ -4405,7 +4436,7 @@ public function invalidateMethodsOnExpression(Expr $expressionToInvalidate): sel continue; } - unset($moreSpecificTypeHolders[$exprString]); + unset($expressionTypes[$exprString]); unset($nativeExpressionTypes[$exprString]); $invalidated = true; } @@ -4417,44 +4448,77 @@ public function invalidateMethodsOnExpression(Expr $expressionToInvalidate): sel return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - $nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, + ); + } + + private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): self + { + if ($this->hasExpressionType($expr)->no()) { + throw new ShouldNotHappenException(); + } + + $originalExprType = $this->getType($expr); + $nativeType = $this->getNativeType($expr); + + return $this->specifyExpressionType( + $expr, + $originalExprType, + $nativeType, + $certainty, + ); + } + + public function addTypeToExpression(Expr $expr, Type $type): self + { + $originalExprType = $this->getType($expr); + $nativeType = $this->getNativeType($expr); + + if ($originalExprType->equals($nativeType)) { + $newType = TypeCombinator::intersect($type, $originalExprType); + if ($newType->isConstantScalarValue()->yes() && $newType->equals($originalExprType)) { + // don't add the same type over and over again to improve performance + return $this; + } + return $this->specifyExpressionType($expr, $newType, $newType, TrinaryLogic::createYes()); + } + + return $this->specifyExpressionType( + $expr, + TypeCombinator::intersect($type, $originalExprType), + TypeCombinator::intersect($type, $nativeType), + TrinaryLogic::createYes(), ); } public function removeTypeFromExpression(Expr $expr, Type $typeToRemove): self { $exprType = $this->getType($expr); - $typeAfterRemove = TypeCombinator::remove($exprType, $typeToRemove); if ( - !$expr instanceof Variable - && $exprType->equals($typeAfterRemove) - && !$exprType instanceof ErrorType - && !$exprType instanceof NeverType + $exprType instanceof NeverType || + $typeToRemove instanceof NeverType ) { return $this; } - $scope = $this->specifyExpressionType( + return $this->specifyExpressionType( $expr, - $typeAfterRemove, + TypeCombinator::remove($exprType, $typeToRemove), + TypeCombinator::remove($this->getNativeType($expr), $typeToRemove), + TrinaryLogic::createYes(), ); - if ($expr instanceof Variable && is_string($expr->name)) { - $scope->nativeExpressionTypes[sprintf('$%s', $expr->name)] = TypeCombinator::remove($this->getNativeType($expr), $typeToRemove); - } - - return $scope; } /** @@ -4497,157 +4561,119 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self { $typeSpecifications = []; foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } $typeSpecifications[] = [ 'sure' => true, - 'exprString' => $exprString, + 'exprString' => (string) $exprString, 'expr' => $expr, 'type' => $type, ]; } foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } $typeSpecifications[] = [ 'sure' => false, - 'exprString' => $exprString, + 'exprString' => (string) $exprString, 'expr' => $expr, 'type' => $type, ]; } usort($typeSpecifications, static function (array $a, array $b): int { - // @phpstan-ignore-next-line - $length = strlen((string) $a['exprString']) - strlen((string) $b['exprString']); + $length = strlen($a['exprString']) - strlen($b['exprString']); if ($length !== 0) { return $length; } - return $b['sure'] - $a['sure']; // @phpstan-ignore-line + return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric }); $scope = $this; - $typeGuards = []; - $skipVariables = []; - $saveConditionalVariables = []; + $specifiedExpressions = []; foreach ($typeSpecifications as $typeSpecification) { $expr = $typeSpecification['expr']; $type = $typeSpecification['type']; - $originalExprType = $this->getType($expr); - if ($typeSpecification['sure']) { - $scope = $scope->specifyExpressionType($expr, $specifiedTypes->shouldOverwrite() ? $type : TypeCombinator::intersect($type, $originalExprType)); - if ($expr instanceof Variable && is_string($expr->name)) { - $scope->nativeExpressionTypes[sprintf('$%s', $expr->name)] = $specifiedTypes->shouldOverwrite() ? $type : TypeCombinator::intersect($type, $this->getNativeType($expr)); - } - } else { - $scope = $scope->removeTypeFromExpression($expr, $type); - } + if ($expr instanceof IssetExpr) { + $issetExpr = $expr; + $expr = $issetExpr->getExpr(); - if ( - !$expr instanceof Variable - || !is_string($expr->name) - || $specifiedTypes->shouldOverwrite() - ) { - // @phpstan-ignore-next-line - $match = Strings::match((string) $typeSpecification['exprString'], '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); - if ($match !== null) { - $skipVariables[$match[1]] = true; + if ($typeSpecification['sure']) { + $scope = $scope->setExpressionCertainty( + $expr, + TrinaryLogic::createMaybe(), + ); + } else { + $scope = $scope->unsetExpression($expr); } - continue; - } - if ($scope->hasVariableType($expr->name)->no()) { continue; } - $saveConditionalVariables[$expr->name] = $scope->getVariableType($expr->name); - } - - foreach ($saveConditionalVariables as $variableName => $typeGuard) { - if (array_key_exists($variableName, $skipVariables)) { - continue; + if ($typeSpecification['sure']) { + if ($specifiedTypes->shouldOverwrite()) { + $scope = $scope->assignExpression($expr, $type, $type); + } else { + $scope = $scope->addTypeToExpression($expr, $type); + } + } else { + $scope = $scope->removeTypeFromExpression($expr, $type); } - - $typeGuards['$' . $variableName] = $typeGuard; + $specifiedExpressions[$this->getNodeKey($expr)] = ExpressionTypeHolder::createYes($expr, $scope->getType($expr)); } - $newConditionalExpressions = $specifiedTypes->getNewConditionalExpressionHolders(); - foreach ($this->conditionalExpressions as $variableExprString => $conditionalExpressions) { - if (array_key_exists($variableExprString, $typeGuards)) { - continue; - } - - $typeHolder = null; - - $variableName = substr($variableExprString, 1); + $conditions = []; + foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) { foreach ($conditionalExpressions as $conditionalExpression) { - $matchingConditions = []; - foreach ($conditionalExpression->getConditionExpressionTypes() as $conditionExprString => $conditionalType) { - if (!array_key_exists($conditionExprString, $typeGuards)) { - continue; - } - - if (!$typeGuards[$conditionExprString]->equals($conditionalType)) { + foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { continue 2; } - - $matchingConditions[$conditionExprString] = $conditionalType; - } - - if (count($matchingConditions) === 0) { - $newConditionalExpressions[$variableExprString][$conditionalExpression->getKey()] = $conditionalExpression; - continue; - } - - if (count($matchingConditions) < count($conditionalExpression->getConditionExpressionTypes())) { - $filteredConditions = $conditionalExpression->getConditionExpressionTypes(); - foreach (array_keys($matchingConditions) as $conditionExprString) { - unset($filteredConditions[$conditionExprString]); - } - - $holder = new ConditionalExpressionHolder($filteredConditions, $conditionalExpression->getTypeHolder()); - $newConditionalExpressions[$variableExprString][$holder->getKey()] = $holder; - continue; } - $typeHolder = $conditionalExpression->getTypeHolder(); - break; - } - - if ($typeHolder === null) { - continue; + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); } + } - if ($typeHolder->getCertainty()->no()) { - unset($scope->variableTypes[$variableName]); + foreach ($conditions as $conditionalExprString => $expressions) { + $certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty()); + if ($certainty->no()) { + unset($scope->expressionTypes[$conditionalExprString]); } else { - $scope->variableTypes[$variableName] = $typeHolder; + $type = TypeCombinator::intersect(...array_map(static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getType(), $expressions)); + + $scope->expressionTypes[$conditionalExprString] = array_key_exists($conditionalExprString, $scope->expressionTypes) + ? new ExpressionTypeHolder( + $scope->expressionTypes[$conditionalExprString]->getExpr(), + TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $type), + TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $certainty), + ) + : $expressions[0]->getTypeHolder(); } } - return $scope->changeConditionalExpressions($newConditionalExpressions); - } - - /** - * @param array $newConditionalExpressionHolders - */ - public function changeConditionalExpressions(array $newConditionalExpressionHolders): self - { - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $this->variableTypes, - $this->moreSpecificTypes, - $newConditionalExpressionHolders, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope, + return $scope->scopeFactory->create( + $scope->context, + $scope->isDeclareStrictTypes(), + $scope->getFunction(), + $scope->getNamespace(), + $scope->expressionTypes, + $scope->nativeExpressionTypes, + array_merge($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), + $scope->inClosureBindScopeClasses, + $scope->anonymousFunctionReflection, + $scope->inFirstLevelStatement, + $scope->currentlyAssignedExpressions, + $scope->currentlyAllowedUndefinedExpressions, + $scope->inFunctionCallsStack, + $scope->afterExtractCall, + $scope->parentScope, + $scope->nativeTypesPromoted, ); } @@ -4661,43 +4687,57 @@ public function addConditionalExpressions(string $exprString, array $conditional return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->variableTypes, - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } public function exitFirstLevelStatements(): self { - return $this->scopeFactory->create( + if (!$this->inFirstLevelStatement) { + return $this; + } + + if ($this->scopeOutOfFirstLevelStatement !== null) { + return $this->scopeOutOfFirstLevelStatement; + } + + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, false, $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, + $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); + $scope->resolvedTypes = $this->resolvedTypes; + $scope->truthyScopes = $this->truthyScopes; + $scope->falseyScopes = $this->falseyScopes; + $this->scopeOutOfFirstLevelStatement = $scope; + + return $scope; } /** @api */ @@ -4706,105 +4746,45 @@ public function isInFirstLevelStatement(): bool return $this->inFirstLevelStatement; } - /** - * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod - * @param Type[] $types - */ - private function addMoreSpecificTypes(array $types): self - { - $moreSpecificTypeHolders = $this->moreSpecificTypes; - foreach ($types as $exprString => $type) { - $moreSpecificTypeHolders[$exprString] = VariableTypeHolder::createYes($type); - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - [], - $this->afterExtractCall, - $this->parentScope, - ); - } - public function mergeWith(?self $otherScope): self { if ($otherScope === null) { return $this; } + $ourExpressionTypes = $this->expressionTypes; + $theirExpressionTypes = $otherScope->expressionTypes; - $variableHolderToType = static fn (VariableTypeHolder $holder): Type => $holder->getType(); - $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); - - $filterVariableHolders = static fn (VariableTypeHolder $holder): bool => $holder->getCertainty()->yes(); - - $ourVariableTypes = $this->getVariableTypes(); - $theirVariableTypes = $otherScope->getVariableTypes(); - if ($this->canAnyVariableExist()) { - foreach (array_keys($theirVariableTypes) as $name) { - if (array_key_exists($name, $ourVariableTypes)) { - continue; - } - - $ourVariableTypes[$name] = VariableTypeHolder::createMaybe(new MixedType()); - } - - foreach (array_keys($ourVariableTypes) as $name) { - if (array_key_exists($name, $theirVariableTypes)) { - continue; - } - - $theirVariableTypes[$name] = VariableTypeHolder::createMaybe(new MixedType()); - } - } - - $mergedVariableHolders = $this->mergeVariableHolders($ourVariableTypes, $theirVariableTypes); + $mergedExpressionTypes = $this->mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes); $conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions); $conditionalExpressions = $this->createConditionalExpressions( $conditionalExpressions, - $ourVariableTypes, - $theirVariableTypes, - $mergedVariableHolders, + $ourExpressionTypes, + $theirExpressionTypes, + $mergedExpressionTypes, ); $conditionalExpressions = $this->createConditionalExpressions( $conditionalExpressions, - $theirVariableTypes, - $ourVariableTypes, - $mergedVariableHolders, + $theirExpressionTypes, + $ourExpressionTypes, + $mergedExpressionTypes, ); return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, array_filter($this->mergeVariableHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes), - ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), - $mergedVariableHolders, - $this->mergeVariableHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes), + $mergedExpressionTypes, + $this->mergeVariableHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes), $conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - array_map($variableHolderToType, array_filter($this->mergeVariableHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes), - ), $filterVariableHolders)), + [], [], $this->afterExtractCall && $otherScope->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -4835,59 +4815,58 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi /** * @param array $conditionalExpressions - * @param array $variableTypes - * @param array $theirVariableTypes - * @param array $mergedVariableHolders + * @param array $ourExpressionTypes + * @param array $theirExpressionTypes + * @param array $mergedExpressionTypes * @return array */ private function createConditionalExpressions( array $conditionalExpressions, - array $variableTypes, - array $theirVariableTypes, - array $mergedVariableHolders, + array $ourExpressionTypes, + array $theirExpressionTypes, + array $mergedExpressionTypes, ): array { - $newVariableTypes = $variableTypes; - foreach ($theirVariableTypes as $name => $holder) { - if (!array_key_exists($name, $mergedVariableHolders)) { + $newVariableTypes = $ourExpressionTypes; + foreach ($theirExpressionTypes as $exprString => $holder) { + if (!array_key_exists($exprString, $mergedExpressionTypes)) { continue; } - if (!$mergedVariableHolders[$name]->getType()->equals($holder->getType())) { + if (!$mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) { continue; } - unset($newVariableTypes[$name]); + unset($newVariableTypes[$exprString]); } $typeGuards = []; - foreach ($newVariableTypes as $name => $holder) { + foreach ($newVariableTypes as $exprString => $holder) { if (!$holder->getCertainty()->yes()) { continue; } - if (!array_key_exists($name, $mergedVariableHolders)) { + if (!array_key_exists($exprString, $mergedExpressionTypes)) { continue; } - if ($mergedVariableHolders[$name]->getType()->equals($holder->getType())) { + if ($mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) { continue; } - $typeGuards['$' . $name] = $holder->getType(); + $typeGuards[$exprString] = $holder; } if (count($typeGuards) === 0) { return $conditionalExpressions; } - foreach ($newVariableTypes as $name => $holder) { + foreach ($newVariableTypes as $exprString => $holder) { if ( - array_key_exists($name, $mergedVariableHolders) - && $mergedVariableHolders[$name]->equals($holder) + array_key_exists($exprString, $mergedExpressionTypes) + && $mergedExpressionTypes[$exprString]->equals($holder) ) { continue; } - $exprString = '$' . $name; $variableTypeGuards = $typeGuards; unset($variableTypeGuards[$exprString]); @@ -4899,92 +4878,125 @@ private function createConditionalExpressions( $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; } - foreach (array_keys($mergedVariableHolders) as $name) { - if (array_key_exists($name, $variableTypes)) { + foreach ($mergedExpressionTypes as $exprString => $mergedExprTypeHolder) { + if (array_key_exists($exprString, $ourExpressionTypes)) { continue; } - $conditionalExpression = new ConditionalExpressionHolder($typeGuards, new VariableTypeHolder(new ErrorType(), TrinaryLogic::createNo())); - $conditionalExpressions['$' . $name][$conditionalExpression->getKey()] = $conditionalExpression; + $conditionalExpression = new ConditionalExpressionHolder($typeGuards, new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo())); + $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; } return $conditionalExpressions; } /** - * @param VariableTypeHolder[] $ourVariableTypeHolders - * @param VariableTypeHolder[] $theirVariableTypeHolders - * @return VariableTypeHolder[] + * @param array $ourVariableTypeHolders + * @param array $theirVariableTypeHolders + * @return array */ private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array { $intersectedVariableTypeHolders = []; - foreach ($ourVariableTypeHolders as $name => $variableTypeHolder) { - if (isset($theirVariableTypeHolders[$name])) { - $intersectedVariableTypeHolders[$name] = $variableTypeHolder->and($theirVariableTypeHolders[$name]); + $globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isGlobalVariable($node->name); + $nodeFinder = new NodeFinder(); + foreach ($ourVariableTypeHolders as $exprString => $variableTypeHolder) { + if (isset($theirVariableTypeHolders[$exprString])) { + if ($variableTypeHolder === $theirVariableTypeHolders[$exprString]) { + $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder; + continue; + } + + $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder->and($theirVariableTypeHolders[$exprString]); } else { - $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $expr = $variableTypeHolder->getExpr(); + if ($nodeFinder->findFirst($expr, $globalVariableCallback) !== null) { + continue; + } + + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); } } - foreach ($theirVariableTypeHolders as $name => $variableTypeHolder) { - if (isset($intersectedVariableTypeHolders[$name])) { + foreach ($theirVariableTypeHolders as $exprString => $variableTypeHolder) { + if (isset($intersectedVariableTypeHolders[$exprString])) { continue; } - $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $expr = $variableTypeHolder->getExpr(); + if ($nodeFinder->findFirst($expr, $globalVariableCallback) !== null) { + continue; + } + + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); } return $intersectedVariableTypeHolders; } - public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self + public function mergeInitializedProperties(self $calledMethodScope): self { - $variableHolderToType = static fn (VariableTypeHolder $holder): Type => $holder->getType(); - $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); - $filterVariableHolders = static fn (VariableTypeHolder $holder): bool => $holder->getCertainty()->yes(); + $scope = $this; + foreach ($calledMethodScope->expressionTypes as $exprString => $typeHolder) { + $exprString = (string) $exprString; + if (!str_starts_with($exprString, '__phpstanPropertyInitialization(')) { + continue; + } + $propertyName = substr($exprString, strlen('__phpstanPropertyInitialization('), -1); + $propertyExpr = new PropertyInitializationExpr($propertyName); + if (!array_key_exists($exprString, $scope->expressionTypes)) { + $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType()); + $scope->expressionTypes[$exprString] = $typeHolder; + continue; + } + + $certainty = $scope->expressionTypes[$exprString]->getCertainty(); + $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType()); + $scope->expressionTypes[$exprString] = new ExpressionTypeHolder( + $typeHolder->getExpr(), + $typeHolder->getType(), + $typeHolder->getCertainty()->or($certainty), + ); + } + + return $scope; + } + public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self + { return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, array_filter($this->processFinallyScopeVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $finallyScope->constantTypes), - array_map($typeToVariableHolder, $originalFinallyScope->constantTypes), - ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), $this->processFinallyScopeVariableTypeHolders( - $this->getVariableTypes(), - $finallyScope->getVariableTypes(), - $originalFinallyScope->getVariableTypes(), + $this->expressionTypes, + $finallyScope->expressionTypes, + $originalFinallyScope->expressionTypes, ), $this->processFinallyScopeVariableTypeHolders( - $this->moreSpecificTypes, - $finallyScope->moreSpecificTypes, - $originalFinallyScope->moreSpecificTypes, + $this->nativeExpressionTypes, + $finallyScope->nativeExpressionTypes, + $originalFinallyScope->nativeExpressionTypes, ), $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - array_map($variableHolderToType, array_filter($this->processFinallyScopeVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $finallyScope->nativeExpressionTypes), - array_map($typeToVariableHolder, $originalFinallyScope->nativeExpressionTypes), - ), $filterVariableHolders)), + [], [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } /** - * @param VariableTypeHolder[] $ourVariableTypeHolders - * @param VariableTypeHolder[] $finallyVariableTypeHolders - * @param VariableTypeHolder[] $originalVariableTypeHolders - * @return VariableTypeHolder[] + * @param array $ourVariableTypeHolders + * @param array $finallyVariableTypeHolders + * @param array $originalVariableTypeHolders + * @return array */ private function processFinallyScopeVariableTypeHolders( array $ourVariableTypeHolders, @@ -4992,27 +5004,27 @@ private function processFinallyScopeVariableTypeHolders( array $originalVariableTypeHolders, ): array { - foreach ($finallyVariableTypeHolders as $name => $variableTypeHolder) { + foreach ($finallyVariableTypeHolders as $exprString => $variableTypeHolder) { if ( - isset($originalVariableTypeHolders[$name]) - && !$originalVariableTypeHolders[$name]->getType()->equals($variableTypeHolder->getType()) + isset($originalVariableTypeHolders[$exprString]) + && !$originalVariableTypeHolders[$exprString]->getType()->equals($variableTypeHolder->getType()) ) { - $ourVariableTypeHolders[$name] = $variableTypeHolder; + $ourVariableTypeHolders[$exprString] = $variableTypeHolder; continue; } - if (isset($originalVariableTypeHolders[$name])) { + if (isset($originalVariableTypeHolders[$exprString])) { continue; } - $ourVariableTypeHolders[$name] = $variableTypeHolder; + $ourVariableTypeHolders[$exprString] = $variableTypeHolder; } return $ourVariableTypeHolders; } /** - * @param Expr\ClosureUse[] $byRefUses + * @param Node\ClosureUse[] $byRefUses */ public function processClosureScope( self $closureScope, @@ -5021,7 +5033,7 @@ public function processClosureScope( ): self { $nativeExpressionTypes = $this->nativeExpressionTypes; - $variableTypes = $this->variableTypes; + $expressionTypes = $this->expressionTypes; if (count($byRefUses) === 0) { return $this; } @@ -5032,10 +5044,12 @@ public function processClosureScope( } $variableName = $use->var->name; + $variableExprString = '$' . $variableName; if (!$closureScope->hasVariableType($variableName)->yes()) { - $variableTypes[$variableName] = VariableTypeHolder::createYes(new NullType()); - $nativeExpressionTypes[sprintf('$%s', $variableName)] = new NullType(); + $holder = ExpressionTypeHolder::createYes($use->var, new NullType()); + $expressionTypes[$variableExprString] = $holder; + $nativeExpressionTypes[$variableExprString] = $holder; continue; } @@ -5045,144 +5059,132 @@ public function processClosureScope( $prevVariableType = $prevScope->getVariableType($variableName); if (!$variableType->equals($prevVariableType)) { $variableType = TypeCombinator::union($variableType, $prevVariableType); - $variableType = self::generalizeType($variableType, $prevVariableType); + $variableType = $this->generalizeType($variableType, $prevVariableType, 0); } } - $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType); - $nativeExpressionTypes[sprintf('$%s', $variableName)] = $variableType; + $expressionTypes[$variableExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); + $nativeExpressionTypes[$variableExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - $nativeExpressionTypes, + [], $this->inFunctionCallsStack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope): self { - $variableTypeHolders = $this->variableTypes; - $nativeTypes = $this->nativeExpressionTypes; - foreach ($finalScope->variableTypes as $name => $variableTypeHolder) { - $nativeTypes[sprintf('$%s', $name)] = $variableTypeHolder->getType(); - if (!isset($variableTypeHolders[$name])) { - $variableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $expressionTypes = $this->expressionTypes; + foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($expressionTypes[$variableExprString])) { + $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); continue; } - $variableTypeHolders[$name] = new VariableTypeHolder( + $expressionTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($variableTypeHolders[$name]->getCertainty()), + $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()), ); } - - $moreSpecificTypes = $this->moreSpecificTypes; - foreach ($finalScope->moreSpecificTypes as $exprString => $variableTypeHolder) { - if (!isset($moreSpecificTypes[$exprString])) { - $moreSpecificTypes[$exprString] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $nativeTypes = $this->nativeExpressionTypes; + foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($nativeTypes[$variableExprString])) { + $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); continue; } - $moreSpecificTypes[$exprString] = new VariableTypeHolder( + $nativeTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($moreSpecificTypes[$exprString]->getCertainty()), + $variableTypeHolder->getCertainty()->and($nativeTypes[$variableExprString]->getCertainty()), ); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypeHolders, - $moreSpecificTypes, + $expressionTypes, + $nativeTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - $nativeTypes, + [], [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } public function generalizeWith(self $otherScope): self { $variableTypeHolders = $this->generalizeVariableTypeHolders( - $this->getVariableTypes(), - $otherScope->getVariableTypes(), + $this->expressionTypes, + $otherScope->expressionTypes, ); - - $moreSpecificTypes = $this->generalizeVariableTypeHolders( - $this->moreSpecificTypes, - $otherScope->moreSpecificTypes, + $nativeTypes = $this->generalizeVariableTypeHolders( + $this->nativeExpressionTypes, + $otherScope->nativeExpressionTypes, ); - $variableHolderToType = static fn (VariableTypeHolder $holder): Type => $holder->getType(); - $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); - $filterVariableHolders = static fn (VariableTypeHolder $holder): bool => $holder->getCertainty()->yes(); - $nativeTypes = array_map($variableHolderToType, array_filter($this->generalizeVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes), - ), $filterVariableHolders)); - return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, array_filter($this->generalizeVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes), - ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), $variableTypeHolders, - $moreSpecificTypes, + $nativeTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - $nativeTypes, + [], [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } /** - * @param VariableTypeHolder[] $variableTypeHolders - * @param VariableTypeHolder[] $otherVariableTypeHolders - * @return VariableTypeHolder[] + * @param array $variableTypeHolders + * @param array $otherVariableTypeHolders + * @return array */ private function generalizeVariableTypeHolders( array $variableTypeHolders, array $otherVariableTypeHolders, ): array { - foreach ($variableTypeHolders as $name => $variableTypeHolder) { - if (!isset($otherVariableTypeHolders[$name])) { + foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) { + if (!isset($otherVariableTypeHolders[$variableExprString])) { continue; } - $variableTypeHolders[$name] = new VariableTypeHolder( - self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$name]->getType()), + $variableTypeHolders[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), + $this->generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0), $variableTypeHolder->getCertainty(), ); } @@ -5190,7 +5192,7 @@ private function generalizeVariableTypeHolders( return $variableTypeHolders; } - private static function generalizeType(Type $a, Type $b): Type + private function generalizeType(Type $a, Type $b, int $depth): Type { if ($a->equals($b)) { return $a; @@ -5226,7 +5228,7 @@ private static function generalizeType(Type $a, Type $b): Type $constantStrings[$key][] = $type; continue; } - if ($type instanceof ConstantArrayType) { + if ($type->isConstantArray()->yes()) { $constantArrays[$key][] = $type; continue; } @@ -5275,24 +5277,40 @@ private static function generalizeType(Type $a, Type $b): Type } else { $constantArraysA = TypeCombinator::union(...$constantArrays['a']); $constantArraysB = TypeCombinator::union(...$constantArrays['b']); - if ($constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType())) { + if ( + $constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType()) + && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes() + ) { $resultArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach (TypeUtils::flattenTypes($constantArraysA->getIterableKeyType()) as $keyType) { $resultArrayBuilder->setOffsetValueType( $keyType, - self::generalizeType( + $this->generalizeType( $constantArraysA->getOffsetValueType($keyType), $constantArraysB->getOffsetValueType($keyType), + $depth + 1, ), + !$constantArraysA->hasOffsetValueType($keyType)->and($constantArraysB->hasOffsetValueType($keyType))->negate()->no(), ); } $resultTypes[] = $resultArrayBuilder->getArray(); } else { - $resultTypes[] = new ArrayType( - TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType())), - TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType())), + $resultType = new ArrayType( + TypeCombinator::union($this->generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType(), $depth + 1)), + TypeCombinator::union($this->generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1)), ); + if ( + $constantArraysA->isIterableAtLeastOnce()->yes() + && $constantArraysB->isIterableAtLeastOnce()->yes() + && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes() + ) { + $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + if ($constantArraysA->isList()->yes() && $constantArraysB->isList()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType()); + } + $resultTypes[] = $resultType; } } } elseif (count($constantArrays['b']) > 0) { @@ -5308,16 +5326,14 @@ private static function generalizeType(Type $a, Type $b): Type $aValueType = $generalArraysA->getIterableValueType(); $bValueType = $generalArraysB->getIterableValueType(); - $aArrays = TypeUtils::getAnyArrays($aValueType); - $bArrays = TypeUtils::getAnyArrays($bValueType); if ( - count($aArrays) === 1 - && !$aArrays[0] instanceof ConstantArrayType - && count($bArrays) === 1 - && !$bArrays[0] instanceof ConstantArrayType + $aValueType->isArray()->yes() + && $aValueType->isConstantArray()->no() + && $bValueType->isArray()->yes() + && $bValueType->isConstantArray()->no() ) { - $aDepth = self::getArrayDepth($aArrays[0]); - $bDepth = self::getArrayDepth($bArrays[0]); + $aDepth = self::getArrayDepth($aValueType) + $depth; + $bDepth = self::getArrayDepth($bValueType) + $depth; if ( ($aDepth > 2 || $bDepth > 2) && abs($aDepth - $bDepth) > 0 @@ -5327,10 +5343,20 @@ private static function generalizeType(Type $a, Type $b): Type } } - $resultTypes[] = new ArrayType( - TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType())), - TypeCombinator::union(self::generalizeType($aValueType, $bValueType)), + $resultType = new ArrayType( + TypeCombinator::union($this->generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType(), $depth + 1)), + TypeCombinator::union($this->generalizeType($aValueType, $bValueType, $depth + 1)), ); + if ($generalArraysA->isIterableAtLeastOnce()->yes() && $generalArraysB->isIterableAtLeastOnce()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + if ($generalArraysA->isList()->yes() && $generalArraysB->isList()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType()); + } + if ($generalArraysA->isOversizedArray()->yes() && $generalArraysB->isOversizedArray()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new OversizedArrayType()); + } + $resultTypes[] = $resultType; } } elseif (count($generalArrays['b']) > 0) { $resultTypes[] = TypeCombinator::union(...$generalArrays['b']); @@ -5467,20 +5493,25 @@ private static function generalizeType(Type $a, Type $b): Type $resultTypes[] = TypeCombinator::union(...$integerRanges['b']); } - return TypeCombinator::union(...$resultTypes, ...$otherTypes); + $accessoryTypes = array_map( + static fn (Type $type): Type => $type->generalize(GeneralizePrecision::moreSpecific()), + TypeUtils::getAccessoryTypes($a), + ); + + return TypeCombinator::union(TypeCombinator::intersect( + TypeCombinator::union(...$resultTypes, ...$otherTypes), + ...$accessoryTypes, + ), ...$otherTypes); } - private static function getArrayDepth(ArrayType $type): int + private static function getArrayDepth(Type $type): int { $depth = 0; - while ($type instanceof ArrayType) { + $arrays = TypeUtils::toBenevolentUnion($type)->getArrays(); + while (count($arrays) > 0) { $temp = $type->getIterableValueType(); - $arrays = TypeUtils::getAnyArrays($temp); - if (count($arrays) === 1) { - $type = $arrays[0]; - } else { - $type = $temp; - } + $type = $temp; + $arrays = TypeUtils::toBenevolentUnion($type)->getArrays(); $depth++; } @@ -5493,65 +5524,115 @@ public function equals(self $otherScope): bool return false; } - if (!$this->compareVariableTypeHolders($this->variableTypes, $otherScope->variableTypes)) { - return false; - } - - if (!$this->compareVariableTypeHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes)) { + if (!$this->compareVariableTypeHolders($this->expressionTypes, $otherScope->expressionTypes)) { return false; } - - $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); - - $nativeExpressionTypesResult = $this->compareVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes), - ); - - if (!$nativeExpressionTypesResult) { - return false; - } - - return $this->compareVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes), - ); + return $this->compareVariableTypeHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes); } /** - * @param VariableTypeHolder[] $variableTypeHolders - * @param VariableTypeHolder[] $otherVariableTypeHolders + * @param array $variableTypeHolders + * @param array $otherVariableTypeHolders */ private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool { if (count($variableTypeHolders) !== count($otherVariableTypeHolders)) { return false; } - foreach ($variableTypeHolders as $name => $variableTypeHolder) { - if (!isset($otherVariableTypeHolders[$name])) { + foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) { + if (!isset($otherVariableTypeHolders[$variableExprString])) { return false; } - if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$name]->getCertainty())) { + if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$variableExprString]->getCertainty())) { return false; } - if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$name]->getType())) { + if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$variableExprString]->getType())) { return false; } - unset($otherVariableTypeHolders[$name]); + unset($otherVariableTypeHolders[$variableExprString]); } return true; } - /** @api */ + private function getBooleanExpressionDepth(Expr $expr, int $depth = 0): int + { + while ( + $expr instanceof BinaryOp\BooleanOr + || $expr instanceof BinaryOp\LogicalOr + || $expr instanceof BinaryOp\BooleanAnd + || $expr instanceof BinaryOp\LogicalAnd + ) { + return $this->getBooleanExpressionDepth($expr->left, $depth + 1); + } + + return $depth; + } + + /** + * @api + * @deprecated Use canReadProperty() or canWriteProperty() + */ public function canAccessProperty(PropertyReflection $propertyReflection): bool { return $this->canAccessClassMember($propertyReflection); } + /** @api */ + public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool + { + return $this->canAccessClassMember($propertyReflection); + } + + /** @api */ + public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool + { + if (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) { + return $this->canAccessClassMember($propertyReflection); + } + + if (!$this->phpVersion->supportsAsymmetricVisibility()) { + return $this->canAccessClassMember($propertyReflection); + } + + $propertyDeclaringClass = $propertyReflection->getDeclaringClass(); + $canAccessClassMember = static function (ClassReflection $classReflection) use ($propertyReflection, $propertyDeclaringClass) { + if ($propertyReflection->isPrivateSet()) { + return $classReflection->getName() === $propertyDeclaringClass->getName(); + } + + // protected set + + if ( + $classReflection->getName() === $propertyDeclaringClass->getName() + || $classReflection->isSubclassOfClass($propertyDeclaringClass) + ) { + return true; + } + + return $propertyReflection->getDeclaringClass()->isSubclassOfClass($classReflection); + }; + + foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) { + if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) { + continue; + } + + if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) { + return true; + } + } + + if ($this->isInClass()) { + return $canAccessClassMember($this->getClassReflection()); + } + + return false; + } + /** @api */ public function canCallMethod(MethodReflection $methodReflection): bool { @@ -5563,40 +5644,50 @@ public function canCallMethod(MethodReflection $methodReflection): bool } /** @api */ - public function canAccessConstant(ConstantReflection $constantReflection): bool + public function canAccessConstant(ClassConstantReflection $constantReflection): bool { return $this->canAccessClassMember($constantReflection); } - private function canAccessClassMember(ClassMemberReflection $classMemberReflection): bool - { - if ($classMemberReflection->isPublic()) { - return true; - } + private function canAccessClassMember(ClassMemberReflection $classMemberReflection): bool + { + if ($classMemberReflection->isPublic()) { + return true; + } + + $classMemberDeclaringClass = $classMemberReflection->getDeclaringClass(); + $canAccessClassMember = static function (ClassReflection $classReflection) use ($classMemberReflection, $classMemberDeclaringClass) { + if ($classMemberReflection->isPrivate()) { + return $classReflection->getName() === $classMemberDeclaringClass->getName(); + } + + // protected + + if ( + $classReflection->getName() === $classMemberDeclaringClass->getName() + || $classReflection->isSubclassOfClass($classMemberDeclaringClass) + ) { + return true; + } + + return $classMemberReflection->getDeclaringClass()->isSubclassOfClass($classReflection); + }; - if ($this->inClosureBindScopeClass !== null && $this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { - $currentClassReflection = $this->reflectionProvider->getClass($this->inClosureBindScopeClass); - } elseif ($this->isInClass()) { - $currentClassReflection = $this->getClassReflection(); - } else { - return false; - } + foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) { + if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) { + continue; + } - $classReflectionName = $classMemberReflection->getDeclaringClass()->getName(); - if ($classMemberReflection->isPrivate()) { - return $currentClassReflection->getName() === $classReflectionName; + if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) { + return true; + } } - // protected - - if ( - $currentClassReflection->getName() === $classReflectionName - || $currentClassReflection->isSubclassOf($classReflectionName) - ) { - return true; + if ($this->isInClass()) { + return $canAccessClassMember($this->getClassReflection()); } - return $classMemberReflection->getDeclaringClass()->isSubclassOf($currentClassReflection->getName()); + return false; } /** @@ -5605,35 +5696,53 @@ private function canAccessClassMember(ClassMemberReflection $classMemberReflecti public function debug(): array { $descriptions = []; - foreach ($this->getVariableTypes() as $name => $variableTypeHolder) { - $key = sprintf('$%s (%s)', $name, $variableTypeHolder->getCertainty()->describe()); + foreach ($this->expressionTypes as $name => $variableTypeHolder) { + $key = sprintf('%s (%s)', $name, $variableTypeHolder->getCertainty()->describe()); $descriptions[$key] = $variableTypeHolder->getType()->describe(VerbosityLevel::precise()); } - foreach ($this->moreSpecificTypes as $exprString => $typeHolder) { - $key = sprintf( - '%s-specified (%s)', - $exprString, - $typeHolder->getCertainty()->describe(), - ); - $descriptions[$key] = $typeHolder->getType()->describe(VerbosityLevel::precise()); - } - foreach ($this->constantTypes as $name => $type) { - $key = sprintf('const %s', $name); - $descriptions[$key] = $type->describe(VerbosityLevel::precise()); + foreach ($this->nativeExpressionTypes as $exprString => $nativeTypeHolder) { + $key = sprintf('native %s (%s)', $exprString, $nativeTypeHolder->getCertainty()->describe()); + $descriptions[$key] = $nativeTypeHolder->getType()->describe(VerbosityLevel::precise()); } - foreach ($this->nativeExpressionTypes as $exprString => $nativeType) { - $key = sprintf('native %s', $exprString); - $descriptions[$key] = $nativeType->describe(VerbosityLevel::precise()); + + foreach ($this->conditionalExpressions as $exprString => $holders) { + foreach (array_values($holders) as $i => $holder) { + $key = sprintf('condition about %s #%d', $exprString, $i + 1); + $parts = []; + foreach ($holder->getConditionExpressionTypeHolders() as $conditionalExprString => $expressionTypeHolder) { + $parts[] = $conditionalExprString . '=' . $expressionTypeHolder->getType()->describe(VerbosityLevel::precise()); + } + $condition = implode(' && ', $parts); + $descriptions[$key] = sprintf( + 'if %s then %s is %s (%s)', + $condition, + $exprString, + $holder->getTypeHolder()->getType()->describe(VerbosityLevel::precise()), + $holder->getTypeHolder()->getCertainty()->describe(), + ); + } } return $descriptions; } + /** + * @param non-empty-string $className + */ private function exactInstantiation(New_ $node, string $className): ?Type { $resolvedClassName = $this->resolveExactName(new Name($className)); + $isStatic = false; if ($resolvedClassName === null) { - return null; + if (strtolower($className) !== 'static') { + return null; + } + + if (!$this->isInClass()) { + return null; + } + $resolvedClassName = $this->getClassReflection()->getName(); + $isStatic = true; } if (!$this->reflectionProvider->hasClass($resolvedClassName)) { @@ -5641,12 +5750,20 @@ private function exactInstantiation(New_ $node, string $className): ?Type } $classReflection = $this->reflectionProvider->getClass($resolvedClassName); + $nonFinalClassReflection = $classReflection; + if (!$isStatic) { + $classReflection = $classReflection->asFinal(); + } if ($classReflection->hasConstructor()) { $constructorMethod = $classReflection->getConstructor(); } else { $constructorMethod = new DummyConstructorReflection($classReflection); } + if ($constructorMethod->getName() === '') { + throw new ShouldNotHappenException(); + } + $resolvedTypes = []; $methodCall = new Expr\StaticCall( new Name($resolvedClassName), @@ -5654,12 +5771,31 @@ private function exactInstantiation(New_ $node, string $className): ?Type $node->getArgs(), ); - foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($classReflection->getName()) as $dynamicStaticMethodReturnTypeExtension) { - if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($constructorMethod)) { - continue; - } + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $this, + $methodCall->getArgs(), + $constructorMethod->getVariants(), + $constructorMethod->getNamedArgumentsVariants(), + ); + $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); - $resolvedTypes[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($constructorMethod, $methodCall, $this); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($classReflection->getName()) as $dynamicStaticMethodReturnTypeExtension) { + if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($constructorMethod)) { + continue; + } + + $resolvedType = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall( + $constructorMethod, + $normalizedMethodCall, + $this, + ); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; + } } if (count($resolvedTypes) > 0) { @@ -5671,20 +5807,14 @@ private function exactInstantiation(New_ $node, string $className): ?Type return $methodResult; } - $objectType = new ObjectType($resolvedClassName); + $objectType = $isStatic ? new StaticType($classReflection) : new ObjectType($resolvedClassName, null, $classReflection); if (!$classReflection->isGeneric()) { return $objectType; } - $parentNode = $node->getAttribute('parent'); - if ( - ( - $parentNode instanceof Expr\Assign - || $parentNode instanceof Expr\AssignRef - ) - && $parentNode->var instanceof PropertyFetch - ) { - $constructorVariant = ParametersAcceptorSelector::selectSingle($constructorMethod->getVariants()); + $assignedToProperty = $node->getAttribute(NewAssignedToPropertyVisitor::ATTRIBUTE_NAME); + if ($assignedToProperty !== null) { + $constructorVariant = $constructorMethod->getOnlyVariant(); $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); $originalClassTemplateTypes = $classTemplateTypes; foreach ($constructorVariant->getParameters() as $parameter) { @@ -5702,119 +5832,231 @@ private function exactInstantiation(New_ $node, string $className): ?Type } if (count($classTemplateTypes) === count($originalClassTemplateTypes)) { - $propertyType = $this->getType($parentNode->var); - if ($objectType->isSuperTypeOf($propertyType)->yes()) { + $propertyType = TypeCombinator::removeNull($this->getType($assignedToProperty)); + $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, null, $nonFinalClassReflection); + if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { return $propertyType; } } } - if ($constructorMethod instanceof DummyConstructorReflection || $constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { + if ($constructorMethod instanceof DummyConstructorReflection) { + if ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + null, + [], + ); + } + + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $types, + null, + $classReflection->withTypes($types)->asFinal(), ); } - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $this, - $methodCall->getArgs(), - $constructorMethod->getVariants(), - ); + if ($constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$constructorMethod->getDeclaringClass()->isGeneric()) { + if ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + null, + [], + ); + } - if ($this->explicitMixedInUnknownGenericNew) { - return new GenericObjectType( - $resolvedClassName, - $classReflection->typeMapToList($parametersAcceptor->getResolvedTemplateTypeMap()), - ); - } + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); + return new GenericObjectType( + $resolvedClassName, + $types, + null, + $classReflection->withTypes($types)->asFinal(), + ); + } + $newType = new GenericObjectType($resolvedClassName, $classReflection->typeMapToList($classReflection->getTemplateTypeMap())); + $ancestorType = $newType->getAncestorWithClassName($constructorMethod->getDeclaringClass()->getName()); + if ($ancestorType === null) { + if ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + null, + [], + ); + } - $resolvedPhpDoc = $classReflection->getResolvedPhpDoc(); - if ($resolvedPhpDoc === null) { - return $objectType; - } + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); + return new GenericObjectType( + $resolvedClassName, + $types, + null, + $classReflection->withTypes($types)->asFinal(), + ); + } + $ancestorClassReflections = $ancestorType->getObjectClassReflections(); + if (count($ancestorClassReflections) !== 1) { + if ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + null, + [], + ); + } - $list = []; - $typeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); - foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $templateType = $typeMap->getType($tag->getName()); - if ($templateType !== null) { - $list[] = $templateType; - continue; + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); + return new GenericObjectType( + $resolvedClassName, + $types, + null, + $classReflection->withTypes($types)->asFinal(), + ); } - $bound = $tag->getBound(); - if ($bound instanceof MixedType && $bound->isExplicitMixed()) { - $bound = new MixedType(false); + + $newParentNode = new New_(new Name($constructorMethod->getDeclaringClass()->getName()), $node->args); + $newParentType = $this->getType($newParentNode); + $newParentTypeClassReflections = $newParentType->getObjectClassReflections(); + if (count($newParentTypeClassReflections) !== 1) { + if ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + null, + [], + ); + } + + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); + return new GenericObjectType( + $resolvedClassName, + $types, + null, + $classReflection->withTypes($types)->asFinal(), + ); } - $list[] = $bound; - } + $newParentTypeClassReflection = $newParentTypeClassReflections[0]; - return new GenericObjectType( - $resolvedClassName, - $list, - ); - } + $ancestorClassReflection = $ancestorClassReflections[0]; + $ancestorMapping = []; + foreach ($ancestorClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } - private function getTypeToInstantiateForNew(Type $type): Type - { - $decideType = static function (Type $type): ?Type { - if ($type instanceof ConstantStringType) { - return new ObjectType($type->getValue()); + $ancestorMapping[$typeName] = $templateType; } - if ($type instanceof GenericClassStringType) { - return $type->getGenericType(); + + $resolvedTypeMap = []; + foreach ($newParentTypeClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $type) { + if (!array_key_exists($typeName, $ancestorMapping)) { + continue; + } + + $ancestorType = $ancestorMapping[$typeName]; + if (!$ancestorType->getBound()->isSuperTypeOf($type)->yes()) { + continue; + } + + if (!array_key_exists($ancestorType->getName(), $resolvedTypeMap)) { + $resolvedTypeMap[$ancestorType->getName()] = $type; + continue; + } + + $resolvedTypeMap[$ancestorType->getName()] = TypeCombinator::union($resolvedTypeMap[$ancestorType->getName()], $type); } - if ((new ObjectWithoutClassType())->isSuperTypeOf($type)->yes()) { - return $type; + + if ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)), + null, + [], + ); } - return null; - }; - if ($type instanceof UnionType) { - $types = []; - foreach ($type->getTypes() as $innerType) { - $decidedType = $decideType($innerType); - if ($decidedType === null) { - return new ObjectWithoutClassType(); + $types = $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)); + return new GenericObjectType( + $resolvedClassName, + $types, + null, + $classReflection->withTypes($types)->asFinal(), + ); + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $this, + $methodCall->getArgs(), + $constructorMethod->getVariants(), + $constructorMethod->getNamedArgumentsVariants(), + ); + + $resolvedTemplateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()); + $newGenericType = new GenericObjectType( + $resolvedClassName, + $types, + null, + $classReflection->withTypes($types)->asFinal(), + ); + if ($isStatic) { + $newGenericType = new GenericStaticType( + $classReflection, + $types, + null, + [], + ); + } + return TypeTraverser::map($newGenericType, static function (Type $type, callable $traverse) use ($resolvedTemplateTypeMap): Type { + if ($type instanceof TemplateType && !$type->isArgument()) { + $newType = $resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $type->getDefault() ?? $type->getBound(); } - $types[] = $decidedType; + return TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); } - return TypeCombinator::union(...$types); + return $traverse($type); + }); + } + + private function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type + { + if ($typeWithMethod instanceof UnionType) { + $typeWithMethod = $typeWithMethod->filterTypes(static fn (Type $innerType) => $innerType->hasMethod($methodName)->yes()); } - $decidedType = $decideType($type); - if ($decidedType === null) { - return new ObjectWithoutClassType(); + if (!$typeWithMethod->hasMethod($methodName)->yes()) { + return null; } - return $decidedType; + return $typeWithMethod; } /** @api */ - public function getMethodReflection(Type $typeWithMethod, string $methodName): ?MethodReflection + public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection { - if ($typeWithMethod instanceof UnionType) { - $newTypes = []; - foreach ($typeWithMethod->getTypes() as $innerType) { - if (!$innerType->hasMethod($methodName)->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return null; - } - $typeWithMethod = TypeCombinator::union(...$newTypes); + $type = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($type === null) { + return null; } - if (!$typeWithMethod->hasMethod($methodName)->yes()) { + return $type->getMethod($methodName, $this); + } + + public function getNakedMethod(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection + { + $type = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($type === null) { return null; } - return $typeWithMethod->getMethod($methodName, $this); + return $type->getUnresolvedMethodPrototype($methodName, $this)->getNakedMethod(); } /** @@ -5822,20 +6064,41 @@ public function getMethodReflection(Type $typeWithMethod, string $methodName): ? */ private function methodCallReturnType(Type $typeWithMethod, string $methodName, Expr $methodCall): ?Type { - $methodReflection = $this->getMethodReflection($typeWithMethod, $methodName); - if ($methodReflection === null) { + $typeWithMethod = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($typeWithMethod === null) { return null; } + $methodReflection = $typeWithMethod->getMethod($methodName, $this); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $this, + $methodCall->getArgs(), + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), + ); + if ($methodCall instanceof MethodCall) { + $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall); + } else { + $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); + } + if ($normalizedMethodCall === null) { + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall); + } + $resolvedTypes = []; - foreach (TypeUtils::getDirectClassNames($typeWithMethod) as $className) { - if ($methodCall instanceof MethodCall) { + foreach ($typeWithMethod->getObjectClassNames() as $className) { + if ($normalizedMethodCall instanceof MethodCall) { foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicMethodReturnTypeExtensionsForClass($className) as $dynamicMethodReturnTypeExtension) { if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) { continue; } - $resolvedTypes[] = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $methodCall, $this); + $resolvedType = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $normalizedMethodCall, $this); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } } else { foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($className) as $dynamicStaticMethodReturnTypeExtension) { @@ -5843,38 +6106,32 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, continue; } - $resolvedTypes[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($methodReflection, $methodCall, $this); + $resolvedType = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall( + $methodReflection, + $normalizedMethodCall, + $this, + ); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } } } if (count($resolvedTypes) > 0) { - return TypeCombinator::union(...$resolvedTypes); + return $this->transformVoidToNull(TypeCombinator::union(...$resolvedTypes), $methodCall); } - return ParametersAcceptorSelector::selectFromArgs( - $this, - $methodCall->getArgs(), - $methodReflection->getVariants(), - )->getReturnType(); + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall); } /** @api */ - public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?PropertyReflection + public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection { if ($typeWithProperty instanceof UnionType) { - $newTypes = []; - foreach ($typeWithProperty->getTypes() as $innerType) { - if (!$innerType->hasProperty($propertyName)->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return null; - } - $typeWithProperty = TypeCombinator::union(...$newTypes); + $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasProperty($propertyName)->yes()); } if (!$typeWithProperty->hasProperty($propertyName)->yes()) { return null; @@ -5900,188 +6157,104 @@ 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 + public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection { - if ($range instanceof IntegerRangeType) { - $rangeMin = $range->getMin(); - $rangeMax = $range->getMax(); - } else { - $rangeMin = $range->getValue(); - $rangeMax = $rangeMin; + if ($typeWithConstant instanceof UnionType) { + $typeWithConstant = $typeWithConstant->filterTypes(static fn (Type $innerType) => $innerType->hasConstant($constantName)->yes()); } - - 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 (!$typeWithConstant->hasConstant($constantName)->yes()) { + return null; } - 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; + return $typeWithConstant->getConstant($constantName); + } - /** @var int|float|null $max */ - $max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax + $operand->getMax() : null; + /** + * @return array + */ + private function getConstantTypes(): array + { + $constantTypes = []; + foreach ($this->expressionTypes as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + if (!$expr instanceof ConstFetch) { + continue; } - } 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; + $constantTypes[$exprString] = $typeHolder; + } + return $constantTypes; + } - /** @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; - } + private function getGlobalConstantType(Name $name): ?Type + { + $fetches = []; + if (!$name->isFullyQualified() && $this->getNamespace() !== null) { + $fetches[] = new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])); + } - 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; - } + $fetches[] = new ConstFetch(new FullyQualified($name->toString())); + $fetches[] = new ConstFetch($name); - if ($min !== null && $max !== null && $min > $max) { - [$min, $max] = [$max, $min]; - } - } + foreach ($fetches as $constFetch) { + if ($this->hasExpressionType($constFetch)->yes()) { + return $this->getType($constFetch); } - } 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; - } + return null; + } - if ($min !== null && $max !== null && $min > $max) { - [$min, $max] = [$max, $min]; + /** + * @return array + */ + private function getNativeConstantTypes(): array + { + $constantTypes = []; + foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + if (!$expr instanceof ConstFetch) { + continue; } + $constantTypes[$exprString] = $typeHolder; + } + return $constantTypes; + } - // 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]; + public function getIterableKeyType(Type $iteratee): Type + { + if ($iteratee instanceof UnionType) { + $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes()); + if (!$filtered instanceof NeverType) { + $iteratee = $filtered; } + } - } 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; - } + return $iteratee->getIterableKeyType(); + } - 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; - } + public function getIterableValueType(Type $iteratee): Type + { + if ($iteratee instanceof UnionType) { + $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes()); + if (!$filtered instanceof NeverType) { + $iteratee = $filtered; } + } - 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 $iteratee->getIterableValueType(); + } - return TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType()); - } + public function getPhpVersion(): PhpVersions + { + $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID')); + if ($constType !== null) { + return new PhpVersions($constType); } - if (is_float($min)) { - $min = null; + if (is_array($this->configPhpVersion)) { + return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max'])); } - if (is_float($max)) { - $max = null; - } - - return IntegerRangeType::fromInterval($min, $max); + return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); } } diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index 683732b28c..2ce18d91be 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -13,21 +13,25 @@ use function implode; use function ltrim; use function sprintf; -use function strpos; +use function str_starts_with; use function strtolower; -/** @api */ -class NameScope +/** + * @api + */ +final class NameScope { private TemplateTypeMap $templateTypeMap; /** * @api + * @param non-empty-string|null $namespace * @param array $uses alias(string) => fullName(string) + * @param array $constUses alias(string) => fullName(string) * @param array $typeAliasesMap */ - public function __construct(private ?string $namespace, private array $uses, private ?string $className = null, private ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, private array $typeAliasesMap = [], private bool $bypassTypeAliases = false) + public function __construct(private ?string $namespace, private array $uses, private ?string $className = null, private ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, private array $typeAliasesMap = [], private bool $bypassTypeAliases = false, private array $constUses = [], private ?string $typeAliasClassName = null) { $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); } @@ -50,14 +54,27 @@ public function hasUseAlias(string $name): bool return isset($this->uses[strtolower($name)]); } + /** + * @return array + */ + public function getConstUses(): array + { + return $this->constUses; + } + public function getClassName(): ?string { return $this->className; } + public function getClassNameForTypeAlias(): ?string + { + return $this->typeAliasClassName ?? $this->className; + } + public function resolveStringName(string $name): string { - if (strpos($name, '\\') === 0) { + if (str_starts_with($name, '\\')) { return ltrim($name, '\\'); } @@ -78,6 +95,37 @@ public function resolveStringName(string $name): string return $name; } + /** + * @return non-empty-list + */ + public function resolveConstantNames(string $name): array + { + if (str_starts_with($name, '\\')) { + return [ltrim($name, '\\')]; + } + + $nameParts = explode('\\', $name); + $firstNamePart = strtolower($nameParts[0]); + + if (count($nameParts) > 1) { + if (isset($this->uses[$firstNamePart])) { + array_shift($nameParts); + return [sprintf('%s\\%s', $this->uses[$firstNamePart], implode('\\', $nameParts))]; + } + } elseif (isset($this->constUses[$firstNamePart])) { + return [$this->constUses[$firstNamePart]]; + } + + if ($this->namespace !== null) { + return [ + sprintf('%s\\%s', $this->namespace, $name), + $name, + ]; + } + + return [$name]; + } + public function getTemplateTypeScope(): ?TemplateTypeScope { if ($this->className !== null) { @@ -121,6 +169,36 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self $map->getTypes(), )), $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, + ); + } + + public function withoutNamespaceAndUses(): self + { + return new self( + null, + [], + $this->className, + $this->functionName, + $this->templateTypeMap, + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, + ); + } + + public function withClassName(string $className): self + { + return new self( + $this->namespace, + $this->uses, + $className, + $this->functionName, + $this->templateTypeMap, + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, ); } @@ -138,12 +216,14 @@ public function unsetTemplateType(string $name): self $this->functionName, $this->templateTypeMap->unsetType($name), $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, ); } public function bypassTypeAliases(): self { - return new self($this->namespace, $this->uses, $this->className, $this->functionName, $this->templateTypeMap, $this->typeAliasesMap, true); + return new self($this->namespace, $this->uses, $this->className, $this->functionName, $this->templateTypeMap, $this->typeAliasesMap, true, $this->constUses); } public function shouldBypassTypeAliases(): bool @@ -156,19 +236,4 @@ public function hasTypeAlias(string $alias): bool return array_key_exists($alias, $this->typeAliasesMap); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['namespace'], - $properties['uses'], - $properties['className'], - $properties['functionName'], - $properties['templateTypeMap'], - $properties['typeAliasesMap'], - ); - } - } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index ba3be2994d..d979a0f24a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4,12 +4,16 @@ use ArrayAccess; use Closure; +use DivisionByZeroError; use PhpParser\Comment\Doc; +use PhpParser\Modifiers; use PhpParser\Node; +use PhpParser\Node\Arg; +use PhpParser\Node\AttributeGroup; +use PhpParser\Node\ComplexType; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; -use PhpParser\Node\Expr\ArrayItem; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\AssignRef; use PhpParser\Node\Expr\BinaryOp; @@ -17,6 +21,7 @@ use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\Coalesce; use PhpParser\Node\Expr\BooleanNot; +use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Cast; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\ErrorSuppress; @@ -31,6 +36,7 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Expr\Ternary; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Class_; @@ -40,14 +46,17 @@ use PhpParser\Node\Stmt\For_; use PhpParser\Node\Stmt\Foreach_; use PhpParser\Node\Stmt\If_; +use PhpParser\Node\Stmt\InlineHTML; use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Stmt\Static_; -use PhpParser\Node\Stmt\StaticVar; use PhpParser\Node\Stmt\Switch_; -use PhpParser\Node\Stmt\Throw_; use PhpParser\Node\Stmt\TryCatch; use PhpParser\Node\Stmt\Unset_; use PhpParser\Node\Stmt\While_; +use PhpParser\NodeFinder; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\CloningVisitor; +use PhpParser\NodeVisitorAbstract; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Reflector; @@ -55,6 +64,8 @@ use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Node\BooleanAndNode; @@ -69,10 +80,17 @@ use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Node\DoWhileLoopConditionNode; use PHPStan\Node\ExecutionEndNode; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Expr\ExistingArrayDimFetch; +use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; +use PHPStan\Node\Expr\PropertyInitializationExpr; +use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; +use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Node\FinallyExitPointsNode; use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\FunctionReturnStatementsNode; @@ -82,33 +100,66 @@ use PHPStan\Node\InClosureNode; use PHPStan\Node\InForeachNode; use PHPStan\Node\InFunctionNode; +use PHPStan\Node\InPropertyHookNode; use PHPStan\Node\InstantiationCallableNode; +use PHPStan\Node\InTraitNode; +use PHPStan\Node\InvalidateExprNode; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Node\MatchExpressionArm; +use PHPStan\Node\MatchExpressionArmBody; use PHPStan\Node\MatchExpressionArmCondition; use PHPStan\Node\MatchExpressionNode; use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodReturnStatementsNode; +use PHPStan\Node\NoopExpressionNode; use PHPStan\Node\PropertyAssignNode; +use PHPStan\Node\PropertyHookReturnStatementsNode; +use PHPStan\Node\PropertyHookStatementNode; use PHPStan\Node\ReturnStatement; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Node\UnreachableStatementNode; +use PHPStan\Node\VariableAssignNode; +use PHPStan\Node\VarTagChangedExpressionTypeNode; +use PHPStan\Parser\ArrowFunctionArgVisitor; +use PHPStan\Parser\ClosureArgVisitor; +use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; +use PHPStan\Parser\LineAttributesVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\PhpDoc\Tag\VarTag; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; +use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Reflection\Php\PhpMethodReflection; +use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\ClosureType; @@ -116,50 +167,63 @@ use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\ParserNodeTypeToPHPStanType; +use PHPStan\Type\ResourceType; use PHPStan\Type\StaticType; use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; -use PHPStan\Type\VoidType; +use ReflectionProperty; use Throwable; use Traversable; use TypeError; +use UnhandledMatchError; use function array_fill_keys; use function array_filter; use function array_key_exists; +use function array_key_last; +use function array_keys; use function array_map; use function array_merge; use function array_pop; use function array_reverse; use function array_slice; +use function array_values; use function base64_decode; use function count; use function in_array; use function is_array; use function is_int; use function is_string; +use function ksort; use function sprintf; +use function str_starts_with; use function strtolower; use function trim; +use function usort; use const PHP_VERSION_ID; +use const SORT_NUMERIC; -class NodeScopeResolver +final class NodeScopeResolver { private const LOOP_SCOPE_ITERATIONS = 3; @@ -168,29 +232,58 @@ class NodeScopeResolver /** @var bool[] filePath(string) => bool(true) */ private array $analysedFiles = []; + /** @var array */ + private array $earlyTerminatingMethodNames; + + /** @var array */ + private array $calledMethodStack = []; + + /** @var array */ + private array $calledMethodResults = []; + /** * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) * @param array $earlyTerminatingFunctionCalls + * @param string[] $universalObjectCratesClasses */ public function __construct( - private 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, + private readonly ReflectionProvider $reflectionProvider, + private readonly InitializerExprTypeResolver $initializerExprTypeResolver, + private readonly Reflector $reflector, + private readonly ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, + private readonly ParameterOutTypeExtensionProvider $parameterOutTypeExtensionProvider, + private readonly Parser $parser, + private readonly FileTypeMapper $fileTypeMapper, + private readonly StubPhpDocProvider $stubPhpDocProvider, + private readonly PhpVersion $phpVersion, + private readonly SignatureMapProvider $signatureMapProvider, + private readonly DeprecationProvider $deprecationProvider, + private readonly AttributeReflectionFactory $attributeReflectionFactory, + private readonly PhpDocInheritanceResolver $phpDocInheritanceResolver, + private readonly FileHelper $fileHelper, + private readonly TypeSpecifier $typeSpecifier, + private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, + private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider, + private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider, + private readonly ScopeFactory $scopeFactory, + private readonly bool $polluteScopeWithLoopInitialAssignments, + private readonly bool $polluteScopeWithAlwaysIterableForeach, + private readonly bool $polluteScopeWithBlock, + private readonly array $earlyTerminatingMethodCalls, + private readonly array $earlyTerminatingFunctionCalls, + private readonly array $universalObjectCratesClasses, + private readonly bool $implicitThrows, + private readonly bool $treatPhpDocTypesAsCertain, + private readonly bool $narrowMethodScopeFromConstructor, ) { + $earlyTerminatingMethodNames = []; + foreach ($this->earlyTerminatingMethodCalls as $methodNames) { + foreach ($methodNames as $methodName) { + $earlyTerminatingMethodNames[strtolower($methodName)] = true; + } + } + $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames; } /** @@ -213,31 +306,58 @@ public function processNodes( callable $nodeCallback, ): void { - $nodesCount = count($nodes); + $alreadyTerminated = false; foreach ($nodes as $i => $node) { - if (!$node instanceof Node\Stmt) { + if ( + !$node instanceof Node\Stmt + || ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike)) + ) { continue; } - $statementResult = $this->processStmtNode($node, $scope, $nodeCallback); + $statementResult = $this->processStmtNode($node, $scope, $nodeCallback, StatementContext::createTopLevel()); $scope = $statementResult->getScope(); - if (!$statementResult->isAlwaysTerminating()) { + if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { continue; } - if ($i < $nodesCount - 1) { - $nextStmt = $nodes[$i + 1]; - if (!$nextStmt instanceof Node\Stmt) { - continue; - } + $alreadyTerminated = true; + $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $i + 1), true); + $this->processUnreachableStatement($nextStmts, $scope, $nodeCallback); + } + } + + /** + * @param Node\Stmt[] $nextStmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, callable $nodeCallback): void + { + if ($nextStmts === []) { + return; + } + + $unreachableStatement = null; + $nextStatements = []; - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); + foreach ($nextStmts as $key => $nextStmt) { + if ($key === 0) { + $unreachableStatement = $nextStmt; + continue; } - break; + + $nextStatements[] = $nextStmt; } + + if (!$unreachableStatement instanceof Node\Stmt) { + return; + } + + $nodeCallback(new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope); } /** + * @api * @param Node\Stmt[] $stmts * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -246,65 +366,91 @@ public function processStmtNodes( array $stmts, MutatingScope $scope, callable $nodeCallback, + StatementContext $context, ): StatementResult { $exitPoints = []; $throwPoints = []; + $impurePoints = []; $alreadyTerminated = false; $hasYield = false; $stmtCount = count($stmts); $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_ || $parentNode instanceof Node\Stmt\ClassMethod + || $parentNode instanceof PropertyHookStatementNode || $parentNode instanceof Expr\Closure; foreach ($stmts as $i => $stmt) { + if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike)) { + continue; + } + $isLast = $i === $stmtCount - 1; $statementResult = $this->processStmtNode( $stmt, $scope, $nodeCallback, + $context, ); $scope = $statementResult->getScope(); $hasYield = $hasYield || $statementResult->hasYield(); if ($shouldCheckLastStatement && $isLast) { - /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */ - $parentNode = $parentNode; - $nodeCallback(new ExecutionEndNode( - $stmt, - new StatementResult( - $scope, - $hasYield, - $statementResult->isAlwaysTerminating(), - $statementResult->getExitPoints(), - $statementResult->getThrowPoints(), - ), - $parentNode->returnType !== null, - ), $scope); + $endStatements = $statementResult->getEndStatements(); + if (count($endStatements) > 0) { + foreach ($endStatements as $endStatement) { + $endStatementResult = $endStatement->getResult(); + $nodeCallback(new ExecutionEndNode( + $endStatement->getStatement(), + new StatementResult( + $endStatementResult->getScope(), + $hasYield, + $endStatementResult->isAlwaysTerminating(), + $endStatementResult->getExitPoints(), + $endStatementResult->getThrowPoints(), + $endStatementResult->getImpurePoints(), + ), + $parentNode->getReturnType() !== null, + ), $endStatementResult->getScope()); + } + } else { + $nodeCallback(new ExecutionEndNode( + $stmt, + new StatementResult( + $scope, + $hasYield, + $statementResult->isAlwaysTerminating(), + $statementResult->getExitPoints(), + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + ), + $parentNode->getReturnType() !== null, + ), $scope); + } } $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints()); - if (!$statementResult->isAlwaysTerminating()) { + if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { continue; } $alreadyTerminated = true; - if ($i < $stmtCount - 1) { - $nextStmt = $stmts[$i + 1]; - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); - } - break; + $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); + $this->processUnreachableStatement($nextStmts, $scope, $nodeCallback); } - $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints); + $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints); if ($stmtCount === 0 && $shouldCheckLastStatement) { - /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */ - $parentNode = $parentNode; + $returnTypeNode = $parentNode->getReturnType(); + if ($parentNode instanceof Expr\Closure) { + $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); + } $nodeCallback(new ExecutionEndNode( $parentNode, $statementResult, - $parentNode->returnType !== null, + $returnTypeNode !== null, ), $scope); } @@ -318,21 +464,18 @@ private function processStmtNode( Node\Stmt $stmt, MutatingScope $scope, callable $nodeCallback, + StatementContext $context, ): StatementResult { if ( - $stmt instanceof Throw_ - || $stmt instanceof Return_ - ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr); - } elseif ( !$stmt instanceof Static_ && !$stmt instanceof Foreach_ && !$stmt instanceof Node\Stmt\Global_ && !$stmt instanceof Node\Stmt\Property - && !$stmt instanceof Node\Stmt\PropertyProperty + && !$stmt instanceof Node\Stmt\ClassConst + && !$stmt instanceof Node\Stmt\Const_ ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, null); + $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback); } if ($stmt instanceof Node\Stmt\ClassMethod) { @@ -345,30 +488,41 @@ private function processStmtNode( ) { $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString()); if ($methodReflection instanceof NativeMethodReflection) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } if ($methodReflection instanceof PhpMethodReflection) { $declaringTrait = $methodReflection->getDeclaringTrait(); if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } } } } - $nodeCallback($stmt, $scope); + $stmtScope = $scope; + if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\Throw_) { + $stmtScope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr->expr, $nodeCallback); + } + if ($stmt instanceof Return_) { + $stmtScope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr, $nodeCallback); + } + + $nodeCallback($stmt, $stmtScope); $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope); if ($stmt instanceof Node\Stmt\Declare_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; + $alwaysTerminating = false; + $exitPoints = []; foreach ($stmt->declares as $declare) { $nodeCallback($declare, $scope); $nodeCallback($declare->value, $scope); if ( $declare->key->name !== 'strict_types' - || !($declare->value instanceof Node\Scalar\LNumber) + || !($declare->value instanceof Node\Scalar\Int_) || $declare->value->value !== 1 ) { continue; @@ -376,26 +530,37 @@ private function processStmtNode( $scope = $scope->enterDeclareStrictTypes(); } + + if ($stmt->stmts !== null) { + $result = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $alwaysTerminating = $result->isAlwaysTerminating(); + $exitPoints = $result->getExitPoints(); + } + + return new StatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Function_) { $hasYield = false; $throwPoints = []; - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } - } - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->getPhpDocs($scope, $stmt); + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } if ($stmt->returnType !== null) { $nodeCallback($stmt->returnType, $scope); } + if (!$isDeprecated) { + [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt); + } + $functionScope = $scope->enterFunction( $stmt, $templateTypeMap, @@ -405,14 +570,26 @@ private function processStmtNode( $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal, $isPure, + $acceptsNamedArguments, + $asserts, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); - $nodeCallback(new InFunctionNode($stmt), $functionScope); + $functionReflection = $functionScope->getFunction(); + if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { + throw new ShouldNotHappenException(); + } + + $nodeCallback(new InFunctionNode($functionReflection, $stmt), $functionScope); $gatheredReturnStatements = []; + $gatheredYieldStatements = []; $executionEnds = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$executionEnds): void { + $functionImpurePoints = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $functionScope->getFunction()) { return; @@ -420,43 +597,61 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof PropertyAssignNode) { + $functionImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } if ($node instanceof ExecutionEndNode) { $executionEnds[] = $node; return; } + if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + $gatheredYieldStatements[] = $node; + } if (!$node instanceof Return_) { return; } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }); + }, StatementContext::createTopLevel()); $nodeCallback(new FunctionReturnStatementsNode( $stmt, $gatheredReturnStatements, + $gatheredYieldStatements, $statementResult, $executionEnds, + array_merge($statementResult->getImpurePoints(), $functionImpurePoints), + $functionReflection, ), $functionScope); } elseif ($stmt instanceof Node\Stmt\ClassMethod) { $hasYield = false; $throwPoints = []; - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } - } - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->getPhpDocs($scope, $stmt); + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } if ($stmt->returnType !== null) { $nodeCallback($stmt->returnType, $scope); } + if (!$isDeprecated) { + [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt); + } + + $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct'; + $isConstructor = $isFromTrait || $stmt->name->toLowerString() === '__construct'; + $methodScope = $scope->enterClassMethod( $stmt, $templateTypeMap, @@ -468,15 +663,29 @@ private function processStmtNode( $isInternal, $isFinal, $isPure, + $acceptsNamedArguments, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, + $isConstructor, ); - if ($stmt->name->toLowerString() === '__construct') { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $classReflection = $scope->getClassReflection(); + + if ($isConstructor) { foreach ($stmt->params as $param) { - if ($param->flags === 0) { + if ($param->flags === 0 && $param->hooks === []) { continue; } - if (!$param->var instanceof Variable || !is_string($param->var->name)) { + if (!$param->var instanceof Variable || !is_string($param->var->name) || $param->var->name === '') { throw new ShouldNotHappenException(); } $phpDoc = null; @@ -486,23 +695,46 @@ private function processStmtNode( $nodeCallback(new ClassPropertyNode( $param->var->name, $param->flags, - $param->type, + $param->type !== null ? ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection) : null, null, $phpDoc, + $phpDocParameterTypes[$param->var->name] ?? null, true, + $isFromTrait, $param, + false, + $scope->isInTrait(), + $classReflection->isReadOnly(), + false, + $classReflection, ), $methodScope); + $this->processPropertyHooks( + $stmt, + $param->type, + $phpDocParameterTypes[$param->var->name] ?? null, + $param->var->name, + $param->hooks, + $scope, + $nodeCallback, + ); + $methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType()); } } if ($stmt->getAttribute('virtual', false) === false) { - $nodeCallback(new InClassMethodNode($stmt), $methodScope); + $methodReflection = $methodScope->getFunction(); + if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope); } if ($stmt->stmts !== null) { $gatheredReturnStatements = []; + $gatheredYieldStatements = []; $executionEnds = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$executionEnds): void { + $methodImpurePoints = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $methodScope->getFunction()) { return; @@ -510,65 +742,165 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof PropertyAssignNode) { + if ( + $node->getPropertyFetch() instanceof Expr\PropertyFetch + && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection + && $scope->getFunction()->getDeclaringClass()->hasConstructor() + && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName() + && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null + ) { + return; + } + $methodImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } if ($node instanceof ExecutionEndNode) { $executionEnds[] = $node; return; } + if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + $gatheredYieldStatements[] = $node; + } if (!$node instanceof Return_) { return; } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }); + }, StatementContext::createTopLevel()); + + $methodReflection = $methodScope->getFunction(); + if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new MethodReturnStatementsNode( $stmt, $gatheredReturnStatements, + $gatheredYieldStatements, $statementResult, $executionEnds, + array_merge($statementResult->getImpurePoints(), $methodImpurePoints), + $classReflection, + $methodReflection, ), $methodScope); + + if ($isConstructor && $this->narrowMethodScopeFromConstructor) { + $finalScope = null; + + foreach ($executionEnds as $executionEnd) { + if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { + continue; + } + + $endScope = $executionEnd->getStatementResult()->getScope(); + if ($finalScope === null) { + $finalScope = $endScope; + continue; + } + + $finalScope = $finalScope->mergeWith($endScope); + } + + foreach ($gatheredReturnStatements as $statement) { + if ($finalScope === null) { + $finalScope = $statement->getScope(); + continue; + } + + $finalScope = $finalScope->mergeWith($statement->getScope()); + } + + if ($finalScope !== null) { + $scope = $finalScope->rememberConstructorScope(); + } + + } } } elseif ($stmt instanceof Echo_) { $hasYield = false; $throwPoints = []; foreach ($stmt->exprs as $echoExpr) { - $result = $this->processExprNode($echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); } $throwPoints = $overridingThrowPoints ?? $throwPoints; + $impurePoints = [ + new ImpurePoint($scope, $stmt, 'echo', 'echo', true), + ]; } elseif ($stmt instanceof Return_) { if ($stmt->expr !== null) { - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); $hasYield = $result->hasYield(); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) { if ($stmt->num !== null) { - $result = $this->processExprNode($stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Expression) { + if ($stmt->expr instanceof Expr\Throw_) { + $scope = $stmtScope; + } $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createTopLevel()); + $hasAssign = false; + $currentScope = $scope; + $result = $this->processExprNode($stmt, $stmt->expr, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void { + $nodeCallback($node, $scope); + if ($scope->getAnonymousFunctionReflection() !== $currentScope->getAnonymousFunctionReflection()) { + return; + } + if ($scope->getFunction() !== $currentScope->getFunction()) { + return; + } + if (!$node instanceof VariableAssignNode && !$node instanceof PropertyAssignNode) { + return; + } + + $hasAssign = true; + }, ExpressionContext::createTopLevel()); + $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit()); + if ( + count($result->getImpurePoints()) === 0 + && count($throwPoints) === 0 + && !$stmt->expr instanceof Expr\PostInc + && !$stmt->expr instanceof Expr\PreInc + && !$stmt->expr instanceof Expr\PostDec + && !$stmt->expr instanceof Expr\PreDec + ) { + $nodeCallback(new NoopExpressionNode($stmt->expr, $hasAssign), $scope); + } $scope = $result->getScope(); $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition( $scope, @@ -577,25 +909,31 @@ private function processStmtNode( )); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ($earlyTerminationExpr !== null) { return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } - return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints); + return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Namespace_) { if ($stmt->name !== null) { $scope = $scope->enterNamespace($stmt->name->toString()); } - $scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback)->getScope(); + $scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context)->getScope(); $hasYield = false; $throwPoints = []; + $impurePoints = []; } elseif ($stmt instanceof Node\Stmt\Trait_) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } elseif ($stmt instanceof Node\Stmt\ClassLike) { + if (!$context->isTopLevel()) { + return new StatementResult($scope, false, false, [], [], []); + } $hasYield = false; $throwPoints = []; + $impurePoints = []; if (isset($stmt->namespacedName)) { $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope); $classScope = $scope->enterClass($classReflection); @@ -604,7 +942,7 @@ private function processStmtNode( if ($stmt->name === null) { throw new ShouldNotHappenException(); } - if ($stmt->getAttribute('anonymousClass', false) === false) { + if (!$stmt->isAnonymous()) { $classReflection = $this->reflectionProvider->getClass($stmt->name->toString()); } else { $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope); @@ -615,80 +953,134 @@ private function processStmtNode( throw new ShouldNotHappenException(); } - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $classScope, $nodeCallback, ExpressionContext::createDeep()); + $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $classStatementsGatherer); + + $classLikeStatements = $stmt->stmts; + if ($this->narrowMethodScopeFromConstructor) { + // analyze static methods first; constructor next; instance methods and property hooks last so we can carry over the scope + usort($classLikeStatements, static function ($a, $b) { + if ($a instanceof Node\Stmt\Property) { + return 1; } - } + if ($b instanceof Node\Stmt\Property) { + return -1; + } + + if (!$a instanceof Node\Stmt\ClassMethod || !$b instanceof Node\Stmt\ClassMethod) { + return 0; + } + + return [!$a->isStatic(), $a->name->toLowerString() !== '__construct'] <=> [!$b->isStatic(), $b->name->toLowerString() !== '__construct']; + }); } - $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback); - $this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer); - $nodeCallback(new ClassPropertiesNode($stmt, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls()), $classScope); - $nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls()), $classScope); - $nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches()), $classScope); + $this->processStmtNodes($stmt, $classLikeStatements, $classScope, $classStatementsGatherer, $context); + $nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope); + $nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope); + $nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope); + $classReflection->evictPrivateSymbols(); + $this->calledMethodResults = []; } elseif ($stmt instanceof Node\Stmt\Property) { $hasYield = false; $throwPoints = []; - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + + $nativePropertyType = $stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null; + + [,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt); + $phpDocType = null; + if (isset($varTags[0]) && count($varTags) === 1) { + $phpDocType = $varTags[0]->getType(); } + foreach ($stmt->props as $prop) { - $this->processStmtNode($prop, $scope, $nodeCallback); - $docComment = $stmt->getDocComment(); + $nodeCallback($prop, $scope); + if ($prop->default !== null) { + $this->processExprNode($stmt, $prop->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + } + + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + $propertyName = $prop->name->toString(); + + if ($phpDocType === null) { + if (isset($varTags[$propertyName])) { + $phpDocType = $varTags[$propertyName]->getType(); + } + } + + $propStmt = clone $stmt; + $propStmt->setAttributes($prop->getAttributes()); $nodeCallback( new ClassPropertyNode( - $prop->name->toString(), + $propertyName, $stmt->flags, - $stmt->type, + $nativePropertyType, $prop->default, - $docComment !== null ? $docComment->getText() : null, + $docComment, + $phpDocType, + false, false, - $prop, + $propStmt, + $isReadOnly, + $scope->isInTrait(), + $scope->getClassReflection()->isReadOnly(), + $isAllowedPrivateMutation, + $scope->getClassReflection(), ), $scope, ); } + if (count($stmt->hooks) > 0) { + if (!isset($propertyName)) { + throw new ShouldNotHappenException('Property name should be known when analysing hooks.'); + } + $this->processPropertyHooks( + $stmt, + $stmt->type, + $phpDocType, + $propertyName, + $stmt->hooks, + $scope, + $nodeCallback, + ); + } + if ($stmt->type !== null) { $nodeCallback($stmt->type, $scope); } - } elseif ($stmt instanceof Node\Stmt\PropertyProperty) { - $hasYield = false; - $throwPoints = []; - if ($stmt->default !== null) { - $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } elseif ($stmt instanceof Throw_) { - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); - $throwPoints = $result->getThrowPoints(); - $throwPoints[] = ThrowPoint::createExplicit($result->getScope(), $scope->getType($stmt->expr), $stmt, false); - return new StatementResult($result->getScope(), $result->hasYield(), true, [ - new StatementExitPoint($stmt, $scope), - ], $throwPoints); } elseif ($stmt instanceof If_) { - $conditionType = $scope->getType($stmt->cond)->toBoolean(); - $ifAlwaysTrue = $conditionType instanceof ConstantBooleanType && $conditionType->getValue(); - $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); + $ifAlwaysTrue = $conditionType->isTrue()->yes(); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); $exitPoints = []; $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); + $endStatements = []; $finalScope = null; $alwaysTerminating = true; $hasYield = $condResult->hasYield(); - $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback); + $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) { $exitPoints = $branchScopeStatementResult->getExitPoints(); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope; $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating(); + if (count($branchScopeStatementResult->getEndStatements()) > 0) { + $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); + } elseif (count($stmt->stmts) > 0) { + $endStatements[] = new EndStatementResult($stmt->stmts[count($stmt->stmts) - 1], $branchScopeStatementResult); + } else { + $endStatements[] = new EndStatementResult($stmt, $branchScopeStatementResult); + } $hasYield = $branchScopeStatementResult->hasYield() || $hasYield; } @@ -698,11 +1090,12 @@ private function processStmtNode( $condScope = $scope; foreach ($stmt->elseifs as $elseif) { $nodeCallback($elseif, $scope); - $elseIfConditionType = $condScope->getType($elseif->cond)->toBoolean(); - $condResult = $this->processExprNode($elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep()); + $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean(); + $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $condScope = $condResult->getScope(); - $branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback); + $branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); if ( !$ifAlwaysTrue @@ -716,15 +1109,22 @@ private function processStmtNode( ) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); + if (count($branchScopeStatementResult->getEndStatements()) > 0) { + $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); + } elseif (count($elseif->stmts) > 0) { + $endStatements[] = new EndStatementResult($elseif->stmts[count($elseif->stmts) - 1], $branchScopeStatementResult); + } else { + $endStatements[] = new EndStatementResult($elseif, $branchScopeStatementResult); + } $hasYield = $hasYield || $branchScopeStatementResult->hasYield(); } if ( - $elseIfConditionType instanceof ConstantBooleanType - && $elseIfConditionType->getValue() + $elseIfConditionType->isTrue()->yes() ) { $lastElseIfConditionIsTrue = true; } @@ -734,20 +1134,28 @@ private function processStmtNode( } if ($stmt->else === null) { - if (!$ifAlwaysTrue) { + if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { $finalScope = $scope->mergeWith($finalScope); $alwaysTerminating = false; } } else { $nodeCallback($stmt->else, $scope); - $branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback); + $branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback, $context); if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); + if (count($branchScopeStatementResult->getEndStatements()) > 0) { + $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); + } elseif (count($stmt->else->stmts) > 0) { + $endStatements[] = new EndStatementResult($stmt->else->stmts[count($stmt->else->stmts) - 1], $branchScopeStatementResult); + } else { + $endStatements[] = new EndStatementResult($stmt->else, $branchScopeStatementResult); + } $hasYield = $hasYield || $branchScopeStatementResult->hasYield(); } } @@ -756,50 +1164,60 @@ private function processStmtNode( $finalScope = $scope; } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints); + if ($stmt->else === null && !$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { + $endStatements[] = new EndStatementResult($stmt, new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints)); + } + + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints, $endStatements); } elseif ($stmt instanceof Node\Stmt\TraitUse) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $this->processTraitUse($stmt, $scope, $nodeCallback); } elseif ($stmt instanceof Foreach_) { - $condResult = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $scope = $condResult->getScope(); $arrayComparisonExpr = new BinaryOp\NotIdentical( $stmt->expr, new Array_([]), ); - $inForeachScope = $scope; if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { - $inForeachScope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); + $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } - $nodeCallback(new InForeachNode($stmt), $inForeachScope); - $bodyScope = $this->enterForeach($scope->filterByTruthyValue($arrayComparisonExpr), $stmt); - $count = 0; - do { - $prevScope = $bodyScope; - $bodyScope = $bodyScope->mergeWith($scope->filterByTruthyValue($arrayComparisonExpr)); - $bodyScope = $this->enterForeach($bodyScope, $stmt); - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - if ($bodyScope->equals($prevScope)) { - break; - } + $nodeCallback(new InForeachNode($stmt), $scope); + $originalScope = $scope; + $bodyScope = $scope; - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + if ($context->isTopLevel()) { + $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope; + $bodyScope = $this->enterForeach($originalScope, $originalScope, $stmt); + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); + $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + } - $bodyScope = $bodyScope->mergeWith($scope->filterByTruthyValue($arrayComparisonExpr)); - $bodyScope = $this->enterForeach($bodyScope, $stmt); - $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); + $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt); + $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); @@ -808,15 +1226,20 @@ private function processStmtNode( $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - $isIterableAtLeastOnce = $scope->getType($stmt->expr)->isIterableAtLeastOnce(); - if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { + $exprType = $scope->getType($stmt->expr); + $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce(); + if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) { + $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr( + new BinaryOp\Identical( + $stmt->expr, + new Array_([]), + ), + new FuncCall(new Name\FullyQualified('is_object'), [ + new Arg($stmt->expr), + ]), + ))); + } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { $finalScope = $scope; - } elseif ($isIterableAtLeastOnce->maybe()) { - if ($this->polluteScopeWithAlwaysIterableForeach) { - $finalScope = $finalScope->mergeWith($scope->filterByFalseyValue($arrayComparisonExpr)); - } else { - $finalScope = $finalScope->mergeWith($scope); - } } elseif (!$this->polluteScopeWithAlwaysIterableForeach) { $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope); // get types from finalScope, but don't create new variables @@ -824,6 +1247,7 @@ private function processStmtNode( if (!$isIterableAtLeastOnce->no()) { $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) { $throwPoints[] = ThrowPoint::createImplicit($scope, $stmt->expr); @@ -835,52 +1259,59 @@ private function processStmtNode( $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(), $finalScopeResult->getExitPointsForOuterLoop(), $throwPoints, + $impurePoints, ); } elseif ($stmt instanceof While_) { - $condResult = $this->processExprNode($stmt->cond, $scope, static function (): void { + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, static function (): void { }, ExpressionContext::createDeep()); $bodyScope = $condResult->getTruthyScope(); - $count = 0; - do { - $prevScope = $bodyScope; - $bodyScope = $bodyScope->mergeWith($scope); - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - if ($bodyScope->equals($prevScope)) { - break; - } - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + if ($context->isTopLevel()) { + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + } $bodyScope = $bodyScope->mergeWith($scope); $bodyScopeMaybeRan = $bodyScope; - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); - $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond); - foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); + + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean(); + $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel(); + $neverIterates = $condBooleanType->isFalse()->yes() && $context->isTopLevel(); + if (!$alwaysIterates) { + foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); + } } + $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); foreach ($breakExitPoints as $breakExitPoint) { $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); } - $beforeCondBooleanType = $scope->getType($stmt->cond)->toBoolean(); - $condBooleanType = $bodyScopeMaybeRan->getType($stmt->cond)->toBoolean(); - $isIterableAtLeastOnce = $beforeCondBooleanType instanceof ConstantBooleanType && $beforeCondBooleanType->getValue(); - $alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); - $neverIterates = $condBooleanType instanceof ConstantBooleanType && !$condBooleanType->getValue(); + $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); + $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes(); $nodeCallback(new BreaklessWhileLoopNode($stmt, $finalScopeResult->getExitPoints()), $bodyScopeMaybeRan); if ($alwaysIterates) { @@ -899,8 +1330,10 @@ private function processStmtNode( } $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); if (!$neverIterates) { $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } return new StatementResult( @@ -909,6 +1342,7 @@ private function processStmtNode( $isAlwaysTerminating, $finalScopeResult->getExitPointsForOuterLoop(), $throwPoints, + $impurePoints, ); } elseif ($stmt instanceof Do_) { $finalScope = null; @@ -916,41 +1350,45 @@ private function processStmtNode( $count = 0; $hasYield = false; $throwPoints = []; + $impurePoints = []; + + if ($context->isTopLevel()) { + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); + foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + } + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + if ($bodyScope->equals($prevScope)) { + break; + } - do { - $prevScope = $bodyScope; - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); - foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { - $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); - } - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); - if ($bodyScope->equals($prevScope)) { - break; - } - - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); - $bodyScope = $bodyScope->mergeWith($scope); + $bodyScope = $bodyScope->mergeWith($scope); + } - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); } - $condBooleanType = $bodyScope->getType($stmt->cond)->toBoolean(); - $alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean(); + $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel(); $nodeCallback(new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->getExitPoints()), $bodyScope); @@ -964,12 +1402,13 @@ private function processStmtNode( $finalScope = $scope; } if (!$alwaysTerminating) { - $condResult = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $finalScope = $condResult->getFalseyScope(); } else { - $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); } foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); @@ -981,75 +1420,85 @@ private function processStmtNode( $alwaysTerminating, $bodyScopeResult->getExitPointsForOuterLoop(), array_merge($throwPoints, $bodyScopeResult->getThrowPoints()), + array_merge($impurePoints, $bodyScopeResult->getImpurePoints()), ); } elseif ($stmt instanceof For_) { $initScope = $scope; $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->init as $initExpr) { - $initResult = $this->processExprNode($initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel()); + $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel()); $initScope = $initResult->getScope(); $hasYield = $hasYield || $initResult->hasYield(); $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints()); } $bodyScope = $initScope; $isIterableAtLeastOnce = TrinaryLogic::createYes(); + $lastCondExpr = $stmt->cond[count($stmt->cond) - 1] ?? null; foreach ($stmt->cond as $condExpr) { - $condResult = $this->processExprNode($condExpr, $bodyScope, static function (): void { + $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { }, ExpressionContext::createDeep()); $initScope = $condResult->getScope(); - $condTruthiness = $condResult->getScope()->getType($condExpr)->toBoolean(); - if ($condTruthiness instanceof ConstantBooleanType) { - $condTruthinessTrinary = TrinaryLogic::createFromBoolean($condTruthiness->getValue()); - } else { - $condTruthinessTrinary = TrinaryLogic::createMaybe(); + $condResultScope = $condResult->getScope(); + + if ($condExpr === $lastCondExpr) { + $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); + $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); } - $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthinessTrinary); + $hasYield = $hasYield || $condResult->hasYield(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $bodyScope = $condResult->getTruthyScope(); } - $count = 0; - do { - $prevScope = $bodyScope; - $bodyScope = $bodyScope->mergeWith($initScope); - foreach ($stmt->cond as $condExpr) { - $bodyScope = $this->processExprNode($condExpr, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); - } - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - foreach ($stmt->loop as $loopExpr) { - $exprResult = $this->processExprNode($loopExpr, $bodyScope, static function (): void { - }, ExpressionContext::createTopLevel()); - $bodyScope = $exprResult->getScope(); - $hasYield = $hasYield || $exprResult->hasYield(); - $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); - } + if ($context->isTopLevel()) { + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($initScope); + if ($lastCondExpr !== null) { + $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + } + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + foreach ($stmt->loop as $loopExpr) { + $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, static function (): void { + }, ExpressionContext::createTopLevel()); + $bodyScope = $exprResult->getScope(); + $hasYield = $hasYield || $exprResult->hasYield(); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + } - if ($bodyScope->equals($prevScope)) { - break; - } + if ($bodyScope->equals($prevScope)) { + break; + } - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + } $bodyScope = $bodyScope->mergeWith($initScope); - foreach ($stmt->cond as $condExpr) { - $bodyScope = $this->processExprNode($condExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + + $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel()); + if ($lastCondExpr !== null) { + $alwaysIterates = $alwaysIterates->and($bodyScope->getType($lastCondExpr)->toBoolean()->isTrue()); + $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); } - $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); @@ -1057,11 +1506,12 @@ private function processStmtNode( $loopScope = $finalScope; foreach ($stmt->loop as $loopExpr) { - $loopScope = $this->processExprNode($loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); } $finalScope = $finalScope->generalizeWith($loopScope); - foreach ($stmt->cond as $condExpr) { - $finalScope = $finalScope->filterByFalseyValue($condExpr); + + if ($lastCondExpr !== null) { + $finalScope = $finalScope->filterByFalseyValue($lastCondExpr); } foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { @@ -1087,15 +1537,24 @@ private function processStmtNode( } } - return new StatementResult( + if ($alwaysIterates->yes()) { + $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0; + } elseif ($isIterableAtLeastOnce->yes()) { + $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating(); + } else { + $isAlwaysTerminating = false; + } + + return new StatementResult( $finalScope, $finalScopeResult->hasYield() || $hasYield, - false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/, + $isAlwaysTerminating, $finalScopeResult->getExitPointsForOuterLoop(), array_merge($throwPoints, $finalScopeResult->getThrowPoints()), + array_merge($impurePoints, $finalScopeResult->getImpurePoints()), ); } elseif ($stmt instanceof Switch_) { - $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $condResult->getScope(); $scopeForBranches = $scope; $finalScope = null; @@ -1105,21 +1564,26 @@ private function processStmtNode( $hasYield = $condResult->hasYield(); $exitPointsForOuterLoop = []; $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); + $fullCondExpr = null; foreach ($stmt->cases as $caseNode) { if ($caseNode->cond !== null) { $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond); - $caseResult = $this->processExprNode($caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep()); + $fullCondExpr = $fullCondExpr === null ? $condExpr : new BooleanOr($fullCondExpr, $condExpr); + $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep()); $scopeForBranches = $caseResult->getScope(); $hasYield = $hasYield || $caseResult->hasYield(); $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints()); - $branchScope = $scopeForBranches->filterByTruthyValue($condExpr); + $impurePoints = array_merge($impurePoints, $caseResult->getImpurePoints()); + $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr); } else { $hasDefaultCase = true; + $fullCondExpr = null; $branchScope = $scopeForBranches; } $branchScope = $branchScope->mergeWith($prevScope); - $branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback); + $branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints(); $hasYield = $hasYield || $branchFinalScopeResult->hasYield(); @@ -1132,11 +1596,13 @@ private function processStmtNode( } $exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop()); $throwPoints = array_merge($throwPoints, $branchFinalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchFinalScopeResult->getImpurePoints()); if ($branchScopeResult->isAlwaysTerminating()) { $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating(); $prevScope = null; - if (isset($condExpr)) { - $scopeForBranches = $scopeForBranches->filterByFalseyValue($condExpr); + if (isset($fullCondExpr)) { + $scopeForBranches = $scopeForBranches->filterByFalseyValue($fullCondExpr); + $fullCondExpr = null; } if (!$branchFinalScopeResult->isAlwaysTerminating()) { $finalScope = $branchScope->mergeWith($finalScope); @@ -1146,7 +1612,9 @@ private function processStmtNode( } } - if (!$hasDefaultCase) { + $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType; + + if (!$hasDefaultCase && !$exhaustive) { $alwaysTerminating = false; } @@ -1155,13 +1623,13 @@ private function processStmtNode( $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating(); } - if (!$hasDefaultCase || $finalScope === null) { + if ((!$hasDefaultCase && !$exhaustive) || $finalScope === null) { $finalScope = $scope->mergeWith($finalScope); } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints); + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints, $impurePoints); } elseif ($stmt instanceof TryCatch) { - $branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback); + $branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope; @@ -1177,7 +1645,7 @@ private function processStmtNode( } foreach ($branchScopeResult->getExitPoints() as $exitPoint) { $finallyExitPoints[] = $exitPoint; - if ($exitPoint->getStatement() instanceof Throw_) { + if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) { continue; } if ($finallyScope !== null) { @@ -1187,60 +1655,115 @@ private function processStmtNode( } $throwPoints = $branchScopeResult->getThrowPoints(); + $impurePoints = $branchScopeResult->getImpurePoints(); $throwPointsForLater = []; $pastCatchTypes = new NeverType(); foreach ($stmt->catches as $catchNode) { $nodeCallback($catchNode, $scope); - $catchType = TypeCombinator::union(...array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types)); - $originalCatchType = $catchType; - $catchType = TypeCombinator::remove($catchType, $pastCatchTypes); + $originalCatchTypes = array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types); + $catchTypes = array_map(static fn (Type $type): Type => TypeCombinator::remove($type, $pastCatchTypes), $originalCatchTypes); + + $originalCatchType = TypeCombinator::union(...$originalCatchTypes); + $catchType = TypeCombinator::union(...$catchTypes); $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType); + $matchingThrowPoints = []; - $newThrowPoints = []; - foreach ($throwPoints as $throwPoint) { - if (!$throwPoint->isExplicit() && !$catchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { + $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), false); + + // throwable matches all + foreach ($originalCatchTypes as $catchTypeIndex => $catchTypeItem) { + if (!$catchTypeItem->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { continue; } - $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType()); - if ($isSuperType->no()) { - continue; + + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + $matchingCatchTypes[$catchTypeIndex] = true; } - $matchingThrowPoints[] = $throwPoint; } - $hasExplicit = count($matchingThrowPoints) > 0; - foreach ($throwPoints as $throwPoint) { - $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType()); - if (!$hasExplicit && !$isSuperType->no()) { - $matchingThrowPoints[] = $throwPoint; + + // explicit only + $onlyExplicitIsThrow = true; + if (count($matchingThrowPoints) === 0) { + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) { + if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) { + continue; + } + + $matchingCatchTypes[$catchTypeIndex] = true; + if (!$throwPoint->isExplicit()) { + continue; + } + $throwNode = $throwPoint->getNode(); + if ( + !$throwNode instanceof Expr\Throw_ + && !($throwNode instanceof Node\Stmt\Expression && $throwNode->expr instanceof Expr\Throw_) + ) { + $onlyExplicitIsThrow = false; + } + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + } } - if ($isSuperType->yes()) { - continue; + } + + // implicit only + if (count($matchingThrowPoints) === 0 || $onlyExplicitIsThrow) { + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + if ($throwPoint->isExplicit()) { + continue; + } + + foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) { + if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) { + continue; + } + + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + } } - $newThrowPoints[] = $throwPoint->subtractCatchType($catchType); } - $throwPoints = $newThrowPoints; + // include previously removed throw points if (count($matchingThrowPoints) === 0) { - $throwableThrowPoints = []; if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) { if (!$originalThrowPoint->canContainAnyThrowable()) { continue; } - $throwableThrowPoints[] = $originalThrowPoint; + $matchingThrowPoints[] = $originalThrowPoint; + $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), true); } } + } + + // emit error + foreach ($matchingCatchTypes as $catchTypeIndex => $matched) { + if ($matched) { + continue; + } + $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope); + } + + if (count($matchingThrowPoints) === 0) { + continue; + } + + // recompute throw points + $newThrowPoints = []; + foreach ($throwPoints as $throwPoint) { + $newThrowPoint = $throwPoint->subtractCatchType($originalCatchType); - if (count($throwableThrowPoints) === 0) { - $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType, $originalCatchType), $scope); + if ($newThrowPoint->getType() instanceof NeverType) { continue; } - $matchingThrowPoints = $throwableThrowPoints; + $newThrowPoints[] = $newThrowPoint; } + $throwPoints = $newThrowPoints; $catchScope = null; foreach ($matchingThrowPoints as $matchingThrowPoint) { @@ -1260,13 +1783,14 @@ private function processStmtNode( $variableName = $catchNode->var->name; } - $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback); + $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback, $context); $catchScopeForFinally = $catchScopeResult->getScope(); $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating(); $hasYield = $hasYield || $catchScopeResult->hasYield(); $catchThrowPoints = $catchScopeResult->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $catchScopeResult->getImpurePoints()); $throwPointsForLater = array_merge($throwPointsForLater, $catchThrowPoints); if ($finallyScope !== null) { @@ -1274,7 +1798,7 @@ private function processStmtNode( } foreach ($catchScopeResult->getExitPoints() as $exitPoint) { $finallyExitPoints[] = $exitPoint; - if ($exitPoint->getStatement() instanceof Throw_) { + if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) { continue; } if ($finallyScope !== null) { @@ -1302,12 +1826,13 @@ private function processStmtNode( $finallyScope = $finallyScope->mergeWith($throwPoint->getScope()); } - if ($finallyScope !== null && $stmt->finally !== null) { + if ($finallyScope !== null) { $originalFinallyScope = $finallyScope; - $finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback); + $finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback, $context); $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating(); $hasYield = $hasYield || $finallyResult->hasYield(); $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints()); $finallyScope = $finallyResult->getScope(); $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope); if (count($finallyResult->getExitPoints()) > 0) { @@ -1319,102 +1844,275 @@ private function processStmtNode( $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints()); } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater)); + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater), $impurePoints); } elseif ($stmt instanceof Unset_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->vars as $var) { - $scope = $this->lookForEnterVariableAssign($scope, $var); - $scope = $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); - $scope = $this->lookForExitVariableAssign($scope, $var); - $scope = $scope->unsetExpression($var); + $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); + $exprResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $scope = $exprResult->getScope(); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); + $hasYield = $hasYield || $exprResult->hasYield(); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + if ($var instanceof ArrayDimFetch && $var->dim !== null) { + $cloningTraverser = new NodeTraverser(); + $cloningTraverser->addVisitor(new CloningVisitor()); + + /** @var Expr $clonedVar */ + [$clonedVar] = $cloningTraverser->traverse([$var->var]); + + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class () extends NodeVisitorAbstract { + + public function leaveNode(Node $node): ?ExistingArrayDimFetch + { + if (!$node instanceof ArrayDimFetch || $node->dim === null) { + return null; + } + + return new ExistingArrayDimFetch($node->var, $node->dim); + } + + }); + + /** @var Expr $clonedVar */ + [$clonedVar] = $traverser->traverse([$clonedVar]); + $scope = $this->processAssignVar( + $scope, + $stmt, + $clonedVar, + new UnsetOffsetExpr($var->var, $var->dim), + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + ExpressionContext::createDeep(), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + false, + )->getScope(); + } elseif ($var instanceof PropertyFetch) { + $scope = $scope->invalidateExpression($var); + $impurePoints[] = new ImpurePoint( + $scope, + $var, + 'propertyUnset', + 'property unset', + true, + ); + } else { + $scope = $scope->invalidateExpression($var); + } + } } elseif ($stmt instanceof Node\Stmt\Use_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->uses as $use) { - $this->processStmtNode($use, $scope, $nodeCallback); + $nodeCallback($use, $scope); } } elseif ($stmt instanceof Node\Stmt\Global_) { $hasYield = false; $throwPoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $stmt, + 'global', + 'global variable', + true, + ), + ]; $vars = []; foreach ($stmt->vars as $var) { if (!$var instanceof Variable) { throw new ShouldNotHappenException(); } - $scope = $this->lookForEnterVariableAssign($scope, $var); - $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep()); - $scope = $this->lookForExitVariableAssign($scope, $var); + $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); + $varResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); if (!is_string($var->name)) { continue; } - $scope = $scope->assignVariable($var->name, new MixedType()); + $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes()); $vars[] = $var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); } elseif ($stmt instanceof Static_) { $hasYield = false; $throwPoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $stmt, + 'static', + 'static variable', + true, + ), + ]; $vars = []; foreach ($stmt->vars as $var) { - $scope = $this->processStmtNode($var, $scope, $nodeCallback)->getScope(); if (!is_string($var->var->name)) { - continue; + throw new ShouldNotHappenException(); + } + + if ($var->default !== null) { + $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints()); } + $scope = $scope->enterExpressionAssign($var->var); + $varResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); + $scope = $scope->exitExpressionAssign($var->var); + + $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes()); $vars[] = $var->var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); - } elseif ($stmt instanceof StaticVar) { + } elseif ($stmt instanceof Node\Stmt\Const_) { $hasYield = false; $throwPoints = []; - if (!is_string($stmt->var->name)) { - throw new ShouldNotHappenException(); - } - if ($stmt->default !== null) { - $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = []; + foreach ($stmt->consts as $const) { + $nodeCallback($const, $scope); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); + if ($const->namespacedName !== null) { + $constantName = new Name\FullyQualified($const->namespacedName->toString()); + } else { + $constantName = new Name\FullyQualified($const->name->toString()); + } + $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value)); } - $scope = $scope->enterExpressionAssign($stmt->var); - $this->processExprNode($stmt->var, $scope, $nodeCallback, ExpressionContext::createDeep()); - $scope = $scope->exitExpressionAssign($stmt->var); - $scope = $scope->assignVariable($stmt->var->name, new MixedType()); - } elseif ($stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\ClassConst) { + } elseif ($stmt instanceof Node\Stmt\ClassConst) { $hasYield = false; $throwPoints = []; - if ($stmt instanceof Node\Stmt\ClassConst) { - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } - } - } + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); foreach ($stmt->consts as $const) { $nodeCallback($const, $scope); - $this->processExprNode($const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - if ($scope->getNamespace() !== null) { - $constName = [$scope->getNamespace(), $const->name->toString()]; - } else { - $constName = $const->name->toString(); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); + if ($scope->getClassReflection() === null) { + throw new ShouldNotHappenException(); } - $scope = $scope->specifyExpressionType(new ConstFetch(new Name\FullyQualified($constName)), $scope->getType($const->value)); + $scope = $scope->assignExpression( + new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name), + $scope->getType($const->value), + $scope->getNativeType($const->value), + ); + } + } elseif ($stmt instanceof Node\Stmt\EnumCase) { + $hasYield = false; + $throwPoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + $impurePoints = []; + if ($stmt->expr !== null) { + $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = $exprResult->getImpurePoints(); + } + } elseif ($stmt instanceof InlineHTML) { + $hasYield = false; + $throwPoints = []; + $impurePoints = [ + new ImpurePoint($scope, $stmt, 'betweenPhpTags', 'output between PHP opening and closing tags', true), + ]; + } elseif ($stmt instanceof Node\Stmt\Block) { + $result = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context); + if ($this->polluteScopeWithBlock) { + return $result; } + + return new StatementResult( + $scope->mergeWith($result->getScope()), + $result->hasYield(), + $result->isAlwaysTerminating(), + $result->getExitPoints(), + $result->getThrowPoints(), + $result->getImpurePoints(), + $result->getEndStatements(), + ); } elseif ($stmt instanceof Node\Stmt\Nop) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, null); $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; + $impurePoints = []; + } elseif ($stmt instanceof Node\Stmt\GroupUse) { + $hasYield = false; + $throwPoints = []; + foreach ($stmt->uses as $use) { + $nodeCallback($use, $scope); + } + $impurePoints = []; } else { $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; + $impurePoints = []; + } + + return new StatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints); + } + + /** + * @return array{bool, string|null} + */ + private function getDeprecatedAttribute(Scope $scope, Node\Stmt\Function_|Node\Stmt\ClassMethod|Node\PropertyHook $stmt): array + { + $initializerExprContext = InitializerExprContext::fromStubParameter( + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->getFile(), + $stmt, + ); + $isDeprecated = false; + $deprecatedDescription = null; + $deprecatedDescriptionType = null; + foreach ($stmt->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() !== 'Deprecated') { + continue; + } + $isDeprecated = true; + $arguments = $attr->args; + foreach ($arguments as $i => $arg) { + $argName = $arg->name; + if ($argName === null) { + if ($i !== 0) { + continue; + } + + $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext); + break; + } + + if ($argName->toString() !== 'message') { + continue; + } + + $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext); + break; + } + } + } + + if ($deprecatedDescriptionType !== null) { + $constantStrings = $deprecatedDescriptionType->getConstantStrings(); + if (count($constantStrings) === 1) { + $deprecatedDescription = $constantStrings[0]->getValue(); + } } - return new StatementResult($scope, $hasYield, false, [], $throwPoints); + return [$isDeprecated, $deprecatedDescription]; } /** @@ -1439,7 +2137,7 @@ private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $s $throwsTag = $resolvedPhpDoc->getThrowsTag(); if ($throwsTag !== null) { $throwsType = $throwsTag->getType(); - if ($throwsType instanceof VoidType) { + if ($throwsType->isVoid()->yes()) { return []; } @@ -1486,123 +2184,122 @@ private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $cla return new ClassReflection( $this->reflectionProvider, + $this->initializerExprTypeResolver, $this->fileTypeMapper, $this->stubPhpDocProvider, $this->phpDocInheritanceResolver, $this->phpVersion, + $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), $betterReflectionClass->getName(), $betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($betterReflectionClass) : new ReflectionClass($betterReflectionClass), null, null, null, + $this->universalObjectCratesClasses, sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()), ); } - private function lookForEnterVariableAssign(MutatingScope $scope, Expr $expr): MutatingScope + private function lookForSetAllowedUndefinedExpressions(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 fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->enterExpressionAssign($expr)); - } - - return $scope; + return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr)); } - private function lookForExitVariableAssign(MutatingScope $scope, Expr $expr): MutatingScope + private function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope { - if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { - $scope = $scope->exitExpressionAssign($expr); - } - if (!$expr instanceof Variable) { - return $this->lookForVariableAssignCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->exitExpressionAssign($expr)); - } - - return $scope; + return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr)); } /** * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback */ - private function lookForVariableAssignCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope + private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope { - if ($expr instanceof Variable) { + if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { $scope = $callback($scope, $expr); - } elseif ($expr instanceof ArrayDimFetch) { - if ($expr->dim !== null) { - $scope = $callback($scope, $expr); - } + } - $scope = $this->lookForVariableAssignCallback($scope, $expr->var, $callback); + if ($expr instanceof ArrayDimFetch) { + $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { - $scope = $this->lookForVariableAssignCallback($scope, $expr->var, $callback); - } elseif ($expr instanceof StaticPropertyFetch) { - if ($expr->class instanceof Expr) { - $scope = $this->lookForVariableAssignCallback($scope, $expr->class, $callback); - } - } elseif ($expr instanceof Array_ || $expr instanceof List_) { + $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); + } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) { + $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback); + } elseif ($expr instanceof List_) { foreach ($expr->items as $item) { if ($item === null) { continue; } - $scope = $this->lookForVariableAssignCallback($scope, $item->value, $callback); + $scope = $this->lookForExpressionCallback($scope, $item->value, $callback); } } return $scope; } - private function ensureShallowNonNullability(MutatingScope $scope, Expr $exprToSpecify): EnsuredNonNullabilityResult + private function ensureShallowNonNullability(MutatingScope $scope, Scope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult { $exprType = $scope->getType($exprToSpecify); + $isNull = $exprType->isNull(); + if ($isNull->yes()) { + return new EnsuredNonNullabilityResult($scope, []); + } + + // keep certainty + $certainty = TrinaryLogic::createYes(); + $hasExpressionType = $originalScope->hasExpressionType($exprToSpecify); + if (!$hasExpressionType->no()) { + $certainty = $hasExpressionType; + } + $exprTypeWithoutNull = TypeCombinator::removeNull($exprType); - if (!$exprType->equals($exprTypeWithoutNull)) { - $nativeType = $scope->getNativeType($exprToSpecify); - $scope = $scope->specifyExpressionType( - $exprToSpecify, - $exprTypeWithoutNull, - TypeCombinator::removeNull($nativeType), - ); + if ($exprType->equals($exprTypeWithoutNull)) { + $originalExprType = $originalScope->getType($exprToSpecify); + if (!$originalExprType->equals($exprTypeWithoutNull)) { + $originalNativeType = $originalScope->getNativeType($exprToSpecify); - return new EnsuredNonNullabilityResult( - $scope, - [ - new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType), - ], - ); + return new EnsuredNonNullabilityResult($scope, [ + new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $certainty), + ]); + } + return new EnsuredNonNullabilityResult($scope, []); } - return new EnsuredNonNullabilityResult($scope, []); + $nativeType = $scope->getNativeType($exprToSpecify); + $scope = $scope->specifyExpressionType( + $exprToSpecify, + $exprTypeWithoutNull, + TypeCombinator::removeNull($nativeType), + TrinaryLogic::createYes(), + ); + + return new EnsuredNonNullabilityResult( + $scope, + [ + new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType, $certainty), + ], + ); } - private function ensureNonNullability(MutatingScope $scope, Expr $expr, bool $findMethods): EnsuredNonNullabilityResult + private function ensureNonNullability(MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult { - $exprToSpecify = $expr; $specifiedExpressions = []; - while (true) { - $result = $this->ensureShallowNonNullability($scope, $exprToSpecify); - $scope = $result->getScope(); + $originalScope = $scope; + $scope = $this->lookForExpressionCallback($scope, $expr, function ($scope, $expr) use (&$specifiedExpressions, $originalScope) { + $result = $this->ensureShallowNonNullability($scope, $originalScope, $expr); foreach ($result->getSpecifiedExpressions() as $specifiedExpression) { $specifiedExpressions[] = $specifiedExpression; } - - if ($exprToSpecify instanceof PropertyFetch) { - $exprToSpecify = $exprToSpecify->var; - } elseif ($exprToSpecify instanceof StaticPropertyFetch && $exprToSpecify->class instanceof Expr) { - $exprToSpecify = $exprToSpecify->class; - } elseif ($findMethods && $exprToSpecify instanceof MethodCall) { - $exprToSpecify = $exprToSpecify->var; - } elseif ($findMethods && $exprToSpecify instanceof StaticCall && $exprToSpecify->class instanceof Expr) { - $exprToSpecify = $exprToSpecify->class; - } else { - break; - } - } + return $result->getScope(); + }); return new EnsuredNonNullabilityResult($scope, $specifiedExpressions); } @@ -1617,6 +2314,7 @@ private function revertNonNullability(MutatingScope $scope, array $specifiedExpr $specifiedExpressionResult->getExpression(), $specifiedExpressionResult->getOriginalType(), $specifiedExpressionResult->getOriginalNativeType(), + $specifiedExpressionResult->getCertainty(), ); } @@ -1626,7 +2324,7 @@ private function revertNonNullability(MutatingScope $scope, array $specifiedExpr private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr { if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) { - if (count($this->earlyTerminatingMethodCalls) > 0) { + if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) { if ($expr instanceof MethodCall) { $methodCalledOnType = $scope->getType($expr->var); } else { @@ -1637,8 +2335,7 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr } } - $directClassNames = TypeUtils::getDirectClassNames($methodCalledOnType); - foreach ($directClassNames as $referencedClass) { + foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) { if (!$this->reflectionProvider->hasClass($referencedClass)) { continue; } @@ -1663,6 +2360,10 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr } } + if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) { + return $expr; + } + $exprType = $scope->getType($expr); if ($exprType instanceof NeverType && $exprType->isExplicit()) { return $expr; @@ -1674,22 +2375,22 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processExprNode(Expr $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context): ExpressionResult + public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context): ExpressionResult { if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { if ($expr instanceof FuncCall) { - $newExpr = new FunctionCallableNode($expr->name, $expr->getAttributes()); + $newExpr = new FunctionCallableNode($expr->name, $expr); } 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()); + $newExpr = new InstantiationCallableNode($expr->class, $expr); } else { throw new ShouldNotHappenException(); } - return $this->processExprNode($newExpr, $scope, $nodeCallback, $context); + return $this->processExprNode($stmt, $newExpr, $scope, $nodeCallback, $context); } $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context); @@ -1697,18 +2398,38 @@ private function processExprNode(Expr $expr, MutatingScope $scope, callable $nod if ($expr instanceof Variable) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->name instanceof Expr) { - return $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + return $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + } elseif (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) { + $impurePoints[] = new ImpurePoint($scope, $expr, 'superglobal', 'access to superglobal variable', true); } } elseif ($expr instanceof Assign || $expr instanceof AssignRef) { $result = $this->processAssignVar( $scope, + $stmt, $expr->var, $expr->expr, $nodeCallback, $context, - function (MutatingScope $scope) use ($expr, $nodeCallback, $context): ExpressionResult { + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult { + $impurePoints = []; if ($expr instanceof AssignRef) { + $referencedExpr = $expr->expr; + while ($referencedExpr instanceof ArrayDimFetch) { + $referencedExpr = $referencedExpr->var; + } + + if ($referencedExpr instanceof PropertyFetch || $referencedExpr instanceof StaticPropertyFetch) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'propertyAssignByRef', + 'property assignment by reference', + false, + ); + } + $scope = $scope->enterExpressionAssign($expr->expr); } @@ -1716,78 +2437,153 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $context = $context->enterRightSideAssign( $expr->var->name, $scope->getType($expr->expr), + $scope->getNativeType($expr->expr), ); } - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); if ($expr instanceof AssignRef) { $scope = $scope->exitExpressionAssign($expr->expr); } - return new ExpressionResult($scope, $hasYield, $throwPoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); }, true, ); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $vars = $this->getAssignedVariables($expr->var); if (count($vars) > 0) { $varChangedScope = false; - $scope = $this->processVarAnnotation($scope, $vars, $expr, $varChangedScope); + $scope = $this->processVarAnnotation($scope, $vars, $stmt, $varChangedScope); if (!$varChangedScope) { - $scope = $this->processStmtVarAnnotation($scope, new Node\Stmt\Expression($expr, [ - 'comments' => $expr->getAttribute('comments'), - ]), null); + $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback); } } } elseif ($expr instanceof Expr\AssignOp) { $result = $this->processAssignVar( $scope, + $stmt, $expr->var, $expr, $nodeCallback, $context, - fn (MutatingScope $scope): ExpressionResult => $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()), + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult { + $originalScope = $scope; + if ($expr instanceof Expr\AssignOp\Coalesce) { + $scope = $scope->filterByFalseyValue( + new BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))), + ); + } + + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + if ($expr instanceof Expr\AssignOp\Coalesce) { + return new ExpressionResult( + $result->getScope()->mergeWith($originalScope), + $result->hasYield(), + $result->getThrowPoints(), + $result->getImpurePoints(), + ); + } + + return $result; + }, $expr instanceof Expr\AssignOp\Coalesce, ); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + if ( + ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) && + !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + ) { + $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); + } } elseif ($expr instanceof FuncCall) { $parametersAcceptor = null; $functionReflection = null; $throwPoints = []; + $impurePoints = []; if ($expr->name instanceof Expr) { $nameType = $scope->getType($expr->name); - if ($nameType->isCallable()->yes()) { + if (!$nameType->isCallable()->no()) { $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), $nameType->getCallableParametersAcceptors($scope), + null, ); } - $nameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); - $throwPoints = $nameResult->getThrowPoints(); + + $nameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $scope = $nameResult->getScope(); + $throwPoints = $nameResult->getThrowPoints(); + $impurePoints = $nameResult->getImpurePoints(); + if ( + $nameType->isObject()->yes() + && $nameType->isCallable()->yes() + && (new ObjectType(Closure::class))->isSuperTypeOf($nameType)->no() + ) { + $invokeResult = $this->processExprNode( + $stmt, + new MethodCall($expr->name, '__invoke', $expr->getArgs(), $expr->getAttributes()), + $scope, + static function (): void { + }, + $context->enterDeep(), + ); + $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints()); + } elseif ($parametersAcceptor instanceof CallableParametersAcceptor) { + $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints()); + if (!$this->implicitThrows) { + $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit())); + } + $throwPoints = array_merge($throwPoints, $callableThrowPoints); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $parametersAcceptor->getImpurePoints())); + + $scope = $this->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables()); + } } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) { $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'functionCall', + 'call to unknown function', + false, ); } - $result = $this->processArgs($functionReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; + } + $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); - if (isset($functionReflection)) { + if ($functionReflection !== null) { $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); if ($functionThrowPoint !== null) { $throwPoints[] = $functionThrowPoint; @@ -1797,7 +2593,14 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } if ( - isset($functionReflection) + $parametersAcceptor instanceof ClosureType && count($parametersAcceptor->getImpurePoints()) > 0 + && $scope->isInClass() + ) { + $scope = $scope->invalidateExpression(new Variable('this'), true); + } + + if ( + $functionReflection !== null && in_array($functionReflection->getName(), ['json_encode', 'json_decode'], true) ) { $scope = $scope->invalidateExpression(new FuncCall(new Name('json_last_error'), [])) @@ -1807,109 +2610,65 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } if ( - isset($functionReflection) + $functionReflection !== null + && $functionReflection->getName() === 'file_put_contents' + && count($expr->getArgs()) > 0 + ) { + $scope = $scope->invalidateExpression(new FuncCall(new Name('file_get_contents'), [$expr->getArgs()[0]])) + ->invalidateExpression(new FuncCall(new Name\FullyQualified('file_get_contents'), [$expr->getArgs()[0]])); + } + + if ( + $functionReflection !== null && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true) && count($expr->getArgs()) >= 1 ) { $arrayArg = $expr->getArgs()[0]->value; - $constantArrays = TypeUtils::getConstantArrays($scope->getType($arrayArg)); - $scope = $scope->invalidateExpression($arrayArg); - if (count($constantArrays) > 0) { - $resultArrayTypes = []; - foreach ($constantArrays as $constantArray) { - if ($functionReflection->getName() === 'array_pop') { - $resultArrayTypes[] = $constantArray->removeLast(); - } else { - $resultArrayTypes[] = $constantArray->removeFirst(); - } - } + $arrayArgType = $scope->getType($arrayArg); + $arrayArgNativeType = $scope->getNativeType($arrayArg); - $scope = $scope->specifyExpressionType( - $arrayArg, - TypeCombinator::union(...$resultArrayTypes), - ); - } else { - $arrays = TypeUtils::getAnyArrays($scope->getType($arrayArg)); - if (count($arrays) > 0) { - $scope = $scope->specifyExpressionType($arrayArg, TypeCombinator::union(...$arrays)); - } - } + $isArrayPop = $functionReflection->getName() === 'array_pop'; + $scope = $scope->invalidateExpression($arrayArg)->assignExpression( + $arrayArg, + $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(), + $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(), + ); } if ( - isset($functionReflection) + $functionReflection !== null && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true) && count($expr->getArgs()) >= 2 ) { - $argumentTypes = []; - foreach (array_slice($expr->getArgs(), 1) as $callArg) { - $callArgType = $scope->getType($callArg->value); - if ($callArg->unpack) { - $iterableValueType = $callArgType->getIterableValueType(); - if ($iterableValueType instanceof UnionType) { - foreach ($iterableValueType->getTypes() as $innerType) { - $argumentTypes[] = $innerType; - } - } else { - $argumentTypes[] = $iterableValueType; - } - continue; - } - - $argumentTypes[] = $callArgType; - } + $arrayType = $this->getArrayFunctionAppendingType($functionReflection, $scope, $expr); + $arrayNativeType = $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $expr); $arrayArg = $expr->getArgs()[0]->value; - $originalArrayType = $scope->getType($arrayArg); - $constantArrays = TypeUtils::getConstantArrays($originalArrayType); - if ( - $functionReflection->getName() === 'array_push' - || ($originalArrayType->isArray()->yes() && count($constantArrays) === 0) - ) { - $arrayType = $originalArrayType; - foreach ($argumentTypes as $argType) { - $arrayType = $arrayType->setOffsetValueType(null, $argType); - } - - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType($arrayArg, TypeCombinator::intersect($arrayType, new NonEmptyArrayType())); - } elseif (count($constantArrays) > 0) { - $defaultArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($argumentTypes as $argType) { - $defaultArrayBuilder->setOffsetValueType(null, $argType); - } + $scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType); + } - $defaultArrayType = $defaultArrayBuilder->getArray(); - - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayType = $defaultArrayType; - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - $valueType = $constantArray->getValueTypes()[$i]; - if ($keyType instanceof ConstantIntegerType) { - $keyType = null; - } - $arrayType = $arrayType->setOffsetValueType($keyType, $valueType); - } - $arrayTypes[] = $arrayType; - } - - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( - $arrayArg, - TypeCombinator::union(...$arrayTypes), - ); - } - } + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) + ) { + $scope = $scope->assignVariable('http_response_header', TypeCombinator::intersect(new ArrayType(new IntegerType(), new StringType()), new AccessoryArrayListType()), new ArrayType(new IntegerType(), new StringType()), TrinaryLogic::createYes()); + } if ( - isset($functionReflection) - && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) + $functionReflection !== null + && $functionReflection->getName() === 'shuffle' ) { - $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType())); + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->assignExpression( + $arrayArg, + $scope->getType($arrayArg)->shuffleArray(), + $scope->getNativeType($arrayArg)->shuffleArray(), + ); } if ( - isset($functionReflection) + $functionReflection !== null && $functionReflection->getName() === 'array_splice' && count($expr->getArgs()) >= 1 ) { @@ -1917,28 +2676,97 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $arrayArgType = $scope->getType($arrayArg); $valueType = $arrayArgType->getIterableValueType(); if (count($expr->getArgs()) >= 4) { - $valueType = TypeCombinator::union($valueType, $scope->getType($expr->getArgs()[3]->value)->getIterableValueType()); + $replacementType = $scope->getType($expr->getArgs()[3]->value)->toArray(); + $valueType = TypeCombinator::union($valueType, $replacementType->getIterableValueType()); } - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( + $scope = $scope->invalidateExpression($arrayArg)->assignExpression( $arrayArg, new ArrayType($arrayArgType->getIterableKeyType(), $valueType), + new ArrayType($arrayArgType->getIterableKeyType(), $valueType), ); } - if (isset($functionReflection) && $functionReflection->getName() === 'extract') { - $scope = $scope->afterExtractCall(); + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true) + && count($expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->assignExpression( + $arrayArg, + $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), + $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)), + ); } - if (isset($functionReflection) && ($functionReflection->getName() === 'clearstatcache' || $functionReflection->getName() === 'unlink')) { - $scope = $scope->afterClearstatcacheCall(); + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true) + && count($expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->assignExpression( + $arrayArg, + $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), + $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)), + ); } - if (isset($functionReflection) && $functionReflection->hasSideEffects()->yes()) { - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); + if ( + $functionReflection !== null + && $functionReflection->getName() === 'extract' + ) { + $extractedArg = $expr->getArgs()[0]->value; + $extractedType = $scope->getType($extractedArg); + $constantArrays = $extractedType->getConstantArrays(); + if (count($constantArrays) > 0) { + $properties = []; + $optionalProperties = []; + $refCount = []; + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + if ($keyType->isString()->no()) { + // integers as variable names not allowed + continue; + } + $key = (string) $keyType->getValue(); + $valueType = $constantArray->getValueTypes()[$i]; + $optional = $constantArray->isOptionalKey($i); + if ($optional) { + $optionalProperties[] = $key; + } + if (isset($properties[$key])) { + $properties[$key] = TypeCombinator::union($properties[$key], $valueType); + $refCount[$key]++; + } else { + $properties[$key] = $valueType; + $refCount[$key] = 1; + } + } + } + foreach ($properties as $name => $type) { + $optional = in_array($name, $optionalProperties, true) || $refCount[$name] < count($constantArrays); + $scope = $scope->assignVariable($name, $type, $type, $optional ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes()); + } + } else { + $scope = $scope->afterExtractCall(); } } + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['clearstatcache', 'unlink'], true) + ) { + $scope = $scope->afterClearstatcacheCall(); + } + + if ( + $functionReflection !== null + && str_starts_with($functionReflection->getName(), 'openssl') + ) { + $scope = $scope->afterOpenSslCall($functionReflection->getName()); + } + } elseif ($expr instanceof MethodCall) { $originalScope = $scope; if ( @@ -1947,24 +2775,28 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression && strtolower($expr->name->name) === 'call' && isset($expr->getArgs()[0]) ) { - $closureCallScope = $scope->enterClosureCall($scope->getType($expr->getArgs()[0]->value)); + $closureCallScope = $scope->enterClosureCall( + $scope->getType($expr->getArgs()[0]->value), + $scope->getNativeType($expr->getArgs()[0]->value), + ); } - $result = $this->processExprNode($expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if (isset($closureCallScope)) { $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); } $parametersAcceptor = null; $methodReflection = null; + $calledOnType = $scope->getType($expr->var); if ($expr->name instanceof Expr) { - $methodNameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $methodNameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = array_merge($throwPoints, $methodNameResult->getThrowPoints()); $scope = $methodNameResult->getScope(); } else { - $calledOnType = $scope->getType($expr->var); $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); if ($methodReflection !== null) { @@ -1972,21 +2804,83 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope, $expr->getArgs(), $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ); + $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } } } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); + + if ($methodReflection !== null) { + $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + 'call to unknown method', + false, + ); + } + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; + } + + $result = $this->processArgs( + $stmt, + $methodReflection, + $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null, + $parametersAcceptor, + $expr, + $scope, + $nodeCallback, + $context, + ); $scope = $result->getScope(); + if ($methodReflection !== null) { $hasSideEffects = $methodReflection->hasSideEffects(); - if ($hasSideEffects->yes()) { + if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') { + $nodeCallback(new InvalidateExprNode($expr->var), $scope); $scope = $scope->invalidateExpression($expr->var, true); - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); + } + if ($parametersAcceptor !== null && !$methodReflection->isStatic()) { + $selfOutType = $methodReflection->getSelfOutType(); + if ($selfOutType !== null) { + $scope = $scope->assignExpression( + $expr->var, + TemplateTypeHelper::resolveTemplateTypes( + $selfOutType, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createCovariant(), + ), + $scope->getNativeType($expr->var), + ); + } + } + + if ( + $scope->isInClass() + && $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName() + /*&& ( + // should not be allowed but in practice has to be + $scope->getClassReflection()->isFinal() + || $methodReflection->isFinal()->yes() + || $methodReflection->isPrivate() + )*/ + && TypeUtils::findThisType($calledOnType) !== null + ) { + $calledMethodScope = $this->processCalledMethod($methodReflection); + if ($calledMethodScope !== null) { + $scope = $scope->mergeInitializedProperties($calledMethodScope); } } } else { @@ -1994,36 +2888,40 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ($expr instanceof Expr\NullsafeMethodCall) { - $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $expr->var); - $exprResult = $this->processExprNode(new MethodCall($expr->var, $expr->name, $expr->args, $expr->getAttributes()), $nonNullabilityResult->getScope(), $nodeCallback, $context); + $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); + $exprResult = $this->processExprNode($stmt, new MethodCall($expr->var, $expr->name, $expr->args, array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); return new ExpressionResult( $scope, $exprResult->hasYield(), $exprResult->getThrowPoints(), + $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticCall) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->class instanceof Expr) { - $objectClasses = TypeUtils::getDirectClassNames($scope->getType($expr->class)); + $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); if (count($objectClasses) !== 1) { - $objectClasses = TypeUtils::getDirectClassNames($scope->getType(new New_($expr->class))); + $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); } if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode(new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, static function (): void { + $objectExprResult = $this->processExprNode($stmt, new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, static function (): void { }, $context->enterDeep()); $additionalThrowPoints = $objectExprResult->getThrowPoints(); } else { $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; } - $classResult = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); + $classResult = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $classResult->getImpurePoints()); foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } @@ -2033,395 +2931,559 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $parametersAcceptor = null; $methodReflection = null; if ($expr->name instanceof Expr) { - $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } elseif ($expr->class instanceof Name) { - $className = $scope->resolveName($expr->class); - if ($this->reflectionProvider->hasClass($className)) { - $classReflection = $this->reflectionProvider->getClass($className); - if (is_string($expr->name)) { - $methodName = $expr->name; - } else { - $methodName = $expr->name->name; + $classType = $scope->resolveTypeByName($expr->class); + $methodName = $expr->name->name; + if ($classType->hasMethod($methodName)->yes()) { + $methodReflection = $classType->getMethod($methodName, $scope); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), + ); + + $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); + if ($methodThrowPoint !== null) { + $throwPoints[] = $methodThrowPoint; } - if ($classReflection->hasMethod($methodName)) { - $methodReflection = $classReflection->getMethod($methodName, $scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $methodReflection->getVariants(), - ); - $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $expr, $scope); - if ($methodThrowPoint !== null) { - $throwPoints[] = $methodThrowPoint; - } - if ( - $classReflection->getName() === 'Closure' - && strtolower($methodName) === 'bind' - ) { - $thisType = null; - if (isset($expr->getArgs()[1])) { - $argType = $scope->getType($expr->getArgs()[1]->value); - if ($argType instanceof NullType) { - $thisType = null; - } else { - $thisType = $argType; - } + + $declaringClass = $methodReflection->getDeclaringClass(); + if ( + $declaringClass->getName() === 'Closure' + && strtolower($methodName) === 'bind' + ) { + $thisType = null; + $nativeThisType = null; + if (isset($expr->getArgs()[1])) { + $argType = $scope->getType($expr->getArgs()[1]->value); + if ($argType->isNull()->yes()) { + $thisType = null; + } else { + $thisType = $argType; + } + + $nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value); + if ($nativeArgType->isNull()->yes()) { + $nativeThisType = null; + } else { + $nativeThisType = $nativeArgType; } - $scopeClass = 'static'; - if (isset($expr->getArgs()[2])) { - $argValue = $expr->getArgs()[2]->value; - $argValueType = $scope->getType($argValue); - - $directClassNames = TypeUtils::getDirectClassNames($argValueType); - if (count($directClassNames) === 1) { - $scopeClass = $directClassNames[0]; - $thisType = new ObjectType($scopeClass); - } elseif ($argValueType instanceof ConstantStringType) { - $scopeClass = $argValueType->getValue(); - $thisType = new ObjectType($scopeClass); - } elseif ( - $argValueType instanceof GenericClassStringType - && $argValueType->getGenericType() instanceof TypeWithClassName - ) { - $scopeClass = $argValueType->getGenericType()->getClassName(); - $thisType = $argValueType->getGenericType(); + } + $scopeClasses = ['static']; + if (isset($expr->getArgs()[2])) { + $argValue = $expr->getArgs()[2]->value; + $argValueType = $scope->getType($argValue); + + $directClassNames = $argValueType->getObjectClassNames(); + if (count($directClassNames) > 0) { + $scopeClasses = $directClassNames; + $thisTypes = []; + foreach ($directClassNames as $directClassName) { + $thisTypes[] = new ObjectType($directClassName); } + $thisType = TypeCombinator::union(...$thisTypes); + } else { + $thisType = $argValueType->getClassStringObjectType(); + $scopeClasses = $thisType->getObjectClassNames(); } - $closureBindScope = $scope->enterClosureBind($thisType, $scopeClass); } - } else { - $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses); } } else { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null); + + if ($methodReflection !== null) { + $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + 'call to unknown method', + false, + ); + } + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; + } + $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context, $closureBindScope ?? null); $scope = $result->getScope(); $scopeFunction = $scope->getFunction(); + if ( $methodReflection !== null - && !$methodReflection->isStatic() - && $methodReflection->hasSideEffects()->yes() - && $scopeFunction instanceof MethodReflection - && !$scopeFunction->isStatic() - && $scope->isInClass() && ( - $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName() - || $scope->getClassReflection()->isSubclassOf($methodReflection->getDeclaringClass()->getName()) + $methodReflection->hasSideEffects()->yes() + || ( + !$methodReflection->isStatic() + && $methodReflection->getName() === '__construct' + ) ) + && $scope->isInClass() + && $scope->getClassReflection()->is($methodReflection->getDeclaringClass()->getName()) ) { $scope = $scope->invalidateExpression(new Variable('this'), true); } - if ($methodReflection !== null) { - if ($methodReflection->hasSideEffects()->yes()) { - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); + if ( + $methodReflection !== null + && !$methodReflection->isStatic() + && $methodReflection->getName() === '__construct' + && $scopeFunction instanceof MethodReflection + && !$scopeFunction->isStatic() + && $scope->isInClass() + && $scope->getClassReflection()->isSubclassOfClass($methodReflection->getDeclaringClass()) + ) { + $thisType = $scope->getType(new Variable('this')); + $methodClassReflection = $methodReflection->getDeclaringClass(); + foreach ($methodClassReflection->getNativeReflection()->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED) as $property) { + if (!$property->isPromoted() || $property->getDeclaringClass()->getName() !== $methodClassReflection->getName()) { + continue; } + + $scope = $scope->assignInitializedProperty($thisType, $property->getName()); } } $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ($expr instanceof PropertyFetch) { - $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep()); + $scopeBeforeVar = $scope; + $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if ($expr->name instanceof Expr) { - $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); + if ($this->phpVersion->supportsPropertyHooks()) { + $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + } + } else { + $propertyName = $expr->name->toString(); + $propertyHolderType = $scopeBeforeVar->getType($expr->var); + $propertyReflection = $scopeBeforeVar->getPropertyReflection($propertyHolderType, $propertyName); + if ($propertyReflection !== null && $this->phpVersion->supportsPropertyHooks()) { + $propertyDeclaringClass = $propertyReflection->getDeclaringClass(); + if ($propertyDeclaringClass->hasNativeProperty($propertyName)) { + $nativeProperty = $propertyDeclaringClass->getNativeProperty($propertyName); + $throwPoints = array_merge($throwPoints, $this->getPropertyReadThrowPointsFromGetHook($scopeBeforeVar, $expr, $nativeProperty)); + } + } } } elseif ($expr instanceof Expr\NullsafePropertyFetch) { - $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $expr->var); - $exprResult = $this->processExprNode(new PropertyFetch($expr->var, $expr->name, $expr->getAttributes()), $nonNullabilityResult->getScope(), $nodeCallback, $context); + $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); + $exprResult = $this->processExprNode($stmt, new PropertyFetch($expr->var, $expr->name, array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); return new ExpressionResult( $scope, $exprResult->hasYield(), $exprResult->getThrowPoints(), + $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticPropertyFetch) { $hasYield = false; $throwPoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $expr, + 'staticPropertyAccess', + 'static property access', + true, + ), + ]; if ($expr->class instanceof Expr) { - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } if ($expr->name instanceof Expr) { - $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof Expr\Closure) { - return $this->processClosureNode($expr, $scope, $nodeCallback, $context, null); - } elseif ($expr instanceof Expr\ClosureUse) { - $this->processExprNode($expr->var, $scope, $nodeCallback, $context); - $hasYield = false; - $throwPoints = []; + $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null); + + return new ExpressionResult( + $processClosureResult->getScope(), + false, + [], + [], + ); } elseif ($expr instanceof Expr\ArrowFunction) { - return $this->processArrowFunctionNode($expr, $scope, $nodeCallback, $context, null); + $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, null); + return new ExpressionResult( + $result->getScope(), + $result->hasYield(), + [], + [], + ); } elseif ($expr instanceof ErrorSuppress) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } elseif ($expr instanceof Exit_) { $hasYield = false; $throwPoints = []; + $kind = $expr->getAttribute('kind', Exit_::KIND_EXIT); + $identifier = $kind === Exit_::KIND_DIE ? 'die' : 'exit'; + $impurePoints = [ + new ImpurePoint($scope, $expr, $identifier, $identifier, true), + ]; if ($expr->expr !== null) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } - } elseif ($expr instanceof Node\Scalar\Encapsed) { + } elseif ($expr instanceof Node\Scalar\InterpolatedString) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($expr->parts as $part) { - $result = $this->processExprNode($part, $scope, $nodeCallback, $context->enterDeep()); + if (!$part instanceof Expr) { + continue; + } + $result = $this->processExprNode($stmt, $part, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof ArrayDimFetch) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->dim !== null) { - $result = $this->processExprNode($expr->dim, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->dim, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } - $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } elseif ($expr instanceof Array_) { $itemNodes = []; $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); - if ($arrayItem === null) { - continue; + $nodeCallback($arrayItem, $scope); + if ($arrayItem->key !== null) { + $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $keyResult->hasYield(); + $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $scope = $keyResult->getScope(); } - $result = $this->processExprNode($arrayItem, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - $scope = $result->getScope(); + + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $valueResult->hasYield(); + $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); + $scope = $valueResult->getScope(); } $nodeCallback(new LiteralArrayNode($expr, $itemNodes), $scope); - } elseif ($expr instanceof ArrayItem) { - $hasYield = false; - $throwPoints = []; - if ($expr->key !== null) { - $result = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $scope = $result->getScope(); - } - $result = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - $scope = $result->getScope(); } elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) { - $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); - $rightResult = $this->processExprNode($expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context); - $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); + $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context); + $rightExprType = $rightResult->getScope()->getType($expr->right); + if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { + $leftMergedWithRightScope = $leftResult->getFalseyScope(); + } else { + $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); + } - $nodeCallback(new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope); + $this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope, $context); return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), + array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr), ); } elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) { - $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); - $rightResult = $this->processExprNode($expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context); - $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); + $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context); + $rightExprType = $rightResult->getScope()->getType($expr->right); + if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { + $leftMergedWithRightScope = $leftResult->getTruthyScope(); + } else { + $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); + } - $nodeCallback(new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope); + $this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope, $context); return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), + array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr), ); } elseif ($expr instanceof Coalesce) { - $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left, false); - - 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(); - } + $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left); + $condScope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left); + $condResult = $this->processExprNode($stmt, $expr->left, $condScope, $nodeCallback, $context->enterDeep()); + $scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left); + + $rightScope = $scope->filterByFalseyValue($expr); + $rightResult = $this->processExprNode($stmt, $expr->right, $rightScope, $nodeCallback, $context->enterDeep()); + $rightExprType = $scope->getType($expr->right); + if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { + $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left])); } else { - $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->left); + $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]))->mergeWith($rightResult->getScope()); } - $result = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $scope = $result->getScope(); - $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); - 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()); - $scope = $result->getScope()->mergeWith($scope); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $hasYield = $condResult->hasYield() || $rightResult->hasYield(); + $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()); + $impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()); } elseif ($expr instanceof BinaryOp) { - $result = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); - $result = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep()); + $impurePoints = $result->getImpurePoints(); + $result = $this->processExprNode($stmt, $expr->right, $scope, $nodeCallback, $context->enterDeep()); + if ( + ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && + !$scope->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + ) { + $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); + } $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ($expr instanceof Expr\Include_) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require', + in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require', + true, + ); + $hasYield = $result->hasYield(); + $scope = $result->getScope()->afterExtractCall(); + } elseif ($expr instanceof Expr\Print_) { + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint($scope, $expr, 'print', 'print', true); + $hasYield = $result->hasYield(); + + $scope = $result->getScope(); + } elseif ($expr instanceof Cast\String_) { + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $hasYield = $result->hasYield(); + + $exprType = $scope->getType($expr->expr); + $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); + if ($toStringMethod !== null) { + if (!$toStringMethod->hasSideEffects()->no()) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()), + $toStringMethod->isPure()->no(), + ); + } + } + $scope = $result->getScope(); } elseif ( $expr instanceof Expr\BitwiseNot || $expr instanceof Cast || $expr instanceof Expr\Clone_ - || $expr instanceof Expr\Print_ || $expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus ) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $hasYield = $result->hasYield(); $scope = $result->getScope(); } elseif ($expr instanceof Expr\Eval_) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint($scope, $expr, 'eval', 'eval', true); $hasYield = $result->hasYield(); $scope = $result->getScope(); } elseif ($expr instanceof Expr\YieldFrom) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'yieldFrom', + 'yield from', + true, + ); $hasYield = true; $scope = $result->getScope(); } elseif ($expr instanceof BooleanNot) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } elseif ($expr instanceof Expr\ClassConstFetch) { - $hasYield = false; - $throwPoints = []; if ($expr->class instanceof Expr) { - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + } else { + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $nodeCallback($expr->class, $scope); + } + + if ($expr->name instanceof Expr) { + $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + $scope = $result->getScope(); + $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + } else { + $nodeCallback($expr->name, $scope); } } elseif ($expr instanceof Expr\Empty_) { - $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr, true); - $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->expr); - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr); + $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); - $scope = $this->lookForExitVariableAssign($scope, $expr->expr); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr); } elseif ($expr instanceof Expr\Isset_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $nonNullabilityResults = []; foreach ($expr->vars as $var) { - $nonNullabilityResult = $this->ensureNonNullability($scope, $var, true); - $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $var); - $result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep()); + $nonNullabilityResult = $this->ensureNonNullability($scope, $var); + $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var); + $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $nonNullabilityResults[] = $nonNullabilityResult; - $scope = $this->lookForExitVariableAssign($scope, $var); + } + foreach (array_reverse($expr->vars) as $var) { + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); } foreach (array_reverse($nonNullabilityResults) as $nonNullabilityResult) { $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); } } elseif ($expr instanceof Instanceof_) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ($expr->class instanceof Expr) { - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } } elseif ($expr instanceof List_) { // only in assign and foreach, processed elsewhere - return new ExpressionResult($scope, false, []); + return new ExpressionResult($scope, false, [], []); } elseif ($expr instanceof New_) { $parametersAcceptor = null; $constructorReflection = null; $hasYield = false; $throwPoints = []; - if ($expr->class instanceof Expr) { - $objectClasses = TypeUtils::getDirectClassNames($scope->getType($expr)); - if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode(new New_(new Name($objectClasses[0])), $scope, static function (): void { - }, $context->enterDeep()); - $additionalThrowPoints = $objectExprResult->getThrowPoints(); + $impurePoints = []; + $className = null; + if ($expr->class instanceof Expr || $expr->class instanceof Name) { + if ($expr->class instanceof Expr) { + $objectClasses = $scope->getType($expr)->getObjectClassNames(); + if (count($objectClasses) === 1) { + $objectExprResult = $this->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, static function (): void { + }, $context->enterDeep()); + $className = $objectClasses[0]; + $additionalThrowPoints = $objectExprResult->getThrowPoints(); + } else { + $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; + } + + $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + foreach ($additionalThrowPoints as $throwPoint) { + $throwPoints[] = $throwPoint; + } } else { - $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; + $className = $scope->resolveName($expr->class); } - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); - $scope = $result->getScope(); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - foreach ($additionalThrowPoints as $throwPoint) { - $throwPoints[] = $throwPoint; - } - } elseif ($expr->class instanceof Class_) { - $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name - $this->processStmtNode($expr->class, $scope, $nodeCallback); - } else { - $className = $scope->resolveName($expr->class); - if ($this->reflectionProvider->hasClass($className)) { + $classReflection = null; + if ($className !== null && $this->reflectionProvider->hasClass($className)) { $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); @@ -2429,8 +3491,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope, $expr->getArgs(), $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), ); - $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $classReflection, $expr, $expr->class, $expr->getArgs(), $scope); + $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, new Name\FullyQualified($className), $expr->getArgs(), $scope); if ($constructorThrowPoint !== null) { $throwPoints[] = $constructorThrowPoint; } @@ -2438,21 +3501,86 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } else { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } - } - $result = $this->processArgs($constructorReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); - $scope = $result->getScope(); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - } elseif ( - $expr instanceof Expr\PreInc - || $expr instanceof Expr\PostInc - || $expr instanceof Expr\PreDec - || $expr instanceof Expr\PostDec - ) { - $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep()); - $scope = $result->getScope(); - $hasYield = $result->hasYield(); - $throwPoints = []; + + if ($constructorReflection !== null) { + if (!$constructorReflection->hasSideEffects()->no()) { + $certain = $constructorReflection->isPure()->no(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'new', + sprintf('instantiation of class %s', $constructorReflection->getDeclaringClass()->getDisplayName()), + $certain, + ); + } + } elseif ($classReflection === null) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'new', + 'instantiation of unknown class', + false, + ); + } + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; + } + + } else { + $classReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name + $constructorResult = null; + $this->processStmtNode($expr->class, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $classReflection, &$constructorResult): void { + $nodeCallback($node, $scope); + if (!$node instanceof MethodReturnStatementsNode) { + return; + } + if ($constructorResult !== null) { + return; + } + $currentClassReflection = $node->getClassReflection(); + if ($currentClassReflection->getName() !== $classReflection->getName()) { + return; + } + if (!$currentClassReflection->hasConstructor()) { + return; + } + if ($currentClassReflection->getConstructor()->getName() !== $node->getMethodReflection()->getName()) { + return; + } + $constructorResult = $node; + }, StatementContext::createTopLevel()); + if ($constructorResult !== null) { + $throwPoints = array_merge($throwPoints, $constructorResult->getStatementResult()->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $constructorResult->getImpurePoints()); + } + if ($classReflection->hasConstructor()) { + $constructorReflection = $classReflection->getConstructor(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), + ); + } + } + + $result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context); + $scope = $result->getScope(); + $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + } elseif ( + $expr instanceof Expr\PreInc + || $expr instanceof Expr\PostInc + || $expr instanceof Expr\PreDec + || $expr instanceof Expr\PostDec + ) { + $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $newExpr = $expr; if ($expr instanceof Expr\PostInc) { @@ -2463,41 +3591,64 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope = $this->processAssignVar( $scope, + $stmt, $expr->var, $newExpr, static function (Node $node, Scope $scope) use ($nodeCallback): void { - if (!$node instanceof PropertyAssignNode) { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { return; } $nodeCallback($node, $scope); }, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), false, )->getScope(); } elseif ($expr instanceof Ternary) { - $ternaryCondResult = $this->processExprNode($expr->cond, $scope, $nodeCallback, $context->enterDeep()); + $ternaryCondResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $ternaryCondResult->getThrowPoints(); + $impurePoints = $ternaryCondResult->getImpurePoints(); $ifTrueScope = $ternaryCondResult->getTruthyScope(); $ifFalseScope = $ternaryCondResult->getFalseyScope(); - + $ifTrueType = null; if ($expr->if !== null) { - $ifResult = $this->processExprNode($expr->if, $ifTrueScope, $nodeCallback, $context); + $ifResult = $this->processExprNode($stmt, $expr->if, $ifTrueScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints()); $ifTrueScope = $ifResult->getScope(); + $ifTrueType = $ifTrueScope->getType($expr->if); } - $elseResult = $this->processExprNode($expr->else, $ifFalseScope, $nodeCallback, $context); + $elseResult = $this->processExprNode($stmt, $expr->else, $ifFalseScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints()); $ifFalseScope = $elseResult->getScope(); - $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + $condType = $scope->getType($expr->cond); + if ($condType->isTrue()->yes()) { + $finalScope = $ifTrueScope; + } elseif ($condType->isFalse()->yes()) { + $finalScope = $ifFalseScope; + } else { + if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { + $finalScope = $ifFalseScope; + } else { + $ifFalseType = $ifFalseScope->getType($expr->else); + + if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { + $finalScope = $ifTrueScope; + } else { + $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + } + } + } return new ExpressionResult( $finalScope, $ternaryCondResult->hasYield(), $throwPoints, + $impurePoints, static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr), ); @@ -2506,33 +3657,186 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $throwPoints = [ ThrowPoint::createImplicit($scope, $expr), ]; + $impurePoints = [ + new ImpurePoint( + $scope, + $expr, + 'yield', + 'yield', + true, + ), + ]; if ($expr->key !== null) { - $keyResult = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep()); + $keyResult = $this->processExprNode($stmt, $expr->key, $scope, $nodeCallback, $context->enterDeep()); $scope = $keyResult->getScope(); $throwPoints = $keyResult->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); } if ($expr->value !== null) { - $valueResult = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep()); + $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $nodeCallback, $context->enterDeep()); $scope = $valueResult->getScope(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); } $hasYield = true; } elseif ($expr instanceof Expr\Match_) { $deepContext = $context->enterDeep(); - $condResult = $this->processExprNode($expr->cond, $scope, $nodeCallback, $deepContext); + $condType = $scope->getType($expr->cond); + $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $deepContext); $scope = $condResult->getScope(); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); - $matchScope = $scope; + $impurePoints = $condResult->getImpurePoints(); + $matchScope = $scope->enterMatch($expr); $armNodes = []; - foreach ($expr->arms as $arm) { + $hasDefaultCond = false; + $hasAlwaysTrueCond = false; + $arms = $expr->arms; + if ($condType->isEnum()->yes()) { + // enum match analysis would work even without this if branch + // but would be much slower + // this avoids using ObjectType::$subtractedType which is slow for huge enums + // because of repeated union type normalization + $enumCases = $condType->getEnumCases(); + if (count($enumCases) > 0) { + $indexedEnumCases = []; + foreach ($enumCases as $enumCase) { + $indexedEnumCases[strtolower($enumCase->getClassName())][$enumCase->getEnumCaseName()] = $enumCase; + } + $unusedIndexedEnumCases = $indexedEnumCases; + foreach ($arms as $i => $arm) { + if ($arm->conds === null) { + continue; + } + + $condNodes = []; + $conditionCases = []; + $conditionExprs = []; + foreach ($arm->conds as $cond) { + if (!$cond instanceof Expr\ClassConstFetch) { + continue 2; + } + if (!$cond->class instanceof Name) { + continue 2; + } + if (!$cond->name instanceof Node\Identifier) { + continue 2; + } + $fetchedClassName = $scope->resolveName($cond->class); + $loweredFetchedClassName = strtolower($fetchedClassName); + if (!array_key_exists($loweredFetchedClassName, $indexedEnumCases)) { + continue 2; + } + + if (!array_key_exists($loweredFetchedClassName, $unusedIndexedEnumCases)) { + throw new ShouldNotHappenException(); + } + + $caseName = $cond->name->toString(); + if (!array_key_exists($caseName, $indexedEnumCases[$loweredFetchedClassName])) { + continue 2; + } + + $enumCase = $indexedEnumCases[$loweredFetchedClassName][$caseName]; + $conditionCases[] = $enumCase; + $armConditionScope = $matchScope; + if (!array_key_exists($caseName, $unusedIndexedEnumCases[$loweredFetchedClassName])) { + // force "always false" + $armConditionScope = $armConditionScope->removeTypeFromExpression( + $expr->cond, + $enumCase, + ); + } else { + $unusedCasesCount = 0; + foreach ($unusedIndexedEnumCases as $cases) { + $unusedCasesCount += count($cases); + } + if ($unusedCasesCount === 1) { + $hasAlwaysTrueCond = true; + + // force "always true" + $armConditionScope = $armConditionScope->addTypeToExpression( + $expr->cond, + $enumCase, + ); + } + } + + $this->processExprNode($stmt, $cond, $armConditionScope, $nodeCallback, $deepContext); + + $condNodes[] = new MatchExpressionArmCondition( + $cond, + $armConditionScope, + $cond->getStartLine(), + ); + $conditionExprs[] = $cond; + + unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]); + } + + $conditionCasesCount = count($conditionCases); + if ($conditionCasesCount === 0) { + throw new ShouldNotHappenException(); + } elseif ($conditionCasesCount === 1) { + $conditionCaseType = $conditionCases[0]; + } else { + $conditionCaseType = new UnionType($conditionCases); + } + + $filteringExpr = $this->getFilteringExprForMatchArm($expr, $conditionExprs); + $matchArmBodyScope = $matchScope->addTypeToExpression( + $expr->cond, + $conditionCaseType, + )->filterByTruthyValue($filteringExpr); + $matchArmBody = new MatchExpressionArmBody($matchArmBodyScope, $arm->body); + $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine()); + + $armResult = $this->processExprNode( + $stmt, + $arm->body, + $matchArmBodyScope, + $nodeCallback, + ExpressionContext::createTopLevel(), + ); + $armScope = $armResult->getScope(); + $scope = $scope->mergeWith($armScope); + $hasYield = $hasYield || $armResult->hasYield(); + $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); + + unset($arms[$i]); + } + + $remainingCases = []; + foreach ($unusedIndexedEnumCases as $cases) { + foreach ($cases as $case) { + $remainingCases[] = $case; + } + } + + $remainingCasesCount = count($remainingCases); + if ($remainingCasesCount === 0) { + $remainingType = new NeverType(); + } elseif ($remainingCasesCount === 1) { + $remainingType = $remainingCases[0]; + } else { + $remainingType = new UnionType($remainingCases); + } + + $matchScope = $matchScope->addTypeToExpression($expr->cond, $remainingType); + } + } + foreach ($arms as $i => $arm) { if ($arm->conds === null) { - $armResult = $this->processExprNode($arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); + $hasDefaultCond = true; + $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); + $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); + $armResult = $this->processExprNode($stmt, $arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); $matchScope = $armResult->getScope(); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); $scope = $scope->mergeWith($matchScope); - $armNodes[] = new MatchExpressionArm([], $arm->getLine()); continue; } @@ -2540,29 +3844,36 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { throw new ShouldNotHappenException(); } - $filteringExpr = null; + $filteringExprs = []; $armCondScope = $matchScope; $condNodes = []; foreach ($arm->conds as $armCond) { - $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getLine()); - $armCondResult = $this->processExprNode($armCond, $armCondScope, $nodeCallback, $deepContext); + $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getStartLine()); + $armCondResult = $this->processExprNode($stmt, $armCond, $armCondScope, $nodeCallback, $deepContext); $hasYield = $hasYield || $armCondResult->hasYield(); $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints()); $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond); - $armCondScope = $armCondResult->getScope()->filterByFalseyValue($armCondExpr); - if ($filteringExpr === null) { - $filteringExpr = $armCondExpr; - continue; + $armCondResultScope = $armCondResult->getScope(); + $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr); + if ($armCondType->isTrue()->yes()) { + $hasAlwaysTrueCond = true; } - - $filteringExpr = new BinaryOp\BooleanOr($filteringExpr, $armCondExpr); + $armCondScope = $armCondResult->getScope()->filterByFalseyValue($armCondExpr); + $filteringExprs[] = $armCond; } - $armNodes[] = new MatchExpressionArm($condNodes, $arm->getLine()); + $filteringExpr = $this->getFilteringExprForMatchArm($expr, $filteringExprs); + + $bodyScope = $this->processExprNode($stmt, $filteringExpr, $matchScope, static function (): void { + }, $deepContext)->getTruthyScope(); + $matchArmBody = new MatchExpressionArmBody($bodyScope, $arm->body); + $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine()); $armResult = $this->processExprNode( + $stmt, $arm->body, - $matchScope->filterByTruthyValue($filteringExpr), + $bodyScope, $nodeCallback, ExpressionContext::createTopLevel(), ); @@ -2570,73 +3881,272 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope = $scope->mergeWith($armScope); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); $matchScope = $matchScope->filterByFalseyValue($filteringExpr); } - $nodeCallback(new MatchExpressionNode($expr->cond, $armNodes, $expr, $matchScope), $scope); + $remainingType = $matchScope->getType($expr->cond); + if (!$hasDefaultCond && !$hasAlwaysTrueCond && !$remainingType instanceof NeverType) { + $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(UnhandledMatchError::class), $expr, false); + } + + ksort($armNodes, SORT_NUMERIC); + + $nodeCallback(new MatchExpressionNode($expr->cond, array_values($armNodes), $expr, $matchScope), $scope); + } elseif ($expr instanceof AlwaysRememberedExpr) { + $result = $this->processExprNode($stmt, $expr->getExpr(), $scope, $nodeCallback, $context); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $scope = $result->getScope(); } elseif ($expr instanceof Expr\Throw_) { $hasYield = false; - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $throwPoints[] = ThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false); } elseif ($expr instanceof FunctionCallableNode) { $throwPoints = []; + $impurePoints = []; $hasYield = false; if ($expr->getName() instanceof Expr) { - $result = $this->processExprNode($expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } } elseif ($expr instanceof MethodCallableNode) { - $result = $this->processExprNode($expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ($expr->getName() instanceof Expr) { - $nameResult = $this->processExprNode($expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $nameResult = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); } } elseif ($expr instanceof StaticMethodCallableNode) { $throwPoints = []; + $impurePoints = []; $hasYield = false; if ($expr->getClass() instanceof Expr) { - $classResult = $this->processExprNode($expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); } if ($expr->getName() instanceof Expr) { - $nameResult = $this->processExprNode($expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); } } elseif ($expr instanceof InstantiationCallableNode) { $throwPoints = []; + $impurePoints = []; $hasYield = false; if ($expr->getClass() instanceof Expr) { - $classResult = $this->processExprNode($expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); } + } elseif ($expr instanceof Node\Scalar) { + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + } elseif ($expr instanceof ConstFetch) { + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $nodeCallback($expr->name, $scope); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new ExpressionResult( $scope, $hasYield, $throwPoints, + $impurePoints, static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } + private function getArrayFunctionAppendingType(FunctionReflection $functionReflection, Scope $scope, FuncCall $expr): Type + { + $arrayArg = $expr->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + $callArgs = array_slice($expr->getArgs(), 1); + + /** + * @param Arg[] $callArgs + * @param callable(?Type, Type, bool): void $setOffsetValueType + */ + $setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void { + foreach ($callArgs as $callArg) { + $callArgType = $scope->getType($callArg->value); + if ($callArg->unpack) { + $constantArrays = $callArgType->getConstantArrays(); + if (count($constantArrays) === 1) { + $iterableValueTypes = $constantArrays[0]->getValueTypes(); + } else { + $iterableValueTypes = [$callArgType->getIterableValueType()]; + $nonConstantArrayWasUnpacked = true; + } + + $isOptional = !$callArgType->isIterableAtLeastOnce()->yes(); + foreach ($iterableValueTypes as $iterableValueType) { + if ($iterableValueType instanceof UnionType) { + foreach ($iterableValueType->getTypes() as $innerType) { + $setOffsetValueType(null, $innerType, $isOptional); + } + } else { + $setOffsetValueType(null, $iterableValueType, $isOptional); + } + } + continue; + } + $setOffsetValueType(null, $callArgType, false); + } + }; + + $constantArrays = $arrayType->getConstantArrays(); + if (count($constantArrays) > 0) { + $newArrayTypes = []; + $prepend = $functionReflection->getName() === 'array_unshift'; + foreach ($constantArrays as $constantArray) { + $arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray); + + $setOffsetValueTypes( + $scope, + $callArgs, + static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void { + $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional); + }, + $nonConstantArrayWasUnpacked, + ); + + if ($prepend) { + $keyTypes = $constantArray->getKeyTypes(); + $valueTypes = $constantArray->getValueTypes(); + foreach ($keyTypes as $k => $keyType) { + $arrayTypeBuilder->setOffsetValueType( + count($keyType->getConstantStrings()) === 1 ? $keyType->getConstantStrings()[0] : null, + $valueTypes[$k], + $constantArray->isOptionalKey($k), + ); + } + } + + $constantArray = $arrayTypeBuilder->getArray(); + + if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) { + $array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType()); + $isList = $constantArray->isList()->yes(); + $constantArray = $constantArray->isIterableAtLeastOnce()->yes() + ? TypeCombinator::intersect($array, new NonEmptyArrayType()) + : $array; + $constantArray = $isList + ? TypeCombinator::intersect($constantArray, new AccessoryArrayListType()) + : $constantArray; + } + + $newArrayTypes[] = $constantArray; + } + + return TypeCombinator::union(...$newArrayTypes); + } + + $setOffsetValueTypes( + $scope, + $callArgs, + static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void { + $isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional; + $arrayType = $arrayType->setOffsetValueType($offsetType, $valueType); + if ($isIterableAtLeastOnce) { + return; + } + + $arrayType = TypeCombinator::union($arrayType, new ConstantArrayType([], [])); + }, + ); + + return $arrayType; + } + + private function getArraySortPreserveListFunctionType(Type $type): Type + { + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if (!$type instanceof ArrayType && !$type instanceof ConstantArrayType) { + return $type; + } + + $newArrayType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $type->getIterableValueType()), new AccessoryArrayListType()); + if ($isIterableAtLeastOnce->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + return $newArrayType; + }); + } + + private function getArraySortDoNotPreserveListFunctionType(Type $type): Type + { + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $types = []; + foreach ($constantArrays as $constantArray) { + $types[] = new ConstantArrayType( + $constantArray->getKeyTypes(), + $constantArray->getValueTypes(), + $constantArray->getNextAutoIndexes(), + $constantArray->getOptionalKeys(), + $constantArray->isList()->and(TrinaryLogic::createMaybe()), + ); + } + + return TypeCombinator::union(...$types); + } + + $newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType()); + if ($isIterableAtLeastOnce->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + return $newArrayType; + }); + } + private function getFunctionThrowPoint( FunctionReflection $functionReflection, ?ParametersAcceptor $parametersAcceptor, @@ -2644,17 +4154,24 @@ private function getFunctionThrowPoint( MutatingScope $scope, ): ?ThrowPoint { - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) { - if (!$extension->isFunctionSupported($functionReflection)) { - continue; - } + $normalizedFuncCall = $funcCall; + if ($parametersAcceptor !== null) { + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $funcCall); + } - $throwType = $extension->getThrowTypeFromFunctionCall($functionReflection, $funcCall, $scope); - if ($throwType === null) { - return null; - } + if ($normalizedFuncCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) { + if (!$extension->isFunctionSupported($functionReflection)) { + continue; + } + + $throwType = $extension->getThrowTypeFromFunctionCall($functionReflection, $normalizedFuncCall, $scope); + if ($throwType === null) { + return null; + } - return ThrowPoint::createExplicit($scope, $throwType, $funcCall, false); + return ThrowPoint::createExplicit($scope, $throwType, $funcCall, false); + } } $throwType = $functionReflection->getThrowType(); @@ -2666,7 +4183,7 @@ private function getFunctionThrowPoint( } if ($throwType !== null) { - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $funcCall, true); } } elseif ($this->implicitThrows) { @@ -2699,17 +4216,20 @@ private function getFunctionThrowPoint( private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint { - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) { - if (!$extension->isMethodSupported($methodReflection)) { - continue; - } + $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) { + if (!$extension->isMethodSupported($methodReflection)) { + continue; + } - $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $methodCall, $scope); - if ($throwType === null) { - return null; - } + $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } - return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + } } $throwType = $methodReflection->getThrowType(); @@ -2721,7 +4241,7 @@ private function getMethodThrowPoint(MethodReflection $methodReflection, Paramet } if ($throwType !== null) { - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true); } } elseif ($this->implicitThrows) { @@ -2737,29 +4257,32 @@ private function getMethodThrowPoint(MethodReflection $methodReflection, Paramet /** * @param Node\Arg[] $args */ - private function getConstructorThrowPoint(MethodReflection $constructorReflection, ClassReflection $classReflection, New_ $new, Name $className, array $args, MutatingScope $scope): ?ThrowPoint + private function getConstructorThrowPoint(MethodReflection $constructorReflection, ParametersAcceptor $parametersAcceptor, ClassReflection $classReflection, New_ $new, Name $className, array $args, MutatingScope $scope): ?ThrowPoint { $methodCall = new StaticCall($className, $constructorReflection->getName(), $args); - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { - if (!$extension->isStaticMethodSupported($constructorReflection)) { - continue; - } + $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { + if (!$extension->isStaticMethodSupported($constructorReflection)) { + continue; + } - $throwType = $extension->getThrowTypeFromStaticMethodCall($constructorReflection, $methodCall, $scope); - if ($throwType === null) { - return null; - } + $throwType = $extension->getThrowTypeFromStaticMethodCall($constructorReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } - return ThrowPoint::createExplicit($scope, $throwType, $new, false); + return ThrowPoint::createExplicit($scope, $throwType, $new, false); + } } if ($constructorReflection->getThrowType() !== null) { $throwType = $constructorReflection->getThrowType(); - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $new, true); } } elseif ($this->implicitThrows) { - if ($classReflection->getName() !== Throwable::class && !$classReflection->isSubclassOf(Throwable::class)) { + if (!$classReflection->is(Throwable::class)) { return ThrowPoint::createImplicit($scope, $methodCall); } } @@ -2767,24 +4290,27 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio return null; } - private function getStaticMethodThrowPoint(MethodReflection $methodReflection, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint + private function getStaticMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint { - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { - if (!$extension->isStaticMethodSupported($methodReflection)) { - continue; - } + $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { + if (!$extension->isStaticMethodSupported($methodReflection)) { + continue; + } - $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $methodCall, $scope); - if ($throwType === null) { - return null; - } + $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } - return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + } } if ($methodReflection->getThrowType() !== null) { $throwType = $methodReflection->getThrowType(); - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true); } } elseif ($this->implicitThrows) { @@ -2797,6 +4323,83 @@ private function getStaticMethodThrowPoint(MethodReflection $methodReflection, S return null; } + /** + * @return ThrowPoint[] + */ + private function getPropertyReadThrowPointsFromGetHook( + MutatingScope $scope, + PropertyFetch $propertyFetch, + PhpPropertyReflection $propertyReflection, + ): array + { + return $this->getThrowPointsFromPropertyHook($scope, $propertyFetch, $propertyReflection, 'get'); + } + + /** + * @return ThrowPoint[] + */ + private function getPropertyAssignThrowPointsFromSetHook( + MutatingScope $scope, + PropertyFetch $propertyFetch, + PhpPropertyReflection $propertyReflection, + ): array + { + return $this->getThrowPointsFromPropertyHook($scope, $propertyFetch, $propertyReflection, 'set'); + } + + /** + * @param 'get'|'set' $hookName + * @return ThrowPoint[] + */ + private function getThrowPointsFromPropertyHook( + MutatingScope $scope, + PropertyFetch $propertyFetch, + PhpPropertyReflection $propertyReflection, + string $hookName, + ): array + { + $scopeFunction = $scope->getFunction(); + if ( + $scopeFunction instanceof PhpMethodFromParserNodeReflection + && $scopeFunction->isPropertyHook() + && $propertyFetch->var instanceof Variable + && $propertyFetch->var->name === 'this' + && $propertyFetch->name instanceof Identifier + && $propertyFetch->name->toString() === $scopeFunction->getHookedPropertyName() + ) { + return []; + } + $declaringClass = $propertyReflection->getDeclaringClass(); + if (!$propertyReflection->hasHook($hookName)) { + if ( + $propertyReflection->isPrivate() + || $propertyReflection->isFinal()->yes() + || $declaringClass->isFinal() + ) { + return []; + } + + if ($this->implicitThrows) { + return [ThrowPoint::createImplicit($scope, $propertyFetch)]; + } + + return []; + } + + $getHook = $propertyReflection->getHook($hookName); + $throwType = $getHook->getThrowType(); + + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + return [ThrowPoint::createExplicit($scope, $throwType, $propertyFetch, true)]; + } + } elseif ($this->implicitThrows) { + return [ThrowPoint::createImplicit($scope, $propertyFetch)]; + } + + return []; + } + /** * @return string[] */ @@ -2810,7 +4413,7 @@ private function getAssignedVariables(Expr $expr): array return []; } - if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) { + if ($expr instanceof Expr\List_) { $names = []; foreach ($expr->items as $item) { if ($item === null) { @@ -2850,35 +4453,27 @@ private function callNodeCallbackWithExpression( * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processClosureNode( + Node\Stmt $stmt, Expr\Closure $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context, ?Type $passedToType, - ): ExpressionResult + ): ProcessClosureResult { foreach ($expr->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } $byRefUses = []; - 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) { - $callableParameters = $acceptors[0]->getParameters(); - } - } else { - $callableParameters = null; - } + $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME); + $callableParameters = $this->createCallableParameters( + $scope, + $expr, + $closureCallArgs, + $passedToType, + ); $useScope = $scope; foreach ($expr->uses as $use) { @@ -2888,9 +4483,11 @@ private function processClosureNode( $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName(); $inAssignRightSideType = $context->getInAssignRightSideType(); + $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType(); if ( $inAssignRightSideVariableName === $use->var->name && $inAssignRightSideType !== null + && $inAssignRightSideNativeType !== null ) { if ($inAssignRightSideType instanceof ClosureType) { $variableType = $inAssignRightSideType; @@ -2902,10 +4499,20 @@ private function processClosureNode( $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); } } - $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType); + if ($inAssignRightSideNativeType instanceof ClosureType) { + $variableNativeType = $inAssignRightSideNativeType; + } else { + $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); + if ($alreadyHasVariableType->no()) { + $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType); + } else { + $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType); + } + } + $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes()); } } - $this->processExprNode($use, $useScope, $nodeCallback, $context); + $this->processExprNode($stmt, $use->var, $useScope, $nodeCallback, $context); if (!$use->byRef) { continue; } @@ -2919,15 +4526,41 @@ private function processClosureNode( $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); - $nodeCallback(new InClosureNode($expr), $closureScope); + $closureType = $closureScope->getAnonymousFunctionReflection(); + if (!$closureType instanceof ClosureType) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new InClosureNode($closureType, $expr), $closureScope); + + $executionEnds = []; $gatheredReturnStatements = []; $gatheredYieldStatements = []; - $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope): void { + $closureImpurePoints = []; + $invalidateExpressions = []; + $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { $nodeCallback($node, $scope); if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; } + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { $gatheredYieldStatements[] = $node; } @@ -2937,30 +4570,41 @@ private function processClosureNode( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }; + if (count($byRefUses) === 0) { - $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback); + $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ExpressionResult($scope, false, []); + return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); } $count = 0; + $closureResultScope = null; do { $prevScope = $closureScope; $intermediaryClosureScopeResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, static function (): void { - }); + }, StatementContext::createTopLevel()); $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); } + + if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) { + $closureResultScope = $intermediaryClosureScope; + break; + } + $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); + if ($closureScope->equals($prevScope)) { break; } @@ -2970,75 +4614,188 @@ private function processClosureNode( $count++; } while ($count < self::LOOP_SCOPE_ITERATIONS); - $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback); + if ($closureResultScope === null) { + $closureResultScope = $closureScope; + } + + $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ExpressionResult($scope->processClosureScope($closureScope, null, $byRefUses), false, []); + return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + } + + /** + * @param InvalidateExprNode[] $invalidatedExpressions + * @param string[] $uses + */ + private function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope + { + if ($scope->isInClass()) { + $uses[] = 'this'; + } + + $finder = new NodeFinder(); + foreach ($invalidatedExpressions as $invalidateExpression) { + $found = false; + foreach ($uses as $use) { + $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && $node->name === $use); + if ($result === null) { + continue; + } + + $found = true; + break; + } + + if (!$found) { + continue; + } + + $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), true); + } + + return $scope; } /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processArrowFunctionNode( + Node\Stmt $stmt, Expr\ArrowFunction $expr, MutatingScope $scope, callable $nodeCallback, - ExpressionContext $context, ?Type $passedToType, ): ExpressionResult { foreach ($expr->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } if ($expr->returnType !== null) { $nodeCallback($expr->returnType, $scope); } - if ($passedToType !== null && !$passedToType->isCallable()->no()) { + $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); + $arrowFunctionScope = $scope->enterArrowFunction($expr, $this->createCallableParameters( + $scope, + $expr, + $arrowFunctionCallArgs, + $passedToType, + )); + $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection(); + if (!$arrowFunctionType instanceof ClosureType) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); + $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); + + return new ExpressionResult($scope, false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + } + + /** + * @param Node\Arg[] $args + * @return ParameterReflection[]|null + */ + public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array + { + $callableParameters = null; + if ($args !== null) { + $closureType = $scope->getType($closureExpr); + + if ($closureType->isCallable()->no()) { + return null; + } + + $acceptors = $closureType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $callableParameters = $acceptors[0]->getParameters(); + + foreach ($callableParameters as $index => $callableParameter) { + if (!isset($args[$index])) { + continue; + } + + $type = $scope->getType($args[$index]->value); + $callableParameters[$index] = new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $type, + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ); + } + } + } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { if ($passedToType instanceof UnionType) { $passedToType = TypeCombinator::union(...array_filter( $passedToType->getTypes(), static fn (Type $type) => $type->isCallable()->yes(), )); + + if ($passedToType->isCallable()->no()) { + return null; + } } - $callableParameters = null; $acceptors = $passedToType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); + if (count($acceptors) > 0) { + foreach ($acceptors as $acceptor) { + if ($callableParameters === null) { + $callableParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ), $acceptor->getParameters()); + continue; + } + + $newParameters = []; + foreach ($acceptor->getParameters() as $i => $callableParameter) { + if (!array_key_exists($i, $callableParameters)) { + $newParameters[] = $callableParameter; + continue; + } + + $newParameters[] = $callableParameters[$i]->union(new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + )); + } + + $callableParameters = $newParameters; + } } - } else { - $callableParameters = null; } - $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters); - $nodeCallback(new InArrowFunctionNode($expr), $arrowFunctionScope); - $this->processExprNode($expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); - - return new ExpressionResult($scope, false, []); + return $callableParameters; } /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processParamNode( + Node\Stmt $stmt, Node\Param $param, MutatingScope $scope, callable $nodeCallback, ): void { - foreach ($param->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - } - } + $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $nodeCallback); $nodeCallback($param, $scope); if ($param->type !== null) { $nodeCallback($param->type, $scope); @@ -3047,85 +4804,535 @@ private function processParamNode( return; } - $this->processExprNode($param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + } + + /** + * @param AttributeGroup[] $attrGroups + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processAttributeGroups( + Node\Stmt $stmt, + array $attrGroups, + MutatingScope $scope, + callable $nodeCallback, + ): void + { + foreach ($attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + foreach ($attr->args as $arg) { + $this->processExprNode($stmt, $arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $nodeCallback($arg, $scope); + } + $nodeCallback($attr, $scope); + } + $nodeCallback($attrGroup, $scope); + } + } + + /** + * @param Node\PropertyHook[] $hooks + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processPropertyHooks( + Node\Stmt $stmt, + Identifier|Name|ComplexType|null $nativeTypeNode, + ?Type $phpDocType, + string $propertyName, + array $hooks, + MutatingScope $scope, + callable $nodeCallback, + ): void + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $classReflection = $scope->getClassReflection(); + + foreach ($hooks as $hook) { + $nodeCallback($hook, $scope); + $this->processAttributeGroups($stmt, $hook->attrGroups, $scope, $nodeCallback); + + [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment] = $this->getPhpDocs($scope, $hook); + + foreach ($hook->params as $param) { + $this->processParamNode($stmt, $param, $scope, $nodeCallback); + } + + [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $hook); + + $hookScope = $scope->enterPropertyHook( + $hook, + $propertyName, + $nativeTypeNode, + $phpDocType, + $phpDocParameterTypes, + $phpDocThrowType, + $deprecatedDescription, + $isDeprecated, + $phpDocComment, + ); + $hookReflection = $hookScope->getFunction(); + if (!$hookReflection instanceof PhpMethodFromParserNodeReflection) { + throw new ShouldNotHappenException(); + } + + if (!$classReflection->hasNativeProperty($propertyName)) { + throw new ShouldNotHappenException(); + } + + $propertyReflection = $classReflection->getNativeProperty($propertyName); + + $nodeCallback(new InPropertyHookNode( + $classReflection, + $hookReflection, + $propertyReflection, + $hook, + ), $hookScope); + + $stmts = $hook->getStmts(); + if ($stmts === null) { + return; + } + + if ($hook->body instanceof Expr) { + // enrich attributes of nodes in short hook body statements + $traverser = new NodeTraverser( + new LineAttributesVisitor($hook->body->getStartLine(), $hook->body->getEndLine()), + ); + $traverser->traverse($stmts); + } + + $gatheredReturnStatements = []; + $executionEnds = []; + $methodImpurePoints = []; + $statementResult = $this->processStmtNodes(new PropertyHookStatementNode($hook), $stmts, $hookScope, static function (Node $node, Scope $scope) use ($nodeCallback, $hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void { + $nodeCallback($node, $scope); + if ($scope->getFunction() !== $hookScope->getFunction()) { + return; + } + if ($scope->isInAnonymousFunction()) { + return; + } + if ($node instanceof PropertyAssignNode) { + $hookImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } + if (!$node instanceof Return_) { + return; + } + + $gatheredReturnStatements[] = new ReturnStatement($scope, $node); + }, StatementContext::createTopLevel()); + + $nodeCallback(new PropertyHookReturnStatementsNode( + $hook, + $gatheredReturnStatements, + $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $methodImpurePoints), + $classReflection, + $hookReflection, + $propertyReflection, + ), $hookScope); + } } /** * @param MethodReflection|FunctionReflection|null $calleeReflection - * @param Node\Arg[] $args * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processArgs( + Node\Stmt $stmt, $calleeReflection, + ?ExtendedMethodReflection $nakedMethodReflection, ?ParametersAcceptor $parametersAcceptor, - array $args, + CallLike $callLike, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context, ?MutatingScope $closureBindScope = null, ): ExpressionResult { + $args = $callLike->getArgs(); + if ($parametersAcceptor !== null) { $parameters = $parametersAcceptor->getParameters(); } - if ($calleeReflection !== null) { - $scope = $scope->pushInFunctionCall($calleeReflection); - } - $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($args as $i => $arg) { - $nodeCallback($arg, $scope); + $assignByReference = false; + $parameter = null; + $parameterType = null; + $parameterNativeType = null; if (isset($parameters) && $parametersAcceptor !== null) { - $assignByReference = false; if (isset($parameters[$i])) { $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); $parameterType = $parameters[$i]->getType(); + + if ($parameters[$i] instanceof ExtendedParameterReflection) { + $parameterNativeType = $parameters[$i]->getNativeType(); + } + $parameter = $parameters[$i]; } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { $lastParameter = $parameters[count($parameters) - 1]; $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); $parameterType = $lastParameter->getType(); - } - if ($assignByReference) { - $argValue = $arg->value; - if ($argValue instanceof Variable && is_string($argValue->name)) { - $scope = $scope->assignVariable($argValue->name, new MixedType()); + if ($lastParameter instanceof ExtendedParameterReflection) { + $parameterNativeType = $lastParameter->getNativeType(); } + $parameter = $lastParameter; } } + $lookForUnset = false; + if ($assignByReference) { + $isBuiltin = false; + if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { + $isBuiltin = true; + } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { + $isBuiltin = true; + } + if ( + $isBuiltin + || ($parameterNativeType === null || !$parameterNativeType->isNull()->no()) + ) { + $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); + $lookForUnset = true; + } + } + + if ($calleeReflection !== null) { + $scope = $scope->pushInFunctionCall($calleeReflection, $parameter); + } + + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $nodeCallback($originalArg, $scope); + $originalScope = $scope; $scopeToPass = $scope; if ($i === 0 && $closureBindScope !== null) { $scopeToPass = $closureBindScope; } + if ($parameter instanceof ExtendedParameterReflection) { + $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); + if ($parameterCallImmediately->maybe()) { + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + } else { + $callCallbackImmediately = $parameterCallImmediately->yes(); + } + } else { + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + } if ($arg->value instanceof Expr\Closure) { + $restoreThisScope = null; + if ( + $closureBindScope === null + && $parameter instanceof ExtendedParameterReflection + && $parameter->getClosureThisType() !== null + && !$arg->value->static + ) { + $restoreThisScope = $scopeToPass; + $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes()); + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + } + } + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $result = $this->processClosureNode($arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + if ($callCallbackImmediately) { + $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); + } + + $uses = []; + foreach ($arg->value->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $uses[] = $use->var->name; + } + + $scope = $closureResult->getScope(); + $invalidateExpressions = $closureResult->getInvalidateExpressions(); + if ($restoreThisScope !== null) { + $nodeFinder = new NodeFinder(); + $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; + foreach ($invalidateExpressions as $j => $invalidateExprNode) { + $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb); + if ($foundThis === null) { + continue; + } + + unset($invalidateExpressions[$j]); + } + $invalidateExpressions = array_values($invalidateExpressions); + $scope = $scope->restoreThis($restoreThisScope); + } + + $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); } elseif ($arg->value instanceof Expr\ArrowFunction) { + if ( + $closureBindScope === null + && $parameter instanceof ExtendedParameterReflection + && $parameter->getClosureThisType() !== null + && !$arg->value->static + ) { + $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes()); + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + } + } + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $result = $this->processArrowFunctionNode($arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $parameterType ?? null); + if ($callCallbackImmediately) { + $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints()); + } } else { - $result = $this->processExprNode($arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); + $exprType = $scope->getType($arg->value); + $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + $scope = $exprResult->getScope(); + $hasYield = $hasYield || $exprResult->hasYield(); + + if ($exprType->isCallable()->yes()) { + $acceptors = $exprType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $scope = $this->processImmediatelyCalledCallable($scope, $acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()); + if ($callCallbackImmediately) { + $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints()); + if (!$this->implicitThrows) { + $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit())); + } + $throwPoints = array_merge($throwPoints, $callableThrowPoints); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints())); + } + } + } } - $scope = $result->getScope(); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + + if ($assignByReference && $lookForUnset) { + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); + } + + if ($calleeReflection !== null) { + $scope = $scope->popInFunctionCall(); + } + if ($i !== 0 || $closureBindScope === null) { continue; } $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); } + foreach ($args as $i => $arg) { + if (!isset($parameters) || $parametersAcceptor === null) { + continue; + } + + $byRefType = new MixedType(); + $assignByReference = false; + $currentParameter = null; + if (isset($parameters[$i])) { + $currentParameter = $parameters[$i]; + } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { + $currentParameter = $parameters[count($parameters) - 1]; + } + + if ($currentParameter !== null) { + $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); + if ($assignByReference) { + if ($currentParameter instanceof ExtendedParameterReflection && $currentParameter->getOutType() !== null) { + $byRefType = $currentParameter->getOutType(); + } elseif ( + $calleeReflection instanceof MethodReflection + && !$calleeReflection->getDeclaringClass()->isBuiltin() + ) { + $byRefType = $currentParameter->getType(); + } elseif ( + $calleeReflection instanceof FunctionReflection + && !$calleeReflection->isBuiltin() + ) { + $byRefType = $currentParameter->getType(); + } + } + } + + if ($assignByReference) { + if ($currentParameter === null) { + throw new ShouldNotHappenException(); + } + + $argValue = $arg->value; + if (!$argValue instanceof Variable || $argValue->name !== 'this') { + $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); + if ($paramOutType !== null) { + $byRefType = $paramOutType; + } + + $result = $this->processAssignVar( + $scope, + $stmt, + $argValue, + new TypeExpr($byRefType), + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + true, + ); + $scope = $result->getScope(); + } + } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { + $argType = $scope->getType($arg->value); + if (!$argType->isObject()->no()) { + $nakedReturnType = null; + if ($nakedMethodReflection !== null) { + $nakedParametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $args, + $nakedMethodReflection->getVariants(), + $nakedMethodReflection->getNamedArgumentsVariants(), + ); + $nakedReturnType = $nakedParametersAcceptor->getReturnType(); + } + if ( + $nakedReturnType === null + || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() + || $nakedMethodReflection->isPure()->no() + ) { + $nodeCallback(new InvalidateExprNode($arg->value), $scope); + $scope = $scope->invalidateExpression($arg->value, true); + } + } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { + $nodeCallback(new InvalidateExprNode($arg->value), $scope); + $scope = $scope->invalidateExpression($arg->value, true); + } + } + } + + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type + { + if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->parameterClosureTypeExtensionProvider->getFunctionParameterClosureTypeExtensions() as $functionParameterClosureTypeExtension) { + if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) { + return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } elseif ($calleeReflection instanceof MethodReflection) { + if ($callLike instanceof StaticCall) { + foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) { + if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { + return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } elseif ($callLike instanceof MethodCall) { + foreach ($this->parameterClosureTypeExtensionProvider->getMethodParameterClosureTypeExtensions() as $methodParameterClosureTypeExtension) { + if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) { + return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } + } + + return null; + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type + { + $paramOutTypes = []; + if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->parameterOutTypeExtensionProvider->getFunctionParameterOutTypeExtensions() as $functionParameterOutTypeExtension) { + if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) { + continue; + } - if ($calleeReflection !== null) { - $scope = $scope->popInFunctionCall(); + $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->parameterOutTypeExtensionProvider->getMethodParameterOutTypeExtensions() as $methodParameterOutTypeExtension) { + if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->parameterOutTypeExtensionProvider->getStaticMethodParameterOutTypeExtensions() as $staticMethodParameterOutTypeExtension) { + if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } } - return new ExpressionResult($scope, $hasYield, $throwPoints); + if (count($paramOutTypes) === 1) { + return $paramOutTypes[0]; + } + + if (count($paramOutTypes) > 1) { + return TypeCombinator::union(...$paramOutTypes); + } + + return null; } /** @@ -3134,6 +5341,7 @@ private function processArgs( */ private function processAssignVar( MutatingScope $scope, + Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback, @@ -3145,19 +5353,50 @@ private function processAssignVar( $nodeCallback($var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope); $hasYield = false; $throwPoints = []; + $impurePoints = []; $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign; if ($var instanceof Variable && is_string($var->name)) { $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + if (in_array($var->name, Scope::SUPERGLOBAL_VARIABLES, true)) { + $impurePoints[] = new ImpurePoint($scope, $var, 'superglobal', 'assign to superglobal variable', true); + } $assignedExpr = $this->unwrapAssign($assignedExpr); $type = $scope->getType($assignedExpr); - $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); - $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); $conditionalExpressions = []; + if ($assignedExpr instanceof Ternary) { + $if = $assignedExpr->if; + if ($if === null) { + $if = $assignedExpr->cond; + } + $condScope = $this->processExprNode($stmt, $assignedExpr->cond, $scope, static function (): void { + }, ExpressionContext::createDeep())->getScope(); + $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createTruthy()); + $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey()); + $truthyScope = $condScope->filterBySpecifiedTypes($truthySpecifiedTypes); + $falsyScope = $condScope->filterBySpecifiedTypes($falseySpecifiedTypes); + $truthyType = $truthyScope->getType($if); + $falseyType = $falsyScope->getType($assignedExpr->else); + + if ( + $truthyType->isSuperTypeOf($falseyType)->no() + && $falseyType->isSuperTypeOf($truthyType)->no() + ) { + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + } + } + + $scope = $result->getScope(); + $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); + $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); - $truthyType = TypeCombinator::remove($type, StaticTypeFactory::falsey()); + $truthyType = TypeCombinator::removeFalsey($type); $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey()); $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); @@ -3165,25 +5404,34 @@ private function processAssignVar( $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); - $scope = $result->getScope()->assignVariable($var->name, $type); + $nodeCallback(new VariableAssignNode($var, $assignedExpr, $isAssignOp), $result->getScope()); + $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes()); foreach ($conditionalExpressions as $exprString => $holders) { $scope = $scope->addConditionalExpressions($exprString, $holders); } } elseif ($var instanceof ArrayDimFetch) { - $dimExprStack = []; + $dimFetchStack = []; $originalVar = $var; $assignedPropertyExpr = $assignedExpr; while ($var instanceof ArrayDimFetch) { - $varForSetOffsetValue = $var->var; - if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { - $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + if ( + $var->var instanceof PropertyFetch + || $var->var instanceof StaticPropertyFetch + ) { + if (((new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($var->var))->yes())) { + $varForSetOffsetValue = $var->var; + } else { + $varForSetOffsetValue = new OriginalPropertyTypeExpr($var->var); + } + } else { + $varForSetOffsetValue = $var->var; } $assignedPropertyExpr = new SetOffsetValueTypeExpr( $varForSetOffsetValue, $var->dim, $assignedPropertyExpr, ); - $dimExprStack[] = $var->dim; + $dimFetchStack[] = $var; $var = $var->var; } @@ -3191,9 +5439,10 @@ private function processAssignVar( if ($enterExpressionAssign) { $scope = $scope->enterExpressionAssign($var); } - $result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if ($enterExpressionAssign) { $scope = $scope->exitExpressionAssign($var); @@ -3201,17 +5450,29 @@ private function processAssignVar( // 2. eval dimensions $offsetTypes = []; - foreach (array_reverse($dimExprStack) as $dimExpr) { + $offsetNativeTypes = []; + $dimFetchStack = array_reverse($dimFetchStack); + $lastDimKey = array_key_last($dimFetchStack); + foreach ($dimFetchStack as $key => $dimFetch) { + $dimExpr = $dimFetch->dim; + + // Callback was already called for last dim at the beginning of the method. + if ($key !== $lastDimKey) { + $nodeCallback($dimFetch, $enterExpressionAssign ? $scope->enterExpressionAssign($dimFetch) : $scope); + } + if ($dimExpr === null) { $offsetTypes[] = null; + $offsetNativeTypes[] = null; } else { $offsetTypes[] = $scope->getType($dimExpr); + $offsetNativeTypes[] = $scope->getNativeType($dimExpr); if ($enterExpressionAssign) { $scope->enterExpressionAssign($dimExpr); } - $result = $this->processExprNode($dimExpr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $dimExpr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $scope = $result->getScope(); @@ -3223,72 +5484,108 @@ private function processAssignVar( } $valueToWrite = $scope->getType($assignedExpr); + $nativeValueToWrite = $scope->getNativeType($assignedExpr); $originalValueToWrite = $valueToWrite; + $originalNativeValueToWrite = $nativeValueToWrite; // 3. eval assigned expr $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); $varType = $scope->getType($var); + $varNativeType = $scope->getNativeType($var); // 4. compose types if ($varType instanceof ErrorType) { $varType = new ConstantArrayType([], []); } + if ($varNativeType instanceof ErrorType) { + $varNativeType = new ConstantArrayType([], []); + } $offsetValueType = $varType; - $offsetValueTypeStack = [$offsetValueType]; - foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { - if ($offsetType === null) { - $offsetValueType = new ConstantArrayType([], []); + $offsetNativeValueType = $varNativeType; - } else { - $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); - if ($offsetValueType instanceof ErrorType) { - $offsetValueType = new ConstantArrayType([], []); + $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); + + if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + } else { + $rewritten = false; + foreach ($offsetTypes as $i => $offsetType) { + $offsetNativeType = $offsetNativeTypes[$i]; + + if ($offsetType === null) { + if ($offsetNativeType !== null) { + throw new ShouldNotHappenException(); + } + + continue; + } elseif ($offsetNativeType === null) { + throw new ShouldNotHappenException(); + } + if ($offsetType->equals($offsetNativeType)) { + continue; } - } - $offsetValueTypeStack[] = $offsetValueType; - } + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + $rewritten = true; + break; + } - foreach (array_reverse($offsetTypes) as $i => $offsetType) { - /** @var Type $offsetValueType */ - $offsetValueType = array_pop($offsetValueTypeStack); - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + if (!$rewritten) { + $nativeValueToWrite = $valueToWrite; + } } - if (!(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { + if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { - $scope = $scope->assignVariable($var->name, $valueToWrite); + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { + $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + } } $scope = $scope->assignExpression( $var, $valueToWrite, + $nativeValueToWrite, ); } if ($originalVar->dim instanceof Variable || $originalVar->dim instanceof Node\Scalar) { $currentVarType = $scope->getType($originalVar); - if (!$originalValueToWrite->isSuperTypeOf($currentVarType)->yes()) { + $currentVarNativeType = $scope->getNativeType($originalVar); + if ( + !$originalValueToWrite->isSuperTypeOf($currentVarType)->yes() + || !$originalNativeValueToWrite->isSuperTypeOf($currentVarNativeType)->yes() + ) { $scope = $scope->assignExpression( $originalVar, $originalValueToWrite, + $originalNativeValueToWrite, ); } } } else { - if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + if ($var instanceof Variable) { + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { + $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + } } } - if (!(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { + if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { $throwPoints = array_merge($throwPoints, $this->processExprNode( + $stmt, new MethodCall($var, 'offsetSet'), $scope, static function (): void { @@ -3297,45 +5594,89 @@ static function (): void { )->getThrowPoints()); } } elseif ($var instanceof PropertyFetch) { - $objectResult = $this->processExprNode($var->var, $scope, $nodeCallback, $context); + $objectResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, $context); $hasYield = $objectResult->hasYield(); $throwPoints = $objectResult->getThrowPoints(); + $impurePoints = $objectResult->getImpurePoints(); $scope = $objectResult->getScope(); $propertyName = null; if ($var->name instanceof Node\Identifier) { $propertyName = $var->name->name; } else { - $propertyNameResult = $this->processExprNode($var->name, $scope, $nodeCallback, $context); + $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); $hasYield = $hasYield || $propertyNameResult->hasYield(); $throwPoints = array_merge($throwPoints, $propertyNameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $propertyNameResult->getImpurePoints()); $scope = $propertyNameResult->getScope(); } $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); + if ($var->name instanceof Expr && $this->phpVersion->supportsPropertyHooks()) { + $throwPoints[] = ThrowPoint::createImplicit($scope, $var); + } + $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, $assignedExprType); + if ($propertyReflection->hasNativeType()) { + $propertyNativeType = $propertyReflection->getNativeType(); + + $assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes(); + if (!$assignedTypeIsCompatible) { + foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { + if ($type->isSuperTypeOf($assignedExprType)->yes()) { + $assignedTypeIsCompatible = true; + break; + } + } + } + + if ($assignedTypeIsCompatible) { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } else { + $scope = $scope->assignExpression( + $var, + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + ); + } + } else { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } } - if (!$propertyReflection->getWritableType()->isSuperTypeOf($assignedExprType)->yes()) { - $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false); + $declaringClass = $propertyReflection->getDeclaringClass(); + if ($declaringClass->hasNativeProperty($propertyName)) { + $nativeProperty = $declaringClass->getNativeProperty($propertyName); + if ( + !$nativeProperty->getNativeType()->accepts($assignedExprType, true)->yes() + ) { + $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false); + } + if ($this->phpVersion->supportsPropertyHooks()) { + $throwPoints = array_merge($throwPoints, $this->getPropertyAssignThrowPointsFromSetHook($scope, $var, $nativeProperty)); + } + if ($enterExpressionAssign) { + $scope = $scope->assignInitializedProperty($propertyHolderType, $propertyName); + } } } else { // fallback $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); - $scope = $scope->assignExpression($var, $assignedExprType); + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); // simulate dynamic property assign by __set to get throw points if (!$propertyHolderType->hasMethod('__set')->no()) { $throwPoints = array_merge($throwPoints, $this->processExprNode( + $stmt, new MethodCall($var->var, '__set'), $scope, static function (): void { @@ -3349,25 +5690,25 @@ static function (): void { if ($var->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($var->class); } else { - $this->processExprNode($var->class, $scope, $nodeCallback, $context); + $this->processExprNode($stmt, $var->class, $scope, $nodeCallback, $context); $propertyHolderType = $scope->getType($var->class); } $propertyName = null; if ($var->name instanceof Node\Identifier) { $propertyName = $var->name->name; - $hasYield = false; - $throwPoints = []; } else { - $propertyNameResult = $this->processExprNode($var->name, $scope, $nodeCallback, $context); + $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); $hasYield = $propertyNameResult->hasYield(); $throwPoints = $propertyNameResult->getThrowPoints(); + $impurePoints = $propertyNameResult->getImpurePoints(); $scope = $propertyNameResult->getScope(); } $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); if ($propertyName !== null) { @@ -3375,18 +5716,43 @@ static function (): void { $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $assignedExprType); + if ($propertyReflection->hasNativeType()) { + $propertyNativeType = $propertyReflection->getNativeType(); + $assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes(); + + if (!$assignedTypeIsCompatible) { + foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { + if ($type->isSuperTypeOf($assignedExprType)->yes()) { + $assignedTypeIsCompatible = true; + break; + } + } + } + + if ($assignedTypeIsCompatible) { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } else { + $scope = $scope->assignExpression( + $var, + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + ); + } + } else { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } } } else { // fallback $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); - $scope = $scope->assignExpression($var, $assignedExprType); + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } - } elseif ($var instanceof List_ || $var instanceof Array_) { + } elseif ($var instanceof List_) { $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); foreach ($var->items as $i => $arrayItem) { if ($arrayItem === null) { @@ -3394,35 +5760,191 @@ static function (): void { } $itemScope = $scope; - if ($arrayItem->value instanceof ArrayDimFetch && $arrayItem->value->dim === null) { + if ($enterExpressionAssign) { $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()); + $itemScope = $this->lookForSetAllowedUndefinedExpressions($itemScope, $arrayItem->value); + $nodeCallback($arrayItem, $itemScope); + if ($arrayItem->key !== null) { + $keyResult = $this->processExprNode($stmt, $arrayItem->key, $itemScope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $keyResult->hasYield(); + $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $itemScope = $keyResult->getScope(); + } + + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $itemScope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $valueResult->hasYield(); + $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); if ($arrayItem->key === null) { - $dimExpr = new Node\Scalar\LNumber($i); + $dimExpr = new Node\Scalar\Int_($i); } else { $dimExpr = $arrayItem->key; } $result = $this->processAssignVar( $scope, + $stmt, $arrayItem->value, new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), $nodeCallback, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), $enterExpressionAssign, ); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + } + } elseif ($var instanceof ExistingArrayDimFetch) { + $dimFetchStack = []; + $assignedPropertyExpr = $assignedExpr; + while ($var instanceof ExistingArrayDimFetch) { + if ( + $var->getVar() instanceof PropertyFetch + || $var->getVar() instanceof StaticPropertyFetch + ) { + if (((new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($var->getVar()))->yes())) { + $varForSetOffsetValue = $var->getVar(); + } else { + $varForSetOffsetValue = new OriginalPropertyTypeExpr($var->getVar()); + } + } else { + $varForSetOffsetValue = $var->getVar(); + } + $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr( + $varForSetOffsetValue, + $var->getDim(), + $assignedPropertyExpr, + ); + $dimFetchStack[] = $var; + $var = $var->getVar(); + } + + $offsetTypes = []; + $offsetNativeTypes = []; + foreach (array_reverse($dimFetchStack) as $dimFetch) { + $dimExpr = $dimFetch->getDim(); + $offsetTypes[] = $scope->getType($dimExpr); + $offsetNativeTypes[] = $scope->getNativeType($dimExpr); + } + + $valueToWrite = $scope->getType($assignedExpr); + $nativeValueToWrite = $scope->getNativeType($assignedExpr); + $varType = $scope->getType($var); + $varNativeType = $scope->getNativeType($var); + + $offsetValueType = $varType; + $offsetNativeValueType = $varNativeType; + $offsetValueTypeStack = [$offsetValueType]; + $offsetValueNativeTypeStack = [$offsetNativeValueType]; + foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { + $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); + $offsetValueTypeStack[] = $offsetValueType; + } + foreach (array_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) { + $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType); + $offsetValueNativeTypeStack[] = $offsetNativeValueType; + } + + foreach (array_reverse($offsetTypes) as $offsetType) { + /** @var Type $offsetValueType */ + $offsetValueType = array_pop($offsetValueTypeStack); + $valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite); + } + foreach (array_reverse($offsetNativeTypes) as $offsetNativeType) { + /** @var Type $offsetNativeValueType */ + $offsetNativeValueType = array_pop($offsetValueNativeTypeStack); + $nativeValueToWrite = $offsetNativeValueType->setExistingOffsetValueType($offsetNativeType, $nativeValueToWrite); + } + + if ($var instanceof Variable && is_string($var->name)) { + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); + } else { + if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + } + $scope = $scope->assignExpression( + $var, + $valueToWrite, + $nativeValueToWrite, + ); + } + } + + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + } + + /** + * @param list $dimFetchStack + * @param list $offsetTypes + */ + private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): Type + { + $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([], []); + } + } + + $offsetValueTypeStack[] = $offsetValueType; + } + + foreach (array_reverse($offsetTypes) as $i => $offsetType) { + /** @var Type $offsetValueType */ + $offsetValueType = array_pop($offsetValueTypeStack); + if ( + !$offsetValueType instanceof MixedType + && !$offsetValueType->isConstantArray()->yes() + ) { + $types = [ + new ArrayType(new MixedType(), new MixedType()), + new ObjectType(ArrayAccess::class), + new NullType(), + ]; + if ($offsetType !== null && $offsetType->isInteger()->yes()) { + $types[] = new StringType(); + } + $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types)); + } + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + + $arrayDimFetch = $dimFetchStack[$i] ?? null; + if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) { + continue; + } + + if ($scope->hasExpressionType($arrayDimFetch)->yes()) { // keep list for $list[$index] assignments + $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); + } elseif ($arrayDimFetch->dim instanceof BinaryOp\Plus) { + if ( // keep list for $list[$index + 1] assignments + $arrayDimFetch->dim->right instanceof Variable + && $arrayDimFetch->dim->left instanceof Node\Scalar\Int_ + && $arrayDimFetch->dim->left->value === 1 + && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes() + ) { + $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); + } elseif ( // keep list for $list[1 + $index] assignments + $arrayDimFetch->dim->left instanceof Variable + && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ + && $arrayDimFetch->dim->right->value === 1 + && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes() + ) { + $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); + } } } - return new ExpressionResult($scope, $hasYield, $throwPoints); + return $valueToWrite; } private function unwrapAssign(Expr $expr): Expr @@ -3448,15 +5970,21 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco continue; } + if ($expr->name === $variableName) { + continue; + } + if (!isset($conditionalExpressions[$exprString])) { $conditionalExpressions[$exprString] = []; } - $conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([ - '$' . $variableName => $variableType, - ], VariableTypeHolder::createYes( + $holder = new ConditionalExpressionHolder([ + '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType), + ], ExpressionTypeHolder::createYes( + $expr, TypeCombinator::intersect($scope->getType($expr), $exprType), )); + $conditionalExpressions[$exprString][$holder->getKey()] = $holder; } return $conditionalExpressions; @@ -3476,21 +6004,30 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $ continue; } + if ($expr->name === $variableName) { + continue; + } + if (!isset($conditionalExpressions[$exprString])) { $conditionalExpressions[$exprString] = []; } - $conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([ - '$' . $variableName => $variableType, - ], VariableTypeHolder::createYes( + $holder = new ConditionalExpressionHolder([ + '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType), + ], ExpressionTypeHolder::createYes( + $expr, TypeCombinator::remove($scope->getType($expr), $exprType), )); + $conditionalExpressions[$exprString][$holder->getKey()] = $holder; } return $conditionalExpressions; } - private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr): MutatingScope + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope { $function = $scope->getFunction(); $variableLessTags = []; @@ -3541,12 +6078,28 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, $certainty = TrinaryLogic::createYes(); } - $scope = $scope->assignVariable($name, $varTag->getType(), $certainty); + $variableNode = new Variable($name, $stmt->getAttributes()); + $originalType = $scope->getVariableType($name); + if (!$originalType->equals($varTag->getType())) { + $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope); + } + + $scope = $scope->assignVariable( + $name, + $varTag->getType(), + $scope->getNativeType($variableNode), + $certainty, + ); } } if (count($variableLessTags) === 1 && $defaultExpr !== null) { - $scope = $scope->specifyExpressionType($defaultExpr, $variableLessTags[0]->getType()); + $originalType = $scope->getType($defaultExpr); + $varTag = $variableLessTags[0]; + if (!$originalType->equals($varTag->getType())) { + $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope); + } + $scope = $scope->assignExpression($defaultExpr, $varTag->getType(), new MixedType()); } return $scope; @@ -3555,7 +6108,7 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, /** * @param array $variableNames */ - private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node $node, bool &$changed = false): MutatingScope + private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node\Stmt $node, bool &$changed = false): MutatingScope { $function = $scope->getFunction(); $varTags = []; @@ -3587,33 +6140,34 @@ private function processVarAnnotation(MutatingScope $scope, array $variableNames $variableType = $varTags[$variableName]->getType(); $changed = true; - $scope = $scope->assignVariable($variableName, $variableType); + $scope = $scope->assignVariable($variableName, $variableType, new MixedType(), TrinaryLogic::createYes()); } if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) { $variableType = $varTags[0]->getType(); $changed = true; - $scope = $scope->assignVariable($variableNames[0], $variableType); + $scope = $scope->assignVariable($variableNames[0], $variableType, new MixedType(), TrinaryLogic::createYes()); } return $scope; } - private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingScope + private function enterForeach(MutatingScope $scope, MutatingScope $originalScope, Foreach_ $stmt): MutatingScope { if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } - $iterateeType = $scope->getType($stmt->expr); - if ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) { + $iterateeType = $originalScope->getType($stmt->expr); + if ( + ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) + && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) + ) { $keyVarName = null; - if ($stmt->keyVar !== null - && $stmt->keyVar instanceof Variable - && is_string($stmt->keyVar->name) - ) { + if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) { $keyVarName = $stmt->keyVar->name; } $scope = $scope->enterForeach( + $originalScope, $stmt->expr, $stmt->valueVar->name, $keyVarName, @@ -3625,41 +6179,69 @@ private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingSco } else { $scope = $this->processAssignVar( $scope, + $stmt, $stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr), static function (): void { }, ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), true, )->getScope(); $vars = $this->getAssignedVariables($stmt->valueVar); if ( $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ) { - $scope = $scope->enterForeachKey($stmt->expr, $stmt->keyVar->name); + $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $stmt->keyVar->name); $vars[] = $stmt->keyVar->name; + } elseif ($stmt->keyVar !== null) { + $scope = $this->processAssignVar( + $scope, + $stmt, + $stmt->keyVar, + new GetIterableKeyTypeExpr($stmt->expr), + static function (): void { + }, + ExpressionContext::createDeep(), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + true, + )->getScope(); + $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar)); } } + $constantArrays = $iterateeType->getConstantArrays(); if ( $stmt->getDocComment() === null - && $iterateeType instanceof ConstantArrayType + && $iterateeType->isConstantArray()->yes() + && count($constantArrays) === 1 && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ) { - $conditionalHolders = []; - foreach ($iterateeType->getKeyTypes() as $i => $keyType) { - $valueType = $iterateeType->getValueTypes()[$i]; - $conditionalHolders[] = new ConditionalExpressionHolder([ - '$' . $stmt->keyVar->name => $keyType, - ], new VariableTypeHolder($valueType, TrinaryLogic::createYes())); + $valueConditionalHolders = []; + $arrayDimFetchConditionalHolders = []; + foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) { + $valueType = $constantArrays[0]->getValueTypes()[$i]; + $holder = new ConditionalExpressionHolder([ + '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType), + ], new ExpressionTypeHolder($stmt->valueVar, $valueType, TrinaryLogic::createYes())); + $valueConditionalHolders[$holder->getKey()] = $holder; + $arrayDimFetchHolder = new ConditionalExpressionHolder([ + '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType), + ], new ExpressionTypeHolder(new ArrayDimFetch($stmt->expr, $stmt->keyVar), $valueType, TrinaryLogic::createYes())); + $arrayDimFetchConditionalHolders[$arrayDimFetchHolder->getKey()] = $arrayDimFetchHolder; } $scope = $scope->addConditionalExpressions( '$' . $stmt->valueVar->name, - $conditionalHolders, + $valueConditionalHolders, ); + if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { + $scope = $scope->addConditionalExpressions( + sprintf('$%s[$%s]', $stmt->expr->name, $stmt->keyVar->name), + $arrayDimFetchConditionalHolders, + ); + } } return $this->processVarAnnotation($scope, $vars, $stmt); @@ -3670,8 +6252,20 @@ static function (): void { */ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, callable $nodeCallback): void { + $parentTraitNames = []; + $parent = $classScope->getParentScope(); + while ($parent !== null) { + if ($parent->isInTrait()) { + $parentTraitNames[] = $parent->getTraitReflection()->getName(); + } + $parent = $parent->getParentScope(); + } + foreach ($node->traits as $trait) { $traitName = (string) $trait; + if (in_array($traitName, $parentTraitNames, true)) { + continue; + } if (!$this->reflectionProvider->hasClass($traitName)) { continue; } @@ -3684,13 +6278,25 @@ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classS if (!isset($this->analysedFiles[$fileName])) { continue; } + $adaptations = []; + foreach ($node->adaptations as $adaptation) { + if ($adaptation->trait === null) { + $adaptations[] = $adaptation; + continue; + } + if ($adaptation->trait->toLowerString() !== $trait->toLowerString()) { + continue; + } + + $adaptations[] = $adaptation; + } $parserNodes = $this->parser->parseFile($fileName); - $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $node->adaptations, $nodeCallback); + $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $adaptations, $nodeCallback); } } /** - * @param Node[]|Node|scalar $node + * @param Node[]|Node|scalar|null $node * @param Node\Stmt\TraitUseAdaptation[] $adaptations * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -3699,16 +6305,22 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection if ($node instanceof Node) { if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) { $methodModifiers = []; + $methodNames = []; foreach ($adaptations as $adaptation) { if (!$adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { continue; } - if ($adaptation->newModifier === null) { + $methodName = $adaptation->method->toLowerString(); + if ($adaptation->newModifier !== null) { + $methodModifiers[$methodName] = $adaptation->newModifier; + } + + if ($adaptation->newName === null) { continue; } - $methodModifiers[$adaptation->method->toLowerString()] = $adaptation->newModifier; + $methodNames[$methodName] = $adaptation->newName; } $stmts = $node->stmts; @@ -3717,15 +6329,26 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection continue; } $methodName = $stmt->name->toLowerString(); - if (!array_key_exists($methodName, $methodModifiers)) { + $methodAst = clone $stmt; + $stmts[$i] = $methodAst; + if (array_key_exists($methodName, $methodModifiers)) { + $methodAst->flags = ($methodAst->flags & ~ Modifiers::VISIBILITY_MASK) | $methodModifiers[$methodName]; + } + + if (!array_key_exists($methodName, $methodNames)) { continue; } - $methodAst = clone $stmt; - $methodAst->flags = ($methodAst->flags & ~ Node\Stmt\Class_::VISIBILITY_MODIFIER_MASK) | $methodModifiers[$methodName]; - $stmts[$i] = $methodAst; + $methodAst->setAttribute('originalTraitMethodName', $methodAst->name->toLowerString()); + $methodAst->name = $methodNames[$methodName]; + } + + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); } - $this->processStmtNodes($node, $stmts, $scope->enterTrait($traitReflection), $nodeCallback); + $traitScope = $scope->enterTrait($traitReflection); + $nodeCallback(new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope); + $this->processStmtNodes($node, $stmts, $traitScope, $nodeCallback, StatementContext::createTopLevel()); return; } if ($node instanceof Node\Stmt\ClassLike) { @@ -3745,22 +6368,163 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection } } + private function processCalledMethod(MethodReflection $methodReflection): ?MutatingScope + { + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->isAnonymous()) { + return null; + } + if ($declaringClass->getFileName() === null) { + return null; + } + + $stackName = sprintf('%s::%s', $declaringClass->getName(), $methodReflection->getName()); + if (array_key_exists($stackName, $this->calledMethodResults)) { + return $this->calledMethodResults[$stackName]; + } + + if (array_key_exists($stackName, $this->calledMethodStack)) { + return null; + } + + if (count($this->calledMethodStack) > 0) { + return null; + } + + $this->calledMethodStack[$stackName] = true; + + $fileName = $this->fileHelper->normalizePath($declaringClass->getFileName()); + if (!isset($this->analysedFiles[$fileName])) { + return null; + } + $parserNodes = $this->parser->parseFile($fileName); + + $returnStatement = null; + $this->processNodesForCalledMethod($parserNodes, $fileName, $methodReflection, static function (Node $node, Scope $scope) use ($methodReflection, &$returnStatement): void { + if (!$node instanceof MethodReturnStatementsNode) { + return; + } + + if ($node->getClassReflection()->getName() !== $methodReflection->getDeclaringClass()->getName()) { + return; + } + + if ($returnStatement !== null) { + return; + } + + $returnStatement = $node; + }); + + $calledMethodEndScope = null; + if ($returnStatement !== null) { + foreach ($returnStatement->getExecutionEnds() as $executionEnd) { + $statementResult = $executionEnd->getStatementResult(); + $endNode = $executionEnd->getNode(); + if ($endNode instanceof Node\Stmt\Expression) { + $exprType = $statementResult->getScope()->getType($endNode->expr); + if ($exprType instanceof NeverType && $exprType->isExplicit()) { + continue; + } + } + if ($calledMethodEndScope === null) { + $calledMethodEndScope = $statementResult->getScope(); + continue; + } + + $calledMethodEndScope = $calledMethodEndScope->mergeWith($statementResult->getScope()); + } + foreach ($returnStatement->getReturnStatements() as $statement) { + if ($calledMethodEndScope === null) { + $calledMethodEndScope = $statement->getScope(); + continue; + } + + $calledMethodEndScope = $calledMethodEndScope->mergeWith($statement->getScope()); + } + } + + unset($this->calledMethodStack[$stackName]); + + $this->calledMethodResults[$stackName] = $calledMethodEndScope; + + return $calledMethodEndScope; + } + + /** + * @param Node[]|Node|scalar|null $node + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processNodesForCalledMethod($node, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void + { + if ($node instanceof Node) { + $declaringClass = $methodReflection->getDeclaringClass(); + if ( + $node instanceof Node\Stmt\Class_ + && isset($node->namespacedName) + && $declaringClass->getName() === (string) $node->namespacedName + && $declaringClass->getNativeReflection()->getStartLine() === $node->getStartLine() + ) { + + $stmts = $node->stmts; + foreach ($stmts as $stmt) { + if (!$stmt instanceof Node\Stmt\ClassMethod) { + continue; + } + + if ($stmt->name->toString() !== $methodReflection->getName()) { + continue; + } + + if ($stmt->getEndLine() - $stmt->getStartLine() > 50) { + continue; + } + + $scope = $this->scopeFactory->create(ScopeContext::create($fileName))->enterClass($declaringClass); + $this->processStmtNode($stmt, $scope, $nodeCallback, StatementContext::createTopLevel()); + } + return; + } + if ($node instanceof Node\Stmt\ClassLike) { + return; + } + if ($node instanceof Node\FunctionLike) { + return; + } + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->{$subNodeName}; + $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); + } + } elseif (is_array($node)) { + foreach ($node as $subNode) { + $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); + } + } + } + /** - * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null} + * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool} */ - public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array + public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { $templateTypeMap = TemplateTypeMap::createEmpty(); $phpDocParameterTypes = []; + $phpDocImmediatelyInvokedCallableParameters = []; + $phpDocClosureThisTypeParameters = []; $phpDocReturnType = null; $phpDocThrowType = null; $deprecatedDescription = null; $isDeprecated = false; $isInternal = false; $isFinal = false; - $isPure = false; - $docComment = $functionLike->getDocComment() !== null - ? $functionLike->getDocComment()->getText() + $isPure = null; + $isAllowedPrivateMutation = false; + $acceptsNamedArguments = true; + $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable(); + $asserts = Assertions::createEmpty(); + $selfOutType = null; + $docComment = $node->getDocComment() !== null + ? $node->getDocComment()->getText() : null; $file = $scope->getFile(); @@ -3768,30 +6532,31 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null; $resolvedPhpDoc = null; $functionName = null; + $phpDocParameterOutTypes = []; - if ($functionLike instanceof Node\Stmt\ClassMethod) { + if ($node instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { throw new ShouldNotHappenException(); } - $functionName = $functionLike->name->name; + $functionName = $node->name->name; $positionalParameterNames = array_map(static function (Node\Param $param): string { if (!$param->var instanceof Variable || !is_string($param->var->name)) { throw new ShouldNotHappenException(); } return $param->var->name; - }, $functionLike->getParams()); + }, $node->getParams()); $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( $docComment, $file, $scope->getClassReflection(), $trait, - $functionLike->name->name, + $node->name->name, $positionalParameterNames, ); - if ($functionLike->name->toLowerString() === '__construct') { - foreach ($functionLike->params as $param) { + if ($node->name->toLowerString() === '__construct') { + foreach ($node->params as $param) { if ($param->flags === 0) { continue; } @@ -3826,8 +6591,13 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $phpDocParameterTypes[$param->var->name] = $phpDocType; } } - } elseif ($functionLike instanceof Node\Stmt\Function_) { - $functionName = trim($scope->getNamespace() . '\\' . $functionLike->name->name, '\\'); + } elseif ($node instanceof Node\Stmt\Function_) { + $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\'); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $functionName = sprintf('$%s::%s', $propertyName, $node->name->toString()); + } } if ($docComment !== null && $resolvedPhpDoc === null) { @@ -3840,8 +6610,10 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array ); } + $varTags = []; if ($resolvedPhpDoc !== null) { $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + $phpDocImmediatelyInvokedCallableParameters = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { if (array_key_exists($paramName, $phpDocParameterTypes)) { continue; @@ -3852,10 +6624,26 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array } $phpDocParameterTypes[$paramName] = $paramType; } - $nativeReturnType = $scope->getFunctionType($functionLike->getReturnType(), false, false); - $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType); - if ($phpDocReturnType !== null && $scope->isInClass()) { - $phpDocReturnType = $this->transformStaticType($scope->getClassReflection(), $phpDocReturnType); + foreach ($resolvedPhpDoc->getParamClosureThisTags() as $paramName => $paramClosureThisTag) { + if (array_key_exists($paramName, $phpDocClosureThisTypeParameters)) { + continue; + } + $paramClosureThisType = $paramClosureThisTag->getType(); + if ($scope->isInClass()) { + $paramClosureThisType = $this->transformStaticType($scope->getClassReflection(), $paramClosureThisType); + } + $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType; + } + + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType(); + } + if ($node instanceof Node\FunctionLike) { + $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false); + $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType); + if ($phpDocReturnType !== null && $scope->isInClass()) { + $phpDocReturnType = $this->transformStaticType($scope->getClassReflection(), $phpDocReturnType); + } } $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; @@ -3863,9 +6651,18 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); + $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); + $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); + if ($acceptsNamedArguments && $scope->isInClass()) { + $acceptsNamedArguments = $scope->getClassReflection()->acceptsNamedArguments(); + } + $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly(); + $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; + $varTags = $resolvedPhpDoc->getVarTags(); } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation]; } private function transformStaticType(ClassReflection $declaringClass, Type $type): Type @@ -3873,7 +6670,7 @@ private function transformStaticType(ClassReflection $declaringClass, Type $type return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type { if ($type instanceof StaticType) { $changedType = $type->changeBaseClass($declaringClass); - if ($declaringClass->isFinal()) { + if ($declaringClass->isFinal() && !$type instanceof ThisType) { $changedType = $changedType->getStaticObjectType(); } return $traverse($changedType); @@ -3904,4 +6701,56 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $ return null; } + /** + * @param array $nodes + * @return list + */ + private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array + { + $stmts = []; + $isPassedUnreachableStatement = false; + foreach ($nodes as $node) { + if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { + continue; + } + if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) { + $stmts[] = $node; + continue; + } + if ($node instanceof Node\Stmt\Nop) { + continue; + } + if (!$node instanceof Node\Stmt) { + continue; + } + $stmts[] = $node; + $isPassedUnreachableStatement = true; + } + return $stmts; + } + + /** + * @param array $conditions + */ + public function getFilteringExprForMatchArm(Expr\Match_ $expr, array $conditions): BinaryOp\Identical|FuncCall + { + if (count($conditions) === 1) { + return new BinaryOp\Identical($expr->cond, $conditions[0]); + } + + $items = []; + foreach ($conditions as $filteringExpr) { + $items[] = new Node\ArrayItem($filteringExpr); + } + + return new FuncCall( + new Name\FullyQualified('in_array'), + [ + new Arg($expr->cond), + new Arg(new Array_($items)), + new Arg(new ConstFetch(new Name\FullyQualified('true'))), + ], + ); + } + } diff --git a/src/Analyser/NullsafeOperatorHelper.php b/src/Analyser/NullsafeOperatorHelper.php index 9b34346264..0b439191c7 100644 --- a/src/Analyser/NullsafeOperatorHelper.php +++ b/src/Analyser/NullsafeOperatorHelper.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr; use PHPStan\Type\TypeCombinator; -class NullsafeOperatorHelper +final class NullsafeOperatorHelper { public static function getNullsafeShortcircuitedExprRespectingScope(Scope $scope, Expr $expr): Expr diff --git a/src/Analyser/OutOfClassScope.php b/src/Analyser/OutOfClassScope.php index d9ddf23f2f..a2215bc25c 100644 --- a/src/Analyser/OutOfClassScope.php +++ b/src/Analyser/OutOfClassScope.php @@ -2,13 +2,14 @@ namespace PHPStan\Analyser; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\PropertyReflection; -class OutOfClassScope implements ClassMemberAccessAnswerer +final class OutOfClassScope implements ClassMemberAccessAnswerer { /** @api */ @@ -31,12 +32,24 @@ public function canAccessProperty(PropertyReflection $propertyReflection): bool return $propertyReflection->isPublic(); } + public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool + { + return $propertyReflection->isPublic(); + } + + public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool + { + return $propertyReflection->isPublic() + && !$propertyReflection->isProtectedSet() + && !$propertyReflection->isPrivateSet(); + } + public function canCallMethod(MethodReflection $methodReflection): bool { return $methodReflection->isPublic(); } - public function canAccessConstant(ConstantReflection $constantReflection): bool + public function canAccessConstant(ClassConstantReflection $constantReflection): bool { return $constantReflection->isPublic(); } diff --git a/src/Analyser/ProcessClosureResult.php b/src/Analyser/ProcessClosureResult.php new file mode 100644 index 0000000000..0051383278 --- /dev/null +++ b/src/Analyser/ProcessClosureResult.php @@ -0,0 +1,53 @@ +scope; + } + + /** + * @return ThrowPoint[] + */ + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + /** + * @return InvalidateExprNode[] + */ + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + +} diff --git a/src/Analyser/ResultCache/ResultCache.php b/src/Analyser/ResultCache/ResultCache.php index c4f5452802..1708a1f53b 100644 --- a/src/Analyser/ResultCache/ResultCache.php +++ b/src/Analyser/ResultCache/ResultCache.php @@ -3,17 +3,28 @@ namespace PHPStan\Analyser\ResultCache; use PHPStan\Analyser\Error; -use PHPStan\Dependency\ExportedNode; +use PHPStan\Analyser\FileAnalyserResult; +use PHPStan\Collectors\CollectedData; +use PHPStan\Dependency\RootExportedNode; -class ResultCache +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + * @phpstan-import-type CollectorData from CollectedData + */ +final class ResultCache { /** * @param string[] $filesToAnalyse * @param mixed[] $meta - * @param array> $errors + * @param array> $errors + * @param array> $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores + * @param CollectorData $collectedData * @param array> $dependencies - * @param array> $exportedNodes + * @param array> $exportedNodes + * @param array $projectExtensionFiles */ public function __construct( private array $filesToAnalyse, @@ -21,8 +32,13 @@ public function __construct( private int $lastFullAnalysisTime, private array $meta, private array $errors, + private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, + private array $collectedData, private array $dependencies, private array $exportedNodes, + private array $projectExtensionFiles, ) { } @@ -54,13 +70,45 @@ public function getMeta(): array } /** - * @return array> + * @return array> */ public function getErrors(): array { return $this->errors; } + /** + * @return array> + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return array + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return array + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + + /** + * @return CollectorData + */ + public function getCollectedData(): array + { + return $this->collectedData; + } + /** * @return array> */ @@ -70,11 +118,19 @@ public function getDependencies(): array } /** - * @return array> + * @return array> */ public function getExportedNodes(): array { return $this->exportedNodes; } + /** + * @return array + */ + public function getProjectExtensionFiles(): array + { + return $this->projectExtensionFiles; + } + } diff --git a/src/Analyser/ResultCache/ResultCacheClearer.php b/src/Analyser/ResultCache/ResultCacheClearer.php index d511a3fa92..75f3d25628 100644 --- a/src/Analyser/ResultCache/ResultCacheClearer.php +++ b/src/Analyser/ResultCache/ResultCacheClearer.php @@ -2,15 +2,14 @@ namespace PHPStan\Analyser\ResultCache; -use Symfony\Component\Finder\Finder; use function dirname; use function is_file; use function unlink; -class ResultCacheClearer +final class ResultCacheClearer { - public function __construct(private string $cacheFilePath, private string $tempResultCachePath) + public function __construct(private string $cacheFilePath) { } @@ -26,12 +25,4 @@ public function clear(): string return $dir; } - public function clearTemporaryCaches(): void - { - $finder = new Finder(); - foreach ($finder->files()->name('*.php')->in($this->tempResultCachePath) as $tmpResultCacheFile) { - @unlink($tmpResultCacheFile->getPathname()); - } - } - } diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 9780ba1cf7..e2ad34e817 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -2,18 +2,23 @@ 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\Analyser\FileAnalyserResult; +use PHPStan\Collectors\CollectedData; use PHPStan\Command\Output; -use PHPStan\Dependency\ExportedNode; use PHPStan\Dependency\ExportedNodeFetcher; +use PHPStan\Dependency\RootExportedNode; +use PHPStan\DependencyInjection\Container; +use PHPStan\DependencyInjection\ProjectConfigHelper; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileFinder; -use PHPStan\File\FileReader; +use PHPStan\File\FileHelper; use PHPStan\File\FileWriter; +use PHPStan\Internal\ArrayHelper; +use PHPStan\Internal\ComposerHelper; +use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; use Throwable; @@ -22,28 +27,33 @@ 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 explode; use function get_loaded_extensions; +use function implode; use function is_array; use function is_file; -use function is_string; use function ksort; -use function sha1; +use function microtime; +use function sha1_file; use function sort; use function sprintf; -use function str_replace; +use function str_starts_with; use function time; use function unlink; use function var_export; use const PHP_VERSION_ID; -class ResultCacheManager +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + * @phpstan-import-type CollectorData from CollectedData + */ +final class ResultCacheManager { - private const CACHE_VERSION = 'v9-project-extensions'; + private const CACHE_VERSION = 'v12-linesToIgnore'; /** @var array */ private array $fileHashes = []; @@ -54,28 +64,29 @@ class ResultCacheManager /** * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths - * @param string[] $stubFiles * @param string[] $bootstrapFiles * @param string[] $scanFiles * @param string[] $scanDirectories - * @param array $fileReplacements + * @param list> $parametersNotInvalidatingCache */ public function __construct( + private Container $container, private ExportedNodeFetcher $exportedNodeFetcher, private FileFinder $scanFileFinder, private ReflectionProvider $reflectionProvider, + private StubFilesProvider $stubFilesProvider, + private FileHelper $fileHelper, private string $cacheFilePath, - private string $tempResultCachePath, private array $analysedPaths, private array $composerAutoloaderProjectPaths, - private array $stubFiles, private string $usedLevel, private ?string $cliAutoloadFile, private array $bootstrapFiles, private array $scanFiles, private array $scanDirectories, - private array $fileReplacements, private bool $checkDependenciesOfProjectExtensionFiles, + private array $parametersNotInvalidatingCache, + private int $skipResultCacheIfOlderThanDays, ) { } @@ -84,90 +95,93 @@ public function __construct( * @param string[] $allAnalysedFiles * @param mixed[]|null $projectConfigArray */ - public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?array $projectConfigArray, Output $output, ?string $resultCacheName = null): ResultCache + public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?array $projectConfigArray, Output $output): ResultCache { + $startTime = microtime(true); if ($debug) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache not used because of debug mode.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } if ($onlyFiles) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache not used because only files were passed as analysed paths.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } $cacheFilePath = $this->cacheFilePath; - if ($resultCacheName !== null) { - $tmpCacheFile = $this->tempResultCachePath . '/' . $resultCacheName . '.php'; - if (is_file($tmpCacheFile)) { - $cacheFilePath = $tmpCacheFile; - } - } - if (!is_file($cacheFilePath)) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache not used because the cache file does not exist.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } try { $data = require $cacheFilePath; } catch (Throwable $e) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted(sprintf('Result cache not used because an error occurred while loading the cache file: %s', $e->getMessage())); } @unlink($cacheFilePath); - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } if (!is_array($data)) { @unlink($cacheFilePath); - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache not used because the cache file is corrupted.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } $meta = $this->getMeta($allAnalysedFiles, $projectConfigArray); if ($this->isMetaDifferent($data['meta'], $meta)) { - if ($output->isDebug()) { - $output->writeLineFormatted('Result cache not used because the metadata do not match.'); + if ($output->isVeryVerbose()) { + $diffs = $this->getMetaKeyDifferences($data['meta'], $meta); + $output->writeLineFormatted('Result cache not used because the metadata do not match: ' . implode(', ', $diffs)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } - if (time() - $data['lastFullAnalysisTime'] >= 60 * 60 * 24 * 7) { - if ($output->isDebug()) { - $output->writeLineFormatted('Result cache not used because it\'s more than 7 days since last full analysis.'); + $daysOldForSkip = $this->skipResultCacheIfOlderThanDays; + if (time() - $data['lastFullAnalysisTime'] >= 60 * 60 * 24 * $daysOldForSkip) { + if ($output->isVeryVerbose()) { + $output->writeLineFormatted(sprintf("Result cache not used because it's more than %d days since last full analysis.", $daysOldForSkip)); } - // run full analysis if the result cache is older than 7 days - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []); + // run full analysis if the result cache is older than X days + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } - foreach ($data['projectExtensionFiles'] as $extensionFile => $fileHash) { + /** + * @var string $fileHash + * @var bool $isAnalysed + */ + foreach ($data['projectExtensionFiles'] as $extensionFile => [$fileHash, $isAnalysed]) { + if (!$isAnalysed) { + continue; + } if (!is_file($extensionFile)) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted(sprintf('Result cache not used because extension file %s was not found.', $extensionFile)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } if ($this->getFileHash($extensionFile) === $fileHash) { continue; } - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted(sprintf('Result cache not used because extension file %s hash does not match.', $extensionFile)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } $invertedDependencies = $data['dependencies']; @@ -175,12 +189,20 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $filesToAnalyse = []; $invertedDependenciesToReturn = []; $errors = $data['errorsCallback'](); + $locallyIgnoredErrors = $data['locallyIgnoredErrorsCallback'](); + $linesToIgnore = $data['linesToIgnore']; + $unmatchedLineIgnores = $data['unmatchedLineIgnores']; + $collectedData = $data['collectedDataCallback'](); $exportedNodes = $data['exportedNodesCallback'](); $filteredErrors = []; + $filteredLocallyIgnoredErrors = []; + $filteredLinesToIgnore = []; + $filteredUnmatchedLineIgnores = []; + $filteredCollectedData = []; $filteredExportedNodes = []; $newFileAppeared = false; - foreach ($this->stubFiles as $stubFile) { + foreach ($this->getStubFiles() as $stubFile) { if (!array_key_exists($stubFile, $errors)) { continue; } @@ -192,6 +214,18 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? if (array_key_exists($analysedFile, $errors)) { $filteredErrors[$analysedFile] = $errors[$analysedFile]; } + if (array_key_exists($analysedFile, $locallyIgnoredErrors)) { + $filteredLocallyIgnoredErrors[$analysedFile] = $locallyIgnoredErrors[$analysedFile]; + } + if (array_key_exists($analysedFile, $linesToIgnore)) { + $filteredLinesToIgnore[$analysedFile] = $linesToIgnore[$analysedFile]; + } + if (array_key_exists($analysedFile, $unmatchedLineIgnores)) { + $filteredUnmatchedLineIgnores[$analysedFile] = $unmatchedLineIgnores[$analysedFile]; + } + if (array_key_exists($analysedFile, $collectedData)) { + $filteredCollectedData[$analysedFile] = $collectedData[$analysedFile]; + } if (array_key_exists($analysedFile, $exportedNodes)) { $filteredExportedNodes[$analysedFile] = $exportedNodes[$analysedFile]; } @@ -220,11 +254,13 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? } $cachedFileExportedNodes = $filteredExportedNodes[$analysedFile]; - if (count($dependentFiles) === 0) { + $exportedNodesChanged = $this->exportedNodesChanged($analysedFile, $cachedFileExportedNodes); + if ($exportedNodesChanged === null) { continue; } - if (!$this->exportedNodesChanged($analysedFile, $cachedFileExportedNodes)) { - continue; + + if ($exportedNodesChanged) { + $newFileAppeared = true; } foreach ($dependentFiles as $dependentFile) { @@ -256,7 +292,24 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? } } - return new ResultCache(array_unique($filesToAnalyse), false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $invertedDependenciesToReturn, $filteredExportedNodes); + $filesToAnalyse = array_unique($filesToAnalyse); + $filesToAnalyseCount = count($filesToAnalyse); + + if ($output->isVeryVerbose()) { + $elapsed = microtime(true) - $startTime; + $elapsedString = $elapsed > 5 + ? sprintf(' in %.1f seconds', $elapsed) + : ''; + + $output->writeLineFormatted(sprintf( + 'Result cache restored%s. %d %s will be reanalysed.', + $elapsedString, + $filesToAnalyseCount, + $filesToAnalyseCount === 1 ? 'file' : 'files', + )); + } + + return new ResultCache($filesToAnalyse, false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $filteredLocallyIgnoredErrors, $filteredLinesToIgnore, $filteredUnmatchedLineIgnores, $filteredCollectedData, $invertedDependenciesToReturn, $filteredExportedNodes, $data['projectExtensionFiles']); } /** @@ -267,6 +320,8 @@ private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool { $projectConfig = $currentMeta['projectConfig']; if ($projectConfig !== null) { + ksort($currentMeta['projectConfig']); + $currentMeta['projectConfig'] = Neon::encode($currentMeta['projectConfig']); } @@ -274,14 +329,58 @@ private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool } /** - * @param array $cachedFileExportedNodes + * @param mixed[] $cachedMeta + * @param mixed[] $currentMeta + * + * @return string[] */ - private function exportedNodesChanged(string $analysedFile, array $cachedFileExportedNodes): bool + private function getMetaKeyDifferences(array $cachedMeta, array $currentMeta): array { - if (array_key_exists($analysedFile, $this->fileReplacements)) { - $analysedFile = $this->fileReplacements[$analysedFile]; + $diffs = []; + foreach ($cachedMeta as $key => $value) { + if (!array_key_exists($key, $currentMeta)) { + $diffs[] = $key; + continue; + } + + if ($value === $currentMeta[$key]) { + continue; + } + + $diffs[] = $key; + } + + if ($diffs === []) { + // when none of the keys is different, + // the order of the keys is the problem + $diffs[] = 'keyOrder'; } + + return $diffs; + } + + /** + * @param array $cachedFileExportedNodes + * @return bool|null null means nothing changed, true means new root symbol appeared, false means nested node changed + */ + private function exportedNodesChanged(string $analysedFile, array $cachedFileExportedNodes): ?bool + { $fileExportedNodes = $this->exportedNodeFetcher->fetchNodes($analysedFile); + + $cachedSymbols = []; + foreach ($cachedFileExportedNodes as $cachedFileExportedNode) { + $cachedSymbols[$cachedFileExportedNode->getType()][] = $cachedFileExportedNode->getName(); + } + + $fileSymbols = []; + foreach ($fileExportedNodes as $fileExportedNode) { + $fileSymbols[$fileExportedNode->getType()][] = $fileExportedNode->getName(); + } + + if ($cachedSymbols !== $fileSymbols) { + return true; + } + if (count($fileExportedNodes) !== count($cachedFileExportedNodes)) { return true; } @@ -289,17 +388,14 @@ private function exportedNodesChanged(string $analysedFile, array $cachedFileExp foreach ($fileExportedNodes as $i => $fileExportedNode) { $cachedExportedNode = $cachedFileExportedNodes[$i]; if (!$cachedExportedNode->equals($fileExportedNode)) { - return true; + return false; } } - return false; + return null; } - /** - * @param bool|string $save - */ - public function process(AnalyserResult $analyserResult, ResultCache $resultCache, Output $output, bool $onlyFiles, $save): ResultCacheProcessResult + public function process(AnalyserResult $analyserResult, ResultCache $resultCache, Output $output, bool $onlyFiles, bool $save): ResultCacheProcessResult { $internalErrors = $analyserResult->getInternalErrors(); $freshErrorsByFile = []; @@ -307,23 +403,34 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $freshErrorsByFile[$error->getFilePath()][] = $error; } + $freshLocallyIgnoredErrorsByFile = []; + foreach ($analyserResult->getLocallyIgnoredErrors() as $error) { + $freshLocallyIgnoredErrorsByFile[$error->getFilePath()][] = $error; + } + + $freshCollectedDataByFile = $analyserResult->getCollectedData(); + $meta = $resultCache->getMeta(); - $doSave = function (array $errorsByFile, ?array $dependencies, array $exportedNodes, ?string $resultCacheName) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { + $projectConfigArray = $meta['projectConfig']; + if ($projectConfigArray !== null) { + $meta['projectConfig'] = Neon::encode($projectConfigArray); + } + $doSave = function (array $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, ?array $dependencies, array $exportedNodes, array $projectExtensionFiles) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { if ($onlyFiles) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache was not saved because only files were passed as analysed paths.'); } return false; } if ($dependencies === null) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache was not saved because of error in dependencies.'); } return false; } if (count($internalErrors) > 0) { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache was not saved because of internal errors.'); } return false; @@ -335,7 +442,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache continue; } - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted(sprintf('Result cache was not saved because of non-ignorable exception: %s', $error->getMessage())); } @@ -343,9 +450,9 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } - $this->save($resultCache->getLastFullAnalysisTime(), $resultCacheName, $errorsByFile, $dependencies, $exportedNodes, $meta); + $this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles, $meta); - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache is saved.'); } @@ -355,9 +462,13 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache if ($resultCache->isFullAnalysis()) { $saved = false; if ($save !== false) { - $saved = $doSave($freshErrorsByFile, $analyserResult->getDependencies(), $analyserResult->getExportedNodes(), is_string($save) ? $save : null); + $projectExtensionFiles = []; + if ($analyserResult->getDependencies() !== null) { + $projectExtensionFiles = $this->getProjectExtensionFiles($projectConfigArray, $analyserResult->getDependencies()); + } + $saved = $doSave($freshErrorsByFile, $freshLocallyIgnoredErrorsByFile, $analyserResult->getLinesToIgnore(), $analyserResult->getUnmatchedLineIgnores(), $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getExportedNodes(), $projectExtensionFiles); } else { - if ($output->isDebug()) { + if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache was not saved because it was not requested.'); } } @@ -366,12 +477,36 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } $errorsByFile = $this->mergeErrors($resultCache, $freshErrorsByFile); + $locallyIgnoredErrorsByFile = $this->mergeLocallyIgnoredErrors($resultCache, $freshLocallyIgnoredErrorsByFile); + $collectedDataByFile = $this->mergeCollectedData($resultCache, $freshCollectedDataByFile); $dependencies = $this->mergeDependencies($resultCache, $analyserResult->getDependencies()); $exportedNodes = $this->mergeExportedNodes($resultCache, $analyserResult->getExportedNodes()); + $linesToIgnore = $this->mergeLinesToIgnore($resultCache, $analyserResult->getLinesToIgnore()); + $unmatchedLineIgnores = $this->mergeUnmatchedLineIgnores($resultCache, $analyserResult->getUnmatchedLineIgnores()); $saved = false; if ($save !== false) { - $saved = $doSave($errorsByFile, $dependencies, $exportedNodes, is_string($save) ? $save : null); + $projectExtensionFiles = []; + foreach ($resultCache->getProjectExtensionFiles() as $file => [$hash, $isAnalysed, $className]) { + if ($isAnalysed) { + continue; + } + + // keep the same file hashes from the old run + // so that the message "When you edit them and re-run PHPStan, the result cache will get stale." + // keeps being shown on subsequent runs + $projectExtensionFiles[$file] = [$hash, false, $className]; + } + if ($dependencies !== null) { + foreach ($this->getProjectExtensionFiles($projectConfigArray, $dependencies) as $file => [$hash, $isAnalysed, $className]) { + if (!$isAnalysed) { + continue; + } + + $projectExtensionFiles[$file] = [$hash, true, $className]; + } + } + $saved = $doSave($errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles); } $flatErrors = []; @@ -381,18 +516,32 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } + $flatLocallyIgnoredErrors = []; + foreach ($locallyIgnoredErrorsByFile as $fileErrors) { + foreach ($fileErrors as $fileError) { + $flatLocallyIgnoredErrors[] = $fileError; + } + } + return new ResultCacheProcessResult(new AnalyserResult( $flatErrors, + $analyserResult->getFilteredPhpErrors(), + $analyserResult->getAllPhpErrors(), + $flatLocallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, $internalErrors, + $collectedDataByFile, $dependencies, $exportedNodes, $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), ), $saved); } /** - * @param array> $freshErrorsByFile - * @return array> + * @param array> $freshErrorsByFile + * @return array> */ private function mergeErrors(ResultCache $resultCache, array $freshErrorsByFile): array { @@ -408,6 +557,42 @@ private function mergeErrors(ResultCache $resultCache, array $freshErrorsByFile) return $errorsByFile; } + /** + * @param array> $freshLocallyIgnoredErrorsByFile + * @return array> + */ + private function mergeLocallyIgnoredErrors(ResultCache $resultCache, array $freshLocallyIgnoredErrorsByFile): array + { + $errorsByFile = $resultCache->getLocallyIgnoredErrors(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshLocallyIgnoredErrorsByFile)) { + unset($errorsByFile[$file]); + continue; + } + $errorsByFile[$file] = $freshLocallyIgnoredErrorsByFile[$file]; + } + + return $errorsByFile; + } + + /** + * @param CollectorData $freshCollectedDataByFile + * @return CollectorData + */ + private function mergeCollectedData(ResultCache $resultCache, array $freshCollectedDataByFile): array + { + $collectedDataByFile = $resultCache->getCollectedData(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshCollectedDataByFile)) { + unset($collectedDataByFile[$file]); + continue; + } + $collectedDataByFile[$file] = $freshCollectedDataByFile[$file]; + } + + return $collectedDataByFile; + } + /** * @param array>|null $freshDependencies * @return array>|null @@ -450,8 +635,8 @@ private function mergeDependencies(ResultCache $resultCache, ?array $freshDepend } /** - * @param array> $freshExportedNodes - * @return array> + * @param array> $freshExportedNodes + * @return array> */ private function mergeExportedNodes(ResultCache $resultCache, array $freshExportedNodes): array { @@ -469,17 +654,64 @@ private function mergeExportedNodes(ResultCache $resultCache, array $freshExport } /** - * @param array> $errors + * @param array $freshLinesToIgnore + * @return array + */ + private function mergeLinesToIgnore(ResultCache $resultCache, array $freshLinesToIgnore): array + { + $newLinesToIgnore = $resultCache->getLinesToIgnore(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshLinesToIgnore)) { + unset($newLinesToIgnore[$file]); + continue; + } + + $newLinesToIgnore[$file] = $freshLinesToIgnore[$file]; + } + + return $newLinesToIgnore; + } + + /** + * @param array $freshUnmatchedLineIgnores + * @return array + */ + private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $freshUnmatchedLineIgnores): array + { + $newUnmatchedLineIgnores = $resultCache->getUnmatchedLineIgnores(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshUnmatchedLineIgnores)) { + unset($newUnmatchedLineIgnores[$file]); + continue; + } + + $newUnmatchedLineIgnores[$file] = $freshUnmatchedLineIgnores[$file]; + } + + return $newUnmatchedLineIgnores; + } + + /** + * @param array> $errors + * @param array> $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores + * @param array>> $collectedData * @param array> $dependencies - * @param array> $exportedNodes + * @param array> $exportedNodes + * @param array $projectExtensionFiles * @param mixed[] $meta */ private function save( int $lastFullAnalysisTime, - ?string $resultCacheName, array $errors, + array $locallyIgnoredErrors, + array $linesToIgnore, + array $unmatchedLineIgnores, + array $collectedData, array $dependencies, array $exportedNodes, + array $projectExtensionFiles, array $meta, ): void { @@ -514,125 +746,105 @@ private function save( } ksort($errors); + ksort($locallyIgnoredErrors); + ksort($linesToIgnore); + ksort($unmatchedLineIgnores); + ksort($collectedData); ksort($invertedDependencies); + foreach ($collectedData as & $collectedDataPerFile) { + ksort($collectedDataPerFile); + } + foreach ($invertedDependencies as $file => $fileData) { $dependentFiles = $fileData['dependentFiles']; sort($dependentFiles); $invertedDependencies[$file]['dependentFiles'] = $dependentFiles; } - $template = <<<'php' - %s, - 'meta' => %s, - 'projectExtensionFiles' => %s, - 'errorsCallback' => static function (): array { return %s; }, - 'dependencies' => %s, - 'exportedNodesCallback' => static function (): array { return %s; }, -]; -php; - ksort($exportedNodes); $file = $this->cacheFilePath; - if ($resultCacheName !== null) { - $file = $this->tempResultCachePath . '/' . $resultCacheName . '.php'; - } - - $projectConfigArray = $meta['projectConfig']; - if ($projectConfigArray !== null) { - $meta['projectConfig'] = Neon::encode($projectConfigArray); - } FileWriter::write( $file, - sprintf( - $template, - var_export($lastFullAnalysisTime, true), - var_export($meta, true), - var_export($this->getProjectExtensionFiles($projectConfigArray, $dependencies), true), - var_export($errors, true), - var_export($invertedDependencies, true), - var_export($exportedNodes, true), - ), + " " . var_export($lastFullAnalysisTime, true) . ", + 'meta' => " . var_export($meta, true) . ", + 'projectExtensionFiles' => " . var_export($projectExtensionFiles, true) . ", + 'errorsCallback' => static function (): array { return " . var_export($errors, true) . "; }, + 'locallyIgnoredErrorsCallback' => static function (): array { return " . var_export($locallyIgnoredErrors, true) . "; }, + 'linesToIgnore' => " . var_export($linesToIgnore, true) . ", + 'unmatchedLineIgnores' => " . var_export($unmatchedLineIgnores, true) . ", + 'collectedDataCallback' => static function (): array { return " . var_export($collectedData, true) . "; }, + 'dependencies' => " . var_export($invertedDependencies, true) . ", + 'exportedNodesCallback' => static function (): array { return " . var_export($exportedNodes, true) . '; }, +]; +', ); } /** * @param mixed[]|null $projectConfig * @param array $dependencies - * @return array + * @return array */ private function getProjectExtensionFiles(?array $projectConfig, array $dependencies): array { $this->alreadyProcessed = []; $projectExtensionFiles = []; if ($projectConfig !== null) { - $services = array_merge( - $projectConfig['services'] ?? [], - $projectConfig['rules'] ?? [], - ); - foreach ($services as $service) { - $classes = $this->getClassesFromConfigDefinition($service); - if (is_array($service)) { - foreach (['class', 'factory', 'implement'] as $key) { - if (!isset($service[$key])) { - continue; - } + $vendorDirs = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloaderProjectPath) { + $composer = ComposerHelper::getComposerConfig($autoloaderProjectPath); + if ($composer === null) { + continue; + } + $vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig($autoloaderProjectPath, $composer); + $vendorDirs[] = $this->fileHelper->normalizePath($vendorDirectory); + } - $classes = array_merge($classes, $this->getClassesFromConfigDefinition($service[$key])); - } + $classes = ProjectConfigHelper::getServiceClassNames($projectConfig); + foreach ($classes as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + continue; } - foreach (array_unique($classes) as $class) { - if (!$this->reflectionProvider->hasClass($class)) { - continue; - } + $classReflection = $this->reflectionProvider->getClass($class); + $fileName = $classReflection->getFileName(); + if ($fileName === null) { + continue; + } - $classReflection = $this->reflectionProvider->getClass($class); - $fileName = $classReflection->getFileName(); - if ($fileName === null) { - continue; - } + if (str_starts_with($fileName, 'phar://')) { + continue; + } - $allServiceFiles = $this->getAllDependencies($fileName, $dependencies); - foreach ($allServiceFiles as $serviceFile) { - if (array_key_exists($serviceFile, $projectExtensionFiles)) { - continue; + $allServiceFiles = $this->getAllDependencies($fileName, $dependencies); + if (count($allServiceFiles) === 0) { + $normalizedFileName = $this->fileHelper->normalizePath($fileName); + foreach ($vendorDirs as $vendorDir) { + if (str_starts_with($normalizedFileName, $vendorDir)) { + continue 2; } - - $projectExtensionFiles[$serviceFile] = $this->getFileHash($serviceFile); } + $projectExtensionFiles[$fileName] = [$this->getFileHash($fileName), false, $class]; + continue; } - } - } - - return $projectExtensionFiles; - } - /** - * @param mixed $definition - * @return string[] - */ - private function getClassesFromConfigDefinition($definition): array - { - if (is_string($definition)) { - return [$definition]; - } + foreach ($allServiceFiles as $serviceFile) { + if (array_key_exists($serviceFile, $projectExtensionFiles)) { + continue; + } - if ($definition instanceof Statement) { - $entity = $definition->entity; - if (is_string($entity)) { - return [$entity]; - } elseif (is_array($entity) && isset($entity[0]) && is_string($entity[0])) { - return [$entity[0]]; + $projectExtensionFiles[$serviceFile] = [$this->getFileHash($serviceFile), true, $class]; + } } } - return []; + return $projectExtensionFiles; } /** @@ -675,19 +887,18 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a sort($extensions); if ($projectConfigArray !== null) { - unset($projectConfigArray['parameters']['ignoreErrors']); - unset($projectConfigArray['parameters']['tipsOfTheDay']); - unset($projectConfigArray['parameters']['parallel']); - unset($projectConfigArray['parameters']['internalErrorsCountLimit']); - unset($projectConfigArray['parameters']['cache']); - unset($projectConfigArray['parameters']['reportUnmatchedIgnoredErrors']); - unset($projectConfigArray['parameters']['memoryLimitFile']); - unset($projectConfigArray['parametersSchema']); + foreach ($this->parametersNotInvalidatingCache as $parameterPath) { + $pathAsArray = is_array($parameterPath) ? $parameterPath : explode('.', $parameterPath); + ArrayHelper::unsetKeyAtPath($projectConfigArray, $pathAsArray); + } + + ksort($projectConfigArray); } return [ 'cacheVersion' => self::CACHE_VERSION, - 'phpstanVersion' => $this->getPhpStanVersion(), + 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), + 'metaExtensions' => $this->getMetaFromPhpStanExtensions(), 'phpVersion' => PHP_VERSION_ID, 'projectConfig' => $projectConfigArray, 'analysedPaths' => $this->analysedPaths, @@ -703,17 +914,14 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a private function getFileHash(string $path): string { - if (array_key_exists($path, $this->fileReplacements)) { - $path = $this->fileReplacements[$path]; - } if (array_key_exists($path, $this->fileHashes)) { return $this->fileHashes[$path]; } - $contents = FileReader::read($path); - $contents = str_replace("\r\n", "\n", $contents); - - $hash = sha1($contents); + $hash = sha1_file($path); + if ($hash === false) { + throw new CouldNotReadFileException($path); + } $this->fileHashes[$path] = $hash; return $hash; @@ -761,15 +969,6 @@ private function getExecutedFileHashes(): array return $hashes; } - private function getPhpStanVersion(): string - { - try { - return PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); - } catch (OutOfBoundsException) { - return 'Version unknown'; - } - } - /** * @return array */ @@ -795,7 +994,13 @@ private function getComposerInstalled(): array { $data = []; foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { - $filePath = $autoloadPath . '/vendor/composer/installed.php'; + $composer = ComposerHelper::getComposerConfig($autoloadPath); + + if ($composer === null) { + continue; + } + + $filePath = ComposerHelper::getVendorDirFromComposerConfig($autoloadPath, $composer) . '/composer/installed.php'; if (!is_file($filePath)) { continue; } @@ -817,7 +1022,7 @@ private function getComposerInstalled(): array private function getStubFiles(): array { $stubFiles = []; - foreach ($this->stubFiles as $stubFile) { + foreach ($this->stubFilesProvider->getProjectStubFiles() as $stubFile) { $stubFiles[$stubFile] = $this->getFileHash($stubFile); } @@ -826,4 +1031,29 @@ private function getStubFiles(): array return $stubFiles; } + /** + * @return array + * @throws ShouldNotHappenException + */ + private function getMetaFromPhpStanExtensions(): array + { + $meta = []; + + /** @var ResultCacheMetaExtension $extension */ + foreach ($this->container->getServicesByTag(ResultCacheMetaExtension::EXTENSION_TAG) as $extension) { + if (array_key_exists($extension->getKey(), $meta)) { + throw new ShouldNotHappenException(sprintf( + 'Duplicate ResultCacheMetaExtension with key "%s" found.', + $extension->getKey(), + )); + } + + $meta[$extension->getKey()] = $extension->getHash(); + } + + ksort($meta); + + return $meta; + } + } diff --git a/src/Analyser/ResultCache/ResultCacheManagerFactory.php b/src/Analyser/ResultCache/ResultCacheManagerFactory.php index 333bc6136e..269f745015 100644 --- a/src/Analyser/ResultCache/ResultCacheManagerFactory.php +++ b/src/Analyser/ResultCache/ResultCacheManagerFactory.php @@ -5,9 +5,6 @@ interface ResultCacheManagerFactory { - /** - * @param array $fileReplacements - */ - public function create(array $fileReplacements): ResultCacheManager; + public function create(): ResultCacheManager; } diff --git a/src/Analyser/ResultCache/ResultCacheMetaExtension.php b/src/Analyser/ResultCache/ResultCacheMetaExtension.php new file mode 100644 index 0000000000..11aac2512f --- /dev/null +++ b/src/Analyser/ResultCache/ResultCacheMetaExtension.php @@ -0,0 +1,39 @@ + + */ + public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult + { + if ( + $expr->left instanceof Variable + && is_string($expr->left->name) + && $expr->right instanceof Variable + && is_string($expr->right->name) + && $expr->left->name === $expr->right->name + ) { + return new TypeResult(new ConstantBooleanType(true), []); + } + + $leftType = $scope->getType($expr->left); + $rightType = $scope->getType($expr->right); + + if (!$scope instanceof MutatingScope) { + return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType); + } + + if ( + ( + $expr->left instanceof Node\Expr\PropertyFetch + || $expr->left instanceof Node\Expr\StaticPropertyFetch + ) + && $rightType->isNull()->yes() + && !$scope->hasPropertyNativeType($expr->left) + ) { + return new TypeResult(new BooleanType(), []); + } + + if ( + ( + $expr->right instanceof Node\Expr\PropertyFetch + || $expr->right instanceof Node\Expr\StaticPropertyFetch + ) + && $leftType->isNull()->yes() + && !$scope->hasPropertyNativeType($expr->right) + ) { + return new TypeResult(new BooleanType(), []); + } + + return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType); + } + + /** + * @return TypeResult + */ + public function getNotIdenticalResult(Scope $scope, Node\Expr\BinaryOp\NotIdentical $expr): TypeResult + { + $identicalResult = $this->getIdenticalResult($scope, new Identical($expr->left, $expr->right)); + $identicalType = $identicalResult->type; + if ($identicalType instanceof ConstantBooleanType) { + return new TypeResult(new ConstantBooleanType(!$identicalType->getValue()), $identicalResult->reasons); + } + + return new TypeResult(new BooleanType(), []); + } + +} diff --git a/src/Analyser/RuleErrorTransformer.php b/src/Analyser/RuleErrorTransformer.php new file mode 100644 index 0000000000..b45ce15acd --- /dev/null +++ b/src/Analyser/RuleErrorTransformer.php @@ -0,0 +1,88 @@ + $nodeType + */ + public function transform( + RuleError $ruleError, + Scope $scope, + string $nodeType, + int $nodeLine, + ): Error + { + $line = $nodeLine; + $canBeIgnored = true; + $fileName = $scope->getFileDescription(); + $filePath = $scope->getFile(); + $traitFilePath = null; + $tip = null; + $identifier = null; + $metadata = []; + if ($scope->isInTrait()) { + $traitReflection = $scope->getTraitReflection(); + if ($traitReflection->getFileName() !== null) { + $traitFilePath = $traitReflection->getFileName(); + } + } + + if ( + $ruleError instanceof LineRuleError + && $ruleError->getLine() !== -1 + ) { + $line = $ruleError->getLine(); + } + if ( + $ruleError instanceof FileRuleError + && $ruleError->getFile() !== '' + ) { + $fileName = $ruleError->getFileDescription(); + $filePath = $ruleError->getFile(); + $traitFilePath = null; + } + + if ($ruleError instanceof TipRuleError) { + $tip = $ruleError->getTip(); + } + + if ($ruleError instanceof IdentifierRuleError) { + $identifier = $ruleError->getIdentifier(); + } + + if ($ruleError instanceof MetadataRuleError) { + $metadata = $ruleError->getMetadata(); + } + + if ($ruleError instanceof NonIgnorableRuleError) { + $canBeIgnored = false; + } + + return new Error( + $ruleError->getMessage(), + $fileName, + $line, + $canBeIgnored, + $filePath, + $traitFilePath, + $tip, + $nodeLine, + $nodeType, + $identifier, + $metadata, + ); + } + +} diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 82436e50ac..1134614b2f 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -6,39 +6,55 @@ use PhpParser\Node\Expr; use PhpParser\Node\Name; use PhpParser\Node\Param; +use PHPStan\Php\PhpVersions; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\NamespaceAnswerer; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; /** @api */ -interface Scope extends ClassMemberAccessAnswerer +interface Scope extends ClassMemberAccessAnswerer, NamespaceAnswerer { + public const SUPERGLOBAL_VARIABLES = [ + 'GLOBALS', + '_SERVER', + '_GET', + '_POST', + '_FILES', + '_COOKIE', + '_SESSION', + '_REQUEST', + '_ENV', + ]; + public function getFile(): string; public function getFileDescription(): string; public function isDeclareStrictTypes(): bool; + /** + * @phpstan-assert-if-true !null $this->getTraitReflection() + */ public function isInTrait(): bool; public function getTraitReflection(): ?ClassReflection; - /** - * @return FunctionReflection|MethodReflection|null - */ - public function getFunction(); + public function getFunction(): ?PhpFunctionFromParserNodeReflection; public function getFunctionName(): ?string; - public function getNamespace(): ?string; - public function getParentScope(): ?self; public function hasVariableType(string $variableName): TrinaryLogic; @@ -52,12 +68,27 @@ public function canAnyVariableExist(): bool; */ public function getDefinedVariables(): array; + /** + * @return array + */ + public function getMaybeDefinedVariables(): array; + public function hasConstant(Name $name): bool; - public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?PropertyReflection; + public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; + + public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection; - public function getMethodReflection(Type $typeWithMethod, string $methodName): ?MethodReflection; + public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection; + public function getIterableKeyType(Type $iteratee): Type; + + public function getIterableValueType(Type $iteratee): Type; + + /** + * @phpstan-assert-if-true !null $this->getAnonymousFunctionReflection() + * @phpstan-assert-if-true !null $this->getAnonymousFunctionReturnType() + */ public function isInAnonymousFunction(): bool; public function getAnonymousFunctionReflection(): ?ParametersAcceptor; @@ -66,15 +97,9 @@ public function getAnonymousFunctionReturnType(): ?Type; public function getType(Expr $node): Type; - /** - * Gets type of an expression with no regards to phpDocs. - * Works for function/method parameters only. - * - * @internal - */ public function getNativeType(Expr $expr): Type; - public function doNotTreatPhpDocTypesAsCertain(): self; + public function getKeepVoidType(Expr $node): Type; public function resolveName(Name $name): string; @@ -85,7 +110,7 @@ public function resolveTypeByName(Name $name): TypeWithClassName; */ public function getTypeFromValue($value): Type; - public function isSpecified(Expr $node): bool; + public function hasExpressionType(Expr $node): TrinaryLogic; public function isInClassExists(string $className): bool; @@ -93,6 +118,12 @@ public function isInFunctionExists(string $functionName): bool; public function isInClosureBind(): bool; + /** @return list */ + public function getFunctionCallStack(): array; + + /** @return list */ + public function getFunctionCallStackWithParameters(): array; + public function isParameterValueNullable(Param $parameter): bool; /** @@ -102,10 +133,14 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type public function isInExpressionAssign(Expr $expr): bool; + public function isUndefinedExpressionAllowed(Expr $expr): bool; + public function filterByTruthyValue(Expr $expr): self; public function filterByFalseyValue(Expr $expr): self; public function isInFirstLevelStatement(): bool; + public function getPhpVersion(): PhpVersions; + } diff --git a/src/Analyser/ScopeContext.php b/src/Analyser/ScopeContext.php index 4118909860..dfa0c1f17b 100644 --- a/src/Analyser/ScopeContext.php +++ b/src/Analyser/ScopeContext.php @@ -5,7 +5,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; -class ScopeContext +final class ScopeContext { private function __construct( diff --git a/src/Analyser/ScopeFactory.php b/src/Analyser/ScopeFactory.php index 1d7db30f3e..ade6e1d894 100644 --- a/src/Analyser/ScopeFactory.php +++ b/src/Analyser/ScopeFactory.php @@ -2,42 +2,19 @@ namespace PHPStan\Analyser; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Type\Type; - -interface ScopeFactory +/** + * @api + */ +final class ScopeFactory { - /** - * @api - * @param array $constantTypes - * @param VariableTypeHolder[] $variablesTypes - * @param VariableTypeHolder[] $moreSpecificTypes - * @param array $conditionalExpressions - * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array $inFunctionCallsStack - * - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - FunctionReflection|MethodReflection|null $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - bool $afterExtractCall = false, - ?Scope $parentScope = null, - ): MutatingScope; + public function __construct(private InternalScopeFactory $internalScopeFactory) + { + } + + public function create(ScopeContext $context): MutatingScope + { + return $this->internalScopeFactory->create($context); + } } diff --git a/src/Analyser/SpecifiedTypes.php b/src/Analyser/SpecifiedTypes.php index aefc6214f2..fd9ddda81d 100644 --- a/src/Analyser/SpecifiedTypes.php +++ b/src/Analyser/SpecifiedTypes.php @@ -6,24 +6,81 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class SpecifiedTypes +final class SpecifiedTypes { + private bool $overwrite = false; + + /** @var array */ + private array $newConditionalExpressionHolders = []; + + private ?Expr $rootExpr = null; + /** * @api * @param array $sureTypes * @param array $sureNotTypes - * @param array $newConditionalExpressionHolders */ public function __construct( private array $sureTypes = [], private array $sureNotTypes = [], - private bool $overwrite = false, - private array $newConditionalExpressionHolders = [], ) { } + /** + * Normally, $sureTypes in truthy context are used to intersect with the pre-existing type. + * And $sureNotTypes are used to remove type from the pre-existing type. + * + * Example: By default, non-empty-string intersected with '' (ConstantStringType) will lead to NeverType. + * Because it's not possible to narrow non-empty-string to an empty string. + * + * In rare cases, a type-specifying extension might want to overwrite the pre-existing types + * without taking the pre-existing types into consideration. + * + * In that case it should also call setAlwaysOverwriteTypes() on + * the returned object. + * + * ! Only do this if you're certain. Otherwise, this is a source of common bugs. ! + * + * @api + */ + public function setAlwaysOverwriteTypes(): self + { + $self = new self($this->sureTypes, $this->sureNotTypes); + $self->overwrite = true; + $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders; + $self->rootExpr = $this->rootExpr; + + return $self; + } + + /** + * @api + */ + public function setRootExpr(?Expr $rootExpr): self + { + $self = new self($this->sureTypes, $this->sureNotTypes); + $self->overwrite = $this->overwrite; + $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders; + $self->rootExpr = $rootExpr; + + return $self; + } + + /** + * @param array $newConditionalExpressionHolders + */ + public function setNewConditionalExpressionHolders(array $newConditionalExpressionHolders): self + { + $self = new self($this->sureTypes, $this->sureNotTypes); + $self->overwrite = $this->overwrite; + $self->newConditionalExpressionHolders = $newConditionalExpressionHolders; + $self->rootExpr = $this->rootExpr; + + return $self; + } + /** * @api * @return array @@ -55,11 +112,17 @@ public function getNewConditionalExpressionHolders(): array return $this->newConditionalExpressionHolders; } + public function getRootExpr(): ?Expr + { + return $this->rootExpr; + } + /** @api */ public function intersectWith(SpecifiedTypes $other): self { $sureTypeUnion = []; $sureNotTypeUnion = []; + $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr); foreach ($this->sureTypes as $exprString => [$exprNode, $type]) { if (!isset($other->sureTypes[$exprString])) { @@ -83,7 +146,12 @@ public function intersectWith(SpecifiedTypes $other): self ]; } - return new self($sureTypeUnion, $sureNotTypeUnion); + $result = new self($sureTypeUnion, $sureNotTypeUnion); + if ($this->overwrite && $other->overwrite) { + $result = $result->setAlwaysOverwriteTypes(); + } + + return $result->setRootExpr($rootExpr); } /** @api */ @@ -91,6 +159,7 @@ public function unionWith(SpecifiedTypes $other): self { $sureTypeUnion = $this->sureTypes + $other->sureTypes; $sureNotTypeUnion = $this->sureNotTypes + $other->sureNotTypes; + $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr); foreach ($this->sureTypes as $exprString => [$exprNode, $type]) { if (!isset($other->sureTypes[$exprString])) { @@ -114,7 +183,12 @@ public function unionWith(SpecifiedTypes $other): self ]; } - return new self($sureTypeUnion, $sureNotTypeUnion); + $result = new self($sureTypeUnion, $sureNotTypeUnion); + if ($this->overwrite || $other->overwrite) { + $result = $result->setAlwaysOverwriteTypes(); + } + + return $result->setRootExpr($rootExpr); } public function normalize(Scope $scope): self @@ -130,7 +204,25 @@ public function normalize(Scope $scope): self $sureTypes[$exprString][1] = TypeCombinator::remove($sureTypes[$exprString][1], $sureNotType); } - return new self($sureTypes, [], $this->overwrite, $this->newConditionalExpressionHolders); + $result = new self($sureTypes, []); + if ($this->overwrite) { + $result = $result->setAlwaysOverwriteTypes(); + } + + return $result->setRootExpr($this->rootExpr); + } + + private function mergeRootExpr(?Expr $rootExprA, ?Expr $rootExprB): ?Expr + { + if ($rootExprA === $rootExprB) { + return $rootExprA; + } + + if ($rootExprA === null || $rootExprB === null) { + return $rootExprA ?? $rootExprB; + } + + return null; } } diff --git a/src/Analyser/StatementContext.php b/src/Analyser/StatementContext.php new file mode 100644 index 0000000000..5fd5381601 --- /dev/null +++ b/src/Analyser/StatementContext.php @@ -0,0 +1,52 @@ +isTopLevel; + } + + public function enterDeep(): self + { + if ($this->isTopLevel) { + return self::createDeep(); + } + + return $this; + } + +} diff --git a/src/Analyser/StatementExitPoint.php b/src/Analyser/StatementExitPoint.php index bb372c1786..5c4916373e 100644 --- a/src/Analyser/StatementExitPoint.php +++ b/src/Analyser/StatementExitPoint.php @@ -4,7 +4,10 @@ use PhpParser\Node\Stmt; -class StatementExitPoint +/** + * @api + */ +final class StatementExitPoint { public function __construct(private Stmt $statement, private MutatingScope $scope) diff --git a/src/Analyser/StatementResult.php b/src/Analyser/StatementResult.php index 729e7a5ec0..dad528dc18 100644 --- a/src/Analyser/StatementResult.php +++ b/src/Analyser/StatementResult.php @@ -2,15 +2,20 @@ namespace PHPStan\Analyser; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; -class StatementResult +/** + * @api + */ +final class StatementResult { /** * @param StatementExitPoint[] $exitPoints * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints + * @param EndStatementResult[] $endStatements */ public function __construct( private MutatingScope $scope, @@ -18,6 +23,8 @@ public function __construct( private bool $isAlwaysTerminating, private array $exitPoints, private array $throwPoints, + private array $impurePoints, + private array $endStatements = [], ) { } @@ -50,15 +57,15 @@ public function filterOutLoopExitPoints(): self } $num = $statement->num; - if (!$num instanceof LNumber) { - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints); + if (!$num instanceof Int_) { + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); } if ($num->value !== 1) { continue; } - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); } return $this; @@ -74,7 +81,7 @@ public function getExitPoints(): array /** * @param class-string|class-string $stmtClass - * @return StatementExitPoint[] + * @return list */ public function getExitPointsByType(string $stmtClass): array { @@ -91,7 +98,7 @@ public function getExitPointsByType(string $stmtClass): array continue; } - if (!$value instanceof LNumber) { + if (!$value instanceof Int_) { $exitPoints[] = $exitPoint; continue; } @@ -108,7 +115,7 @@ public function getExitPointsByType(string $stmtClass): array } /** - * @return StatementExitPoint[] + * @return list */ public function getExitPointsForOuterLoop(): array { @@ -122,7 +129,7 @@ public function getExitPointsForOuterLoop(): array if ($statement->num === null) { continue; } - if (!$statement->num instanceof LNumber) { + if (!$statement->num instanceof Int_) { continue; } $value = $statement->num->value; @@ -132,7 +139,7 @@ public function getExitPointsForOuterLoop(): array $newNode = null; if ($value > 2) { - $newNode = new LNumber($value - 1); + $newNode = new Int_($value - 1); } if ($statement instanceof Stmt\Continue_) { $newStatement = new Stmt\Continue_($newNode); @@ -154,4 +161,33 @@ public function getThrowPoints(): array return $this->throwPoints; } + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + /** + * Top-level StatementResult represents the state of the code + * at the end of control flow statements like If_ or TryCatch. + * + * It shows how Scope etc. looks like after If_ no matter + * which code branch was executed. + * + * For If_, "end statements" contain the state of the code + * at the end of each branch - if, elseifs, else, including the last + * statement node in each branch. + * + * For nested ifs, end statements try to contain the last non-control flow + * statement like Return_ or Throw_, instead of If_, TryCatch, or Foreach_. + * + * @return EndStatementResult[] + */ + public function getEndStatements(): array + { + return $this->endStatements; + } + } diff --git a/src/Analyser/ThrowPoint.php b/src/Analyser/ThrowPoint.php index c25c5dc05d..873c11e425 100644 --- a/src/Analyser/ThrowPoint.php +++ b/src/Analyser/ThrowPoint.php @@ -8,7 +8,10 @@ use PHPStan\Type\TypeCombinator; use Throwable; -class ThrowPoint +/** + * @api + */ +final class ThrowPoint { /** diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 7b95f49614..761aa267ae 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use Countable; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; @@ -18,26 +19,42 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Name; -use PhpParser\PrettyPrinter\Standard; -use PHPStan\Node\VirtualNode; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\IssetExpr; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\ResolvedFunctionVariant; +use PHPStan\Rules\Arrays\AllowedArrayKeysTypes; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\ConstantType; -use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\FloatType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -48,6 +65,7 @@ use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\ResourceType; use PHPStan\Type\StaticMethodTypeSpecifyingExtension; use PHPStan\Type\StaticType; use PHPStan\Type\StaticTypeFactory; @@ -55,17 +73,20 @@ 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_key_exists; +use function array_map; use function array_merge; use function array_reverse; +use function array_shift; use function count; use function in_array; use function is_string; use function strtolower; +use function substr; +use const COUNT_NORMAL; -class TypeSpecifier +final class TypeSpecifier { /** @var MethodTypeSpecifyingExtension[][]|null */ @@ -80,11 +101,13 @@ class TypeSpecifier * @param StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions */ public function __construct( - private Standard $printer, + private ExprPrinter $exprPrinter, private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, private array $functionTypeSpecifyingExtensions, private array $methodTypeSpecifyingExtensions, private array $staticMethodTypeSpecifyingExtensions, + private bool $rememberPossiblyImpureFunctionValues, ) { foreach (array_merge($functionTypeSpecifyingExtensions, $methodTypeSpecifyingExtensions, $staticMethodTypeSpecifyingExtensions) as $extension) { @@ -104,7 +127,7 @@ public function specifyTypesInCondition( ): SpecifiedTypes { if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { - return new SpecifiedTypes(); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } if ($expr instanceof Instanceof_) { @@ -128,18 +151,21 @@ public function specifyTypesInCondition( } else { $type = new ObjectType($className); } - return $this->create($exprNode, $type, $context, false, $scope); + return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr); } $classType = $scope->getType($expr->class); - $type = TypeTraverser::map($classType, static function (Type $type, callable $traverse): Type { + $uncertainty = false; + $type = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use (&$uncertainty): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } - if ($type instanceof TypeWithClassName) { + if ($type->getObjectClassNames() !== []) { + $uncertainty = true; return $type; } if ($type instanceof GenericClassStringType) { + $uncertainty = true; return $type->getGenericType(); } if ($type instanceof ConstantStringType) { @@ -154,325 +180,70 @@ public function specifyTypesInCondition( $type, new ObjectWithoutClassType(), ); - return $this->create($exprNode, $type, $context, false, $scope); - } elseif ($context->false()) { + return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr); + } elseif ($context->false() && !$uncertainty) { $exprType = $scope->getType($expr->expr); if (!$type->isSuperTypeOf($exprType)->yes()) { - return $this->create($exprNode, $type, $context, false, $scope); + return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr); } } } if ($context->true()) { - return $this->create($exprNode, new ObjectWithoutClassType(), $context, false, $scope); + return $this->create($exprNode, new ObjectWithoutClassType(), $context, $scope)->setRootExpr($exprNode); } } elseif ($expr instanceof Node\Expr\BinaryOp\Identical) { - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); - if ($expressions !== null) { - /** @var Expr $exprNode */ - $exprNode = $expressions[0]; - /** @var ConstantScalarType $constantType */ - $constantType = $expressions[1]; - 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(), - )); - } - - if ($constantType->getValue() === true) { - $types = $this->create($exprNode, $constantType, $context, false, $scope); - return $types->unionWith($this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(), - )); - } - - if ($constantType->getValue() === null) { - return $this->create($exprNode, $constantType, $context, false, $scope); - } - - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) === 1 - && $exprNode->name instanceof Name - && in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true) - && $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->isArray()->yes()) { - $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->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); - } - } - } - } - - $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(); - $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; - $leftTypes = $this->create($expr->left, $never, $contextForTypes, false, $scope); - $rightTypes = $this->create($expr->right, $never, $contextForTypes, false, $scope); - return $leftTypes->unionWith($rightTypes); - } - } - - $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, - ); - } - } - 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, - ); - if ($types !== null) { - $types = $types->unionWith($leftType); - } else { - $types = $leftType; - } - } - } - - 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)); - } + return $this->resolveIdentical($expr, $scope, $context); } 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, - ); + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\Bool_) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\Equal($expr->expr, new ConstFetch(new Name\FullyQualified('true'))), + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\String_) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\String_('')), + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\Int_) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\LNumber(0)), + $context, + )->setRootExpr($expr); + } elseif ($expr instanceof Expr\Cast\Double) { + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\DNumber(0.0)), + $context, + )->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\BinaryOp\Equal) { - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); - if ($expressions !== null) { - /** @var Expr $exprNode */ - $exprNode = $expressions[0]; - /** @var ConstantScalarType $constantType */ - $constantType = $expressions[1]; - if ($constantType->getValue() === false || $constantType->getValue() === null) { - return $this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate(), - ); - } - - if ($constantType->getValue() === true) { - return $this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate(), - ); - } - } - - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); - - $leftBooleanType = $leftType->toBoolean(); - if ($leftBooleanType instanceof ConstantBooleanType && $rightType instanceof BooleanType) { - return $this->specifyTypesInCondition( - $scope, - new Expr\BinaryOp\Identical( - new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')), - $expr->right, - ), - $context, - ); - } - - $rightBooleanType = $rightType->toBoolean(); - if ($rightBooleanType instanceof ConstantBooleanType && $leftType instanceof BooleanType) { - return $this->specifyTypesInCondition( - $scope, - new Expr\BinaryOp\Identical( - $expr->left, - new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')), - ), - $context, - ); - } - - 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->getArgs()[0]) - && $rightType instanceof ConstantStringType - ) { - return $this->specifyTypesInCondition( - $scope, - new Instanceof_( - $expr->left->getArgs()[0]->value, - new Name($rightType->getValue()), - ), - $context, - ); - } - - if ( - $expr->right instanceof FuncCall - && $expr->right->name instanceof Name - && strtolower($expr->right->name->toString()) === 'get_class' - && isset($expr->right->getArgs()[0]) - && $leftType instanceof ConstantStringType - ) { - return $this->specifyTypesInCondition( - $scope, - new Instanceof_( - $expr->right->getArgs()[0]->value, - new Name($leftType->getValue()), - ), - $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)); + return $this->resolveEqual($expr, $scope, $context); } 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, - ); + )->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) { - $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; - $offset = $orEqual ? 0 : 1; - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); if ( $expr->left instanceof FuncCall - && count($expr->left->getArgs()) === 1 + && count($expr->left->getArgs()) >= 1 && $expr->left->name instanceof Name - && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen'], true) + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) && ( !$expr->right instanceof FuncCall || !$expr->right->name instanceof Name - || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen'], true) + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) ) ) { $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller @@ -483,45 +254,119 @@ public function specifyTypesInCondition( $scope, new Node\Expr\BooleanNot($inverseOperator), $context, - ); + )->setRootExpr($expr); } - $result = new SpecifiedTypes(); + $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; + $offset = $orEqual ? 0 : 1; + $leftType = $scope->getType($expr->left); + $result = (new SpecifiedTypes([], []))->setRootExpr($expr); if ( !$context->null() && $expr->right instanceof FuncCall - && count($expr->right->getArgs()) === 1 + && count($expr->right->getArgs()) >= 1 && $expr->right->name instanceof Name && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) - && (new IntegerType())->isSuperTypeOf($leftType)->yes() + && $leftType->isInteger()->yes() ) { + $argType = $scope->getType($expr->right->getArgs()[0]->value); + + if ($leftType instanceof ConstantIntegerType) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + } + } elseif ($leftType instanceof IntegerRangeType) { + $sizeType = $leftType->shift($offset); + } else { + $sizeType = $leftType; + } + + $specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + $result = $result->unionWith($specifiedTypes); + } + if ( - $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); + if ($context->truthy() && $argType->isArray()->maybe()) { + $countables = []; + if ($argType instanceof UnionType) { + $countableInterface = new ObjectType(Countable::class); + foreach ($argType->getTypes() as $innerType) { + if ($innerType->isArray()->yes()) { + $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType); + $countables[] = $innerType; + } + + if (!$countableInterface->isSuperTypeOf($innerType)->yes()) { + continue; + } + + $countables[] = $innerType; + } + } + + if (count($countables) > 0) { + $countableType = TypeCombinator::union(...$countables); + + return $this->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); + } + } + if ($argType->isArray()->yes()) { - $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope)); + $newType = new NonEmptyArrayType(); + if ($context->true() && $argType->isList()->yes()) { + $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); + } + + $result = $result->unionWith( + $this->create($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr), + ); } } } + if ( + !$context->null() + && $expr->right instanceof FuncCall + && count($expr->right->getArgs()) >= 3 + && $expr->right->name instanceof Name + && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\NotIdentical($expr->right, new ConstFetch(new Name('false'))), + $context, + )->setRootExpr($expr); + } + 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() + && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) + && $leftType->isInteger()->yes() ) { if ( - $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($argType instanceof StringType) { - $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new AccessoryNonEmptyStringType(), $context, false, $scope)); + if ($argType->isString()->yes()) { + $accessory = new AccessoryNonEmptyStringType(); + + if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } + + $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); } } } @@ -529,18 +374,21 @@ public function specifyTypesInCondition( if ($leftType instanceof ConstantIntegerType) { if ($expr->right instanceof Expr\PostInc) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), $context, )); } elseif ($expr->right instanceof Expr\PostDec) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), $context, )); } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), $context, @@ -548,21 +396,25 @@ public function specifyTypesInCondition( } } + $rightType = $scope->getType($expr->right); if ($rightType instanceof ConstantIntegerType) { if ($expr->left instanceof Expr\PostInc) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1), $context, )); } elseif ($expr->left instanceof Expr\PostDec) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), $context, )); } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), $context, @@ -575,22 +427,20 @@ public function specifyTypesInCondition( $result = $result->unionWith( $this->create( $expr->left, - $orEqual ? $rightType->getSmallerOrEqualType() : $rightType->getSmallerType(), + $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion), TypeSpecifierContext::createTruthy(), - false, $scope, - ), + )->setRootExpr($expr), ); } if (!$expr->right instanceof Node\Scalar) { $result = $result->unionWith( $this->create( $expr->right, - $orEqual ? $leftType->getGreaterOrEqualType() : $leftType->getGreaterType(), + $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion), TypeSpecifierContext::createTruthy(), - false, $scope, - ), + )->setRootExpr($expr), ); } } elseif ($context->false()) { @@ -598,22 +448,20 @@ public function specifyTypesInCondition( $result = $result->unionWith( $this->create( $expr->left, - $orEqual ? $rightType->getGreaterType() : $rightType->getGreaterOrEqualType(), + $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), TypeSpecifierContext::createTruthy(), - false, $scope, - ), + )->setRootExpr($expr), ); } if (!$expr->right instanceof Node\Scalar) { $result = $result->unionWith( $this->create( $expr->right, - $orEqual ? $leftType->getSmallerType() : $leftType->getSmallerOrEqualType(), + $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), TypeSpecifierContext::createTruthy(), - false, $scope, - ), + )->setRootExpr($expr), ); } } @@ -621,10 +469,10 @@ public function specifyTypesInCondition( return $result; } elseif ($expr instanceof Node\Expr\BinaryOp\Greater) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Smaller($expr->right, $expr->left), $context); + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Smaller($expr->right, $expr->left), $context)->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\BinaryOp\GreaterOrEqual) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context); + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context)->setRootExpr($expr); } elseif ($expr instanceof FuncCall && $expr->name instanceof Name) { if ($this->reflectionProvider->hasFunction($expr->name, $scope)) { @@ -636,19 +484,46 @@ public function specifyTypesInCondition( return $extension->specifyTypes($functionReflection, $expr, $scope, $context); } + + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + + $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + + $assertions = $functionReflection->getAsserts(); + if ($assertions->getAll() !== []) { + $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + + $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } } return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) { $methodCalledOnType = $scope->getType($expr->var); - $referencedClasses = TypeUtils::getDirectClassNames($methodCalledOnType); - if ( - count($referencedClasses) === 1 - && $this->reflectionProvider->hasClass($referencedClasses[0]) - ) { - $methodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]); - if ($methodClassReflection->hasMethod($expr->name->name)) { - $methodReflection = $methodClassReflection->getMethod($expr->name->name, $scope); + $methodReflection = $scope->getMethodReflection($methodCalledOnType, $expr->name->name); + if ($methodReflection !== null) { + $referencedClasses = $methodCalledOnType->getObjectClassNames(); + if ( + count($referencedClasses) === 1 + && $this->reflectionProvider->hasClass($referencedClasses[0]) + ) { + $methodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]); foreach ($this->getMethodTypeSpecifyingExtensionsForClass($methodClassReflection->getName()) as $extension) { if (!$extension->isMethodSupported($methodReflection, $expr, $context)) { continue; @@ -657,6 +532,33 @@ public function specifyTypesInCondition( return $extension->specifyTypes($methodReflection, $expr, $scope, $context); } } + + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + + $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + + $assertions = $methodReflection->getAsserts(); + if ($assertions->getAll() !== []) { + $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + + $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } } return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); @@ -669,7 +571,7 @@ public function specifyTypesInCondition( $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name); if ($staticMethodReflection !== null) { - $referencedClasses = TypeUtils::getDirectClassNames($calleeType); + $referencedClasses = $calleeType->getObjectClassNames(); if ( count($referencedClasses) === 1 && $this->reflectionProvider->hasClass($referencedClasses[0]) @@ -683,6 +585,33 @@ public function specifyTypesInCondition( return $extension->specifyTypes($staticMethodReflection, $expr, $scope, $context); } } + + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + + $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + + $assertions = $staticMethodReflection->getAsserts(); + if ($assertions->getAll() !== []) { + $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + + $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } } return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); @@ -690,20 +619,20 @@ public function specifyTypesInCondition( if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); } - $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context); + $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); $rightScope = $scope->filterByTruthyValue($expr->left); - $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context); + $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); $types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); if ($context->false()) { - return new SpecifiedTypes( + return (new SpecifiedTypes( $types->getSureTypes(), $types->getSureNotTypes(), - false, - array_merge( - $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes), - ), - ); + ))->setNewConditionalExpressionHolders(array_merge( + $this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes), + $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes), + ))->setRootExpr($expr); } return $types; @@ -711,151 +640,351 @@ public function specifyTypesInCondition( if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); } - $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context); + $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); $rightScope = $scope->filterByFalseyValue($expr->left); - $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context); + $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); $types = $context->true() ? $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)) : $leftTypes->unionWith($rightTypes); if ($context->true()) { - return new SpecifiedTypes( + return (new SpecifiedTypes( $types->getSureTypes(), $types->getSureNotTypes(), - false, - array_merge( - $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes), - ), - ); + ))->setNewConditionalExpressionHolders(array_merge( + $this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes), + $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes), + ))->setRootExpr($expr); } return $types; } elseif ($expr instanceof Node\Expr\BooleanNot && !$context->null()) { - return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate()); + return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate())->setRootExpr($expr); } elseif ($expr instanceof Node\Expr\Assign) { if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); } + if ($context->null()) { - return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context); + $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr); + + // infer $arr[$key] after $key = array_key_first/last($arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && count($expr->expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->expr->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + $iterableValueType = $expr->expr->name->toLowerString() === 'array_key_first' + ? $arrayType->getFirstIterableValueType() + : $arrayType->getLastIterableValueType(); + + return $specifiedTypes->unionWith( + $this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope), + ); + } + } + + // infer $list[$count] after $count = count($list) - 1 + if ( + $expr->expr instanceof Expr\BinaryOp\Minus + && $expr->expr->left instanceof FuncCall + && $expr->expr->left->name instanceof Name + && in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true) + && count($expr->expr->left->getArgs()) >= 1 + && $expr->expr->right instanceof Node\Scalar\Int_ + && $expr->expr->right->value === 1 + ) { + $arrayArg = $expr->expr->left->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayType->isList()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + return $specifiedTypes->unionWith( + $this->create($dimFetch, $arrayType->getLastIterableValueType(), TypeSpecifierContext::createTrue(), $scope), + ); + } + } + + return $specifiedTypes; } - return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context); + $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr); + + if ($context->true()) { + // infer $arr[$key] after $key = array_search($needle, $arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && $expr->expr->name->toLowerString() === 'array_search' + && count($expr->expr->getArgs()) >= 2 + ) { + $arrayArg = $expr->expr->getArgs()[1]->value; + $arrayType = $scope->getType($arrayArg); + + if ($arrayType->isArray()->yes()) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + $iterableValueType = $arrayType->getIterableValueType(); + + return $specifiedTypes->unionWith( + $this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope), + ); + } + } + } + return $specifiedTypes; } elseif ( $expr instanceof Expr\Isset_ && count($expr->vars) > 0 - && $context->true() + && !$context->null() ) { - $vars = []; - foreach ($expr->vars as $var) { - $tmpVars = [$var]; + // rewrite multi param isset() to and-chained single param isset() + if (count($expr->vars) > 1) { + $issets = []; + foreach ($expr->vars as $var) { + $issets[] = new Expr\Isset_([$var], $expr->getAttributes()); + } - while ( - $var instanceof ArrayDimFetch - || $var instanceof PropertyFetch - || ( - $var instanceof StaticPropertyFetch - && $var->class instanceof Expr - ) - ) { - if ($var instanceof StaticPropertyFetch) { - /** @var Expr $var */ - $var = $var->class; - } else { - $var = $var->var; + $first = array_shift($issets); + $andChain = null; + foreach ($issets as $isset) { + if ($andChain === null) { + $andChain = new BooleanAnd($first, $isset); + continue; } - $tmpVars[] = $var; + + $andChain = new BooleanAnd($andChain, $isset); } - $vars = array_merge($vars, array_reverse($tmpVars)); - } + if ($andChain === null) { + throw new ShouldNotHappenException(); + } - if (count($vars) === 0) { - throw new ShouldNotHappenException(); + return $this->specifyTypesInCondition($scope, $andChain, $context)->setRootExpr($expr); } - $types = null; - foreach ($vars as $var) { - if ($var instanceof Expr\Variable && is_string($var->name)) { - if ($scope->hasVariableType($var->name)->no()) { - return new SpecifiedTypes([], []); - } + $issetExpr = $expr->vars[0]; + + if (!$context->true()) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $isset = $scope->issetCheck($issetExpr, static fn () => true); + + if ($isset === false) { + return new SpecifiedTypes(); + } + + $type = $scope->getType($issetExpr); + $isNullable = !$type->isNull()->no(); + $exprType = $this->create( + $issetExpr, + new NullType(), + $context->negate(), + $scope, + )->setRootExpr($expr); + + if ($issetExpr instanceof Expr\Variable && is_string($issetExpr->name)) { + if ($isset === true) { + if ($isNullable) { + return $exprType; + } + + // variable cannot exist in !isset() + return $exprType->unionWith($this->create( + new IssetExpr($issetExpr), + new NullType(), + $context, + $scope, + ))->setRootExpr($expr); + } + + if ($isNullable) { + // reduces variable certainty to maybe + return $exprType->unionWith($this->create( + new IssetExpr($issetExpr), + new NullType(), + $context->negate(), + $scope, + ))->setRootExpr($expr); + } + + // variable cannot exist in !isset() + return $this->create( + new IssetExpr($issetExpr), + new NullType(), + $context, + $scope, + )->setRootExpr($expr); + } + + if ($isNullable && $isset === true) { + return $exprType; + } + + return new SpecifiedTypes(); + } + + $tmpVars = [$issetExpr]; + while ( + $issetExpr instanceof ArrayDimFetch + || $issetExpr instanceof PropertyFetch + || ( + $issetExpr instanceof StaticPropertyFetch + && $issetExpr->class instanceof Expr + ) + ) { + if ($issetExpr instanceof StaticPropertyFetch) { + /** @var Expr $issetExpr */ + $issetExpr = $issetExpr->class; + } else { + $issetExpr = $issetExpr->var; + } + $tmpVars[] = $issetExpr; + } + $vars = array_reverse($tmpVars); + + $types = new SpecifiedTypes(); + foreach ($vars as $var) { + + if ($var instanceof Expr\Variable && is_string($var->name)) { + if ($scope->hasVariableType($var->name)->no()) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } } + 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); + $dimType = $scope->getType($var->dim); + + if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { + $types = $types->unionWith( + $this->create( + $var->var, + new HasOffsetType($dimType), + $context, + $scope, + )->setRootExpr($expr), + ); + } else { + $varType = $scope->getType($var->var); + $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType, $dimType); + if ($narrowedKey !== null) { + $types = $types->unionWith( + $this->create( + $var->dim, + $narrowedKey, + $context, + $scope, + )->setRootExpr($expr), + ); + } + } } if ( $var instanceof PropertyFetch && $var->name instanceof Node\Identifier ) { - $type = $type->unionWith($this->create($var->var, new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType($var->name->toString()), - ]), TypeSpecifierContext::createTruthy(), false, $scope)); + $types = $types->unionWith( + $this->create($var->var, new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($var->name->toString()), + ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr), + ); } elseif ( $var instanceof StaticPropertyFetch && $var->class instanceof Expr && $var->name instanceof Node\VarLikeIdentifier ) { - $type = $type->unionWith($this->create($var->class, new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType($var->name->toString()), - ]), TypeSpecifierContext::createTruthy(), false, $scope)); + $types = $types->unionWith( + $this->create($var->class, new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($var->name->toString()), + ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr), + ); } - if ($types === null) { - $types = $type; - } else { - $types = $types->unionWith($type); - } + $types = $types->unionWith( + $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr), + ); } return $types; } elseif ( $expr instanceof Expr\BinaryOp\Coalesce - && $context->true() - && ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right))->yes()) + && !$context->null() ) { - return $this->create( - $expr->left, - new NullType(), - TypeSpecifierContext::createFalse(), - false, - $scope, - ); + if (!$context->true()) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $isset = $scope->issetCheck($expr->left, static fn () => true); + + if ($isset !== true) { + return new SpecifiedTypes(); + } + + return $this->create( + $expr->left, + new NullType(), + $context->negate(), + $scope, + )->setRootExpr($expr); + } + + if ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right)->toBoolean())->yes()) { + return $this->create( + $expr->left, + new NullType(), + TypeSpecifierContext::createFalse(), + $scope, + )->setRootExpr($expr); + } + } elseif ( $expr instanceof Expr\Empty_ ) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $isset = $scope->issetCheck($expr->expr, static fn () => true); + if ($isset === false) { + return new SpecifiedTypes(); + } + return $this->specifyTypesInCondition($scope, new BooleanOr( new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), new Expr\BooleanNot($expr->expr), - ), $context); + ), $context)->setRootExpr($expr); } elseif ($expr instanceof Expr\ErrorSuppress) { - return $this->specifyTypesInCondition($scope, $expr->expr, $context); + return $this->specifyTypesInCondition($scope, $expr->expr, $context)->setRootExpr($expr); } elseif ( $expr instanceof Expr\Ternary && !$context->null() - && ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->else))->yes()) + && $scope->getType($expr->else)->isFalse()->yes() ) { $conditionExpr = $expr->cond; if ($expr->if !== null) { $conditionExpr = new BooleanAnd($conditionExpr, $expr->if); } - return $this->specifyTypesInCondition($scope, $conditionExpr, $context); + return $this->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr); } elseif ($expr instanceof Expr\NullsafePropertyFetch && !$context->null()) { $types = $this->specifyTypesInCondition( @@ -865,7 +994,7 @@ public function specifyTypesInCondition( new PropertyFetch($expr->var, $expr->name), ), $context, - ); + )->setRootExpr($expr); $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); @@ -877,37 +1006,572 @@ public function specifyTypesInCondition( new MethodCall($expr->var, $expr->name, $expr->args), ), $context, - ); + )->setRootExpr($expr); $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); + } elseif ( + $expr instanceof Expr\New_ + && $expr->class instanceof Name + && $this->reflectionProvider->hasClass($expr->class->toString()) + ) { + $classReflection = $this->reflectionProvider->getClass($expr->class->toString()); + + if ($classReflection->hasConstructor()) { + $methodReflection = $classReflection->getConstructor(); + $asserts = $methodReflection->getAsserts(); + + if ($asserts->getAll() !== []) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + + $asserts = $asserts->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + } } elseif (!$context->null()) { return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } - return new SpecifiedTypes(); + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + private function specifyTypesForCountFuncCall( + FuncCall $countFuncCall, + Type $type, + Type $sizeType, + TypeSpecifierContext $context, + Scope $scope, + Expr $rootExpr, + ): ?SpecifiedTypes + { + if (count($countFuncCall->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($type->getIterableValueType()->isArray()->negate()); + } + + $isConstantArray = $type->isConstantArray(); + $isList = $type->isList(); + $oneOrMore = IntegerRangeType::fromInterval(1, null); + if ( + !$isNormalCount->yes() + || (!$isConstantArray->yes() && !$isList->yes()) + || !$oneOrMore->isSuperTypeOf($sizeType)->yes() + || $sizeType->isSuperTypeOf($type->getArraySize())->yes() + ) { + return null; + } + + $resultTypes = []; + foreach ($type->getArrays() as $arrayType) { + $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($arrayType->getArraySize()); + if ($isSizeSuperTypeOfArraySize->no()) { + continue; + } + + if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) { + continue; + } + + if ( + $sizeType instanceof ConstantIntegerType + && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT + && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getValue() - 1))->yes() + ) { + // turn optional offsets non-optional + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0; $i < $sizeType->getValue(); $i++) { + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType)); + } + $resultTypes[] = $valueTypesBuilder->getArray(); + continue; + } + + if ( + $sizeType instanceof IntegerRangeType + && $sizeType->getMin() !== null + && $sizeType->getMin() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT + && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, ($sizeType->getMax() ?? $sizeType->getMin()) - 1))->yes() + ) { + $builderData = []; + // turn optional offsets non-optional + for ($i = 0; $i < $sizeType->getMin(); $i++) { + $offsetType = new ConstantIntegerType($i); + $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), false]; + } + if ($sizeType->getMax() !== null) { + if ($sizeType->getMax() - $sizeType->getMin() > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $resultTypes[] = $arrayType; + continue; + } + for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) { + $offsetType = new ConstantIntegerType($i); + $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), true]; + } + } elseif ($arrayType->isConstantArray()->yes()) { + for ($i = $sizeType->getMin();; $i++) { + $offsetType = new ConstantIntegerType($i); + $hasOffset = $arrayType->hasOffsetValueType($offsetType); + if ($hasOffset->no()) { + break; + } + $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()]; + } + } else { + $resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + continue; + } + + if (count($builderData) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $resultTypes[] = $arrayType; + continue; + } + + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($builderData as [$offsetType, $valueType, $optional]) { + $builder->setOffsetValueType($offsetType, $valueType, $optional); + } + + $resultTypes[] = $builder->getArray(); + continue; + } + + $resultTypes[] = $arrayType; + } + + return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$resultTypes), $context, $scope)->setRootExpr($rootExpr); + } + + private function specifyTypesForConstantBinaryExpression( + Expr $exprNode, + Type $constantType, + TypeSpecifierContext $context, + Scope $scope, + Expr $rootExpr, + ): ?SpecifiedTypes + { + if (!$context->null() && $constantType->isFalse()->yes()) { + $types = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr); + if (!$context->true() && ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch)) { + return $types; + } + + return $types->unionWith($this->specifyTypesInCondition( + $scope, + $exprNode, + $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate(), + )->setRootExpr($rootExpr)); + } + + if (!$context->null() && $constantType->isTrue()->yes()) { + $types = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr); + if (!$context->true() && ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch)) { + return $types; + } + + return $types->unionWith($this->specifyTypesInCondition( + $scope, + $exprNode, + $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(), + )->setRootExpr($rootExpr)); + } + + return null; + } + + private function specifyTypesForConstantStringBinaryExpression( + Expr $exprNode, + Type $constantType, + TypeSpecifierContext $context, + Scope $scope, + Expr $rootExpr, + ): ?SpecifiedTypes + { + $scalarValues = $constantType->getConstantScalarValues(); + if (count($scalarValues) !== 1 || !is_string($scalarValues[0])) { + return null; + } + $constantStringValue = $scalarValues[0]; + + if ( + $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && strtolower($exprNode->name->toString()) === 'gettype' + && isset($exprNode->getArgs()[0]) + ) { + $type = null; + if ($constantStringValue === 'string') { + $type = new StringType(); + } + if ($constantStringValue === 'array') { + $type = new ArrayType(new MixedType(), new MixedType()); + } + if ($constantStringValue === 'boolean') { + $type = new BooleanType(); + } + if (in_array($constantStringValue, ['resource', 'resource (closed)'], true)) { + $type = new ResourceType(); + } + if ($constantStringValue === 'integer') { + $type = new IntegerType(); + } + if ($constantStringValue === 'double') { + $type = new FloatType(); + } + if ($constantStringValue === 'NULL') { + $type = new NullType(); + } + if ($constantStringValue === 'object') { + $type = new ObjectWithoutClassType(); + } + + if ($type !== null) { + $callType = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr); + $argType = $this->create($exprNode->getArgs()[0]->value, $type, $context, $scope)->setRootExpr($rootExpr); + return $callType->unionWith($argType); + } + } + + if ( + $context->true() + && $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && strtolower((string) $exprNode->name) === 'get_parent_class' + && isset($exprNode->getArgs()[0]) + ) { + $argType = $scope->getType($exprNode->getArgs()[0]->value); + $objectType = new ObjectType($constantStringValue); + $classStringType = new GenericClassStringType($objectType); + + if ($argType->isString()->yes()) { + return $this->create( + $exprNode->getArgs()[0]->value, + $classStringType, + $context, + $scope, + )->setRootExpr($rootExpr); + } + + if ($argType->isObject()->yes()) { + return $this->create( + $exprNode->getArgs()[0]->value, + $objectType, + $context, + $scope, + )->setRootExpr($rootExpr); + } + + return $this->create( + $exprNode->getArgs()[0]->value, + TypeCombinator::union($objectType, $classStringType), + $context, + $scope, + )->setRootExpr($rootExpr); + } + + return null; } private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr, Scope $scope): SpecifiedTypes { if ($context->null()) { - return new SpecifiedTypes(); + return (new SpecifiedTypes([], []))->setRootExpr($expr); } if (!$context->truthy()) { $type = StaticTypeFactory::truthy(); - return $this->create($expr, $type, TypeSpecifierContext::createFalse(), false, $scope); + return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr); } elseif (!$context->falsey()) { $type = StaticTypeFactory::falsey(); - return $this->create($expr, $type, TypeSpecifierContext::createFalse(), false, $scope); + return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr); + } + + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + private function specifyTypesFromConditionalReturnType( + TypeSpecifierContext $context, + Expr\CallLike $call, + ParametersAcceptor $parametersAcceptor, + Scope $scope, + ): ?SpecifiedTypes + { + if (!$parametersAcceptor instanceof ResolvedFunctionVariant) { + return null; + } + + $returnType = $parametersAcceptor->getOriginalParametersAcceptor()->getReturnType(); + if (!$returnType instanceof ConditionalTypeForParameter) { + return null; + } + + if ($context->true()) { + $leftType = new ConstantBooleanType(true); + $rightType = new ConstantBooleanType(false); + } elseif ($context->false()) { + $leftType = new ConstantBooleanType(false); + $rightType = new ConstantBooleanType(true); + } elseif ($context->null()) { + $leftType = new MixedType(); + $rightType = new NeverType(); + } else { + return null; + } + + $argsMap = []; + $parameters = $parametersAcceptor->getParameters(); + foreach ($call->getArgs() as $i => $arg) { + if ($arg->unpack) { + continue; + } + + if ($arg->name !== null) { + $paramName = $arg->name->toString(); + } elseif (isset($parameters[$i])) { + $paramName = $parameters[$i]->getName(); + } else { + continue; + } + + $argsMap['$' . $paramName] = $arg->value; + } + + return $this->getConditionalSpecifiedTypes($returnType, $leftType, $rightType, $scope, $argsMap); + } + + /** + * @param array $argsMap + */ + public function getConditionalSpecifiedTypes( + ConditionalTypeForParameter $conditionalType, + Type $leftType, + Type $rightType, + Scope $scope, + array $argsMap, + ): ?SpecifiedTypes + { + $parameterName = $conditionalType->getParameterName(); + if (!array_key_exists($parameterName, $argsMap)) { + return null; + } + + $targetType = $conditionalType->getTarget(); + $ifType = $conditionalType->getIf(); + $elseType = $conditionalType->getElse(); + + if ($leftType->isSuperTypeOf($ifType)->yes() && $rightType->isSuperTypeOf($elseType)->yes()) { + $context = $conditionalType->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(); + } elseif ($leftType->isSuperTypeOf($elseType)->yes() && $rightType->isSuperTypeOf($ifType)->yes()) { + $context = $conditionalType->isNegated() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse(); + } else { + return null; + } + + $specifiedTypes = $this->create( + $argsMap[$parameterName], + $targetType, + $context, + $scope, + ); + + if ($targetType instanceof ConstantBooleanType) { + if (!$targetType->getValue()) { + $context = $context->negate(); + } + + $specifiedTypes = $specifiedTypes->unionWith($this->specifyTypesInCondition($scope, $argsMap[$parameterName], $context)); + } + + return $specifiedTypes; + } + + private function specifyTypesFromAsserts(TypeSpecifierContext $context, Expr\CallLike $call, Assertions $assertions, ParametersAcceptor $parametersAcceptor, Scope $scope): ?SpecifiedTypes + { + if ($context->null()) { + $asserts = $assertions->getAsserts(); + } elseif ($context->true()) { + $asserts = $assertions->getAssertsIfTrue(); + } elseif ($context->false()) { + $asserts = $assertions->getAssertsIfFalse(); + } else { + throw new ShouldNotHappenException(); + } + + if (count($asserts) === 0) { + return null; + } + + $argsMap = []; + $parameters = $parametersAcceptor->getParameters(); + foreach ($call->getArgs() as $i => $arg) { + if ($arg->unpack) { + continue; + } + + if ($arg->name !== null) { + $paramName = $arg->name->toString(); + } elseif (isset($parameters[$i])) { + $paramName = $parameters[$i]->getName(); + } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { + $lastParameter = $parameters[count($parameters) - 1]; + $paramName = $lastParameter->getName(); + } else { + continue; + } + + $argsMap[$paramName][] = $arg->value; + } + + if ($call instanceof MethodCall) { + $argsMap['this'] = [$call->var]; + } + + /** @var SpecifiedTypes|null $types */ + $types = null; + + foreach ($asserts as $assert) { + foreach ($argsMap[substr($assert->getParameter()->getParameterName(), 1)] ?? [] as $parameterExpr) { + $assertedType = TypeTraverser::map($assert->getType(), static function (Type $type, callable $traverse) use ($argsMap, $scope): Type { + if ($type instanceof ConditionalTypeForParameter) { + $parameterName = substr($type->getParameterName(), 1); + if (array_key_exists($parameterName, $argsMap)) { + $argType = TypeCombinator::union(...array_map(static fn (Expr $expr) => $scope->getType($expr), $argsMap[$parameterName])); + $type = $type->toConditional($argType); + } + } + + return $traverse($type); + }); + + $assertExpr = $assert->getParameter()->getExpr($parameterExpr); + + $templateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); + $containsUnresolvedTemplate = false; + TypeTraverser::map( + $assert->getOriginalType(), + static function (Type $type, callable $traverse) use ($templateTypeMap, &$containsUnresolvedTemplate) { + if ($type instanceof TemplateType && $type->getScope()->getClassName() !== null) { + $resolvedType = $templateTypeMap->getType($type->getName()); + if ($resolvedType === null || $type->getBound()->equals($resolvedType)) { + $containsUnresolvedTemplate = true; + return $type; + } + } + + return $traverse($type); + }, + ); + + $newTypes = $this->create( + $assertExpr, + $assertedType, + $assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(), + $scope, + )->setRootExpr($containsUnresolvedTemplate || $assert->isEquality() ? $call : null); + $types = $types !== null ? $types->unionWith($newTypes) : $newTypes; + + if (!$context->null() || !$assertedType instanceof ConstantBooleanType) { + continue; + } + + $subContext = $assertedType->getValue() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse(); + if ($assert->isNegated()) { + $subContext = $subContext->negate(); + } + + $types = $types->unionWith($this->specifyTypesInCondition( + $scope, + $assertExpr, + $subContext, + )); + } + } + + return $types; + } + + /** + * @return array + */ + private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array + { + $conditionExpressionTypes = []; + foreach ($leftTypes->getSureTypes() as $exprString => [$expr, $type]) { + if (!$expr instanceof Expr\Variable) { + continue; + } + if (!is_string($expr->name)) { + continue; + } + + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( + $expr, + TypeCombinator::remove($scope->getType($expr), $type), + ); + } + + if (count($conditionExpressionTypes) > 0) { + $holders = []; + foreach ($rightTypes->getSureTypes() as $exprString => [$expr, $type]) { + if (!$expr instanceof Expr\Variable) { + continue; + } + if (!is_string($expr->name)) { + continue; + } + + if (!isset($holders[$exprString])) { + $holders[$exprString] = []; + } + + $conditions = $conditionExpressionTypes; + foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { + $conditionExpr = $conditionExprTypeHolder->getExpr(); + if (!$conditionExpr instanceof Expr\Variable) { + continue; + } + if (!is_string($conditionExpr->name)) { + continue; + } + if ($conditionExpr->name !== $expr->name) { + continue; + } + + unset($conditions[$conditionExprString]); + } + + if (count($conditions) === 0) { + continue; + } + + $holder = new ConditionalExpressionHolder( + $conditions, + new ExpressionTypeHolder($expr, TypeCombinator::intersect($scope->getType($expr), $type), TrinaryLogic::createYes()), + ); + $holders[$exprString][$holder->getKey()] = $holder; + } + + return $holders; } - return new SpecifiedTypes(); + return []; } /** * @return array */ - private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array + private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array { $conditionExpressionTypes = []; foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) { @@ -918,7 +1582,10 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le continue; } - $conditionExpressionTypes[$exprString] = TypeCombinator::intersect($scope->getType($expr), $type); + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( + $expr, + TypeCombinator::intersect($scope->getType($expr), $type), + ); } if (count($conditionExpressionTypes) > 0) { @@ -935,10 +1602,31 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le $holders[$exprString] = []; } - $holders[$exprString][] = new ConditionalExpressionHolder( - $conditionExpressionTypes, - new VariableTypeHolder(TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()), + $conditions = $conditionExpressionTypes; + foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { + $conditionExpr = $conditionExprTypeHolder->getExpr(); + if (!$conditionExpr instanceof Expr\Variable) { + continue; + } + if (!is_string($conditionExpr->name)) { + continue; + } + if ($conditionExpr->name !== $expr->name) { + continue; + } + + unset($conditions[$conditionExprString]); + } + + if (count($conditions) === 0) { + continue; + } + + $holder = new ConditionalExpressionHolder( + $conditions, + new ExpressionTypeHolder($expr, TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()), ); + $holders[$exprString][$holder->getKey()] = $holder; } return $holders; @@ -948,24 +1636,35 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le } /** - * @return (Expr|ConstantScalarType)[]|null + * @return array{Expr, ConstantScalarType, Type}|null */ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array { $leftType = $scope->getType($binaryOperation->left); $rightType = $scope->getType($binaryOperation->right); + + $rightExpr = $binaryOperation->right; + if ($rightExpr instanceof AlwaysRememberedExpr) { + $rightExpr = $rightExpr->getExpr(); + } + + $leftExpr = $binaryOperation->left; + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftExpr = $leftExpr->getExpr(); + } + if ( $leftType instanceof ConstantScalarType - && !$binaryOperation->right instanceof ConstFetch - && !$binaryOperation->right instanceof ClassConstFetch + && !$rightExpr instanceof ConstFetch + && !$rightExpr instanceof ClassConstFetch ) { - return [$binaryOperation->right, $leftType]; + return [$binaryOperation->right, $leftType, $rightType]; } elseif ( $rightType instanceof ConstantScalarType - && !$binaryOperation->left instanceof ConstFetch - && !$binaryOperation->left instanceof ClassConstFetch + && !$leftExpr instanceof ConstFetch + && !$leftExpr instanceof ClassConstFetch ) { - return [$binaryOperation->left, $rightType]; + return [$binaryOperation->left, $rightType, $leftType]; } return null; @@ -976,31 +1675,76 @@ public function create( Expr $expr, Type $type, TypeSpecifierContext $context, - bool $overwrite = false, - ?Scope $scope = null, + Scope $scope, ): SpecifiedTypes { - if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_ || $expr instanceof VirtualNode) { - return new SpecifiedTypes(); + if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); } - while ($expr instanceof Expr\Assign) { - $expr = $expr->var; + $specifiedExprs = []; + if ($expr instanceof AlwaysRememberedExpr) { + $specifiedExprs[] = $expr; + $expr = $expr->expr; } - if ($scope !== null) { - if ($context->true()) { - $resultType = TypeCombinator::intersect($scope->getType($expr), $type); - } elseif ($context->false()) { - $resultType = TypeCombinator::remove($scope->getType($expr), $type); + if ($expr instanceof Expr\Assign) { + $specifiedExprs[] = $expr->var; + $specifiedExprs[] = $expr->expr; + + while ($expr->expr instanceof Expr\Assign) { + $specifiedExprs[] = $expr->expr->var; + $expr = $expr->expr; + } + } elseif ($expr instanceof Expr\AssignOp\Coalesce) { + $specifiedExprs[] = $expr->var; + } else { + $specifiedExprs[] = $expr; + } + + $types = null; + + foreach ($specifiedExprs as $specifiedExpr) { + $newTypes = $this->createForExpr($specifiedExpr, $type, $context, $scope); + + if ($types === null) { + $types = $newTypes; + } else { + $types = $types->unionWith($newTypes); } } + return $types; + } + + private function createForExpr( + Expr $expr, + Type $type, + TypeSpecifierContext $context, + Scope $scope, + ): SpecifiedTypes + { + if ($context->true()) { + $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no(); + } elseif ($context->false()) { + $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no(); + } + $originalExpr = $expr; - if (isset($resultType) && !TypeCombinator::containsNull($resultType)) { + if (isset($containsNull) && !$containsNull) { $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr); } + if ( + !$context->null() + && $expr instanceof Expr\BinaryOp\Coalesce + ) { + $rightIsSuperType = $type->isSuperTypeOf($scope->getType($expr->right)); + if (($context->true() && $rightIsSuperType->no()) || ($context->false() && $rightIsSuperType->yes())) { + $expr = $expr->left; + } + } + if ( $expr instanceof FuncCall && $expr->name instanceof Name @@ -1008,43 +1752,83 @@ public function create( $has = $this->reflectionProvider->hasFunction($expr->name, $scope); if (!$has) { // backwards compatibility with previous behaviour - return new SpecifiedTypes(); + return new SpecifiedTypes([], []); } $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); - if ($functionReflection->hasSideEffects()->yes()) { - return new SpecifiedTypes(); + $hasSideEffects = $functionReflection->hasSideEffects(); + if ($hasSideEffects->yes()) { + return new SpecifiedTypes([], []); + } + + if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) { + return new SpecifiedTypes([], []); } } if ( $expr instanceof MethodCall && $expr->name instanceof Node\Identifier - && $scope !== null ) { $methodName = $expr->name->toString(); $calledOnType = $scope->getType($expr->var); $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); - if ($methodReflection === null || $methodReflection->hasSideEffects()->yes()) { - if (isset($resultType) && !TypeCombinator::containsNull($resultType)) { + if ( + $methodReflection === null + || $methodReflection->hasSideEffects()->yes() + || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) + ) { + if (isset($containsNull) && !$containsNull) { return $this->createNullsafeTypes($originalExpr, $scope, $context, $type); } - return new SpecifiedTypes(); + return new SpecifiedTypes([], []); + } + } + + if ( + $expr instanceof StaticCall + && $expr->name instanceof Node\Identifier + ) { + $methodName = $expr->name->toString(); + if ($expr->class instanceof Name) { + $calledOnType = $scope->resolveTypeByName($expr->class); + } else { + $calledOnType = $scope->getType($expr->class); + } + + $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); + if ( + $methodReflection === null + || $methodReflection->hasSideEffects()->yes() + || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) + ) { + if (isset($containsNull) && !$containsNull) { + return $this->createNullsafeTypes($originalExpr, $scope, $context, $type); + } + + return new SpecifiedTypes([], []); } } $sureTypes = []; $sureNotTypes = []; - $exprString = $this->printer->prettyPrintExpr($expr); + $exprString = $this->exprPrinter->printExpr($expr); + $originalExprString = $this->exprPrinter->printExpr($originalExpr); if ($context->false()) { $sureNotTypes[$exprString] = [$expr, $type]; + if ($exprString !== $originalExprString) { + $sureNotTypes[$originalExprString] = [$originalExpr, $type]; + } } elseif ($context->true()) { $sureTypes[$exprString] = [$expr, $type]; + if ($exprString !== $originalExprString) { + $sureTypes[$originalExprString] = [$originalExpr, $type]; + } } - $types = new SpecifiedTypes($sureTypes, $sureNotTypes, $overwrite); - if ($scope !== null && isset($resultType) && !TypeCombinator::containsNull($resultType)) { + $types = new SpecifiedTypes($sureTypes, $sureNotTypes); + if (isset($containsNull) && !$containsNull) { return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types); } @@ -1055,25 +1839,25 @@ private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierCont { if ($expr instanceof Expr\NullsafePropertyFetch) { if ($type !== null) { - $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, false, $scope); + $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, $scope); } else { - $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), false, $scope); + $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), $scope); } return $propertyFetchTypes->unionWith( - $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope), + $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope), ); } if ($expr instanceof Expr\NullsafeMethodCall) { if ($type !== null) { - $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, false, $scope); + $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, $scope); } else { - $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), false, $scope); + $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), $scope); } return $methodCallTypes->unionWith( - $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope), + $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope), ); } @@ -1097,15 +1881,15 @@ private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierCont return $this->createNullsafeTypes($expr->class, $scope, $context, null); } - return new SpecifiedTypes(); + return new SpecifiedTypes([], []); } - private function createRangeTypes(Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes + private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes { $sureNotTypes = []; if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { - $exprString = $this->printer->prettyPrintExpr($expr); + $exprString = $this->exprPrinter->printExpr($expr); if ($context->false()) { $sureNotTypes[$exprString] = [$expr, $type]; } elseif ($context->true()) { @@ -1114,7 +1898,7 @@ private function createRangeTypes(Expr $expr, Type $type, TypeSpecifierContext $ } } - return new SpecifiedTypes([], $sureNotTypes); + return (new SpecifiedTypes([], $sureNotTypes))->setRootExpr($rootExpr); } /** @@ -1176,4 +1960,570 @@ private function getTypeSpecifyingExtensionsForType(array $extensions, string $c return array_merge(...$extensionsForClass); } + public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); + if ($expressions !== null) { + $exprNode = $expressions[0]; + $constantType = $expressions[1]; + $otherType = $expressions[2]; + + if (!$context->null() && $constantType->getValue() === null) { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + new ConstantArrayType([], []), + ]; + return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr); + } + + if (!$context->null() && $constantType->getValue() === false) { + return $this->specifyTypesInCondition( + $scope, + $exprNode, + $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate(), + )->setRootExpr($expr); + } + + if (!$context->null() && $constantType->getValue() === true) { + return $this->specifyTypesInCondition( + $scope, + $exprNode, + $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate(), + )->setRootExpr($expr); + } + + if (!$context->null() && $constantType->getValue() === 0 && !$otherType->isInteger()->yes() && !$otherType->isBoolean()->yes()) { + /* There is a difference between php 7.x and 8.x on the equality + * behavior between zero and the empty string, so to be conservative + * we leave it untouched regardless of the language version */ + if ($context->true()) { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new StringType(), + ]; + } else { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType('0'), + ]; + } + return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr); + } + + if (!$context->null() && $constantType->getValue() === '') { + /* There is a difference between php 7.x and 8.x on the equality + * behavior between zero and the empty string, so to be conservative + * we leave it untouched regardless of the language version */ + if ($context->true()) { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + ]; + } else { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantStringType(''), + ]; + } + return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr); + } + + if ( + $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && in_array(strtolower($exprNode->name->toString()), ['gettype', 'get_class', 'get_debug_type'], true) + && isset($exprNode->getArgs()[0]) + && $constantType->isString()->yes() + ) { + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr); + } + + if ( + $context->true() + && $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && $exprNode->name->toLowerString() === 'preg_match' + && (new ConstantIntegerType(1))->isSuperTypeOf($constantType)->yes() + ) { + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr); + } + } + + $leftType = $scope->getType($expr->left); + $rightType = $scope->getType($expr->right); + + $leftBooleanType = $leftType->toBoolean(); + if ($leftBooleanType instanceof ConstantBooleanType && $rightType->isBoolean()->yes()) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\Identical( + new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')), + $expr->right, + ), + $context, + )->setRootExpr($expr); + } + + $rightBooleanType = $rightType->toBoolean(); + if ($rightBooleanType instanceof ConstantBooleanType && $leftType->isBoolean()->yes()) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\Identical( + $expr->left, + new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')), + ), + $context, + )->setRootExpr($expr); + } + + if ( + !$context->null() + && $rightType->isArray()->yes() + && $leftType->isConstantArray()->yes() && $leftType->isIterableAtLeastOnce()->no() + ) { + return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); + } + + if ( + !$context->null() + && $leftType->isArray()->yes() + && $rightType->isConstantArray()->yes() && $rightType->isIterableAtLeastOnce()->no() + ) { + return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); + } + + if ( + ($leftType->isString()->yes() && $rightType->isString()->yes()) + || ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) + || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes()) + || ($leftType->isEnum()->yes() && $rightType->isEnum()->yes()) + ) { + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr); + } + + $leftExprString = $this->exprPrinter->printExpr($expr->left); + $rightExprString = $this->exprPrinter->printExpr($expr->right); + if ($leftExprString === $rightExprString) { + if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + } + + $leftTypes = $this->create($expr->left, $leftType, $context, $scope)->setRootExpr($expr); + $rightTypes = $this->create($expr->right, $rightType, $context, $scope)->setRootExpr($expr); + + return $context->true() + ? $leftTypes->unionWith($rightTypes) + : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope)); + } + + public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + // Normalize to: fn() === expr + $leftExpr = $expr->left; + $rightExpr = $expr->right; + if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { + [$leftExpr, $rightExpr] = [$rightExpr, $leftExpr]; + } + + $unwrappedLeftExpr = $leftExpr; + if ($leftExpr instanceof AlwaysRememberedExpr) { + $unwrappedLeftExpr = $leftExpr->getExpr(); + } + $unwrappedRightExpr = $rightExpr; + if ($rightExpr instanceof AlwaysRememberedExpr) { + $unwrappedRightExpr = $rightExpr->getExpr(); + } + + $rightType = $scope->getType($rightExpr); + + // (count($a) === $b) + if ( + !$context->null() + && $unwrappedLeftExpr instanceof FuncCall + && count($unwrappedLeftExpr->getArgs()) >= 1 + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true) + && $rightType->isInteger()->yes() + ) { + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) { + return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr); + } + + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType); + if ($isZero->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); + + if ($context->truthy() && !$argType->isArray()->yes()) { + $newArgType = new UnionType([ + new ObjectType(Countable::class), + new ConstantArrayType([], []), + ]); + } else { + $newArgType = new ConstantArrayType([], []); + } + + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, $newArgType, $context, $scope)->setRootExpr($expr), + ); + } + + $specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + + if ($context->truthy() && $argType->isArray()->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); + if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr), + ); + } + + return $funcTypes; + } + } + + // strlen($a) === $b + if ( + !$context->null() + && $unwrappedLeftExpr instanceof FuncCall + && count($unwrappedLeftExpr->getArgs()) === 1 + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], true) + && $rightType->isInteger()->yes() + ) { + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) { + return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr); + } + + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType); + if ($isZero->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, new ConstantStringType(''), $context, $scope)->setRootExpr($expr), + ); + } + + if ($context->truthy() && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + if ($argType->isString()->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); + + $accessory = new AccessoryNonEmptyStringType(); + if (IntegerRangeType::fromInterval(2, null)->isSuperTypeOf($rightType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } + $valueTypes = $this->create($unwrappedLeftExpr->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr); + + return $funcTypes->unionWith($valueTypes); + } + } + } + + // preg_match($a) === $b + if ( + $context->true() + && $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && $unwrappedLeftExpr->name->toLowerString() === 'preg_match' + && (new ConstantIntegerType(1))->isSuperTypeOf($rightType)->yes() + ) { + return $this->specifyTypesInCondition( + $scope, + $leftExpr, + $context, + )->setRootExpr($expr); + } + + // get_class($a) === 'Foo' + if ( + $context->true() + && $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true) + && isset($unwrappedLeftExpr->getArgs()[0]) + ) { + if ($rightType instanceof ConstantStringType && $this->reflectionProvider->hasClass($rightType->getValue())) { + return $this->create( + $unwrappedLeftExpr->getArgs()[0]->value, + new ObjectType($rightType->getValue(), null, $this->reflectionProvider->getClass($rightType->getValue())->asFinal()), + $context, + $scope, + )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr); + } + if ($rightType->getClassStringObjectType()->isObject()->yes()) { + return $this->create( + $unwrappedLeftExpr->getArgs()[0]->value, + $rightType->getClassStringObjectType(), + $context, + $scope, + )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr); + } + } + + if ( + $context->truthy() + && $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower($unwrappedLeftExpr->name->toString()), [ + 'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst', + 'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst', + 'ucwords', 'mb_convert_case', 'mb_convert_kana', + ], true) + && isset($unwrappedLeftExpr->getArgs()[0]) + && $rightType->isNonEmptyString()->yes() + ) { + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + + if ($argType->isString()->yes()) { + if ($rightType->isNonFalsyString()->yes()) { + return $this->create( + $unwrappedLeftExpr->getArgs()[0]->value, + TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()), + $context, + $scope, + )->setRootExpr($expr); + } + + return $this->create( + $unwrappedLeftExpr->getArgs()[0]->value, + TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()), + $context, + $scope, + )->setRootExpr($expr); + } + } + + if ($rightType->isString()->yes()) { + $types = null; + foreach ($rightType->getConstantStrings() as $constantString) { + $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $expr); + + if ($specifiedType === null) { + continue; + } + if ($types === null) { + $types = $specifiedType; + continue; + } + + $types = $types->intersectWith($specifiedType); + } + + if ($types !== null) { + if ($leftExpr !== $unwrappedLeftExpr) { + $types = $types->unionWith($this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr)); + } + return $types; + } + } + + $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); + if ($expressions !== null) { + $exprNode = $expressions[0]; + $constantType = $expressions[1]; + + $unwrappedExprNode = $exprNode; + if ($exprNode instanceof AlwaysRememberedExpr) { + $unwrappedExprNode = $exprNode->getExpr(); + } + + $specifiedType = $this->specifyTypesForConstantBinaryExpression($unwrappedExprNode, $constantType, $context, $scope, $expr); + if ($specifiedType !== null) { + if ($exprNode !== $unwrappedExprNode) { + $specifiedType = $specifiedType->unionWith( + $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($expr), + ); + } + return $specifiedType; + } + } + + // $a::class === 'Foo' + if ( + $context->true() && + $unwrappedLeftExpr instanceof ClassConstFetch && + $unwrappedLeftExpr->class instanceof Expr && + $unwrappedLeftExpr->name instanceof Node\Identifier && + $unwrappedRightExpr instanceof ClassConstFetch && + $rightType instanceof ConstantStringType && + $rightType->getValue() !== '' && + strtolower($unwrappedLeftExpr->name->toString()) === 'class' + ) { + if ($this->reflectionProvider->hasClass($rightType->getValue())) { + return $this->create( + $unwrappedLeftExpr->class, + new ObjectType($rightType->getValue(), null, $this->reflectionProvider->getClass($rightType->getValue())->asFinal()), + $context, + $scope, + )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr); + } + return $this->specifyTypesInCondition( + $scope, + new Instanceof_( + $unwrappedLeftExpr->class, + new Name($rightType->getValue()), + ), + $context, + )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr); + } + + $leftType = $scope->getType($leftExpr); + + // 'Foo' === $a::class + if ( + $context->true() && + $unwrappedRightExpr instanceof ClassConstFetch && + $unwrappedRightExpr->class instanceof Expr && + $unwrappedRightExpr->name instanceof Node\Identifier && + $unwrappedLeftExpr instanceof ClassConstFetch && + $leftType instanceof ConstantStringType && + $leftType->getValue() !== '' && + strtolower($unwrappedRightExpr->name->toString()) === 'class' + ) { + if ($this->reflectionProvider->hasClass($leftType->getValue())) { + return $this->create( + $unwrappedRightExpr->class, + new ObjectType($leftType->getValue(), null, $this->reflectionProvider->getClass($leftType->getValue())->asFinal()), + $context, + $scope, + )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr)); + } + + return $this->specifyTypesInCondition( + $scope, + new Instanceof_( + $unwrappedRightExpr->class, + new Name($leftType->getValue()), + ), + $context, + )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr)); + } + + if ($context->false()) { + $identicalType = $scope->getType($expr); + if ($identicalType instanceof ConstantBooleanType) { + $never = new NeverType(); + $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; + $leftTypes = $this->create($leftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr); + $rightTypes = $this->create($rightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr); + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftTypes = $leftTypes->unionWith( + $this->create($unwrappedLeftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr), + ); + } + if ($rightExpr instanceof AlwaysRememberedExpr) { + $rightTypes = $rightTypes->unionWith( + $this->create($unwrappedRightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr), + ); + } + return $leftTypes->unionWith($rightTypes); + } + } + + $types = null; + if ( + count($leftType->getFiniteTypes()) === 1 + || ( + $context->true() + && $leftType->isConstantValue()->yes() + && !$rightType->equals($leftType) + && $rightType->isSuperTypeOf($leftType)->yes()) + ) { + $types = $this->create( + $rightExpr, + $leftType, + $context, + $scope, + )->setRootExpr($expr); + if ($rightExpr instanceof AlwaysRememberedExpr) { + $types = $types->unionWith($this->create( + $unwrappedRightExpr, + $leftType, + $context, + $scope, + ))->setRootExpr($expr); + } + } + if ( + count($rightType->getFiniteTypes()) === 1 + || ( + $context->true() + && $rightType->isConstantValue()->yes() + && !$leftType->equals($rightType) + && $leftType->isSuperTypeOf($rightType)->yes() + ) + ) { + $leftTypes = $this->create( + $leftExpr, + $rightType, + $context, + $scope, + )->setRootExpr($expr); + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftTypes = $leftTypes->unionWith($this->create( + $unwrappedLeftExpr, + $rightType, + $context, + $scope, + ))->setRootExpr($expr); + } + if ($types !== null) { + $types = $types->unionWith($leftTypes); + } else { + $types = $leftTypes; + } + } + + if ($types !== null) { + return $types; + } + + $leftExprString = $this->exprPrinter->printExpr($unwrappedLeftExpr); + $rightExprString = $this->exprPrinter->printExpr($unwrappedRightExpr); + if ($leftExprString === $rightExprString) { + if (!$unwrappedLeftExpr instanceof Expr\Variable || !$unwrappedRightExpr instanceof Expr\Variable) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + } + + if ($context->true()) { + $leftTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr); + $rightTypes = $this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr); + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftTypes = $leftTypes->unionWith( + $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr), + ); + } + if ($rightExpr instanceof AlwaysRememberedExpr) { + $rightTypes = $rightTypes->unionWith( + $this->create($unwrappedRightExpr, $leftType, $context, $scope)->setRootExpr($expr), + ); + } + return $leftTypes->unionWith($rightTypes); + } elseif ($context->false()) { + return $this->create($leftExpr, $leftType, $context, $scope)->setRootExpr($expr)->normalize($scope) + ->intersectWith($this->create($rightExpr, $rightType, $context, $scope)->setRootExpr($expr)->normalize($scope)); + } + + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + } diff --git a/src/Analyser/TypeSpecifierContext.php b/src/Analyser/TypeSpecifierContext.php index a14aba7112..fe09aa861c 100644 --- a/src/Analyser/TypeSpecifierContext.php +++ b/src/Analyser/TypeSpecifierContext.php @@ -4,8 +4,10 @@ use PHPStan\ShouldNotHappenException; -/** @api */ -class TypeSpecifierContext +/** + * @api + */ +final class TypeSpecifierContext { public const CONTEXT_TRUE = 0b0001; diff --git a/src/Analyser/TypeSpecifierFactory.php b/src/Analyser/TypeSpecifierFactory.php index 17d638f457..83315b6e0c 100644 --- a/src/Analyser/TypeSpecifierFactory.php +++ b/src/Analyser/TypeSpecifierFactory.php @@ -2,13 +2,14 @@ namespace PHPStan\Analyser; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Broker\BrokerFactory; use PHPStan\DependencyInjection\Container; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use function array_merge; -class TypeSpecifierFactory +final class TypeSpecifierFactory { public const FUNCTION_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.typeSpecifier.functionTypeSpecifyingExtension'; @@ -22,11 +23,13 @@ public function __construct(private Container $container) public function create(): TypeSpecifier { $typeSpecifier = new TypeSpecifier( - $this->container->getByType(Standard::class), + $this->container->getByType(ExprPrinter::class), $this->container->getByType(ReflectionProvider::class), + $this->container->getByType(PhpVersion::class), $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->getParameter('rememberPossiblyImpureFunctionValues'), ); foreach (array_merge( diff --git a/src/Analyser/UndefinedVariableException.php b/src/Analyser/UndefinedVariableException.php index 6719de349c..4755296c6b 100644 --- a/src/Analyser/UndefinedVariableException.php +++ b/src/Analyser/UndefinedVariableException.php @@ -5,7 +5,7 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class UndefinedVariableException extends AnalysedCodeException +final class UndefinedVariableException extends AnalysedCodeException { public function __construct(private Scope $scope, private string $variableName) diff --git a/src/Analyser/VariableTypeHolder.php b/src/Analyser/VariableTypeHolder.php deleted file mode 100644 index 70273de965..0000000000 --- a/src/Analyser/VariableTypeHolder.php +++ /dev/null @@ -1,58 +0,0 @@ -certainty->equals($other->certainty)) { - return false; - } - - return $this->type->equals($other->type); - } - - public function and(self $other): self - { - if ($this->getType()->equals($other->getType())) { - $type = $this->getType(); - } else { - $type = TypeCombinator::union($this->getType(), $other->getType()); - } - return new self( - $type, - $this->getCertainty()->and($other->getCertainty()), - ); - } - - public function getType(): Type - { - return $this->type; - } - - public function getCertainty(): TrinaryLogic - { - return $this->certainty; - } - -} diff --git a/src/Broker/AnonymousClassNameHelper.php b/src/Broker/AnonymousClassNameHelper.php index b0e9bdfb40..0f9e82cb21 100644 --- a/src/Broker/AnonymousClassNameHelper.php +++ b/src/Broker/AnonymousClassNameHelper.php @@ -5,11 +5,12 @@ use PhpParser\Node; use PHPStan\File\FileHelper; use PHPStan\File\RelativePathHelper; +use PHPStan\Parser\AnonymousClassVisitor; use PHPStan\ShouldNotHappenException; use function md5; use function sprintf; -class AnonymousClassNameHelper +final class AnonymousClassNameHelper { public function __construct( @@ -19,6 +20,9 @@ public function __construct( { } + /** + * @return non-empty-string + */ public function getAnonymousClassName( Node\Stmt\Class_ $classNode, string $filename, @@ -32,9 +36,17 @@ public function getAnonymousClassName( $this->fileHelper->normalizePath($filename, '/'), ); + /** @var int|null $lineIndex */ + $lineIndex = $classNode->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX); + if ($lineIndex === null) { + $hash = md5(sprintf('%s:%s', $filename, $classNode->getStartLine())); + } else { + $hash = md5(sprintf('%s:%s:%d', $filename, $classNode->getStartLine(), $lineIndex)); + } + return sprintf( 'AnonymousClass%s', - md5(sprintf('%s:%s', $filename, $classNode->getLine())), + $hash, ); } diff --git a/src/Broker/Broker.php b/src/Broker/Broker.php deleted file mode 100644 index 871078e13d..0000000000 --- a/src/Broker/Broker.php +++ /dev/null @@ -1,143 +0,0 @@ -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(); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - return $this->reflectionProvider->getAnonymousClassReflection($classNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasFunction($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection - { - return $this->reflectionProvider->getFunction($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveFunctionName($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasConstant($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - return $this->reflectionProvider->getConstant($nameNode, $scope); - } - - /** - * @deprecated Use PHPStan\Reflection\ReflectionProvider instead - */ - public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveConstantName($nameNode, $scope); - } - - /** - * @deprecated Inject %universalObjectCratesClasses% parameter instead. - * - * @return string[] - */ - public function getUniversalObjectCratesClasses(): array - { - return $this->universalObjectCratesClasses; - } - -} diff --git a/src/Broker/BrokerFactory.php b/src/Broker/BrokerFactory.php index 182b0ca3ca..bbd8d97a3d 100644 --- a/src/Broker/BrokerFactory.php +++ b/src/Broker/BrokerFactory.php @@ -2,29 +2,16 @@ namespace PHPStan\Broker; -use PHPStan\DependencyInjection\Container; -use PHPStan\Reflection\ReflectionProvider; - -class BrokerFactory +final class BrokerFactory { public const PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.propertiesClassReflectionExtension'; public const METHODS_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.methodsClassReflectionExtension'; + public const ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.allowedSubTypesClassReflectionExtension'; public const DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicMethodReturnTypeExtension'; public const DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicStaticMethodReturnTypeExtension'; public const DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicFunctionReturnTypeExtension'; public const OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.broker.operatorTypeSpecifyingExtension'; - - public function __construct(private Container $container) - { - } - - public function create(): Broker - { - return new Broker( - $this->container->getByType(ReflectionProvider::class), - $this->container->getParameter('universalObjectCratesClasses'), - ); - } + public const EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG = 'phpstan.broker.expressionTypeResolverExtension'; } diff --git a/src/Broker/ClassAutoloadingException.php b/src/Broker/ClassAutoloadingException.php index 23e99fe5a5..5451987023 100644 --- a/src/Broker/ClassAutoloadingException.php +++ b/src/Broker/ClassAutoloadingException.php @@ -7,7 +7,7 @@ use function get_class; use function sprintf; -class ClassAutoloadingException extends AnalysedCodeException +final class ClassAutoloadingException extends AnalysedCodeException { private string $className; @@ -39,7 +39,7 @@ public function getClassName(): string return $this->className; } - public function getTip(): ?string + public function getTip(): string { return 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; } diff --git a/src/Broker/ClassNotFoundException.php b/src/Broker/ClassNotFoundException.php index d86a1bcb08..1276d663ff 100644 --- a/src/Broker/ClassNotFoundException.php +++ b/src/Broker/ClassNotFoundException.php @@ -5,7 +5,7 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class ClassNotFoundException extends AnalysedCodeException +final class ClassNotFoundException extends AnalysedCodeException { public function __construct(private string $className) @@ -18,7 +18,7 @@ public function getClassName(): string return $this->className; } - public function getTip(): ?string + public function getTip(): string { return 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; } diff --git a/src/Broker/ConstantNotFoundException.php b/src/Broker/ConstantNotFoundException.php index 2c490e3653..5d633de380 100644 --- a/src/Broker/ConstantNotFoundException.php +++ b/src/Broker/ConstantNotFoundException.php @@ -5,7 +5,7 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class ConstantNotFoundException extends AnalysedCodeException +final class ConstantNotFoundException extends AnalysedCodeException { public function __construct(private string $constantName) @@ -18,7 +18,7 @@ public function getConstantName(): string return $this->constantName; } - public function getTip(): ?string + public function getTip(): string { return 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; } diff --git a/src/Broker/FunctionNotFoundException.php b/src/Broker/FunctionNotFoundException.php index de2728001f..a313b60e2a 100644 --- a/src/Broker/FunctionNotFoundException.php +++ b/src/Broker/FunctionNotFoundException.php @@ -5,7 +5,7 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class FunctionNotFoundException extends AnalysedCodeException +final class FunctionNotFoundException extends AnalysedCodeException { public function __construct(private string $functionName) @@ -18,7 +18,7 @@ public function getFunctionName(): string return $this->functionName; } - public function getTip(): ?string + public function getTip(): string { return 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; } diff --git a/src/Cache/Cache.php b/src/Cache/Cache.php index 0180ab6610..a4b66596fa 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -2,7 +2,7 @@ namespace PHPStan\Cache; -class Cache +final class Cache { public function __construct(private CacheStorage $storage) diff --git a/src/Cache/CacheItem.php b/src/Cache/CacheItem.php index 61d9946c90..101bbfe2fd 100644 --- a/src/Cache/CacheItem.php +++ b/src/Cache/CacheItem.php @@ -2,7 +2,7 @@ namespace PHPStan\Cache; -class CacheItem +final class CacheItem { /** diff --git a/src/Cache/FileCacheStorage.php b/src/Cache/FileCacheStorage.php index fbf0bb359e..1b66f26e2a 100644 --- a/src/Cache/FileCacheStorage.php +++ b/src/Cache/FileCacheStorage.php @@ -4,44 +4,42 @@ use InvalidArgumentException; use Nette\Utils\Random; +use PHPStan\File\CouldNotReadFileException; +use PHPStan\File\CouldNotWriteFileException; +use PHPStan\File\FileReader; use PHPStan\File\FileWriter; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; use PHPStan\ShouldNotHappenException; -use function clearstatcache; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use function array_keys; +use function closedir; +use function dirname; use function error_get_last; use function is_dir; use function is_file; -use function mkdir; +use function opendir; +use function readdir; use function rename; +use function rmdir; use function sha1; use function sprintf; +use function str_starts_with; +use function strlen; use function substr; +use function uksort; use function unlink; use function var_export; use const DIRECTORY_SEPARATOR; -class FileCacheStorage implements CacheStorage +final class FileCacheStorage implements CacheStorage { - public function __construct(private string $directory) - { - } + private const CACHED_CLEARED_VERSION = 'v2-new'; - private function makeDir(string $directory): void + public function __construct(private string $directory) { - if (is_dir($directory)) { - return; - } - - $result = @mkdir($directory, 0777); - if ($result === false) { - clearstatcache(); - if (is_dir($directory)) { - return; - } - - $error = error_get_last(); - throw new InvalidArgumentException(sprintf('Failed to create directory "%s" (%s).', $this->directory, $error !== null ? $error['message'] : 'unknown cause')); - } } /** @@ -52,11 +50,7 @@ public function load(string $key, string $variableKey) [,, $filePath] = $this->getFilePaths($key); return (static function () use ($variableKey, $filePath) { - if (!is_file($filePath)) { - return null; - } - - $cacheItem = require $filePath; + $cacheItem = @include $filePath; if (!$cacheItem instanceof CacheItem) { return null; } @@ -70,13 +64,14 @@ public function load(string $key, string $variableKey) /** * @param mixed $data + * @throws DirectoryCreatorException */ public function save(string $key, string $variableKey, $data): void { [$firstDirectory, $secondDirectory, $path] = $this->getFilePaths($key); - $this->makeDir($this->directory); - $this->makeDir($firstDirectory); - $this->makeDir($secondDirectory); + DirectoryCreator::ensureDirectoryExists($this->directory, 0777); + DirectoryCreator::ensureDirectoryExists($firstDirectory, 0777); + DirectoryCreator::ensureDirectoryExists($secondDirectory, 0777); $tmpPath = sprintf('%s/%s.tmp', $this->directory, Random::generate()); $errorBefore = error_get_last(); @@ -88,7 +83,8 @@ public function save(string $key, string $variableKey, $data): void FileWriter::write( $tmpPath, sprintf( - "directory)) { + return; + } + + $cachedClearedFile = $this->directory . '/cache-cleared'; + if (is_file($cachedClearedFile)) { + try { + $cachedClearedContents = FileReader::read($cachedClearedFile); + if ($cachedClearedContents === self::CACHED_CLEARED_VERSION) { + return; + } + } catch (CouldNotReadFileException) { + return; + } + } + + $iterator = new RecursiveDirectoryIterator($this->directory); + $iterator->setFlags(RecursiveDirectoryIterator::SKIP_DOTS); + $files = new RecursiveIteratorIterator($iterator); + $beginFunction = sprintf( + "getPathname(); + $contents = FileReader::read($path); + if ( + !str_starts_with($contents, $beginFunction) + && !str_starts_with($contents, $beginMethod) + && str_starts_with($contents, $beginNew) + ) { + continue; + } + + $emptyDirectoriesToCheck[dirname($path)] = true; + $emptyDirectoriesToCheck[dirname($path, 2)] = true; + + @unlink($path); + } catch (CouldNotReadFileException) { + continue; + } + } + + uksort($emptyDirectoriesToCheck, static fn ($a, $b) => strlen($b) - strlen($a)); + + foreach (array_keys($emptyDirectoriesToCheck) as $directory) { + if (!$this->isDirectoryEmpty($directory)) { + continue; + } + + @rmdir($directory); + } + + try { + FileWriter::write($cachedClearedFile, self::CACHED_CLEARED_VERSION); + } catch (CouldNotWriteFileException) { + // pass + } + } + + private function isDirectoryEmpty(string $directory): bool + { + $handle = opendir($directory); + if ($handle === false) { + return false; + } + while (($entry = readdir($handle)) !== false) { + if ($entry !== '.' && $entry !== '..') { + closedir($handle); + return false; + } + } + + closedir($handle); + return true; + } + } diff --git a/src/Cache/MemoryCacheStorage.php b/src/Cache/MemoryCacheStorage.php index 6e7e0559c5..324dfcf37f 100644 --- a/src/Cache/MemoryCacheStorage.php +++ b/src/Cache/MemoryCacheStorage.php @@ -2,7 +2,9 @@ namespace PHPStan\Cache; -class MemoryCacheStorage implements CacheStorage +use function var_export; + +final class MemoryCacheStorage implements CacheStorage { /** @var array */ @@ -30,7 +32,9 @@ public function load(string $key, string $variableKey) */ public function save(string $key, string $variableKey, $data): void { - $this->storage[$key] = new CacheItem($variableKey, $data); + $item = new CacheItem($variableKey, $data); + @var_export($item, true); + $this->storage[$key] = $item; } } diff --git a/src/Classes/ForbiddenClassNameExtension.php b/src/Classes/ForbiddenClassNameExtension.php new file mode 100644 index 0000000000..7d545d83d4 --- /dev/null +++ b/src/Classes/ForbiddenClassNameExtension.php @@ -0,0 +1,32 @@ + */ + public function getClassPrefixes(): array; + +} diff --git a/src/Collectors/CollectedData.php b/src/Collectors/CollectedData.php new file mode 100644 index 0000000000..f6057f8cf8 --- /dev/null +++ b/src/Collectors/CollectedData.php @@ -0,0 +1,89 @@ +>, list>> + */ +final class CollectedData implements JsonSerializable +{ + + /** + * @param mixed $data + * @param class-string> $collectorType + */ + public function __construct( + private $data, + private string $filePath, + private string $collectorType, + ) + { + } + + public function getData(): mixed + { + return $this->data; + } + + public function getFilePath(): string + { + return $this->filePath; + } + + public function changeFilePath(string $newFilePath): self + { + return new self($this->data, $newFilePath, $this->collectorType); + } + + /** + * @return class-string> + */ + public function getCollectorType(): string + { + return $this->collectorType; + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'data' => $this->data, + 'filePath' => $this->filePath, + 'collectorType' => $this->collectorType, + ]; + } + + /** + * @param mixed[] $json + */ + public static function decode(array $json): self + { + return new self( + $json['data'], + $json['filePath'], + $json['collectorType'], + ); + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['data'], + $properties['filePath'], + $properties['collectorType'], + ); + } + +} diff --git a/src/Collectors/Collector.php b/src/Collectors/Collector.php new file mode 100644 index 0000000000..d7c87c8ecc --- /dev/null +++ b/src/Collectors/Collector.php @@ -0,0 +1,40 @@ + + */ + public function getNodeType(): string; + + /** + * @param TNodeType $node + * @return TValue|null Collected data + */ + public function processNode(Node $node, Scope $scope); + +} diff --git a/src/Collectors/Registry.php b/src/Collectors/Registry.php new file mode 100644 index 0000000000..cc0ae09a97 --- /dev/null +++ b/src/Collectors/Registry.php @@ -0,0 +1,56 @@ +collectors[$collector->getNodeType()][] = $collector; + } + } + + /** + * @template TNodeType of Node + * @param class-string $nodeType + * @return array> + */ + public function getCollectors(string $nodeType): array + { + if (!isset($this->cache[$nodeType])) { + $parentNodeTypes = [$nodeType] + class_parents($nodeType) + class_implements($nodeType); + + $collectors = []; + foreach ($parentNodeTypes as $parentNodeType) { + foreach ($this->collectors[$parentNodeType] ?? [] as $collector) { + $collectors[] = $collector; + } + } + + $this->cache[$nodeType] = $collectors; + } + + /** + * @var array> $selectedCollectors + */ + $selectedCollectors = $this->cache[$nodeType]; + + return $selectedCollectors; + } + +} diff --git a/src/Collectors/RegistryFactory.php b/src/Collectors/RegistryFactory.php new file mode 100644 index 0000000000..675740d99a --- /dev/null +++ b/src/Collectors/RegistryFactory.php @@ -0,0 +1,23 @@ +container->getServicesByTag(self::COLLECTOR_TAG), + ); + } + +} diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 8e175c7725..81793c6ba0 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -3,26 +3,36 @@ namespace PHPStan\Command; use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\IgnoredErrorHelper; +use PHPStan\Analyser\AnalyserResultFinalizer; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\Collectors\CollectedData; use PHPStan\Internal\BytesHelper; +use PHPStan\PhpDoc\StubFilesProvider; 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 is_file; use function memory_get_peak_usage; +use function microtime; +use function sha1_file; use function sprintf; -class AnalyseApplication +/** + * @phpstan-import-type CollectorData from CollectedData + */ +final class AnalyseApplication { public function __construct( private AnalyserRunner $analyserRunner, + private AnalyserResultFinalizer $analyserResultFinalizer, private StubValidator $stubValidator, private ResultCacheManagerFactory $resultCacheManagerFactory, private IgnoredErrorHelper $ignoredErrorHelper, - private int $internalErrorsCountLimit, + private StubFilesProvider $stubFilesProvider, ) { } @@ -43,16 +53,21 @@ public function analyse( InputInterface $input, ): AnalysisResult { - $resultCacheManager = $this->resultCacheManagerFactory->create([]); + $isResultCacheUsed = false; + $resultCacheManager = $this->resultCacheManagerFactory->create(); $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); + $fileSpecificErrors = []; if (count($ignoredErrorHelperResult->getErrors()) > 0) { - $errors = $ignoredErrorHelperResult->getErrors(); + $notFileSpecificErrors = $ignoredErrorHelperResult->getErrors(); $internalErrors = []; + $collectedData = []; $savedResultCache = false; - if ($errorOutput->isDebug()) { + $memoryUsageBytes = memory_get_peak_usage(true); + if ($errorOutput->isVeryVerbose()) { $errorOutput->writeLineFormatted('Result cache was not saved because of ignoredErrorHelperResult errors.'); } + $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; } else { $resultCache = $resultCacheManager->restore($files, $debug, $onlyFiles, $projectConfigArray, $errorOutput); $intermediateAnalyserResult = $this->runAnalyser( @@ -65,41 +80,73 @@ public function analyse( $input, ); - $projectStubFiles = []; - if ($projectConfigArray !== null) { - $projectStubFiles = $projectConfigArray['parameters']['stubFiles'] ?? []; - } - if ($resultCache->isFullAnalysis() && count($projectStubFiles) !== 0) { + $projectStubFiles = $this->stubFilesProvider->getProjectStubFiles(); + + $forceValidateStubFiles = (bool) ($_SERVER['__PHPSTAN_FORCE_VALIDATE_STUB_FILES'] ?? false); + if ( + $resultCache->isFullAnalysis() + && count($projectStubFiles) !== 0 + && (!$onlyFiles || $forceValidateStubFiles) + ) { $stubErrors = $this->stubValidator->validate($projectStubFiles, $debug); $intermediateAnalyserResult = new AnalyserResult( - array_merge($intermediateAnalyserResult->getErrors(), $stubErrors), + array_merge($intermediateAnalyserResult->getUnorderedErrors(), $stubErrors), + $intermediateAnalyserResult->getFilteredPhpErrors(), + $intermediateAnalyserResult->getAllPhpErrors(), + $intermediateAnalyserResult->getLocallyIgnoredErrors(), + $intermediateAnalyserResult->getLinesToIgnore(), + $intermediateAnalyserResult->getUnmatchedLineIgnores(), $intermediateAnalyserResult->getInternalErrors(), + $intermediateAnalyserResult->getCollectedData(), $intermediateAnalyserResult->getDependencies(), $intermediateAnalyserResult->getExportedNodes(), $intermediateAnalyserResult->hasReachedInternalErrorsCountLimit(), + $intermediateAnalyserResult->getPeakMemoryUsageBytes(), ); } $resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true); - $analyserResult = $resultCacheResult->getAnalyserResult(); + $analyserResult = $this->analyserResultFinalizer->finalize($resultCacheResult->getAnalyserResult(), $onlyFiles, $debug)->getAnalyserResult(); $internalErrors = $analyserResult->getInternalErrors(); - $errors = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $files, count($internalErrors) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit()); - $savedResultCache = $resultCacheResult->isSaved(); - if ($analyserResult->hasReachedInternalErrorsCountLimit()) { - $errors[] = sprintf('Reached internal errors count limit of %d, exiting...', $this->internalErrorsCountLimit); - } - $errors = array_merge($errors, $internalErrors); - } - - $fileSpecificErrors = []; - $notFileSpecificErrors = []; - foreach ($errors as $error) { - if (is_string($error)) { - $notFileSpecificErrors[] = $error; - continue; + $errors = array_merge( + $analyserResult->getErrors(), + $analyserResult->getFilteredPhpErrors(), + ); + $hasInternalErrors = count($internalErrors) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit(); + $memoryUsageBytes = $analyserResult->getPeakMemoryUsageBytes(); + $isResultCacheUsed = !$resultCache->isFullAnalysis(); + + $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; + if ( + $isResultCacheUsed + && $resultCacheResult->isSaved() + && !$onlyFiles + && $projectConfigArray !== null + ) { + foreach ($resultCache->getProjectExtensionFiles() as $file => [$hash, $isAnalysed, $className]) { + if ($isAnalysed) { + continue; + } + + if (!is_file($file)) { + $changedProjectExtensionFilesOutsideOfAnalysedPaths[$file] = $className; + continue; + } + + $newHash = sha1_file($file); + if ($newHash === $hash) { + continue; + } + + $changedProjectExtensionFilesOutsideOfAnalysedPaths[$file] = $className; + } } - $fileSpecificErrors[] = $error; + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($errors, $onlyFiles, $files, $hasInternalErrors); + $fileSpecificErrors = $ignoredErrorHelperProcessedResult->getNotIgnoredErrors(); + $notFileSpecificErrors = $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages(); + $collectedData = $analyserResult->getCollectedData(); + $savedResultCache = $resultCacheResult->isSaved(); } return new AnalysisResult( @@ -107,12 +154,32 @@ public function analyse( $notFileSpecificErrors, $internalErrors, [], + $this->mapCollectedData($collectedData), $defaultLevelUsed, $projectConfigFile, $savedResultCache, + $memoryUsageBytes, + $isResultCacheUsed, + $changedProjectExtensionFilesOutsideOfAnalysedPaths, ); } + /** + * @param CollectorData $collectedData + * + * @return list + */ + private function mapCollectedData(array $collectedData): array + { + $result = []; + foreach ($collectedData as $file => $dataPerCollector) { + foreach ($dataPerCollector as $collectorType => $rawData) { + $result[] = new CollectedData($rawData, $file, $collectorType); + } + } + return $result; + } + /** * @param string[] $files * @param string[] $allAnalysedFiles @@ -133,38 +200,41 @@ private function runAnalyser( $errorOutput->getStyle()->progressStart($allAnalysedFilesCount); $errorOutput->getStyle()->progressAdvance($allAnalysedFilesCount); $errorOutput->getStyle()->progressFinish(); - return new AnalyserResult([], [], [], [], false); + return new AnalyserResult([], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); } if (!$debug) { - $progressStarted = false; $preFileCallback = null; - $postFileCallback = static function (int $step) use ($errorOutput, &$progressStarted, $allAnalysedFilesCount, $filesCount): void { - if (!$progressStarted) { - $errorOutput->getStyle()->progressStart($allAnalysedFilesCount); - $errorOutput->getStyle()->progressAdvance($allAnalysedFilesCount - $filesCount); - $progressStarted = true; - } + $postFileCallback = static function (int $step) use ($errorOutput): void { $errorOutput->getStyle()->progressAdvance($step); }; + + $errorOutput->getStyle()->progressStart($allAnalysedFilesCount); + $errorOutput->getStyle()->progressAdvance($allAnalysedFilesCount - $filesCount); } 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; }; } } - $analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, null, null, $input); + $analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, $input); - if (isset($progressStarted) && $progressStarted) { + if (!$debug) { $errorOutput->getStyle()->progressFinish(); } diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index c7c3e9f8ab..a81115e108 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -3,17 +3,25 @@ namespace PHPStan\Command; use OndraM\CiDetector\CiDetector; -use OndraM\CiDetector\Exception\CiNotDetectedException; -use PHPStan\Analyser\ResultCache\ResultCacheClearer; +use PHPStan\Analyser\InternalError; use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter; +use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter; use PHPStan\Command\ErrorFormatter\ErrorFormatter; -use PHPStan\Command\ErrorFormatter\TableErrorFormatter; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; +use PHPStan\DependencyInjection\Container; +use PHPStan\Diagnose\DiagnoseExtension; +use PHPStan\Diagnose\PHPStanDiagnoseExtension; use PHPStan\File\CouldNotWriteFileException; +use PHPStan\File\FileHelper; +use PHPStan\File\FileReader; use PHPStan\File\FileWriter; use PHPStan\File\ParentDirectoryRelativePathHelper; use PHPStan\File\PathNotFoundException; +use PHPStan\File\RelativePathHelper; +use PHPStan\Internal\BytesHelper; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -23,27 +31,38 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Throwable; +use function array_intersect; +use function array_key_exists; +use function array_keys; use function array_map; +use function array_reverse; +use function array_unique; +use function array_values; use function count; use function dirname; +use function filesize; use function fopen; use function get_class; use function implode; +use function in_array; use function is_array; use function is_bool; -use function is_dir; +use function is_file; use function is_string; -use function mkdir; use function pathinfo; use function rewind; use function sprintf; +use function str_contains; use function stream_get_contents; use function strlen; use function substr; use const PATHINFO_BASENAME; use const PATHINFO_EXTENSION; -class AnalyseCommand extends Command +/** + * @phpstan-import-type Trace from InternalError as InternalErrorTrace + */ +final class AnalyseCommand extends Command { private const NAME = 'analyse'; @@ -57,6 +76,7 @@ class AnalyseCommand extends Command */ public function __construct( private array $composerAutoloaderProjectPaths, + private float $analysisStartTime, ) { parent::__construct(); @@ -77,10 +97,11 @@ protected function configure(): void new InputOption('generate-baseline', 'b', InputOption::VALUE_OPTIONAL, 'Path to a file where the baseline should be saved', false), new InputOption('allow-empty-baseline', null, InputOption::VALUE_NONE, 'Do not error out when the generated baseline is empty'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), new InputOption('fix', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), new InputOption('watch', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), new InputOption('pro', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), + new InputOption('fail-without-result-cache', null, InputOption::VALUE_NONE, 'Return non-zero exit code when result cache is not used'), ]); } @@ -97,7 +118,7 @@ protected function initialize(InputInterface $input, OutputInterface $output): v if ((bool) $input->getOption('debug')) { $application = $this->getApplication(); if ($application === null) { - throw new ShouldNotHappenException(); + return; } $application->setCatchExceptions(false); return; @@ -114,6 +135,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $allowXdebug = $input->getOption('xdebug'); $debugEnabled = (bool) $input->getOption('debug'); $fix = (bool) $input->getOption('fix') || (bool) $input->getOption('watch') || (bool) $input->getOption('pro'); + $failWithoutResultCache = (bool) $input->getOption('fail-without-result-cache'); /** @var string|false|null $generateBaselineFile */ $generateBaselineFile = $input->getOption('generate-baseline'); @@ -149,6 +171,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, $debugEnabled, + true, ); } catch (InceptionNotSuccessfulException $e) { return 1; @@ -156,39 +179,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($generateBaselineFile === null && $allowEmptyBaseline) { $inceptionResult->getStdOutput()->getStyle()->error('You must pass the --generate-baseline option alongside --allow-empty-baseline.'); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } $errorOutput = $inceptionResult->getErrorOutput(); - $obsoleteDockerImage = $_SERVER['PHPSTAN_OBSOLETE_DOCKER_IMAGE'] ?? 'false'; - if ($obsoleteDockerImage === 'true') { - $errorOutput->writeLineFormatted('⚠️ You\'re using an obsolete PHPStan Docker image. ⚠️️'); - $errorOutput->writeLineFormatted(' You can obtain the current one from ghcr.io/phpstan/phpstan.'); - $errorOutput->writeLineFormatted(' Read more about it here:'); - $errorOutput->writeLineFormatted(' https://phpstan.org/user-guide/docker'); - $errorOutput->writeLineFormatted(''); - } - $errorFormat = $input->getOption('error-format'); if (!is_string($errorFormat) && $errorFormat !== null) { throw new ShouldNotHappenException(); } + if ($errorFormat === null) { + $errorFormat = $inceptionResult->getContainer()->getParameter('errorFormat'); + } + if ($errorFormat === null) { $errorFormat = 'table'; - $ciDetector = new CiDetector(); - - try { - $ci = $ciDetector->detect(); - if ($ci->getCiName() === CiDetector::CI_GITHUB_ACTIONS) { - $errorFormat = 'github'; - } elseif ($ci->getCiName() === CiDetector::CI_TEAMCITY) { - $errorFormat = 'teamcity'; - } - } catch (CiNotDetectedException) { - // pass - } } $container = $inceptionResult->getContainer(); @@ -207,24 +213,65 @@ protected function execute(InputInterface $input, OutputInterface $output): int $baselineExtension = pathinfo($generateBaselineFile, PATHINFO_EXTENSION); if ($baselineExtension === '') { $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename must have an extension, %s provided instead.', pathinfo($generateBaselineFile, PATHINFO_BASENAME))); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } - if ($baselineExtension !== 'neon') { - $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename extension must be .neon, .%s was used instead.', $baselineExtension)); + if (!in_array($baselineExtension, ['neon', 'php'], true)) { + $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename extension must be .neon or .php, .%s was used instead.', $baselineExtension)); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } } try { [$files, $onlyFiles] = $inceptionResult->getFiles(); } catch (PathNotFoundException $e) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; + } catch (InceptionNotSuccessfulException) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + return 1; } - /** @var AnalyseApplication $application */ + if (count($files) === 0) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + + $inceptionResult->getErrorOutput()->getStyle()->error('No files found to analyse.'); + + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + + $analysedConfigFiles = array_intersect($files, $container->getParameter('allConfigFiles')); + /** @var RelativePathHelper $relativePathHelper */ + $relativePathHelper = $container->getService('relativePathHelper'); + foreach ($analysedConfigFiles as $analysedConfigFile) { + $fileSize = @filesize($analysedConfigFile); + if ($fileSize === false) { + continue; + } + + if ($fileSize <= 512 * 1024) { + continue; + } + + $inceptionResult->getErrorOutput()->getStyle()->warning(sprintf( + 'Configuration file %s (%s) is too big and might slow down PHPStan. Consider adding it to excludePaths.', + $relativePathHelper->getRelativePath($analysedConfigFile), + BytesHelper::bytes($fileSize), + )); + } + + if ($fix) { + if ($generateBaselineFile !== null) { + $inceptionResult->getStdOutput()->getStyle()->error('You cannot pass the --generate-baseline option when running PHPStan Pro.'); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + + return $this->runFixer($inceptionResult, $container, $onlyFiles, $input, $output, $files); + } + + /** @var AnalyseApplication $application */ $application = $container->getByType(AnalyseApplication::class); $debug = $input->getOption('debug'); @@ -246,200 +293,427 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); } catch (Throwable $t) { if ($debug) { - $inceptionResult->getStdOutput()->writeRaw(sprintf( + $stdOutput = $inceptionResult->getStdOutput(); + $stdOutput->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(''); + $stdOutput->writeLineFormatted(''); + $stdOutput->writeRaw($t->getTraceAsString()); + $stdOutput->writeLineFormatted(''); + + $previous = $t->getPrevious(); + while ($previous !== null) { + $stdOutput->writeLineFormatted(''); + $stdOutput->writeLineFormatted('Caused by:'); + $stdOutput->writeRaw(sprintf( + 'Uncaught %s: %s in %s:%d', + get_class($previous), + $previous->getMessage(), + $previous->getFile(), + $previous->getLine(), + )); + $stdOutput->writeRaw($previous->getTraceAsString()); + $stdOutput->writeLineFormatted(''); + $previous = $previous->getPrevious(); + } - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } throw $t; } - if ($generateBaselineFile !== null) { - if (!$allowEmptyBaseline && !$analysisResult->hasErrors()) { - $inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.'); - $inceptionResult->getStdOutput()->writeLineFormatted('To allow generating empty baselines, pass --allow-empty-baseline option.'); + /** + * Variable $internalErrorsTuples contains both "internal errors" + * and "errors with non-ignorable exception" as InternalError objects. + */ + $internalErrorsTuples = []; + $internalFileSpecificErrors = []; + foreach ($analysisResult->getInternalErrorObjects() as $internalError) { + $internalErrorsTuples[$internalError->getMessage()] = [new InternalError( + $internalError->getTraceAsString() !== null ? sprintf('Internal error: %s', $internalError->getMessage()) : $internalError->getMessage(), + $internalError->getContextDescription(), + $internalError->getTrace(), + $internalError->getTraceAsString(), + $internalError->shouldReportBug(), + ), false]; + } + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { + continue; + } - return $inceptionResult->handleReturn(1); + $message = $fileSpecificError->getMessage(); + $metadata = $fileSpecificError->getMetadata(); + $hasStackTrace = false; + if ( + $fileSpecificError->getIdentifier() === 'phpstan.internal' + && array_key_exists(InternalError::STACK_TRACE_AS_STRING_METADATA_KEY, $metadata) + ) { + $message = sprintf('Internal error: %s', $message); + $hasStackTrace = true; } - if ($analysisResult->hasInternalErrors()) { - $inceptionResult->getStdOutput()->getStyle()->error('An internal error occurred. Baseline could not be generated. Re-run PHPStan without --generate-baseline to see what\'s going on.'); - return $inceptionResult->handleReturn(1); + if (!$hasStackTrace) { + if (!array_key_exists($fileSpecificError->getMessage(), $internalFileSpecificErrors)) { + $internalFileSpecificErrors[$fileSpecificError->getMessage()] = $fileSpecificError; + } } - $baselineFileDirectory = dirname($generateBaselineFile); - $baselineErrorFormatter = new BaselineNeonErrorFormatter(new ParentDirectoryRelativePathHelper($baselineFileDirectory)); + $internalErrorsTuples[$fileSpecificError->getMessage()] = [new InternalError( + $message, + sprintf('analysing file %s', $fileSpecificError->getTraitFilePath() ?? $fileSpecificError->getFilePath()), + $metadata[InternalError::STACK_TRACE_METADATA_KEY] ?? [], + $metadata[InternalError::STACK_TRACE_AS_STRING_METADATA_KEY] ?? null, + true, + ), !$hasStackTrace]; + } - $streamOutput = $this->createStreamOutput(); - $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $streamOutput); - $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); - $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput); + $internalErrorsTuples = array_values($internalErrorsTuples); - $stream = $streamOutput->getStream(); - rewind($stream); - $baselineContents = stream_get_contents($stream); - if ($baselineContents === false) { - throw new ShouldNotHappenException(); + $fileHelper = $container->getByType(FileHelper::class); + + /** + * Variable $internalErrors only contains non-file-specific "internal errors". + */ + $internalErrors = []; + foreach ($internalErrorsTuples as [$internalError, $isInFileSpecificErrors]) { + if ($isInFileSpecificErrors) { + continue; } - if (!is_dir($baselineFileDirectory)) { - $mkdirResult = @mkdir($baselineFileDirectory, 0644, true); - if ($mkdirResult === false) { - $inceptionResult->getStdOutput()->writeLineFormatted(sprintf('Failed to create directory "%s".', $baselineFileDirectory)); + $internalErrors[] = new InternalError( + $this->getMessageFromInternalError($fileHelper, $internalError, $output->getVerbosity()), + $internalError->getContextDescription(), + $internalError->getTrace(), + $internalError->getTraceAsString(), + $internalError->shouldReportBug(), + ); + } - return $inceptionResult->handleReturn(1); + if ($generateBaselineFile !== null) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + if (count($internalErrorsTuples) > 0) { + foreach ($internalErrorsTuples as [$internalError]) { + $inceptionResult->getStdOutput()->writeLineFormatted($internalError->getMessage()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); } - } - try { - FileWriter::write($generateBaselineFile, $baselineContents); - } catch (CouldNotWriteFileException $e) { - $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); + $inceptionResult->getStdOutput()->getStyle()->error(sprintf( + '%s occurred. Baseline could not be generated.', + count($internalErrors) === 1 ? 'An internal error' : 'Internal errors', + )); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } - $errorsCount = 0; - $unignorableCount = 0; - foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { - if (!$fileSpecificError->canBeIgnored()) { - $unignorableCount++; - if ($output->isVeryVerbose()) { - $inceptionResult->getStdOutput()->writeLineFormatted('Unignorable could not be added to the baseline:'); - $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getMessage()); - $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getFile()); - $inceptionResult->getStdOutput()->writeLineFormatted(''); - } - continue; + return $this->generateBaseline($generateBaselineFile, $inceptionResult, $analysisResult, $output, $allowEmptyBaseline, $baselineExtension, $failWithoutResultCache); + } + + /** @var ErrorFormatter $errorFormatter */ + $errorFormatter = $container->getService($errorFormatterServiceName); + + if (count($internalErrorsTuples) > 0) { + $analysisResult = new AnalysisResult( + array_values($internalFileSpecificErrors), + array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $internalErrors), + [], + [], + [], + $analysisResult->isDefaultLevelUsed(), + $analysisResult->getProjectConfigFile(), + $analysisResult->isResultCacheSaved(), + $analysisResult->getPeakMemoryUsageBytes(), + $analysisResult->isResultCacheUsed(), + $analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths(), + ); + + $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); + + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + + $errorOutput->writeLineFormatted('⚠️ Result is incomplete because of severe errors. ⚠️'); + $errorOutput->writeLineFormatted(' Fix these errors first and then re-run PHPStan'); + $errorOutput->writeLineFormatted(' to get all reported errors.'); + $errorOutput->writeLineFormatted(''); + + return $inceptionResult->handleReturn( + $exitCode, + $analysisResult->getPeakMemoryUsageBytes(), + $this->analysisStartTime, + ); + } + + $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); + if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + $exitCode = 2; + } + + if ( + $analysisResult->isResultCacheUsed() + && $analysisResult->isResultCacheSaved() + && !$onlyFiles + && $inceptionResult->getProjectConfigArray() !== null + ) { + $projectServicesNotInAnalysedPaths = array_values(array_unique($analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths())); + $projectServiceFileNamesNotInAnalysedPaths = array_keys($analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths()); + + if (count($projectServicesNotInAnalysedPaths) > 0) { + $one = count($projectServicesNotInAnalysedPaths) === 1; + $errorOutput->writeLineFormatted('Result cache might not behave correctly.'); + $errorOutput->writeLineFormatted(sprintf('You\'re using custom %s in your project config', $one ? 'extension' : 'extensions')); + $errorOutput->writeLineFormatted(sprintf('but %s not part of analysed paths:', $one ? 'this extension is' : 'these extensions are')); + $errorOutput->writeLineFormatted(''); + foreach ($projectServicesNotInAnalysedPaths as $service) { + $errorOutput->writeLineFormatted(sprintf('- %s', $service)); } - $errorsCount++; - } + $errorOutput->writeLineFormatted(''); - $message = sprintf('Baseline generated with %d %s.', $errorsCount, $errorsCount === 1 ? 'error' : 'errors'); + $errorOutput->writeLineFormatted('When you edit them and re-run PHPStan, the result cache will get stale.'); - if ( - $unignorableCount === 0 - && count($analysisResult->getNotFileSpecificErrors()) === 0 - ) { - $inceptionResult->getStdOutput()->getStyle()->success($message); - } else { - $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline. Re-run PHPStan and fix them."); + $directoriesToAdd = []; + foreach ($projectServiceFileNamesNotInAnalysedPaths as $path) { + $directoriesToAdd[] = dirname($relativePathHelper->getRelativePath($path)); + } + + $directoriesToAdd = array_unique($directoriesToAdd); + $oneDirectory = count($directoriesToAdd) === 1; + + $errorOutput->writeLineFormatted(sprintf('Add %s to your analysed paths to get rid of this problem:', $oneDirectory ? 'this directory' : 'these directories')); + + $errorOutput->writeLineFormatted(''); + + foreach ($directoriesToAdd as $directory) { + $errorOutput->writeLineFormatted(sprintf('- %s', $directory)); + } + + $errorOutput->writeLineFormatted(''); + + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } + } - return $inceptionResult->handleReturn(0); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + + return $inceptionResult->handleReturn( + $exitCode, + $analysisResult->getPeakMemoryUsageBytes(), + $this->analysisStartTime, + ); + } + + private function createStreamOutput(): StreamOutput + { + $resource = fopen('php://memory', 'w', false); + if ($resource === false) { + throw new ShouldNotHappenException(); } + return new StreamOutput($resource); + } - if ($fix) { - $ciDetector = new CiDetector(); - if ($ciDetector->isCiDetected()) { - $inceptionResult->getStdOutput()->writeLineFormatted('PHPStan Pro can\'t run in CI environment yet. Stay tuned!'); + private function getMessageFromInternalError(FileHelper $fileHelper, InternalError $internalError, int $verbosity): string + { + $message = sprintf('%s while %s', $internalError->getMessage(), $internalError->getContextDescription()); + $hasLarastan = false; + $isLaravelLast = false; + + foreach (array_reverse($internalError->getTrace()) as $traceItem) { + if ($traceItem['file'] === null) { + continue; + } + + $file = $fileHelper->normalizePath($traceItem['file'], '/'); - return $inceptionResult->handleReturn(1); + if (str_contains($file, '/larastan/')) { + $hasLarastan = true; + $isLaravelLast = false; + continue; } - $container->getByType(ResultCacheClearer::class)->clearTemporaryCaches(); - $hasInternalErrors = $analysisResult->hasInternalErrors(); - $nonIgnorableErrorsByException = []; - foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { - if (!$fileSpecificError->hasNonIgnorableException()) { - continue; + + if (!str_contains($file, '/laravel/framework/')) { + continue; + } + + $isLaravelLast = true; + } + if ($hasLarastan) { + if ($isLaravelLast) { + $message .= "\n"; + $message .= "\n" . 'This message is coming from Laravel Framework itself.'; + $message .= "\n" . 'Larastan boots up your application in order to provide'; + $message .= "\n" . 'smarter static analysis of your codebase.'; + $message .= "\n"; + $message .= "\n" . 'In order to do that, the environment you run PHPStan in'; + $message .= "\n" . 'must match the environment you run your application in.'; + $message .= "\n"; + $message .= "\n" . 'Make sure you\'ve set your environment variables'; + $message .= "\n" . 'or the .env file correctly.'; + + return $message; + } + + $bugReportUrl = 'https://github.com/larastan/larastan/issues/new?template=bug-report.md'; + } else { + $bugReportUrl = 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.yaml'; + } + if ($internalError->getTraceAsString() !== null) { + if (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) { + $firstTraceItem = $internalError->getTrace()[0] ?? null; + $trace = ''; + if ($firstTraceItem !== null && $firstTraceItem['file'] !== null && $firstTraceItem['line'] !== null) { + $trace = sprintf('## %s(%d)%s', $firstTraceItem['file'], $firstTraceItem['line'], "\n"); } + $trace .= $internalError->getTraceAsString(); - $nonIgnorableErrorsByException[] = $fileSpecificError; + if ($internalError->shouldReportBug()) { + $message .= sprintf('%sPost the following stack trace to %s: %s%s', "\n", $bugReportUrl, "\n", $trace); + } else { + $message .= sprintf('%s%s', "\n\n", $trace); + } + } else { + if ($internalError->shouldReportBug()) { + $message .= sprintf('%sRun PHPStan with -v option and post the stack trace to:%s%s%s', "\n\n", "\n", $bugReportUrl, "\n"); + } else { + $message .= sprintf('%sRun PHPStan with -v option to see the stack trace', "\n"); + } } + } - if ($hasInternalErrors || count($nonIgnorableErrorsByException) > 0) { - $fixerAnalysisResult = new AnalysisResult( - $nonIgnorableErrorsByException, - $analysisResult->getInternalErrors(), - $analysisResult->getInternalErrors(), - [], - $analysisResult->isDefaultLevelUsed(), - $analysisResult->getProjectConfigFile(), - $analysisResult->isResultCacheSaved(), - ); + return $message; + } - $stdOutput = $inceptionResult->getStdOutput(); - $stdOutput->getStyle()->error('PHPStan Pro can\'t be launched because of these errors:'); + private function generateBaseline(string $generateBaselineFile, InceptionResult $inceptionResult, AnalysisResult $analysisResult, OutputInterface $output, bool $allowEmptyBaseline, string $baselineExtension, bool $failWithoutResultCache): int + { + if (!$allowEmptyBaseline && !$analysisResult->hasErrors()) { + $inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.'); + $inceptionResult->getStdOutput()->writeLineFormatted('To allow generating empty baselines, pass --allow-empty-baseline option.'); - /** @var TableErrorFormatter $tableErrorFormatter */ - $tableErrorFormatter = $container->getService('errorFormatter.table'); - $tableErrorFormatter->formatErrors($fixerAnalysisResult, $stdOutput); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); + } - $stdOutput->writeLineFormatted('Please fix them first and then re-run PHPStan.'); + $streamOutput = $this->createStreamOutput(); + $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $streamOutput); + $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); + $baselineFileDirectory = dirname($generateBaselineFile); + $baselinePathHelper = new ParentDirectoryRelativePathHelper($baselineFileDirectory); - if ($stdOutput->isDebug()) { - $stdOutput->writeLineFormatted(sprintf('hasInternalErrors: %s', $hasInternalErrors ? 'true' : 'false')); - $stdOutput->writeLineFormatted(sprintf('nonIgnorableErrorsByExceptionCount: %d', count($nonIgnorableErrorsByException))); - } + if ($baselineExtension === 'php') { + $baselineErrorFormatter = new BaselinePhpErrorFormatter($baselinePathHelper); + $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput); + } else { + $baselineErrorFormatter = new BaselineNeonErrorFormatter($baselinePathHelper); + $existingBaselineContent = is_file($generateBaselineFile) ? FileReader::read($generateBaselineFile) : ''; + $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput, $existingBaselineContent); + } - return $inceptionResult->handleReturn(1); - } + $stream = $streamOutput->getStream(); + rewind($stream); + $baselineContents = stream_get_contents($stream); + if ($baselineContents === false) { + throw new ShouldNotHappenException(); + } - if (!$analysisResult->isResultCacheSaved() && !$onlyFiles) { - // this can happen only if there are some regex-related errors in ignoreErrors configuration - $stdOutput = $inceptionResult->getStdOutput(); - if (count($analysisResult->getFileSpecificErrors()) > 0) { - $stdOutput->getStyle()->error('Unknown error. Please report this as a bug.'); - return $inceptionResult->handleReturn(1); - } + try { + DirectoryCreator::ensureDirectoryExists($baselineFileDirectory, 0644); + } catch (DirectoryCreatorException $e) { + $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); - $stdOutput->getStyle()->error('PHPStan Pro can\'t be launched because of these errors:'); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); + } - /** @var TableErrorFormatter $tableErrorFormatter */ - $tableErrorFormatter = $container->getService('errorFormatter.table'); - $tableErrorFormatter->formatErrors($analysisResult, $stdOutput); + try { + FileWriter::write($generateBaselineFile, $baselineContents); + } catch (CouldNotWriteFileException $e) { + $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); - $stdOutput->writeLineFormatted('Please fix them first and then re-run PHPStan.'); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); + } - if ($stdOutput->isDebug()) { - $stdOutput->writeLineFormatted('Result cache was not saved.'); + $errorsCount = 0; + $unignorableCount = 0; + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->canBeIgnored()) { + $unignorableCount++; + if ($output->isVeryVerbose()) { + $inceptionResult->getStdOutput()->writeLineFormatted('Unignorable errors could not be added to the baseline:'); + $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getMessage()); + $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getFile()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); } + continue; + } + + $errorsCount++; + } + + $message = sprintf('Baseline generated with %d %s.', $errorsCount, $errorsCount === 1 ? 'error' : 'errors'); - return $inceptionResult->handleReturn(1); + if ( + $unignorableCount === 0 + && count($analysisResult->getNotFileSpecificErrors()) === 0 + ) { + $inceptionResult->getStdOutput()->getStyle()->success($message); + } else { + if ($output->isVeryVerbose()) { + $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline."); + } else { + $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline. Re-run PHPStan with \"-vv\" and fix them."); } + } - $inceptionResult->handleReturn(0); + $exitCode = 0; + if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + $exitCode = 2; + } - /** @var FixerApplication $fixerApplication */ - $fixerApplication = $container->getByType(FixerApplication::class); + return $inceptionResult->handleReturn($exitCode, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); + } - return $fixerApplication->run( - $inceptionResult->getProjectConfigFile(), - $inceptionResult, - $input, - $output, - $analysisResult->getFileSpecificErrors(), - $analysisResult->getNotFileSpecificErrors(), - count($files), - $_SERVER['argv'][0], - ); + /** + * @param string[] $files + */ + private function runFixer(InceptionResult $inceptionResult, Container $container, bool $onlyFiles, InputInterface $input, OutputInterface $output, array $files): int + { + $ciDetector = new CiDetector(); + if ($ciDetector->isCiDetected()) { + $inceptionResult->getStdOutput()->writeLineFormatted('PHPStan Pro can\'t run in CI environment yet. Stay tuned!'); + + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } - /** @var ErrorFormatter $errorFormatter */ - $errorFormatter = $container->getService($errorFormatterServiceName); + /** @var FixerApplication $fixerApplication */ + $fixerApplication = $container->getByType(FixerApplication::class); - return $inceptionResult->handleReturn( - $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()), + return $fixerApplication->run( + $inceptionResult->getProjectConfigFile(), + $input, + $output, + count($files), + $_SERVER['argv'][0], ); } - private function createStreamOutput(): StreamOutput + private function runDiagnoseExtensions(Container $container, Output $errorOutput): void { - $resource = fopen('php://memory', 'w', false); - if ($resource === false) { - throw new ShouldNotHappenException(); + if (!$errorOutput->isDebug()) { + return; + } + + /** @var PHPStanDiagnoseExtension $phpstanDiagnoseExtension */ + $phpstanDiagnoseExtension = $container->getService('phpstanDiagnoseExtension'); + + // not using tag for this extension to make sure it's always first + $phpstanDiagnoseExtension->print($errorOutput); + + /** @var DiagnoseExtension $extension */ + foreach ($container->getServicesByTag(DiagnoseExtension::EXTENSION_TAG) as $extension) { + $extension->print($errorOutput); } - return new StreamOutput($resource); } } diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index 633b11e344..5b88529382 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -8,13 +8,15 @@ use PHPStan\Parallel\ParallelAnalyser; use PHPStan\Parallel\Scheduler; use PHPStan\Process\CpuCoreCounter; +use PHPStan\ShouldNotHappenException; +use React\EventLoop\StreamSelectLoop; use Symfony\Component\Console\Input\InputInterface; -use function array_filter; -use function array_values; use function count; +use function function_exists; use function is_file; +use function memory_get_peak_usage; -class AnalyserRunner +final class AnalyserRunner { public function __construct( @@ -40,14 +42,12 @@ public function runAnalyser( bool $debug, bool $allowParallel, ?string $projectConfigFile, - ?string $tmpFile, - ?string $insteadOfFile, InputInterface $input, ): AnalyserResult { $filesCount = count($files); if ($filesCount === 0) { - return new AnalyserResult([], [], [], [], false); + return new AnalyserResult([], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); } $schedule = $this->scheduler->scheduleWork($this->cpuCoreCounter->getNumberOfCpuCores(), $files); @@ -59,42 +59,30 @@ public function runAnalyser( if ( !$debug && $allowParallel + && function_exists('proc_open') && $mainScript !== null && $schedule->getNumberOfProcesses() > 0 ) { - return $this->parallelAnalyser->analyse($schedule, $mainScript, $postFileCallback, $projectConfigFile, $tmpFile, $insteadOfFile, $input); + $loop = new StreamSelectLoop(); + $result = null; + $promise = $this->parallelAnalyser->analyse($loop, $schedule, $mainScript, $postFileCallback, $projectConfigFile, $input, null); + $promise->then(static function (AnalyserResult $tmp) use (&$result): void { + $result = $tmp; + }); + $loop->run(); + if ($result === null) { + throw new ShouldNotHappenException(); + } + return $result; } return $this->analyser->analyse( - $this->switchTmpFile($files, $insteadOfFile, $tmpFile), + $files, $preFileCallback, $postFileCallback, $debug, - $this->switchTmpFile($allAnalysedFiles, $insteadOfFile, $tmpFile), + $allAnalysedFiles, ); } - /** - * @param string[] $analysedFiles - * @return string[] - */ - private function switchTmpFile( - array $analysedFiles, - ?string $insteadOfFile, - ?string $tmpFile, - ): array - { - $analysedFiles = array_values(array_filter($analysedFiles, static function (string $file) use ($insteadOfFile): bool { - if ($insteadOfFile === null) { - return true; - } - return $file !== $insteadOfFile; - })); - if ($tmpFile !== null) { - $analysedFiles[] = $tmpFile; - } - - return $analysedFiles; - } - } diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php index 92cb217e21..1697b5f38a 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -3,30 +3,40 @@ namespace PHPStan\Command; use PHPStan\Analyser\Error; +use PHPStan\Analyser\InternalError; +use PHPStan\Collectors\CollectedData; use function count; use function usort; -/** @api */ -class AnalysisResult +/** + * @api + */ +final class AnalysisResult { - /** @var Error[] sorted by their file name, line number and message */ + /** @var list sorted by their file name, line number and message */ private array $fileSpecificErrors; /** - * @param Error[] $fileSpecificErrors - * @param string[] $notFileSpecificErrors - * @param string[] $internalErrors - * @param string[] $warnings + * @param list $fileSpecificErrors + * @param list $notFileSpecificErrors + * @param list $internalErrors + * @param list $warnings + * @param list $collectedData + * @param array $changedProjectExtensionFilesOutsideOfAnalysedPaths */ public function __construct( array $fileSpecificErrors, private array $notFileSpecificErrors, private array $internalErrors, private array $warnings, + private array $collectedData, private bool $defaultLevelUsed, private ?string $projectConfigFile, private bool $savedResultCache, + private int $peakMemoryUsageBytes, + private bool $isResultCacheUsed, + private array $changedProjectExtensionFilesOutsideOfAnalysedPaths, ) { usort( @@ -56,7 +66,7 @@ public function getTotalErrorsCount(): int } /** - * @return Error[] sorted by their file name, line number and message + * @return list sorted by their file name, line number and message */ public function getFileSpecificErrors(): array { @@ -64,7 +74,7 @@ public function getFileSpecificErrors(): array } /** - * @return string[] + * @return list */ public function getNotFileSpecificErrors(): array { @@ -72,15 +82,15 @@ public function getNotFileSpecificErrors(): array } /** - * @return string[] + * @return list */ - public function getInternalErrors(): array + public function getInternalErrorObjects(): array { return $this->internalErrors; } /** - * @return string[] + * @return list */ public function getWarnings(): array { @@ -92,6 +102,14 @@ public function hasWarnings(): bool return count($this->warnings) > 0; } + /** + * @return list + */ + public function getCollectedData(): array + { + return $this->collectedData; + } + public function isDefaultLevelUsed(): bool { return $this->defaultLevelUsed; @@ -112,4 +130,22 @@ public function isResultCacheSaved(): bool return $this->savedResultCache; } + public function getPeakMemoryUsageBytes(): int + { + return $this->peakMemoryUsageBytes; + } + + public function isResultCacheUsed(): bool + { + return $this->isResultCacheUsed; + } + + /** + * @return array + */ + public function getChangedProjectExtensionFilesOutsideOfAnalysedPaths(): array + { + return $this->changedProjectExtensionFilesOutsideOfAnalysedPaths; + } + } diff --git a/src/Command/ClearResultCacheCommand.php b/src/Command/ClearResultCacheCommand.php index 9730b74325..ac41019e2b 100644 --- a/src/Command/ClearResultCacheCommand.php +++ b/src/Command/ClearResultCacheCommand.php @@ -8,9 +8,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function is_bool; use function is_string; -class ClearResultCacheCommand extends Command +final class ClearResultCacheCommand extends Command { private const NAME = 'clear-result-cache'; @@ -32,20 +33,37 @@ 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('debug', null, InputOption::VALUE_NONE, 'Show debug information - which file is analysed, do not catch internal errors'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for clearing result cache'), + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), ]); } + protected function initialize(InputInterface $input, OutputInterface $output): void + { + if ((bool) $input->getOption('debug')) { + $application = $this->getApplication(); + if ($application === null) { + throw new ShouldNotHappenException(); + } + $application->setCatchExceptions(false); + return; + } + } + protected function execute(InputInterface $input, OutputInterface $output): int { $autoloadFile = $input->getOption('autoload-file'); $configuration = $input->getOption('configuration'); $memoryLimit = $input->getOption('memory-limit'); + $debugEnabled = (bool) $input->getOption('debug'); + $allowXdebug = $input->getOption('xdebug'); if ( (!is_string($autoloadFile) && $autoloadFile !== null) || (!is_string($configuration) && $configuration !== null) || (!is_string($memoryLimit) && $memoryLimit !== null) + || (!is_bool($allowXdebug)) ) { throw new ShouldNotHappenException(); } @@ -54,14 +72,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $inceptionResult = CommandHelper::begin( $input, $output, - ['.'], + [], $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, $configuration, null, '0', - false, + $allowXdebug, + $debugEnabled, + true, ); } catch (InceptionNotSuccessfulException) { return 1; @@ -69,7 +89,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $container = $inceptionResult->getContainer(); - /** @var ResultCacheClearer $resultCacheClearer */ $resultCacheClearer = $container->getByType(ResultCacheClearer::class); $path = $resultCacheClearer->clear(); diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index c3de7b2e84..a142ecdca8 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -2,9 +2,8 @@ namespace PHPStan\Command; -use Closure; +use Composer\Semver\Semver; use Composer\XdebugHandler\XdebugHandler; -use Nette\DI\Config\Adapters\PhpAdapter; use Nette\DI\Helpers; use Nette\DI\InvalidConfigurationException; use Nette\DI\ServiceCreationException; @@ -13,35 +12,42 @@ use Nette\Schema\ValidationException; use Nette\Utils\AssertionException; use Nette\Utils\Strings; -use Nette\Utils\Validators; +use PHPStan\Cache\FileCacheStorage; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ContainerFactory; +use PHPStan\DependencyInjection\DuplicateIncludedFilesException; +use PHPStan\DependencyInjection\InvalidExcludePathsException; use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException; use PHPStan\DependencyInjection\LoaderFactory; -use PHPStan\DependencyInjection\NeonAdapter; use PHPStan\ExtensionInstaller\GeneratedConfig; +use PHPStan\File\FileExcluder; use PHPStan\File\FileFinder; use PHPStan\File\FileHelper; +use PHPStan\File\ParentDirectoryRelativePathHelper; +use PHPStan\File\SimpleRelativePathHelper; +use PHPStan\Internal\ComposerHelper; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\ShouldNotHappenException; use ReflectionClass; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Throwable; -use function array_diff_key; +use function array_filter; use function array_key_exists; use function array_map; -use function array_merge; -use function array_unique; +use function array_values; 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 getenv; use function gettype; use function implode; use function ini_get; @@ -50,21 +56,17 @@ 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 spl_autoload_functions; use function sprintf; -use function str_ends_with; +use function str_contains; use function str_repeat; -use function strpos; use function sys_get_temp_dir; use const DIRECTORY_SEPARATOR; use const E_ERROR; use const PHP_VERSION_ID; -use const SIGINT; -class CommandHelper +final class CommandHelper { public const DEFAULT_LEVEL = '0'; @@ -88,12 +90,17 @@ public static function begin( ?string $generateBaselineFile, ?string $level, bool $allowXdebug, - bool $debugEnabled = false, - ?string $singleReflectionFile = null, - ?string $singleReflectionInsteadOfFile = null, - bool $cleanupContainerCache = true, + bool $debugEnabled, + bool $cleanupContainerCache, ): InceptionResult { + $stdOutput = new SymfonyOutput($output, new SymfonyStyle(new ErrorsConsoleStyle($input, $output))); + + $errorOutput = (static function () use ($input, $output): Output { + $symfonyErrorOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; + return new SymfonyOutput($symfonyErrorOutput, new SymfonyStyle(new ErrorsConsoleStyle($input, $symfonyErrorOutput))); + })(); + if (!$allowXdebug) { $xdebug = new XdebugHandler('phpstan'); $xdebug->setPersistent(); @@ -101,13 +108,25 @@ public static function begin( unset($xdebug); } - $stdOutput = new SymfonyOutput($output, new SymfonyStyle(new ErrorsConsoleStyle($input, $output))); + if ($allowXdebug) { + if (!XdebugHandler::isXdebugActive()) { + $errorOutput->getStyle()->note('You are running with "--xdebug" enabled, but the Xdebug PHP extension is not active. The process will not halt at breakpoints.'); + } else { + $errorOutput->getStyle()->note("You are running with \"--xdebug\" enabled, and the Xdebug PHP extension is active.\nThe process will halt at breakpoints, but PHPStan will run much slower.\nUse this only if you are debugging PHPStan itself or your custom extensions."); + } + } elseif (XdebugHandler::isXdebugActive()) { + $errorOutput->getStyle()->note('The Xdebug PHP extension is active, but "--xdebug" is not used. This may slow down performance and the process will not halt at breakpoints.'); + } elseif ($debugEnabled) { + $v = XdebugHandler::getSkippedVersion(); + if ($v !== '') { + $errorOutput->getStyle()->note( + "The Xdebug PHP extension is active, but \"--xdebug\" is not used.\n" . + "The process was restarted and it will not halt at breakpoints.\n" . + 'Use "--xdebug" if you want to halt at breakpoints.', + ); + } + } - /** @var Output $errorOutput */ - $errorOutput = (static function () use ($input, $output): Output { - $symfonyErrorOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; - return new SymfonyOutput($symfonyErrorOutput, new SymfonyStyle(new ErrorsConsoleStyle($input, $symfonyErrorOutput))); - })(); if ($memoryLimit !== null) { if (Strings::match($memoryLimit, '#^-?\d+[kMG]?$#i') === null) { $errorOutput->writeLineFormatted(sprintf('Invalid memory limit format "%s".', $memoryLimit)); @@ -130,7 +149,7 @@ public static function begin( return; } - if (strpos($error['message'], 'Allowed memory size') === false) { + if (!str_contains($error['message'], 'Allowed memory size')) { return; } @@ -145,6 +164,10 @@ public static function begin( } $currentWorkingDirectoryFileHelper = new FileHelper($currentWorkingDirectory); $currentWorkingDirectory = $currentWorkingDirectoryFileHelper->getWorkingDirectory(); + + /** @var list|false $autoloadFunctionsBefore */ + $autoloadFunctionsBefore = spl_autoload_functions(); + if ($autoloadFile !== null) { $autoloadFile = $currentWorkingDirectoryFileHelper->absolutizePath($autoloadFile); if (!is_file($autoloadFile)) { @@ -157,7 +180,15 @@ public static function begin( })($autoloadFile); } if ($projectConfigFile === null) { - foreach (['phpstan.neon', 'phpstan.neon.dist', 'phpstan.dist.neon'] as $discoverableConfigName) { + $discoverableConfigNames = [ + '.phpstan.neon', + 'phpstan.neon', + '.phpstan.neon.dist', + 'phpstan.neon.dist', + '.phpstan.dist.neon', + 'phpstan.dist.neon', + ]; + foreach ($discoverableConfigNames as $discoverableConfigName) { $discoverableConfigFile = $currentWorkingDirectory . DIRECTORY_SEPARATOR . $discoverableConfigName; if (is_file($discoverableConfigFile)) { $projectConfigFile = $discoverableConfigFile; @@ -183,6 +214,9 @@ public static function begin( $analysedPathsFromConfig = []; $containerFactory = new ContainerFactory($currentWorkingDirectory); + if ($cleanupContainerCache) { + $containerFactory->setJournalContainer(); + } $projectConfig = null; if ($projectConfigFile !== null) { if (!is_file($projectConfigFile)) { @@ -206,6 +240,7 @@ public static function begin( $defaultParameters = [ 'rootDir' => $containerFactory->getRootDirectory(), 'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(), + 'env' => getenv(), ]; if (isset($projectConfig['parameters']['tmpDir'])) { @@ -260,6 +295,43 @@ public static function begin( $additionalConfigFiles[] = $includedFilePath; } } + + if ( + count($additionalConfigFiles) > 0 + && $generatedConfigReflection->hasConstant('PHPSTAN_VERSION_CONSTRAINT') + ) { + $generatedConfigPhpStanVersionConstraint = $generatedConfigReflection->getConstant('PHPSTAN_VERSION_CONSTRAINT'); + if ($generatedConfigPhpStanVersionConstraint !== null) { + $phpstanSemverVersion = ComposerHelper::getPhpStanVersion(); + if ( + $phpstanSemverVersion !== ComposerHelper::UNKNOWN_VERSION + && !str_contains($phpstanSemverVersion, '@') + && !Semver::satisfies($phpstanSemverVersion, $generatedConfigPhpStanVersionConstraint) + ) { + $errorOutput->writeLineFormatted('Running PHPStan with incompatible extensions'); + $errorOutput->writeLineFormatted('You\'re running PHPStan from a different Composer project'); + $errorOutput->writeLineFormatted('than the one where you installed extensions.'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted(sprintf('Your PHPStan version is: %s', $phpstanSemverVersion)); + $errorOutput->writeLineFormatted(sprintf('Installed PHPStan extensions support: %s', $generatedConfigPhpStanVersionConstraint)); + + $errorOutput->writeLineFormatted(''); + if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { + $mainScript = $_SERVER['argv'][0]; + $errorOutput->writeLineFormatted(sprintf('PHPStan is running from: %s', $currentWorkingDirectoryFileHelper->absolutizePath(dirname($mainScript)))); + } + + $errorOutput->writeLineFormatted(sprintf('Extensions were installed in: %s', dirname($generatedConfigDirectory, 3))); + $errorOutput->writeLineFormatted(''); + + $simpleRelativePathHelper = new SimpleRelativePathHelper($currentWorkingDirectory); + $errorOutput->writeLineFormatted(sprintf('Run PHPStan with %s to fix this problem.', $simpleRelativePathHelper->getRelativePath(dirname($generatedConfigDirectory, 3) . '/bin/phpstan'))); + + $errorOutput->writeLineFormatted(''); + throw new InceptionNotSuccessfulException(); + } + } + } } if ( @@ -269,21 +341,11 @@ public static function begin( $additionalConfigFiles[] = $projectConfigFile; } - $loaderParameters = [ - 'rootDir' => $containerFactory->getRootDirectory(), - 'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(), - ]; - - self::detectDuplicateIncludedFiles( - $errorOutput, - $currentWorkingDirectoryFileHelper, - $additionalConfigFiles, - $loaderParameters, - ); - $createDir = static function (string $path) use ($errorOutput): void { - if (!is_dir($path) && !@mkdir($path, 0777) && !is_dir($path)) { - $errorOutput->writeLineFormatted(sprintf('Cannot create a temp directory %s', $path)); + try { + DirectoryCreator::ensureDirectoryExists($path, 0777); + } catch (DirectoryCreatorException $e) { + $errorOutput->writeLineFormatted($e->getMessage()); throw new InceptionNotSuccessfulException(); } }; @@ -294,7 +356,7 @@ public static function begin( } try { - $container = $containerFactory->create($tmpDir, $additionalConfigFiles, $paths, $composerAutoloaderProjectPaths, $analysedPathsFromConfig, $level ?? self::DEFAULT_LEVEL, $generateBaselineFile, $autoloadFile, $singleReflectionFile, $singleReflectionInsteadOfFile); + $container = $containerFactory->create($tmpDir, $additionalConfigFiles, $paths, $composerAutoloaderProjectPaths, $analysedPathsFromConfig, $level ?? self::DEFAULT_LEVEL, $generateBaselineFile, $autoloadFile); } catch (InvalidConfigurationException | AssertionException $e) { $errorOutput->writeLineFormatted('Invalid configuration:'); $errorOutput->writeLineFormatted($e->getMessage()); @@ -305,6 +367,44 @@ public static function begin( $errorOutput->writeLineFormatted($error); $errorOutput->writeLineFormatted(''); } + + $errorOutput->writeLineFormatted('To ignore non-existent paths in ignoreErrors,'); + $errorOutput->writeLineFormatted('set reportUnmatchedIgnoredErrors: false in your configuration file.'); + $errorOutput->writeLineFormatted(''); + + throw new InceptionNotSuccessfulException(); + } catch (InvalidExcludePathsException $e) { + $errorOutput->writeLineFormatted(sprintf('Invalid %s in excludePaths:', count($e->getErrors()) === 1 ? 'entry' : 'entries')); + foreach ($e->getErrors() as $error) { + $errorOutput->writeLineFormatted($error); + $errorOutput->writeLineFormatted(''); + } + + $suggestOptional = $e->getSuggestOptional(); + if (count($suggestOptional) > 0) { + $baselinePathHelper = null; + if ($projectConfigFile !== null) { + $baselinePathHelper = new ParentDirectoryRelativePathHelper(dirname($projectConfigFile)); + } + $errorOutput->writeLineFormatted('If the excluded path can sometimes exist, append (?)'); + $errorOutput->writeLineFormatted('to its config entry to mark it as optional. Example:'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('parameters:'); + $errorOutput->writeLineFormatted("\texcludePaths:"); + foreach ($suggestOptional as $key => $suggestOptionalPaths) { + $errorOutput->writeLineFormatted(sprintf("\t\t%s:", $key)); + foreach ($suggestOptionalPaths as $suggestOptionalPath) { + if ($baselinePathHelper === null) { + $errorOutput->writeLineFormatted(sprintf("\t\t\t- %s (?)", $suggestOptionalPath)); + continue; + } + + $errorOutput->writeLineFormatted(sprintf("\t\t\t- %s (?)", $baselinePathHelper->getRelativePath($suggestOptionalPath))); + } + } + $errorOutput->writeLineFormatted(''); + } + throw new InceptionNotSuccessfulException(); } catch (ValidationException $e) { foreach ($e->getMessages() as $message) { @@ -343,18 +443,28 @@ public static function begin( $errorOutput->writeLineFormatted(''); throw new InceptionNotSuccessfulException(); - } + } catch (DuplicateIncludedFilesException $e) { + $format = "These files are included multiple times:\n- %s"; + if (count($e->getFiles()) === 1) { + $format = "This file is included multiple times:\n- %s"; + } + $errorOutput->writeLineFormatted(sprintf($format, implode("\n- ", $e->getFiles()))); - if ($cleanupContainerCache) { - $containerFactory->clearOldContainers($tmpDir); - } + if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) { + $errorOutput->writeLineFormatted(''); + $errorOutput->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.'); + } - if (count($paths) === 0) { - $errorOutput->writeLineFormatted('At least one path must be specified to analyse.'); throw new InceptionNotSuccessfulException(); } - self::setUpSignalHandler($errorOutput); + if ($cleanupContainerCache) { + $cacheStorage = $container->getService('cacheStorage'); + if ($cacheStorage instanceof FileCacheStorage) { + $cacheStorage->clearUnusedFiles(); + } + } + /** @var bool|null $customRulesetUsed */ $customRulesetUsed = $container->getParameter('customRulesetUsed'); if ($customRulesetUsed === null) { @@ -377,6 +487,24 @@ public static function begin( self::executeBootstrapFile($bootstrapFileFromArray, $container, $errorOutput, $debugEnabled); } + /** @var list|false $autoloadFunctionsAfter */ + $autoloadFunctionsAfter = spl_autoload_functions(); + + if ($autoloadFunctionsBefore !== false && $autoloadFunctionsAfter !== false) { + $newAutoloadFunctions = $GLOBALS['__phpstanAutoloadFunctions'] ?? []; + foreach ($autoloadFunctionsAfter as $after) { + foreach ($autoloadFunctionsBefore as $before) { + if ($after === $before) { + continue 2; + } + } + + $newAutoloadFunctions[] = $after; + } + + $GLOBALS['__phpstanAutoloadFunctions'] = $newAutoloadFunctions; + } + if (PHP_VERSION_ID >= 80000) { require_once __DIR__ . '/../../stubs/runtime/Enum/UnitEnum.php'; require_once __DIR__ . '/../../stubs/runtime/Enum/BackedEnum.php'; @@ -424,36 +552,27 @@ public static function begin( throw new InceptionNotSuccessfulException(); } - $excludesAnalyse = $container->getParameter('excludes_analyse'); - $excludePaths = $container->getParameter('excludePaths'); - if (count($excludesAnalyse) > 0 && $excludePaths !== null) { - $errorOutput->writeLineFormatted(sprintf('Configuration parameters excludes_analyse and excludePaths cannot be used at the same time.')); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted(sprintf('Parameter excludes_analyse has been deprecated so use excludePaths only from now on.')); - $errorOutput->writeLineFormatted(''); - - 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'); - $createDir($tempResultCachePath); - /** @var FileFinder $fileFinder */ $fileFinder = $container->getService('fileFinderAnalyse'); $pathRoutingParser = $container->getService('pathRoutingParser'); - /** @var Closure(): array{string[], bool} $filesCallback */ - $filesCallback = static function () use ($fileFinder, $pathRoutingParser, $paths): array { + $stubFilesProvider = $container->getByType(StubFilesProvider::class); + + $filesCallback = static function () use ($currentWorkingDirectoryFileHelper, $stubFilesProvider, $fileFinder, $pathRoutingParser, $paths, $errorOutput): array { + if (count($paths) === 0) { + $errorOutput->writeLineFormatted('At least one path must be specified to analyse.'); + throw new InceptionNotSuccessfulException(); + } $fileFinderResult = $fileFinder->findFiles($paths); $files = $fileFinderResult->getFiles(); $pathRoutingParser->setAnalysedFiles($files); + $stubFilesExcluder = new FileExcluder($currentWorkingDirectoryFileHelper, $stubFilesProvider->getProjectStubFiles()); + + $files = array_values(array_filter($files, static fn (string $file) => !$stubFilesExcluder->isExcludedFromAnalysing($file))); + return [$files, $fileFinderResult->isOnlyFiles()]; }; @@ -498,103 +617,4 @@ private static function executeBootstrapFile( } } - private static function setUpSignalHandler(Output $output): void - { - if (!function_exists('pcntl_signal')) { - return; - } - - pcntl_async_signals(true); - pcntl_signal(SIGINT, static function () use ($output): void { - $output->writeLineFormatted(''); - exit(1); - }); - } - - /** - * @param string[] $configFiles - * @param array $loaderParameters - * @throws InceptionNotSuccessfulException - */ - private static function detectDuplicateIncludedFiles( - Output $output, - FileHelper $fileHelper, - array $configFiles, - array $loaderParameters, - ): void - { - $neonAdapter = new NeonAdapter(); - $phpAdapter = new PhpAdapter(); - $allConfigFiles = []; - foreach ($configFiles as $configFile) { - $allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null)); - } - - $normalized = array_map(static fn (string $file): string => $fileHelper->normalizePath($file), $allConfigFiles); - - $deduplicated = array_unique($normalized); - if (count($normalized) <= count($deduplicated)) { - return; - } - - $duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated)); - - $format = "These files are included multiple times:\n- %s"; - if (count($duplicateFiles) === 1) { - $format = "This file is included multiple times:\n- %s"; - } - $output->writeLineFormatted(sprintf($format, implode("\n- ", $duplicateFiles))); - - if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) { - $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 InceptionNotSuccessfulException(); - } - - /** - * @param array $loaderParameters - * @return string[] - */ - private static function getConfigFiles( - FileHelper $fileHelper, - NeonAdapter $neonAdapter, - PhpAdapter $phpAdapter, - string $configFile, - array $loaderParameters, - ?string $generateBaselineFile, - ): array - { - if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) { - return []; - } - if (!is_file($configFile) || !is_readable($configFile)) { - return []; - } - - if (str_ends_with($configFile, '.php')) { - $data = $phpAdapter->load($configFile); - } else { - $data = $neonAdapter->load($configFile); - } - $allConfigFiles = [$configFile]; - if (isset($data['includes'])) { - Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile)); - $includes = Helpers::expand($data['includes'], $loaderParameters); - foreach ($includes as $include) { - $include = self::expandIncludedFile($include, $configFile); - $allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile)); - } - } - - return $allConfigFiles; - } - - private static function expandIncludedFile(string $includedFile, string $mainFile): string - { - return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute - ? $includedFile - : dirname($mainFile) . '/' . $includedFile; - } - } diff --git a/src/Command/DiagnoseCommand.php b/src/Command/DiagnoseCommand.php new file mode 100644 index 0000000000..5a9f17e0ab --- /dev/null +++ b/src/Command/DiagnoseCommand.php @@ -0,0 +1,106 @@ +setName(self::NAME) + ->setDescription('Shows diagnose information about PHPStan and extensions') + ->setDefinition([ + 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'), + new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - do not catch internal errors'), + new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for clearing result cache'), + ]); + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + if ((bool) $input->getOption('debug')) { + $application = $this->getApplication(); + if ($application === null) { + throw new ShouldNotHappenException(); + } + $application->setCatchExceptions(false); + return; + } + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $memoryLimit = $input->getOption('memory-limit'); + $autoloadFile = $input->getOption('autoload-file'); + $configuration = $input->getOption('configuration'); + $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); + + if ( + (!is_string($memoryLimit) && $memoryLimit !== null) + || (!is_string($autoloadFile) && $autoloadFile !== null) + || (!is_string($configuration) && $configuration !== null) + || (!is_string($level) && $level !== null) + ) { + throw new ShouldNotHappenException(); + } + + try { + $inceptionResult = CommandHelper::begin( + $input, + $output, + [], + $memoryLimit, + $autoloadFile, + $this->composerAutoloaderProjectPaths, + $configuration, + null, + $level, + false, + false, + false, + ); + } catch (InceptionNotSuccessfulException) { + return 1; + } + + $container = $inceptionResult->getContainer(); + $output = $inceptionResult->getStdOutput(); + + /** @var PHPStanDiagnoseExtension $phpstanDiagnoseExtension */ + $phpstanDiagnoseExtension = $container->getService('phpstanDiagnoseExtension'); + + // not using tag for this extension to make sure it's always first + $phpstanDiagnoseExtension->print($output); + + /** @var DiagnoseExtension $extension */ + foreach ($container->getServicesByTag(DiagnoseExtension::EXTENSION_TAG) as $extension) { + $extension->print($output); + } + + return 0; + } + +} diff --git a/src/Command/DumpParametersCommand.php b/src/Command/DumpParametersCommand.php new file mode 100644 index 0000000000..ccb8188933 --- /dev/null +++ b/src/Command/DumpParametersCommand.php @@ -0,0 +1,112 @@ +setName(self::NAME) + ->setDescription('Dumps all parameters') + ->setDefinition([ + 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'), + new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - which file is analysed, do not catch internal errors'), + new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for clearing result cache'), + new InputOption('json', null, InputOption::VALUE_NONE, 'Dump parameters as JSON instead of NEON'), + ]); + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + if ((bool) $input->getOption('debug')) { + $application = $this->getApplication(); + if ($application === null) { + throw new ShouldNotHappenException(); + } + $application->setCatchExceptions(false); + return; + } + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $memoryLimit = $input->getOption('memory-limit'); + $autoloadFile = $input->getOption('autoload-file'); + $configuration = $input->getOption('configuration'); + $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); + $json = (bool) $input->getOption('json'); + + if ( + (!is_string($memoryLimit) && $memoryLimit !== null) + || (!is_string($autoloadFile) && $autoloadFile !== null) + || (!is_string($configuration) && $configuration !== null) + || (!is_string($level) && $level !== null) + ) { + throw new ShouldNotHappenException(); + } + + try { + $inceptionResult = CommandHelper::begin( + $input, + $output, + [], + $memoryLimit, + $autoloadFile, + $this->composerAutoloaderProjectPaths, + $configuration, + null, + $level, + false, + false, + false, + ); + } catch (InceptionNotSuccessfulException) { + return 1; + } + + $parameters = $inceptionResult->getContainer()->getParameters(); + + // always set to '.' + unset($parameters['analysedPaths']); + // irrelevant Nette parameters + unset($parameters['debugMode']); + unset($parameters['productionMode']); + unset($parameters['tempDir']); + unset($parameters['__validate']); + + if ($json) { + $encoded = Json::encode($parameters, Json::PRETTY); + } else { + $encoded = Neon::encode($parameters, true); + } + + $output->writeln($encoded); + + return 0; + } + +} diff --git a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php index 244f978551..ac02e1f9e1 100644 --- a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php @@ -4,14 +4,18 @@ use Nette\DI\Helpers; use Nette\Neon\Neon; +use Nette\Utils\Strings; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use PHPStan\ShouldNotHappenException; +use function count; use function ksort; use function preg_quote; +use function substr; use const SORT_STRING; -class BaselineNeonErrorFormatter implements ErrorFormatter +final class BaselineNeonErrorFormatter { public function __construct(private RelativePathHelper $relativePathHelper) @@ -21,14 +25,11 @@ public function __construct(private RelativePathHelper $relativePathHelper) public function formatErrors( AnalysisResult $analysisResult, Output $output, + string $existingBaselineContent, ): int { if (!$analysisResult->hasErrors()) { - $output->writeRaw(Neon::encode([ - 'parameters' => [ - 'ignoreErrors' => [], - ], - ], Neon::BLOCK)); + $output->writeRaw($this->getNeon([], $existingBaselineContent)); return 0; } @@ -37,39 +38,90 @@ public function formatErrors( if (!$fileSpecificError->canBeIgnored()) { continue; } - $fileErrors[$this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError->getMessage(); + $fileErrors[$this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError; } ksort($fileErrors, SORT_STRING); $errorsToOutput = []; - foreach ($fileErrors as $file => $errorMessages) { - $fileErrorsCounts = []; - foreach ($errorMessages as $errorMessage) { - if (!isset($fileErrorsCounts[$errorMessage])) { - $fileErrorsCounts[$errorMessage] = 1; + foreach ($fileErrors as $file => $errors) { + $fileErrorsByMessage = []; + foreach ($errors as $error) { + $errorMessage = $error->getMessage(); + $identifier = $error->getIdentifier(); + if (!isset($fileErrorsByMessage[$errorMessage])) { + $fileErrorsByMessage[$errorMessage] = [ + 1, + $identifier !== null ? [$identifier => 1] : [], + ]; continue; } - $fileErrorsCounts[$errorMessage]++; + $fileErrorsByMessage[$errorMessage][0]++; + + if ($identifier === null) { + continue; + } + + if (!isset($fileErrorsByMessage[$errorMessage][1][$identifier])) { + $fileErrorsByMessage[$errorMessage][1][$identifier] = 1; + continue; + } + + $fileErrorsByMessage[$errorMessage][1][$identifier]++; } - ksort($fileErrorsCounts, SORT_STRING); - - foreach ($fileErrorsCounts as $message => $count) { - $errorsToOutput[] = [ - 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), - 'count' => $count, - 'path' => Helpers::escape($file), - ]; + ksort($fileErrorsByMessage, SORT_STRING); + + foreach ($fileErrorsByMessage as $message => [$totalCount, $identifiers]) { + ksort($identifiers, SORT_STRING); + if (count($identifiers) > 0) { + foreach ($identifiers as $identifier => $identifierCount) { + $errorsToOutput[] = [ + 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), + 'identifier' => $identifier, + 'count' => $identifierCount, + 'path' => Helpers::escape($file), + ]; + } + } else { + $errorsToOutput[] = [ + 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), + 'count' => $totalCount, + 'path' => Helpers::escape($file), + ]; + } } } - $output->writeRaw(Neon::encode([ + $output->writeRaw($this->getNeon($errorsToOutput, $existingBaselineContent)); + + return 1; + } + + /** + * @param array $ignoreErrors + */ + private function getNeon(array $ignoreErrors, string $existingBaselineContent): string + { + $neon = Neon::encode([ 'parameters' => [ - 'ignoreErrors' => $errorsToOutput, + 'ignoreErrors' => $ignoreErrors, ], - ], Neon::BLOCK)); + ], Neon::BLOCK); - return 1; + if (substr($neon, -2) !== "\n\n") { + throw new ShouldNotHappenException(); + } + + if ($existingBaselineContent === '') { + return substr($neon, 0, -1); + } + + $existingBaselineContentEndOfFileNewlinesMatches = Strings::match($existingBaselineContent, "~(\n)+$~"); + $existingBaselineContentEndOfFileNewlines = $existingBaselineContentEndOfFileNewlinesMatches !== null + ? $existingBaselineContentEndOfFileNewlinesMatches[0] + : ''; + + return substr($neon, 0, -2) . $existingBaselineContentEndOfFileNewlines; } } diff --git a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php new file mode 100644 index 0000000000..65cafffb9f --- /dev/null +++ b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php @@ -0,0 +1,110 @@ +hasErrors()) { + $php = 'writeRaw($php); + return 0; + } + + $fileErrors = []; + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->canBeIgnored()) { + continue; + } + $fileErrors['/' . $this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError; + } + ksort($fileErrors, SORT_STRING); + + $php = ' $errors) { + $fileErrorsByMessage = []; + foreach ($errors as $error) { + $errorMessage = $error->getMessage(); + $identifier = $error->getIdentifier(); + if (!isset($fileErrorsByMessage[$errorMessage])) { + $fileErrorsByMessage[$errorMessage] = [ + 1, + $identifier !== null ? [$identifier => 1] : [], + ]; + continue; + } + + $fileErrorsByMessage[$errorMessage][0]++; + + if ($identifier === null) { + continue; + } + + if (!isset($fileErrorsByMessage[$errorMessage][1][$identifier])) { + $fileErrorsByMessage[$errorMessage][1][$identifier] = 1; + continue; + } + + $fileErrorsByMessage[$errorMessage][1][$identifier]++; + } + ksort($fileErrorsByMessage, SORT_STRING); + + foreach ($fileErrorsByMessage as $message => [$totalCount, $identifiers]) { + ksort($identifiers, SORT_STRING); + if (count($identifiers) > 0) { + foreach ($identifiers as $identifier => $identifierCount) { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t'message' => %s,\n\t'identifier' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + var_export(Helpers::escape('#^' . preg_quote($message, '#') . '$#'), true), + var_export(Helpers::escape($identifier), true), + var_export($identifierCount, true), + var_export(Helpers::escape($file), true), + ); + } + } else { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t'message' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + var_export(Helpers::escape('#^' . preg_quote($message, '#') . '$#'), true), + var_export($totalCount, true), + var_export(Helpers::escape($file), true), + ); + } + } + } + + $php .= "\n"; + $php .= 'return [\'parameters\' => [\'ignoreErrors\' => $ignoreErrors]];'; + $php .= "\n"; + + $output->writeRaw($php); + + return 1; + } + +} diff --git a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php index 33eb65bb37..9072bd8dd2 100644 --- a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php +++ b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php @@ -12,7 +12,7 @@ use const ENT_COMPAT; use const ENT_XML1; -class CheckstyleErrorFormatter implements ErrorFormatter +final class CheckstyleErrorFormatter implements ErrorFormatter { public function __construct(private RelativePathHelper $relativePathHelper) @@ -38,9 +38,10 @@ public function formatErrors( foreach ($errors as $error) { $output->writeRaw(sprintf( - ' ', + ' ', $this->escape((string) $error->getLine()), $this->escape($error->getMessage()), + $error->getIdentifier() !== null ? sprintf(' source="%s"', $this->escape($error->getIdentifier())) : '', )); $output->writeLineFormatted(''); } diff --git a/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php b/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php new file mode 100644 index 0000000000..38d0a67439 --- /dev/null +++ b/src/Command/ErrorFormatter/CiDetectedErrorFormatter.php @@ -0,0 +1,45 @@ +detect(); + if ($ci->getCiName() === CiDetector::CI_GITHUB_ACTIONS) { + return $this->githubErrorFormatter->formatErrors($analysisResult, $output); + } elseif ($ci->getCiName() === CiDetector::CI_TEAMCITY) { + return $this->teamcityErrorFormatter->formatErrors($analysisResult, $output); + } + } catch (CiNotDetectedException) { + // pass + } + + if (!$analysisResult->hasErrors() && !$analysisResult->hasWarnings()) { + return 0; + } + + return $analysisResult->getTotalErrorsCount() > 0 ? 1 : 0; + } + +} diff --git a/src/Command/ErrorFormatter/ErrorFormatter.php b/src/Command/ErrorFormatter/ErrorFormatter.php index cc4b48df3a..2d4e17ee31 100644 --- a/src/Command/ErrorFormatter/ErrorFormatter.php +++ b/src/Command/ErrorFormatter/ErrorFormatter.php @@ -5,7 +5,20 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; -/** @api */ +/** + * This is the interface custom error formatters implement. Register it in the configuration file + * like this: + * + * ``` + * services: + * errorFormatter.myFormat: + * class: App\PHPStan\AwesomeErrorFormatter + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/error-formatters + * + * @api + */ interface ErrorFormatter { diff --git a/src/Command/ErrorFormatter/GithubErrorFormatter.php b/src/Command/ErrorFormatter/GithubErrorFormatter.php index 33ee0f3b8e..21ffdd2650 100644 --- a/src/Command/ErrorFormatter/GithubErrorFormatter.php +++ b/src/Command/ErrorFormatter/GithubErrorFormatter.php @@ -14,20 +14,17 @@ * Allow errors to be reported in pull-requests diff when run in a GitHub Action * @see https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message */ -class GithubErrorFormatter implements ErrorFormatter +final class GithubErrorFormatter implements ErrorFormatter { public function __construct( private RelativePathHelper $relativePathHelper, - private ErrorFormatter $errorFormatter, ) { } public function formatErrors(AnalysisResult $analysisResult, Output $output): int { - $this->errorFormatter->formatErrors($analysisResult, $output); - foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { $metas = [ 'file' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()), diff --git a/src/Command/ErrorFormatter/GitlabErrorFormatter.php b/src/Command/ErrorFormatter/GitlabErrorFormatter.php index 6aa4b61bef..9a8ccb35cd 100644 --- a/src/Command/ErrorFormatter/GitlabErrorFormatter.php +++ b/src/Command/ErrorFormatter/GitlabErrorFormatter.php @@ -12,7 +12,7 @@ /** * @see https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html#implementing-a-custom-tool */ -class GitlabErrorFormatter implements ErrorFormatter +final class GitlabErrorFormatter implements ErrorFormatter { public function __construct(private RelativePathHelper $relativePathHelper) diff --git a/src/Command/ErrorFormatter/JsonErrorFormatter.php b/src/Command/ErrorFormatter/JsonErrorFormatter.php index 4c075f0ac7..0a4174d4e0 100644 --- a/src/Command/ErrorFormatter/JsonErrorFormatter.php +++ b/src/Command/ErrorFormatter/JsonErrorFormatter.php @@ -5,10 +5,12 @@ use Nette\Utils\Json; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; -use function array_key_exists; +use stdClass; +use Symfony\Component\Console\Formatter\OutputFormatter; use function count; +use function property_exists; -class JsonErrorFormatter implements ErrorFormatter +final class JsonErrorFormatter implements ErrorFormatter { public function __construct(private bool $pretty) @@ -22,25 +24,37 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in 'errors' => count($analysisResult->getNotFileSpecificErrors()), 'file_errors' => count($analysisResult->getFileSpecificErrors()), ], - 'files' => [], + 'files' => new stdClass(), 'errors' => [], ]; + $tipFormatter = new OutputFormatter(false); + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { $file = $fileSpecificError->getFile(); - if (!array_key_exists($file, $errorsArray['files'])) { - $errorsArray['files'][$file] = [ + if (!property_exists($errorsArray['files'], $file)) { + $errorsArray['files']->$file = [ 'errors' => 0, 'messages' => [], ]; } - $errorsArray['files'][$file]['errors']++; + $errorsArray['files']->$file['errors']++; - $errorsArray['files'][$file]['messages'][] = [ + $message = [ 'message' => $fileSpecificError->getMessage(), 'line' => $fileSpecificError->getLine(), 'ignorable' => $fileSpecificError->canBeIgnored(), ]; + + if ($fileSpecificError->getTip() !== null) { + $message['tip'] = $tipFormatter->format($fileSpecificError->getTip()); + } + + if ($fileSpecificError->getIdentifier() !== null) { + $message['identifier'] = $fileSpecificError->getIdentifier(); + } + + $errorsArray['files']->$file['messages'][] = $message; } foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) { diff --git a/src/Command/ErrorFormatter/JunitErrorFormatter.php b/src/Command/ErrorFormatter/JunitErrorFormatter.php index 072f10483f..59131dcf01 100644 --- a/src/Command/ErrorFormatter/JunitErrorFormatter.php +++ b/src/Command/ErrorFormatter/JunitErrorFormatter.php @@ -10,7 +10,7 @@ use const ENT_COMPAT; use const ENT_XML1; -class JunitErrorFormatter implements ErrorFormatter +final class JunitErrorFormatter implements ErrorFormatter { public function __construct(private RelativePathHelper $relativePathHelper) @@ -22,11 +22,14 @@ public function formatErrors( Output $output, ): int { + $totalFailuresCount = $analysisResult->getTotalErrorsCount(); + $totalTestsCount = $analysisResult->hasErrors() ? $totalFailuresCount : 1; + $result = ''; $result .= sprintf( '', - $analysisResult->getTotalErrorsCount(), - $analysisResult->getTotalErrorsCount(), + $totalFailuresCount, + $totalTestsCount, ); foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { @@ -34,16 +37,16 @@ public function formatErrors( $result .= $this->createTestCase( sprintf('%s:%s', $fileName, (string) $fileSpecificError->getLine()), 'ERROR', - $this->escape($fileSpecificError->getMessage()), + $fileSpecificError->getMessage(), ); } foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) { - $result .= $this->createTestCase('General error', 'ERROR', $this->escape($notFileSpecificError)); + $result .= $this->createTestCase('General error', 'ERROR', $notFileSpecificError); } foreach ($analysisResult->getWarnings() as $warning) { - $result .= $this->createTestCase('Warning', 'WARNING', $this->escape($warning)); + $result .= $this->createTestCase('Warning', 'WARNING', $warning); } if (!$analysisResult->hasErrors()) { diff --git a/src/Command/ErrorFormatter/RawErrorFormatter.php b/src/Command/ErrorFormatter/RawErrorFormatter.php index 4625983275..f761a5a47e 100644 --- a/src/Command/ErrorFormatter/RawErrorFormatter.php +++ b/src/Command/ErrorFormatter/RawErrorFormatter.php @@ -6,7 +6,7 @@ use PHPStan\Command\Output; use function sprintf; -class RawErrorFormatter implements ErrorFormatter +final class RawErrorFormatter implements ErrorFormatter { public function formatErrors( @@ -19,13 +19,20 @@ public function formatErrors( $output->writeLineFormatted(''); } + $outputIdentifiers = $output->isVerbose(); foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + $identifier = ''; + if ($outputIdentifiers && $fileSpecificError->getIdentifier() !== null) { + $identifier = sprintf(' [identifier=%s]', $fileSpecificError->getIdentifier()); + } + $output->writeRaw( sprintf( - '%s:%d:%s', + '%s:%d:%s%s', $fileSpecificError->getFile(), $fileSpecificError->getLine() ?? '?', $fileSpecificError->getMessage(), + $identifier, ), ); $output->writeLineFormatted(''); diff --git a/src/Command/ErrorFormatter/TableErrorFormatter.php b/src/Command/ErrorFormatter/TableErrorFormatter.php index 7a7504f7cf..f69eca834e 100644 --- a/src/Command/ErrorFormatter/TableErrorFormatter.php +++ b/src/Command/ErrorFormatter/TableErrorFormatter.php @@ -7,20 +7,30 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use PHPStan\File\SimpleRelativePathHelper; use Symfony\Component\Console\Formatter\OutputFormatter; use function array_map; use function count; +use function explode; +use function getenv; +use function in_array; use function is_string; +use function ltrim; +use function rtrim; use function sprintf; +use function str_contains; use function str_replace; -class TableErrorFormatter implements ErrorFormatter +final class TableErrorFormatter implements ErrorFormatter { public function __construct( private RelativePathHelper $relativePathHelper, + private SimpleRelativePathHelper $simpleRelativePathHelper, + private CiDetectedErrorFormatter $ciDetectedErrorFormatter, private bool $showTipsOfTheDay, private ?string $editorUrl, + private ?string $editorUrlTitle, ) { } @@ -31,6 +41,7 @@ public function formatErrors( Output $output, ): int { + $this->ciDetectedErrorFormatter->formatErrors($analysisResult, $output); $projectConfigFile = 'phpstan.neon'; if ($analysisResult->getProjectConfigFile() !== null) { $projectConfigFile = $this->relativePathHelper->getRelativePath($analysisResult->getProjectConfigFile()); @@ -40,6 +51,7 @@ public function formatErrors( if (!$analysisResult->hasErrors() && !$analysisResult->hasWarnings()) { $style->success('No errors'); + if ($this->showTipsOfTheDay) { if ($analysisResult->isDefaultLevelUsed()) { $output->writeLineFormatted('💡 Tip of the Day:'); @@ -69,18 +81,55 @@ public function formatErrors( $rows = []; foreach ($errors as $error) { $message = $error->getMessage(); + $filePath = $error->getTraitFilePath() ?? $error->getFilePath(); + if ($error->getIdentifier() !== null && $error->canBeIgnored()) { + $message .= "\n"; + $message .= '🪪 ' . $error->getIdentifier(); + } if ($error->getTip() !== null) { $tip = $error->getTip(); $tip = str_replace('%configurationFile%', $projectConfigFile, $tip); - $message .= "\n💡 " . $tip; + + $message .= "\n"; + if (str_contains($tip, "\n")) { + $lines = explode("\n", $tip); + foreach ($lines as $line) { + $message .= '💡 ' . ltrim($line, ' •') . "\n"; + } + $message = rtrim($message, "\n"); + } else { + $message .= '💡 ' . $tip; + } } if (is_string($this->editorUrl)) { - $editorFile = $error->getTraitFilePath() ?? $error->getFilePath(); - $url = str_replace(['%file%', '%line%'], [$editorFile, (string) $error->getLine()], $this->editorUrl); - $message .= "\n✏️ ' . $this->relativePathHelper->getRelativePath($editorFile) . ''; + $url = str_replace( + ['%file%', '%relFile%', '%line%'], + [$filePath, $this->simpleRelativePathHelper->getRelativePath($filePath), (string) $error->getLine()], + $this->editorUrl, + ); + + if (is_string($this->editorUrlTitle)) { + $title = str_replace( + ['%file%', '%relFile%', '%line%'], + [$filePath, $this->simpleRelativePathHelper->getRelativePath($filePath), (string) $error->getLine()], + $this->editorUrlTitle, + ); + } else { + $title = $this->relativePathHelper->getRelativePath($filePath); + } + + $message .= "\n✏️ ' . $title . ''; } + + if ( + $error->getIdentifier() !== null + && in_array($error->getIdentifier(), ['phpstan.type', 'phpstan.nativeType', 'phpstan.variable', 'phpstan.dumpType', 'phpstan.unknownExpectation'], true) + ) { + $message = '' . $message . ''; + } + $rows[] = [ - (string) $error->getLine(), + $this->formatLineNumber($error->getLine()), $message, ]; } @@ -89,12 +138,12 @@ public function formatErrors( } if (count($analysisResult->getNotFileSpecificErrors()) > 0) { - $style->table(['', 'Error'], array_map(static fn (string $error): array => ['', $error], $analysisResult->getNotFileSpecificErrors())); + $style->table(['', 'Error'], array_map(static fn (string $error): array => ['', OutputFormatter::escape($error)], $analysisResult->getNotFileSpecificErrors())); } $warningsCount = count($analysisResult->getWarnings()); if ($warningsCount > 0) { - $style->table(['', 'Warning'], array_map(static fn (string $warning): array => ['', $warning], $analysisResult->getWarnings())); + $style->table(['', 'Warning'], array_map(static fn (string $warning): array => ['', OutputFormatter::escape($warning)], $analysisResult->getWarnings())); } $finalMessage = sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount()); @@ -111,4 +160,18 @@ public function formatErrors( return $analysisResult->getTotalErrorsCount() > 0 ? 1 : 0; } + private function formatLineNumber(?int $lineNumber): string + { + if ($lineNumber === null) { + return ''; + } + + $isRunningInVSCodeTerminal = getenv('TERM_PROGRAM') === 'vscode'; + if ($isRunningInVSCodeTerminal) { + return ':' . $lineNumber; + } + + return (string) $lineNumber; + } + } diff --git a/src/Command/ErrorFormatter/TeamcityErrorFormatter.php b/src/Command/ErrorFormatter/TeamcityErrorFormatter.php index a6e6e2cf32..070896a051 100644 --- a/src/Command/ErrorFormatter/TeamcityErrorFormatter.php +++ b/src/Command/ErrorFormatter/TeamcityErrorFormatter.php @@ -10,12 +10,13 @@ use function count; use function is_string; use function preg_replace; +use function sprintf; use const PHP_EOL; /** * @see https://www.jetbrains.com/help/teamcity/build-script-interaction-with-teamcity.html#Reporting+Inspections */ -class TeamcityErrorFormatter implements ErrorFormatter +final class TeamcityErrorFormatter implements ErrorFormatter { public function __construct(private RelativePathHelper $relativePathHelper) @@ -41,9 +42,15 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in ]); foreach ($fileSpecificErrors as $fileSpecificError) { + $message = $fileSpecificError->getMessage(); + + if ($fileSpecificError->getIdentifier() !== null && $fileSpecificError->canBeIgnored()) { + $message .= sprintf(' (🪪 %s)', $fileSpecificError->getIdentifier()); + } + $result .= $this->createTeamcityLine('inspection', [ 'typeId' => 'phpstan', - 'message' => $fileSpecificError->getMessage(), + 'message' => $message, 'file' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()), 'line' => $fileSpecificError->getLine(), // additional attributes diff --git a/src/Command/ErrorsConsoleStyle.php b/src/Command/ErrorsConsoleStyle.php index 76954e735f..f953d40c49 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -18,7 +18,7 @@ use function wordwrap; use const DIRECTORY_SEPARATOR; -class ErrorsConsoleStyle extends SymfonyStyle +final class ErrorsConsoleStyle extends SymfonyStyle { public const OPTION_NO_PROGRESS = 'no-progress'; @@ -101,12 +101,26 @@ private function wrap(array $rows, int $terminalWidth, int $maxHeaderWidth): arr if (str_starts_with($columnRow, '✏️')) { continue; } - $columnRows[$k] = wordwrap( + $wrapped = wordwrap( $columnRow, $terminalWidth - $maxHeaderWidth - 5, - "\n", - true, ); + if (str_starts_with($columnRow, '💡 ')) { + $wrappedLines = explode("\n", $wrapped); + $newWrappedLines = []; + foreach ($wrappedLines as $l => $line) { + if ($l === 0) { + $newWrappedLines[] = $line; + continue; + } + + $newWrappedLines[] = ' ' . $line; + } + $columnRows[$k] = implode("\n", $newWrappedLines); + } else { + $columnRows[$k] = $wrapped; + } + } $rows[$i] = implode("\n", $columnRows); @@ -119,6 +133,11 @@ public function createProgressBar(int $max = 0): ProgressBar { $this->progressBar = parent::createProgressBar($max); + $format = $this->getProgressBarFormat(); + if ($format !== null) { + $this->progressBar->setFormat($format); + } + $ci = $this->isCiDetected(); $this->progressBar->setOverwrite(!$ci); @@ -136,6 +155,31 @@ public function createProgressBar(int $max = 0): ProgressBar return $this->progressBar; } + private function getProgressBarFormat(): ?string + { + switch ($this->getVerbosity()) { + case OutputInterface::VERBOSITY_NORMAL: + $formatName = ProgressBar::FORMAT_NORMAL; + break; + case OutputInterface::VERBOSITY_VERBOSE: + $formatName = ProgressBar::FORMAT_VERBOSE; + break; + case OutputInterface::VERBOSITY_VERY_VERBOSE: + case OutputInterface::VERBOSITY_DEBUG: + $formatName = ProgressBar::FORMAT_VERY_VERBOSE; + break; + default: + $formatName = null; + break; + } + + if ($formatName === null) { + return null; + } + + return ProgressBar::getFormatDefinition($formatName); + } + public function progressStart(int $max = 0): void { if (!$this->showProgress) { diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index 9d01812a39..1a9e64d7fe 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -8,36 +8,30 @@ 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\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\InternalError; 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\Internal\ComposerHelper; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\PhpDoc\StubFilesProvider; 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\Dns\Config\Config; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\EventLoop\StreamSelectLoop; use React\Http\Browser; -use React\Promise\CancellablePromiseInterface; -use React\Promise\ExtendedPromiseInterface; use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\Connector; @@ -48,66 +42,65 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Throwable; -use function Clue\React\Block\await; +use function array_merge; use function count; use function defined; use function escapeshellarg; use function fclose; use function fopen; use function fwrite; +use function get_class; use function getenv; +use function http_build_query; use function ini_get; -use function is_dir; use function is_file; -use function is_string; -use function min; -use function mkdir; use function parse_url; -use function React\Promise\resolve; +use function React\Async\await; use function sprintf; use function strlen; -use function strpos; use function unlink; +use const JSON_INVALID_UTF8_IGNORE; use const PHP_BINARY; use const PHP_URL_PORT; +use const PHP_VERSION_ID; -class FixerApplication +final class FixerApplication { - /** @var (ExtendedPromiseInterface&CancellablePromiseInterface)|null */ - private $processInProgress; + /** @var PromiseInterface|null */ + private PromiseInterface|null $processInProgress = null; - private ?string $fixerSuggestionId = null; + private bool $fileMonitorActive = true; /** * @param string[] $analysedPaths + * @param list $dnsServers + * @param string[] $composerAutoloaderProjectPaths + * @param string[] $allConfigFiles + * @param string[] $bootstrapFiles */ public function __construct( private FileMonitor $fileMonitor, - private ResultCacheManagerFactory $resultCacheManagerFactory, - private ResultCacheClearer $resultCacheClearer, private IgnoredErrorHelper $ignoredErrorHelper, - private CpuCoreCounter $cpuCoreCounter, - private Scheduler $scheduler, + private StubFilesProvider $stubFilesProvider, private array $analysedPaths, private string $currentWorkingDirectory, - private string $fixerTmpDir, - private int $maximumNumberOfProcesses, + private string $proTmpDir, + private array $dnsServers, + private array $composerAutoloaderProjectPaths, + private array $allConfigFiles, + private ?string $cliAutoloadFile, + private array $bootstrapFiles, + private ?string $editorUrl, + private string $usedLevel, ) { } - /** - * @param Error[] $fileSpecificErrors - * @param string[] $notFileSpecificErrors - */ public function run( ?string $projectConfigFile, - InceptionResult $inceptionResult, InputInterface $input, OutputInterface $output, - array $fileSpecificErrors, - array $notFileSpecificErrors, int $filesCount, string $mainScript, ): int @@ -117,121 +110,80 @@ public function run( /** @var string $serverAddress */ $serverAddress = $server->getAddress(); - /** @var int $serverPort */ + /** @var int<0, 65535> $serverPort */ $serverPort = parse_url(http://github.com/$serverAddress,%20PHP_URL_PORT); - $reanalyseProcessQueue = new RunnableQueue( - new class () implements RunnableQueueLogger { - - public function log(string $message): void - { - } - - }, - min($this->cpuCoreCounter->getNumberOfCpuCores(), $this->maximumNumberOfProcesses), - ); - - $server->on('connection', function (ConnectionInterface $connection) use ($loop, $projectConfigFile, $input, $output, $fileSpecificErrors, $notFileSpecificErrors, $mainScript, $filesCount, $reanalyseProcessQueue, $inceptionResult): void { + $server->on('connection', function (ConnectionInterface $connection) use ($loop, $projectConfigFile, $input, $output, $mainScript, $filesCount): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore); $encoder->write(['action' => 'initialData', 'data' => [ - 'fileSpecificErrors' => $fileSpecificErrors, - 'notFileSpecificErrors' => $notFileSpecificErrors, 'currentWorkingDirectory' => $this->currentWorkingDirectory, 'analysedPaths' => $this->analysedPaths, 'projectConfigFile' => $projectConfigFile, 'filesCount' => $filesCount, - 'phpstanVersion' => $this->getPhpstanVersion(), + 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), + 'editorUrl' => $this->editorUrl, + 'ruleLevel' => $this->usedLevel, ]]); $decoder->on('data', function (array $data) use ( - $loop, - $encoder, - $projectConfigFile, - $input, $output, - $mainScript, - $reanalyseProcessQueue, - $inceptionResult ): void { if ($data['action'] === 'webPort') { $output->writeln(sprintf('Open your web browser at: http://127.0.0.1:%d', $data['data']['port'])); $output->writeln('Press [Ctrl-C] to quit.'); return; } - if ($data['action'] === 'restoreResultCache') { - $this->fixerSuggestionId = $data['data']['fixerSuggestionId']; + if ($data['action'] === 'resumeFileMonitor') { + $this->fileMonitorActive = true; + return; } - if ($data['action'] !== 'reanalyse') { + if ($data['action'] === 'pauseFileMonitor') { + $this->fileMonitorActive = false; return; } + }); - $id = $data['id']; + $this->fileMonitor->initialize(array_merge( + $this->analysedPaths, + $this->getComposerLocks(), + $this->getComposerInstalled(), + $this->getExecutedFiles(), + $this->getStubFiles(), + $this->allConfigFiles, + )); - $this->reanalyseWithTmpFile( - $loop, - $inceptionResult, - $mainScript, - $reanalyseProcessQueue, - $projectConfigFile, - $data['data']['tmpFile'], - $data['data']['insteadOfFile'], - $data['data']['fixerSuggestionId'], - $input, - )->done(static function (string $output) use ($encoder, $id): void { - $encoder->write(['id' => $id, 'response' => Json::decode($output, Json::FORCE_ARRAY)]); - }, static function (Throwable $e) use ($encoder, $id, $output): void { - if ($e instanceof ProcessCrashedException) { - $output->writeln('Worker process exited: ' . $e->getMessage() . ''); - $encoder->write(['id' => $id, 'error' => $e->getMessage()]); - return; - } - if ($e instanceof ProcessCanceledException) { - $encoder->write(['id' => $id, 'error' => $e->getMessage()]); - return; - } - - $output->writeln('Unexpected error: ' . $e->getMessage() . ''); - $encoder->write(['id' => $id, 'error' => $e->getMessage()]); - }); - }); + $this->analyse( + $loop, + $mainScript, + $projectConfigFile, + $input, + $output, + $encoder, + ); - $this->fileMonitor->initialize($this->analysedPaths); - $this->monitorFileChanges($loop, function (FileMonitorResult $changes) use ($loop, $mainScript, $projectConfigFile, $input, $encoder, $output, $reanalyseProcessQueue, $inceptionResult): void { - $reanalyseProcessQueue->cancelAll(); + $this->monitorFileChanges($loop, function (FileMonitorResult $changes) use ($loop, $mainScript, $projectConfigFile, $input, $encoder, $output): void { if ($this->processInProgress !== null) { $this->processInProgress->cancel(); $this->processInProgress = null; - } else { - $encoder->write(['action' => 'analysisStart']); } - $this->reanalyseAfterFileChanges( + if (count($changes->getChangedFiles()) > 0) { + $encoder->write(['action' => 'changedFiles', 'data' => [ + 'paths' => $changes->getChangedFiles(), + ]]); + } + + $this->analyse( $loop, - $inceptionResult, $mainScript, $projectConfigFile, - $this->fixerSuggestionId, $input, - )->done(function (array $json) use ($encoder, $changes): void { - $this->processInProgress = null; - $this->fixerSuggestionId = null; - $encoder->write(['action' => 'analysisEnd', 'data' => [ - 'fileSpecificErrors' => $json['fileSpecificErrors'], - 'notFileSpecificErrors' => $json['notFileSpecificErrors'], - 'filesCount' => $changes->getTotalFilesCount(), - ]]); - $this->resultCacheClearer->clearTemporaryCaches(); - }, function (Throwable $e) use ($encoder, $output): void { - $this->processInProgress = null; - $this->fixerSuggestionId = null; - $output->writeln('Worker process exited: ' . $e->getMessage() . ''); - $encoder->write(['action' => 'analysisCrash', 'data' => [ - 'error' => $e->getMessage(), - ]]); - }); + $output, + $encoder, + ); }); }); @@ -242,7 +194,7 @@ public function log(string $message): void } $fixerProcess->start($loop); - $fixerProcess->on('exit', static function ($exitCode) use ($output, $loop): void { + $fixerProcess->on('exit', function ($exitCode) use ($output, $loop): void { $loop->stop(); if ($exitCode === null) { return; @@ -251,6 +203,7 @@ public function log(string $message): void return; } $output->writeln(sprintf('PHPStan Pro process exited with code %d.', $exitCode)); + @unlink($this->proTmpDir . '/phar-info.json'); }); $loop->run(); @@ -263,20 +216,21 @@ public function log(string $message): void */ private function getFixerProcess(OutputInterface $output, int $serverPort): Process { - if (!@mkdir($this->fixerTmpDir, 0777) && !is_dir($this->fixerTmpDir)) { - $output->writeln(sprintf('Cannot create a temp directory %s', $this->fixerTmpDir)); + try { + DirectoryCreator::ensureDirectoryExists($this->proTmpDir, 0777); + } catch (DirectoryCreatorException $e) { + $output->writeln($e->getMessage()); throw new FixerProcessException(); } - $pharPath = $this->fixerTmpDir . '/phpstan-fixer.phar'; - $infoPath = $this->fixerTmpDir . '/phar-info.json'; + $pharPath = $this->proTmpDir . '/phpstan-fixer.phar'; + $infoPath = $this->proTmpDir . '/phar-info.json'; try { $this->downloadPhar($output, $pharPath, $infoPath); } catch (RuntimeException $e) { if (!is_file($pharPath)) { - $output->writeln('Could not download the PHPStan Pro executable.'); - $output->writeln($e->getMessage()); + $this->printDownloadError($output, $e); throw new FixerProcessException(); } @@ -287,10 +241,11 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc try { $phar = new Phar($pharPath); - } catch (Throwable) { + } catch (Throwable $e) { @unlink($pharPath); @unlink($infoPath); $output->writeln('PHPStan Pro PHAR signature is corrupted.'); + $output->writeln(sprintf('%s: %s', get_class($e), $e->getMessage())); throw new FixerProcessException(); } @@ -299,11 +254,13 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc @unlink($pharPath); @unlink($infoPath); $output->writeln('PHPStan Pro PHAR signature is corrupted.'); + $output->writeln(sprintf('Wrong hash type: %s', $phar->getSignature()['hash_type'])); throw new FixerProcessException(); } $env = getenv(); + $env['PHPSTAN_PRO_TMP_DIR'] = $this->proTmpDir; $forcedPort = $_SERVER['PHPSTAN_PRO_WEB_PORT'] ?? null; if ($forcedPort !== null) { $env['PHPSTAN_PRO_WEB_PORT'] = $_SERVER['PHPSTAN_PRO_WEB_PORT']; @@ -315,7 +272,7 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc $output->writeln(sprintf(' -p 127.0.0.1:%d:%d', $_SERVER['PHPSTAN_PRO_WEB_PORT'], $_SERVER['PHPSTAN_PRO_WEB_PORT'])); $output->writeln('2) Map the temp directory to a persistent volume'); $output->writeln(' so that you don\'t have to log in every time:'); - $output->writeln(sprintf(' -v ~/.phpstan-pro:%s', $this->fixerTmpDir)); + $output->writeln(sprintf(' -v ~/.phpstan-pro:%s', $this->proTmpDir)); $output->writeln(''); } } else { @@ -331,12 +288,12 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc $output->writeln(' -p 127.0.0.1:11111:11111'); $output->writeln('4) Map the temp directory to a persistent volume'); $output->writeln(' so that you don\'t have to log in every time:'); - $output->writeln(sprintf(' -v ~/phpstan-pro:%s', $this->fixerTmpDir)); + $output->writeln(sprintf(' -v ~/phpstan-pro:%s', $this->proTmpDir)); $output->writeln(''); } } - return new Process(sprintf('%s -d memory_limit=%s %s --port %d', PHP_BINARY, escapeshellarg(ini_get('memory_limit')), escapeshellarg($pharPath), $serverPort), null, $env, []); + return new Process(sprintf('%s -d memory_limit=%s %s --port %d', escapeshellarg(PHP_BINARY), escapeshellarg(ini_get('memory_limit')), escapeshellarg($pharPath), $serverPort), null, $env, []); } private function downloadPhar( @@ -346,43 +303,47 @@ private function downloadPhar( ): void { $currentVersion = null; + $branch = '2.0.x'; if (is_file($pharPath) && is_file($infoPath)) { - /** @var array{version: string, date: string} $currentInfo */ + /** @var array{version: string, date: string, branch?: string} $currentInfo */ $currentInfo = Json::decode(FileReader::read($infoPath), Json::FORCE_ARRAY); $currentVersion = $currentInfo['version']; + $currentBranch = $currentInfo['branch'] ?? 'master'; $currentDate = DateTime::createFromFormat(DateTime::ATOM, $currentInfo['date']); if ($currentDate === false) { throw new ShouldNotHappenException(); } - if ((new DateTimeImmutable('', new DateTimeZone('UTC'))) <= $currentDate->modify('+24 hours')) { + if ( + $currentBranch === $branch + && (new DateTimeImmutable('', new DateTimeZone('UTC'))) <= $currentDate->modify('+24 hours') + ) { return; } $output->writeln('Checking if there\'s a new PHPStan Pro release...'); } - $loop = new StreamSelectLoop(); + $dnsConfig = new Config(); + $dnsConfig->nameservers = $this->dnsServers; + $client = new Browser( - $loop, new Connector( - $loop, [ 'timeout' => 5, 'tls' => [ 'cafile' => CaBundle::getBundledCaBundlePath(), ], - 'dns' => '1.1.1.1', + 'dns' => $dnsConfig, ], ), ); /** * @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); + $latestInfo = Json::decode((string) await($client->get(sprintf('https://fixer-download-api.phpstan.com/latest?%s', http_build_query(['phpVersion' => PHP_VERSION_ID, 'branch' => $branch]))))->getBody(), Json::FORCE_ARRAY); if ($currentVersion !== null && $latestInfo['version'] === $currentVersion) { - $this->writeInfoFile($infoPath, $latestInfo['version']); + $this->writeInfoFile($infoPath, $latestInfo['version'], $branch); $output->writeln('You\'re running the latest PHPStan Pro!'); return; } @@ -394,7 +355,7 @@ private function downloadPhar( 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 { + $client->requestStreaming('GET', $latestInfo['url'])->then(static function (ResponseInterface $response) use ($progressBar, $pharPathResource): void { $body = $response->getBody(); if (!$body instanceof ReadableStreamInterface) { throw new ShouldNotHappenException(); @@ -411,11 +372,11 @@ private function downloadPhar( fwrite($pharPathResource, $chunk); $progressBar->setProgress($bytes); }); - }, static function (Throwable $e) use ($output): void { - $output->writeln(sprintf('Could not download the PHPStan Pro executable: %s', $e->getMessage())); + }, function (Throwable $e) use ($output): void { + $this->printDownloadError($output, $e); }); - $loop->run(); + Loop::run(); fclose($pharPathResource); @@ -423,13 +384,27 @@ private function downloadPhar( $output->writeln(''); $output->writeln(''); - $this->writeInfoFile($infoPath, $latestInfo['version']); + $this->writeInfoFile($infoPath, $latestInfo['version'], $branch); } - private function writeInfoFile(string $infoPath, string $version): void + private function printDownloadError(OutputInterface $output, Throwable $e): void + { + $output->writeln(sprintf('Could not download the PHPStan Pro executable: %s', $e->getMessage())); + $output->writeln(''); + $output->writeln('Try different DNS servers in your configuration file:'); + $output->writeln(''); + $output->writeln('parameters:'); + $output->writeln("\tpro:"); + $output->writeln("\t\tdnsServers!:"); + $output->writeln("\t\t\t- '8.8.8.8'"); + $output->writeln(''); + } + + private function writeInfoFile(string $infoPath, string $version, string $branch): void { FileWriter::write($infoPath, Json::encode([ 'version' => $version, + 'branch' => $branch, 'date' => (new DateTimeImmutable('', new DateTimeZone('UTC')))->format(DateTime::ATOM), ])); } @@ -440,6 +415,14 @@ private function writeInfoFile(string $infoPath, string $version): void private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCallback): void { $callback = function () use (&$callback, $loop, $hasChangesCallback): void { + if (!$this->fileMonitorActive) { + $loop->addTimer(1.0, $callback); + return; + } + if ($this->processInProgress !== null) { + $loop->addTimer(1.0, $callback); + return; + } $changes = $this->fileMonitor->getChanges(); if ($changes->hasAnyChanges()) { @@ -451,131 +434,155 @@ private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCal $loop->addTimer(1.0, $callback); } - private function reanalyseWithTmpFile( + private function analyse( LoopInterface $loop, - InceptionResult $inceptionResult, string $mainScript, - RunnableQueue $runnableQueue, ?string $projectConfigFile, - string $tmpFile, - string $insteadOfFile, - string $fixerSuggestionId, InputInterface $input, - ): PromiseInterface + OutputInterface $output, + Encoder $phpstanFixerEncoder, + ): void { - $resultCacheManager = $this->resultCacheManagerFactory->create([$insteadOfFile => $tmpFile]); - [$inceptionFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $inceptionResult->getProjectConfigArray(), $inceptionResult->getErrorOutput()); - $schedule = $this->scheduler->scheduleWork($this->cpuCoreCounter->getNumberOfCpuCores(), $resultCache->getFilesToAnalyse()); + $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); + if (count($ignoredErrorHelperResult->getErrors()) > 0) { + throw new ShouldNotHappenException(); + } + + // TCP server for fixer:worker (TCP client) + $server = new TcpServer('127.0.0.1:0', $loop); + /** @var string $serverAddress */ + $serverAddress = $server->getAddress(); + /** @var int<0, 65535> $serverPort */ + $serverPort = parse_url(http://github.com/$serverAddress,%20PHP_URL_PORT); - $process = new ProcessPromise($loop, $fixerSuggestionId, ProcessHelper::getWorkerCommand( + $server->on('connection', static function (ConnectionInterface $connection) use ($phpstanFixerEncoder): void { + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); + $decoder->on('data', static function (array $data) use ($phpstanFixerEncoder): void { + $phpstanFixerEncoder->write($data); + }); + }); + + $process = new ProcessPromise($loop, 'changedFileAnalysis', ProcessHelper::getWorkerCommand( $mainScript, 'fixer:worker', $projectConfigFile, [ - '--tmp-file', - escapeshellarg($tmpFile), - '--instead-of', - escapeshellarg($insteadOfFile), - '--save-result-cache', - escapeshellarg($fixerSuggestionId), - '--allow-parallel', + '--server-port', + (string) $serverPort, ], $input, )); + $this->processInProgress = $process->run(); + + $this->processInProgress->then(function () use ($server): void { + $this->processInProgress = null; + $server->close(); + }, function (Throwable $e) use ($server, $phpstanFixerEncoder): void { + $this->processInProgress = null; + $server->close(); - return $runnableQueue->queue($process, $schedule->getNumberOfProcesses()); + if ($e instanceof ProcessCanceledException) { + return; + } + + if ($e instanceof ProcessCrashedException) { + $message = 'Analysis crashed'; + $traceAsString = $e->getMessage(); + $trace = []; + } else { + $message = $e->getMessage(); + $traceAsString = $e->getTraceAsString(); + $trace = InternalError::prepareTrace($e); + } + $phpstanFixerEncoder->write(['action' => 'analysisCrash', 'data' => [ + 'internalErrors' => [new InternalError( + $message, + 'running PHPStan Pro worker', + $trace, + $traceAsString, + false, + )], + ]]); + }); } - private function reanalyseAfterFileChanges( - LoopInterface $loop, - InceptionResult $inceptionResult, - string $mainScript, - ?string $projectConfigFile, - ?string $fixerSuggestionId, - InputInterface $input, - ): PromiseInterface + private function isDockerRunning(): bool { - $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); - if (count($ignoredErrorHelperResult->getErrors()) > 0) { - throw new ShouldNotHappenException(); + return is_file('/.dockerenv'); + } + + /** + * @return list + */ + private function getComposerLocks(): array + { + $locks = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { + $lockPath = $autoloadPath . '/composer.lock'; + if (!is_file($lockPath)) { + continue; + } + + $locks[] = $lockPath; } - $projectConfigArray = $inceptionResult->getProjectConfigArray(); - - $resultCacheManager = $this->resultCacheManagerFactory->create([]); - [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput(), $fixerSuggestionId); - if (count($resultCache->getFilesToAnalyse()) === 0) { - $result = $resultCacheManager->process( - new AnalyserResult([], [], [], [], false), - $resultCache, - $inceptionResult->getErrorOutput(), - false, - true, - )->getAnalyserResult(); - $intermediateErrors = $ignoredErrorHelperResult->process( - $result->getErrors(), - $isOnlyFiles, - $inceptionFiles, - count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit(), - ); - $finalFileSpecificErrors = []; - $finalNotFileSpecificErrors = []; - foreach ($intermediateErrors as $intermediateError) { - if (is_string($intermediateError)) { - $finalNotFileSpecificErrors[] = $intermediateError; - continue; - } + return $locks; + } - $finalFileSpecificErrors[] = $intermediateError; + /** + * @return list + */ + private function getComposerInstalled(): array + { + $files = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { + $composer = ComposerHelper::getComposerConfig($autoloadPath); + if ($composer === null) { + continue; } - return resolve([ - 'fileSpecificErrors' => $finalFileSpecificErrors, - 'notFileSpecificErrors' => $finalNotFileSpecificErrors, - ]); - } + $filePath = ComposerHelper::getVendorDirFromComposerConfig($autoloadPath, $composer) . '/composer/installed.php'; + if (!is_file($filePath)) { + continue; + } - $options = ['--save-result-cache', '--allow-parallel']; - if ($fixerSuggestionId !== null) { - $options[] = '--restore-result-cache'; - $options[] = $fixerSuggestionId; + $files[] = $filePath; } - $process = new ProcessPromise($loop, 'changedFileAnalysis', ProcessHelper::getWorkerCommand( - $mainScript, - 'fixer:worker', - $projectConfigFile, - $options, - $input, - )); - $this->processInProgress = $process->run(); - return $this->processInProgress->then(static fn (string $output): array => Json::decode($output, Json::FORCE_ARRAY)); + return $files; } - private function getPhpstanVersion(): string + /** + * @return list + */ + private function getExecutedFiles(): array { - try { - return PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); - } catch (OutOfBoundsException) { - return 'Version unknown'; + $files = []; + if ($this->cliAutoloadFile !== null) { + $files[] = $this->cliAutoloadFile; } - } - private function isDockerRunning(): bool - { - if (!is_file('/proc/1/cgroup')) { - return false; + foreach ($this->bootstrapFiles as $bootstrapFile) { + $files[] = $bootstrapFile; } - try { - $contents = FileReader::read('/proc/1/cgroup'); + return $files; + } - return strpos($contents, 'docker') !== false; - } catch (CouldNotReadFileException) { - return false; + /** + * @return list + */ + private function getStubFiles(): array + { + $stubFiles = []; + foreach ($this->stubFilesProvider->getProjectStubFiles() as $stubFile) { + $stubFiles[] = $stubFile; } + + return $stubFiles; } } diff --git a/src/Command/FixerProcessException.php b/src/Command/FixerProcessException.php index 9473f35716..c9e4097d58 100644 --- a/src/Command/FixerProcessException.php +++ b/src/Command/FixerProcessException.php @@ -4,7 +4,7 @@ use Exception; -class FixerProcessException extends Exception +final class FixerProcessException extends Exception { } diff --git a/src/Command/FixerWorkerCommand.php b/src/Command/FixerWorkerCommand.php index 72ec027415..70a7624453 100644 --- a/src/Command/FixerWorkerCommand.php +++ b/src/Command/FixerWorkerCommand.php @@ -2,23 +2,47 @@ namespace PHPStan\Command; -use Nette\Utils\Json; +use Clue\React\NDJson\Encoder; use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\IgnoredErrorHelper; +use PHPStan\Analyser\AnalyserResultFinalizer; +use PHPStan\Analyser\Error; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\Ignore\IgnoredErrorHelperResult; +use PHPStan\Analyser\InternalError; use PHPStan\Analyser\ResultCache\ResultCacheManager; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\DependencyInjection\Container; +use PHPStan\File\PathNotFoundException; +use PHPStan\Parallel\ParallelAnalyser; +use PHPStan\Parallel\Scheduler; +use PHPStan\Process\CpuCoreCounter; use PHPStan\ShouldNotHappenException; +use React\EventLoop\LoopInterface; +use React\EventLoop\StreamSelectLoop; +use React\Promise\PromiseInterface; +use React\Socket\ConnectionInterface; +use React\Socket\TcpConnector; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function array_diff; +use function array_key_exists; use function count; +use function filemtime; +use function in_array; use function is_array; use function is_bool; +use function is_file; use function is_string; +use function memory_get_peak_usage; +use function React\Promise\resolve; +use function sprintf; +use function usort; +use const JSON_INVALID_UTF8_IGNORE; -class FixerWorkerCommand extends Command +final class FixerWorkerCommand extends Command { private const NAME = 'fixer:worker'; @@ -43,13 +67,10 @@ protected function configure(): void new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), - new InputOption('tmp-file', null, InputOption::VALUE_REQUIRED), - new InputOption('instead-of', null, InputOption::VALUE_REQUIRED), - new InputOption('save-result-cache', null, InputOption::VALUE_OPTIONAL, '', false), - new InputOption('restore-result-cache', null, InputOption::VALUE_REQUIRED), - new InputOption('allow-parallel', null, InputOption::VALUE_NONE, 'Allow parallel analysis'), - ]); + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), + new InputOption('server-port', null, InputOption::VALUE_REQUIRED, 'Server port for FixerApplication'), + ]) + ->setHidden(true); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -60,7 +81,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $configuration = $input->getOption('configuration'); $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); $allowXdebug = $input->getOption('xdebug'); - $allowParallel = $input->getOption('allow-parallel'); + $serverPort = $input->getOption('server-port'); if ( !is_array($paths) @@ -69,37 +90,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int || (!is_string($configuration) && $configuration !== null) || (!is_string($level) && $level !== null) || (!is_bool($allowXdebug)) - || (!is_bool($allowParallel)) + || (!is_string($serverPort)) ) { throw new ShouldNotHappenException(); } - /** @var string|null $tmpFile */ - $tmpFile = $input->getOption('tmp-file'); - - /** @var string|null $insteadOfFile */ - $insteadOfFile = $input->getOption('instead-of'); - - /** @var false|string|null $saveResultCache */ - $saveResultCache = $input->getOption('save-result-cache'); - - /** @var string|null $restoreResultCache */ - $restoreResultCache = $input->getOption('restore-result-cache'); - if (is_string($tmpFile)) { - if (!is_string($insteadOfFile)) { - throw new ShouldNotHappenException(); - } - } elseif (is_string($insteadOfFile)) { - throw new ShouldNotHappenException(); - } elseif ($saveResultCache === false) { - throw new ShouldNotHappenException(); - } - - $singleReflectionFile = null; - if ($tmpFile !== null) { - $singleReflectionFile = $tmpFile; - } - try { $inceptionResult = CommandHelper::begin( $input, @@ -113,8 +108,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, - $singleReflectionFile, - $insteadOfFile, false, ); } catch (InceptionNotSuccessfulException) { @@ -130,129 +123,291 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new ShouldNotHappenException(); } - /** @var AnalyserRunner $analyserRunner */ - $analyserRunner = $container->getByType(AnalyserRunner::class); - - $fileReplacements = []; - if ($insteadOfFile !== null && $tmpFile !== null) { - $fileReplacements = [$insteadOfFile => $tmpFile]; - } - /** @var ResultCacheManager $resultCacheManager */ - $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create($fileReplacements); - $projectConfigArray = $inceptionResult->getProjectConfigArray(); - [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput(), $restoreResultCache); - - $intermediateAnalyserResult = $analyserRunner->runAnalyser( - $resultCache->getFilesToAnalyse(), - $inceptionFiles, - null, - null, - false, - $allowParallel, - $configuration, - $tmpFile, - $insteadOfFile, - $input, - ); - $result = $resultCacheManager->process( - $this->switchTmpFileInAnalyserResult($intermediateAnalyserResult, $tmpFile, $insteadOfFile), - $resultCache, - $inceptionResult->getErrorOutput(), - false, - is_string($saveResultCache) ? $saveResultCache : $saveResultCache === null, - )->getAnalyserResult(); - - $intermediateErrors = $ignoredErrorHelperResult->process( - $result->getErrors(), - $isOnlyFiles, - $inceptionFiles, - count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit(), - ); - $finalFileSpecificErrors = []; - $finalNotFileSpecificErrors = []; - foreach ($intermediateErrors as $intermediateError) { - if (is_string($intermediateError)) { - $finalNotFileSpecificErrors[] = $intermediateError; - continue; + $loop = new StreamSelectLoop(); + $tcpConnector = new TcpConnector($loop); + $tcpConnector->connect(sprintf('127.0.0.1:%d', $serverPort))->then(function (ConnectionInterface $connection) use ($container, $inceptionResult, $configuration, $input, $ignoredErrorHelperResult, $loop): void { + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $out = new Encoder($connection, $jsonInvalidUtf8Ignore); + //$in = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); + + /** @var ResultCacheManager $resultCacheManager */ + $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create(); + $projectConfigArray = $inceptionResult->getProjectConfigArray(); + + /** @var AnalyserResultFinalizer $analyserResultFinalizer */ + $analyserResultFinalizer = $container->getByType(AnalyserResultFinalizer::class); + + try { + [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); + } catch (PathNotFoundException | InceptionNotSuccessfulException) { + throw new ShouldNotHappenException(); } - $finalFileSpecificErrors[] = $intermediateError; - } + $out->write([ + 'action' => 'analysisStart', + 'result' => [ + 'analysedFiles' => $inceptionFiles, + ], + ]); - $output->writeln(Json::encode([ - 'fileSpecificErrors' => $finalFileSpecificErrors, - 'notFileSpecificErrors' => $finalNotFileSpecificErrors, - ]), OutputInterface::OUTPUT_RAW); + $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput()); - return 0; - } + $errorsFromResultCacheTmp = $resultCache->getErrors(); + $locallyIgnoredErrorsFromResultCacheTmp = $resultCache->getLocallyIgnoredErrors(); + foreach ($resultCache->getFilesToAnalyse() as $fileToAnalyse) { + unset($errorsFromResultCacheTmp[$fileToAnalyse]); + unset($locallyIgnoredErrorsFromResultCacheTmp[$fileToAnalyse]); + } - private function switchTmpFileInAnalyserResult( - AnalyserResult $analyserResult, - ?string $insteadOfFile, - ?string $tmpFile, - ): AnalyserResult - { - $fileSpecificErrors = []; - foreach ($analyserResult->getErrors() as $error) { - if ( - $tmpFile !== null - && $insteadOfFile !== null - ) { - if ($error->getFilePath() === $insteadOfFile) { - $error = $error->changeFilePath($tmpFile); + $errorsFromResultCache = []; + foreach ($errorsFromResultCacheTmp as $errorsByFile) { + foreach ($errorsByFile as $error) { + $errorsFromResultCache[] = $error; } - if ($error->getTraitFilePath() === $insteadOfFile) { - $error = $error->changeTraitFilePath($tmpFile); + } + + [$errorsFromResultCache, $ignoredErrorsFromResultCache] = $this->filterErrors($errorsFromResultCache, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, false); + + foreach ($locallyIgnoredErrorsFromResultCacheTmp as $locallyIgnoredErrors) { + foreach ($locallyIgnoredErrors as $locallyIgnoredError) { + $ignoredErrorsFromResultCache[] = [$locallyIgnoredError, null]; } } - $fileSpecificErrors[] = $error; - } + $out->write([ + 'action' => 'analysisStream', + 'result' => [ + 'errors' => $errorsFromResultCache, + 'ignoredErrors' => $ignoredErrorsFromResultCache, + 'analysedFiles' => array_diff($inceptionFiles, $resultCache->getFilesToAnalyse()), + ], + ]); + + $filesToAnalyse = $resultCache->getFilesToAnalyse(); + usort($filesToAnalyse, static function (string $a, string $b): int { + $aTime = @filemtime($a); + if ($aTime === false) { + return 1; + } + + $bTime = @filemtime($b); + if ($bTime === false) { + return -1; + } + + // files are sorted from the oldest + // because ParallelAnalyser reverses the scheduler jobs to do the smallest + // jobs first + return $aTime <=> $bTime; + }); + + $this->runAnalyser( + $loop, + $container, + $filesToAnalyse, + $configuration, + $input, + function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use ($out, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles): void { + $internalErrors = []; + foreach ($errors as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { + continue; + } + + $internalErrors[] = $this->transformErrorIntoInternalError($fileSpecificError); + } - $dependencies = null; - if ($analyserResult->getDependencies() !== null) { - $dependencies = []; - foreach ($analyserResult->getDependencies() as $dependencyFile => $dependentFiles) { - $new = []; - foreach ($dependentFiles as $file) { - if ($file === $insteadOfFile && $tmpFile !== null) { - $new[] = $tmpFile; + if (count($internalErrors) > 0) { + $out->write(['action' => 'analysisCrash', 'data' => [ + 'internalErrors' => $internalErrors, + ]]); + return; + } + + [$errors, $ignoredErrors] = $this->filterErrors($errors, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, false); + foreach ($locallyIgnoredErrors as $locallyIgnoredError) { + $ignoredErrors[] = [$locallyIgnoredError, null]; + } + $out->write([ + 'action' => 'analysisStream', + 'result' => [ + 'errors' => $errors, + 'ignoredErrors' => $ignoredErrors, + 'analysedFiles' => $analysedFiles, + ], + ]); + }, + )->then(function (AnalyserResult $intermediateAnalyserResult) use ($analyserResultFinalizer, $resultCacheManager, $resultCache, $inceptionResult, $isOnlyFiles, $ignoredErrorHelperResult, $inceptionFiles, $out): void { + $analyserResult = $resultCacheManager->process( + $intermediateAnalyserResult, + $resultCache, + $inceptionResult->getErrorOutput(), + false, + true, + )->getAnalyserResult(); + $finalizerResult = $analyserResultFinalizer->finalize($analyserResult, $isOnlyFiles, false); + + $internalErrors = []; + foreach ($finalizerResult->getAnalyserResult()->getInternalErrors() as $internalError) { + $internalErrors[] = new InternalError( + $internalError->getTraceAsString() !== null ? sprintf('Internal error: %s', $internalError->getMessage()) : $internalError->getMessage(), + $internalError->getContextDescription(), + $internalError->getTrace(), + $internalError->getTraceAsString(), + $internalError->shouldReportBug(), + ); + } + + foreach ($finalizerResult->getAnalyserResult()->getUnorderedErrors() as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { continue; } - $new[] = $file; + $internalErrors[] = $this->transformErrorIntoInternalError($fileSpecificError); } - $key = $dependencyFile; - if ($key === $insteadOfFile && $tmpFile !== null) { - $key = $tmpFile; + $hasInternalErrors = count($internalErrors) > 0 || $finalizerResult->getAnalyserResult()->hasReachedInternalErrorsCountLimit(); + + if ($hasInternalErrors) { + $out->write(['action' => 'analysisCrash', 'data' => [ + 'internalErrors' => count($internalErrors) > 0 ? $internalErrors : [ + new InternalError( + 'Internal error occurred', + 'running analyser in PHPStan Pro worker', + [], + null, + false, + ), + ], + ]]); } - $dependencies[$key] = $new; - } + [$collectorErrors, $ignoredCollectorErrors] = $this->filterErrors($finalizerResult->getCollectorErrors(), $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, $hasInternalErrors); + foreach ($finalizerResult->getLocallyIgnoredCollectorErrors() as $locallyIgnoredCollectorError) { + $ignoredCollectorErrors[] = [$locallyIgnoredCollectorError, null]; + } + $out->write([ + 'action' => 'analysisStream', + 'result' => [ + 'errors' => $collectorErrors, + 'ignoredErrors' => $ignoredCollectorErrors, + 'analysedFiles' => [], + ], + ]); + + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process( + $finalizerResult->getErrors(), + $isOnlyFiles, + $inceptionFiles, + $hasInternalErrors, + ); + $ignoreFileErrors = []; + foreach ($ignoredErrorHelperProcessedResult->getNotIgnoredErrors() as $error) { + if ($error->getIdentifier() === null) { + continue; + } + if (!in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched', 'ignore.unmatchedLine', 'ignore.unmatchedIdentifier'], true)) { + continue; + } + $ignoreFileErrors[] = $error; + } + + $out->end([ + 'action' => 'analysisEnd', + 'result' => [ + 'ignoreFileErrors' => $ignoreFileErrors, + 'ignoreNotFileErrors' => $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages(), + ], + ]); + }); + }); + $loop->run(); + + return 0; + } + + private function transformErrorIntoInternalError(Error $error): InternalError + { + $message = $error->getMessage(); + $metadata = $error->getMetadata(); + if ( + $error->getIdentifier() === 'phpstan.internal' + && array_key_exists(InternalError::STACK_TRACE_AS_STRING_METADATA_KEY, $metadata) + ) { + $message = sprintf('Internal error: %s', $message); } - $exportedNodes = []; - foreach ($analyserResult->getExportedNodes() as $file => $fileExportedNodes) { - if ( - $tmpFile !== null - && $insteadOfFile !== null - && $file === $insteadOfFile - ) { - $file = $tmpFile; + return new InternalError( + $message, + sprintf('analysing file %s', $error->getTraitFilePath() ?? $error->getFilePath()), + $metadata[InternalError::STACK_TRACE_METADATA_KEY] ?? [], + $metadata[InternalError::STACK_TRACE_AS_STRING_METADATA_KEY] ?? null, + true, + ); + } + + /** + * @param string[] $inceptionFiles + * @param array $errors + * @return array{list, list} + */ + private function filterErrors(array $errors, IgnoredErrorHelperResult $ignoredErrorHelperResult, bool $onlyFiles, array $inceptionFiles, bool $hasInternalErrors): array + { + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($errors, $onlyFiles, $inceptionFiles, $hasInternalErrors); + $finalErrors = []; + foreach ($ignoredErrorHelperProcessedResult->getNotIgnoredErrors() as $error) { + if ($error->getIdentifier() === null) { + $finalErrors[] = $error; + continue; } + if (in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched'], true)) { + continue; + } + $finalErrors[] = $error; + } + + return [ + $finalErrors, + $ignoredErrorHelperProcessedResult->getIgnoredErrors(), + ]; + } - $exportedNodes[$file] = $fileExportedNodes; + /** + * @param string[] $files + * @param callable(list, list, string[]): void $onFileAnalysisHandler + * @return PromiseInterface + */ + private function runAnalyser(LoopInterface $loop, Container $container, array $files, ?string $configuration, InputInterface $input, callable $onFileAnalysisHandler): PromiseInterface + { + /** @var ParallelAnalyser $parallelAnalyser */ + $parallelAnalyser = $container->getByType(ParallelAnalyser::class); + $filesCount = count($files); + if ($filesCount === 0) { + return resolve(new AnalyserResult([], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true))); + } + + /** @var Scheduler $scheduler */ + $scheduler = $container->getByType(Scheduler::class); + + /** @var CpuCoreCounter $cpuCoreCounter */ + $cpuCoreCounter = $container->getByType(CpuCoreCounter::class); + + $schedule = $scheduler->scheduleWork($cpuCoreCounter->getNumberOfCpuCores(), $files); + $mainScript = null; + if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { + $mainScript = $_SERVER['argv'][0]; } - return new AnalyserResult( - $fileSpecificErrors, - $analyserResult->getInternalErrors(), - $dependencies, - $exportedNodes, - $analyserResult->hasReachedInternalErrorsCountLimit(), + return $parallelAnalyser->analyse( + $loop, + $schedule, + $mainScript, + null, + $configuration, + $input, + $onFileAnalysisHandler, ); } diff --git a/src/Command/IgnoredRegexValidator.php b/src/Command/IgnoredRegexValidator.php index 59efbf03b5..4340e0bd99 100644 --- a/src/Command/IgnoredRegexValidator.php +++ b/src/Command/IgnoredRegexValidator.php @@ -12,11 +12,10 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use function count; -use function strpos; use function strrpos; use function substr; -class IgnoredRegexValidator +final class IgnoredRegexValidator { public function __construct( @@ -33,19 +32,17 @@ public function validate(string $regex): IgnoredRegexValidatorResult try { /** @var TreeNode $ast */ $ast = $this->parser->parse($regex); - } catch (Exception $e) { - if (strpos($e->getMessage(), 'Unexpected token "|" (alternation) at line 1') === 0) { - return new IgnoredRegexValidatorResult([], false, true, '||', '\|\|'); - } - if ( - strpos($regex, '()') !== false - && strpos($e->getMessage(), 'Unexpected token ")" (_capturing) at line 1') === 0 - ) { - return new IgnoredRegexValidatorResult([], false, true, '()', '\(\)'); - } + } catch (Exception) { return new IgnoredRegexValidatorResult([], false, false); } + if (Strings::match($regex, '~(?getIgnoredTypes($ast), $this->hasAnchorsInTheMiddle($ast), @@ -86,15 +83,17 @@ private function getIgnoredTypes(TreeNode $ast): array continue; } - if ($type->describe(VerbosityLevel::typeOnly()) !== $matches[1]) { + if ($type instanceof ObjectType) { continue; } - if ($type instanceof ObjectType) { + $typeDescription = $type->describe(VerbosityLevel::typeOnly()); + + if ($typeDescription !== $matches[1]) { continue; } - $types[$type->describe(VerbosityLevel::typeOnly())] = $text; + $types[$typeDescription] = $text; } return $types; diff --git a/src/Command/IgnoredRegexValidatorResult.php b/src/Command/IgnoredRegexValidatorResult.php index fcda6a015b..0906a3c84b 100644 --- a/src/Command/IgnoredRegexValidatorResult.php +++ b/src/Command/IgnoredRegexValidatorResult.php @@ -2,7 +2,7 @@ namespace PHPStan\Command; -class IgnoredRegexValidatorResult +final class IgnoredRegexValidatorResult { /** diff --git a/src/Command/InceptionNotSuccessfulException.php b/src/Command/InceptionNotSuccessfulException.php index f17c3e08ec..5cbcf4425e 100644 --- a/src/Command/InceptionNotSuccessfulException.php +++ b/src/Command/InceptionNotSuccessfulException.php @@ -4,7 +4,7 @@ use Exception; -class InceptionNotSuccessfulException extends Exception +final class InceptionNotSuccessfulException extends Exception { } diff --git a/src/Command/InceptionResult.php b/src/Command/InceptionResult.php index 8a0010c8c2..fc6056eccb 100644 --- a/src/Command/InceptionResult.php +++ b/src/Command/InceptionResult.php @@ -3,11 +3,17 @@ namespace PHPStan\Command; use PHPStan\DependencyInjection\Container; +use PHPStan\File\PathNotFoundException; use PHPStan\Internal\BytesHelper; +use function floor; +use function implode; +use function max; use function memory_get_peak_usage; +use function microtime; +use function round; use function sprintf; -class InceptionResult +final class InceptionResult { /** @var callable(): (array{string[], bool}) */ @@ -32,12 +38,15 @@ public function __construct( } /** + * @throws InceptionNotSuccessfulException + * @throws PathNotFoundException * @return array{string[], bool} */ public function getFiles(): array { $callback = $this->filesCallback; + /** @throws InceptionNotSuccessfulException|PathNotFoundException */ return $callback(); } @@ -79,13 +88,40 @@ public function getGenerateBaselineFile(): ?string return $this->generateBaselineFile; } - public function handleReturn(int $exitCode): int + public function handleReturn(int $exitCode, ?int $peakMemoryUsageBytes, float $analysisStartTime): int { if ($this->getErrorOutput()->isVerbose()) { - $this->getErrorOutput()->writeLineFormatted(sprintf('Used memory: %s', BytesHelper::bytes(memory_get_peak_usage(true)))); + $this->getErrorOutput()->writeLineFormatted(sprintf( + 'Elapsed time: %s', + $this->formatDuration((int) round(microtime(true) - $analysisStartTime)), + )); + } + + if ($peakMemoryUsageBytes !== null && $this->getErrorOutput()->isVerbose()) { + $this->getErrorOutput()->writeLineFormatted(sprintf( + 'Used memory: %s', + BytesHelper::bytes(max(memory_get_peak_usage(true), $peakMemoryUsageBytes)), + )); } return $exitCode; } + private function formatDuration(int $seconds): string + { + $minutes = (int) floor($seconds / 60); + $remainingSeconds = $seconds % 60; + + $result = []; + if ($minutes > 0) { + $result[] = $minutes . ' minute' . ($minutes > 1 ? 's' : ''); + } + + if ($remainingSeconds > 0) { + $result[] = $remainingSeconds . ' second' . ($remainingSeconds > 1 ? 's' : ''); + } + + return implode(' ', $result); + } + } diff --git a/src/Command/Output.php b/src/Command/Output.php index 34b26c6c18..b0efcd648a 100644 --- a/src/Command/Output.php +++ b/src/Command/Output.php @@ -16,6 +16,10 @@ public function getStyle(): OutputStyle; public function isVerbose(): bool; + public function isVeryVerbose(): bool; + public function isDebug(): bool; + public function isDecorated(): bool; + } diff --git a/src/Command/Symfony/SymfonyOutput.php b/src/Command/Symfony/SymfonyOutput.php index 5e1a46273a..2d66f11a38 100644 --- a/src/Command/Symfony/SymfonyOutput.php +++ b/src/Command/Symfony/SymfonyOutput.php @@ -9,7 +9,7 @@ /** * @internal */ -class SymfonyOutput implements Output +final class SymfonyOutput implements Output { public function __construct( @@ -44,9 +44,19 @@ public function isVerbose(): bool return $this->symfonyOutput->isVerbose(); } + public function isVeryVerbose(): bool + { + return $this->symfonyOutput->isVeryVerbose(); + } + public function isDebug(): bool { return $this->symfonyOutput->isDebug(); } + public function isDecorated(): bool + { + return $this->symfonyOutput->isDecorated(); + } + } diff --git a/src/Command/Symfony/SymfonyStyle.php b/src/Command/Symfony/SymfonyStyle.php index 99ed87ec33..e8782a5f59 100644 --- a/src/Command/Symfony/SymfonyStyle.php +++ b/src/Command/Symfony/SymfonyStyle.php @@ -8,7 +8,7 @@ /** * @internal */ -class SymfonyStyle implements OutputStyle +final class SymfonyStyle implements OutputStyle { public function __construct(private StyleInterface $symfonyStyle) diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index c55d30bd62..27fdb1ce80 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -5,10 +5,12 @@ use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\InternalError; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\DependencyInjection\Container; use PHPStan\File\PathNotFoundException; -use PHPStan\Rules\Registry; +use PHPStan\Rules\Registry as RuleRegistry; use PHPStan\ShouldNotHappenException; use React\EventLoop\StreamSelectLoop; use React\Socket\ConnectionInterface; @@ -22,16 +24,15 @@ use Symfony\Component\Console\Output\OutputInterface; use Throwable; use function array_fill_keys; -use function array_filter; -use function array_values; -use function count; +use function array_merge; use function defined; use function is_array; use function is_bool; use function is_string; +use function memory_get_peak_usage; use function sprintf; -class WorkerCommand extends Command +final class WorkerCommand extends Command { private const NAME = 'worker'; @@ -58,12 +59,11 @@ protected function configure(): void new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), new InputOption('port', null, InputOption::VALUE_REQUIRED), new InputOption('identifier', null, InputOption::VALUE_REQUIRED), - new InputOption('tmp-file', null, InputOption::VALUE_REQUIRED), - new InputOption('instead-of', null, InputOption::VALUE_REQUIRED), - ]); + ]) + ->setHidden(true); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -90,17 +90,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new ShouldNotHappenException(); } - /** @var string|null $tmpFile */ - $tmpFile = $input->getOption('tmp-file'); - - /** @var string|null $insteadOfFile */ - $insteadOfFile = $input->getOption('instead-of'); - - $singleReflectionFile = null; - if ($tmpFile !== null) { - $singleReflectionFile = $tmpFile; - } - try { $inceptionResult = CommandHelper::begin( $input, @@ -114,8 +103,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, - $singleReflectionFile, - null, false, ); } catch (InceptionNotSuccessfulException $e) { @@ -127,27 +114,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { [$analysedFiles] = $inceptionResult->getFiles(); - $analysedFiles = $this->switchTmpFile($analysedFiles, $insteadOfFile, $tmpFile); } catch (PathNotFoundException $e) { $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; + } catch (InceptionNotSuccessfulException) { + return 1; } - /** @var NodeScopeResolver $nodeScopeResolver */ $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); $nodeScopeResolver->setAnalysedFiles($analysedFiles); $analysedFiles = array_fill_keys($analysedFiles, true); - $tcpConector = new TcpConnector($loop); - $tcpConector->connect(sprintf('127.0.0.1:%d', $port))->done(function (ConnectionInterface $connection) use ($container, $identifier, $output, $analysedFiles, $tmpFile, $insteadOfFile): void { + $tcpConnector = new TcpConnector($loop); + $tcpConnector->connect(sprintf('127.0.0.1:%d', $port))->then(function (ConnectionInterface $connection) use ($container, $identifier, $output, $analysedFiles): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable $out = new Encoder($connection, $jsonInvalidUtf8Ignore); $in = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, $container->getParameter('parallel')['buffer']); $out->write(['action' => 'hello', 'identifier' => $identifier]); - $this->runWorker($container, $out, $in, $output, $analysedFiles, $tmpFile, $insteadOfFile); + $this->runWorker($container, $out, $in, $output, $analysedFiles); }); $loop->run(); @@ -168,8 +155,6 @@ private function runWorker( ReadableStreamInterface $in, OutputInterface $output, array $analysedFiles, - ?string $tmpFile, - ?string $insteadOfFile, ): void { $handleError = function (Throwable $error) use ($out, $output): void { @@ -178,20 +163,36 @@ private function runWorker( $out->write([ 'action' => 'result', 'result' => [ - 'errors' => [$error->getMessage()], + 'errors' => [], + 'internalErrors' => [ + new InternalError( + $error->getMessage(), + 'communicating with main process in parallel worker', + InternalError::prepareTrace($error), + $error->getTraceAsString(), + true, + ), + ], + 'filteredPhpErrors' => [], + 'allPhpErrors' => [], + 'locallyIgnoredErrors' => [], + 'linesToIgnore' => [], + 'unmatchedLineIgnores' => [], + 'collectedData' => [], + 'memoryUsage' => memory_get_peak_usage(true), 'dependencies' => [], - 'filesCount' => 0, + 'exportedNodes' => [], + 'files' => [], 'internalErrorsCount' => 1, ], ]); $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, $output): void { + $ruleRegistry = $container->getByType(RuleRegistry::class); + $collectorRegistry = $container->getByType(CollectorRegistry::class); + $in->on('data', static function (array $json) use ($fileAnalyser, $ruleRegistry, $collectorRegistry, $out, $analysedFiles): void { $action = $json['action']; if ($action !== 'analyse') { return; @@ -200,33 +201,47 @@ private function runWorker( $internalErrorsCount = 0; $files = $json['files']; $errors = []; + $internalErrors = []; + $filteredPhpErrors = []; + $allPhpErrors = []; + $locallyIgnoredErrors = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; + $collectedData = []; $dependencies = []; $exportedNodes = []; foreach ($files as $file) { try { - if ($file === $insteadOfFile) { - $file = $tmpFile; - } - $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $registry, null); + $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $ruleRegistry, $collectorRegistry, null); $fileErrors = $fileAnalyserResult->getErrors(); + $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); + $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); + $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore(); + $unmatchedLineIgnores[$file] = $fileAnalyserResult->getUnmatchedLineIgnores(); $dependencies[$file] = $fileAnalyserResult->getDependencies(); $exportedNodes[$file] = $fileAnalyserResult->getExportedNodes(); foreach ($fileErrors as $fileError) { $errors[] = $fileError; } + foreach ($fileAnalyserResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { + $locallyIgnoredErrors[] = $locallyIgnoredError; + } + foreach ($fileAnalyserResult->getCollectedData() as $collectedFile => $dataPerCollector) { + foreach ($dataPerCollector as $collectorType => $collectorData) { + foreach ($collectorData as $data) { + $collectedData[$collectedFile][$collectorType][] = $data; + } + } + } } catch (Throwable $t) { - $this->errorCount++; $internalErrorsCount++; - $internalErrorMessage = sprintf('Internal error: %s in file %s', $t->getMessage(), $file); - - $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; + $internalErrors[] = new InternalError( + $t->getMessage(), + sprintf('analysing file %s', $file), + InternalError::prepareTrace($t), + $t->getTraceAsString(), + true, + ); } } @@ -234,36 +249,21 @@ private function runWorker( 'action' => 'result', 'result' => [ 'errors' => $errors, + 'internalErrors' => $internalErrors, + 'filteredPhpErrors' => $filteredPhpErrors, + 'allPhpErrors' => $allPhpErrors, + 'locallyIgnoredErrors' => $locallyIgnoredErrors, + 'linesToIgnore' => $linesToIgnore, + 'unmatchedLineIgnores' => $unmatchedLineIgnores, + 'collectedData' => $collectedData, + 'memoryUsage' => memory_get_peak_usage(true), 'dependencies' => $dependencies, 'exportedNodes' => $exportedNodes, - 'filesCount' => count($files), + 'files' => $files, 'internalErrorsCount' => $internalErrorsCount, ]]); }); $in->on('error', $handleError); } - /** - * @param string[] $analysedFiles - * @return string[] - */ - private function switchTmpFile( - array $analysedFiles, - ?string $insteadOfFile, - ?string $tmpFile, - ): array - { - $analysedFiles = array_values(array_filter($analysedFiles, static function (string $file) use ($insteadOfFile): bool { - if ($insteadOfFile === null) { - return true; - } - return $file !== $insteadOfFile; - })); - if ($tmpFile !== null) { - $analysedFiles[] = $tmpFile; - } - - return $analysedFiles; - } - } diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 8c650dd8b3..77a4f957fe 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -12,26 +12,28 @@ use PHPStan\Broker\ClassNotFoundException; use PHPStan\Broker\FunctionNotFoundException; use PHPStan\File\FileHelper; +use PHPStan\Node\ClassPropertyNode; use PHPStan\Node\InClassMethodNode; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ClosureType; -use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Type; use function array_merge; use function count; -class DependencyResolver +final class DependencyResolver { public function __construct( private FileHelper $fileHelper, private ReflectionProvider $reflectionProvider, private ExportedNodeResolver $exportedNodeResolver, + private FileTypeMapper $fileTypeMapper, ) { } @@ -41,6 +43,9 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $dependenciesReflections = []; if ($node instanceof Node\Stmt\Class_) { + if (isset($node->namespacedName)) { + $this->addClassToDependencies($node->namespacedName->toString(), $dependenciesReflections); + } if ($node->extends !== null) { $this->addClassToDependencies($node->extends->toString(), $dependenciesReflections); } @@ -48,51 +53,108 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($className->toString(), $dependenciesReflections); } } elseif ($node instanceof Node\Stmt\Interface_) { + if ($node->namespacedName !== null) { + $this->addClassToDependencies($node->namespacedName->toString(), $dependenciesReflections); + } foreach ($node->extends as $className) { $this->addClassToDependencies($className->toString(), $dependenciesReflections); } } elseif ($node instanceof Node\Stmt\Enum_) { + if ($node->namespacedName !== null) { + $this->addClassToDependencies($node->namespacedName->toString(), $dependenciesReflections); + } 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 ParametersAcceptorWithPhpDocs) { - $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + $nativeMethod = $node->getMethodReflection(); + $this->extractThrowType($nativeMethod->getThrowType(), $dependenciesReflections); + $this->extractFromParametersAcceptor($nativeMethod, $dependenciesReflections); + foreach ($nativeMethod->getAsserts()->getAll() as $assertTag) { + foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + foreach ($assertTag->getOriginalType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($nativeMethod->getSelfOutType() !== null) { + foreach ($nativeMethod->getSelfOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } elseif ($node instanceof ClassPropertyNode) { + $nativeType = $node->getNativeType(); + if ($nativeType !== null) { + foreach ($nativeType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + $phpDocType = $node->getPhpDocType(); + if ($phpDocType !== null) { + foreach ($phpDocType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } } elseif ($node instanceof InFunctionNode) { - $functionReflection = $scope->getFunction(); - if ($functionReflection !== null) { - $this->extractThrowType($functionReflection->getThrowType(), $dependenciesReflections); - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); + $functionReflection = $node->getFunctionReflection(); + $this->extractThrowType($functionReflection->getThrowType(), $dependenciesReflections); - if ($parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { - $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + $this->extractFromParametersAcceptor($functionReflection, $dependenciesReflections); + foreach ($functionReflection->getAsserts()->getAll() as $assertTag) { + foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } - } elseif ($node instanceof Closure) { - /** @var ClosureType $closureType */ - $closureType = $scope->getType($node); - foreach ($closureType->getParameters() as $parameter) { - $referencedClasses = $parameter->getType()->getReferencedClasses(); - foreach ($referencedClasses as $referencedClass) { + foreach ($assertTag->getOriginalType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } + } elseif ($node instanceof Closure || $node instanceof Node\Expr\ArrowFunction) { + $closureType = $scope->getType($node); + if ($closureType instanceof ClosureType) { + foreach ($closureType->getParameters() as $parameter) { + $referencedClasses = $parameter->getType()->getReferencedClasses(); + foreach ($referencedClasses as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } - $returnTypeReferencedClasses = $closureType->getReturnType()->getReferencedClasses(); - foreach ($returnTypeReferencedClasses as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + $returnTypeReferencedClasses = $closureType->getReturnType()->getReferencedClasses(); + foreach ($returnTypeReferencedClasses as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } } elseif ($node instanceof Node\Expr\FuncCall) { $functionName = $node->name; if ($functionName instanceof Node\Name) { try { - $dependenciesReflections[] = $this->getFunctionReflection($functionName, $scope); + $functionReflection = $this->getFunctionReflection($functionName, $scope); + $dependenciesReflections[] = $functionReflection; + + foreach ($functionReflection->getVariants() as $functionVariant) { + foreach ($functionVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } + + foreach ($functionReflection->getAsserts()->getAll() as $assertTag) { + foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + foreach ($assertTag->getOriginalType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } } catch (FunctionNotFoundException) { // pass } @@ -105,6 +167,23 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies foreach ($referencedClasses as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } + + foreach ($variant->getParameters() as $parameter) { + if (!$parameter instanceof ExtendedParameterReflection) { + continue; + } + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } } } } @@ -113,8 +192,9 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies foreach ($returnType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } elseif ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { - $classNames = $scope->getType($node->var)->getReferencedClasses(); + } elseif ($node instanceof Node\Expr\MethodCall) { + $calledOnType = $scope->getType($node->var); + $classNames = $calledOnType->getReferencedClasses(); foreach ($classNames as $className) { $this->addClassToDependencies($className, $dependenciesReflections); } @@ -123,11 +203,62 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies foreach ($returnType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } elseif ( - $node instanceof Node\Expr\StaticCall - || $node instanceof Node\Expr\ClassConstFetch - || $node instanceof Node\Expr\StaticPropertyFetch - ) { + + if ($node->name instanceof Node\Identifier) { + $methodReflection = $scope->getMethodReflection($calledOnType, $node->name->toString()); + if ($methodReflection !== null) { + $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); + foreach ($methodReflection->getVariants() as $methodVariant) { + foreach ($methodVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } + + foreach ($methodReflection->getAsserts()->getAll() as $assertTag) { + foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + foreach ($assertTag->getOriginalType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + + if ($methodReflection->getSelfOutType() !== null) { + foreach ($methodReflection->getSelfOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } + } + } elseif ($node instanceof Node\Expr\PropertyFetch) { + $fetchedOnType = $scope->getType($node->var); + $classNames = $fetchedOnType->getReferencedClasses(); + foreach ($classNames as $className) { + $this->addClassToDependencies($className, $dependenciesReflections); + } + + $propertyType = $scope->getType($node); + foreach ($propertyType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + + if ($node->name instanceof Node\Identifier) { + $propertyReflection = $scope->getPropertyReflection($fetchedOnType, $node->name->toString()); + if ($propertyReflection !== null) { + $this->addClassToDependencies($propertyReflection->getDeclaringClass()->getName(), $dependenciesReflections); + } + } + } elseif ($node instanceof Node\Expr\StaticCall) { if ($node->class instanceof Node\Name) { $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); } else { @@ -140,15 +271,157 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies foreach ($returnType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } + + if ($node->name instanceof Node\Identifier) { + if ($node->class instanceof Node\Name) { + $className = $scope->resolveName($node->class); + if ($this->reflectionProvider->hasClass($className)) { + $methodClassReflection = $this->reflectionProvider->getClass($className); + if ($methodClassReflection->hasMethod($node->name->toString())) { + $methodReflection = $methodClassReflection->getMethod($node->name->toString(), $scope); + $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); + foreach ($methodReflection->getVariants() as $methodVariant) { + foreach ($methodVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } + } + } + } else { + $methodReflection = $scope->getMethodReflection($scope->getType($node->class), $node->name->toString()); + if ($methodReflection !== null) { + $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); + foreach ($methodReflection->getVariants() as $methodVariant) { + foreach ($methodVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } + } + } + } + } elseif ($node instanceof Node\Expr\ClassConstFetch) { + if ($node->class instanceof Node\Name) { + $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); + } else { + foreach ($scope->getType($node->class)->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + + $returnType = $scope->getType($node); + foreach ($returnType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + + if ($node->name instanceof Node\Identifier && $node->name->toLowerString() !== 'class') { + if ($node->class instanceof Node\Name) { + $className = $scope->resolveName($node->class); + if ($this->reflectionProvider->hasClass($className)) { + $constantClassReflection = $this->reflectionProvider->getClass($className); + if ($constantClassReflection->hasConstant($node->name->toString())) { + $constantReflection = $constantClassReflection->getConstant($node->name->toString()); + $this->addClassToDependencies($constantReflection->getDeclaringClass()->getName(), $dependenciesReflections); + } + } + } else { + $constantReflection = $scope->getConstantReflection($scope->getType($node->class), $node->name->toString()); + if ($constantReflection !== null) { + $this->addClassToDependencies($constantReflection->getDeclaringClass()->getName(), $dependenciesReflections); + } + } + } + } elseif ($node instanceof Node\Expr\StaticPropertyFetch) { + if ($node->class instanceof Node\Name) { + $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); + } else { + foreach ($scope->getType($node->class)->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + + $returnType = $scope->getType($node); + foreach ($returnType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + + if ($node->name instanceof Node\Identifier) { + if ($node->class instanceof Node\Name) { + $className = $scope->resolveName($node->class); + if ($this->reflectionProvider->hasClass($className)) { + $propertyClassReflection = $this->reflectionProvider->getClass($className); + if ($propertyClassReflection->hasProperty($node->name->toString())) { + $propertyReflection = $propertyClassReflection->getProperty($node->name->toString(), $scope); + $this->addClassToDependencies($propertyReflection->getDeclaringClass()->getName(), $dependenciesReflections); + } + } + } else { + $propertyReflection = $scope->getPropertyReflection($scope->getType($node->class), $node->name->toString()); + if ($propertyReflection !== null) { + $this->addClassToDependencies($propertyReflection->getDeclaringClass()->getName(), $dependenciesReflections); + } + } + } } elseif ( $node instanceof Node\Expr\New_ && $node->class instanceof Node\Name ) { $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); + } elseif ($node instanceof Node\Stmt\Trait_ && $node->namespacedName !== null) { + try { + $classReflection = $this->reflectionProvider->getClass($node->namespacedName->toString()); + + foreach ($classReflection->getRequireImplementsTags() as $implementsTag) { + foreach ($implementsTag->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } catch (ClassNotFoundException) { + // pass + } } elseif ($node instanceof Node\Stmt\TraitUse) { foreach ($node->traits as $traitName) { $this->addClassToDependencies($traitName->toString(), $dependenciesReflections); } + + $docComment = $node->getDocComment(); + if ($docComment !== null) { + $usesTags = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + null, + $docComment->getText(), + )->getUsesTags(); + foreach ($usesTags as $usesTag) { + foreach ($usesTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } } elseif ($node instanceof Node\Expr\Instanceof_) { if ($node->class instanceof Name) { $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); @@ -167,12 +440,13 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } elseif ($node instanceof Foreach_) { $exprType = $scope->getType($node->expr); if ($node->keyVar !== null) { - foreach ($exprType->getIterableKeyType()->getReferencedClasses() as $referencedClass) { + + foreach ($scope->getIterableKeyType($exprType)->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } - foreach ($exprType->getIterableValueType()->getReferencedClasses() as $referencedClass) { + foreach ($scope->getIterableValueType($exprType)->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } elseif ( @@ -200,16 +474,8 @@ private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode): return false; } - if ($items[0] === null) { - return false; - } - $itemType = $scope->getType($items[0]->value); - if (!$itemType instanceof ConstantStringType) { - return false; - } - - return $itemType->isClassString(); + return $itemType->isClassString()->yes(); } /** @@ -234,6 +500,117 @@ private function addClassToDependencies(string $className, array &$dependenciesR $dependenciesReflections[] = $trait; } + foreach ($classReflection->getResolvedMixinTypes() as $mixinType) { + foreach ($mixinType->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + foreach ($classReflection->getRequireExtendsTags() as $extendsTag) { + foreach ($extendsTag->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + foreach ($classReflection->getTemplateTags() as $templateTag) { + foreach ($templateTag->getBound()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + + $default = $templateTag->getDefault(); + if ($default === null) { + continue; + } + foreach ($default->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + foreach ($classReflection->getPropertyTags() as $propertyTag) { + if ($propertyTag->isReadable()) { + foreach ($propertyTag->getReadableType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + if (!$propertyTag->isWritable()) { + continue; + } + + foreach ($propertyTag->getWritableType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + foreach ($classReflection->getMethodTags() as $methodTag) { + foreach ($methodTag->getReturnType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + foreach ($methodTag->getParameters() as $parameter) { + foreach ($parameter->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + if ($parameter->getDefaultValue() === null) { + continue; + } + foreach ($parameter->getDefaultValue()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + } + + foreach ($classReflection->getExtendsTags() as $extendsTag) { + foreach ($extendsTag->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + foreach ($classReflection->getImplementsTags() as $implementsTag) { + foreach ($implementsTag->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + $phpDoc = $classReflection->getResolvedPhpDoc(); + if ($phpDoc !== null) { + foreach ($phpDoc->getTypeAliasImportTags() as $importTag) { + $dependenciesReflections[] = $this->reflectionProvider->getClass($importTag->getImportedFrom()); + } + } + $classReflection = $classReflection->getParentClass(); } while ($classReflection !== null); } @@ -247,7 +624,7 @@ private function getFunctionReflection(Node\Name $nameNode, ?Scope $scope): Func * @param array $dependenciesReflections */ private function extractFromParametersAcceptor( - ParametersAcceptorWithPhpDocs $parametersAcceptor, + ExtendedParametersAcceptor $parametersAcceptor, array &$dependenciesReflections, ): void { @@ -260,6 +637,18 @@ private function extractFromParametersAcceptor( foreach ($referencedClasses as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } + + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } $returnTypeReferencedClasses = array_merge( diff --git a/src/Dependency/ExportedNode/ExportedAttributeNode.php b/src/Dependency/ExportedNode/ExportedAttributeNode.php new file mode 100644 index 0000000000..f7d00465ad --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedAttributeNode.php @@ -0,0 +1,83 @@ + $args argument name or index(string|int) => value expression (string) + */ + public function __construct( + private string $name, + private array $args, + ) + { + } + + public function equals(ExportedNode $node): bool + { + if (!$node instanceof self) { + return false; + } + + if ($this->name !== $node->name) { + return false; + } + + if (count($this->args) !== count($node->args)) { + return false; + } + + foreach ($this->args as $argName => $argValue) { + if (!isset($node->args[$argName]) || $argValue !== $node->args[$argName]) { + return false; + } + } + + return true; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['args'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'args' => $this->args, + ], + ]; + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['args'], + ); + } + +} diff --git a/src/Dependency/ExportedNode/ExportedClassConstantNode.php b/src/Dependency/ExportedNode/ExportedClassConstantNode.php index c24510b3cc..2aec4d44fa 100644 --- a/src/Dependency/ExportedNode/ExportedClassConstantNode.php +++ b/src/Dependency/ExportedNode/ExportedClassConstantNode.php @@ -4,12 +4,22 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; +use function array_map; +use function count; -class ExportedClassConstantNode implements ExportedNode, JsonSerializable +final class ExportedClassConstantNode implements ExportedNode, JsonSerializable { - public function __construct(private string $name, private string $value) + /** + * @param ExportedAttributeNode[] $attributes + */ + public function __construct( + private string $name, + private string $value, + private array $attributes, + ) { } @@ -19,31 +29,46 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->name === $node->name && $this->value === $node->value; } /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], $properties['value'], + $properties['attributes'], ); } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], $data['value'], + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } @@ -58,6 +83,7 @@ public function jsonSerialize() 'data' => [ 'name' => $this->name, 'value' => $this->value, + 'attributes' => $this->attributes, ], ]; } diff --git a/src/Dependency/ExportedNode/ExportedClassConstantsNode.php b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php index 04bace7282..e555874fc7 100644 --- a/src/Dependency/ExportedNode/ExportedClassConstantsNode.php +++ b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php @@ -9,7 +9,7 @@ use function array_map; use function count; -class ExportedClassConstantsNode implements ExportedNode, JsonSerializable +final class ExportedClassConstantsNode implements ExportedNode, JsonSerializable { /** @@ -54,9 +54,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['constants'], @@ -69,9 +68,8 @@ public static function __set_state(array $properties): ExportedNode /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( array_map(static function (array $constantData): ExportedClassConstantNode { diff --git a/src/Dependency/ExportedNode/ExportedClassNode.php b/src/Dependency/ExportedNode/ExportedClassNode.php index c46001f515..cb57bddb4c 100644 --- a/src/Dependency/ExportedNode/ExportedClassNode.php +++ b/src/Dependency/ExportedNode/ExportedClassNode.php @@ -4,12 +4,13 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\RootExportedNode; use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; use function array_map; use function count; -class ExportedClassNode implements ExportedNode, JsonSerializable +final class ExportedClassNode implements RootExportedNode, JsonSerializable { /** @@ -17,6 +18,7 @@ class ExportedClassNode implements ExportedNode, JsonSerializable * @param string[] $usedTraits * @param ExportedTraitUseAdaptation[] $traitUseAdaptations * @param ExportedNode[] $statements + * @param ExportedAttributeNode[] $attributes */ public function __construct( private string $name, @@ -28,6 +30,7 @@ public function __construct( private array $usedTraits, private array $traitUseAdaptations, private array $statements, + private array $attributes, ) { } @@ -50,6 +53,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + if (count($this->traitUseAdaptations) !== count($node->traitUseAdaptations)) { return false; } @@ -83,9 +96,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -97,6 +109,7 @@ public static function __set_state(array $properties): ExportedNode $properties['usedTraits'], $properties['traitUseAdaptations'], $properties['statements'], + $properties['attributes'], ); } @@ -118,15 +131,15 @@ public function jsonSerialize() 'usedTraits' => $this->usedTraits, 'traitUseAdaptations' => $this->traitUseAdaptations, 'statements' => $this->statements, + 'attributes' => $this->attributes, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -147,7 +160,26 @@ public static function decode(array $data): ExportedNode return $nodeType::decode($node['data']); }, $data['statements']), + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } + /** + * @return self::TYPE_CLASS + */ + public function getType(): string + { + return self::TYPE_CLASS; + } + + public function getName(): string + { + return $this->name; + } + } diff --git a/src/Dependency/ExportedNode/ExportedEnumCaseNode.php b/src/Dependency/ExportedNode/ExportedEnumCaseNode.php index da304b5893..65b3273315 100644 --- a/src/Dependency/ExportedNode/ExportedEnumCaseNode.php +++ b/src/Dependency/ExportedNode/ExportedEnumCaseNode.php @@ -6,7 +6,7 @@ use PHPStan\Dependency\ExportedNode; use ReturnTypeWillChange; -class ExportedEnumCaseNode implements ExportedNode, JsonSerializable +final class ExportedEnumCaseNode implements ExportedNode, JsonSerializable { public function __construct(private string $name, private ?string $value, private ?ExportedPhpDocNode $phpDoc) @@ -37,9 +37,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -50,9 +49,8 @@ public static function __set_state(array $properties): ExportedNode /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], diff --git a/src/Dependency/ExportedNode/ExportedEnumNode.php b/src/Dependency/ExportedNode/ExportedEnumNode.php index 831a823658..b703518ab9 100644 --- a/src/Dependency/ExportedNode/ExportedEnumNode.php +++ b/src/Dependency/ExportedNode/ExportedEnumNode.php @@ -4,18 +4,28 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\RootExportedNode; +use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; use function array_map; use function count; -class ExportedEnumNode implements ExportedNode, JsonSerializable +final class ExportedEnumNode implements RootExportedNode, JsonSerializable { /** * @param string[] $implements * @param ExportedNode[] $statements + * @param ExportedAttributeNode[] $attributes */ - public function __construct(private string $name, private ?string $scalarType, private ?ExportedPhpDocNode $phpDoc, private array $implements, private array $statements) + public function __construct( + private string $name, + private ?string $scalarType, + private ?ExportedPhpDocNode $phpDoc, + private array $implements, + private array $statements, + private array $attributes, + ) { } @@ -49,6 +59,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->name === $node->name && $this->scalarType === $node->scalarType && $this->implements === $node->implements; @@ -56,9 +76,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -66,6 +85,7 @@ public static function __set_state(array $properties): ExportedNode $properties['phpDoc'], $properties['implements'], $properties['statements'], + $properties['attributes'], ); } @@ -83,15 +103,15 @@ public function jsonSerialize() 'phpDoc' => $this->phpDoc, 'implements' => $this->implements, 'statements' => $this->statements, + 'attributes' => $this->attributes, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -103,7 +123,26 @@ public static function decode(array $data): ExportedNode return $nodeType::decode($node['data']); }, $data['statements']), + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } + /** + * @return self::TYPE_ENUM + */ + public function getType(): string + { + return self::TYPE_ENUM; + } + + public function getName(): string + { + return $this->name; + } + } diff --git a/src/Dependency/ExportedNode/ExportedFunctionNode.php b/src/Dependency/ExportedNode/ExportedFunctionNode.php index 58f2704c79..d0ebd50613 100644 --- a/src/Dependency/ExportedNode/ExportedFunctionNode.php +++ b/src/Dependency/ExportedNode/ExportedFunctionNode.php @@ -4,16 +4,18 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\RootExportedNode; use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; use function array_map; use function count; -class ExportedFunctionNode implements ExportedNode, JsonSerializable +final class ExportedFunctionNode implements RootExportedNode, JsonSerializable { /** * @param ExportedParameterNode[] $parameters + * @param ExportedAttributeNode[] $attributes */ public function __construct( private string $name, @@ -21,6 +23,7 @@ public function __construct( private bool $byRef, private ?string $returnType, private array $parameters, + private array $attributes, ) { } @@ -54,6 +57,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->name === $node->name && $this->byRef === $node->byRef && $this->returnType === $node->returnType; @@ -61,9 +74,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -71,6 +83,7 @@ public static function __set_state(array $properties): ExportedNode $properties['byRef'], $properties['returnType'], $properties['parameters'], + $properties['attributes'], ); } @@ -88,15 +101,15 @@ public function jsonSerialize() 'byRef' => $this->byRef, 'returnType' => $this->returnType, 'parameters' => $this->parameters, + 'attributes' => $this->attributes, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -109,7 +122,26 @@ public static function decode(array $data): ExportedNode } return ExportedParameterNode::decode($parameterData['data']); }, $data['parameters']), + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } + /** + * @return self::TYPE_FUNCTION + */ + public function getType(): string + { + return self::TYPE_FUNCTION; + } + + public function getName(): string + { + return $this->name; + } + } diff --git a/src/Dependency/ExportedNode/ExportedInterfaceNode.php b/src/Dependency/ExportedNode/ExportedInterfaceNode.php index 285db67f87..0e6d6632db 100644 --- a/src/Dependency/ExportedNode/ExportedInterfaceNode.php +++ b/src/Dependency/ExportedNode/ExportedInterfaceNode.php @@ -4,11 +4,12 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\RootExportedNode; use ReturnTypeWillChange; use function array_map; use function count; -class ExportedInterfaceNode implements ExportedNode, JsonSerializable +final class ExportedInterfaceNode implements RootExportedNode, JsonSerializable { /** @@ -55,9 +56,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -86,9 +86,8 @@ public function jsonSerialize() /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -102,4 +101,17 @@ public static function decode(array $data): ExportedNode ); } + /** + * @return self::TYPE_INTERFACE + */ + public function getType(): string + { + return self::TYPE_INTERFACE; + } + + public function getName(): string + { + return $this->name; + } + } diff --git a/src/Dependency/ExportedNode/ExportedMethodNode.php b/src/Dependency/ExportedNode/ExportedMethodNode.php index 7cb3376fab..af2b4255ce 100644 --- a/src/Dependency/ExportedNode/ExportedMethodNode.php +++ b/src/Dependency/ExportedNode/ExportedMethodNode.php @@ -9,11 +9,12 @@ use function array_map; use function count; -class ExportedMethodNode implements ExportedNode, JsonSerializable +final class ExportedMethodNode implements ExportedNode, JsonSerializable { /** * @param ExportedParameterNode[] $parameters + * @param ExportedAttributeNode[] $attributes */ public function __construct( private string $name, @@ -26,6 +27,7 @@ public function __construct( private bool $static, private ?string $returnType, private array $parameters, + private array $attributes, ) { } @@ -59,6 +61,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->name === $node->name && $this->byRef === $node->byRef && $this->public === $node->public @@ -71,9 +83,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -86,6 +97,7 @@ public static function __set_state(array $properties): ExportedNode $properties['static'], $properties['returnType'], $properties['parameters'], + $properties['attributes'], ); } @@ -108,15 +120,15 @@ public function jsonSerialize() 'static' => $this->static, 'returnType' => $this->returnType, 'parameters' => $this->parameters, + 'attributes' => $this->attributes, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -134,6 +146,12 @@ public static function decode(array $data): ExportedNode } return ExportedParameterNode::decode($parameterData['data']); }, $data['parameters']), + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedParameterNode.php b/src/Dependency/ExportedNode/ExportedParameterNode.php index ad36f20c42..9f4cf02d61 100644 --- a/src/Dependency/ExportedNode/ExportedParameterNode.php +++ b/src/Dependency/ExportedNode/ExportedParameterNode.php @@ -4,17 +4,24 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; +use function array_map; +use function count; -class ExportedParameterNode implements ExportedNode, JsonSerializable +final class ExportedParameterNode implements ExportedNode, JsonSerializable { + /** + * @param ExportedAttributeNode[] $attributes + */ public function __construct( private string $name, private ?string $type, private bool $byRef, private bool $variadic, private bool $hasDefault, + private array $attributes, ) { } @@ -25,6 +32,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->name === $node->name && $this->type === $node->type && $this->byRef === $node->byRef @@ -34,9 +51,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['name'], @@ -44,6 +60,7 @@ public static function __set_state(array $properties): ExportedNode $properties['byRef'], $properties['variadic'], $properties['hasDefault'], + $properties['attributes'], ); } @@ -61,15 +78,15 @@ public function jsonSerialize() 'byRef' => $this->byRef, 'variadic' => $this->variadic, 'hasDefault' => $this->hasDefault, + 'attributes' => $this->attributes, ], ]; } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['name'], @@ -77,6 +94,12 @@ public static function decode(array $data): ExportedNode $data['byRef'], $data['variadic'], $data['hasDefault'], + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedPhpDocNode.php b/src/Dependency/ExportedNode/ExportedPhpDocNode.php index fbb3d3bd0d..9288a58107 100644 --- a/src/Dependency/ExportedNode/ExportedPhpDocNode.php +++ b/src/Dependency/ExportedNode/ExportedPhpDocNode.php @@ -6,18 +6,15 @@ use PHPStan\Dependency\ExportedNode; use ReturnTypeWillChange; -class ExportedPhpDocNode implements ExportedNode, JsonSerializable +final class ExportedPhpDocNode implements ExportedNode, JsonSerializable { - /** @var array alias(string) => fullName(string) */ - private array $uses; - /** - * @param array $uses + * @param array $uses alias(string) => fullName(string) + * @param array $constUses alias(string) => fullName(string) */ - public function __construct(private string $phpDocString, private ?string $namespace, array $uses) + public function __construct(private string $phpDocString, private ?string $namespace, private array $uses, private array $constUses) { - $this->uses = $uses; } public function equals(ExportedNode $node): bool @@ -28,7 +25,8 @@ public function equals(ExportedNode $node): bool return $this->phpDocString === $node->phpDocString && $this->namespace === $node->namespace - && $this->uses === $node->uses; + && $this->uses === $node->uses + && $this->constUses === $node->constUses; } /** @@ -43,26 +41,25 @@ public function jsonSerialize() 'phpDocString' => $this->phpDocString, 'namespace' => $this->namespace, 'uses' => $this->uses, + 'constUses' => $this->constUses, ], ]; } /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { - return new self($properties['phpDocString'], $properties['namespace'], $properties['uses']); + return new self($properties['phpDocString'], $properties['namespace'], $properties['uses'], $properties['constUses'] ?? []); } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { - return new self($data['phpDocString'], $data['namespace'], $data['uses']); + return new self($data['phpDocString'], $data['namespace'], $data['uses'], $data['constUses'] ?? []); } } diff --git a/src/Dependency/ExportedNode/ExportedPropertiesNode.php b/src/Dependency/ExportedNode/ExportedPropertiesNode.php index 2b61556824..af58d51738 100644 --- a/src/Dependency/ExportedNode/ExportedPropertiesNode.php +++ b/src/Dependency/ExportedNode/ExportedPropertiesNode.php @@ -4,14 +4,17 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; +use function array_map; use function count; -class ExportedPropertiesNode implements JsonSerializable, ExportedNode +final class ExportedPropertiesNode implements JsonSerializable, ExportedNode { /** * @param string[] $names + * @param ExportedAttributeNode[] $attributes */ public function __construct( private array $names, @@ -21,6 +24,7 @@ public function __construct( private bool $private, private bool $static, private bool $readonly, + private array $attributes, ) { } @@ -53,6 +57,16 @@ public function equals(ExportedNode $node): bool } } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->type === $node->type && $this->public === $node->public && $this->private === $node->private @@ -62,9 +76,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['names'], @@ -74,14 +87,14 @@ public static function __set_state(array $properties): ExportedNode $properties['private'], $properties['static'], $properties['readonly'], + $properties['attributes'], ); } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['names'], @@ -91,6 +104,12 @@ public static function decode(array $data): ExportedNode $data['private'], $data['static'], $data['readonly'], + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } @@ -110,6 +129,7 @@ public function jsonSerialize() 'private' => $this->private, 'static' => $this->static, 'readonly' => $this->readonly, + 'attributes' => $this->attributes, ], ]; } diff --git a/src/Dependency/ExportedNode/ExportedTraitNode.php b/src/Dependency/ExportedNode/ExportedTraitNode.php index 7874fca68c..8f6808f2b3 100644 --- a/src/Dependency/ExportedNode/ExportedTraitNode.php +++ b/src/Dependency/ExportedNode/ExportedTraitNode.php @@ -4,9 +4,10 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\Dependency\RootExportedNode; use ReturnTypeWillChange; -class ExportedTraitNode implements ExportedNode, JsonSerializable +final class ExportedTraitNode implements RootExportedNode, JsonSerializable { public function __construct(private string $traitName) @@ -20,18 +21,16 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self($properties['traitName']); } /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self($data['traitName']); } @@ -50,4 +49,17 @@ public function jsonSerialize() ]; } + /** + * @return self::TYPE_TRAIT + */ + public function getType(): string + { + return self::TYPE_TRAIT; + } + + public function getName(): string + { + return $this->traitName; + } + } diff --git a/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php b/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php index 57f3491010..85c515fac4 100644 --- a/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php +++ b/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php @@ -6,7 +6,7 @@ use PHPStan\Dependency\ExportedNode; use ReturnTypeWillChange; -class ExportedTraitUseAdaptation implements ExportedNode, JsonSerializable +final class ExportedTraitUseAdaptation implements ExportedNode, JsonSerializable { /** @@ -59,9 +59,8 @@ public function equals(ExportedNode $node): bool /** * @param mixed[] $properties - * @return self */ - public static function __set_state(array $properties): ExportedNode + public static function __set_state(array $properties): self { return new self( $properties['traitName'], @@ -74,9 +73,8 @@ public static function __set_state(array $properties): ExportedNode /** * @param mixed[] $data - * @return self */ - public static function decode(array $data): ExportedNode + public static function decode(array $data): self { return new self( $data['traitName'], diff --git a/src/Dependency/ExportedNodeFetcher.php b/src/Dependency/ExportedNodeFetcher.php index 95d82d5057..dbc36c155a 100644 --- a/src/Dependency/ExportedNodeFetcher.php +++ b/src/Dependency/ExportedNodeFetcher.php @@ -2,12 +2,11 @@ namespace PHPStan\Dependency; -use PhpParser\Node; use PhpParser\NodeTraverser; use PHPStan\Parser\Parser; use PHPStan\Parser\ParserErrorsException; -class ExportedNodeFetcher +final class ExportedNodeFetcher { public function __construct( @@ -18,7 +17,7 @@ public function __construct( } /** - * @return ExportedNode[] + * @return RootExportedNode[] */ public function fetchNodes(string $fileName): array { @@ -26,7 +25,6 @@ public function fetchNodes(string $fileName): array $nodeTraverser->addVisitor($this->visitor); try { - /** @var Node[] $ast */ $ast = $this->parser->parseFile($fileName); } catch (ParserErrorsException) { return []; diff --git a/src/Dependency/ExportedNodeResolver.php b/src/Dependency/ExportedNodeResolver.php index 0a2dc26633..441c785c99 100644 --- a/src/Dependency/ExportedNodeResolver.php +++ b/src/Dependency/ExportedNodeResolver.php @@ -7,7 +7,7 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Dependency\ExportedNode\ExportedAttributeNode; use PHPStan\Dependency\ExportedNode\ExportedClassConstantNode; use PHPStan\Dependency\ExportedNode\ExportedClassConstantsNode; use PHPStan\Dependency\ExportedNode\ExportedClassNode; @@ -21,20 +21,21 @@ use PHPStan\Dependency\ExportedNode\ExportedPropertiesNode; use PHPStan\Dependency\ExportedNode\ExportedTraitNode; use PHPStan\Dependency\ExportedNode\ExportedTraitUseAdaptation; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\NodeTypePrinter; use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use function array_map; -use function implode; use function is_string; -class ExportedNodeResolver +final class ExportedNodeResolver { - public function __construct(private FileTypeMapper $fileTypeMapper, private Standard $printer) + public function __construct(private FileTypeMapper $fileTypeMapper, private ExprPrinter $exprPrinter) { } - public function resolve(string $fileName, Node $node): ?ExportedNode + public function resolve(string $fileName, Node $node): ?RootExportedNode { if ($node instanceof Class_ && isset($node->namespacedName)) { $docComment = $node->getDocComment(); @@ -95,6 +96,7 @@ public function resolve(string $fileName, Node $node): ?ExportedNode throw new ShouldNotHappenException(); }, $adaptations), $this->exportClassStatements($node->stmts, $fileName, $className), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -138,6 +140,7 @@ public function resolve(string $fileName, Node $node): ?ExportedNode ), $implementsNames, $this->exportClassStatements($node->stmts, $fileName, $enumName), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -162,56 +165,15 @@ public function resolve(string $fileName, Node $node): ?ExportedNode $docComment !== null ? $docComment->getText() : null, ), $node->byRef, - $this->printType($node->returnType), + NodeTypePrinter::printType($node->returnType), $this->exportParameterNodes($node->params), + $this->exportAttributeNodes($node->attrGroups), ); } return null; } - /** - * @param Node\Identifier|Node\Name|Node\ComplexType|null $type - */ - private function printType($type): ?string - { - if ($type === null) { - return null; - } - - if ($type instanceof Node\NullableType) { - return '?' . $this->printType($type->type); - } - - if ($type instanceof Node\UnionType) { - return implode('|', array_map(function ($innerType): string { - $printedType = $this->printType($innerType); - if ($printedType === null) { - 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)); - } - - if ($type instanceof Node\Identifier || $type instanceof Name) { - return $type->toString(); - } - - throw new ShouldNotHappenException(); - } - /** * @param Node\Param[] $params * @return ExportedParameterNode[] @@ -239,10 +201,11 @@ private function exportParameterNodes(array $params): array } $nodes[] = new ExportedParameterNode( $param->var->name, - $this->printType($type), + NodeTypePrinter::printType($type), $param->byRef, $param->variadic, $param->default !== null, + $this->exportAttributeNodes($param->attrGroups), ); } @@ -273,7 +236,7 @@ private function exportPhpDocNode( return null; } - return new ExportedPhpDocNode($text, $nameScope->getNamespace(), $nameScope->getUses()); + return new ExportedPhpDocNode($text, $nameScope->getNamespace(), $nameScope->getUses(), $nameScope->getConstUses()); } /** @@ -316,8 +279,9 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string $node->isAbstract(), $node->isFinal(), $node->isStatic(), - $this->printType($node->returnType), + NodeTypePrinter::printType($node->returnType), $this->exportParameterNodes($node->params), + $this->exportAttributeNodes($node->attrGroups), ); } } @@ -330,18 +294,19 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string $docComment = $node->getDocComment(); return new ExportedPropertiesNode( - array_map(static fn (Node\Stmt\PropertyProperty $prop): string => $prop->name->toString(), $node->props), + array_map(static fn (Node\PropertyItem $prop): string => $prop->name->toString(), $node->props), $this->exportPhpDocNode( $fileName, $namespacedName, null, $docComment !== null ? $docComment->getText() : null, ), - $this->printType($node->type), + NodeTypePrinter::printType($node->type), $node->isPublic(), $node->isPrivate(), $node->isStatic(), $node->isReadonly(), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -356,7 +321,8 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string foreach ($node->consts as $const) { $constants[] = new ExportedClassConstantNode( $const->name->toString(), - $this->printer->prettyPrintExpr($const->value), + $this->exprPrinter->printExpr($const->value), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -379,7 +345,7 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string return new ExportedEnumCaseNode( $node->name->toString(), - $node->expr !== null ? $this->printer->prettyPrintExpr($node->expr) : null, + $node->expr !== null ? $this->exprPrinter->printExpr($node->expr) : null, $this->exportPhpDocNode( $fileName, $namespacedName, @@ -392,4 +358,28 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string return null; } + /** + * @param Node\AttributeGroup[] $attributeGroups + * @return ExportedAttributeNode[] + */ + private function exportAttributeNodes(array $attributeGroups): array + { + $nodes = []; + foreach ($attributeGroups as $attributeGroup) { + foreach ($attributeGroup->attrs as $attribute) { + $args = []; + foreach ($attribute->args as $i => $arg) { + $args[$arg->name->name ?? $i] = $this->exprPrinter->printExpr($arg->value); + } + + $nodes[] = new ExportedAttributeNode( + $attribute->name->toString(), + $args, + ); + } + } + + return $nodes; + } + } diff --git a/src/Dependency/ExportedNodeVisitor.php b/src/Dependency/ExportedNodeVisitor.php index 61579fba4d..34dfd1efe1 100644 --- a/src/Dependency/ExportedNodeVisitor.php +++ b/src/Dependency/ExportedNodeVisitor.php @@ -3,16 +3,16 @@ namespace PHPStan\Dependency; use PhpParser\Node; -use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; use PHPStan\ShouldNotHappenException; -class ExportedNodeVisitor extends NodeVisitorAbstract +final class ExportedNodeVisitor extends NodeVisitorAbstract { private ?string $fileName = null; - /** @var ExportedNode[] */ + /** @var RootExportedNode[] */ private array $currentNodes = []; /** @@ -30,7 +30,7 @@ public function reset(string $fileName): void } /** - * @return ExportedNode[] + * @return RootExportedNode[] */ public function getExportedNodes(): array { @@ -52,7 +52,7 @@ public function enterNode(Node $node): ?int || $node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\Trait_ ) { - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } return null; diff --git a/src/Dependency/NodeDependencies.php b/src/Dependency/NodeDependencies.php index 2b35652747..ac30175b35 100644 --- a/src/Dependency/NodeDependencies.php +++ b/src/Dependency/NodeDependencies.php @@ -7,7 +7,7 @@ use PHPStan\Reflection\FunctionReflection; use function array_values; -class NodeDependencies +final class NodeDependencies { /** @@ -16,7 +16,7 @@ class NodeDependencies public function __construct( private FileHelper $fileHelper, private array $reflections, - private ?ExportedNode $exportedNode, + private ?RootExportedNode $exportedNode, ) { } @@ -50,7 +50,7 @@ public function getFileDependencies(string $currentFile, array $analysedFiles): return array_values($dependencies); } - public function getExportedNode(): ?ExportedNode + public function getExportedNode(): ?RootExportedNode { return $this->exportedNode; } diff --git a/src/Dependency/RootExportedNode.php b/src/Dependency/RootExportedNode.php new file mode 100644 index 0000000000..9434c91337 --- /dev/null +++ b/src/Dependency/RootExportedNode.php @@ -0,0 +1,23 @@ + $bool, BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG => $bool, BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG => $bool, BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG => $bool, BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG => $bool, + BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG => $bool, BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG => $bool, - RegistryFactory::RULE_TAG => $bool, + BrokerFactory::ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG => $bool, + LazyRegistry::RULE_TAG => $bool, TypeNodeResolverExtension::EXTENSION_TAG => $bool, + StubFilesExtension::EXTENSION_TAG => $bool, + AlwaysUsedClassConstantsExtensionProvider::EXTENSION_TAG => $bool, + ReadWritePropertiesExtensionProvider::EXTENSION_TAG => $bool, TypeSpecifierFactory::FUNCTION_TYPE_SPECIFYING_EXTENSION_TAG => $bool, TypeSpecifierFactory::METHOD_TYPE_SPECIFYING_EXTENSION_TAG => $bool, TypeSpecifierFactory::STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG => $bool, + RichParser::VISITOR_SERVICE_TAG => $bool, + CollectorRegistryFactory::COLLECTOR_TAG => $bool, + LazyDynamicThrowTypeExtensionProvider::FUNCTION_TAG => $bool, + LazyDynamicThrowTypeExtensionProvider::METHOD_TAG => $bool, + LazyDynamicThrowTypeExtensionProvider::STATIC_METHOD_TAG => $bool, + LazyParameterClosureTypeExtensionProvider::FUNCTION_TAG => $bool, + LazyParameterClosureTypeExtensionProvider::METHOD_TAG => $bool, + LazyParameterClosureTypeExtensionProvider::STATIC_METHOD_TAG => $bool, + LazyParameterOutTypeExtensionProvider::FUNCTION_TAG => $bool, + LazyParameterOutTypeExtensionProvider::METHOD_TAG => $bool, + LazyParameterOutTypeExtensionProvider::STATIC_METHOD_TAG => $bool, + DiagnoseExtension::EXTENSION_TAG => $bool, + ResultCacheMetaExtension::EXTENSION_TAG => $bool, + ClassConstantDeprecationExtension::CLASS_CONSTANT_EXTENSION_TAG => $bool, + ClassDeprecationExtension::CLASS_EXTENSION_TAG => $bool, + EnumCaseDeprecationExtension::ENUM_CASE_EXTENSION_TAG => $bool, + FunctionDeprecationExtension::FUNCTION_EXTENSION_TAG => $bool, + MethodDeprecationExtension::METHOD_EXTENSION_TAG => $bool, + PropertyDeprecationExtension::PROPERTY_EXTENSION_TAG => $bool, + RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG => $bool, + RestrictedClassNameUsageExtension::CLASS_NAME_EXTENSION_TAG => $bool, + RestrictedFunctionUsageExtension::FUNCTION_EXTENSION_TAG => $bool, + RestrictedPropertyUsageExtension::PROPERTY_EXTENSION_TAG => $bool, + RestrictedClassConstantUsageExtension::CLASS_CONSTANT_EXTENSION_TAG => $bool, ])->min(1)); } @@ -47,6 +99,9 @@ public function beforeCompile(): void } foreach ($services as $service) { foreach ($tags as $tag => $parameter) { + if (is_array($parameter)) { + $parameter = array_reduce($parameter, static fn ($carry, $item) => $carry && (bool) $item, true); + } if ((bool) $parameter) { $service->addTag($tag); continue; diff --git a/src/DependencyInjection/Configurator.php b/src/DependencyInjection/Configurator.php index 1d3ba2b654..9536925b9d 100644 --- a/src/DependencyInjection/Configurator.php +++ b/src/DependencyInjection/Configurator.php @@ -2,16 +2,42 @@ namespace PHPStan\DependencyInjection; +use DirectoryIterator; use Nette\DI\Config\Loader; +use Nette\DI\Container as OriginalNetteContainer; use Nette\DI\ContainerLoader; +use PHPStan\File\CouldNotReadFileException; +use PHPStan\File\CouldNotWriteFileException; +use PHPStan\File\FileReader; +use PHPStan\File\FileWriter; use function array_keys; +use function count; +use function error_reporting; +use function explode; +use function implode; +use function in_array; +use function is_dir; +use function is_file; +use function restore_error_handler; +use function set_error_handler; +use function sha1_file; +use function sprintf; +use function str_ends_with; +use function substr; +use function time; +use function trim; +use function unlink; +use const E_USER_DEPRECATED; use const PHP_RELEASE_VERSION; use const PHP_VERSION_ID; -class Configurator extends \Nette\Bootstrap\Configurator +final class Configurator extends \Nette\Bootstrap\Configurator { - public function __construct(private LoaderFactory $loaderFactory) + /** @var string[] */ + private array $allConfigFiles = []; + + public function __construct(private LoaderFactory $loaderFactory, private bool $journalContainer) { parent::__construct(); } @@ -21,6 +47,14 @@ protected function createLoader(): Loader return $this->loaderFactory->createLoader(); } + /** + * @param string[] $allConfigFiles + */ + public function setAllConfigFiles(array $allConfigFiles): void + { + $this->allConfigFiles = $allConfigFiles; + } + /** * @return mixed[] */ @@ -41,10 +75,145 @@ public function loadContainer(): string $this->staticParameters['debugMode'], ); - return $loader->load( + $className = $loader->load( [$this, 'generateContainer'], - [$this->staticParameters, 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, $this->getAllConfigFilesHashes()], ); + + if ($this->journalContainer) { + $this->journal($className); + } + + return $className; + } + + private function journal(string $currentContainerClassName): void + { + $directory = $this->getContainerCacheDirectory(); + if (!is_dir($directory)) { + return; + } + + $journalFile = $directory . '/container.journal'; + if (!is_file($journalFile)) { + try { + FileWriter::write($journalFile, sprintf("%s:%d\n", $currentContainerClassName, time())); + } catch (CouldNotWriteFileException) { + // pass + } + + return; + } + + try { + $journalContents = FileReader::read($journalFile); + } catch (CouldNotReadFileException) { + return; + } + + $journalLines = explode("\n", trim($journalContents)); + $linesToWrite = []; + $usedInTheLastWeek = []; + $now = time(); + $currentAlreadyInTheJournal = false; + foreach ($journalLines as $journalLine) { + if ($journalLine === '') { + continue; + } + $journalLineParts = explode(':', $journalLine); + if (count($journalLineParts) !== 2) { + return; + } + $className = $journalLineParts[0]; + $containerLastUsedTime = (int) $journalLineParts[1]; + + $week = 3600 * 24 * 7; + + if ($containerLastUsedTime + $week < $now) { + continue; + } + + $usedInTheLastWeek[] = $className; + + if ($currentContainerClassName !== $className) { + $linesToWrite[] = sprintf('%s:%d', $className, $containerLastUsedTime); + continue; + } + + $linesToWrite[] = sprintf('%s:%d', $currentContainerClassName, $now); + $currentAlreadyInTheJournal = true; + } + + if (!$currentAlreadyInTheJournal) { + $linesToWrite[] = sprintf('%s:%d', $currentContainerClassName, $now); + $usedInTheLastWeek[] = $currentContainerClassName; + } + + try { + FileWriter::write($journalFile, implode("\n", $linesToWrite) . "\n"); + } catch (CouldNotWriteFileException) { + return; + } + + foreach (new DirectoryIterator($directory) as $fileInfo) { + if ($fileInfo->isDot()) { + continue; + } + $fileName = $fileInfo->getFilename(); + if ($fileName === 'container.journal') { + continue; + } + if (!str_ends_with($fileName, '.php')) { + continue; + } + $fileClassName = substr($fileName, 0, -4); + if (in_array($fileClassName, $usedInTheLastWeek, true)) { + continue; + } + $basePathname = $fileInfo->getPathname(); + @unlink($basePathname); + @unlink($basePathname . '.lock'); + @unlink($basePathname . '.meta'); + } + } + + public function createContainer(bool $initialize = true): OriginalNetteContainer + { + set_error_handler(static function (int $errno): bool { + if ((error_reporting() & $errno) === 0) { + // silence @ operator + return true; + } + + return $errno === E_USER_DEPRECATED; + }); + + try { + $container = parent::createContainer($initialize); + } finally { + restore_error_handler(); + } + + return $container; + } + + /** + * @return string[] + */ + private function getAllConfigFilesHashes(): array + { + $hashes = []; + foreach ($this->allConfigFiles as $file) { + $hash = sha1_file($file); + + if ($hash === false) { + throw new CouldNotReadFileException($file); + } + + $hashes[$file] = $hash; + } + + return $hashes; } } diff --git a/src/DependencyInjection/Container.php b/src/DependencyInjection/Container.php index 07a7a574e1..cd785677b1 100644 --- a/src/DependencyInjection/Container.php +++ b/src/DependencyInjection/Container.php @@ -14,10 +14,9 @@ public function hasService(string $serviceName): bool; public function getService(string $serviceName); /** - * @phpstan-template T of object - * @phpstan-param class-string $className - * @phpstan-return T - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className); diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 9a3d2829e8..c28e08a77c 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -2,31 +2,57 @@ namespace PHPStan\DependencyInjection; +use Nette\Bootstrap\Extensions\PhpExtension; +use Nette\DI\Config\Adapters\PhpAdapter; +use Nette\DI\Definitions\Statement; use Nette\DI\Extensions\ExtensionsExtension; -use Nette\DI\Extensions\PhpExtension; +use Nette\DI\Helpers; +use Nette\Schema\Context as SchemaContext; +use Nette\Schema\Elements\AnyOf; +use Nette\Schema\Elements\Structure; +use Nette\Schema\Elements\Type; +use Nette\Schema\Expect; +use Nette\Schema\Processor; +use Nette\Schema\Schema; +use Nette\Utils\Strings; +use Nette\Utils\Validators; use Phar; 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\Node\Printer\Printer; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use Symfony\Component\Finder\Finder; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ObjectType; +use function array_diff_key; +use function array_key_exists; +use function array_map; +use function array_merge; +use function array_unique; +use function count; use function dirname; use function extension_loaded; +use function getenv; use function ini_get; -use function is_dir; -use function sys_get_temp_dir; -use function time; -use function unlink; +use function is_array; +use function is_file; +use function is_readable; +use function spl_object_id; +use function sprintf; +use function str_ends_with; +use function substr; -/** @api */ -class ContainerFactory +/** + * @api + */ +final class ContainerFactory { private FileHelper $fileHelper; @@ -35,6 +61,10 @@ class ContainerFactory private string $configDirectory; + private static ?int $lastInitializedContainerId = null; + + private bool $journalContainer = false; + /** @api */ public function __construct(private string $currentWorkingDirectory) { @@ -52,6 +82,11 @@ public function __construct(private string $currentWorkingDirectory) $this->configDirectory = $originalRootDir . '/conf'; } + public function setJournalContainer(): void + { + $this->journalContainer = true; + } + /** * @param string[] $additionalConfigFiles * @param string[] $analysedPaths @@ -67,16 +102,23 @@ public function create( string $usedLevel = CommandHelper::DEFAULT_LEVEL, ?string $generateBaselineFile = null, ?string $cliAutoloadFile = null, - ?string $singleReflectionFile = null, - ?string $singleReflectionInsteadOfFile = null, ): Container { + [$allConfigFiles, $projectConfig] = $this->detectDuplicateIncludedFiles( + array_merge([__DIR__ . '/../../conf/parametersSchema.neon'], $additionalConfigFiles), + [ + 'rootDir' => $this->rootDirectory, + 'currentWorkingDirectory' => $this->currentWorkingDirectory, + 'env' => getenv(), + ], + ); + $configurator = new Configurator(new LoaderFactory( $this->fileHelper, $this->rootDirectory, $this->currentWorkingDirectory, $generateBaselineFile, - )); + ), $this->journalContainer); $configurator->defaultExtensions = [ 'php' => PhpExtension::class, 'extensions' => ExtensionsExtension::class, @@ -89,15 +131,14 @@ public function create( 'cliArgumentsVariablesRegistered' => ini_get('register_argc_argv') === '1', 'tmpDir' => $tempDirectory, 'additionalConfigFiles' => $additionalConfigFiles, + 'allConfigFiles' => $allConfigFiles, 'composerAutoloaderProjectPaths' => $composerAutoloaderProjectPaths, 'generateBaselineFile' => $generateBaselineFile, 'usedLevel' => $usedLevel, 'cliAutoloadFile' => $cliAutoloadFile, - 'fixerTmpDir' => sys_get_temp_dir() . '/phpstan-fixer', + 'env' => getenv(), ]); $configurator->addDynamicParameters([ - 'singleReflectionFile' => $singleReflectionFile, - 'singleReflectionInsteadOfFile' => $singleReflectionInsteadOfFile, 'analysedPaths' => $analysedPaths, 'analysedPathsFromConfig' => $analysedPathsFromConfig, ]); @@ -106,7 +147,24 @@ public function create( $configurator->addConfig($additionalConfigFile); } - $container = $configurator->createContainer(); + $configurator->setAllConfigFiles($allConfigFiles); + + $container = $configurator->createContainer()->getByType(Container::class); + $this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']); + self::postInitializeContainer($container); + + return $container; + } + + /** @internal */ + public static function postInitializeContainer(Container $container): void + { + $containerId = spl_object_id($container); + if ($containerId === self::$lastInitializedContainerId) { + return; + } + + self::$lastInitializedContainerId = $containerId; /** @var SourceLocator $sourceLocator */ $sourceLocator = $container->getService('betterReflectionSourceLocator'); @@ -123,68 +181,214 @@ public function create( $reflector, $phpParser, $container->getByType(PhpStormStubsSourceStubber::class), + $container->getByType(Printer::class), ); - /** @var Broker $broker */ - $broker = $container->getByType(Broker::class); - Broker::registerInstance($broker); ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class)); + PhpVersionStaticAccessor::registerInstance($container->getByType(PhpVersion::class)); + ObjectType::resetCaches(); $container->getService('typeSpecifier'); - BleedingEdgeToggle::setBleedingEdge($container->parameters['featureToggles']['bleedingEdge']); + BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']); + } - return $container->getByType(Container::class); + public function getCurrentWorkingDirectory(): string + { + return $this->currentWorkingDirectory; } - public function clearOldContainers(string $tempDirectory): void + public function getRootDirectory(): string { - $configurator = new Configurator(new LoaderFactory( - $this->fileHelper, - $this->rootDirectory, - $this->currentWorkingDirectory, - null, - )); - $configurator->setDebugMode(true); - $configurator->setTempDirectory($tempDirectory); + return $this->rootDirectory; + } - $containerDirectory = $configurator->getContainerCacheDirectory(); - if (!is_dir($containerDirectory)) { - return; + public function getConfigDirectory(): string + { + return $this->configDirectory; + } + + /** + * @param string[] $configFiles + * @param array $loaderParameters + * @return array{list, array} + * @throws DuplicateIncludedFilesException + */ + private function detectDuplicateIncludedFiles( + array $configFiles, + array $loaderParameters, + ): array + { + $neonAdapter = new NeonAdapter(); + $phpAdapter = new PhpAdapter(); + $allConfigFiles = []; + $configArray = []; + foreach ($configFiles as $configFile) { + [$tmpConfigFiles, $tmpConfigArray] = self::getConfigFiles($this->fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null); + $allConfigFiles = array_merge($allConfigFiles, $tmpConfigFiles); + + /** @var array $configArray */ + $configArray = \Nette\Schema\Helpers::merge($tmpConfigArray, $configArray); } - $finder = new Finder(); - $finder->name('Container_*')->in($containerDirectory); - $twoDaysAgo = time() - 24 * 60 * 60 * 2; + $normalized = array_map(fn (string $file): string => $this->fileHelper->normalizePath($file), $allConfigFiles); - foreach ($finder as $containerFile) { - $path = $containerFile->getRealPath(); - if ($path === false) { - continue; - } - if ($containerFile->getATime() > $twoDaysAgo) { - continue; - } - if ($containerFile->getCTime() > $twoDaysAgo) { - continue; - } + $deduplicated = array_unique($normalized); + if (count($normalized) <= count($deduplicated)) { + return [$normalized, $configArray]; + } + + $duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated)); + + throw new DuplicateIncludedFilesException($duplicateFiles); + } + + /** + * @param array $loaderParameters + * @return array{list, array} + */ + private static function getConfigFiles( + FileHelper $fileHelper, + NeonAdapter $neonAdapter, + PhpAdapter $phpAdapter, + string $configFile, + array $loaderParameters, + ?string $generateBaselineFile, + ): array + { + if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) { + return [[], []]; + } + if (!is_file($configFile) || !is_readable($configFile)) { + return [[], []]; + } - @unlink($path); + if (str_ends_with($configFile, '.php')) { + $data = $phpAdapter->load($configFile); + } else { + $data = $neonAdapter->load($configFile); + } + $allConfigFiles = [$configFile]; + if (isset($data['includes'])) { + Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile)); + $includes = Helpers::expand($data['includes'], $loaderParameters); + foreach ($includes as $include) { + $include = self::expandIncludedFile($include, $configFile); + [$tmpConfigFiles, $tmpConfigArray] = self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile); + $allConfigFiles = array_merge($allConfigFiles, $tmpConfigFiles); + + /** @var array $data */ + $data = \Nette\Schema\Helpers::merge($tmpConfigArray, $data); + } } + + return [$allConfigFiles, $data]; } - public function getCurrentWorkingDirectory(): string + private static function expandIncludedFile(string $includedFile, string $mainFile): string { - return $this->currentWorkingDirectory; + return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute + ? $includedFile + : dirname($mainFile) . '/' . $includedFile; } - public function getRootDirectory(): string + /** + * @param array $parameters + * @param array $parametersSchema + */ + private function validateParameters(array $parameters, array $parametersSchema): void { - return $this->rootDirectory; + if (!(bool) $parameters['__validate']) { + return; + } + + $schema = $this->processArgument( + new Statement('schema', [ + new Statement('structure', [$parametersSchema]), + ]), + ); + $processor = new Processor(); + $processor->onNewContext[] = static function (SchemaContext $context): void { + $context->path = ['parameters']; + }; + $processor->process($schema, $parameters); + + if ( + !array_key_exists('phpVersion', $parameters) + || !is_array($parameters['phpVersion'])) { + return; + } + + $phpVersion = $parameters['phpVersion']; + + if ($phpVersion['max'] < $phpVersion['min']) { + throw new InvalidPhpVersionException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } } - public function getConfigDirectory(): string + /** + * @param Statement[] $statements + */ + private function processSchema(array $statements, bool $required = true): Schema { - return $this->configDirectory; + if (count($statements) === 0) { + throw new ShouldNotHappenException(); + } + + $parameterSchema = null; + foreach ($statements as $statement) { + $processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments); + if ($parameterSchema === null) { + /** @var Type|AnyOf|Structure $parameterSchema */ + $parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments); + } else { + $parameterSchema->{$statement->getEntity()}(...$processedArguments); + } + } + + if ($required) { + $parameterSchema->required(); + } + + return $parameterSchema; + } + + /** + * @param mixed $argument + * @return mixed + */ + private function processArgument($argument, bool $required = true) + { + if ($argument instanceof Statement) { + if ($argument->entity === 'schema') { + $arguments = []; + foreach ($argument->arguments as $schemaArgument) { + if (!$schemaArgument instanceof Statement) { + throw new ShouldNotHappenException('schema() should contain another statement().'); + } + + $arguments[] = $schemaArgument; + } + + if (count($arguments) === 0) { + throw new ShouldNotHappenException('schema() should have at least one argument.'); + } + + return $this->processSchema($arguments, $required); + } + + return $this->processSchema([$argument], $required); + } elseif (is_array($argument)) { + $processedArray = []; + foreach ($argument as $key => $val) { + $required = $key[0] !== '?'; + $key = $required ? $key : substr($key, 1); + $processedArray[$key] = $this->processArgument($val, $required); + } + + return $processedArray; + } + + return $argument; } } diff --git a/src/DependencyInjection/DerivativeContainerFactory.php b/src/DependencyInjection/DerivativeContainerFactory.php index c859340da9..218eb4e252 100644 --- a/src/DependencyInjection/DerivativeContainerFactory.php +++ b/src/DependencyInjection/DerivativeContainerFactory.php @@ -4,7 +4,7 @@ use function array_merge; -class DerivativeContainerFactory +final class DerivativeContainerFactory { /** @@ -23,8 +23,6 @@ public function __construct( private string $usedLevel, private ?string $generateBaselineFile, private ?string $cliAutoloadFile, - private ?string $singleReflectionFile, - private ?string $singleReflectionInsteadOfFile, ) { } @@ -37,6 +35,7 @@ public function create(array $additionalConfigFiles): Container $containerFactory = new ContainerFactory( $this->currentWorkingDirectory, ); + $containerFactory->setJournalContainer(); return $containerFactory->create( $this->tempDirectory, @@ -47,8 +46,6 @@ public function create(array $additionalConfigFiles): Container $this->usedLevel, $this->generateBaselineFile, $this->cliAutoloadFile, - $this->singleReflectionFile, - $this->singleReflectionInsteadOfFile, ); } diff --git a/src/DependencyInjection/DuplicateIncludedFilesException.php b/src/DependencyInjection/DuplicateIncludedFilesException.php new file mode 100644 index 0000000000..e377e42553 --- /dev/null +++ b/src/DependencyInjection/DuplicateIncludedFilesException.php @@ -0,0 +1,28 @@ +files))); + } + + /** + * @return string[] + */ + public function getFiles(): array + { + return $this->files; + } + +} diff --git a/src/DependencyInjection/InvalidExcludePathsException.php b/src/DependencyInjection/InvalidExcludePathsException.php new file mode 100644 index 0000000000..b2ae030782 --- /dev/null +++ b/src/DependencyInjection/InvalidExcludePathsException.php @@ -0,0 +1,36 @@ +, analyseAndScan?: list} $suggestOptional + */ + public function __construct(private array $errors, private array $suggestOptional) + { + parent::__construct(implode("\n", $this->errors)); + } + + /** + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * @return array{analyse?: list, analyseAndScan?: list} + */ + public function getSuggestOptional(): array + { + return $this->suggestOptional; + } + +} diff --git a/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php b/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php index dd9258c64d..bd4f2466a1 100644 --- a/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php +++ b/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php @@ -5,7 +5,7 @@ use Exception; use function implode; -class InvalidIgnoredErrorPatternsException extends Exception +final class InvalidIgnoredErrorPatternsException extends Exception { /** diff --git a/src/DependencyInjection/InvalidPhpVersionException.php b/src/DependencyInjection/InvalidPhpVersionException.php new file mode 100644 index 0000000000..f9b41690a3 --- /dev/null +++ b/src/DependencyInjection/InvalidPhpVersionException.php @@ -0,0 +1,10 @@ +setParameters([ 'rootDir' => $this->rootDir, 'currentWorkingDirectory' => $this->currentWorkingDirectory, + 'env' => getenv(), ]); return $loader; diff --git a/src/DependencyInjection/MemoizingContainer.php b/src/DependencyInjection/MemoizingContainer.php index 07e92fbac5..bdd24bf291 100644 --- a/src/DependencyInjection/MemoizingContainer.php +++ b/src/DependencyInjection/MemoizingContainer.php @@ -4,7 +4,7 @@ use function array_key_exists; -class MemoizingContainer implements Container +final class MemoizingContainer implements Container { /** @var array */ diff --git a/src/DependencyInjection/Neon/OptionalPath.php b/src/DependencyInjection/Neon/OptionalPath.php new file mode 100644 index 0000000000..7794f3b872 --- /dev/null +++ b/src/DependencyInjection/Neon/OptionalPath.php @@ -0,0 +1,12 @@ +process([$val->value], $fileKeyToPass, $file); - $val = new Statement($tmp[0], $this->process($val->attributes, $fileKeyToPass, $file)); + if ( + in_array($keyToResolve, [ + '[parameters][excludePaths][]', + '[parameters][excludePaths][analyse][]', + '[parameters][excludePaths][analyseAndScan][]', + ], true) + && count($val->attributes) === 1 + && $val->attributes[0] === '?' + && is_string($val->value) + && !str_contains($val->value, '%') + && !str_starts_with($val->value, '*') + ) { + $fileHelper = $this->createFileHelperByFile($file); + $val = new OptionalPath($fileHelper->normalizePath($fileHelper->absolutizePath($val->value))); + } else { + $tmp = $this->process([$val->value], $fileKeyToPass, $file); + $val = new Statement($tmp[0], $this->process($val->attributes, $fileKeyToPass, $file)); + } } } - $keyToResolve = $fileKey; - if (is_int($key)) { - $keyToResolve .= '[]'; - } else { - $keyToResolve .= '[' . $key . ']'; - } - if (in_array($keyToResolve, [ '[parameters][paths][]', - '[parameters][excludes_analyse][]', '[parameters][excludePaths][]', '[parameters][excludePaths][analyse][]', '[parameters][excludePaths][analyseAndScan][]', @@ -112,15 +130,14 @@ public function process(array $arr, string $fileKey, string $file): array '[parameters][scanFiles][]', '[parameters][scanDirectories][]', '[parameters][tmpDir]', + '[parameters][pro][tmpDir]', '[parameters][memoryLimitFile]', '[parameters][benchmarkFile]', '[parameters][stubFiles][]', - '[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) { + ], true) && is_string($val) && !str_contains($val, '%') && !str_starts_with($val, '*')) { $fileHelper = $this->createFileHelperByFile($file); $val = $fileHelper->normalizePath($fileHelper->absolutizePath($val)); } diff --git a/src/DependencyInjection/NeonLoader.php b/src/DependencyInjection/NeonLoader.php index 4f797d1af7..010b80cfab 100644 --- a/src/DependencyInjection/NeonLoader.php +++ b/src/DependencyInjection/NeonLoader.php @@ -5,7 +5,7 @@ use Nette\DI\Config\Loader; use PHPStan\File\FileHelper; -class NeonLoader extends Loader +final class NeonLoader extends Loader { public function __construct( diff --git a/src/DependencyInjection/Nette/NetteContainer.php b/src/DependencyInjection/Nette/NetteContainer.php index 9d9466c8d7..914d0a43f1 100644 --- a/src/DependencyInjection/Nette/NetteContainer.php +++ b/src/DependencyInjection/Nette/NetteContainer.php @@ -11,7 +11,7 @@ /** * @internal */ -class NetteContainer implements Container +final class NetteContainer implements Container { public function __construct(private \Nette\DI\Container $container) @@ -32,10 +32,9 @@ public function getService(string $serviceName) } /** - * @phpstan-template T of object - * @phpstan-param class-string $className - * @phpstan-return T - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className) { @@ -64,12 +63,12 @@ public function getServicesByTag(string $tagName): array */ public function getParameters(): array { - return $this->container->parameters; + return $this->container->getParameters(); } public function hasParameter(string $parameterName): bool { - return array_key_exists($parameterName, $this->container->parameters); + return array_key_exists($parameterName, $this->container->getParameters()); } /** @@ -81,7 +80,7 @@ public function getParameter(string $parameterName) throw new ParameterNotFoundException($parameterName); } - return $this->container->parameters[$parameterName]; + return $this->container->getParameter($parameterName); } /** diff --git a/src/DependencyInjection/ParameterNotFoundException.php b/src/DependencyInjection/ParameterNotFoundException.php index 07a92a376f..ad461ddef5 100644 --- a/src/DependencyInjection/ParameterNotFoundException.php +++ b/src/DependencyInjection/ParameterNotFoundException.php @@ -5,7 +5,7 @@ use Exception; use function sprintf; -class ParameterNotFoundException extends Exception +final class ParameterNotFoundException extends Exception { public function __construct(string $parameterName) diff --git a/src/DependencyInjection/ParametersSchemaExtension.php b/src/DependencyInjection/ParametersSchemaExtension.php index 3daebbbbad..9a703ed8e9 100644 --- a/src/DependencyInjection/ParametersSchemaExtension.php +++ b/src/DependencyInjection/ParametersSchemaExtension.php @@ -4,20 +4,10 @@ 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 CompilerExtension +final class ParametersSchemaExtension extends CompilerExtension { public function getConfigSchema(): Schema @@ -25,91 +15,4 @@ public function getConfigSchema(): Schema return Expect::arrayOf(Expect::type(Statement::class))->min(1); } - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - if (!$builder->parameters['__validate']) { - return; - } - - /** @var mixed[] $config */ - $config = $this->config; - $config['analysedPaths'] = new Statement(DynamicParameter::class); - $config['analysedPathsFromConfig'] = new Statement(DynamicParameter::class); - $config['singleReflectionFile'] = new Statement(DynamicParameter::class); - $config['singleReflectionInsteadOfFile'] = new Statement(DynamicParameter::class); - $schema = $this->processArgument( - new Statement('schema', [ - new Statement('structure', [$config]), - ]), - ); - $processor = new Processor(); - $processor->onNewContext[] = static function (SchemaContext $context): void { - $context->path = ['parameters']; - }; - $processor->process($schema, $builder->parameters); - } - - /** - * @param Statement[] $statements - */ - private function processSchema(array $statements): Schema - { - if (count($statements) === 0) { - throw new ShouldNotHappenException(); - } - - $parameterSchema = null; - foreach ($statements as $statement) { - $processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments); - if ($parameterSchema === null) { - /** @var Type|AnyOf|Structure $parameterSchema */ - $parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments); - } else { - $parameterSchema->{$statement->getEntity()}(...$processedArguments); - } - } - - $parameterSchema->required(); - - return $parameterSchema; - } - - /** - * @param mixed $argument - * @return mixed - */ - private function processArgument($argument) - { - if ($argument instanceof Statement) { - if ($argument->entity === 'schema') { - $arguments = []; - foreach ($argument->arguments as $schemaArgument) { - if (!$schemaArgument instanceof Statement) { - throw new ShouldNotHappenException('schema() should contain another statement().'); - } - - $arguments[] = $schemaArgument; - } - - if (count($arguments) === 0) { - throw new ShouldNotHappenException('schema() should have at least one argument.'); - } - - return $this->processSchema($arguments); - } - - return $this->processSchema([$argument]); - } elseif (is_array($argument)) { - $processedArray = []; - foreach ($argument as $key => $val) { - $processedArray[$key] = $this->processArgument($val); - } - - return $processedArray; - } - - return $argument; - } - } diff --git a/src/DependencyInjection/ProjectConfigHelper.php b/src/DependencyInjection/ProjectConfigHelper.php new file mode 100644 index 0000000000..47e8481399 --- /dev/null +++ b/src/DependencyInjection/ProjectConfigHelper.php @@ -0,0 +1,66 @@ + $projectConfig + * @return list + */ + public static function getServiceClassNames(array $projectConfig): array + { + $services = array_merge( + $projectConfig['services'] ?? [], + $projectConfig['rules'] ?? [], + ); + $classes = []; + foreach ($services as $service) { + $classes = array_merge($classes, self::getClassesFromConfigDefinition($service)); + if (!is_array($service)) { + continue; + } + + foreach (['class', 'factory', 'implement'] as $key) { + if (!isset($service[$key])) { + continue; + } + + $classes = array_merge($classes, self::getClassesFromConfigDefinition($service[$key])); + } + } + + return array_values(array_unique($classes)); + } + + /** + * @param mixed $definition + * @return string[] + */ + private static function getClassesFromConfigDefinition($definition): array + { + if (is_string($definition)) { + return [$definition]; + } + + if ($definition instanceof Statement) { + $entity = $definition->entity; + if (is_string($entity)) { + return [$entity]; + } elseif (is_array($entity) && isset($entity[0]) && is_string($entity[0])) { + return [$entity[0]]; + } + } + + return []; + } + +} diff --git a/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php b/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php deleted file mode 100644 index fa557b070a..0000000000 --- a/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php +++ /dev/null @@ -1,53 +0,0 @@ -broker = $broker; - } - - public function addPropertiesClassReflectionExtension(PropertiesClassReflectionExtension $extension): void - { - $this->propertiesClassReflectionExtensions[] = $extension; - } - - public function addMethodsClassReflectionExtension(MethodsClassReflectionExtension $extension): void - { - $this->methodsClassReflectionExtensions[] = $extension; - } - - public function getRegistry(): ClassReflectionExtensionRegistry - { - return new ClassReflectionExtensionRegistry( - $this->broker, - $this->propertiesClassReflectionExtensions, - $this->methodsClassReflectionExtensions, - ); - } - -} diff --git a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php index 259600a280..7e47b6498c 100644 --- a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php +++ b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php @@ -2,16 +2,19 @@ namespace PHPStan\DependencyInjection\Reflection; -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\Mixin\MixinMethodsClassReflectionExtension; +use PHPStan\Reflection\Mixin\MixinPropertiesClassReflectionExtension; use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; use function array_merge; -class LazyClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider +final class LazyClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider { private ?ClassReflectionExtensionRegistry $registry = null; @@ -27,10 +30,15 @@ public function getRegistry(): ClassReflectionExtensionRegistry $annotationsMethodsClassReflectionExtension = $this->container->getByType(AnnotationsMethodsClassReflectionExtension::class); $annotationsPropertiesClassReflectionExtension = $this->container->getByType(AnnotationsPropertiesClassReflectionExtension::class); + $mixinMethodsClassReflectionExtension = $this->container->getByType(MixinMethodsClassReflectionExtension::class); + $mixinPropertiesClassReflectionExtension = $this->container->getByType(MixinPropertiesClassReflectionExtension::class); + $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::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsPropertiesClassReflectionExtension, $mixinPropertiesClassReflectionExtension]), + array_merge([$phpClassReflectionExtension], $this->container->getServicesByTag(BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsMethodsClassReflectionExtension, $mixinMethodsClassReflectionExtension]), + $this->container->getServicesByTag(BrokerFactory::ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG), + $this->container->getByType(RequireExtendsPropertiesClassReflectionExtension::class), + $this->container->getByType(RequireExtendsMethodsClassReflectionExtension::class), ); } diff --git a/src/DependencyInjection/RulesExtension.php b/src/DependencyInjection/RulesExtension.php index 391fdb9a1d..f38caac26d 100644 --- a/src/DependencyInjection/RulesExtension.php +++ b/src/DependencyInjection/RulesExtension.php @@ -5,9 +5,9 @@ use Nette\DI\CompilerExtension; use Nette\Schema\Expect; use Nette\Schema\Schema; -use PHPStan\Rules\RegistryFactory; +use PHPStan\Rules\LazyRegistry; -class RulesExtension extends CompilerExtension +final class RulesExtension extends CompilerExtension { public function getConfigSchema(): Schema @@ -25,7 +25,7 @@ public function loadConfiguration(): void $builder->addDefinition($this->prefix((string) $key)) ->setFactory($rule) ->setAutowired($rule) - ->addTag(RegistryFactory::RULE_TAG); + ->addTag(LazyRegistry::RULE_TAG); } } diff --git a/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php b/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php new file mode 100644 index 0000000000..5eb6180ca0 --- /dev/null +++ b/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php @@ -0,0 +1,12 @@ +registry === null) { $this->registry = new DynamicReturnTypeExtensionRegistry( - $this->container->getByType(Broker::class), $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), diff --git a/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php index 4696fb8b21..0eb55cbf5b 100644 --- a/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php +++ b/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php @@ -4,7 +4,7 @@ use PHPStan\DependencyInjection\Container; -class LazyDynamicThrowTypeExtensionProvider implements DynamicThrowTypeExtensionProvider +final class LazyDynamicThrowTypeExtensionProvider implements DynamicThrowTypeExtensionProvider { public const FUNCTION_TAG = 'phpstan.dynamicFunctionThrowTypeExtension'; diff --git a/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php b/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php new file mode 100644 index 0000000000..7136eb40d3 --- /dev/null +++ b/src/DependencyInjection/Type/LazyExpressionTypeResolverExtensionRegistryProvider.php @@ -0,0 +1,29 @@ +registry === null) { + $this->registry = new ExpressionTypeResolverExtensionRegistry( + $this->container->getServicesByTag(BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG), + ); + } + + return $this->registry; + } + +} diff --git a/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php b/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php index 22e356fe70..2be97ee777 100644 --- a/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php +++ b/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php @@ -2,12 +2,11 @@ namespace PHPStan\DependencyInjection\Type; -use PHPStan\Broker\Broker; use PHPStan\Broker\BrokerFactory; use PHPStan\DependencyInjection\Container; use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry; -class LazyOperatorTypeSpecifyingExtensionRegistryProvider implements OperatorTypeSpecifyingExtensionRegistryProvider +final class LazyOperatorTypeSpecifyingExtensionRegistryProvider implements OperatorTypeSpecifyingExtensionRegistryProvider { private ?OperatorTypeSpecifyingExtensionRegistry $registry = null; @@ -20,7 +19,6 @@ 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), ); } diff --git a/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php new file mode 100644 index 0000000000..b2877e5993 --- /dev/null +++ b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php @@ -0,0 +1,33 @@ +container->getServicesByTag(self::FUNCTION_TAG); + } + + public function getMethodParameterClosureTypeExtensions(): array + { + return $this->container->getServicesByTag(self::METHOD_TAG); + } + + public function getStaticMethodParameterClosureTypeExtensions(): array + { + return $this->container->getServicesByTag(self::STATIC_METHOD_TAG); + } + +} diff --git a/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php new file mode 100644 index 0000000000..5fa75939c5 --- /dev/null +++ b/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php @@ -0,0 +1,33 @@ +container->getServicesByTag(self::FUNCTION_TAG); + } + + public function getMethodParameterOutTypeExtensions(): array + { + return $this->container->getServicesByTag(self::METHOD_TAG); + } + + public function getStaticMethodParameterOutTypeExtensions(): array + { + return $this->container->getServicesByTag(self::STATIC_METHOD_TAG); + } + +} diff --git a/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php b/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php new file mode 100644 index 0000000000..817bd6acf1 --- /dev/null +++ b/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php @@ -0,0 +1,21 @@ +getContainerBuilder(); + $excludePaths = $builder->parameters['excludePaths']; + if ($excludePaths === null) { + return; + } + + $newExcludePaths = []; + if (array_key_exists('analyseAndScan', $excludePaths)) { + $newExcludePaths['analyseAndScan'] = $excludePaths['analyseAndScan']; + } + if (array_key_exists('analyse', $excludePaths)) { + $newExcludePaths['analyse'] = $excludePaths['analyse']; + } + + $errors = []; + $suggestOptional = []; + if ($builder->parameters['__validate']) { + foreach ($newExcludePaths as $key => $paths) { + foreach ($paths as $path) { + if ($path instanceof OptionalPath) { + continue; + } + if (FileExcluder::isAbsolutePath($path)) { + if (is_dir($path)) { + continue; + } + if (is_file($path)) { + continue; + } + } + if (FileExcluder::isFnmatchPattern($path)) { + continue; + } + + $suggestOptional[$key][] = $path; + $errors[] = sprintf('Path "%s" is neither a directory, nor a file path, nor a fnmatch pattern.', $path); + } + } + } + + if (count($errors) !== 0) { + throw new InvalidExcludePathsException($errors, $suggestOptional); + } + + foreach ($newExcludePaths as $key => $p) { + $newExcludePaths[$key] = array_map( + static fn ($path) => $path instanceof OptionalPath ? $path->path : $path, + $p, + ); + } + + $builder->parameters['excludePaths'] = $newExcludePaths; + } + +} diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index 260cee8f6b..68a41d3e9f 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -7,8 +7,13 @@ use Nette\DI\CompilerExtension; use Nette\Utils\RegexpException; use Nette\Utils\Strings; +use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; use PHPStan\Command\IgnoredRegexValidator; +use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; +use PHPStan\File\FileExcluder; +use PHPStan\Php\ComposerPhpVersionFactory; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeNodeResolverExtensionRegistry; @@ -16,10 +21,15 @@ use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\TypeParser; +use PHPStan\PhpDocParser\ParserConfig; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; use PHPStan\Reflection\ReflectionProvider\DummyReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Type\Constant\OversizedArrayBuilder; use PHPStan\Type\DirectTypeAliasResolverProvider; +use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry; use PHPStan\Type\Type; use PHPStan\Type\TypeAliasResolver; use function array_keys; @@ -27,9 +37,12 @@ use function count; use function implode; use function is_array; +use function is_dir; +use function is_file; use function sprintf; +use const PHP_VERSION_ID; -class ValidateIgnoredErrorsExtension extends CompilerExtension +final class ValidateIgnoredErrorsExtension extends CompilerExtension { /** @@ -48,14 +61,20 @@ public function loadConfiguration(): void } /** @throws void */ - $parser = Llk::load(new Read('hoa://Library/Regex/Grammar.pp')); + $parser = Llk::load(new Read(__DIR__ . '/../../resources/RegexGrammar.pp')); $reflectionProvider = new DummyReflectionProvider(); + $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); ReflectionProviderStaticAccessor::registerInstance($reflectionProvider); + PhpVersionStaticAccessor::registerInstance(new PhpVersion(PHP_VERSION_ID)); + $composerPhpVersionFactory = new ComposerPhpVersionFactory([]); + $constantResolver = new ConstantResolver($reflectionProviderProvider, [], null, $composerPhpVersionFactory); + + $phpDocParserConfig = new ParserConfig([]); $ignoredRegexValidator = new IgnoredRegexValidator( $parser, new TypeStringResolver( - new Lexer(), - new TypeParser(new ConstExprParser()), + new Lexer($phpDocParserConfig), + new TypeParser($phpDocParserConfig, new ConstExprParser($phpDocParserConfig)), new TypeNodeResolver( new DirectTypeNodeResolverExtensionRegistryProvider( new class implements TypeNodeResolverExtensionRegistry { @@ -67,7 +86,7 @@ public function getExtensions(): array }, ), - new DirectReflectionProviderProvider($reflectionProvider), + $reflectionProviderProvider, new DirectTypeAliasResolverProvider(new class implements TypeAliasResolver { public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool @@ -81,38 +100,76 @@ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type } }), + $constantResolver, + new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), new class implements OperatorTypeSpecifyingExtensionRegistryProvider { + + public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry + { + return new OperatorTypeSpecifyingExtensionRegistry([]); + } + + }, new OversizedArrayBuilder(), true), ), ), ); - $errors = []; + $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']; + if (is_array($ignoreError)) { + if (isset($ignoreError['count'])) { + continue; // ignoreError coming from baseline will be correct + } + if (isset($ignoreError['messages'])) { + $ignoreMessages = $ignoreError['messages']; + } elseif (isset($ignoreError['message'])) { + $ignoreMessages = [$ignoreError['message']]; } else { - $ignoreMessage = $ignoreError; + continue; + } + } else { + $ignoreMessages = [$ignoreError]; + } + + foreach ($ignoreMessages as $ignoreMessage) { + $error = $this->validateMessage($ignoredRegexValidator, $ignoreMessage); + if ($error === null) { + continue; } + $errors[] = $error; + } + } + + $reportUnmatched = (bool) $builder->parameters['reportUnmatchedIgnoredErrors']; - Strings::match('', $ignoreMessage); - $validationResult = $ignoredRegexValidator->validate($ignoreMessage); - $ignoredTypes = $validationResult->getIgnoredTypes(); - if (count($ignoredTypes) > 0) { - $errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes); + if ($reportUnmatched) { + foreach ($ignoreErrors as $ignoreError) { + if (!is_array($ignoreError)) { + continue; } - if ($validationResult->hasAnchorsInTheMiddle()) { - $errors[] = $this->createAnchorInTheMiddleError($ignoreMessage); + if (isset($ignoreError['path'])) { + $ignorePaths = [$ignoreError['path']]; + } elseif (isset($ignoreError['paths'])) { + $ignorePaths = $ignoreError['paths']; + } else { + continue; } - 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()); + foreach ($ignorePaths as $ignorePath) { + if (FileExcluder::isAbsolutePath($ignorePath)) { + if (is_dir($ignorePath)) { + continue; + } + if (is_file($ignorePath)) { + continue; + } + } + if (FileExcluder::isFnmatchPattern($ignorePath)) { + continue; + } + + $errors[] = sprintf('Path "%s" is neither a directory, nor a file path, nor a fnmatch pattern.', $ignorePath); } - } catch (RegexpException $e) { - $errors[] = $e->getMessage(); } } @@ -123,6 +180,29 @@ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type throw new InvalidIgnoredErrorPatternsException($errors); } + private function validateMessage(IgnoredRegexValidator $ignoredRegexValidator, string $ignoreMessage): ?string + { + try { + Strings::match('', $ignoreMessage); + $validationResult = $ignoredRegexValidator->validate($ignoreMessage); + $ignoredTypes = $validationResult->getIgnoredTypes(); + if (count($ignoredTypes) > 0) { + return $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes); + } + + if ($validationResult->hasAnchorsInTheMiddle()) { + return $this->createAnchorInTheMiddleError($ignoreMessage); + } + + if ($validationResult->areAllErrorsIgnored()) { + return 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) { + return $e->getMessage(); + } + return null; + } + /** * @param array $ignoredTypes */ diff --git a/src/Diagnose/DiagnoseExtension.php b/src/Diagnose/DiagnoseExtension.php new file mode 100644 index 0000000000..084134495a --- /dev/null +++ b/src/Diagnose/DiagnoseExtension.php @@ -0,0 +1,31 @@ +writeLineFormatted(sprintf( + 'PHP runtime version: %s', + $phpRuntimeVersion->getVersionString(), + )); + + if ( + $this->phpVersion->getSource() === PhpVersion::SOURCE_CONFIG + && is_array($this->configPhpVersion) + ) { + $minVersion = new PhpVersion($this->configPhpVersion['min']); + $maxVersion = new PhpVersion($this->configPhpVersion['max']); + + $output->writeLineFormatted(sprintf( + 'PHP version for analysis: %s-%s (from %s)', + $minVersion->getVersionString(), + $maxVersion->getVersionString(), + $this->phpVersion->getSourceLabel(), + )); + + } else { + $minComposerPhpVersion = $this->composerPhpVersionFactory->getMinVersion(); + $maxComposerPhpVersion = $this->composerPhpVersionFactory->getMaxVersion(); + if ($minComposerPhpVersion !== null && $maxComposerPhpVersion !== null) { + if ($minComposerPhpVersion->getVersionId() !== $maxComposerPhpVersion->getVersionId()) { + $output->writeLineFormatted(sprintf( + 'PHP composer.json required version: %s-%s', + $minComposerPhpVersion->getVersionString(), + $maxComposerPhpVersion->getVersionString(), + )); + } else { + $output->writeLineFormatted(sprintf( + 'PHP composer.json required version: %s', + $minComposerPhpVersion->getVersionString(), + )); + } + } + + $output->writeLineFormatted(sprintf( + 'PHP version for analysis: %s (from %s)', + $this->phpVersion->getVersionString(), + $this->phpVersion->getSourceLabel(), + )); + } + $output->writeLineFormatted(''); + + $output->writeLineFormatted(sprintf( + 'PHPStan version: %s', + ComposerHelper::getPhpStanVersion(), + )); + $output->writeLineFormatted('PHPStan running from:'); + $pharRunning = Phar::running(false); + if ($pharRunning !== '') { + $output->writeLineFormatted(dirname($pharRunning)); + } else { + if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { + $output->writeLineFormatted($_SERVER['argv'][0]); + } else { + $output->writeLineFormatted('Unknown'); + } + } + $output->writeLineFormatted(''); + + $configFilesFromExtensionInstaller = []; + if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) { + $output->writeLineFormatted('Extension installer:'); + if (count(GeneratedConfig::EXTENSIONS) === 0) { + $output->writeLineFormatted('No extensions installed'); + } + + $generatedConfigReflection = new ReflectionClass('PHPStan\ExtensionInstaller\GeneratedConfig'); + $generatedConfigDirectory = dirname($generatedConfigReflection->getFileName()); + foreach (GeneratedConfig::EXTENSIONS as $name => $extensionConfig) { + $output->writeLineFormatted(sprintf('%s: %s', $name, $extensionConfig['version'] ?? 'Unknown version')); + foreach ($extensionConfig['extra']['includes'] ?? [] as $includedFile) { + $includedFilePath = null; + if (isset($extensionConfig['relative_install_path'])) { + $includedFilePath = sprintf('%s/%s/%s', $generatedConfigDirectory, $extensionConfig['relative_install_path'], $includedFile); + if (!is_file($includedFilePath) || !is_readable($includedFilePath)) { + $includedFilePath = null; + } + } + + if ($includedFilePath === null) { + $includedFilePath = sprintf('%s/%s', $extensionConfig['install_path'], $includedFile); + } + + $configFilesFromExtensionInstaller[] = $this->fileHelper->normalizePath($includedFilePath, '/'); + } + } + } else { + $output->writeLineFormatted('Extension installer: Not installed'); + } + $output->writeLineFormatted(''); + + $thirdPartyIncludedConfigs = []; + foreach ($this->allConfigFiles as $configFile) { + $configFile = $this->fileHelper->normalizePath($configFile, '/'); + if (in_array($configFile, $configFilesFromExtensionInstaller, true)) { + continue; + } + foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { + $composerConfig = ComposerHelper::getComposerConfig($composerAutoloaderProjectPath); + if ($composerConfig === null) { + continue; + } + $vendorDir = $this->fileHelper->normalizePath(ComposerHelper::getVendorDirFromComposerConfig($composerAutoloaderProjectPath, $composerConfig), '/'); + if (!str_starts_with($configFile, $vendorDir)) { + continue; + } + + $installedPath = $vendorDir . '/composer/installed.php'; + if (!is_file($installedPath)) { + continue; + } + + $installed = require $installedPath; + + $trimmed = substr($configFile, strlen($vendorDir) + 1); + $parts = explode('/', $trimmed); + $package = implode('/', array_slice($parts, 0, 2)); + $configPath = implode('/', array_slice($parts, 2)); + if (!array_key_exists($package, $installed['versions'])) { + continue; + } + + $packageVersion = $installed['versions'][$package]['pretty_version'] ?? null; + if ($packageVersion === null) { + continue; + } + + $thirdPartyIncludedConfigs[] = [$package, $packageVersion, $configPath]; + } + } + + if (count($thirdPartyIncludedConfigs) > 0) { + $output->writeLineFormatted('Included configs from Composer packages:'); + foreach ($thirdPartyIncludedConfigs as [$package, $packageVersion, $configPath]) { + $output->writeLineFormatted(sprintf('%s (%s): %s', $package, $configPath, $packageVersion)); + } + $output->writeLineFormatted(''); + } + + $composerAutoloaderProjectPathsCount = count($this->composerAutoloaderProjectPaths); + $output->writeLineFormatted(sprintf( + 'Discovered Composer project %s:', + $composerAutoloaderProjectPathsCount === 1 ? 'root' : 'roots', + )); + if ($composerAutoloaderProjectPathsCount === 0) { + $output->writeLineFormatted('None'); + } + foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { + $output->writeLineFormatted($composerAutoloaderProjectPath); + } + $output->writeLineFormatted(''); + } + +} diff --git a/src/File/CouldNotReadFileException.php b/src/File/CouldNotReadFileException.php index 5803fd53df..63d5a2e41d 100644 --- a/src/File/CouldNotReadFileException.php +++ b/src/File/CouldNotReadFileException.php @@ -5,7 +5,7 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class CouldNotReadFileException extends AnalysedCodeException +final class CouldNotReadFileException extends AnalysedCodeException { public function __construct(string $fileName) diff --git a/src/File/CouldNotWriteFileException.php b/src/File/CouldNotWriteFileException.php index 0fffa54dcf..72e00464b7 100644 --- a/src/File/CouldNotWriteFileException.php +++ b/src/File/CouldNotWriteFileException.php @@ -5,7 +5,7 @@ use PHPStan\AnalysedCodeException; use function sprintf; -class CouldNotWriteFileException extends AnalysedCodeException +final class CouldNotWriteFileException extends AnalysedCodeException { public function __construct(string $fileName, string $error) diff --git a/src/File/FileExcluder.php b/src/File/FileExcluder.php index 3c5cf85f3b..ba314cbb67 100644 --- a/src/File/FileExcluder.php +++ b/src/File/FileExcluder.php @@ -2,26 +2,42 @@ namespace PHPStan\File; -use function array_merge; use function fnmatch; use function in_array; +use function is_dir; +use function is_file; use function preg_match; +use function str_starts_with; use function strlen; -use function strpos; +use function substr; use const DIRECTORY_SEPARATOR; use const FNM_CASEFOLD; use const FNM_NOESCAPE; -class FileExcluder +final class FileExcluder { /** - * Directories to exclude from analysing + * Paths to exclude from analysing * * @var string[] */ private array $literalAnalyseExcludes = []; + /** + * Directories to exclude from analysing + * + * @var string[] + */ + private array $literalAnalyseDirectoryExcludes = []; + + /** + * Files to exclude from analysing + * + * @var string[] + */ + private array $literalAnalyseFilesExcludes = []; + /** * fnmatch() patterns to use for excluding files and directories from analysing * @var string[] @@ -32,15 +48,13 @@ class FileExcluder /** * @param string[] $analyseExcludes - * @param string[] $stubFiles */ public function __construct( - FileHelper $fileHelper, + private FileHelper $fileHelper, array $analyseExcludes, - array $stubFiles, ) { - foreach (array_merge($analyseExcludes, $stubFiles) as $exclude) { + foreach ($analyseExcludes as $exclude) { $len = strlen($exclude); $trailingDirSeparator = ($len > 0 && in_array($exclude[$len - 1], ['\\', '/'], true)); @@ -50,10 +64,18 @@ public function __construct( $normalized .= DIRECTORY_SEPARATOR; } - if ($this->isFnmatchPattern($normalized)) { + if (self::isFnmatchPattern($normalized)) { $this->fnmatchAnalyseExcludes[] = $normalized; } else { - $this->literalAnalyseExcludes[] = $fileHelper->absolutizePath($normalized); + if (is_file($normalized)) { + $this->literalAnalyseFilesExcludes[] = $normalized; + } elseif (is_dir($normalized)) { + if (!$trailingDirSeparator) { + $normalized .= DIRECTORY_SEPARATOR; + } + + $this->literalAnalyseDirectoryExcludes[] = $normalized; + } } } @@ -67,8 +89,20 @@ public function __construct( public function isExcludedFromAnalysing(string $file): bool { + $file = $this->fileHelper->normalizePath($file); + foreach ($this->literalAnalyseExcludes as $exclude) { - if (strpos($file, $exclude) === 0) { + if (str_starts_with($file, $exclude)) { + return true; + } + } + foreach ($this->literalAnalyseDirectoryExcludes as $exclude) { + if (str_starts_with($file, $exclude)) { + return true; + } + } + foreach ($this->literalAnalyseFilesExcludes as $exclude) { + if ($file === $exclude) { return true; } } @@ -81,7 +115,20 @@ public function isExcludedFromAnalysing(string $file): bool return false; } - private function isFnmatchPattern(string $path): bool + public static function isAbsolutePath(string $path): bool + { + if (DIRECTORY_SEPARATOR === '/') { + if (str_starts_with($path, '/')) { + return true; + } + } elseif (substr($path, 1, 1) === ':') { + return true; + } + + return false; + } + + public static function isFnmatchPattern(string $path): bool { return preg_match('~[*?[\]]~', $path) > 0; } diff --git a/src/File/FileExcluderFactory.php b/src/File/FileExcluderFactory.php index 26976a3dfb..0bdae44c20 100644 --- a/src/File/FileExcluderFactory.php +++ b/src/File/FileExcluderFactory.php @@ -7,27 +7,21 @@ use function array_unique; use function array_values; -class FileExcluderFactory +final class FileExcluderFactory { /** - * @param string[] $obsoleteExcludesAnalyse - * @param array{analyse?: array, analyseAndScan?: array}|null $excludePaths + * @param array{analyse?: array, analyseAndScan?: array} $excludePaths */ public function __construct( private FileExcluderRawFactory $fileExcluderRawFactory, - private array $obsoleteExcludesAnalyse, - private ?array $excludePaths, + private array $excludePaths, ) { } public function createAnalyseFileExcluder(): FileExcluder { - if ($this->excludePaths === null) { - return $this->fileExcluderRawFactory->create($this->obsoleteExcludesAnalyse); - } - $paths = []; if (array_key_exists('analyse', $this->excludePaths)) { $paths = $this->excludePaths['analyse']; @@ -41,10 +35,6 @@ public function createAnalyseFileExcluder(): FileExcluder public function createScanFileExcluder(): FileExcluder { - if ($this->excludePaths === null) { - return $this->fileExcluderRawFactory->create($this->obsoleteExcludesAnalyse); - } - $paths = []; if (array_key_exists('analyseAndScan', $this->excludePaths)) { $paths = $this->excludePaths['analyseAndScan']; diff --git a/src/File/FileFinder.php b/src/File/FileFinder.php index d549d9c2cc..34ab2c5a16 100644 --- a/src/File/FileFinder.php +++ b/src/File/FileFinder.php @@ -4,12 +4,13 @@ use Symfony\Component\Finder\Finder; use function array_filter; +use function array_unique; use function array_values; use function file_exists; use function implode; use function is_file; -class FileFinder +final class FileFinder { /** @@ -45,7 +46,7 @@ public function findFiles(array $paths): FileFinderResult } } - $files = array_values(array_filter($files, fn (string $file): bool => !$this->fileExcluder->isExcludedFromAnalysing($file))); + $files = array_values(array_unique(array_filter($files, fn (string $file): bool => !$this->fileExcluder->isExcludedFromAnalysing($file)))); return new FileFinderResult($files, $onlyFiles); } diff --git a/src/File/FileFinderResult.php b/src/File/FileFinderResult.php index d88bcbb84c..7ae7634c96 100644 --- a/src/File/FileFinderResult.php +++ b/src/File/FileFinderResult.php @@ -2,7 +2,7 @@ namespace PHPStan\File; -class FileFinderResult +final class FileFinderResult { /** diff --git a/src/File/FileHelper.php b/src/File/FileHelper.php index 06bfcdcf11..32fad2d253 100644 --- a/src/File/FileHelper.php +++ b/src/File/FileHelper.php @@ -7,15 +7,18 @@ use function explode; use function implode; use function ltrim; +use function preg_match; use function rtrim; +use function str_ends_with; use function str_replace; use function str_starts_with; -use function strpos; +use function strlen; +use function strtolower; use function substr; use function trim; use const DIRECTORY_SEPARATOR; -class FileHelper +final class FileHelper { private string $workingDirectory; @@ -34,15 +37,14 @@ public function getWorkingDirectory(): string public function absolutizePath(string $path): string { if (DIRECTORY_SEPARATOR === '/') { - if (substr($path, 0, 1) === '/') { - return $path; - } - } else { - if (substr($path, 1, 1) === ':') { + if (str_starts_with($path, '/')) { return $path; } + } elseif (substr($path, 1, 1) === ':') { + return $path; } - if (str_starts_with($path, 'phar://')) { + + if (preg_match('~^[a-z0-9+\-.]+://~i', $path) === 1) { return $path; } @@ -52,15 +54,23 @@ public function absolutizePath(string $path): string /** @api */ public function normalizePath(string $originalPath, string $directorySeparator = DIRECTORY_SEPARATOR): string { - $isLocalPath = $originalPath !== '' && $originalPath[0] === '/'; + $isLocalPath = false; + if ($originalPath !== '') { + if ($originalPath[0] === '/') { + $isLocalPath = true; + } elseif (strlen($originalPath) >= 3 && $originalPath[1] === ':' && $originalPath[2] === '\\') { // e.g. C:\ + $isLocalPath = true; + } + } $matches = null; if (!$isLocalPath) { - $matches = Strings::match($originalPath, '~^([a-z]+)\\:\\/\\/(.+)~'); + $matches = Strings::match($originalPath, '~^([a-z0-9+\-.]+)://(.+)$~is'); } if ($matches !== null) { [, $scheme, $path] = $matches; + $scheme = strtolower($scheme); } else { $scheme = null; $path = $originalPath; @@ -68,7 +78,7 @@ public function normalizePath(string $originalPath, string $directorySeparator = $path = str_replace(['\\', '//', '///', '////'], '/', $path); - $pathRoot = strpos($path, '/') === 0 ? $directorySeparator : ''; + $pathRoot = str_starts_with($path, '/') ? $directorySeparator : ''; $pathParts = explode('/', trim($path, '/')); $normalizedPathParts = []; @@ -77,12 +87,10 @@ public function normalizePath(string $originalPath, string $directorySeparator = continue; } if ($pathPart === '..') { - /** @var string $removedPart */ $removedPart = array_pop($normalizedPathParts); - if ($scheme === 'phar' && substr($removedPart, -5) === '.phar') { + if ($scheme === 'phar' && $removedPart !== null && str_ends_with($removedPart, '.phar')) { $scheme = null; } - } else { $normalizedPathParts[] = $pathPart; } diff --git a/src/File/FileMonitor.php b/src/File/FileMonitor.php index 52d91b0729..6fd0eaf8ef 100644 --- a/src/File/FileMonitor.php +++ b/src/File/FileMonitor.php @@ -6,9 +6,9 @@ use function array_key_exists; use function array_keys; use function count; -use function sha1; +use function sha1_file; -class FileMonitor +final class FileMonitor { /** @var array|null */ @@ -81,7 +81,13 @@ public function getChanges(): FileMonitorResult private function getFileHash(string $filePath): string { - return sha1(FileReader::read($filePath)); + $hash = sha1_file($filePath); + + if ($hash === false) { + throw new CouldNotReadFileException($filePath); + } + + return $hash; } } diff --git a/src/File/FileMonitorResult.php b/src/File/FileMonitorResult.php index 9da77e0bcc..8c7e405dc0 100644 --- a/src/File/FileMonitorResult.php +++ b/src/File/FileMonitorResult.php @@ -4,7 +4,7 @@ use function count; -class FileMonitorResult +final class FileMonitorResult { /** @@ -21,6 +21,14 @@ public function __construct( { } + /** + * @return string[] + */ + public function getChangedFiles(): array + { + return $this->changedFiles; + } + public function hasAnyChanges(): bool { return count($this->newFiles) > 0 diff --git a/src/File/FileReader.php b/src/File/FileReader.php index 5bc5220ae7..7f5a8dc369 100644 --- a/src/File/FileReader.php +++ b/src/File/FileReader.php @@ -3,17 +3,29 @@ namespace PHPStan\File; use function file_get_contents; -use function is_file; +use function stream_resolve_include_path; -class FileReader +final class FileReader { + /** + * @throws CouldNotReadFileException + */ public static function read(string $fileName): string { - if (!is_file($fileName)) { - throw new CouldNotReadFileException($fileName); + $path = $fileName; + + $contents = @file_get_contents($path); + if ($contents === false) { + $path = stream_resolve_include_path($fileName); + + if ($path === false) { + throw new CouldNotReadFileException($fileName); + } + + $contents = @file_get_contents($path); } - $contents = @file_get_contents($fileName); + if ($contents === false) { throw new CouldNotReadFileException($fileName); } diff --git a/src/File/FileWriter.php b/src/File/FileWriter.php index 2a40c35bb1..b3659d9536 100644 --- a/src/File/FileWriter.php +++ b/src/File/FileWriter.php @@ -5,7 +5,7 @@ use function error_get_last; use function file_put_contents; -class FileWriter +final class FileWriter { public static function write(string $fileName, string $contents): void diff --git a/src/File/FuzzyRelativePathHelper.php b/src/File/FuzzyRelativePathHelper.php index 47452d60a3..cf56ee124a 100644 --- a/src/File/FuzzyRelativePathHelper.php +++ b/src/File/FuzzyRelativePathHelper.php @@ -9,12 +9,12 @@ use function ltrim; use function realpath; use function str_ends_with; +use function str_starts_with; use function strlen; -use function strpos; use function substr; use const DIRECTORY_SEPARATOR; -class FuzzyRelativePathHelper implements RelativePathHelper +final class FuzzyRelativePathHelper implements RelativePathHelper { private string $directorySeparator; @@ -40,7 +40,7 @@ public function __construct( $pathBeginning = null; $pathToTrimArray = null; $trimBeginning = static function (string $path): array { - if (substr($path, 0, 1) === '/') { + if (str_starts_with($path, '/')) { return [ '/', substr($path, 1), @@ -61,17 +61,16 @@ public function __construct( ) { [$pathBeginning, $currentWorkingDirectory] = $trimBeginning($currentWorkingDirectory); - /** @var string[] $pathToTrimArray */ $pathToTrimArray = explode($directorySeparator, $currentWorkingDirectory); } foreach ($analysedPaths as $pathNumber => $path) { [$tempPathBeginning, $path] = $trimBeginning($path); - /** @var string[] $pathArray */ $pathArray = explode($directorySeparator, $path); $pathTempParts = []; + $pathArraySize = count($pathArray); foreach ($pathArray as $i => $pathPart) { - if (str_ends_with($pathPart, '.php')) { + if ($i === $pathArraySize - 1 && str_ends_with($pathPart, '.php')) { continue; } if (!isset($pathToTrimArray[$i])) { @@ -108,7 +107,7 @@ public function getRelativePath(string $filename): string { if ( $this->pathToTrim !== null - && strpos($filename, $this->pathToTrim) === 0 + && str_starts_with($filename, $this->pathToTrim) ) { return ltrim(substr($filename, strlen($this->pathToTrim)), $this->directorySeparator); } diff --git a/src/File/NullRelativePathHelper.php b/src/File/NullRelativePathHelper.php index 1556984a90..5e5a07dc62 100644 --- a/src/File/NullRelativePathHelper.php +++ b/src/File/NullRelativePathHelper.php @@ -2,7 +2,7 @@ namespace PHPStan\File; -class NullRelativePathHelper implements RelativePathHelper +final class NullRelativePathHelper implements RelativePathHelper { public function getRelativePath(string $filename): string diff --git a/src/File/ParentDirectoryRelativePathHelper.php b/src/File/ParentDirectoryRelativePathHelper.php index 218d4597fc..7654b3ce1e 100644 --- a/src/File/ParentDirectoryRelativePathHelper.php +++ b/src/File/ParentDirectoryRelativePathHelper.php @@ -14,7 +14,7 @@ use function substr; use function trim; -class ParentDirectoryRelativePathHelper implements RelativePathHelper +final class ParentDirectoryRelativePathHelper implements RelativePathHelper { public function __construct(private string $parentDirectory) diff --git a/src/File/PathNotFoundException.php b/src/File/PathNotFoundException.php index fb8f4dd191..b58cda6e15 100644 --- a/src/File/PathNotFoundException.php +++ b/src/File/PathNotFoundException.php @@ -5,7 +5,7 @@ use Exception; use function sprintf; -class PathNotFoundException extends Exception +final class PathNotFoundException extends Exception { public function __construct(private string $path) diff --git a/src/File/SimpleRelativePathHelper.php b/src/File/SimpleRelativePathHelper.php index 14181895ca..eff28f5541 100644 --- a/src/File/SimpleRelativePathHelper.php +++ b/src/File/SimpleRelativePathHelper.php @@ -3,11 +3,11 @@ namespace PHPStan\File; use function str_replace; +use function str_starts_with; use function strlen; -use function strpos; use function substr; -class SimpleRelativePathHelper implements RelativePathHelper +final class SimpleRelativePathHelper implements RelativePathHelper { public function __construct(private string $currentWorkingDirectory) @@ -16,7 +16,7 @@ public function __construct(private string $currentWorkingDirectory) public function getRelativePath(string $filename): string { - if ($this->currentWorkingDirectory !== '' && strpos($filename, $this->currentWorkingDirectory) === 0) { + if ($this->currentWorkingDirectory !== '' && str_starts_with($filename, $this->currentWorkingDirectory)) { return str_replace('\\', '/', substr($filename, strlen($this->currentWorkingDirectory) + 1)); } diff --git a/src/File/SystemAgnosticSimpleRelativePathHelper.php b/src/File/SystemAgnosticSimpleRelativePathHelper.php new file mode 100644 index 0000000000..bd4ac68d67 --- /dev/null +++ b/src/File/SystemAgnosticSimpleRelativePathHelper.php @@ -0,0 +1,26 @@ +fileHelper->getWorkingDirectory(); + if ($cwd !== '' && str_starts_with($filename, $cwd)) { + return substr($filename, strlen($cwd) + 1); + } + + return $filename; + } + +} diff --git a/src/Internal/ArrayHelper.php b/src/Internal/ArrayHelper.php new file mode 100644 index 0000000000..2498f61efb --- /dev/null +++ b/src/Internal/ArrayHelper.php @@ -0,0 +1,27 @@ + $path + */ + public static function unsetKeyAtPath(array &$array, array $path): void + { + [$head, $tail] = [$path[0], array_slice($path, 1)]; + + if (count($tail) === 0) { + unset($array[$head]); + + } elseif (isset($array[$head])) { + self::unsetKeyAtPath($array[$head], $tail); + } + } + +} diff --git a/src/Internal/BytesHelper.php b/src/Internal/BytesHelper.php index 48a852c0a3..3279501f51 100644 --- a/src/Internal/BytesHelper.php +++ b/src/Internal/BytesHelper.php @@ -7,7 +7,7 @@ use function end; use function round; -class BytesHelper +final class BytesHelper { public static function bytes(int $bytes): string diff --git a/src/Internal/CombinationsHelper.php b/src/Internal/CombinationsHelper.php new file mode 100644 index 0000000000..feeb550400 --- /dev/null +++ b/src/Internal/CombinationsHelper.php @@ -0,0 +1,35 @@ + $arrays + * @return iterable + */ + public static function combinations(array $arrays): iterable + { + // from https://stackoverflow.com/a/70800936/565782 by Arnaud Le Blanc + if ($arrays === []) { + yield []; + return; + } + + $head = array_shift($arrays); + + foreach ($head as $elem) { + foreach (self::combinations($arrays) as $combination) { + $comb = [$elem]; + foreach ($combination as $c) { + $comb[] = $c; + } + yield $comb; + } + } + } + +} diff --git a/src/Internal/ComposerHelper.php b/src/Internal/ComposerHelper.php new file mode 100644 index 0000000000..e1995bc34d --- /dev/null +++ b/src/Internal/ComposerHelper.php @@ -0,0 +1,91 @@ +|null */ + public static function getComposerConfig(string $root): ?array + { + $composerJsonPath = self::getComposerJsonPath($root); + + if (!is_file($composerJsonPath)) { + return null; + } + + try { + $composerJsonContents = FileReader::read($composerJsonPath); + + return Json::decode($composerJsonContents, Json::FORCE_ARRAY); + } catch (CouldNotReadFileException | JsonException) { + return null; + } + } + + private static function getComposerJsonPath(string $root): string + { + $envComposer = getenv('COMPOSER'); + $fileName = is_string($envComposer) ? $envComposer : 'composer.json'; + $fileName = basename(trim($fileName)); + + return $root . '/' . $fileName; + } + + /** + * @param array $composerConfig + */ + public static function getVendorDirFromComposerConfig(string $root, array $composerConfig): string + { + $vendorDirectory = $composerConfig['config']['vendor-dir'] ?? 'vendor'; + + return $root . '/' . trim($vendorDirectory, '/'); + } + + /** + * @param array $composerConfig + */ + public static function getBinDirFromComposerConfig(string $root, array $composerConfig): string + { + $vendorDirectory = $composerConfig['config']['bin-dir'] ?? 'vendor/bin'; + + return $root . '/' . trim($vendorDirectory, '/'); + } + + public static function getPhpStanVersion(): string + { + if (self::$phpstanVersion !== null) { + return self::$phpstanVersion; + } + + $installed = require __DIR__ . '/../../vendor/composer/installed.php'; + $rootPackage = $installed['root'] ?? null; + if ($rootPackage === null) { + return self::$phpstanVersion = self::UNKNOWN_VERSION; + } + + if (preg_match('/[^v\d.]/', $rootPackage['pretty_version']) === 0) { + // Handles tagged versions, see https://github.com/Jean85/pretty-package-versions/blob/2.0.5/src/Version.php#L31 + return self::$phpstanVersion = $rootPackage['pretty_version']; + } + + return self::$phpstanVersion = $rootPackage['pretty_version'] . '@' . substr((string) $rootPackage['reference'], 0, 7); + } + +} diff --git a/src/Internal/ContainerDynamicReturnTypeExtension.php b/src/Internal/ContainerDynamicReturnTypeExtension.php deleted file mode 100644 index afb196dbbd..0000000000 --- a/src/Internal/ContainerDynamicReturnTypeExtension.php +++ /dev/null @@ -1,55 +0,0 @@ -getName(), [ - 'getByType', - ], true); - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - if (count($methodCall->getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - $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->getArgs()) >= 2) { - $argType = $scope->getType($methodCall->getArgs()[1]->value); - if ($argType instanceof ConstantBooleanType && $argType->getValue()) { - $type = TypeCombinator::addNull($type); - } - } - - return $type; - } - -} diff --git a/src/Internal/DeprecatedAttributeHelper.php b/src/Internal/DeprecatedAttributeHelper.php new file mode 100644 index 0000000000..8217fa1ef4 --- /dev/null +++ b/src/Internal/DeprecatedAttributeHelper.php @@ -0,0 +1,45 @@ + $attributes + */ + public static function getDeprecatedDescription(array $attributes): ?string + { + $deprecated = ReflectionAttributeHelper::filterAttributesByName($attributes, 'Deprecated'); + foreach ($deprecated as $attr) { + $arguments = $attr->getArguments(); + foreach ($arguments as $i => $arg) { + if (!is_string($arg)) { + continue; + } + + if (is_int($i)) { + if ($i !== 0) { + continue; + } + + return $arg; + } + + if ($i !== 'message') { + continue; + } + + return $arg; + } + } + + return null; + } + +} diff --git a/src/Internal/DirectoryCreator.php b/src/Internal/DirectoryCreator.php new file mode 100644 index 0000000000..3ed7c2ff11 --- /dev/null +++ b/src/Internal/DirectoryCreator.php @@ -0,0 +1,36 @@ +typeSpecifier = $typeSpecifier; - } - - public function getClass(): string - { - return ClassMemberAccessAnswerer::class; - } - - public function isMethodSupported( - MethodReflection $methodReflection, - MethodCall $node, - TypeSpecifierContext $context, - ): bool - { - return $methodReflection->getName() === $this->isInMethodName - && !$context->null(); - } - - public function specifyTypes( - MethodReflection $methodReflection, - MethodCall $node, - Scope $scope, - TypeSpecifierContext $context, - ): SpecifiedTypes - { - $scopeClass = $this->reflectionProvider->getClass(Scope::class); - $methodVariants = $scopeClass - ->getMethod($this->removeNullMethodName, $scope) - ->getVariants(); - - return $this->typeSpecifier->create( - new MethodCall($node->var, $this->removeNullMethodName), - TypeCombinator::removeNull( - ParametersAcceptorSelector::selectSingle($methodVariants)->getReturnType(), - ), - $context, - false, - $scope, - ); - } - -} diff --git a/src/Internal/SprintfHelper.php b/src/Internal/SprintfHelper.php index 30d08500fa..6938c898f1 100644 --- a/src/Internal/SprintfHelper.php +++ b/src/Internal/SprintfHelper.php @@ -4,7 +4,7 @@ use function str_replace; -class SprintfHelper +final class SprintfHelper { public static function escapeFormatString(string $format): string diff --git a/src/Internal/UnionTypeGetInternalDynamicReturnTypeExtension.php b/src/Internal/UnionTypeGetInternalDynamicReturnTypeExtension.php deleted file mode 100644 index d193023133..0000000000 --- a/src/Internal/UnionTypeGetInternalDynamicReturnTypeExtension.php +++ /dev/null @@ -1,41 +0,0 @@ -getName() === 'getInternal'; - } - - public function getTypeFromMethodCall( - MethodReflection $methodReflection, - MethodCall $methodCall, - Scope $scope, - ): Type - { - if (count($methodCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $getterClosureType = $scope->getType($methodCall->getArgs()[1]->value); - return ParametersAcceptorSelector::selectSingle($getterClosureType->getCallableParametersAcceptors($scope))->getReturnType(); - } - -} diff --git a/src/Node/AnonymousClassNode.php b/src/Node/AnonymousClassNode.php new file mode 100644 index 0000000000..b5d8333496 --- /dev/null +++ b/src/Node/AnonymousClassNode.php @@ -0,0 +1,32 @@ +getSubNodeNames() as $subNodeName) { + $subNodes[$subNodeName] = $node->$subNodeName; + } + + return new AnonymousClassNode( + $node->name, + $subNodes, + $node->getAttributes(), + ); + } + + public function isAnonymous(): bool + { + return true; + } + +} diff --git a/src/Node/BooleanAndNode.php b/src/Node/BooleanAndNode.php index c4b750de00..361177c705 100644 --- a/src/Node/BooleanAndNode.php +++ b/src/Node/BooleanAndNode.php @@ -2,13 +2,15 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; use PhpParser\Node\Expr\BinaryOp\LogicalAnd; -use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; -/** @api */ -class BooleanAndNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class BooleanAndNode extends Expr implements VirtualNode { public function __construct(private BooleanAnd|LogicalAnd $originalNode, private Scope $rightScope) diff --git a/src/Node/BooleanOrNode.php b/src/Node/BooleanOrNode.php index fb0e04134a..c2ca5d14be 100644 --- a/src/Node/BooleanOrNode.php +++ b/src/Node/BooleanOrNode.php @@ -2,13 +2,15 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\LogicalOr; -use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; -/** @api */ -class BooleanOrNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class BooleanOrNode extends Expr implements VirtualNode { public function __construct(private BooleanOr|LogicalOr $originalNode, private Scope $rightScope) diff --git a/src/Node/BreaklessWhileLoopNode.php b/src/Node/BreaklessWhileLoopNode.php index 8a824778bc..f7df71bf19 100644 --- a/src/Node/BreaklessWhileLoopNode.php +++ b/src/Node/BreaklessWhileLoopNode.php @@ -6,8 +6,10 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementExitPoint; -/** @api */ -class BreaklessWhileLoopNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class BreaklessWhileLoopNode extends NodeAbstract implements VirtualNode { /** diff --git a/src/Node/CatchWithUnthrownExceptionNode.php b/src/Node/CatchWithUnthrownExceptionNode.php index 007fa83218..9f06bf2009 100644 --- a/src/Node/CatchWithUnthrownExceptionNode.php +++ b/src/Node/CatchWithUnthrownExceptionNode.php @@ -6,8 +6,10 @@ use PhpParser\NodeAbstract; use PHPStan\Type\Type; -/** @api */ -class CatchWithUnthrownExceptionNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class CatchWithUnthrownExceptionNode extends NodeAbstract implements VirtualNode { public function __construct(private Catch_ $originalNode, private Type $caughtType, private Type $originalCaughtType) diff --git a/src/Node/ClassConstantsNode.php b/src/Node/ClassConstantsNode.php index 4b725b6bcd..4da543a2d7 100644 --- a/src/Node/ClassConstantsNode.php +++ b/src/Node/ClassConstantsNode.php @@ -6,16 +6,19 @@ use PhpParser\Node\Stmt\ClassLike; use PhpParser\NodeAbstract; use PHPStan\Node\Constant\ClassConstantFetch; +use PHPStan\Reflection\ClassReflection; -/** @api */ -class ClassConstantsNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClassConstantsNode extends NodeAbstract implements VirtualNode { /** * @param ClassConst[] $constants * @param ClassConstantFetch[] $fetches */ - public function __construct(private ClassLike $class, private array $constants, private array $fetches) + public function __construct(private ClassLike $class, private array $constants, private array $fetches, private ClassReflection $classReflection) { parent::__construct($class->getAttributes()); } @@ -43,7 +46,7 @@ public function getFetches(): array public function getType(): string { - return 'PHPStan_Node_ClassPropertiesNode'; + return 'PHPStan_Node_ClassConstantsNode'; } /** @@ -54,4 +57,9 @@ public function getSubNodeNames(): array return []; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + } diff --git a/src/Node/ClassMethod.php b/src/Node/ClassMethod.php new file mode 100644 index 0000000000..2aec877cf5 --- /dev/null +++ b/src/Node/ClassMethod.php @@ -0,0 +1,28 @@ +node; + } + + public function isDeclaredInTrait(): bool + { + return $this->isDeclaredInTrait; + } + +} diff --git a/src/Node/ClassMethodsNode.php b/src/Node/ClassMethodsNode.php index 545441e9b4..4c46fd9253 100644 --- a/src/Node/ClassMethodsNode.php +++ b/src/Node/ClassMethodsNode.php @@ -3,19 +3,21 @@ namespace PHPStan\Node; use PhpParser\Node\Stmt\ClassLike; -use PhpParser\Node\Stmt\ClassMethod; use PhpParser\NodeAbstract; use PHPStan\Node\Method\MethodCall; +use PHPStan\Reflection\ClassReflection; -/** @api */ -class ClassMethodsNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClassMethodsNode extends NodeAbstract implements VirtualNode { /** * @param ClassMethod[] $methods * @param array $methodCalls */ - public function __construct(private ClassLike $class, private array $methods, private array $methodCalls) + public function __construct(private ClassLike $class, private array $methods, private array $methodCalls, private ClassReflection $classReflection) { parent::__construct($class->getAttributes()); } @@ -54,4 +56,9 @@ public function getSubNodeNames(): array return []; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + } diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index feefd78061..ec4dcba592 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -11,29 +11,46 @@ use PhpParser\Node\Stmt\ClassLike; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\PropertyInitializationExpr; use PHPStan\Node\Method\MethodCall; +use PHPStan\Node\Property\PropertyAssign; use PHPStan\Node\Property\PropertyRead; use PHPStan\Node\Property\PropertyWrite; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; -use PHPStan\Rules\Properties\ReadWritePropertiesExtension; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; +use PHPStan\TrinaryLogic; +use PHPStan\Type\NeverType; +use PHPStan\Type\TypeUtils; +use function array_diff_key; use function array_key_exists; use function array_keys; -use function count; use function in_array; +use function strtolower; -/** @api */ -class ClassPropertiesNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClassPropertiesNode extends NodeAbstract implements VirtualNode { /** * @param ClassPropertyNode[] $properties * @param array $propertyUsages * @param array $methodCalls + * @param array $returnStatementNodes + * @param list $propertyAssigns */ - public function __construct(private ClassLike $class, private array $properties, private array $propertyUsages, private array $methodCalls) + public function __construct( + private ClassLike $class, + private ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider, + private array $properties, + private array $propertyUsages, + private array $methodCalls, + private array $returnStatementNodes, + private array $propertyAssigns, + private ClassReflection $classReflection, + ) { parent::__construct($class->getAttributes()); } @@ -72,61 +89,84 @@ public function getSubNodeNames(): array return []; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + /** * @param string[] $constructors - * @param ReadWritePropertiesExtension[] $extensions - * @return array{array, array, array} + * @return array{array, array, array} */ public function getUninitializedProperties( Scope $scope, array $constructors, - array $extensions, ): array { if (!$this->getClass() instanceof Class_) { return [[], [], []]; } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $this->getClassReflection(); - $properties = []; + $uninitializedProperties = []; + $originalProperties = []; + $initialInitializedProperties = []; + $initializedProperties = []; + $extensions = $this->readWritePropertiesExtensionProvider->getExtensions(); + $initializedViaExtension = []; foreach ($this->getProperties() as $property) { if ($property->isStatic()) { continue; } + if ($property->isAbstract()) { + continue; + } if ($property->getNativeType() === null) { continue; } if ($property->getDefault() !== null) { continue; } - $properties[$property->getName()] = $property; - } - - foreach (array_keys($properties) as $name) { - foreach ($extensions as $extension) { - if (!$classReflection->hasNativeProperty($name)) { + $originalProperties[$property->getName()] = $property; + $is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait()); + if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) { + $propertyReflection = $classReflection->getNativeProperty($property->getName()); + if ($propertyReflection->isVirtual()->yes()) { continue; } - $propertyReflection = $classReflection->getNativeProperty($name); - if (!$extension->isInitialized($propertyReflection, $name)) { - continue; + + foreach ($extensions as $extension) { + if (!$extension->isInitialized($propertyReflection, $property->getName())) { + continue; + } + $is = TrinaryLogic::createYes(); + $initializedViaExtension[$property->getName()] = true; + break; } - unset($properties[$name]); - break; } + $initialInitializedProperties[$property->getName()] = $is; + foreach ($constructors as $constructor) { + $initializedProperties[$constructor][$property->getName()] = $is; + } + if ($is->yes()) { + continue; + } + $uninitializedProperties[$property->getName()] = $property; } if ($constructors === []) { - return [$properties, [], []]; + return [$uninitializedProperties, [], []]; } - $classType = new ObjectType($scope->getClassReflection()->getName()); - $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classType, $this->methodCalls, $constructors); + + $initializedInConstructor = []; + if ($classReflection->hasConstructor()) { + $initializedInConstructor = array_diff_key($uninitializedProperties, $this->collectUninitializedProperties([$classReflection->getConstructor()->getName()], $uninitializedProperties)); + } + + $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $constructors, $initializedInConstructor); $prematureAccess = []; $additionalAssigns = []; - $originalProperties = $properties; + foreach ($this->getPropertyUsages() as $usage) { $fetch = $usage->getFetch(); if (!$fetch instanceof PropertyFetch) { @@ -143,61 +183,153 @@ public function getUninitializedProperties( if ($function->getDeclaringClass()->getName() !== $classReflection->getName()) { continue; } - if (!in_array($function->getName(), $methodsCalledFromConstructor, true)) { + if (!array_key_exists($function->getName(), $methodsCalledFromConstructor)) { continue; } + $initializedPropertiesMap = $methodsCalledFromConstructor[$function->getName()]; + if (!$fetch->name instanceof Identifier) { continue; } $propertyName = $fetch->name->toString(); $fetchedOnType = $usageScope->getType($fetch->var); - if ($classType->isSuperTypeOf($fetchedOnType)->no()) { + if (TypeUtils::findThisType($fetchedOnType) === null) { + continue; + } + + $propertyReflection = $usageScope->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { continue; } - if ($fetchedOnType instanceof MixedType) { + if ($propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { continue; } if ($usage instanceof PropertyWrite) { - if (array_key_exists($propertyName, $properties)) { - unset($properties[$propertyName]); - } elseif (array_key_exists($propertyName, $originalProperties)) { - $additionalAssigns[] = [ + if (array_key_exists($propertyName, $initializedPropertiesMap)) { + $hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName))); + if ( + !$hasInitialization->no() + && !$usage->isPromotedPropertyWrite() + && !array_key_exists($propertyName, $initializedViaExtension) + ) { + $additionalAssigns[] = [ + $propertyName, + $fetch->getStartLine(), + $originalProperties[$propertyName], + ]; + } + } + } elseif (array_key_exists($propertyName, $initializedPropertiesMap)) { + if ( + strtolower($function->getName()) !== '__construct' + && array_key_exists($propertyName, $initializedInConstructor) + && in_array($function->getName(), $constructors, true) + ) { + continue; + } + $hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName))); + if (!$hasInitialization->yes() && $usageScope->isInAnonymousFunction() && $usageScope->getParentScope() !== null) { + $hasInitialization = $hasInitialization->or($usageScope->getParentScope()->hasExpressionType(new PropertyInitializationExpr($propertyName))); + } + if (!$hasInitialization->yes()) { + $prematureAccess[] = [ $propertyName, - $fetch->getLine(), + $fetch->getStartLine(), $originalProperties[$propertyName], + $usageScope->getFile(), + $usageScope->getFileDescription(), ]; } - } elseif (array_key_exists($propertyName, $properties)) { - $prematureAccess[] = [ - $propertyName, - $fetch->getLine(), - $properties[$propertyName], - ]; } } return [ - $properties, + $this->collectUninitializedProperties(array_keys($methodsCalledFromConstructor), $uninitializedProperties), $prematureAccess, $additionalAssigns, ]; } /** - * @param MethodCall[] $methodCalls + * @param list $constructors + * @param array $uninitializedProperties + * @return array + */ + private function collectUninitializedProperties(array $constructors, array $uninitializedProperties): array + { + foreach ($constructors as $constructor) { + $lowerConstructorName = strtolower($constructor); + if (!array_key_exists($lowerConstructorName, $this->returnStatementNodes)) { + continue; + } + + $returnStatementsNode = $this->returnStatementNodes[$lowerConstructorName]; + $methodScope = null; + foreach ($returnStatementsNode->getExecutionEnds() as $executionEnd) { + $statementResult = $executionEnd->getStatementResult(); + $endNode = $executionEnd->getNode(); + if ($statementResult->isAlwaysTerminating()) { + if ($endNode instanceof Node\Stmt\Expression) { + $exprType = $statementResult->getScope()->getType($endNode->expr); + if ($exprType instanceof NeverType && $exprType->isExplicit()) { + continue; + } + } + } + if ($methodScope === null) { + $methodScope = $statementResult->getScope(); + continue; + } + + $methodScope = $methodScope->mergeWith($statementResult->getScope()); + } + + foreach ($returnStatementsNode->getReturnStatements() as $returnStatement) { + if ($methodScope === null) { + $methodScope = $returnStatement->getScope(); + continue; + } + $methodScope = $methodScope->mergeWith($returnStatement->getScope()); + } + + if ($methodScope === null) { + continue; + } + + foreach (array_keys($uninitializedProperties) as $propertyName) { + if (!$methodScope->hasExpressionType(new PropertyInitializationExpr($propertyName))->yes()) { + continue; + } + + unset($uninitializedProperties[$propertyName]); + } + } + + return $uninitializedProperties; + } + + /** * @param string[] $methods - * @return string[] + * @param array $initialInitializedProperties + * @param array> $initializedProperties + * @param array $initializedInConstructorProperties + * + * @return array> */ private function getMethodsCalledFromConstructor( - ObjectType $classType, - array $methodCalls, + ClassReflection $classReflection, + array $initialInitializedProperties, + array $initializedProperties, array $methods, + array $initializedInConstructorProperties, ): array { - $originalCount = count($methods); - foreach ($methodCalls as $methodCall) { + $originalMap = $initializedProperties; + $originalMethods = $methods; + + foreach ($this->methodCalls as $methodCall) { $methodCallNode = $methodCall->getNode(); if ($methodCallNode instanceof Array_) { continue; @@ -215,31 +347,69 @@ private function getMethodsCalledFromConstructor( $calledOnType = $callScope->resolveTypeByName($methodCallNode->class); } - if ($classType->isSuperTypeOf($calledOnType)->no()) { + + if (TypeUtils::findThisType($calledOnType) === null) { + continue; + } + + $inMethod = $callScope->getFunction(); + if (!$inMethod instanceof MethodReflection) { continue; } - if ($calledOnType instanceof MixedType) { + if (!in_array($inMethod->getName(), $methods, true)) { continue; } + + if ($inMethod->getName() !== '__construct') { + foreach ($initializedInConstructorProperties as $propertyName => $propertyNode) { + $initializedProperties[$inMethod->getName()][$propertyName] = TrinaryLogic::createYes(); + } + } + $methodName = $methodCallNode->name->toString(); - if (in_array($methodName, $methods, true)) { + if (array_key_exists($methodName, $initializedProperties)) { + foreach ($this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties) as $propertyName => $isInitialized) { + $initializedProperties[$methodName][$propertyName] = $initializedProperties[$methodName][$propertyName]->and($isInitialized); + } continue; } - $inMethod = $callScope->getFunction(); - if (!$inMethod instanceof MethodReflection) { + $methodReflection = $callScope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection === null) { continue; } - if (!in_array($inMethod->getName(), $methods, true)) { + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { continue; } + $initializedProperties[$methodName] = $this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties); $methods[] = $methodName; } - if ($originalCount === count($methods)) { - return $methods; + if ($originalMap === $initializedProperties && $originalMethods === $methods) { + return $initializedProperties; + } + + return $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $methods, $initializedInConstructorProperties); + } + + /** + * @param array $initialInitializedProperties + * @return array + */ + private function getInitializedProperties(Scope $scope, array $initialInitializedProperties): array + { + foreach ($initialInitializedProperties as $propertyName => $isInitialized) { + $initialInitializedProperties[$propertyName] = $isInitialized->or($scope->hasExpressionType(new PropertyInitializationExpr($propertyName))); } - return $this->getMethodsCalledFromConstructor($classType, $methodCalls, $methods); + return $initialInitializedProperties; + } + + /** + * @return list + */ + public function getPropertyAssigns(): array + { + return $this->propertyAssigns; } } diff --git a/src/Node/ClassPropertyNode.php b/src/Node/ClassPropertyNode.php index b45d2fd931..a3eb0567ea 100644 --- a/src/Node/ClassPropertyNode.php +++ b/src/Node/ClassPropertyNode.php @@ -2,30 +2,43 @@ namespace PHPStan\Node; +use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\Expr; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name; -use PhpParser\Node\Stmt\Class_; use PhpParser\NodeAbstract; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Type\Type; -/** @api */ -class ClassPropertyNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClassPropertyNode extends NodeAbstract implements VirtualNode { + /** + * @param non-empty-string $name + */ public function __construct( private string $name, private int $flags, - private Identifier|Name|Node\ComplexType|null $type, + private ?Type $type, private ?Expr $default, private ?string $phpDoc, + private ?Type $phpDocType, private bool $isPromoted, - Node $originalNode, + private bool $isPromotedFromTrait, + private Node\Stmt\Property|Node\Param $originalNode, + private bool $isReadonlyByPhpDoc, + private bool $isDeclaredInTrait, + private bool $isReadonlyClass, + private bool $isAllowedPrivateMutation, + private ClassReflection $classReflection, ) { parent::__construct($originalNode->getAttributes()); } + /** @return non-empty-string */ public function getName(): string { return $this->name; @@ -46,43 +59,88 @@ public function isPromoted(): bool return $this->isPromoted; } + public function isPromotedFromTrait(): bool + { + return $this->isPromotedFromTrait; + } + public function getPhpDoc(): ?string { return $this->phpDoc; } + public function getPhpDocType(): ?Type + { + return $this->phpDocType; + } + public function isPublic(): bool { - return ($this->flags & Class_::MODIFIER_PUBLIC) !== 0 - || ($this->flags & Class_::VISIBILITY_MODIFIER_MASK) === 0; + return ($this->flags & Modifiers::PUBLIC) !== 0 + || ($this->flags & Modifiers::VISIBILITY_MASK) === 0; } public function isProtected(): bool { - return (bool) ($this->flags & Class_::MODIFIER_PROTECTED); + return (bool) ($this->flags & Modifiers::PROTECTED); } public function isPrivate(): bool { - return (bool) ($this->flags & Class_::MODIFIER_PRIVATE); + return (bool) ($this->flags & Modifiers::PRIVATE); + } + + public function isFinal(): bool + { + return (bool) ($this->flags & Modifiers::FINAL); } public function isStatic(): bool { - return (bool) ($this->flags & Class_::MODIFIER_STATIC); + return (bool) ($this->flags & Modifiers::STATIC); } public function isReadOnly(): bool { - return (bool) ($this->flags & Class_::MODIFIER_READONLY); + return (bool) ($this->flags & Modifiers::READONLY) || $this->isReadonlyClass; + } + + public function isReadOnlyByPhpDoc(): bool + { + return $this->isReadonlyByPhpDoc; + } + + public function isDeclaredInTrait(): bool + { + return $this->isDeclaredInTrait; + } + + public function isAllowedPrivateMutation(): bool + { + return $this->isAllowedPrivateMutation; + } + + public function isAbstract(): bool + { + return (bool) ($this->flags & Modifiers::ABSTRACT); + } + + public function getNativeType(): ?Type + { + return $this->type; } /** - * @return Identifier|Name|Node\ComplexType|null + * @return Node\Identifier|Node\Name|Node\ComplexType|null */ - public function getNativeType() + public function getNativeTypeNode() { - return $this->type; + return $this->originalNode->type; + } + + public function getClassReflection(): ClassReflection + { + return $this->classReflection; } public function getType(): string @@ -98,4 +156,32 @@ public function getSubNodeNames(): array return []; } + public function hasHooks(): bool + { + return $this->getHooks() !== []; + } + + /** + * @return Node\PropertyHook[] + */ + public function getHooks(): array + { + return $this->originalNode->hooks; + } + + public function isVirtual(): bool + { + return $this->classReflection->getNativeProperty($this->name)->isVirtual()->yes(); + } + + public function isWritable(): bool + { + return $this->classReflection->getNativeProperty($this->name)->isWritable(); + } + + public function isReadable(): bool + { + return $this->classReflection->getNativeProperty($this->name)->isReadable(); + } + } diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index 80f5a3e3d6..a2bb6889d5 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -13,22 +13,32 @@ use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; use PHPStan\Node\Constant\ClassConstantFetch; +use PHPStan\Node\Property\PropertyAssign; use PHPStan\Node\Property\PropertyRead; use PHPStan\Node\Property\PropertyWrite; use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\TypeUtils; +use ReflectionProperty; use function count; +use function in_array; +use function strtolower; -class ClassStatementsGatherer +final class ClassStatementsGatherer { + private const PROPERTY_ENUMERATING_FUNCTIONS = [ + 'get_object_vars', + 'array_walk', + ]; + /** @var callable(Node $node, Scope $scope): void */ private $nodeCallback; /** @var ClassPropertyNode[] */ private array $properties = []; - /** @var Node\Stmt\ClassMethod[] */ + /** @var ClassMethod[] */ private array $methods = []; /** @var \PHPStan\Node\Method\MethodCall[] */ @@ -43,6 +53,12 @@ class ClassStatementsGatherer /** @var ClassConstantFetch[] */ private array $constantFetches = []; + /** @var array */ + private array $returnStatementNodes = []; + + /** @var list */ + private array $propertyAssigns = []; + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -63,7 +79,7 @@ public function getProperties(): array } /** - * @return Node\Stmt\ClassMethod[] + * @return ClassMethod[] */ public function getMethods(): array { @@ -102,6 +118,22 @@ public function getConstantFetches(): array return $this->constantFetches; } + /** + * @return array + */ + public function getReturnStatementsNodes(): array + { + return $this->returnStatementNodes; + } + + /** + * @return list + */ + public function getPropertyAssigns(): array + { + return $this->propertyAssigns; + } + public function __invoke(Node $node, Scope $scope): void { $nodeCallback = $this->nodeCallback; @@ -117,18 +149,19 @@ private function gatherNodes(Node $node, Scope $scope): void if ($scope->getClassReflection()->getName() !== $this->classReflection->getName()) { return; } - if ($node instanceof ClassPropertyNode && !$scope->isInTrait()) { + if ($node instanceof ClassPropertyNode) { $this->properties[] = $node; if ($node->isPromoted()) { $this->propertyUsages[] = new PropertyWrite( new PropertyFetch(new Expr\Variable('this'), new Identifier($node->getName())), $scope, + true, ); } return; } - if ($node instanceof Node\Stmt\ClassMethod && !$scope->isInTrait()) { - $this->methods[] = $node; + if ($node instanceof Node\Stmt\ClassMethod) { + $this->methods[] = new ClassMethod($node, $scope->isInTrait()); return; } if ($node instanceof Node\Stmt\ClassConst) { @@ -137,12 +170,27 @@ private function gatherNodes(Node $node, Scope $scope): void } if ($node instanceof MethodCall || $node instanceof StaticCall) { $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node, $scope); + if ($node instanceof StaticCall && $node->name instanceof Identifier && $node->name->toLowerString() === '__construct') { + $this->tryToApplyPropertyWritesFromAncestorConstructor($node, $scope); + } return; } if ($node instanceof MethodCallableNode || $node instanceof StaticMethodCallableNode) { $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node->getOriginalNode(), $scope); return; } + if ($node instanceof MethodReturnStatementsNode) { + $this->returnStatementNodes[strtolower($node->getMethodName())] = $node; + return; + } + if ( + $node instanceof Expr\FuncCall + && $node->name instanceof Node\Name + && in_array($node->name->toLowerString(), self::PROPERTY_ENUMERATING_FUNCTIONS, true) + ) { + $this->tryToApplyPropertyReads($node, $scope); + return; + } if ($node instanceof Array_ && count($node->items) === 2) { $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node, $scope); return; @@ -152,7 +200,8 @@ private function gatherNodes(Node $node, Scope $scope): void return; } if ($node instanceof PropertyAssignNode) { - $this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope); + $this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false); + $this->propertyAssigns[] = new PropertyAssign($node, $scope); return; } if (!$node instanceof Expr) { @@ -169,11 +218,13 @@ private function gatherNodes(Node $node, Scope $scope): void } $this->propertyUsages[] = new PropertyRead($node->expr, $scope); - $this->propertyUsages[] = new PropertyWrite($node->expr, $scope); + $this->propertyUsages[] = new PropertyWrite($node->expr, $scope, false); return; } - if ($node instanceof Node\Scalar\EncapsedStringPart) { - return; + if ($node instanceof FunctionCallableNode) { + $node = $node->getOriginalNode(); + } elseif ($node instanceof InstantiationCallableNode) { + $node = $node->getOriginalNode(); } $inAssign = $scope->isInExpressionAssign($node); @@ -191,4 +242,58 @@ private function gatherNodes(Node $node, Scope $scope): void $this->propertyUsages[] = new PropertyRead($node, $scope); } + private function tryToApplyPropertyReads(Expr\FuncCall $node, Scope $scope): void + { + $args = $node->getArgs(); + if (count($args) === 0) { + return; + } + + $firstArgValue = $args[0]->value; + if (TypeUtils::findThisType($scope->getType($firstArgValue)) === null) { + return; + } + + $classProperties = $this->classReflection->getNativeReflection()->getProperties(); + foreach ($classProperties as $property) { + if ($property->isStatic()) { + continue; + } + if ($property->getName() === '') { + throw new ShouldNotHappenException(); + } + $this->propertyUsages[] = new PropertyRead( + new PropertyFetch(new Expr\Variable('this'), new Identifier($property->getName())), + $scope, + ); + } + } + + private function tryToApplyPropertyWritesFromAncestorConstructor(StaticCall $ancestorConstructorCall, Scope $scope): void + { + if (!$ancestorConstructorCall->class instanceof Node\Name) { + return; + } + + $calledOnType = $scope->resolveTypeByName($ancestorConstructorCall->class); + if ($calledOnType->getClassReflection() === null || TypeUtils::findThisType($calledOnType) === null) { + return; + } + + $classReflection = $calledOnType->getClassReflection()->getNativeReflection(); + foreach ($classReflection->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED) as $property) { + if (!$property->isPromoted() || $property->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } + if ($property->getName() === '') { + throw new ShouldNotHappenException(); + } + $this->propertyUsages[] = new PropertyWrite( + new PropertyFetch(new Expr\Variable('this'), new Identifier($property->getName()), $ancestorConstructorCall->getAttributes()), + $scope, + false, + ); + } + } + } diff --git a/src/Node/ClosureReturnStatementsNode.php b/src/Node/ClosureReturnStatementsNode.php index 04c229b438..920b61750b 100644 --- a/src/Node/ClosureReturnStatementsNode.php +++ b/src/Node/ClosureReturnStatementsNode.php @@ -7,23 +7,31 @@ use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; +use function count; -/** @api */ -class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode +/** + * @api + */ +final class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { private Node\Expr\Closure $closureExpr; /** - * @param ReturnStatement[] $returnStatements - * @param array $yieldStatements + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( Closure $closureExpr, private array $returnStatements, private array $yieldStatements, private StatementResult $statementResult, + private array $executionEnds, + private array $impurePoints, ) { parent::__construct($closureExpr->getAttributes()); @@ -35,22 +43,36 @@ public function getClosureExpr(): Closure return $this->closureExpr; } - /** - * @return ReturnStatement[] - */ + public function hasNativeReturnTypehint(): bool + { + return $this->closureExpr->returnType !== null; + } + public function getReturnStatements(): array { return $this->returnStatements; } - /** - * @return array - */ + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function getYieldStatements(): array { return $this->yieldStatements; } + public function isGenerator(): bool + { + return count($this->yieldStatements) > 0; + } + public function getStatementResult(): StatementResult { return $this->statementResult; diff --git a/src/Node/CollectedDataNode.php b/src/Node/CollectedDataNode.php new file mode 100644 index 0000000000..acc4f24f2d --- /dev/null +++ b/src/Node/CollectedDataNode.php @@ -0,0 +1,70 @@ + + * @template TValue + * @param class-string $collectorType + * @return array> + */ + public function get(string $collectorType): array + { + $result = []; + foreach ($this->collectedData as $filePath => $collectedDataPerCollector) { + if (!isset($collectedDataPerCollector[$collectorType])) { + continue; + } + + foreach ($collectedDataPerCollector[$collectorType] as $rawData) { + $result[$filePath][] = $rawData; + } + } + + return $result; + } + + /** + * Indicates that only files were passed to the analyser, not directory paths. + * + * True being returned strongly suggests that it's a partial analysis, not full project analysis. + */ + public function isOnlyFilesAnalysis(): bool + { + return $this->onlyFiles; + } + + public function getType(): string + { + return 'PHPStan_Node_CollectedDataNode'; + } + + /** + * @return array{} + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Constant/ClassConstantFetch.php b/src/Node/Constant/ClassConstantFetch.php index 30129007d0..bda533900b 100644 --- a/src/Node/Constant/ClassConstantFetch.php +++ b/src/Node/Constant/ClassConstantFetch.php @@ -5,8 +5,10 @@ use PhpParser\Node\Expr\ClassConstFetch; use PHPStan\Analyser\Scope; -/** @api */ -class ClassConstantFetch +/** + * @api + */ +final class ClassConstantFetch { public function __construct(private ClassConstFetch $node, private Scope $scope) diff --git a/src/Node/DoWhileLoopConditionNode.php b/src/Node/DoWhileLoopConditionNode.php index c6cbc86572..89f6c7f4bf 100644 --- a/src/Node/DoWhileLoopConditionNode.php +++ b/src/Node/DoWhileLoopConditionNode.php @@ -6,7 +6,7 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementExitPoint; -class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode +final class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode { /** diff --git a/src/Node/ExecutionEndNode.php b/src/Node/ExecutionEndNode.php index 225036a5ba..5e0ddf13da 100644 --- a/src/Node/ExecutionEndNode.php +++ b/src/Node/ExecutionEndNode.php @@ -6,12 +6,14 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementResult; -/** @api */ -class ExecutionEndNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ExecutionEndNode extends NodeAbstract implements VirtualNode { public function __construct( - private Node $node, + private Node\Stmt $node, private StatementResult $statementResult, private bool $hasNativeReturnTypehint, ) @@ -19,7 +21,7 @@ public function __construct( parent::__construct($node->getAttributes()); } - public function getNode(): Node + public function getNode(): Node\Stmt { return $this->node; } diff --git a/src/Node/Expr/AlwaysRememberedExpr.php b/src/Node/Expr/AlwaysRememberedExpr.php new file mode 100644 index 0000000000..b3ed1cc12e --- /dev/null +++ b/src/Node/Expr/AlwaysRememberedExpr.php @@ -0,0 +1,45 @@ +expr; + } + + public function getExprType(): Type + { + return $this->type; + } + + public function getNativeExprType(): Type + { + return $this->nativeType; + } + + public function getType(): string + { + return 'PHPStan_Node_AlwaysRememberedExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return ['expr']; + } + +} diff --git a/src/Node/Expr/ExistingArrayDimFetch.php b/src/Node/Expr/ExistingArrayDimFetch.php new file mode 100644 index 0000000000..c388667cac --- /dev/null +++ b/src/Node/Expr/ExistingArrayDimFetch.php @@ -0,0 +1,39 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getType(): string + { + return 'PHPStan_Node_ExistingArrayDimFetch'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/GetIterableKeyTypeExpr.php b/src/Node/Expr/GetIterableKeyTypeExpr.php new file mode 100644 index 0000000000..4bc9c39ecc --- /dev/null +++ b/src/Node/Expr/GetIterableKeyTypeExpr.php @@ -0,0 +1,34 @@ +expr; + } + + public function getType(): string + { + return 'PHPStan_Node_GetIterableKeyTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/GetIterableValueTypeExpr.php b/src/Node/Expr/GetIterableValueTypeExpr.php index 54e481b2c9..43d3a19a09 100644 --- a/src/Node/Expr/GetIterableValueTypeExpr.php +++ b/src/Node/Expr/GetIterableValueTypeExpr.php @@ -5,12 +5,12 @@ use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class GetIterableValueTypeExpr extends Expr implements VirtualNode +final class GetIterableValueTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr $expr) { - parent::__construct($expr->getAttributes()); + parent::__construct([]); } public function getExpr(): Expr diff --git a/src/Node/Expr/GetOffsetValueTypeExpr.php b/src/Node/Expr/GetOffsetValueTypeExpr.php index 3399e4d226..6bb047212a 100644 --- a/src/Node/Expr/GetOffsetValueTypeExpr.php +++ b/src/Node/Expr/GetOffsetValueTypeExpr.php @@ -5,12 +5,12 @@ use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class GetOffsetValueTypeExpr extends Expr implements VirtualNode +final class GetOffsetValueTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr $var, private Expr $dim) { - parent::__construct($var->getAttributes()); + parent::__construct([]); } public function getVar(): Expr diff --git a/src/Node/Expr/OriginalPropertyTypeExpr.php b/src/Node/Expr/OriginalPropertyTypeExpr.php index 113134a994..b04990efdc 100644 --- a/src/Node/Expr/OriginalPropertyTypeExpr.php +++ b/src/Node/Expr/OriginalPropertyTypeExpr.php @@ -5,12 +5,12 @@ use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class OriginalPropertyTypeExpr extends Expr implements VirtualNode +final class OriginalPropertyTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr\PropertyFetch|Expr\StaticPropertyFetch $propertyFetch) { - parent::__construct($propertyFetch->getAttributes()); + parent::__construct([]); } public function getPropertyFetch(): Expr\PropertyFetch|Expr\StaticPropertyFetch diff --git a/src/Node/Expr/ParameterVariableOriginalValueExpr.php b/src/Node/Expr/ParameterVariableOriginalValueExpr.php new file mode 100644 index 0000000000..7f3408becb --- /dev/null +++ b/src/Node/Expr/ParameterVariableOriginalValueExpr.php @@ -0,0 +1,34 @@ +variableName; + } + + public function getType(): string + { + return 'PHPStan_Node_ParameterVariableOriginalValueExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/PropertyInitializationExpr.php b/src/Node/Expr/PropertyInitializationExpr.php new file mode 100644 index 0000000000..6963d25670 --- /dev/null +++ b/src/Node/Expr/PropertyInitializationExpr.php @@ -0,0 +1,34 @@ +propertyName; + } + + public function getType(): string + { + return 'PHPStan_Node_PropertyInitializationExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/SetExistingOffsetValueTypeExpr.php b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php new file mode 100644 index 0000000000..ee4292f65c --- /dev/null +++ b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php @@ -0,0 +1,44 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getValue(): Expr + { + return $this->value; + } + + public function getType(): string + { + return 'PHPStan_Node_SetExistingOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/SetOffsetValueTypeExpr.php b/src/Node/Expr/SetOffsetValueTypeExpr.php index 63a9a1f9cc..ed6ddae657 100644 --- a/src/Node/Expr/SetOffsetValueTypeExpr.php +++ b/src/Node/Expr/SetOffsetValueTypeExpr.php @@ -5,12 +5,12 @@ use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; -class SetOffsetValueTypeExpr extends Expr implements VirtualNode +final class SetOffsetValueTypeExpr extends Expr implements VirtualNode { public function __construct(private Expr $var, private ?Expr $dim, private Expr $value) { - parent::__construct($var->getAttributes()); + parent::__construct([]); } public function getVar(): Expr diff --git a/src/Node/Expr/TypeExpr.php b/src/Node/Expr/TypeExpr.php new file mode 100644 index 0000000000..c21a2099c3 --- /dev/null +++ b/src/Node/Expr/TypeExpr.php @@ -0,0 +1,39 @@ +exprType; + } + + public function getType(): string + { + return 'PHPStan_Node_TypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/UnsetOffsetExpr.php b/src/Node/Expr/UnsetOffsetExpr.php new file mode 100644 index 0000000000..422affc9af --- /dev/null +++ b/src/Node/Expr/UnsetOffsetExpr.php @@ -0,0 +1,39 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getType(): string + { + return 'PHPStan_Node_UnsetOffsetExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/FileNode.php b/src/Node/FileNode.php index 13a6596ced..286168fb6a 100644 --- a/src/Node/FileNode.php +++ b/src/Node/FileNode.php @@ -5,8 +5,10 @@ use PhpParser\Node; use PhpParser\NodeAbstract; -/** @api */ -class FileNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class FileNode extends NodeAbstract implements VirtualNode { /** diff --git a/src/Node/FinallyExitPointsNode.php b/src/Node/FinallyExitPointsNode.php index a87bdc516c..fed8d4888d 100644 --- a/src/Node/FinallyExitPointsNode.php +++ b/src/Node/FinallyExitPointsNode.php @@ -5,8 +5,10 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementExitPoint; -/** @api */ -class FinallyExitPointsNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class FinallyExitPointsNode extends NodeAbstract implements VirtualNode { /** diff --git a/src/Node/FunctionCallableNode.php b/src/Node/FunctionCallableNode.php index 4b2c5589d9..9cd2cfc8c8 100644 --- a/src/Node/FunctionCallableNode.php +++ b/src/Node/FunctionCallableNode.php @@ -5,16 +5,15 @@ use PhpParser\Node\Expr; use PhpParser\Node\Name; -/** @api */ -class FunctionCallableNode extends Expr implements VirtualNode +/** + * @api + */ +final class FunctionCallableNode extends Expr implements VirtualNode { - /** - * @param mixed[] $attributes - */ - public function __construct(private Name|Expr $name, array $attributes = []) + public function __construct(private Name|Expr $name, private Expr\FuncCall $originalNode) { - parent::__construct($attributes); + parent::__construct($this->originalNode->getAttributes()); } /** @@ -25,6 +24,11 @@ public function getName() return $this->name; } + public function getOriginalNode(): Expr\FuncCall + { + return $this->originalNode; + } + public function getType(): string { return 'PHPStan_Node_FunctionCallableNode'; diff --git a/src/Node/FunctionReturnStatementsNode.php b/src/Node/FunctionReturnStatementsNode.php index 459da89468..14582f309b 100644 --- a/src/Node/FunctionReturnStatementsNode.php +++ b/src/Node/FunctionReturnStatementsNode.php @@ -2,31 +2,41 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Function_; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; +use function count; -/** @api */ -class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode +/** + * @api + */ +final class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { /** - * @param ReturnStatement[] $returnStatements - * @param ExecutionEndNode[] $executionEnds + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( private Function_ $function, private array $returnStatements, + private array $yieldStatements, private StatementResult $statementResult, private array $executionEnds, + private array $impurePoints, + private PhpFunctionFromParserNodeReflection $functionReflection, ) { parent::__construct($function->getAttributes()); } - /** - * @return ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -37,14 +47,16 @@ public function getStatementResult(): StatementResult return $this->statementResult; } - /** - * @return ExecutionEndNode[] - */ public function getExecutionEnds(): array { return $this->executionEnds; } + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function returnsByRef(): bool { return $this->function->byRef; @@ -55,6 +67,16 @@ public function hasNativeReturnTypehint(): bool return $this->function->returnType !== null; } + public function getYieldStatements(): array + { + return $this->yieldStatements; + } + + public function isGenerator(): bool + { + return count($this->yieldStatements) > 0; + } + public function getType(): string { return 'PHPStan_Node_FunctionReturnStatementsNode'; @@ -68,4 +90,17 @@ public function getSubNodeNames(): array return []; } + public function getFunctionReflection(): PhpFunctionFromParserNodeReflection + { + return $this->functionReflection; + } + + /** + * @return Stmt[] + */ + public function getStatements(): array + { + return $this->function->getStmts(); + } + } diff --git a/src/Node/InArrowFunctionNode.php b/src/Node/InArrowFunctionNode.php index dd018f32d7..20acb7c36f 100644 --- a/src/Node/InArrowFunctionNode.php +++ b/src/Node/InArrowFunctionNode.php @@ -5,19 +5,27 @@ use PhpParser\Node; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\NodeAbstract; +use PHPStan\Type\ClosureType; -/** @api */ -class InArrowFunctionNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class InArrowFunctionNode extends NodeAbstract implements VirtualNode { private Node\Expr\ArrowFunction $originalNode; - public function __construct(ArrowFunction $originalNode) + public function __construct(private ClosureType $closureType, ArrowFunction $originalNode) { parent::__construct($originalNode->getAttributes()); $this->originalNode = $originalNode; } + public function getClosureType(): ClosureType + { + return $this->closureType; + } + public function getOriginalNode(): Node\Expr\ArrowFunction { return $this->originalNode; diff --git a/src/Node/InClassMethodNode.php b/src/Node/InClassMethodNode.php index e6c331c208..52b50c17fd 100644 --- a/src/Node/InClassMethodNode.php +++ b/src/Node/InClassMethodNode.php @@ -3,16 +3,34 @@ namespace PHPStan\Node; use PhpParser\Node; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; -/** @api */ -class InClassMethodNode extends Node\Stmt implements VirtualNode +/** + * @api + */ +final class InClassMethodNode extends Node\Stmt implements VirtualNode { - public function __construct(private Node\Stmt\ClassMethod $originalNode) + public function __construct( + private ClassReflection $classReflection, + private PhpMethodFromParserNodeReflection $methodReflection, + private Node\Stmt\ClassMethod $originalNode, + ) { parent::__construct($originalNode->getAttributes()); } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + public function getMethodReflection(): PhpMethodFromParserNodeReflection + { + return $this->methodReflection; + } + public function getOriginalNode(): Node\Stmt\ClassMethod { return $this->originalNode; diff --git a/src/Node/InClassNode.php b/src/Node/InClassNode.php index 23310aaec0..84a4ecab83 100644 --- a/src/Node/InClassNode.php +++ b/src/Node/InClassNode.php @@ -6,8 +6,10 @@ use PhpParser\Node\Stmt\ClassLike; use PHPStan\Reflection\ClassReflection; -/** @api */ -class InClassNode extends Node\Stmt implements VirtualNode +/** + * @api + */ +final class InClassNode extends Node\Stmt implements VirtualNode { public function __construct(private ClassLike $originalNode, private ClassReflection $classReflection) diff --git a/src/Node/InClosureNode.php b/src/Node/InClosureNode.php index 43db41bced..3e95aea867 100644 --- a/src/Node/InClosureNode.php +++ b/src/Node/InClosureNode.php @@ -5,19 +5,27 @@ use PhpParser\Node; use PhpParser\Node\Expr\Closure; use PhpParser\NodeAbstract; +use PHPStan\Type\ClosureType; -/** @api */ -class InClosureNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class InClosureNode extends NodeAbstract implements VirtualNode { private Node\Expr\Closure $originalNode; - public function __construct(Closure $originalNode) + public function __construct(private ClosureType $closureType, Closure $originalNode) { parent::__construct($originalNode->getAttributes()); $this->originalNode = $originalNode; } + public function getClosureType(): ClosureType + { + return $this->closureType; + } + public function getOriginalNode(): Closure { return $this->originalNode; diff --git a/src/Node/InForeachNode.php b/src/Node/InForeachNode.php index b39026a9c9..4e474c98b2 100644 --- a/src/Node/InForeachNode.php +++ b/src/Node/InForeachNode.php @@ -5,7 +5,7 @@ use PhpParser\Node\Stmt\Foreach_; use PhpParser\NodeAbstract; -class InForeachNode extends NodeAbstract implements VirtualNode +final class InForeachNode extends NodeAbstract implements VirtualNode { public function __construct(private Foreach_ $originalNode) diff --git a/src/Node/InFunctionNode.php b/src/Node/InFunctionNode.php index fc4649c0bb..ce90bb7f38 100644 --- a/src/Node/InFunctionNode.php +++ b/src/Node/InFunctionNode.php @@ -3,16 +3,27 @@ namespace PHPStan\Node; use PhpParser\Node; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; -/** @api */ -class InFunctionNode extends Node\Stmt implements VirtualNode +/** + * @api + */ +final class InFunctionNode extends Node\Stmt implements VirtualNode { - public function __construct(private Node\Stmt\Function_ $originalNode) + public function __construct( + private PhpFunctionFromParserNodeReflection $functionReflection, + private Node\Stmt\Function_ $originalNode, + ) { parent::__construct($originalNode->getAttributes()); } + public function getFunctionReflection(): PhpFunctionFromParserNodeReflection + { + return $this->functionReflection; + } + public function getOriginalNode(): Node\Stmt\Function_ { return $this->originalNode; diff --git a/src/Node/InPropertyHookNode.php b/src/Node/InPropertyHookNode.php new file mode 100644 index 0000000000..99de6b73a0 --- /dev/null +++ b/src/Node/InPropertyHookNode.php @@ -0,0 +1,60 @@ +getAttributes()); + } + + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + public function getHookReflection(): PhpMethodFromParserNodeReflection + { + return $this->hookReflection; + } + + public function getPropertyReflection(): PhpPropertyReflection + { + return $this->propertyReflection; + } + + public function getOriginalNode(): Node\PropertyHook + { + return $this->originalNode; + } + + public function getType(): string + { + return 'PHPStan_Node_InPropertyHookNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InTraitNode.php b/src/Node/InTraitNode.php new file mode 100644 index 0000000000..b7834e713f --- /dev/null +++ b/src/Node/InTraitNode.php @@ -0,0 +1,47 @@ +getAttributes()); + } + + public function getOriginalNode(): Node\Stmt\Trait_ + { + return $this->originalNode; + } + + public function getTraitReflection(): ClassReflection + { + return $this->traitReflection; + } + + public function getImplementingClassReflection(): ClassReflection + { + return $this->implementingClassReflection; + } + + public function getType(): string + { + return 'PHPStan_Stmt_InTraitNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InstantiationCallableNode.php b/src/Node/InstantiationCallableNode.php index acf9a44bf9..98d838b1be 100644 --- a/src/Node/InstantiationCallableNode.php +++ b/src/Node/InstantiationCallableNode.php @@ -5,16 +5,15 @@ use PhpParser\Node\Expr; use PhpParser\Node\Name; -/** @api */ -class InstantiationCallableNode extends Expr implements VirtualNode +/** + * @api + */ +final class InstantiationCallableNode extends Expr implements VirtualNode { - /** - * @param mixed[] $attributes - */ - public function __construct(private Name|Expr $class, array $attributes = []) + public function __construct(private Name|Expr $class, private Expr\New_ $originalNode) { - parent::__construct($attributes); + parent::__construct($this->originalNode->getAttributes()); } /** @@ -25,6 +24,11 @@ public function getClass() return $this->class; } + public function getOriginalNode(): Expr\New_ + { + return $this->originalNode; + } + public function getType(): string { return 'PHPStan_Node_InstantiationCallableNode'; diff --git a/src/Node/InvalidateExprNode.php b/src/Node/InvalidateExprNode.php new file mode 100644 index 0000000000..5fb5ba27de --- /dev/null +++ b/src/Node/InvalidateExprNode.php @@ -0,0 +1,37 @@ +getAttributes()); + } + + public function getExpr(): Expr + { + return $this->expr; + } + + public function getType(): string + { + return 'PHPStan_Node_InvalidateExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/IssetExpr.php b/src/Node/IssetExpr.php new file mode 100644 index 0000000000..be1558fe7b --- /dev/null +++ b/src/Node/IssetExpr.php @@ -0,0 +1,41 @@ +expr; + } + + public function getType(): string + { + return 'PHPStan_Node_IssetExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/LiteralArrayItem.php b/src/Node/LiteralArrayItem.php index 48dd0e3705..4d9699121b 100644 --- a/src/Node/LiteralArrayItem.php +++ b/src/Node/LiteralArrayItem.php @@ -2,11 +2,13 @@ namespace PHPStan\Node; -use PhpParser\Node\Expr\ArrayItem; +use PhpParser\Node\ArrayItem; use PHPStan\Analyser\Scope; -/** @api */ -class LiteralArrayItem +/** + * @api + */ +final class LiteralArrayItem { public function __construct(private Scope $scope, private ?ArrayItem $arrayItem) diff --git a/src/Node/LiteralArrayNode.php b/src/Node/LiteralArrayNode.php index bcbd3f0a93..9c8a693ff6 100644 --- a/src/Node/LiteralArrayNode.php +++ b/src/Node/LiteralArrayNode.php @@ -5,8 +5,10 @@ use PhpParser\Node\Expr\Array_; use PhpParser\NodeAbstract; -/** @api */ -class LiteralArrayNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class LiteralArrayNode extends NodeAbstract implements VirtualNode { /** diff --git a/src/Node/MatchExpressionArm.php b/src/Node/MatchExpressionArm.php index 53832b1bcb..bad6265698 100644 --- a/src/Node/MatchExpressionArm.php +++ b/src/Node/MatchExpressionArm.php @@ -2,17 +2,24 @@ namespace PHPStan\Node; -/** @api */ -class MatchExpressionArm +/** + * @api + */ +final class MatchExpressionArm { /** * @param MatchExpressionArmCondition[] $conditions */ - public function __construct(private array $conditions, private int $line) + public function __construct(private MatchExpressionArmBody $body, private array $conditions, private int $line) { } + public function getBody(): MatchExpressionArmBody + { + return $this->body; + } + /** * @return MatchExpressionArmCondition[] */ diff --git a/src/Node/MatchExpressionArmBody.php b/src/Node/MatchExpressionArmBody.php new file mode 100644 index 0000000000..dbb6f3f917 --- /dev/null +++ b/src/Node/MatchExpressionArmBody.php @@ -0,0 +1,28 @@ +scope; + } + + public function getBody(): Expr + { + return $this->body; + } + +} diff --git a/src/Node/MatchExpressionArmCondition.php b/src/Node/MatchExpressionArmCondition.php index 2928175be7..95a291cce3 100644 --- a/src/Node/MatchExpressionArmCondition.php +++ b/src/Node/MatchExpressionArmCondition.php @@ -5,8 +5,10 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -/** @api */ -class MatchExpressionArmCondition +/** + * @api + */ +final class MatchExpressionArmCondition { public function __construct(private Expr $condition, private Scope $scope, private int $line) diff --git a/src/Node/MatchExpressionNode.php b/src/Node/MatchExpressionNode.php index 00ec3bcf8c..fb7f360642 100644 --- a/src/Node/MatchExpressionNode.php +++ b/src/Node/MatchExpressionNode.php @@ -6,8 +6,10 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; -/** @api */ -class MatchExpressionNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class MatchExpressionNode extends NodeAbstract implements VirtualNode { /** diff --git a/src/Node/Method/MethodCall.php b/src/Node/Method/MethodCall.php index 19c77316fb..d3726915a9 100644 --- a/src/Node/Method/MethodCall.php +++ b/src/Node/Method/MethodCall.php @@ -7,8 +7,10 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; -/** @api */ -class MethodCall +/** + * @api + */ +final class MethodCall { public function __construct( diff --git a/src/Node/MethodCallableNode.php b/src/Node/MethodCallableNode.php index aff7b5cefc..b1e52bf1e6 100644 --- a/src/Node/MethodCallableNode.php +++ b/src/Node/MethodCallableNode.php @@ -5,8 +5,10 @@ use PhpParser\Node\Expr; use PhpParser\Node\Identifier; -/** @api */ -class MethodCallableNode extends Expr implements VirtualNode +/** + * @api + */ +final class MethodCallableNode extends Expr implements VirtualNode { public function __construct( diff --git a/src/Node/MethodReturnStatementsNode.php b/src/Node/MethodReturnStatementsNode.php index ca0e3abb82..2444df30b5 100644 --- a/src/Node/MethodReturnStatementsNode.php +++ b/src/Node/MethodReturnStatementsNode.php @@ -2,34 +2,46 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use function count; -/** @api */ -class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode +/** + * @api + */ +final class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { private ClassMethod $classMethod; /** - * @param ReturnStatement[] $returnStatements - * @param ExecutionEndNode[] $executionEnds + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( ClassMethod $method, private array $returnStatements, + private array $yieldStatements, private StatementResult $statementResult, private array $executionEnds, + private array $impurePoints, + private ClassReflection $classReflection, + private PhpMethodFromParserNodeReflection $methodReflection, ) { parent::__construct($method->getAttributes()); $this->classMethod = $method; } - /** - * @return ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -40,14 +52,16 @@ public function getStatementResult(): StatementResult return $this->statementResult; } - /** - * @return ExecutionEndNode[] - */ public function getExecutionEnds(): array { return $this->executionEnds; } + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function returnsByRef(): bool { return $this->classMethod->byRef; @@ -58,9 +72,47 @@ public function hasNativeReturnTypehint(): bool return $this->classMethod->returnType !== null; } + public function getMethodName(): string + { + return $this->classMethod->name->toString(); + } + + public function getYieldStatements(): array + { + return $this->yieldStatements; + } + + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + public function getMethodReflection(): PhpMethodFromParserNodeReflection + { + return $this->methodReflection; + } + + /** + * @return Stmt[] + */ + public function getStatements(): array + { + $stmts = $this->classMethod->getStmts(); + if ($stmts === null) { + return []; + } + + return $stmts; + } + + public function isGenerator(): bool + { + return count($this->yieldStatements) > 0; + } + public function getType(): string { - return 'PHPStan_Node_FunctionReturnStatementsNode'; + return 'PHPStan_Node_MethodReturnStatementsNode'; } /** diff --git a/src/Node/NoopExpressionNode.php b/src/Node/NoopExpressionNode.php new file mode 100644 index 0000000000..6723ee9259 --- /dev/null +++ b/src/Node/NoopExpressionNode.php @@ -0,0 +1,39 @@ +originalExpr->getAttributes()); + } + + public function getOriginalExpr(): Expr + { + return $this->originalExpr; + } + + public function hasAssign(): bool + { + return $this->hasAssign; + } + + public function getType(): string + { + return 'PHPStan_Node_NoopExpressionNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Printer/ExprPrinter.php b/src/Node/Printer/ExprPrinter.php new file mode 100644 index 0000000000..32505ef568 --- /dev/null +++ b/src/Node/Printer/ExprPrinter.php @@ -0,0 +1,29 @@ +getAttribute('phpstan_cache_printer'); + if ($exprString === null) { + $exprString = $this->printer->prettyPrintExpr($expr); + $expr->setAttribute('phpstan_cache_printer', $exprString); + } + + return $exprString; + } + +} diff --git a/src/Node/Printer/NodeTypePrinter.php b/src/Node/Printer/NodeTypePrinter.php new file mode 100644 index 0000000000..f2a110f048 --- /dev/null +++ b/src/Node/Printer/NodeTypePrinter.php @@ -0,0 +1,52 @@ +type); + } + + if ($type instanceof Node\UnionType) { + return implode('|', array_map(static function ($innerType): string { + $printedType = self::printType($innerType); + if ($printedType === null) { + throw new ShouldNotHappenException(); + } + + return $printedType; + }, $type->types)); + } + + if ($type instanceof Node\IntersectionType) { + return implode('&', array_map(static function ($innerType): string { + $printedType = self::printType($innerType); + if ($printedType === null) { + throw new ShouldNotHappenException(); + } + + return $printedType; + }, $type->types)); + } + + if ($type instanceof Node\Identifier || $type instanceof Node\Name) { + return $type->toString(); + } + + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php new file mode 100644 index 0000000000..6d9dab1062 --- /dev/null +++ b/src/Node/Printer/Printer.php @@ -0,0 +1,93 @@ +getExprType()->describe(VerbosityLevel::precise())); + } + + protected function pPHPStan_Node_GetOffsetValueTypeExpr(GetOffsetValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanGetOffsetValueType(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + + protected function pPHPStan_Node_UnsetOffsetExpr(UnsetOffsetExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanUnsetOffset(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + + protected function pPHPStan_Node_GetIterableValueTypeExpr(GetIterableValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanGetIterableValueType(%s)', $this->p($expr->getExpr())); + } + + protected function pPHPStan_Node_GetIterableKeyTypeExpr(GetIterableKeyTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanGetIterableKeyType(%s)', $this->p($expr->getExpr())); + } + + protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $expr): string // phpcs:ignore + { + return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + + protected function pPHPStan_Node_OriginalPropertyTypeExpr(OriginalPropertyTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanOriginalPropertyType(%s)', $this->p($expr->getPropertyFetch())); + } + + protected function pPHPStan_Node_SetOffsetValueTypeExpr(SetOffsetValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanSetOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $expr->getDim() !== null ? $this->p($expr->getDim()) : 'null', $this->p($expr->getValue())); + } + + protected function pPHPStan_Node_SetExistingOffsetValueTypeExpr(SetExistingOffsetValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanSetExistingOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()), $this->p($expr->getValue())); + } + + protected function pPHPStan_Node_AlwaysRememberedExpr(AlwaysRememberedExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanRembered(%s)', $this->p($expr->getExpr())); + } + + protected function pPHPStan_Node_PropertyInitializationExpr(PropertyInitializationExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanPropertyInitialization(%s)', $expr->getPropertyName()); + } + + protected function pPHPStan_Node_ParameterVariableOriginalValueExpr(ParameterVariableOriginalValueExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanParameterVariableOriginalValue(%s)', $expr->getVariableName()); + } + + protected function pPHPStan_Node_IssetExpr(IssetExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanIssetExpr(%s)', $this->p($expr->getExpr())); + } + +} diff --git a/src/Node/Property/PropertyAssign.php b/src/Node/Property/PropertyAssign.php new file mode 100644 index 0000000000..a88d384a73 --- /dev/null +++ b/src/Node/Property/PropertyAssign.php @@ -0,0 +1,31 @@ +assign; + } + + public function getScope(): Scope + { + return $this->scope; + } + +} diff --git a/src/Node/Property/PropertyRead.php b/src/Node/Property/PropertyRead.php index 27bc3ba5c4..86c220b77f 100644 --- a/src/Node/Property/PropertyRead.php +++ b/src/Node/Property/PropertyRead.php @@ -6,8 +6,10 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Analyser\Scope; -/** @api */ -class PropertyRead +/** + * @api + */ +final class PropertyRead { public function __construct( diff --git a/src/Node/Property/PropertyWrite.php b/src/Node/Property/PropertyWrite.php index 00970b832d..df39b83d0b 100644 --- a/src/Node/Property/PropertyWrite.php +++ b/src/Node/Property/PropertyWrite.php @@ -6,11 +6,13 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Analyser\Scope; -/** @api */ -class PropertyWrite +/** + * @api + */ +final class PropertyWrite { - public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope) + public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite) { } @@ -27,4 +29,9 @@ public function getScope(): Scope return $this->scope; } + public function isPromotedPropertyWrite(): bool + { + return $this->promotedPropertyWrite; + } + } diff --git a/src/Node/PropertyAssignNode.php b/src/Node/PropertyAssignNode.php index 62a5d7ac1e..7537b8935d 100644 --- a/src/Node/PropertyAssignNode.php +++ b/src/Node/PropertyAssignNode.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr; use PhpParser\NodeAbstract; -class PropertyAssignNode extends NodeAbstract implements VirtualNode +final class PropertyAssignNode extends NodeAbstract implements VirtualNode { public function __construct( diff --git a/src/Node/PropertyHookReturnStatementsNode.php b/src/Node/PropertyHookReturnStatementsNode.php new file mode 100644 index 0000000000..42db85ee6d --- /dev/null +++ b/src/Node/PropertyHookReturnStatementsNode.php @@ -0,0 +1,111 @@ + $returnStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints + */ + public function __construct( + private PropertyHook $hook, + private array $returnStatements, + private StatementResult $statementResult, + private array $executionEnds, + private array $impurePoints, + private ClassReflection $classReflection, + private PhpMethodFromParserNodeReflection $hookReflection, + private PhpPropertyReflection $propertyReflection, + ) + { + parent::__construct($hook->getAttributes()); + } + + public function getPropertyHookNode(): PropertyHook + { + return $this->hook; + } + + public function returnsByRef(): bool + { + return $this->hook->byRef; + } + + public function hasNativeReturnTypehint(): bool + { + return false; + } + + public function getYieldStatements(): array + { + return []; + } + + public function isGenerator(): bool + { + return false; + } + + public function getReturnStatements(): array + { + return $this->returnStatements; + } + + public function getStatementResult(): StatementResult + { + return $this->statementResult; + } + + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + public function getHookReflection(): PhpMethodFromParserNodeReflection + { + return $this->hookReflection; + } + + public function getPropertyReflection(): PhpPropertyReflection + { + return $this->propertyReflection; + } + + public function getType(): string + { + return 'PHPStan_Node_PropertyHookReturnStatementsNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/PropertyHookStatementNode.php b/src/Node/PropertyHookStatementNode.php new file mode 100644 index 0000000000..34bdbcfd31 --- /dev/null +++ b/src/Node/PropertyHookStatementNode.php @@ -0,0 +1,51 @@ +propertyHook->getAttributes()); + } + + public function getPropertyHook(): PropertyHook + { + return $this->propertyHook; + } + + /** + * @return null + */ + public function getReturnType() + { + return null; + } + + public function getType(): string + { + return 'PHPStan_Node_PropertyHookStatementNode'; + } + + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/ReturnStatement.php b/src/Node/ReturnStatement.php index 99767fba7b..153faf1534 100644 --- a/src/Node/ReturnStatement.php +++ b/src/Node/ReturnStatement.php @@ -6,8 +6,10 @@ use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -/** @api */ -class ReturnStatement +/** + * @api + */ +final class ReturnStatement { private Node\Stmt\Return_ $returnNode; diff --git a/src/Node/ReturnStatementsNode.php b/src/Node/ReturnStatementsNode.php index f54506d201..34c28ef538 100644 --- a/src/Node/ReturnStatementsNode.php +++ b/src/Node/ReturnStatementsNode.php @@ -2,6 +2,9 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; /** @api */ @@ -9,12 +12,31 @@ interface ReturnStatementsNode extends VirtualNode { /** - * @return ReturnStatement[] + * @return list */ public function getReturnStatements(): array; public function getStatementResult(): StatementResult; + /** + * @return list + */ + public function getExecutionEnds(): array; + + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array; + public function returnsByRef(): bool; + public function hasNativeReturnTypehint(): bool; + + /** + * @return list + */ + public function getYieldStatements(): array; + + public function isGenerator(): bool; + } diff --git a/src/Node/StaticMethodCallableNode.php b/src/Node/StaticMethodCallableNode.php index a4c6e675ef..407a5cfa4c 100644 --- a/src/Node/StaticMethodCallableNode.php +++ b/src/Node/StaticMethodCallableNode.php @@ -6,8 +6,10 @@ use PhpParser\Node\Identifier; use PhpParser\Node\Name; -/** @api */ -class StaticMethodCallableNode extends Expr implements VirtualNode +/** + * @api + */ +final class StaticMethodCallableNode extends Expr implements VirtualNode { public function __construct( diff --git a/src/Node/UnreachableStatementNode.php b/src/Node/UnreachableStatementNode.php index fb05c30a50..3e2c72e29b 100644 --- a/src/Node/UnreachableStatementNode.php +++ b/src/Node/UnreachableStatementNode.php @@ -4,11 +4,14 @@ use PhpParser\Node\Stmt; -/** @api */ -class UnreachableStatementNode extends Stmt implements VirtualNode +/** + * @api + */ +final class UnreachableStatementNode extends Stmt implements VirtualNode { - public function __construct(private Stmt $originalStatement) + /** @param Stmt[] $nextStatements */ + public function __construct(private Stmt $originalStatement, private array $nextStatements = []) { parent::__construct($originalStatement->getAttributes()); } @@ -31,4 +34,12 @@ public function getSubNodeNames(): array return []; } + /** + * @return Stmt[] + */ + public function getNextStatements(): array + { + return $this->nextStatements; + } + } diff --git a/src/Node/VarTagChangedExpressionTypeNode.php b/src/Node/VarTagChangedExpressionTypeNode.php new file mode 100644 index 0000000000..c57a90f7a5 --- /dev/null +++ b/src/Node/VarTagChangedExpressionTypeNode.php @@ -0,0 +1,40 @@ +getAttributes()); + } + + public function getVarTag(): VarTag + { + return $this->varTag; + } + + public function getExpr(): Expr + { + return $this->expr; + } + + public function getType(): string + { + return 'PHPStan_Node_VarTagChangedExpressionType'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/VariableAssignNode.php b/src/Node/VariableAssignNode.php new file mode 100644 index 0000000000..695f59bf2a --- /dev/null +++ b/src/Node/VariableAssignNode.php @@ -0,0 +1,48 @@ +getAttributes()); + } + + public function getVariable(): Expr\Variable + { + return $this->variable; + } + + public function getAssignedExpr(): Expr + { + return $this->assignedExpr; + } + + public function isAssignOp(): bool + { + return $this->assignOp; + } + + public function getType(): string + { + return 'PHPStan_Node_VariableAssignNodeNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/NodeVisitor/StatementOrderVisitor.php b/src/NodeVisitor/StatementOrderVisitor.php deleted file mode 100644 index 896c230275..0000000000 --- a/src/NodeVisitor/StatementOrderVisitor.php +++ /dev/null @@ -1,89 +0,0 @@ -orderStack = [0]; - $this->depth = 0; - - return null; - } - - /** - * @return null - */ - public function enterNode(Node $node) - { - $order = $this->orderStack[count($this->orderStack) - 1]; - $node->setAttribute('statementOrder', $order); - $node->setAttribute('statementDepth', $this->depth); - - if ( - ($node instanceof Node\Expr || $node instanceof Node\Arg) - && count($this->expressionOrderStack) > 0 - ) { - $expressionOrder = $this->expressionOrderStack[count($this->expressionOrderStack) - 1]; - $node->setAttribute('expressionOrder', $expressionOrder); - $node->setAttribute('expressionDepth', $this->expressionDepth); - $this->expressionOrderStack[count($this->expressionOrderStack) - 1] = $expressionOrder + 1; - $this->expressionOrderStack[] = 0; - $this->expressionDepth++; - } - - if (!$node instanceof Node\Stmt) { - return null; - } - - $this->orderStack[count($this->orderStack) - 1] = $order + 1; - $this->orderStack[] = 0; - $this->depth++; - - $this->expressionOrderStack = [0]; - $this->expressionDepth = 0; - - return null; - } - - /** - * @return null - */ - public function leaveNode(Node $node) - { - if ($node instanceof Node\Expr) { - array_pop($this->expressionOrderStack); - $this->expressionDepth--; - } - if (!$node instanceof Node\Stmt) { - return null; - } - - array_pop($this->orderStack); - $this->depth--; - - return null; - } - -} diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index 3cae4c553b..96dcd36820 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -8,9 +8,12 @@ use Nette\Utils\Random; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; -use PHPStan\Dependency\ExportedNode; +use PHPStan\Analyser\InternalError; +use PHPStan\Dependency\RootExportedNode; use PHPStan\Process\ProcessHelper; -use React\EventLoop\StreamSelectLoop; +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\TcpServer; use Symfony\Component\Console\Input\InputInterface; @@ -18,16 +21,18 @@ use function array_map; use function array_pop; use function array_reverse; +use function array_sum; use function count; use function defined; -use function escapeshellarg; -use function is_string; +use function ini_get; use function max; +use function memory_get_usage; use function parse_url; use function sprintf; +use function str_contains; use const PHP_URL_PORT; -class ParallelAnalyser +final class ParallelAnalyser { private const DEFAULT_TIMEOUT = 600.0; @@ -47,26 +52,68 @@ public function __construct( /** * @param Closure(int ): void|null $postFileCallback + * @param (callable(list, list, string[]): void)|null $onFileAnalysisHandler + * @return PromiseInterface */ public function analyse( + LoopInterface $loop, Schedule $schedule, string $mainScript, ?Closure $postFileCallback, ?string $projectConfigFile, - ?string $tmpFile, - ?string $insteadOfFile, InputInterface $input, - ): AnalyserResult + ?callable $onFileAnalysisHandler, + ): PromiseInterface { $jobs = array_reverse($schedule->getJobs()); - $loop = new StreamSelectLoop(); $numberOfProcesses = $schedule->getNumberOfProcesses(); + $someChildEnded = false; $errors = []; + $filteredPhpErrors = []; + $allPhpErrors = []; + $locallyIgnoredErrors = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; + $peakMemoryUsages = []; $internalErrors = []; + $internalErrorsCount = 0; + $collectedData = []; + $dependencies = []; + $reachedInternalErrorsCountLimit = false; + $exportedNodes = []; + + /** @var Deferred $deferred */ + $deferred = new Deferred(); $server = new TcpServer('127.0.0.1:0', $loop); - $this->processPool = new ProcessPool($server); + $this->processPool = new ProcessPool($server, static function () use ($deferred, &$jobs, &$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$exportedNodes, &$peakMemoryUsages): void { + if (count($jobs) > 0 && $internalErrorsCount === 0) { + $internalErrors[] = new InternalError( + 'Some parallel worker jobs have not finished.', + 'running parallel worker', + [], + null, + true, + ); + $internalErrorsCount++; + } + + $deferred->resolve(new AnalyserResult( + $errors, + $filteredPhpErrors, + $allPhpErrors, + $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, + $internalErrors, + $collectedData, + $internalErrorsCount === 0 ? $dependencies : null, + $exportedNodes, + $reachedInternalErrorsCountLimit, + array_sum($peakMemoryUsages), // not 100% correct as the peak usages of workers might not have met + )); + }); $server->on('connection', function (ConnectionInterface $connection) use (&$jobs): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; @@ -93,22 +140,22 @@ public function analyse( /** @var string $serverAddress */ $serverAddress = $server->getAddress(); - /** @var int $serverPort */ + /** @var int<0, 65535> $serverPort */ $serverPort = parse_url(http://github.com/$serverAddress,%20PHP_URL_PORT); - $internalErrorsCount = 0; - - $reachedInternalErrorsCountLimit = false; - $handleError = function (Throwable $error) use (&$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit): void { - $internalErrors[] = sprintf('Internal error: ' . $error->getMessage()); + $internalErrors[] = new InternalError( + $error->getMessage(), + 'communicating with parallel worker', + InternalError::prepareTrace($error), + $error->getTraceAsString(), + !$error instanceof ProcessTimedOutException, + ); $internalErrorsCount++; $reachedInternalErrorsCountLimit = true; $this->processPool->quitAll(); }; - $dependencies = []; - $exportedNodes = []; for ($i = 0; $i < $numberOfProcesses; $i++) { if (count($jobs) === 0) { break; @@ -122,13 +169,6 @@ public function analyse( $processIdentifier, ]; - if ($tmpFile !== null && $insteadOfFile !== null) { - $commandOptions[] = '--tmp-file'; - $commandOptions[] = escapeshellarg($tmpFile); - $commandOptions[] = '--instead-of'; - $commandOptions[] = escapeshellarg($insteadOfFile); - } - $process = new Process(ProcessHelper::getWorkerCommand( $mainScript, 'worker', @@ -136,14 +176,46 @@ public function analyse( $commandOptions, $input, ), $loop, $this->processTimeout); - $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$dependencies, &$exportedNodes, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier): void { + $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$exportedNodes, &$peakMemoryUsages, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier, $onFileAnalysisHandler): void { + $fileErrors = []; foreach ($json['errors'] as $jsonError) { - if (is_string($jsonError)) { - $internalErrors[] = sprintf('Internal error: %s', $jsonError); - continue; - } + $fileErrors[] = Error::decode($jsonError); + } + foreach ($json['internalErrors'] as $internalJsonError) { + $internalErrors[] = InternalError::decode($internalJsonError); + } + + foreach ($json['filteredPhpErrors'] as $filteredPhpError) { + $filteredPhpErrors[] = Error::decode($filteredPhpError); + } + + foreach ($json['allPhpErrors'] as $allPhpError) { + $allPhpErrors[] = Error::decode($allPhpError); + } + + $locallyIgnoredFileErrors = []; + foreach ($json['locallyIgnoredErrors'] as $locallyIgnoredJsonError) { + $locallyIgnoredFileErrors[] = Error::decode($locallyIgnoredJsonError); + } + + if ($onFileAnalysisHandler !== null) { + $onFileAnalysisHandler($fileErrors, $locallyIgnoredFileErrors, $json['files']); + } + + foreach ($fileErrors as $fileError) { + $errors[] = $fileError; + } + + foreach ($locallyIgnoredFileErrors as $locallyIgnoredFileError) { + $locallyIgnoredErrors[] = $locallyIgnoredFileError; + } - $errors[] = Error::decode($jsonError); + foreach ($json['collectedData'] as $file => $jsonDataByCollector) { + foreach ($jsonDataByCollector as $collectorType => $listOfCollectedData) { + foreach ($listOfCollectedData as $rawCollectedData) { + $collectedData[$file][$collectorType][] = $rawCollectedData; + } + } } /** @@ -154,6 +226,20 @@ public function analyse( $dependencies[$file] = $fileDependencies; } + foreach ($json['linesToIgnore'] as $file => $fileLinesToIgnore) { + if (count($fileLinesToIgnore) === 0) { + continue; + } + $linesToIgnore[$file] = $fileLinesToIgnore; + } + + foreach ($json['unmatchedLineIgnores'] as $file => $fileUnmatchedLineIgnores) { + if (count($fileUnmatchedLineIgnores) === 0) { + continue; + } + $unmatchedLineIgnores[$file] = $fileUnmatchedLineIgnores; + } + /** * @var string $file * @var array $fileExportedNodes @@ -162,7 +248,7 @@ public function analyse( if (count($fileExportedNodes) === 0) { continue; } - $exportedNodes[$file] = array_map(static function (array $node): ExportedNode { + $exportedNodes[$file] = array_map(static function (array $node): RootExportedNode { $class = $node['type']; return $class::decode($node['data']); @@ -170,7 +256,11 @@ public function analyse( } if ($postFileCallback !== null) { - $postFileCallback($json['filesCount']); + $postFileCallback(count($json['files'])); + } + + if (!isset($peakMemoryUsages[$processIdentifier]) || $peakMemoryUsages[$processIdentifier] < $json['memoryUsage']) { + $peakMemoryUsages[$processIdentifier] = $json['memoryUsage']; } $internalErrorsCount += $json['internalErrorsCount']; @@ -186,35 +276,50 @@ public function analyse( $job = array_pop($jobs); $process->request(['action' => 'analyse', 'files' => $job]); - }, $handleError, function ($exitCode, string $output) use (&$internalErrors, &$internalErrorsCount, $processIdentifier): void { - $this->processPool->tryQuitProcess($processIdentifier); + }, $handleError, function ($exitCode, string $output) use (&$someChildEnded, &$peakMemoryUsages, &$internalErrors, &$internalErrorsCount, $processIdentifier): void { + if ($someChildEnded === false) { + $peakMemoryUsages['main'] = memory_get_usage(true); + } + $someChildEnded = true; + if ($exitCode === 0) { + $this->processPool->tryQuitProcess($processIdentifier); return; } if ($exitCode === null) { + $this->processPool->tryQuitProcess($processIdentifier); return; } - $internalErrors[] = sprintf('Child process error (exit code %d): %s', $exitCode, $output); + $memoryLimitMessage = 'PHPStan process crashed because it reached configured PHP memory limit'; + if (str_contains($output, $memoryLimitMessage)) { + foreach ($internalErrors as $internalError) { + if (!str_contains($internalError->getMessage(), $memoryLimitMessage)) { + continue; + } + + $this->processPool->tryQuitProcess($processIdentifier); + return; + } + $internalErrors[] = new InternalError(sprintf( + "Child process error: %s: %s\n%s\n", + $memoryLimitMessage, + ini_get('memory_limit'), + 'Increase your memory limit in php.ini or run PHPStan with --memory-limit CLI option.', + ), 'running parallel worker', [], null, false); + $internalErrorsCount++; + $this->processPool->tryQuitProcess($processIdentifier); + return; + } + + $internalErrors[] = new InternalError(sprintf('Child process error (exit code %d): %s', $exitCode, $output), 'running parallel worker', [], null, true); $internalErrorsCount++; + $this->processPool->tryQuitProcess($processIdentifier); }); $this->processPool->attachProcess($processIdentifier, $process); } - $loop->run(); - - if (count($jobs) > 0 && $internalErrorsCount === 0) { - $internalErrors[] = 'Some parallel worker jobs have not finished.'; - $internalErrorsCount++; - } - - return new AnalyserResult( - $errors, - $internalErrors, - $internalErrorsCount === 0 ? $dependencies : null, - $exportedNodes, - $reachedInternalErrorsCountLimit, - ); + return $deferred->promise(); } } diff --git a/src/Parallel/Process.php b/src/Parallel/Process.php index 0cb84ee154..e5cf90566f 100644 --- a/src/Parallel/Process.php +++ b/src/Parallel/Process.php @@ -2,7 +2,6 @@ namespace PHPStan\Parallel; -use Exception; use PHPStan\ShouldNotHappenException; use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; @@ -16,12 +15,12 @@ use function stream_get_contents; use function tmpfile; -class Process +final class Process { public \React\ChildProcess\Process $process; - private WritableStreamInterface $in; + private ?WritableStreamInterface $in = null; /** @var resource */ private $stdOut; @@ -106,10 +105,13 @@ private function cancelTimer(): void public function request(array $data): void { $this->cancelTimer(); + if ($this->in === null) { + throw new ShouldNotHappenException(); + } $this->in->write($data); $this->timer = $this->loop->addTimer($this->timeoutSeconds, function (): void { $onError = $this->onError; - $onError(new Exception(sprintf('Child process timed out after %.1f seconds. Try making it longer with parallel.processTimeout setting.', $this->timeoutSeconds))); + $onError(new ProcessTimedOutException(sprintf('Child process timed out after %.1f seconds. Try making it longer with parallel.processTimeout setting.', $this->timeoutSeconds))); }); } @@ -124,6 +126,10 @@ public function quit(): void $pipe->close(); } + if ($this->in === null) { + return; + } + $this->in->end(); } diff --git a/src/Parallel/ProcessPool.php b/src/Parallel/ProcessPool.php index bebb8f8293..574cabcf6e 100644 --- a/src/Parallel/ProcessPool.php +++ b/src/Parallel/ProcessPool.php @@ -9,14 +9,21 @@ use function count; use function sprintf; -class ProcessPool +final class ProcessPool { /** @var array */ private array $processes = []; - public function __construct(private TcpServer $server) + /** @var callable(): void */ + private $onServerClose; + + /** + * @param callable(): void $onServerClose + */ + public function __construct(private TcpServer $server, callable $onServerClose) { + $this->onServerClose = $onServerClose; } public function getProcess(string $identifier): Process @@ -52,6 +59,8 @@ private function quitProcess(string $identifier): void } $this->server->close(); + $callback = $this->onServerClose; + $callback(); } public function quitAll(): void diff --git a/src/Parallel/ProcessTimedOutException.php b/src/Parallel/ProcessTimedOutException.php new file mode 100644 index 0000000000..50667999a1 --- /dev/null +++ b/src/Parallel/ProcessTimedOutException.php @@ -0,0 +1,10 @@ +maximumNumberOfProcesses), $jobs); + $usedNumberOfProcesses = min($numberOfProcesses, $this->maximumNumberOfProcesses); + $this->storedData = [$cpuCores, count($files), count($jobs), $usedNumberOfProcesses]; + + return new Schedule($usedNumberOfProcesses, $jobs); + } + + public function print(Output $output): void + { + if ($this->storedData === null) { + return; + } + + [$cpuCores, $filesCount, $jobsCount, $usedNumberOfProcesses] = $this->storedData; + + $output->writeLineFormatted('Parallel processing scheduler:'); + $output->writeLineFormatted(sprintf( + '# of detected CPU %s: %s%d', + $cpuCores === 1 ? 'core' : 'cores', + $cpuCores === 1 ? '' : ' ', + $cpuCores, + )); + $output->writeLineFormatted(sprintf('# of analysed files: %d', $filesCount)); + $output->writeLineFormatted(sprintf('# of jobs: %d', $jobsCount)); + $output->writeLineFormatted(sprintf('# of spawned processes: %d', $usedNumberOfProcesses)); + $output->writeLineFormatted(''); } } diff --git a/src/Parser/AnonymousClassVisitor.php b/src/Parser/AnonymousClassVisitor.php new file mode 100644 index 0000000000..16fbe58dec --- /dev/null +++ b/src/Parser/AnonymousClassVisitor.php @@ -0,0 +1,52 @@ +> */ + private array $nodesPerLine = []; + + public function beforeTraverse(array $nodes): ?array + { + $this->nodesPerLine = []; + return null; + } + + public function enterNode(Node $node): ?Node + { + if (!$node instanceof Node\Stmt\Class_ || !$node->isAnonymous()) { + return null; + } + + $node = AnonymousClassNode::createFromClassNode($node); + $node->setAttribute('anonymousClass', true); // We keep this for backward compatibility + $this->nodesPerLine[$node->getStartLine()][] = $node; + + return $node; + } + + public function afterTraverse(array $nodes): ?array + { + foreach ($this->nodesPerLine as $nodesOnLine) { + if (count($nodesOnLine) === 1) { + continue; + } + for ($i = 0; $i < count($nodesOnLine); $i++) { + $nodesOnLine[$i]->setAttribute(self::ATTRIBUTE_LINE_INDEX, $i + 1); + } + } + + $this->nodesPerLine = []; + return null; + } + +} diff --git a/src/Parser/ArrayFilterArgVisitor.php b/src/Parser/ArrayFilterArgVisitor.php new file mode 100644 index 0000000000..a2730deb3d --- /dev/null +++ b/src/Parser/ArrayFilterArgVisitor.php @@ -0,0 +1,27 @@ +name instanceof Node\Name) { + $functionName = $node->name->toLowerString(); + if ($functionName === 'array_filter') { + $args = $node->getRawArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + } + return null; + } + +} diff --git a/src/Parser/ArrayFindArgVisitor.php b/src/Parser/ArrayFindArgVisitor.php new file mode 100644 index 0000000000..0e798eb5c0 --- /dev/null +++ b/src/Parser/ArrayFindArgVisitor.php @@ -0,0 +1,28 @@ +name instanceof Node\Name) { + $functionName = $node->name->toLowerString(); + if (in_array($functionName, ['array_all', 'array_any', 'array_find', 'array_find_key'], true)) { + $args = $node->getRawArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + } + return null; + } + +} diff --git a/src/Parser/ArrayMapArgVisitor.php b/src/Parser/ArrayMapArgVisitor.php new file mode 100644 index 0000000000..0c62d0c7c4 --- /dev/null +++ b/src/Parser/ArrayMapArgVisitor.php @@ -0,0 +1,32 @@ +name instanceof Node\Name && !$node->isFirstClassCallable()) { + $functionName = $node->name->toLowerString(); + if ($functionName === 'array_map') { + $args = $node->getArgs(); + if (isset($args[0])) { + $slicedArgs = array_slice($args, 1); + if (count($slicedArgs) > 0) { + $args[0]->value->setAttribute(self::ATTRIBUTE_NAME, $slicedArgs); + } + } + } + } + return null; + } + +} diff --git a/src/Parser/ArrayWalkArgVisitor.php b/src/Parser/ArrayWalkArgVisitor.php new file mode 100644 index 0000000000..ad776bb175 --- /dev/null +++ b/src/Parser/ArrayWalkArgVisitor.php @@ -0,0 +1,27 @@ +name instanceof Node\Name) { + $functionName = $node->name->toLowerString(); + if ($functionName === 'array_walk') { + $args = $node->getRawArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + } + return null; + } + +} diff --git a/src/Parser/ArrowFunctionArgVisitor.php b/src/Parser/ArrowFunctionArgVisitor.php new file mode 100644 index 0000000000..93cce45dd9 --- /dev/null +++ b/src/Parser/ArrowFunctionArgVisitor.php @@ -0,0 +1,41 @@ +isFirstClassCallable()) { + return null; + } + + if ($node->name instanceof Node\Expr\Assign && $node->name->expr instanceof Node\Expr\ArrowFunction) { + $arrow = $node->name->expr; + } elseif ($node->name instanceof Node\Expr\ArrowFunction) { + $arrow = $node->name; + } else { + return null; + } + + $args = $node->getArgs(); + + if (count($args) > 0) { + $arrow->setAttribute(self::ATTRIBUTE_NAME, $args); + } + + return null; + } + +} diff --git a/src/Parser/CachedParser.php b/src/Parser/CachedParser.php index 225375678c..62bba62a2b 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -6,7 +6,7 @@ use PHPStan\File\FileReader; use function array_slice; -class CachedParser implements Parser +final class CachedParser implements Parser { /** @var array*/ diff --git a/src/Parser/CleaningParser.php b/src/Parser/CleaningParser.php index 98db0e64ef..0f874eafbf 100644 --- a/src/Parser/CleaningParser.php +++ b/src/Parser/CleaningParser.php @@ -6,7 +6,7 @@ use PhpParser\NodeTraverser; use PHPStan\Php\PhpVersion; -class CleaningParser implements Parser +final class CleaningParser implements Parser { private NodeTraverser $traverser; diff --git a/src/Parser/CleaningVisitor.php b/src/Parser/CleaningVisitor.php index b5d406cf17..80f5b2f594 100644 --- a/src/Parser/CleaningVisitor.php +++ b/src/Parser/CleaningVisitor.php @@ -7,8 +7,9 @@ use PhpParser\NodeVisitorAbstract; use PHPStan\Reflection\ParametersAcceptor; use function in_array; +use function is_array; -class CleaningVisitor extends NodeVisitorAbstract +final class CleaningVisitor extends NodeVisitorAbstract { private NodeFinder $nodeFinder; @@ -21,20 +22,28 @@ public function __construct() public function enterNode(Node $node): ?Node { if ($node instanceof Node\Stmt\Function_) { - $node->stmts = $this->keepVariadicsAndYields($node->stmts); + $node->stmts = $this->keepVariadicsAndYields($node->stmts, null); return $node; } if ($node instanceof Node\Stmt\ClassMethod && $node->stmts !== null) { - $node->stmts = $this->keepVariadicsAndYields($node->stmts); + $node->stmts = $this->keepVariadicsAndYields($node->stmts, null); return $node; } if ($node instanceof Node\Expr\Closure) { - $node->stmts = $this->keepVariadicsAndYields($node->stmts); + $node->stmts = $this->keepVariadicsAndYields($node->stmts, null); return $node; } + if ($node instanceof Node\PropertyHook && is_array($node->body)) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $node->body = $this->keepVariadicsAndYields($node->body, $propertyName); + return $node; + } + } + return null; } @@ -42,9 +51,9 @@ public function enterNode(Node $node): ?Node * @param Node\Stmt[] $stmts * @return Node\Stmt[] */ - private function keepVariadicsAndYields(array $stmts): array + private function keepVariadicsAndYields(array $stmts, ?string $hookedPropertyName): array { - $results = $this->nodeFinder->find($stmts, static function (Node $node): bool { + $results = $this->nodeFinder->find($stmts, static function (Node $node) use ($hookedPropertyName): bool { if ($node instanceof Node\Expr\YieldFrom || $node instanceof Node\Expr\Yield_) { return true; } @@ -52,11 +61,33 @@ private function keepVariadicsAndYields(array $stmts): array return in_array($node->name->toLowerString(), ParametersAcceptor::VARIADIC_FUNCTIONS, true); } + if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) { + return true; + } + + if ($hookedPropertyName !== null) { + if ( + $node instanceof Node\Expr\PropertyFetch + && $node->var instanceof Node\Expr\Variable + && $node->var->name === 'this' + && $node->name instanceof Node\Identifier + && $node->name->toString() === $hookedPropertyName + ) { + return true; + } + } + return false; }); $newStmts = []; foreach ($results as $result) { - if ($result instanceof Node\Expr\Yield_ || $result instanceof Node\Expr\YieldFrom) { + if ( + $result instanceof Node\Expr\Yield_ + || $result instanceof Node\Expr\YieldFrom + || $result instanceof Node\Expr\Closure + || $result instanceof Node\Expr\ArrowFunction + || $result instanceof Node\Expr\PropertyFetch + ) { $newStmts[] = new Node\Stmt\Expression($result); continue; } diff --git a/src/Parser/ClosureArgVisitor.php b/src/Parser/ClosureArgVisitor.php new file mode 100644 index 0000000000..c9435f826e --- /dev/null +++ b/src/Parser/ClosureArgVisitor.php @@ -0,0 +1,41 @@ +isFirstClassCallable()) { + return null; + } + + if ($node->name instanceof Node\Expr\Assign && $node->name->expr instanceof Node\Expr\Closure) { + $closure = $node->name->expr; + } elseif ($node->name instanceof Node\Expr\Closure) { + $closure = $node->name; + } else { + return null; + } + + $args = $node->getArgs(); + + if (count($args) > 0) { + $closure->setAttribute(self::ATTRIBUTE_NAME, $args); + } + + return null; + } + +} diff --git a/src/Parser/ClosureBindArgVisitor.php b/src/Parser/ClosureBindArgVisitor.php new file mode 100644 index 0000000000..291ede59b4 --- /dev/null +++ b/src/Parser/ClosureBindArgVisitor.php @@ -0,0 +1,33 @@ +class instanceof Node\Name + && $node->class->toLowerString() === 'closure' + && $node->name instanceof Identifier + && $node->name->toLowerString() === 'bind' + && !$node->isFirstClassCallable() + ) { + $args = $node->getArgs(); + if (count($args) > 1) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + return null; + } + +} diff --git a/src/Parser/ClosureBindToVarVisitor.php b/src/Parser/ClosureBindToVarVisitor.php new file mode 100644 index 0000000000..7196d50093 --- /dev/null +++ b/src/Parser/ClosureBindToVarVisitor.php @@ -0,0 +1,30 @@ +name instanceof Identifier + && $node->name->toLowerString() === 'bindto' + && !$node->isFirstClassCallable() + ) { + $args = $node->getArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, $node->var); + } + } + return null; + } + +} diff --git a/src/Parser/CurlSetOptArgVisitor.php b/src/Parser/CurlSetOptArgVisitor.php new file mode 100644 index 0000000000..f9be2fd5c3 --- /dev/null +++ b/src/Parser/CurlSetOptArgVisitor.php @@ -0,0 +1,27 @@ +name instanceof Node\Name) { + $functionName = $node->name->toLowerString(); + if ($functionName === 'curl_setopt') { + $args = $node->getRawArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + } + return null; + } + +} diff --git a/src/Parser/DeclarePositionVisitor.php b/src/Parser/DeclarePositionVisitor.php new file mode 100644 index 0000000000..e0d523603f --- /dev/null +++ b/src/Parser/DeclarePositionVisitor.php @@ -0,0 +1,44 @@ +isFirstStatement = true; + return null; + } + + public function enterNode(Node $node): ?Node + { + // ignore shebang + if ( + $this->isFirstStatement + && $node instanceof Node\Stmt\InlineHTML + && str_starts_with($node->value, '#!') + ) { + return null; + } + + if ($node instanceof Node\Stmt) { + if ($node instanceof Node\Stmt\Declare_) { + $node->setAttribute(self::ATTRIBUTE_NAME, $this->isFirstStatement); + } + + $this->isFirstStatement = false; + } + + return null; + } + +} diff --git a/src/Parser/FunctionCallStatementFinder.php b/src/Parser/FunctionCallStatementFinder.php deleted file mode 100644 index 4f1b190d35..0000000000 --- a/src/Parser/FunctionCallStatementFinder.php +++ /dev/null @@ -1,47 +0,0 @@ -findFunctionCallInStatements($functionNames, $statement); - if ($result !== null) { - return $result; - } - } - - if (!($statement instanceof Node)) { - continue; - } - - if ($statement instanceof FuncCall && $statement->name instanceof Name) { - if (in_array((string) $statement->name, $functionNames, true)) { - return $statement; - } - } - - $result = $this->findFunctionCallInStatements($functionNames, $statement); - if ($result !== null) { - return $result; - } - } - - return null; - } - -} diff --git a/src/Parser/ImmediatelyInvokedClosureVisitor.php b/src/Parser/ImmediatelyInvokedClosureVisitor.php new file mode 100644 index 0000000000..c77059e214 --- /dev/null +++ b/src/Parser/ImmediatelyInvokedClosureVisitor.php @@ -0,0 +1,22 @@ +name instanceof Node\Expr\Closure) { + $node->name->setAttribute(self::ATTRIBUTE_NAME, true); + } + + return null; + } + +} diff --git a/src/Parser/LastConditionVisitor.php b/src/Parser/LastConditionVisitor.php new file mode 100644 index 0000000000..d20a8f4b90 --- /dev/null +++ b/src/Parser/LastConditionVisitor.php @@ -0,0 +1,93 @@ +elseifs !== []) { + $lastElseIf = count($node->elseifs) - 1; + + $elseIsMissingOrThrowing = $node->else === null + || ( + count($node->else->stmts) === 1 + && $node->else->stmts[0] instanceof Node\Stmt\Expression + && $node->else->stmts[0]->expr instanceof Node\Expr\Throw_ + ); + + foreach ($node->elseifs as $i => $elseif) { + $isLast = $i === $lastElseIf && $elseIsMissingOrThrowing; + $elseif->cond->setAttribute(self::ATTRIBUTE_NAME, $isLast); + } + } + + if ($node instanceof Node\Expr\Match_ && $node->arms !== []) { + $lastArm = count($node->arms) - 1; + + foreach ($node->arms as $i => $arm) { + if ($arm->conds === null || $arm->conds === []) { + continue; + } + + $isLast = $i === $lastArm; + $index = count($arm->conds) - 1; + $arm->conds[$index]->setAttribute(self::ATTRIBUTE_NAME, $isLast); + $arm->conds[$index]->setAttribute(self::ATTRIBUTE_IS_MATCH_NAME, true); + } + } + + if ( + $node instanceof Node\Stmt\Function_ + || $node instanceof Node\Stmt\ClassMethod + || $node instanceof Node\Stmt\If_ + || $node instanceof Node\Stmt\ElseIf_ + || $node instanceof Node\Stmt\Else_ + || $node instanceof Node\Stmt\Case_ + || $node instanceof Node\Stmt\Catch_ + || $node instanceof Node\Stmt\Do_ + || $node instanceof Node\Stmt\Finally_ + || $node instanceof Node\Stmt\For_ + || $node instanceof Node\Stmt\Foreach_ + || $node instanceof Node\Stmt\Namespace_ + || $node instanceof Node\Stmt\TryCatch + || $node instanceof Node\Stmt\While_ + ) { + $statements = $node->stmts ?? []; + $statementCount = count($statements); + + if ($statementCount < 2) { + return null; + } + + $lastStatement = $statements[$statementCount - 1]; + + if (!$lastStatement instanceof Node\Stmt\Expression) { + return null; + } + + if (!$lastStatement->expr instanceof Node\Expr\Throw_) { + return null; + } + + if (!$statements[$statementCount - 2] instanceof Node\Stmt\If_ || $statements[$statementCount - 2]->else !== null) { + return null; + } + + $if = $statements[$statementCount - 2]; + $cond = count($if->elseifs) > 0 ? $if->elseifs[count($if->elseifs) - 1]->cond : $if->cond; + $cond->setAttribute(self::ATTRIBUTE_NAME, true); + } + + return null; + } + +} diff --git a/src/Parser/LexerFactory.php b/src/Parser/LexerFactory.php index a2d52481f5..e02bc5ed2c 100644 --- a/src/Parser/LexerFactory.php +++ b/src/Parser/LexerFactory.php @@ -6,7 +6,7 @@ use PHPStan\Php\PhpVersion; use const PHP_VERSION_ID; -class LexerFactory +final class LexerFactory { public function __construct(private PhpVersion $phpVersion) @@ -15,14 +15,16 @@ public function __construct(private PhpVersion $phpVersion) public function create(): Lexer { - $options = ['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]; if ($this->phpVersion->getVersionId() === PHP_VERSION_ID) { - return new Lexer($options); + return new Lexer(); } - $options['phpVersion'] = $this->phpVersion->getVersionString(); + return new Lexer\Emulative(\PhpParser\PhpVersion::fromString($this->phpVersion->getVersionString())); + } - return new Lexer\Emulative($options); + public function createEmulative(): Lexer\Emulative + { + return new Lexer\Emulative(); } } diff --git a/src/Parser/LineAttributesVisitor.php b/src/Parser/LineAttributesVisitor.php new file mode 100644 index 0000000000..53f3f3f50f --- /dev/null +++ b/src/Parser/LineAttributesVisitor.php @@ -0,0 +1,28 @@ +getStartLine() === -1) { + $node->setAttribute('startLine', $this->startLine); + } + + if ($node->getEndLine() === -1) { + $node->setAttribute('endLine', $this->endLine); + } + + return $node; + } + +} diff --git a/src/Parser/MagicConstantParamDefaultVisitor.php b/src/Parser/MagicConstantParamDefaultVisitor.php new file mode 100644 index 0000000000..455c341e4e --- /dev/null +++ b/src/Parser/MagicConstantParamDefaultVisitor.php @@ -0,0 +1,21 @@ +default instanceof Node\Scalar\MagicConst) { + $node->default->setAttribute(self::ATTRIBUTE_NAME, true); + } + return null; + } + +} diff --git a/src/Parser/NewAssignedToPropertyVisitor.php b/src/Parser/NewAssignedToPropertyVisitor.php new file mode 100644 index 0000000000..209e72730d --- /dev/null +++ b/src/Parser/NewAssignedToPropertyVisitor.php @@ -0,0 +1,26 @@ +var instanceof Node\Expr\PropertyFetch || $node->var instanceof Node\Expr\StaticPropertyFetch) + && $node->expr instanceof Node\Expr\New_ + ) { + $node->expr->setAttribute(self::ATTRIBUTE_NAME, $node->var); + } + } + return null; + } + +} diff --git a/src/Parser/NodeList.php b/src/Parser/NodeList.php deleted file mode 100644 index 13e69eef13..0000000000 --- a/src/Parser/NodeList.php +++ /dev/null @@ -1,37 +0,0 @@ -next !== null) { - $current = $current->next; - } - - $new = new self($node); - $current->next = $new; - - return $new; - } - - public function getNode(): Node - { - return $this->node; - } - - public function getNext(): ?self - { - return $this->next; - } - -} diff --git a/src/Parser/ParentStmtTypesVisitor.php b/src/Parser/ParentStmtTypesVisitor.php new file mode 100644 index 0000000000..a7da560658 --- /dev/null +++ b/src/Parser/ParentStmtTypesVisitor.php @@ -0,0 +1,50 @@ +> */ + private array $typeStack = []; + + public function beforeTraverse(array $nodes): ?array + { + $this->typeStack = []; + return null; + } + + public function enterNode(Node $node): ?Node + { + if (!$node instanceof Node\Stmt && !$node instanceof Node\Expr\Closure) { + return null; + } + + if (count($this->typeStack) > 0) { + $node->setAttribute(self::ATTRIBUTE_NAME, $this->typeStack); + } + $this->typeStack[] = get_class($node); + + return null; + } + + public function leaveNode(Node $node): ?Node + { + if (!$node instanceof Node\Stmt && !$node instanceof Node\Expr\Closure) { + return null; + } + + array_pop($this->typeStack); + + return null; + } + +} diff --git a/src/Parser/ParserErrorsException.php b/src/Parser/ParserErrorsException.php index 240be67455..d68d18220a 100644 --- a/src/Parser/ParserErrorsException.php +++ b/src/Parser/ParserErrorsException.php @@ -5,11 +5,15 @@ use Exception; use PhpParser\Error; use function array_map; +use function count; use function implode; -class ParserErrorsException extends Exception +final class ParserErrorsException extends Exception { + /** @var mixed[] */ + private array $attributes; + /** * @param Error[] $errors */ @@ -18,7 +22,12 @@ public function __construct( private ?string $parsedFile, ) { - parent::__construct(implode(', ', array_map(static fn (Error $error): string => $error->getMessage(), $errors))); + parent::__construct(implode(', ', array_map(static fn (Error $error): string => $error->getRawMessage(), $errors))); + if (count($errors) > 0) { + $this->attributes = $errors[0]->getAttributes(); + } else { + $this->attributes = []; + } } /** @@ -34,4 +43,12 @@ public function getParsedFile(): ?string return $this->parsedFile; } + /** + * @return mixed[] + */ + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Parser/PathRoutingParser.php b/src/Parser/PathRoutingParser.php index 9420459618..83fae0a891 100644 --- a/src/Parser/PathRoutingParser.php +++ b/src/Parser/PathRoutingParser.php @@ -4,9 +4,16 @@ use PHPStan\File\FileHelper; use function array_fill_keys; -use function strpos; +use function array_slice; +use function count; +use function explode; +use function implode; +use function is_link; +use function realpath; +use function str_contains; +use const DIRECTORY_SEPARATOR; -class PathRoutingParser implements Parser +final class PathRoutingParser implements Parser { /** @var bool[] filePath(string) => bool(true) */ @@ -31,12 +38,34 @@ public function setAnalysedFiles(array $files): void public function parseFile(string $file): array { - if (strpos($this->fileHelper->normalizePath($file, '/'), 'vendor/jetbrains/phpstorm-stubs') !== false) { + $normalizedPath = $this->fileHelper->normalizePath($file, '/'); + if (str_contains($normalizedPath, 'vendor/jetbrains/phpstorm-stubs')) { + return $this->php8Parser->parseFile($file); + } + if (str_contains($normalizedPath, 'vendor/phpstan/php-8-stubs/stubs')) { return $this->php8Parser->parseFile($file); } $file = $this->fileHelper->normalizePath($file); if (!isset($this->analysedFiles[$file])) { + // check symlinked file that still might be in analysedFiles + $pathParts = explode(DIRECTORY_SEPARATOR, $file); + for ($i = count($pathParts); $i > 1; $i--) { + $joinedPartOfPath = implode(DIRECTORY_SEPARATOR, array_slice($pathParts, 0, $i)); + if (!@is_link($joinedPartOfPath)) { + continue; + } + + $realFilePath = realpath($file); + if ($realFilePath !== false) { + $normalizedRealFilePath = $this->fileHelper->normalizePath($realFilePath); + if (isset($this->analysedFiles[$normalizedRealFilePath])) { + return $this->currentPhpVersionRichParser->parseFile($file); + } + } + break; + } + return $this->currentPhpVersionSimpleParser->parseFile($file); } diff --git a/src/Parser/PhpParserDecorator.php b/src/Parser/PhpParserDecorator.php index d50ddee17b..7481574450 100644 --- a/src/Parser/PhpParserDecorator.php +++ b/src/Parser/PhpParserDecorator.php @@ -6,9 +6,10 @@ use PhpParser\ErrorHandler; use PhpParser\Node; use PhpParser\Parser; +use PHPStan\ShouldNotHappenException; use function sprintf; -class PhpParserDecorator implements Parser +final class PhpParserDecorator implements Parser { public function __construct(private \PHPStan\Parser\Parser $wrappedParser) @@ -27,8 +28,13 @@ public function parse(string $code, ?ErrorHandler $errorHandler = null): array if ($e->getParsedFile() !== null) { $message .= sprintf(' in file %s', $e->getParsedFile()); } - throw new Error($message); + throw new Error($message, $e->getAttributes()); } } + public function getTokens(): array + { + throw new ShouldNotHappenException('PhpParserDecorator::getTokens() should not be called'); + } + } diff --git a/src/Parser/PhpParserFactory.php b/src/Parser/PhpParserFactory.php new file mode 100644 index 0000000000..3a1f2cb4ea --- /dev/null +++ b/src/Parser/PhpParserFactory.php @@ -0,0 +1,28 @@ +phpVersion->getVersionString()); + if ($this->phpVersion->getVersionId() >= 80000) { + return new Php8($this->lexer, $phpVersion); + } + + return new Php7($this->lexer, $phpVersion); + } + +} diff --git a/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php b/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php index 076ddcccfd..b57afc5fba 100644 --- a/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php +++ b/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php @@ -8,22 +8,15 @@ use function count; use function version_compare; -class RemoveUnusedCodeByPhpVersionIdVisitor extends NodeVisitorAbstract +final class RemoveUnusedCodeByPhpVersionIdVisitor extends NodeVisitorAbstract { public function __construct(private string $phpVersionString) { } - public function enterNode(Node $node): Node|int|null + public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Stmt\ClassLike) { - return null; - } - if ($node instanceof Node\FunctionLike) { - return null; - } - if (!$node instanceof Node\Stmt\If_) { return null; } @@ -53,8 +46,7 @@ public function enterNode(Node $node): Node|int|null $operator = $cond->getOperatorSigil(); if ($operator === '===') { $operator = '=='; - } - if ($operator === '!==') { + } elseif ($operator === '!==') { $operator = '!='; } @@ -85,7 +77,7 @@ public function enterNode(Node $node): Node|int|null private function getOperands(Node\Expr $left, Node\Expr $right): ?array { if ( - $left instanceof Node\Scalar\LNumber + $left instanceof Node\Scalar\Int_ && $right instanceof Node\Expr\ConstFetch && $right->name->toString() === 'PHP_VERSION_ID' ) { @@ -93,7 +85,7 @@ private function getOperands(Node\Expr $left, Node\Expr $right): ?array } if ( - $right instanceof Node\Scalar\LNumber + $right instanceof Node\Scalar\Int_ && $left instanceof Node\Expr\ConstFetch && $left->name->toString() === 'PHP_VERSION_ID' ) { diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index 51ae991940..a50ec45dbe 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -3,29 +3,47 @@ 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 PhpParser\Token; +use PHPStan\Analyser\Ignore\IgnoreLexer; +use PHPStan\Analyser\Ignore\IgnoreParseException; +use PHPStan\DependencyInjection\Container; use PHPStan\File\FileReader; -use PHPStan\NodeVisitor\StatementOrderVisitor; use PHPStan\ShouldNotHappenException; -use function is_string; +use function array_filter; +use function array_map; +use function count; +use function implode; +use function in_array; +use function preg_match_all; +use function sprintf; +use function str_contains; +use function strlen; use function strpos; +use function substr; use function substr_count; +use const ARRAY_FILTER_USE_KEY; +use const PREG_OFFSET_CAPTURE; use const T_COMMENT; use const T_DOC_COMMENT; +use const T_WHITESPACE; -class RichParser implements Parser +final class RichParser implements Parser { + public const VISITOR_SERVICE_TAG = 'phpstan.parser.richParserNodeVisitor'; + + private const PHPDOC_TAG_REGEX = '(@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+)'; + + private const PHPDOC_DOCTRINE_TAG_REGEX = '(@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*)'; + public function __construct( private \PhpParser\Parser $parser, - private Lexer $lexer, private NameResolver $nameResolver, - private NodeConnectingVisitor $nodeConnectingVisitor, - private StatementOrderVisitor $statementOrderVisitor, + private Container $container, + private IgnoreLexer $ignoreLexer, ) { } @@ -50,7 +68,8 @@ public function parseString(string $sourceCode): array { $errorHandler = new Collecting(); $nodes = $this->parser->parse($sourceCode, $errorHandler); - $tokens = $this->lexer->getTokens(); + + $tokens = $this->parser->getTokens(); if ($errorHandler->hasErrors()) { throw new ParserErrorsException($errorHandler->getErrors(), null); } @@ -60,49 +79,268 @@ public function parseString(string $sourceCode): array $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor($this->nameResolver); - $nodeTraverser->addVisitor($this->nodeConnectingVisitor); - $nodeTraverser->addVisitor($this->statementOrderVisitor); + + $traitCollectingVisitor = new TraitCollectingVisitor(); + $nodeTraverser->addVisitor($traitCollectingVisitor); + + foreach ($this->container->getServicesByTag(self::VISITOR_SERVICE_TAG) as $visitor) { + $nodeTraverser->addVisitor($visitor); + } /** @var array */ $nodes = $nodeTraverser->traverse($nodes); + ['lines' => $linesToIgnore, 'errors' => $ignoreParseErrors] = $this->getLinesToIgnore($tokens); if (isset($nodes[0])) { - $nodes[0]->setAttribute('linesToIgnore', $this->getLinesToIgnore($tokens)); + $nodes[0]->setAttribute('linesToIgnore', $linesToIgnore); + if (count($ignoreParseErrors) > 0) { + $nodes[0]->setAttribute('linesToIgnoreParseErrors', $ignoreParseErrors); + } + } + + foreach ($traitCollectingVisitor->traits as $trait) { + $preexisting = $trait->getAttribute('linesToIgnore', []); + $filteredLinesToIgnore = array_filter($linesToIgnore, static fn (int $line): bool => $line >= $trait->getStartLine() && $line <= $trait->getEndLine(), ARRAY_FILTER_USE_KEY); + foreach ($preexisting as $line => $ignores) { + $filteredLinesToIgnore[$line] = $ignores; + } + $trait->setAttribute('linesToIgnore', $filteredLinesToIgnore); } return $nodes; } /** - * @param mixed[] $tokens - * @return int[] + * @param Token[] $tokens + * @return array{lines: array|null>, errors: array>} */ private function getLinesToIgnore(array $tokens): array { $lines = []; + $previousToken = null; + $pendingToken = null; + $errors = []; foreach ($tokens as $token) { - if (is_string($token)) { + $type = $token->id; + $line = $token->line; + if ($type !== T_COMMENT && $type !== T_DOC_COMMENT) { + if ($type !== T_WHITESPACE) { + if ($pendingToken !== null) { + [$pendingText, $pendingIgnorePos, $tokenLine, $pendingLine] = $pendingToken; + + try { + $identifiers = $this->parseIdentifiers($pendingText, $pendingIgnorePos); + } catch (IgnoreParseException $e) { + $errors[] = [$tokenLine + $e->getPhpDocLine(), $e->getMessage()]; + $pendingToken = null; + continue; + } + + if ($line !== $pendingLine + 1) { + $lineToAdd = $pendingLine; + } else { + $lineToAdd = $line; + } + + foreach ($identifiers as $identifier) { + $lines[$lineToAdd][] = $identifier; + } + + $pendingToken = null; + } + $previousToken = $token; + } continue; } - $type = $token[0]; - if ($type !== T_COMMENT && $type !== T_DOC_COMMENT) { + $text = $token->text; + $isNextLine = str_contains($text, '@phpstan-ignore-next-line'); + $isCurrentLine = str_contains($text, '@phpstan-ignore-line'); + + if ($type === T_DOC_COMMENT) { + $lines += $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-line', false); + if ($isNextLine) { + $pattern = sprintf('~%s~si', implode('|', [self::PHPDOC_TAG_REGEX, self::PHPDOC_DOCTRINE_TAG_REGEX])); + $r = preg_match_all($pattern, $text, $pregMatches, PREG_OFFSET_CAPTURE); + if ($r !== false) { + $c = count($pregMatches[0]); + if ($c > 0) { + [$lastMatchTag, $lastMatchOffset] = $pregMatches[0][$c - 1]; + if ($lastMatchTag === '@phpstan-ignore-next-line') { + // this will let us ignore errors outside of PHPDoc + // and also cut off the PHPDoc text before the last tag + $lineToIgnore = $line + 1 + substr_count($text, "\n"); + $lines[$lineToIgnore] = null; + $text = substr($text, 0, $lastMatchOffset); + } + } + } + + $lines += $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-next-line', true); + } + + if ($isNextLine || $isCurrentLine) { + continue; + } + + } else { + if ($isNextLine) { + $line++; + } + if ($isNextLine || $isCurrentLine) { + $line += substr_count($token->text, "\n"); + + $lines[$line] = null; + continue; + } + } + + $ignorePos = strpos($text, '@phpstan-ignore'); + if ($ignorePos === false) { continue; } - $text = $token[1]; - $line = $token[2]; - if (strpos($text, '@phpstan-ignore-next-line') !== false) { - $line++; - } elseif (strpos($text, '@phpstan-ignore-line') === false) { + $ignoreLine = substr_count(substr($text, 0, $ignorePos), "\n") - 1; + + if ($previousToken !== null && $previousToken->line === $line) { + try { + foreach ($this->parseIdentifiers($text, $ignorePos) as $identifier) { + $lines[$line][] = $identifier; + } + } catch (IgnoreParseException $e) { + $errors[] = [$token->line + $e->getPhpDocLine() + $ignoreLine, $e->getMessage()]; + } + continue; } - $line += substr_count($token[1], "\n"); + $line += substr_count($token->text, "\n"); + $pendingToken = [$text, $ignorePos, $token->line + $ignoreLine, $line]; + } + + if ($pendingToken !== null) { + [$pendingText, $pendingIgnorePos, $tokenLine, $pendingLine] = $pendingToken; - $lines[] = $line; + try { + foreach ($this->parseIdentifiers($pendingText, $pendingIgnorePos) as $identifier) { + $lines[$pendingLine][] = $identifier; + } + } catch (IgnoreParseException $e) { + $errors[] = [$tokenLine + $e->getPhpDocLine(), $e->getMessage()]; + } + } + + $processedErrors = []; + foreach ($errors as [$line, $message]) { + $processedErrors[$line][] = $message; + } + + return [ + 'lines' => $lines, + 'errors' => $processedErrors, + ]; + } + + /** + * @return array + */ + private function getLinesToIgnoreForTokenByIgnoreComment( + string $tokenText, + int $tokenLine, + string $ignoreComment, + bool $ignoreNextLine, + ): array + { + $lines = []; + $positionsOfIgnoreComment = []; + $offset = 0; + + while (($pos = strpos($tokenText, $ignoreComment, $offset)) !== false) { + $positionsOfIgnoreComment[] = $pos; + $offset = $pos + 1; + } + + foreach ($positionsOfIgnoreComment as $pos) { + $line = $tokenLine + substr_count(substr($tokenText, 0, $pos), "\n") + ($ignoreNextLine ? 1 : 0); + $lines[$line] = null; } return $lines; } + /** + * @return non-empty-list + * @throws IgnoreParseException + */ + private function parseIdentifiers(string $text, int $ignorePos): array + { + $text = substr($text, $ignorePos + strlen('@phpstan-ignore')); + $originalTokens = $this->ignoreLexer->tokenize($text); + $tokens = []; + + foreach ($originalTokens as $originalToken) { + if ($originalToken[IgnoreLexer::TYPE_OFFSET] === IgnoreLexer::TOKEN_WHITESPACE) { + continue; + } + $tokens[] = $originalToken; + } + + $c = count($tokens); + + $identifiers = []; + $openParenthesisCount = 0; + $expected = [IgnoreLexer::TOKEN_IDENTIFIER]; + + for ($i = 0; $i < $c; $i++) { + $lastTokenTypeLabel = isset($tokenType) ? $this->ignoreLexer->getLabel($tokenType) : '@phpstan-ignore'; + [IgnoreLexer::VALUE_OFFSET => $content, IgnoreLexer::TYPE_OFFSET => $tokenType, IgnoreLexer::LINE_OFFSET => $tokenLine] = $tokens[$i]; + + if ($expected !== null && !in_array($tokenType, $expected, true)) { + $tokenTypeLabel = $this->ignoreLexer->getLabel($tokenType); + $otherTokenContent = $tokenType === IgnoreLexer::TOKEN_OTHER ? sprintf(" '%s'", $content) : ''; + $expectedLabels = implode(' or ', array_map(fn ($token) => $this->ignoreLexer->getLabel($token), $expected)); + + throw new IgnoreParseException(sprintf('Unexpected %s%s after %s, expected %s', $tokenTypeLabel, $otherTokenContent, $lastTokenTypeLabel, $expectedLabels), $tokenLine); + } + + if ($tokenType === IgnoreLexer::TOKEN_OPEN_PARENTHESIS) { + $openParenthesisCount++; + $expected = null; + continue; + } + + if ($tokenType === IgnoreLexer::TOKEN_CLOSE_PARENTHESIS) { + $openParenthesisCount--; + if ($openParenthesisCount === 0) { + $expected = [IgnoreLexer::TOKEN_COMMA, IgnoreLexer::TOKEN_END]; + } + continue; + } + + if ($openParenthesisCount > 0) { + continue; // waiting for comment end + } + + if ($tokenType === IgnoreLexer::TOKEN_IDENTIFIER) { + $identifiers[] = $content; + $expected = [IgnoreLexer::TOKEN_COMMA, IgnoreLexer::TOKEN_END, IgnoreLexer::TOKEN_OPEN_PARENTHESIS]; + continue; + } + + if ($tokenType === IgnoreLexer::TOKEN_COMMA) { + $expected = [IgnoreLexer::TOKEN_IDENTIFIER]; + continue; + } + } + + if ($openParenthesisCount > 0) { + throw new IgnoreParseException('Unexpected end, unclosed opening parenthesis', $tokenLine ?? 1); + } + + if (count($identifiers) === 0) { + throw new IgnoreParseException('Missing identifier', 1); + } + + return $identifiers; + } + } diff --git a/src/Parser/SimpleParser.php b/src/Parser/SimpleParser.php index efcf47d786..8fbd112742 100644 --- a/src/Parser/SimpleParser.php +++ b/src/Parser/SimpleParser.php @@ -9,12 +9,14 @@ use PHPStan\File\FileReader; use PHPStan\ShouldNotHappenException; -class SimpleParser implements Parser +final class SimpleParser implements Parser { public function __construct( private \PhpParser\Parser $parser, private NameResolver $nameResolver, + private VariadicMethodsVisitor $variadicMethodsVisitor, + private VariadicFunctionsVisitor $variadicFunctionsVisitor, ) { } @@ -48,6 +50,8 @@ public function parseString(string $sourceCode): array $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor($this->nameResolver); + $nodeTraverser->addVisitor($this->variadicMethodsVisitor); + $nodeTraverser->addVisitor($this->variadicFunctionsVisitor); /** @var array */ return $nodeTraverser->traverse($nodes); diff --git a/src/Parser/StandaloneThrowExprVisitor.php b/src/Parser/StandaloneThrowExprVisitor.php new file mode 100644 index 0000000000..386c903281 --- /dev/null +++ b/src/Parser/StandaloneThrowExprVisitor.php @@ -0,0 +1,28 @@ +expr instanceof Node\Expr\Throw_) { + return null; + } + + $node->expr->setAttribute(self::ATTRIBUTE_NAME, true); + + return $node; + } + +} diff --git a/src/Parser/StubParser.php b/src/Parser/StubParser.php new file mode 100644 index 0000000000..d98a2cc721 --- /dev/null +++ b/src/Parser/StubParser.php @@ -0,0 +1,56 @@ +parseString(FileReader::read($file)); + } catch (ParserErrorsException $e) { + throw new ParserErrorsException($e->getErrors(), $file); + } + } + + /** + * @return Node\Stmt[] + */ + public function parseString(string $sourceCode): array + { + $errorHandler = new Collecting(); + $nodes = $this->parser->parse($sourceCode, $errorHandler); + if ($errorHandler->hasErrors()) { + throw new ParserErrorsException($errorHandler->getErrors(), null); + } + if ($nodes === null) { + throw new ShouldNotHappenException(); + } + + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor($this->nameResolver); + + /** @var array */ + return $nodeTraverser->traverse($nodes); + } + +} diff --git a/src/Parser/TraitCollectingVisitor.php b/src/Parser/TraitCollectingVisitor.php new file mode 100644 index 0000000000..e6341a9de6 --- /dev/null +++ b/src/Parser/TraitCollectingVisitor.php @@ -0,0 +1,25 @@ + */ + public array $traits = []; + + public function enterNode(Node $node): ?Node + { + if (!$node instanceof Node\Stmt\Trait_) { + return null; + } + + $this->traits[] = $node; + + return null; + } + +} diff --git a/src/Parser/TryCatchTypeVisitor.php b/src/Parser/TryCatchTypeVisitor.php new file mode 100644 index 0000000000..cca8bf4e3a --- /dev/null +++ b/src/Parser/TryCatchTypeVisitor.php @@ -0,0 +1,74 @@ +|null> */ + private array $typeStack = []; + + public function beforeTraverse(array $nodes): ?array + { + $this->typeStack = []; + return null; + } + + public function enterNode(Node $node): ?Node + { + if ($node instanceof Node\Stmt || $node instanceof Node\Expr\Match_) { + if (count($this->typeStack) > 0) { + $node->setAttribute(self::ATTRIBUTE_NAME, $this->typeStack[count($this->typeStack) - 1]); + } + } + + if ($node instanceof Node\FunctionLike) { + $this->typeStack[] = null; + } + + if ($node instanceof Node\Stmt\TryCatch) { + $types = []; + foreach (array_reverse($this->typeStack) as $stackTypes) { + if ($stackTypes === null) { + break; + } + + foreach ($stackTypes as $type) { + $types[] = $type; + } + } + foreach ($node->catches as $catch) { + foreach ($catch->types as $type) { + $types[] = $type->toString(); + } + } + + $this->typeStack[] = $types; + } + + return null; + } + + public function leaveNode(Node $node): ?Node + { + if ( + !$node instanceof Node\Stmt\TryCatch + && !$node instanceof Node\FunctionLike + ) { + return null; + } + + array_pop($this->typeStack); + + return null; + } + +} diff --git a/src/Parser/TypeTraverserInstanceofVisitor.php b/src/Parser/TypeTraverserInstanceofVisitor.php new file mode 100644 index 0000000000..e353d31af3 --- /dev/null +++ b/src/Parser/TypeTraverserInstanceofVisitor.php @@ -0,0 +1,56 @@ +depth = 0; + return null; + } + + public function enterNode(Node $node): ?Node + { + if ($node instanceof Node\Expr\Instanceof_ && $this->depth > 0) { + $node->setAttribute(self::ATTRIBUTE_NAME, true); + return null; + } + + if ( + $node instanceof Node\Expr\StaticCall + && $node->class instanceof Node\Name + && $node->class->toLowerString() === 'phpstan\\type\\typetraverser' + && $node->name instanceof Node\Identifier + && $node->name->toLowerString() === 'map' + ) { + $this->depth++; + } + + return null; + } + + public function leaveNode(Node $node): ?Node + { + if ( + $node instanceof Node\Expr\StaticCall + && $node->class instanceof Node\Name + && $node->class->toLowerString() === 'phpstan\\type\\typetraverser' + && $node->name instanceof Node\Identifier + && $node->name->toLowerString() === 'map' + ) { + $this->depth--; + } + + return null; + } + +} diff --git a/src/Parser/VariadicFunctionsVisitor.php b/src/Parser/VariadicFunctionsVisitor.php new file mode 100644 index 0000000000..5276d0eb47 --- /dev/null +++ b/src/Parser/VariadicFunctionsVisitor.php @@ -0,0 +1,94 @@ + */ + public static array $cache = []; + + /** @var array */ + private array $variadicFunctions = []; + + public const ATTRIBUTE_NAME = 'variadicFunctions'; + + public function beforeTraverse(array $nodes): ?array + { + $this->topNode = null; + $this->variadicFunctions = []; + $this->inNamespace = null; + $this->inFunction = null; + + return null; + } + + public function enterNode(Node $node): ?Node + { + if ($this->topNode === null) { + $this->topNode = $node; + } + + if ($node instanceof Node\Stmt\Namespace_ && $node->name !== null) { + $this->inNamespace = $node->name->toString(); + } + + if ($node instanceof Node\Stmt\Function_) { + $this->inFunction = $this->inNamespace !== null ? $this->inNamespace . '\\' . $node->name->name : $node->name->name; + } + + if ( + $this->inFunction !== null + && $node instanceof Node\Expr\FuncCall + && $node->name instanceof Name + && in_array((string) $node->name, ParametersAcceptor::VARIADIC_FUNCTIONS, true) + && !array_key_exists($this->inFunction, $this->variadicFunctions) + ) { + $this->variadicFunctions[$this->inFunction] = true; + } + + return null; + } + + public function leaveNode(Node $node): ?Node + { + if ($node instanceof Node\Stmt\Namespace_ && $node->name !== null) { + $this->inNamespace = null; + } + + if ($node instanceof Node\Stmt\Function_ && $this->inFunction !== null) { + $this->variadicFunctions[$this->inFunction] ??= false; + $this->inFunction = null; + } + + return null; + } + + public function afterTraverse(array $nodes): ?array + { + if ($this->topNode !== null && $this->variadicFunctions !== []) { + foreach ($this->variadicFunctions as $name => $variadic) { + self::$cache[$name] = $variadic; + } + $functions = array_filter($this->variadicFunctions, static fn (bool $variadic) => $variadic); + $this->topNode->setAttribute(self::ATTRIBUTE_NAME, $functions); + } + + return null; + } + +} diff --git a/src/Parser/VariadicMethodsVisitor.php b/src/Parser/VariadicMethodsVisitor.php new file mode 100644 index 0000000000..cc3821d9f2 --- /dev/null +++ b/src/Parser/VariadicMethodsVisitor.php @@ -0,0 +1,135 @@ + */ + private array $classStack = []; + + private ?string $inMethod = null; + + /** @var array> */ + public static array $cache = []; + + /** @var array> */ + private array $variadicMethods = []; + + public function beforeTraverse(array $nodes): ?array + { + $this->topNode = null; + $this->variadicMethods = []; + $this->inNamespace = null; + $this->classStack = []; + $this->inMethod = null; + + return null; + } + + public function enterNode(Node $node): ?Node + { + if ($this->topNode === null) { + $this->topNode = $node; + } + + if ($node instanceof Node\Stmt\Namespace_ && $node->name !== null) { + $this->inNamespace = $node->name->toString(); + } + + if ($node instanceof Node\Stmt\ClassLike) { + if (!$node->name instanceof Node\Identifier) { + $className = sprintf('%s:%s:%s', self::ANONYMOUS_CLASS_PREFIX, $node->getStartLine(), $node->getEndLine()); + $this->classStack[] = $className; + } else { + $className = $node->name->name; + $this->classStack[] = $this->inNamespace !== null ? $this->inNamespace . '\\' . $className : $className; + } + } + + if ($node instanceof ClassMethod) { + $this->inMethod = $node->name->name; + } + + if ( + $this->inMethod !== null + && $node instanceof Node\Expr\FuncCall + && $node->name instanceof Name + && in_array((string) $node->name, ParametersAcceptor::VARIADIC_FUNCTIONS, true) + ) { + $lastClass = $this->classStack[count($this->classStack) - 1] ?? null; + if ($lastClass !== null) { + if ( + !array_key_exists($lastClass, $this->variadicMethods) + || !array_key_exists($this->inMethod, $this->variadicMethods[$lastClass]) + ) { + $this->variadicMethods[$lastClass][$this->inMethod] = true; + } + } + + } + + return null; + } + + public function leaveNode(Node $node): ?Node + { + if ($node instanceof ClassMethod) { + $lastClass = $this->classStack[count($this->classStack) - 1] ?? null; + if ($lastClass !== null) { + $this->variadicMethods[$lastClass][$this->inMethod] ??= false; + } + $this->inMethod = null; + } + + if ($node instanceof Node\Stmt\ClassLike) { + array_pop($this->classStack); + } + + if ($node instanceof Node\Stmt\Namespace_ && $node->name !== null) { + $this->inNamespace = null; + } + + return null; + } + + public function afterTraverse(array $nodes): ?array + { + if ($this->topNode !== null && $this->variadicMethods !== []) { + $filteredMethods = []; + foreach ($this->variadicMethods as $class => $methods) { + foreach ($methods as $name => $variadic) { + self::$cache[$class][$name] = $variadic; + if (!$variadic) { + continue; + } + + $filteredMethods[$class][$name] = true; + } + } + $this->topNode->setAttribute(self::ATTRIBUTE_NAME, $filteredMethods); + } + + return null; + } + +} diff --git a/src/Php/ComposerPhpVersionFactory.php b/src/Php/ComposerPhpVersionFactory.php new file mode 100644 index 0000000000..88b81022ee --- /dev/null +++ b/src/Php/ComposerPhpVersionFactory.php @@ -0,0 +1,121 @@ +initialized = true; + + // don't limit minVersion... PHPStan can analyze even PHP5 + $this->maxVersion = new PhpVersion(PhpVersionFactory::MAX_PHP_VERSION); + + // fallback to composer.json based php-version constraint + $composerPhpVersion = $this->getComposerRequireVersion(); + if ($composerPhpVersion === null) { + return; + } + + $parser = new VersionParser(); + $constraint = $parser->parseConstraints($composerPhpVersion); + + if (!$constraint->getLowerBound()->isZero()) { + $minVersion = $this->buildVersion($constraint->getLowerBound()->getVersion(), false); + + if ($minVersion !== null) { + $this->minVersion = new PhpVersion($minVersion->getVersionId()); + } + } + if ($constraint->getUpperBound()->isPositiveInfinity()) { + return; + } + + $this->maxVersion = $this->buildVersion($constraint->getUpperBound()->getVersion(), true); + } + + public function getMinVersion(): ?PhpVersion + { + if ($this->initialized === false) { + $this->initializeVersions(); + } + + return $this->minVersion; + } + + public function getMaxVersion(): ?PhpVersion + { + if ($this->initialized === false) { + $this->initializeVersions(); + } + + return $this->maxVersion; + } + + private function getComposerRequireVersion(): ?string + { + $composerPhpVersion = null; + + if (count($this->composerAutoloaderProjectPaths) > 0) { + $composer = ComposerHelper::getComposerConfig(end($this->composerAutoloaderProjectPaths)); + if ($composer !== null) { + $requiredVersion = $composer['require']['php'] ?? null; + + if (is_string($requiredVersion)) { + $composerPhpVersion = $requiredVersion; + } + } + } + + return $composerPhpVersion; + } + + private function buildVersion(string $version, bool $isMaxVersion): ?PhpVersion + { + $matches = Strings::match($version, '#^(\d+)\.(\d+)(?:\.(\d+))?#'); + if ($matches === null) { + return null; + } + + $major = $matches[1]; + $minor = $matches[2]; + $patch = $matches[3] ?? 0; + $versionId = (int) sprintf('%d%02d%02d', $major, $minor, $patch); + + if ($isMaxVersion && $version === '6.0.0.0-dev') { + $versionId = min($versionId, PhpVersionFactory::MAX_PHP5_VERSION); + } elseif ($isMaxVersion && $version === '8.0.0.0-dev') { + $versionId = min($versionId, PhpVersionFactory::MAX_PHP7_VERSION); + } else { + $versionId = min($versionId, PhpVersionFactory::MAX_PHP_VERSION); + } + + return new PhpVersion($versionId); + } + +} diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index e565cf227a..5215a30606 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -4,24 +4,71 @@ use function floor; -/** @api */ -class PhpVersion +/** + * @api + */ +final class PhpVersion { - public function __construct(private int $versionId) + public const SOURCE_RUNTIME = 1; + public const SOURCE_CONFIG = 2; + public const SOURCE_COMPOSER_PLATFORM_PHP = 3; + public const SOURCE_UNKNOWN = 4; + + /** + * @param self::SOURCE_* $source + */ + public function __construct(private int $versionId, private int $source = self::SOURCE_UNKNOWN) { } + /** + * @return self::SOURCE_* + */ + public function getSource(): int + { + return $this->source; + } + + public function getSourceLabel(): string + { + switch ($this->source) { + case self::SOURCE_RUNTIME: + return 'runtime'; + case self::SOURCE_CONFIG: + return 'config'; + case self::SOURCE_COMPOSER_PLATFORM_PHP: + return 'config.platform.php in composer.json'; + } + + return 'unknown'; + } + public function getVersionId(): int { return $this->versionId; } + public function getMajorVersionId(): int + { + return (int) floor($this->versionId / 10000); + } + + public function getMinorVersionId(): int + { + return (int) floor(($this->versionId % 10000) / 100); + } + + public function getPatchVersionId(): int + { + return (int) floor($this->versionId % 100); + } + public function getVersionString(): string { - $first = (int) floor($this->versionId / 10000); - $second = (int) floor(($this->versionId % 10000) / 100); - $third = (int) floor($this->versionId % 100); + $first = $this->getMajorVersionId(); + $second = $this->getMinorVersionId(); + $third = $this->getPatchVersionId(); return $first . '.' . $second . ($third !== 0 ? '.' . $third : ''); } @@ -41,6 +88,11 @@ public function supportsReturnCovariance(): bool return $this->versionId >= 70400; } + public function supportsNoncapturingCatches(): bool + { + return $this->versionId >= 80000; + } + public function supportsNativeUnionTypes(): bool { return $this->versionId >= 80000; @@ -51,6 +103,16 @@ public function deprecatesRequiredParameterAfterOptional(): bool return $this->versionId >= 80000; } + public function deprecatesRequiredParameterAfterOptionalNullableAndDefaultNull(): bool + { + return $this->versionId >= 80100; + } + + public function deprecatesRequiredParameterAfterOptionalUnionOrMixed(): bool + { + return $this->versionId >= 80300; + } + public function supportsLessOverridenParametersWithVariadic(): bool { return $this->versionId >= 80000; @@ -181,4 +243,166 @@ public function throwsOnInvalidMbStringEncoding(): bool return $this->versionId >= 80000; } + public function supportsPassNoneEncodings(): bool + { + return $this->versionId < 70300; + } + + public function producesWarningForFinalPrivateMethods(): bool + { + return $this->versionId >= 80000; + } + + public function deprecatesDynamicProperties(): bool + { + return $this->versionId >= 80200; + } + + public function strSplitReturnsEmptyArray(): bool + { + return $this->versionId >= 80200; + } + + public function supportsDisjunctiveNormalForm(): bool + { + return $this->versionId >= 80200; + } + + public function serializableRequiresMagicMethods(): bool + { + return $this->versionId >= 80100; + } + + public function arrayFunctionsReturnNullWithNonArray(): bool + { + return $this->versionId < 80000; + } + + // see https://www.php.net/manual/en/migration80.incompatible.php#migration80.incompatible.core.string-number-comparision + public function castsNumbersToStringsOnLooseComparison(): bool + { + return $this->versionId >= 80000; + } + + public function nonNumericStringAndIntegerIsFalseOnLooseComparison(): bool + { + return $this->versionId >= 80000; + } + + public function supportsCallableInstanceMethods(): bool + { + return $this->versionId < 80000; + } + + public function supportsJsonValidate(): bool + { + return $this->versionId >= 80300; + } + + public function supportsConstantsInTraits(): bool + { + return $this->versionId >= 80200; + } + + public function supportsNativeTypesInClassConstants(): bool + { + return $this->versionId >= 80300; + } + + public function supportsAbstractTraitMethods(): bool + { + return $this->versionId >= 80000; + } + + public function supportsOverrideAttribute(): bool + { + return $this->versionId >= 80300; + } + + public function supportsDynamicClassConstantFetch(): bool + { + return $this->versionId >= 80300; + } + + public function supportsReadOnlyClasses(): bool + { + return $this->versionId >= 80200; + } + + public function supportsReadOnlyAnonymousClasses(): bool + { + return $this->versionId >= 80300; + } + + public function supportsNeverReturnTypeInArrowFunction(): bool + { + return $this->versionId >= 80200; + } + + public function supportsPregUnmatchedAsNull(): bool + { + // while PREG_UNMATCHED_AS_NULL is defined in php-src since 7.2.x it starts working as expected with 7.4.x + // https://3v4l.org/v3HE4 + return $this->versionId >= 70400; + } + + public function supportsPregCaptureOnlyNamedGroups(): bool + { + // https://php.watch/versions/8.2/preg-n-no-capture-modifier + return $this->versionId >= 80200; + } + + public function supportsPropertyHooks(): bool + { + return $this->versionId >= 80400; + } + + public function supportsFinalProperties(): bool + { + return $this->versionId >= 80400; + } + + public function supportsAsymmetricVisibility(): bool + { + return $this->versionId >= 80400; + } + + public function supportsLazyObjects(): bool + { + return $this->versionId >= 80400; + } + + public function hasDateTimeExceptions(): bool + { + return $this->versionId >= 80300; + } + + public function isCurloptUrlCheckingFileSchemeWithOpenBasedir(): bool + { + // Before PHP 8.0, when setting CURLOPT_URL, an unparsable URL or a file:// scheme would fail if open_basedir is used + // https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158 + // https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L128-L130 + return $this->versionId < 80000; + } + + public function highlightStringDoesNotReturnFalse(): bool + { + return $this->versionId >= 80400; + } + + public function deprecatesImplicitlyNullableParameterTypes(): bool + { + return $this->versionId >= 80400; + } + + public function substrReturnFalseInsteadOfEmptyString(): bool + { + return $this->versionId < 80000; + } + + public function supportsBcMathNumberOperatorOverloading(): bool + { + return $this->versionId >= 80400; + } + } diff --git a/src/Php/PhpVersionFactory.php b/src/Php/PhpVersionFactory.php index fcf77a16ec..bd1bfcabf5 100644 --- a/src/Php/PhpVersionFactory.php +++ b/src/Php/PhpVersionFactory.php @@ -7,9 +7,14 @@ use function min; use const PHP_VERSION_ID; -class PhpVersionFactory +final class PhpVersionFactory { + public const MIN_PHP_VERSION = 70100; + public const MAX_PHP_VERSION = 80499; + public const MAX_PHP5_VERSION = 50699; + public const MAX_PHP7_VERSION = 70499; + public function __construct( private ?int $versionId, private ?string $composerPhpVersion, @@ -20,18 +25,20 @@ public function __construct( public function create(): PhpVersion { $versionId = $this->versionId; - if ($versionId === null && $this->composerPhpVersion !== null) { + if ($versionId !== null) { + $source = PhpVersion::SOURCE_CONFIG; + } elseif ($this->composerPhpVersion !== null) { $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, 80199); - } - - if ($versionId === null) { + $tmp = max($tmp, self::MIN_PHP_VERSION); + $versionId = min($tmp, self::MAX_PHP_VERSION); + $source = PhpVersion::SOURCE_COMPOSER_PLATFORM_PHP; + } else { $versionId = PHP_VERSION_ID; + $source = PhpVersion::SOURCE_RUNTIME; } - return new PhpVersion($versionId); + return new PhpVersion($versionId, $source); } } diff --git a/src/Php/PhpVersionFactoryFactory.php b/src/Php/PhpVersionFactoryFactory.php index 870d1ff276..0190ca0e82 100644 --- a/src/Php/PhpVersionFactoryFactory.php +++ b/src/Php/PhpVersionFactoryFactory.php @@ -8,17 +8,20 @@ use PHPStan\File\FileReader; use function count; use function end; +use function is_array; use function is_file; +use function is_int; use function is_string; -class PhpVersionFactoryFactory +final class PhpVersionFactoryFactory { /** + * @param int|array{min: int, max: int}|null $phpVersion * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - private ?int $versionId, + private int|array|null $phpVersion, private array $composerAutoloaderProjectPaths, ) { @@ -43,7 +46,17 @@ public function create(): PhpVersionFactory } } - return new PhpVersionFactory($this->versionId, $composerPhpVersion); + $versionId = null; + + if (is_int($this->phpVersion)) { + $versionId = $this->phpVersion; + } + + if (is_array($this->phpVersion)) { + $versionId = $this->phpVersion['min']; + } + + return new PhpVersionFactory($versionId, $composerPhpVersion); } } diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php new file mode 100644 index 0000000000..96bf233209 --- /dev/null +++ b/src/Php/PhpVersions.php @@ -0,0 +1,46 @@ +phpVersions; + } + + public function supportsNoncapturingCatches(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function producesWarningForFinalPrivateMethods(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function supportsNamedArguments(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function supportsNamedArgumentAfterUnpackedArgument(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80100, null)->isSuperTypeOf($this->phpVersions)->result; + } + +} diff --git a/src/PhpDoc/ConstExprNodeResolver.php b/src/PhpDoc/ConstExprNodeResolver.php index 8a22e2da77..257883af2c 100644 --- a/src/PhpDoc/ConstExprNodeResolver.php +++ b/src/PhpDoc/ConstExprNodeResolver.php @@ -2,6 +2,7 @@ namespace PHPStan\PhpDoc; +use PHPStan\Analyser\NameScope; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -10,60 +11,129 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; -use PHPStan\Type\ConstantTypeHelper; -use PHPStan\Type\MixedType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; +use PHPStan\Type\ErrorType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; +use function strtolower; -class ConstExprNodeResolver +final class ConstExprNodeResolver { - public function resolve(ConstExprNode $node): Type + public function __construct( + private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private InitializerExprTypeResolver $initializerExprTypeResolver, + ) + { + } + + public function resolve(ConstExprNode $node, NameScope $nameScope): Type { if ($node instanceof ConstExprArrayNode) { - return $this->resolveArrayNode($node); + return $this->resolveArrayNode($node, $nameScope); } if ($node instanceof ConstExprFalseNode) { - return ConstantTypeHelper::getTypeFromValue(false); + return new ConstantBooleanType(false); } if ($node instanceof ConstExprTrueNode) { - return ConstantTypeHelper::getTypeFromValue(true); + return new ConstantBooleanType(true); } if ($node instanceof ConstExprFloatNode) { - return ConstantTypeHelper::getTypeFromValue((float) $node->value); + return new ConstantFloatType((float) $node->value); } if ($node instanceof ConstExprIntegerNode) { - return ConstantTypeHelper::getTypeFromValue((int) $node->value); + return new ConstantIntegerType((int) $node->value); } if ($node instanceof ConstExprNullNode) { - return ConstantTypeHelper::getTypeFromValue(null); + return new NullType(); } if ($node instanceof ConstExprStringNode) { - return ConstantTypeHelper::getTypeFromValue($node->value); + return new ConstantStringType($node->value); } - return new MixedType(); + if ($node instanceof ConstFetchNode) { + if ($nameScope->getClassName() !== null) { + switch (strtolower($node->className)) { + case 'static': + case 'self': + $className = $nameScope->getClassName(); + break; + + case 'parent': + if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) { + $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); + if ($classReflection->getParentClass() === null) { + return new ErrorType(); + + } + + $className = $classReflection->getParentClass()->getName(); + } + break; + } + } + if (!isset($className)) { + $className = $nameScope->resolveStringName($node->className); + } + if (!$this->getReflectionProvider()->hasClass($className)) { + return new ErrorType(); + } + $classReflection = $this->getReflectionProvider()->getClass($className); + if (!$classReflection->hasConstant($node->name)) { + return new ErrorType(); + } + if ($classReflection->isEnum() && $classReflection->hasEnumCase($node->name)) { + return new EnumCaseObjectType($classReflection->getName(), $node->name); + } + + $reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($node->name); + if ($reflectionConstant === false) { + return new ErrorType(); + } + $declaringClass = $reflectionConstant->getDeclaringClass(); + + return $this->initializerExprTypeResolver->getType( + $reflectionConstant->getValueExpression(), + InitializerExprContext::fromClass($declaringClass->getName(), $declaringClass->getFileName() ?: null), + ); + } + + return new ErrorType(); } - private function resolveArrayNode(ConstExprArrayNode $node): Type + private function resolveArrayNode(ConstExprArrayNode $node, NameScope $nameScope): Type { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($node->items as $item) { if ($item->key === null) { $key = null; } else { - $key = $this->resolve($item->key); + $key = $this->resolve($item->key, $nameScope); } - $arrayBuilder->setOffsetValueType($key, $this->resolve($item->value)); + $arrayBuilder->setOffsetValueType($key, $this->resolve($item->value, $nameScope)); } return $arrayBuilder->getArray(); } + private function getReflectionProvider(): ReflectionProvider + { + return $this->reflectionProviderProvider->getReflectionProvider(); + } + } diff --git a/src/PhpDoc/DefaultStubFilesProvider.php b/src/PhpDoc/DefaultStubFilesProvider.php new file mode 100644 index 0000000000..98bc0e45fe --- /dev/null +++ b/src/PhpDoc/DefaultStubFilesProvider.php @@ -0,0 +1,74 @@ +cachedFiles !== null) { + return $this->cachedFiles; + } + + $files = $this->stubFiles; + $extensions = $this->container->getServicesByTag(StubFilesExtension::EXTENSION_TAG); + foreach ($extensions as $extension) { + foreach ($extension->getFiles() as $extensionFile) { + $files[] = $extensionFile; + } + } + + return $this->cachedFiles = $files; + } + + public function getProjectStubFiles(): array + { + if ($this->cachedProjectFiles !== null) { + return $this->cachedProjectFiles; + } + + $filteredStubFiles = $this->getStubFiles(); + foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { + $composerConfig = ComposerHelper::getComposerConfig($composerAutoloaderProjectPath); + if ($composerConfig === null) { + continue; + } + + $vendorDir = ComposerHelper::getVendorDirFromComposerConfig($composerAutoloaderProjectPath, $composerConfig); + $vendorDir = strtr($vendorDir, '\\', '/'); + $filteredStubFiles = array_filter( + $filteredStubFiles, + static fn (string $file): bool => !str_contains(strtr($file, '\\', '/'), $vendorDir) + ); + } + + return $this->cachedProjectFiles = array_values($filteredStubFiles); + } + +} diff --git a/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php b/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php index cd912d76bb..c1c038ca81 100644 --- a/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php +++ b/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php @@ -2,7 +2,7 @@ namespace PHPStan\PhpDoc; -class DirectTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider +final class DirectTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider { public function __construct(private TypeNodeResolverExtensionRegistry $registry) diff --git a/src/PhpDoc/JsonValidateStubFilesExtension.php b/src/PhpDoc/JsonValidateStubFilesExtension.php new file mode 100644 index 0000000000..3bfcfe862c --- /dev/null +++ b/src/PhpDoc/JsonValidateStubFilesExtension.php @@ -0,0 +1,23 @@ +phpVersion->supportsJsonValidate()) { + return []; + } + + return [__DIR__ . '/../../stubs/json_validate.stub']; + } + +} diff --git a/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php b/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php index 4a041e369e..7c00dfe34d 100644 --- a/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php +++ b/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php @@ -4,7 +4,7 @@ use PHPStan\DependencyInjection\Container; -class LazyTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider +final class LazyTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider { private ?TypeNodeResolverExtensionRegistry $registry = null; diff --git a/src/PhpDoc/NameScopeAlreadyBeingCreatedException.php b/src/PhpDoc/NameScopeAlreadyBeingCreatedException.php new file mode 100644 index 0000000000..4d781e7f00 --- /dev/null +++ b/src/PhpDoc/NameScopeAlreadyBeingCreatedException.php @@ -0,0 +1,10 @@ +docComment; } - public function getFile(): string + public function getFile(): ?string { return $this->file; } @@ -84,63 +85,96 @@ public function transformArrayKeysWithParameterNameMapping(array $array): array return $newArray; } - /** - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - */ + public function transformConditionalReturnTypeWithParameterNameMapping(Type $type): Type + { + return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { + if ($type instanceof ConditionalTypeForParameter) { + $parameterName = substr($type->getParameterName(), 1); + if (array_key_exists($parameterName, $this->parameterNameMapping)) { + $type = $type->changeParameterName('$' . $this->parameterNameMapping[$parameterName]); + } + } + + return $traverse($type); + }); + } + + public function transformAssertTagParameterWithParameterNameMapping(AssertTagParameter $parameter): AssertTagParameter + { + $parameterName = substr($parameter->getParameterName(), 1); + if (array_key_exists($parameterName, $this->parameterNameMapping)) { + $parameter = $parameter->changeParameterName('$' . $this->parameterNameMapping[$parameterName]); + } + + return $parameter; + } + public static function resolvePhpDocBlockForProperty( ?string $docComment, ClassReflection $classReflection, ?string $trait, string $propertyName, - string $file, + ?string $file, ?bool $explicit, - array $originalPositionalParameterNames, // unused - array $newPositionalParameterNames, // unused ): self { - return self::resolvePhpDocBlockTree( - $docComment, + $docBlocksFromParents = []; + foreach (self::getParentReflections($classReflection) as $parentReflection) { + $oneResult = self::resolvePropertyPhpDocBlockFromClass( + $parentReflection, + $propertyName, + $explicit ?? $docComment !== null, + ); + + if ($oneResult === null) { // Null if it is private or from a wrong trait. + continue; + } + + $docBlocksFromParents[] = $oneResult; + } + + return new self( + $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $file, $classReflection, $trait, - $propertyName, - $file, - 'hasNativeProperty', - 'getNativeProperty', - __FUNCTION__, - $explicit, - [], + $explicit ?? true, [], + $docBlocksFromParents, ); } - /** - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - */ public static function resolvePhpDocBlockForConstant( ?string $docComment, ClassReflection $classReflection, - ?string $trait, // unused string $constantName, - string $file, + ?string $file, ?bool $explicit, - array $originalPositionalParameterNames, // unused - array $newPositionalParameterNames, // unused ): self { - return self::resolvePhpDocBlockTree( - $docComment, + $docBlocksFromParents = []; + foreach (self::getParentReflections($classReflection) as $parentReflection) { + $oneResult = self::resolveConstantPhpDocBlockFromClass( + $parentReflection, + $constantName, + $explicit ?? $docComment !== null, + ); + + if ($oneResult === null) { // Null if it is private or from a wrong trait. + continue; + } + + $docBlocksFromParents[] = $oneResult; + } + + return new self( + $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $file, $classReflection, null, - $constantName, - $file, - 'hasConstant', - 'getConstant', - __FUNCTION__, - $explicit, - [], + $explicit ?? true, [], + $docBlocksFromParents, ); } @@ -153,57 +187,61 @@ public static function resolvePhpDocBlockForMethod( ClassReflection $classReflection, ?string $trait, string $methodName, - string $file, + ?string $file, ?bool $explicit, array $originalPositionalParameterNames, array $newPositionalParameterNames, ): self { - return self::resolvePhpDocBlockTree( - $docComment, - $classReflection, - $trait, - $methodName, - $file, - 'hasNativeMethod', - 'getNativeMethod', - __FUNCTION__, - $explicit, - $originalPositionalParameterNames, - $newPositionalParameterNames, - ); - } + $docBlocksFromParents = []; + foreach (self::getParentReflections($classReflection) as $parentReflection) { + $oneResult = self::resolveMethodPhpDocBlockFromClass( + $parentReflection, + $methodName, + $explicit ?? $docComment !== null, + $newPositionalParameterNames, + ); - /** - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - */ - private static function resolvePhpDocBlockTree( - ?string $docComment, - ClassReflection $classReflection, - ?string $trait, - string $name, - string $file, - string $hasMethodName, - string $getMethodName, - string $resolveMethodName, - ?bool $explicit, - array $originalPositionalParameterNames, - array $newPositionalParameterNames, - ): self - { - $docBlocksFromParents = self::resolveParentPhpDocBlocks( - $classReflection, - $name, - $hasMethodName, - $getMethodName, - $resolveMethodName, - $explicit ?? $docComment !== null, - $newPositionalParameterNames, - ); + if ($oneResult === null) { // Null if it is private or from a wrong trait. + continue; + } + + $docBlocksFromParents[] = $oneResult; + } + + foreach ($classReflection->getTraits(true) as $traitReflection) { + if (!$traitReflection->hasNativeMethod($methodName)) { + continue; + } + $traitMethod = $traitReflection->getNativeMethod($methodName); + $abstract = $traitMethod->isAbstract(); + if (is_bool($abstract)) { + if (!$abstract) { + continue; + } + } elseif (!$abstract->yes()) { + continue; + } + + $methodVariant = $traitMethod->getOnlyVariant(); + $positionalMethodParameterNames = []; + foreach ($methodVariant->getParameters() as $methodParameter) { + $positionalMethodParameterNames[] = $methodParameter->getName(); + } + + $docBlocksFromParents[] = new self( + $traitMethod->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $classReflection->getFileName(), + $classReflection, + $traitReflection->getName(), + $explicit ?? $traitMethod->getDocComment() !== null, + self::remapParameterNames($newPositionalParameterNames, $positionalMethodParameterNames), + [], + ); + } return new self( - $docComment ?? '/** */', + $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, $file, $classReflection, $trait, @@ -235,116 +273,125 @@ private static function remapParameterNames( } /** - * @param array $positionalParameterNames - * @return array + * @return array */ - private static function resolveParentPhpDocBlocks( + private static function getParentReflections(ClassReflection $classReflection): array + { + $result = []; + + $parent = $classReflection->getParentClass(); + if ($parent !== null) { + $result[] = $parent; + } + + foreach ($classReflection->getInterfaces() as $interface) { + $result[] = $interface; + } + + return $result; + } + + private static function resolveConstantPhpDocBlockFromClass( ClassReflection $classReflection, string $name, - string $hasMethodName, - string $getMethodName, - string $resolveMethodName, bool $explicit, - array $positionalParameterNames, - ): array + ): ?self { - $result = []; - $parentReflections = self::getParentReflections($classReflection); + if ($classReflection->hasConstant($name)) { + $parentReflection = $classReflection->getConstant($name); + if ($parentReflection->isPrivate()) { + return null; + } - foreach ($parentReflections as $parentReflection) { - $oneResult = self::resolvePhpDocBlockFromClass( - $parentReflection, + $classReflection = $parentReflection->getDeclaringClass(); + + return self::resolvePhpDocBlockForConstant( + $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $classReflection, $name, - $hasMethodName, - $getMethodName, - $resolveMethodName, + $classReflection->getFileName(), $explicit, - $positionalParameterNames, ); - - if ($oneResult === null) { // Null if it is private or from a wrong trait. - continue; - } - - $result[] = $oneResult; } - return $result; + return null; } - /** - * @return array - */ - private static function getParentReflections(ClassReflection $classReflection): array + private static function resolvePropertyPhpDocBlockFromClass( + ClassReflection $classReflection, + string $name, + bool $explicit, + ): ?self { - $result = []; + if ($classReflection->hasNativeProperty($name)) { + $parentReflection = $classReflection->getNativeProperty($name); + if ($parentReflection->isPrivate()) { + return null; + } - $parent = $classReflection->getParentClass(); - if ($parent !== null) { - $result[] = $parent; - } + $classReflection = $parentReflection->getDeclaringClass(); + $traitReflection = $parentReflection->getDeclaringTrait(); - foreach ($classReflection->getInterfaces() as $interface) { - $result[] = $interface; + $trait = $traitReflection !== null + ? $traitReflection->getName() + : null; + + return self::resolvePhpDocBlockForProperty( + $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, + $classReflection, + $trait, + $name, + $classReflection->getFileName(), + $explicit, + ); } - return $result; + return null; } /** * @param array $positionalParameterNames */ - private static function resolvePhpDocBlockFromClass( + private static function resolveMethodPhpDocBlockFromClass( ClassReflection $classReflection, string $name, - string $hasMethodName, - string $getMethodName, - string $resolveMethodName, bool $explicit, array $positionalParameterNames, ): ?self { - if ($classReflection->getFileName() !== null && $classReflection->$hasMethodName($name)) { - /** @var PropertyReflection|MethodReflection|ConstantReflection $parentReflection */ - $parentReflection = $classReflection->$getMethodName($name); + if ($classReflection->hasNativeMethod($name)) { + $parentReflection = $classReflection->getNativeMethod($name); if ($parentReflection->isPrivate()) { return null; } - if ($parentReflection instanceof PhpPropertyReflection || $parentReflection instanceof ResolvedPropertyReflection) { + $classReflection = $parentReflection->getDeclaringClass(); + $traitReflection = null; + if ($parentReflection instanceof PhpMethodReflection || $parentReflection instanceof ResolvedMethodReflection) { $traitReflection = $parentReflection->getDeclaringTrait(); - $positionalMethodParameterNames = []; - } elseif ($parentReflection instanceof MethodReflection) { - $traitReflection = null; - if ($parentReflection instanceof PhpMethodReflection || $parentReflection instanceof ResolvedMethodReflection) { - $traitReflection = $parentReflection->getDeclaringTrait(); - } - $methodVariants = $parentReflection->getVariants(); - $positionalMethodParameterNames = []; - $lowercaseMethodName = strtolower($parentReflection->getName()); - if ( - count($methodVariants) === 1 - && $lowercaseMethodName !== '__construct' - && $lowercaseMethodName !== strtolower($parentReflection->getDeclaringClass()->getName()) - ) { - $methodParameters = $methodVariants[0]->getParameters(); - foreach ($methodParameters as $methodParameter) { - $positionalMethodParameterNames[] = $methodParameter->getName(); - } - } else { - $positionalMethodParameterNames = $positionalParameterNames; + } + $methodVariants = $parentReflection->getVariants(); + $positionalMethodParameterNames = []; + $lowercaseMethodName = strtolower($parentReflection->getName()); + if ( + count($methodVariants) === 1 + && $lowercaseMethodName !== '__construct' + && $lowercaseMethodName !== strtolower($parentReflection->getDeclaringClass()->getName()) + ) { + $methodParameters = $methodVariants[0]->getParameters(); + foreach ($methodParameters as $methodParameter) { + $positionalMethodParameterNames[] = $methodParameter->getName(); } } else { - $traitReflection = null; - $positionalMethodParameterNames = []; + $positionalMethodParameterNames = $positionalParameterNames; } $trait = $traitReflection !== null ? $traitReflection->getName() : null; - return self::$resolveMethodName( - $parentReflection->getDocComment() ?? '/** */', + return self::resolvePhpDocBlockForMethod( + $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, $classReflection, $trait, $name, diff --git a/src/PhpDoc/PhpDocInheritanceResolver.php b/src/PhpDoc/PhpDocInheritanceResolver.php index 3d0b571146..5b6aaacc3b 100644 --- a/src/PhpDoc/PhpDocInheritanceResolver.php +++ b/src/PhpDoc/PhpDocInheritanceResolver.php @@ -2,13 +2,13 @@ namespace PHPStan\PhpDoc; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\Reflection\ClassReflection; use PHPStan\Type\FileTypeMapper; -use ReflectionParameter; use function array_map; use function strtolower; -class PhpDocInheritanceResolver +final class PhpDocInheritanceResolver { public function __construct( @@ -21,7 +21,7 @@ public function __construct( public function resolvePhpDocForProperty( ?string $docComment, ClassReflection $classReflection, - string $classReflectionFileName, + ?string $classReflectionFileName, ?string $declaringTraitName, string $propertyName, ): ResolvedPhpDocBlock @@ -33,8 +33,6 @@ public function resolvePhpDocForProperty( $propertyName, $classReflectionFileName, null, - [], - [], ); return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $declaringTraitName, null, $propertyName, null); @@ -43,19 +41,16 @@ public function resolvePhpDocForProperty( public function resolvePhpDocForConstant( ?string $docComment, ClassReflection $classReflection, - string $classReflectionFileName, + ?string $classReflectionFileName, string $constantName, ): ResolvedPhpDocBlock { $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForConstant( $docComment, $classReflection, - null, $constantName, $classReflectionFileName, null, - [], - [], ); return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, null, null, null, $constantName); @@ -66,7 +61,7 @@ public function resolvePhpDocForConstant( */ public function resolvePhpDocForMethod( ?string $docComment, - string $fileName, + ?string $fileName, ClassReflection $classReflection, ?string $declaringTraitName, string $methodName, @@ -94,9 +89,9 @@ private function docBlockTreeToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?strin foreach ($phpDocBlock->getParents() as $parentPhpDocBlock) { if ( - $parentPhpDocBlock->getClassReflection()->isBuiltin() - && $functionName !== null + $functionName !== null && strtolower($functionName) === '__construct' + && $parentPhpDocBlock->getClassReflection()->isBuiltin() ) { continue; } @@ -119,7 +114,7 @@ private function docBlockToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $t $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())); + $stub = $this->stubPhpDocProvider->findMethodPhpDoc($classReflection->getName(), $classReflection->getName(), $functionName, array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); if ($stub !== null) { return $stub; } @@ -127,6 +122,16 @@ private function docBlockToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $t if ($propertyName !== null && $classReflection->getNativeReflection()->hasProperty($propertyName)) { $stub = $this->stubPhpDocProvider->findPropertyPhpDoc($classReflection->getName(), $propertyName); + + if ($stub === null) { + $propertyReflection = $classReflection->getNativeReflection()->getProperty($propertyName); + + $propertyDeclaringClass = $propertyReflection->getBetterReflection()->getDeclaringClass(); + + if ($propertyDeclaringClass->isTrait() && (! $propertyReflection->getDeclaringClass()->isTrait() || $propertyReflection->getDeclaringClass()->getName() !== $propertyDeclaringClass->getName())) { + $stub = $this->stubPhpDocProvider->findPropertyPhpDoc($propertyDeclaringClass->getName(), $propertyName); + } + } if ($stub !== null) { return $stub; } diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 4aa2730423..1c31ee55d0 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -3,15 +3,22 @@ namespace PHPStan\PhpDoc; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\Tag\AssertTag; +use PHPStan\PhpDoc\Tag\AssertTagParameter; use PHPStan\PhpDoc\Tag\DeprecatedTag; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MethodTagParameter; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamClosureThisTag; +use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; +use PHPStan\PhpDoc\Tag\RequireExtendsTag; +use PHPStan\PhpDoc\Tag\RequireImplementsTag; use PHPStan\PhpDoc\Tag\ReturnTag; +use PHPStan\PhpDoc\Tag\SelfOutTypeTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; @@ -24,18 +31,25 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\Reflection\PassedByReference; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_key_exists; use function array_map; +use function array_merge; use function array_reverse; use function count; use function in_array; -use function strpos; +use function method_exists; +use function str_starts_with; use function substr; -class PhpDocNodeResolver +final class PhpDocNodeResolver { public function __construct( @@ -53,7 +67,7 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar { $resolved = []; $resolvedByTag = []; - foreach (['@var', '@psalm-var', '@phpstan-var'] as $tagName) { + foreach (['@var', '@phan-var', '@psalm-var', '@phpstan-var'] as $tagName) { $tagResolved = []; foreach ($phpDocNode->getVarTagValues($tagName) as $tagValue) { $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); @@ -97,8 +111,7 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope $resolved[$propertyName] = new PropertyTag( $propertyType, - true, - true, + $propertyType, ); } } @@ -108,10 +121,14 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope $propertyName = substr($tagValue->propertyName, 1); $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + $writableType = null; + if (array_key_exists($propertyName, $resolved)) { + $writableType = $resolved[$propertyName]->getWritableType(); + } + $resolved[$propertyName] = new PropertyTag( $propertyType, - true, - false, + $writableType, ); } } @@ -121,10 +138,14 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope $propertyName = substr($tagValue->propertyName, 1); $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + $readableType = null; + if (array_key_exists($propertyName, $resolved)) { + $readableType = $resolved[$propertyName]->getReadableType(); + } + $resolved[$propertyName] = new PropertyTag( + $readableType, $propertyType, - false, - true, ); } } @@ -138,9 +159,32 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $resolved = []; + $originalNameScope = $nameScope; - foreach (['@method', '@psalm-method', '@phpstan-method'] as $tagName) { + foreach (['@method', '@phan-method', '@psalm-method', '@phpstan-method'] as $tagName) { foreach ($phpDocNode->getMethodTagValues($tagName) as $tagValue) { + $nameScope = $originalNameScope; + $templateTags = []; + + if (count($tagValue->templateTypes) > 0 && $nameScope->getClassName() !== null) { + foreach ($tagValue->templateTypes as $templateType) { + $templateTags[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) + : new MixedType(), + $templateType->default !== null + ? $this->typeNodeResolver->resolve($templateType->default, $nameScope) + : null, + TemplateTypeVariance::createInvariant(), + ); + } + + $templateTypeScope = TemplateTypeScope::createWithMethod($nameScope->getClassName(), $tagValue->methodName); + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + } + $parameters = []; foreach ($tagValue->parameters as $parameterNode) { $parameterName = substr($parameterNode->parameterName, 1); @@ -152,7 +196,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): } $defaultValue = null; if ($parameterNode->defaultValue !== null) { - $defaultValue = $this->constExprNodeResolver->resolve($parameterNode->defaultValue); + $defaultValue = $this->constExprNodeResolver->resolve($parameterNode->defaultValue, $nameScope); } $parameters[$parameterName] = new MethodTagParameter( @@ -172,6 +216,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): : new MixedType(), $tagValue->isStatic, $parameters, + $templateTags, ); } } @@ -186,7 +231,7 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope) { $resolved = []; - foreach (['@extends', '@template-extends', '@phpstan-extends'] as $tagName) { + foreach (['@extends', '@phan-extends', '@phan-inherits', '@template-extends', '@phpstan-extends'] as $tagName) { foreach ($phpDocNode->getExtendsTagValues($tagName) as $tagValue) { $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ExtendsTag( $this->typeNodeResolver->resolve($tagValue->type, $nameScope), @@ -243,8 +288,9 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope $prefixPriority = [ '' => 0, - 'psalm' => 1, - 'phpstan' => 2, + 'phan' => 1, + 'psalm' => 2, + 'phpstan' => 3, ]; foreach ($phpDocNode->getTags() as $phpDocTagNode) { @@ -254,17 +300,21 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope } $tagName = $phpDocTagNode->name; - if (in_array($tagName, ['@template', '@psalm-template', '@phpstan-template'], true)) { + if (in_array($tagName, ['@template', '@phan-template', '@psalm-template', '@phpstan-template'], true)) { $variance = TemplateTypeVariance::createInvariant(); } elseif (in_array($tagName, ['@template-covariant', '@psalm-template-covariant', '@phpstan-template-covariant'], true)) { $variance = TemplateTypeVariance::createCovariant(); + } elseif (in_array($tagName, ['@template-contravariant', '@psalm-template-contravariant', '@phpstan-template-contravariant'], true)) { + $variance = TemplateTypeVariance::createContravariant(); } else { continue; } - if (strpos($tagName, '@psalm-') === 0) { + if (str_starts_with($tagName, '@phan-')) { + $prefix = 'phan'; + } elseif (str_starts_with($tagName, '@psalm-')) { $prefix = 'psalm'; - } elseif (strpos($tagName, '@phpstan-') === 0) { + } elseif (str_starts_with($tagName, '@phpstan-')) { $prefix = 'phpstan'; } else { $prefix = ''; @@ -277,9 +327,12 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope } } + $nameScopeWithoutCurrent = $nameScope->unsetTemplateType($valueNode->name); + $resolved[$valueNode->name] = new TemplateTag( $valueNode->name, - $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(true), + $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScopeWithoutCurrent) : new MixedType(true), + $valueNode->default !== null ? $this->typeNodeResolver->resolve($valueNode->default, $nameScopeWithoutCurrent) : null, $variance, ); $resolvedPrefix[$valueNode->name] = $prefix; @@ -295,7 +348,7 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): { $resolved = []; - foreach (['@param', '@psalm-param', '@phpstan-param'] as $tagName) { + foreach (['@param', '@phan-param', '@psalm-param', '@phpstan-param'] as $tagName) { foreach ($phpDocNode->getParamTagValues($tagName) as $tagValue) { $parameterName = substr($tagValue->parameterName, 1); $parameterType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); @@ -313,11 +366,82 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): return $resolved; } + /** + * @return array + */ + public function resolveParamOutTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + if (!method_exists($phpDocNode, 'getParamOutTypeTagValues')) { + return []; + } + + $resolved = []; + + foreach (['@param-out', '@psalm-param-out', '@phpstan-param-out'] as $tagName) { + foreach ($phpDocNode->getParamOutTypeTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameterType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + if ($this->shouldSkipType($tagName, $parameterType)) { + continue; + } + + $resolved[$parameterName] = new ParamOutTag( + $parameterType, + ); + } + } + + return $resolved; + } + + /** + * @return array + */ + public function resolveParamImmediatelyInvokedCallable(PhpDocNode $phpDocNode): array + { + $parameters = []; + foreach (['@param-immediately-invoked-callable', '@phpstan-param-immediately-invoked-callable'] as $tagName) { + foreach ($phpDocNode->getParamImmediatelyInvokedCallableTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameters[$parameterName] = true; + } + } + foreach (['@param-later-invoked-callable', '@phpstan-param-later-invoked-callable'] as $tagName) { + foreach ($phpDocNode->getParamLaterInvokedCallableTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameters[$parameterName] = false; + } + } + + return $parameters; + } + + /** + * @return array + */ + public function resolveParamClosureThisTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $closureThisTypes = []; + foreach (['@param-closure-this', '@phpstan-param-closure-this'] as $tagName) { + foreach ($phpDocNode->getParamClosureThisTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $closureThisTypes[$parameterName] = new ParamClosureThisTag( + TypeCombinator::intersect( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), + new ObjectWithoutClassType(), + ), + ); + } + } + + return $closureThisTypes; + } + public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?ReturnTag { $resolved = null; - foreach (['@return', '@psalm-return', '@phpstan-return'] as $tagName) { + foreach (['@return', '@phan-return', '@phan-real-return', '@psalm-return', '@phpstan-return'] as $tagName) { foreach ($phpDocNode->getReturnTagValues($tagName) as $tagValue) { $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); if ($this->shouldSkipType($tagName, $type)) { @@ -362,6 +486,42 @@ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope): ), $phpDocNode->getMixinTagValues()); } + /** + * @return array + */ + public function resolveRequireExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@psalm-require-extends', '@phpstan-require-extends'] as $tagName) { + foreach ($phpDocNode->getRequireExtendsTagValues($tagName) as $tagValue) { + $resolved[] = new RequireExtendsTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), + ); + } + } + + return $resolved; + } + + /** + * @return array + */ + public function resolveRequireImplementsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@psalm-require-implements', '@phpstan-require-implements'] as $tagName) { + foreach ($phpDocNode->getRequireImplementsTagValues($tagName) as $tagValue) { + $resolved[] = new RequireImplementsTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), + ); + } + } + + return $resolved; + } + /** * @return array */ @@ -369,7 +529,7 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScop { $resolved = []; - foreach (['@psalm-type', '@phpstan-type'] as $tagName) { + foreach (['@phan-type', '@psalm-type', '@phpstan-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) { $alias = $typeAliasTagValue->alias; $typeNode = $typeAliasTagValue->type; @@ -399,6 +559,71 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na return $resolved; } + /** + * @return AssertTag[] + */ + public function resolveAssertTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + foreach (['@phpstan', '@psalm', '@phan'] as $prefix) { + $resolved = array_merge( + $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert', AssertTag::NULL), + $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert-if-true', AssertTag::IF_TRUE), + $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert-if-false', AssertTag::IF_FALSE), + ); + + if (count($resolved) > 0) { + return $resolved; + } + } + + return []; + } + + /** + * @param AssertTag::NULL|AssertTag::IF_TRUE|AssertTag::IF_FALSE $if + * @return AssertTag[] + */ + private function resolveAssertTagsFor(PhpDocNode $phpDocNode, NameScope $nameScope, string $tagName, string $if): array + { + $resolved = []; + + foreach ($phpDocNode->getAssertTagValues($tagName) as $assertTagValue) { + $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); + $parameter = new AssertTagParameter($assertTagValue->parameter, null, null); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality, true); + } + + foreach ($phpDocNode->getAssertPropertyTagValues($tagName) as $assertTagValue) { + $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); + $parameter = new AssertTagParameter($assertTagValue->parameter, $assertTagValue->property, null); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality, true); + } + + foreach ($phpDocNode->getAssertMethodTagValues($tagName) as $assertTagValue) { + $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); + $parameter = new AssertTagParameter($assertTagValue->parameter, null, $assertTagValue->method); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality, true); + } + + return $resolved; + } + + public function resolveSelfOutTypeTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?SelfOutTypeTag + { + if (!method_exists($phpDocNode, 'getSelfOutTypeTagValues')) { + return null; + } + + foreach (['@phpstan-this-out', '@phpstan-self-out', '@psalm-this-out', '@psalm-self-out'] as $tagName) { + foreach ($phpDocNode->getSelfOutTypeTagValues($tagName) as $selfOutTypeTagValue) { + $type = $this->typeNodeResolver->resolve($selfOutTypeTagValue->type, $nameScope); + return new SelfOutTypeTag($type); + } + } + + return null; + } + public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?DeprecatedTag { foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) { @@ -416,6 +641,13 @@ public function resolveIsDeprecated(PhpDocNode $phpDocNode): bool return count($deprecatedTags) > 0; } + public function resolveIsNotDeprecated(PhpDocNode $phpDocNode): bool + { + $notDeprecatedTags = $phpDocNode->getTagsByName('@not-deprecated'); + + return count($notDeprecatedTags) > 0; + } + public function resolveIsInternal(PhpDocNode $phpDocNode): bool { $internalTags = $phpDocNode->getTagsByName('@internal'); @@ -433,7 +665,7 @@ public function resolveIsFinal(PhpDocNode $phpDocNode): bool public function resolveIsPure(PhpDocNode $phpDocNode): bool { foreach ($phpDocNode->getTags() as $phpDocTagNode) { - if (in_array($phpDocTagNode->name, ['@pure', '@psalm-pure', '@phpstan-pure'], true)) { + if (in_array($phpDocTagNode->name, ['@pure', '@phan-pure', '@phan-side-effect-free', '@psalm-pure', '@phpstan-pure'], true)) { return true; } } @@ -452,13 +684,70 @@ public function resolveIsImpure(PhpDocNode $phpDocNode): bool return false; } + public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool + { + foreach (['@readonly', '@phan-read-only', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) { + $tags = $phpDocNode->getTagsByName($tagName); + + if (count($tags) > 0) { + return true; + } + } + + return false; + } + + public function resolveIsImmutable(PhpDocNode $phpDocNode): bool + { + foreach (['@immutable', '@phan-immutable', '@psalm-immutable', '@phpstan-immutable'] as $tagName) { + $tags = $phpDocNode->getTagsByName($tagName); + + if (count($tags) > 0) { + return true; + } + } + + return false; + } + + public function resolveHasConsistentConstructor(PhpDocNode $phpDocNode): bool + { + foreach (['@consistent-constructor', '@phpstan-consistent-constructor', '@psalm-consistent-constructor'] as $tagName) { + $tags = $phpDocNode->getTagsByName($tagName); + + if (count($tags) > 0) { + return true; + } + } + + return false; + } + + public function resolveAcceptsNamedArguments(PhpDocNode $phpDocNode): bool + { + return count($phpDocNode->getTagsByName('@no-named-arguments')) === 0; + } + private function shouldSkipType(string $tagName, Type $type): bool { - if (strpos($tagName, '@psalm-') !== 0) { + if (!str_starts_with($tagName, '@psalm-')) { return false; } return $this->unresolvableTypeHelper->containsUnresolvableType($type); } + public function resolveAllowPrivateMutation(PhpDocNode $phpDocNode): bool + { + foreach (['@phpstan-readonly-allow-private-mutation', '@phpstan-allow-private-mutation', '@psalm-readonly-allow-private-mutation', '@psalm-allow-private-mutation'] as $tagName) { + $tags = $phpDocNode->getTagsByName($tagName); + + if (count($tags) > 0) { + return true; + } + } + + return false; + } + } diff --git a/src/PhpDoc/PhpDocStringResolver.php b/src/PhpDoc/PhpDocStringResolver.php index 73b968826d..7c8129a3cc 100644 --- a/src/PhpDoc/PhpDocStringResolver.php +++ b/src/PhpDoc/PhpDocStringResolver.php @@ -7,7 +7,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; -class PhpDocStringResolver +final class PhpDocStringResolver { public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) @@ -18,7 +18,7 @@ public function resolve(string $phpDocString): PhpDocNode { $tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString)); $phpDocNode = $this->phpDocParser->parse($tokens); - $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore-line + $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore missingType.checkedException return $phpDocNode; } diff --git a/src/PhpDoc/ReflectionClassStubFilesExtension.php b/src/PhpDoc/ReflectionClassStubFilesExtension.php new file mode 100644 index 0000000000..1abd672f68 --- /dev/null +++ b/src/PhpDoc/ReflectionClassStubFilesExtension.php @@ -0,0 +1,27 @@ +phpVersion->supportsLazyObjects()) { + return [ + __DIR__ . '/../../stubs/ReflectionClass.stub', + ]; + } + + return [ + __DIR__ . '/../../stubs/ReflectionClassWithLazyObjects.stub', + ]; + } + +} diff --git a/src/PhpDoc/ReflectionEnumStubFilesExtension.php b/src/PhpDoc/ReflectionEnumStubFilesExtension.php new file mode 100644 index 0000000000..ed9b43b6be --- /dev/null +++ b/src/PhpDoc/ReflectionEnumStubFilesExtension.php @@ -0,0 +1,27 @@ +phpVersion->supportsEnums()) { + return []; + } + + if (!$this->phpVersion->supportsLazyObjects()) { + return [__DIR__ . '/../../stubs/ReflectionEnum.stub']; + } + + return [__DIR__ . '/../../stubs/ReflectionEnumWithLazyObjects.stub']; + } + +} diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 38350d4cf4..6cd991341b 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -3,14 +3,20 @@ namespace PHPStan\PhpDoc; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\Tag\AssertTag; use PHPStan\PhpDoc\Tag\DeprecatedTag; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamClosureThisTag; +use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; +use PHPStan\PhpDoc\Tag\RequireExtendsTag; +use PHPStan\PhpDoc\Tag\RequireImplementsTag; use PHPStan\PhpDoc\Tag\ReturnTag; +use PHPStan\PhpDoc\Tag\SelfOutTypeTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; @@ -19,16 +25,29 @@ use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\StaticType; +use PHPStan\Type\Type; +use PHPStan\Type\TypeTraverser; use function array_key_exists; +use function array_map; use function count; use function is_bool; +use function substr; -/** @api */ -class ResolvedPhpDocBlock +/** + * @api + */ +final class ResolvedPhpDocBlock { + public const EMPTY_DOC_STRING = '/** */'; + private PhpDocNode $phpDocNode; /** @var PhpDocNode[] */ @@ -47,6 +66,8 @@ class ResolvedPhpDocBlock private PhpDocNodeResolver $phpDocNodeResolver; + private ReflectionProvider $reflectionProvider; + /** @var array<(string|int), VarTag>|false */ private array|false $varTags = false; @@ -68,6 +89,15 @@ class ResolvedPhpDocBlock /** @var array|false */ private array|false $paramTags = false; + /** @var array|false */ + private array|false $paramOutTags = false; + + /** @var array|false */ + private array|false $paramsImmediatelyInvokedCallable = false; + + /** @var array|false */ + private array|false $paramClosureThisTags = false; + private ReturnTag|false|null $returnTag = false; private ThrowsTag|false|null $throwsTag = false; @@ -75,16 +105,29 @@ class ResolvedPhpDocBlock /** @var array|false */ private array|false $mixinTags = false; + /** @var array|false */ + private array|false $requireExtendsTags = false; + + /** @var array|false */ + private array|false $requireImplementsTags = false; + /** @var array|false */ private array|false $typeAliasTags = false; /** @var array|false */ private array|false $typeAliasImportTags = false; + /** @var array|false */ + private array|false $assertTags = false; + + private SelfOutTypeTag|false|null $selfOutTypeTag = false; + private DeprecatedTag|false|null $deprecatedTag = false; private ?bool $isDeprecated = null; + private ?bool $isNotDeprecated = null; + private ?bool $isInternal = null; private ?bool $isFinal = null; @@ -92,6 +135,16 @@ class ResolvedPhpDocBlock /** @var bool|'notLoaded'|null */ private bool|string|null $isPure = 'notLoaded'; + private ?bool $isReadOnly = null; + + private ?bool $isImmutable = null; + + private ?bool $isAllowedPrivateMutation = null; + + private ?bool $hasConsistentConstructor = null; + + private ?bool $acceptsNamedArguments = null; + private function __construct() { } @@ -102,14 +155,15 @@ private function __construct() public static function create( PhpDocNode $phpDocNode, string $phpDocString, - string $filename, + ?string $filename, NameScope $nameScope, TemplateTypeMap $templateTypeMap, array $templateTags, PhpDocNodeResolver $phpDocNodeResolver, + ReflectionProvider $reflectionProvider, ): self { - // new property also needs to be added to createEmpty() and merge() + // new property also needs to be added to withNameScope(), createEmpty() and merge() $self = new self(); $self->phpDocNode = $phpDocNode; $self->phpDocNodes = [$phpDocNode]; @@ -119,6 +173,23 @@ public static function create( $self->templateTypeMap = $templateTypeMap; $self->templateTags = $templateTags; $self->phpDocNodeResolver = $phpDocNodeResolver; + $self->reflectionProvider = $reflectionProvider; + + return $self; + } + + public function withNameScope(NameScope $nameScope): self + { + $self = new self(); + $self->phpDocNode = $this->phpDocNode; + $self->phpDocNodes = $this->phpDocNodes; + $self->phpDocString = $this->phpDocString; + $self->filename = $this->filename; + $self->nameScope = $nameScope; + $self->templateTypeMap = $this->templateTypeMap; + $self->templateTags = $this->templateTags; + $self->phpDocNodeResolver = $this->phpDocNodeResolver; + $self->reflectionProvider = $this->reflectionProvider; return $self; } @@ -127,7 +198,7 @@ public static function createEmpty(): self { // new property also needs to be added to merge() $self = new self(); - $self->phpDocString = '/** */'; + $self->phpDocString = self::EMPTY_DOC_STRING; $self->phpDocNodes = []; $self->filename = null; $self->templateTypeMap = TemplateTypeMap::createEmpty(); @@ -139,16 +210,29 @@ public static function createEmpty(): self $self->implementsTags = []; $self->usesTags = []; $self->paramTags = []; + $self->paramOutTags = []; + $self->paramsImmediatelyInvokedCallable = []; + $self->paramClosureThisTags = []; $self->returnTag = null; $self->throwsTag = null; $self->mixinTags = []; + $self->requireExtendsTags = []; + $self->requireImplementsTags = []; $self->typeAliasTags = []; $self->typeAliasImportTags = []; + $self->assertTags = []; + $self->selfOutTypeTag = null; $self->deprecatedTag = null; $self->isDeprecated = false; + $self->isNotDeprecated = false; $self->isInternal = false; $self->isFinal = false; $self->isPure = null; + $self->isReadOnly = false; + $self->isImmutable = false; + $self->isAllowedPrivateMutation = false; + $self->hasConsistentConstructor = false; + $self->acceptsNamedArguments = true; return $self; } @@ -159,18 +243,25 @@ public static function createEmpty(): self */ public function merge(array $parents, array $parentPhpDocBlocks): self { + $className = $this->nameScope !== null ? $this->nameScope->getClassName() : null; + $classReflection = $className !== null && $this->reflectionProvider->hasClass($className) + ? $this->reflectionProvider->getClass($className) + : null; + // new property also needs to be added to createEmpty() $result = new self(); // we will resolve everything on $this here so these properties don't have to be populated // skip $result->phpDocNode - // skip $result->phpDocString - just for stubs $phpDocNodes = $this->phpDocNodes; + $acceptsNamedArguments = $this->acceptsNamedArguments(); foreach ($parents as $parent) { foreach ($parent->phpDocNodes as $phpDocNode) { $phpDocNodes[] = $phpDocNode; + $acceptsNamedArguments = $acceptsNamedArguments && $parent->acceptsNamedArguments(); } } $result->phpDocNodes = $phpDocNodes; + $result->phpDocString = $this->phpDocString; $result->filename = $this->filename; // skip $result->nameScope $result->templateTypeMap = $this->templateTypeMap; @@ -183,16 +274,29 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->implementsTags = $this->getImplementsTags(); $result->usesTags = $this->getUsesTags(); $result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks); - $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $parents, $parentPhpDocBlocks); + $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks); + $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parents, $parentPhpDocBlocks); + $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parents, $parentPhpDocBlocks); + $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $classReflection, $parents, $parentPhpDocBlocks); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents); $result->mixinTags = $this->getMixinTags(); + $result->requireExtendsTags = $this->getRequireExtendsTags(); + $result->requireImplementsTags = $this->getRequireImplementsTags(); $result->typeAliasTags = $this->getTypeAliasTags(); $result->typeAliasImportTags = $this->getTypeAliasImportTags(); - $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $parents); + $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); + $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents); + $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $this->isNotDeprecated(), $parents); $result->isDeprecated = $result->deprecatedTag !== null; + $result->isNotDeprecated = $this->isNotDeprecated(); $result->isInternal = $this->isInternal(); $result->isFinal = $this->isFinal(); - $result->isPure = $this->isPure(); + $result->isPure = self::mergePureTags($this->isPure(), $parents); + $result->isReadOnly = $this->isReadOnly(); + $result->isImmutable = $this->isImmutable(); + $result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation(); + $result->hasConsistentConstructor = $this->hasConsistentConstructor(); + $result->acceptsNamedArguments = $acceptsNamedArguments; return $result; } @@ -206,14 +310,71 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self return $this; } - $paramTags = $this->getParamTags(); + $mapParameterCb = static function (Type $type, callable $traverse) use ($parameterNameMapping): Type { + if ($type instanceof ConditionalTypeForParameter) { + $parameterName = substr($type->getParameterName(), 1); + if (array_key_exists($parameterName, $parameterNameMapping)) { + $type = $type->changeParameterName('$' . $parameterNameMapping[$parameterName]); + } + } + + return $traverse($type); + }; $newParamTags = []; - foreach ($paramTags as $key => $paramTag) { + foreach ($this->getParamTags() as $key => $paramTag) { if (!array_key_exists($key, $parameterNameMapping)) { continue; } - $newParamTags[$parameterNameMapping[$key]] = $paramTag; + $transformedType = TypeTraverser::map($paramTag->getType(), $mapParameterCb); + $newParamTags[$parameterNameMapping[$key]] = $paramTag->withType($transformedType); + } + + $newParamOutTags = []; + foreach ($this->getParamOutTags() as $key => $paramOutTag) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + + $transformedType = TypeTraverser::map($paramOutTag->getType(), $mapParameterCb); + $newParamOutTags[$parameterNameMapping[$key]] = $paramOutTag->withType($transformedType); + } + + $newParamsImmediatelyInvokedCallable = []; + foreach ($this->getParamsImmediatelyInvokedCallable() as $key => $immediatelyInvokedCallable) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + + $newParamsImmediatelyInvokedCallable[$parameterNameMapping[$key]] = $immediatelyInvokedCallable; + } + + $paramClosureThisTags = $this->getParamClosureThisTags(); + $newParamClosureThisTags = []; + foreach ($paramClosureThisTags as $key => $paramClosureThisTag) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + + $transformedType = TypeTraverser::map($paramClosureThisTag->getType(), $mapParameterCb); + $newParamClosureThisTags[$parameterNameMapping[$key]] = $paramClosureThisTag->withType($transformedType); + } + + $returnTag = $this->getReturnTag(); + if ($returnTag !== null) { + $transformedType = TypeTraverser::map($returnTag->getType(), $mapParameterCb); + $returnTag = $returnTag->withType($transformedType); + } + + $assertTags = $this->getAssertTags(); + if (count($assertTags) > 0) { + $assertTags = array_map(static function (AssertTag $tag) use ($parameterNameMapping): AssertTag { + $parameterName = substr($tag->getParameter()->getParameterName(), 1); + if (array_key_exists($parameterName, $parameterNameMapping)) { + $tag = $tag->withParameter($tag->getParameter()->changeParameterName('$' . $parameterNameMapping[$parameterName])); + } + return $tag; + }, $assertTags); } $self = new self(); @@ -225,6 +386,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->templateTypeMap = $this->templateTypeMap; $self->templateTags = $this->templateTags; $self->phpDocNodeResolver = $this->phpDocNodeResolver; + $self->reflectionProvider = $this->reflectionProvider; $self->varTags = $this->varTags; $self->methodTags = $this->methodTags; $self->propertyTags = $this->propertyTags; @@ -232,13 +394,21 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->implementsTags = $this->implementsTags; $self->usesTags = $this->usesTags; $self->paramTags = $newParamTags; - $self->returnTag = $this->returnTag; + $self->paramOutTags = $newParamOutTags; + $self->paramsImmediatelyInvokedCallable = $newParamsImmediatelyInvokedCallable; + $self->paramClosureThisTags = $newParamClosureThisTags; + $self->returnTag = $returnTag; $self->throwsTag = $this->throwsTag; $self->mixinTags = $this->mixinTags; + $self->requireImplementsTags = $this->requireImplementsTags; + $self->requireExtendsTags = $this->requireExtendsTags; $self->typeAliasTags = $this->typeAliasTags; $self->typeAliasImportTags = $this->typeAliasImportTags; + $self->assertTags = $assertTags; + $self->selfOutTypeTag = $this->selfOutTypeTag; $self->deprecatedTag = $this->deprecatedTag; $self->isDeprecated = $this->isDeprecated; + $self->isNotDeprecated = $this->isNotDeprecated; $self->isInternal = $this->isInternal; $self->isFinal = $this->isFinal; $self->isPure = $this->isPure; @@ -246,6 +416,11 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self return $self; } + public function hasPhpDocString(): bool + { + return $this->phpDocString !== self::EMPTY_DOC_STRING; + } + public function getPhpDocString(): string { return $this->phpDocString; @@ -380,6 +555,47 @@ public function getParamTags(): array return $this->paramTags; } + /** + * @return array + */ + public function getParamOutTags(): array + { + if ($this->paramOutTags === false) { + $this->paramOutTags = $this->phpDocNodeResolver->resolveParamOutTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + return $this->paramOutTags; + } + + /** + * @return array + */ + public function getParamsImmediatelyInvokedCallable(): array + { + if ($this->paramsImmediatelyInvokedCallable === false) { + $this->paramsImmediatelyInvokedCallable = $this->phpDocNodeResolver->resolveParamImmediatelyInvokedCallable($this->phpDocNode); + } + + return $this->paramsImmediatelyInvokedCallable; + } + + /** + * @return array + */ + public function getParamClosureThisTags(): array + { + if ($this->paramClosureThisTags === false) { + $this->paramClosureThisTags = $this->phpDocNodeResolver->resolveParamClosureThisTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->paramClosureThisTags; + } + public function getReturnTag(): ?ReturnTag { if (is_bool($this->returnTag)) { @@ -417,6 +633,36 @@ public function getMixinTags(): array return $this->mixinTags; } + /** + * @return array + */ + public function getRequireExtendsTags(): array + { + if ($this->requireExtendsTags === false) { + $this->requireExtendsTags = $this->phpDocNodeResolver->resolveRequireExtendsTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->requireExtendsTags; + } + + /** + * @return array + */ + public function getRequireImplementsTags(): array + { + if ($this->requireImplementsTags === false) { + $this->requireImplementsTags = $this->phpDocNodeResolver->resolveRequireImplementsTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->requireImplementsTags; + } + /** * @return array */ @@ -447,6 +693,33 @@ public function getTypeAliasImportTags(): array return $this->typeAliasImportTags; } + /** + * @return array + */ + public function getAssertTags(): array + { + if ($this->assertTags === false) { + $this->assertTags = $this->phpDocNodeResolver->resolveAssertTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->assertTags; + } + + public function getSelfOutTag(): ?SelfOutTypeTag + { + if ($this->selfOutTypeTag === false) { + $this->selfOutTypeTag = $this->phpDocNodeResolver->resolveSelfOutTypeTag( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->selfOutTypeTag; + } + public function getDeprecatedTag(): ?DeprecatedTag { if (is_bool($this->deprecatedTag)) { @@ -468,6 +741,19 @@ public function isDeprecated(): bool return $this->isDeprecated; } + /** + * @internal + */ + public function isNotDeprecated(): bool + { + if ($this->isNotDeprecated === null) { + $this->isNotDeprecated = $this->phpDocNodeResolver->resolveIsNotDeprecated( + $this->phpDocNode, + ); + } + return $this->isNotDeprecated; + } + public function isInternal(): bool { if ($this->isInternal === null) { @@ -488,6 +774,26 @@ public function isFinal(): bool return $this->isFinal; } + public function hasConsistentConstructor(): bool + { + if ($this->hasConsistentConstructor === null) { + $this->hasConsistentConstructor = $this->phpDocNodeResolver->resolveHasConsistentConstructor( + $this->phpDocNode, + ); + } + return $this->hasConsistentConstructor; + } + + public function acceptsNamedArguments(): bool + { + if ($this->acceptsNamedArguments === null) { + $this->acceptsNamedArguments = $this->phpDocNodeResolver->resolveAcceptsNamedArguments( + $this->phpDocNode, + ); + } + return $this->acceptsNamedArguments; + } + public function getTemplateTypeMap(): TemplateTypeMap { return $this->templateTypeMap; @@ -502,14 +808,14 @@ public function isPure(): ?bool if ($pure) { $this->isPure = true; return $this->isPure; - } else { - $impure = $this->phpDocNodeResolver->resolveIsImpure( - $this->phpDocNode, - ); - if ($impure) { - $this->isPure = false; - return $this->isPure; - } + } + + $impure = $this->phpDocNodeResolver->resolveIsImpure( + $this->phpDocNode, + ); + if ($impure) { + $this->isPure = false; + return $this->isPure; } $this->isPure = null; @@ -518,6 +824,37 @@ public function isPure(): ?bool return $this->isPure; } + public function isReadOnly(): bool + { + if ($this->isReadOnly === null) { + $this->isReadOnly = $this->phpDocNodeResolver->resolveIsReadOnly( + $this->phpDocNode, + ); + } + return $this->isReadOnly; + } + + public function isImmutable(): bool + { + if ($this->isImmutable === null) { + $this->isImmutable = $this->phpDocNodeResolver->resolveIsImmutable( + $this->phpDocNode, + ); + } + return $this->isImmutable; + } + + public function isAllowedPrivateMutation(): bool + { + if ($this->isAllowedPrivateMutation === null) { + $this->isAllowedPrivateMutation = $this->phpDocNodeResolver->resolveAllowPrivateMutation( + $this->phpDocNode, + ); + } + + return $this->isAllowedPrivateMutation; + } + /** * @param array $varTags * @param array $parents @@ -544,13 +881,12 @@ private static function mergeVarTags(array $varTags, array $parents, array $pare } /** - * @param ResolvedPhpDocBlock $parent * @return array|null */ private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array { foreach ($parent->getVarTags() as $key => $parentVarTag) { - return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock)]; + return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock, TemplateTypeVariance::createInvariant())]; } return null; @@ -573,7 +909,6 @@ private static function mergeParamTags(array $paramTags, array $parents, array $ /** * @param array $paramTags - * @param ResolvedPhpDocBlock $parent * @return array */ private static function mergeOneParentParamTags(array $paramTags, self $parent, PhpDocBlock $phpDocBlock): array @@ -585,7 +920,11 @@ private static function mergeOneParentParamTags(array $paramTags, self $parent, continue; } - $paramTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock); + $paramTags[$name] = self::resolveTemplateTypeInTag( + $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), + $phpDocBlock, + TemplateTypeVariance::createContravariant(), + ); } return $paramTags; @@ -596,14 +935,14 @@ private static function mergeOneParentParamTags(array $paramTags, self $parent, * @param array $parentPhpDocBlocks * @return ReturnTag|Null */ - private static function mergeReturnTags(?ReturnTag $returnTag, array $parents, array $parentPhpDocBlocks): ?ReturnTag + private static function mergeReturnTags(?ReturnTag $returnTag, ?ClassReflection $classReflection, array $parents, array $parentPhpDocBlocks): ?ReturnTag { if ($returnTag !== null) { return $returnTag; } foreach ($parents as $i => $parent) { - $result = self::mergeOneParentReturnTag($returnTag, $parent, $parentPhpDocBlocks[$i]); + $result = self::mergeOneParentReturnTag($returnTag, $classReflection, $parent, $parentPhpDocBlocks[$i]); if ($result === null) { continue; } @@ -614,7 +953,7 @@ private static function mergeReturnTags(?ReturnTag $returnTag, array $parents, a return null; } - private static function mergeOneParentReturnTag(?ReturnTag $returnTag, self $parent, PhpDocBlock $phpDocBlock): ?ReturnTag + private static function mergeOneParentReturnTag(?ReturnTag $returnTag, ?ClassReflection $classReflection, self $parent, PhpDocBlock $phpDocBlock): ?ReturnTag { $parentReturnTag = $parent->getReturnTag(); if ($parentReturnTag === null) { @@ -623,26 +962,105 @@ private static function mergeOneParentReturnTag(?ReturnTag $returnTag, self $par $parentType = $parentReturnTag->getType(); + if ($classReflection !== null) { + $parentType = TypeTraverser::map( + $parentType, + static function (Type $type, callable $traverse) use ($classReflection): Type { + if ($type instanceof StaticType) { + return $type->changeBaseClass($classReflection); + } + + return $traverse($type); + }, + ); + + $parentReturnTag = $parentReturnTag->withType($parentType); + } + // Each parent would overwrite the previous one except if it returns a less specific type. // Do not care for incompatible types as there is a separate rule for that. if ($returnTag !== null && $parentType->isSuperTypeOf($returnTag->getType())->yes()) { return null; } - return self::resolveTemplateTypeInTag($parentReturnTag->toImplicit(), $phpDocBlock); + return self::resolveTemplateTypeInTag( + $parentReturnTag->withType( + $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentReturnTag->getType()), + )->toImplicit(), + $phpDocBlock, + TemplateTypeVariance::createCovariant(), + ); } /** + * @param array $assertTags * @param array $parents + * @param array $parentPhpDocBlocks + * @return array */ - private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, array $parents): ?DeprecatedTag + private static function mergeAssertTags(array $assertTags, array $parents, array $parentPhpDocBlocks): array + { + if (count($assertTags) > 0) { + return $assertTags; + } + foreach ($parents as $i => $parent) { + $result = $parent->getAssertTags(); + if (count($result) === 0) { + continue; + } + + $phpDocBlock = $parentPhpDocBlocks[$i]; + + return array_map( + static fn (AssertTag $assertTag) => self::resolveTemplateTypeInTag( + $assertTag->withParameter( + $phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()), + )->toImplicit(), + $phpDocBlock, + TemplateTypeVariance::createCovariant(), + ), + $result, + ); + } + + return $assertTags; + } + + /** + * @param array $parents + */ + private static function mergeSelfOutTypeTags(?SelfOutTypeTag $selfOutTypeTag, array $parents): ?SelfOutTypeTag + { + if ($selfOutTypeTag !== null) { + return $selfOutTypeTag; + } + foreach ($parents as $parent) { + $result = $parent->getSelfOutTag(); + if ($result === null) { + continue; + } + return $result; + } + + return null; + } + + /** + * @param array $parents + */ + private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, bool $hasNotDeprecatedTag, array $parents): ?DeprecatedTag { if ($deprecatedTag !== null) { return $deprecatedTag; } + + if ($hasNotDeprecatedTag) { + return null; + } + foreach ($parents as $parent) { $result = $parent->getDeprecatedTag(); - if ($result === null) { + if ($result === null && !$parent->isNotDeprecated()) { continue; } return $result; @@ -671,16 +1089,155 @@ private static function mergeThrowsTags(?ThrowsTag $throwsTag, array $parents): return null; } + /** + * @param array $paramOutTags + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamOutTags(array $paramOutTags, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramOutTags = self::mergeOneParentParamOutTags($paramOutTags, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramOutTags; + } + + /** + * @param array $paramOutTags + * @return array + */ + private static function mergeOneParentParamOutTags(array $paramOutTags, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentParamOutTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamOutTags()); + + foreach ($parentParamOutTags as $name => $parentParamTag) { + if (array_key_exists($name, $paramOutTags)) { + continue; + } + + $paramOutTags[$name] = self::resolveTemplateTypeInTag( + $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), + $phpDocBlock, + TemplateTypeVariance::createCovariant(), + ); + } + + return $paramOutTags; + } + + /** + * @param array $paramsImmediatelyInvokedCallable + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamsImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramsImmediatelyInvokedCallable = self::mergeOneParentParamImmediatelyInvokedCallable($paramsImmediatelyInvokedCallable, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramsImmediatelyInvokedCallable; + } + + /** + * @param array $paramsImmediatelyInvokedCallable + * @return array + */ + private static function mergeOneParentParamImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentImmediatelyInvokedCallable = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamsImmediatelyInvokedCallable()); + + foreach ($parentImmediatelyInvokedCallable as $name => $parentIsImmediatelyInvokedCallable) { + if (array_key_exists($name, $paramsImmediatelyInvokedCallable)) { + continue; + } + + $paramsImmediatelyInvokedCallable[$name] = $parentIsImmediatelyInvokedCallable; + } + + return $paramsImmediatelyInvokedCallable; + } + + /** + * @param array $paramsClosureThisTags + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamClosureThisTags(array $paramsClosureThisTags, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramsClosureThisTags = self::mergeOneParentParamClosureThisTag($paramsClosureThisTags, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramsClosureThisTags; + } + + /** + * @param array $paramsClosureThisTags + * @return array + */ + private static function mergeOneParentParamClosureThisTag(array $paramsClosureThisTags, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentClosureThisTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamClosureThisTags()); + + foreach ($parentClosureThisTags as $name => $parentParamClosureThisTag) { + if (array_key_exists($name, $paramsClosureThisTags)) { + continue; + } + + $paramsClosureThisTags[$name] = self::resolveTemplateTypeInTag( + $parentParamClosureThisTag->withType( + $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamClosureThisTag->getType()), + ), + $phpDocBlock, + TemplateTypeVariance::createContravariant(), + ); + } + + return $paramsClosureThisTags; + } + + /** + * @param array $parents + */ + private static function mergePureTags(?bool $isPure, array $parents): ?bool + { + if ($isPure !== null) { + return $isPure; + } + + foreach ($parents as $parent) { + $parentIsPure = $parent->isPure(); + if ($parentIsPure === null) { + continue; + } + + return $parentIsPure; + } + + return null; + } + /** * @template T of TypedTag * @param T $tag * @return T */ - private static function resolveTemplateTypeInTag(TypedTag $tag, PhpDocBlock $phpDocBlock): TypedTag + private static function resolveTemplateTypeInTag( + TypedTag $tag, + PhpDocBlock $phpDocBlock, + TemplateTypeVariance $positionVariance, + ): TypedTag { $type = TemplateTypeHelper::resolveTemplateTypes( $tag->getType(), $phpDocBlock->getClassReflection()->getActiveTemplateTypeMap(), + $phpDocBlock->getClassReflection()->getCallSiteVarianceMap(), + $positionVariance, ); return $tag->withType($type); } diff --git a/src/PhpDoc/SocketSelectStubFilesExtension.php b/src/PhpDoc/SocketSelectStubFilesExtension.php new file mode 100644 index 0000000000..c5d60ea909 --- /dev/null +++ b/src/PhpDoc/SocketSelectStubFilesExtension.php @@ -0,0 +1,23 @@ +phpVersion->getVersionId() >= 80000) { + return [__DIR__ . '/../../stubs/socket_select_php8.stub']; + } + + return [__DIR__ . '/../../stubs/socket_select.stub']; + } + +} diff --git a/src/PhpDoc/StubFilesExtension.php b/src/PhpDoc/StubFilesExtension.php index 237f2a04d3..91267fa583 100644 --- a/src/PhpDoc/StubFilesExtension.php +++ b/src/PhpDoc/StubFilesExtension.php @@ -2,7 +2,22 @@ namespace PHPStan\PhpDoc; -/** @api */ +/** + * This is the extension interface to implement if you want to dynamically + * load stub files based on your logic. As opposed to simply list them in the configuration file. + * + * To register it in the configuration file use the `phpstan.stubFilesExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.stubFilesExtension + * ``` + * + * @api + */ interface StubFilesExtension { diff --git a/src/PhpDoc/StubFilesProvider.php b/src/PhpDoc/StubFilesProvider.php new file mode 100644 index 0000000000..c0e9be78d6 --- /dev/null +++ b/src/PhpDoc/StubFilesProvider.php @@ -0,0 +1,14 @@ + */ @@ -58,14 +57,10 @@ class StubPhpDocProvider /** @var array> */ private array $knownFunctionParameterNames = []; - /** - * @param string[] $stubFiles - */ public function __construct( private Parser $parser, private FileTypeMapper $fileTypeMapper, - private Container $container, - private array $stubFiles, + private StubFilesProvider $stubFilesProvider, ) { } @@ -151,7 +146,12 @@ public function findClassConstantPhpDoc(string $className, string $constantName) /** * @param array $positionalParameterNames */ - public function findMethodPhpDoc(string $className, string $methodName, array $positionalParameterNames): ?ResolvedPhpDocBlock + public function findMethodPhpDoc( + string $className, + string $implementingClassName, + string $methodName, + array $positionalParameterNames, + ): ?ResolvedPhpDocBlock { if (!$this->isKnownClass($className)) { return null; @@ -175,6 +175,12 @@ public function findMethodPhpDoc(string $className, string $methodName, array $p throw new ShouldNotHappenException(); } + if ($className !== $implementingClassName && $resolvedPhpDoc->getNullableNameScope() !== null) { + $resolvedPhpDoc = $resolvedPhpDoc->withNameScope( + $resolvedPhpDoc->getNullableNameScope()->withClassName($implementingClassName), + ); + } + $methodParameterNames = $this->knownMethodsParameterNames[$className][$methodName]; $parameterNameMapping = []; foreach ($positionalParameterNames as $i => $parameterName) { @@ -269,7 +275,7 @@ private function initializeKnownElements(): void $this->initializing = true; try { - foreach ($this->getStubFiles() as $stubFile) { + foreach ($this->stubFilesProvider->getStubFiles() as $stubFile) { $nodes = $this->parser->parseFile($stubFile); foreach ($nodes as $node) { $this->initializeKnownElementNode($stubFile, $node); @@ -281,23 +287,6 @@ 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_) { diff --git a/src/PhpDoc/StubSourceLocatorFactory.php b/src/PhpDoc/StubSourceLocatorFactory.php index f4253c14cc..6eca6b0bab 100644 --- a/src/PhpDoc/StubSourceLocatorFactory.php +++ b/src/PhpDoc/StubSourceLocatorFactory.php @@ -6,22 +6,23 @@ use PHPStan\BetterReflection\SourceLocator\Ast\Locator; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator; +use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping; use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedPsrAutoloaderLocatorFactory; use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository; +use function dirname; -class StubSourceLocatorFactory +final class StubSourceLocatorFactory { - /** - * @param string[] $stubFiles - */ public function __construct( private Parser $php8Parser, private PhpStormStubsSourceStubber $phpStormStubsSourceStubber, private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, - private array $stubFiles, + private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, + private StubFilesProvider $stubFilesProvider, ) { } @@ -30,10 +31,21 @@ public function create(): SourceLocator { $locators = []; $astPhp8Locator = new Locator($this->php8Parser); - foreach ($this->stubFiles as $stubFile) { + foreach ($this->stubFilesProvider->getStubFiles() as $stubFile) { $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($stubFile); } + $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( + Psr4Mapping::fromArrayMappings([ + 'PHPStan\\' => [dirname(__DIR__) . '/'], + ]), + ); + $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( + Psr4Mapping::fromArrayMappings([ + 'PhpParser\\' => [dirname(__DIR__, 2) . '/vendor/nikic/php-parser/lib/PhpParser/'], + ]), + ); + $locators[] = new PhpInternalSourceLocator($astPhp8Locator, $this->phpStormStubsSourceStubber); return new MemoizingSourceLocator(new AggregateSourceLocator($locators)); diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 5bbf713a50..b0737cbcc9 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -4,26 +4,51 @@ use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\InternalError; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Broker\Broker; +use PHPStan\Collectors\Registry as CollectorRegistry; 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\Php\PhpClassReflectionExtension; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\Classes\DuplicateClassDeclarationRule; +use PHPStan\Rules\Classes\DuplicateDeclarationRule; use PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule; use PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule; use PHPStan\Rules\Classes\ExistingClassInClassExtendsRule; use PHPStan\Rules\Classes\ExistingClassInTraitUseRule; +use PHPStan\Rules\Classes\LocalTypeAliasesCheck; +use PHPStan\Rules\Classes\LocalTypeAliasesRule; +use PHPStan\Rules\Classes\LocalTypeTraitAliasesRule; +use PHPStan\Rules\Classes\LocalTypeTraitUseAliasesRule; +use PHPStan\Rules\Classes\MethodTagCheck; +use PHPStan\Rules\Classes\MethodTagRule; +use PHPStan\Rules\Classes\MethodTagTraitRule; +use PHPStan\Rules\Classes\MethodTagTraitUseRule; +use PHPStan\Rules\Classes\MixinCheck; +use PHPStan\Rules\Classes\MixinRule; +use PHPStan\Rules\Classes\MixinTraitRule; +use PHPStan\Rules\Classes\MixinTraitUseRule; +use PHPStan\Rules\Classes\PropertyTagCheck; +use PHPStan\Rules\Classes\PropertyTagRule; +use PHPStan\Rules\Classes\PropertyTagTraitRule; +use PHPStan\Rules\Classes\PropertyTagTraitUseRule; +use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Functions\DuplicateFunctionDeclarationRule; use PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule; use PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule; use PHPStan\Rules\Generics\ClassAncestorsRule; use PHPStan\Rules\Generics\ClassTemplateTypeRule; use PHPStan\Rules\Generics\CrossCheckInterfacesHelper; +use PHPStan\Rules\Generics\EnumAncestorsRule; +use PHPStan\Rules\Generics\EnumTemplateTypeRule; use PHPStan\Rules\Generics\FunctionSignatureVarianceRule; use PHPStan\Rules\Generics\FunctionTemplateTypeRule; use PHPStan\Rules\Generics\GenericAncestorsCheck; @@ -31,24 +56,44 @@ use PHPStan\Rules\Generics\InterfaceAncestorsRule; use PHPStan\Rules\Generics\InterfaceTemplateTypeRule; use PHPStan\Rules\Generics\MethodSignatureVarianceRule; +use PHPStan\Rules\Generics\MethodTagTemplateTypeCheck; +use PHPStan\Rules\Generics\MethodTagTemplateTypeRule; +use PHPStan\Rules\Generics\MethodTagTemplateTypeTraitRule; use PHPStan\Rules\Generics\MethodTemplateTypeRule; +use PHPStan\Rules\Generics\PropertyVarianceRule; use PHPStan\Rules\Generics\TemplateTypeCheck; use PHPStan\Rules\Generics\TraitTemplateTypeRule; +use PHPStan\Rules\Generics\UsedTraitsRule; use PHPStan\Rules\Generics\VarianceCheck; use PHPStan\Rules\Methods\ExistingClassesInTypehintsRule; +use PHPStan\Rules\Methods\MethodParameterComparisonHelper; use PHPStan\Rules\Methods\MethodSignatureRule; +use PHPStan\Rules\Methods\MethodVisibilityComparisonHelper; use PHPStan\Rules\Methods\MissingMethodParameterTypehintRule; use PHPStan\Rules\Methods\MissingMethodReturnTypehintRule; +use PHPStan\Rules\Methods\MissingMethodSelfOutTypeRule; use PHPStan\Rules\Methods\OverridingMethodRule; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\PhpDoc\AssertRuleHelper; +use PHPStan\Rules\PhpDoc\ConditionalReturnTypeRuleHelper; +use PHPStan\Rules\PhpDoc\FunctionAssertRule; +use PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule; +use PHPStan\Rules\PhpDoc\GenericCallableRuleHelper; +use PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule; +use PHPStan\Rules\PhpDoc\IncompatibleParamImmediatelyInvokedCallableRule; +use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeCheck; use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule; use PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule; +use PHPStan\Rules\PhpDoc\IncompatibleSelfOutTypeRule; use PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule; +use PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule; use PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule; +use PHPStan\Rules\PhpDoc\MethodAssertRule; +use PHPStan\Rules\PhpDoc\MethodConditionalReturnTypeRule; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\ExistingClassesInPropertiesRule; use PHPStan\Rules\Properties\MissingPropertyTypehintRule; -use PHPStan\Rules\Registry; +use PHPStan\Rules\Registry as RuleRegistry; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\ObjectType; use Throwable; @@ -56,7 +101,7 @@ use function count; use function sprintf; -class StubValidator +final class StubValidator { public function __construct( @@ -67,7 +112,7 @@ public function __construct( /** * @param string[] $stubFiles - * @return Error[] + * @return list */ public function validate(array $stubFiles, bool $debug): array { @@ -75,18 +120,17 @@ public function validate(array $stubFiles, bool $debug): array return []; } - $originalBroker = Broker::getInstance(); $originalReflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + $originalPhpVersion = PhpVersionStaticAccessor::getInstance(); $container = $this->derivativeContainerFactory->create([ __DIR__ . '/../../conf/config.stubValidator.neon', ]); $ruleRegistry = $this->getRuleRegistry($container); + $collectorRegistry = $this->getCollectorRegistry($container); - /** @var FileAnalyser $fileAnalyser */ $fileAnalyser = $container->getByType(FileAnalyser::class); - /** @var NodeScopeResolver $nodeScopeResolver */ $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); $nodeScopeResolver->setAnalysedFiles($stubFiles); @@ -102,6 +146,7 @@ public function validate(array $stubFiles, bool $debug): array $stubFile, $analysedFiles, $ruleRegistry, + $collectorRegistry, static function (): void { }, )->getErrors(); @@ -114,18 +159,23 @@ static function (): void { } $internalErrorMessage = sprintf('Internal error: %s', $e->getMessage()); - $errors[] = new Error($internalErrorMessage, $stubFile, null, $e); + $errors[] = (new Error($internalErrorMessage, $stubFile, null, $e)) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); } } - Broker::registerInstance($originalBroker); ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); + PhpVersionStaticAccessor::registerInstance($originalPhpVersion); ObjectType::resetCaches(); return $errors; } - private function getRuleRegistry(Container $container): Registry + private function getRuleRegistry(Container $container): RuleRegistry { $fileTypeMapper = $container->getByType(FileTypeMapper::class); $genericObjectTypeCheck = $container->getByType(GenericObjectTypeCheck::class); @@ -133,23 +183,47 @@ private function getRuleRegistry(Container $container): Registry $templateTypeCheck = $container->getByType(TemplateTypeCheck::class); $varianceCheck = $container->getByType(VarianceCheck::class); $reflectionProvider = $container->getByType(ReflectionProvider::class); - $classCaseSensitivityCheck = $container->getByType(ClassCaseSensitivityCheck::class); + $classNameCheck = $container->getByType(ClassNameCheck::class); $functionDefinitionCheck = $container->getByType(FunctionDefinitionCheck::class); $missingTypehintCheck = $container->getByType(MissingTypehintCheck::class); $unresolvableTypeHelper = $container->getByType(UnresolvableTypeHelper::class); $crossCheckInterfacesHelper = $container->getByType(CrossCheckInterfacesHelper::class); $phpVersion = $container->getByType(PhpVersion::class); + $localTypeAliasesCheck = $container->getByType(LocalTypeAliasesCheck::class); + $phpClassReflectionExtension = $container->getByType(PhpClassReflectionExtension::class); + $genericCallableRuleHelper = $container->getByType(GenericCallableRuleHelper::class); + $methodTagTemplateTypeCheck = $container->getByType(MethodTagTemplateTypeCheck::class); + $mixinCheck = $container->getByType(MixinCheck::class); + $discoveringSymbolsTip = $container->getParameter('tips')['discoveringSymbols']; + $methodTagCheck = new MethodTagCheck($reflectionProvider, $classNameCheck, $genericObjectTypeCheck, $missingTypehintCheck, $unresolvableTypeHelper, true, true, $discoveringSymbolsTip); + $propertyTagCheck = new PropertyTagCheck($reflectionProvider, $classNameCheck, $genericObjectTypeCheck, $missingTypehintCheck, $unresolvableTypeHelper, true, true, $discoveringSymbolsTip); + $reflector = $container->getService('stubReflector'); + $relativePathHelper = $container->getService('simpleRelativePathHelper'); + $assertRuleHelper = $container->getByType(AssertRuleHelper::class); + $conditionalReturnTypeRuleHelper = $container->getByType(ConditionalReturnTypeRuleHelper::class); $rules = [ // level 0 - new ExistingClassesInClassImplementsRule($classCaseSensitivityCheck, $reflectionProvider), - new ExistingClassesInInterfaceExtendsRule($classCaseSensitivityCheck, $reflectionProvider), - new ExistingClassInClassExtendsRule($classCaseSensitivityCheck, $reflectionProvider), - new ExistingClassInTraitUseRule($classCaseSensitivityCheck, $reflectionProvider), + new ExistingClassesInClassImplementsRule($classNameCheck, $reflectionProvider, $discoveringSymbolsTip), + new ExistingClassesInInterfaceExtendsRule($classNameCheck, $reflectionProvider, $discoveringSymbolsTip), + new ExistingClassInClassExtendsRule($classNameCheck, $reflectionProvider, $discoveringSymbolsTip), + new ExistingClassInTraitUseRule($classNameCheck, $reflectionProvider, $discoveringSymbolsTip), new ExistingClassesInTypehintsRule($functionDefinitionCheck), new \PHPStan\Rules\Functions\ExistingClassesInTypehintsRule($functionDefinitionCheck), - new ExistingClassesInPropertiesRule($reflectionProvider, $classCaseSensitivityCheck, $unresolvableTypeHelper, $phpVersion, true, false), - new OverridingMethodRule($phpVersion, new MethodSignatureRule(true, true), true), + new ExistingClassesInPropertiesRule($reflectionProvider, $classNameCheck, $unresolvableTypeHelper, $phpVersion, true, false, $discoveringSymbolsTip), + new OverridingMethodRule( + $phpVersion, + new MethodSignatureRule($phpClassReflectionExtension, true, true), + true, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + $phpClassReflectionExtension, + $container->getParameter('checkMissingOverrideMethodAttribute'), + ), + new DuplicateDeclarationRule(), + new LocalTypeAliasesRule($localTypeAliasesCheck), + new LocalTypeTraitAliasesRule($localTypeAliasesCheck, $reflectionProvider), + new LocalTypeTraitUseAliasesRule($localTypeAliasesCheck), // level 2 new ClassAncestorsRule($genericAncestorsCheck, $crossCheckInterfacesHelper), @@ -159,19 +233,41 @@ private function getRuleRegistry(Container $container): Registry new InterfaceAncestorsRule($genericAncestorsCheck, $crossCheckInterfacesHelper), new InterfaceTemplateTypeRule($templateTypeCheck), new MethodTemplateTypeRule($fileTypeMapper, $templateTypeCheck), + new MethodTagTemplateTypeRule($methodTagTemplateTypeCheck), new MethodSignatureVarianceRule($varianceCheck), new TraitTemplateTypeRule($fileTypeMapper, $templateTypeCheck), - new IncompatiblePhpDocTypeRule( - $fileTypeMapper, - $genericObjectTypeCheck, - $unresolvableTypeHelper, - ), - new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper), + new IncompatiblePhpDocTypeRule($fileTypeMapper, new IncompatiblePhpDocTypeCheck($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper)), + new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), new InvalidPhpDocTagValueRule( $container->getByType(Lexer::class), $container->getByType(PhpDocParser::class), ), + new IncompatibleParamImmediatelyInvokedCallableRule($fileTypeMapper), + new IncompatibleSelfOutTypeRule($unresolvableTypeHelper, $genericObjectTypeCheck), + new IncompatibleClassConstantPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper), + new InvalidPHPStanDocTagRule( + $container->getByType(Lexer::class), + $container->getByType(PhpDocParser::class), + ), new InvalidThrowsPhpDocValueRule($fileTypeMapper), + new MixinTraitRule($mixinCheck, $reflectionProvider), + new MixinRule($mixinCheck), + new MixinTraitUseRule($mixinCheck), + new MethodTagRule($methodTagCheck), + new MethodTagTraitRule($methodTagCheck, $reflectionProvider), + new MethodTagTraitUseRule($methodTagCheck), + new MethodTagTemplateTypeTraitRule($methodTagTemplateTypeCheck, $reflectionProvider), + new PropertyTagRule($propertyTagCheck), + new PropertyTagTraitRule($propertyTagCheck, $reflectionProvider), + new PropertyTagTraitUseRule($propertyTagCheck), + new EnumAncestorsRule($genericAncestorsCheck, $crossCheckInterfacesHelper), + new EnumTemplateTypeRule(), + new PropertyVarianceRule($varianceCheck), + new UsedTraitsRule($fileTypeMapper, $genericAncestorsCheck), + new FunctionAssertRule($assertRuleHelper), + new MethodAssertRule($assertRuleHelper), + new FunctionConditionalReturnTypeRule($conditionalReturnTypeRuleHelper), + new MethodConditionalReturnTypeRule($conditionalReturnTypeRuleHelper), // level 6 new MissingFunctionParameterTypehintRule($missingTypehintCheck), @@ -179,9 +275,19 @@ private function getRuleRegistry(Container $container): Registry new MissingMethodParameterTypehintRule($missingTypehintCheck), new MissingMethodReturnTypehintRule($missingTypehintCheck), new MissingPropertyTypehintRule($missingTypehintCheck), + new MissingMethodSelfOutTypeRule($missingTypehintCheck), + + // duplicate stubs + new DuplicateClassDeclarationRule($reflector, $relativePathHelper), + new DuplicateFunctionDeclarationRule($reflector, $relativePathHelper), ]; - return new Registry($rules); + return new DirectRuleRegistry($rules); + } + + private function getCollectorRegistry(Container $container): CollectorRegistry + { + return new CollectorRegistry([]); } } diff --git a/src/PhpDoc/Tag/AssertTag.php b/src/PhpDoc/Tag/AssertTag.php new file mode 100644 index 0000000000..301b5b4876 --- /dev/null +++ b/src/PhpDoc/Tag/AssertTag.php @@ -0,0 +1,96 @@ +if; + } + + public function getType(): Type + { + return $this->type; + } + + public function getOriginalType(): Type + { + return $this->originalType ??= $this->type; + } + + public function getParameter(): AssertTagParameter + { + return $this->parameter; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function isEquality(): bool + { + return $this->equality; + } + + /** + * @return static + */ + public function withType(Type $type): TypedTag + { + $tag = new self($this->if, $type, $this->parameter, $this->negated, $this->equality, $this->isExplicit); + $tag->originalType = $this->getOriginalType(); + return $tag; + } + + public function withParameter(AssertTagParameter $parameter): self + { + $tag = new self($this->if, $this->type, $parameter, $this->negated, $this->equality, $this->isExplicit); + $tag->originalType = $this->getOriginalType(); + return $tag; + } + + public function negate(): self + { + if ($this->isEquality()) { + throw new ShouldNotHappenException(); + } + + $tag = new self($this->if, $this->type, $this->parameter, !$this->negated, $this->equality, $this->isExplicit); + $tag->originalType = $this->getOriginalType(); + return $tag; + } + + public function isExplicit(): bool + { + return $this->isExplicit; + } + + public function toImplicit(): self + { + return new self($this->if, $this->type, $this->parameter, $this->negated, $this->equality, false); + } + +} diff --git a/src/PhpDoc/Tag/AssertTagParameter.php b/src/PhpDoc/Tag/AssertTagParameter.php new file mode 100644 index 0000000000..a3c3250326 --- /dev/null +++ b/src/PhpDoc/Tag/AssertTagParameter.php @@ -0,0 +1,59 @@ +parameterName; + } + + public function changeParameterName(string $parameterName): self + { + return new self( + $parameterName, + $this->property, + $this->method, + ); + } + + public function describe(): string + { + if ($this->property !== null) { + return sprintf('%s->%s', $this->parameterName, $this->property); + } + + if ($this->method !== null) { + return sprintf('%s->%s()', $this->parameterName, $this->method); + } + + return $this->parameterName; + } + + public function getExpr(Expr $parameter): Expr + { + if ($this->property !== null) { + return new Expr\PropertyFetch($parameter, $this->property); + } + + if ($this->method !== null) { + return new Expr\MethodCall($parameter, $this->method); + } + + return $parameter; + } + +} diff --git a/src/PhpDoc/Tag/DeprecatedTag.php b/src/PhpDoc/Tag/DeprecatedTag.php index ea798c2aab..9bc036e1d8 100644 --- a/src/PhpDoc/Tag/DeprecatedTag.php +++ b/src/PhpDoc/Tag/DeprecatedTag.php @@ -2,8 +2,10 @@ namespace PHPStan\PhpDoc\Tag; -/** @api */ -class DeprecatedTag +/** + * @api + */ +final class DeprecatedTag { public function __construct(private ?string $message) diff --git a/src/PhpDoc/Tag/ExtendsTag.php b/src/PhpDoc/Tag/ExtendsTag.php index 74ed32b7a2..72cb97f7cf 100644 --- a/src/PhpDoc/Tag/ExtendsTag.php +++ b/src/PhpDoc/Tag/ExtendsTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ExtendsTag +/** + * @api + */ +final class ExtendsTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/ImplementsTag.php b/src/PhpDoc/Tag/ImplementsTag.php index cc9376b47a..556959b68d 100644 --- a/src/PhpDoc/Tag/ImplementsTag.php +++ b/src/PhpDoc/Tag/ImplementsTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ImplementsTag +/** + * @api + */ +final class ImplementsTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/MethodTag.php b/src/PhpDoc/Tag/MethodTag.php index e640418ea8..43bda4cf97 100644 --- a/src/PhpDoc/Tag/MethodTag.php +++ b/src/PhpDoc/Tag/MethodTag.php @@ -4,17 +4,21 @@ use PHPStan\Type\Type; -/** @api */ -class MethodTag +/** + * @api + */ +final class MethodTag { /** * @param array $parameters + * @param array $templateTags */ public function __construct( private Type $returnType, private bool $isStatic, private array $parameters, + private array $templateTags, ) { } @@ -37,4 +41,12 @@ public function getParameters(): array return $this->parameters; } + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + } diff --git a/src/PhpDoc/Tag/MethodTagParameter.php b/src/PhpDoc/Tag/MethodTagParameter.php index 1326c4cbc9..3e4c817bf8 100644 --- a/src/PhpDoc/Tag/MethodTagParameter.php +++ b/src/PhpDoc/Tag/MethodTagParameter.php @@ -5,8 +5,10 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; -/** @api */ -class MethodTagParameter +/** + * @api + */ +final class MethodTagParameter { public function __construct( diff --git a/src/PhpDoc/Tag/MixinTag.php b/src/PhpDoc/Tag/MixinTag.php index 2a97b73264..c115c2cacb 100644 --- a/src/PhpDoc/Tag/MixinTag.php +++ b/src/PhpDoc/Tag/MixinTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class MixinTag +/** + * @api + */ +final class MixinTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/ParamClosureThisTag.php b/src/PhpDoc/Tag/ParamClosureThisTag.php new file mode 100644 index 0000000000..92a91a4da8 --- /dev/null +++ b/src/PhpDoc/Tag/ParamClosureThisTag.php @@ -0,0 +1,29 @@ +type; + } + + public function withType(Type $type): self + { + return new self($type); + } + +} diff --git a/src/PhpDoc/Tag/ParamOutTag.php b/src/PhpDoc/Tag/ParamOutTag.php new file mode 100644 index 0000000000..f720897deb --- /dev/null +++ b/src/PhpDoc/Tag/ParamOutTag.php @@ -0,0 +1,27 @@ +type; + } + + public function withType(Type $type): self + { + return new self($type); + } + +} diff --git a/src/PhpDoc/Tag/ParamTag.php b/src/PhpDoc/Tag/ParamTag.php index 651064cec1..498dd64ce7 100644 --- a/src/PhpDoc/Tag/ParamTag.php +++ b/src/PhpDoc/Tag/ParamTag.php @@ -4,11 +4,16 @@ use PHPStan\Type\Type; -/** @api */ -class ParamTag implements TypedTag +/** + * @api + */ +final class ParamTag implements TypedTag { - public function __construct(private Type $type, private bool $isVariadic) + public function __construct( + private Type $type, + private bool $isVariadic, + ) { } @@ -22,10 +27,7 @@ public function isVariadic(): bool return $this->isVariadic; } - /** - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type, $this->isVariadic); } diff --git a/src/PhpDoc/Tag/PropertyTag.php b/src/PhpDoc/Tag/PropertyTag.php index b204ce4bb3..16090c44b0 100644 --- a/src/PhpDoc/Tag/PropertyTag.php +++ b/src/PhpDoc/Tag/PropertyTag.php @@ -4,31 +4,43 @@ use PHPStan\Type\Type; -/** @api */ -class PropertyTag +/** + * @api + */ +final class PropertyTag { public function __construct( - private Type $type, - private bool $readable, - private bool $writable, + private ?Type $readableType, + private ?Type $writableType, ) { } - public function getType(): Type + public function getReadableType(): ?Type { - return $this->type; + return $this->readableType; } + public function getWritableType(): ?Type + { + return $this->writableType; + } + + /** + * @phpstan-assert-if-true !null $this->getReadableType() + */ public function isReadable(): bool { - return $this->readable; + return $this->readableType !== null; } + /** + * @phpstan-assert-if-true !null $this->getWritableType() + */ public function isWritable(): bool { - return $this->writable; + return $this->writableType !== null; } } diff --git a/src/PhpDoc/Tag/RequireExtendsTag.php b/src/PhpDoc/Tag/RequireExtendsTag.php new file mode 100644 index 0000000000..97bf685468 --- /dev/null +++ b/src/PhpDoc/Tag/RequireExtendsTag.php @@ -0,0 +1,22 @@ +type; + } + +} diff --git a/src/PhpDoc/Tag/RequireImplementsTag.php b/src/PhpDoc/Tag/RequireImplementsTag.php new file mode 100644 index 0000000000..aafd560260 --- /dev/null +++ b/src/PhpDoc/Tag/RequireImplementsTag.php @@ -0,0 +1,22 @@ +type; + } + +} diff --git a/src/PhpDoc/Tag/ReturnTag.php b/src/PhpDoc/Tag/ReturnTag.php index 683b0cb259..b501dd67e1 100644 --- a/src/PhpDoc/Tag/ReturnTag.php +++ b/src/PhpDoc/Tag/ReturnTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ReturnTag implements TypedTag +/** + * @api + */ +final class ReturnTag implements TypedTag { public function __construct(private Type $type, private bool $isExplicit) @@ -22,10 +24,7 @@ public function isExplicit(): bool return $this->isExplicit; } - /** - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type, $this->isExplicit); } diff --git a/src/PhpDoc/Tag/SelfOutTypeTag.php b/src/PhpDoc/Tag/SelfOutTypeTag.php new file mode 100644 index 0000000000..10bb054179 --- /dev/null +++ b/src/PhpDoc/Tag/SelfOutTypeTag.php @@ -0,0 +1,27 @@ +type; + } + + public function withType(Type $type): self + { + return new self($type); + } + +} diff --git a/src/PhpDoc/Tag/TemplateTag.php b/src/PhpDoc/Tag/TemplateTag.php index 4a2180c052..bafa555833 100644 --- a/src/PhpDoc/Tag/TemplateTag.php +++ b/src/PhpDoc/Tag/TemplateTag.php @@ -5,14 +5,22 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; -/** @api */ -class TemplateTag +/** + * @api + */ +final class TemplateTag { - public function __construct(private string $name, private Type $bound, private TemplateTypeVariance $variance) + /** + * @param non-empty-string $name + */ + public function __construct(private string $name, private Type $bound, private ?Type $default, private TemplateTypeVariance $variance) { } + /** + * @return non-empty-string + */ public function getName(): string { return $this->name; @@ -23,6 +31,11 @@ public function getBound(): Type return $this->bound; } + public function getDefault(): ?Type + { + return $this->default; + } + public function getVariance(): TemplateTypeVariance { return $this->variance; diff --git a/src/PhpDoc/Tag/ThrowsTag.php b/src/PhpDoc/Tag/ThrowsTag.php index 15c1ac94d9..1c1e30b897 100644 --- a/src/PhpDoc/Tag/ThrowsTag.php +++ b/src/PhpDoc/Tag/ThrowsTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class ThrowsTag +/** + * @api + */ +final class ThrowsTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php index 35b613417a..d5cd10e5d6 100644 --- a/src/PhpDoc/Tag/TypeAliasTag.php +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -6,8 +6,10 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\TypeAlias; -/** @api */ -class TypeAliasTag +/** + * @api + */ +final class TypeAliasTag { public function __construct( diff --git a/src/PhpDoc/Tag/UsesTag.php b/src/PhpDoc/Tag/UsesTag.php index 453eb5b250..1679997ed3 100644 --- a/src/PhpDoc/Tag/UsesTag.php +++ b/src/PhpDoc/Tag/UsesTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class UsesTag +/** + * @api + */ +final class UsesTag { public function __construct(private Type $type) diff --git a/src/PhpDoc/Tag/VarTag.php b/src/PhpDoc/Tag/VarTag.php index 672cb81d43..85c26f1b6c 100644 --- a/src/PhpDoc/Tag/VarTag.php +++ b/src/PhpDoc/Tag/VarTag.php @@ -4,8 +4,10 @@ use PHPStan\Type\Type; -/** @api */ -class VarTag implements TypedTag +/** + * @api + */ +final class VarTag implements TypedTag { public function __construct(private Type $type) @@ -17,10 +19,7 @@ public function getType(): Type return $this->type; } - /** - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type); } diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 318697ac4c..6abb943d73 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -7,7 +7,10 @@ use Iterator; use IteratorAggregate; use Nette\Utils\Strings; +use PhpParser\Node\Name; +use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -20,21 +23,34 @@ use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; +use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode; +use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; +use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; +use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -42,28 +58,44 @@ use PHPStan\Type\CallableType; use PHPStan\Type\ClassStringType; use PHPStan\Type\ClosureType; +use PHPStan\Type\ConditionalType; +use PHPStan\Type\ConditionalTypeForParameter; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; +use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Helper\GetTemplateTypeType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IterableType; +use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; +use PHPStan\Type\NewObjectType; +use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\OffsetAccessType; use PHPStan\Type\ResourceType; use PHPStan\Type\StaticType; +use PHPStan\Type\StaticTypeFactory; +use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType; use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; @@ -71,27 +103,39 @@ use PHPStan\Type\TypeAliasResolverProvider; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; +use PHPStan\Type\ValueOfType; use PHPStan\Type\VoidType; use Traversable; +use function array_key_exists; use function array_map; +use function array_values; use function count; +use function explode; use function get_class; use function in_array; +use function max; +use function min; +use function preg_match; use function preg_quote; +use function str_contains; use function str_replace; -use function strpos; +use function str_starts_with; use function strtolower; use function substr; -class TypeNodeResolver +final class TypeNodeResolver { + /** @var array */ + private array $genericTypeResolvingStack = []; + public function __construct( private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private TypeAliasResolverProvider $typeAliasResolverProvider, + private ConstantResolver $constantResolver, + private InitializerExprTypeResolver $initializerExprTypeResolver, ) { } @@ -121,6 +165,12 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type } elseif ($typeNode instanceof IntersectionTypeNode) { return $this->resolveIntersectionTypeNode($typeNode, $nameScope); + } elseif ($typeNode instanceof ConditionalTypeNode) { + return $this->resolveConditionalTypeNode($typeNode, $nameScope); + + } elseif ($typeNode instanceof ConditionalTypeForParameterNode) { + return $this->resolveConditionalTypeForParameterNode($typeNode, $nameScope); + } elseif ($typeNode instanceof ArrayTypeNode) { return $this->resolveArrayTypeNode($typeNode, $nameScope); @@ -132,8 +182,14 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type } elseif ($typeNode instanceof ArrayShapeNode) { return $this->resolveArrayShapeNode($typeNode, $nameScope); + } elseif ($typeNode instanceof ObjectShapeNode) { + return $this->resolveObjectShapeNode($typeNode, $nameScope); } elseif ($typeNode instanceof ConstTypeNode) { return $this->resolveConstTypeNode($typeNode, $nameScope); + } elseif ($typeNode instanceof OffsetAccessTypeNode) { + return $this->resolveOffsetAccessNode($typeNode, $nameScope); + } elseif ($typeNode instanceof InvalidTypeNode) { + return new MixedType(true); } return new ErrorType(); @@ -143,7 +199,15 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco { switch (strtolower($typeNode->name)) { case 'int': + return new IntegerType(); + case 'integer': + $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); + + if ($type !== null) { + return $type; + } + return new IntegerType(); case 'positive-int': @@ -152,9 +216,27 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'negative-int': return IntegerRangeType::fromInterval(null, -1); + case 'non-positive-int': + return IntegerRangeType::fromInterval(null, 0); + + case 'non-negative-int': + return IntegerRangeType::fromInterval(0, null); + + case 'non-zero-int': + return new UnionType([ + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), + ]); + case 'string': return new StringType(); + case 'lowercase-string': + return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + + case 'uppercase-string': + return new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + case 'literal-string': return new IntersectionType([new StringType(), new AccessoryLiteralStringType()]); @@ -163,6 +245,9 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'trait-string': return new ClassStringType(); + case 'enum-string': + return new GenericClassStringType(new ObjectType('UnitEnum')); + case 'callable-string': return new IntersectionType([new StringType(), new CallableType()]); @@ -178,6 +263,18 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]); + case 'empty-scalar': + return TypeCombinator::intersect( + new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]), + StaticTypeFactory::falsey(), + ); + + case 'non-empty-scalar': + return TypeCombinator::remove( + new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]), + StaticTypeFactory::falsey(), + ); + case 'number': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); @@ -215,6 +312,34 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco new AccessoryNonEmptyStringType(), ]); + case 'non-empty-lowercase-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryLowercaseStringType(), + ]); + + case 'non-empty-uppercase-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryUppercaseStringType(), + ]); + + case 'truthy-string': + case 'non-falsy-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + + case 'non-empty-literal-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryLiteralStringType(), + ]); + case 'bool': return new BooleanType(); @@ -264,6 +389,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'callable': return new CallableType(); + case 'pure-callable': + return new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()); + + case 'pure-closure': + return ClosureType::createPure(); + case 'resource': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); @@ -273,37 +404,61 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new ResourceType(); + case 'open-resource': + case 'closed-resource': + return new ResourceType(); + case 'mixed': return new MixedType(true); + case 'non-empty-mixed': + return new MixedType(true, StaticTypeFactory::falsey()); + case 'void': return new VoidType(); case 'object': return new ObjectWithoutClassType(); + case 'callable-object': + return new IntersectionType([new ObjectWithoutClassType(), new CallableType()]); + + case 'callable-array': + return new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]); + case 'never': + case 'noreturn': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); if ($type !== null) { return $type; } - return new NeverType(true); + return new NonAcceptingNeverType(); case 'never-return': case 'never-returns': case 'no-return': - case 'noreturn': - return new NeverType(true); + return new NonAcceptingNeverType(); case 'list': - return new ArrayType(new IntegerType(), new MixedType()); + return TypeCombinator::intersect(new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), new AccessoryArrayListType()); case 'non-empty-list': return TypeCombinator::intersect( - new ArrayType(new IntegerType(), new MixedType()), + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), new NonEmptyArrayType(), + new AccessoryArrayListType(), ); + + case 'empty': + $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); + if ($type !== null) { + return $type; + } + + return StaticTypeFactory::falsey(); + case '__stringandstringable': + return new StringAlwaysAcceptingObjectWithToStringType(); } if ($nameScope->getClassName() !== null) { @@ -344,13 +499,38 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco } $stringName = $nameScope->resolveStringName($typeNode->name); - if (strpos($stringName, '-') !== false && strpos($stringName, 'OCI-') !== 0) { + if (str_contains($stringName, '-') && !str_starts_with($stringName, 'OCI-')) { return new ErrorType(); } + if ($this->mightBeConstant($typeNode->name) && !$this->getReflectionProvider()->hasClass($stringName)) { + $constType = $this->tryResolveConstant($typeNode->name, $nameScope); + if ($constType !== null) { + return $constType; + } + } + return new ObjectType($stringName); } + private function mightBeConstant(string $name): bool + { + return preg_match('((?:^|\\\\)[A-Z_][A-Z0-9_]*$)', $name) > 0; + } + + private function tryResolveConstant(string $name, NameScope $nameScope): ?Type + { + foreach ($nameScope->resolveConstantNames($name) as $constName) { + $nameNode = new Name\FullyQualified(explode('\\', $constName)); + $constType = $this->constantResolver->resolveConstant($nameNode, null); + if ($constType !== null) { + return $constType; + } + } + + return null; + } + private function tryResolvePseudoTypeClassType(IdentifierTypeNode $typeNode, NameScope $nameScope): ?Type { if ($nameScope->hasUseAlias($typeNode->name)) { @@ -384,7 +564,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 @@ -411,10 +591,12 @@ private function resolveUnionTypeNode(UnionTypeNode $typeNode, NameScope $nameSc continue; } - if ($type instanceof ObjectType) { + if ($type instanceof ObjectType && !$type instanceof GenericObjectType) { $type = new IntersectionType([$type, new IterableType(new MixedType(), $arrayTypeType)]); } elseif ($type instanceof ArrayType) { $type = new ArrayType(new MixedType(), $arrayTypeType); + } elseif ($type instanceof ConstantArrayType) { + $type = new ArrayType(new MixedType(), $arrayTypeType); } elseif ($type instanceof IterableType) { $type = new IterableType(new MixedType(), $arrayTypeType); } else { @@ -438,6 +620,28 @@ private function resolveIntersectionTypeNode(IntersectionTypeNode $typeNode, Nam return TypeCombinator::intersect(...$types); } + private function resolveConditionalTypeNode(ConditionalTypeNode $typeNode, NameScope $nameScope): Type + { + return new ConditionalType( + $this->resolve($typeNode->subjectType, $nameScope), + $this->resolve($typeNode->targetType, $nameScope), + $this->resolve($typeNode->if, $nameScope), + $this->resolve($typeNode->else, $nameScope), + $typeNode->negated, + ); + } + + private function resolveConditionalTypeForParameterNode(ConditionalTypeForParameterNode $typeNode, NameScope $nameScope): Type + { + return new ConditionalTypeForParameter( + $typeNode->parameterName, + $this->resolve($typeNode->targetType, $nameScope), + $this->resolve($typeNode->if, $nameScope), + $this->resolve($typeNode->else, $nameScope), + $typeNode->negated, + ); + } + private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type { $itemType = $this->resolve($typeNode->type, $nameScope); @@ -448,16 +652,41 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na { $mainTypeName = strtolower($typeNode->type->name); $genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope); + $variances = array_map( + static function (string $variance): TemplateTypeVariance { + switch ($variance) { + case GenericTypeNode::VARIANCE_INVARIANT: + return TemplateTypeVariance::createInvariant(); + case GenericTypeNode::VARIANCE_COVARIANT: + return TemplateTypeVariance::createCovariant(); + case GenericTypeNode::VARIANCE_CONTRAVARIANT: + return TemplateTypeVariance::createContravariant(); + case GenericTypeNode::VARIANCE_BIVARIANT: + return TemplateTypeVariance::createBivariant(); + } + }, + $typeNode->variances, + ); - if ($mainTypeName === 'array' || $mainTypeName === 'non-empty-array') { + if (in_array($mainTypeName, ['array', 'non-empty-array'], true)) { if (count($genericTypes) === 1) { // array $arrayType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $genericTypes[0]); } elseif (count($genericTypes) === 2) { // array - $keyType = TypeCombinator::intersect($genericTypes[0], new UnionType([ + $keyType = TypeCombinator::intersect($genericTypes[0]->toArrayKey(), new UnionType([ new IntegerType(), new StringType(), - ])); - $arrayType = new ArrayType(!$keyType instanceof NeverType ? ArrayType::castToArrayKeyType($keyType) : $keyType, $genericTypes[1]); + ]))->toArrayKey(); + $finiteTypes = $keyType->getFiniteTypes(); + if ( + count($finiteTypes) === 1 + && ($finiteTypes[0] instanceof ConstantStringType || $finiteTypes[0] instanceof ConstantIntegerType) + ) { + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $arrayBuilder->setOffsetValueType($finiteTypes[0], $genericTypes[1], true); + $arrayType = $arrayBuilder->getArray(); + } else { + $arrayType = new ArrayType($keyType, $genericTypes[1]); + } } else { return new ErrorType(); } @@ -467,9 +696,9 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na } return $arrayType; - } elseif ($mainTypeName === 'list' || $mainTypeName === 'non-empty-list') { + } elseif (in_array($mainTypeName, ['list', 'non-empty-list'], true)) { if (count($genericTypes) === 1) { // list - $listType = new ArrayType(new IntegerType(), $genericTypes[0]); + $listType = TypeCombinator::intersect(new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $genericTypes[0]), new AccessoryArrayListType()); if ($mainTypeName === 'non-empty-list') { return TypeCombinator::intersect($listType, new NonEmptyArrayType()); } @@ -490,11 +719,18 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na } elseif (in_array($mainTypeName, ['class-string', 'interface-string'], true)) { if (count($genericTypes) === 1) { $genericType = $genericTypes[0]; - if ((new ObjectWithoutClassType())->isSuperTypeOf($genericType)->yes() || $genericType instanceof MixedType) { + if ($genericType->isObject()->yes() || $genericType instanceof MixedType) { return new GenericClassStringType($genericType); } } + return new ErrorType(); + } elseif ($mainTypeName === 'enum-string') { + if (count($genericTypes) === 1) { + $genericType = $genericTypes[0]; + return new GenericClassStringType(TypeCombinator::intersect($genericType, new ObjectType('UnitEnum'))); + } + return new ErrorType(); } elseif ($mainTypeName === 'int') { if (count($genericTypes) === 2) { // int, int<1, 3> @@ -519,13 +755,34 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na } } elseif ($mainTypeName === 'key-of') { if (count($genericTypes) === 1) { // key-of - return $genericTypes[0]->getIterableKeyType(); + $type = new KeyOfType($genericTypes[0]); + return $type->isResolvable() ? $type->resolve() : $type; } return new ErrorType(); } elseif ($mainTypeName === 'value-of') { if (count($genericTypes) === 1) { // value-of - return $genericTypes[0]->getIterableValueType(); + $type = new ValueOfType($genericTypes[0]); + + return $type->isResolvable() ? $type->resolve() : $type; + } + + return new ErrorType(); + } elseif ($mainTypeName === 'int-mask-of') { + if (count($genericTypes) === 1) { // int-mask-of + $maskType = $this->expandIntMaskToType($genericTypes[0]); + if ($maskType !== null) { + return $maskType; + } + } + + return new ErrorType(); + } elseif ($mainTypeName === 'int-mask') { + if (count($genericTypes) > 0) { // int-mask<1, 2, 4> + $maskType = $this->expandIntMaskToType(TypeCombinator::union(...$genericTypes)); + if ($maskType !== null) { + return $maskType; + } } return new ErrorType(); @@ -533,90 +790,166 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na if (count($genericTypes) === 1) { return TypeUtils::toBenevolentUnion($genericTypes[0]); } + return new ErrorType(); + } elseif ($mainTypeName === 'template-type') { + if (count($genericTypes) === 3) { + $result = []; + /** @var class-string $ancestorClassName */ + foreach ($genericTypes[1]->getObjectClassNames() as $ancestorClassName) { + foreach ($genericTypes[2]->getConstantStrings() as $templateTypeName) { + $result[] = new GetTemplateTypeType($genericTypes[0], $ancestorClassName, $templateTypeName->getValue()); + } + } + + return TypeCombinator::union(...$result); + } + + return new ErrorType(); + } elseif ($mainTypeName === 'new') { + if (count($genericTypes) === 1) { + $type = new NewObjectType($genericTypes[0]); + return $type->isResolvable() ? $type->resolve() : $type; + } + + return new ErrorType(); + } elseif ($mainTypeName === 'static') { + if ($nameScope->getClassName() !== null && $this->getReflectionProvider()->hasClass($nameScope->getClassName())) { + $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); + + return new GenericStaticType($classReflection, $genericTypes, null, $variances); + } + return new ErrorType(); } $mainType = $this->resolveIdentifierTypeNode($typeNode->type, $nameScope); + $mainTypeObjectClassNames = $mainType->getObjectClassNames(); + if (count($mainTypeObjectClassNames) > 1) { + if ($mainType instanceof TemplateType) { + return new ErrorType(); + } + throw new ShouldNotHappenException(); + } + $mainTypeClassName = $mainTypeObjectClassNames[0] ?? null; - if ($mainType instanceof TypeWithClassName) { - if (!$this->getReflectionProvider()->hasClass($mainType->getClassName())) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + if ($mainTypeClassName !== null) { + if (!$this->getReflectionProvider()->hasClass($mainTypeClassName)) { + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } - $classReflection = $this->getReflectionProvider()->getClass($mainType->getClassName()); + $classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName); if ($classReflection->isGeneric()) { - if (in_array($mainType->getClassName(), [ + $templateTypes = array_values($classReflection->getTemplateTypeMap()->getTypes()); + for ($i = count($genericTypes), $templateTypesCount = count($templateTypes); $i < $templateTypesCount; $i++) { + $templateType = $templateTypes[$i]; + if (!$templateType instanceof TemplateType || $templateType->getDefault() === null) { + continue; + } + $genericTypes[] = $templateType->getDefault(); + } + + if (in_array($mainTypeClassName, [ Traversable::class, IteratorAggregate::class, Iterator::class, ], true)) { if (count($genericTypes) === 1) { - return new GenericObjectType($mainType->getClassName(), [ + return new GenericObjectType($mainTypeClassName, [ new MixedType(true), $genericTypes[0], + ], null, null, [ + TemplateTypeVariance::createInvariant(), + $variances[0], ]); } if (count($genericTypes) === 2) { - return new GenericObjectType($mainType->getClassName(), [ + return new GenericObjectType($mainTypeClassName, [ $genericTypes[0], $genericTypes[1], + ], null, null, [ + $variances[0], + $variances[1], ]); } } - if ($mainType->getClassName() === Generator::class) { + if ($mainTypeClassName === Generator::class) { if (count($genericTypes) === 1) { $mixed = new MixedType(true); - return new GenericObjectType($mainType->getClassName(), [ + return new GenericObjectType($mainTypeClassName, [ $mixed, $genericTypes[0], $mixed, $mixed, + ], null, null, [ + TemplateTypeVariance::createInvariant(), + $variances[0], + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createInvariant(), ]); } if (count($genericTypes) === 2) { $mixed = new MixedType(true); - return new GenericObjectType($mainType->getClassName(), [ + return new GenericObjectType($mainTypeClassName, [ $genericTypes[0], $genericTypes[1], $mixed, $mixed, + ], null, null, [ + $variances[0], + $variances[1], + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createInvariant(), ]); } } if (!$mainType->isIterable()->yes()) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } if ( count($genericTypes) !== 1 || $classReflection->getTemplateTypeMap()->count() === 1 ) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } } } if ($mainType->isIterable()->yes()) { - if (count($genericTypes) === 1) { // Foo - return TypeCombinator::intersect( - $mainType, - new IterableType(new MixedType(true), $genericTypes[0]), - ); + if ($mainTypeClassName !== null) { + if (isset($this->genericTypeResolvingStack[$mainTypeClassName])) { + return new ErrorType(); + } + + $this->genericTypeResolvingStack[$mainTypeClassName] = true; } - if (count($genericTypes) === 2) { // Foo - return TypeCombinator::intersect( - $mainType, - new IterableType($genericTypes[0], $genericTypes[1]), - ); + try { + if (count($genericTypes) === 1) { // Foo + return TypeCombinator::intersect( + $mainType, + new IterableType(new MixedType(true), $genericTypes[0]), + ); + } + + if (count($genericTypes) === 2) { // Foo + return TypeCombinator::intersect( + $mainType, + new IterableType($genericTypes[0], $genericTypes[1]), + ); + } + } finally { + if ($mainTypeClassName !== null) { + unset($this->genericTypeResolvingStack[$mainTypeClassName]); + } } } - if ($mainType instanceof TypeWithClassName) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + if ($mainTypeClassName !== null) { + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } return new ErrorType(); @@ -624,15 +957,44 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $nameScope): Type { + $templateTags = []; + + if (count($typeNode->templateTypes) > 0) { + foreach ($typeNode->templateTypes as $templateType) { + $templateTags[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->resolve($templateType->bound, $nameScope) + : new MixedType(), + $templateType->default !== null + ? $this->resolve($templateType->default, $nameScope) + : null, + TemplateTypeVariance::createInvariant(), + ); + } + $templateTypeScope = TemplateTypeScope::createWithAnonymousFunction(); + + $templateTypeMap = new TemplateTypeMap(array_map( + static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), + $templateTags, + )); + + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + } else { + $templateTypeMap = TemplateTypeMap::createEmpty(); + } + $mainType = $this->resolve($typeNode->identifier, $nameScope); + $isVariadic = false; - $parameters = array_map( + $parameters = array_values(array_map( function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadic): NativeParameterReflection { $isVariadic = $isVariadic || $parameterNode->isVariadic; $parameterName = $parameterNode->parameterName; - if (strpos($parameterName, '$') === 0) { + if (str_starts_with($parameterName, '$')) { $parameterName = substr($parameterName, 1); } + return new NativeParameterReflection( $parameterName, $parameterNode->isOptional || $parameterNode->isVariadic, @@ -643,17 +1005,36 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi ); }, $typeNode->parameters, - ); + )); + $returnType = $this->resolve($typeNode->returnType, $nameScope); if ($mainType instanceof CallableType) { - return new CallableType($parameters, $returnType, $isVariadic); + $pure = $mainType->isPure(); + if ($pure->yes() && $returnType->isVoid()->yes()) { + return new ErrorType(); + } + + return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags, $pure); } elseif ( $mainType instanceof ObjectType && $mainType->getClassName() === Closure::class ) { - return new ClosureType($parameters, $returnType, $isVariadic); + return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], [ + new SimpleImpurePoint( + 'functionCall', + 'call to a Closure', + false, + ), + ]); + } elseif ($mainType instanceof ClosureType) { + $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], $mainType->getImpurePoints(), $mainType->getInvalidateExpressions(), $mainType->getUsedVariables(), $mainType->acceptsNamedArguments()); + if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) { + return new ErrorType(); + } + + return $closure; } return new ErrorType(); @@ -662,6 +1043,9 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $nameScope): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); + if (count($typeNode->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $builder->degradeToGeneralArray(true); + } foreach ($typeNode->items as $itemNode) { $offsetType = null; @@ -677,7 +1061,43 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name $builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional); } - return $builder->getArray(); + $arrayType = $builder->getArray(); + if (in_array($typeNode->kind, [ + ArrayShapeNode::KIND_LIST, + ArrayShapeNode::KIND_NON_EMPTY_LIST, + ], true)) { + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } + + if (in_array($typeNode->kind, [ + ArrayShapeNode::KIND_NON_EMPTY_ARRAY, + ArrayShapeNode::KIND_NON_EMPTY_LIST, + ], true)) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + + return $arrayType; + } + + private function resolveObjectShapeNode(ObjectShapeNode $typeNode, NameScope $nameScope): Type + { + $properties = []; + $optionalProperties = []; + foreach ($typeNode->items as $itemNode) { + if ($itemNode->keyName instanceof IdentifierTypeNode) { + $propertyName = $itemNode->keyName->name; + } elseif ($itemNode->keyName instanceof ConstExprStringNode) { + $propertyName = $itemNode->keyName->value; + } + + if ($itemNode->optional) { + $optionalProperties[] = $propertyName; + } + + $properties[$propertyName] = $this->resolve($itemNode->valueType, $nameScope); + } + + return new ObjectShapeType($properties, $optionalProperties); } private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameScope): Type @@ -717,6 +1137,7 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc $className = $classReflection->getParentClass()->getName(); } + break; } } @@ -735,7 +1156,8 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc // 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) { + foreach ($classReflection->getNativeReflection()->getReflectionConstants() as $reflectionConstant) { + $classConstantName = $reflectionConstant->getName(); if (Strings::match($classConstantName, $pattern) === null) { continue; } @@ -745,7 +1167,17 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc continue; } - $constantTypes[] = ConstantTypeHelper::getTypeFromValue($constantValue); + $declaringClassName = $reflectionConstant->getDeclaringClass()->getName(); + if (!$this->getReflectionProvider()->hasClass($declaringClassName)) { + continue; + } + + $constantTypes[] = $this->initializerExprTypeResolver->getType( + $reflectionConstant->getValueExpression(), + InitializerExprContext::fromClassReflection( + $this->getReflectionProvider()->getClass($declaringClassName), + ), + ); } if (count($constantTypes) === 0) { @@ -763,28 +1195,78 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc return new EnumCaseObjectType($classReflection->getName(), $constantName); } - return ConstantTypeHelper::getTypeFromValue($classReflection->getConstant($constantName)->getValue()); + $reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName); + if ($reflectionConstant === false) { + return new ErrorType(); + } + $declaringClass = $reflectionConstant->getDeclaringClass(); + + return $this->initializerExprTypeResolver->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($declaringClass->getName(), $declaringClass->getFileName() ?: null)); } if ($constExpr instanceof ConstExprFloatNode) { - return ConstantTypeHelper::getTypeFromValue((float) $constExpr->value); + return new ConstantFloatType((float) $constExpr->value); } if ($constExpr instanceof ConstExprIntegerNode) { - return ConstantTypeHelper::getTypeFromValue((int) $constExpr->value); + return new ConstantIntegerType((int) $constExpr->value); } if ($constExpr instanceof ConstExprStringNode) { - return ConstantTypeHelper::getTypeFromValue($constExpr->value); + return new ConstantStringType($constExpr->value); } return new ErrorType(); } + private function resolveOffsetAccessNode(OffsetAccessTypeNode $typeNode, NameScope $nameScope): Type + { + $type = $this->resolve($typeNode->type, $nameScope); + $offset = $this->resolve($typeNode->offset, $nameScope); + + if ($type->isOffsetAccessible()->no() || $type->hasOffsetValueType($offset)->no()) { + return new ErrorType(); + } + + return new OffsetAccessType($type, $offset); + } + + private function expandIntMaskToType(Type $type): ?Type + { + $ints = array_map(static fn (ConstantIntegerType $type) => $type->getValue(), TypeUtils::getConstantIntegers($type)); + if (count($ints) === 0) { + return null; + } + + $values = []; + + foreach ($ints as $int) { + if ($int !== 0 && !array_key_exists($int, $values)) { + foreach ($values as $value) { + $computedValue = $value | $int; + $values[$computedValue] = $computedValue; + } + } + + $values[$int] = $int; + } + + $values[0] = 0; + + $min = min($values); + $max = max($values); + + if ($max - $min === count($values) - 1) { + return IntegerRangeType::fromInterval($min, $max); + } + + return TypeCombinator::union(...array_map(static fn ($value) => new ConstantIntegerType($value), $values)); + } + /** * @api * @param TypeNode[] $typeNodes - * @return Type[] + * @return list */ public function resolveMultiple(array $typeNodes, NameScope $nameScope): array { diff --git a/src/PhpDoc/TypeNodeResolverExtension.php b/src/PhpDoc/TypeNodeResolverExtension.php index 36508c7834..de004ecbba 100644 --- a/src/PhpDoc/TypeNodeResolverExtension.php +++ b/src/PhpDoc/TypeNodeResolverExtension.php @@ -6,7 +6,23 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\Type; -/** @api */ +/** + * This is the interface type node resolver extensions implement for custom PHPDoc types. + * + * To register it in the configuration file use the `phpstan.phpDoc.typeNodeResolverExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.phpDoc.typeNodeResolverExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/custom-phpdoc-types + * + * @api + */ interface TypeNodeResolverExtension { diff --git a/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php b/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php index a41b9c631c..a525078b2e 100644 --- a/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php +++ b/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php @@ -2,7 +2,7 @@ namespace PHPStan\PhpDoc; -class TypeNodeResolverExtensionAwareRegistry implements TypeNodeResolverExtensionRegistry +final class TypeNodeResolverExtensionAwareRegistry implements TypeNodeResolverExtensionRegistry { /** diff --git a/src/PhpDoc/TypeStringResolver.php b/src/PhpDoc/TypeStringResolver.php index 3933e1d25b..2bdb4ff94f 100644 --- a/src/PhpDoc/TypeStringResolver.php +++ b/src/PhpDoc/TypeStringResolver.php @@ -8,7 +8,7 @@ use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\Type\Type; -class TypeStringResolver +final class TypeStringResolver { public function __construct(private Lexer $typeLexer, private TypeParser $typeParser, private TypeNodeResolver $typeNodeResolver) @@ -20,7 +20,7 @@ public function resolve(string $typeString, ?NameScope $nameScope = null): Type { $tokens = new TokenIterator($this->typeLexer->tokenize($typeString)); $typeNode = $this->typeParser->parse($tokens); - $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore-line + $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore missingType.checkedException return $this->typeNodeResolver->resolve($typeNode, $nameScope ?? new NameScope(null, [])); } diff --git a/src/Process/CpuCoreCounter.php b/src/Process/CpuCoreCounter.php index 609baad081..2fd49e7cfa 100644 --- a/src/Process/CpuCoreCounter.php +++ b/src/Process/CpuCoreCounter.php @@ -2,18 +2,10 @@ namespace PHPStan\Process; -use function count; -use function fgets; -use function file_get_contents; -use function function_exists; -use function is_file; -use function is_resource; -use function pclose; -use function popen; -use function preg_match_all; -use const DIRECTORY_SEPARATOR; +use Fidry\CpuCoreCounter\CpuCoreCounter as FidryCpuCoreCounter; +use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound; -class CpuCoreCounter +final class CpuCoreCounter { private ?int $count = null; @@ -24,42 +16,13 @@ public function getNumberOfCpuCores(): int return $this->count; } - if (!function_exists('proc_open')) { - return $this->count = 1; + try { + $this->count = (new FidryCpuCoreCounter())->getCount(); + } catch (NumberOfCpuCoreNotFound) { + $this->count = 1; } - // from brianium/paratest - if (@is_file('/proc/cpuinfo')) { - // Linux (and potentially Windows with linux sub systems) - $cpuinfo = @file_get_contents('/proc/cpuinfo'); - if ($cpuinfo !== false) { - preg_match_all('/^processor/m', $cpuinfo, $matches); - return $this->count = count($matches[0]); - } - } - - if (DIRECTORY_SEPARATOR === '\\') { - // Windows - $process = @popen('wmic cpu get NumberOfLogicalProcessors', 'rb'); - if (is_resource($process)) { - fgets($process); - $cores = (int) fgets($process); - pclose($process); - - return $this->count = $cores; - } - } - - $process = @popen('sysctl -n hw.ncpu', 'rb'); - if (is_resource($process)) { - // *nix (Linux, BSD and Mac) - $cores = (int) fgets($process); - pclose($process); - - return $this->count = $cores; - } - - return $this->count = 2; + return $this->count; } } diff --git a/src/Process/ProcessCanceledException.php b/src/Process/ProcessCanceledException.php index 9d37c5b6ca..ae42c75d3b 100644 --- a/src/Process/ProcessCanceledException.php +++ b/src/Process/ProcessCanceledException.php @@ -4,7 +4,7 @@ use Exception; -class ProcessCanceledException extends Exception +final class ProcessCanceledException extends Exception { } diff --git a/src/Process/ProcessCrashedException.php b/src/Process/ProcessCrashedException.php index d6278a50e7..fb75a7d94d 100644 --- a/src/Process/ProcessCrashedException.php +++ b/src/Process/ProcessCrashedException.php @@ -4,7 +4,7 @@ use Exception; -class ProcessCrashedException extends Exception +final class ProcessCrashedException extends Exception { } diff --git a/src/Process/ProcessHelper.php b/src/Process/ProcessHelper.php index 2597e771a8..591e916fee 100644 --- a/src/Process/ProcessHelper.php +++ b/src/Process/ProcessHelper.php @@ -13,7 +13,7 @@ use function sprintf; use const PHP_BINARY; -class ProcessHelper +final class ProcessHelper { /** diff --git a/src/Process/ProcessPromise.php b/src/Process/ProcessPromise.php index 26b7fb256a..31f975460a 100644 --- a/src/Process/ProcessPromise.php +++ b/src/Process/ProcessPromise.php @@ -2,21 +2,20 @@ 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 React\Promise\PromiseInterface; use function fclose; use function rewind; use function stream_get_contents; use function tmpfile; -class ProcessPromise implements Runnable +final class ProcessPromise { + /** @var Deferred */ private Deferred $deferred; private ?Process $process = null; @@ -34,9 +33,9 @@ public function getName(): string } /** - * @return ExtendedPromiseInterface&CancellablePromiseInterface + * @return PromiseInterface */ - public function run(): CancellablePromiseInterface + public function run(): PromiseInterface { $tmpStdOutResource = tmpfile(); if ($tmpStdOutResource === false) { @@ -73,6 +72,9 @@ public function run(): CancellablePromiseInterface } if ($exitCode === 0) { + if ($stdOut === false) { + $stdOut = ''; + } $this->deferred->resolve($stdOut); return; } @@ -80,7 +82,6 @@ public function run(): CancellablePromiseInterface $this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr)); }); - /** @var ExtendedPromiseInterface&CancellablePromiseInterface */ return $this->deferred->promise(); } diff --git a/src/Process/Runnable/Runnable.php b/src/Process/Runnable/Runnable.php deleted file mode 100644 index e25e9154cc..0000000000 --- a/src/Process/Runnable/Runnable.php +++ /dev/null @@ -1,16 +0,0 @@ - */ - private array $queue = []; - - /** @var SplObjectStorage */ - private SplObjectStorage $running; - - public function __construct(private RunnableQueueLogger $logger, private int $maxSize) - { - /** @var SplObjectStorage $running */ - $running = new SplObjectStorage(); - $this->running = $running; - } - - public function getQueueSize(): int - { - $allSize = 0; - foreach ($this->queue as [$runnable, $size, $deferred]) { - $allSize += $size; - } - - return $allSize; - } - - public function getRunningSize(): int - { - $allSize = 0; - foreach ($this->running as $running) { // phpcs:ignore - [$size] = $this->running->getInfo(); - $allSize += $size; - } - - return $allSize; - } - - public function queue(Runnable $runnable, int $size): CancellablePromiseInterface - { - if ($size > $this->maxSize) { - throw new ShouldNotHappenException('Runnable size exceeds queue maxSize.'); - } - - $deferred = new Deferred(static function () use ($runnable): void { - $runnable->cancel(); - }); - $this->queue[] = [$runnable, $size, $deferred]; - $this->drainQueue(); - - /** @var CancellablePromiseInterface */ - return $deferred->promise(); - } - - private function drainQueue(): void - { - if (count($this->queue) === 0) { - $this->logger->log('Queue empty'); - return; - } - - $currentQueueSize = $this->getRunningSize(); - if ($currentQueueSize > $this->maxSize) { - throw new ShouldNotHappenException('Running overflow'); - } - - if ($currentQueueSize === $this->maxSize) { - $this->logger->log('Queue is full'); - return; - } - - $this->logger->log('Queue not full - looking at first item in the queue'); - - [$runnable, $runnableSize, $deferred] = $this->queue[0]; - - $newSize = $currentQueueSize + $runnableSize; - if ($newSize > $this->maxSize) { - $this->logger->log( - sprintf( - 'Canot remote first item from the queue - it has size %d, current queue size is %d, new size would be %d', - $runnableSize, - $currentQueueSize, - $newSize, - ), - ); - return; - } - - $this->logger->log(sprintf('Removing top item from queue - new size is %d', $newSize)); - - /** @var array{Runnable, int, Deferred} $popped */ - $popped = array_shift($this->queue); - if ($popped[0] !== $runnable || $popped[1] !== $runnableSize || $popped[2] !== $deferred) { - throw new ShouldNotHappenException(); - } - - $this->running->attach($runnable, [$runnableSize, $deferred]); - $this->logger->log(sprintf('Running process %s', $runnable->getName())); - $runnable->run()->then(function ($value) use ($runnable, $deferred): void { - $this->logger->log(sprintf('Process %s finished successfully', $runnable->getName())); - $deferred->resolve($value); - $this->running->detach($runnable); - $this->drainQueue(); - }, function (Throwable $e) use ($runnable, $deferred): void { - $this->logger->log(sprintf('Process %s finished unsuccessfully: %s', $runnable->getName(), $e->getMessage())); - $deferred->reject($e); - $this->running->detach($runnable); - $this->drainQueue(); - }); - } - - public function cancelAll(): void - { - foreach ($this->queue as [$runnable, $size, $deferred]) { - $deferred->promise()->cancel(); // @phpstan-ignore-line - } - - $runningDeferreds = []; - foreach ($this->running as $running) { // phpcs:ignore - [,$deferred] = $this->running->getInfo(); - $runningDeferreds[] = $deferred; - } - - foreach ($runningDeferreds as $deferred) { - $deferred->promise()->cancel(); // @phpstan-ignore-line - } - } - -} diff --git a/src/Process/Runnable/RunnableQueueLogger.php b/src/Process/Runnable/RunnableQueueLogger.php deleted file mode 100644 index a29ada14b3..0000000000 --- a/src/Process/Runnable/RunnableQueueLogger.php +++ /dev/null @@ -1,10 +0,0 @@ - + */ + public function getAllowedSubTypes(ClassReflection $classReflection): array; + +} diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index d33665d4b8..696e0e5b08 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -2,23 +2,26 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\MixedType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; -class AnnotationMethodReflection implements MethodReflection +final class AnnotationMethodReflection implements ExtendedMethodReflection { - /** @var FunctionVariant[]|null */ + /** @var list|null */ private ?array $variants = null; /** - * @param AnnotationsMethodParameterReflection[] $parameters + * @param list $parameters */ public function __construct( private string $name, @@ -27,6 +30,8 @@ public function __construct( private array $parameters, private bool $isStatic, private bool $isVariadic, + private ?Type $throwType, + private TemplateTypeMap $templateTypeMap, ) { } @@ -61,25 +66,34 @@ public function getName(): string return $this->name; } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { if ($this->variants === null) { $this->variants = [ - new FunctionVariant( - TemplateTypeMap::createEmpty(), + new ExtendedFunctionVariant( + $this->templateTypeMap, null, $this->parameters, $this->isVariadic, $this->returnType, + $this->returnType, + new MixedType(), ), ]; } return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -95,18 +109,36 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function getThrowType(): ?Type { - return null; + return $this->throwType; } public function hasSideEffects(): TrinaryLogic { + if ($this->returnType->isVoid()->yes()) { + return TrinaryLogic::createYes(); + } + + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->returnType)->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } @@ -115,4 +147,43 @@ public function getDocComment(): ?string return null; } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->acceptsNamedArguments()); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); + } + + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Annotations/AnnotationPropertyReflection.php b/src/Reflection/Annotations/AnnotationPropertyReflection.php index 3613e87707..8f4f322d08 100644 --- a/src/Reflection/Annotations/AnnotationPropertyReflection.php +++ b/src/Reflection/Annotations/AnnotationPropertyReflection.php @@ -3,22 +3,32 @@ namespace PHPStan\Reflection\Annotations; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class AnnotationPropertyReflection implements PropertyReflection +final class AnnotationPropertyReflection implements ExtendedPropertyReflection { public function __construct( + private string $name, private ClassReflection $declaringClass, - private Type $type, - private bool $readable = true, - private bool $writable = true, + private Type $readableType, + private Type $writableType, + private bool $readable, + private bool $writable, ) { } + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -39,19 +49,39 @@ public function isPublic(): bool return true; } + public function hasPhpDocType(): bool + { + return true; + } + + public function getPhpDocType(): Type + { + return $this->readableType; + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + public function getReadableType(): Type { - return $this->type; + return $this->readableType; } public function getWritableType(): Type { - return $this->type; + return $this->writableType; } public function canChangeTypeAfterAssignment(): bool { - return true; + return $this->readableType->equals($this->writableType); } public function isReadable(): bool @@ -84,4 +114,44 @@ public function getDocComment(): ?string return null; } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function hasHook(string $hookType): bool + { + return false; + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + throw new ShouldNotHappenException(); + } + + public function isProtectedSet(): bool + { + return false; + } + + public function isPrivateSet(): bool + { + return false; + } + + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index 8b51b325cf..b01a6db6ff 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -2,11 +2,13 @@ namespace PHPStan\Reflection\Annotations; -use PHPStan\Reflection\ParameterReflection; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class AnnotationsMethodParameterReflection implements ParameterReflection +final class AnnotationsMethodParameterReflection implements ExtendedParameterReflection { public function __construct(private string $name, private Type $type, private PassedByReference $passedByReference, private bool $isOptional, private bool $isVariadic, private ?Type $defaultValue) @@ -28,6 +30,36 @@ public function getType(): Type return $this->type; } + public function getPhpDocType(): Type + { + return $this->type; + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + + public function getOutType(): ?Type + { + return null; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClosureThisType(): ?Type + { + return null; + } + public function passedByReference(): PassedByReference { return $this->passedByReference; @@ -43,4 +75,9 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index a51ab77b65..9ad470b0ee 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -2,16 +2,23 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; +use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Type; +use function array_map; use function count; -class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension +final class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension { - /** @var MethodReflection[][] */ + /** @var ExtendedMethodReflection[][] */ private array $methods = []; public function hasMethod(ClassReflection $classReflection, string $methodName): bool @@ -27,7 +34,7 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): return isset($this->methods[$classReflection->getCacheKey()][$methodName]); } - public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + public function getMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection { return $this->methods[$classReflection->getCacheKey()][$methodName]; } @@ -36,7 +43,7 @@ private function findClassReflectionWithMethod( ClassReflection $classReflection, ClassReflection $declaringClass, string $methodName, - ): ?MethodReflection + ): ?ExtendedMethodReflection { $methodTags = $classReflection->getMethodTags(); if (isset($methodTags[$methodName])) { @@ -52,16 +59,32 @@ private function findClassReflectionWithMethod( ); } + $templateTypeScope = TemplateTypeScope::createWithClass($classReflection->getName()); + + $templateTypeMap = new TemplateTypeMap(array_map( + static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), + $methodTags[$methodName]->getTemplateTags(), + )); + + $isStatic = $methodTags[$methodName]->isStatic(); + $nativeCallMethodName = $isStatic ? '__callStatic' : '__call'; + return new AnnotationMethodReflection( $methodName, $declaringClass, TemplateTypeHelper::resolveTemplateTypes( $methodTags[$methodName]->getReturnType(), $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ), $parameters, - $methodTags[$methodName]->isStatic(), + $isStatic, $this->detectMethodVariadic($parameters), + $classReflection->hasNativeMethod($nativeCallMethodName) + ? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType() + : null, + $templateTypeMap, ); } @@ -74,21 +97,14 @@ private function findClassReflectionWithMethod( return $methodWithDeclaringClass; } - foreach ($classReflection->getParents() as $parentClass) { + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { $methodWithDeclaringClass = $this->findClassReflectionWithMethod($parentClass, $parentClass, $methodName); - if ($methodWithDeclaringClass === null) { - foreach ($parentClass->getTraits() as $traitClass) { - $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithMethod($traitClass, $parentClass, $methodName); - if ($parentTraitMethodWithDeclaringClass === null) { - continue; - } - - return $parentTraitMethodWithDeclaringClass; - } - continue; + if ($methodWithDeclaringClass !== null) { + return $methodWithDeclaringClass; } - return $methodWithDeclaringClass; + $parentClass = $parentClass->getParentClass(); } foreach ($classReflection->getInterfaces() as $interfaceClass) { diff --git a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php index 324f14b2ab..5c19c29ae7 100644 --- a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php @@ -3,14 +3,16 @@ namespace PHPStan\Reflection\Annotations; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\NeverType; -class AnnotationsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension +final class AnnotationsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { - /** @var PropertyReflection[][] */ + /** @var ExtendedPropertyReflection[][] */ private array $properties = []; public function hasProperty(ClassReflection $classReflection, string $propertyName): bool @@ -26,7 +28,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa return isset($this->properties[$classReflection->getCacheKey()][$propertyName]); } - public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection { return $this->properties[$classReflection->getCacheKey()][$propertyName]; } @@ -35,18 +37,37 @@ private function findClassReflectionWithProperty( ClassReflection $classReflection, ClassReflection $declaringClass, string $propertyName, - ): ?PropertyReflection + ): ?ExtendedPropertyReflection { $propertyTags = $classReflection->getPropertyTags(); if (isset($propertyTags[$propertyName])) { + $propertyTag = $propertyTags[$propertyName]; + + $isReadable = $propertyTags[$propertyName]->isReadable(); + $isWritable = $propertyTags[$propertyName]->isWritable(); + if ($classReflection->hasNativeProperty($propertyName)) { + $nativeProperty = $classReflection->getNativeProperty($propertyName); + $isReadable = $isReadable || $nativeProperty->isReadable(); + $isWritable = $isWritable || $nativeProperty->isWritable(); + } + return new AnnotationPropertyReflection( + $propertyName, $declaringClass, TemplateTypeHelper::resolveTemplateTypes( - $propertyTags[$propertyName]->getType(), + $propertyTag->getReadableType() ?? new NeverType(), + $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), + ), + TemplateTypeHelper::resolveTemplateTypes( + $propertyTag->getWritableType() ?? new NeverType(), $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), ), - $propertyTags[$propertyName]->isReadable(), - $propertyTags[$propertyName]->isWritable(), + $isReadable, + $isWritable, ); } @@ -59,21 +80,14 @@ private function findClassReflectionWithProperty( return $methodWithDeclaringClass; } - foreach ($classReflection->getParents() as $parentClass) { + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { $methodWithDeclaringClass = $this->findClassReflectionWithProperty($parentClass, $parentClass, $propertyName); - if ($methodWithDeclaringClass === null) { - foreach ($parentClass->getTraits() as $traitClass) { - $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithProperty($traitClass, $parentClass, $propertyName); - if ($parentTraitMethodWithDeclaringClass === null) { - continue; - } - - return $parentTraitMethodWithDeclaringClass; - } - continue; + if ($methodWithDeclaringClass !== null) { + return $methodWithDeclaringClass; } - return $methodWithDeclaringClass; + $parentClass = $parentClass->getParentClass(); } foreach ($classReflection->getInterfaces() as $interfaceClass) { diff --git a/src/Reflection/Assertions.php b/src/Reflection/Assertions.php new file mode 100644 index 0000000000..a1f7ebfa6d --- /dev/null +++ b/src/Reflection/Assertions.php @@ -0,0 +1,111 @@ +asserts; + } + + /** + * @return AssertTag[] + */ + public function getAsserts(): array + { + return array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::NULL); + } + + /** + * @return AssertTag[] + */ + public function getAssertsIfTrue(): array + { + return array_merge( + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_TRUE), + array_map( + static fn (AssertTag $assert) => $assert->negate(), + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_FALSE && !$assert->isEquality()), + ), + ); + } + + /** + * @return AssertTag[] + */ + public function getAssertsIfFalse(): array + { + return array_merge( + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_FALSE), + array_map( + static fn (AssertTag $assert) => $assert->negate(), + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_TRUE && !$assert->isEquality()), + ), + ); + } + + /** + * @param callable(Type): Type $callable + */ + public function mapTypes(callable $callable): self + { + $assertTagsCallback = static fn (AssertTag $tag): AssertTag => $tag->withType($callable($tag->getType())); + + return new self(array_map($assertTagsCallback, $this->asserts)); + } + + public function intersectWith(Assertions $other): self + { + return new self(array_merge($this->getAll(), $other->getAll())); + } + + public static function createEmpty(): self + { + $empty = self::$empty; + + if ($empty !== null) { + return $empty; + } + + $empty = new self([]); + self::$empty = $empty; + + return $empty; + } + + public static function createFromResolvedPhpDocBlock(ResolvedPhpDocBlock $phpDocBlock): self + { + $tags = $phpDocBlock->getAssertTags(); + if (count($tags) === 0) { + return self::createEmpty(); + } + + return new self($tags); + } + +} diff --git a/src/Reflection/AttributeReflection.php b/src/Reflection/AttributeReflection.php new file mode 100644 index 0000000000..74d6874223 --- /dev/null +++ b/src/Reflection/AttributeReflection.php @@ -0,0 +1,33 @@ + $argumentTypes + */ + public function __construct(private string $name, private array $argumentTypes) + { + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return array + */ + public function getArgumentTypes(): array + { + return $this->argumentTypes; + } + +} diff --git a/src/Reflection/AttributeReflectionFactory.php b/src/Reflection/AttributeReflectionFactory.php new file mode 100644 index 0000000000..74a7d0efaa --- /dev/null +++ b/src/Reflection/AttributeReflectionFactory.php @@ -0,0 +1,134 @@ + $reflections + * @return list + */ + public function fromNativeReflection(array $reflections, InitializerExprContext $context): array + { + $attributes = []; + foreach ($reflections as $reflection) { + $attribute = $this->fromNameAndArgumentExpressions($reflection->getName(), $reflection->getArgumentsExpressions(), $context); + if ($attribute === null) { + continue; + } + + $attributes[] = $attribute; + } + + return $attributes; + } + + /** + * @param AttributeGroup[] $attrGroups + * @return list + */ + public function fromAttrGroups(array $attrGroups, InitializerExprContext $context): array + { + $attributes = []; + foreach ($attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $arguments = []; + foreach ($attr->args as $i => $arg) { + if ($arg->name === null) { + $argName = $i; + } else { + $argName = $arg->name->toString(); + } + + $arguments[$argName] = $arg->value; + } + $attributeReflection = $this->fromNameAndArgumentExpressions($attr->name->toString(), $arguments, $context); + if ($attributeReflection === null) { + continue; + } + + $attributes[] = $attributeReflection; + } + } + + return $attributes; + } + + /** + * @param array $arguments + */ + private function fromNameAndArgumentExpressions(string $name, array $arguments, InitializerExprContext $context): ?AttributeReflection + { + if (count($arguments) === 0) { + return new AttributeReflection($name, []); + } + + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + if (!$reflectionProvider->hasClass($name)) { + return null; + } + + $classReflection = $reflectionProvider->getClass($name); + if (!$classReflection->hasConstructor()) { + return null; + } + + if (!$classReflection->isAttributeClass()) { + return null; + } + + $constructor = $classReflection->getConstructor(); + $parameters = $constructor->getOnlyVariant()->getParameters(); + $namedArgTypes = []; + foreach ($arguments as $i => $argExpr) { + if (is_int($i)) { + if (isset($parameters[$i])) { + $namedArgTypes[$parameters[$i]->getName()] = $this->initializerExprTypeResolver->getType($argExpr, $context); + continue; + } + if (count($parameters) > 0) { + $lastParameter = $parameters[count($parameters) - 1]; + if ($lastParameter->isVariadic()) { + $parameterName = $lastParameter->getName(); + if (array_key_exists($parameterName, $namedArgTypes)) { + $namedArgTypes[$parameterName] = TypeCombinator::union($namedArgTypes[$parameterName], $this->initializerExprTypeResolver->getType($argExpr, $context)); + continue; + } + $namedArgTypes[$parameterName] = $this->initializerExprTypeResolver->getType($argExpr, $context); + } + } + continue; + } + + foreach ($parameters as $parameter) { + if ($parameter->getName() !== $i) { + continue; + } + + $namedArgTypes[$i] = $this->initializerExprTypeResolver->getType($argExpr, $context); + break; + } + } + + return new AttributeReflection($classReflection->getName(), $namedArgTypes); + } + +} diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index 7a3a5abd3b..707aeca28a 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -3,15 +3,14 @@ namespace PHPStan\Reflection\BetterReflection; use Closure; +use Nette\Utils\Strings; use PhpParser\Node; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName; use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; -use PHPStan\BetterReflection\Reflection\Exception\NotAClassReflection; -use PHPStan\BetterReflection\Reflection\Exception\NotAnInterfaceReflection; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\BetterReflection\Reflector\Reflector; @@ -23,35 +22,46 @@ use PHPStan\Broker\FunctionNotFoundException; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\File\FileHelper; +use PHPStan\File\FileReader; use PHPStan\File\RelativePathHelper; +use PHPStan\Parser\AnonymousClassVisitor; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; -use PHPStan\PhpDoc\Tag\ParamTag; +use PHPStan\PhpDoc\Tag\ParamClosureThisTag; +use PHPStan\PhpDoc\Tag\ParamOutTag; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflectionFactory; use PHPStan\Reflection\ClassNameHelper; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Constant\RuntimeConstantReflection; +use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\Deprecation\DeprecationProvider; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\FunctionReflectionFactory; -use PHPStan\Reflection\GlobalConstantReflection; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\NamespaceAnswerer; +use PHPStan\Reflection\Php\ExitFunctionReflection; use PHPStan\Reflection\Php\PhpFunctionReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ConstantTypeHelper; +use PHPStan\TrinaryLogic; 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_key_first; use function array_map; use function base64_decode; +use function in_array; use function sprintf; use function strtolower; use const PHP_VERSION_ID; -class BetterReflectionProvider implements ReflectionProvider +final class BetterReflectionProvider implements ReflectionProvider { /** @var FunctionReflection[] */ @@ -63,24 +73,31 @@ class BetterReflectionProvider implements ReflectionProvider /** @var ClassReflection[] */ private static array $anonymousClasses = []; - /** @var array */ + /** @var array */ private array $cachedConstants = []; + /** + * @param list $universalObjectCratesClasses + */ public function __construct( private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private InitializerExprTypeResolver $initializerExprTypeResolver, private ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, private Reflector $reflector, private FileTypeMapper $fileTypeMapper, private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private DeprecationProvider $deprecationProvider, 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, + private SignatureMapProvider $signatureMapProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + private array $universalObjectCratesClasses, ) { } @@ -113,7 +130,7 @@ public function getClass(string $className): ClassReflection try { $reflectionClass = $this->reflector->reflectClass($className); - } catch (IdentifierNotFound) { + } catch (IdentifierNotFound | InvalidIdentifierName) { throw new ClassNotFoundException($className); } @@ -127,17 +144,25 @@ public function getClass(string $className): ClassReflection $classReflection = new ClassReflection( $this->reflectionProviderProvider->getReflectionProvider(), + $this->initializerExprTypeResolver, $this->fileTypeMapper, $this->stubPhpDocProvider, $this->phpDocInheritanceResolver, $this->phpVersion, + $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), $reflectionClass->getName(), $reflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($reflectionClass) : new ReflectionClass($reflectionClass), null, null, $this->stubPhpDocProvider->findClassPhpDoc($reflectionClass->getName()), + $this->universalObjectCratesClasses, ); $this->classReflections[$reflectionClassName] = $classReflection; @@ -160,11 +185,6 @@ public function getClassName(string $className): string return $reflectionClass->getName(); } - public function supportsAnonymousClasses(): bool - { - return true; - } - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { if (isset($classNode->namespacedName)) { @@ -186,7 +206,7 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ $scopeFile, ); $classNode->name = new Node\Identifier($className); - $classNode->setAttribute('anonymousClass', true); + $classNode->namespacedName = null; if (isset(self::$anonymousClasses[$className])) { return self::$anonymousClasses[$className]; @@ -195,37 +215,69 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ $reflectionClass = \PHPStan\BetterReflection\Reflection\ReflectionClass::createFromNode( $this->reflector, $classNode, - new LocatedSource($this->printer->prettyPrint([$classNode]), $className, $scopeFile), + new LocatedSource(FileReader::read($scopeFile), $className, $scopeFile), null, ); + $displayParentName = $reflectionClass->getParentClassName(); + if ($displayParentName === null) { + // https://3v4l.org/6FBuP + $classInterfaceNames = $reflectionClass->getInterfaceNames(); + if ($classInterfaceNames !== []) { + $displayParentName = $classInterfaceNames[array_key_first($classInterfaceNames)]; + } else { + $displayParentName = 'class'; + } + } + + /** @var int|null $classLineIndex */ + $classLineIndex = $classNode->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX); + if ($classLineIndex === null) { + $displayName = sprintf('%s@anonymous/%s:%s', $displayParentName, $filename, $classNode->getStartLine()); + } else { + $displayName = sprintf('%s@anonymous/%s:%s:%d', $displayParentName, $filename, $classNode->getStartLine(), $classLineIndex); + } + self::$anonymousClasses[$className] = new ClassReflection( $this->reflectionProviderProvider->getReflectionProvider(), + $this->initializerExprTypeResolver, $this->fileTypeMapper, $this->stubPhpDocProvider, $this->phpDocInheritanceResolver, $this->phpVersion, + $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), - sprintf('class@anonymous/%s:%s', $filename, $classNode->getLine()), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), + $displayName, new ReflectionClass($reflectionClass), $scopeFile, null, $this->stubPhpDocProvider->findClassPhpDoc($className), + $this->universalObjectCratesClasses, ); $this->classReflections[$className] = self::$anonymousClasses[$className]; return self::$anonymousClasses[$className]; } - public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool + public function getUniversalObjectCratesClasses(): array { - return $this->resolveFunctionName($nameNode, $scope) !== null; + return $this->universalObjectCratesClasses; } - public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool { - $functionName = $this->resolveFunctionName($nameNode, $scope); + return $this->resolveFunctionName($nameNode, $namespaceAnswerer) !== null; + } + + public function getFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): FunctionReflection + { + $functionName = $this->resolveFunctionName($nameNode, $namespaceAnswerer); if ($functionName === null) { throw new FunctionNotFoundException((string) $nameNode); } @@ -235,6 +287,10 @@ public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflect return $this->functionReflections[$lowerCasedFunctionName]; } + if (in_array($lowerCasedFunctionName, ['exit', 'die'], true)) { + return $this->functionReflections[$lowerCasedFunctionName] = new ExitFunctionReflection($lowerCasedFunctionName); + } + $nativeFunctionReflection = $this->nativeFunctionReflectionProvider->findFunctionReflection($lowerCasedFunctionName); if ($nativeFunctionReflection !== null) { $this->functionReflections[$lowerCasedFunctionName] = $nativeFunctionReflection; @@ -250,14 +306,23 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection { $reflectionFunction = new ReflectionFunction($this->reflector->reflectFunction($functionName)); $templateTypeMap = TemplateTypeMap::createEmpty(); - $phpDocParameterTags = []; + $phpDocParameterTypes = []; $phpDocReturnTag = null; $phpDocThrowsTag = null; - $deprecatedTag = null; - $isDeprecated = false; + + $deprecation = $this->deprecationProvider->getFunctionDeprecation($reflectionFunction); + $deprecationDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; + $isInternal = false; - $isFinal = false; $isPure = null; + $asserts = Assertions::createEmpty(); + $acceptsNamedArguments = true; + $phpDocComment = null; + $phpDocParameterOutTags = []; + $phpDocParameterImmediatelyInvokedCallable = []; + $phpDocParameterClosureThisTypeTags = []; + $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) { $docComment = $reflectionFunction->getDocComment(); @@ -266,33 +331,53 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection if ($resolvedPhpDoc !== null) { $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - $phpDocParameterTags = $resolvedPhpDoc->getParamTags(); + $phpDocParameterTypes = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamTags()); $phpDocReturnTag = $resolvedPhpDoc->getReturnTag(); $phpDocThrowsTag = $resolvedPhpDoc->getThrowsTag(); - $deprecatedTag = $resolvedPhpDoc->getDeprecatedTag(); - $isDeprecated = $resolvedPhpDoc->isDeprecated(); + if (!$isDeprecated) { + $deprecationDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : $deprecationDescription; + $isDeprecated = $resolvedPhpDoc->isDeprecated(); + } $isInternal = $resolvedPhpDoc->isInternal(); - $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); + $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + if ($resolvedPhpDoc->hasPhpDocString()) { + $phpDocComment = $resolvedPhpDoc->getPhpDocString(); + } + $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); + $phpDocParameterOutTags = $resolvedPhpDoc->getParamOutTags(); + $phpDocParameterImmediatelyInvokedCallable = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); + $phpDocParameterClosureThisTypeTags = $resolvedPhpDoc->getParamClosureThisTags(); } return $this->functionReflectionFactory->create( $reflectionFunction, $templateTypeMap, - array_map(static fn (ParamTag $paramTag): Type => $paramTag->getType(), $phpDocParameterTags), + $phpDocParameterTypes, $phpDocReturnTag !== null ? $phpDocReturnTag->getType() : null, $phpDocThrowsTag !== null ? $phpDocThrowsTag->getType() : null, - $deprecatedTag !== null ? $deprecatedTag->getMessage() : null, + $deprecationDescription, $isDeprecated, $isInternal, - $isFinal, $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null, $isPure, + $asserts, + $acceptsNamedArguments, + $phpDocComment, + array_map(static fn (ParamOutTag $paramOutTag): Type => $paramOutTag->getType(), $phpDocParameterOutTags), + $phpDocParameterImmediatelyInvokedCallable, + array_map(static fn (ParamClosureThisTag $tag): Type => $tag->getType(), $phpDocParameterClosureThisTypeTags), + $this->attributeReflectionFactory->fromNativeReflection($reflectionFunction->getAttributes(), InitializerExprContext::fromFunction($reflectionFunction->getName(), $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null)), ); } - public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string + public function resolveFunctionName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string { + $name = $nameNode->toLowerString(); + if (in_array($name, ['exit', 'die'], true)) { + return $name; + } + return $this->resolveName($nameNode, function (string $name): bool { try { $this->reflector->reflectFunction($name); @@ -307,17 +392,17 @@ public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string return $this->phpstormStubsSourceStubber->isPresentFunction($name) !== false; } return false; - }, $scope); + }, $namespaceAnswerer); } - public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool + public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool { - return $this->resolveConstantName($nameNode, $scope) !== null; + return $this->resolveConstantName($nameNode, $namespaceAnswerer) !== null; } - public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection { - $constantName = $this->resolveConstantName($nameNode, $scope); + $constantName = $this->resolveConstantName($nameNode, $namespaceAnswerer); if ($constantName === null) { throw new ConstantNotFoundException((string) $nameNode); } @@ -327,23 +412,47 @@ public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantR } $constantReflection = $this->reflector->reflectConstant($constantName); - try { - $constantValue = $constantReflection->getValue(); - $constantValueType = ConstantTypeHelper::getTypeFromValue($constantValue); - $fileName = $constantReflection->getFileName(); - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection) { - $constantValueType = new MixedType(); - $fileName = null; + $fileName = $constantReflection->getFileName(); + $constantValueType = $this->initializerExprTypeResolver->getType($constantReflection->getValueExpression(), InitializerExprContext::fromGlobalConstant($constantReflection)); + $docComment = $constantReflection->getDocComment(); + + $deprecation = $this->deprecationProvider->getConstantDeprecation($constantReflection); + $isDeprecated = $deprecation !== null; + $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); + + if ($isDeprecated === false && $docComment !== null) { + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, null, $docComment); + $isDeprecated = $resolvedPhpDoc->isDeprecated(); + + if ($isDeprecated && $resolvedPhpDoc->getDeprecatedTag() !== null) { + $deprecatedMessage = $resolvedPhpDoc->getDeprecatedTag()->getMessage(); + + $matches = Strings::match($deprecatedMessage ?? '', '#^(\d+)\.(\d+)(?:\.(\d+))?$#'); + if ($matches !== null) { + $major = $matches[1]; + $minor = $matches[2]; + $patch = $matches[3] ?? 0; + $versionId = sprintf('%d%02d%02d', $major, $minor, $patch); + + $isDeprecated = $this->phpVersion->getVersionId() >= $versionId; + } else { + // filter raw version number messages like in + // https://github.com/JetBrains/phpstorm-stubs/blob/9608c953230b08f07b703ecfe459cc58d5421437/filter/filter.php#L478 + $deprecatedDescription = $deprecatedMessage; + } + } } return $this->cachedConstants[$constantName] = new RuntimeConstantReflection( $constantName, $constantValueType, $fileName, + TrinaryLogic::createFromBoolean($isDeprecated), + $deprecatedDescription, ); } - public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string + public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string { return $this->resolveName($nameNode, function (string $name): bool { try { @@ -351,11 +460,11 @@ public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string return true; } catch (IdentifierNotFound) { // pass - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection) { + } catch (UnableToCompileNode) { // pass } return false; - }, $scope); + }, $namespaceAnswerer); } /** @@ -364,12 +473,12 @@ public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string private function resolveName( Node\Name $nameNode, Closure $existsCallback, - ?Scope $scope, + ?NamespaceAnswerer $namespaceAnswerer, ): ?string { $name = (string) $nameNode; - if ($scope !== null && $scope->getNamespace() !== null && !$nameNode->isFullyQualified()) { - $namespacedName = sprintf('%s\\%s', $scope->getNamespace(), $name); + if ($namespaceAnswerer !== null && $namespaceAnswerer->getNamespace() !== null && !$nameNode->isFullyQualified()) { + $namespacedName = sprintf('%s\\%s', $namespaceAnswerer->getNamespace(), $name); if ($existsCallback($namespacedName)) { return $namespacedName; } diff --git a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php index 8e88819ffa..8632d6b59c 100644 --- a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php @@ -2,127 +2,76 @@ namespace PHPStan\Reflection\BetterReflection; -use PhpParser\Node; - -use PHPStan\DependencyInjection\Container; -use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator; -use PHPStan\Reflection\BetterReflection\SourceLocator\ClassBlacklistSourceLocator; -use PHPStan\Reflection\BetterReflection\SourceLocator\ClassWhitelistSourceLocator; -use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker; -use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository; -use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository; -use PHPStan\Reflection\BetterReflection\SourceLocator\PhpVersionBlacklistSourceLocator; -use PHPStan\Reflection\BetterReflection\SourceLocator\SkipClassAliasSourceLocator; +use Phar; +use PhpParser\Parser; use PHPStan\BetterReflection\SourceLocator\Ast\Locator; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber; -use PHPStan\BetterReflection\SourceLocator\SourceStubber\SourceStubber; use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator; +use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping; use PHPStan\BetterReflection\SourceLocator\Type\EvaledCodeSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadFunctionsSourceLocator; +use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator; +use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker; +use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; +use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository; +use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedPsrAutoloaderLocatorFactory; +use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository; +use PHPStan\Reflection\BetterReflection\SourceLocator\PhpVersionBlacklistSourceLocator; +use PHPStan\Reflection\BetterReflection\SourceLocator\ReflectionClassSourceLocator; +use PHPStan\Reflection\BetterReflection\SourceLocator\RewriteClassAliasSourceLocator; +use PHPStan\Reflection\BetterReflection\SourceLocator\SkipClassAliasSourceLocator; +use function array_merge; +use function array_unique; +use function extension_loaded; +use function is_dir; +use function is_file; -class BetterReflectionSourceLocatorFactory +final class BetterReflectionSourceLocatorFactory { - /** @var \PhpParser\Parser */ - private $parser; - - /** @var \PhpParser\Parser */ - private $php8Parser; - - /** @var PhpStormStubsSourceStubber */ - private $phpstormStubsSourceStubber; - - /** @var ReflectionSourceStubber */ - private $reflectionSourceStubber; - - /** @var \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository */ - private $optimizedSingleFileSourceLocatorRepository; - - /** @var \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository */ - private $optimizedDirectorySourceLocatorRepository; - - /** @var ComposerJsonAndInstalledJsonSourceLocatorMaker */ - private $composerJsonAndInstalledJsonSourceLocatorMaker; - - /** @var AutoloadSourceLocator */ - private $autoloadSourceLocator; - - /** @var string[] */ - private $scanFiles; - - /** @var string[] */ - private $scanDirectories; - - /** @var string[] */ - private $analysedPaths; - - /** @var string[] */ - private $composerAutoloaderProjectPaths; - - /** @var string[] */ - private $analysedPathsFromConfig; - - /** @var string|null */ - private $singleReflectionFile; - - /** @var string[] */ - private array $staticReflectionClassNamePatterns; - /** * @param string[] $scanFiles * @param string[] $scanDirectories * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths * @param string[] $analysedPathsFromConfig - * @param string|null $singleReflectionFile, - * @param string[] $staticReflectionClassNamePatterns */ public function __construct( - \PhpParser\Parser $parser, - \PhpParser\Parser $php8Parser, - PhpStormStubsSourceStubber $phpstormStubsSourceStubber, - ReflectionSourceStubber $reflectionSourceStubber, - OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, - OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, - ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, - AutoloadSourceLocator $autoloadSourceLocator, - array $scanFiles, - array $scanDirectories, - array $analysedPaths, - array $composerAutoloaderProjectPaths, - array $analysedPathsFromConfig, - ?string $singleReflectionFile, - array $staticReflectionClassNamePatterns + private Parser $parser, + private Parser $php8Parser, + private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, + private ReflectionSourceStubber $reflectionSourceStubber, + private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, + private OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, + private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, + private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, + private FileNodesFetcher $fileNodesFetcher, + private array $scanFiles, + private array $scanDirectories, + private array $analysedPaths, + private array $composerAutoloaderProjectPaths, + private array $analysedPathsFromConfig, + private bool $playgroundMode, // makes all PHPStan classes in the PHAR discoverable with PSR-4 ) { - $this->parser = $parser; - $this->php8Parser = $php8Parser; - $this->phpstormStubsSourceStubber = $phpstormStubsSourceStubber; - $this->reflectionSourceStubber = $reflectionSourceStubber; - $this->optimizedSingleFileSourceLocatorRepository = $optimizedSingleFileSourceLocatorRepository; - $this->optimizedDirectorySourceLocatorRepository = $optimizedDirectorySourceLocatorRepository; - $this->composerJsonAndInstalledJsonSourceLocatorMaker = $composerJsonAndInstalledJsonSourceLocatorMaker; - $this->autoloadSourceLocator = $autoloadSourceLocator; - $this->scanFiles = $scanFiles; - $this->scanDirectories = $scanDirectories; - $this->analysedPaths = $analysedPaths; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - $this->analysedPathsFromConfig = $analysedPathsFromConfig; - $this->singleReflectionFile = $singleReflectionFile; - $this->staticReflectionClassNamePatterns = $staticReflectionClassNamePatterns; } public function create(): SourceLocator { $locators = []; - if ($this->singleReflectionFile !== null) { - $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($this->singleReflectionFile); - } + $astLocator = new Locator($this->parser); + $locators[] = new AutoloadFunctionsSourceLocator( + new AutoloadSourceLocator($this->fileNodesFetcher, false), + new ReflectionClassSourceLocator( + $astLocator, + $this->reflectionSourceStubber, + ), + ); $analysedDirectories = []; $analysedFiles = []; @@ -140,29 +89,48 @@ public function create(): SourceLocator $analysedDirectories[] = $analysedPath; } + $fileLocators = []; $analysedFiles = array_unique(array_merge($analysedFiles, $this->scanFiles)); foreach ($analysedFiles as $analysedFile) { - $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($analysedFile); + $fileLocators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($analysedFile); } $directories = array_unique(array_merge($analysedDirectories, $this->scanDirectories)); foreach ($directories as $directory) { - $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($directory); + $fileLocators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($directory); } - $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); foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { $locator = $this->composerJsonAndInstalledJsonSourceLocatorMaker->create($composerAutoloaderProjectPath); if ($locator === null) { continue; } - $locators[] = $locator; + $fileLocators[] = $locator; } - $locators[] = new ClassWhitelistSourceLocator($this->autoloadSourceLocator, $this->staticReflectionClassNamePatterns); + + if (extension_loaded('phar')) { + $pharProtocolPath = Phar::running(); + if ($pharProtocolPath !== '') { + $mappings = [ + 'PHPStan\\BetterReflection\\' => [$pharProtocolPath . '/vendor/ondrejmirtes/better-reflection/src/'], + ]; + if ($this->playgroundMode) { + $mappings['PHPStan\\'] = [$pharProtocolPath . '/src/']; + } else { + $mappings['PHPStan\\Testing\\'] = [$pharProtocolPath . '/src/Testing/']; + } + $fileLocators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( + Psr4Mapping::fromArrayMappings($mappings), + ); + } + } + + $locators[] = new RewriteClassAliasSourceLocator(new AggregateSourceLocator($fileLocators)); + $locators[] = new SkipClassAliasSourceLocator(new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber)); + + $locators[] = new AutoloadSourceLocator($this->fileNodesFetcher, true); $locators[] = new PhpVersionBlacklistSourceLocator(new PhpInternalSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); $locators[] = new PhpVersionBlacklistSourceLocator(new EvaledCodeSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); diff --git a/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php new file mode 100644 index 0000000000..7070fc318b --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php @@ -0,0 +1,58 @@ +isClass()) { + return null; + } + + $className = $identifier->getName(); + if (class_exists($className, false) || interface_exists($className, false) || trait_exists($className, false)) { + return null; + } + + $autoloadFunctions = autoloadFunctions(); + foreach ($autoloadFunctions as $autoloadFunction) { + $autoloadFunction($className); + $reflection = $this->autoloadSourceLocator->locateIdentifier($reflector, $identifier); + if ($reflection !== null) { + return $reflection; + } + + $reflection = $this->reflectionClassSourceLocator->locateIdentifier($reflector, $identifier); + if ($reflection !== null) { + return $reflection; + } + } + + return null; + } + + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array + { + return []; + } + +} diff --git a/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php index aedb8f39f0..7506682426 100644 --- a/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php @@ -2,11 +2,11 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\Const_; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; use PHPStan\BetterReflection\Reflection\Reflection; @@ -15,7 +15,10 @@ use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Reflection\ConstantNameHelper; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ConstantTypeHelper; use ReflectionClass; use ReflectionFunction; use function array_key_exists; @@ -28,6 +31,7 @@ use function interface_exists; use function is_file; use function is_string; +use function opcache_invalidate; use function restore_error_handler; use function set_error_handler; use function spl_autoload_functions; @@ -43,25 +47,23 @@ * * Modified code from Roave/BetterReflection, Copyright (c) 2017 Roave, LLC. */ -class AutoloadSourceLocator implements SourceLocator +final class AutoloadSourceLocator implements SourceLocator { - /** @var array>> */ - private array $classNodes = []; - - /** @var array */ - private array $classReflections = []; - - /** @var array> */ - private array $functionNodes = []; - - /** @var array> */ - private array $constantNodes = []; + /** @var array{classes: array, functions: array, constants: array} */ + private array $presentSymbols = [ + 'classes' => [], + 'functions' => [], + 'constants' => [], + ]; /** @var array */ - private array $fetchedNodesByFile = []; + private array $scannedFiles = []; - public function __construct(private FileNodesFetcher $fileNodesFetcher, private bool $disableRuntimeReflectionProvider) + /** @var array */ + private array $startLineByClass = []; + + public function __construct(private FileNodesFetcher $fileNodesFetcher, private bool $executeAutoloadersInFileReadTrap) { } @@ -70,14 +72,8 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): if ($identifier->isFunction()) { $functionName = $identifier->getName(); $loweredFunctionName = strtolower($functionName); - if (array_key_exists($loweredFunctionName, $this->functionNodes)) { - $nodeToReflection = new NodeToReflection(); - return $nodeToReflection->__invoke( - $reflector, - $this->functionNodes[$loweredFunctionName]->getNode(), - $this->functionNodes[$loweredFunctionName]->getLocatedSource(), - $this->functionNodes[$loweredFunctionName]->getNamespace(), - ); + if (array_key_exists($loweredFunctionName, $this->presentSymbols['functions'])) { + return $this->findReflection($reflector, $this->presentSymbols['functions'][$loweredFunctionName], $identifier, null); } if (!function_exists($functionName)) { return null; @@ -97,62 +93,31 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } if ($identifier->isConstant()) { - $constantName = $identifier->getName(); - $nodeToReflection = new NodeToReflection(); - foreach ($this->constantNodes as $stmtConst) { - if ($stmtConst->getNode() instanceof FuncCall) { - $constantReflection = $nodeToReflection->__invoke( - $reflector, - $stmtConst->getNode(), - $stmtConst->getLocatedSource(), - $stmtConst->getNamespace(), - ); - if (!$constantReflection instanceof ReflectionConstant) { - throw new ShouldNotHappenException(); - } - if ($constantReflection->getName() !== $identifier->getName()) { - continue; - } - - return $constantReflection; - } - - foreach (array_keys($stmtConst->getNode()->consts) as $i) { - $constantReflection = $nodeToReflection->__invoke( - $reflector, - $stmtConst->getNode(), - $stmtConst->getLocatedSource(), - $stmtConst->getNamespace(), - $i, - ); - if (!$constantReflection instanceof ReflectionConstant) { - throw new ShouldNotHappenException(); - } - if ($constantReflection->getName() !== $identifier->getName()) { - continue; - } - - return $constantReflection; - } + $constantName = ConstantNameHelper::normalize($identifier->getName()); + if (array_key_exists($constantName, $this->presentSymbols['constants'])) { + return $this->findReflection($reflector, $this->presentSymbols['constants'][$constantName], $identifier, null); } if (!defined($constantName)) { return null; } - $reflection = ReflectionConstant::createFromNode( + $constantValue = @constant($constantName); + return ReflectionConstant::createFromNode( $reflector, new FuncCall(new Name('define'), [ new Arg(new String_($constantName)), - new Arg(new String_('')), // not actually used + new Arg(new TypeExpr(ConstantTypeHelper::getTypeFromValue($constantValue))), + ], [ + 'startLine' => 1, + 'endLine' => 1, + 'startFilePos' => 1, + 'endFilePos' => 4, ]), - new LocatedSource('', $constantName, null), + new LocatedSource('populateValue(@constant($constantName)); - - return $reflection; } if (!$identifier->isClass()) { @@ -160,61 +125,81 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } $loweredClassName = strtolower($identifier->getName()); - if (array_key_exists($loweredClassName, $this->classReflections)) { - return $this->classReflections[$loweredClassName]; + if (array_key_exists($loweredClassName, $this->presentSymbols['classes'])) { + $startLine = null; + if (array_key_exists($loweredClassName, $this->startLineByClass)) { + $startLine = $this->startLineByClass[$loweredClassName]; + } else { + $reflection = $this->getReflectionClass($identifier->getName()); + if ( + $reflection !== null + && $reflection->getStartLine() !== false + && is_string($reflection->getFileName()) + && is_file($reflection->getFileName()) + && $reflection->getFileName() === $this->presentSymbols['classes'][$loweredClassName] + ) { + $startLine = $reflection->getStartLine(); + } + } + return $this->findReflection($reflector, $this->presentSymbols['classes'][$loweredClassName], $identifier, $startLine); } - $locateResult = $this->locateClassByName($identifier->getName()); if ($locateResult === null) { - if (array_key_exists($loweredClassName, $this->classNodes)) { - foreach ($this->classNodes[$loweredClassName] as $classNode) { - $nodeToReflection = new NodeToReflection(); - return $this->classReflections[$loweredClassName] = $nodeToReflection->__invoke( - $reflector, - $classNode->getNode(), - $classNode->getLocatedSource(), - $classNode->getNamespace(), - ); - } - } return null; } - [$potentiallyLocatedFile, $className, $startLine] = $locateResult; + [$potentiallyLocatedFiles, $className, $startLine] = $locateResult; + if ($startLine !== null) { + $this->startLineByClass[strtolower($className)] = $startLine; + } + + $newIdentifier = new Identifier($className, $identifier->getType()); - return $this->findReflection($reflector, $potentiallyLocatedFile, new Identifier($className, $identifier->getType()), $startLine); + foreach ($potentiallyLocatedFiles as $potentiallyLocatedFile) { + $reflection = $this->findReflection($reflector, $potentiallyLocatedFile, $newIdentifier, $startLine); + if ($reflection === null) { + continue; + } + + return $reflection; + } + + return null; } private function findReflection(Reflector $reflector, string $file, Identifier $identifier, ?int $startLine): ?Reflection { - if (!array_key_exists($file, $this->fetchedNodesByFile)) { - $result = $this->fileNodesFetcher->fetchNodes($file); - foreach ($result->getClassNodes() as $className => $fetchedClassNodes) { - foreach ($fetchedClassNodes as $fetchedClassNode) { - $this->classNodes[$className][] = $fetchedClassNode; + $result = $this->fileNodesFetcher->fetchNodes($file); + if (!array_key_exists($file, $this->scannedFiles)) { + foreach (array_keys($result->getClassNodes()) as $className) { + if (array_key_exists($className, $this->presentSymbols['classes'])) { + continue; } + $this->presentSymbols['classes'][$className] = $file; } - foreach ($result->getFunctionNodes() as $functionName => $fetchedFunctionNode) { - $this->functionNodes[$functionName] = $fetchedFunctionNode; + foreach (array_keys($result->getFunctionNodes()) as $functionName) { + if (array_key_exists($functionName, $this->presentSymbols['functions'])) { + continue; + } + $this->presentSymbols['functions'][$functionName] = $file; } - foreach ($result->getConstantNodes() as $fetchedConstantNode) { - $this->constantNodes[] = $fetchedConstantNode; + foreach (array_keys($result->getConstantNodes()) as $constantName) { + if (array_key_exists($constantName, $this->presentSymbols['constants'])) { + continue; + } + $this->presentSymbols['constants'][$constantName] = $file; } - - $this->fetchedNodesByFile[$file] = true; + $this->scannedFiles[$file] = true; } $nodeToReflection = new NodeToReflection(); if ($identifier->isClass()) { $identifierName = strtolower($identifier->getName()); - if (array_key_exists($identifierName, $this->classReflections)) { - return $this->classReflections[$identifierName]; - } - if (!array_key_exists($identifierName, $this->classNodes)) { + if (!array_key_exists($identifierName, $result->getClassNodes())) { return null; } - $classNodesCount = count($this->classNodes[$identifierName]); - foreach ($this->classNodes[$identifierName] as $classNode) { + $classNodesCount = count($result->getClassNodes()[$identifierName]); + foreach ($result->getClassNodes()[$identifierName] as $classNode) { if ($classNodesCount > 1 && $startLine !== null) { if (count($classNode->getNode()->attrGroups) > 0 && PHP_VERSION_ID < 80000) { $startLine--; @@ -224,7 +209,7 @@ private function findReflection(Reflector $reflector, string $file, Identifier $ } } - return $this->classReflections[$identifierName] = $nodeToReflection->__invoke( + return $nodeToReflection->__invoke( $reflector, $classNode->getNode(), $classNode->getLocatedSource(), @@ -236,16 +221,58 @@ private function findReflection(Reflector $reflector, string $file, Identifier $ } if ($identifier->isFunction()) { $identifierName = strtolower($identifier->getName()); - if (!array_key_exists($identifierName, $this->functionNodes)) { + if (!array_key_exists($identifierName, $result->getFunctionNodes())) { return null; } - return $nodeToReflection->__invoke( - $reflector, - $this->functionNodes[$identifierName]->getNode(), - $this->functionNodes[$identifierName]->getLocatedSource(), - $this->functionNodes[$identifierName]->getNamespace(), - ); + foreach ($result->getFunctionNodes()[$identifierName] as $functionNode) { + return $nodeToReflection->__invoke( + $reflector, + $functionNode->getNode(), + $functionNode->getLocatedSource(), + $functionNode->getNamespace(), + ); + } + } + + if ($identifier->isConstant()) { + $identifierName = ConstantNameHelper::normalize($identifier->getName()); + $constantNodes = $result->getConstantNodes(); + + if (!array_key_exists($identifierName, $constantNodes)) { + return null; + } + + foreach ($constantNodes[$identifierName] as $fetchedConstantNode) { + $constantNode = $fetchedConstantNode->getNode(); + + $positionInNode = null; + if ($constantNode instanceof Const_) { + foreach ($constantNode->consts as $constPosition => $const) { + if ($const->namespacedName === null) { + throw new ShouldNotHappenException(); + } + + if (ConstantNameHelper::normalize($const->namespacedName->toString()) === $identifierName) { + /** @var int $positionInNode */ + $positionInNode = $constPosition; + break; + } + } + + if ($positionInNode === null) { + throw new ShouldNotHappenException(); + } + } + + return $nodeToReflection->__invoke( + $reflector, + $constantNode, + $fetchedConstantNode->getLocatedSource(), + $fetchedConstantNode->getNamespace(), + $positionInNode, + ); + } } return null; @@ -256,6 +283,18 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id return []; } + /** + * @return ReflectionClass|null + */ + private function getReflectionClass(string $className): ?ReflectionClass + { + if (class_exists($className, false) || interface_exists($className, false) || trait_exists($className, false)) { + return new ReflectionClass($className); + } + + return null; + } + /** * Attempt to locate a class by name. * @@ -269,34 +308,31 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id * that it cannot find the file, so we squelch the errors by overriding the * error handler temporarily. * - * @return array{string, string, int|null}|null + * @return array{string[], string, int|null}|null */ private function locateClassByName(string $className): ?array { - if (class_exists($className, !$this->disableRuntimeReflectionProvider) || interface_exists($className, !$this->disableRuntimeReflectionProvider) || trait_exists($className, !$this->disableRuntimeReflectionProvider)) { - $reflection = new ReflectionClass($className); + $reflection = $this->getReflectionClass($className); + if ($reflection !== null) { $filename = $reflection->getFileName(); - if (!is_string($filename)) { return null; } - if (!is_file($filename)) { return null; } - return [$filename, $reflection->getName(), $reflection->getStartLine() !== false ? $reflection->getStartLine() : null]; + return [[$filename], $reflection->getName(), $reflection->getStartLine() !== false ? $reflection->getStartLine() : null]; } - if (!$this->disableRuntimeReflectionProvider) { + if (!$this->executeAutoloadersInFileReadTrap) { return null; } $this->silenceErrors(); try { - /** @var array{string, string, null}|null */ - return FileReadTrapStreamWrapper::withStreamWrapperOverride( + $result = FileReadTrapStreamWrapper::withStreamWrapperOverride( static function () use ($className): ?array { $functions = spl_autoload_functions(); if ($functions === false) { @@ -312,14 +348,27 @@ static function () use ($className): ?array { * * This will not be `null` when the autoloader tried to read a file. */ - if (FileReadTrapStreamWrapper::$autoloadLocatedFile !== null) { - return [FileReadTrapStreamWrapper::$autoloadLocatedFile, $className, null]; + if (FileReadTrapStreamWrapper::$autoloadLocatedFiles !== []) { + return [FileReadTrapStreamWrapper::$autoloadLocatedFiles, $className, null]; } } return null; }, ); + if ($result === null) { + return null; + } + + if (!function_exists('opcache_invalidate')) { + return $result; + } + + foreach ($result[0] as $file) { + opcache_invalidate($file, true); + } + + return $result; } finally { restore_error_handler(); } diff --git a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php index c0ba0501f0..6eb1f57604 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php @@ -2,19 +2,17 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PhpParser\BuilderHelpers; use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; -use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor; 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 PHPStan\Reflection\ConstantNameHelper; use function strtolower; -class CachingVisitor extends NodeVisitorAbstract +final class CachingVisitor extends NodeVisitorAbstract { private string $fileName; @@ -24,10 +22,10 @@ class CachingVisitor extends NodeVisitorAbstract /** @var array>> */ private array $classNodes; - /** @var array> */ + /** @var array>> */ private array $functionNodes; - /** @var array> */ + /** @var array>> */ private array $constantNodes; private ?Node\Stmt\Namespace_ $currentNamespaceNode = null; @@ -36,6 +34,8 @@ public function enterNode(Node $node): ?int { if ($node instanceof Namespace_) { $this->currentNamespaceNode = $node; + + return null; } if ($node instanceof Node\Stmt\ClassLike) { @@ -47,37 +47,40 @@ public function enterNode(Node $node): ?int $this->classNodes[strtolower($fullClassName)][] = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName, new LocatedSource($this->contents, $fullClassName, $this->fileName), ); } - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } if ($node instanceof Node\Stmt\Function_) { if ($node->namespacedName !== null) { $functionName = $node->namespacedName->toString(); - $this->functionNodes[strtolower($functionName)] = new FetchedNode( + $this->functionNodes[strtolower($functionName)][] = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName, new LocatedSource($this->contents, $functionName, $this->fileName), ); } - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } if ($node instanceof Node\Stmt\Const_) { - $this->constantNodes[] = new FetchedNode( - $node, - $this->currentNamespaceNode, - $this->fileName, - new LocatedSource($this->contents, null, $this->fileName), - ); + foreach ($node->consts as $const) { + if ($const->namespacedName === null) { + continue; + } - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + $this->constantNodes[ConstantNameHelper::normalize($const->namespacedName->toString())][] = new FetchedNode( + $node, + $this->currentNamespaceNode, + new LocatedSource($this->contents, null, $this->fileName), + ); + } + + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } if ($node instanceof Node\Expr\FuncCall) { @@ -91,20 +94,14 @@ public function enterNode(Node $node): ?int $nameNode = $node->getArgs()[0]->value; $constantName = $nameNode->value; - if (defined($constantName)) { - $constantValue = @constant($constantName); - $node->getArgs()[1]->value = BuilderHelpers::normalizeValue($constantValue); - } - $constantNode = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName, new LocatedSource($this->contents, $constantName, $this->fileName), ); - $this->constantNodes[] = $constantNode; + $this->constantNodes[ConstantNameHelper::normalize($constantName)][] = $constantNode; - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } return null; @@ -132,7 +129,7 @@ public function getClassNodes(): array } /** - * @return array> + * @return array>> */ public function getFunctionNodes(): array { @@ -140,7 +137,7 @@ public function getFunctionNodes(): array } /** - * @return array> + * @return array>> */ public function getConstantNodes(): array { diff --git a/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php deleted file mode 100644 index 08972e2a6b..0000000000 --- a/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php +++ /dev/null @@ -1,43 +0,0 @@ -isClass()) { - foreach ($this->patterns as $pattern) { - if (Strings::match($identifier->getName(), $pattern) !== null) { - return null; - } - } - } - - return $this->sourceLocator->locateIdentifier($reflector, $identifier); - } - - public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array - { - return $this->sourceLocator->locateIdentifiersByType($reflector, $identifierType); - } - -} diff --git a/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php deleted file mode 100644 index 3dd35fe193..0000000000 --- a/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php +++ /dev/null @@ -1,45 +0,0 @@ -isClass()) { - foreach ($this->patterns as $pattern) { - if (Strings::match($identifier->getName(), $pattern) !== null) { - return $this->sourceLocator->locateIdentifier($reflector, $identifier); - } - } - - return null; - } - - return $this->sourceLocator->locateIdentifier($reflector, $identifier); - } - - public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array - { - return $this->sourceLocator->locateIdentifiersByType($reflector, $identifierType); - } - -} diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index 93172ebe37..9e687100f7 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -10,47 +10,50 @@ use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileReader; -use function array_filter; +use PHPStan\Internal\ComposerHelper; +use PHPStan\Php\PhpVersion; use function array_key_exists; use function array_map; use function array_merge; use function array_merge_recursive; +use function array_reverse; use function count; use function dirname; +use function glob; use function is_dir; use function is_file; +use function str_contains; +use const GLOB_ONLYDIR; -class ComposerJsonAndInstalledJsonSourceLocatorMaker +final class ComposerJsonAndInstalledJsonSourceLocatorMaker { public function __construct( private OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, private OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory, + private PhpVersion $phpVersion, ) { } public function create(string $projectInstallationPath): ?SourceLocator { - $composerJsonPath = $projectInstallationPath . '/composer.json'; - if (!is_file($composerJsonPath)) { + $composer = ComposerHelper::getComposerConfig($projectInstallationPath); + + if ($composer === null) { return null; } - $installedJsonPath = $projectInstallationPath . '/vendor/composer/installed.json'; + + $vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig($projectInstallationPath, $composer); + + $installedJsonPath = $vendorDirectory . '/composer/installed.json'; if (!is_file($installedJsonPath)) { return null; } $installedJsonDirectoryPath = dirname($installedJsonPath); - try { - $composerJsonContents = FileReader::read($composerJsonPath); - $composer = Json::decode($composerJsonContents, Json::FORCE_ARRAY); - } catch (CouldNotReadFileException | JsonException) { - return null; - } - try { $installedJsonContents = FileReader::read($installedJsonPath); $installedJson = Json::decode($installedJsonContents, Json::FORCE_ARRAY); @@ -66,17 +69,15 @@ public function create(string $projectInstallationPath): ?SourceLocator $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), + $this->packagePrefixPath($installedJsonDirectoryPath, $package, $vendorDirectory), ), $installed), ); - $classMapFiles = array_filter($classMapPaths, 'is_file'); - $classMapDirectories = array_filter($classMapPaths, 'is_dir'); $filePaths = array_merge( $this->prefixPaths($this->packageToFilePaths($composer), $projectInstallationPath . '/'), $dev ? $this->prefixPaths($this->packageToFilePaths($composer, 'autoload-dev'), $projectInstallationPath . '/') : [], ...array_map(fn (array $package): array => $this->prefixPaths( $this->packageToFilePaths($package), - $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package), + $this->packagePrefixPath($installedJsonDirectoryPath, $package, $vendorDirectory), ), $installed), ); @@ -87,9 +88,9 @@ public function create(string $projectInstallationPath): ?SourceLocator $dev ? $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer, 'autoload-dev'), $projectInstallationPath) : [], ...array_map(fn (array $package): array => $this->prefixWithPackagePath( $this->packageToPsr4AutoloadNamespaces($package), - $projectInstallationPath, $installedJsonDirectoryPath, $package, + $vendorDirectory, ), $installed), )), ); @@ -100,23 +101,25 @@ public function create(string $projectInstallationPath): ?SourceLocator $dev ? $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer, 'autoload-dev'), $projectInstallationPath) : [], ...array_map(fn (array $package): array => $this->prefixWithPackagePath( $this->packageToPsr0AutoloadNamespaces($package), - $projectInstallationPath, $installedJsonDirectoryPath, $package, + $vendorDirectory, ), $installed), )), ); - foreach ($classMapDirectories as $classMapDirectory) { - if (!is_dir($classMapDirectory)) { + $files = []; + foreach ($classMapPaths as $classMapPath) { + if (is_dir($classMapPath)) { + $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($classMapPath); continue; } - $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($classMapDirectory); + if (!is_file($classMapPath)) { + continue; + } + $files[] = $classMapPath; } - - $files = []; - - foreach (array_merge($classMapFiles, $filePaths) as $file) { + foreach ($filePaths as $file) { if (!is_file($file)) { continue; } @@ -127,6 +130,39 @@ public function create(string $projectInstallationPath): ?SourceLocator $locators[] = $this->optimizedDirectorySourceLocatorFactory->createByFiles($files); } + $binDir = ComposerHelper::getBinDirFromComposerConfig($projectInstallationPath, $composer); + $phpunitBridgeDir = $binDir . '/.phpunit'; + if (!is_dir($vendorDirectory . '/phpunit/phpunit') && is_dir($phpunitBridgeDir)) { + // from https://github.com/composer/composer/blob/8ff237afb61b8766efa576b8ae1cc8560c8aed96/phpstan/locate-phpunit-autoloader.php + $bestDirFound = null; + $phpunitBridgeDirectories = glob($phpunitBridgeDir . '/phpunit-*', GLOB_ONLYDIR); + if ($phpunitBridgeDirectories !== false) { + foreach (array_reverse($phpunitBridgeDirectories) as $dir) { + $bestDirFound = $dir; + if ($this->phpVersion->getVersionId() >= 80100 && str_contains($dir, 'phpunit-10')) { + break; + } + if ($this->phpVersion->getVersionId() >= 80000) { + if (str_contains($dir, 'phpunit-9')) { + break; + } + continue; + } + + if (str_contains($dir, 'phpunit-8') || str_contains($dir, 'phpunit-7')) { + break; + } + } + + if ($bestDirFound !== null) { + $phpunitBridgeLocator = $this->create($bestDirFound); + if ($phpunitBridgeLocator !== null) { + $locators[] = $phpunitBridgeLocator; + } + } + } + } + return new AggregateSourceLocator($locators); } @@ -174,16 +210,16 @@ private function packageToFilePaths(array $package, string $autoloadSection = 'a * @param mixed[] $package */ private function packagePrefixPath( - string $projectInstallationPath, string $installedJsonDirectoryPath, array $package, + string $vendorDirectory, ): string { if (array_key_exists('install-path', $package)) { return $installedJsonDirectoryPath . '/' . $package['install-path'] . '/'; } - return $projectInstallationPath . '/vendor/' . $package['name'] . '/'; + return $vendorDirectory . '/' . $package['name'] . '/'; } /** @@ -192,9 +228,9 @@ private function packagePrefixPath( * * @return array> */ - private function prefixWithPackagePath(array $paths, string $projectInstallationPath, string $installedJsonDirectoryPath, array $package): array + private function prefixWithPackagePath(array $paths, string $installedJsonDirectoryPath, array $package, string $vendorDirectory): array { - $prefix = $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package); + $prefix = $this->packagePrefixPath($installedJsonDirectoryPath, $package, $vendorDirectory); return array_map(fn (array $paths): array => $this->prefixPaths($paths, $prefix), $paths); } diff --git a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php index 5af9211c14..70eaadffbe 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php +++ b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php @@ -8,7 +8,7 @@ /** * @template-covariant T of Node */ -class FetchedNode +final class FetchedNode { /** @@ -17,7 +17,6 @@ class FetchedNode public function __construct( private Node $node, private ?Node\Stmt\Namespace_ $namespace, - private string $fileName, private LocatedSource $locatedSource, ) { @@ -36,11 +35,6 @@ public function getNamespace(): ?Node\Stmt\Namespace_ return $this->namespace; } - public function getFileName(): string - { - return $this->fileName; - } - public function getLocatedSource(): LocatedSource { return $this->locatedSource; diff --git a/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php b/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php index af65371f20..ac90178d49 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php +++ b/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php @@ -4,13 +4,13 @@ use PhpParser\Node; -class FetchedNodesResult +final class FetchedNodesResult { /** * @param array>> $classNodes - * @param array> $functionNodes - * @param array> $constantNodes + * @param array>> $functionNodes + * @param array>> $constantNodes */ public function __construct( private array $classNodes, @@ -29,7 +29,7 @@ public function getClassNodes(): array } /** - * @return array> + * @return array>> */ public function getFunctionNodes(): array { @@ -37,7 +37,7 @@ public function getFunctionNodes(): array } /** - * @return array> + * @return array>> */ public function getConstantNodes(): array { diff --git a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php index bd7f0c77dd..bd6892cf88 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php @@ -2,13 +2,12 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PhpParser\Node; use PhpParser\NodeTraverser; use PHPStan\File\FileReader; use PHPStan\Parser\Parser; use PHPStan\Parser\ParserErrorsException; -class FileNodesFetcher +final class FileNodesFetcher { public function __construct( @@ -26,7 +25,6 @@ public function fetchNodes(string $fileName): FetchedNodesResult $contents = FileReader::read($fileName); try { - /** @var Node[] $ast */ $ast = $this->parser->parseFile($fileName); } catch (ParserErrorsException) { return new FetchedNodesResult([], [], []); @@ -34,11 +32,15 @@ public function fetchNodes(string $fileName): FetchedNodesResult $this->cachingVisitor->reset($fileName, $contents); $nodeTraverser->traverse($ast); - return new FetchedNodesResult( + $result = new FetchedNodesResult( $this->cachingVisitor->getClassNodes(), $this->cachingVisitor->getFunctionNodes(), $this->cachingVisitor->getConstantNodes(), ); + + $this->cachingVisitor->reset($fileName, $contents); + + return $result; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php index e4c01a64b3..4a35d07ca2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php @@ -3,7 +3,10 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PHPStan\ShouldNotHappenException; +use function is_dir; +use function is_file; use function stat; +use function stream_resolve_include_path; use function stream_wrapper_register; use function stream_wrapper_restore; use function stream_wrapper_unregister; @@ -34,7 +37,8 @@ final class FileReadTrapStreamWrapper /** @var string[]|null */ private static ?array $registeredStreamWrapperProtocols; - public static ?string $autoloadLocatedFile = null; + /** @var string[] */ + public static array $autoloadLocatedFiles = []; private bool $readFromFile = false; @@ -55,7 +59,7 @@ public static function withStreamWrapperOverride( ) { self::$registeredStreamWrapperProtocols = $streamWrapperProtocols; - self::$autoloadLocatedFile = null; + self::$autoloadLocatedFiles = []; try { foreach ($streamWrapperProtocols as $protocol) { @@ -71,7 +75,7 @@ public static function withStreamWrapperOverride( } self::$registeredStreamWrapperProtocols = null; - self::$autoloadLocatedFile = null; + self::$autoloadLocatedFiles = []; return $result; } @@ -93,11 +97,15 @@ public static function withStreamWrapperOverride( */ public function stream_open($path, $mode, $options, &$openedPath): bool { - self::$autoloadLocatedFile = $path; + $exists = is_file($path) || (stream_resolve_include_path($path) !== false); + + if ($exists) { + self::$autoloadLocatedFiles[] = $path; + } $this->readFromFile = false; $this->seekPosition = 0; - return true; + return $exists; } /** @@ -137,11 +145,11 @@ public function stream_close(): void */ public function stream_stat() { - if (self::$autoloadLocatedFile === null) { + if (self::$autoloadLocatedFiles === []) { return false; } - return $this->url_stat(self::$autoloadLocatedFile, STREAM_URL_STAT_QUIET); + return $this->url_stat(self::$autoloadLocatedFiles[0], STREAM_URL_STAT_QUIET); } /** @@ -161,6 +169,21 @@ public function stream_stat() * @return mixed[]|bool */ public function url_stat($path, $flags) + { + return $this->invokeWithRealFileStreamWrapper(static function ($path, $flags) { + if (($flags & STREAM_URL_STAT_QUIET) !== 0) { + return @stat($path); + } + + return stat($path); + }, [$path, $flags]); + } + + /** + * @param mixed[] $args + * @return mixed + */ + private function invokeWithRealFileStreamWrapper(callable $cb, array $args) { if (self::$registeredStreamWrapperProtocols === null) { throw new ShouldNotHappenException(self::class . ' not registered: cannot operate. Do not call this method directly.'); @@ -170,11 +193,7 @@ public function url_stat($path, $flags) stream_wrapper_restore($protocol); } - if (($flags & STREAM_URL_STAT_QUIET) !== 0) { - $result = @stat($path); - } else { - $result = stat($path); - } + $result = $cb(...$args); foreach (self::$registeredStreamWrapperProtocols as $protocol) { stream_wrapper_unregister($protocol); @@ -241,4 +260,14 @@ public function stream_set_option($option, $arg1, $arg2): bool return false; } + public function dir_opendir(string $path, int $options): bool + { + return is_dir($path); + } + + public function dir_readdir(): string + { + return ''; + } + } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php index 8c21b5983b..b56a981bab 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -9,112 +9,105 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ConstantNameHelper; 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 array_values; +use function current; use function strtolower; -class OptimizedDirectorySourceLocator implements SourceLocator +final class OptimizedDirectorySourceLocator implements SourceLocator { - private PhpFileCleaner $cleaner; - - private string $extraTypes; - - /** @var array|null */ - private ?array $classToFile = null; - - /** @var array>|null */ - private ?array $functionToFiles = null; - - /** @var array> */ - private array $classNodes = []; - - /** @var array> */ - private array $functionNodes = []; - /** - * @param string[] $files + * @param array $classToFile + * @param array> $functionToFiles + * @param array $constantToFile */ public function __construct( private FileNodesFetcher $fileNodesFetcher, - private PhpVersion $phpVersion, - private array $files, + private array $classToFile, + private array $functionToFiles, + private array $constantToFile, ) { - $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 { if ($identifier->isClass()) { $className = strtolower($identifier->getName()); - if (array_key_exists($className, $this->classNodes)) { - return $this->nodeToReflection($reflector, $this->classNodes[$className]); - } - $file = $this->findFileByClass($className); if ($file === null) { return null; } - $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - foreach ($fetchedNodesResult->getClassNodes() as $identifierName => $fetchedClassNodes) { - foreach ($fetchedClassNodes as $fetchedClassNode) { - $this->classNodes[$identifierName] = $fetchedClassNode; - break; - } - } + $fetchedClassNodes = $this->fileNodesFetcher->fetchNodes($file)->getClassNodes(); - if (!array_key_exists($className, $this->classNodes)) { + if (!array_key_exists($className, $fetchedClassNodes)) { return null; } - return $this->nodeToReflection($reflector, $this->classNodes[$className]); + /** @var FetchedNode $fetchedClassNode */ + $fetchedClassNode = current($fetchedClassNodes[$className]); + + return $this->nodeToReflection($reflector, $fetchedClassNode); } if ($identifier->isFunction()) { $functionName = strtolower($identifier->getName()); - if (array_key_exists($functionName, $this->functionNodes)) { - return $this->nodeToReflection($reflector, $this->functionNodes[$functionName]); - } - $files = $this->findFilesByFunction($functionName); + + $fetchedFunctionNode = null; foreach ($files as $file) { - $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - foreach ($fetchedNodesResult->getFunctionNodes() as $identifierName => $fetchedFunctionNode) { - $this->functionNodes[$identifierName] = $fetchedFunctionNode; + $fetchedFunctionNodes = $this->fileNodesFetcher->fetchNodes($file)->getFunctionNodes(); + + if (!array_key_exists($functionName, $fetchedFunctionNodes)) { + continue; } + + /** @var FetchedNode $fetchedFunctionNode */ + $fetchedFunctionNode = current($fetchedFunctionNodes[$functionName]); + } + + if ($fetchedFunctionNode === null) { + return null; + } + + return $this->nodeToReflection($reflector, $fetchedFunctionNode); + } + + if ($identifier->isConstant()) { + $constantName = ConstantNameHelper::normalize($identifier->getName()); + $file = $this->findFileByConstant($constantName); + + if ($file === null) { + return null; } - if (!array_key_exists($functionName, $this->functionNodes)) { + $fetchedConstantNodes = $this->fileNodesFetcher->fetchNodes($file)->getConstantNodes(); + + if (!array_key_exists($constantName, $fetchedConstantNodes)) { return null; } - return $this->nodeToReflection($reflector, $this->functionNodes[$functionName]); + /** @var FetchedNode $fetchedConstantNode */ + $fetchedConstantNode = current($fetchedConstantNodes[$constantName]); + + return $this->nodeToReflection( + $reflector, + $fetchedConstantNode, + $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $constantName), + ); } return null; } /** - * @param FetchedNode|FetchedNode $fetchedNode + * @param FetchedNode|FetchedNode|FetchedNode $fetchedNode */ - private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode): Reflection + private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode, ?int $positionInNode = null): Reflection { $nodeToReflection = new NodeToReflection(); return $nodeToReflection->__invoke( @@ -122,18 +115,12 @@ private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode $fetchedNode->getNode(), $fetchedNode->getLocatedSource(), $fetchedNode->getNamespace(), + $positionInNode, ); } private function findFileByClass(string $className): ?string { - if ($this->classToFile === null) { - $this->init(); - if ($this->classToFile === null) { - throw new ShouldNotHappenException(); - } - } - if (!array_key_exists($className, $this->classToFile)) { return null; } @@ -141,18 +128,20 @@ private function findFileByClass(string $className): ?string return $this->classToFile[$className]; } + private function findFileByConstant(string $constantName): ?string + { + if (!array_key_exists($constantName, $this->constantToFile)) { + return null; + } + + return $this->constantToFile[$constantName]; + } + /** * @return string[] */ private function findFilesByFunction(string $functionName): array { - if ($this->functionToFiles === null) { - $this->init(); - if ($this->functionToFiles === null) { - throw new ShouldNotHappenException(); - } - } - if (!array_key_exists($functionName, $this->functionToFiles)) { return []; } @@ -160,95 +149,69 @@ private function findFilesByFunction(string $functionName): array return $this->functionToFiles[$functionName]; } - private function init(): void + /** + * @return list + */ + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { - $classToFile = []; - $functionToFiles = []; - foreach ($this->files as $file) { - $symbols = $this->findSymbols($file); - $classesInFile = $symbols['classes']; - $functionsInFile = $symbols['functions']; - foreach ($classesInFile as $classInFile) { - $classToFile[$classInFile] = $file; + $reflections = []; + if ($identifierType->isClass()) { + foreach ($this->classToFile as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getClassNodes() as $identifierName => $fetchedClassNodes) { + foreach ($fetchedClassNodes as $fetchedClassNode) { + $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedClassNode); + } + } } - foreach ($functionsInFile as $functionInFile) { - if (!array_key_exists($functionInFile, $functionToFiles)) { - $functionToFiles[$functionInFile] = []; + } elseif ($identifierType->isFunction()) { + foreach ($this->functionToFiles as $files) { + foreach ($files as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getFunctionNodes() as $identifierName => $fetchedFunctionNodes) { + foreach ($fetchedFunctionNodes as $fetchedFunctionNode) { + $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedFunctionNode); + continue 2; + } + } + } + } + } elseif ($identifierType->isConstant()) { + foreach ($this->constantToFile as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getConstantNodes() as $identifierName => $fetchedConstantNodes) { + foreach ($fetchedConstantNodes as $fetchedConstantNode) { + $reflections[$identifierName] = $this->nodeToReflection( + $reflector, + $fetchedConstantNode, + $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $identifierName), + ); + } } - $functionToFiles[$functionInFile][] = $file; } } - $this->classToFile = $classToFile; - $this->functionToFiles = $functionToFiles; + return array_values($reflections); } - /** - * Inspired by Composer\Autoload\ClassMapGenerator::findClasses() - * @link https://github.com/composer/composer/blob/45d3e133a4691eccb12e9cd6f9dfd76eddc1906d/src/Composer/Autoload/ClassMapGenerator.php#L216 - * - * @return array{classes: string[], functions: string[]} - */ - private function findSymbols(string $file): array + private function findConstantPositionInConstNode(Node\Stmt\Const_|Node\Expr\FuncCall $constantNode, string $constantName): ?int { - $contents = @php_strip_whitespace($file); - if ($contents === '') { - return ['classes' => [], 'functions' => []]; + if ($constantNode instanceof Node\Expr\FuncCall) { + return null; } - $matchResults = (bool) preg_match_all(sprintf('{\b(?:class|interface|trait|function%s)\s}i', $this->extraTypes), $contents, $matches); - if (!$matchResults) { - return ['classes' => [], 'functions' => []]; - } + /** @var int $position */ + foreach ($constantNode->consts as $position => $const) { + if ($const->namespacedName === null) { + throw new ShouldNotHappenException(); + } - $contents = $this->cleaner->clean($contents, count($matches[0])); - - preg_match_all(sprintf('{ - (?: - \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', $this->extraTypes), $contents, $matches); - - $classes = []; - $functions = []; - $namespace = ''; - - for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { - if (!empty($matches['ns'][$i])) { // phpcs:disable - $namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\'; - } else { - $name = $matches['name'][$i]; - // skip anon classes extending/implementing - if ($name === 'extends' || $name === 'implements') { - continue; - } - $namespacedName = strtolower(ltrim($namespace . $name, '\\')); - - $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; - } + if (ConstantNameHelper::normalize($const->namespacedName->toString()) === $constantName) { + return $position; } } - return [ - 'classes' => $classes, - 'functions' => $functions, - ]; - } - - public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array - { - return []; + throw new ShouldNotHappenException(); } } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php index ec6d1e60db..620284912f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php @@ -2,22 +2,88 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\Cache\Cache; use PHPStan\File\FileFinder; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ConstantNameHelper; +use function array_key_exists; +use function count; +use function in_array; +use function ltrim; +use function php_strip_whitespace; +use function preg_match_all; +use function preg_replace; +use function sha1_file; +use function sprintf; +use function strtolower; -class OptimizedDirectorySourceLocatorFactory +final class OptimizedDirectorySourceLocatorFactory { - public function __construct(private FileNodesFetcher $fileNodesFetcher, private FileFinder $fileFinder, private PhpVersion $phpVersion) + private PhpFileCleaner $cleaner; + + private string $extraTypes; + + public function __construct( + private FileNodesFetcher $fileNodesFetcher, + private FileFinder $fileFinder, + private PhpVersion $phpVersion, + private Cache $cache, + ) { + $this->extraTypes = $this->phpVersion->supportsEnums() ? '|enum' : ''; + $this->cleaner = new PhpFileCleaner(); } public function createByDirectory(string $directory): OptimizedDirectorySourceLocator { + $files = $this->fileFinder->findFiles([$directory])->getFiles(); + $fileHashes = []; + foreach ($files as $file) { + $hash = sha1_file($file); + if ($hash === false) { + continue; + } + $fileHashes[$file] = $hash; + } + + $cacheKey = sprintf('odsl-%s', $directory); + $variableCacheKey = 'v1'; + + /** @var array|null $cached */ + $cached = $this->cache->load($cacheKey, $variableCacheKey); + if ($cached !== null) { + foreach ($cached as $file => [$hash, $classes, $functions, $constants]) { + if (!array_key_exists($file, $fileHashes)) { + unset($cached[$file]); + continue; + } + $newHash = $fileHashes[$file]; + unset($fileHashes[$file]); + if ($hash === $newHash) { + continue; + } + + [$newClasses, $newFunctions, $newConstants] = $this->findSymbols($file); + $cached[$file] = [$newHash, $newClasses, $newFunctions, $newConstants]; + } + } else { + $cached = []; + } + + foreach ($fileHashes as $file => $newHash) { + [$newClasses, $newFunctions, $newConstants] = $this->findSymbols($file); + $cached[$file] = [$newHash, $newClasses, $newFunctions, $newConstants]; + } + $this->cache->save($cacheKey, $variableCacheKey, $cached); + + [$classToFile, $functionToFiles, $constantToFile] = $this->changeStructure($cached); + return new OptimizedDirectorySourceLocator( $this->fileNodesFetcher, - $this->phpVersion, - $this->fileFinder->findFiles([$directory])->getFiles(), + $classToFile, + $functionToFiles, + $constantToFile, ); } @@ -26,11 +92,125 @@ public function createByDirectory(string $directory): OptimizedDirectorySourceLo */ public function createByFiles(array $files): OptimizedDirectorySourceLocator { + $symbols = []; + foreach ($files as $file) { + [$newClasses, $newFunctions, $newConstants] = $this->findSymbols($file); + $symbols[$file] = ['', $newClasses, $newFunctions, $newConstants]; + } + + [$classToFile, $functionToFiles, $constantToFile] = $this->changeStructure($symbols); + return new OptimizedDirectorySourceLocator( $this->fileNodesFetcher, - $this->phpVersion, - $files, + $classToFile, + $functionToFiles, + $constantToFile, ); } + /** + * @param array $symbols + * @return array{array, array>, array} + */ + private function changeStructure(array $symbols): array + { + $classToFile = []; + $constantToFile = []; + $functionToFiles = []; + foreach ($symbols as $file => [, $classes, $functions, $constants]) { + foreach ($classes as $classInFile) { + $classToFile[$classInFile] = $file; + } + foreach ($functions as $functionInFile) { + if (!array_key_exists($functionInFile, $functionToFiles)) { + $functionToFiles[$functionInFile] = []; + } + $functionToFiles[$functionInFile][] = $file; + } + foreach ($constants as $constantInFile) { + $constantToFile[$constantInFile] = $file; + } + } + + return [ + $classToFile, + $functionToFiles, + $constantToFile, + ]; + } + + /** + * Inspired by Composer\Autoload\ClassMapGenerator::findClasses() + * @link https://github.com/composer/composer/blob/45d3e133a4691eccb12e9cd6f9dfd76eddc1906d/src/Composer/Autoload/ClassMapGenerator.php#L216 + * + * @return array{string[], string[], string[]} + */ + private function findSymbols(string $file): array + { + $contents = @php_strip_whitespace($file); + if ($contents === '') { + return [[], [], []]; + } + + $matchResults = (bool) preg_match_all(sprintf('{\b(?:(?:class|interface|trait|const|function%s)\s)|(?:define\s*\()}i', $this->extraTypes), $contents, $matches); + if (!$matchResults) { + return [[], [], []]; + } + + $contents = $this->cleaner->clean($contents, count($matches[0])); + + preg_match_all(sprintf('{ + (?: + \b(?])(?: + (?: (?Pclass|interface|trait%s) \s++ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) ) + | (?: (?Pfunction) \s++ (?:&\s*)? (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) \s*+ [&\(] ) + | (?: (?Pconst) \s++ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) \s*+ [^;] ) + | (?: (?:\\\)? (?Pdefine) \s*+ \( \s*+ [\'"] (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:[\\\\]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+) ) + | (?: (?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] ) + ) + ) + }ix', $this->extraTypes), $contents, $matches); + + $classes = []; + $functions = []; + $constants = []; + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (isset($matches['ns'][$i]) && $matches['ns'][$i] !== '') { + $namespace = preg_replace('~\s+~', '', strtolower($matches['nsname'][$i])) . '\\'; + continue; + } + + if ($matches['function'][$i] !== '') { + $functions[] = strtolower(ltrim($namespace . $matches['fname'][$i], '\\')); + continue; + } + + if ($matches['constant'][$i] !== '') { + $constants[] = ConstantNameHelper::normalize(ltrim($namespace . $matches['cname'][$i], '\\')); + } + + if ($matches['define'][$i] !== '') { + $constants[] = ConstantNameHelper::normalize($matches['dname'][$i]); + continue; + } + + $name = $matches['name'][$i]; + + // skip anon classes extending/implementing + if (in_array($name, ['extends', 'implements'], true)) { + continue; + } + + $classes[] = strtolower(ltrim($namespace . $name, '\\')); + } + + return [ + $classes, + $functions, + $constants, + ]; + } + } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php index 89c4ec0bcc..f71d4dcf8a 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php @@ -4,7 +4,7 @@ use function array_key_exists; -class OptimizedDirectorySourceLocatorRepository +final class OptimizedDirectorySourceLocatorRepository { /** @var array */ diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php index 36bd379882..78fb07f24d 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php @@ -10,9 +10,12 @@ use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; use function is_file; -class OptimizedPsrAutoloaderLocator implements SourceLocator +final class OptimizedPsrAutoloaderLocator implements SourceLocator { + /** @var array */ + private array $locators = []; + public function __construct( private PsrAutoloaderMapping $mapping, private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, @@ -22,16 +25,28 @@ public function __construct( public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { + foreach ($this->locators as $locator) { + $reflection = $locator->locateIdentifier($reflector, $identifier); + if ($reflection === null) { + continue; + } + + return $reflection; + } + foreach ($this->mapping->resolvePossibleFilePaths($identifier) as $file) { if (!is_file($file)) { continue; } - $reflection = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($file)->locateIdentifier($reflector, $identifier); + $locator = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($file); + $reflection = $locator->locateIdentifier($reflector, $identifier); if ($reflection === null) { continue; } + $this->locators[$file] = $locator; + return $reflection; } @@ -39,7 +54,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } /** - * @return array + * @return list */ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php index 92e6c154c3..4285d93bbd 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Stmt\Const_; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; use PHPStan\BetterReflection\Reflection\Reflection; @@ -12,15 +12,17 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Reflection\ConstantNameHelper; use PHPStan\ShouldNotHappenException; use function array_key_exists; use function array_keys; use function strtolower; -class OptimizedSingleFileSourceLocator implements SourceLocator +final class OptimizedSingleFileSourceLocator implements SourceLocator { - private ?FetchedNodesResult $fetchedNodesResult = null; + /** @var array{classes: array, functions: array, constants: array}|null */ + private ?array $presentSymbols = null; public function __construct( private FileNodesFetcher $fileNodesFetcher, @@ -31,12 +33,48 @@ public function __construct( public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { - if ($this->fetchedNodesResult === null) { - $this->fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($this->fileName); + if ($this->presentSymbols !== null) { + if ($identifier->isClass()) { + $className = strtolower($identifier->getName()); + if (!array_key_exists($className, $this->presentSymbols['classes'])) { + return null; + } + } + if ($identifier->isFunction()) { + $className = strtolower($identifier->getName()); + if (!array_key_exists($className, $this->presentSymbols['functions'])) { + return null; + } + } + if ($identifier->isConstant()) { + $constantName = ConstantNameHelper::normalize($identifier->getName()); + if (!array_key_exists($constantName, $this->presentSymbols['constants'])) { + return null; + } + } + } + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($this->fileName); + if ($this->presentSymbols === null) { + $presentSymbols = [ + 'classes' => [], + 'functions' => [], + 'constants' => [], + ]; + foreach (array_keys($fetchedNodesResult->getClassNodes()) as $className) { + $presentSymbols['classes'][$className] = true; + } + foreach (array_keys($fetchedNodesResult->getFunctionNodes()) as $functionName) { + $presentSymbols['functions'][$functionName] = true; + } + foreach (array_keys($fetchedNodesResult->getConstantNodes()) as $constantName) { + $presentSymbols['constants'][$constantName] = true; + } + + $this->presentSymbols = $presentSymbols; } $nodeToReflection = new NodeToReflection(); if ($identifier->isClass()) { - $classNodes = $this->fetchedNodesResult->getClassNodes(); + $classNodes = $fetchedNodesResult->getClassNodes(); $className = strtolower($identifier->getName()); if (!array_key_exists($className, $classNodes)) { return null; @@ -58,73 +96,165 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } if ($identifier->isFunction()) { - $functionNodes = $this->fetchedNodesResult->getFunctionNodes(); + $functionNodes = $fetchedNodesResult->getFunctionNodes(); $functionName = strtolower($identifier->getName()); if (!array_key_exists($functionName, $functionNodes)) { return null; } - $functionReflection = $nodeToReflection->__invoke( - $reflector, - $functionNodes[$functionName]->getNode(), - $functionNodes[$functionName]->getLocatedSource(), - $functionNodes[$functionName]->getNamespace(), - ); - if (!$functionReflection instanceof ReflectionFunction) { - throw new ShouldNotHappenException(); - } + foreach ($functionNodes[$functionName] as $functionNode) { + $functionReflection = $nodeToReflection->__invoke( + $reflector, + $functionNode->getNode(), + $functionNode->getLocatedSource(), + $functionNode->getNamespace(), + ); + if (!$functionReflection instanceof ReflectionFunction) { + throw new ShouldNotHappenException(); + } - return $functionReflection; + return $functionReflection; + } } if ($identifier->isConstant()) { - $constantNodes = $this->fetchedNodesResult->getConstantNodes(); - foreach ($constantNodes as $stmtConst) { - if ($stmtConst->getNode() instanceof FuncCall) { - $constantReflection = $nodeToReflection->__invoke( + $constantNodes = $fetchedNodesResult->getConstantNodes(); + $constantName = ConstantNameHelper::normalize($identifier->getName()); + + if (!array_key_exists($constantName, $constantNodes)) { + return null; + } + + foreach ($constantNodes[$constantName] as $fetchedConstantNode) { + $constantNode = $fetchedConstantNode->getNode(); + + $positionInNode = null; + if ($constantNode instanceof Const_) { + foreach ($constantNode->consts as $constPosition => $const) { + if ($const->namespacedName === null) { + throw new ShouldNotHappenException(); + } + + if (ConstantNameHelper::normalize($const->namespacedName->toString()) === $constantName) { + /** @var int $positionInNode */ + $positionInNode = $constPosition; + break; + } + } + + if ($positionInNode === null) { + throw new ShouldNotHappenException(); + } + } + + $constantReflection = $nodeToReflection->__invoke( + $reflector, + $fetchedConstantNode->getNode(), + $fetchedConstantNode->getLocatedSource(), + $fetchedConstantNode->getNamespace(), + $positionInNode, + ); + if (!$constantReflection instanceof ReflectionConstant) { + throw new ShouldNotHappenException(); + } + + return $constantReflection; + } + + return null; + } + + throw new ShouldNotHappenException(); + } + + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array + { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($this->fileName); + $nodeToReflection = new NodeToReflection(); + $reflections = []; + if ($identifierType->isClass()) { + $classNodes = $fetchedNodesResult->getClassNodes(); + + foreach ($classNodes as $classNodesArray) { + foreach ($classNodesArray as $classNode) { + $classReflection = $nodeToReflection->__invoke( $reflector, - $stmtConst->getNode(), - $stmtConst->getLocatedSource(), - $stmtConst->getNamespace(), + $classNode->getNode(), + $classNode->getLocatedSource(), + $classNode->getNamespace(), ); - if (!$constantReflection instanceof ReflectionConstant) { + + if (!$classReflection instanceof ReflectionClass) { throw new ShouldNotHappenException(); } - if ($constantReflection->getName() !== $identifier->getName()) { - continue; - } - return $constantReflection; + $reflections[] = $classReflection; + } + } + } + + if ($identifierType->isFunction()) { + $functionNodes = $fetchedNodesResult->getFunctionNodes(); + + foreach ($functionNodes as $functionNodesArray) { + foreach ($functionNodesArray as $functionNode) { + $functionReflection = $nodeToReflection->__invoke( + $reflector, + $functionNode->getNode(), + $functionNode->getLocatedSource(), + $functionNode->getNamespace(), + ); + + $reflections[] = $functionReflection; } + } + } + + if ($identifierType->isConstant()) { + $constantNodes = $fetchedNodesResult->getConstantNodes(); + foreach ($constantNodes as $constantNodesArray) { + foreach ($constantNodesArray as $fetchedConstantNode) { + $constantNode = $fetchedConstantNode->getNode(); + + if ($constantNode instanceof Const_) { + foreach ($constantNode->consts as $constPosition => $const) { + if ($const->namespacedName === null) { + throw new ShouldNotHappenException(); + } + + $constantReflection = $nodeToReflection->__invoke( + $reflector, + $constantNode, + $fetchedConstantNode->getLocatedSource(), + $fetchedConstantNode->getNamespace(), + $constPosition, + ); + if (!$constantReflection instanceof ReflectionConstant) { + throw new ShouldNotHappenException(); + } + + $reflections[] = $constantReflection; + } + + continue; + } - foreach (array_keys($stmtConst->getNode()->consts) as $i) { $constantReflection = $nodeToReflection->__invoke( $reflector, - $stmtConst->getNode(), - $stmtConst->getLocatedSource(), - $stmtConst->getNamespace(), - $i, + $constantNode, + $fetchedConstantNode->getLocatedSource(), + $fetchedConstantNode->getNamespace(), ); if (!$constantReflection instanceof ReflectionConstant) { throw new ShouldNotHappenException(); } - if ($constantReflection->getName() !== $identifier->getName()) { - continue; - } - return $constantReflection; + $reflections[] = $constantReflection; } } - - return null; } - throw new ShouldNotHappenException(); - } - - public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array - { - return []; + return $reflections; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php index 59e26df126..bd857f7489 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php @@ -4,7 +4,7 @@ use function array_key_exists; -class OptimizedSingleFileSourceLocatorRepository +final class OptimizedSingleFileSourceLocatorRepository { /** @var array */ diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php index 715546bbb4..84985abbce 100644 --- a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php +++ b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php @@ -4,6 +4,7 @@ use function array_keys; use function implode; +use function in_array; use function preg_match; use function preg_quote; use function strlen; @@ -13,7 +14,7 @@ * @author Jordi Boggiano * @see https://github.com/composer/composer/pull/10107 */ -class PhpFileCleaner +final class PhpFileCleaner { /** @var array */ @@ -27,20 +28,17 @@ class PhpFileCleaner private int $index = 0; - /** - * @param string[] $types - */ - public function __construct(array $types) + public function __construct() { - foreach ($types as $type) { + foreach (['class', 'interface', 'trait', 'enum'] 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', + 'pattern' => '{.\b(?])' . $type . '\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+}Ais', ]; } - $this->restPattern = '{[^?"\'typeConfig)) . ']+}A'; + $this->restPattern = '{[^{}?"\'typeConfig)) . ']+}A'; } public function clean(string $contents, int $maxMatches): string @@ -49,6 +47,11 @@ public function clean(string $contents, int $maxMatches): string $this->len = strlen($contents); $this->index = 0; + $inType = false; + $typeLevel = 0; + + $inDefine = false; + $clean = ''; while ($this->index < $this->len) { $this->skipToPhp(); @@ -62,15 +65,39 @@ public function clean(string $contents, int $maxMatches): string continue 2; } - if ($char === '"') { - $this->skipString('"'); - $clean .= 'null'; + if (in_array($char, ['"', "'"], true)) { + if ($inDefine) { + $clean .= $char . $this->consumeString($char); + $inDefine = false; + } else { + $this->skipString($char); + $clean .= 'null'; + } + continue; } - if ($char === "'") { - $this->skipString("'"); - $clean .= 'null'; + if ($char === '{') { + if ($inType) { + $typeLevel++; + } + + $clean .= $char; + $this->index++; + continue; + } + + if ($char === '}') { + if ($inType) { + $typeLevel--; + + if ($typeLevel === 0) { + $inType = false; + } + } + + $clean .= $char; + $this->index++; continue; } @@ -88,16 +115,37 @@ public function clean(string $contents, int $maxMatches): string } if ($this->peek('*')) { $this->skipComment(); + continue; } } - if ($maxMatches === 1 && isset($this->typeConfig[$char])) { + if ( + $inType + && $char === 'c' + && $this->match('~.\b(?])const(\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+)~Ais', $match, $this->index - 1) + ) { + // It's invalid PHP but it does not matter + $clean .= 'class_const' . $match[1]; + $this->index += strlen($match[0]) - 1; + continue; + } + + if ($char === 'd' && $this->match('~.\b(?])define\s*+\(~Ais', $match, $this->index - 1)) { + $inDefine = true; + $clean .= $match[0]; + $this->index += strlen($match[0]) - 1; + continue; + } + + if (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]; + + if (substr($this->contents, $this->index, $type['length']) === $type['name']) { + if ($maxMatches === 1 && $this->match($type['pattern'], $match, $this->index - 1)) { + return $clean . $match[0]; + } + + $inType = true; } } @@ -126,6 +174,33 @@ private function skipToPhp(): void } } + private function consumeString(string $delimiter): string + { + $string = ''; + + $this->index += 1; + while ($this->index < $this->len) { + if ($this->contents[$this->index] === '\\' && ($this->peek('\\') || $this->peek($delimiter))) { + $string .= $this->contents[$this->index]; + $string .= $this->contents[$this->index + 1]; + + $this->index += 2; + continue; + } + + if ($this->contents[$this->index] === $delimiter) { + $string .= $delimiter; + $this->index += 1; + break; + } + + $string .= $this->contents[$this->index]; + $this->index += 1; + } + + return $string; + } + private function skipString(string $delimiter): void { $this->index += 1; @@ -158,7 +233,7 @@ private function skipComment(): void private function skipToNewline(): void { while ($this->index < $this->len) { - if ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n") { + if (in_array($this->contents[$this->index], ["\r", "\n"], true)) { return; } $this->index += 1; @@ -210,14 +285,11 @@ private function peek(string $char): bool /** * @param string[]|null $match + * @param-out string[] $match */ - private function match(string $regex, ?array &$match = null): bool + private function match(string $regex, ?array &$match = null, ?int $offset = null): bool { - if (preg_match($regex, $this->contents, $match, 0, $this->index)) { - return true; - } - - return false; + return preg_match($regex, $this->contents, $match, 0, $offset ?? $this->index) === 1; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php index 0731d733b0..f2225f9b8f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php @@ -9,7 +9,7 @@ use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -class PhpVersionBlacklistSourceLocator implements SourceLocator +final class PhpVersionBlacklistSourceLocator implements SourceLocator { public function __construct( diff --git a/src/Reflection/BetterReflection/SourceLocator/ReflectionClassSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/ReflectionClassSourceLocator.php new file mode 100644 index 0000000000..604b1ad58d --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/ReflectionClassSourceLocator.php @@ -0,0 +1,53 @@ +isClass()) { + return null; + } + + /** @var class-string $className */ + $className = $identifier->getName(); + + $stub = $this->reflectionSourceStubber->generateClassStub($className); + if ($stub === null) { + return null; + } + + $reflection = new ReflectionClass($className); + + return $this->astLocator->findReflection( + $reflector, + new LocatedSource($stub->getStub(), $reflection->getName(), null), + new Identifier($reflection->getName(), new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + ); + } + + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array + { + return []; + } + +} diff --git a/src/Reflection/BetterReflection/SourceLocator/RewriteClassAliasSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/RewriteClassAliasSourceLocator.php new file mode 100644 index 0000000000..564950462f --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/RewriteClassAliasSourceLocator.php @@ -0,0 +1,46 @@ +isClass()) { + return $this->originalSourceLocator->locateIdentifier($reflector, $identifier); + } + + if ( + class_exists($identifier->getName(), false) + || interface_exists($identifier->getName(), false) + || trait_exists($identifier->getName(), false) + ) { + $classReflection = new CoreReflectionClass($identifier->getName()); + + return $this->originalSourceLocator->locateIdentifier($reflector, new Identifier($classReflection->getName(), $identifier->getType())); + } + + return $this->originalSourceLocator->locateIdentifier($reflector, $identifier); + } + + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array + { + return $this->originalSourceLocator->locateIdentifiersByType($reflector, $identifierType); + } + +} diff --git a/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php index fc8e931ffe..2b4f272646 100644 --- a/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php @@ -10,7 +10,7 @@ use ReflectionClass; use function class_exists; -class SkipClassAliasSourceLocator implements SourceLocator +final class SkipClassAliasSourceLocator implements SourceLocator { public function __construct(private SourceLocator $sourceLocator) diff --git a/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php b/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php deleted file mode 100644 index d3a264c6db..0000000000 --- a/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php +++ /dev/null @@ -1,65 +0,0 @@ -getExtensionFromFilePath($relativeFilePath), $file); - } - - public function generateFunctionStub(string $functionName): ?StubData - { - $lowerFunctionName = strtolower($functionName); - if (!array_key_exists($lowerFunctionName, Php8StubsMap::FUNCTIONS)) { - return null; - } - - $relativeFilePath = Php8StubsMap::FUNCTIONS[$lowerFunctionName]; - $file = self::DIRECTORY . '/' . $relativeFilePath; - - return new StubData(FileReader::read($file), $this->getExtensionFromFilePath($relativeFilePath), $file); - } - - public function generateConstantStub(string $constantName): ?StubData - { - return null; - } - - private function getExtensionFromFilePath(string $relativeFilePath): string - { - $pathParts = explode('/', $relativeFilePath); - if ($pathParts[1] === 'Zend') { - return 'Core'; - } - - return $pathParts[2]; - } - -} diff --git a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php index 273dc62e3b..9ea23cd6a9 100644 --- a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php +++ b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php @@ -4,18 +4,19 @@ use PhpParser\Parser; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; +use PHPStan\Node\Printer\Printer; use PHPStan\Php\PhpVersion; -class PhpStormStubsSourceStubberFactory +final class PhpStormStubsSourceStubberFactory { - public function __construct(private Parser $phpParser, private PhpVersion $phpVersion) + public function __construct(private Parser $phpParser, private Printer $printer, private PhpVersion $phpVersion) { } public function create(): PhpStormStubsSourceStubber { - return new PhpStormStubsSourceStubber($this->phpParser, $this->phpVersion->getVersionId()); + return new PhpStormStubsSourceStubber($this->phpParser, $this->printer, $this->phpVersion->getVersionId()); } } diff --git a/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php new file mode 100644 index 0000000000..8f7533ba5f --- /dev/null +++ b/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php @@ -0,0 +1,21 @@ +printer, $this->phpVersion->getVersionId()); + } + +} diff --git a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..4e1cfbcea6 --- /dev/null +++ b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php @@ -0,0 +1,65 @@ +class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return in_array($methodReflection->getName(), [ + 'getDocComment', + 'getType', + ], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if ($this->phpVersion->getVersionId() >= 80000) { + return null; + } + + if ($methodReflection->getName() === 'getDocComment') { + return new UnionType([ + new StringType(), + new ConstantBooleanType(false), + ]); + } + + if ($methodReflection->getName() === 'getType') { + return new UnionType([ + new ObjectType(ReflectionType::class), + new NullType(), + ]); + } + + return null; + } + +} diff --git a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..dfa3218442 --- /dev/null +++ b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php @@ -0,0 +1,102 @@ +getName(), [ + 'getFileName', + 'getStartLine', + 'getEndLine', + 'getDocComment', + 'getReflectionConstant', + 'getParentClass', + 'getExtensionName', + 'getBackingType', + ], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if ($this->phpVersion->getVersionId() >= 80000) { + return null; + } + + if (in_array($methodReflection->getName(), ['getFileName', 'getExtensionName'], true)) { + return new UnionType([ + new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]), + new ConstantBooleanType(false), + ]); + } + + if ($methodReflection->getName() === 'getDocComment') { + return new UnionType([ + new StringType(), + new ConstantBooleanType(false), + ]); + } + + if (in_array($methodReflection->getName(), ['getStartLine', 'getEndLine'], true)) { + return new IntegerType(); + } + + if ($methodReflection->getName() === 'getReflectionConstant') { + return new UnionType([ + new ObjectType(ReflectionClassConstant::class), + new ConstantBooleanType(false), + ]); + } + + if ($methodReflection->getName() === 'getParentClass') { + return new UnionType([ + new ObjectType(ReflectionClass::class), + new ConstantBooleanType(false), + ]); + } + + if ($methodReflection->getName() === 'getBackingType') { + return new UnionType([ + new ObjectType(ReflectionNamedType::class), + new NullType(), + ]); + } + + return null; + } + +} diff --git a/src/Reflection/BrokerAwareExtension.php b/src/Reflection/BrokerAwareExtension.php deleted file mode 100644 index 9ca104d127..0000000000 --- a/src/Reflection/BrokerAwareExtension.php +++ /dev/null @@ -1,16 +0,0 @@ - new self($function, $variant), $variants); + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->variant->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->variant->getResolvedTemplateTypeMap(); + } + + /** + * @return list + */ + public function getParameters(): array + { + return $this->variant->getParameters(); + } + + public function isVariadic(): bool + { + return $this->variant->isVariadic(); + } + + public function getReturnType(): Type + { + return $this->variant->getReturnType(); + } + + public function getPhpDocReturnType(): Type + { + return $this->variant->getPhpDocReturnType(); + } + + public function getNativeReturnType(): Type + { + return $this->variant->getNativeReturnType(); + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->variant->getCallSiteVarianceMap(); + } + + public function getThrowPoints(): array + { + if ($this->throwPoints !== null) { + return $this->throwPoints; + } + + if ($this->variant instanceof CallableParametersAcceptor) { + return $this->throwPoints = $this->variant->getThrowPoints(); + } + + $returnType = $this->variant->getReturnType(); + $throwType = $this->function->getThrowType(); + if ($throwType === null) { + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + $throwPoints = []; + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true); + } + } else { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnType)->yes()) { + $throwPoints[] = SimpleThrowPoint::createImplicit(); + } + } + + return $this->throwPoints = $throwPoints; + } + + public function isPure(): TrinaryLogic + { + $impurePoints = $this->getImpurePoints(); + if (count($impurePoints) === 0) { + return TrinaryLogic::createYes(); + } + + $certainCount = 0; + foreach ($impurePoints as $impurePoint) { + if (!$impurePoint->isCertain()) { + continue; + } + + $certainCount++; + } + + return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + if ($this->impurePoints !== null) { + return $this->impurePoints; + } + + if ($this->variant instanceof CallableParametersAcceptor) { + return $this->impurePoints = $this->variant->getImpurePoints(); + } + + $impurePoint = SimpleImpurePoint::createFromVariant($this->function, $this->variant); + if ($impurePoint === null) { + return $this->impurePoints = []; + } + + return $this->impurePoints = [$impurePoint]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->function->acceptsNamedArguments(); + } + +} diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php new file mode 100644 index 0000000000..79a3c96761 --- /dev/null +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -0,0 +1,72 @@ +hasSideEffects()->no()) { + $certain = $function->isPure()->no(); + if ($variant !== null) { + $certain = $certain || $variant->getReturnType()->isVoid()->yes(); + } + + if ($function instanceof FunctionReflection) { + return new SimpleImpurePoint( + 'functionCall', + sprintf('call to function %s()', $function->getName()), + $certain, + ); + } + + return new SimpleImpurePoint( + 'methodCall', + sprintf('call to method %s::%s()', $function->getDeclaringClass()->getDisplayName(), $function->getName()), + $certain, + ); + } + + return null; + } + + /** + * @return ImpurePointIdentifier + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getDescription(): string + { + return $this->description; + } + + public function isCertain(): bool + { + return $this->certain; + } + +} diff --git a/src/Reflection/Callables/SimpleThrowPoint.php b/src/Reflection/Callables/SimpleThrowPoint.php new file mode 100644 index 0000000000..5cde43a155 --- /dev/null +++ b/src/Reflection/Callables/SimpleThrowPoint.php @@ -0,0 +1,45 @@ +type; + } + + public function isExplicit(): bool + { + return $this->explicit; + } + + public function canContainAnyThrowable(): bool + { + return $this->canContainAnyThrowable; + } + +} diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index 6334ec6864..6d0452caff 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -2,132 +2,28 @@ namespace PHPStan\Reflection; -use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; -use PHPStan\Php\PhpVersion; -use PHPStan\TrinaryLogic; -use PHPStan\Type\ConstantTypeHelper; +use PhpParser\Node\Expr; use PHPStan\Type\Type; -use ReflectionClassConstant; -use function method_exists; -use const NAN; -class ClassConstantReflection implements ConstantReflection +/** @api */ +interface ClassConstantReflection extends ClassMemberReflection, ConstantReflection { - private ?Type $valueType = null; + public function getValueExpr(): Expr; - public function __construct( - private ClassReflection $declaringClass, - private ReflectionClassConstant $reflection, - private ?Type $phpDocType, - private PhpVersion $phpVersion, - private ?string $deprecatedDescription, - private bool $isDeprecated, - private bool $isInternal, - ) - { - } + public function isFinal(): bool; - public function getName(): string - { - return $this->reflection->getName(); - } + public function hasPhpDocType(): bool; - public function getFileName(): ?string - { - return $this->declaringClass->getFileName(); - } + public function getPhpDocType(): ?Type; - /** - * @return mixed - */ - public function 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) { - if ($this->phpDocType === null) { - $this->valueType = ConstantTypeHelper::getTypeFromValue($this->getValue()); - } else { - $this->valueType = $this->phpDocType; - } - } - - return $this->valueType; - } - - public function getDeclaringClass(): ClassReflection - { - return $this->declaringClass; - } - - public function isStatic(): bool - { - return true; - } - - public function isPrivate(): bool - { - return $this->reflection->isPrivate(); - } - - public function isPublic(): bool - { - return $this->reflection->isPublic(); - } + public function hasNativeType(): bool; - public function isFinal(): bool - { - if (method_exists($this->reflection, 'isFinal')) { - return $this->reflection->isFinal(); - } + public function getNativeType(): ?Type; - if (!$this->phpVersion->isInterfaceConstantImplicitlyFinal()) { - return false; - } - - return $this->declaringClass->isInterface(); - } - - public function isDeprecated(): TrinaryLogic - { - return TrinaryLogic::createFromBoolean($this->isDeprecated); - } - - public function getDeprecatedDescription(): ?string - { - if ($this->isDeprecated) { - return $this->deprecatedDescription; - } - - return null; - } - - public function isInternal(): TrinaryLogic - { - return TrinaryLogic::createFromBoolean($this->isInternal); - } - - public function getDocComment(): ?string - { - $docComment = $this->reflection->getDocComment(); - if ($docComment === false) { - return null; - } - - return $docComment; - } + /** + * @return list + */ + public function getAttributes(): array; } diff --git a/src/Reflection/ClassMemberAccessAnswerer.php b/src/Reflection/ClassMemberAccessAnswerer.php index b30294e758..9eeb979821 100644 --- a/src/Reflection/ClassMemberAccessAnswerer.php +++ b/src/Reflection/ClassMemberAccessAnswerer.php @@ -6,14 +6,24 @@ interface ClassMemberAccessAnswerer { + /** + * @phpstan-assert-if-true !null $this->getClassReflection() + */ public function isInClass(): bool; public function getClassReflection(): ?ClassReflection; + /** + * @deprecated Use canReadProperty() or canWriteProperty() + */ public function canAccessProperty(PropertyReflection $propertyReflection): bool; + public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool; + + public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool; + public function canCallMethod(MethodReflection $methodReflection): bool; - public function canAccessConstant(ConstantReflection $constantReflection): bool; + public function canAccessConstant(ClassConstantReflection $constantReflection): bool; } diff --git a/src/Reflection/ClassNameHelper.php b/src/Reflection/ClassNameHelper.php index adf7905bfc..8fe817e91d 100644 --- a/src/Reflection/ClassNameHelper.php +++ b/src/Reflection/ClassNameHelper.php @@ -5,7 +5,7 @@ use Nette\Utils\Strings; use function ltrim; -class ClassNameHelper +final class ClassNameHelper { public static function isValidClassName(string $name): bool diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 7be2d1651a..a4adc7b9b1 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -3,6 +3,15 @@ namespace PHPStan\Reflection; use Attribute; +use PhpParser\Node\Arg; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name\FullyQualified; +use PHPStan\Analyser\ArgumentsNormalizer; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; @@ -12,14 +21,21 @@ use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MixinTag; use PHPStan\PhpDoc\Tag\PropertyTag; +use PHPStan\PhpDoc\Tag\RequireExtendsTag; +use PHPStan\PhpDoc\Tag\RequireImplementsTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; use PHPStan\PhpDoc\Tag\TypeAliasTag; +use PHPStan\Reflection\Deprecation\DeprecationProvider; use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Reflection\Php\PhpPropertyReflection; +use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\CircularTypeAliasDefinitionException; -use PHPStan\Type\ConstantTypeHelper; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\GenericObjectType; @@ -27,15 +43,15 @@ use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Generic\TypeProjectionHelper; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeAlias; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VerbosityLevel; -use ReflectionClass; -use ReflectionEnum; -use ReflectionEnumBackedCase; use ReflectionException; -use ReflectionMethod; use function array_diff; use function array_filter; use function array_key_exists; @@ -49,24 +65,29 @@ use function in_array; use function is_bool; use function is_file; -use function method_exists; +use function is_int; use function reset; use function sprintf; use function strtolower; -/** @api */ -class ClassReflection +/** + * @api + */ +final class ClassReflection { - /** @var MethodReflection[] */ + /** @var ExtendedMethodReflection[] */ private array $methods = []; - /** @var PropertyReflection[] */ + /** @var ExtendedPropertyReflection[] */ private array $properties = []; - /** @var ConstantReflection[] */ + /** @var RealClassClassConstantReflection[] */ private array $constants = []; + /** @var EnumCaseReflection[]|null */ + private ?array $enumCases = null; + /** @var int[]|null */ private ?array $classHierarchyDistances = null; @@ -80,10 +101,20 @@ class ClassReflection private ?bool $isFinal = null; + private ?bool $isImmutable = null; + + private ?bool $hasConsistentConstructor = null; + + private ?bool $acceptsNamedArguments = null; + private ?TemplateTypeMap $templateTypeMap = null; private ?TemplateTypeMap $activeTemplateTypeMap = null; + private ?TemplateTypeVarianceMap $defaultCallSiteVarianceMap = null; + + private ?TemplateTypeVarianceMap $callSiteVarianceMap = null; + /** @var array|null */ private ?array $ancestors = null; @@ -96,7 +127,11 @@ class ClassReflection private string|false|null $reflectionDocComment = false; - /** @var ClassReflection[]|null */ + private false|ResolvedPhpDocBlock $resolvedPhpDocBlock = false; + + private false|ResolvedPhpDocBlock $traitContextResolvedPhpDocBlock = false; + + /** @var array|null */ private ?array $cachedInterfaces = null; private ClassReflection|false|null $cachedParentClass = false; @@ -107,29 +142,47 @@ class ClassReflection /** @var array */ private static array $resolvingTypeAliasImports = []; + /** @var array */ + private array $hasMethodCache = []; + + /** @var array */ + private array $hasPropertyCache = []; + /** * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions + * @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions + * @param string[] $universalObjectCratesClasses */ public function __construct( private ReflectionProvider $reflectionProvider, + private InitializerExprTypeResolver $initializerExprTypeResolver, private FileTypeMapper $fileTypeMapper, private StubPhpDocProvider $stubPhpDocProvider, private PhpDocInheritanceResolver $phpDocInheritanceResolver, private PhpVersion $phpVersion, + private SignatureMapProvider $signatureMapProvider, + private DeprecationProvider $deprecationProvider, + private AttributeReflectionFactory $attributeReflectionFactory, private array $propertiesClassReflectionExtensions, private array $methodsClassReflectionExtensions, + private array $allowedSubTypesClassReflectionExtensions, + private RequireExtendsPropertiesClassReflectionExtension $requireExtendsPropertiesClassReflectionExtension, + private RequireExtendsMethodsClassReflectionExtension $requireExtendsMethodsClassReflectionExtension, private string $displayName, - private ReflectionClass $reflection, + private ReflectionClass|ReflectionEnum $reflection, private ?string $anonymousFilename, private ?TemplateTypeMap $resolvedTemplateTypeMap, private ?ResolvedPhpDocBlock $stubPhpDocBlock, + private array $universalObjectCratesClasses, private ?string $extraCacheKey = null, + private ?TemplateTypeVarianceMap $resolvedCallSiteVarianceMap = null, + private ?bool $finalByKeywordOverride = null, ) { } - public function getNativeReflection(): ReflectionClass + public function getNativeReflection(): ReflectionClass|ReflectionEnum { return $this->reflection; } @@ -155,14 +208,6 @@ public function getFileName(): ?string return $this->filename = $fileName; } - /** - * @deprecated Use getFileName() - */ - public function getFileNameWithPhpDocs(): ?string - { - return $this->getFileName(); - } - public function getParentClass(): ?ClassReflection { if (!is_bool($this->cachedParentClass)) { @@ -184,7 +229,8 @@ public function getParentClass(): ?ClassReflection $extendedType = TemplateTypeHelper::resolveTemplateTypes( $extendedType, $this->getPossiblyIncompleteActiveTemplateTypeMap(), - true, + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), ); } @@ -217,17 +263,26 @@ public function getName(): string public function getDisplayName(bool $withTemplateTypes = true): string { - $name = $this->displayName; - if ( $withTemplateTypes === false || $this->resolvedTemplateTypeMap === null || count($this->resolvedTemplateTypeMap->getTypes()) === 0 ) { - return $name; + return $this->displayName; + } + + $templateTypes = []; + $variances = $this->getCallSiteVarianceMap()->getVariances(); + foreach ($this->getActiveTemplateTypeMap()->getTypes() as $name => $templateType) { + $variance = $variances[$name] ?? null; + if ($variance === null) { + continue; + } + + $templateTypes[] = TypeProjectionHelper::describe($templateType, $variance, VerbosityLevel::typeOnly()); } - return $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->getActiveTemplateTypeMap()->getTypes())) . '>'; + return $this->displayName . '<' . implode(',', $templateTypes) . '>'; } public function getCacheKey(): string @@ -240,7 +295,22 @@ public function getCacheKey(): string $cacheKey = $this->displayName; if ($this->resolvedTemplateTypeMap !== null) { - $cacheKey .= '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::cache()), $this->resolvedTemplateTypeMap->getTypes())) . '>'; + $templateTypes = []; + $variances = $this->getCallSiteVarianceMap()->getVariances(); + foreach ($this->getActiveTemplateTypeMap()->getTypes() as $name => $templateType) { + $variance = $variances[$name] ?? null; + if ($variance === null) { + continue; + } + + $templateTypes[] = TypeProjectionHelper::describe($templateType, $variance, VerbosityLevel::cache()); + } + + $cacheKey .= '<' . implode(',', $templateTypes) . '>'; + } + + if ($this->hasFinalByKeywordOverride()) { + $cacheKey .= '-f=' . ($this->isFinalByKeyword() ? 't' : 'f'); } if ($this->extraCacheKey !== null) { @@ -304,10 +374,9 @@ public function getClassHierarchyDistances(): array } /** - * @param ReflectionClass $class - * @return ReflectionClass[] + * @return list */ - private function collectTraits(ReflectionClass $class): array + private function collectTraits(ReflectionClass|ReflectionEnum $class): array { $traits = []; $traitsLeftToAnalyze = $class->getTraits(); @@ -330,45 +399,128 @@ private function collectTraits(ReflectionClass $class): array return $traits; } + public function allowsDynamicProperties(): bool + { + if ($this->isEnum()) { + return false; + } + + if (!$this->phpVersion->deprecatesDynamicProperties()) { + return true; + } + + if ($this->isReadOnly()) { + return false; + } + + if (UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( + $this->reflectionProvider, + $this, + )) { + return true; + } + + $class = $this; + $attributes = $class->reflection->getAttributes('AllowDynamicProperties'); + while (count($attributes) === 0 && $class->getParentClass() !== null) { + $attributes = $class->getParentClass()->reflection->getAttributes('AllowDynamicProperties'); + $class = $class->getParentClass(); + } + + return count($attributes) > 0; + } + + private function allowsDynamicPropertiesExtensions(): bool + { + if ($this->allowsDynamicProperties()) { + return true; + } + + $hasMagicMethod = $this->hasNativeMethod('__get') || $this->hasNativeMethod('__set') || $this->hasNativeMethod('__isset'); + if ($hasMagicMethod) { + return true; + } + + foreach ($this->getRequireExtendsTags() as $extendsTag) { + $type = $extendsTag->getType(); + if (!$type instanceof ObjectType) { + continue; + } + + $reflection = $type->getClassReflection(); + if ($reflection === null) { + continue; + } + + if (!$reflection->allowsDynamicPropertiesExtensions()) { + continue; + } + + return true; + } + + return false; + } + public function hasProperty(string $propertyName): bool { + if (array_key_exists($propertyName, $this->hasPropertyCache)) { + return $this->hasPropertyCache[$propertyName]; + } + if ($this->isEnum()) { - return $this->hasNativeProperty($propertyName); + return $this->hasPropertyCache[$propertyName] = $this->hasNativeProperty($propertyName); } - foreach ($this->propertiesClassReflectionExtensions as $extension) { + foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { + if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { + break; + } if ($extension->hasProperty($this, $propertyName)) { - return true; + return $this->hasPropertyCache[$propertyName] = true; } } - return false; + if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { + return $this->hasPropertyCache[$propertyName] = true; + } + + return $this->hasPropertyCache[$propertyName] = false; } public function hasMethod(string $methodName): bool { + if (array_key_exists($methodName, $this->hasMethodCache)) { + return $this->hasMethodCache[$methodName]; + } + foreach ($this->methodsClassReflectionExtensions as $extension) { if ($extension->hasMethod($this, $methodName)) { - return true; + return $this->hasMethodCache[$methodName] = true; } } - return false; + if ($this->requireExtendsMethodsClassReflectionExtension->hasMethod($this, $methodName)) { + return $this->hasMethodCache[$methodName] = true; + } + + return $this->hasMethodCache[$methodName] = false; } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { $key = $methodName; if ($scope->isInClass()) { $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey()); } + if (!isset($this->methods[$key])) { foreach ($this->methodsClassReflectionExtensions as $extension) { if (!$extension->hasMethod($this, $methodName)) { continue; } - $method = $extension->getMethod($this, $methodName); + $method = $this->wrapExtendedMethod($extension->getMethod($this, $methodName)); if ($scope->canCallMethod($method)) { return $this->methods[$key] = $method; } @@ -376,6 +528,13 @@ public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): } } + if (!isset($this->methods[$key])) { + if ($this->requireExtendsMethodsClassReflectionExtension->hasMethod($this, $methodName)) { + $method = $this->requireExtendsMethodsClassReflectionExtension->getMethod($this, $methodName); + $this->methods[$key] = $method; + } + } + if (!isset($this->methods[$key])) { throw new MissingMethodFromReflectionException($this->getName(), $methodName); } @@ -383,12 +542,30 @@ public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): return $this->methods[$key]; } + private function wrapExtendedMethod(MethodReflection $method): ExtendedMethodReflection + { + if ($method instanceof ExtendedMethodReflection) { + return $method; + } + + return new WrappedExtendedMethodReflection($method); + } + + private function wrapExtendedProperty(string $propertyName, PropertyReflection $method): ExtendedPropertyReflection + { + if ($method instanceof ExtendedPropertyReflection) { + return $method; + } + + return new WrappedExtendedPropertyReflection($propertyName, $method); + } + public function hasNativeMethod(string $methodName): bool { return $this->getPhpExtension()->hasNativeMethod($this, $methodName); } - public function getNativeMethod(string $methodName): MethodReflection + public function getNativeMethod(string $methodName): ExtendedMethodReflection { if (!$this->hasNativeMethod($methodName)) { throw new MissingMethodFromReflectionException($this->getName(), $methodName); @@ -401,7 +578,7 @@ public function hasConstructor(): bool return $this->findConstructor() !== null; } - public function getConstructor(): MethodReflection + public function getConstructor(): ExtendedMethodReflection { $constructor = $this->findConstructor(); if ($constructor === null) { @@ -438,7 +615,34 @@ private function getPhpExtension(): PhpClassReflectionExtension return $extension; } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + /** @internal */ + public function evictPrivateSymbols(): void + { + foreach ($this->constants as $name => $constant) { + if (!$constant->isPrivate()) { + continue; + } + + unset($this->constants[$name]); + } + foreach ($this->properties as $name => $property) { + if (!$property->isPrivate()) { + continue; + } + + unset($this->properties[$name]); + } + foreach ($this->methods as $name => $method) { + if (!$method->isPrivate()) { + continue; + } + + unset($this->methods[$name]); + } + $this->getPhpExtension()->evictPrivateSymbols($this->getCacheKey()); + } + + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { if ($this->isEnum()) { return $this->getNativeProperty($propertyName); @@ -448,20 +652,32 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco if ($scope->isInClass()) { $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey()); } + if (!isset($this->properties[$key])) { - foreach ($this->propertiesClassReflectionExtensions as $extension) { + foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { + if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { + break; + } + if (!$extension->hasProperty($this, $propertyName)) { continue; } - $property = $extension->getProperty($this, $propertyName); - if ($scope->canAccessProperty($property)) { + $property = $this->wrapExtendedProperty($propertyName, $extension->getProperty($this, $propertyName)); + if ($scope->canReadProperty($property)) { return $this->properties[$key] = $property; } $this->properties[$key] = $property; } } + if (!isset($this->properties[$key])) { + if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { + $property = $this->requireExtendsPropertiesClassReflectionExtension->getProperty($this, $propertyName); + $this->properties[$key] = $property; + } + } + if (!isset($this->properties[$key])) { throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); } @@ -498,13 +714,34 @@ public function isTrait(): bool return $this->reflection->isTrait(); } + /** + * @phpstan-assert-if-true ReflectionEnum $this->reflection + * @phpstan-assert-if-true ReflectionEnum $this->getNativeReflection() + */ public function isEnum(): bool { - if (method_exists($this->reflection, 'isEnum')) { - return $this->reflection->isEnum(); + return $this->reflection instanceof ReflectionEnum && $this->reflection->isEnum(); + } + + /** + * @return 'Interface'|'Trait'|'Enum'|'Class' + */ + public function getClassTypeDescription(): string + { + if ($this->isInterface()) { + return 'Interface'; + } elseif ($this->isTrait()) { + return 'Trait'; + } elseif ($this->isEnum()) { + return 'Enum'; } - return false; + return 'Class'; + } + + public function isReadOnly(): bool + { + return $this->reflection->isReadOnly(); } public function isBackedEnum(): bool @@ -526,12 +763,7 @@ public function getBackedEnumType(): ?Type return null; } - $reflectionType = $this->reflection->getBackingType(); - if ($reflectionType === null) { - return null; - } - - return TypehintHelper::decideTypeFromReflection($reflectionType); + return TypehintHelper::decideTypeFromReflection($this->reflection->getBackingType()); } public function hasEnumCase(string $name): bool @@ -540,10 +772,6 @@ public function hasEnumCase(string $name): bool return false; } - if (!method_exists($this->reflection, 'hasCase')) { - return false; - } - return $this->reflection->hasCase($name); } @@ -552,22 +780,27 @@ public function hasEnumCase(string $name): bool */ public function getEnumCases(): array { - if (!$this->reflection instanceof ReflectionEnum) { + if (!$this->isEnum()) { throw new ShouldNotHappenException(); } + if ($this->enumCases !== null) { + return $this->enumCases; + } + $cases = []; + $initializerExprContext = InitializerExprContext::fromClassReflection($this); foreach ($this->reflection->getCases() as $case) { $valueType = null; if ($case instanceof ReflectionEnumBackedCase) { - $valueType = ConstantTypeHelper::getTypeFromValue($case->getBackingValue()); + $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), $initializerExprContext); } - /** @var string $caseName */ $caseName = $case->getName(); - $cases[$caseName] = new EnumCaseReflection($this, $caseName, $valueType); + $attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); + $cases[$caseName] = new EnumCaseReflection($this, $case, $valueType, $attributes, $this->deprecationProvider); } - return $cases; + return $this->enumCases = $cases; } public function getEnumCase(string $name): EnumCaseReflection @@ -580,13 +813,19 @@ public function getEnumCase(string $name): EnumCaseReflection throw new ShouldNotHappenException(); } + if ($this->enumCases !== null && array_key_exists($name, $this->enumCases)) { + return $this->enumCases[$name]; + } + $case = $this->reflection->getCase($name); $valueType = null; if ($case instanceof ReflectionEnumBackedCase) { - $valueType = ConstantTypeHelper::getTypeFromValue($case->getBackingValue()); + $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this)); } - return new EnumCaseReflection($this, $name, $valueType); + $attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); + + return new EnumCaseReflection($this, $case, $valueType, $attributes, $this->deprecationProvider); } public function isClass(): bool @@ -599,20 +838,38 @@ public function isAnonymous(): bool return $this->anonymousFilename !== null; } + public function is(string $className): bool + { + return $this->getName() === $className || $this->isSubclassOf($className); + } + + /** + * @deprecated Use isSubclassOfClass instead. + */ public function isSubclassOf(string $className): bool { - if (isset($this->subclasses[$className])) { - return $this->subclasses[$className]; + if (!$this->reflectionProvider->hasClass($className)) { + return false; } - if (!$this->reflectionProvider->hasClass($className)) { - return $this->subclasses[$className] = false; + return $this->isSubclassOfClass($this->reflectionProvider->getClass($className)); + } + + public function isSubclassOfClass(self $class): bool + { + $cacheKey = $class->getCacheKey(); + if (isset($this->subclasses[$cacheKey])) { + return $this->subclasses[$cacheKey]; + } + + if ($class->isFinalByKeyword() || $class->isAnonymous()) { + return $this->subclasses[$cacheKey] = false; } try { - return $this->subclasses[$className] = $this->reflection->isSubclassOf($className); + return $this->subclasses[$cacheKey] = $this->reflection->isSubclassOf($class->getName()); } catch (ReflectionException) { - return $this->subclasses[$className] = false; + return $this->subclasses[$cacheKey] = false; } } @@ -626,7 +883,7 @@ public function implementsInterface(string $className): bool } /** - * @return ClassReflection[] + * @return list */ public function getParents(): array { @@ -641,7 +898,7 @@ public function getParents(): array } /** - * @return ClassReflection[] + * @return array */ public function getInterfaces(): array { @@ -675,7 +932,7 @@ public function getInterfaces(): array } /** - * @return ClassReflection[] + * @return array */ private function collectInterfaces(ClassReflection $interface): array { @@ -691,7 +948,7 @@ private function collectInterfaces(ClassReflection $interface): array } /** - * @return ClassReflection[] + * @return array */ public function getImmediateInterfaces(): array { @@ -732,6 +989,8 @@ public function getImmediateInterfaces(): array $implementedType = TemplateTypeHelper::resolveTemplateTypes( $implementedType, $this->getPossiblyIncompleteActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), true, ); } @@ -790,15 +1049,15 @@ public function getTraits(bool $recursive = false): array } /** - * @return string[] + * @return list */ public function getParentClassesNames(): array { $parentNames = []; - $currentClassReflection = $this; - while ($currentClassReflection->getParentClass() !== null) { - $parentNames[] = $currentClassReflection->getParentClass()->getName(); - $currentClassReflection = $currentClassReflection->getParentClass(); + $parentClass = $this->getParentClass(); + while ($parentClass !== null) { + $parentNames[] = $parentClass->getName(); + $parentClass = $parentClass->getParentClass(); } return $parentNames; @@ -818,7 +1077,7 @@ public function hasConstant(string $name): bool return $this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName()); } - public function getConstant(string $name): ConstantReflection + public function getConstant(string $name): ClassConstantReflection { if (!isset($this->constants[$name])) { $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); @@ -826,9 +1085,10 @@ public function getConstant(string $name): ConstantReflection throw new MissingConstantFromReflectionException($this->getName(), $name); } - $deprecatedDescription = null; - $isDeprecated = false; - $isInternal = false; + $deprecation = $this->deprecationProvider->getClassConstantDeprecation($reflectionConstant); + $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; + $declaringClass = $this->reflectionProvider->getClass($reflectionConstant->getDeclaringClass()->getName()); $fileName = $declaringClass->getFileName(); $phpDocType = null; @@ -836,7 +1096,7 @@ public function getConstant(string $name): ConstantReflection $declaringClass->getName(), $name, ); - if ($resolvedPhpDoc === null && $fileName !== null) { + if ($resolvedPhpDoc === null) { $docComment = null; if ($reflectionConstant->getDocComment() !== false) { $docComment = $reflectionConstant->getDocComment(); @@ -849,24 +1109,35 @@ public function getConstant(string $name): ConstantReflection ); } - if ($resolvedPhpDoc !== null) { + if (!$isDeprecated) { $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(); - } + } + $isInternal = $resolvedPhpDoc->isInternal(); + $isFinal = $resolvedPhpDoc->isFinal(); + $varTags = $resolvedPhpDoc->getVarTags(); + if (isset($varTags[0]) && count($varTags) === 1) { + $phpDocType = $varTags[0]->getType(); + } + + $nativeType = null; + if ($reflectionConstant->getType() !== null) { + $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), null, $declaringClass); + } elseif ($this->signatureMapProvider->hasClassConstantMetadata($declaringClass->getName(), $name)) { + $nativeType = $this->signatureMapProvider->getClassConstantMetadata($declaringClass->getName(), $name)['nativeType']; } - $this->constants[$name] = new ClassConstantReflection( + $this->constants[$name] = new RealClassClassConstantReflection( + $this->initializerExprTypeResolver, $declaringClass, $reflectionConstant, + $nativeType, $phpDocType, - $this->phpVersion, $deprecatedDescription, $isDeprecated, $isInternal, + $isFinal, + $this->attributeReflectionFactory->fromNativeReflection($reflectionConstant->getAttributes(), InitializerExprContext::fromClass($declaringClass->getName(), $fileName)), ); } return $this->constants[$name]; @@ -878,12 +1149,12 @@ public function hasTraitUse(string $traitName): bool } /** - * @return string[] + * @return list */ private function getTraitNames(): array { $class = $this->reflection; - $traitNames = $class->getTraitNames(); + $traitNames = array_map(static fn (ReflectionClass $class) => $class->getName(), $this->collectTraits($class)); while ($class->getParentClass() !== false) { $traitNames = array_values(array_unique(array_merge($traitNames, $class->getParentClass()->getTraitNames()))); $class = $class->getParentClass(); @@ -951,11 +1222,8 @@ public function getTypeAliases(): array public function getDeprecatedDescription(): ?string { - if ($this->deprecatedDescription === null && $this->isDeprecated()) { - $resolvedPhpDoc = $this->getResolvedPhpDoc(); - if ($resolvedPhpDoc !== null && $resolvedPhpDoc->getDeprecatedTag() !== null) { - $this->deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag()->getMessage(); - } + if ($this->isDeprecated === null) { + $this->resolveDeprecation(); } return $this->deprecatedDescription; @@ -964,13 +1232,36 @@ public function getDeprecatedDescription(): ?string public function isDeprecated(): bool { if ($this->isDeprecated === null) { - $resolvedPhpDoc = $this->getResolvedPhpDoc(); - $this->isDeprecated = $resolvedPhpDoc !== null && $resolvedPhpDoc->isDeprecated(); + $this->resolveDeprecation(); } return $this->isDeprecated; } + /** + * @phpstan-assert bool $this->isDeprecated + */ + private function resolveDeprecation(): void + { + $deprecation = $this->deprecationProvider->getClassDeprecation($this->reflection); + if ($deprecation !== null) { + $this->isDeprecated = true; + $this->deprecatedDescription = $deprecation->getDescription(); + return; + } + + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + + if ($resolvedPhpDoc !== null && $resolvedPhpDoc->isDeprecated()) { + $this->isDeprecated = true; + $this->deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; + return; + } + + $this->isDeprecated = false; + $this->deprecatedDescription = null; + } + public function isBuiltin(): bool { return $this->reflection->isInternal(); @@ -1000,43 +1291,131 @@ public function isFinal(): bool return $this->isFinal; } + public function isImmutable(): bool + { + if ($this->isImmutable === null) { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + $this->isImmutable = $resolvedPhpDoc !== null && ($resolvedPhpDoc->isImmutable() || $resolvedPhpDoc->isReadOnly()); + + $parentClass = $this->getParentClass(); + if ($parentClass !== null && !$this->isImmutable) { + $this->isImmutable = $parentClass->isImmutable(); + } + } + + return $this->isImmutable; + } + + public function hasConsistentConstructor(): bool + { + if ($this->hasConsistentConstructor === null) { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + $this->hasConsistentConstructor = $resolvedPhpDoc !== null && $resolvedPhpDoc->hasConsistentConstructor(); + } + + return $this->hasConsistentConstructor; + } + + public function acceptsNamedArguments(): bool + { + if ($this->acceptsNamedArguments === null) { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + $this->acceptsNamedArguments = $resolvedPhpDoc === null || $resolvedPhpDoc->acceptsNamedArguments(); + } + + return $this->acceptsNamedArguments; + } + + public function hasFinalByKeywordOverride(): bool + { + return $this->finalByKeywordOverride !== null; + } + public function isFinalByKeyword(): bool { + if ($this->isAnonymous()) { + return true; + } + + if ($this->finalByKeywordOverride !== null) { + return $this->finalByKeywordOverride; + } + return $this->reflection->isFinal(); } public function isAttributeClass(): bool { - return $this->findAttributeClass() !== null; + return $this->findAttributeFlags() !== null; } - private function findAttributeClass(): ?Attribute + private function findAttributeFlags(): ?int { if ($this->isInterface() || $this->isTrait() || $this->isEnum()) { return null; } - if (!method_exists($this->reflection, 'getAttributes')) { - return null; - } - $nativeAttributes = $this->reflection->getAttributes(Attribute::class); if (count($nativeAttributes) === 1) { - /** @var Attribute */ - return $nativeAttributes[0]->newInstance(); + if (!$this->reflectionProvider->hasClass(Attribute::class)) { + return null; + } + + $attributeClass = $this->reflectionProvider->getClass(Attribute::class); + $arguments = []; + foreach ($nativeAttributes[0]->getArgumentsExpressions() as $i => $expression) { + if ($i === '') { + throw new ShouldNotHappenException(); + } + $arguments[] = new Arg($expression, false, false, [], is_int($i) ? null : new Identifier($i)); + } + + if (!$attributeClass->hasConstructor()) { + return null; + } + $attributeConstructor = $attributeClass->getConstructor(); + $attributeConstructorVariant = $attributeConstructor->getOnlyVariant(); + + if (count($arguments) === 0) { + $flagType = $attributeConstructorVariant->getParameters()[0]->getDefaultValue(); + } else { + $staticCall = ArgumentsNormalizer::reorderStaticCallArguments( + $attributeConstructorVariant, + new StaticCall(new FullyQualified(Attribute::class), $attributeConstructor->getName(), $arguments), + ); + if ($staticCall === null) { + return null; + } + $flagExpr = $staticCall->getArgs()[0]->value; + $flagType = $this->initializerExprTypeResolver->getType($flagExpr, InitializerExprContext::fromClassReflection($this)); + } + + if (!$flagType instanceof ConstantIntegerType) { + return null; + } + + return $flagType->getValue(); } return null; } + /** + * @return list + */ + public function getAttributes(): array + { + return $this->attributeReflectionFactory->fromNativeReflection($this->reflection->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); + } + public function getAttributeClassFlags(): int { - $attribute = $this->findAttributeClass(); - if ($attribute === null) { + $flags = $this->findAttributeFlags(); + if ($flags === null) { throw new ShouldNotHappenException(); } - return $attribute->flags; + return $flags; } public function getTemplateTypeMap(): TemplateTypeMap @@ -1072,7 +1451,7 @@ public function getActiveTemplateTypeMap(): TemplateTypeMap if ($type instanceof ErrorType) { $templateType = $templateTypeMap->getType($name); if ($templateType !== null) { - return TemplateTypeHelper::resolveToBounds($templateType); + return TemplateTypeHelper::resolveToDefaults($templateType); } } @@ -1088,6 +1467,32 @@ public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap(); } + private function getDefaultCallSiteVarianceMap(): TemplateTypeVarianceMap + { + if ($this->defaultCallSiteVarianceMap !== null) { + return $this->defaultCallSiteVarianceMap; + } + + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + $this->defaultCallSiteVarianceMap = TemplateTypeVarianceMap::createEmpty(); + return $this->defaultCallSiteVarianceMap; + } + + $map = []; + foreach ($this->getTemplateTags() as $templateTag) { + $map[$templateTag->getName()] = TemplateTypeVariance::createInvariant(); + } + + $this->defaultCallSiteVarianceMap = new TemplateTypeVarianceMap($map); + return $this->defaultCallSiteVarianceMap; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap ??= $this->resolvedCallSiteVarianceMap ?? $this->getDefaultCallSiteVarianceMap(); + } + public function isGeneric(): bool { if ($this->isGeneric === null) { @@ -1114,14 +1519,34 @@ public function typeMapFromList(array $types): TemplateTypeMap $map = []; $i = 0; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $map[$tag->getName()] = $types[$i] ?? new ErrorType(); + $map[$tag->getName()] = $types[$i] ?? $tag->getDefault() ?? $tag->getBound(); $i++; } return new TemplateTypeMap($map); } - /** @return array */ + /** + * @param array $variances + */ + public function varianceMapFromList(array $variances): TemplateTypeVarianceMap + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return new TemplateTypeVarianceMap([]); + } + + $map = []; + $i = 0; + foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { + $map[$tag->getName()] = $variances[$i] ?? TemplateTypeVariance::createInvariant(); + $i++; + } + + return new TemplateTypeVarianceMap($map); + } + + /** @return list */ public function typeMapToList(TemplateTypeMap $typeMap): array { $resolvedPhpDoc = $this->getResolvedPhpDoc(); @@ -1131,7 +1556,23 @@ public function typeMapToList(TemplateTypeMap $typeMap): array $list = []; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $list[] = $typeMap->getType($tag->getName()) ?? $tag->getBound(); + $list[] = $typeMap->getType($tag->getName()) ?? $tag->getDefault() ?? $tag->getBound(); + } + + return $list; + } + + /** @return list */ + public function varianceMapToList(TemplateTypeVarianceMap $varianceMap): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + $list = []; + foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { + $list[] = $varianceMap->getVariance($tag->getName()) ?? TemplateTypeVariance::createInvariant(); } return $list; @@ -1144,17 +1585,102 @@ public function withTypes(array $types): self { return new self( $this->reflectionProvider, + $this->initializerExprTypeResolver, $this->fileTypeMapper, $this->stubPhpDocProvider, $this->phpDocInheritanceResolver, $this->phpVersion, + $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, $this->propertiesClassReflectionExtensions, $this->methodsClassReflectionExtensions, + $this->allowedSubTypesClassReflectionExtensions, + $this->requireExtendsPropertiesClassReflectionExtension, + $this->requireExtendsMethodsClassReflectionExtension, $this->displayName, $this->reflection, $this->anonymousFilename, $this->typeMapFromList($types), $this->stubPhpDocBlock, + $this->universalObjectCratesClasses, + null, + $this->resolvedCallSiteVarianceMap, + $this->finalByKeywordOverride, + ); + } + + /** + * @param array $variances + */ + public function withVariances(array $variances): self + { + return new self( + $this->reflectionProvider, + $this->initializerExprTypeResolver, + $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, + $this->phpVersion, + $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, + $this->propertiesClassReflectionExtensions, + $this->methodsClassReflectionExtensions, + $this->allowedSubTypesClassReflectionExtensions, + $this->requireExtendsPropertiesClassReflectionExtension, + $this->requireExtendsMethodsClassReflectionExtension, + $this->displayName, + $this->reflection, + $this->anonymousFilename, + $this->resolvedTemplateTypeMap, + $this->stubPhpDocBlock, + $this->universalObjectCratesClasses, + null, + $this->varianceMapFromList($variances), + $this->finalByKeywordOverride, + ); + } + + public function asFinal(): self + { + if ($this->getNativeReflection()->isFinal()) { + return $this; + } + if ($this->finalByKeywordOverride === true) { + return $this; + } + if (!$this->isClass()) { + return $this; + } + if ($this->isAbstract()) { + return $this; + } + + return new self( + $this->reflectionProvider, + $this->initializerExprTypeResolver, + $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, + $this->phpVersion, + $this->signatureMapProvider, + $this->deprecationProvider, + $this->attributeReflectionFactory, + $this->propertiesClassReflectionExtensions, + $this->methodsClassReflectionExtensions, + $this->allowedSubTypesClassReflectionExtensions, + $this->requireExtendsPropertiesClassReflectionExtension, + $this->requireExtendsMethodsClassReflectionExtension, + $this->displayName, + $this->reflection, + $this->anonymousFilename, + $this->resolvedTemplateTypeMap, + $this->stubPhpDocBlock, + $this->universalObjectCratesClasses, + null, + $this->resolvedCallSiteVarianceMap, + true, ); } @@ -1165,10 +1691,31 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock } $fileName = $this->getFileName(); - if ($fileName === null) { + if (is_bool($this->reflectionDocComment)) { + $docComment = $this->reflection->getDocComment(); + $this->reflectionDocComment = $docComment !== false ? $docComment : null; + } + + if ($this->reflectionDocComment === null) { return null; } + if ($this->resolvedPhpDocBlock !== false) { + return $this->resolvedPhpDocBlock; + } + + return $this->resolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment); + } + + public function getTraitContextResolvedPhpDoc(self $implementingClass): ?ResolvedPhpDocBlock + { + if (!$this->isTrait()) { + throw new ShouldNotHappenException(); + } + if ($implementingClass->isTrait()) { + throw new ShouldNotHappenException(); + } + $fileName = $this->getFileName(); if (is_bool($this->reflectionDocComment)) { $docComment = $this->reflection->getDocComment(); $this->reflectionDocComment = $docComment !== false ? $docComment : null; @@ -1178,7 +1725,11 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock return null; } - return $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment); + if ($this->traitContextResolvedPhpDocBlock !== false) { + return $this->traitContextResolvedPhpDocBlock; + } + + return $this->traitContextResolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $implementingClass->getName(), $this->getName(), null, $this->reflectionDocComment); } private function getFirstExtendsTag(): ?ExtendsTag @@ -1307,7 +1858,33 @@ public function getMixinTags(): array } /** - * @return array + * @return array + */ + public function getRequireExtendsTags(): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + return $resolvedPhpDoc->getRequireExtendsTags(); + } + + /** + * @return array + */ + public function getRequireImplementsTags(): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + return $resolvedPhpDoc->getRequireImplementsTags(); + } + + /** + * @return array */ public function getPropertyTags(): array { @@ -1320,7 +1897,7 @@ public function getPropertyTags(): array } /** - * @return array + * @return array */ public function getMethodTags(): array { @@ -1333,7 +1910,7 @@ public function getMethodTags(): array } /** - * @return array + * @return list */ public function getResolvedMixinTypes(): array { @@ -1347,10 +1924,26 @@ public function getResolvedMixinTypes(): array $types[] = TemplateTypeHelper::resolveTemplateTypes( $mixinTag->getType(), $this->getActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), ); } return $types; } + /** + * @return array|null + */ + public function getAllowedSubTypes(): ?array + { + foreach ($this->allowedSubTypesClassReflectionExtensions as $allowedSubTypesClassReflectionExtension) { + if ($allowedSubTypesClassReflectionExtension->supports($this)) { + return $allowedSubTypesClassReflectionExtension->getAllowedSubTypes($this); + } + } + + return null; + } + } diff --git a/src/Reflection/ClassReflectionExtensionRegistry.php b/src/Reflection/ClassReflectionExtensionRegistry.php index e18880e0be..1705f47461 100644 --- a/src/Reflection/ClassReflectionExtensionRegistry.php +++ b/src/Reflection/ClassReflectionExtensionRegistry.php @@ -2,29 +2,25 @@ namespace PHPStan\Reflection; -use PHPStan\Broker\Broker; -use function array_merge; +use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; -class ClassReflectionExtensionRegistry +final class ClassReflectionExtensionRegistry { /** * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions + * @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions */ public function __construct( - Broker $broker, private array $propertiesClassReflectionExtensions, private array $methodsClassReflectionExtensions, + private array $allowedSubTypesClassReflectionExtensions, + private RequireExtendsPropertiesClassReflectionExtension $requireExtendsPropertiesClassReflectionExtension, + private RequireExtendsMethodsClassReflectionExtension $requireExtendsMethodsClassReflectionExtension, ) { - foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions) as $extension) { - if (!($extension instanceof BrokerAwareExtension)) { - continue; - } - - $extension->setBroker($broker); - } } /** @@ -43,4 +39,22 @@ public function getMethodsClassReflectionExtensions(): array return $this->methodsClassReflectionExtensions; } + /** + * @return AllowedSubTypesClassReflectionExtension[] + */ + public function getAllowedSubTypesClassReflectionExtensions(): array + { + return $this->allowedSubTypesClassReflectionExtensions; + } + + public function getRequireExtendsPropertyClassReflectionExtension(): RequireExtendsPropertiesClassReflectionExtension + { + return $this->requireExtendsPropertiesClassReflectionExtension; + } + + public function getRequireExtendsMethodsClassReflectionExtension(): RequireExtendsMethodsClassReflectionExtension + { + return $this->requireExtendsMethodsClassReflectionExtension; + } + } diff --git a/src/Reflection/Constant/RuntimeConstantReflection.php b/src/Reflection/Constant/RuntimeConstantReflection.php index 341ae68cc4..4b31de502d 100644 --- a/src/Reflection/Constant/RuntimeConstantReflection.php +++ b/src/Reflection/Constant/RuntimeConstantReflection.php @@ -2,17 +2,19 @@ namespace PHPStan\Reflection\Constant; -use PHPStan\Reflection\GlobalConstantReflection; +use PHPStan\Reflection\ConstantReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class RuntimeConstantReflection implements GlobalConstantReflection +final class RuntimeConstantReflection implements ConstantReflection { public function __construct( private string $name, private Type $valueType, private ?string $fileName, + private TrinaryLogic $isDeprecated, + private ?string $deprecatedDescription, ) { } @@ -34,12 +36,12 @@ public function getFileName(): ?string public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::createNo(); + return $this->isDeprecated; } public function getDeprecatedDescription(): ?string { - return null; + return $this->deprecatedDescription; } public function isInternal(): TrinaryLogic diff --git a/src/Reflection/ConstantNameHelper.php b/src/Reflection/ConstantNameHelper.php new file mode 100644 index 0000000000..45b65f43a6 --- /dev/null +++ b/src/Reflection/ConstantNameHelper.php @@ -0,0 +1,26 @@ + $part !== ''); + return strtolower(implode('\\', array_slice($nameParts, 0, -1))) . '\\' . end($nameParts); + } + +} diff --git a/src/Reflection/ConstantReflection.php b/src/Reflection/ConstantReflection.php index 186da6e891..ebae755849 100644 --- a/src/Reflection/ConstantReflection.php +++ b/src/Reflection/ConstantReflection.php @@ -2,13 +2,23 @@ namespace PHPStan\Reflection; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Type; + /** @api */ -interface ConstantReflection extends ClassMemberReflection, GlobalConstantReflection +interface ConstantReflection { - /** - * @return mixed - */ - public function getValue(); + public function getName(): string; + + public function getValueType(): Type; + + public function isDeprecated(): TrinaryLogic; + + public function getDeprecatedDescription(): ?string; + + public function isInternal(): TrinaryLogic; + + public function getFileName(): ?string; } diff --git a/src/Reflection/ConstructorsHelper.php b/src/Reflection/ConstructorsHelper.php new file mode 100644 index 0000000000..6a721f2dd7 --- /dev/null +++ b/src/Reflection/ConstructorsHelper.php @@ -0,0 +1,77 @@ +> */ + private array $additionalConstructorsCache = []; + + /** + * @param list $additionalConstructors + */ + public function __construct( + private Container $container, + private array $additionalConstructors, + ) + { + } + + /** + * @return list + */ + public 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(); + } + + /** @var AdditionalConstructorsExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(AdditionalConstructorsExtension::EXTENSION_TAG); + foreach ($extensions as $extension) { + $extensionConstructors = $extension->getAdditionalConstructors($classReflection); + foreach ($extensionConstructors as $extensionConstructor) { + $constructors[] = $extensionConstructor; + } + } + + $nativeReflection = $classReflection->getNativeReflection(); + foreach ($this->additionalConstructors as $additionalConstructor) { + [$className, $methodName] = explode('::', $additionalConstructor); + 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/Reflection/Deprecation/ClassConstantDeprecationExtension.php b/src/Reflection/Deprecation/ClassConstantDeprecationExtension.php new file mode 100644 index 0000000000..39e09047ff --- /dev/null +++ b/src/Reflection/Deprecation/ClassConstantDeprecationExtension.php @@ -0,0 +1,29 @@ +description; + } + + public static function createWithDescription(string $description): self + { + $clone = new self(); + $clone->description = $description; + + return $clone; + } + +} diff --git a/src/Reflection/Deprecation/DeprecationProvider.php b/src/Reflection/Deprecation/DeprecationProvider.php new file mode 100644 index 0000000000..9c526121e9 --- /dev/null +++ b/src/Reflection/Deprecation/DeprecationProvider.php @@ -0,0 +1,144 @@ + $propertyDeprecationExtensions */ + private ?array $propertyDeprecationExtensions = null; + + /** @var ?array $methodDeprecationExtensions */ + private ?array $methodDeprecationExtensions = null; + + /** @var ?array $classConstantDeprecationExtensions */ + private ?array $classConstantDeprecationExtensions = null; + + /** @var ?array $classDeprecationExtensions */ + private ?array $classDeprecationExtensions = null; + + /** @var ?array $functionDeprecationExtensions */ + private ?array $functionDeprecationExtensions = null; + + /** @var ?array $constantDeprecationExtensions */ + private ?array $constantDeprecationExtensions = null; + + /** @var ?array $enumCaseDeprecationExtensions */ + private ?array $enumCaseDeprecationExtensions = null; + + public function __construct( + private Container $container, + ) + { + } + + public function getPropertyDeprecation(ReflectionProperty $reflectionProperty): ?Deprecation + { + $this->propertyDeprecationExtensions ??= $this->container->getServicesByTag(PropertyDeprecationExtension::PROPERTY_EXTENSION_TAG); + + foreach ($this->propertyDeprecationExtensions as $extension) { + $deprecation = $extension->getPropertyDeprecation($reflectionProperty); + if ($deprecation !== null) { + return $deprecation; + } + } + + return null; + } + + public function getMethodDeprecation(ReflectionMethod $methodReflection): ?Deprecation + { + $this->methodDeprecationExtensions ??= $this->container->getServicesByTag(MethodDeprecationExtension::METHOD_EXTENSION_TAG); + + foreach ($this->methodDeprecationExtensions as $extension) { + $deprecation = $extension->getMethodDeprecation($methodReflection); + if ($deprecation !== null) { + return $deprecation; + } + } + + return null; + } + + public function getClassConstantDeprecation(ReflectionClassConstant $reflectionConstant): ?Deprecation + { + $this->classConstantDeprecationExtensions ??= $this->container->getServicesByTag(ClassConstantDeprecationExtension::CLASS_CONSTANT_EXTENSION_TAG); + + foreach ($this->classConstantDeprecationExtensions as $extension) { + $deprecation = $extension->getClassConstantDeprecation($reflectionConstant); + if ($deprecation !== null) { + return $deprecation; + } + } + + return null; + } + + public function getClassDeprecation(ReflectionClass|ReflectionEnum $reflection): ?Deprecation + { + $this->classDeprecationExtensions ??= $this->container->getServicesByTag(ClassDeprecationExtension::CLASS_EXTENSION_TAG); + + foreach ($this->classDeprecationExtensions as $extension) { + $deprecation = $extension->getClassDeprecation($reflection); + if ($deprecation !== null) { + return $deprecation; + } + } + + return null; + } + + public function getFunctionDeprecation(ReflectionFunction $reflectionFunction): ?Deprecation + { + $this->functionDeprecationExtensions ??= $this->container->getServicesByTag(FunctionDeprecationExtension::FUNCTION_EXTENSION_TAG); + + foreach ($this->functionDeprecationExtensions as $extension) { + $deprecation = $extension->getFunctionDeprecation($reflectionFunction); + if ($deprecation !== null) { + return $deprecation; + } + } + + return null; + } + + public function getConstantDeprecation(ReflectionConstant $constantReflection): ?Deprecation + { + $this->constantDeprecationExtensions ??= $this->container->getServicesByTag(ConstantDeprecationExtension::CONSTANT_EXTENSION_TAG); + + foreach ($this->constantDeprecationExtensions as $extension) { + $deprecation = $extension->getConstantDeprecation($constantReflection); + if ($deprecation !== null) { + return $deprecation; + } + } + + return null; + } + + public function getEnumCaseDeprecation(ReflectionEnumUnitCase|ReflectionEnumBackedCase $enumCaseReflection): ?Deprecation + { + $this->enumCaseDeprecationExtensions ??= $this->container->getServicesByTag(EnumCaseDeprecationExtension::ENUM_CASE_EXTENSION_TAG); + + foreach ($this->enumCaseDeprecationExtensions as $extension) { + $deprecation = $extension->getEnumCaseDeprecation($enumCaseReflection); + if ($deprecation !== null) { + return $deprecation; + } + } + + return null; + } + +} diff --git a/src/Reflection/Deprecation/EnumCaseDeprecationExtension.php b/src/Reflection/Deprecation/EnumCaseDeprecationExtension.php new file mode 100644 index 0000000000..af51945d8e --- /dev/null +++ b/src/Reflection/Deprecation/EnumCaseDeprecationExtension.php @@ -0,0 +1,30 @@ + $variants + * @param list|null $namedArgumentsVariants */ - public function __construct(private ClassReflection $declaringClass, private MethodReflection $reflection, private array $variants) + public function __construct( + private ClassReflection $declaringClass, + private ExtendedMethodReflection $reflection, + private array $variants, + private ?array $namedArgumentsVariants, + private ?Type $selfOutType, + ) { } @@ -59,6 +70,21 @@ public function getVariants(): array return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + $variants = $this->getVariants(); + if (count($variants) !== 1) { + throw new ShouldNotHappenException(); + } + + return $variants[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return $this->namedArgumentsVariants; + } + public function isDeprecated(): TrinaryLogic { return $this->reflection->isDeprecated(); @@ -74,11 +100,26 @@ public function isFinal(): TrinaryLogic return $this->reflection->isFinal(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->reflection->isFinalByKeyword(); + } + public function isInternal(): TrinaryLogic { return $this->reflection->isInternal(); } + public function isBuiltin(): TrinaryLogic + { + $builtin = $this->reflection->isBuiltin(); + if (is_bool($builtin)) { + return TrinaryLogic::createFromBoolean($builtin); + } + + return $builtin; + } + public function getThrowType(): ?Type { return $this->reflection->getThrowType(); @@ -89,4 +130,44 @@ public function hasSideEffects(): TrinaryLogic return $this->reflection->hasSideEffects(); } + public function getAsserts(): Assertions + { + return $this->reflection->getAsserts(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->reflection->acceptsNamedArguments(); + } + + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + + public function returnsByReference(): TrinaryLogic + { + return $this->reflection->returnsByReference(); + } + + public function isAbstract(): TrinaryLogic + { + $abstract = $this->reflection->isAbstract(); + if (is_bool($abstract)) { + return TrinaryLogic::createFromBoolean($abstract); + } + + return $abstract; + } + + public function isPure(): TrinaryLogic + { + return $this->reflection->isPure(); + } + + public function getAttributes(): array + { + return $this->reflection->getAttributes(); + } + } diff --git a/src/Reflection/Dummy/ChangedTypePropertyReflection.php b/src/Reflection/Dummy/ChangedTypePropertyReflection.php index 018d8593fa..3bf6a6eb84 100644 --- a/src/Reflection/Dummy/ChangedTypePropertyReflection.php +++ b/src/Reflection/Dummy/ChangedTypePropertyReflection.php @@ -3,18 +3,24 @@ namespace PHPStan\Reflection\Dummy; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\WrapperPropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class ChangedTypePropertyReflection implements WrapperPropertyReflection +final class ChangedTypePropertyReflection implements WrapperPropertyReflection { - public function __construct(private ClassReflection $declaringClass, private PropertyReflection $reflection, private Type $readableType, private Type $writableType) + public function __construct(private ClassReflection $declaringClass, private ExtendedPropertyReflection $reflection, private Type $readableType, private Type $writableType, private Type $phpDocType, private Type $nativeType) { } + public function getName(): string + { + return $this->reflection->getName(); + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -40,6 +46,26 @@ public function getDocComment(): ?string return $this->reflection->getDocComment(); } + public function hasPhpDocType(): bool + { + return $this->reflection->hasPhpDocType(); + } + + public function getPhpDocType(): Type + { + return $this->phpDocType; + } + + public function hasNativeType(): bool + { + return $this->reflection->hasNativeType(); + } + + public function getNativeType(): Type + { + return $this->nativeType; + } + public function getReadableType(): Type { return $this->readableType; @@ -80,9 +106,49 @@ public function isInternal(): TrinaryLogic return $this->reflection->isInternal(); } - public function getOriginalReflection(): PropertyReflection + public function getOriginalReflection(): ExtendedPropertyReflection { return $this->reflection; } + public function isAbstract(): TrinaryLogic + { + return $this->reflection->isAbstract(); + } + + public function isFinal(): TrinaryLogic + { + return $this->reflection->isFinal(); + } + + public function isVirtual(): TrinaryLogic + { + return $this->reflection->isVirtual(); + } + + public function hasHook(string $hookType): bool + { + return $this->reflection->hasHook($hookType); + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + return $this->reflection->getHook($hookType); + } + + public function isProtectedSet(): bool + { + return $this->reflection->isProtectedSet(); + } + + public function isPrivateSet(): bool + { + return $this->reflection->isPrivateSet(); + } + + public function getAttributes(): array + { + return $this->reflection->getAttributes(); + } + } diff --git a/src/Reflection/Dummy/DummyClassConstantReflection.php b/src/Reflection/Dummy/DummyClassConstantReflection.php new file mode 100644 index 0000000000..ffc6afe7c9 --- /dev/null +++ b/src/Reflection/Dummy/DummyClassConstantReflection.php @@ -0,0 +1,114 @@ +getClass(stdClass::class); + } + + public function isFinal(): bool + { + return false; + } + + public function getFileName(): ?string + { + return null; + } + + public function isStatic(): bool + { + return true; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getName(): string + { + return $this->name; + } + + public function getValueType(): Type + { + return new MixedType(); + } + + public function getValueExpr(): Expr + { + return new TypeExpr(new MixedType()); + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getDocComment(): ?string + { + return null; + } + + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): ?Type + { + return null; + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): ?Type + { + return null; + } + + public function getAttributes(): array + { + return []; + } + +} diff --git a/src/Reflection/Dummy/DummyConstantReflection.php b/src/Reflection/Dummy/DummyConstantReflection.php deleted file mode 100644 index 2b908355e5..0000000000 --- a/src/Reflection/Dummy/DummyConstantReflection.php +++ /dev/null @@ -1,86 +0,0 @@ -getClass(stdClass::class); - } - - public function getFileName(): ?string - { - return null; - } - - public function isStatic(): bool - { - return true; - } - - public function isPrivate(): bool - { - return false; - } - - public function isPublic(): bool - { - return true; - } - - public function getName(): string - { - return $this->name; - } - - /** - * @return mixed - */ - public function getValue() - { - // so that Scope::getTypeFromValue() returns mixed - return new stdClass(); - } - - public function getValueType(): Type - { - return new MixedType(); - } - - public function isDeprecated(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function getDeprecatedDescription(): ?string - { - return null; - } - - public function isInternal(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function getDocComment(): ?string - { - return null; - } - -} diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index 1bcd01f6af..c48d6904ce 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -2,16 +2,19 @@ namespace PHPStan\Reflection\Dummy; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VoidType; -class DummyConstructorReflection implements MethodReflection +final class DummyConstructorReflection implements ExtendedMethodReflection { public function __construct(private ClassReflection $declaringClass) @@ -51,16 +54,29 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { return [ - new FunctionVariant( + new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, [], false, new VoidType(), + new MixedType(), + new MixedType(), + null, ), ]; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -81,6 +97,11 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getThrowType(): ?Type { return null; @@ -96,4 +117,44 @@ public function getDocComment(): ?string return null; } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->acceptsNamedArguments()); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index 77b06eb5ae..dced9b6206 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -2,17 +2,18 @@ namespace PHPStan\Reflection\Dummy; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use stdClass; -class DummyMethodReflection implements MethodReflection +final class DummyMethodReflection implements ExtendedMethodReflection { public function __construct(private string $name) @@ -51,9 +52,6 @@ public function getPrototype(): ClassMemberReflection return $this; } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { return [ @@ -61,6 +59,16 @@ public function getVariants(): array ]; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -76,11 +84,21 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getThrowType(): ?Type { return null; @@ -96,4 +114,39 @@ public function getDocComment(): ?string return null; } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Dummy/DummyPropertyReflection.php b/src/Reflection/Dummy/DummyPropertyReflection.php index b72a011960..ca828a53fe 100644 --- a/src/Reflection/Dummy/DummyPropertyReflection.php +++ b/src/Reflection/Dummy/DummyPropertyReflection.php @@ -3,16 +3,27 @@ namespace PHPStan\Reflection\Dummy; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use stdClass; -class DummyPropertyReflection implements PropertyReflection +final class DummyPropertyReflection implements ExtendedPropertyReflection { + public function __construct(private string $name) + { + } + + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); @@ -35,6 +46,26 @@ public function isPublic(): bool return true; } + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): Type + { + return new MixedType(); + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + public function getReadableType(): Type { return new MixedType(); @@ -80,4 +111,44 @@ public function getDocComment(): ?string return null; } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function hasHook(string $hookType): bool + { + return false; + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + throw new ShouldNotHappenException(); + } + + public function isProtectedSet(): bool + { + return false; + } + + public function isPrivateSet(): bool + { + return false; + } + + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/EnumCaseReflection.php b/src/Reflection/EnumCaseReflection.php index b538f44b3f..7a86e38726 100644 --- a/src/Reflection/EnumCaseReflection.php +++ b/src/Reflection/EnumCaseReflection.php @@ -2,14 +2,47 @@ namespace PHPStan\Reflection; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumUnitCase; +use PHPStan\Internal\DeprecatedAttributeHelper; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -/** @api */ -class EnumCaseReflection +/** + * @api + */ +final class EnumCaseReflection { - public function __construct(private ClassReflection $declaringEnum, private string $name, private ?Type $backingValueType) + private bool $isDeprecated; + + private ?string $deprecatedDescription; + + /** + * @param list $attributes + */ + public function __construct( + private ClassReflection $declaringEnum, + private ReflectionEnumUnitCase|ReflectionEnumBackedCase $reflection, + private ?Type $backingValueType, + private array $attributes, + DeprecationProvider $deprecationProvider, + ) { + $deprecation = $deprecationProvider->getEnumCaseDeprecation($reflection); + if ($deprecation !== null) { + $this->isDeprecated = true; + $this->deprecatedDescription = $deprecation->getDescription(); + + } elseif ($reflection->isDeprecated()) { + $attributes = $this->reflection->getBetterReflection()->getAttributes(); + $this->isDeprecated = true; + $this->deprecatedDescription = DeprecatedAttributeHelper::getDeprecatedDescription($attributes); + } else { + $this->isDeprecated = false; + $this->deprecatedDescription = null; + } } public function getDeclaringEnum(): ClassReflection @@ -19,7 +52,7 @@ public function getDeclaringEnum(): ClassReflection public function getName(): string { - return $this->name; + return $this->reflection->getName(); } public function getBackingValueType(): ?Type @@ -27,4 +60,22 @@ public function getBackingValueType(): ?Type return $this->backingValueType; } + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->isDeprecated); + } + + public function getDeprecatedDescription(): ?string + { + return $this->deprecatedDescription; + } + + /** + * @return list + */ + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/ExtendedCallableFunctionVariant.php b/src/Reflection/ExtendedCallableFunctionVariant.php new file mode 100644 index 0000000000..5e2d3a9c10 --- /dev/null +++ b/src/Reflection/ExtendedCallableFunctionVariant.php @@ -0,0 +1,83 @@ + $parameters + * @param SimpleThrowPoint[] $throwPoints + * @param SimpleImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + * @param string[] $usedVariables + */ + public function __construct( + TemplateTypeMap $templateTypeMap, + ?TemplateTypeMap $resolvedTemplateTypeMap, + array $parameters, + bool $isVariadic, + Type $returnType, + Type $phpDocReturnType, + Type $nativeReturnType, + ?TemplateTypeVarianceMap $callSiteVarianceMap, + private array $throwPoints, + private TrinaryLogic $isPure, + private array $impurePoints, + private array $invalidateExpressions, + private array $usedVariables, + private TrinaryLogic $acceptsNamedArguments, + ) + { + parent::__construct( + $templateTypeMap, + $resolvedTemplateTypeMap, + $parameters, + $isVariadic, + $returnType, + $phpDocReturnType, + $nativeReturnType, + $callSiteVarianceMap, + ); + } + + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + public function isPure(): TrinaryLogic + { + return $this->isPure; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + + public function getUsedVariables(): array + { + return $this->usedVariables; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->acceptsNamedArguments; + } + +} diff --git a/src/Reflection/ExtendedFunctionVariant.php b/src/Reflection/ExtendedFunctionVariant.php new file mode 100644 index 0000000000..e45f402bb0 --- /dev/null +++ b/src/Reflection/ExtendedFunctionVariant.php @@ -0,0 +1,61 @@ + $parameters + * @api + */ + public function __construct( + TemplateTypeMap $templateTypeMap, + ?TemplateTypeMap $resolvedTemplateTypeMap, + array $parameters, + bool $isVariadic, + Type $returnType, + private Type $phpDocReturnType, + private Type $nativeReturnType, + ?TemplateTypeVarianceMap $callSiteVarianceMap = null, + ) + { + parent::__construct( + $templateTypeMap, + $resolvedTemplateTypeMap, + $parameters, + $isVariadic, + $returnType, + $callSiteVarianceMap, + ); + } + + /** + * @return list + */ + public function getParameters(): array + { + /** @var list $parameters */ + $parameters = parent::getParameters(); + + return $parameters; + } + + public function getPhpDocReturnType(): Type + { + return $this->phpDocReturnType; + } + + public function getNativeReturnType(): Type + { + return $this->nativeReturnType; + } + +} diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php new file mode 100644 index 0000000000..5cea392754 --- /dev/null +++ b/src/Reflection/ExtendedMethodReflection.php @@ -0,0 +1,68 @@ + + */ + public function getVariants(): array; + + /** + * @internal + */ + public function getOnlyVariant(): ExtendedParametersAcceptor; + + /** + * @return list|null + */ + public function getNamedArgumentsVariants(): ?array; + + public function acceptsNamedArguments(): TrinaryLogic; + + public function getAsserts(): Assertions; + + public function getSelfOutType(): ?Type; + + public function returnsByReference(): TrinaryLogic; + + public function isFinalByKeyword(): TrinaryLogic; + + public function isAbstract(): TrinaryLogic|bool; + + public function isBuiltin(): TrinaryLogic|bool; + + /** + * This indicates whether the method has phpstan-pure + * or phpstan-impure annotation above it. + * + * In most cases asking hasSideEffects() is much more practical + * as it also accounts for void return type (method being always impure). + */ + public function isPure(): TrinaryLogic; + + /** + * @return list + */ + public function getAttributes(): array; + +} diff --git a/src/Reflection/ExtendedParameterReflection.php b/src/Reflection/ExtendedParameterReflection.php new file mode 100644 index 0000000000..ab50b76bd8 --- /dev/null +++ b/src/Reflection/ExtendedParameterReflection.php @@ -0,0 +1,29 @@ + + */ + public function getAttributes(): array; + +} diff --git a/src/Reflection/ExtendedParametersAcceptor.php b/src/Reflection/ExtendedParametersAcceptor.php new file mode 100644 index 0000000000..77fb213b49 --- /dev/null +++ b/src/Reflection/ExtendedParametersAcceptor.php @@ -0,0 +1,23 @@ + + */ + public function getParameters(): array; + + public function getPhpDocReturnType(): Type; + + public function getNativeReturnType(): Type; + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap; + +} diff --git a/src/Reflection/ExtendedPropertyReflection.php b/src/Reflection/ExtendedPropertyReflection.php new file mode 100644 index 0000000000..1027c193f1 --- /dev/null +++ b/src/Reflection/ExtendedPropertyReflection.php @@ -0,0 +1,64 @@ + + */ + public function getAttributes(): array; + +} diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 8ef8a806b2..297e4dd7d3 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -14,16 +14,26 @@ public function getName(): string; public function getFileName(): ?string; /** - * @return ParametersAcceptor[] + * @return list */ public function getVariants(): array; + /** + * @internal + */ + public function getOnlyVariant(): ExtendedParametersAcceptor; + + /** + * @return list|null + */ + public function getNamedArgumentsVariants(): ?array; + + public function acceptsNamedArguments(): TrinaryLogic; + public function isDeprecated(): TrinaryLogic; public function getDeprecatedDescription(): ?string; - public function isFinal(): TrinaryLogic; - public function isInternal(): TrinaryLogic; public function getThrowType(): ?Type; @@ -32,4 +42,24 @@ public function hasSideEffects(): TrinaryLogic; public function isBuiltin(): bool; + public function getAsserts(): Assertions; + + public function getDocComment(): ?string; + + public function returnsByReference(): TrinaryLogic; + + /** + * This indicates whether the function has phpstan-pure + * or phpstan-impure annotation above it. + * + * In most cases asking hasSideEffects() is much more practical + * as it also accounts for void return type (method being always impure). + */ + public function isPure(): TrinaryLogic; + + /** + * @return list + */ + public function getAttributes(): array; + } diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index f2a2f7e355..993bf34b3b 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -2,16 +2,20 @@ namespace PHPStan\Reflection; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; use PHPStan\Reflection\Php\PhpFunctionReflection; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; -use ReflectionFunction; interface FunctionReflectionFactory { /** - * @param Type[] $phpDocParameterTypes + * @param array $phpDocParameterTypes + * @param array $phpDocParameterOutTypes + * @param array $phpDocParameterImmediatelyInvokedCallable + * @param array $phpDocParameterClosureThisTypes + * @param list $attributes */ public function create( ReflectionFunction $reflection, @@ -22,9 +26,15 @@ public function create( ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, ?string $filename, - ?bool $isPure = null, + ?bool $isPure, + Assertions $asserts, + bool $acceptsNamedArguments, + ?string $phpDocComment, + array $phpDocParameterOutTypes, + array $phpDocParameterImmediatelyInvokedCallable, + array $phpDocParameterClosureThisTypes, + array $attributes, ): PhpFunctionReflection; } diff --git a/src/Reflection/FunctionVariant.php b/src/Reflection/FunctionVariant.php index 9936ae9244..7c69274ef0 100644 --- a/src/Reflection/FunctionVariant.php +++ b/src/Reflection/FunctionVariant.php @@ -3,15 +3,20 @@ namespace PHPStan\Reflection; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -/** @api */ +/** + * @api + */ class FunctionVariant implements ParametersAcceptor { + private TemplateTypeVarianceMap $callSiteVarianceMap; + /** * @api - * @param array $parameters + * @param list $parameters */ public function __construct( private TemplateTypeMap $templateTypeMap, @@ -19,8 +24,10 @@ public function __construct( private array $parameters, private bool $isVariadic, private Type $returnType, + ?TemplateTypeVarianceMap $callSiteVarianceMap = null, ) { + $this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty(); } public function getTemplateTypeMap(): TemplateTypeMap @@ -33,8 +40,13 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); } + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; + } + /** - * @return array + * @return list */ public function getParameters(): array { diff --git a/src/Reflection/FunctionVariantWithPhpDocs.php b/src/Reflection/FunctionVariantWithPhpDocs.php deleted file mode 100644 index aae15cb864..0000000000 --- a/src/Reflection/FunctionVariantWithPhpDocs.php +++ /dev/null @@ -1,56 +0,0 @@ - $parameters - */ - public function __construct( - TemplateTypeMap $templateTypeMap, - ?TemplateTypeMap $resolvedTemplateTypeMap, - array $parameters, - bool $isVariadic, - Type $returnType, - private Type $phpDocReturnType, - private Type $nativeReturnType, - ) - { - parent::__construct( - $templateTypeMap, - $resolvedTemplateTypeMap, - $parameters, - $isVariadic, - $returnType, - ); - } - - /** - * @return array - */ - public function getParameters(): array - { - /** @var ParameterReflectionWithPhpDocs[] $parameters */ - $parameters = parent::getParameters(); - - return $parameters; - } - - public function getPhpDocReturnType(): Type - { - return $this->phpDocReturnType; - } - - public function getNativeReturnType(): Type - { - return $this->nativeReturnType; - } - -} diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 2e79b78c9b..35f70bf37d 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -2,42 +2,136 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Php\ExtendedDummyParameter; +use PHPStan\TrinaryLogic; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function array_key_exists; +use function array_map; use function array_merge; +use function count; +use function is_int; -class GenericParametersAcceptorResolver +final class GenericParametersAcceptorResolver { /** * @api - * @param Type[] $argTypes + * @param array $argTypes */ - public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ParametersAcceptor + public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ExtendedParametersAcceptor { $typeMap = TemplateTypeMap::createEmpty(); + $passedArgs = []; - foreach ($parametersAcceptor->getParameters() as $i => $param) { - if (isset($argTypes[$i])) { - $argType = $argTypes[$i]; + $parameters = $parametersAcceptor->getParameters(); + $namedArgTypes = []; + foreach ($argTypes as $i => $argType) { + if (is_int($i)) { + if (isset($parameters[$i])) { + $namedArgTypes[$parameters[$i]->getName()] = $argType; + continue; + } + if (count($parameters) > 0) { + $lastParameter = $parameters[count($parameters) - 1]; + if ($lastParameter->isVariadic()) { + $parameterName = $lastParameter->getName(); + if (array_key_exists($parameterName, $namedArgTypes)) { + $namedArgTypes[$parameterName] = TypeCombinator::union($namedArgTypes[$parameterName], $argType); + continue; + } + $namedArgTypes[$parameterName] = $argType; + } + } + continue; + } + + $namedArgTypes[$i] = $argType; + } + + foreach ($parametersAcceptor->getParameters() as $param) { + if (isset($namedArgTypes[$param->getName()])) { + $argType = $namedArgTypes[$param->getName()]; } elseif ($param->getDefaultValue() !== null) { $argType = $param->getDefaultValue(); } else { - break; + continue; } $paramType = $param->getType(); $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)); + $passedArgs['$' . $param->getName()] = $argType; + } + + $returnType = $parametersAcceptor->getReturnType(); + if ( + $returnType instanceof ConditionalTypeForParameter + && !$returnType->isNegated() + && array_key_exists($returnType->getParameterName(), $passedArgs) + ) { + $paramType = $returnType->getTarget(); + $argType = $passedArgs[$returnType->getParameterName()]; + $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)); } - return new ResolvedFunctionVariant( + $resolvedTemplateTypeMap = new TemplateTypeMap(array_merge( + $parametersAcceptor->getTemplateTypeMap()->map(static fn (string $name, Type $type): Type => new ErrorType())->getTypes(), + $typeMap->getTypes(), + )); + + $originalParametersAcceptor = $parametersAcceptor; + + if (!$parametersAcceptor instanceof ExtendedParametersAcceptor) { + $parametersAcceptor = new ExtendedFunctionVariant( + $parametersAcceptor->getTemplateTypeMap(), + $parametersAcceptor->getResolvedTemplateTypeMap(), + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( + $parameter->getName(), + $parameter->getType(), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + new MixedType(), + $parameter->getType(), + null, + TrinaryLogic::createMaybe(), + null, + [], + ), $parametersAcceptor->getParameters()), + $parametersAcceptor->isVariadic(), + $parametersAcceptor->getReturnType(), + $parametersAcceptor->getReturnType(), + new MixedType(), + TemplateTypeVarianceMap::createEmpty(), + ); + } + + $result = new ResolvedFunctionVariantWithOriginal( $parametersAcceptor, - new TemplateTypeMap(array_merge( - $parametersAcceptor->getTemplateTypeMap()->map(static fn (string $name, Type $type): Type => new ErrorType())->getTypes(), - $typeMap->getTypes(), - )), + $resolvedTemplateTypeMap, + $parametersAcceptor->getCallSiteVarianceMap(), + $passedArgs, ); + if ($originalParametersAcceptor instanceof CallableParametersAcceptor) { + return new ResolvedFunctionVariantWithCallable( + $result, + $originalParametersAcceptor->getThrowPoints(), + $originalParametersAcceptor->isPure(), + $originalParametersAcceptor->getImpurePoints(), + $originalParametersAcceptor->getInvalidateExpressions(), + $originalParametersAcceptor->getUsedVariables(), + $originalParametersAcceptor->acceptsNamedArguments(), + ); + } + + return $result; } } diff --git a/src/Reflection/GlobalConstantReflection.php b/src/Reflection/GlobalConstantReflection.php deleted file mode 100644 index 6483e72e86..0000000000 --- a/src/Reflection/GlobalConstantReflection.php +++ /dev/null @@ -1,24 +0,0 @@ -methodReflection; } @@ -28,9 +32,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return TemplateTypeMap::createEmpty(); } - /** - * @return array - */ + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + public function getParameters(): array { return []; @@ -46,4 +52,40 @@ public function getReturnType(): Type return new MixedType(); } + public function getThrowPoints(): array + { + return []; + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + return [ + new SimpleImpurePoint( + 'methodCall', + 'call to unknown method', + false, + ), + ]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->methodReflection->acceptsNamedArguments(); + } + } diff --git a/src/Reflection/InitializerExprContext.php b/src/Reflection/InitializerExprContext.php new file mode 100644 index 0000000000..e6fb8ab6e5 --- /dev/null +++ b/src/Reflection/InitializerExprContext.php @@ -0,0 +1,250 @@ +getFunction(); + + return new self( + $scope->getFile(), + $scope->getNamespace(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $scope->isInAnonymousFunction() ? '{closure}' : ($function !== null ? $function->getName() : null), + $scope->isInAnonymousFunction() ? '{closure}' : ($function instanceof MethodReflection + ? sprintf('%s::%s', $function->getDeclaringClass()->getName(), $function->getName()) + : ($function instanceof FunctionReflection ? $function->getName() : null)), + $function instanceof PhpMethodFromParserNodeReflection && $function->isPropertyHook() ? $function->getHookedPropertyName() : null, + ); + } + + /** + * @return non-empty-string|null + */ + private static function parseNamespace(string $name): ?string + { + $parts = explode('\\', $name); + if (count($parts) > 1) { + $ns = implode('\\', array_slice($parts, 0, -1)); + if ($ns === '') { + throw new ShouldNotHappenException('Namespace cannot be empty.'); + } + return $ns; + } + + return null; + } + + public static function fromClassReflection(ClassReflection $classReflection): self + { + return self::fromClass($classReflection->getName(), $classReflection->getFileName()); + } + + public static function fromClass(string $className, ?string $fileName): self + { + return new self( + $fileName, + self::parseNamespace($className), + $className, + null, + null, + null, + null, + ); + } + + public static function fromFunction(string $functionName, ?string $fileName): self + { + return new self( + $fileName, + self::parseNamespace($functionName), + null, + null, + $functionName, + $functionName, + null, + ); + } + + public static function fromClassMethod(string $className, ?string $traitName, string $methodName, ?string $fileName): self + { + return new self( + $fileName, + self::parseNamespace($className), + $className, + $traitName, + $methodName, + sprintf('%s::%s', $className, $methodName), + null, + ); + } + + public static function fromReflectionParameter(ReflectionParameter $parameter): self + { + $declaringFunction = $parameter->getDeclaringFunction(); + if ($declaringFunction instanceof ReflectionFunction) { + $file = $declaringFunction->getFileName(); + return new self( + $file === false ? null : $file, + self::parseNamespace($declaringFunction->getName()), + null, + null, + $declaringFunction->getName(), + $declaringFunction->getName(), + null, // Property hook parameter cannot have a default value. fromReflectionParameter is only used for that + ); + } + + $file = $declaringFunction->getFileName(); + + $betterReflection = $declaringFunction->getBetterReflection(); + + return new self( + $file === false ? null : $file, + self::parseNamespace($betterReflection->getDeclaringClass()->getName()), + $declaringFunction->getDeclaringClass()->getName(), + $betterReflection->getDeclaringClass()->isTrait() ? $betterReflection->getDeclaringClass()->getName() : null, + $declaringFunction->getName(), + sprintf('%s::%s', $declaringFunction->getDeclaringClass()->getName(), $declaringFunction->getName()), + null, // Property hook parameter cannot have a default value. fromReflectionParameter is only used for that + ); + } + + public static function fromStubParameter( + ?string $className, + string $stubFile, + ClassMethod|Function_|PropertyHook $function, + ): self + { + $namespace = null; + if ($className !== null) { + $namespace = self::parseNamespace($className); + } else { + if ($function instanceof Function_ && $function->namespacedName !== null) { + $namespace = self::parseNamespace($function->namespacedName->toString()); + } + } + + $functionName = null; + $propertyName = null; + if ($function instanceof Function_ && $function->namespacedName !== null) { + $functionName = $function->namespacedName->toString(); + } elseif ($function instanceof ClassMethod) { + $functionName = $function->name->toString(); + } elseif ($function instanceof PropertyHook) { + $propertyName = $function->getAttribute('propertyName'); + $functionName = sprintf('$%s::%s', $propertyName, $function->name->toString()); + } + + $methodName = null; + if ($function instanceof ClassMethod && $className !== null) { + $methodName = sprintf('%s::%s', $className, $function->name->toString()); + } elseif ($function instanceof PropertyHook) { + $propertyName = $function->getAttribute('propertyName'); + $methodName = sprintf('%s::$%s::%s', $className, $propertyName, $function->name->toString()); + } elseif ($function instanceof Function_ && $function->namespacedName !== null) { + $methodName = $function->namespacedName->toString(); + } + + return new self( + $stubFile, + $namespace, + $className, + null, + $functionName, + $methodName, + $propertyName, + ); + } + + public static function fromGlobalConstant(ReflectionConstant $constant): self + { + return new self( + $constant->getFileName(), + $constant->getNamespaceName(), + null, + null, + null, + null, + null, + ); + } + + public static function createEmpty(): self + { + return new self(null, null, null, null, null, null, null); + } + + public function getFile(): ?string + { + return $this->file; + } + + public function getClassName(): ?string + { + return $this->className; + } + + public function getNamespace(): ?string + { + return $this->namespace; + } + + public function getTraitName(): ?string + { + return $this->traitName; + } + + public function getFunction(): ?string + { + return $this->function; + } + + public function getMethod(): ?string + { + return $this->method; + } + + public function getProperty(): ?string + { + return $this->property; + } + +} diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php new file mode 100644 index 0000000000..fb935ac630 --- /dev/null +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -0,0 +1,2184 @@ + */ + private array $currentlyResolvingClassConstant = []; + + public function __construct( + private ConstantResolver $constantResolver, + private ReflectionProviderProvider $reflectionProviderProvider, + private PhpVersion $phpVersion, + private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, + private OversizedArrayBuilder $oversizedArrayBuilder, + private bool $usePathConstantsAsConstantString, + ) + { + } + + /** @api */ + public function getType(Expr $expr, InitializerExprContext $context): Type + { + if ($expr instanceof TypeExpr) { + return $expr->getExprType(); + } + if ($expr instanceof Int_) { + return new ConstantIntegerType($expr->value); + } + if ($expr instanceof Float_) { + return new ConstantFloatType($expr->value); + } + if ($expr instanceof String_) { + return new ConstantStringType($expr->value); + } + if ($expr instanceof ConstFetch) { + $constName = (string) $expr->name; + $loweredConstName = strtolower($constName); + if ($loweredConstName === 'true') { + return new ConstantBooleanType(true); + } elseif ($loweredConstName === 'false') { + return new ConstantBooleanType(false); + } elseif ($loweredConstName === 'null') { + return new NullType(); + } + + $constant = $this->constantResolver->resolveConstant($expr->name, $context); + if ($constant !== null) { + return $constant; + } + + return new ErrorType(); + } + if ($expr instanceof File) { + $file = $context->getFile(); + if ($file === null) { + return new StringType(); + } + $stringType = new ConstantStringType($file); + return $this->usePathConstantsAsConstantString ? $stringType : $stringType->generalize(GeneralizePrecision::moreSpecific()); + } + if ($expr instanceof Dir) { + $file = $context->getFile(); + if ($file === null) { + return new StringType(); + } + $stringType = new ConstantStringType(dirname($file)); + return $this->usePathConstantsAsConstantString ? $stringType : $stringType->generalize(GeneralizePrecision::moreSpecific()); + } + if ($expr instanceof Line) { + return new ConstantIntegerType($expr->getStartLine()); + } + if ($expr instanceof Expr\New_) { + if ($expr->class instanceof Name) { + return new ObjectType((string) $expr->class); + } + + return new ObjectWithoutClassType(); + } + if ($expr instanceof Expr\Array_) { + return $this->getArrayType($expr, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { + $var = $this->getType($expr->var, $context); + $dim = $this->getType($expr->dim, $context); + return $var->getOffsetValueType($dim); + } + if ($expr instanceof ClassConstFetch && $expr->name instanceof Identifier) { + return $this->getClassConstFetchType($expr->class, $expr->name->toString(), $context->getClassName(), fn (Expr $expr): Type => $this->getType($expr, $context)); + } + if ($expr instanceof Expr\UnaryPlus) { + return $this->getType($expr->expr, $context)->toNumber(); + } + if ($expr instanceof Expr\UnaryMinus) { + return $this->getUnaryMinusType($expr->expr, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + if ($expr instanceof Expr\BinaryOp\Coalesce) { + $leftType = $this->getType($expr->left, $context); + $rightType = $this->getType($expr->right, $context); + + return TypeCombinator::union(TypeCombinator::removeNull($leftType), $rightType); + } + + if ($expr instanceof Expr\Ternary) { + $condType = $this->getType($expr->cond, $context); + $elseType = $this->getType($expr->else, $context); + if ($expr->if === null) { + return TypeCombinator::union( + TypeCombinator::removeFalsey($condType), + $elseType, + ); + } + + $ifType = $this->getType($expr->if, $context); + + return TypeCombinator::union( + TypeCombinator::removeFalsey($ifType), + $elseType, + ); + } + + if ($expr instanceof Expr\FuncCall && $expr->name instanceof Name && $expr->name->toLowerString() === 'constant') { + $firstArg = $expr->args[0] ?? null; + if ($firstArg instanceof Arg && $firstArg->value instanceof String_) { + $constant = $this->constantResolver->resolvePredefinedConstant($firstArg->value->value); + if ($constant !== null) { + return $constant; + } + } + } + + if ($expr instanceof Expr\BooleanNot) { + $exprBooleanType = $this->getType($expr->expr, $context)->toBoolean(); + + if ($exprBooleanType instanceof ConstantBooleanType) { + return new ConstantBooleanType(!$exprBooleanType->getValue()); + } + + return new BooleanType(); + } + + if ($expr instanceof Expr\BitwiseNot) { + return $this->getBitwiseNotType($expr->expr, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\Concat) { + return $this->getConcatType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\BitwiseAnd) { + return $this->getBitwiseAndType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\BitwiseOr) { + return $this->getBitwiseOrType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\BitwiseXor) { + return $this->getBitwiseXorType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\Spaceship) { + return $this->getSpaceshipType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ( + $expr instanceof Expr\BinaryOp\BooleanAnd + || $expr instanceof Expr\BinaryOp\LogicalAnd + || $expr instanceof Expr\BinaryOp\BooleanOr + || $expr instanceof Expr\BinaryOp\LogicalOr + ) { + return new BooleanType(); + } + + if ($expr instanceof Expr\BinaryOp\Div) { + return $this->getDivType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\Mod) { + return $this->getModType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\Plus) { + return $this->getPlusType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\Minus) { + return $this->getMinusType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\Mul) { + return $this->getMulType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\Pow) { + return $this->getPowType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\ShiftLeft) { + return $this->getShiftLeftType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof Expr\BinaryOp\ShiftRight) { + return $this->getShiftRightType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context)); + } + + if ($expr instanceof BinaryOp\Identical) { + return $this->resolveIdenticalType( + $this->getType($expr->left, $context), + $this->getType($expr->right, $context), + )->type; + } + + if ($expr instanceof BinaryOp\NotIdentical) { + return $this->getType(new Expr\BooleanNot(new BinaryOp\Identical($expr->left, $expr->right)), $context); + } + + if ($expr instanceof BinaryOp\Equal) { + return $this->resolveEqualType( + $this->getType($expr->left, $context), + $this->getType($expr->right, $context), + )->type; + } + + if ($expr instanceof BinaryOp\NotEqual) { + return $this->getType(new Expr\BooleanNot(new BinaryOp\Equal($expr->left, $expr->right)), $context); + } + + if ($expr instanceof Expr\BinaryOp\Smaller) { + return $this->getType($expr->left, $context)->isSmallerThan($this->getType($expr->right, $context), $this->phpVersion)->toBooleanType(); + } + + if ($expr instanceof Expr\BinaryOp\SmallerOrEqual) { + return $this->getType($expr->left, $context)->isSmallerThanOrEqual($this->getType($expr->right, $context), $this->phpVersion)->toBooleanType(); + } + + if ($expr instanceof Expr\BinaryOp\Greater) { + return $this->getType($expr->right, $context)->isSmallerThan($this->getType($expr->left, $context), $this->phpVersion)->toBooleanType(); + } + + if ($expr instanceof Expr\BinaryOp\GreaterOrEqual) { + return $this->getType($expr->right, $context)->isSmallerThanOrEqual($this->getType($expr->left, $context), $this->phpVersion)->toBooleanType(); + } + + if ($expr instanceof Expr\BinaryOp\LogicalXor) { + $leftBooleanType = $this->getType($expr->left, $context)->toBoolean(); + $rightBooleanType = $this->getType($expr->right, $context)->toBoolean(); + + if ( + $leftBooleanType instanceof ConstantBooleanType + && $rightBooleanType instanceof ConstantBooleanType + ) { + return new ConstantBooleanType( + $leftBooleanType->getValue() xor $rightBooleanType->getValue(), + ); + } + + return new BooleanType(); + } + + if ($expr instanceof MagicConst\Class_) { + if ($context->getTraitName() !== null) { + return TypeCombinator::intersect( + new ClassStringType(), + new AccessoryLiteralStringType(), + ); + } + + if ($context->getClassName() === null) { + return new ConstantStringType(''); + } + + return new ConstantStringType($context->getClassName(), true); + } + + if ($expr instanceof MagicConst\Namespace_) { + if ($context->getTraitName() !== null) { + return TypeCombinator::intersect( + new StringType(), + new AccessoryLiteralStringType(), + ); + } + + return new ConstantStringType($context->getNamespace() ?? ''); + } + + if ($expr instanceof MagicConst\Method) { + return new ConstantStringType($context->getMethod() ?? ''); + } + + if ($expr instanceof MagicConst\Function_) { + return new ConstantStringType($context->getFunction() ?? ''); + } + + if ($expr instanceof MagicConst\Trait_) { + if ($context->getTraitName() === null) { + return new ConstantStringType(''); + } + + return new ConstantStringType($context->getTraitName(), true); + } + + if ($expr instanceof MagicConst\Property) { + $contextProperty = $context->getProperty(); + if ($contextProperty === null) { + return new ConstantStringType(''); + } + + return new ConstantStringType($contextProperty); + } + + if ($expr instanceof PropertyFetch && $expr->name instanceof Identifier) { + $fetchedOnType = $this->getType($expr->var, $context); + if (!$fetchedOnType->hasProperty($expr->name->name)->yes()) { + return new ErrorType(); + } + + return $fetchedOnType->getProperty($expr->name->name, new OutOfClassScope())->getReadableType(); + } + + return new MixedType(); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getConcatType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + return $this->resolveConcatType($leftType, $rightType); + } + + public function resolveConcatType(Type $left, Type $right): Type + { + $leftStringType = $left->toString(); + $rightStringType = $right->toString(); + if (TypeCombinator::union( + $leftStringType, + $rightStringType, + ) 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); + } + + $leftConstantStrings = $leftStringType->getConstantStrings(); + $rightConstantStrings = $rightStringType->getConstantStrings(); + $combinedConstantStringsCount = count($leftConstantStrings) * count($rightConstantStrings); + + // we limit the number of union-types for performance reasons + if ($combinedConstantStringsCount > 0 && $combinedConstantStringsCount <= 16) { + $strings = []; + + foreach ($leftConstantStrings as $leftConstantString) { + if ($leftConstantString->getValue() === '') { + $strings = array_merge($strings, $rightConstantStrings); + + continue; + } + + foreach ($rightConstantStrings as $rightConstantString) { + if ($rightConstantString->getValue() === '') { + $strings[] = $leftConstantString; + + continue; + } + + $strings[] = $leftConstantString->append($rightConstantString); + } + } + + if (count($strings) > 0) { + return TypeCombinator::union(...$strings); + } + } + + $accessoryTypes = []; + if ($leftStringType->isNonEmptyString()->and($rightStringType->isNonEmptyString())->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($leftStringType->isNonFalsyString()->or($rightStringType->isNonFalsyString())->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($leftStringType->isNonEmptyString()->or($rightStringType->isNonEmptyString())->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + + if ($leftStringType->isLiteralString()->and($rightStringType->isLiteralString())->yes()) { + $accessoryTypes[] = new AccessoryLiteralStringType(); + } + + if ($leftStringType->isLowercaseString()->and($rightStringType->isLowercaseString())->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + + if ($leftStringType->isUppercaseString()->and($rightStringType->isUppercaseString())->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } + + $leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType('')); + if ($leftNumericStringNonEmpty->isNumericString()->yes()) { + $allRightConstantsZeroOrMore = false; + foreach ($rightConstantStrings as $rightConstantString) { + if ($rightConstantString->getValue() === '') { + continue; + } + + if ( + !is_numeric($rightConstantString->getValue()) + || Strings::match($rightConstantString->getValue(), '#^[0-9]+$#') === null + ) { + $allRightConstantsZeroOrMore = false; + break; + } + + $allRightConstantsZeroOrMore = true; + } + + $zeroOrMoreInteger = IntegerRangeType::fromInterval(0, null); + $nonNegativeRight = $allRightConstantsZeroOrMore || $zeroOrMoreInteger->isSuperTypeOf($right)->yes(); + if ($nonNegativeRight) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } + } + + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + return new IntersectionType($accessoryTypes); + } + + return new StringType(); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type + { + if (count($expr->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + return $this->oversizedArrayBuilder->build($expr, $getTypeCallback); + } + + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $isList = null; + foreach ($expr->items as $arrayItem) { + $valueType = $getTypeCallback($arrayItem->value); + if ($arrayItem->unpack) { + $constantArrays = $valueType->getConstantArrays(); + if (count($constantArrays) === 1) { + $constantArrayType = $constantArrays[0]; + $hasStringKey = false; + foreach ($constantArrayType->getKeyTypes() as $keyType) { + if ($keyType->isString()->yes()) { + $hasStringKey = true; + break; + } + } + + foreach ($constantArrayType->getValueTypes() as $i => $innerValueType) { + if ($hasStringKey && $this->phpVersion->supportsArrayUnpackingWithStringKeys()) { + $arrayBuilder->setOffsetValueType($constantArrayType->getKeyTypes()[$i], $innerValueType, $constantArrayType->isOptionalKey($i)); + } else { + $arrayBuilder->setOffsetValueType(null, $innerValueType, $constantArrayType->isOptionalKey($i)); + } + } + } else { + $arrayBuilder->degradeToGeneralArray(); + + if ($this->phpVersion->supportsArrayUnpackingWithStringKeys() && !$valueType->getIterableKeyType()->isString()->no()) { + $isList = false; + $offsetType = $valueType->getIterableKeyType(); + } else { + $isList ??= $arrayBuilder->isList(); + $offsetType = new IntegerType(); + } + + $arrayBuilder->setOffsetValueType($offsetType, $valueType->getIterableValueType(), !$valueType->isIterableAtLeastOnce()->yes()); + } + } else { + $arrayBuilder->setOffsetValueType( + $arrayItem->key !== null ? $getTypeCallback($arrayItem->key) : null, + $valueType, + ); + } + } + + $arrayType = $arrayBuilder->getArray(); + if ($isList === true) { + return TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } + + return $arrayType; + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return $this->getNeverType($leftType, $rightType); + } + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + if ($leftTypeInner instanceof ConstantStringType && $rightTypeInner instanceof ConstantStringType) { + $resultType = $this->getTypeFromValue($leftTypeInner->getValue() & $rightTypeInner->getValue()); + } else { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + $resultType = $this->getTypeFromValue($leftNumberType->getValue() & $rightNumberType->getValue()); + } + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + if ($leftType->isString()->yes() && $rightType->isString()->yes()) { + return new StringType(); + } + + $leftNumberType = $leftType->toNumber(); + $rightNumberType = $rightType->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if ($rightNumberType instanceof ConstantIntegerType && $rightNumberType->getValue() >= 0) { + return IntegerRangeType::fromInterval(0, $rightNumberType->getValue()); + } + if ($leftNumberType instanceof ConstantIntegerType && $leftNumberType->getValue() >= 0) { + return IntegerRangeType::fromInterval(0, $leftNumberType->getValue()); + } + + return new IntegerType(); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return $this->getNeverType($leftType, $rightType); + } + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + if ($leftTypeInner instanceof ConstantStringType && $rightTypeInner instanceof ConstantStringType) { + $resultType = $this->getTypeFromValue($leftTypeInner->getValue() | $rightTypeInner->getValue()); + } else { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + $resultType = $this->getTypeFromValue($leftNumberType->getValue() | $rightNumberType->getValue()); + } + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + if ($leftType->isString()->yes() && $rightType->isString()->yes()) { + return new StringType(); + } + + if (TypeCombinator::union($leftType->toNumber(), $rightType->toNumber()) instanceof ErrorType) { + return new ErrorType(); + } + + return new IntegerType(); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return $this->getNeverType($leftType, $rightType); + } + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + if ($leftTypeInner instanceof ConstantStringType && $rightTypeInner instanceof ConstantStringType) { + $resultType = $this->getTypeFromValue($leftTypeInner->getValue() ^ $rightTypeInner->getValue()); + } else { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + $resultType = $this->getTypeFromValue($leftNumberType->getValue() ^ $rightNumberType->getValue()); + } + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + if ($leftType->isString()->yes() && $rightType->isString()->yes()) { + return new StringType(); + } + + if (TypeCombinator::union($leftType->toNumber(), $rightType->toNumber()) instanceof ErrorType) { + return new ErrorType(); + } + + return new IntegerType(); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getSpaceshipType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $callbackLeftType = $getTypeCallback($left); + $callbackRightType = $getTypeCallback($right); + + if ($callbackLeftType instanceof NeverType || $callbackRightType instanceof NeverType) { + return $this->getNeverType($callbackLeftType, $callbackRightType); + } + + $leftTypes = $callbackLeftType->getConstantScalarTypes(); + $rightTypes = $callbackRightType->getConstantScalarTypes(); + + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0 && $leftTypesCount * $rightTypesCount <= self::CALCULATE_SCALARS_LIMIT) { + $resultTypes = []; + foreach ($leftTypes as $leftType) { + foreach ($rightTypes as $rightType) { + $leftValue = $leftType->getValue(); + $rightValue = $rightType->getValue(); + $resultType = $this->getTypeFromValue($leftValue <=> $rightValue); + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + return IntegerRangeType::fromInterval(-1, 1); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getDivType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + if (in_array($rightNumberType->getValue(), [0, 0.0], true)) { + return new ErrorType(); + } + + $resultType = $this->getTypeFromValue($leftNumberType->getValue() / $rightNumberType->getValue()); // @phpstan-ignore binaryOp.invalid + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + $rightScalarValues = $rightType->toNumber()->getConstantScalarValues(); + foreach ($rightScalarValues as $scalarValue) { + if ($scalarValue === 0 || $scalarValue === 0.0) { + return new ErrorType(); + } + } + + return $this->resolveCommonMath(new BinaryOp\Div($left, $right), $leftType, $rightType); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getModType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return $this->getNeverType($leftType, $rightType); + } + + $extensionSpecified = $this->callOperatorTypeSpecifyingExtensions(new BinaryOp\Mod($left, $right), $leftType, $rightType); + if ($extensionSpecified !== null) { + return $extensionSpecified; + } + + if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) { + return new ErrorType(); + } + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + $rightIntegerValue = (int) $rightNumberType->getValue(); + if ($rightIntegerValue === 0) { + return new ErrorType(); + } + + $resultType = $this->getTypeFromValue((int) $leftNumberType->getValue() % $rightIntegerValue); + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + return TypeCombinator::union(...$resultTypes); + } + + $integerType = $rightType->toInteger(); + if ($integerType instanceof ConstantIntegerType && $integerType->getValue() === 1) { + return new ConstantIntegerType(0); + } + + $rightScalarValues = $rightType->toNumber()->getConstantScalarValues(); + foreach ($rightScalarValues as $scalarValue) { + + if ($scalarValue === 0 || $scalarValue === 0.0) { + return new ErrorType(); + } + } + + $positiveInt = IntegerRangeType::fromInterval(0, null); + if ($rightType->isInteger()->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(); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return $this->getNeverType($leftType, $rightType); + } + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + $resultType = $this->getTypeFromValue($leftNumberType->getValue() + $rightNumberType->getValue()); + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + $leftConstantArrays = $leftType->getConstantArrays(); + $rightConstantArrays = $rightType->getConstantArrays(); + + $leftCount = count($leftConstantArrays); + $rightCount = count($rightConstantArrays); + if ($leftCount > 0 && $rightCount > 0 + && ($leftCount + $rightCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT)) { + $resultTypes = []; + foreach ($rightConstantArrays as $rightConstantArray) { + foreach ($leftConstantArrays as $leftConstantArray) { + $newArrayBuilder = ConstantArrayTypeBuilder::createFromConstantArray($rightConstantArray); + foreach ($leftConstantArray->getKeyTypes() as $i => $leftKeyType) { + $optional = $leftConstantArray->isOptionalKey($i); + $valueType = $leftConstantArray->getOffsetValueType($leftKeyType); + if (!$optional) { + if ($rightConstantArray->hasOffsetValueType($leftKeyType)->maybe()) { + $valueType = TypeCombinator::union($valueType, $rightConstantArray->getOffsetValueType($leftKeyType)); + } + } + $newArrayBuilder->setOffsetValueType( + $leftKeyType, + $valueType, + $optional, + ); + } + $resultTypes[] = $newArrayBuilder->getArray(); + } + } + return TypeCombinator::union(...$resultTypes); + } + + $leftIsArray = $leftType->isArray(); + $rightIsArray = $rightType->isArray(); + if ($leftIsArray->yes() && $rightIsArray->yes()) { + if ($leftType->getIterableKeyType()->equals($rightType->getIterableKeyType())) { + // to preserve BenevolentUnionType + $keyType = $leftType->getIterableKeyType(); + } else { + $keyTypes = []; + foreach ([ + $leftType->getIterableKeyType(), + $rightType->getIterableKeyType(), + ] as $keyType) { + $keyTypes[] = $keyType; + } + $keyType = TypeCombinator::union(...$keyTypes); + } + + $arrayType = new ArrayType( + $keyType, + TypeCombinator::union($leftType->getIterableValueType(), $rightType->getIterableValueType()), + ); + + if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + if ($leftType->isList()->yes() && $rightType->isList()->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } + + return $arrayType; + } + + if ($leftType instanceof MixedType && $rightType instanceof MixedType) { + if ( + ($leftIsArray->no() && $rightIsArray->no()) + ) { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + new ArrayType(new MixedType(), new MixedType()), + ]); + } + + if ( + ($leftIsArray->yes() && $rightIsArray->no()) + || ($leftIsArray->no() && $rightIsArray->yes()) + ) { + return new ErrorType(); + } + + if ( + ($leftIsArray->yes() && $rightIsArray->maybe()) + || ($leftIsArray->maybe() && $rightIsArray->yes()) + ) { + $resultType = new ArrayType(new MixedType(), new MixedType()); + if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { + return TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + + return $resultType; + } + + if ($leftIsArray->maybe() && $rightIsArray->maybe()) { + $plusable = new UnionType([ + new StringType(), + new FloatType(), + new IntegerType(), + new ArrayType(new MixedType(), new MixedType()), + new BooleanType(), + ]); + + $plusableSuperTypeOfLeft = $plusable->isSuperTypeOf($leftType)->yes(); + $plusableSuperTypeOfRight = $plusable->isSuperTypeOf($rightType)->yes(); + if ($plusableSuperTypeOfLeft && $plusableSuperTypeOfRight) { + return TypeCombinator::union($leftType, $rightType); + } + if ($plusableSuperTypeOfLeft && $rightType instanceof MixedType) { + return $leftType; + } + if ($plusableSuperTypeOfRight && $leftType instanceof MixedType) { + return $rightType; + } + } + + return $this->resolveCommonMath(new BinaryOp\Plus($left, $right), $leftType, $rightType); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getMinusType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + $resultType = $this->getTypeFromValue($leftNumberType->getValue() - $rightNumberType->getValue()); + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + return $this->resolveCommonMath(new BinaryOp\Minus($left, $right), $leftType, $rightType); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getMulType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + $resultType = $this->getTypeFromValue($leftNumberType->getValue() * $rightNumberType->getValue()); + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + $leftNumberType = $leftType->toNumber(); + if ($leftNumberType instanceof ConstantIntegerType && $leftNumberType->getValue() === 0) { + if ($rightType->isFloat()->yes()) { + return new ConstantFloatType(0.0); + } + return new ConstantIntegerType(0); + } + $rightNumberType = $rightType->toNumber(); + if ($rightNumberType instanceof ConstantIntegerType && $rightNumberType->getValue() === 0) { + if ($leftType->isFloat()->yes()) { + return new ConstantFloatType(0.0); + } + return new ConstantIntegerType(0); + } + + return $this->resolveCommonMath(new BinaryOp\Mul($left, $right), $leftType, $rightType); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getPowType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + $extensionSpecified = $this->callOperatorTypeSpecifyingExtensions(new BinaryOp\Pow($left, $right), $leftType, $rightType); + if ($extensionSpecified !== null) { + return $extensionSpecified; + } + + $exponentiatedTyped = $leftType->exponentiate($rightType); + if (!$exponentiatedTyped instanceof ErrorType) { + return $exponentiatedTyped; + } + + return new ErrorType(); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return $this->getNeverType($leftType, $rightType); + } + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + if ($rightNumberType->getValue() < 0) { + return new ErrorType(); + } + + $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) << intval($rightNumberType->getValue())); + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + $leftNumberType = $leftType->toNumber(); + $rightNumberType = $rightType->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + return $this->resolveCommonMath(new Expr\BinaryOp\ShiftLeft($left, $right), $leftType, $rightType); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCallback): Type + { + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return $this->getNeverType($leftType, $rightType); + } + + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { + $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; + foreach ($leftTypes as $leftTypeInner) { + foreach ($rightTypes as $rightTypeInner) { + $leftNumberType = $leftTypeInner->toNumber(); + $rightNumberType = $rightTypeInner->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + if ($rightNumberType->getValue() < 0) { + return new ErrorType(); + } + + $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) >> intval($rightNumberType->getValue())); + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; + } + } + + return TypeCombinator::union(...$resultTypes); + } + + $leftNumberType = $leftType->toNumber(); + $rightNumberType = $rightType->toNumber(); + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + + return $this->resolveCommonMath(new Expr\BinaryOp\ShiftRight($left, $right), $leftType, $rightType); + } + + /** + * @return TypeResult + */ + public function resolveIdenticalType(Type $leftType, Type $rightType): TypeResult + { + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return new TypeResult(new ConstantBooleanType(false), []); + } + + if ($leftType instanceof ConstantScalarType && $rightType instanceof ConstantScalarType) { + return new TypeResult(new ConstantBooleanType($leftType->getValue() === $rightType->getValue()), []); + } + + $leftTypeFiniteTypes = $leftType->getFiniteTypes(); + $rightTypeFiniteType = $rightType->getFiniteTypes(); + if (count($leftTypeFiniteTypes) === 1 && count($rightTypeFiniteType) === 1) { + return new TypeResult(new ConstantBooleanType($leftTypeFiniteTypes[0]->equals($rightTypeFiniteType[0])), []); + } + + $leftIsSuperTypeOfRight = $leftType->isSuperTypeOf($rightType); + $rightIsSuperTypeOfLeft = $rightType->isSuperTypeOf($leftType); + if ($leftIsSuperTypeOfRight->no() && $rightIsSuperTypeOfLeft->no()) { + return new TypeResult(new ConstantBooleanType(false), array_merge($leftIsSuperTypeOfRight->reasons, $rightIsSuperTypeOfLeft->reasons)); + } + + if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) { + return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveIdenticalType($leftValueType, $rightValueType)); + } + + return new TypeResult(new BooleanType(), []); + } + + /** + * @return TypeResult + */ + public function resolveEqualType(Type $leftType, Type $rightType): TypeResult + { + if ( + ($leftType->isEnum()->yes() && $rightType->isTrue()->no()) + || ($rightType->isEnum()->yes() && $leftType->isTrue()->no()) + ) { + return $this->resolveIdenticalType($leftType, $rightType); + } + + if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) { + return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveEqualType($leftValueType, $rightValueType)); + } + + return new TypeResult($leftType->looseCompare($rightType, $this->phpVersion), []); + } + + /** + * @param callable(Type, Type): TypeResult $valueComparisonCallback + * @return TypeResult + */ + private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, ConstantArrayType $rightType, callable $valueComparisonCallback): TypeResult + { + $leftKeyTypes = $leftType->getKeyTypes(); + $rightKeyTypes = $rightType->getKeyTypes(); + $leftValueTypes = $leftType->getValueTypes(); + $rightValueTypes = $rightType->getValueTypes(); + + $resultType = new ConstantBooleanType(true); + + foreach ($leftKeyTypes as $i => $leftKeyType) { + $leftOptional = $leftType->isOptionalKey($i); + if ($leftOptional) { + $resultType = new BooleanType(); + } + + if (count($rightKeyTypes) === 0) { + if (!$leftOptional) { + return new TypeResult(new ConstantBooleanType(false), []); + } + continue; + } + + $found = false; + foreach ($rightKeyTypes as $j => $rightKeyType) { + unset($rightKeyTypes[$j]); + + if ($leftKeyType->equals($rightKeyType)) { + $found = true; + break; + } elseif (!$rightType->isOptionalKey($j)) { + return new TypeResult(new ConstantBooleanType(false), []); + } + } + + if (!$found) { + if (!$leftOptional) { + return new TypeResult(new ConstantBooleanType(false), []); + } + continue; + } + + if (!isset($j)) { + throw new ShouldNotHappenException(); + } + + $rightOptional = $rightType->isOptionalKey($j); + if ($rightOptional) { + $resultType = new BooleanType(); + if ($leftOptional) { + continue; + } + } + + $leftIdenticalToRightResult = $valueComparisonCallback($leftValueTypes[$i], $rightValueTypes[$j]); + $leftIdenticalToRight = $leftIdenticalToRightResult->type; + if ($leftIdenticalToRight->isFalse()->yes()) { + return $leftIdenticalToRightResult; + } + $resultType = TypeCombinator::union($resultType, $leftIdenticalToRight); + } + + foreach (array_keys($rightKeyTypes) as $j) { + if (!$rightType->isOptionalKey($j)) { + return new TypeResult(new ConstantBooleanType(false), []); + } + $resultType = new BooleanType(); + } + + return new TypeResult($resultType->toBoolean(), []); + } + + private function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type $leftType, Type $rightType): ?Type + { + $operatorSigil = $expr->getOperatorSigil(); + $operatorTypeSpecifyingExtensions = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()->getOperatorTypeSpecifyingExtensions($operatorSigil, $leftType, $rightType); + + /** @var Type[] $extensionTypes */ + $extensionTypes = []; + + foreach ($operatorTypeSpecifyingExtensions as $extension) { + $extensionTypes[] = $extension->specifyType($operatorSigil, $leftType, $rightType); + } + + if (count($extensionTypes) > 0) { + return TypeCombinator::union(...$extensionTypes); + } + + return null; + } + + /** + * @param BinaryOp\Plus|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Div|BinaryOp\ShiftLeft|BinaryOp\ShiftRight $expr + */ + private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type + { + $types = TypeCombinator::union($leftType, $rightType); + $leftNumberType = $leftType->toNumber(); + $rightNumberType = $rightType->toNumber(); + + if ( + !$types instanceof MixedType + && ( + $rightNumberType instanceof IntegerRangeType + || $rightNumberType instanceof ConstantIntegerType + || $rightNumberType instanceof UnionType + ) + ) { + if ($leftNumberType instanceof IntegerRangeType || $leftNumberType instanceof ConstantIntegerType) { + return $this->integerRangeMath( + $leftNumberType, + $expr, + $rightNumberType, + ); + } elseif ($leftNumberType instanceof UnionType) { + $unionParts = []; + + foreach ($leftNumberType->getTypes() as $type) { + $numberType = $type->toNumber(); + if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) { + $unionParts[] = $this->integerRangeMath($numberType, $expr, $rightNumberType); + } else { + $unionParts[] = $numberType; + } + } + + $union = TypeCombinator::union(...$unionParts); + if ($leftNumberType instanceof BenevolentUnionType) { + return TypeUtils::toBenevolentUnion($union)->toNumber(); + } + + return $union->toNumber(); + } + } + + $specifiedTypes = $this->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + + if ( + $leftType->isArray()->yes() + || $rightType->isArray()->yes() + || $types->isArray()->yes() + ) { + return new ErrorType(); + } + + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } + if ($leftNumberType instanceof NeverType || $rightNumberType instanceof NeverType) { + return $this->getNeverType($leftNumberType, $rightNumberType); + } + + if ( + $leftNumberType->isFloat()->yes() + || $rightNumberType->isFloat()->yes() + ) { + if ($expr instanceof Expr\BinaryOp\ShiftLeft || $expr instanceof Expr\BinaryOp\ShiftRight) { + return new IntegerType(); + } + return new FloatType(); + } + + $resultType = TypeCombinator::union($leftNumberType, $rightNumberType); + if ($expr instanceof Expr\BinaryOp\Div) { + if ($types instanceof MixedType || $resultType->isInteger()->yes()) { + return new BenevolentUnionType([new IntegerType(), new FloatType()]); + } + + return new UnionType([new IntegerType(), new FloatType()]); + } + + if ($types instanceof MixedType + || $leftType instanceof BenevolentUnionType + || $rightType instanceof BenevolentUnionType + ) { + return TypeUtils::toBenevolentUnion($resultType); + } + + return $resultType; + } + + /** + * @param ConstantIntegerType|IntegerRangeType $range + * @param BinaryOp\Div|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Plus|BinaryOp\ShiftLeft|BinaryOp\ShiftRight $node + */ + private function integerRangeMath(Type $range, BinaryOp $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) { + $numberType = $type->toNumber(); + if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) { + $unionParts[] = $this->integerRangeMath($range, $node, $numberType); + } else { + $unionParts[] = $type->toNumber(); + } + } + + $union = TypeCombinator::union(...$unionParts); + if ($operand instanceof BenevolentUnionType) { + return TypeUtils::toBenevolentUnion($union)->toNumber(); + } + + return $union->toNumber(); + } + + $operand = $operand->toNumber(); + if ($operand instanceof IntegerRangeType) { + $operandMin = $operand->getMin(); + $operandMax = $operand->getMax(); + } elseif ($operand instanceof ConstantIntegerType) { + $operandMin = $operand->getValue(); + $operandMax = $operand->getValue(); + } else { + return $operand; + } + + if ($node instanceof BinaryOp\Plus) { + if ($operand instanceof ConstantIntegerType) { + /** @var int|float|null $min */ + $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 BinaryOp\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 Expr\BinaryOp\Mul) { + $min1 = $rangeMin === 0 || $operandMin === 0 ? 0 : ($rangeMin ?? -INF) * ($operandMin ?? -INF); + $min2 = $rangeMin === 0 || $operandMax === 0 ? 0 : ($rangeMin ?? -INF) * ($operandMax ?? INF); + $max1 = $rangeMax === 0 || $operandMin === 0 ? 0 : ($rangeMax ?? INF) * ($operandMin ?? -INF); + $max2 = $rangeMax === 0 || $operandMax === 0 ? 0 : ($rangeMax ?? INF) * ($operandMax ?? INF); + + $min = min($min1, $min2, $max1, $max2); + $max = max($min1, $min2, $max1, $max2); + + if (!is_finite($min)) { + $min = null; + } + if (!is_finite($max)) { + $max = null; + } + } elseif ($node instanceof Expr\BinaryOp\Div) { + 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 { + // Avoid division by zero when looking for the min and the max by using the closest int + $operandMin = $operandMin !== 0 ? $operandMin : 1; + $operandMax = $operandMax !== 0 ? $operandMax : -1; + + if ( + ($operandMin < 0 || $operandMin === null) + && ($operandMax > 0 || $operandMax === null) + ) { + $negativeOperand = IntegerRangeType::fromInterval($operandMin, 0); + assert($negativeOperand instanceof IntegerRangeType); + $positiveOperand = IntegerRangeType::fromInterval(0, $operandMax); + assert($positiveOperand instanceof IntegerRangeType); + + $result = TypeCombinator::union( + $this->integerRangeMath($range, $node, $negativeOperand), + $this->integerRangeMath($range, $node, $positiveOperand), + )->toNumber(); + + if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { + return new BenevolentUnionType([new IntegerType(), new FloatType()]); + } + + return $result; + } + if ( + ($rangeMin < 0 || $rangeMin === null) + && ($rangeMax > 0 || $rangeMax === null) + ) { + $negativeRange = IntegerRangeType::fromInterval($rangeMin, 0); + assert($negativeRange instanceof IntegerRangeType); + $positiveRange = IntegerRangeType::fromInterval(0, $rangeMax); + assert($positiveRange instanceof IntegerRangeType); + + $result = TypeCombinator::union( + $this->integerRangeMath($negativeRange, $node, $operand), + $this->integerRangeMath($positiveRange, $node, $operand), + )->toNumber(); + + if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { + return new BenevolentUnionType([new IntegerType(), new FloatType()]); + } + + return $result; + } + + $rangeMinSign = ($rangeMin ?? -INF) <=> 0; + $rangeMaxSign = ($rangeMax ?? INF) <=> 0; + + $min1 = $operandMin !== null ? ($rangeMin ?? -INF) / $operandMin : $rangeMinSign * -0.1; + $min2 = $operandMax !== null ? ($rangeMin ?? -INF) / $operandMax : $rangeMinSign * 0.1; + $max1 = $operandMin !== null ? ($rangeMax ?? INF) / $operandMin : $rangeMaxSign * -0.1; + $max2 = $operandMax !== null ? ($rangeMax ?? INF) / $operandMax : $rangeMaxSign * 0.1; + + $min = min($min1, $min2, $max1, $max2); + $max = max($min1, $min2, $max1, $max2); + + if ($min === -INF) { + $min = null; + } + if ($max === INF) { + $max = null; + } + } + + if ($min !== null && $max !== null && $min > $max) { + [$min, $max] = [$max, $min]; + } + + if (is_float($min)) { + $min = (int) ceil($min); + } + if (is_float($max)) { + $max = (int) floor($max); + } + + // 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()); + } elseif ($node instanceof Expr\BinaryOp\ShiftLeft) { + if (!$operand instanceof ConstantIntegerType) { + return new IntegerType(); + } + if ($operand->getValue() < 0) { + return new ErrorType(); + } + $min = $rangeMin !== null ? intval($rangeMin) << $operand->getValue() : null; + $max = $rangeMax !== null ? intval($rangeMax) << $operand->getValue() : null; + } elseif ($node instanceof Expr\BinaryOp\ShiftRight) { + if (!$operand instanceof ConstantIntegerType) { + return new IntegerType(); + } + if ($operand->getValue() < 0) { + return new ErrorType(); + } + $min = $rangeMin !== null ? intval($rangeMin) >> $operand->getValue() : null; + $max = $rangeMax !== null ? intval($rangeMax) >> $operand->getValue() : null; + } else { + throw new ShouldNotHappenException(); + } + + if (is_float($min)) { + $min = null; + } + if (is_float($max)) { + $max = null; + } + + return IntegerRangeType::fromInterval($min, $max); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getClassConstFetchTypeByReflection(Name|Expr $class, string $constantName, ?ClassReflection $classReflection, callable $getTypeCallback): Type + { + $isObject = false; + if ($class instanceof Name) { + $constantClass = (string) $class; + $constantClassType = new ObjectType($constantClass); + $namesToResolve = [ + 'self', + 'parent', + ]; + if ($classReflection !== null) { + if ($classReflection->isFinal()) { + $namesToResolve[] = 'static'; + } elseif (strtolower($constantClass) === 'static') { + if (strtolower($constantName) === 'class') { + return new GenericClassStringType(new StaticType($classReflection)); + } + + $namesToResolve[] = 'static'; + $isObject = true; + } + } + if (in_array(strtolower($constantClass), $namesToResolve, true)) { + $resolvedName = $this->resolveName($class, $classReflection); + if (strtolower($resolvedName) === 'parent' && strtolower($constantName) === 'class') { + return new ClassStringType(); + } + $constantClassType = $this->resolveTypeByName($class, $classReflection); + } + + if (strtolower($constantName) === 'class') { + return new ConstantStringType($constantClassType->getClassName(), true); + } + } elseif ($class instanceof String_ && strtolower($constantName) === 'class') { + return new ConstantStringType($class->value, true); + } else { + $constantClassType = $getTypeCallback($class); + $isObject = true; + } + + if (strtolower($constantName) === 'class') { + return TypeTraverser::map( + $constantClassType, + function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof NullType) { + return $type; + } + + if ($type instanceof EnumCaseObjectType) { + return TypeCombinator::intersect( + new GenericClassStringType(new ObjectType($type->getClassName())), + new AccessoryLiteralStringType(), + ); + } + + $objectClassNames = $type->getObjectClassNames(); + if (count($objectClassNames) > 1) { + throw new ShouldNotHappenException(); + } + + if ($type instanceof TemplateType && $objectClassNames === []) { + return TypeCombinator::intersect( + new GenericClassStringType($type), + new AccessoryLiteralStringType(), + ); + } elseif ($objectClassNames !== [] && $this->getReflectionProvider()->hasClass($objectClassNames[0])) { + $reflection = $this->getReflectionProvider()->getClass($objectClassNames[0]); + if ($reflection->isFinalByKeyword()) { + return new ConstantStringType($reflection->getName(), true); + } + + return TypeCombinator::intersect( + new GenericClassStringType($type), + new AccessoryLiteralStringType(), + ); + } elseif ($type->isObject()->yes()) { + return TypeCombinator::intersect( + new ClassStringType(), + new AccessoryLiteralStringType(), + ); + } + + return new ErrorType(); + }, + ); + } + + if ($constantClassType->isClassString()->yes()) { + if ($constantClassType->isConstantScalarValue()->yes()) { + $isObject = false; + } + $constantClassType = $constantClassType->getClassStringObjectType(); + } + + $types = []; + foreach ($constantClassType->getObjectClassNames() as $referencedClass) { + if (!$this->getReflectionProvider()->hasClass($referencedClass)) { + continue; + } + + $constantClassReflection = $this->getReflectionProvider()->getClass($referencedClass); + if (!$constantClassReflection->hasConstant($constantName)) { + continue; + } + + if ($constantClassReflection->isEnum() && $constantClassReflection->hasEnumCase($constantName)) { + $types[] = new EnumCaseObjectType($constantClassReflection->getName(), $constantName); + continue; + } + + $resolvingName = sprintf('%s::%s', $constantClassReflection->getName(), $constantName); + if (array_key_exists($resolvingName, $this->currentlyResolvingClassConstant)) { + $types[] = new MixedType(); + continue; + } + + $this->currentlyResolvingClassConstant[$resolvingName] = true; + + if (!$isObject) { + $reflectionConstant = $constantClassReflection->getNativeReflection()->getReflectionConstant($constantName); + if ($reflectionConstant === false) { + unset($this->currentlyResolvingClassConstant[$resolvingName]); + continue; + } + $reflectionConstantDeclaringClass = $reflectionConstant->getDeclaringClass(); + $constantType = $this->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($reflectionConstantDeclaringClass->getName(), $reflectionConstantDeclaringClass->getFileName() ?: null)); + $nativeType = null; + if ($reflectionConstant->getType() !== null) { + $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), null, $constantClassReflection); + } + $types[] = $this->constantResolver->resolveClassConstantType( + $constantClassReflection->getName(), + $constantName, + $constantType, + $nativeType, + ); + unset($this->currentlyResolvingClassConstant[$resolvingName]); + continue; + } + + $constantReflection = $constantClassReflection->getConstant($constantName); + if ( + !$constantClassReflection->isFinal() + && !$constantReflection->isFinal() + && !$constantReflection->hasPhpDocType() + && !$constantReflection->hasNativeType() + ) { + unset($this->currentlyResolvingClassConstant[$resolvingName]); + return new MixedType(); + } + + if (!$constantClassReflection->isFinal()) { + $constantType = $constantReflection->getValueType(); + } else { + $constantType = $this->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass())); + } + + $nativeType = $constantReflection->getNativeType(); + $constantType = $this->constantResolver->resolveClassConstantType( + $constantClassReflection->getName(), + $constantName, + $constantType, + $nativeType, + ); + unset($this->currentlyResolvingClassConstant[$resolvingName]); + $types[] = $constantType; + } + + if (count($types) > 0) { + return TypeCombinator::union(...$types); + } + + if (!$constantClassType->hasConstant($constantName)->yes()) { + return new ErrorType(); + } + + return $constantClassType->getConstant($constantName)->getValueType(); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getClassConstFetchType(Name|Expr $class, string $constantName, ?string $className, callable $getTypeCallback): Type + { + $classReflection = null; + if ($className !== null && $this->getReflectionProvider()->hasClass($className)) { + $classReflection = $this->getReflectionProvider()->getClass($className); + } + + return $this->getClassConstFetchTypeByReflection($class, $constantName, $classReflection, $getTypeCallback); + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type + { + $type = $getTypeCallback($expr)->toNumber(); + $scalarValues = $type->getConstantScalarValues(); + + if (count($scalarValues) > 0) { + $newTypes = []; + foreach ($scalarValues as $scalarValue) { + if (is_int($scalarValue)) { + /** @var int|float $newValue */ + $newValue = -$scalarValue; + if (!is_int($newValue)) { + return $type; + } + $newTypes[] = new ConstantIntegerType($newValue); + } elseif (is_float($scalarValue)) { + $newTypes[] = new ConstantFloatType(-$scalarValue); + } + } + + return TypeCombinator::union(...$newTypes); + } + + if ($type instanceof IntegerRangeType) { + return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); + } + + return $type; + } + + /** + * @param callable(Expr): Type $getTypeCallback + */ + public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type + { + $exprType = $getTypeCallback($expr); + return TypeTraverser::map($exprType, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + if ($type instanceof ConstantStringType) { + return new ConstantStringType(~$type->getValue()); + } + if ($type->isString()->yes()) { + $accessories = [ + new StringType(), + ]; + if ($type->isNonEmptyString()->yes()) { + $accessories[] = new AccessoryNonEmptyStringType(); + } + // it is not useful to apply numeric and literal strings here. + // numeric string isn't certainly kept numeric: 3v4l.org/JERDB + + return TypeCombinator::intersect(...$accessories); + } + if ($type->isInteger()->yes() || $type->isFloat()->yes()) { + return new IntegerType(); //no const types here, result depends on PHP_INT_SIZE + } + return new ErrorType(); + }); + } + + private function resolveName(Name $name, ?ClassReflection $classReflection): string + { + $originalClass = (string) $name; + if ($classReflection !== null) { + $lowerClass = strtolower($originalClass); + + if (in_array($lowerClass, [ + 'self', + 'static', + ], true)) { + return $classReflection->getName(); + } elseif ($lowerClass === 'parent') { + if ($classReflection->getParentClass() !== null) { + return $classReflection->getParentClass()->getName(); + } + } + } + + return $originalClass; + } + + private function resolveTypeByName(Name $name, ?ClassReflection $classReflection): TypeWithClassName + { + if ($name->toLowerString() === 'static' && $classReflection !== null) { + return new StaticType($classReflection); + } + + $originalClass = $this->resolveName($name, $classReflection); + if ($classReflection !== null) { + $thisType = new ThisType($classReflection); + $ancestor = $thisType->getAncestorWithClassName($originalClass); + if ($ancestor !== null) { + return $ancestor; + } + } + + return new ObjectType($originalClass); + } + + /** + * @param mixed $value + */ + private function getTypeFromValue($value): Type + { + return ConstantTypeHelper::getTypeFromValue($value); + } + + private function getReflectionProvider(): ReflectionProvider + { + return $this->reflectionProviderProvider->getReflectionProvider(); + } + + private function getNeverType(Type $leftType, Type $rightType): Type + { + // make sure we don't lose the explicit flag in the process + if ($leftType instanceof NeverType && $leftType->isExplicit()) { + return $leftType; + } + if ($rightType instanceof NeverType && $rightType->isExplicit()) { + return $rightType; + } + return new NeverType(); + } + +} diff --git a/src/Reflection/MethodPrototypeReflection.php b/src/Reflection/MethodPrototypeReflection.php index 648f161df0..c92c6c5b74 100644 --- a/src/Reflection/MethodPrototypeReflection.php +++ b/src/Reflection/MethodPrototypeReflection.php @@ -4,7 +4,7 @@ use PHPStan\Type\Type; -class MethodPrototypeReflection implements ClassMemberReflection +final class MethodPrototypeReflection implements ClassMemberReflection { /** @@ -18,6 +18,7 @@ public function __construct( private bool $isPublic, private bool $isAbstract, private bool $isFinal, + private bool $isInternal, private array $variants, private ?Type $tentativeReturnType, ) @@ -59,6 +60,11 @@ public function isFinal(): bool return $this->isFinal; } + public function isInternal(): bool + { + return $this->isInternal; + } + public function getDocComment(): ?string { return null; diff --git a/src/Reflection/MethodReflection.php b/src/Reflection/MethodReflection.php index 8d601e9471..529a5011dd 100644 --- a/src/Reflection/MethodReflection.php +++ b/src/Reflection/MethodReflection.php @@ -14,7 +14,7 @@ public function getName(): string; public function getPrototype(): ClassMemberReflection; /** - * @return ParametersAcceptor[] + * @return list */ public function getVariants(): array; diff --git a/src/Reflection/MethodsClassReflectionExtension.php b/src/Reflection/MethodsClassReflectionExtension.php index 6b12291b0b..5817ac7657 100644 --- a/src/Reflection/MethodsClassReflectionExtension.php +++ b/src/Reflection/MethodsClassReflectionExtension.php @@ -2,7 +2,23 @@ namespace PHPStan\Reflection; -/** @api */ +/** + * This is the interface custom methods class reflection extensions implement. + * + * To register it in the configuration file use the `phpstan.broker.methodsClassReflectionExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyMethodsClassReflectionExtension + * tags: + * - phpstan.broker.methodsClassReflectionExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/class-reflection-extensions + * + * @api + */ interface MethodsClassReflectionExtension { diff --git a/src/Reflection/MissingConstantFromReflectionException.php b/src/Reflection/MissingConstantFromReflectionException.php index 230ba5902d..e57d3c3f4f 100644 --- a/src/Reflection/MissingConstantFromReflectionException.php +++ b/src/Reflection/MissingConstantFromReflectionException.php @@ -5,7 +5,7 @@ use Exception; use function sprintf; -class MissingConstantFromReflectionException extends Exception +final class MissingConstantFromReflectionException extends Exception { public function __construct( diff --git a/src/Reflection/MissingMethodFromReflectionException.php b/src/Reflection/MissingMethodFromReflectionException.php index 49c7778cd6..48051aafc1 100644 --- a/src/Reflection/MissingMethodFromReflectionException.php +++ b/src/Reflection/MissingMethodFromReflectionException.php @@ -5,7 +5,7 @@ use Exception; use function sprintf; -class MissingMethodFromReflectionException extends Exception +final class MissingMethodFromReflectionException extends Exception { public function __construct( diff --git a/src/Reflection/MissingPropertyFromReflectionException.php b/src/Reflection/MissingPropertyFromReflectionException.php index 4d62565c1f..2e64aee94c 100644 --- a/src/Reflection/MissingPropertyFromReflectionException.php +++ b/src/Reflection/MissingPropertyFromReflectionException.php @@ -5,7 +5,7 @@ use Exception; use function sprintf; -class MissingPropertyFromReflectionException extends Exception +final class MissingPropertyFromReflectionException extends Exception { public function __construct( diff --git a/src/Reflection/Mixin/MixinMethodReflection.php b/src/Reflection/Mixin/MixinMethodReflection.php index 745a8cae15..15fc4d8ad8 100644 --- a/src/Reflection/Mixin/MixinMethodReflection.php +++ b/src/Reflection/Mixin/MixinMethodReflection.php @@ -8,7 +8,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class MixinMethodReflection implements MethodReflection +final class MixinMethodReflection implements MethodReflection { public function __construct(private MethodReflection $reflection, private bool $static) diff --git a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php index be29968120..58ae58ca2f 100644 --- a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php @@ -7,12 +7,11 @@ 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 +final class MixinMethodsClassReflectionExtension implements MethodsClassReflectionExtension { /** @var array> */ @@ -44,7 +43,7 @@ private function findMethod(ClassReflection $classReflection, string $methodName { $mixinTypes = $classReflection->getResolvedMixinTypes(); foreach ($mixinTypes as $type) { - if (count(array_intersect(TypeUtils::getDirectClassNames($type), $this->mixinExcludeClasses)) > 0) { + if (count(array_intersect($type->getObjectClassNames(), $this->mixinExcludeClasses)) > 0) { continue; } @@ -75,13 +74,23 @@ private function findMethod(ClassReflection $classReflection, string $methodName return new MixinMethodReflection($method, $static); } - foreach ($classReflection->getParents() as $parentClass) { - $method = $this->findMethod($parentClass, $methodName); - if ($method === null) { + foreach ($classReflection->getTraits() as $traitClass) { + $methodWithDeclaringClass = $this->findMethod($traitClass, $methodName); + if ($methodWithDeclaringClass === null) { continue; } - return $method; + return $methodWithDeclaringClass; + } + + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { + $method = $this->findMethod($parentClass, $methodName); + if ($method !== null) { + return $method; + } + + $parentClass = $parentClass->getParentClass(); } return null; diff --git a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php index 7e60150737..4b21f92451 100644 --- a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php @@ -7,12 +7,11 @@ 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 +final class MixinPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { /** @var array> */ @@ -44,7 +43,7 @@ private function findProperty(ClassReflection $classReflection, string $property { $mixinTypes = $classReflection->getResolvedMixinTypes(); foreach ($mixinTypes as $type) { - if (count(array_intersect(TypeUtils::getDirectClassNames($type), $this->mixinExcludeClasses)) > 0) { + if (count(array_intersect($type->getObjectClassNames(), $this->mixinExcludeClasses)) > 0) { continue; } @@ -66,13 +65,23 @@ private function findProperty(ClassReflection $classReflection, string $property return $property; } - foreach ($classReflection->getParents() as $parentClass) { - $property = $this->findProperty($parentClass, $propertyName); - if ($property === null) { + foreach ($classReflection->getTraits() as $traitClass) { + $methodWithDeclaringClass = $this->findProperty($traitClass, $propertyName); + if ($methodWithDeclaringClass === null) { continue; } - return $property; + return $methodWithDeclaringClass; + } + + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { + $property = $this->findProperty($parentClass, $propertyName); + if ($property !== null) { + return $property; + } + + $parentClass = $parentClass->getParentClass(); } return null; diff --git a/src/Reflection/NamespaceAnswerer.php b/src/Reflection/NamespaceAnswerer.php new file mode 100644 index 0000000000..4e908a6d8e --- /dev/null +++ b/src/Reflection/NamespaceAnswerer.php @@ -0,0 +1,14 @@ + $attributes + */ + public function __construct( + private string $name, + private bool $optional, + private Type $type, + private Type $phpDocType, + private Type $nativeType, + private PassedByReference $passedByReference, + private bool $variadic, + private ?Type $defaultValue, + private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, + private array $attributes, + ) + { + } + + public function getName(): string + { + return $this->name; + } + + public function isOptional(): bool + { + return $this->optional; + } + + public function getType(): Type + { + return $this->type; + } + + public function getPhpDocType(): Type + { + return $this->phpDocType; + } + + public function hasNativeType(): bool + { + return !$this->nativeType instanceof MixedType || $this->nativeType->isExplicitMixed(); + } + + public function getNativeType(): Type + { + return $this->nativeType; + } + + public function passedByReference(): PassedByReference + { + return $this->passedByReference; + } + + public function isVariadic(): bool + { + return $this->variadic; + } + + public function getDefaultValue(): ?Type + { + return $this->defaultValue; + } + + public function getOutType(): ?Type + { + return $this->outType; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + + public function getAttributes(): array + { + return $this->attributes; + } + +} diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 2142675d94..7668d51f9e 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -2,26 +2,43 @@ namespace PHPStan\Reflection\Native; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -use PHPStan\Type\VoidType; +use function count; -class NativeFunctionReflection implements FunctionReflection +final class NativeFunctionReflection implements FunctionReflection { + private Assertions $assertions; + + private TrinaryLogic $returnsByReference; + /** - * @param ParametersAcceptor[] $variants + * @param list $variants + * @param list|null $namedArgumentsVariants + * @param list $attributes */ public function __construct( private string $name, private array $variants, + private ?array $namedArgumentsVariants, private ?Type $throwType, private TrinaryLogic $hasSideEffects, private bool $isDeprecated, + ?Assertions $assertions, + private ?string $phpDocComment, + ?TrinaryLogic $returnsByReference, + private bool $acceptsNamedArguments, + private array $attributes, ) { + $this->assertions = $assertions ?? Assertions::createEmpty(); + $this->returnsByReference = $returnsByReference ?? TrinaryLogic::createMaybe(); } public function getName(): string @@ -34,14 +51,26 @@ public function getFileName(): ?string return null; } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + $variants = $this->getVariants(); + if (count($variants) !== 1) { + throw new ShouldNotHappenException(); + } + + return $variants[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return $this->namedArgumentsVariants; + } + public function getThrowType(): ?Type { return $this->throwType; @@ -62,11 +91,6 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isFinal(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - public function hasSideEffects(): TrinaryLogic { if ($this->isVoid()) { @@ -76,10 +100,19 @@ public function hasSideEffects(): TrinaryLogic return $this->hasSideEffects; } + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + private function isVoid(): bool { foreach ($this->variants as $variant) { - if (!$variant->getReturnType() instanceof VoidType) { + if (!$variant->getReturnType()->isVoid()->yes()) { return false; } } @@ -92,4 +125,29 @@ public function isBuiltin(): bool return true; } + public function getAsserts(): Assertions + { + return $this->assertions; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return $this->returnsByReference; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->acceptsNamedArguments); + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 5748707bb1..34ec4e3e51 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -2,33 +2,44 @@ namespace PHPStan\Reflection\Native; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodPrototypeReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; -use PHPStan\Reflection\Php\BuiltinMethodReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\VoidType; use ReflectionException; +use function count; use function strtolower; -class NativeMethodReflection implements MethodReflection +final class NativeMethodReflection implements ExtendedMethodReflection { /** - * @param ParametersAcceptorWithPhpDocs[] $variants + * @param list $variants + * @param list|null $namedArgumentsVariants + * @param list $attributes */ public function __construct( private ReflectionProvider $reflectionProvider, private ClassReflection $declaringClass, - private BuiltinMethodReflection $reflection, + private ReflectionMethod $reflection, private array $variants, + private ?array $namedArgumentsVariants, private TrinaryLogic $hasSideEffects, private ?Type $throwType, + private Assertions $assertions, + private bool $acceptsNamedArguments, + private ?Type $selfOutType, + private ?string $phpDocComment, + private array $attributes, ) { } @@ -53,20 +64,27 @@ public function isPublic(): bool return $this->reflection->isPublic(); } - public function isAbstract(): bool + public function isAbstract(): TrinaryLogic { - return $this->reflection->isAbstract(); + return TrinaryLogic::createFromBoolean($this->reflection->isAbstract()); } public function getPrototype(): ClassMemberReflection { try { $prototypeMethod = $this->reflection->getPrototype(); - $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + $prototypeDeclaringClass = $this->declaringClass->getAncestorWithClassName($prototypeMethod->getDeclaringClass()->getName()); + if ($prototypeDeclaringClass === null) { + $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + } + + if (!$prototypeDeclaringClass->hasNativeMethod($prototypeMethod->getName())) { + return $this; + } $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { - $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); + $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType(), null, $prototypeDeclaringClass); } return new MethodPrototypeReflection( @@ -77,6 +95,7 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), + $prototypeMethod->isInternal(), $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), $tentativeReturnType, ); @@ -90,14 +109,26 @@ public function getName(): string return $this->reflection->getName(); } - /** - * @return ParametersAcceptorWithPhpDocs[] - */ public function getVariants(): array { return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + $variants = $this->getVariants(); + if (count($variants) !== 1) { + throw new ShouldNotHappenException(); + } + + return $variants[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return $this->namedArgumentsVariants; + } + public function getDeprecatedDescription(): ?string { return null; @@ -105,7 +136,7 @@ public function getDeprecatedDescription(): ?string public function isDeprecated(): TrinaryLogic { - return $this->reflection->isDeprecated(); + return TrinaryLogic::createFromBoolean($this->reflection->isDeprecated()); } public function isInternal(): TrinaryLogic @@ -113,11 +144,21 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isInternal()); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::createFromBoolean($this->reflection->isFinal()); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->isFinal(); + } + public function getThrowType(): ?Type { return $this->throwType; @@ -137,10 +178,19 @@ public function hasSideEffects(): TrinaryLogic return $this->hasSideEffects; } + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + private function isVoid(): bool { foreach ($this->variants as $variant) { - if (!$variant->getReturnType() instanceof VoidType) { + if (!$variant->getReturnType()->isVoid()->yes()) { return false; } } @@ -150,7 +200,32 @@ private function isVoid(): bool public function getDocComment(): ?string { - return $this->reflection->getDocComment(); + return $this->phpDocComment; + } + + public function getAsserts(): Assertions + { + return $this->assertions; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->acceptsNamedArguments() && $this->acceptsNamedArguments); + } + + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); + } + + public function getAttributes(): array + { + return $this->attributes; } } diff --git a/src/Reflection/Native/NativeParameterReflection.php b/src/Reflection/Native/NativeParameterReflection.php index 219f48069d..e812086830 100644 --- a/src/Reflection/Native/NativeParameterReflection.php +++ b/src/Reflection/Native/NativeParameterReflection.php @@ -5,8 +5,9 @@ use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; -class NativeParameterReflection implements ParameterReflection +final class NativeParameterReflection implements ParameterReflection { public function __construct( @@ -50,18 +51,15 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self + public function union(self $other): self { return new self( - $properties['name'], - $properties['optional'], - $properties['type'], - $properties['passedByReference'], - $properties['variadic'], - $properties['defaultValue'], + $this->name, + $this->optional && $other->optional, + TypeCombinator::union($this->type, $other->type), + $this->passedByReference->combine($other->passedByReference), + $this->variadic && $other->variadic, + $this->optional && $other->optional ? $this->defaultValue : null, ); } diff --git a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php b/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php deleted file mode 100644 index b5c01f9b90..0000000000 --- a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php +++ /dev/null @@ -1,82 +0,0 @@ -name; - } - - public function isOptional(): bool - { - return $this->optional; - } - - public function getType(): Type - { - return $this->type; - } - - public function getPhpDocType(): Type - { - return $this->phpDocType; - } - - public function getNativeType(): Type - { - return $this->nativeType; - } - - public function passedByReference(): PassedByReference - { - return $this->passedByReference; - } - - public function isVariadic(): bool - { - return $this->variadic; - } - - public function getDefaultValue(): ?Type - { - return $this->defaultValue; - } - - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['name'], - $properties['optional'], - $properties['type'], - $properties['phpDocType'], - $properties['nativeType'], - $properties['passedByReference'], - $properties['variadic'], - $properties['defaultValue'], - ); - } - -} diff --git a/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..fc9b44f79a --- /dev/null +++ b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php @@ -0,0 +1,43 @@ +className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === $this->methodName; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if ($this->phpVersion->getVersionId() >= 80000) { + return null; + } + + return new ObjectType(ReflectionClass::class); + } + +} diff --git a/src/Reflection/ParameterReflectionWithPhpDocs.php b/src/Reflection/ParameterReflectionWithPhpDocs.php deleted file mode 100644 index e0ede3fd51..0000000000 --- a/src/Reflection/ParameterReflectionWithPhpDocs.php +++ /dev/null @@ -1,14 +0,0 @@ - + * @return list */ public function getParameters(): array; diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 899a8a87d1..81bc3a2a99 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -2,62 +2,75 @@ namespace PHPStan\Reflection; +use Closure; use PhpParser\Node; -use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name; +use PHPStan\Analyser\ArgumentsNormalizer; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; +use PHPStan\Parser\ArrayFilterArgVisitor; +use PHPStan\Parser\ArrayFindArgVisitor; +use PHPStan\Parser\ArrayMapArgVisitor; +use PHPStan\Parser\ArrayWalkArgVisitor; +use PHPStan\Parser\ClosureBindArgVisitor; +use PHPStan\Parser\ClosureBindToVarVisitor; +use PHPStan\Parser\CurlSetOptArgVisitor; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\ArrayType; +use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\IntegerType; +use PHPStan\Type\LateResolvableType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\ResourceType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; +use function array_key_exists; +use function array_key_last; +use function array_map; +use function array_merge; use function array_slice; +use function array_values; +use function constant; use function count; +use function defined; +use function is_string; use function sprintf; use const ARRAY_FILTER_USE_BOTH; use const ARRAY_FILTER_USE_KEY; +use const CURLOPT_SSL_VERIFYHOST; -/** @api */ -class ParametersAcceptorSelector +/** + * @api + */ +final class ParametersAcceptorSelector { - /** - * @template T of ParametersAcceptor - * @param T[] $parametersAcceptors - * @return T - */ - public static function selectSingle( - array $parametersAcceptors, - ): ParametersAcceptor - { - $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 Node\Arg[] $args * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants */ public static function selectFromArgs( Scope $scope, array $args, array $parametersAcceptors, + ?array $namedArgumentsVariants = null, ): ParametersAcceptor { $types = []; @@ -66,29 +79,25 @@ public static function selectFromArgs( 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]) - ) { + $arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + if ($arrayMapArgs !== null) { $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 = []; + foreach ($arrayMapArgs as $arg) { + $argType = $scope->getType($arg->value); + if ($arg->unpack) { + $constantArrays = $argType->getConstantArrays(); + if (count($constantArrays) > 0) { + foreach ($constantArrays as $constantArray) { + $valueTypes = $constantArray->getValueTypes(); + foreach ($valueTypes as $valueType) { + $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($valueType), false, PassedByReference::createNo(), false, null); + } + } } - - $callbackParameters[] = new DummyParameter('item', $scope->getType($arg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null); + } else { + $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null); } } $parameters[0] = new NativeParameterReflection( @@ -109,25 +118,55 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } - if ( - $functionName === 'array_filter' - && isset($args[0]) - ) { + if (count($args) >= 3 && (bool) $args[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) { + $optType = $scope->getType($args[1]->value); + if ($optType instanceof ConstantIntegerType) { + $optValueType = self::getCurlOptValueType($optType->getValue()); + + if ($optValueType !== null) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + + $parameters[2] = new NativeParameterReflection( + $parameters[2]->getName(), + $parameters[2]->isOptional(), + $optValueType, + $parameters[2]->passedByReference(), + $parameters[2]->isVariadic(), + $parameters[2]->getDefaultValue(), + ); + + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_values($parameters), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + + if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) { 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), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { $arrayFilterParameters = [ - new DummyParameter('item', $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), + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; } } @@ -135,14 +174,79 @@ public static function selectFromArgs( $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + new UnionType([ + new CallableType( + $arrayFilterParameters ?? [ + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + ], + new BooleanType(), + false, + ), + new NullType(), + ]), + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_values($parameters), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + + if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { + $arrayWalkParameters = [ + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + ]; + if (isset($args[2])) { + $arrayWalkParameters[] = new DummyParameter('arg', $scope->getType($args[2]->value), false, PassedByReference::createNo(), false, null); + } + + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + new CallableType($arrayWalkParameters, new MixedType(), false), + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_values($parameters), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + + if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $argType = $scope->getType($args[0]->value); $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 DummyParameter('value', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($argType), false, PassedByReference::createNo(), false, null), ], - new MixedType(), + new BooleanType(), false, ), $parameters[1]->passedByReference(), @@ -153,29 +257,213 @@ public static function selectFromArgs( new FunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - $parameters, + array_values($parameters), $acceptor->isVariadic(), $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } + + if (isset($args[0])) { + $closureBindToVar = $args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); + if ( + $closureBindToVar !== null + && $closureBindToVar instanceof Node\Expr\Variable + && is_string($closureBindToVar->name) + ) { + $varType = $scope->getType($closureBindToVar); + if ((new ObjectType(Closure::class))->isSuperTypeOf($varType)->yes()) { + $inFunction = $scope->getFunction(); + if ($inFunction !== null) { + $closureThisParameters = []; + foreach ($inFunction->getParameters() as $parameter) { + if ($parameter->getClosureThisType() === null) { + continue; + } + $closureThisParameters[$parameter->getName()] = $parameter->getClosureThisType(); + } + if (array_key_exists($closureBindToVar->name, $closureThisParameters)) { + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($closureBindToVar->name))->yes()) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[0] = new NativeParameterReflection( + $parameters[0]->getName(), + $parameters[0]->isOptional(), + $closureThisParameters[$closureBindToVar->name], + $parameters[0]->passedByReference(), + $parameters[0]->isVariadic(), + $parameters[0]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + } + } + + if ( + $args[0]->getAttribute(ClosureBindArgVisitor::ATTRIBUTE_NAME) !== null + && $args[0]->value instanceof Node\Expr\Variable + && is_string($args[0]->value->name) + ) { + $closureVarName = $args[0]->value->name; + $inFunction = $scope->getFunction(); + if ($inFunction !== null) { + $closureThisParameters = []; + foreach ($inFunction->getParameters() as $parameter) { + if ($parameter->getClosureThisType() === null) { + continue; + } + $closureThisParameters[$parameter->getName()] = $parameter->getClosureThisType(); + } + if (array_key_exists($closureVarName, $closureThisParameters)) { + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($closureVarName))->yes()) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + $closureThisParameters[$closureVarName], + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_values($parameters), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + } + } + } + + if (count($parametersAcceptors) === 1) { + $acceptor = $parametersAcceptors[0]; + if (!self::hasAcceptorTemplateOrLateResolvableType($acceptor)) { + return $acceptor; + } + } + + $reorderedArgs = $args; + $parameters = null; + $singleParametersAcceptor = null; + if (count($parametersAcceptors) === 1) { + $reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args); + $singleParametersAcceptor = $parametersAcceptors[0]; } - foreach ($args as $arg) { - $type = $scope->getType($arg->value); - if ($arg->unpack) { + $hasName = false; + foreach ($reorderedArgs ?? $args as $i => $arg) { + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $parameter = null; + if ($singleParametersAcceptor !== null) { + $parameters = $singleParametersAcceptor->getParameters(); + if (isset($parameters[$i])) { + $parameter = $parameters[$i]; + } elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) { + $parameter = $parameters[count($parameters) - 1]; + } + } + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->pushInFunctionCall(null, $parameter); + } + + $type = $scope->getType($originalArg->value); + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->popInFunctionCall(); + } + + if ($originalArg->name !== null) { + $index = $originalArg->name->toString(); + $hasName = true; + } else { + $index = $i; + } + if ($originalArg->unpack) { $unpack = true; - $types[] = $type->getIterableValueType(); + $types[$index] = $type->getIterableValueType(); } else { - $types[] = $type; + $types[$index] = $type; } } + if ($hasName && $namedArgumentsVariants !== null) { + return self::selectFromTypes($types, $namedArgumentsVariants, $unpack); + } + return self::selectFromTypes($types, $parametersAcceptors, $unpack); } + private static function hasAcceptorTemplateOrLateResolvableType(ParametersAcceptor $acceptor): bool + { + if (self::hasTemplateOrLateResolvableType($acceptor->getReturnType())) { + return true; + } + + foreach ($acceptor->getParameters() as $parameter) { + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getOutType() !== null + && self::hasTemplateOrLateResolvableType($parameter->getOutType()) + ) { + return true; + } + + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getClosureThisType() !== null + && self::hasTemplateOrLateResolvableType($parameter->getClosureThisType()) + ) { + return true; + } + + if (!self::hasTemplateOrLateResolvableType($parameter->getType())) { + continue; + } + + return true; + } + + return false; + } + + private static function hasTemplateOrLateResolvableType(Type $type): bool + { + $has = false; + TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$has): Type { + if ($type instanceof TemplateType || $type instanceof LateResolvableType) { + $has = true; + return $type; + } + + return $traverse($type); + }); + + return $has; + } + /** - * @param Type[] $types + * @param array $types * @param ParametersAcceptor[] $parametersAcceptors */ public static function selectFromTypes( @@ -246,7 +534,7 @@ public static function selectFromTypes( break; } - $type = $types[count($types) - 1]; + $type = $types[array_key_last($types)]; } else { $type = $types[$i]; } @@ -254,7 +542,7 @@ public static function selectFromTypes( if ($parameter->getType() instanceof MixedType) { $isSuperType = $isSuperType->and(TrinaryLogic::createMaybe()); } else { - $isSuperType = $isSuperType->and($parameter->getType()->isSuperTypeOf($type)); + $isSuperType = $isSuperType->and($parameter->getType()->isSuperTypeOf($type)->result); } } @@ -280,13 +568,13 @@ public static function selectFromTypes( return GenericParametersAcceptorResolver::resolve($types, self::combineAcceptors($acceptableAcceptors)); } - return self::combineAcceptors($winningAcceptors); + return GenericParametersAcceptorResolver::resolve($types, self::combineAcceptors($winningAcceptors)); } /** * @param ParametersAcceptor[] $acceptors */ - public static function combineAcceptors(array $acceptors): ParametersAcceptor + public static function combineAcceptors(array $acceptors): ExtendedParametersAcceptor { if (count($acceptors) === 0) { throw new ShouldNotHappenException( @@ -294,7 +582,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor ); } if (count($acceptors) === 1) { - return $acceptors[0]; + return self::wrapAcceptor($acceptors[0]); } $minimumNumberOfParameters = null; @@ -317,25 +605,50 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor $parameters = []; $isVariadic = false; - $returnType = null; + $returnTypes = []; + $phpDocReturnTypes = []; + $nativeReturnTypes = []; + $callableOccurred = false; + $throwPoints = []; + $isPure = TrinaryLogic::createNo(); + $impurePoints = []; + $invalidateExpressions = []; + $usedVariables = []; + $acceptsNamedArguments = TrinaryLogic::createNo(); foreach ($acceptors as $acceptor) { - if ($returnType === null) { - $returnType = $acceptor->getReturnType(); - } else { - $returnType = TypeCombinator::union($returnType, $acceptor->getReturnType()); + $returnTypes[] = $acceptor->getReturnType(); + + if ($acceptor instanceof ExtendedParametersAcceptor) { + $phpDocReturnTypes[] = $acceptor->getPhpDocReturnType(); + $nativeReturnTypes[] = $acceptor->getNativeReturnType(); + } + if ($acceptor instanceof CallableParametersAcceptor) { + $callableOccurred = true; + $throwPoints = array_merge($throwPoints, $acceptor->getThrowPoints()); + $isPure = $isPure->or($acceptor->isPure()); + $impurePoints = array_merge($impurePoints, $acceptor->getImpurePoints()); + $invalidateExpressions = array_merge($invalidateExpressions, $acceptor->getInvalidateExpressions()); + $usedVariables = array_merge($usedVariables, $acceptor->getUsedVariables()); + $acceptsNamedArguments = $acceptsNamedArguments->or($acceptor->acceptsNamedArguments()); } $isVariadic = $isVariadic || $acceptor->isVariadic(); foreach ($acceptor->getParameters() as $i => $parameter) { if (!isset($parameters[$i])) { - $parameters[$i] = new NativeParameterReflection( + $parameters[$i] = new ExtendedDummyParameter( $parameter->getName(), - $i + 1 > $minimumNumberOfParameters, $parameter->getType(), + $i + 1 > $minimumNumberOfParameters, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue(), + $parameter instanceof ExtendedParameterReflection ? $parameter->getNativeType() : new MixedType(), + $parameter instanceof ExtendedParameterReflection ? $parameter->getPhpDocType() : new MixedType(), + $parameter instanceof ExtendedParameterReflection ? $parameter->getOutType() : null, + $parameter instanceof ExtendedParameterReflection ? $parameter->isImmediatelyInvokedCallable() : TrinaryLogic::createMaybe(), + $parameter instanceof ExtendedParameterReflection ? $parameter->getClosureThisType() : null, + $parameter instanceof ExtendedParameterReflection ? $parameter->getAttributes() : [], ); continue; } @@ -349,13 +662,52 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor $defaultValue = null; } - $parameters[$i] = new NativeParameterReflection( + $type = TypeCombinator::union($parameters[$i]->getType(), $parameter->getType()); + $nativeType = $parameters[$i]->getNativeType(); + $phpDocType = $parameters[$i]->getPhpDocType(); + $outType = $parameters[$i]->getOutType(); + $immediatelyInvokedCallable = $parameters[$i]->isImmediatelyInvokedCallable(); + $closureThisType = $parameters[$i]->getClosureThisType(); + $attributes = $parameters[$i]->getAttributes(); + if ($parameter instanceof ExtendedParameterReflection) { + $nativeType = TypeCombinator::union($nativeType, $parameter->getNativeType()); + $phpDocType = TypeCombinator::union($phpDocType, $parameter->getPhpDocType()); + + if ($parameter->getOutType() !== null) { + $outType = $outType === null ? null : TypeCombinator::union($outType, $parameter->getOutType()); + } else { + $outType = null; + } + + if ($parameter->getClosureThisType() !== null && $closureThisType !== null) { + $closureThisType = TypeCombinator::union($closureThisType, $parameter->getClosureThisType()); + } else { + $closureThisType = null; + } + + $immediatelyInvokedCallable = $parameter->isImmediatelyInvokedCallable()->or($immediatelyInvokedCallable); + $attributes = array_merge($attributes, $parameter->getAttributes()); + } else { + $nativeType = new MixedType(); + $phpDocType = $type; + $outType = null; + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + $closureThisType = null; + } + + $parameters[$i] = new ExtendedDummyParameter( $parameters[$i]->getName() !== $parameter->getName() ? sprintf('%s|%s', $parameters[$i]->getName(), $parameter->getName()) : $parameter->getName(), + $type, $i + 1 > $minimumNumberOfParameters, - TypeCombinator::union($parameters[$i]->getType(), $parameter->getType()), $parameters[$i]->passedByReference()->combine($parameter->passedByReference()), $isVariadic, $defaultValue, + $nativeType, + $phpDocType, + $outType, + $immediatelyInvokedCallable, + $closureThisType, + $attributes, ); if ($isVariadic) { @@ -365,13 +717,385 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor } } - return new FunctionVariant( + $returnType = TypeCombinator::union(...$returnTypes); + $phpDocReturnType = $phpDocReturnTypes === [] ? null : TypeCombinator::union(...$phpDocReturnTypes); + $nativeReturnType = $nativeReturnTypes === [] ? null : TypeCombinator::union(...$nativeReturnTypes); + + if ($callableOccurred) { + return new ExtendedCallableFunctionVariant( + TemplateTypeMap::createEmpty(), + null, + array_values($parameters), + $isVariadic, + $returnType, + $phpDocReturnType ?? $returnType, + $nativeReturnType ?? new MixedType(), + null, + $throwPoints, + $isPure, + $impurePoints, + $invalidateExpressions, + $usedVariables, + $acceptsNamedArguments, + ); + } + + return new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, - $parameters, + array_values($parameters), $isVariadic, $returnType, + $phpDocReturnType ?? $returnType, + $nativeReturnType ?? new MixedType(), + ); + } + + private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedParametersAcceptor + { + if ($acceptor instanceof ExtendedParametersAcceptor) { + return $acceptor; + } + + if ($acceptor instanceof CallableParametersAcceptor) { + return new ExtendedCallableFunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => self::wrapParameter($parameter), $acceptor->getParameters()), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor->getReturnType(), + new MixedType(), + TemplateTypeVarianceMap::createEmpty(), + $acceptor->getThrowPoints(), + $acceptor->isPure(), + $acceptor->getImpurePoints(), + $acceptor->getInvalidateExpressions(), + $acceptor->getUsedVariables(), + $acceptor->acceptsNamedArguments(), + ); + } + + return new ExtendedFunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => self::wrapParameter($parameter), $acceptor->getParameters()), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor->getReturnType(), + new MixedType(), + TemplateTypeVarianceMap::createEmpty(), + ); + } + + private static function wrapParameter(ParameterReflection $parameter): ExtendedParameterReflection + { + return $parameter instanceof ExtendedParameterReflection ? $parameter : new ExtendedDummyParameter( + $parameter->getName(), + $parameter->getType(), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + new MixedType(), + $parameter->getType(), + null, + TrinaryLogic::createMaybe(), + null, + [], ); } + private static function getCurlOptValueType(int $curlOpt): ?Type + { + if (defined('CURLOPT_SSL_VERIFYHOST') && $curlOpt === CURLOPT_SSL_VERIFYHOST) { + return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(2)]); + } + + $boolConstants = [ + 'CURLOPT_AUTOREFERER', + 'CURLOPT_COOKIESESSION', + 'CURLOPT_CERTINFO', + 'CURLOPT_CONNECT_ONLY', + 'CURLOPT_CRLF', + 'CURLOPT_DISALLOW_USERNAME_IN_URL', + 'CURLOPT_DNS_SHUFFLE_ADDRESSES', + 'CURLOPT_HAPROXYPROTOCOL', + 'CURLOPT_SSH_COMPRESSION', + 'CURLOPT_DNS_USE_GLOBAL_CACHE', + 'CURLOPT_FAILONERROR', + 'CURLOPT_SSL_FALSESTART', + 'CURLOPT_FILETIME', + 'CURLOPT_FOLLOWLOCATION', + 'CURLOPT_FORBID_REUSE', + 'CURLOPT_FRESH_CONNECT', + 'CURLOPT_FTP_USE_EPRT', + 'CURLOPT_FTP_USE_EPSV', + 'CURLOPT_FTP_CREATE_MISSING_DIRS', + 'CURLOPT_FTPAPPEND', + 'CURLOPT_TCP_NODELAY', + 'CURLOPT_FTPASCII', + 'CURLOPT_FTPLISTONLY', + 'CURLOPT_HEADER', + 'CURLOPT_HTTP09_ALLOWED', + 'CURLOPT_HTTPGET', + 'CURLOPT_HTTPPROXYTUNNEL', + 'CURLOPT_HTTP_CONTENT_DECODING', + 'CURLOPT_KEEP_SENDING_ON_ERROR', + 'CURLOPT_MUTE', + 'CURLOPT_NETRC', + 'CURLOPT_NOBODY', + 'CURLOPT_NOPROGRESS', + 'CURLOPT_NOSIGNAL', + 'CURLOPT_PATH_AS_IS', + 'CURLOPT_PIPEWAIT', + 'CURLOPT_POST', + 'CURLOPT_PUT', + 'CURLOPT_RETURNTRANSFER', + 'CURLOPT_SASL_IR', + 'CURLOPT_SSL_ENABLE_ALPN', + 'CURLOPT_SSL_ENABLE_NPN', + 'CURLOPT_SSL_VERIFYPEER', + 'CURLOPT_SSL_VERIFYSTATUS', + 'CURLOPT_PROXY_SSL_VERIFYPEER', + 'CURLOPT_SUPPRESS_CONNECT_HEADERS', + 'CURLOPT_TCP_FASTOPEN', + 'CURLOPT_TFTP_NO_OPTIONS', + 'CURLOPT_TRANSFERTEXT', + 'CURLOPT_UNRESTRICTED_AUTH', + 'CURLOPT_UPLOAD', + 'CURLOPT_VERBOSE', + ]; + foreach ($boolConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new BooleanType(); + } + } + + $intConstants = [ + 'CURLOPT_BUFFERSIZE', + 'CURLOPT_CONNECTTIMEOUT', + 'CURLOPT_CONNECTTIMEOUT_MS', + 'CURLOPT_DNS_CACHE_TIMEOUT', + 'CURLOPT_EXPECT_100_TIMEOUT_MS', + 'CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS', + 'CURLOPT_FTPSSLAUTH', + 'CURLOPT_HEADEROPT', + 'CURLOPT_HTTP_VERSION', + 'CURLOPT_HTTPAUTH', + 'CURLOPT_INFILESIZE', + 'CURLOPT_LOW_SPEED_LIMIT', + 'CURLOPT_LOW_SPEED_TIME', + 'CURLOPT_MAXCONNECTS', + 'CURLOPT_MAXREDIRS', + 'CURLOPT_PORT', + 'CURLOPT_POSTREDIR', + 'CURLOPT_PROTOCOLS', + 'CURLOPT_PROXYAUTH', + 'CURLOPT_PROXYPORT', + 'CURLOPT_PROXYTYPE', + 'CURLOPT_REDIR_PROTOCOLS', + 'CURLOPT_RESUME_FROM', + 'CURLOPT_SOCKS5_AUTH', + 'CURLOPT_SSL_OPTIONS', + 'CURLOPT_SSL_VERIFYHOST', + 'CURLOPT_SSLVERSION', + 'CURLOPT_PROXY_SSL_OPTIONS', + 'CURLOPT_PROXY_SSL_VERIFYHOST', + 'CURLOPT_PROXY_SSLVERSION', + 'CURLOPT_STREAM_WEIGHT', + 'CURLOPT_TCP_KEEPALIVE', + 'CURLOPT_TCP_KEEPIDLE', + 'CURLOPT_TCP_KEEPINTVL', + 'CURLOPT_TIMECONDITION', + 'CURLOPT_TIMEOUT', + 'CURLOPT_TIMEOUT_MS', + 'CURLOPT_TIMEVALUE', + 'CURLOPT_TIMEVALUE_LARGE', + 'CURLOPT_MAX_RECV_SPEED_LARGE', + 'CURLOPT_SSH_AUTH_TYPES', + 'CURLOPT_IPRESOLVE', + 'CURLOPT_FTP_FILEMETHOD', + ]; + foreach ($intConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new IntegerType(); + } + } + + $nullableStringConstants = [ + 'CURLOPT_CUSTOMREQUEST', + 'CURLOPT_DNS_INTERFACE', + 'CURLOPT_DNS_LOCAL_IP4', + 'CURLOPT_DNS_LOCAL_IP6', + 'CURLOPT_DOH_URL', + 'CURLOPT_FTP_ACCOUNT', + 'CURLOPT_FTPPORT', + 'CURLOPT_HSTS', + 'CURLOPT_KRBLEVEL', + 'CURLOPT_RANGE', + 'CURLOPT_RTSP_SESSION_ID', + 'CURLOPT_UNIX_SOCKET_PATH', + 'CURLOPT_XOAUTH2_BEARER', + ]; + foreach ($nullableStringConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new UnionType([ + new NullType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + ]); + } + } + + $nonEmptyStringConstants = [ + 'CURLOPT_ABSTRACT_UNIX_SOCKET', + 'CURLOPT_ALTSVC', + 'CURLOPT_AWS_SIGV4', + 'CURLOPT_CAINFO', + 'CURLOPT_CAPATH', + 'CURLOPT_COOKIE', + 'CURLOPT_COOKIEJAR', + 'CURLOPT_COOKIELIST', + 'CURLOPT_DEFAULT_PROTOCOL', + 'CURLOPT_DNS_SERVERS', + 'CURLOPT_EGDSOCKET', + 'CURLOPT_FTP_ALTERNATIVE_TO_USER', + 'CURLOPT_INTERFACE', + 'CURLOPT_KEYPASSWD', + 'CURLOPT_KRB4LEVEL', + 'CURLOPT_LOGIN_OPTIONS', + 'CURLOPT_MAIL_AUTH', + 'CURLOPT_MAIL_FROM', + 'CURLOPT_NOPROXY', + 'CURLOPT_PASSWORD', + 'CURLOPT_PINNEDPUBLICKEY', + 'CURLOPT_PROTOCOLS_STR', + 'CURLOPT_PROXY_CAINFO', + 'CURLOPT_PROXY_CAPATH', + 'CURLOPT_PROXY_CRLFILE', + 'CURLOPT_PROXY_ISSUERCERT', + 'CURLOPT_PROXY_KEYPASSWD', + 'CURLOPT_PROXY_PINNEDPUBLICKEY', + 'CURLOPT_PROXY_SERVICE_NAME', + 'CURLOPT_PROXY_SSL_CIPHER_LIST', + 'CURLOPT_PROXY_SSLCERT', + 'CURLOPT_PROXY_SSLCERTTYPE', + 'CURLOPT_PROXY_SSLKEY', + 'CURLOPT_PROXY_SSLKEYTYPE', + 'CURLOPT_PROXY_TLS13_CIPHERS', + 'CURLOPT_PROXY_TLSAUTH_PASSWORD', + 'CURLOPT_PROXY_TLSAUTH_TYPE', + 'CURLOPT_PROXY_TLSAUTH_USERNAME', + 'CURLOPT_PROXYPASSWORD', + 'CURLOPT_PROXYUSERNAME', + 'CURLOPT_PROXYUSERPWD', + 'CURLOPT_RANDOM_FILE', + 'CURLOPT_REDIR_PROTOCOLS_STR', + 'CURLOPT_REFERER', + 'CURLOPT_REQUEST_TARGET', + 'CURLOPT_RTSP_STREAM_URI', + 'CURLOPT_RTSP_TRANSPORT', + 'CURLOPT_SASL_AUTHZID', + 'CURLOPT_SERVICE_NAME', + 'CURLOPT_SOCKS5_GSSAPI_SERVICE', + 'CURLOPT_SSH_HOST_PUBLIC_KEY_MD5', + 'CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256', + 'CURLOPT_SSH_PRIVATE_KEYFILE', + 'CURLOPT_SSH_PUBLIC_KEYFILE', + 'CURLOPT_SSL_CIPHER_LIST', + 'CURLOPT_SSL_EC_CURVES', + 'CURLOPT_SSLCERT', + 'CURLOPT_SSLCERTPASSWD', + 'CURLOPT_SSLCERTTYPE', + 'CURLOPT_SSLENGINE', + 'CURLOPT_SSLENGINE_DEFAULT', + 'CURLOPT_SSLKEY', + 'CURLOPT_SSLKEYPASSWD', + 'CURLOPT_SSLKEYTYPE', + 'CURLOPT_TLS13_CIPHERS', + 'CURLOPT_TLSAUTH_PASSWORD', + 'CURLOPT_TLSAUTH_TYPE', + 'CURLOPT_TLSAUTH_USERNAME', + 'CURLOPT_TRANSFER_ENCODING', + 'CURLOPT_URL', + 'CURLOPT_USERAGENT', + 'CURLOPT_USERNAME', + 'CURLOPT_USERPWD', + ]; + foreach ($nonEmptyStringConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ); + } + } + + $stringConstants = [ + 'CURLOPT_COOKIEFILE', + 'CURLOPT_ENCODING', // Alias: CURLOPT_ACCEPT_ENCODING + 'CURLOPT_PRE_PROXY', + 'CURLOPT_PRIVATE', + 'CURLOPT_PROXY', + ]; + foreach ($stringConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new StringType(); + } + } + + $intArrayStringKeysConstants = [ + 'CURLOPT_HTTPHEADER', + ]; + foreach ($intArrayStringKeysConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new ArrayType(new IntegerType(), new StringType()); + } + } + + $arrayConstants = [ + 'CURLOPT_CONNECT_TO', + 'CURLOPT_HTTP200ALIASES', + 'CURLOPT_POSTQUOTE', + 'CURLOPT_PROXYHEADER', + 'CURLOPT_QUOTE', + 'CURLOPT_RESOLVE', + ]; + foreach ($arrayConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new ArrayType(new MixedType(), new MixedType()); + } + } + + $arrayOrStringConstants = [ + 'CURLOPT_POSTFIELDS', + ]; + foreach ($arrayOrStringConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new UnionType([ + new StringType(), + new ArrayType(new MixedType(), new MixedType()), + ]); + } + } + + $resourceConstants = [ + 'CURLOPT_FILE', + 'CURLOPT_INFILE', + 'CURLOPT_STDERR', + 'CURLOPT_WRITEHEADER', + ]; + foreach ($resourceConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new ResourceType(); + } + } + + // unknown constant + return null; + } + } diff --git a/src/Reflection/ParametersAcceptorWithPhpDocs.php b/src/Reflection/ParametersAcceptorWithPhpDocs.php deleted file mode 100644 index 11200bc1f5..0000000000 --- a/src/Reflection/ParametersAcceptorWithPhpDocs.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ - public function getParameters(): array; - - public function getPhpDocReturnType(): Type; - - public function getNativeReturnType(): Type; - -} diff --git a/src/Reflection/PassedByReference.php b/src/Reflection/PassedByReference.php index d4741bc1b6..804d049b43 100644 --- a/src/Reflection/PassedByReference.php +++ b/src/Reflection/PassedByReference.php @@ -4,8 +4,10 @@ use function array_key_exists; -/** @api */ -class PassedByReference +/** + * @api + */ +final class PassedByReference { private const NO = 1; @@ -74,12 +76,4 @@ public function combine(self $other): self return $this; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self($properties['value']); - } - } diff --git a/src/Reflection/Php/BuiltinMethodReflection.php b/src/Reflection/Php/BuiltinMethodReflection.php deleted file mode 100644 index 50a5956c7a..0000000000 --- a/src/Reflection/Php/BuiltinMethodReflection.php +++ /dev/null @@ -1,55 +0,0 @@ -nativeMethodReflection->getPrototype(); } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { $parameters = $this->closureType->getParameters(); @@ -78,16 +81,42 @@ public function getVariants(): array array_unshift($parameters, $newThis); return [ - new FunctionVariant( + new ExtendedFunctionVariant( $this->closureType->getTemplateTypeMap(), $this->closureType->getResolvedTemplateTypeMap(), - $parameters, + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( + $parameter->getName(), + $parameter->getType(), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + new MixedType(), + $parameter->getType(), + null, + TrinaryLogic::createMaybe(), + null, + [], + ), $parameters), $this->closureType->isVariadic(), $this->closureType->getReturnType(), + $this->closureType->getReturnType(), + new MixedType(), + $this->closureType->getCallSiteVarianceMap(), ), ]; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return $this->nativeMethodReflection->isDeprecated(); @@ -103,11 +132,26 @@ public function isFinal(): TrinaryLogic return $this->nativeMethodReflection->isFinal(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->nativeMethodReflection->isFinalByKeyword(); + } + public function isInternal(): TrinaryLogic { return $this->nativeMethodReflection->isInternal(); } + public function isBuiltin(): TrinaryLogic + { + $builtin = $this->nativeMethodReflection->isBuiltin(); + if (is_bool($builtin)) { + return TrinaryLogic::createFromBoolean($builtin); + } + + return $builtin; + } + public function getThrowType(): ?Type { return $this->nativeMethodReflection->getThrowType(); @@ -118,4 +162,44 @@ public function hasSideEffects(): TrinaryLogic return $this->nativeMethodReflection->hasSideEffects(); } + public function getAsserts(): Assertions + { + return $this->nativeMethodReflection->getAsserts(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->nativeMethodReflection->acceptsNamedArguments(); + } + + public function getSelfOutType(): ?Type + { + return $this->nativeMethodReflection->getSelfOutType(); + } + + public function returnsByReference(): TrinaryLogic + { + return $this->nativeMethodReflection->returnsByReference(); + } + + public function isAbstract(): TrinaryLogic + { + $abstract = $this->nativeMethodReflection->isAbstract(); + if (is_bool($abstract)) { + return TrinaryLogic::createFromBoolean($abstract); + } + + return $abstract; + } + + public function isPure(): TrinaryLogic + { + return $this->nativeMethodReflection->isPure(); + } + + public function getAttributes(): array + { + return $this->nativeMethodReflection->getAttributes(); + } + } diff --git a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php index a67891dd95..abeb693630 100644 --- a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php @@ -2,12 +2,12 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Type\ClosureType; use PHPStan\Type\Type; -class ClosureCallUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class ClosureCallUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { public function __construct(private UnresolvedMethodPrototypeReflection $prototype, private ClosureType $closure) @@ -19,12 +19,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return new self($this->prototype->doNotResolveTemplateTypeMapToBounds(), $this->closure); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->getTransformedMethod(); } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { return new ClosureCallMethodReflection($this->prototype->getTransformedMethod(), $this->closure); } diff --git a/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php b/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php new file mode 100644 index 0000000000..04f4fbe8e7 --- /dev/null +++ b/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php @@ -0,0 +1,28 @@ +isEnum(); + } + + public function getAllowedSubTypes(ClassReflection $classReflection): array + { + $cases = []; + foreach (array_keys($classReflection->getEnumCases()) as $name) { + $cases[] = new EnumCaseObjectType($classReflection->getName(), $name); + } + + return $cases; + } + +} diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index d5f86b45c8..ecf72e435d 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -2,17 +2,19 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class EnumCasesMethodReflection implements MethodReflection +final class EnumCasesMethodReflection implements ExtendedMethodReflection { public function __construct(private ClassReflection $declaringClass, private Type $returnType) @@ -59,22 +61,31 @@ public function getPrototype(): ClassMemberReflection return $unitEnum->getNativeMethod('cases'); } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { return [ - new FunctionVariant( + new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), TemplateTypeMap::createEmpty(), [], false, $this->returnType, + new MixedType(), + $this->returnType, ), ]; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -90,11 +101,21 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function getThrowType(): ?Type { return null; @@ -105,4 +126,39 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->acceptsNamedArguments()); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Php/EnumPropertyReflection.php b/src/Reflection/Php/EnumPropertyReflection.php index e3f7927dec..912b779e67 100644 --- a/src/Reflection/Php/EnumPropertyReflection.php +++ b/src/Reflection/Php/EnumPropertyReflection.php @@ -3,17 +3,25 @@ namespace PHPStan\Reflection\Php; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class EnumPropertyReflection implements PropertyReflection +final class EnumPropertyReflection implements ExtendedPropertyReflection { - public function __construct(private ClassReflection $declaringClass, private Type $type) + public function __construct(private string $name, private ClassReflection $declaringClass, private Type $type) { } + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -39,6 +47,26 @@ public function getDocComment(): ?string return null; } + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): Type + { + return new MixedType(); + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + public function getReadableType(): Type { return $this->type; @@ -79,4 +107,44 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function hasHook(string $hookType): bool + { + return false; + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + throw new ShouldNotHappenException(); + } + + public function isProtectedSet(): bool + { + return false; + } + + public function isPrivateSet(): bool + { + return false; + } + + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php new file mode 100644 index 0000000000..5e5a048e7c --- /dev/null +++ b/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php @@ -0,0 +1,36 @@ +property; + } + + public function getTransformedProperty(): ExtendedPropertyReflection + { + return $this->property; + } + + public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection + { + return $this; + } + +} diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php new file mode 100644 index 0000000000..4020bbdc09 --- /dev/null +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -0,0 +1,146 @@ +name; + } + + public function getFileName(): ?string + { + return null; + } + + public function getVariants(): array + { + $parameterType = new UnionType([ + new StringType(), + new IntegerType(), + ]); + return [ + new ExtendedFunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + [ + new ExtendedDummyParameter( + 'status', + $parameterType, + true, + PassedByReference::createNo(), + false, + new ConstantIntegerType(0), + $parameterType, + new MixedType(), + null, + TrinaryLogic::createNo(), + null, + [], + ), + ], + false, + new NeverType(true), + new MixedType(), + new NeverType(true), + TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + /** + * @return list + */ + public function getNamedArgumentsVariants(): array + { + return $this->getVariants(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getThrowType(): ?Type + { + return null; + } + + public function hasSideEffects(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isBuiltin(): bool + { + return true; + } + + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function getDocComment(): ?string + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getAttributes(): array + { + return []; + } + +} diff --git a/src/Reflection/Php/ExtendedDummyParameter.php b/src/Reflection/Php/ExtendedDummyParameter.php new file mode 100644 index 0000000000..19a917e0a1 --- /dev/null +++ b/src/Reflection/Php/ExtendedDummyParameter.php @@ -0,0 +1,71 @@ + $attributes + */ + public function __construct( + string $name, + Type $type, + bool $optional, + ?PassedByReference $passedByReference, + bool $variadic, + ?Type $defaultValue, + private Type $nativeType, + private Type $phpDocType, + private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, + private array $attributes, + ) + { + parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); + } + + public function getPhpDocType(): Type + { + return $this->phpDocType; + } + + public function hasNativeType(): bool + { + return !$this->nativeType instanceof MixedType || $this->nativeType->isExplicitMixed(); + } + + public function getNativeType(): Type + { + return $this->nativeType; + } + + public function getOutType(): ?Type + { + return $this->outType; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + + public function getAttributes(): array + { + return $this->attributes; + } + +} diff --git a/src/Reflection/Php/FakeBuiltinMethodReflection.php b/src/Reflection/Php/FakeBuiltinMethodReflection.php deleted file mode 100644 index c190c3f6e3..0000000000 --- a/src/Reflection/Php/FakeBuiltinMethodReflection.php +++ /dev/null @@ -1,120 +0,0 @@ -methodName; - } - - public function getReflection(): ?ReflectionMethod - { - return null; - } - - public function getFileName(): ?string - { - return null; - } - - public function getDeclaringClass(): ReflectionClass - { - return $this->declaringClass; - } - - public function getStartLine(): ?int - { - return null; - } - - public function getEndLine(): ?int - { - return null; - } - - public function getDocComment(): ?string - { - return null; - } - - public function isStatic(): bool - { - return false; - } - - public function isPrivate(): bool - { - return false; - } - - public function isPublic(): bool - { - return true; - } - - public function getPrototype(): BuiltinMethodReflection - { - throw new ReflectionException(); - } - - public function isDeprecated(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isVariadic(): bool - { - return false; - } - - public function isFinal(): bool - { - return false; - } - - public function isInternal(): bool - { - return false; - } - - public function isAbstract(): bool - { - return false; - } - - public function getReturnType(): ?ReflectionType - { - return null; - } - - public function getTentativeReturnType(): ?ReflectionType - { - return null; - } - - /** - * @return ReflectionParameter[] - */ - public function getParameters(): array - { - return []; - } - -} diff --git a/src/Reflection/Php/NativeBuiltinMethodReflection.php b/src/Reflection/Php/NativeBuiltinMethodReflection.php deleted file mode 100644 index b9286a490b..0000000000 --- a/src/Reflection/Php/NativeBuiltinMethodReflection.php +++ /dev/null @@ -1,146 +0,0 @@ -reflection->getName(); - } - - public function getReflection(): ?ReflectionMethod - { - return $this->reflection; - } - - public function getFileName(): ?string - { - $fileName = $this->reflection->getFileName(); - if ($fileName === false) { - return null; - } - - return $fileName; - } - - public function getDeclaringClass(): ReflectionClass - { - return $this->reflection->getDeclaringClass(); - } - - public function getStartLine(): ?int - { - $line = $this->reflection->getStartLine(); - if ($line === false) { - return null; - } - - return $line; - } - - public function getEndLine(): ?int - { - $line = $this->reflection->getEndLine(); - if ($line === false) { - return null; - } - - return $line; - } - - public function getDocComment(): ?string - { - $docComment = $this->reflection->getDocComment(); - if ($docComment === false) { - return null; - } - - return $docComment; - } - - public function isStatic(): bool - { - return $this->reflection->isStatic(); - } - - public function isPrivate(): bool - { - return $this->reflection->isPrivate(); - } - - public function isPublic(): bool - { - return $this->reflection->isPublic(); - } - - public function isConstructor(): bool - { - return $this->reflection->isConstructor(); - } - - public function getPrototype(): BuiltinMethodReflection - { - return new self($this->reflection->getPrototype()); - } - - public function isDeprecated(): TrinaryLogic - { - return TrinaryLogic::createFromBoolean($this->reflection->isDeprecated()); - } - - public function isFinal(): bool - { - return $this->reflection->isFinal(); - } - - public function isInternal(): bool - { - return $this->reflection->isInternal(); - } - - public function isAbstract(): bool - { - return $this->reflection->isAbstract(); - } - - public function isVariadic(): bool - { - return $this->reflection->isVariadic(); - } - - 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[] - */ - public function getParameters(): array - { - return $this->reflection->getParameters(); - } - -} diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index c1fb32a16e..fc0dd70dbe 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -11,6 +11,7 @@ use PHPStan\Analyser\ScopeContext; use PHPStan\Analyser\ScopeFactory; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; use PHPStan\Parser\Parser; use PHPStan\PhpDoc\PhpDocInheritanceResolver; @@ -18,14 +19,19 @@ use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflectionFactory; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; +use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeMethodReflection; -use PHPStan\Reflection\Native\NativeParameterWithPhpDocsReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\SignatureMap\FunctionSignature; use PHPStan\Reflection\SignatureMap\ParameterSignature; @@ -33,52 +39,46 @@ 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\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; -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 +final class PhpClassReflectionExtension implements PropertiesClassReflectionExtension, MethodsClassReflectionExtension { - /** @var PropertyReflection[][] */ + /** @var ExtendedPropertyReflection[][] */ private array $propertiesIncludingAnnotations = []; /** @var PhpPropertyReflection[][] */ private array $nativeProperties = []; - /** @var MethodReflection[][] */ + /** @var ExtendedMethodReflection[][] */ private array $methodsIncludingAnnotations = []; - /** @var MethodReflection[][] */ + /** @var ExtendedMethodReflection[][] */ private array $nativeMethods = []; /** @var array> */ @@ -87,14 +87,12 @@ class PhpClassReflectionExtension /** @var array */ private array $inferClassConstructorPropertyTypesInProcess = []; - /** - * @param string[] $universalObjectCratesClasses - */ public function __construct( private ScopeFactory $scopeFactory, private NodeScopeResolver $nodeScopeResolver, private PhpMethodReflectionFactory $methodReflectionFactory, private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private DeprecationProvider $deprecationProvider, private AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension, private AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension, private SignatureMapProvider $signatureMapProvider, @@ -102,18 +100,66 @@ public function __construct( private StubPhpDocProvider $stubPhpDocProvider, private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private FileTypeMapper $fileTypeMapper, + private AttributeReflectionFactory $attributeReflectionFactory, private bool $inferPrivatePropertyTypeFromConstructor, - private array $universalObjectCratesClasses, ) { } + public function evictPrivateSymbols(string $classCacheKey): void + { + foreach ($this->propertiesIncludingAnnotations as $key => $properties) { + if ($key !== $classCacheKey) { + continue; + } + foreach ($properties as $name => $property) { + if (!$property->isPrivate()) { + continue; + } + unset($this->propertiesIncludingAnnotations[$key][$name]); + } + } + foreach ($this->nativeProperties as $key => $properties) { + if ($key !== $classCacheKey) { + continue; + } + foreach ($properties as $name => $property) { + if (!$property->isPrivate()) { + continue; + } + unset($this->nativeProperties[$key][$name]); + } + } + foreach ($this->methodsIncludingAnnotations as $key => $methods) { + if ($key !== $classCacheKey) { + continue; + } + foreach ($methods as $name => $method) { + if (!$method->isPrivate()) { + continue; + } + unset($this->methodsIncludingAnnotations[$key][$name]); + } + } + foreach ($this->nativeMethods as $key => $methods) { + if ($key !== $classCacheKey) { + continue; + } + foreach ($methods as $name => $method) { + if (!$method->isPrivate()) { + continue; + } + unset($this->nativeMethods[$key][$name]); + } + } + } + public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { return $classReflection->getNativeReflection()->hasProperty($propertyName); } - public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection { if (!isset($this->propertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName])) { $this->propertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName] = $this->createProperty($classReflection, $propertyName, true); @@ -137,7 +183,7 @@ private function createProperty( ClassReflection $classReflection, string $propertyName, bool $includingAnnotations, - ): PropertyReflection + ): ExtendedPropertyReflection { $propertyReflection = $classReflection->getNativeReflection()->getProperty($propertyName); $propertyName = $propertyReflection->getName(); @@ -172,13 +218,16 @@ private function createProperty( $types[] = $value; } - return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, false, false); + return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, null, null, false, false, false, false, []); } } - $deprecatedDescription = null; - $isDeprecated = false; + $deprecation = $this->deprecationProvider->getPropertyDeprecation($propertyReflection); + $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; $isInternal = false; + $isReadOnlyByPhpDoc = $classReflection->isImmutable(); + $isAllowedPrivateMutation = false; if ( $includingAnnotations @@ -200,7 +249,7 @@ private function createProperty( throw new ShouldNotHappenException(); } - if ($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] < $hierarchyDistances[$distanceDeclaringClass]) { + if ($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] <= $hierarchyDistances[$distanceDeclaringClass]) { return $annotationProperty; } } @@ -209,37 +258,34 @@ private function createProperty( ? $propertyReflection->getDocComment() : null; - $declaringTraitName = null; $phpDocType = null; $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(); - } + $declaringTraitName = $this->findPropertyTrait($propertyReflection); + $constructorName = null; + if ($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, + ); } + $phpDocBlockClassReflection = $declaringClassReflection; if ($resolvedPhpDoc !== null) { $varTags = $resolvedPhpDoc->getVarTags(); @@ -248,17 +294,32 @@ private function createProperty( } elseif (isset($varTags[$propertyName])) { $phpDocType = $varTags[$propertyName]->getType(); } + + $phpDocType = $phpDocType !== null ? TemplateTypeHelper::resolveTemplateTypes( + $phpDocType, + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createInvariant(), + ) : null; + + if (!$isDeprecated) { + $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; + $isDeprecated = $resolvedPhpDoc->isDeprecated(); + } + $isInternal = $resolvedPhpDoc->isInternal(); + $isReadOnlyByPhpDoc = $isReadOnlyByPhpDoc || $resolvedPhpDoc->isReadOnly(); + $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); } if ($phpDocType === null) { - if (isset($constructorName) && $declaringClassReflection->getFileName() !== null) { + if (isset($constructorName)) { $constructorDocComment = $declaringClassReflection->getConstructor()->getDocComment(); $nativeClassReflection = $declaringClassReflection->getNativeReflection(); $positionalParameterNames = []; if ($nativeClassReflection->getConstructor() !== null) { $positionalParameterNames = array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $nativeClassReflection->getConstructor()->getParameters()); } - $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( + $resolvedConstructorPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( $constructorDocComment, $declaringClassReflection->getFileName(), $declaringClassReflection, @@ -266,32 +327,20 @@ private function createProperty( $constructorName, $positionalParameterNames, ); - $paramTags = $resolvedPhpDoc->getParamTags(); + $paramTags = $resolvedConstructorPhpDoc->getParamTags(); if (isset($paramTags[$propertyReflection->getName()])) { $phpDocType = $paramTags[$propertyReflection->getName()]->getType(); } } } - if ($resolvedPhpDoc !== null) { - if (!isset($phpDocBlockClassReflection)) { - throw new ShouldNotHappenException(); - } - $phpDocType = $phpDocType !== null ? TemplateTypeHelper::resolveTemplateTypes( - $phpDocType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap(), - ) : null; - $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; - $isDeprecated = $resolvedPhpDoc->isDeprecated(); - $isInternal = $resolvedPhpDoc->isInternal(); - } - if ( $phpDocType === null && $this->inferPrivatePropertyTypeFromConstructor && $declaringClassReflection->getFileName() !== null && $propertyReflection->isPrivate() - && (!method_exists($propertyReflection, 'hasType') || !$propertyReflection->hasType()) + && !$propertyReflection->isPromoted() + && !$propertyReflection->hasType() && $declaringClassReflection->hasConstructor() && $declaringClassReflection->getConstructor()->getDeclaringClass()->getName() === $declaringClassReflection->getName() ) { @@ -302,7 +351,7 @@ private function createProperty( } $nativeType = null; - if (method_exists($propertyReflection, 'getType') && $propertyReflection->getType() !== null) { + if ($propertyReflection->getType() !== null) { $nativeType = $propertyReflection->getType(); } @@ -314,15 +363,78 @@ private function createProperty( $declaringTrait = $reflectionProvider->getClass($declaringTraitName); } + $getHook = null; + $setHook = null; + + $betterReflection = $propertyReflection->getBetterReflection(); + if ($betterReflection->hasHook('get')) { + $betterReflectionGetHook = $betterReflection->getHook('get'); + if ($betterReflectionGetHook === null) { + throw new ShouldNotHappenException(); + } + $getHook = $this->createUserlandMethodReflection( + $declaringClassReflection, + $declaringClassReflection, + new ReflectionMethod($betterReflectionGetHook), + $declaringTraitName, + ); + + if ($phpDocType !== null) { + $getHookMethodReflectionVariant = $getHook->getOnlyVariant(); + $getHookMethodReflectionVariantPhpDocReturnType = $getHookMethodReflectionVariant->getPhpDocReturnType(); + if ( + $getHookMethodReflectionVariantPhpDocReturnType instanceof MixedType + && !$getHookMethodReflectionVariantPhpDocReturnType instanceof TemplateMixedType + && !$getHookMethodReflectionVariantPhpDocReturnType->isExplicitMixed() + ) { + $getHook = $getHook->changePropertyGetHookPhpDocType($phpDocType); + } + } + } + + if ($betterReflection->hasHook('set')) { + $betterReflectionSetHook = $betterReflection->getHook('set'); + if ($betterReflectionSetHook === null) { + throw new ShouldNotHappenException(); + } + $setHook = $this->createUserlandMethodReflection( + $declaringClassReflection, + $declaringClassReflection, + new ReflectionMethod($betterReflectionSetHook), + $declaringTraitName, + ); + + if ($phpDocType !== null) { + $setHookMethodReflectionVariant = $setHook->getOnlyVariant(); + $setHookMethodReflectionParameters = $setHookMethodReflectionVariant->getParameters(); + if (isset($setHookMethodReflectionParameters[0])) { + $setHookMethodReflectionParameter = $setHookMethodReflectionParameters[0]; + $setHookMethodReflectionParameterPhpDocType = $setHookMethodReflectionParameter->getPhpDocType(); + if ( + $setHookMethodReflectionParameterPhpDocType instanceof MixedType + && !$setHookMethodReflectionParameterPhpDocType instanceof TemplateMixedType + && !$setHookMethodReflectionParameterPhpDocType->isExplicitMixed() + ) { + $setHook = $setHook->changePropertySetHookPhpDocType($setHookMethodReflectionParameter->getName(), $phpDocType); + } + } + } + } + return new PhpPropertyReflection( $declaringClassReflection, $declaringTrait, $nativeType, $phpDocType, $propertyReflection, + $getHook, + $setHook, $deprecatedDescription, $isDeprecated, $isInternal, + $isReadOnlyByPhpDoc, + $isAllowedPrivateMutation, + $this->attributeReflectionFactory->fromNativeReflection($propertyReflection->getAttributes(), InitializerExprContext::fromClass($declaringClassReflection->getName(), $declaringClassReflection->getFileName())), ); } @@ -331,13 +443,13 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): return $classReflection->getNativeReflection()->hasMethod($methodName); } - public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + public function getMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection { if (isset($this->methodsIncludingAnnotations[$classReflection->getCacheKey()][$methodName])) { return $this->methodsIncludingAnnotations[$classReflection->getCacheKey()][$methodName]; } - $nativeMethodReflection = new NativeBuiltinMethodReflection($classReflection->getNativeReflection()->getMethod($methodName)); + $nativeMethodReflection = $classReflection->getNativeReflection()->getMethod($methodName); if (!isset($this->methodsIncludingAnnotations[$classReflection->getCacheKey()][$nativeMethodReflection->getName()])) { $method = $this->createMethod($classReflection, $nativeMethodReflection, true); $this->methodsIncludingAnnotations[$classReflection->getCacheKey()][$nativeMethodReflection->getName()] = $method; @@ -351,49 +463,21 @@ public function getMethod(ClassReflection $classReflection, string $methodName): public function hasNativeMethod(ClassReflection $classReflection, string $methodName): bool { - $hasMethod = $this->hasMethod($classReflection, $methodName); - if ($hasMethod) { - return true; - } - - if ($methodName === '__get' && UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( - $this->reflectionProviderProvider->getReflectionProvider(), - $this->universalObjectCratesClasses, - $classReflection, - )) { - return true; - } - - return false; + return $this->hasMethod($classReflection, $methodName); } - public function getNativeMethod(ClassReflection $classReflection, string $methodName): MethodReflection + public function getNativeMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection { if (isset($this->nativeMethods[$classReflection->getCacheKey()][$methodName])) { return $this->nativeMethods[$classReflection->getCacheKey()][$methodName]; } - if ($classReflection->getNativeReflection()->hasMethod($methodName)) { - $nativeMethodReflection = new NativeBuiltinMethodReflection( - $classReflection->getNativeReflection()->getMethod($methodName), - ); - } else { - if ( - $methodName !== '__get' - || !UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( - $this->reflectionProviderProvider->getReflectionProvider(), - $this->universalObjectCratesClasses, - $classReflection, - )) { - throw new ShouldNotHappenException(); - } - - $nativeMethodReflection = new FakeBuiltinMethodReflection( - $methodName, - $classReflection->getNativeReflection(), - ); + if (!$classReflection->getNativeReflection()->hasMethod($methodName)) { + throw new ShouldNotHappenException(); } + $nativeMethodReflection = $classReflection->getNativeReflection()->getMethod($methodName); + if (!isset($this->nativeMethods[$classReflection->getCacheKey()][$nativeMethodReflection->getName()])) { $method = $this->createMethod($classReflection, $nativeMethodReflection, false); $this->nativeMethods[$classReflection->getCacheKey()][$nativeMethodReflection->getName()] = $method; @@ -404,9 +488,9 @@ public function getNativeMethod(ClassReflection $classReflection, string $method private function createMethod( ClassReflection $classReflection, - BuiltinMethodReflection $methodReflection, + ReflectionMethod $methodReflection, bool $includingAnnotations, - ): MethodReflection + ): ExtendedMethodReflection { if ($includingAnnotations && $this->annotationsMethodsClassReflectionExtension->hasMethod($classReflection, $methodReflection->getName())) { $hierarchyDistances = $classReflection->getClassHierarchyDistances(); @@ -424,7 +508,7 @@ private function createMethod( throw new ShouldNotHappenException(); } - if ($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()] < $hierarchyDistances[$distanceDeclaringClass]) { + if ($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()] <= $hierarchyDistances[$distanceDeclaringClass]) { return $annotationMethod; } } @@ -453,96 +537,141 @@ private function createMethod( } if ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName())) { - $variantNumbers = [0]; - $i = 1; - while ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName(), $i)) { - $variantNumbers[] = $i; - $i++; - } - - $variants = []; - $reflectionMethod = null; + $variantsByType = ['positional' => []]; $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()); - if ($reflectionClass->hasMethod($methodReflection->getName())) { - $reflectionMethod = $reflectionClass->getMethod($methodReflection->getName()); - } - } - foreach ($variantNumbers as $variantNumber) { - $methodSignature = $this->signatureMapProvider->getMethodSignature($declaringClassName, $methodReflection->getName(), $reflectionMethod, $variantNumber); - $phpDocParameterNameMapping = []; - foreach ($methodSignature->getParameters() as $parameter) { - $phpDocParameterNameMapping[$parameter->getName()] = $parameter->getName(); + $asserts = Assertions::createEmpty(); + $acceptsNamedArguments = true; + $selfOutType = null; + $phpDocComment = null; + $methodSignaturesResult = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $methodReflection); + foreach ($methodSignaturesResult as $signatureType => $methodSignatures) { + if ($methodSignatures === null) { + continue; } - $stubPhpDocReturnType = null; - $stubPhpDocParameterTypes = []; - $stubPhpDocParameterVariadicity = []; - $phpDocParameterTypes = []; - $phpDocReturnType = null; - $stubPhpDocPair = null; - if (count($variantNumbers) === 1) { - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); - if ($stubPhpDocPair !== null) { - [$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair; - $templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap(); - $returnTag = $stubPhpDoc->getReturnTag(); - if ($returnTag !== null) { - $stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( - $returnTag->getType(), - $templateTypeMap, - ); - } - foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { - $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( - $paramTag->getType(), - $templateTypeMap, - ); - $stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic(); - } + foreach ($methodSignatures as $methodSignature) { + $phpDocParameterNameMapping = []; + foreach ($methodSignature->getParameters() as $parameter) { + $phpDocParameterNameMapping[$parameter->getName()] = $parameter->getName(); + } + $stubPhpDocReturnType = null; + $stubPhpDocParameterTypes = []; + $stubPhpDocParameterVariadicity = []; + $phpDocParameterTypes = []; + $phpDocReturnType = null; + $stubPhpDocPair = null; + $stubPhpParameterOutTypes = []; + $phpDocParameterOutTypes = []; + $immediatelyInvokedCallableParameters = []; + $closureThisParameters = []; + $stubImmediatelyInvokedCallableParameters = []; + $stubClosureThisParameters = []; + if (count($methodSignatures) === 1) { + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); + if ($stubPhpDocPair !== null) { + [$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair; + $templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $stubDeclaringClass->getCallSiteVarianceMap(); + $returnTag = $stubPhpDoc->getReturnTag(); + $stubImmediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $stubPhpDoc->getParamsImmediatelyInvokedCallable()); + if ($returnTag !== null) { + $stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( + $returnTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ); + } + + $stubClosureThisParameters = array_map(static fn ($tag) => $tag->getType(), $stubPhpDoc->getParamClosureThisTags()); + foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { + $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( + $paramTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), + ); + $stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic(); + } + + $throwsTag = $stubPhpDoc->getThrowsTag(); + if ($throwsTag !== null) { + $throwType = $throwsTag->getType(); + } + + $asserts = Assertions::createFromResolvedPhpDocBlock($stubPhpDoc); + $acceptsNamedArguments = $stubPhpDoc->acceptsNamedArguments(); + + $selfOutTypeTag = $stubPhpDoc->getSelfOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); + } + + foreach ($stubPhpDoc->getParamOutTags() as $name => $paramOutTag) { + $stubPhpParameterOutTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ); + } - $throwsTag = $stubPhpDoc->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); + if ($declaringClassName === $stubDeclaringClass->getName() && $stubPhpDoc->hasPhpDocString()) { + $phpDocComment = $stubPhpDoc->getPhpDocString(); + } } } - } - if ($stubPhpDocPair === null && $reflectionMethod !== null && $reflectionMethod->getDocComment() !== false) { - $filename = $reflectionMethod->getFileName(); - if ($filename !== false) { - $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( - $filename, - $declaringClassName, - null, - $reflectionMethod->getName(), - $reflectionMethod->getDocComment(), - ); - $throwsTag = $phpDocBlock->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); - } - $returnTag = $phpDocBlock->getReturnTag(); - if ($returnTag !== null) { - $phpDocReturnType = $returnTag->getType(); - } - foreach ($phpDocBlock->getParamTags() as $name => $paramTag) { - $phpDocParameterTypes[$name] = $paramTag->getType(); - } + if ($stubPhpDocPair === null && $methodReflection->getDocComment() !== false) { + $filename = $methodReflection->getFileName(); + if ($filename !== false) { + $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( + $filename, + $declaringClassName, + null, + $methodReflection->getName(), + $methodReflection->getDocComment(), + ); + $throwsTag = $phpDocBlock->getThrowsTag(); + if ($throwsTag !== null) { + $throwType = $throwsTag->getType(); + } + $returnTag = $phpDocBlock->getReturnTag(); + if ($returnTag !== null && count($methodSignatures) === 1) { + $phpDocReturnType = $returnTag->getType(); + } + $immediatelyInvokedCallableParameters = array_map(static fn ($immediate) => TrinaryLogic::createFromBoolean($immediate), $phpDocBlock->getParamsImmediatelyInvokedCallable()); + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $phpDocBlock->getParamClosureThisTags()); + foreach ($phpDocBlock->getParamTags() as $name => $paramTag) { + $phpDocParameterTypes[$name] = $paramTag->getType(); + } + $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); + $acceptsNamedArguments = $phpDocBlock->acceptsNamedArguments(); + + $selfOutTypeTag = $phpDocBlock->getSelfOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); + } - $signatureParameters = $methodSignature->getParameters(); - foreach ($reflectionMethod->getParameters() as $paramI => $reflectionParameter) { - if (!array_key_exists($paramI, $signatureParameters)) { - continue; + if ($phpDocBlock->hasPhpDocString()) { + $phpDocComment = $phpDocBlock->getPhpDocString(); } - $phpDocParameterNameMapping[$signatureParameters[$paramI]->getName()] = $reflectionParameter->getName(); + foreach ($phpDocBlock->getParamOutTags() as $name => $paramOutTag) { + $phpDocParameterOutTypes[$name] = $paramOutTag->getType(); + } + + $signatureParameters = $methodSignature->getParameters(); + foreach ($methodReflection->getParameters() as $paramI => $reflectionParameter) { + if (!array_key_exists($paramI, $signatureParameters)) { + continue; + } + + $phpDocParameterNameMapping[$signatureParameters[$paramI]->getName()] = $reflectionParameter->getName(); + } } } + $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $stubPhpParameterOutTypes, $phpDocParameterOutTypes, $stubImmediatelyInvokedCallableParameters, $immediatelyInvokedCallableParameters, $stubClosureThisParameters, $closureThisParameters, $signatureType !== 'named'); } - $variants[] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping); } if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { @@ -554,35 +683,69 @@ private function createMethod( $this->reflectionProviderProvider->getReflectionProvider(), $declaringClass, $methodReflection, - $variants, + $variantsByType['positional'], + $variantsByType['named'] ?? null, $hasSideEffects, $throwType, + $asserts, + $acceptsNamedArguments, + $selfOutType, + $phpDocComment, + $this->attributeReflectionFactory->fromNativeReflection($methodReflection->getAttributes(), InitializerExprContext::fromClassMethod($declaringClassName, null, $methodReflection->getName(), null)), ); } - $declaringTraitName = $this->findMethodTrait($methodReflection); + return $this->createUserlandMethodReflection( + $declaringClass, + $declaringClass, + $methodReflection, + $this->findMethodTrait($methodReflection), + ); + } + + public function createUserlandMethodReflection(ClassReflection $fileDeclaringClass, ClassReflection $actualDeclaringClass, ReflectionMethod $methodReflection, ?string $declaringTraitName): PhpMethodReflection + { + $deprecation = $this->deprecationProvider->getMethodDeprecation($methodReflection); + $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; + $resolvedPhpDoc = null; - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); - $phpDocBlockClassReflection = $declaringClass; + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($fileDeclaringClass, $fileDeclaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); + $phpDocBlockClassReflection = $fileDeclaringClass; + + $methodDeclaringClass = $methodReflection->getBetterReflection()->getDeclaringClass(); + + if ($stubPhpDocPair === null && $methodDeclaringClass->isTrait()) { + if (! $methodReflection->getDeclaringClass()->isTrait() || $methodDeclaringClass->getName() !== $methodReflection->getDeclaringClass()->getName()) { + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors( + $this->reflectionProviderProvider->getReflectionProvider()->getClass($methodDeclaringClass->getName()), + $this->reflectionProviderProvider->getReflectionProvider()->getClass($methodReflection->getDeclaringClass()->getName()), + $methodReflection->getName(), + array_map( + static fn (ReflectionParameter $parameter): string => $parameter->getName(), + $methodReflection->getParameters(), + ), + ); + } + } + if ($stubPhpDocPair !== null) { [$resolvedPhpDoc, $phpDocBlockClassReflection] = $stubPhpDocPair; } if ($resolvedPhpDoc === null) { - if ($declaringClass->getFileName() !== null) { - $docComment = $methodReflection->getDocComment(); - $positionalParameterNames = array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()); - - $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( - $docComment, - $declaringClass->getFileName(), - $declaringClass, - $declaringTraitName, - $methodReflection->getName(), - $positionalParameterNames, - ); - $phpDocBlockClassReflection = $declaringClass; - } + $docComment = $methodReflection->getDocComment() !== false ? $methodReflection->getDocComment() : null; + $positionalParameterNames = array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()); + + $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( + $docComment, + $actualDeclaringClass->getFileName(), + $actualDeclaringClass, + $declaringTraitName, + $methodReflection->getName(), + $positionalParameterNames, + ); + $phpDocBlockClassReflection = $fileDeclaringClass; } $declaringTrait = null; @@ -593,22 +756,10 @@ private function createMethod( $declaringTrait = $reflectionProvider->getClass($declaringTraitName); } - $templateTypeMap = TemplateTypeMap::createEmpty(); $phpDocParameterTypes = []; - $phpDocReturnType = null; - $phpDocThrowType = null; - $deprecatedDescription = null; - $isDeprecated = false; - $isInternal = false; - $isFinal = false; - $isPure = false; - if ( - $methodReflection instanceof NativeBuiltinMethodReflection - && $methodReflection->isConstructor() - && $declaringClass->getFileName() !== null - ) { + if ($methodReflection->isConstructor()) { foreach ($methodReflection->getParameters() as $parameter) { - if (!method_exists($parameter, 'isPromoted') || !$parameter->isPromoted()) { + if (!$parameter->isPromoted()) { continue; } @@ -617,7 +768,7 @@ private function createMethod( } $parameterProperty = $methodReflection->getDeclaringClass()->getProperty($parameter->getName()); - if (!method_exists($parameterProperty, 'isPromoted') || !$parameterProperty->isPromoted()) { + if (!$parameterProperty->isPromoted()) { continue; } if ($parameterProperty->getDocComment() === false) { @@ -625,8 +776,8 @@ private function createMethod( } $propertyDocblock = $this->fileTypeMapper->getResolvedPhpDoc( - $declaringClass->getFileName(), - $declaringClassName, + $fileDeclaringClass->getFileName(), + $fileDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), $parameterProperty->getDocComment(), @@ -643,36 +794,60 @@ private function createMethod( $phpDocParameterTypes[$parameter->getName()] = $phpDocType; } } - if ($resolvedPhpDoc !== null) { - $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { - if (array_key_exists($paramName, $phpDocParameterTypes)) { - continue; - } - $phpDocParameterTypes[$paramName] = $paramTag->getType(); - } - foreach ($phpDocParameterTypes as $paramName => $paramType) { - $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( - $paramType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap(), - ); + + $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + $immediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $resolvedPhpDoc->getParamsImmediatelyInvokedCallable()); + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamClosureThisTags()); + + foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { + if (array_key_exists($paramName, $phpDocParameterTypes)) { + continue; } - $nativeReturnType = TypehintHelper::decideTypeFromReflection( - $methodReflection->getReturnType(), - null, - $declaringClass->getName(), + $phpDocParameterTypes[$paramName] = $paramTag->getType(); + } + foreach ($phpDocParameterTypes as $paramName => $paramType) { + $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramType, + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), ); - $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); - $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; + } + + $phpDocParameterOutTypes = []; + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), + ); + } + + $nativeReturnType = TypehintHelper::decideTypeFromReflection( + $methodReflection->getReturnType(), + null, + $actualDeclaringClass, + ); + $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); + $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; + if (!$isDeprecated) { $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; $isDeprecated = $resolvedPhpDoc->isDeprecated(); - $isInternal = $resolvedPhpDoc->isInternal(); - $isFinal = $resolvedPhpDoc->isFinal(); - $isPure = $resolvedPhpDoc->isPure(); + } + $isInternal = $resolvedPhpDoc->isInternal(); + $isFinal = $resolvedPhpDoc->isFinal(); + $isPure = $resolvedPhpDoc->isPure(); + $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); + $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; + $phpDocComment = null; + if ($resolvedPhpDoc->hasPhpDocString()) { + $phpDocComment = $resolvedPhpDoc->getPhpDocString(); } return $this->methodReflectionFactory->create( - $declaringClass, + $actualDeclaringClass, $declaringTrait, $methodReflection, $templateTypeMap, @@ -684,6 +859,14 @@ private function createMethod( $isInternal, $isFinal, $isPure, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $immediatelyInvokedCallableParameters, + $closureThisParameters, + $acceptsNamedArguments, + $this->attributeReflectionFactory->fromNativeReflection($methodReflection->getAttributes(), InitializerExprContext::fromClassMethod($actualDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), $actualDeclaringClass->getFileName())), ); } @@ -692,6 +875,12 @@ private function createMethod( * @param array $stubPhpDocParameterVariadicity * @param array $phpDocParameterTypes * @param array $phpDocParameterNameMapping + * @param array $stubPhpDocParameterOutTypes + * @param array $phpDocParameterOutTypes + * @param array $stubImmediatelyInvokedCallableParameters + * @param array $immediatelyInvokedCallableParameters + * @param array $stubClosureThisParameters + * @param array $closureThisParameters */ private function createNativeMethodVariant( FunctionSignature $methodSignature, @@ -701,12 +890,20 @@ private function createNativeMethodVariant( array $phpDocParameterTypes, ?Type $phpDocReturnType, array $phpDocParameterNameMapping, - ): FunctionVariantWithPhpDocs + array $stubPhpDocParameterOutTypes, + array $phpDocParameterOutTypes, + array $stubImmediatelyInvokedCallableParameters, + array $immediatelyInvokedCallableParameters, + array $stubClosureThisParameters, + array $closureThisParameters, + bool $usePhpDocParameterNames, + ): ExtendedFunctionVariant { $parameters = []; foreach ($methodSignature->getParameters() as $parameterSignature) { $type = null; $phpDocType = null; + $parameterOutType = null; $phpDocParameterName = $phpDocParameterNameMapping[$parameterSignature->getName()] ?? $parameterSignature->getName(); @@ -717,160 +914,93 @@ private function createNativeMethodVariant( $phpDocType = $phpDocParameterTypes[$phpDocParameterName]; } - $parameters[] = new NativeParameterWithPhpDocsReflection( - $phpDocParameterName, + if (isset($stubPhpDocParameterOutTypes[$parameterSignature->getName()])) { + $parameterOutType = $stubPhpDocParameterOutTypes[$parameterSignature->getName()]; + } elseif (isset($phpDocParameterOutTypes[$phpDocParameterName])) { + $parameterOutType = $phpDocParameterOutTypes[$phpDocParameterName]; + } + + if (isset($stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()])) { + $immediatelyInvoked = $stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()]; + } elseif (isset($immediatelyInvokedCallableParameters[$phpDocParameterName])) { + $immediatelyInvoked = $immediatelyInvokedCallableParameters[$phpDocParameterName]; + } else { + $immediatelyInvoked = TrinaryLogic::createMaybe(); + } + + $closureThisType = null; + if (isset($stubClosureThisParameters[$parameterSignature->getName()])) { + $closureThisType = $stubClosureThisParameters[$parameterSignature->getName()]; + } elseif (isset($closureThisParameters[$phpDocParameterName])) { + $closureThisType = $closureThisParameters[$phpDocParameterName]; + } + + $parameters[] = new ExtendedNativeParameterReflection( + $usePhpDocParameterNames + ? $phpDocParameterName + : $parameterSignature->getName(), $parameterSignature->isOptional(), $type ?? $parameterSignature->getType(), $phpDocType ?? new MixedType(), $parameterSignature->getNativeType(), $parameterSignature->passedByReference(), $stubPhpDocParameterVariadicity[$parameterSignature->getName()] ?? $parameterSignature->isVariadic(), - null, + $parameterSignature->getDefaultValue(), + $parameterOutType ?? $parameterSignature->getOutType(), + $immediatelyInvoked, + $closureThisType, + [], ); } - $returnType = null; if ($stubPhpDocReturnType !== null) { $returnType = $stubPhpDocReturnType; $phpDocReturnType = $stubPhpDocReturnType; + } else { + $returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType); } - return new FunctionVariantWithPhpDocs( + return new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, $parameters, $methodSignature->isVariadic(), - $returnType ?? $methodSignature->getReturnType(), + $returnType, $phpDocReturnType ?? new MixedType(), $methodSignature->getNativeReturnType(), ); } - private function findPropertyTrait(\ReflectionProperty $propertyReflection): ?string + private function findPropertyTrait(ReflectionProperty $propertyReflection): ?string { - if ($propertyReflection instanceof ReflectionProperty) { - $declaringClass = $propertyReflection->getBetterReflection()->getDeclaringClass(); - if ($declaringClass->isTrait()) { - if ($propertyReflection->getDeclaringClass()->isTrait() && $propertyReflection->getDeclaringClass()->getName() === $declaringClass->getName()) { - return null; - } - - return $declaringClass->getName(); + $declaringClass = $propertyReflection->getBetterReflection()->getDeclaringClass(); + if ($declaringClass->isTrait()) { + if ($propertyReflection->getDeclaringClass()->isTrait() && $propertyReflection->getDeclaringClass()->getName() === $declaringClass->getName()) { + return null; } - return null; - } - $declaringClass = $propertyReflection->getDeclaringClass(); - $trait = $this->deepScanTraitsForProperty($declaringClass->getTraits(), $propertyReflection); - if ($trait !== null) { - return $trait; - } - - return null; - } - - /** - * @param ReflectionClass[] $traits - */ - private function deepScanTraitsForProperty( - array $traits, - \ReflectionProperty $propertyReflection, - ): ?string - { - foreach ($traits as $trait) { - $result = $this->deepScanTraitsForProperty($trait->getTraits(), $propertyReflection); - if ($result !== null) { - return $result; - } - - if (!$trait->hasProperty($propertyReflection->getName())) { - continue; - } - - $traitProperty = $trait->getProperty($propertyReflection->getName()); - if ($traitProperty->getDocComment() === $propertyReflection->getDocComment()) { - return $trait->getName(); - } + return $declaringClass->getName(); } return null; } private function findMethodTrait( - BuiltinMethodReflection $methodReflection, + ReflectionMethod $methodReflection, ): ?string { - if ($methodReflection->getReflection() instanceof ReflectionMethod) { - $declaringClass = $methodReflection->getReflection()->getBetterReflection()->getDeclaringClass(); - if ($declaringClass->isTrait()) { - if ($methodReflection->getDeclaringClass()->isTrait() && $declaringClass->getName() === $methodReflection->getDeclaringClass()->getName()) { - return null; - } - - return $declaringClass->getName(); - } - - return null; - } - - $declaringClass = $methodReflection->getDeclaringClass(); - if ( - $methodReflection->getFileName() === $declaringClass->getFileName() - && $methodReflection->getStartLine() >= $declaringClass->getStartLine() - && $methodReflection->getEndLine() <= $declaringClass->getEndLine() - ) { - return null; - } - - $declaringClass = $methodReflection->getDeclaringClass(); - $traitAliases = $declaringClass->getTraitAliases(); - if (array_key_exists($methodReflection->getName(), $traitAliases)) { - return explode('::', $traitAliases[$methodReflection->getName()])[0]; - } - - foreach ($this->collectTraits($declaringClass) as $traitReflection) { - if (!$traitReflection->hasMethod($methodReflection->getName())) { - continue; + $declaringClass = $methodReflection->getBetterReflection()->getDeclaringClass(); + if ($declaringClass->isTrait()) { + if ($methodReflection->getDeclaringClass()->isTrait() && $declaringClass->getName() === $methodReflection->getDeclaringClass()->getName()) { + return null; } - if ( - $methodReflection->getFileName() === $traitReflection->getFileName() - && $methodReflection->getStartLine() >= $traitReflection->getStartLine() - && $methodReflection->getEndLine() <= $traitReflection->getEndLine() - ) { - return $traitReflection->getName(); - } + return $declaringClass->getName(); } return null; } - /** - * @return ReflectionClass[] - */ - private function collectTraits(ReflectionClass $class): array - { - $traits = []; - $traitsLeftToAnalyze = $class->getTraits(); - - while (count($traitsLeftToAnalyze) !== 0) { - $trait = reset($traitsLeftToAnalyze); - $traits[] = $trait; - - foreach ($trait->getTraits() as $subTrait) { - if (in_array($subTrait, $traits, true)) { - continue; - } - - $traitsLeftToAnalyze[] = $subTrait; - } - - array_shift($traitsLeftToAnalyze); - } - - return $traits; - } - private function inferPrivatePropertyType( string $propertyName, MethodReflection $constructor, @@ -923,14 +1053,12 @@ private function inferAndCachePropertyTypes( $namespace = implode('\\', array_slice($classNameParts, 0, -1)); } - $classScope = $this->scopeFactory->create( - ScopeContext::create($fileName), - false, - [], - $constructor, - $namespace, - )->enterClass($declaringClass); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); + $classScope = $this->scopeFactory->create(ScopeContext::create($fileName)); + if ($namespace !== null) { + $classScope = $classScope->enterNamespace($namespace); + } + $classScope = $classScope->enterClass($declaringClass); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -942,6 +1070,13 @@ private function inferAndCachePropertyTypes( $isInternal, $isFinal, $isPure, + $acceptsNamedArguments, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); $propertyTypes = []; @@ -974,7 +1109,7 @@ private function inferAndCachePropertyTypes( } $propertyType = $propertyType->generalize(GeneralizePrecision::lessSpecific()); - if ($propertyType instanceof ConstantArrayType) { + if ($propertyType->isConstantArray()->yes()) { $propertyType = new ArrayType(new MixedType(true), new MixedType(true)); } @@ -1047,6 +1182,8 @@ private function getPhpDocReturnType(ClassReflection $phpDocBlockClassReflection $phpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( $phpDocReturnType, $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ); if ($returnTag->isExplicit() || $nativeReturnType->isSuperTypeOf($phpDocReturnType)->yes()) { @@ -1060,10 +1197,15 @@ private function getPhpDocReturnType(ClassReflection $phpDocBlockClassReflection * @param array $positionalParameterNames * @return array{ResolvedPhpDocBlock, ClassReflection}|null */ - private function findMethodPhpDocIncludingAncestors(ClassReflection $declaringClass, string $methodName, array $positionalParameterNames): ?array + private function findMethodPhpDocIncludingAncestors( + ClassReflection $declaringClass, + ClassReflection $implementingClass, + string $methodName, + array $positionalParameterNames, + ): ?array { $declaringClassName = $declaringClass->getName(); - $resolved = $this->stubPhpDocProvider->findMethodPhpDoc($declaringClassName, $methodName, $positionalParameterNames); + $resolved = $this->stubPhpDocProvider->findMethodPhpDoc($declaringClassName, $implementingClass->getName(), $methodName, $positionalParameterNames); if ($resolved !== null) { return [$resolved, $declaringClass]; } @@ -1080,7 +1222,7 @@ private function findMethodPhpDocIncludingAncestors(ClassReflection $declaringCl continue; } - $resolved = $this->stubPhpDocProvider->findMethodPhpDoc($ancestor->getName(), $methodName, $positionalParameterNames); + $resolved = $this->stubPhpDocProvider->findMethodPhpDoc($ancestor->getName(), $ancestor->getName(), $methodName, $positionalParameterNames); if ($resolved === null) { continue; } diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index cb4ec0b402..02a5b642af 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -7,36 +7,46 @@ use PhpParser\Node\FunctionLike; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; 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\Generic\TemplateTypeVarianceMap; 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 FunctionReflection +/** + * @api + */ +class PhpFunctionFromParserNodeReflection implements FunctionReflection, ExtendedParametersAcceptor { - /** @var Function_|ClassMethod */ + /** @var Function_|ClassMethod|Node\PropertyHook */ private Node\FunctionLike $functionLike; - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; /** - * @param Function_|ClassMethod $functionLike + * @param Function_|ClassMethod|Node\PropertyHook $functionLike * @param Type[] $realParameterTypes * @param Type[] $phpDocParameterTypes * @param Type[] $realParameterDefaultValues + * @param array> $parameterAttributes + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters + * @param list $attributes */ public function __construct( FunctionLike $functionLike, @@ -45,14 +55,21 @@ public function __construct( private array $realParameterTypes, private array $phpDocParameterTypes, private array $realParameterDefaultValues, + private array $parameterAttributes, private Type $realReturnType, private ?Type $phpDocReturnType, private ?Type $throwType, private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, - private bool $isFinal, - private ?bool $isPure, + protected ?bool $isPure, + private bool $acceptsNamedArguments, + private Assertions $assertions, + private ?string $phpDocComment, + private array $parameterOutTypes, + private array $immediatelyInvokedCallableParameters, + private array $phpDocClosureThisTypeParameters, + private array $attributes, ) { $this->functionLike = $functionLike; @@ -74,6 +91,11 @@ public function getName(): string return $this->functionLike->name->name; } + if (!$this->functionLike instanceof Function_) { + // PropertyHook is handled in PhpMethodFromParserNodeReflection subclass + throw new ShouldNotHappenException(); + } + if ($this->functionLike->namespacedName === null) { throw new ShouldNotHappenException(); } @@ -81,21 +103,18 @@ public function getName(): string return (string) $this->functionLike->namespacedName; } - /** - * @return ParametersAcceptorWithPhpDocs[] - */ public function getVariants(): array { if ($this->variants === null) { $this->variants = [ - new FunctionVariantWithPhpDocs( - $this->templateTypeMap, - null, + new ExtendedFunctionVariant( + $this->getTemplateTypeMap(), + $this->getResolvedTemplateTypeMap(), $this->getParameters(), $this->isVariadic(), $this->getReturnType(), - $this->phpDocReturnType ?? new MixedType(), - $this->realReturnType, + $this->getPhpDocReturnType(), + $this->getNativeReturnType(), ), ]; } @@ -103,10 +122,30 @@ public function getVariants(): array return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->templateTypeMap; + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return TemplateTypeMap::createEmpty(); + } + /** - * @return ParameterReflectionWithPhpDocs[] + * @return list */ - private function getParameters(): array + public function getParameters(): array { $parameters = []; $isOptional = true; @@ -120,6 +159,19 @@ private function getParameters(): array if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { throw new ShouldNotHappenException(); } + + if (isset($this->immediatelyInvokedCallableParameters[$parameter->var->name])) { + $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($this->immediatelyInvokedCallableParameters[$parameter->var->name]); + } else { + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + } + + if (isset($this->phpDocClosureThisTypeParameters[$parameter->var->name])) { + $closureThisType = $this->phpDocClosureThisTypeParameters[$parameter->var->name]; + } else { + $closureThisType = null; + } + $parameters[] = new PhpParameterFromParserNodeReflection( $parameter->var->name, $isOptional, @@ -130,13 +182,17 @@ private function getParameters(): array : PassedByReference::createNo(), $this->realParameterDefaultValues[$parameter->var->name] ?? null, $parameter->variadic, + $this->parameterOutTypes[$parameter->var->name] ?? null, + $immediatelyInvokedCallable, + $closureThisType, + $this->parameterAttributes[$parameter->var->name] ?? [], ); } return array_reverse($parameters); } - private function isVariadic(): bool + public function isVariadic(): bool { foreach ($this->functionLike->getParams() as $parameter) { if ($parameter->variadic) { @@ -147,11 +203,26 @@ private function isVariadic(): bool return false; } - private function getReturnType(): Type + public function getReturnType(): Type { return TypehintHelper::decideType($this->realReturnType, $this->phpDocReturnType); } + public function getPhpDocReturnType(): Type + { + return $this->phpDocReturnType ?? new MixedType(); + } + + public function getNativeReturnType(): Type + { + return $this->realReturnType; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + public function getDeprecatedDescription(): ?string { if ($this->isDeprecated) { @@ -171,15 +242,6 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isInternal); } - public function isFinal(): TrinaryLogic - { - $finalMethod = false; - if ($this->functionLike instanceof ClassMethod) { - $finalMethod = $this->functionLike->isFinal(); - } - return TrinaryLogic::createFromBoolean($finalMethod || $this->isFinal); - } - public function getThrowType(): ?Type { return $this->throwType; @@ -187,7 +249,7 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - if ($this->getReturnType() instanceof VoidType) { + if ($this->getReturnType()->isVoid()->yes()) { return TrinaryLogic::createYes(); } if ($this->isPure !== null) { @@ -207,6 +269,11 @@ public function isGenerator(): bool return $this->nodeIsOrContainsYield($this->functionLike); } + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->acceptsNamedArguments); + } + private function nodeIsOrContainsYield(Node $node): bool { if ($node instanceof Node\Expr\Yield_) { @@ -238,4 +305,33 @@ private function nodeIsOrContainsYield(Node $node): bool return false; } + public function getAsserts(): Assertions + { + return $this->assertions; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->functionLike->returnsByRef()); + } + + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index fc53462093..368ac3f471 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -2,47 +2,51 @@ namespace PHPStan\Reflection\Php; -use PhpParser\Node; -use PhpParser\Node\Stmt\ClassLike; -use PhpParser\Node\Stmt\Declare_; -use PhpParser\Node\Stmt\Function_; -use PhpParser\Node\Stmt\Namespace_; -use PHPStan\Cache\Cache; -use PHPStan\Parser\FunctionCallStatementFinder; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; +use PHPStan\Internal\DeprecatedAttributeHelper; use PHPStan\Parser\Parser; +use PHPStan\Parser\VariadicFunctionsVisitor; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; 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 ReflectionFunction; -use ReflectionParameter; +use function array_key_exists; use function array_map; -use function filemtime; +use function count; +use function is_array; use function is_file; -use function sprintf; -use function time; -class PhpFunctionReflection implements FunctionReflection +final class PhpFunctionReflection implements FunctionReflection { - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; + private ?bool $containsVariadicCalls = null; + /** - * @param Type[] $phpDocParameterTypes + * @param array $phpDocParameterTypes + * @param array $phpDocParameterOutTypes + * @param array $phpDocParameterImmediatelyInvokedCallable + * @param array $phpDocParameterClosureThisTypes + * @param list $attributes */ public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, private ReflectionFunction $reflection, private Parser $parser, - private FunctionCallStatementFinder $functionCallStatementFinder, - private Cache $cache, + private AttributeReflectionFactory $attributeReflectionFactory, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -50,9 +54,15 @@ public function __construct( private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, - private bool $isFinal, private ?string $filename, private ?bool $isPure, + private Assertions $asserts, + private bool $acceptsNamedArguments, + private ?string $phpDocComment, + private array $phpDocParameterOutTypes, + private array $phpDocParameterImmediatelyInvokedCallable, + private array $phpDocParameterClosureThisTypes, + private array $attributes, ) { } @@ -75,14 +85,11 @@ public function getFileName(): ?string return $this->filename; } - /** - * @return ParametersAcceptorWithPhpDocs[] - */ public function getVariants(): array { if ($this->variants === null) { $this->variants = [ - new FunctionVariantWithPhpDocs( + new ExtendedFunctionVariant( $this->templateTypeMap, null, $this->getParameters(), @@ -97,82 +104,70 @@ public function getVariants(): array return $this->variants; } - /** - * @return ParameterReflectionWithPhpDocs[] - */ - private function getParameters(): array + public function getOnlyVariant(): ExtendedParametersAcceptor { - return array_map(fn (ReflectionParameter $reflection): PhpParameterReflection => new PhpParameterReflection( - $reflection, - $this->phpDocParameterTypes[$reflection->getName()] ?? null, - null, - ), $this->reflection->getParameters()); + return $this->getVariants()[0]; } - private function isVariadic(): bool + public function getNamedArgumentsVariants(): ?array { - $isNativelyVariadic = $this->reflection->isVariadic(); - if (!$isNativelyVariadic && $this->reflection->getFileName() !== false) { - $fileName = $this->reflection->getFileName(); - if (is_file($fileName)) { - $functionName = $this->reflection->getName(); - $modifiedTime = filemtime($fileName); - if ($modifiedTime === false) { - $modifiedTime = time(); - } - $variableCacheKey = sprintf('%d-v3', $modifiedTime); - $key = sprintf('variadic-function-%s-%s', $functionName, $fileName); - $cachedResult = $this->cache->load($key, $variableCacheKey); - if ($cachedResult === null) { - $nodes = $this->parser->parseFile($fileName); - $result = $this->callsFuncGetArgs($nodes); - $this->cache->save($key, $variableCacheKey, $result); - return $result; - } - - return $cachedResult; - } - } - - return $isNativelyVariadic; + return null; } /** - * @param Node[] $nodes + * @return list */ - private function callsFuncGetArgs(array $nodes): bool + private function getParameters(): array { - foreach ($nodes as $node) { - if ($node instanceof Function_) { - $functionName = (string) $node->namespacedName; + return array_map(function (ReflectionParameter $reflection): PhpParameterReflection { + if (array_key_exists($reflection->getName(), $this->phpDocParameterImmediatelyInvokedCallable)) { + $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($this->phpDocParameterImmediatelyInvokedCallable[$reflection->getName()]); + } else { + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + } + return new PhpParameterReflection( + $this->initializerExprTypeResolver, + $reflection, + $this->phpDocParameterTypes[$reflection->getName()] ?? null, + null, + $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, + $immediatelyInvokedCallable, + $this->phpDocParameterClosureThisTypes[$reflection->getName()] ?? null, + $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), + ); + }, $this->reflection->getParameters()); + } - if ($functionName === $this->reflection->getName()) { - return $this->functionCallStatementFinder->findFunctionCallInStatements(ParametersAcceptor::VARIADIC_FUNCTIONS, $node->getStmts()) !== null; - } + private function isVariadic(): bool + { + $isNativelyVariadic = $this->reflection->isVariadic(); + if (!$isNativelyVariadic && $this->reflection->getFileName() !== false && !$this->isBuiltin()) { + $filename = $this->reflection->getFileName(); - continue; + if ($this->containsVariadicCalls !== null) { + return $this->containsVariadicCalls; } - if ($node instanceof ClassLike) { - continue; + if (array_key_exists($this->reflection->getName(), VariadicFunctionsVisitor::$cache)) { + return $this->containsVariadicCalls = VariadicFunctionsVisitor::$cache[$this->reflection->getName()]; } - if ($node instanceof Namespace_) { - if ($this->callsFuncGetArgs($node->stmts)) { - return true; - } - } + $nodes = $this->parser->parseFile($filename); + if (count($nodes) > 0) { + $variadicFunctions = $nodes[0]->getAttribute(VariadicFunctionsVisitor::ATTRIBUTE_NAME); - if (!$node instanceof Declare_ || $node->stmts === null) { - continue; + if ( + is_array($variadicFunctions) + && array_key_exists($this->reflection->getName(), $variadicFunctions) + ) { + return $this->containsVariadicCalls = $variadicFunctions[$this->reflection->getName()]; + } } - if ($this->callsFuncGetArgs($node->stmts)) { - return true; - } + return $this->containsVariadicCalls = false; } - return false; + return $isNativelyVariadic; } private function getReturnType(): Type @@ -203,6 +198,11 @@ public function getDeprecatedDescription(): ?string return $this->deprecatedDescription; } + if ($this->reflection->isDeprecated()) { + $attributes = $this->reflection->getBetterReflection()->getAttributes(); + return DeprecatedAttributeHelper::getDeprecatedDescription($attributes); + } + return null; } @@ -218,11 +218,6 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isInternal); } - public function isFinal(): TrinaryLogic - { - return TrinaryLogic::createFromBoolean($this->isFinal); - } - public function getThrowType(): ?Type { return $this->phpDocThrowType; @@ -230,7 +225,7 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - if ($this->getReturnType() instanceof VoidType) { + if ($this->getReturnType()->isVoid()->yes()) { return TrinaryLogic::createYes(); } if ($this->isPure !== null) { @@ -240,9 +235,43 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + public function isBuiltin(): bool { return $this->reflection->isInternal(); } + public function getAsserts(): Assertions + { + return $this->asserts; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->acceptsNamedArguments); + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 26fa3a7833..cd0e6fcbf6 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -4,55 +4,85 @@ use PhpParser\Node; use PhpParser\Node\Stmt\ClassMethod; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MissingMethodFromReflectionException; +use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VoidType; +use function in_array; +use function sprintf; use function strtolower; -class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeReflection implements MethodReflection +/** + * @api + */ +final class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeReflection implements ExtendedMethodReflection { /** * @param Type[] $realParameterTypes * @param Type[] $phpDocParameterTypes * @param Type[] $realParameterDefaultValues + * @param array> $parameterAttributes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters + * @param list $attributes */ public function __construct( private ClassReflection $declaringClass, - ClassMethod $classMethod, + private ClassMethod|Node\PropertyHook $classMethod, + private ?string $hookForProperty, string $fileName, TemplateTypeMap $templateTypeMap, array $realParameterTypes, array $phpDocParameterTypes, array $realParameterDefaultValues, + array $parameterAttributes, Type $realReturnType, ?Type $phpDocReturnType, ?Type $throwType, ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, + private bool $isFinal, ?bool $isPure, + bool $acceptsNamedArguments, + Assertions $assertions, + private ?Type $selfOutType, + ?string $phpDocComment, + array $parameterOutTypes, + array $immediatelyInvokedCallableParameters, + array $phpDocClosureThisTypeParameters, + private bool $isConstructor, + array $attributes, ) { + if ($this->classMethod instanceof Node\PropertyHook) { + if ($this->hookForProperty === null) { + throw new ShouldNotHappenException('Hook was provided but property was not'); + } + } elseif ($this->hookForProperty !== null) { + throw new ShouldNotHappenException('Hooked property was provided but hook was not'); + } + $name = strtolower($classMethod->name->name); - if ( - $name === '__construct' - || $name === '__destruct' - || $name === '__unset' - || $name === '__wakeup' - || $name === '__clone' - ) { + if ($this->isConstructor) { + $realReturnType = new VoidType(); + } + if (in_array($name, ['__destruct', '__unset', '__wakeup', '__clone'], true)) { $realReturnType = new VoidType(); } if ($name === '__tostring') { @@ -67,6 +97,22 @@ public function __construct( if ($name === '__set_state') { $realReturnType = TypeCombinator::intersect(new ObjectWithoutClassType(), $realReturnType); } + if ($name === '__set') { + $realReturnType = new VoidType(); + } + + if ($name === '__debuginfo') { + $realReturnType = TypeCombinator::intersect(TypeCombinator::addNull( + new ArrayType(new MixedType(true), new MixedType(true)), + ), $realReturnType); + } + + if ($name === '__unserialize') { + $realReturnType = new VoidType(); + } + if ($name === '__serialize') { + $realReturnType = new ArrayType(new MixedType(true), new MixedType(true)); + } parent::__construct( $classMethod, @@ -75,14 +121,21 @@ public function __construct( $realParameterTypes, $phpDocParameterTypes, $realParameterDefaultValues, + $parameterAttributes, $realReturnType, $phpDocReturnType, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal || $classMethod->isFinal(), $isPure, + $acceptsNamedArguments, + $assertions, + $phpDocComment, + $parameterOutTypes, + $immediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, + $attributes, ); } @@ -100,31 +153,102 @@ public function getPrototype(): ClassMemberReflection } } - private function getClassMethod(): ClassMethod + private function getClassMethod(): ClassMethod|Node\PropertyHook { - /** @var Node\Stmt\ClassMethod $functionLike */ + /** @var Node\Stmt\ClassMethod|Node\PropertyHook $functionLike */ $functionLike = $this->getFunctionLike(); return $functionLike; } + public function getName(): string + { + $function = $this->getFunctionLike(); + if (!$function instanceof Node\PropertyHook) { + return parent::getName(); + } + + if ($this->hookForProperty === null) { + throw new ShouldNotHappenException('Hook was provided but property was not'); + } + + return sprintf('$%s::%s', $this->hookForProperty, $function->name->toString()); + } + + /** + * @phpstan-assert-if-true !null $this->getHookedPropertyName() + * @phpstan-assert-if-true !null $this->getPropertyHookName() + */ + public function isPropertyHook(): bool + { + return $this->hookForProperty !== null; + } + + public function getHookedPropertyName(): ?string + { + return $this->hookForProperty; + } + + /** + * @return 'get'|'set'|null + */ + public function getPropertyHookName(): ?string + { + $function = $this->getFunctionLike(); + if (!$function instanceof Node\PropertyHook) { + return null; + } + + $name = $function->name->toLowerString(); + if (!in_array($name, ['get', 'set'], true)) { + throw new ShouldNotHappenException(sprintf('Unknown property hook: %s', $name)); + } + + return $name; + } + public function isStatic(): bool { - return $this->getClassMethod()->isStatic(); + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return false; + } + + return $method->isStatic(); } public function isPrivate(): bool { - return $this->getClassMethod()->isPrivate(); + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return false; + } + + return $method->isPrivate(); } public function isPublic(): bool { - return $this->getClassMethod()->isPublic(); + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return true; + } + + return $method->isPublic(); + } + + public function isFinal(): TrinaryLogic + { + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return TrinaryLogic::createFromBoolean($method->isFinal()); + } + + return TrinaryLogic::createFromBoolean($method->isFinal() || $this->isFinal); } - public function getDocComment(): ?string + public function isFinalByKeyword(): TrinaryLogic { - return null; + return TrinaryLogic::createFromBoolean($this->getClassMethod()->isFinal()); } public function isBuiltin(): bool @@ -132,4 +256,44 @@ public function isBuiltin(): bool return false; } + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getClassMethod()->returnsByRef()); + } + + public function isAbstract(): TrinaryLogic + { + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return TrinaryLogic::createFromBoolean($method->body === null); + } + + return TrinaryLogic::createFromBoolean($method->isAbstract()); + } + + public function isConstructor(): bool + { + return $this->isConstructor; + } + + public function hasSideEffects(): TrinaryLogic + { + if ( + strtolower($this->getName()) !== '__construct' + && $this->getReturnType()->isVoid()->yes() + ) { + return TrinaryLogic::createYes(); + } + if ($this->isPure !== null) { + return TrinaryLogic::createFromBoolean(!$this->isPure); + } + + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 563793f82b..b141bcec7e 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -2,22 +2,23 @@ namespace PHPStan\Reflection\Php; -use PhpParser\Node; -use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Stmt\Declare_; -use PhpParser\Node\Stmt\Function_; -use PhpParser\Node\Stmt\Namespace_; -use PHPStan\Cache\Cache; -use PHPStan\Parser\FunctionCallStatementFinder; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; +use PHPStan\Internal\DeprecatedAttributeHelper; use PHPStan\Parser\Parser; +use PHPStan\Parser\VariadicMethodsVisitor; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\AttributeReflectionFactory; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodPrototypeReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; @@ -27,46 +28,54 @@ use PHPStan\Type\MixedType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VoidType; use ReflectionException; -use ReflectionParameter; +use function array_key_exists; use function array_map; +use function count; use function explode; -use function filemtime; -use function is_bool; -use function is_file; +use function in_array; +use function is_array; use function sprintf; use function strtolower; -use function time; use const PHP_VERSION_ID; -/** @api */ -class PhpMethodReflection implements MethodReflection +/** + * @api + */ +final class PhpMethodReflection implements ExtendedMethodReflection { - /** @var PhpParameterReflection[]|null */ + /** @var list|null */ private ?array $parameters = null; private ?Type $returnType = null; private ?Type $nativeReturnType = null; - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; + private ?bool $containsVariadicCalls = null; + /** * @param Type[] $phpDocParameterTypes + * @param Type[] $phpDocParameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters + * @param list $attributes */ public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, private ClassReflection $declaringClass, private ?ClassReflection $declaringTrait, - private BuiltinMethodReflection $reflection, + private ReflectionMethod $reflection, private ReflectionProvider $reflectionProvider, + private AttributeReflectionFactory $attributeReflectionFactory, private Parser $parser, - private FunctionCallStatementFinder $functionCallStatementFinder, - private Cache $cache, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -76,6 +85,14 @@ public function __construct( private bool $isInternal, private bool $isFinal, private ?bool $isPure, + private Assertions $asserts, + private bool $acceptsNamedArguments, + private ?Type $selfOutType, + private ?string $phpDocComment, + private array $phpDocParameterOutTypes, + private array $immediatelyInvokedCallableParameters, + private array $phpDocClosureThisTypeParameters, + private array $attributes, ) { } @@ -90,11 +107,6 @@ public function getDeclaringTrait(): ?ClassReflection return $this->declaringTrait; } - public function getDocComment(): ?string - { - return $this->reflection->getDocComment(); - } - /** * @return self|MethodPrototypeReflection */ @@ -102,11 +114,18 @@ public function getPrototype(): ClassMemberReflection { try { $prototypeMethod = $this->reflection->getPrototype(); - $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + $prototypeDeclaringClass = $this->declaringClass->getAncestorWithClassName($prototypeMethod->getDeclaringClass()->getName()); + if ($prototypeDeclaringClass === null) { + $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + } + + if (!$prototypeDeclaringClass->hasNativeMethod($prototypeMethod->getName())) { + return $this; + } $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { - $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); + $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType(), null, $prototypeDeclaringClass); } return new MethodPrototypeReflection( @@ -117,6 +136,7 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), + $prototypeMethod->isInternal(), $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), $tentativeReturnType, ); @@ -171,13 +191,13 @@ private function getMethodNameWithCorrectCase(string $lowercaseMethodName, strin } /** - * @return ParametersAcceptorWithPhpDocs[] + * @return list */ public function getVariants(): array { if ($this->variants === null) { $this->variants = [ - new FunctionVariantWithPhpDocs( + new ExtendedFunctionVariant( $this->templateTypeMap, null, $this->getParameters(), @@ -192,16 +212,31 @@ public function getVariants(): array return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + /** - * @return ParameterReflectionWithPhpDocs[] + * @return list */ private function getParameters(): array { if ($this->parameters === null) { $this->parameters = array_map(fn (ReflectionParameter $reflection): PhpParameterReflection => new PhpParameterReflection( + $this->initializerExprTypeResolver, $reflection, $this->phpDocParameterTypes[$reflection->getName()] ?? null, - $this->getDeclaringClass()->getName(), + $this->getDeclaringClass(), + $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, + $this->immediatelyInvokedCallableParameters[$reflection->getName()] ?? TrinaryLogic::createMaybe(), + $this->phpDocClosureThisTypeParameters[$reflection->getName()] ?? null, + $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), ), $this->reflection->getParameters()); } @@ -218,82 +253,40 @@ private function isVariadic(): bool $filename = $this->declaringTrait->getFileName(); } - if (!$isNativelyVariadic && $filename !== null && is_file($filename)) { - $modifiedTime = filemtime($filename); - if ($modifiedTime === false) { - $modifiedTime = time(); + if (!$isNativelyVariadic && $filename !== null && !$this->declaringClass->isBuiltin()) { + if ($this->containsVariadicCalls !== null) { + return $this->containsVariadicCalls; } - $key = sprintf('variadic-method-%s-%s-%s', $declaringClass->getName(), $this->reflection->getName(), $filename); - $variableCacheKey = sprintf('%d-v4', $modifiedTime); - $cachedResult = $this->cache->load($key, $variableCacheKey); - if ($cachedResult === null || !is_bool($cachedResult)) { - $nodes = $this->parser->parseFile($filename); - $result = $this->callsFuncGetArgs($declaringClass, $nodes); - $this->cache->save($key, $variableCacheKey, $result); - return $result; - } - - return $cachedResult; - } - return $isNativelyVariadic; - } - - /** - * @param Node[] $nodes - */ - private function callsFuncGetArgs(ClassReflection $declaringClass, array $nodes): bool - { - foreach ($nodes as $node) { - if ( - $node instanceof Node\Stmt\ClassLike - ) { - if (!isset($node->namespacedName)) { - continue; - } - if ($declaringClass->getName() !== (string) $node->namespacedName) { - continue; - } - if ($this->callsFuncGetArgs($declaringClass, $node->stmts)) { - return true; - } - continue; + $className = $declaringClass->getName(); + if ($declaringClass->isAnonymous()) { + $className = sprintf('%s:%s:%s', VariadicMethodsVisitor::ANONYMOUS_CLASS_PREFIX, $declaringClass->getNativeReflection()->getStartLine(), $declaringClass->getNativeReflection()->getEndLine()); } - - if ($node instanceof ClassMethod) { - if ($node->getStmts() === null) { - continue; // interface - } - - $methodName = $node->name->name; - if ($methodName === $this->reflection->getName()) { - return $this->functionCallStatementFinder->findFunctionCallInStatements(ParametersAcceptor::VARIADIC_FUNCTIONS, $node->getStmts()) !== null; + if (array_key_exists($className, VariadicMethodsVisitor::$cache)) { + if (array_key_exists($this->reflection->getName(), VariadicMethodsVisitor::$cache[$className])) { + return $this->containsVariadicCalls = VariadicMethodsVisitor::$cache[$className][$this->reflection->getName()]; } - continue; + return $this->containsVariadicCalls = false; } - if ($node instanceof Function_) { - continue; - } + $nodes = $this->parser->parseFile($filename); + if (count($nodes) > 0) { + $variadicMethods = $nodes[0]->getAttribute(VariadicMethodsVisitor::ATTRIBUTE_NAME); - if ($node instanceof Namespace_) { - if ($this->callsFuncGetArgs($declaringClass, $node->stmts)) { - return true; + if ( + is_array($variadicMethods) + && array_key_exists($className, $variadicMethods) + && array_key_exists($this->reflection->getName(), $variadicMethods[$className]) + ) { + return $this->containsVariadicCalls = $variadicMethods[$className][$this->reflection->getName()]; } - continue; - } - - if (!$node instanceof Declare_ || $node->stmts === null) { - continue; } - if ($this->callsFuncGetArgs($declaringClass, $node->stmts)) { - return true; - } + return $this->containsVariadicCalls = false; } - return false; + return $isNativelyVariadic; } public function isPrivate(): bool @@ -309,33 +302,9 @@ public function isPublic(): bool private function getReturnType(): Type { if ($this->returnType === null) { - $name = strtolower($this->getName()); - if ( - $name === '__construct' - || $name === '__destruct' - || $name === '__unset' - || $name === '__wakeup' - || $name === '__clone' - ) { - return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType); - } - if ($name === '__tostring') { - return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType); - } - if ($name === '__isset') { - return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType); - } - if ($name === '__sleep') { - return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType); - } - if ($name === '__set_state') { - return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType); - } - - $this->returnType = TypehintHelper::decideTypeFromReflection( - $this->reflection->getReturnType(), + $this->returnType = TypehintHelper::decideType( + $this->getNativeReturnType(), $this->phpDocReturnType, - $this->declaringClass->getName(), ); } @@ -354,10 +323,30 @@ private function getPhpDocReturnType(): Type private function getNativeReturnType(): Type { if ($this->nativeReturnType === null) { + $returnType = $this->reflection->getReturnType(); + if ($returnType === null) { + $name = strtolower($this->getName()); + if (in_array($this->getName(), ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) { + return $this->nativeReturnType = new VoidType(); + } + if ($name === '__tostring') { + return $this->nativeReturnType = new StringType(); + } + if ($name === '__isset') { + return $this->nativeReturnType = new BooleanType(); + } + if ($name === '__sleep') { + return $this->nativeReturnType = new ArrayType(new IntegerType(), new StringType()); + } + if ($name === '__set_state') { + return $this->nativeReturnType = new ObjectWithoutClassType(); + } + } + $this->nativeReturnType = TypehintHelper::decideTypeFromReflection( - $this->reflection->getReturnType(), + $returnType, null, - $this->declaringClass->getName(), + $this->declaringClass, ); } @@ -370,22 +359,41 @@ public function getDeprecatedDescription(): ?string return $this->deprecatedDescription; } + if ($this->reflection->isDeprecated()) { + $attributes = $this->reflection->getBetterReflection()->getAttributes(); + return DeprecatedAttributeHelper::getDeprecatedDescription($attributes); + } + return null; } public function isDeprecated(): TrinaryLogic { - return $this->reflection->isDeprecated()->or(TrinaryLogic::createFromBoolean($this->isDeprecated)); + if ($this->isDeprecated) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createFromBoolean($this->reflection->isDeprecated()); } public function isInternal(): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->reflection->isInternal() || $this->isInternal); + return TrinaryLogic::createFromBoolean($this->isInternal); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isInternal()); } public function isFinal(): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->reflection->isFinal() || $this->isFinal); + return TrinaryLogic::createFromBoolean($this->isFinal || $this->reflection->isFinal()); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isFinal()); } public function isAbstract(): bool @@ -400,12 +408,9 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - $name = strtolower($this->getName()); - $isVoid = $this->getReturnType() instanceof VoidType; - if ( - $name !== '__construct' - && $isVoid + strtolower($this->getName()) !== '__construct' + && $this->getReturnType()->isVoid()->yes() ) { return TrinaryLogic::createYes(); } @@ -413,11 +418,115 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createFromBoolean(!$this->isPure); } - if ($isVoid) { + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->getReturnType())->yes()) { return TrinaryLogic::createYes(); } return TrinaryLogic::createMaybe(); } + public function getAsserts(): Assertions + { + return $this->asserts; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean( + $this->declaringClass->acceptsNamedArguments() && $this->acceptsNamedArguments, + ); + } + + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); + } + + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + + public function changePropertyGetHookPhpDocType(Type $phpDocType): self + { + return new self( + $this->initializerExprTypeResolver, + $this->declaringClass, + $this->declaringTrait, + $this->reflection, + $this->reflectionProvider, + $this->attributeReflectionFactory, + $this->parser, + $this->templateTypeMap, + $this->phpDocParameterTypes, + $phpDocType, + $this->phpDocThrowType, + $this->deprecatedDescription, + $this->isDeprecated, + $this->isInternal, + $this->isFinal, + $this->isPure, + $this->asserts, + $this->acceptsNamedArguments, + $this->selfOutType, + $this->phpDocComment, + $this->phpDocParameterOutTypes, + $this->immediatelyInvokedCallableParameters, + $this->phpDocClosureThisTypeParameters, + $this->attributes, + ); + } + + public function changePropertySetHookPhpDocType(string $parameterName, Type $phpDocType): self + { + $phpDocParameterTypes = $this->phpDocParameterTypes; + $phpDocParameterTypes[$parameterName] = $phpDocType; + + return new self( + $this->initializerExprTypeResolver, + $this->declaringClass, + $this->declaringTrait, + $this->reflection, + $this->reflectionProvider, + $this->attributeReflectionFactory, + $this->parser, + $this->templateTypeMap, + $phpDocParameterTypes, + $this->phpDocReturnType, + $this->phpDocThrowType, + $this->deprecatedDescription, + $this->isDeprecated, + $this->isInternal, + $this->isFinal, + $this->isPure, + $this->asserts, + $this->acceptsNamedArguments, + $this->selfOutType, + $this->phpDocComment, + $this->phpDocParameterOutTypes, + $this->immediatelyInvokedCallableParameters, + $this->phpDocClosureThisTypeParameters, + $this->attributes, + ); + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index 8b56043ac5..ec95a2de81 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -2,7 +2,11 @@ namespace PHPStan\Reflection\Php; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassReflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; @@ -11,12 +15,15 @@ interface PhpMethodReflectionFactory /** * @param Type[] $phpDocParameterTypes - * + * @param Type[] $phpDocParameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters + * @param list $attributes */ public function create( ClassReflection $declaringClass, ?ClassReflection $declaringTrait, - BuiltinMethodReflection $reflection, + ReflectionMethod $reflection, TemplateTypeMap $templateTypeMap, array $phpDocParameterTypes, ?Type $phpDocReturnType, @@ -25,7 +32,15 @@ public function create( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure = null, + ?bool $isPure, + Assertions $asserts, + ?Type $selfOutType, + ?string $phpDocComment, + array $phpDocParameterOutTypes, + array $immediatelyInvokedCallableParameters, + array $phpDocClosureThisTypeParameters, + bool $acceptsNamedArguments, + array $attributes, ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index 0625921242..f048ea7100 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -2,19 +2,23 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; -class PhpParameterFromParserNodeReflection implements ParameterReflectionWithPhpDocs +final class PhpParameterFromParserNodeReflection implements ExtendedParameterReflection { private ?Type $type = null; + /** + * @param list $attributes + */ public function __construct( private string $name, private bool $optional, @@ -23,6 +27,10 @@ public function __construct( private PassedByReference $passedByReference, private ?Type $defaultValue, private bool $variadic, + private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, + private array $attributes, ) { } @@ -42,7 +50,7 @@ public function getType(): Type if ($this->type === null) { $phpDocType = $this->phpDocType; if ($phpDocType !== null && $this->defaultValue !== null) { - if ($this->defaultValue instanceof NullType) { + if ($this->defaultValue->isNull()->yes()) { $inferred = $phpDocType->inferTemplateTypes($this->defaultValue); if ($inferred->isEmpty()) { $phpDocType = TypeCombinator::addNull($phpDocType); @@ -60,6 +68,11 @@ public function getPhpDocType(): Type return $this->phpDocType ?? new MixedType(); } + public function hasNativeType(): bool + { + return !$this->realType instanceof MixedType || $this->realType->isExplicitMixed(); + } + public function getNativeType(): Type { return $this->realType; @@ -80,4 +93,24 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function getOutType(): ?Type + { + return $this->outType; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index 35dc2e8216..01f69e3ccb 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -2,27 +2,38 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\PassedByReference; -use PHPStan\Type\ConstantTypeHelper; +use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; -use ReflectionParameter; -use Throwable; -class PhpParameterReflection implements ParameterReflectionWithPhpDocs +final class PhpParameterReflection implements ExtendedParameterReflection { private ?Type $type = null; private ?Type $nativeType = null; + /** + * @param list $attributes + */ public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, private ReflectionParameter $reflection, private ?Type $phpDocType, - private ?string $declaringClassName, + private ?ClassReflection $declaringClass, + private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, + private array $attributes, ) { } @@ -41,23 +52,23 @@ public function getType(): Type { if ($this->type === null) { $phpDocType = $this->phpDocType; - if ($phpDocType !== null) { - try { - if ( - $this->reflection->isDefaultValueAvailable() - && $this->reflection->getDefaultValue() === null - ) { - $phpDocType = TypeCombinator::addNull($phpDocType); - } - } catch (Throwable) { - // pass + if ( + $phpDocType !== null + && $this->reflection->isDefaultValueAvailable() + ) { + $defaultValueType = $this->initializerExprTypeResolver->getType( + $this->reflection->getDefaultValueExpression(), + InitializerExprContext::fromReflectionParameter($this->reflection), + ); + if ($defaultValueType->isNull()->yes()) { + $phpDocType = TypeCombinator::addNull($phpDocType); } } $this->type = TypehintHelper::decideTypeFromReflection( $this->reflection->getType(), $phpDocType, - $this->declaringClassName, + $this->declaringClass, $this->isVariadic(), ); } @@ -86,13 +97,18 @@ public function getPhpDocType(): Type return new MixedType(); } + public function hasNativeType(): bool + { + return $this->reflection->getType() !== null; + } + public function getNativeType(): Type { if ($this->nativeType === null) { $this->nativeType = TypehintHelper::decideTypeFromReflection( $this->reflection->getType(), null, - $this->declaringClassName, + $this->declaringClass, $this->isVariadic(), ); } @@ -102,16 +118,34 @@ public function getNativeType(): Type public function getDefaultValue(): ?Type { - try { - if ($this->reflection->isDefaultValueAvailable()) { - $defaultValue = $this->reflection->getDefaultValue(); - return ConstantTypeHelper::getTypeFromValue($defaultValue); - } - } catch (Throwable) { - return null; + if ($this->reflection->isDefaultValueAvailable()) { + return $this->initializerExprTypeResolver->getType( + $this->reflection->getDefaultValueExpression(), + InitializerExprContext::fromReflectionParameter($this->reflection), + ); } return null; } + public function getOutType(): ?Type + { + return $this->outType; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/PhpPropertyReflection.php b/src/Reflection/Php/PhpPropertyReflection.php index 3e0fab20c1..8307f36d7b 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -2,37 +2,57 @@ namespace PHPStan\Reflection\Php; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionIntersectionType; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionUnionType; +use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use ReflectionProperty; -use ReflectionType; -use function method_exists; +use function sprintf; -/** @api */ -class PhpPropertyReflection implements PropertyReflection +/** + * @api + */ +final class PhpPropertyReflection implements ExtendedPropertyReflection { private ?Type $finalNativeType = null; private ?Type $type = null; + /** + * @param list $attributes + */ public function __construct( private ClassReflection $declaringClass, private ?ClassReflection $declaringTrait, - private ?ReflectionType $nativeType, + private ReflectionUnionType|ReflectionNamedType|ReflectionIntersectionType|null $nativeType, private ?Type $phpDocType, private ReflectionProperty $reflection, + private ?ExtendedMethodReflection $getHook, + private ?ExtendedMethodReflection $setHook, private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, + private bool $isReadOnlyByPhpDoc, + private bool $isAllowedPrivateMutation, + private array $attributes, ) { } + public function getName(): string + { + return $this->reflection->getName(); + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -70,11 +90,12 @@ public function isPublic(): bool public function isReadOnly(): bool { - if (method_exists($this->reflection, 'isReadOnly')) { - return $this->reflection->isReadOnly(); - } + return $this->reflection->isReadOnly(); + } - return false; + public function isReadOnlyByPhpDoc(): bool + { + return $this->isReadOnlyByPhpDoc; } public function getReadableType(): Type @@ -83,7 +104,7 @@ public function getReadableType(): Type $this->type = TypehintHelper::decideTypeFromReflection( $this->nativeType, $this->phpDocType, - $this->declaringClass->getName(), + $this->declaringClass, ); } @@ -92,20 +113,40 @@ public function getReadableType(): Type public function getWritableType(): Type { + if ($this->hasHook('set')) { + $setHookVariant = $this->getHook('set')->getOnlyVariant(); + $parameters = $setHookVariant->getParameters(); + if (isset($parameters[0])) { + return $parameters[0]->getType(); + } + } + return $this->getReadableType(); } public function canChangeTypeAfterAssignment(): bool { + if ($this->isStatic()) { + return true; + } + + if ($this->isVirtual()->yes()) { + return false; + } + + if ($this->hasHook('get')) { + return false; + } + + if ($this->hasHook('set')) { + return false; + } + return true; } public function isPromoted(): bool { - if (!method_exists($this->reflection, 'isPromoted')) { - return false; - } - return $this->reflection->isPromoted(); } @@ -134,7 +175,7 @@ public function getNativeType(): Type $this->finalNativeType = TypehintHelper::decideTypeFromReflection( $this->nativeType, null, - $this->declaringClass->getName(), + $this->declaringClass, ); } @@ -143,12 +184,28 @@ public function getNativeType(): Type public function isReadable(): bool { - return true; + if ($this->isStatic()) { + return true; + } + + if (!$this->isVirtual()->yes()) { + return true; + } + + return $this->hasHook('get'); } public function isWritable(): bool { - return true; + if ($this->isStatic()) { + return true; + } + + if (!$this->isVirtual()->yes()) { + return true; + } + + return $this->hasHook('set'); } public function getDeprecatedDescription(): ?string @@ -170,9 +227,75 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isInternal); } + public function isAllowedPrivateMutation(): bool + { + return $this->isAllowedPrivateMutation; + } + public function getNativeReflection(): ReflectionProperty { return $this->reflection; } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isAbstract()); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isFinal()); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isVirtual()); + } + + public function hasHook(string $hookType): bool + { + if ($hookType === 'get') { + return $this->getHook !== null; + } + + return $this->setHook !== null; + } + + public function isHooked(): bool + { + return $this->getHook !== null || $this->setHook !== null; + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + if ($hookType === 'get') { + if ($this->getHook === null) { + throw new MissingMethodFromReflectionException($this->declaringClass->getName(), sprintf('$%s::get', $this->reflection->getName())); + } + + return $this->getHook; + } + + if ($this->setHook === null) { + throw new MissingMethodFromReflectionException($this->declaringClass->getName(), sprintf('$%s::set', $this->reflection->getName())); + } + + return $this->setHook; + } + + public function isProtectedSet(): bool + { + return $this->reflection->isProtectedSet(); + } + + public function isPrivateSet(): bool + { + return $this->reflection->isPrivateSet(); + } + + public function getAttributes(): array + { + return $this->attributes; + } + } diff --git a/src/Reflection/Php/SimpleXMLElementProperty.php b/src/Reflection/Php/SimpleXMLElementProperty.php index 9668d523be..29975a5e3f 100644 --- a/src/Reflection/Php/SimpleXMLElementProperty.php +++ b/src/Reflection/Php/SimpleXMLElementProperty.php @@ -3,25 +3,34 @@ namespace PHPStan\Reflection\Php; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\BooleanType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class SimpleXMLElementProperty implements PropertyReflection +final class SimpleXMLElementProperty implements ExtendedPropertyReflection { public function __construct( + private string $name, private ClassReflection $declaringClass, private Type $type, ) { } + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -42,6 +51,26 @@ public function isPublic(): bool return true; } + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): Type + { + return new MixedType(); + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + public function getReadableType(): Type { return $this->type; @@ -93,4 +122,44 @@ public function getDocComment(): ?string return null; } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function hasHook(string $hookType): bool + { + return false; + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + throw new ShouldNotHappenException(); + } + + public function isProtectedSet(): bool + { + return false; + } + + public function isPrivateSet(): bool + { + return false; + } + + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Php/Soap/SoapClientMethodReflection.php b/src/Reflection/Php/Soap/SoapClientMethodReflection.php index 15a814b20a..1017888c59 100644 --- a/src/Reflection/Php/Soap/SoapClientMethodReflection.php +++ b/src/Reflection/Php/Soap/SoapClientMethodReflection.php @@ -12,7 +12,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -class SoapClientMethodReflection implements MethodReflection +final class SoapClientMethodReflection implements MethodReflection { public function __construct(private ClassReflection $declaringClass, private string $name) @@ -87,7 +87,7 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } - public function getThrowType(): ?Type + public function getThrowType(): Type { return new ObjectType('SoapFault'); } diff --git a/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php b/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php index 73924b6081..431026f938 100644 --- a/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php +++ b/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php @@ -6,12 +6,12 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; -class SoapClientMethodsClassReflectionExtension implements MethodsClassReflectionExtension +final class SoapClientMethodsClassReflectionExtension implements MethodsClassReflectionExtension { public function hasMethod(ClassReflection $classReflection, string $methodName): bool { - return $classReflection->getName() === 'SoapClient' || $classReflection->isSubclassOf('SoapClient'); + return $classReflection->is('SoapClient'); } public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection diff --git a/src/Reflection/Php/UniversalObjectCrateProperty.php b/src/Reflection/Php/UniversalObjectCrateProperty.php index 3d766f5fee..564613e219 100644 --- a/src/Reflection/Php/UniversalObjectCrateProperty.php +++ b/src/Reflection/Php/UniversalObjectCrateProperty.php @@ -3,14 +3,18 @@ namespace PHPStan\Reflection\Php; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class UniversalObjectCrateProperty implements PropertyReflection +final class UniversalObjectCrateProperty implements ExtendedPropertyReflection { public function __construct( + private string $name, private ClassReflection $declaringClass, private Type $readableType, private Type $writableType, @@ -18,6 +22,11 @@ public function __construct( { } + public function getName(): string + { + return $this->name; + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; @@ -38,6 +47,26 @@ public function isPublic(): bool return true; } + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): Type + { + return new MixedType(); + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + public function getReadableType(): Type { return $this->readableType; @@ -83,4 +112,44 @@ public function getDocComment(): ?string return null; } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function hasHook(string $hookType): bool + { + return false; + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + throw new ShouldNotHappenException(); + } + + public function isProtectedSet(): bool + { + return false; + } + + public function isPrivateSet(): bool + { + return false; + } + + public function getAttributes(): array + { + return []; + } + } diff --git a/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php b/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php index 6260882e02..0cd79eb232 100644 --- a/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php +++ b/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php @@ -2,37 +2,53 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; 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 +final class UniversalObjectCratesClassReflectionExtension implements PropertiesClassReflectionExtension { /** - * @param string[] $classes + * @param list $classes */ - public function __construct(private ReflectionProvider $reflectionProvider, private array $classes) + public function __construct( + private ReflectionProvider $reflectionProvider, + private array $classes, + private AnnotationsPropertiesClassReflectionExtension $annotationClassReflection, + ) { } public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - return self::isUniversalObjectCrate( + return self::isUniversalObjectCrateImplementation( $this->reflectionProvider, $this->classes, $classReflection, ); } + public static function isUniversalObjectCrate( + ReflectionProvider $reflectionProvider, + ClassReflection $classReflection, + ): bool + { + return self::isUniversalObjectCrateImplementation( + $reflectionProvider, + $reflectionProvider->getUniversalObjectCratesClasses(), + $classReflection, + ); + } + /** - * @param string[] $classes + * @param list $classes */ - public static function isUniversalObjectCrate( + private static function isUniversalObjectCrateImplementation( ReflectionProvider $reflectionProvider, array $classes, ClassReflection $classReflection, @@ -43,10 +59,7 @@ public static function isUniversalObjectCrate( continue; } - if ( - $classReflection->getName() === $className - || $classReflection->isSubclassOf($className) - ) { + if ($classReflection->is($className)) { return true; } } @@ -56,19 +69,23 @@ public static function isUniversalObjectCrate( public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { + if ($this->annotationClassReflection->hasProperty($classReflection, $propertyName)) { + return $this->annotationClassReflection->getProperty($classReflection, $propertyName); + } + if ($classReflection->hasNativeMethod('__get')) { - $readableType = ParametersAcceptorSelector::selectSingle($classReflection->getNativeMethod('__get')->getVariants())->getReturnType(); + $readableType = $classReflection->getNativeMethod('__get')->getOnlyVariant()->getReturnType(); } else { $readableType = new MixedType(); } if ($classReflection->hasNativeMethod('__set')) { - $writableType = ParametersAcceptorSelector::selectSingle($classReflection->getNativeMethod('__set')->getVariants())->getParameters()[1]->getType(); + $writableType = $classReflection->getNativeMethod('__set')->getOnlyVariant()->getParameters()[1]->getType(); } else { $writableType = new MixedType(); } - return new UniversalObjectCrateProperty($classReflection, $readableType, $writableType); + return new UniversalObjectCrateProperty($propertyName, $classReflection, $readableType, $writableType); } } diff --git a/src/Reflection/PhpVersionStaticAccessor.php b/src/Reflection/PhpVersionStaticAccessor.php new file mode 100644 index 0000000000..363109b7c0 --- /dev/null +++ b/src/Reflection/PhpVersionStaticAccessor.php @@ -0,0 +1,30 @@ + $attributes + */ + public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ClassReflection $declaringClass, + private ReflectionClassConstant $reflection, + private ?Type $nativeType, + private ?Type $phpDocType, + private ?string $deprecatedDescription, + private bool $isDeprecated, + private bool $isInternal, + private bool $isFinal, + private array $attributes, + ) + { + } + + public function getName(): string + { + return $this->reflection->getName(); + } + + public function getFileName(): ?string + { + return $this->declaringClass->getFileName(); + } + + public function getValueExpr(): Expr + { + return $this->reflection->getValueExpression(); + } + + public function hasPhpDocType(): bool + { + return $this->phpDocType !== null; + } + + public function getPhpDocType(): ?Type + { + return $this->phpDocType; + } + + public function hasNativeType(): bool + { + return $this->nativeType !== null; + } + + public function getNativeType(): ?Type + { + return $this->nativeType; + } + + public function getValueType(): Type + { + if ($this->valueType === null) { + if ($this->phpDocType !== null) { + if ($this->nativeType !== null) { + return $this->valueType = TypehintHelper::decideType( + $this->nativeType, + $this->phpDocType, + ); + } + + return $this->phpDocType; + } elseif ($this->nativeType !== null) { + return $this->nativeType; + } + + $this->valueType = $this->initializerExprTypeResolver->getType($this->getValueExpr(), InitializerExprContext::fromClassReflection($this->declaringClass)); + } + + return $this->valueType; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + public function isStatic(): bool + { + return true; + } + + public function isPrivate(): bool + { + return $this->reflection->isPrivate(); + } + + public function isPublic(): bool + { + return $this->reflection->isPublic(); + } + + public function isFinal(): bool + { + return $this->isFinal || $this->reflection->isFinal(); + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->isDeprecated || $this->reflection->isDeprecated()); + } + + public function getDeprecatedDescription(): ?string + { + if ($this->isDeprecated) { + return $this->deprecatedDescription; + } + + if ($this->reflection->isDeprecated()) { + $attributes = $this->reflection->getBetterReflection()->getAttributes(); + return DeprecatedAttributeHelper::getDeprecatedDescription($attributes); + } + + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->isInternal); + } + + public function getDocComment(): ?string + { + $docComment = $this->reflection->getDocComment(); + if ($docComment === false) { + return null; + } + + return $docComment; + } + + public function getAttributes(): array + { + return $this->attributes; + } + +} diff --git a/src/Reflection/ReflectionProvider.php b/src/Reflection/ReflectionProvider.php index a41202567b..3b7387f85a 100644 --- a/src/Reflection/ReflectionProvider.php +++ b/src/Reflection/ReflectionProvider.php @@ -9,29 +9,31 @@ interface ReflectionProvider { + /** @phpstan-assert-if-true =class-string $className */ public function hasClass(string $className): bool; public function getClass(string $className): ClassReflection; public function getClassName(string $className): string; - public function supportsAnonymousClasses(): bool; - public function getAnonymousClassReflection( Node\Stmt\Class_ $classNode, Scope $scope, ): ClassReflection; - public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool; + /** @return list */ + public function getUniversalObjectCratesClasses(): array; + + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool; - public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection; + public function getFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): FunctionReflection; - public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string; + public function resolveFunctionName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string; - public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool; + public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool; - public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection; + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection; - public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string; + public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string; } diff --git a/src/Reflection/ReflectionProvider/ChainReflectionProvider.php b/src/Reflection/ReflectionProvider/ChainReflectionProvider.php deleted file mode 100644 index f8b1183c68..0000000000 --- a/src/Reflection/ReflectionProvider/ChainReflectionProvider.php +++ /dev/null @@ -1,173 +0,0 @@ -providers as $provider) { - if (!$provider->hasClass($className)) { - continue; - } - - return true; - } - - return false; - } - - public function getClass(string $className): ClassReflection - { - foreach ($this->providers as $provider) { - if (!$provider->hasClass($className)) { - continue; - } - - return $provider->getClass($className); - } - - throw new ClassNotFoundException($className); - } - - public function getClassName(string $className): string - { - foreach ($this->providers as $provider) { - if (!$provider->hasClass($className)) { - continue; - } - - return $provider->getClassName($className); - } - - throw new ClassNotFoundException($className); - } - - public function supportsAnonymousClasses(): bool - { - foreach ($this->providers as $provider) { - if (!$provider->supportsAnonymousClasses()) { - continue; - } - - return true; - } - - return false; - } - - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - foreach ($this->providers as $provider) { - if (!$provider->supportsAnonymousClasses()) { - continue; - } - - return $provider->getAnonymousClassReflection($classNode, $scope); - } - - throw new ShouldNotHappenException(); - } - - public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool - { - foreach ($this->providers as $provider) { - if (!$provider->hasFunction($nameNode, $scope)) { - continue; - } - - return true; - } - - return false; - } - - public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection - { - foreach ($this->providers as $provider) { - if (!$provider->hasFunction($nameNode, $scope)) { - continue; - } - - return $provider->getFunction($nameNode, $scope); - } - - throw new FunctionNotFoundException((string) $nameNode); - } - - public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string - { - foreach ($this->providers as $provider) { - $resolvedName = $provider->resolveFunctionName($nameNode, $scope); - if ($resolvedName === null) { - continue; - } - - return $resolvedName; - } - - return null; - } - - public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool - { - foreach ($this->providers as $provider) { - if (!$provider->hasConstant($nameNode, $scope)) { - continue; - } - - return true; - } - - return false; - } - - public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - foreach ($this->providers as $provider) { - if (!$provider->hasConstant($nameNode, $scope)) { - continue; - } - - return $provider->getConstant($nameNode, $scope); - } - - throw new ConstantNotFoundException((string) $nameNode); - } - - public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string - { - foreach ($this->providers as $provider) { - $resolvedName = $provider->resolveConstantName($nameNode, $scope); - if ($resolvedName === null) { - continue; - } - - return $resolvedName; - } - - return null; - } - -} diff --git a/src/Reflection/ReflectionProvider/ClassBlacklistReflectionProvider.php b/src/Reflection/ReflectionProvider/ClassBlacklistReflectionProvider.php deleted file mode 100644 index 734b9f5636..0000000000 --- a/src/Reflection/ReflectionProvider/ClassBlacklistReflectionProvider.php +++ /dev/null @@ -1,157 +0,0 @@ -isClassBlacklisted($className)) { - return false; - } - - $has = $this->reflectionProvider->hasClass($className); - if (!$has) { - return false; - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ($this->singleReflectionInsteadOfFile !== null) { - if ($classReflection->getFileName() === $this->singleReflectionInsteadOfFile) { - return false; - } - } - - foreach ($classReflection->getParentClassesNames() as $parentClassName) { - if ($this->isClassBlacklisted($parentClassName)) { - return false; - } - } - - foreach ($classReflection->getNativeReflection()->getInterfaceNames() as $interfaceName) { - if ($this->isClassBlacklisted($interfaceName)) { - return false; - } - } - - return true; - } - - private function isClassBlacklisted(string $className): bool - { - if ($this->phpStormStubsSourceStubber->hasClass($className)) { - // check that userland class isn't aliased to the same name as a class from stubs - if (!class_exists($className, false)) { - return true; - } - $reflection = new ReflectionClass($className); - if ($reflection->getFileName() === false) { - return true; - } - } - - foreach ($this->patterns as $pattern) { - if (Strings::match($className, $pattern) !== null) { - return true; - } - } - - return false; - } - - public function getClass(string $className): ClassReflection - { - if (!$this->hasClass($className)) { - throw new ClassNotFoundException($className); - } - - return $this->reflectionProvider->getClass($className); - } - - public function getClassName(string $className): string - { - if (!$this->hasClass($className)) { - throw new ClassNotFoundException($className); - } - - return $this->reflectionProvider->getClassName($className); - } - - public function supportsAnonymousClasses(): bool - { - return false; - } - - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - throw new ShouldNotHappenException(); - } - - public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool - { - $has = $this->reflectionProvider->hasFunction($nameNode, $scope); - if (!$has) { - return false; - } - - if ($this->singleReflectionInsteadOfFile === null) { - return true; - } - - $functionReflection = $this->reflectionProvider->getFunction($nameNode, $scope); - - return $functionReflection->getFileName() !== $this->singleReflectionInsteadOfFile; - } - - public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection - { - return $this->reflectionProvider->getFunction($nameNode, $scope); - } - - public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveFunctionName($nameNode, $scope); - } - - public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasConstant($nameNode, $scope); - } - - public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - return $this->reflectionProvider->getConstant($nameNode, $scope); - } - - 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 e1c7e31e1f..2fccc5b3c4 100644 --- a/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php +++ b/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php @@ -4,7 +4,7 @@ use PHPStan\Reflection\ReflectionProvider; -class DirectReflectionProviderProvider implements ReflectionProviderProvider +final class DirectReflectionProviderProvider implements ReflectionProviderProvider { public function __construct(private ReflectionProvider $reflectionProvider) diff --git a/src/Reflection/ReflectionProvider/DummyReflectionProvider.php b/src/Reflection/ReflectionProvider/DummyReflectionProvider.php index 9af3f4539d..7d18639f8c 100644 --- a/src/Reflection/ReflectionProvider/DummyReflectionProvider.php +++ b/src/Reflection/ReflectionProvider/DummyReflectionProvider.php @@ -5,12 +5,13 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\GlobalConstantReflection; +use PHPStan\Reflection\NamespaceAnswerer; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; -class DummyReflectionProvider implements ReflectionProvider +final class DummyReflectionProvider implements ReflectionProvider { public function hasClass(string $className): bool @@ -28,42 +29,42 @@ public function getClassName(string $className): string return $className; } - public function supportsAnonymousClasses(): bool + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { - return false; + throw new ShouldNotHappenException(); } - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getUniversalObjectCratesClasses(): array { - throw new ShouldNotHappenException(); + return []; } - public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool { return false; } - public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection + public function getFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): FunctionReflection { throw new ShouldNotHappenException(); } - public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string + public function resolveFunctionName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string { return null; } - public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool + public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool { return false; } - public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection { throw new ShouldNotHappenException(); } - public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string + public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string { return null; } diff --git a/src/Reflection/ReflectionProvider/LazyReflectionProviderProvider.php b/src/Reflection/ReflectionProvider/LazyReflectionProviderProvider.php index 9e4b1957a0..240d3d3bc5 100644 --- a/src/Reflection/ReflectionProvider/LazyReflectionProviderProvider.php +++ b/src/Reflection/ReflectionProvider/LazyReflectionProviderProvider.php @@ -5,7 +5,7 @@ use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\ReflectionProvider; -class LazyReflectionProviderProvider implements ReflectionProviderProvider +final class LazyReflectionProviderProvider implements ReflectionProviderProvider { public function __construct(private Container $container) diff --git a/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php b/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php index 1cb6f44eb9..00f4301c7e 100644 --- a/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php +++ b/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php @@ -5,12 +5,13 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\GlobalConstantReflection; +use PHPStan\Reflection\NamespaceAnswerer; use PHPStan\Reflection\ReflectionProvider; use function strtolower; -class MemoizingReflectionProvider implements ReflectionProvider +final class MemoizingReflectionProvider implements ReflectionProvider { /** @var array */ @@ -55,44 +56,44 @@ public function getClassName(string $className): string return $this->classNames[$lowerClassName] = $this->provider->getClassName($className); } - public function supportsAnonymousClasses(): bool + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { - return $this->provider->supportsAnonymousClasses(); + return $this->provider->getAnonymousClassReflection($classNode, $scope); } - public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getUniversalObjectCratesClasses(): array { - return $this->provider->getAnonymousClassReflection($classNode, $scope); + return $this->provider->getUniversalObjectCratesClasses(); } - public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool { - return $this->provider->hasFunction($nameNode, $scope); + return $this->provider->hasFunction($nameNode, $namespaceAnswerer); } - public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection + public function getFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): FunctionReflection { - return $this->provider->getFunction($nameNode, $scope); + return $this->provider->getFunction($nameNode, $namespaceAnswerer); } - public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string + public function resolveFunctionName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string { - return $this->provider->resolveFunctionName($nameNode, $scope); + return $this->provider->resolveFunctionName($nameNode, $namespaceAnswerer); } - public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool + public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool { - return $this->provider->hasConstant($nameNode, $scope); + return $this->provider->hasConstant($nameNode, $namespaceAnswerer); } - public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection { - return $this->provider->getConstant($nameNode, $scope); + return $this->provider->getConstant($nameNode, $namespaceAnswerer); } - public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string + public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string { - return $this->provider->resolveConstantName($nameNode, $scope); + return $this->provider->resolveConstantName($nameNode, $namespaceAnswerer); } } diff --git a/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php b/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php index 9f54ac31b3..06441ef778 100644 --- a/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php +++ b/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php @@ -3,30 +3,19 @@ namespace PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProvider; -use function count; -class ReflectionProviderFactory +final class ReflectionProviderFactory { public function __construct( - private ReflectionProvider $runtimeReflectionProvider, private ReflectionProvider $staticReflectionProvider, - private bool $disableRuntimeReflectionProvider, ) { } public function create(): ReflectionProvider { - $providers = []; - - if (!$this->disableRuntimeReflectionProvider) { - $providers[] = $this->runtimeReflectionProvider; - } - - $providers[] = $this->staticReflectionProvider; - - return new MemoizingReflectionProvider(count($providers) === 1 ? $providers[0] : new ChainReflectionProvider($providers)); + return new MemoizingReflectionProvider($this->staticReflectionProvider); } } diff --git a/src/Reflection/ReflectionProvider/SetterReflectionProviderProvider.php b/src/Reflection/ReflectionProvider/SetterReflectionProviderProvider.php index 9e897084cb..2ae0d7c9ff 100644 --- a/src/Reflection/ReflectionProvider/SetterReflectionProviderProvider.php +++ b/src/Reflection/ReflectionProvider/SetterReflectionProviderProvider.php @@ -4,7 +4,7 @@ use PHPStan\Reflection\ReflectionProvider; -class SetterReflectionProviderProvider implements ReflectionProviderProvider +final class SetterReflectionProviderProvider implements ReflectionProviderProvider { private ReflectionProvider $reflectionProvider; diff --git a/src/Reflection/ReflectionProviderStaticAccessor.php b/src/Reflection/ReflectionProviderStaticAccessor.php index bb772064c7..90ad0dd185 100644 --- a/src/Reflection/ReflectionProviderStaticAccessor.php +++ b/src/Reflection/ReflectionProviderStaticAccessor.php @@ -4,7 +4,7 @@ use PHPStan\ShouldNotHappenException; -class ReflectionProviderStaticAccessor +final class ReflectionProviderStaticAccessor { private static ?ReflectionProvider $instance = null; diff --git a/src/Reflection/RequireExtension/RequireExtendsMethodsClassReflectionExtension.php b/src/Reflection/RequireExtension/RequireExtendsMethodsClassReflectionExtension.php new file mode 100644 index 0000000000..ee6ada6f6f --- /dev/null +++ b/src/Reflection/RequireExtension/RequireExtendsMethodsClassReflectionExtension.php @@ -0,0 +1,57 @@ +findMethod($classReflection, $methodName) !== null; + } + + public function getMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection + { + $method = $this->findMethod($classReflection, $methodName); + if ($method === null) { + throw new ShouldNotHappenException(); + } + + return $method; + } + + private function findMethod(ClassReflection $classReflection, string $methodName): ?ExtendedMethodReflection + { + if (!$classReflection->isInterface()) { + return null; + } + + $extendsTags = $classReflection->getRequireExtendsTags(); + foreach ($extendsTags as $extendsTag) { + $type = $extendsTag->getType(); + + if (!$type->hasMethod($methodName)->yes()) { + continue; + } + + return $type->getMethod($methodName, new OutOfClassScope()); + } + + $interfaces = $classReflection->getInterfaces(); + foreach ($interfaces as $interface) { + $method = $this->findMethod($interface, $methodName); + if ($method !== null) { + return $method; + } + } + + return null; + } + +} diff --git a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php new file mode 100644 index 0000000000..550a7bee59 --- /dev/null +++ b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php @@ -0,0 +1,57 @@ +findProperty($classReflection, $propertyName) !== null; + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + $property = $this->findProperty($classReflection, $propertyName); + if ($property === null) { + throw new ShouldNotHappenException(); + } + + return $property; + } + + private function findProperty(ClassReflection $classReflection, string $propertyName): ?ExtendedPropertyReflection + { + if (!$classReflection->isInterface()) { + return null; + } + + $requireExtendsTags = $classReflection->getRequireExtendsTags(); + foreach ($requireExtendsTags as $requireExtendsTag) { + $type = $requireExtendsTag->getType(); + + if (!$type->hasProperty($propertyName)->yes()) { + continue; + } + + return $type->getProperty($propertyName, new OutOfClassScope()); + } + + $interfaces = $classReflection->getInterfaces(); + foreach ($interfaces as $interface) { + $property = $this->findProperty($interface, $propertyName); + if ($property !== null) { + return $property; + } + } + + return null; + } + +} diff --git a/src/Reflection/ResolvedFunctionVariant.php b/src/Reflection/ResolvedFunctionVariant.php index bfdb215c07..5b5cb6b4e6 100644 --- a/src/Reflection/ResolvedFunctionVariant.php +++ b/src/Reflection/ResolvedFunctionVariant.php @@ -2,81 +2,15 @@ namespace PHPStan\Reflection; -use PHPStan\Reflection\Php\DummyParameter; -use PHPStan\Type\Generic\TemplateTypeHelper; -use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; -use function array_map; -class ResolvedFunctionVariant implements ParametersAcceptor +interface ResolvedFunctionVariant extends ExtendedParametersAcceptor { - /** @var ParameterReflection[]|null */ - private ?array $parameters = null; + public function getOriginalParametersAcceptor(): ParametersAcceptor; - private ?Type $returnType = null; + public function getReturnTypeWithUnresolvableTemplateTypes(): Type; - public function __construct( - private ParametersAcceptor $parametersAcceptor, - private TemplateTypeMap $resolvedTemplateTypeMap, - ) - { - } - - public function getOriginalParametersAcceptor(): ParametersAcceptor - { - return $this->parametersAcceptor; - } - - public function getTemplateTypeMap(): TemplateTypeMap - { - return $this->parametersAcceptor->getTemplateTypeMap(); - } - - public function getResolvedTemplateTypeMap(): TemplateTypeMap - { - return $this->resolvedTemplateTypeMap; - } - - public function getParameters(): array - { - $parameters = $this->parameters; - - if ($parameters === null) { - $parameters = array_map(fn (ParameterReflection $param): ParameterReflection => new DummyParameter( - $param->getName(), - 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; - } + public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type; } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php new file mode 100644 index 0000000000..7dbd382405 --- /dev/null +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -0,0 +1,120 @@ +parametersAcceptor->getOriginalParametersAcceptor(); + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->parametersAcceptor->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->parametersAcceptor->getResolvedTemplateTypeMap(); + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->parametersAcceptor->getCallSiteVarianceMap(); + } + + public function getParameters(): array + { + return $this->parametersAcceptor->getParameters(); + } + + public function isVariadic(): bool + { + return $this->parametersAcceptor->isVariadic(); + } + + public function getReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->parametersAcceptor->getReturnTypeWithUnresolvableTemplateTypes(); + } + + public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->parametersAcceptor->getPhpDocReturnTypeWithUnresolvableTemplateTypes(); + } + + public function getReturnType(): Type + { + return $this->parametersAcceptor->getReturnType(); + } + + public function getPhpDocReturnType(): Type + { + return $this->parametersAcceptor->getPhpDocReturnType(); + } + + public function getNativeReturnType(): Type + { + return $this->parametersAcceptor->getNativeReturnType(); + } + + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + public function isPure(): TrinaryLogic + { + return $this->isPure; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + + public function getUsedVariables(): array + { + return $this->usedVariables; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->acceptsNamedArguments; + } + +} diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php new file mode 100644 index 0000000000..d7d2f09acc --- /dev/null +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -0,0 +1,300 @@ +|null */ + private ?array $parameters = null; + + private ?Type $returnTypeWithUnresolvableTemplateTypes = null; + + private ?Type $phpDocReturnTypeWithUnresolvableTemplateTypes = null; + + private ?Type $returnType = null; + + private ?Type $phpDocReturnType = null; + + /** + * @param array $passedArgs + */ + public function __construct( + private ExtendedParametersAcceptor $parametersAcceptor, + private TemplateTypeMap $resolvedTemplateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + private array $passedArgs, + ) + { + } + + public function getOriginalParametersAcceptor(): ParametersAcceptor + { + return $this->parametersAcceptor; + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->parametersAcceptor->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->resolvedTemplateTypeMap; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; + } + + public function getParameters(): array + { + $parameters = $this->parameters; + + if ($parameters === null) { + $parameters = array_map( + function (ExtendedParameterReflection $param): ExtendedParameterReflection { + $paramType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($param->getType()), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), + ), + false, + ); + + $paramOutType = $param->getOutType(); + if ($paramOutType !== null) { + $paramOutType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($paramOutType), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + } + + $closureThisType = $param->getClosureThisType(); + if ($closureThisType !== null) { + $closureThisType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($closureThisType), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + } + + return new ExtendedDummyParameter( + $param->getName(), + $paramType, + $param->isOptional(), + $param->passedByReference(), + $param->isVariadic(), + $param->getDefaultValue(), + $param->getNativeType(), + $param->getPhpDocType(), + $paramOutType, + $param->isImmediatelyInvokedCallable(), + $closureThisType, + $param->getAttributes(), + ); + }, + $this->parametersAcceptor->getParameters(), + ); + + $this->parameters = $parameters; + } + + return $parameters; + } + + public function isVariadic(): bool + { + return $this->parametersAcceptor->isVariadic(); + } + + public function getReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->returnTypeWithUnresolvableTemplateTypes ??= + $this->resolveConditionalTypesForParameter( + $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getReturnType(), TemplateTypeVariance::createCovariant()), + ); + } + + public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->phpDocReturnTypeWithUnresolvableTemplateTypes ??= + $this->resolveConditionalTypesForParameter( + $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getPhpDocReturnType(), TemplateTypeVariance::createCovariant()), + ); + } + + public function getReturnType(): Type + { + $type = $this->returnType; + + if ($type === null) { + $type = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->getReturnTypeWithUnresolvableTemplateTypes(), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + + $this->returnType = $type; + } + + return $type; + } + + public function getPhpDocReturnType(): Type + { + $type = $this->phpDocReturnType; + + if ($type === null) { + $type = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->getPhpDocReturnTypeWithUnresolvableTemplateTypes(), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + + $this->phpDocReturnType = $type; + } + + return $type; + } + + public function getNativeReturnType(): Type + { + return $this->parametersAcceptor->getNativeReturnType(); + } + + private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance $positionVariance): Type + { + $references = $type->getReferencedTemplateTypes($positionVariance); + + $objectCb = function (Type $type, callable $traverse) use ($references): Type { + if ( + $type instanceof TemplateType + && !$type->isArgument() + && $type->getScope()->getFunctionName() !== null + ) { + $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $traverse($type); + } + + $newType = TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); + $variance = TemplateTypeVariance::createInvariant(); + foreach ($references as $reference) { + // this uses identity to distinguish between different occurrences of the same template type + // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details + if ($reference->getType() === $type) { + $variance = $reference->getPositionVariance(); + break; + } + } + + $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { + return $traverse($type->getBound()); + } + + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + + return $newType; + } + + return $traverse($type); + }; + + return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb): Type { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { + return TypeTraverser::map($type, $objectCb); + } + + if ($type instanceof TemplateType && !$type->isArgument()) { + $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $traverse($type); + } + + $variance = TemplateTypeVariance::createInvariant(); + foreach ($references as $reference) { + // this uses identity to distinguish between different occurrences of the same template type + // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details + if ($reference->getType() === $type) { + $variance = $reference->getPositionVariance(); + break; + } + } + + $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { + return $traverse($type->getBound()); + } + + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + + return $newType; + } + + return $traverse($type); + }); + } + + private function resolveConditionalTypesForParameter(Type $type): Type + { + return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { + if ($type instanceof ConditionalTypeForParameter && array_key_exists($type->getParameterName(), $this->passedArgs)) { + $type = $type->toConditional($this->passedArgs[$type->getParameterName()]); + } + + return $traverse($type); + }); + } + +} diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index eeab6cab92..134e566eca 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -4,16 +4,31 @@ use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; +use function is_bool; -class ResolvedMethodReflection implements MethodReflection +final class ResolvedMethodReflection implements ExtendedMethodReflection { - /** @var ParametersAcceptor[]|null */ + /** @var list|null */ private ?array $variants = null; - public function __construct(private MethodReflection $reflection, private TemplateTypeMap $resolvedTemplateTypeMap) + /** @var list|null */ + private ?array $namedArgumentVariants = null; + + private ?Assertions $asserts = null; + + private Type|false|null $selfOutType = false; + + public function __construct( + private ExtendedMethodReflection $reflection, + private TemplateTypeMap $resolvedTemplateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + ) { } @@ -27,9 +42,6 @@ public function getPrototype(): ClassMemberReflection return $this->reflection->getPrototype(); } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { $variants = $this->variants; @@ -37,17 +49,46 @@ public function getVariants(): array return $variants; } - $variants = []; - foreach ($this->reflection->getVariants() as $variant) { - $variants[] = new ResolvedFunctionVariant( + return $this->variants = $this->resolveVariants($this->reflection->getVariants()); + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + $variants = $this->namedArgumentVariants; + if ($variants !== null) { + return $variants; + } + + $innerVariants = $this->reflection->getNamedArgumentsVariants(); + if ($innerVariants === null) { + return null; + } + + return $this->namedArgumentVariants = $this->resolveVariants($innerVariants); + } + + /** + * @param ExtendedParametersAcceptor[] $variants + * @return list + */ + private function resolveVariants(array $variants): array + { + $result = []; + foreach ($variants as $variant) { + $result[] = new ResolvedFunctionVariantWithOriginal( $variant, $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + [], ); } - $this->variants = $variants; - - return $variants; + return $result; } public function getDeclaringClass(): ClassReflection @@ -99,11 +140,26 @@ public function isFinal(): TrinaryLogic return $this->reflection->isFinal(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->reflection->isFinalByKeyword(); + } + public function isInternal(): TrinaryLogic { return $this->reflection->isInternal(); } + public function isBuiltin(): TrinaryLogic + { + $builtin = $this->reflection->isBuiltin(); + if (is_bool($builtin)) { + return TrinaryLogic::createFromBoolean($builtin); + } + + return $builtin; + } + public function getThrowType(): ?Type { return $this->reflection->getThrowType(); @@ -114,4 +170,63 @@ public function hasSideEffects(): TrinaryLogic return $this->reflection->hasSideEffects(); } + public function isPure(): TrinaryLogic + { + return $this->reflection->isPure(); + } + + public function getAsserts(): Assertions + { + return $this->asserts ??= $this->reflection->getAsserts()->mapTypes(fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createInvariant(), + )); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->reflection->acceptsNamedArguments(); + } + + public function getSelfOutType(): ?Type + { + if ($this->selfOutType === false) { + $selfOutType = $this->reflection->getSelfOutType(); + if ($selfOutType !== null) { + $selfOutType = TemplateTypeHelper::resolveTemplateTypes( + $selfOutType, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createInvariant(), + ); + } + + $this->selfOutType = $selfOutType; + } + + return $this->selfOutType; + } + + public function returnsByReference(): TrinaryLogic + { + return $this->reflection->returnsByReference(); + } + + public function isAbstract(): TrinaryLogic + { + $abstract = $this->reflection->isAbstract(); + if (is_bool($abstract)) { + return TrinaryLogic::createFromBoolean($abstract); + } + + return $abstract; + } + + public function getAttributes(): array + { + return $this->reflection->getAttributes(); + } + } diff --git a/src/Reflection/ResolvedPropertyReflection.php b/src/Reflection/ResolvedPropertyReflection.php index 0596d61d6b..df7a33f84d 100644 --- a/src/Reflection/ResolvedPropertyReflection.php +++ b/src/Reflection/ResolvedPropertyReflection.php @@ -6,20 +6,31 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -class ResolvedPropertyReflection implements WrapperPropertyReflection +final class ResolvedPropertyReflection implements WrapperPropertyReflection { private ?Type $readableType = null; private ?Type $writableType = null; - public function __construct(private PropertyReflection $reflection, private TemplateTypeMap $templateTypeMap) + public function __construct( + private ExtendedPropertyReflection $reflection, + private TemplateTypeMap $templateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + ) { } - public function getOriginalReflection(): PropertyReflection + public function getName(): string + { + return $this->reflection->getName(); + } + + public function getOriginalReflection(): ExtendedPropertyReflection { return $this->reflection; } @@ -53,6 +64,26 @@ public function isPublic(): bool return $this->reflection->isPublic(); } + public function hasPhpDocType(): bool + { + return $this->reflection->hasPhpDocType(); + } + + public function getPhpDocType(): Type + { + return $this->reflection->getPhpDocType(); + } + + public function hasNativeType(): bool + { + return $this->reflection->hasNativeType(); + } + + public function getNativeType(): Type + { + return $this->reflection->getNativeType(); + } + public function getReadableType(): Type { $type = $this->readableType; @@ -63,10 +94,14 @@ public function getReadableType(): Type $type = TemplateTypeHelper::resolveTemplateTypes( $this->reflection->getReadableType(), $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), ); $type = TemplateTypeHelper::resolveTemplateTypes( $type, $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), ); $this->readableType = $type; @@ -84,10 +119,14 @@ public function getWritableType(): Type $type = TemplateTypeHelper::resolveTemplateTypes( $this->reflection->getWritableType(), $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), ); $type = TemplateTypeHelper::resolveTemplateTypes( $type, $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), ); $this->writableType = $type; @@ -130,4 +169,48 @@ public function isInternal(): TrinaryLogic return $this->reflection->isInternal(); } + public function isAbstract(): TrinaryLogic + { + return $this->reflection->isAbstract(); + } + + public function isFinal(): TrinaryLogic + { + return $this->reflection->isFinal(); + } + + public function isVirtual(): TrinaryLogic + { + return $this->reflection->isVirtual(); + } + + public function hasHook(string $hookType): bool + { + return $this->reflection->hasHook($hookType); + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + return new ResolvedMethodReflection( + $this->reflection->getHook($hookType), + $this->templateTypeMap, + $this->callSiteVarianceMap, + ); + } + + public function isProtectedSet(): bool + { + return $this->reflection->isProtectedSet(); + } + + public function isPrivateSet(): bool + { + return $this->reflection->isPrivateSet(); + } + + public function getAttributes(): array + { + return $this->reflection->getAttributes(); + } + } diff --git a/src/Reflection/Runtime/RuntimeReflectionProvider.php b/src/Reflection/Runtime/RuntimeReflectionProvider.php deleted file mode 100644 index faf34914cb..0000000000 --- a/src/Reflection/Runtime/RuntimeReflectionProvider.php +++ /dev/null @@ -1,402 +0,0 @@ - */ - private array $cachedConstants = []; - - public function __construct( - 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, - ) - { - } - - public function getClass(string $className): ClassReflection - { - /** @var class-string $className */ - $className = $className; - if (!$this->hasClass($className)) { - throw new ClassNotFoundException($className); - } - - if (isset(self::$anonymousClasses[$className])) { - return self::$anonymousClasses[$className]; - } - - if (!isset($this->classReflections[$className])) { - $reflectionClass = new ReflectionClass($className); - $filename = null; - if ($reflectionClass->getFileName() !== false) { - $filename = $reflectionClass->getFileName(); - } - - $classReflection = $this->getClassFromReflection( - $reflectionClass, - $reflectionClass->getName(), - $reflectionClass->isAnonymous() ? $filename : null, - ); - $this->classReflections[$className] = $classReflection; - if ($className !== $reflectionClass->getName()) { - // class alias optimization - $this->classReflections[$reflectionClass->getName()] = $classReflection; - } - } - - return $this->classReflections[$className]; - } - - public function getClassName(string $className): string - { - if (!$this->hasClass($className)) { - throw new ClassNotFoundException($className); - } - - /** @var class-string $className */ - $className = $className; - $reflectionClass = new ReflectionClass($className); - $realName = $reflectionClass->getName(); - - if (isset(self::$anonymousClasses[$realName])) { - return self::$anonymousClasses[$realName]->getDisplayName(); - } - - return $realName; - } - - public function supportsAnonymousClasses(): bool - { - return false; - } - - public function getAnonymousClassReflection( - Node\Stmt\Class_ $classNode, - Scope $scope, - ): ClassReflection - { - throw new ShouldNotHappenException(); - } - - /** - * @param ReflectionClass $reflectionClass - */ - 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(), - $displayName, - $reflectionClass, - $anonymousFilename, - null, - $this->stubPhpDocProvider->findClassPhpDoc($className), - ); - $this->classReflections[$className] = $classReflection; - } - - return $this->classReflections[$className]; - } - - public function hasClass(string $className): bool - { - if (!ClassNameHelper::isValidClassName($className)) { - return $this->hasClassCache[$className] = false; - } - - $className = trim($className, '\\'); - if (isset($this->hasClassCache[$className])) { - return $this->hasClassCache[$className]; - } - - spl_autoload_register($autoloader = function (string $autoloadedClassName) use ($className): void { - $autoloadedClassName = trim($autoloadedClassName, '\\'); - if ($autoloadedClassName !== $className && !$this->isExistsCheckCall()) { - throw new ClassAutoloadingException($autoloadedClassName); - } - }); - - try { - return $this->hasClassCache[$className] = class_exists($className) || interface_exists($className) || trait_exists($className); - } catch (ClassAutoloadingException $e) { - throw $e; - } catch (Throwable $t) { - throw new ClassAutoloadingException( - $className, - $t, - ); - } finally { - spl_autoload_unregister($autoloader); - } - } - - public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection - { - $functionName = $this->resolveFunctionName($nameNode, $scope); - if ($functionName === null) { - throw new FunctionNotFoundException((string) $nameNode); - } - - $lowerCasedFunctionName = strtolower($functionName); - if (isset($this->functionReflections[$lowerCasedFunctionName])) { - return $this->functionReflections[$lowerCasedFunctionName]; - } - - $nativeFunctionReflection = $this->nativeFunctionReflectionProvider->findFunctionReflection($lowerCasedFunctionName); - if ($nativeFunctionReflection !== null) { - $this->functionReflections[$lowerCasedFunctionName] = $nativeFunctionReflection; - return $nativeFunctionReflection; - } - - $this->functionReflections[$lowerCasedFunctionName] = $this->getCustomFunction($nameNode, $scope); - - return $this->functionReflections[$lowerCasedFunctionName]; - } - - public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool - { - return $this->resolveFunctionName($nameNode, $scope) !== null; - } - - private function hasCustomFunction(Node\Name $nameNode, ?Scope $scope): bool - { - $functionName = $this->resolveFunctionName($nameNode, $scope); - if ($functionName === null) { - return false; - } - - return $this->nativeFunctionReflectionProvider->findFunctionReflection($functionName) === null; - } - - private function getCustomFunction(Node\Name $nameNode, ?Scope $scope): PhpFunctionReflection - { - if (!$this->hasCustomFunction($nameNode, $scope)) { - throw new FunctionNotFoundException((string) $nameNode); - } - - /** @var string $functionName */ - $functionName = $this->resolveFunctionName($nameNode, $scope); - if (!function_exists($functionName)) { - throw new FunctionNotFoundException($functionName); - } - $lowerCasedFunctionName = strtolower($functionName); - if (isset($this->customFunctionReflections[$lowerCasedFunctionName])) { - return $this->customFunctionReflections[$lowerCasedFunctionName]; - } - - $reflectionFunction = new ReflectionFunction($functionName); - $templateTypeMap = TemplateTypeMap::createEmpty(); - $phpDocParameterTags = []; - $phpDocReturnTag = null; - $phpDocThrowsTag = null; - $deprecatedTag = null; - $isDeprecated = false; - $isInternal = false; - $isFinal = false; - $isPure = null; - $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); - } - - if ($resolvedPhpDoc !== null) { - $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - $phpDocParameterTags = $resolvedPhpDoc->getParamTags(); - $phpDocReturnTag = $resolvedPhpDoc->getReturnTag(); - $phpDocThrowsTag = $resolvedPhpDoc->getThrowsTag(); - $deprecatedTag = $resolvedPhpDoc->getDeprecatedTag(); - $isDeprecated = $resolvedPhpDoc->isDeprecated(); - $isInternal = $resolvedPhpDoc->isInternal(); - $isFinal = $resolvedPhpDoc->isFinal(); - $isPure = $resolvedPhpDoc->isPure(); - } - - $functionReflection = $this->functionReflectionFactory->create( - $reflectionFunction, - $templateTypeMap, - 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() !== false ? $reflectionFunction->getFileName() : null, - $isPure, - ); - $this->customFunctionReflections[$lowerCasedFunctionName] = $functionReflection; - - return $functionReflection; - } - - 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; - if ($exists) { - if ($this->phpStormStubsSourceStubber->isPresentFunction($name) === false) { - return false; - } - - return true; - } - - return false; - }, $scope); - } - - public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool - { - return $this->resolveConstantName($nameNode, $scope) !== null; - } - - public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - $constantName = $this->resolveConstantName($nameNode, $scope); - if ($constantName === null) { - throw new ConstantNotFoundException((string) $nameNode); - } - - if (array_key_exists($constantName, $this->cachedConstants)) { - return $this->cachedConstants[$constantName]; - } - - return $this->cachedConstants[$constantName] = new RuntimeConstantReflection( - $constantName, - ConstantTypeHelper::getTypeFromValue(@constant($constantName)), - null, - ); - } - - public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->resolveName($nameNode, static fn (string $name): bool => defined($name), $scope); - } - - /** - * @param Closure(string $name): bool $existsCallback - */ - private function resolveName( - Node\Name $nameNode, - Closure $existsCallback, - ?Scope $scope, - ): ?string - { - $name = (string) $nameNode; - if ($scope !== null && $scope->getNamespace() !== null && !$nameNode->isFullyQualified()) { - $namespacedName = sprintf('%s\\%s', $scope->getNamespace(), $name); - if ($existsCallback($namespacedName)) { - return $namespacedName; - } - } - - if ($existsCallback($name)) { - return $name; - } - - return null; - } - - private function isExistsCheckCall(): bool - { - $debugBacktrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $existsCallTypes = [ - 'class_exists' => true, - 'interface_exists' => true, - 'trait_exists' => true, - ]; - - foreach ($debugBacktrace as $traceStep) { - if ( - isset($existsCallTypes[$traceStep['function']]) - // We must ignore the self::hasClass calls - && (!isset($traceStep['file']) || $traceStep['file'] !== __FILE__) - ) { - return true; - } - } - - return false; - } - -} diff --git a/src/Reflection/SignatureMap/FunctionSignature.php b/src/Reflection/SignatureMap/FunctionSignature.php index 8473684acc..f9107d4b23 100644 --- a/src/Reflection/SignatureMap/FunctionSignature.php +++ b/src/Reflection/SignatureMap/FunctionSignature.php @@ -4,11 +4,11 @@ use PHPStan\Type\Type; -class FunctionSignature +final class FunctionSignature { /** - * @param array $parameters + * @param list $parameters */ public function __construct( private array $parameters, @@ -20,7 +20,7 @@ public function __construct( } /** - * @return array + * @return list */ public function getParameters(): array { diff --git a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php index 6afcff805f..7f58e257fe 100644 --- a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php +++ b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php @@ -2,11 +2,15 @@ namespace PHPStan\Reflection\SignatureMap; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\TypehintHelper; -use ReflectionMethod; +use ReflectionFunctionAbstract; use function array_change_key_case; use function array_key_exists; use function array_keys; @@ -15,43 +19,72 @@ use function strtolower; use const CASE_LOWER; -class FunctionSignatureMapProvider implements SignatureMapProvider +final class FunctionSignatureMapProvider implements SignatureMapProvider { - /** @var mixed[]|null */ - private ?array $signatureMap = null; + /** @var array */ + private static array $signatureMaps = []; /** @var array|null */ - private ?array $functionMetadata = null; + private static ?array $functionMetadata = null; + + public function __construct( + private SignatureMapParser $parser, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private PhpVersion $phpVersion, + private bool $stricterFunctionMap, + ) + { + } + + public function hasMethodSignature(string $className, string $methodName): bool + { + return $this->hasFunctionSignature(sprintf('%s::%s', $className, $methodName)); + } - public function __construct(private SignatureMapParser $parser, private PhpVersion $phpVersion) + public function hasFunctionSignature(string $name): bool { + return array_key_exists(strtolower($name), $this->getSignatureMap()); } - public function hasMethodSignature(string $className, string $methodName, int $variant = 0): bool + public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array { - return $this->hasFunctionSignature(sprintf('%s::%s', $className, $methodName), $variant); + return $this->getFunctionSignatures(sprintf('%s::%s', $className, $methodName), $className, $reflectionMethod); } - public function hasFunctionSignature(string $name, int $variant = 0): bool + public function getFunctionSignatures(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): array { - $signatureMap = $this->getSignatureMap(); - if ($variant > 0) { - $name .= '\'' . $variant; + $functionName = strtolower($functionName); + + $signatures = [$this->createSignature($functionName, $className, $reflectionFunction)]; + $i = 1; + $variantFunctionName = $functionName . '\'' . $i; + while ($this->hasFunctionSignature($variantFunctionName)) { + $signatures[] = $this->createSignature($variantFunctionName, $className, $reflectionFunction); + $i++; + $variantFunctionName = $functionName . '\'' . $i; } - return array_key_exists(strtolower($name), $signatureMap); + + return ['positional' => $signatures, 'named' => null]; } - public function getMethodSignature(string $className, string $methodName, ?ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature + private function createSignature(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): FunctionSignature { - $signature = $this->getFunctionSignature(sprintf('%s::%s', $className, $methodName), $className, $variant); + if (!$reflectionFunction instanceof ReflectionMethod && !$reflectionFunction instanceof ReflectionFunction && $reflectionFunction !== null) { + throw new ShouldNotHappenException(); + } + $signatureMap = self::getSignatureMap(); + $signature = $this->parser->getFunctionSignature( + $signatureMap[$functionName], + $className, + ); $parameters = []; foreach ($signature->getParameters() as $i => $parameter) { - if ($reflectionMethod === null) { + if ($reflectionFunction === null) { $parameters[] = $parameter; continue; } - $nativeParameters = $reflectionMethod->getParameters(); + $nativeParameters = $reflectionFunction->getParameters(); if (!array_key_exists($i, $nativeParameters)) { $parameters[] = $parameter; continue; @@ -64,13 +97,18 @@ public function getMethodSignature(string $className, string $methodName, ?Refle TypehintHelper::decideTypeFromReflection($nativeParameters[$i]->getType()), $parameter->passedByReference(), $parameter->isVariadic(), + $nativeParameters[$i]->isDefaultValueAvailable() ? $this->initializerExprTypeResolver->getType( + $nativeParameters[$i]->getDefaultValueExpression(), + InitializerExprContext::fromReflectionParameter($nativeParameters[$i]), + ) : null, + $parameter->getOutType(), ); } - if ($reflectionMethod === null) { + if ($reflectionFunction === null) { $nativeReturnType = new MixedType(); } else { - $nativeReturnType = TypehintHelper::decideTypeFromReflection($reflectionMethod->getReturnType()); + $nativeReturnType = TypehintHelper::decideTypeFromReflection($reflectionFunction->getReturnType()); } return new FunctionSignature( @@ -81,25 +119,6 @@ public function getMethodSignature(string $className, string $methodName, ?Refle ); } - public function getFunctionSignature(string $functionName, ?string $className, int $variant = 0): FunctionSignature - { - $functionName = strtolower($functionName); - if ($variant > 0) { - $functionName .= '\'' . $variant; - } - - if (!$this->hasFunctionSignature($functionName)) { - throw new ShouldNotHappenException(); - } - - $signatureMap = self::getSignatureMap(); - - return $this->parser->getFunctionSignature( - $signatureMap[$functionName], - $className, - ); - } - public function hasMethodMetadata(string $className, string $methodName): bool { return $this->hasFunctionMetadata(sprintf('%s::%s', $className, $methodName)); @@ -107,7 +126,7 @@ public function hasMethodMetadata(string $className, string $methodName): bool public function hasFunctionMetadata(string $name): bool { - $signatureMap = $this->getFunctionMetadataMap(); + $signatureMap = self::getFunctionMetadataMap(); return array_key_exists(strtolower($name), $signatureMap); } @@ -130,21 +149,21 @@ public function getFunctionMetadata(string $functionName): array throw new ShouldNotHappenException(); } - return $this->getFunctionMetadataMap()[$functionName]; + return self::getFunctionMetadataMap()[$functionName]; } /** * @return array */ - private function getFunctionMetadataMap(): array + private static function getFunctionMetadataMap(): array { - if ($this->functionMetadata === null) { + if (self::$functionMetadata === null) { /** @var array $metadata */ $metadata = require __DIR__ . '/../../../resources/functionMetadata.php'; - $this->functionMetadata = array_change_key_case($metadata, CASE_LOWER); + self::$functionMetadata = array_change_key_case($metadata, CASE_LOWER); } - return $this->functionMetadata; + return self::$functionMetadata; } /** @@ -152,36 +171,65 @@ private function getFunctionMetadataMap(): array */ public function getSignatureMap(): array { - if ($this->signatureMap === null) { - $signatureMap = require __DIR__ . '/../../../resources/functionMap.php'; - if (!is_array($signatureMap)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } + $cacheKey = sprintf('%d-%d', $this->phpVersion->getVersionId(), $this->stricterFunctionMap ? 1 : 0); + if (array_key_exists($cacheKey, self::$signatureMaps)) { + return self::$signatureMaps[$cacheKey]; + } - $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); + $signatureMap = require __DIR__ . '/../../../resources/functionMap.php'; + if (!is_array($signatureMap)) { + throw new ShouldNotHappenException('Signature map could not be loaded.'); + } - if ($this->phpVersion->getVersionId() >= 70400) { - $php74MapDelta = require __DIR__ . '/../../../resources/functionMap_php74delta.php'; - if (!is_array($php74MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } + $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); - $signatureMap = $this->computeSignatureMap($signatureMap, $php74MapDelta); - } + if ($this->stricterFunctionMap) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_bleedingEdge.php'); + } + + if ($this->phpVersion->getVersionId() >= 70400) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php74delta.php'); + } - if ($this->phpVersion->getVersionId() >= 80000) { - $php80MapDelta = require __DIR__ . '/../../../resources/functionMap_php80delta.php'; - if (!is_array($php80MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } + if ($this->phpVersion->getVersionId() >= 80000) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php80delta.php'); - $signatureMap = $this->computeSignatureMap($signatureMap, $php80MapDelta); + if ($this->stricterFunctionMap) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php80delta_bleedingEdge.php'); } + } - $this->signatureMap = $signatureMap; + if ($this->phpVersion->getVersionId() >= 80100) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php81delta.php'); } - return $this->signatureMap; + if ($this->phpVersion->getVersionId() >= 80200) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php82delta.php'); + } + + if ($this->phpVersion->getVersionId() >= 80300) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php83delta.php'); + } + + if ($this->phpVersion->getVersionId() >= 80400) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php84delta.php'); + } + + return self::$signatureMaps[$cacheKey] = $signatureMap; + } + + /** + * @param array $signatureMap + * @return array + */ + private function computeSignatureMapFile(array $signatureMap, string $file): array + { + $signatureMapDelta = include $file; + if (!is_array($signatureMapDelta)) { + throw new ShouldNotHappenException(sprintf('Signature map file "%s" could not be loaded.', $file)); + } + + return $this->computeSignatureMap($signatureMap, $signatureMapDelta); } /** @@ -201,4 +249,14 @@ private function computeSignatureMap(array $signatureMap, array $delta): array return $signatureMap; } + public function hasClassConstantMetadata(string $className, string $constantName): bool + { + return false; + } + + public function getClassConstantMetadata(string $className, string $constantName): array + { + throw new ShouldNotHappenException(); + } + } diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index fbd1794848..766e665115 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -3,163 +3,169 @@ namespace PHPStan\Reflection\SignatureMap; use PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; -use PHPStan\Reflection\FunctionVariant; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeFunctionReflection; -use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\TrinaryLogic; -use PHPStan\Type\ArrayType; -use PHPStan\Type\BooleanType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\FloatType; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\IntegerType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType; -use PHPStan\Type\StringType; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\UnionType; +use function array_key_exists; use function array_map; use function strtolower; -class NativeFunctionReflectionProvider +final class NativeFunctionReflectionProvider { /** @var NativeFunctionReflection[] */ - private static array $functionMap = []; - - public function __construct(private SignatureMapProvider $signatureMapProvider, private Reflector $reflector, private FileTypeMapper $fileTypeMapper, private StubPhpDocProvider $stubPhpDocProvider) + private array $functionMap = []; + + public function __construct( + private SignatureMapProvider $signatureMapProvider, + private Reflector $reflector, + private FileTypeMapper $fileTypeMapper, + private StubPhpDocProvider $stubPhpDocProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + ) { } public function findFunctionReflection(string $functionName): ?NativeFunctionReflection { $lowerCasedFunctionName = strtolower($functionName); - if (isset(self::$functionMap[$lowerCasedFunctionName])) { - return self::$functionMap[$lowerCasedFunctionName]; + $realFunctionName = $lowerCasedFunctionName; + if (isset($this->functionMap[$lowerCasedFunctionName])) { + return $this->functionMap[$lowerCasedFunctionName]; } if (!$this->signatureMapProvider->hasFunctionSignature($lowerCasedFunctionName)) { return null; } - $reflectionFunction = $this->signatureMapProvider->getFunctionSignature($lowerCasedFunctionName, null); - - $phpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($lowerCasedFunctionName, array_map(static fn (ParameterSignature $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); - - $variants = []; - $i = 0; - while ($this->signatureMapProvider->hasFunctionSignature($lowerCasedFunctionName, $i)) { - $functionSignature = $this->signatureMapProvider->getFunctionSignature($lowerCasedFunctionName, null, $i); - $variants[] = new FunctionVariant( - TemplateTypeMap::createEmpty(), - null, - array_map(static function (ParameterSignature $parameterSignature) use ($lowerCasedFunctionName, $phpDoc): NativeParameterReflection { - $type = $parameterSignature->getType(); - $defaultValue = null; - - $phpDocType = null; - if ($phpDoc !== null) { - $phpDocParam = $phpDoc->getParamTags()[$parameterSignature->getName()] ?? null; - if ($phpDocParam !== null) { - $phpDocType = $phpDocParam->getType(); - } - } - if ( - $parameterSignature->getName() === 'values' - && ( - $lowerCasedFunctionName === 'printf' - || $lowerCasedFunctionName === 'sprintf' - ) - ) { - $type = new UnionType([ - new StringAlwaysAcceptingObjectWithToStringType(), - new IntegerType(), - new FloatType(), - new NullType(), - new BooleanType(), - ]); - } - - if ( - $parameterSignature->getName() === 'fields' - && $lowerCasedFunctionName === 'fputcsv' - ) { - $type = new ArrayType( - new UnionType([ - new StringType(), - new IntegerType(), - ]), - new UnionType([ - new StringAlwaysAcceptingObjectWithToStringType(), - new IntegerType(), - new FloatType(), - new NullType(), - new BooleanType(), - ]), - ); - } - - if ( - $lowerCasedFunctionName === 'array_reduce' - && $parameterSignature->getName() === 'initial' - ) { - $defaultValue = new NullType(); - } - - return new NativeParameterReflection( - $parameterSignature->getName(), - $parameterSignature->isOptional(), - TypehintHelper::decideType($type, $phpDocType), - $parameterSignature->passedByReference(), - $parameterSignature->isVariadic(), - $defaultValue, - ); - }, $functionSignature->getParameters()), - $functionSignature->isVariadic(), - TypehintHelper::decideType($functionSignature->getReturnType(), $phpDoc !== null ? $this->getReturnTypeFromPhpDoc($phpDoc) : null), - ); - - $i++; - } - - if ($this->signatureMapProvider->hasFunctionMetadata($lowerCasedFunctionName)) { - $hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getFunctionMetadata($lowerCasedFunctionName)['hasSideEffects']); - } else { - $hasSideEffects = TrinaryLogic::createMaybe(); - } $throwType = null; + $reflectionFunctionAdapter = null; $isDeprecated = false; + $phpDocReturnType = null; + $asserts = Assertions::createEmpty(); + $docComment = null; + $returnsByReference = TrinaryLogic::createMaybe(); + $acceptsNamedArguments = true; + $fileName = null; + $attributes = []; try { $reflectionFunction = $this->reflector->reflectFunction($functionName); + $reflectionFunctionAdapter = new ReflectionFunction($reflectionFunction); + $attributes = $reflectionFunctionAdapter->getAttributes(); + $returnsByReference = TrinaryLogic::createFromBoolean($reflectionFunctionAdapter->returnsReference()); + $realFunctionName = $reflectionFunction->getName(); + $isDeprecated = $reflectionFunction->isDeprecated(); if ($reflectionFunction->getFileName() !== null) { $fileName = $reflectionFunction->getFileName(); $docComment = $reflectionFunction->getDocComment(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, $reflectionFunction->getName(), $docComment); - $throwsTag = $resolvedPhpDoc->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); + if ($docComment !== null) { + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, $reflectionFunction->getName(), $docComment); + $throwsTag = $resolvedPhpDoc->getThrowsTag(); + if ($throwsTag !== null) { + $throwType = $throwsTag->getType(); + } } - $isDeprecated = $reflectionFunction->isDeprecated(); } - } catch (IdentifierNotFound) { - // pass - } catch (InvalidIdentifierName) { + } catch (IdentifierNotFound | InvalidIdentifierName) { // pass } + $functionSignaturesResult = $this->signatureMapProvider->getFunctionSignatures($lowerCasedFunctionName, null, $reflectionFunctionAdapter); + + $phpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($lowerCasedFunctionName, array_map(static fn (ParameterSignature $parameter): string => $parameter->getName(), $functionSignaturesResult['positional'][0]->getParameters())); + if ($phpDoc !== null) { + if ($phpDoc->hasPhpDocString()) { + $docComment = $phpDoc->getPhpDocString(); + } + if ($phpDoc->getThrowsTag() !== null) { + $throwType = $phpDoc->getThrowsTag()->getType(); + } + $asserts = Assertions::createFromResolvedPhpDocBlock($phpDoc); + $phpDocReturnType = $this->getReturnTypeFromPhpDoc($phpDoc); + $acceptsNamedArguments = $phpDoc->acceptsNamedArguments(); + } + + $variantsByType = ['positional' => []]; + foreach ($functionSignaturesResult as $signatureType => $functionSignatures) { + foreach ($functionSignatures ?? [] as $functionSignature) { + $variantsByType[$signatureType][] = new ExtendedFunctionVariant( + TemplateTypeMap::createEmpty(), + null, + array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc): ExtendedNativeParameterReflection { + $type = $parameterSignature->getType(); + + $phpDocType = null; + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + $closureThisType = null; + if ($phpDoc !== null) { + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamTags())) { + $phpDocType = $phpDoc->getParamTags()[$parameterSignature->getName()]->getType(); + } + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamsImmediatelyInvokedCallable())) { + $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($phpDoc->getParamsImmediatelyInvokedCallable()[$parameterSignature->getName()]); + } + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamClosureThisTags())) { + $closureThisType = $phpDoc->getParamClosureThisTags()[$parameterSignature->getName()]->getType(); + } + } + + return new ExtendedNativeParameterReflection( + $parameterSignature->getName(), + $parameterSignature->isOptional(), + TypehintHelper::decideType($type, $phpDocType), + $phpDocType ?? new MixedType(), + $type, + $parameterSignature->passedByReference(), + $parameterSignature->isVariadic(), + $parameterSignature->getDefaultValue(), + $phpDoc !== null ? NativeFunctionReflectionProvider::getParamOutTypeFromPhpDoc($parameterSignature->getName(), $phpDoc) : null, + $immediatelyInvokedCallable, + $closureThisType, + [], + ); + }, $functionSignature->getParameters()), + $functionSignature->isVariadic(), + TypehintHelper::decideType($functionSignature->getReturnType(), $phpDocReturnType), + $phpDocReturnType ?? new MixedType(), + $functionSignature->getReturnType(), + ); + } + } + + if ($this->signatureMapProvider->hasFunctionMetadata($lowerCasedFunctionName)) { + $hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getFunctionMetadata($lowerCasedFunctionName)['hasSideEffects']); + } else { + $hasSideEffects = TrinaryLogic::createMaybe(); + } + $functionReflection = new NativeFunctionReflection( - $lowerCasedFunctionName, - $variants, + $realFunctionName, + $variantsByType['positional'], + $variantsByType['named'] ?? null, $throwType, $hasSideEffects, $isDeprecated, + $asserts, + $docComment, + $returnsByReference, + $acceptsNamedArguments, + $this->attributeReflectionFactory->fromNativeReflection($attributes, InitializerExprContext::fromFunction($realFunctionName, $fileName)), ); - self::$functionMap[$lowerCasedFunctionName] = $functionReflection; + $this->functionMap[$lowerCasedFunctionName] = $functionReflection; return $functionReflection; } @@ -174,4 +180,15 @@ private function getReturnTypeFromPhpDoc(ResolvedPhpDocBlock $phpDoc): ?Type return $returnTag->getType(); } + private static function getParamOutTypeFromPhpDoc(string $paramName, ResolvedPhpDocBlock $stubPhpDoc): ?Type + { + $paramOutTags = $stubPhpDoc->getParamOutTags(); + + if (array_key_exists($paramName, $paramOutTags)) { + return $paramOutTags[$paramName]->getType(); + } + + return null; + } + } diff --git a/src/Reflection/SignatureMap/ParameterSignature.php b/src/Reflection/SignatureMap/ParameterSignature.php index 45b8f5f294..7649825119 100644 --- a/src/Reflection/SignatureMap/ParameterSignature.php +++ b/src/Reflection/SignatureMap/ParameterSignature.php @@ -5,7 +5,7 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; -class ParameterSignature +final class ParameterSignature { public function __construct( @@ -15,6 +15,8 @@ public function __construct( private Type $nativeType, private PassedByReference $passedByReference, private bool $variadic, + private ?Type $defaultValue, + private ?Type $outType, ) { } @@ -49,4 +51,14 @@ public function isVariadic(): bool return $this->variadic; } + public function getDefaultValue(): ?Type + { + return $this->defaultValue; + } + + public function getOutType(): ?Type + { + return $this->outType; + } + } diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index 9e2fd564d0..b787a4201b 100644 --- a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -2,29 +2,39 @@ namespace PHPStan\Reflection\SignatureMap; +use PhpParser\Node\AttributeGroup; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Variable; -use PhpParser\Node\FunctionLike; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\Php\PhpVersion; use PHPStan\Php8StubsMap; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\MixedType; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; -use ReflectionMethod; +use ReflectionFunctionAbstract; use function array_key_exists; use function array_map; use function count; +use function explode; use function is_string; use function sprintf; use function strtolower; -class Php8SignatureMapProvider implements SignatureMapProvider +final class Php8SignatureMapProvider implements SignatureMapProvider { private const DIRECTORY = __DIR__ . '/../../../vendor/phpstan/php-8-stubs'; @@ -32,27 +42,35 @@ class Php8SignatureMapProvider implements SignatureMapProvider /** @var array> */ private array $methodNodes = []; + /** @var array> */ + private array $constantTypes = []; + + private Php8StubsMap $map; + public function __construct( private FunctionSignatureMapProvider $functionSignatureMapProvider, private FileNodesFetcher $fileNodesFetcher, private FileTypeMapper $fileTypeMapper, + private PhpVersion $phpVersion, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionProviderProvider $reflectionProviderProvider, ) { + $this->map = new Php8StubsMap($phpVersion->getVersionId()); } - public function hasMethodSignature(string $className, string $methodName, int $variant = 0): bool + public function hasMethodSignature(string $className, string $methodName): bool { $lowerClassName = strtolower($className); - if (!array_key_exists($lowerClassName, Php8StubsMap::CLASSES)) { - return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName, $variant); + if ($lowerClassName === 'backedenum') { + return false; } - - if ($variant > 0) { - return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName, $variant); + if (!array_key_exists($lowerClassName, $this->map->classes)) { + return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName); } if ($this->findMethodNode($className, $methodName) === null) { - return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName, $variant); + return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName); } return true; @@ -60,7 +78,6 @@ public function hasMethodSignature(string $className, string $methodName, int $v /** * @return array{ClassMethod, string}|null - * @throws ShouldNotHappenException */ private function findMethodNode(string $className, string $methodName): ?array { @@ -70,7 +87,7 @@ private function findMethodNode(string $className, string $methodName): ?array return $this->methodNodes[$lowerClassName][$lowerMethodName]; } - $stubFile = self::DIRECTORY . '/' . Php8StubsMap::CLASSES[$lowerClassName]; + $stubFile = self::DIRECTORY . '/' . $this->map->classes[$lowerClassName]; $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); $classes = $nodes->getClassNodes(); if (count($classes) !== 1) { @@ -88,6 +105,9 @@ private function findMethodNode(string $className, string $methodName): ?array } if ($stmt->name->toLowerString() === $lowerMethodName) { + if (!$this->isForCurrentVersion($stmt->attrGroups)) { + continue; + } return $this->methodNodes[$lowerClassName][$lowerMethodName] = [$stmt, $stubFile]; } } @@ -95,84 +115,181 @@ private function findMethodNode(string $className, string $methodName): ?array return null; } - public function hasFunctionSignature(string $name, int $variant = 0): bool + /** + * @param AttributeGroup[] $attrGroups + */ + private function isForCurrentVersion(array $attrGroups): bool { - $lowerName = strtolower($name); - if (!array_key_exists($lowerName, Php8StubsMap::FUNCTIONS)) { - return $this->functionSignatureMapProvider->hasFunctionSignature($name, $variant); - } - - if ($variant > 0) { - return $this->functionSignatureMapProvider->hasFunctionSignature($name, $variant); + foreach ($attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() === 'Until') { + $arg = $attr->args[0]->value; + if (!$arg instanceof String_) { + throw new ShouldNotHappenException(); + } + $parts = explode('.', $arg->value); + $versionId = (int) $parts[0] * 10000 + (int) ($parts[1] ?? 0) * 100 + (int) ($parts[2] ?? 0); + if ($this->phpVersion->getVersionId() >= $versionId) { + return false; + } + } + if ($attr->name->toString() !== 'Since') { + continue; + } + + $arg = $attr->args[0]->value; + if (!$arg instanceof String_) { + throw new ShouldNotHappenException(); + } + $parts = explode('.', $arg->value); + $versionId = (int) $parts[0] * 10000 + (int) ($parts[1] ?? 0) * 100 + (int) ($parts[2] ?? 0); + if ($this->phpVersion->getVersionId() < $versionId) { + return false; + } + } } return true; } - public function getMethodSignature(string $className, string $methodName, ?ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature + public function hasFunctionSignature(string $name): bool { - $lowerClassName = strtolower($className); - if (!array_key_exists($lowerClassName, Php8StubsMap::CLASSES)) { - return $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant); + $lowerName = strtolower($name); + if (!array_key_exists($lowerName, $this->map->functions)) { + return $this->functionSignatureMapProvider->hasFunctionSignature($name); } - if ($variant > 0) { - return $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant); - } + return true; + } - if ($this->functionSignatureMapProvider->hasMethodSignature($className, $methodName, 1)) { - return $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant); + public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array + { + $lowerClassName = strtolower($className); + if (!array_key_exists($lowerClassName, $this->map->classes)) { + return $this->functionSignatureMapProvider->getMethodSignatures($className, $methodName, $reflectionMethod); } $methodNode = $this->findMethodNode($className, $methodName); if ($methodNode === null) { - return $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant); + return $this->functionSignatureMapProvider->getMethodSignatures($className, $methodName, $reflectionMethod); } [$methodNode, $stubFile] = $methodNode; $signature = $this->getSignature($methodNode, $className, $stubFile); if ($this->functionSignatureMapProvider->hasMethodSignature($className, $methodName)) { - return $this->mergeSignatures( - $signature, - $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant), - ); + $functionMapSignatures = $this->functionSignatureMapProvider->getMethodSignatures($className, $methodName, $reflectionMethod); + + return $this->getMergedSignatures($signature, $functionMapSignatures); } - return $signature; + return ['positional' => [$signature], 'named' => null]; } - public function getFunctionSignature(string $functionName, ?string $className, int $variant = 0): FunctionSignature + public function getFunctionSignatures(string $functionName, ?string $className, ReflectionFunctionAbstract|null $reflectionFunction): array { $lowerName = strtolower($functionName); - if (!array_key_exists($lowerName, Php8StubsMap::FUNCTIONS)) { - return $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className, $variant); + if (!array_key_exists($lowerName, $this->map->functions)) { + return $this->functionSignatureMapProvider->getFunctionSignatures($functionName, $className, $reflectionFunction); } - if ($variant > 0) { - return $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className, $variant); + $stubFile = self::DIRECTORY . '/' . $this->map->functions[$lowerName]; + $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); + $functions = $nodes->getFunctionNodes(); + if (!array_key_exists($lowerName, $functions)) { + throw new ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); + } + foreach ($functions[$lowerName] as $functionNode) { + if (!$this->isForCurrentVersion($functionNode->getNode()->getAttrGroups())) { + continue; + } + + $signature = $this->getSignature($functionNode->getNode(), null, $stubFile); + if ($this->functionSignatureMapProvider->hasFunctionSignature($functionName)) { + $functionMapSignatures = $this->functionSignatureMapProvider->getFunctionSignatures($functionName, $className, $reflectionFunction); + + return $this->getMergedSignatures($signature, $functionMapSignatures); + } + + return ['positional' => [$signature], 'named' => null]; } - if ($this->functionSignatureMapProvider->hasFunctionSignature($functionName, 1)) { - return $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className, $variant); + throw new ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); + } + + /** + * @param array{positional: array, named: ?array} $functionMapSignatures + * @return array{positional: array, named: ?array} + */ + private function getMergedSignatures(FunctionSignature $nativeSignature, array $functionMapSignatures): array + { + if (count($functionMapSignatures['positional']) === 1) { + return ['positional' => [$this->mergeSignatures($nativeSignature, $functionMapSignatures['positional'][0])], 'named' => null]; } - $stubFile = self::DIRECTORY . '/' . Php8StubsMap::FUNCTIONS[$lowerName]; - $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); - $functions = $nodes->getFunctionNodes(); - if (count($functions) !== 1) { - throw new ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); + if (count($functionMapSignatures['positional']) === 0) { + return ['positional' => [], 'named' => null]; } - $signature = $this->getSignature($functions[$lowerName]->getNode(), null, $stubFile); - if ($this->functionSignatureMapProvider->hasFunctionSignature($functionName)) { - return $this->mergeSignatures( - $signature, - $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className), + $nativeParams = $nativeSignature->getParameters(); + $namedArgumentsVariants = []; + $allParamNamesMatchNative = true; + foreach ($functionMapSignatures['positional'] as $functionMapSignature) { + $isPrevParamVariadic = false; + $hasMiddleVariadicParam = false; + // avoid weird functions like array_diff_uassoc + foreach ($functionMapSignature->getParameters() as $i => $functionParam) { + $nativeParam = $nativeParams[$i] ?? null; + $allParamNamesMatchNative = $allParamNamesMatchNative && $nativeParam !== null && $functionParam->getName() === $nativeParam->getName(); + $hasMiddleVariadicParam = $hasMiddleVariadicParam || $isPrevParamVariadic; + $isPrevParamVariadic = $functionParam->isVariadic() || ( + $nativeParam !== null + ? $nativeParam->isVariadic() + : false + ); + } + + if ($hasMiddleVariadicParam) { + continue; + } + + $parameters = []; + foreach ($functionMapSignature->getParameters() as $i => $functionParam) { + if (!array_key_exists($i, $nativeParams)) { + continue 2; + } + + // it seems that variadic parameters cannot be named in native functions/methods. + $nativeParam = $nativeParams[$i]; + if ($nativeParam->isVariadic()) { + break; + } + + $parameters[] = new ParameterSignature( + $nativeParam->getName(), + $functionParam->isOptional(), + $functionParam->getType(), + $functionParam->getNativeType(), + $functionParam->passedByReference(), + $functionParam->isVariadic(), + $functionParam->getDefaultValue() ?? $nativeParam->getDefaultValue(), + $functionParam->getOutType() ?? $nativeParam->getOutType(), + ); + } + + $namedArgumentsVariants[] = new FunctionSignature( + $parameters, + $functionMapSignature->getReturnType(), + $functionMapSignature->getNativeReturnType(), + $functionMapSignature->isVariadic(), ); } - return $signature; + if ($allParamNamesMatchNative || count($namedArgumentsVariants) === 0) { + $namedArgumentsVariants = null; + } + + return ['positional' => $functionMapSignatures['positional'], 'named' => $namedArgumentsVariants]; } private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSignature $functionMapSignature): FunctionSignature @@ -199,6 +316,8 @@ private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSig $nativeParameterType, $nativeParameter->passedByReference()->yes() ? $functionMapParameter->passedByReference() : $nativeParameter->passedByReference(), $nativeParameter->isVariadic(), + $nativeParameter->getDefaultValue(), + $nativeParameter->getOutType(), ); } @@ -249,11 +368,8 @@ public function getFunctionMetadata(string $functionName): array return $this->functionSignatureMapProvider->getFunctionMetadata($functionName); } - /** - * @param ClassMethod|Function_ $function - */ private function getSignature( - FunctionLike $function, + ClassMethod|Function_ $function, ?string $className, string $stubFile, ): FunctionSignature @@ -280,6 +396,13 @@ private function getSignature( $phpDocReturnType = $phpDoc->getReturnTag()->getType(); } } + + $classReflection = null; + if ($className !== null) { + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + $classReflection = $reflectionProvider->getClass($className); + } + $parameters = []; $variadic = false; foreach ($function->getParams() as $param) { @@ -287,20 +410,38 @@ private function getSignature( if (!$name instanceof Variable || !is_string($name->name)) { throw new ShouldNotHappenException(); } - $parameterType = ParserNodeTypeToPHPStanType::resolve($param->type, null); + $parameterType = ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection); + $phpDocParameterType = $phpDocParameterTypes[$name->name] ?? null; + + if ($param->default instanceof ConstFetch) { + $constName = (string) $param->default->name; + $loweredConstName = strtolower($constName); + if ($loweredConstName === 'null') { + $parameterType = TypeCombinator::addNull($parameterType); + if ($phpDocParameterType !== null) { + $phpDocParameterType = TypeCombinator::addNull($phpDocParameterType); + } + } + } + $parameters[] = new ParameterSignature( $name->name, $param->default !== null || $param->variadic, - TypehintHelper::decideType($parameterType, $phpDocParameterTypes[$name->name] ?? null), + TypehintHelper::decideType($parameterType, $phpDocParameterType), $parameterType, $param->byRef ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), $param->variadic, + $param->default !== null ? $this->initializerExprTypeResolver->getType( + $param->default, + InitializerExprContext::fromStubParameter($className, $stubFile, $function), + ) : null, + null, ); $variadic = $variadic || $param->variadic; } - $returnType = ParserNodeTypeToPHPStanType::resolve($function->getReturnType(), null); + $returnType = ParserNodeTypeToPHPStanType::resolve($function->getReturnType(), $classReflection); return new FunctionSignature( $parameters, @@ -310,4 +451,76 @@ private function getSignature( ); } + public function hasClassConstantMetadata(string $className, string $constantName): bool + { + $lowerClassName = strtolower($className); + if (!array_key_exists($lowerClassName, $this->map->classes)) { + return false; + } + + return $this->findConstantType($className, $constantName) !== null; + } + + public function getClassConstantMetadata(string $className, string $constantName): array + { + $lowerClassName = strtolower($className); + if (!array_key_exists($lowerClassName, $this->map->classes)) { + throw new ShouldNotHappenException(); + } + + $type = $this->findConstantType($className, $constantName); + if ($type === null) { + throw new ShouldNotHappenException(); + } + + return [ + 'nativeType' => $type, + ]; + } + + private function findConstantType(string $className, string $constantName): ?Type + { + $lowerClassName = strtolower($className); + $lowerConstantName = strtolower($constantName); + if (isset($this->constantTypes[$lowerClassName][$lowerConstantName])) { + return $this->constantTypes[$lowerClassName][$lowerConstantName]; + } + + $stubFile = self::DIRECTORY . '/' . $this->map->classes[$lowerClassName]; + $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); + $classes = $nodes->getClassNodes(); + if (count($classes) !== 1) { + throw new ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); + } + + $class = $classes[$lowerClassName]; + if (count($class) !== 1) { + throw new ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); + } + + foreach ($class[0]->getNode()->stmts as $stmt) { + if (!$stmt instanceof ClassConst) { + continue; + } + + foreach ($stmt->consts as $const) { + if ($const->name->toString() !== $constantName) { + continue; + } + + if (!$this->isForCurrentVersion($stmt->attrGroups)) { + continue; + } + + if ($stmt->type === null) { + return null; + } + + return $this->constantTypes[$lowerClassName][$lowerConstantName] = ParserNodeTypeToPHPStanType::resolve($stmt->type, null); + } + } + + return null; + } + } diff --git a/src/Reflection/SignatureMap/SignatureMapParser.php b/src/Reflection/SignatureMap/SignatureMapParser.php index 68cf3fbdf6..e60cede66d 100644 --- a/src/Reflection/SignatureMap/SignatureMapParser.php +++ b/src/Reflection/SignatureMap/SignatureMapParser.php @@ -10,10 +10,10 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use function array_slice; -use function strpos; +use function str_starts_with; use function substr; -class SignatureMapParser +final class SignatureMapParser { private TypeStringResolver $typeStringResolver; @@ -57,7 +57,7 @@ private function getTypeFromString(string $typeString, ?string $className): Type /** * @param array $parameterMap - * @return array + * @return list */ private function getParameters(array $parameterMap): array { @@ -71,6 +71,8 @@ private function getParameters(array $parameterMap): array new MixedType(), $passedByReference, $isVariadic, + null, + null, ); } @@ -93,13 +95,13 @@ private function getParameterInfoFromName(string $parameterNameString): array $isVariadic = $matches['variadic'] !== ''; $reference = $matches['reference']; - if (strpos($reference, '&...') === 0) { + if (str_starts_with($reference, '&...')) { $reference = '&' . substr($reference, 4); $isVariadic = true; } - if (strpos($reference, '&rw') === 0) { + if (str_starts_with($reference, '&rw')) { $passedByReference = PassedByReference::createReadsArgument(); - } elseif (strpos($reference, '&w') === 0 || strpos($reference, '&') === 0) { + } elseif (str_starts_with($reference, '&')) { $passedByReference = PassedByReference::createCreatesNewVariable(); } else { $passedByReference = PassedByReference::createNo(); diff --git a/src/Reflection/SignatureMap/SignatureMapProvider.php b/src/Reflection/SignatureMap/SignatureMapProvider.php index b721ea2b04..f7ec5ed5ce 100644 --- a/src/Reflection/SignatureMap/SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/SignatureMapProvider.php @@ -2,18 +2,22 @@ namespace PHPStan\Reflection\SignatureMap; -use ReflectionMethod; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\Type\Type; +use ReflectionFunctionAbstract; interface SignatureMapProvider { - public function hasMethodSignature(string $className, string $methodName, int $variant = 0): bool; + public function hasMethodSignature(string $className, string $methodName): bool; - public function hasFunctionSignature(string $name, int $variant = 0): bool; + public function hasFunctionSignature(string $name): bool; - public function getMethodSignature(string $className, string $methodName, ?ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature; + /** @return array{positional: array, named: ?array} */ + public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array; - public function getFunctionSignature(string $functionName, ?string $className, int $variant = 0): FunctionSignature; + /** @return array{positional: array, named: ?array} */ + public function getFunctionSignatures(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): array; public function hasMethodMetadata(string $className, string $methodName): bool; @@ -29,4 +33,11 @@ public function getMethodMetadata(string $className, string $methodName): array; */ public function getFunctionMetadata(string $functionName): array; + public function hasClassConstantMetadata(string $className, string $constantName): bool; + + /** + * @return array{nativeType: Type} + */ + public function getClassConstantMetadata(string $className, string $constantName): array; + } diff --git a/src/Reflection/SignatureMap/SignatureMapProviderFactory.php b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php index 2e318eefeb..4aa6d510da 100644 --- a/src/Reflection/SignatureMap/SignatureMapProviderFactory.php +++ b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php @@ -4,7 +4,7 @@ use PHPStan\Php\PhpVersion; -class SignatureMapProviderFactory +final class SignatureMapProviderFactory { public function __construct( diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 9c773dfc77..ea6b278145 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -2,16 +2,23 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use function sprintf; -/** @api */ -class TrivialParametersAcceptor implements ParametersAcceptor +/** + * @api + */ +final class TrivialParametersAcceptor implements ExtendedParametersAcceptor, CallableParametersAcceptor { /** @api */ - public function __construct() + public function __construct(private string $callableName = 'callable') { } @@ -25,9 +32,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return TemplateTypeMap::createEmpty(); } - /** - * @return array - */ + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + public function getParameters(): array { return []; @@ -43,4 +52,50 @@ public function getReturnType(): Type return new MixedType(); } + public function getPhpDocReturnType(): Type + { + return new MixedType(); + } + + public function getNativeReturnType(): Type + { + return new MixedType(); + } + + public function getThrowPoints(): array + { + return []; + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + return [ + new SimpleImpurePoint( + 'functionCall', + sprintf('call to a %s', $this->callableName), + false, + ), + ]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + } diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 51e964c685..980a3f293f 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -4,22 +4,24 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\Reflection\ResolvedMethodReflection; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function array_map; -class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { /** @var callable(Type): Type */ private $transformStaticTypeCallback; - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -27,7 +29,7 @@ class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPro * @param callable(Type): Type $transformStaticTypeCallback */ public function __construct( - private MethodReflection $methodReflection, + private ExtendedMethodReflection $methodReflection, private ClassReflection $resolvedDeclaringClass, private bool $resolveTemplateTypeMapToBounds, callable $transformStaticTypeCallback, @@ -50,21 +52,23 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype ); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->methodReflection; } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedMethod = new ResolvedMethodReflection( $this->transformMethodWithStaticType($this->resolvedDeclaringClass, $this->methodReflection), $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -78,24 +82,57 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio ); } - private function transformMethodWithStaticType(ClassReflection $declaringClass, MethodReflection $method): MethodReflection + private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variants = array_map(fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( - $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); + $selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null; + $variantFn = function (ExtendedParametersAcceptor $acceptor) use (&$selfOutType): ExtendedParametersAcceptor { + $originalReturnType = $acceptor->getReturnType(); + if ($originalReturnType instanceof ThisType && $selfOutType !== null) { + $returnType = TypeCombinator::intersect($selfOutType, $this->transformStaticType($originalReturnType)); + $selfOutType = $returnType; + } else { + $returnType = $this->transformStaticType($originalReturnType); + } + return new ExtendedFunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map( + fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $parameter->getNativeType(), + $this->transformStaticType($parameter->getPhpDocType()), + $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, + $parameter->getAttributes(), + ), + $acceptor->getParameters(), + ), + $acceptor->isVariadic(), + $returnType, + $this->transformStaticType($acceptor->getPhpDocReturnType()), + $this->transformStaticType($acceptor->getNativeReturnType()), + $acceptor->getCallSiteVarianceMap(), + ); + }; + $variants = array_map($variantFn, $method->getVariants()); + $namedArgumentVariants = $method->getNamedArgumentsVariants(); + $namedArgumentVariants = $namedArgumentVariants !== null + ? array_map($variantFn, $namedArgumentVariants) + : null; + + return new ChangedTypeMethodReflection( + $declaringClass, + $method, + $variants, + $namedArgumentVariants, + $selfOutType, + ); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php index ab53a24f99..06069f8410 100644 --- a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php @@ -4,17 +4,17 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypePropertyReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ResolvedPropertyReflection; use PHPStan\Type\Type; -class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { /** @var callable(Type): Type */ private $transformStaticTypeCallback; - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -22,7 +22,7 @@ class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropert * @param callable(Type): Type $transformStaticTypeCallback */ public function __construct( - private PropertyReflection $propertyReflection, + private ExtendedPropertyReflection $propertyReflection, private ClassReflection $resolvedDeclaringClass, private bool $resolveTemplateTypeMapToBounds, callable $transformStaticTypeCallback, @@ -45,21 +45,23 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy ); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->propertyReflection; } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedProperty = new ResolvedPropertyReflection( $this->transformPropertyWithStaticType($this->resolvedDeclaringClass, $this->propertyReflection), $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -73,12 +75,14 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect ); } - private function transformPropertyWithStaticType(ClassReflection $declaringClass, PropertyReflection $property): PropertyReflection + private function transformPropertyWithStaticType(ClassReflection $declaringClass, ExtendedPropertyReflection $property): ExtendedPropertyReflection { $readableType = $this->transformStaticType($property->getReadableType()); $writableType = $this->transformStaticType($property->getWritableType()); + $phpDocType = $this->transformStaticType($property->getPhpDocType()); + $nativeType = $this->transformStaticType($property->getNativeType()); - return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType); + return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType, $phpDocType, $nativeType); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index dfd47cbef2..c78a435583 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -4,26 +4,29 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\Reflection\ResolvedMethodReflection; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\StaticType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use function array_map; +use function count; -class CalledOnTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class CalledOnTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; public function __construct( - private MethodReflection $methodReflection, + private ExtendedMethodReflection $methodReflection, private ClassReflection $resolvedDeclaringClass, private bool $resolveTemplateTypeMapToBounds, private Type $calledOnType, @@ -45,21 +48,23 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype ); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->methodReflection; } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedMethod = new ResolvedMethodReflection( $this->transformMethodWithStaticType($this->resolvedDeclaringClass, $this->methodReflection), $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -73,29 +78,71 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio ); } - private function transformMethodWithStaticType(ClassReflection $declaringClass, MethodReflection $method): MethodReflection + private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variants = array_map(fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( - $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); + $selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null; + $variantFn = function (ExtendedParametersAcceptor $acceptor) use ($selfOutType): ExtendedParametersAcceptor { + $originalReturnType = $acceptor->getReturnType(); + if ($originalReturnType instanceof ThisType && $selfOutType !== null) { + $returnType = $selfOutType; + } else { + $returnType = $this->transformStaticType($originalReturnType); + } + return new ExtendedFunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map( + fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $parameter->getNativeType(), + $this->transformStaticType($parameter->getPhpDocType()), + $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, + $parameter->getAttributes(), + ), + $acceptor->getParameters(), + ), + $acceptor->isVariadic(), + $returnType, + $this->transformStaticType($acceptor->getPhpDocReturnType()), + $this->transformStaticType($acceptor->getNativeReturnType()), + $acceptor->getCallSiteVarianceMap(), + ); + }; + $variants = array_map($variantFn, $method->getVariants()); + $namedArgumentsVariants = $method->getNamedArgumentsVariants(); + $namedArgumentsVariants = $namedArgumentsVariants !== null + ? array_map($variantFn, $namedArgumentsVariants) + : null; + + return new ChangedTypeMethodReflection( + $declaringClass, + $method, + $variants, + $namedArgumentsVariants, + $selfOutType, + ); } private function transformStaticType(Type $type): Type { return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { + if ($type instanceof GenericStaticType) { + $calledOnTypeReflections = $this->calledOnType->getObjectClassReflections(); + if (count($calledOnTypeReflections) === 1) { + $calledOnTypeReflection = $calledOnTypeReflections[0]; + + return $traverse($type->changeBaseClass($calledOnTypeReflection)->getStaticObjectType()); + } + + return $this->calledOnType; + } if ($type instanceof StaticType) { return $this->calledOnType; } diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php index 13e7f5b875..18beaf3f8e 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php @@ -4,21 +4,21 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypePropertyReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ResolvedPropertyReflection; use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -class CalledOnTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class CalledOnTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; public function __construct( - private PropertyReflection $propertyReflection, + private ExtendedPropertyReflection $propertyReflection, private ClassReflection $resolvedDeclaringClass, private bool $resolveTemplateTypeMapToBounds, private Type $fetchedOnType, @@ -40,21 +40,23 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy ); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->propertyReflection; } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedProperty = new ResolvedPropertyReflection( $this->transformPropertyWithStaticType($this->resolvedDeclaringClass, $this->propertyReflection), $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -68,12 +70,14 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect ); } - private function transformPropertyWithStaticType(ClassReflection $declaringClass, PropertyReflection $property): PropertyReflection + private function transformPropertyWithStaticType(ClassReflection $declaringClass, ExtendedPropertyReflection $property): ExtendedPropertyReflection { $readableType = $this->transformStaticType($property->getReadableType()); $writableType = $this->transformStaticType($property->getWritableType()); + $phpDocType = $this->transformStaticType($property->getPhpDocType()); + $nativeType = $this->transformStaticType($property->getNativeType()); - return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType); + return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType, $phpDocType, $nativeType); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index 8c547ca3d0..eafd314157 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -2,23 +2,28 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariant; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function array_map; use function count; use function implode; +use function is_bool; -class IntersectionTypeMethodReflection implements MethodReflection +final class IntersectionTypeMethodReflection implements ExtendedMethodReflection { /** - * @param MethodReflection[] $methods + * @param ExtendedMethodReflection[] $methods */ public function __construct(private string $methodName, private array $methods) { @@ -75,19 +80,39 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { $returnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getReturnType(), $method->getVariants())), $this->methods)); + $phpDocReturnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getPhpDocReturnType(), $method->getVariants())), $this->methods)); + $nativeReturnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getNativeReturnType(), $method->getVariants())), $this->methods)); - return array_map(static fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( + return array_map(static fn (ExtendedParametersAcceptor $acceptor): ExtendedParametersAcceptor => new ExtendedFunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), $acceptor->getParameters(), $acceptor->isVariadic(), $returnType, + $phpDocReturnType, + $nativeReturnType, + $acceptor->getCallSiteVarianceMap(), ), $this->methods[0]->getVariants()); } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + $variants = $this->getVariants(); + if (count($variants) !== 1) { + throw new ShouldNotHappenException(); + } + + return $variants[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated(), $this->methods)); + return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated()); } public function getDeprecatedDescription(): ?string @@ -114,12 +139,22 @@ public function getDeprecatedDescription(): ?string public function isFinal(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isFinal(), $this->methods)); + return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isFinal()); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isFinalByKeyword()); } public function isInternal(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isInternal(), $this->methods)); + return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isInternal()); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => is_bool($method->isBuiltin()) ? TrinaryLogic::createFromBoolean($method->isBuiltin()) : $method->isBuiltin()); } public function getThrowType(): ?Type @@ -144,7 +179,12 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects(), $this->methods)); + return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects()); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isPure()); } public function getDocComment(): ?string @@ -152,4 +192,40 @@ public function getDocComment(): ?string return null; } + public function getAsserts(): Assertions + { + $assertions = Assertions::createEmpty(); + + foreach ($this->methods as $method) { + $assertions = $assertions->intersectWith($method->getAsserts()); + } + + return $assertions; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->acceptsNamedArguments()); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->returnsByReference()); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => is_bool($method->isAbstract()) ? TrinaryLogic::createFromBoolean($method->isAbstract()) : $method->isAbstract()); + } + + public function getAttributes(): array + { + return $this->methods[0]->getAttributes(); + } + } diff --git a/src/Reflection/Type/IntersectionTypePropertyReflection.php b/src/Reflection/Type/IntersectionTypePropertyReflection.php index 68db339d3c..71de28e1e6 100644 --- a/src/Reflection/Type/IntersectionTypePropertyReflection.php +++ b/src/Reflection/Type/IntersectionTypePropertyReflection.php @@ -3,7 +3,9 @@ namespace PHPStan\Reflection\Type; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -11,16 +13,21 @@ use function count; use function implode; -class IntersectionTypePropertyReflection implements PropertyReflection +final class IntersectionTypePropertyReflection implements ExtendedPropertyReflection { /** - * @param PropertyReflection[] $properties + * @param ExtendedPropertyReflection[] $properties */ public function __construct(private array $properties) { } + public function getName(): string + { + return $this->properties[0]->getName(); + } + public function getDeclaringClass(): ClassReflection { return $this->properties[0]->getDeclaringClass(); @@ -28,40 +35,22 @@ public function getDeclaringClass(): ClassReflection public function isStatic(): bool { - foreach ($this->properties as $property) { - if ($property->isStatic()) { - return true; - } - } - - return false; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isStatic()); } public function isPrivate(): bool { - foreach ($this->properties as $property) { - if (!$property->isPrivate()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivate()); } public function isPublic(): bool { - foreach ($this->properties as $property) { - if ($property->isPublic()) { - return true; - } - } - - return false; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPublic()); } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static fn (PropertyReflection $property): TrinaryLogic => $property->isDeprecated(), $this->properties)); + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); } public function getDeprecatedDescription(): ?string @@ -88,7 +77,7 @@ public function getDeprecatedDescription(): ?string public function isInternal(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static fn (PropertyReflection $property): TrinaryLogic => $property->isInternal(), $this->properties)); + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); } public function getDocComment(): ?string @@ -96,47 +85,119 @@ public function getDocComment(): ?string return null; } + public function hasPhpDocType(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasPhpDocType()); + } + + public function getPhpDocType(): Type + { + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getPhpDocType(), $this->properties)); + } + + public function hasNativeType(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasNativeType()); + } + + public function getNativeType(): Type + { + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getNativeType(), $this->properties)); + } + public function getReadableType(): Type { - return TypeCombinator::intersect(...array_map(static fn (PropertyReflection $property): Type => $property->getReadableType(), $this->properties)); + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties)); } public function getWritableType(): Type { - return TypeCombinator::intersect(...array_map(static fn (PropertyReflection $property): Type => $property->getWritableType(), $this->properties)); + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties)); } public function canChangeTypeAfterAssignment(): bool { - foreach ($this->properties as $property) { - if (!$property->canChangeTypeAfterAssignment()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->canChangeTypeAfterAssignment()); } public function isReadable(): bool { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isReadable()); + } + + public function isWritable(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isWritable()); + } + + /** + * @param callable(ExtendedPropertyReflection): bool $cb + */ + private function computeResult(callable $cb): bool + { + $result = false; foreach ($this->properties as $property) { - if (!$property->isReadable()) { - return false; - } + $result = $result || $cb($property); } - return true; + return $result; } - public function isWritable(): bool + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract()); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinal()); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isVirtual()); + } + + public function hasHook(string $hookType): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasHook($hookType)); + } + + public function getHook(string $hookType): ExtendedMethodReflection { + $hooks = []; foreach ($this->properties as $property) { - if (!$property->isWritable()) { - return false; + if (!$property->hasHook($hookType)) { + continue; } + + $hooks[] = $property->getHook($hookType); } - return true; + if (count($hooks) === 0) { + throw new ShouldNotHappenException(); + } + + if (count($hooks) === 1) { + return $hooks[0]; + } + + return new IntersectionTypeMethodReflection($hooks[0]->getName(), $hooks); + } + + public function isProtectedSet(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isProtectedSet()); + } + + public function isPrivateSet(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivateSet()); + } + + public function getAttributes(): array + { + return $this->properties[0]->getAttributes(); } } diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php index be6d2d944f..fe3e09bd4e 100644 --- a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php @@ -2,14 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Type; use function array_map; -class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -32,12 +33,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->getTransformedMethod(); } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php index fdfea4ebf9..51e6ccaf46 100644 --- a/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php @@ -2,14 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Type; use function array_map; -class IntersectionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class IntersectionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -32,12 +33,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->getTransformedProperty(); } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index cde9bb3f69..b330b6fdad 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -2,23 +2,27 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function array_map; +use function array_merge; use function count; use function implode; +use function is_bool; -class UnionTypeMethodReflection implements MethodReflection +final class UnionTypeMethodReflection implements ExtendedMethodReflection { /** - * @param MethodReflection[] $methods + * @param ExtendedMethodReflection[] $methods */ public function __construct(private string $methodName, private array $methods) { @@ -74,21 +78,24 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { - $variants = $this->methods[0]->getVariants(); - $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)); + $variants = array_merge(...array_map(static fn (MethodReflection $method) => $method->getVariants(), $this->methods)); - return array_map(static fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - $acceptor->getParameters(), - $acceptor->isVariadic(), - $returnType, - ), $variants); + return [ParametersAcceptorSelector::combineAcceptors($variants)]; + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated(), $this->methods)); + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated()); } public function getDeprecatedDescription(): ?string @@ -115,12 +122,22 @@ public function getDeprecatedDescription(): ?string public function isFinal(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isFinal(), $this->methods)); + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isFinal()); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isFinalByKeyword()); } public function isInternal(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isInternal(), $this->methods)); + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isInternal()); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => is_bool($method->isBuiltin()) ? TrinaryLogic::createFromBoolean($method->isBuiltin()) : $method->isBuiltin()); } public function getThrowType(): ?Type @@ -145,7 +162,12 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects(), $this->methods)); + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects()); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isPure()); } public function getDocComment(): ?string @@ -153,4 +175,34 @@ public function getDocComment(): ?string return null; } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->acceptsNamedArguments()); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->returnsByReference()); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => is_bool($method->isAbstract()) ? TrinaryLogic::createFromBoolean($method->isAbstract()) : $method->isAbstract()); + } + + public function getAttributes(): array + { + return $this->methods[0]->getAttributes(); + } + } diff --git a/src/Reflection/Type/UnionTypePropertyReflection.php b/src/Reflection/Type/UnionTypePropertyReflection.php index d14cfa7b7a..77e1ed0397 100644 --- a/src/Reflection/Type/UnionTypePropertyReflection.php +++ b/src/Reflection/Type/UnionTypePropertyReflection.php @@ -3,7 +3,9 @@ namespace PHPStan\Reflection\Type; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -11,16 +13,21 @@ use function count; use function implode; -class UnionTypePropertyReflection implements PropertyReflection +final class UnionTypePropertyReflection implements ExtendedPropertyReflection { /** - * @param PropertyReflection[] $properties + * @param ExtendedPropertyReflection[] $properties */ public function __construct(private array $properties) { } + public function getName(): string + { + return $this->properties[0]->getName(); + } + public function getDeclaringClass(): ClassReflection { return $this->properties[0]->getDeclaringClass(); @@ -28,40 +35,22 @@ public function getDeclaringClass(): ClassReflection public function isStatic(): bool { - foreach ($this->properties as $property) { - if (!$property->isStatic()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isStatic()); } public function isPrivate(): bool { - foreach ($this->properties as $property) { - if ($property->isPrivate()) { - return true; - } - } - - return false; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivate()); } public function isPublic(): bool { - foreach ($this->properties as $property) { - if (!$property->isPublic()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPublic()); } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static fn (PropertyReflection $property): TrinaryLogic => $property->isDeprecated(), $this->properties)); + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); } public function getDeprecatedDescription(): ?string @@ -88,7 +77,7 @@ public function getDeprecatedDescription(): ?string public function isInternal(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static fn (PropertyReflection $property): TrinaryLogic => $property->isInternal(), $this->properties)); + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); } public function getDocComment(): ?string @@ -96,47 +85,119 @@ public function getDocComment(): ?string return null; } + public function hasPhpDocType(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasPhpDocType()); + } + + public function getPhpDocType(): Type + { + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getPhpDocType(), $this->properties)); + } + + public function hasNativeType(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasNativeType()); + } + + public function getNativeType(): Type + { + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getNativeType(), $this->properties)); + } + public function getReadableType(): Type { - return TypeCombinator::union(...array_map(static fn (PropertyReflection $property): Type => $property->getReadableType(), $this->properties)); + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties)); } public function getWritableType(): Type { - return TypeCombinator::union(...array_map(static fn (PropertyReflection $property): Type => $property->getWritableType(), $this->properties)); + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties)); } public function canChangeTypeAfterAssignment(): bool { - foreach ($this->properties as $property) { - if (!$property->canChangeTypeAfterAssignment()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->canChangeTypeAfterAssignment()); } public function isReadable(): bool { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isReadable()); + } + + public function isWritable(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isWritable()); + } + + /** + * @param callable(ExtendedPropertyReflection): bool $cb + */ + private function computeResult(callable $cb): bool + { + $result = true; foreach ($this->properties as $property) { - if (!$property->isReadable()) { - return false; - } + $result = $result && $cb($property); } - return true; + return $result; } - public function isWritable(): bool + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract()); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinal()); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isVirtual()); + } + + public function hasHook(string $hookType): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->hasHook($hookType)); + } + + public function getHook(string $hookType): ExtendedMethodReflection { + $hooks = []; foreach ($this->properties as $property) { - if (!$property->isWritable()) { - return false; + if (!$property->hasHook($hookType)) { + continue; } + + $hooks[] = $property->getHook($hookType); } - return true; + if (count($hooks) === 0) { + throw new ShouldNotHappenException(); + } + + if (count($hooks) === 1) { + return $hooks[0]; + } + + return new UnionTypeMethodReflection($hooks[0]->getName(), $hooks); + } + + public function isProtectedSet(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isProtectedSet()); + } + + public function isPrivateSet(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivateSet()); + } + + public function getAttributes(): array + { + return $this->properties[0]->getAttributes(); } } diff --git a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php index 4d8cf48a19..627a1e5b80 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php @@ -2,14 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Type; use function array_map; -class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection +final class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -32,12 +33,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->getTransformedMethod(); } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; diff --git a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php index 746992aadc..b28625e3c3 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php @@ -2,16 +2,15 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Type; use function array_map; -class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection +final class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private string $propertyName; - - private ?PropertyReflection $transformedProperty = null; + private ?ExtendedPropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -19,11 +18,10 @@ class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedProper * @param UnresolvedPropertyPrototypeReflection[] $propertyPrototypes */ public function __construct( - string $methodName, + private string $propertyName, private array $propertyPrototypes, ) { - $this->propertyName = $methodName; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototypeReflection @@ -34,12 +32,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); } - public function getNakedProperty(): PropertyReflection + public function getNakedProperty(): ExtendedPropertyReflection { return $this->getTransformedProperty(); } - public function getTransformedProperty(): PropertyReflection + public function getTransformedProperty(): ExtendedPropertyReflection { if ($this->transformedProperty !== null) { return $this->transformedProperty; diff --git a/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php index 27f36dfaa3..4665670cb4 100644 --- a/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Type\Type; interface UnresolvedMethodPrototypeReflection @@ -10,9 +10,9 @@ interface UnresolvedMethodPrototypeReflection public function doNotResolveTemplateTypeMapToBounds(): self; - public function getNakedMethod(): MethodReflection; + public function getNakedMethod(): ExtendedMethodReflection; - public function getTransformedMethod(): MethodReflection; + public function getTransformedMethod(): ExtendedMethodReflection; public function withCalledOnType(Type $type): self; diff --git a/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php index 867f3c1dda..441d4a36c3 100644 --- a/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Type\Type; interface UnresolvedPropertyPrototypeReflection @@ -10,9 +10,9 @@ interface UnresolvedPropertyPrototypeReflection public function doNotResolveTemplateTypeMapToBounds(): self; - public function getNakedProperty(): PropertyReflection; + public function getNakedProperty(): ExtendedPropertyReflection; - public function getTransformedProperty(): PropertyReflection; + public function getTransformedProperty(): ExtendedPropertyReflection; public function withFechedOnType(Type $type): self; diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php new file mode 100644 index 0000000000..5a9ea238cf --- /dev/null +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -0,0 +1,176 @@ +method->getDeclaringClass(); + } + + public function isStatic(): bool + { + return $this->method->isStatic(); + } + + public function isPrivate(): bool + { + return $this->method->isPrivate(); + } + + public function isPublic(): bool + { + return $this->method->isPublic(); + } + + public function getDocComment(): ?string + { + return $this->method->getDocComment(); + } + + public function getName(): string + { + return $this->method->getName(); + } + + public function getPrototype(): ClassMemberReflection + { + return $this->method->getPrototype(); + } + + public function getVariants(): array + { + $variants = []; + foreach ($this->method->getVariants() as $variant) { + if ($variant instanceof ExtendedParametersAcceptor) { + $variants[] = $variant; + continue; + } + + $variants[] = new ExtendedFunctionVariant( + $variant->getTemplateTypeMap(), + $variant->getResolvedTemplateTypeMap(), + array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => $parameter instanceof ExtendedParameterReflection ? $parameter : new ExtendedDummyParameter( + $parameter->getName(), + $parameter->getType(), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + new MixedType(), + $parameter->getType(), + null, + TrinaryLogic::createMaybe(), + null, + [], + ), $variant->getParameters()), + $variant->isVariadic(), + $variant->getReturnType(), + $variant->getReturnType(), + new MixedType(), + TemplateTypeVarianceMap::createEmpty(), + ); + } + + return $variants; + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + + public function isDeprecated(): TrinaryLogic + { + return $this->method->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->method->getDeprecatedDescription(); + } + + public function isFinal(): TrinaryLogic + { + return $this->method->isFinal(); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return $this->isFinal(); + } + + public function isInternal(): TrinaryLogic + { + return $this->method->isInternal(); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getThrowType(): ?Type + { + return $this->method->getThrowType(); + } + + public function hasSideEffects(): TrinaryLogic + { + return $this->method->hasSideEffects(); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getDeclaringClass()->acceptsNamedArguments()); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getAttributes(): array + { + return []; + } + +} diff --git a/src/Reflection/WrappedExtendedPropertyReflection.php b/src/Reflection/WrappedExtendedPropertyReflection.php new file mode 100644 index 0000000000..04eceb4848 --- /dev/null +++ b/src/Reflection/WrappedExtendedPropertyReflection.php @@ -0,0 +1,147 @@ +name; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->property->getDeclaringClass(); + } + + public function isStatic(): bool + { + return $this->property->isStatic(); + } + + public function isPrivate(): bool + { + return $this->property->isPrivate(); + } + + public function isPublic(): bool + { + return $this->property->isPublic(); + } + + public function getDocComment(): ?string + { + return $this->property->getDocComment(); + } + + public function hasPhpDocType(): bool + { + return false; + } + + public function getPhpDocType(): Type + { + return new MixedType(); + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + + public function getReadableType(): Type + { + return $this->property->getReadableType(); + } + + public function getWritableType(): Type + { + return $this->property->getWritableType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return $this->property->canChangeTypeAfterAssignment(); + } + + public function isReadable(): bool + { + return $this->property->isReadable(); + } + + public function isWritable(): bool + { + return $this->property->isWritable(); + } + + public function isDeprecated(): TrinaryLogic + { + return $this->property->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->property->getDeprecatedDescription(); + } + + public function isInternal(): TrinaryLogic + { + return $this->property->isInternal(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function hasHook(string $hookType): bool + { + return false; + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + throw new ShouldNotHappenException(); + } + + public function isProtectedSet(): bool + { + return false; + } + + public function isPrivateSet(): bool + { + return false; + } + + public function getAttributes(): array + { + return []; + } + +} diff --git a/src/Reflection/WrapperPropertyReflection.php b/src/Reflection/WrapperPropertyReflection.php index c34a3c7cfb..e7bb397ace 100644 --- a/src/Reflection/WrapperPropertyReflection.php +++ b/src/Reflection/WrapperPropertyReflection.php @@ -2,9 +2,9 @@ namespace PHPStan\Reflection; -interface WrapperPropertyReflection extends PropertyReflection +interface WrapperPropertyReflection extends ExtendedPropertyReflection { - public function getOriginalReflection(): PropertyReflection; + public function getOriginalReflection(): ExtendedPropertyReflection; } diff --git a/src/Rules/Api/ApiClassConstFetchRule.php b/src/Rules/Api/ApiClassConstFetchRule.php new file mode 100644 index 0000000000..1503fe6f07 --- /dev/null +++ b/src/Rules/Api/ApiClassConstFetchRule.php @@ -0,0 +1,89 @@ + + */ +final class ApiClassConstFetchRule implements Rule +{ + + public function __construct( + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + 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 Node\Name) { + return []; + } + + $className = $scope->resolveName($node->class); + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName())) { + return []; + } + + $ruleError = RuleErrorBuilder::message(sprintf( + 'Accessing %s::%s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', + $classReflection->getDisplayName(), + $node->name->toString(), + ))->identifier('phpstanApi.classConstant')->tip(sprintf( + "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ))->build(); + + $docBlock = $classReflection->getResolvedPhpDoc(); + if ($docBlock !== null) { + foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { + $apiTags = $phpDocNode->getTagsByName('@api'); + if (count($apiTags) > 0) { + return []; + } + } + } + + if ($node->name->toLowerString() === 'class') { + foreach ($classReflection->getNativeReflection()->getMethods() as $methodReflection) { + $methodDocComment = $methodReflection->getDocComment(); + if ($methodDocComment === false) { + continue; + } + + if (!str_contains($methodDocComment, '@api')) { + continue; + } + + return []; + } + } + + return [$ruleError]; + } + +} diff --git a/src/Rules/Api/ApiClassExtendsRule.php b/src/Rules/Api/ApiClassExtendsRule.php index dd90c44f5c..cf908f004b 100644 --- a/src/Rules/Api/ApiClassExtendsRule.php +++ b/src/Rules/Api/ApiClassExtendsRule.php @@ -15,7 +15,7 @@ /** * @implements Rule */ -class ApiClassExtendsRule implements Rule +final class ApiClassExtendsRule implements Rule { public function __construct( @@ -42,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array } $extendedClassReflection = $this->reflectionProvider->getClass($extendedClassName); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedClassReflection->getName(), $extendedClassReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedClassReflection->getName(), $extendedClassReflection->getFileName())) { return []; } @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Extending %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', $extendedClassReflection->getDisplayName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.class')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); diff --git a/src/Rules/Api/ApiClassImplementsRule.php b/src/Rules/Api/ApiClassImplementsRule.php index 2054fa1cd4..d1615303b5 100644 --- a/src/Rules/Api/ApiClassImplementsRule.php +++ b/src/Rules/Api/ApiClassImplementsRule.php @@ -6,18 +6,18 @@ use PhpParser\Node\Stmt\Class_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Type; use function array_merge; use function count; +use function in_array; use function sprintf; /** * @implements Rule */ -class ApiClassImplementsRule implements Rule +final class ApiClassImplementsRule implements Rule { public function __construct( @@ -43,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function checkName(Scope $scope, Node\Name $name): array { @@ -53,19 +53,19 @@ private function checkName(Scope $scope, Node\Name $name): array } $implementedClassReflection = $this->reflectionProvider->getClass($implementedClassName); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $implementedClassReflection->getName(), $implementedClassReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $implementedClassReflection->getName(), $implementedClassReflection->getFileName())) { return []; } $ruleError = RuleErrorBuilder::message(sprintf( 'Implementing %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', $implementedClassReflection->getDisplayName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.interface')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); - if ($implementedClassReflection->getName() === Type::class) { + if (in_array($implementedClassReflection->getName(), BcUncoveredInterface::CLASSES, true)) { return [$ruleError]; } diff --git a/src/Rules/Api/ApiInstanceofRule.php b/src/Rules/Api/ApiInstanceofRule.php new file mode 100644 index 0000000000..a176905563 --- /dev/null +++ b/src/Rules/Api/ApiInstanceofRule.php @@ -0,0 +1,118 @@ + + */ +final class ApiInstanceofRule implements Rule +{ + + public function __construct( + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\Instanceof_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->class instanceof Node\Name) { + return []; + } + + $className = $scope->resolveName($node->class); + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName())) { + return []; + } + + $ruleError = RuleErrorBuilder::message(sprintf( + 'Asking about instanceof %s is not covered by backward compatibility promise. The %s might change in a minor PHPStan version.', + $classReflection->getDisplayName(), + strtolower($classReflection->getClassTypeDescription()), + )) + ->identifier(sprintf('phpstanApi.%s', strtolower($classReflection->getClassTypeDescription()))) + ->tip(sprintf( + "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ))->build(); + + $docBlock = $classReflection->getResolvedPhpDoc(); + if ($docBlock === null) { + return [$ruleError]; + } + + foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { + $apiTags = $phpDocNode->getTagsByName('@api'); + if (count($apiTags) > 0) { + return $this->processCoveredClass($node, $scope, $classReflection); + } + } + + return [$ruleError]; + } + + /** + * @return list + */ + private function processCoveredClass(Node\Expr\Instanceof_ $node, Scope $scope, ClassReflection $classReflection): array + { + if ($classReflection->is(Type::class)) { + return []; + } + if ($classReflection->isInterface()) { + return []; + } + + $instanceofType = $scope->getType($node); + if ($instanceofType->isTrue()->or($instanceofType->isFalse())->yes()) { + return []; + } + + $classType = new ObjectType($classReflection->getName(), null, $classReflection); + + $exprType = $scope->getType($node->expr); + if ($exprType instanceof UnionType) { + foreach ($exprType->getTypes() as $innerType) { + if ($innerType->getObjectClassNames() !== [] && $classType->isSuperTypeOf($innerType)->yes()) { + return []; + } + } + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Although %s is covered by backward compatibility promise, this instanceof assumption might break because it\'s not guaranteed to always stay the same.', + $classReflection->getDisplayName(), + ))->identifier('phpstanApi.instanceofAssumption')->tip(sprintf( + "In case of questions how to solve this correctly, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ))->build(), + ]; + } + +} diff --git a/src/Rules/Api/ApiInstanceofTypeRule.php b/src/Rules/Api/ApiInstanceofTypeRule.php new file mode 100644 index 0000000000..3913479a89 --- /dev/null +++ b/src/Rules/Api/ApiInstanceofTypeRule.php @@ -0,0 +1,157 @@ + + */ +final class ApiInstanceofTypeRule implements Rule +{ + + private const MAP = [ + TypeWithClassName::class => 'Type::getObjectClassNames() or Type::getObjectClassReflections()', + EnumCaseObjectType::class => 'Type::getEnumCases()', + ConstantArrayType::class => 'Type::getConstantArrays()', + ArrayType::class => 'Type::isArray() or Type::getArrays()', + ConstantStringType::class => 'Type::getConstantStrings()', + StringType::class => 'Type::isString()', + ClassStringType::class => 'Type::isClassStringType()', + IntegerType::class => 'Type::isInteger()', + FloatType::class => 'Type::isFloat()', + NullType::class => 'Type::isNull()', + VoidType::class => 'Type::isVoid()', + BooleanType::class => 'Type::isBoolean()', + ConstantBooleanType::class => 'Type::isTrue() or Type::isFalse()', + CallableType::class => 'Type::isCallable() and Type::getCallableParametersAcceptors()', + IterableType::class => 'Type::isIterable()', + ObjectWithoutClassType::class => 'Type::isObject()', + ObjectType::class => 'Type::isObject() or Type::getObjectClassNames()', + GenericClassStringType::class => 'Type::isClassStringType() and Type::getClassStringObjectType()', + GenericObjectType::class => null, + IntersectionType::class => null, + ConstantScalarType::class => 'Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues()', + ObjectShapeType::class => 'Type::isObject() and Type::hasProperty()', + + // accessory types + NonEmptyArrayType::class => 'Type::isIterableAtLeastOnce()', + OversizedArrayType::class => 'Type::isOversizedArray()', + AccessoryArrayListType::class => 'Type::isList()', + AccessoryNumericStringType::class => 'Type::isNumericString()', + AccessoryLiteralStringType::class => 'Type::isLiteralString()', + AccessoryLowercaseStringType::class => 'Type::isLowercaseString()', + AccessoryUppercaseStringType::class => 'Type::isUppercaseString()', + AccessoryNonEmptyStringType::class => 'Type::isNonEmptyString()', + AccessoryNonFalsyStringType::class => 'Type::isNonFalsyString()', + HasMethodType::class => 'Type::hasMethod()', + HasPropertyType::class => 'Type::hasProperty()', + HasOffsetType::class => 'Type::hasOffsetValueType()', + AccessoryType::class => 'methods on PHPStan\\Type\\Type', + ]; + + public function __construct( + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Instanceof_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->class instanceof Node\Name) { + return []; + } + + if ($node->getAttribute(TypeTraverserInstanceofVisitor::ATTRIBUTE_NAME, false) === true) { + return []; + } + + $lowerMap = []; + foreach (self::MAP as $className => $method) { + $lowerMap[strtolower($className)] = $method; + } + + $className = $scope->resolveName($node->class); + $lowerClassName = strtolower($className); + if (!array_key_exists($lowerClassName, $lowerMap)) { + return []; + } + + if ($this->reflectionProvider->hasClass($className)) { + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->is(AccessoryType::class)) { + if ($className === $classReflection->getName()) { + return []; + } + } + } + + $tip = 'Learn more: https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated'; + if ($lowerMap[$lowerClassName] === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Doing instanceof %s is error-prone and deprecated.', + $className, + ))->identifier('phpstanApi.instanceofType')->tip($tip)->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Doing instanceof %s is error-prone and deprecated. Use %s instead.', + $className, + $lowerMap[$lowerClassName], + ))->identifier('phpstanApi.instanceofType')->tip($tip)->build(), + ]; + } + +} diff --git a/src/Rules/Api/ApiInstantiationRule.php b/src/Rules/Api/ApiInstantiationRule.php index f98f17af0d..80079c5495 100644 --- a/src/Rules/Api/ApiInstantiationRule.php +++ b/src/Rules/Api/ApiInstantiationRule.php @@ -8,12 +8,12 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule */ -class ApiInstantiationRule implements Rule +final class ApiInstantiationRule implements Rule { public function __construct( @@ -40,14 +40,14 @@ public function processNode(Node $node, Scope $scope): array } $classReflection = $this->reflectionProvider->getClass($className); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName())) { return []; } $ruleError = RuleErrorBuilder::message(sprintf( 'Creating new %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', $classReflection->getDisplayName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.constructor')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); @@ -62,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array return [$ruleError]; } - if (strpos($docComment, '@api') === false) { + if (!str_contains($docComment, '@api')) { return [$ruleError]; } diff --git a/src/Rules/Api/ApiInterfaceExtendsRule.php b/src/Rules/Api/ApiInterfaceExtendsRule.php index 94667cad97..048aa82a1b 100644 --- a/src/Rules/Api/ApiInterfaceExtendsRule.php +++ b/src/Rules/Api/ApiInterfaceExtendsRule.php @@ -6,18 +6,18 @@ use PhpParser\Node\Stmt\Interface_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Type; use function array_merge; use function count; +use function in_array; use function sprintf; /** * @implements Rule */ -class ApiInterfaceExtendsRule implements Rule +final class ApiInterfaceExtendsRule implements Rule { public function __construct( @@ -43,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function checkName(Scope $scope, Node\Name $name): array { @@ -53,19 +53,19 @@ private function checkName(Scope $scope, Node\Name $name): array } $extendedInterfaceReflection = $this->reflectionProvider->getClass($extendedInterface); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedInterfaceReflection->getName(), $extendedInterfaceReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedInterfaceReflection->getName(), $extendedInterfaceReflection->getFileName())) { return []; } $ruleError = RuleErrorBuilder::message(sprintf( 'Extending %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', $extendedInterfaceReflection->getDisplayName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.interface')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); - if ($extendedInterfaceReflection->getName() === Type::class) { + if (in_array($extendedInterfaceReflection->getName(), BcUncoveredInterface::CLASSES, true)) { return [$ruleError]; } diff --git a/src/Rules/Api/ApiMethodCallRule.php b/src/Rules/Api/ApiMethodCallRule.php index 0b61144103..8c733903c4 100644 --- a/src/Rules/Api/ApiMethodCallRule.php +++ b/src/Rules/Api/ApiMethodCallRule.php @@ -9,12 +9,12 @@ use PHPStan\Rules\RuleErrorBuilder; use function count; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule */ -class ApiMethodCallRule implements Rule +final class ApiMethodCallRule implements Rule { public function __construct(private ApiRuleHelper $apiRuleHelper) @@ -38,7 +38,7 @@ public function processNode(Node $node, Scope $scope): array } $declaringClass = $methodReflection->getDeclaringClass(); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName())) { return []; } @@ -50,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', $declaringClass->getDisplayName(), $methodReflection->getName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.method')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); @@ -76,7 +76,7 @@ private function isCovered(MethodReflection $methodReflection): bool return false; } - return strpos($methodDocComment, '@api') !== false; + return str_contains($methodDocComment, '@api'); } } diff --git a/src/Rules/Api/ApiRuleHelper.php b/src/Rules/Api/ApiRuleHelper.php index 59123647d8..8fe066e60b 100644 --- a/src/Rules/Api/ApiRuleHelper.php +++ b/src/Rules/Api/ApiRuleHelper.php @@ -6,12 +6,12 @@ use PHPStan\File\ParentDirectoryRelativePathHelper; use function dirname; use function pathinfo; +use function str_starts_with; use function stripos; -use function strpos; use function strtolower; use const PATHINFO_BASENAME; -class ApiRuleHelper +final class ApiRuleHelper { public function isPhpStanCode(Scope $scope, string $namespace, ?string $declaringFile): bool @@ -72,11 +72,11 @@ public function isPhpStanName(string $namespace): bool return true; } - if (strpos($namespace, 'PHPStan\\PhpDocParser\\') === 0) { + if (str_starts_with($namespace, 'PHPStan\\PhpDocParser\\')) { return false; } - if (strpos($namespace, 'PHPStan\\BetterReflection\\') === 0) { + if (str_starts_with($namespace, 'PHPStan\\BetterReflection\\')) { return false; } diff --git a/src/Rules/Api/ApiStaticCallRule.php b/src/Rules/Api/ApiStaticCallRule.php index b4ee6f08ca..8c1b53457a 100644 --- a/src/Rules/Api/ApiStaticCallRule.php +++ b/src/Rules/Api/ApiStaticCallRule.php @@ -10,12 +10,12 @@ use PHPStan\Rules\RuleErrorBuilder; use function count; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule */ -class ApiStaticCallRule implements Rule +final class ApiStaticCallRule implements Rule { public function __construct( @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection = $classReflection->getNativeMethod($methodName); $declaringClass = $methodReflection->getDeclaringClass(); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName())) { return []; } @@ -65,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', $declaringClass->getDisplayName(), $methodReflection->getName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.method')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); @@ -91,7 +91,7 @@ private function isCovered(MethodReflection $methodReflection): bool return false; } - return strpos($methodDocComment, '@api') !== false; + return str_contains($methodDocComment, '@api'); } } diff --git a/src/Rules/Api/ApiTraitUseRule.php b/src/Rules/Api/ApiTraitUseRule.php index bd2a7ae10c..a0074cbd4a 100644 --- a/src/Rules/Api/ApiTraitUseRule.php +++ b/src/Rules/Api/ApiTraitUseRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class ApiTraitUseRule implements Rule +final class ApiTraitUseRule implements Rule { public function __construct( @@ -41,14 +41,14 @@ public function processNode(Node $node, Scope $scope): array } $traitReflection = $this->reflectionProvider->getClass($traitName); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $traitReflection->getName(), $traitReflection->getFileName() ?: null)) { + if (!$this->apiRuleHelper->isPhpStanCode($scope, $traitReflection->getName(), $traitReflection->getFileName())) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( 'Using %s is not covered by backward compatibility promise. The trait might change in a minor PHPStan version.', $traitReflection->getDisplayName(), - ))->tip($tip)->build(); + ))->identifier('phpstanApi.trait')->tip($tip)->build(); } return $errors; diff --git a/src/Rules/Api/BcUncoveredInterface.php b/src/Rules/Api/BcUncoveredInterface.php new file mode 100644 index 0000000000..3bf9c0c61d --- /dev/null +++ b/src/Rules/Api/BcUncoveredInterface.php @@ -0,0 +1,72 @@ + + */ +final class GetTemplateTypeRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $args = $node->getArgs(); + if (count($args) < 2) { + return []; + } + if (!$node->name instanceof Node\Identifier) { + return []; + } + + if ($node->name->toLowerString() !== 'gettemplatetype') { + return []; + } + + $calledOnType = $scope->getType($node->var); + $methodReflection = $scope->getMethodReflection($calledOnType, $node->name->toString()); + if ($methodReflection === null) { + return []; + } + + if (!$methodReflection->getDeclaringClass()->is(Type::class)) { + return []; + } + + $classType = $scope->getType($args[0]->value); + $templateType = $scope->getType($args[1]->value); + $errors = []; + foreach ($classType->getConstantStrings() as $classNameType) { + if (!$this->reflectionProvider->hasClass($classNameType->getValue())) { + continue; + } + $classReflection = $this->reflectionProvider->getClass($classNameType->getValue()); + $templateTypeMap = $classReflection->getTemplateTypeMap(); + foreach ($templateType->getConstantStrings() as $templateTypeName) { + if ($templateTypeMap->hasType($templateTypeName->getValue())) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s::%s() references unknown template type %s on class %s.', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName(), + $templateTypeName->getValue(), + $classReflection->getDisplayName(), + ))->identifier('phpstanApi.getTemplateType')->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Api/NodeConnectingVisitorAttributesRule.php b/src/Rules/Api/NodeConnectingVisitorAttributesRule.php new file mode 100644 index 0000000000..7ad74631b9 --- /dev/null +++ b/src/Rules/Api/NodeConnectingVisitorAttributesRule.php @@ -0,0 +1,77 @@ + + */ +final class NodeConnectingVisitorAttributesRule implements Rule +{ + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if ($node->name->toLowerString() !== 'getattribute') { + return []; + } + $calledOnType = $scope->getType($node->var); + if (!(new ObjectType(Node::class))->isSuperTypeOf($calledOnType)->yes()) { + return []; + } + $args = $node->getArgs(); + if (!isset($args[0])) { + return []; + } + $argType = $scope->getType($args[0]->value); + if (!$argType instanceof ConstantStringType) { + return []; + } + if (!in_array($argType->getValue(), ['parent', 'previous', 'next'], true)) { + return []; + } + if (!$scope->isInClass()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + $hasPhpStanInterface = false; + foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { + continue; + } + + $hasPhpStanInterface = true; + } + + if (!$hasPhpStanInterface) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Node attribute \'%s\' is no longer available.', $argType->getValue())) + ->identifier('phpParser.nodeConnectingAttribute') + ->tip('See: https://phpstan.org/blog/preprocessing-ast-for-custom-rules') + ->build(), + ]; + } + +} diff --git a/src/Rules/Api/OldPhpParser4ClassRule.php b/src/Rules/Api/OldPhpParser4ClassRule.php new file mode 100644 index 0000000000..8c86e3c713 --- /dev/null +++ b/src/Rules/Api/OldPhpParser4ClassRule.php @@ -0,0 +1,79 @@ + + */ +final class OldPhpParser4ClassRule implements Rule +{ + + private const NAME_MAPPING = [ + // from https://github.com/nikic/PHP-Parser/blob/master/UPGRADE-5.0.md#renamed-nodes + 'PhpParser\Node\Scalar\LNumber' => Node\Scalar\Int_::class, + 'PhpParser\Node\Scalar\DNumber' => Node\Scalar\Float_::class, + 'PhpParser\Node\Scalar\Encapsed' => Node\Scalar\InterpolatedString::class, + 'PhpParser\Node\Scalar\EncapsedStringPart' => Node\InterpolatedStringPart::class, + 'PhpParser\Node\Expr\ArrayItem' => Node\ArrayItem::class, + 'PhpParser\Node\Expr\ClosureUse' => Node\ClosureUse::class, + 'PhpParser\Node\Stmt\DeclareDeclare' => Node\DeclareItem::class, + 'PhpParser\Node\Stmt\PropertyProperty' => Node\PropertyItem::class, + 'PhpParser\Node\Stmt\StaticVar' => Node\StaticVar::class, + 'PhpParser\Node\Stmt\UseUse' => Node\UseItem::class, + ]; + + public function getNodeType(): string + { + return Name::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $nameMapping = array_change_key_case(self::NAME_MAPPING); + $lowerName = $node->toLowerString(); + if (!array_key_exists($lowerName, $nameMapping)) { + return []; + } + + $newName = $nameMapping[$lowerName]; + + if (!$scope->isInClass()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + $hasPhpStanInterface = false; + foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { + continue; + } + + $hasPhpStanInterface = true; + } + + if (!$hasPhpStanInterface) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Class %s not found. It has been renamed to %s in PHP-Parser v5.', + $node->toString(), + $newName, + ))->identifier('phpParser.classRenamed') + ->build(), + ]; + } + +} diff --git a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php index 892bbc1660..8547f7b2ed 100644 --- a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php +++ b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php @@ -13,12 +13,12 @@ use function dirname; use function is_dir; use function is_file; -use function strpos; +use function str_starts_with; /** * @implements Rule */ -class PhpStanNamespaceIn3rdPartyPackageRule implements Rule +final class PhpStanNamespaceIn3rdPartyPackageRule implements Rule { public function __construct(private ApiRuleHelper $apiRuleHelper) @@ -46,12 +46,13 @@ public function processNode(Node $node, Scope $scope): array } $packageName = $composerJson['name'] ?? null; - if ($packageName !== null && strpos($packageName, 'phpstan/') === 0) { + if ($packageName !== null && str_starts_with($packageName, 'phpstan/')) { return []; } return [ RuleErrorBuilder::message('Declaring PHPStan namespace is not allowed in 3rd party packages.') + ->identifier('phpstanApi.phpstanNamespace') ->tip("See:\n https://phpstan.org/developing-extensions/backward-compatibility-promise") ->build(), ]; diff --git a/src/Rules/Api/RuntimeReflectionFunctionRule.php b/src/Rules/Api/RuntimeReflectionFunctionRule.php new file mode 100644 index 0000000000..229b681818 --- /dev/null +++ b/src/Rules/Api/RuntimeReflectionFunctionRule.php @@ -0,0 +1,76 @@ + + */ +final class RuntimeReflectionFunctionRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Expr\FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if (!in_array($functionReflection->getName(), [ + 'is_a', + 'is_subclass_of', + 'class_parents', + 'class_implements', + 'class_uses', + ], true)) { + return []; + } + + if (!$scope->isInClass()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + $hasPhpStanInterface = false; + foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { + continue; + } + + $hasPhpStanInterface = true; + } + + if (!$hasPhpStanInterface) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Function %s() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', $functionReflection->getName()), + )->identifier('phpstanApi.runtimeReflection')->build(), + ]; + } + +} diff --git a/src/Rules/Api/RuntimeReflectionInstantiationRule.php b/src/Rules/Api/RuntimeReflectionInstantiationRule.php new file mode 100644 index 0000000000..f358c5070a --- /dev/null +++ b/src/Rules/Api/RuntimeReflectionInstantiationRule.php @@ -0,0 +1,95 @@ + + */ +final class RuntimeReflectionInstantiationRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Expr\New_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->class instanceof Node\Name) { + return []; + } + + $className = $scope->resolveName($node->class); + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!in_array($classReflection->getName(), [ + ReflectionMethod::class, + ReflectionClass::class, + ReflectionClassConstant::class, + 'ReflectionEnum', + 'ReflectionEnumBackedCase', + ReflectionZendExtension::class, + ReflectionExtension::class, + ReflectionFunction::class, + ReflectionObject::class, + ReflectionParameter::class, + ReflectionProperty::class, + ReflectionGenerator::class, + 'ReflectionFiber', + ], true)) { + return []; + } + + if (!$scope->isInClass()) { + return []; + } + + $scopeClassReflection = $scope->getClassReflection(); + $hasPhpStanInterface = false; + foreach (array_keys($scopeClassReflection->getInterfaces()) as $interfaceName) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { + continue; + } + + $hasPhpStanInterface = true; + } + + if (!$hasPhpStanInterface) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Creating new %s is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', $classReflection->getName()), + )->identifier('phpstanApi.runtimeReflection')->build(), + ]; + } + +} diff --git a/src/Rules/Arrays/AllowedArrayKeysTypes.php b/src/Rules/Arrays/AllowedArrayKeysTypes.php index 6a2cc62542..2b15a4eb65 100644 --- a/src/Rules/Arrays/AllowedArrayKeysTypes.php +++ b/src/Rules/Arrays/AllowedArrayKeysTypes.php @@ -2,15 +2,23 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\ResourceType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; -class AllowedArrayKeysTypes +final class AllowedArrayKeysTypes { public static function getType(): Type @@ -24,4 +32,55 @@ public static function getType(): Type ]); } + public static function narrowOffsetKeyType(Type $varType, Type $keyType): ?Type + { + if (!$varType->isArray()->yes() || $varType->isIterableAtLeastOnce()->no()) { + return null; + } + + $varIterableKeyType = $varType->getIterableKeyType(); + + if ($varIterableKeyType->isConstantScalarValue()->yes()) { + $narrowedKey = TypeCombinator::union( + $varIterableKeyType, + TypeCombinator::remove($varIterableKeyType->toString(), new ConstantStringType('')), + ); + + if (!$varType->hasOffsetValueType(new ConstantIntegerType(0))->no()) { + $narrowedKey = TypeCombinator::union( + $narrowedKey, + new ConstantBooleanType(false), + ); + } + + if (!$varType->hasOffsetValueType(new ConstantIntegerType(1))->no()) { + $narrowedKey = TypeCombinator::union( + $narrowedKey, + new ConstantBooleanType(true), + ); + } + + if (!$varType->hasOffsetValueType(new ConstantStringType(''))->no()) { + $narrowedKey = TypeCombinator::addNull($narrowedKey); + } + + if (!$varIterableKeyType->isNumericString()->no() || !$varIterableKeyType->isInteger()->no()) { + $narrowedKey = TypeCombinator::union($narrowedKey, new FloatType()); + } + + return $narrowedKey; + } elseif ($varIterableKeyType->isInteger()->yes() && $keyType->isString()->yes()) { + return TypeCombinator::intersect($varIterableKeyType->toString(), $keyType); + } + + return new MixedType( + false, + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new ObjectWithoutClassType(), + new ResourceType(), + ]), + ); + } + } diff --git a/src/Rules/Arrays/AppendedArrayItemTypeRule.php b/src/Rules/Arrays/AppendedArrayItemTypeRule.php deleted file mode 100644 index 396de12949..0000000000 --- a/src/Rules/Arrays/AppendedArrayItemTypeRule.php +++ /dev/null @@ -1,90 +0,0 @@ - - */ -class AppendedArrayItemTypeRule implements Rule -{ - - public function __construct( - private PropertyReflectionFinder $propertyReflectionFinder, - private RuleLevelHelper $ruleLevelHelper, - ) - { - } - - public function getNodeType(): string - { - return Node\Expr::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ( - !$node instanceof Assign - && !$node instanceof AssignOp - && !$node instanceof AssignRef - ) { - return []; - } - - if (!($node->var instanceof ArrayDimFetch)) { - return []; - } - - if ( - !$node->var->var instanceof Node\Expr\PropertyFetch - && !$node->var->var instanceof Node\Expr\StaticPropertyFetch - ) { - return []; - } - - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node->var->var, $scope); - if ($propertyReflection === null) { - return []; - } - - $assignedToType = $propertyReflection->getWritableType(); - if (!($assignedToType instanceof ArrayType)) { - return []; - } - - if ($node instanceof Assign || $node instanceof AssignRef) { - $assignedValueType = $scope->getType($node->expr); - } else { - $assignedValueType = $scope->getType($node); - } - - $itemType = $assignedToType->getItemType(); - if (!$this->ruleLevelHelper->accepts($itemType, $assignedValueType, $scope->isDeclareStrictTypes())) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($itemType, $assignedValueType); - return [ - RuleErrorBuilder::message(sprintf( - 'Array (%s) does not accept %s.', - $assignedToType->describe($verbosityLevel), - $assignedValueType->describe($verbosityLevel), - ))->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php b/src/Rules/Arrays/AppendedArrayKeyTypeRule.php deleted file mode 100644 index aa45ce31b4..0000000000 --- a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php +++ /dev/null @@ -1,90 +0,0 @@ - - */ -class AppendedArrayKeyTypeRule implements Rule -{ - - public function __construct( - private PropertyReflectionFinder $propertyReflectionFinder, - private bool $checkUnionTypes, - ) - { - } - - public function getNodeType(): string - { - return Assign::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!($node->var instanceof ArrayDimFetch)) { - return []; - } - - if ( - !$node->var->var instanceof Node\Expr\PropertyFetch - && !$node->var->var instanceof Node\Expr\StaticPropertyFetch - ) { - return []; - } - - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node->var->var, $scope); - if ($propertyReflection === null) { - return []; - } - - $arrayType = $propertyReflection->getReadableType(); - if (!$arrayType instanceof ArrayType) { - return []; - } - - if ($node->var->dim !== null) { - $dimensionType = $scope->getType($node->var->dim); - $isValidKey = AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimensionType); - if (!$isValidKey->yes()) { - // already handled by InvalidKeyInArrayDimFetchRule - return []; - } - - $keyType = ArrayType::castToArrayKeyType($dimensionType); - if (!$this->checkUnionTypes && $keyType instanceof UnionType) { - return []; - } - } else { - $keyType = new IntegerType(); - } - - 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($verbosity), - $keyType->describe(VerbosityLevel::value()), - ))->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/ArrayDestructuringRule.php b/src/Rules/Arrays/ArrayDestructuringRule.php index 37f63d4d7c..1d9537d274 100644 --- a/src/Rules/Arrays/ArrayDestructuringRule.php +++ b/src/Rules/Arrays/ArrayDestructuringRule.php @@ -6,14 +6,13 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Scalar\LNumber; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -24,7 +23,7 @@ /** * @implements Rule */ -class ArrayDestructuringRule implements Rule +final class ArrayDestructuringRule implements Rule { public function __construct( @@ -41,7 +40,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->var instanceof Node\Expr\List_ && !$node->var instanceof Node\Expr\Array_) { + if (!$node->var instanceof Node\Expr\List_) { return []; } @@ -53,10 +52,9 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Node\Expr\List_|Node\Expr\Array_ $var - * @return RuleError[] + * @return list */ - private function getErrors(Scope $scope, Expr $var, Expr $expr): array + private function getErrors(Scope $scope, Node\Expr\List_ $var, Expr $expr): array { $exprTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, @@ -70,7 +68,9 @@ private function getErrors(Scope $scope, Expr $var, Expr $expr): array } if (!$exprType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($exprType)->yes()) { return [ - RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly())))->build(), + RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly()))) + ->identifier('offsetAccess.nonArray') + ->build(), ]; } @@ -85,14 +85,10 @@ private function getErrors(Scope $scope, Expr $var, Expr $expr): array $keyExpr = null; if ($item->key === null) { $keyType = new ConstantIntegerType($i); - $keyExpr = new Node\Scalar\LNumber($i); + $keyExpr = new Node\Scalar\Int_($i); } else { $keyType = $scope->getType($item->key); - if ($keyType instanceof ConstantIntegerType) { - $keyExpr = new LNumber($keyType->getValue()); - } elseif ($keyType instanceof ConstantStringType) { - $keyExpr = new Node\Scalar\String_($keyType->getValue()); - } + $keyExpr = new TypeExpr($keyType); } $itemErrors = $this->nonexistentOffsetInArrayDimFetchCheck->check( @@ -103,12 +99,7 @@ private function getErrors(Scope $scope, Expr $var, Expr $expr): array ); $errors = array_merge($errors, $itemErrors); - if ($keyExpr === null) { - $i++; - continue; - } - - if (!$item->value instanceof Node\Expr\List_ && !$item->value instanceof Node\Expr\Array_) { + if (!$item->value instanceof Node\Expr\List_) { $i++; continue; } diff --git a/src/Rules/Arrays/ArrayUnpackingRule.php b/src/Rules/Arrays/ArrayUnpackingRule.php new file mode 100644 index 0000000000..f1573bec6d --- /dev/null +++ b/src/Rules/Arrays/ArrayUnpackingRule.php @@ -0,0 +1,65 @@ + + */ +final class ArrayUnpackingRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion, private RuleLevelHelper $ruleLevelHelper) + { + } + + public function getNodeType(): string + { + return ArrayItem::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->unpack === false || $this->phpVersion->supportsArrayUnpackingWithStringKeys()) { + return []; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + new GetIterableKeyTypeExpr($node->value), + '', + static fn (Type $type): bool => $type->isString()->no(), + ); + + $keyType = $typeResult->getType(); + if ($keyType instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $isString = $keyType->isString(); + if ($isString->no()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Array unpacking cannot be used on an array with %sstring keys: %s', + $isString->yes() ? '' : 'potential ', + $scope->getType($node->value)->describe(VerbosityLevel::value()), + ))->identifier('arrayUnpacking.stringOffset')->build(), + ]; + } + +} diff --git a/src/Rules/Arrays/DeadForeachRule.php b/src/Rules/Arrays/DeadForeachRule.php index 0423c78fc5..61d2085fec 100644 --- a/src/Rules/Arrays/DeadForeachRule.php +++ b/src/Rules/Arrays/DeadForeachRule.php @@ -10,7 +10,7 @@ /** * @implements Rule */ -class DeadForeachRule implements Rule +final class DeadForeachRule implements Rule { public function getNodeType(): string @@ -30,7 +30,9 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Empty array passed to foreach.')->build(), + RuleErrorBuilder::message('Empty array passed to foreach.') + ->identifier('foreach.emptyArray') + ->build(), ]; } diff --git a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php index f3a856d3d2..46b5e712e1 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -3,26 +3,28 @@ namespace PHPStan\Rules\Arrays; use PhpParser\Node; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; use PHPStan\Node\LiteralArrayNode; +use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ConstantScalarType; use function array_keys; use function count; use function implode; +use function max; use function sprintf; use function var_export; /** * @implements Rule */ -class DuplicateKeysInLiteralArraysRule implements Rule +final class DuplicateKeysInLiteralArraysRule implements Rule { public function __construct( - private Standard $printer, + private ExprPrinter $exprPrinter, ) { } @@ -38,29 +40,59 @@ public function processNode(Node $node, Scope $scope): array $duplicateKeys = []; $printedValues = []; $valueLines = []; + + /** + * @var int|false|null $autoGeneratedIndex + * - An int value represent the biggest integer used as array key. + * When no key is provided this value + 1 will be used. + * - Null is used as initializer instead of 0 to avoid issue with negative keys. + * - False means a non-scalar value was encountered and we cannot be sure of the next keys. + */ + $autoGeneratedIndex = null; foreach ($node->getItemNodes() as $itemNode) { $item = $itemNode->getArrayItem(); if ($item === null) { - continue; - } - if ($item->key === null) { + $autoGeneratedIndex = false; continue; } $key = $item->key; - $keyType = $itemNode->getScope()->getType($key); - if ( - !$keyType instanceof ConstantScalarType - ) { + if ($key === null) { + if ($autoGeneratedIndex === false) { + continue; + } + + if ($autoGeneratedIndex === null) { + $autoGeneratedIndex = 0; + $keyType = new ConstantIntegerType(0); + } else { + $keyType = new ConstantIntegerType(++$autoGeneratedIndex); + } + } else { + $keyType = $itemNode->getScope()->getType($key); + + $arrayKeyValue = $keyType->toArrayKey(); + if ($arrayKeyValue instanceof ConstantIntegerType) { + $autoGeneratedIndex = $autoGeneratedIndex === null + ? $arrayKeyValue->getValue() + : max($autoGeneratedIndex, $arrayKeyValue->getValue()); + } + } + + if (!$keyType instanceof ConstantScalarType) { + $autoGeneratedIndex = false; continue; } - $printedValue = $this->printer->prettyPrintExpr($key); $value = $keyType->getValue(); + $printedValue = $key !== null + ? $this->exprPrinter->printExpr($key) + : $value; + $printedValues[$value][] = $printedValue; if (!isset($valueLines[$value])) { - $valueLines[$value] = $item->getLine(); + $valueLines[$value] = $item->getStartLine(); } $previousCount = count($values); @@ -80,7 +112,7 @@ public function processNode(Node $node, Scope $scope): array count($printedValues[$value]) === 1 ? 'duplicate key' : 'duplicate keys', var_export($value, true), implode(', ', $printedValues[$value]), - ))->line($valueLines[$value])->build(); + ))->identifier('array.duplicateKey')->line($valueLines[$value])->build(); } return $messages; diff --git a/src/Rules/Arrays/EmptyArrayItemRule.php b/src/Rules/Arrays/EmptyArrayItemRule.php deleted file mode 100644 index 9bc3a395b2..0000000000 --- a/src/Rules/Arrays/EmptyArrayItemRule.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ -class EmptyArrayItemRule implements Rule -{ - - public function getNodeType(): string - { - return LiteralArrayNode::class; - } - - public function processNode(Node $node, Scope $scope): array - { - foreach ($node->getItemNodes() as $itemNode) { - $item = $itemNode->getArrayItem(); - if ($item !== null) { - continue; - } - - return [ - RuleErrorBuilder::message('Literal array contains empty item.') - ->nonIgnorable() - ->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index 7e0d752e99..e74b192a67 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -6,19 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function count; use function sprintf; /** * @implements Rule */ -class InvalidKeyInArrayDimFetchRule implements Rule +final class InvalidKeyInArrayDimFetchRule implements Rule { - public function __construct(private bool $reportMaybes) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMaybes, + ) { } @@ -33,28 +37,32 @@ public function processNode(Node $node, Scope $scope): array return []; } - $varType = $scope->getType($node->var); - if (count(TypeUtils::getArrays($varType)) === 0) { + $dimensionType = $scope->getType($node->dim); + if ($dimensionType instanceof MixedType) { + return []; + } + + $varType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->var, + '', + static fn (Type $varType): bool => $varType->isArray()->no() || AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimensionType)->yes(), + )->getType(); + + if ($varType instanceof ErrorType || $varType->isArray()->no()) { return []; } - $dimensionType = $scope->getType($node->dim); $isSuperType = AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimensionType); - if ($isSuperType->no()) { - return [ - RuleErrorBuilder::message( - sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), - )->build(), - ]; - } elseif ($this->reportMaybes && $isSuperType->maybe() && !$dimensionType instanceof MixedType) { - return [ - RuleErrorBuilder::message( - sprintf('Possibly invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), - )->build(), - ]; + if ($isSuperType->yes() || ($isSuperType->maybe() && !$this->reportMaybes)) { + return []; } - return []; + return [ + RuleErrorBuilder::message( + sprintf('%s array key type %s.', $isSuperType->no() ? 'Invalid' : 'Possibly invalid', $dimensionType->describe(VerbosityLevel::typeOnly())), + )->identifier('offsetAccess.invalidOffset')->build(), + ]; } } diff --git a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php index b692bbe15a..fb4ab23162 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php @@ -11,9 +11,9 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ -class InvalidKeyInArrayItemRule implements Rule +final class InvalidKeyInArrayItemRule implements Rule { public function __construct(private bool $reportMaybes) @@ -22,7 +22,7 @@ public function __construct(private bool $reportMaybes) public function getNodeType(): string { - return Node\Expr\ArrayItem::class; + return Node\ArrayItem::class; } public function processNode(Node $node, Scope $scope): array @@ -37,13 +37,13 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), - )->build(), + )->identifier('array.invalidKey')->build(), ]; } elseif ($this->reportMaybes && $isSuperType->maybe() && !$dimensionType instanceof MixedType) { return [ RuleErrorBuilder::message( sprintf('Possibly invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), - )->build(), + )->identifier('array.invalidKey')->build(), ]; } diff --git a/src/Rules/Arrays/IterableInForeachRule.php b/src/Rules/Arrays/IterableInForeachRule.php index 57fd2080f3..fd6351a003 100644 --- a/src/Rules/Arrays/IterableInForeachRule.php +++ b/src/Rules/Arrays/IterableInForeachRule.php @@ -16,7 +16,7 @@ /** * @implements Rule */ -class IterableInForeachRule implements Rule +final class IterableInForeachRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -49,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Argument of an invalid type %s supplied for foreach, only iterables are supported.', $type->describe(VerbosityLevel::typeOnly()), - ))->line($originalNode->expr->getLine())->build(), + ))->identifier('foreach.nonIterable')->line($originalNode->expr->getStartLine())->build(), ]; } diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php index ae4847e61e..5b81f82f1a 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\BenevolentUnionType; @@ -17,15 +17,20 @@ use function count; use function sprintf; -class NonexistentOffsetInArrayDimFetchCheck +final class NonexistentOffsetInArrayDimFetchCheck { - public function __construct(private RuleLevelHelper $ruleLevelHelper, private bool $reportMaybes) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMaybes, + private bool $reportPossiblyNonexistentGeneralArrayOffset, + private bool $reportPossiblyNonexistentConstantArrayOffset, + ) { } /** - * @return RuleError[] + * @return list */ public function check( Scope $scope, @@ -45,27 +50,44 @@ public function check( return $typeResult->getUnknownClassErrors(); } - $hasOffsetValueType = $type->hasOffsetValueType($dimType); - $report = $hasOffsetValueType->no(); - if ($hasOffsetValueType->maybe()) { - $constantArrays = TypeUtils::getOldConstantArrays($type); - if (count($constantArrays) > 0) { - foreach ($constantArrays as $constantArray) { - if ($constantArray->hasOffsetValueType($dimType)->no()) { - $report = true; - break; - } - } - } + if ($scope->isInExpressionAssign($var) || $scope->isUndefinedExpressionAllowed($var)) { + return []; + } + + if ($type->hasOffsetValueType($dimType)->no()) { + return [ + RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(count($dimType->getConstantStrings()) > 0 ? VerbosityLevel::precise() : VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))) + ->identifier('offsetAccess.notFound') + ->build(), + ]; } - if (!$report && $this->reportMaybes) { + if ($this->reportMaybes) { + $report = false; + if ($type instanceof BenevolentUnionType) { $flattenedTypes = [$type]; } else { $flattenedTypes = TypeUtils::flattenTypes($type); } foreach ($flattenedTypes as $innerType) { + if ( + $this->reportPossiblyNonexistentGeneralArrayOffset + && $innerType->isArray()->yes() + && !$innerType->isConstantArray()->yes() + && !$innerType->hasOffsetValueType($dimType)->yes() + ) { + $report = true; + break; + } + if ( + $this->reportPossiblyNonexistentConstantArrayOffset + && $innerType->isConstantArray()->yes() + && !$innerType->hasOffsetValueType($dimType)->yes() + ) { + $report = true; + break; + } if ($dimType instanceof UnionType) { if ($innerType->hasOffsetValueType($dimType)->no()) { $report = true; @@ -76,20 +98,18 @@ public function check( foreach (TypeUtils::flattenTypes($dimType) as $innerDimType) { if ($innerType->hasOffsetValueType($innerDimType)->no()) { $report = true; - break; + break 2; } } } - } - if ($report) { - if ($scope->isInExpressionAssign($var)) { - return []; + if ($report) { + return [ + RuleErrorBuilder::message(sprintf('Offset %s might not exist on %s.', $dimType->describe(count($dimType->getConstantStrings()) > 0 ? VerbosityLevel::precise() : VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))) + ->identifier('offsetAccess.notFound') + ->build(), + ]; } - - return [ - RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value())))->build(), - ]; } return []; diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index 7a8a66490c..b0edfcadf9 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -12,12 +12,15 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function count; +use function in_array; +use function is_string; use function sprintf; /** * @implements Rule */ -class NonexistentOffsetInArrayDimFetchRule implements Rule +final class NonexistentOffsetInArrayDimFetchRule implements Rule { public function __construct( @@ -54,21 +57,29 @@ public function processNode(Node $node, Scope $scope): array return $isOffsetAccessibleTypeResult->getUnknownClassErrors(); } + if ($scope->hasExpressionType($node)->yes()) { + return []; + } + $isOffsetAccessible = $isOffsetAccessibleType->isOffsetAccessible(); if ($scope->isInExpressionAssign($node) && $isOffsetAccessible->yes()) { return []; } + if ($scope->isUndefinedExpressionAllowed($node) && $isOffsetAccessibleType->isOffsetAccessLegal()->yes()) { + return []; + } + if (!$isOffsetAccessible->yes()) { if ($isOffsetAccessible->no() || $this->reportMaybes) { if ($dimType !== null) { return [ RuleErrorBuilder::message(sprintf( 'Cannot access offset %s on %s.', - $dimType->describe(VerbosityLevel::value()), + $dimType->describe(count($dimType->getConstantStrings()) > 0 ? VerbosityLevel::precise() : VerbosityLevel::value()), $isOffsetAccessibleType->describe(VerbosityLevel::value()), - ))->build(), + ))->identifier('offsetAccess.nonOffsetAccessible')->build(), ]; } @@ -76,7 +87,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Cannot access an offset on %s.', $isOffsetAccessibleType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAccess.nonOffsetAccessible')->build(), ]; } @@ -87,6 +98,49 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ( + $node->dim instanceof Node\Expr\FuncCall + && $node->dim->name instanceof Node\Name + && in_array($node->dim->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && count($node->dim->getArgs()) >= 1 + ) { + $arrayArg = $node->dim->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayArg instanceof Node\Expr\Variable + && $node->var instanceof Node\Expr\Variable + && is_string($arrayArg->name) + && $arrayArg->name === $node->var->name + && $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + return []; + } + } + + if ( + $node->dim instanceof Node\Expr\BinaryOp\Minus + && $node->dim->left instanceof Node\Expr\FuncCall + && $node->dim->left->name instanceof Node\Name + && in_array($node->dim->left->name->toLowerString(), ['count', 'sizeof'], true) + && count($node->dim->left->getArgs()) >= 1 + && $node->dim->right instanceof Node\Scalar\Int_ + && $node->dim->right->value === 1 + ) { + $arrayArg = $node->dim->left->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayArg instanceof Node\Expr\Variable + && $node->var instanceof Node\Expr\Variable + && is_string($arrayArg->name) + && $arrayArg->name === $node->var->name + && $arrayType->isList()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + return []; + } + } + return $this->nonexistentOffsetInArrayDimFetchCheck->check( $scope, $node->var, diff --git a/src/Rules/Arrays/OffsetAccessAssignOpRule.php b/src/Rules/Arrays/OffsetAccessAssignOpRule.php index 46e3e1fb4e..5ab745647b 100644 --- a/src/Rules/Arrays/OffsetAccessAssignOpRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignOpRule.php @@ -17,7 +17,7 @@ /** * @implements Rule */ -class OffsetAccessAssignOpRule implements Rule +final class OffsetAccessAssignOpRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -81,7 +81,7 @@ static function (Type $dimType) use ($varType): bool { RuleErrorBuilder::message(sprintf( 'Cannot assign new offset to %s.', $varType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.dimType')->build(), ]; } @@ -90,7 +90,7 @@ static function (Type $dimType) use ($varType): bool { 'Cannot assign offset %s to %s.', $dimType->describe(VerbosityLevel::value()), $varType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.dimType')->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessAssignmentRule.php b/src/Rules/Arrays/OffsetAccessAssignmentRule.php index ec4ca4dc9c..c6c0f92b14 100644 --- a/src/Rules/Arrays/OffsetAccessAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignmentRule.php @@ -17,7 +17,7 @@ /** * @implements Rule */ -class OffsetAccessAssignmentRule implements Rule +final class OffsetAccessAssignmentRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -82,7 +82,7 @@ static function (Type $dimType) use ($varType): bool { RuleErrorBuilder::message(sprintf( 'Cannot assign new offset to %s.', $varType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.dimType')->build(), ]; } @@ -91,7 +91,7 @@ static function (Type $dimType) use ($varType): bool { 'Cannot assign offset %s to %s.', $dimType->describe(VerbosityLevel::value()), $varType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.dimType')->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index 47d58c87fa..9145005d20 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -19,7 +19,7 @@ /** * @implements Rule */ -class OffsetAccessValueAssignmentRule implements Rule +final class OffsetAccessValueAssignmentRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -46,6 +46,10 @@ public function processNode(Node $node, Scope $scope): array } $arrayDimFetch = $node->var; + $varType = $scope->getType($arrayDimFetch->var); + if ($varType->isObject()->no()) { + return []; + } if ($node instanceof Assign || $node instanceof Expr\AssignRef) { $assignedValueType = $scope->getType($node->expr); @@ -53,7 +57,6 @@ public function processNode(Node $node, Scope $scope): array $assignedValueType = $scope->getType($node); } - $originalArrayType = $scope->getType($arrayDimFetch->var); $arrayTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $arrayDimFetch->var, @@ -76,12 +79,14 @@ static function (Type $varType) use ($assignedValueType): bool { return []; } + $originalArrayType = $scope->getType($arrayDimFetch->var); + return [ RuleErrorBuilder::message(sprintf( '%s does not accept %s.', $originalArrayType->describe(VerbosityLevel::value()), $assignedValueType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.valueType')->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php index 2f93bd119f..8d83452357 100644 --- a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php +++ b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php @@ -10,7 +10,7 @@ /** * @implements Rule */ -class OffsetAccessWithoutDimForReadingRule implements Rule +final class OffsetAccessWithoutDimForReadingRule implements Rule { public function getNodeType(): string @@ -29,7 +29,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Cannot use [] for reading.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Cannot use [] for reading.') + ->identifier('offsetAccess.noDim') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Arrays/UnpackIterableInArrayRule.php b/src/Rules/Arrays/UnpackIterableInArrayRule.php index 08ba2b7e84..bc515b9d04 100644 --- a/src/Rules/Arrays/UnpackIterableInArrayRule.php +++ b/src/Rules/Arrays/UnpackIterableInArrayRule.php @@ -16,7 +16,7 @@ /** * @implements Rule */ -class UnpackIterableInArrayRule implements Rule +final class UnpackIterableInArrayRule implements Rule { public function __construct( @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Only iterables can be unpacked, %s given.', $type->describe(VerbosityLevel::typeOnly()), - ))->line($item->getLine())->build(); + ))->identifier('arrayUnpacking.nonIterable')->line($item->getStartLine())->build(); } return $errors; diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 1349fe5c35..c391d69123 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -14,21 +14,22 @@ use function sprintf; use function strtolower; -class AttributesCheck +final class AttributesCheck { public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $functionCallParametersCheck, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, + private bool $deprecationRulesInstalled, ) { } /** * @param AttributeGroup[] $attrGroups - * @param Attribute::TARGET_* $requiredTarget - * @return RuleError[] + * @param int-mask-of $requiredTarget + * @return list */ public function check( Scope $scope, @@ -43,57 +44,81 @@ public function check( foreach ($attrGroup->attrs as $attribute) { $name = $attribute->name->toString(); if (!$this->reflectionProvider->hasClass($name)) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not exist.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not exist.', $name)) + ->line($attribute->getStartLine()) + ->identifier('attribute.notFound') + ->build(); continue; } $attributeClass = $this->reflectionProvider->getClass($name); if (!$attributeClass->isAttributeClass()) { - $classLikeDescription = 'Class'; - if ($attributeClass->isInterface()) { - $classLikeDescription = 'Interface'; - } elseif ($attributeClass->isTrait()) { - $classLikeDescription = 'Trait'; - } elseif ($attributeClass->isEnum()) { - $classLikeDescription = 'Enum'; - } - - $errors[] = RuleErrorBuilder::message(sprintf('%s %s is not an Attribute class.', $classLikeDescription, $attributeClass->getDisplayName()))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s %s is not an Attribute class.', $attributeClass->getClassTypeDescription(), $attributeClass->getDisplayName())) + ->identifier('attribute.notAttribute') + ->line($attribute->getStartLine()) + ->build(); continue; } if ($attributeClass->isAbstract()) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is abstract.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is abstract.', $name)) + ->identifier('attribute.abstract') + ->line($attribute->getStartLine()) + ->build(); } - foreach ($this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($name, $attribute)]) as $caseSensitivityError) { + foreach ($this->classCheck->checkClassNames($scope, [new ClassNameNodePair($name, $attribute)], ClassNameUsageLocation::from(ClassNameUsageLocation::ATTRIBUTE)) as $caseSensitivityError) { $errors[] = $caseSensitivityError; } $flags = $attributeClass->getAttributeClassFlags(); if (($flags & $requiredTarget) === 0) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have the %s target.', $name, $targetName))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have the %s target.', $name, $targetName)) + ->identifier('attribute.target') + ->line($attribute->getStartLine()) + ->build(); } if (($flags & Attribute::IS_REPEATABLE) === 0) { $loweredName = strtolower($name); if (array_key_exists($loweredName, $alreadyPresent)) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is not repeatable but is already present above the %s.', $name, $targetName))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is not repeatable but is already present above the %s.', $name, $targetName)) + ->identifier('attribute.nonRepeatable') + ->line($attribute->getStartLine()) + ->build(); } $alreadyPresent[$loweredName] = true; } + if ($this->deprecationRulesInstalled && $attributeClass->isDeprecated()) { + if ($attributeClass->getDeprecatedDescription() !== null) { + $deprecatedError = sprintf('Attribute class %s is deprecated: %s', $name, $attributeClass->getDeprecatedDescription()); + } else { + $deprecatedError = sprintf('Attribute class %s is deprecated.', $name); + } + $errors[] = RuleErrorBuilder::message($deprecatedError) + ->identifier('attribute.deprecated') + ->line($attribute->getStartLine()) + ->build(); + } + if (!$attributeClass->hasConstructor()) { if (count($attribute->args) > 0) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have a constructor and must be instantiated without any parameters.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have a constructor and must be instantiated without any parameters.', $name)) + ->identifier('attribute.noConstructor') + ->line($attribute->getStartLine()) + ->build(); } continue; } $attributeConstructor = $attributeClass->getConstructor(); if (!$attributeConstructor->isPublic()) { - $errors[] = RuleErrorBuilder::message(sprintf('Constructor of attribute class %s is not public.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Constructor of attribute class %s is not public.', $name)) + ->identifier('attribute.constructorNotPublic') + ->line($attribute->getStartLine()) + ->build(); } $attributeClassName = SprintfHelper::escapeFormatString($attributeClass->getDisplayName()); @@ -102,25 +127,32 @@ public function check( $nodeAttributes['isAttribute'] = true; $parameterErrors = $this->functionCallParametersCheck->check( - ParametersAcceptorSelector::selectSingle($attributeConstructor->getVariants()), + ParametersAcceptorSelector::selectFromArgs( + $scope, + $attribute->args, + $attributeConstructor->getVariants(), + $attributeConstructor->getNamedArgumentsVariants(), + ), $scope, $attributeConstructor->getDeclaringClass()->isBuiltin(), new New_($attribute->name, $attribute->args, $nodeAttributes), - [ - '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 ' . $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.', - ], + 'attribute', + $attributeConstructor->acceptsNamedArguments(), + '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.', + '%s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.', + '', // constructor does not have a return type + '%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.', + '%s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', ); foreach ($parameterErrors as $error) { diff --git a/src/Rules/Cast/EchoRule.php b/src/Rules/Cast/EchoRule.php index 705dc48ebd..697822b55f 100644 --- a/src/Rules/Cast/EchoRule.php +++ b/src/Rules/Cast/EchoRule.php @@ -15,7 +15,7 @@ /** * @implements Rule */ -class EchoRule implements Rule +final class EchoRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -49,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array 'Parameter #%d (%s) of echo cannot be converted to string.', $key + 1, $typeResult->getType()->describe(VerbosityLevel::value()), - ))->line($expr->getLine())->build(); + ))->identifier('echo.nonString')->line($expr->getStartLine())->build(); } return $messages; } diff --git a/src/Rules/Cast/InvalidCastRule.php b/src/Rules/Cast/InvalidCastRule.php index 4179390523..4e3bddad6e 100644 --- a/src/Rules/Cast/InvalidCastRule.php +++ b/src/Rules/Cast/InvalidCastRule.php @@ -19,7 +19,7 @@ /** * @implements Rule */ -class InvalidCastRule implements Rule +final class InvalidCastRule implements Rule { public function __construct( @@ -36,15 +36,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $castTypeCallback = static function (Type $type) use ($node): ?Type { + $castTypeCallback = static function (Type $type) use ($node): ?array { if ($node instanceof Node\Expr\Cast\Int_) { - return $type->toInteger(); + return [$type->toInteger(), 'int']; } elseif ($node instanceof Node\Expr\Cast\Bool_) { - return $type->toBoolean(); + return [$type->toBoolean(), 'bool']; } elseif ($node instanceof Node\Expr\Cast\Double) { - return $type->toFloat(); + return [$type->toFloat(), 'double']; } elseif ($node instanceof Node\Expr\Cast\String_) { - return $type->toString(); + return [$type->toString(), 'string']; } return null; @@ -55,11 +55,13 @@ public function processNode(Node $node, Scope $scope): array $node->expr, '', static function (Type $type) use ($castTypeCallback): bool { - $castType = $castTypeCallback($type); - if ($castType === null) { + $castResult = $castTypeCallback($type); + if ($castResult === null) { return true; } + [$castType] = $castResult; + return !$castType instanceof ErrorType; }, ); @@ -68,7 +70,13 @@ static function (Type $type) use ($castTypeCallback): bool { return []; } - $castType = $castTypeCallback($type); + $castResult = $castTypeCallback($type); + if ($castResult === null) { + return []; + } + + [$castType, $castIdentifier] = $castResult; + if ($castType instanceof ErrorType) { $classReflection = $this->reflectionProvider->getClass(get_class($node)); $shortName = $classReflection->getNativeReflection()->getShortName(); @@ -84,7 +92,7 @@ static function (Type $type) use ($castTypeCallback): bool { 'Cannot cast %s to %s.', $scope->getType($node->expr)->describe(VerbosityLevel::value()), $shortName, - ))->line($node->getLine())->build(), + ))->identifier(sprintf('cast.%s', $castIdentifier))->line($node->getStartLine())->build(), ]; } diff --git a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php index 293adeac5a..a2c04dc054 100644 --- a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php +++ b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php @@ -3,8 +3,8 @@ namespace PHPStan\Rules\Cast; use PhpParser\Node; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -14,13 +14,13 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ -class InvalidPartOfEncapsedStringRule implements Rule +final class InvalidPartOfEncapsedStringRule implements Rule { public function __construct( - private Standard $printer, + private ExprPrinter $exprPrinter, private RuleLevelHelper $ruleLevelHelper, ) { @@ -28,14 +28,14 @@ public function __construct( public function getNodeType(): string { - return Node\Scalar\Encapsed::class; + return Node\Scalar\InterpolatedString::class; } public function processNode(Node $node, Scope $scope): array { $messages = []; foreach ($node->parts as $part) { - if ($part instanceof Node\Scalar\EncapsedStringPart) { + if ($part instanceof Node\InterpolatedStringPart) { continue; } @@ -56,9 +56,9 @@ public function processNode(Node $node, Scope $scope): array } $messages[] = RuleErrorBuilder::message(sprintf( 'Part %s (%s) of encapsed string cannot be cast to string.', - $this->printer->prettyPrintExpr($part), + $this->exprPrinter->printExpr($part), $partType->describe(VerbosityLevel::value()), - ))->line($part->getLine())->build(); + ))->identifier('encapsedStringPart.nonString')->line($part->getStartLine())->build(); } return $messages; diff --git a/src/Rules/Cast/PrintRule.php b/src/Rules/Cast/PrintRule.php index e511f76ebf..a8f525c91e 100644 --- a/src/Rules/Cast/PrintRule.php +++ b/src/Rules/Cast/PrintRule.php @@ -15,7 +15,7 @@ /** * @implements Rule */ -class PrintRule implements Rule +final class PrintRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -42,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array return [RuleErrorBuilder::message(sprintf( 'Parameter %s of print cannot be converted to string.', $typeResult->getType()->describe(VerbosityLevel::value()), - ))->line($node->expr->getLine())->build()]; + ))->identifier('print.nonString')->line($node->expr->getStartLine())->build()]; } return []; diff --git a/src/Rules/Cast/UnsetCastRule.php b/src/Rules/Cast/UnsetCastRule.php index 6260d9805b..9d0bdbf724 100644 --- a/src/Rules/Cast/UnsetCastRule.php +++ b/src/Rules/Cast/UnsetCastRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class UnsetCastRule implements Rule +final class UnsetCastRule implements Rule { public function __construct(private PhpVersion $phpVersion) @@ -30,7 +30,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('The (unset) cast is no longer supported in PHP 8.0 and later.')->nonIgnorable()->build(), + RuleErrorBuilder::message('The (unset) cast is no longer supported in PHP 8.0 and later.') + ->identifier('cast.unset') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/ClassCaseSensitivityCheck.php b/src/Rules/ClassCaseSensitivityCheck.php index cb3f572609..b58a589074 100644 --- a/src/Rules/ClassCaseSensitivityCheck.php +++ b/src/Rules/ClassCaseSensitivityCheck.php @@ -2,12 +2,11 @@ namespace PHPStan\Rules; -use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; use function sprintf; use function strtolower; -class ClassCaseSensitivityCheck +final class ClassCaseSensitivityCheck { public function __construct(private ReflectionProvider $reflectionProvider, private bool $checkInternalClassCaseSensitivity) @@ -16,7 +15,7 @@ public function __construct(private ReflectionProvider $reflectionProvider, priv /** * @param ClassNameNodePair[] $pairs - * @return RuleError[] + * @return list */ public function checkClassNames(array $pairs): array { @@ -38,28 +37,19 @@ public function checkClassNames(array $pairs): array continue; } + $typeName = $classReflection->getClassTypeDescription(); $errors[] = RuleErrorBuilder::message(sprintf( '%s %s referenced with incorrect case: %s.', - $this->getTypeName($classReflection), + $typeName, $realClassName, $className, - ))->line($pair->getNode()->getLine())->build(); + )) + ->identifier(sprintf('%s.nameCase', strtolower($typeName))) + ->line($pair->getNode()->getStartLine()) + ->build(); } return $errors; } - private function getTypeName(ClassReflection $classReflection): string - { - if ($classReflection->isInterface()) { - return 'Interface'; - } elseif ($classReflection->isTrait()) { - return 'Trait'; - } elseif ($classReflection->isEnum()) { - return 'Enum'; - } - - return 'Class'; - } - } diff --git a/src/Rules/ClassForbiddenNameCheck.php b/src/Rules/ClassForbiddenNameCheck.php new file mode 100644 index 0000000000..f1f9f032a3 --- /dev/null +++ b/src/Rules/ClassForbiddenNameCheck.php @@ -0,0 +1,93 @@ + '_PHPStan_', + 'Rector' => 'RectorPrefix', + 'PHP-Scoper' => '_PhpScoper', + 'PHPUnit' => 'PHPUnitPHAR', + 'Box' => '_HumbugBox', + ]; + + public function __construct(private Container $container) + { + } + + /** + * @param ClassNameNodePair[] $pairs + * @return list + */ + public function checkClassNames(array $pairs): array + { + $extensions = $this->container->getServicesByTag(ForbiddenClassNameExtension::EXTENSION_TAG); + + $classPrefixes = array_merge( + self::INTERNAL_CLASS_PREFIXES, + ...array_map( + static fn (ForbiddenClassNameExtension $extension): array => $extension->getClassPrefixes(), + $extensions, + ), + ); + + $errors = []; + foreach ($pairs as $pair) { + $className = $pair->getClassName(); + + $projectName = null; + $withoutPrefixClassName = null; + foreach ($classPrefixes as $project => $prefix) { + if (!str_starts_with($className, $prefix)) { + continue; + } + + $projectName = $project; + $withoutPrefixClassName = substr($className, strlen($prefix)); + + if (strpos($withoutPrefixClassName, '\\') === false) { + continue; + } + + $withoutPrefixClassName = substr($withoutPrefixClassName, strpos($withoutPrefixClassName, '\\')); + } + + if ($projectName === null) { + continue; + } + + $error = RuleErrorBuilder::message(sprintf( + 'Referencing prefixed %s class: %s.', + $projectName, + $className, + )) + ->line($pair->getNode()->getStartLine()) + ->identifier('class.prefixed') + ->nonIgnorable(); + + if ($withoutPrefixClassName !== null) { + $error->tip(sprintf( + 'This is most likely unintentional. Did you mean to type %s?', + $withoutPrefixClassName, + )); + } + + $errors[] = $error->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/ClassNameCheck.php b/src/Rules/ClassNameCheck.php new file mode 100644 index 0000000000..7ce8b7a27e --- /dev/null +++ b/src/Rules/ClassNameCheck.php @@ -0,0 +1,76 @@ + + */ + public function checkClassNames( + Scope $scope, + array $pairs, + ?ClassNameUsageLocation $location, + bool $checkClassCaseSensitivity = true, + ): array + { + $errors = []; + + if ($checkClassCaseSensitivity) { + foreach ($this->classCaseSensitivityCheck->checkClassNames($pairs) as $error) { + $errors[] = $error; + } + } + foreach ($this->classForbiddenNameCheck->checkClassNames($pairs) as $error) { + $errors[] = $error; + } + + if ($location === null) { + return $errors; + } + + /** @var RestrictedClassNameUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedClassNameUsageExtension::CLASS_NAME_EXTENSION_TAG); + if ($extensions === []) { + return $errors; + } + + foreach ($pairs as $pair) { + if (!$this->reflectionProvider->hasClass($pair->getClassName())) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($pair->getClassName()); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedClassNameUsage($classReflection, $scope, $location); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->line($pair->getNode()->getStartLine()) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/ClassNameNodePair.php b/src/Rules/ClassNameNodePair.php index 355fa61e01..cf39baa35c 100644 --- a/src/Rules/ClassNameNodePair.php +++ b/src/Rules/ClassNameNodePair.php @@ -4,7 +4,7 @@ use PhpParser\Node; -class ClassNameNodePair +final class ClassNameNodePair { public function __construct(private string $className, private Node $node) diff --git a/src/Rules/ClassNameUsageLocation.php b/src/Rules/ClassNameUsageLocation.php new file mode 100644 index 0000000000..d8727ac5f2 --- /dev/null +++ b/src/Rules/ClassNameUsageLocation.php @@ -0,0 +1,304 @@ +data['method'] ?? null; + } + + public function getProperty(): ?ExtendedPropertyReflection + { + return $this->data['property'] ?? null; + } + + public function getFunction(): ?FunctionReflection + { + return $this->data['function'] ?? null; + } + + public function getPhpDocTagName(): ?string + { + return $this->data['phpDocTagName'] ?? null; + } + + public function getAssertedExprString(): ?string + { + return $this->data['assertedExprString'] ?? null; + } + + public function getClassConstant(): ?ClassConstantReflection + { + return $this->data['classConstant'] ?? null; + } + + public function getCurrentClassName(): ?string + { + return $this->data['currentClassName'] ?? null; + } + + public function getParameterName(): ?string + { + return $this->data['parameterName'] ?? null; + } + + public function getTypeAliasName(): ?string + { + return $this->data['typeAliasName'] ?? null; + } + + public function getMethodTagName(): ?string + { + return $this->data['methodTagName'] ?? null; + } + + public function getPropertyTagName(): ?string + { + return $this->data['propertyTagName'] ?? null; + } + + public function getTemplateTagName(): ?string + { + return $this->data['templateTagName'] ?? null; + } + + public function isInAnomyousFunction(): bool + { + return $this->data['isInAnonymousFunction'] ?? false; + } + + public function createMessage(string $part): string + { + switch ($this->value) { + case self::TRAIT_USE: + if ($this->getCurrentClassName() !== null) { + return sprintf('Usage of %s in class %s.', $part, $this->getCurrentClassName()); + } + return sprintf('Usage of %s.', $part); + case self::STATIC_PROPERTY_ACCESS: + $property = $this->getProperty(); + if ($property !== null) { + return sprintf('Access to static property $%s on %s.', $property->getName(), $part); + } + + return sprintf('Access to static property on %s.', $part); + case self::PHPDOC_TAG_ASSERT: + $phpDocTagName = $this->getPhpDocTagName(); + $assertExprString = $this->getAssertedExprString(); + if ($phpDocTagName !== null && $assertExprString !== null) { + return sprintf('PHPDoc tag %s for %s references %s.', $phpDocTagName, $assertExprString, $part); + } + + return sprintf('Assert tag references %s.', $part); + case self::ATTRIBUTE: + return sprintf('Attribute references %s.', $part); + case self::EXCEPTION_CATCH: + return sprintf('Catching %s.', $part); + case self::CLASS_CONSTANT_ACCESS: + if ($this->getClassConstant() !== null) { + return sprintf('Access to constant %s on %s.', $this->getClassConstant()->getName(), $part); + } + return sprintf('Access to constant on %s.', $part); + case self::CLASS_IMPLEMENTS: + if ($this->getCurrentClassName() !== null) { + return sprintf('Class %s implements %s.', $this->getCurrentClassName(), $part); + } + + return sprintf('Anonymous class implements %s.', $part); + case self::ENUM_IMPLEMENTS: + if ($this->getCurrentClassName() !== null) { + return sprintf('Enum %s implements %s.', $this->getCurrentClassName(), $part); + } + + return sprintf('Enum implements %s.', $part); + case self::INTERFACE_EXTENDS: + if ($this->getCurrentClassName() !== null) { + return sprintf('Interface %s extends %s.', $this->getCurrentClassName(), $part); + } + + return sprintf('Interface extends %s.', $part); + case self::CLASS_EXTENDS: + if ($this->getCurrentClassName() !== null) { + return sprintf('Class %s extends %s.', $this->getCurrentClassName(), $part); + } + + return sprintf('Anonymous class extends %s.', $part); + case self::INSTANCEOF: + return sprintf('Instanceof references %s.', $part); + case self::PROPERTY_TYPE: + $property = $this->getProperty(); + if ($property !== null) { + return sprintf('Property $%s references %s in its type.', $property->getName(), $part); + } + return sprintf('Property references %s in its type.', $part); + case self::PARAMETER_TYPE: + $parameterName = $this->getParameterName(); + if ($parameterName !== null) { + if ($this->isInAnomyousFunction()) { + return sprintf('Parameter $%s of anonymous function has typehint with %s.', $parameterName, $part); + } + if ($this->getMethod() !== null) { + if ($this->getCurrentClassName() !== null) { + return sprintf('Parameter $%s of method %s::%s() has typehint with %s.', $parameterName, $this->getCurrentClassName(), $this->getMethod()->getName(), $part); + } + + return sprintf('Parameter $%s of method %s() in anonymous class has typehint with %s.', $parameterName, $this->getMethod()->getName(), $part); + } + + if ($this->getFunction() !== null) { + return sprintf('Parameter $%s of function %s() has typehint with %s.', $parameterName, $this->getFunction()->getName(), $part); + } + + return sprintf('Parameter $%s has typehint with %s.', $parameterName, $part); + } + + return sprintf('Parameter has typehint with %s.', $part); + case self::RETURN_TYPE: + if ($this->isInAnomyousFunction()) { + return sprintf('Return type of anonymous function has typehint with %s.', $part); + } + if ($this->getMethod() !== null) { + if ($this->getCurrentClassName() !== null) { + return sprintf('Return type of method %s::%s() has typehint with %s.', $this->getCurrentClassName(), $this->getMethod()->getName(), $part); + } + + return sprintf('Return type of method %s() in anonymous class has typehint with %s.', $this->getMethod()->getName(), $part); + } + + if ($this->getFunction() !== null) { + return sprintf('Return type of function %s() has typehint with %s.', $this->getFunction()->getName(), $part); + } + + return sprintf('Return type has typehint with %s.', $part); + case self::PHPDOC_TAG_SELF_OUT: + return sprintf('PHPDoc tag @phpstan-self-out references %s.', $part); + case self::PHPDOC_TAG_VAR: + return sprintf('PHPDoc tag @var references %s.', $part); + case self::INSTANTIATION: + return sprintf('Instantiation of %s.', $part); + case self::TYPE_ALIAS: + if ($this->getTypeAliasName() !== null) { + return sprintf('Type alias %s references %s.', $this->getTypeAliasName(), $part); + } + + return sprintf('Type alias references %s.', $part); + case self::PHPDOC_TAG_METHOD: + if ($this->getMethodTagName() !== null) { + return sprintf('PHPDoc tag @method for %s() references %s.', $this->getMethodTagName(), $part); + } + return sprintf('PHPDoc tag @method references %s.', $part); + case self::PHPDOC_TAG_MIXIN: + return sprintf('PHPDoc tag @mixin references %s.', $part); + case self::PHPDOC_TAG_PROPERTY: + if ($this->getPropertyTagName() !== null) { + return sprintf('PHPDoc tag @property for $%s references %s.', $this->getPropertyTagName(), $part); + } + return sprintf('PHPDoc tag @property references %s.', $part); + case self::PHPDOC_TAG_REQUIRE_EXTENDS: + return sprintf('PHPDoc tag @phpstan-require-extends references %s.', $part); + case self::PHPDOC_TAG_REQUIRE_IMPLEMENTS: + return sprintf('PHPDoc tag @phpstan-require-implements references %s.', $part); + case self::STATIC_METHOD_CALL: + $method = $this->getMethod(); + if ($method !== null) { + return sprintf('Call to static method %s() on %s.', $method->getName(), $part); + } + + return sprintf('Call to static method on %s.', $part); + case self::PHPDOC_TAG_TEMPLATE_BOUND: + if ($this->getTemplateTagName() !== null) { + return sprintf('PHPDoc tag @template %s bound references %s.', $this->getTemplateTagName(), $part); + } + + return sprintf('PHPDoc tag @template bound references %s.', $part); + case self::PHPDOC_TAG_TEMPLATE_DEFAULT: + if ($this->getTemplateTagName() !== null) { + return sprintf('PHPDoc tag @template %s default references %s.', $this->getTemplateTagName(), $part); + } + + return sprintf('PHPDoc tag @template default references %s.', $part); + } + } + + public function createIdentifier(string $secondPart): string + { + if ($this->value === self::CLASS_IMPLEMENTS) { + return sprintf('class.implements%s', ucfirst($secondPart)); + } + if ($this->value === self::ENUM_IMPLEMENTS) { + return sprintf('enum.implements%s', ucfirst($secondPart)); + } + if ($this->value === self::INTERFACE_EXTENDS) { + return sprintf('interface.extends%s', ucfirst($secondPart)); + } + if ($this->value === self::CLASS_EXTENDS) { + return sprintf('class.extends%s', ucfirst($secondPart)); + } + if ($this->value === self::PHPDOC_TAG_TEMPLATE_BOUND) { + return sprintf('generics.%sBound', $secondPart); + } + if ($this->value === self::PHPDOC_TAG_TEMPLATE_DEFAULT) { + return sprintf('generics.%sDefault', $secondPart); + } + + return sprintf('%s.%s', $this->value, $secondPart); + } + +} diff --git a/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php index a2ca28b670..551f56c464 100644 --- a/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php +++ b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class AccessPrivateConstantThroughStaticRule implements Rule +final class AccessPrivateConstantThroughStaticRule implements Rule { public function getNodeType(): string @@ -54,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array 'Unsafe access to private constant %s::%s through static::.', $constant->getDeclaringClass()->getDisplayName(), $constantName, - ))->build(), + ))->identifier('staticClassAccess.privateConstant')->build(), ]; } diff --git a/src/Rules/Classes/AllowedSubTypesRule.php b/src/Rules/Classes/AllowedSubTypesRule.php new file mode 100644 index 0000000000..5e58d49b3a --- /dev/null +++ b/src/Rules/Classes/AllowedSubTypesRule.php @@ -0,0 +1,68 @@ + + */ +final class AllowedSubTypesRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + /** + * @param InClassNode $node + */ + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $className = $classReflection->getName(); + + $parents = array_values($classReflection->getImmediateInterfaces()); + $parentClass = $classReflection->getParentClass(); + if ($parentClass !== null) { + $parents[] = $parentClass; + } + + $messages = []; + + foreach ($parents as $parentReflection) { + $allowedSubTypes = $parentReflection->getAllowedSubTypes(); + if ($allowedSubTypes === null) { + continue; + } + + foreach ($allowedSubTypes as $allowedSubType) { + if (!$allowedSubType->isObject()->yes()) { + continue; + } + + if ($allowedSubType->getObjectClassNames() === [$className]) { + continue 2; + } + } + + $identifierType = strtolower($classReflection->getClassTypeDescription()); + $messages[] = RuleErrorBuilder::message(sprintf( + 'Type %s is not allowed to be a subtype of %s.', + $className, + $parentReflection->getName(), + ))->identifier(sprintf('%s.disallowedSubtype', $identifierType))->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index 21d6bda749..190be4331b 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -5,13 +5,17 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; /** - * @implements Rule + * @implements Rule */ -class ClassAttributesRule implements Rule +final class ClassAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) @@ -20,17 +24,46 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\ClassLike::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - return $this->attributesCheck->check( + $classLikeNode = $node->getOriginalNode(); + + $errors = $this->attributesCheck->check( $scope, - $node->attrGroups, + $classLikeNode->attrGroups, Attribute::TARGET_CLASS, 'class', ); + + $classReflection = $node->getClassReflection(); + if ( + $classReflection->isReadOnly() + || $classReflection->isEnum() + || $classReflection->isInterface() + ) { + $typeName = 'readonly class'; + $identifier = 'class.allowDynamicPropertiesReadonly'; + if ($classReflection->isEnum()) { + $typeName = 'enum'; + $identifier = 'enum.allowDynamicProperties'; + } + if ($classReflection->isInterface()) { + $typeName = 'interface'; + $identifier = 'interface.allowDynamicProperties'; + } + + if (count($classReflection->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class AllowDynamicProperties cannot be used with %s.', $typeName)) + ->identifier($identifier) + ->nonIgnorable() + ->build(); + } + } + + return $errors; } } diff --git a/src/Rules/Classes/ClassConstantAttributesRule.php b/src/Rules/Classes/ClassConstantAttributesRule.php index d8256191a0..71597d271a 100644 --- a/src/Rules/Classes/ClassConstantAttributesRule.php +++ b/src/Rules/Classes/ClassConstantAttributesRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class ClassConstantAttributesRule implements Rule +final class ClassConstantAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 98aba984e9..a14428d890 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -3,14 +3,18 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Scalar\String_; 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\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -28,13 +32,13 @@ /** * @implements Rule */ -class ClassConstantRule implements Rule +final class ClassConstantRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private PhpVersion $phpVersion, ) { @@ -47,11 +51,34 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; + $errors = []; + if ($node->name instanceof Node\Identifier) { + $constantNameScopes = [$node->name->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $constantNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $constantNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } + } + + foreach ($constantNameScopes as $constantName => $constantScope) { + $errors = array_merge($errors, $this->processSingleClassConstFetch( + $constantScope, + $node, + (string) $constantName, // @phpstan-ignore cast.useless + )); } - $constantName = $node->name->name; + return $errors; + } + + /** + * @return list + */ + private function processSingleClassConstFetch(Scope $scope, ClassConstFetch $node, string $constantName): array + { $class = $node->class; $messages = []; if ($class instanceof Node\Name) { @@ -60,7 +87,9 @@ public function processNode(Node $node, Scope $scope): array if (in_array($lowercasedClassName, ['self', 'static'], true)) { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className)) + ->identifier(sprintf('outOfClass.%s', $lowercasedClassName)) + ->build(), ]; } @@ -68,7 +97,9 @@ public function processNode(Node $node, Scope $scope): array } elseif ($lowercasedClassName === 'parent') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className)) + ->identifier(sprintf('outOfClass.%s', $lowercasedClassName)) + ->build(), ]; } $currentClassReflection = $scope->getClassReflection(); @@ -78,7 +109,7 @@ public function processNode(Node $node, Scope $scope): array 'Access to parent::%s but %s does not extend any class.', $constantName, $currentClassReflection->getDisplayName(), - ))->build(), + ))->identifier('class.noParent')->build(), ]; } $classType = $scope->resolveTypeByName($class); @@ -90,20 +121,51 @@ public function processNode(Node $node, Scope $scope): array if (strtolower($constantName) === 'class') { return [ - RuleErrorBuilder::message(sprintf('Class %s not found.', $className))->discoveringSymbolsTip()->build(), + RuleErrorBuilder::message(sprintf('Class %s not found.', $className)) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(), ]; } return [ RuleErrorBuilder::message( sprintf('Access to constant %s on an unknown class %s.', $constantName, $className), - )->discoveringSymbolsTip()->build(), + ) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); } $classType = $scope->resolveTypeByName($class); + if (strtolower($constantName) !== 'class') { + foreach ($classType->getObjectClassReflections() as $classTypeReflection) { + if (!$classTypeReflection->isTrait()) { + continue; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot access constant %s on trait %s.', + $constantName, + $classTypeReflection->getDisplayName(), + ))->identifier('classConstant.onTrait')->build(), + ]; + } + } + + $locationData = []; + $locationClassReflection = $this->reflectionProvider->getClass($className); + if ($locationClassReflection->hasConstant($constantName)) { + $locationData['classConstant'] = $locationClassReflection->getConstant($constantName); + } + + $messages = $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($className, $class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::CLASS_CONSTANT_ACCESS, $locationData), + ); } if (strtolower($constantName) === 'class') { @@ -125,14 +187,16 @@ public function processNode(Node $node, Scope $scope): array if (!$this->phpVersion->supportsClassConstantOnExpression()) { return [ RuleErrorBuilder::message('Accessing ::class constant on an expression is supported only on PHP 8.0 and later.') + ->identifier('classConstant.notSupported') ->nonIgnorable() ->build(), ]; } - if ((new StringType())->isSuperTypeOf($classType)->yes()) { + if (!$class instanceof Node\Scalar\String_ && $classType->isString()->yes()) { return [ RuleErrorBuilder::message('Accessing ::class constant on a dynamic string is not supported in PHP.') + ->identifier('classConstant.dynamicString') ->nonIgnorable() ->build(), ]; @@ -140,7 +204,7 @@ public function processNode(Node $node, Scope $scope): array } } - if ((new StringType())->isSuperTypeOf($classType)->yes()) { + if ($classType->isString()->yes()) { return $messages; } @@ -156,11 +220,11 @@ public function processNode(Node $node, Scope $scope): array 'Cannot access constant %s on %s.', $constantName, $typeForDescribe->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('classConstant.nonObject')->build(), ]); } - if (strtolower($constantName) === 'class') { + if (strtolower($constantName) === 'class' || $scope->hasExpressionType($node)->yes()) { return $messages; } @@ -170,7 +234,7 @@ public function processNode(Node $node, Scope $scope): array 'Access to undefined constant %s::%s.', $typeForDescribe->describe(VerbosityLevel::typeOnly()), $constantName, - ))->build(), + ))->identifier('classConstant.notFound')->build(), ]); } @@ -182,6 +246,9 @@ public function processNode(Node $node, Scope $scope): array $constantReflection->isPrivate() ? 'private' : 'protected', $constantName, $constantReflection->getDeclaringClass()->getDisplayName(), + ))->identifier(sprintf( + 'classConstant.%s', + $constantReflection->isPrivate() ? 'private' : 'protected', ))->build(), ]); } diff --git a/src/Rules/Classes/DuplicateClassDeclarationRule.php b/src/Rules/Classes/DuplicateClassDeclarationRule.php new file mode 100644 index 0000000000..bf8ac7f05d --- /dev/null +++ b/src/Rules/Classes/DuplicateClassDeclarationRule.php @@ -0,0 +1,66 @@ + + */ +final class DuplicateClassDeclarationRule implements Rule +{ + + public function __construct(private Reflector $reflector, private RelativePathHelper $relativePathHelper) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $thisClass = $node->getClassReflection(); + $className = $thisClass->getName(); + $allClasses = $this->reflector->reflectAllClasses(); + $filteredClasses = []; + foreach ($allClasses as $reflectionClass) { + if ($reflectionClass->getName() !== $className) { + continue; + } + + $filteredClasses[] = $reflectionClass; + } + + if (count($filteredClasses) < 2) { + return []; + } + + $filteredClasses = array_filter($filteredClasses, static fn (ReflectionClass $class) => $class->getStartLine() !== $thisClass->getNativeReflection()->getStartLine()); + + $identifierType = strtolower($thisClass->getClassTypeDescription()); + + return [ + RuleErrorBuilder::message(sprintf( + "Class %s declared multiple times:\n%s", + $thisClass->getDisplayName(), + implode("\n", array_map(fn (ReflectionClass $class) => sprintf('- %s:%d', $this->relativePathHelper->getRelativePath($class->getFileName() ?? 'unknown'), $class->getStartLine()), $filteredClasses)), + ))->identifier(sprintf('%s.duplicate', $identifierType))->build(), + ]; + } + +} diff --git a/src/Rules/Classes/DuplicateDeclarationRule.php b/src/Rules/Classes/DuplicateDeclarationRule.php index 257bf2eaf7..047ce4cee9 100644 --- a/src/Rules/Classes/DuplicateDeclarationRule.php +++ b/src/Rules/Classes/DuplicateDeclarationRule.php @@ -18,7 +18,7 @@ /** * @implements Rule */ -class DuplicateDeclarationRule implements Rule +final class DuplicateDeclarationRule implements Rule { public function getNodeType(): string @@ -28,10 +28,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $classReflection = $scope->getClassReflection(); - if ($classReflection === null) { - throw new ShouldNotHappenException(); - } + $classReflection = $node->getClassReflection(); + + $identifierType = strtolower($classReflection->getClassTypeDescription()); $errors = []; @@ -43,7 +42,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare enum case %s::%s.', $classReflection->getDisplayName(), $stmtNode->name->name, - ))->line($stmtNode->getLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateEnumCase', $identifierType)) + ->line($stmtNode->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredClassConstantsOrEnumCases[$stmtNode->name->name] = true; } @@ -54,7 +56,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare constant %s::%s.', $classReflection->getDisplayName(), $classConstNode->name->name, - ))->line($classConstNode->getLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateConstant', $identifierType)) + ->line($classConstNode->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredClassConstantsOrEnumCases[$classConstNode->name->name] = true; } @@ -70,7 +75,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare property %s::$%s.', $classReflection->getDisplayName(), $property->name->name, - ))->line($property->getLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateProperty', $identifierType)) + ->line($property->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredProperties[$property->name->name] = true; } @@ -96,7 +104,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare property %s::$%s.', $classReflection->getDisplayName(), $propertyName, - ))->line($param->getLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateProperty', $identifierType)) + ->line($param->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredProperties[$propertyName] = true; } @@ -107,7 +118,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare method %s::%s().', $classReflection->getDisplayName(), $method->name->name, - ))->line($method->getStartLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateMethod', $identifierType)) + ->line($method->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredFunctions[strtolower($method->name->name)] = true; } diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index af3de8df85..51c07fdfd8 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -4,16 +4,23 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; +use PHPStan\Type\IntegerType; +use PHPStan\Type\StringType; +use PHPStan\Type\VerbosityLevel; +use Serializable; use function array_key_exists; +use function count; +use function implode; +use function in_array; use function sprintf; /** - * @implements Rule + * @implements Rule */ -class EnumSanityRule implements Rule +final class EnumSanityRule implements Rule { private const ALLOWED_MAGIC_METHODS = [ @@ -24,83 +31,193 @@ class EnumSanityRule implements Rule public function getNodeType(): string { - return Node\Stmt\Enum_::class; + return InClassNode::class; } - /** - * @param Node\Stmt\Enum_ $node - */ public function processNode(Node $node, Scope $scope): array { - $errors = []; - - if ($node->namespacedName === null) { - throw new ShouldNotHappenException(); + $classReflection = $node->getClassReflection(); + if (!$classReflection->isEnum()) { + return []; } - foreach ($node->getMethods() as $methodNode) { - if ($methodNode->isAbstract()) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Enum %s contains abstract method %s().', - $node->namespacedName->toString(), - $methodNode->name->name, - ))->line($methodNode->getLine())->nonIgnorable()->build(); - } + /** @var Node\Stmt\Enum_ $enumNode */ + $enumNode = $node->getOriginalNode(); - $lowercasedMethodName = $methodNode->name->toLowerString(); + $errors = []; + foreach ($enumNode->getMethods() as $methodNode) { + $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(); + $classReflection->getDisplayName(), + )) + ->identifier('enum.constructor') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } elseif ($lowercasedMethodName === '__destruct') { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s contains destructor.', - $node->namespacedName->toString(), - ))->line($methodNode->getLine())->nonIgnorable()->build(); + $classReflection->getDisplayName(), + )) + ->identifier('enum.destructor') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } elseif (!array_key_exists($lowercasedMethodName, self::ALLOWED_MAGIC_METHODS)) { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s contains magic method %s().', - $node->namespacedName->toString(), + $classReflection->getDisplayName(), $methodNode->name->name, - ))->line($methodNode->getLine())->nonIgnorable()->build(); + )) + ->identifier('enum.magicMethod') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } } if ($lowercasedMethodName === 'cases') { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s cannot redeclare native method %s().', - $node->namespacedName->toString(), + $classReflection->getDisplayName(), $methodNode->name->name, - ))->line($methodNode->getLine())->nonIgnorable()->build(); + )) + ->identifier('enum.methodRedeclaration') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } - if ($node->scalarType === null) { + if ($enumNode->scalarType === null) { continue; } - if ($lowercasedMethodName !== 'from' && $lowercasedMethodName !== 'tryfrom') { + if (!in_array($lowercasedMethodName, ['from', 'tryfrom'], true)) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s cannot redeclare native method %s().', - $node->namespacedName->toString(), + $classReflection->getDisplayName(), $methodNode->name->name, - ))->line($methodNode->getLine())->nonIgnorable()->build(); + )) + ->identifier('enum.methodRedeclaration') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } if ( - $node->scalarType !== null - && $node->scalarType->name !== 'int' - && $node->scalarType->name !== 'string' + $enumNode->scalarType !== null + && !in_array($enumNode->scalarType->name, ['int', 'string'], true) ) { $errors[] = RuleErrorBuilder::message(sprintf( 'Backed enum %s can have only "int" or "string" type.', - $node->namespacedName->toString(), - ))->line($node->scalarType->getLine())->nonIgnorable()->build(); + $classReflection->getDisplayName(), + )) + ->identifier('enum.backingType') + ->line($enumNode->scalarType->getStartLine()) + ->nonIgnorable() + ->build(); + } + + if ($classReflection->implementsInterface(Serializable::class)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s cannot implement the Serializable interface.', + $classReflection->getDisplayName(), + )) + ->identifier('enum.serializable') + ->line($enumNode->getStartLine()) + ->nonIgnorable() + ->build(); + } + + $enumCases = []; + foreach ($enumNode->stmts as $stmt) { + if (!$stmt instanceof Node\Stmt\EnumCase) { + continue; + } + $caseName = $stmt->name->name; + + if ($stmt->expr instanceof Node\Scalar\Int_ || $stmt->expr instanceof Node\Scalar\String_) { + if ($enumNode->scalarType === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s is not backed, but case %s has value %s.', + $classReflection->getDisplayName(), + $caseName, + $stmt->expr->value, + )) + ->identifier('enum.caseWithValue') + ->line($stmt->getStartLine()) + ->nonIgnorable() + ->build(); + } else { + $caseValue = $stmt->expr->value; + + if (!isset($enumCases[$caseValue])) { + $enumCases[$caseValue] = []; + } + + $enumCases[$caseValue][] = $caseName; + } + } + + if ($enumNode->scalarType === null) { + continue; + } + + if ($stmt->expr === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum case %s::%s does not have a value but the enum is backed with the "%s" type.', + $classReflection->getDisplayName(), + $caseName, + $enumNode->scalarType->name, + )) + ->identifier('enum.missingCase') + ->line($stmt->getStartLine()) + ->nonIgnorable() + ->build(); + continue; + } + + $exprType = $scope->getType($stmt->expr); + $scalarType = $enumNode->scalarType->toLowerString() === 'int' ? new IntegerType() : new StringType(); + if ($scalarType->isSuperTypeOf($exprType)->yes()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum case %s::%s value %s does not match the "%s" type.', + $classReflection->getDisplayName(), + $caseName, + $exprType->describe(VerbosityLevel::value()), + $scalarType->describe(VerbosityLevel::typeOnly()), + )) + ->identifier('enum.caseType') + ->line($stmt->getStartLine()) + ->nonIgnorable() + ->build(); + } + + foreach ($enumCases as $caseValue => $caseNames) { + if (count($caseNames) <= 1) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s has duplicate value %s for cases %s.', + $classReflection->getDisplayName(), + $caseValue, + implode(', ', $caseNames), + )) + ->identifier('enum.duplicateValue') + ->line($enumNode->getStartLine()) + ->nonIgnorable() + ->build(); } return $errors; diff --git a/src/Rules/Classes/ExistingClassInClassExtendsRule.php b/src/Rules/Classes/ExistingClassInClassExtendsRule.php index 513c7efcd5..9bd5e0ef5e 100644 --- a/src/Rules/Classes/ExistingClassInClassExtendsRule.php +++ b/src/Rules/Classes/ExistingClassInClassExtendsRule.php @@ -5,8 +5,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -14,12 +15,13 @@ /** * @implements Rule */ -class ExistingClassInClassExtendsRule implements Rule +final class ExistingClassInClassExtendsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, ) { } @@ -35,18 +37,33 @@ public function processNode(Node $node, Scope $scope): array return []; } $extendedClassName = (string) $node->extends; - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($extendedClassName, $node->extends)]); $currentClassName = null; if (isset($node->namespacedName)) { $currentClassName = (string) $node->namespacedName; } + $messages = $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($extendedClassName, $node->extends)], + ClassNameUsageLocation::from(ClassNameUsageLocation::CLASS_EXTENDS, [ + 'currentClassName' => $currentClassName, + ]), + ); + if (!$this->reflectionProvider->hasClass($extendedClassName)) { if (!$scope->isInClassExists($extendedClassName)) { - $messages[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( '%s extends unknown class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $extendedClassName, - ))->nonIgnorable()->discoveringSymbolsTip()->build(); + )) + ->identifier('class.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedClassName); @@ -55,31 +72,69 @@ public function processNode(Node $node, Scope $scope): array '%s extends interface %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('class.extendsInterface') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends trait %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('class.extendsTrait') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends enum %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('class.extendsEnum') + ->nonIgnorable() + ->build(); } elseif ($reflection->isFinalByKeyword()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends final class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('class.extendsFinal') + ->nonIgnorable() + ->build(); } elseif ($reflection->isFinal()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends @final class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->build(); + )) + ->identifier('class.extendsFinalByPhpDoc') + ->build(); + } + + if ($reflection->isClass()) { + if ($node->isReadonly()) { + if (!$reflection->isReadOnly()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends non-readonly class %s.', + $currentClassName !== null ? sprintf('Readonly class %s', $currentClassName) : 'Anonymous readonly class', + $reflection->getDisplayName(), + )) + ->identifier('class.readOnly') + ->nonIgnorable() + ->build(); + } + } elseif ($reflection->isReadOnly()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends readonly class %s.', + $currentClassName !== null ? sprintf('Non-readonly class %s', $currentClassName) : 'Anonymous non-readonly class', + $reflection->getDisplayName(), + )) + ->identifier('class.nonReadOnly') + ->nonIgnorable() + ->build(); + } } } diff --git a/src/Rules/Classes/ExistingClassInInstanceOfRule.php b/src/Rules/Classes/ExistingClassInInstanceOfRule.php index 775eadf450..7a4d4d21eb 100644 --- a/src/Rules/Classes/ExistingClassInInstanceOfRule.php +++ b/src/Rules/Classes/ExistingClassInInstanceOfRule.php @@ -6,10 +6,13 @@ use PhpParser\Node\Expr\Instanceof_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\VerbosityLevel; +use function array_merge; use function in_array; use function sprintf; use function strtolower; @@ -17,13 +20,14 @@ /** * @implements Rule */ -class ExistingClassInInstanceOfRule implements Rule +final class ExistingClassInInstanceOfRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkClassCaseSensitivity, + private bool $discoveringSymbolsTip, ) { } @@ -50,26 +54,59 @@ public function processNode(Node $node, Scope $scope): array ], true)) { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $lowercaseName))->line($class->getLine())->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $lowercaseName)) + ->identifier(sprintf('outOfClass.%s', $lowercaseName)) + ->line($class->getStartLine()) + ->build(), ]; } return []; } + $errors = []; + if (!$this->reflectionProvider->hasClass($name)) { if ($scope->isInClassExists($name)) { return []; } + $errorBuilder = RuleErrorBuilder::message(sprintf('Class %s not found.', $name)) + ->identifier('class.notFound') + ->line($class->getStartLine()); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf('Class %s not found.', $name))->line($class->getLine())->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; - } elseif ($this->checkClassCaseSensitivity) { - return $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($name, $class)]); } - return []; + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($name, $class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::INSTANCEOF), + $this->checkClassCaseSensitivity, + ), + ); + + $classReflection = $this->reflectionProvider->getClass($name); + + if ($classReflection->isTrait()) { + $expressionType = $scope->getType($node->expr); + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Instanceof between %s and trait %s will always evaluate to false.', + $expressionType->describe(VerbosityLevel::typeOnly()), + $name, + ))->identifier('instanceof.trait')->build(); + } + + return $errors; } } diff --git a/src/Rules/Classes/ExistingClassInTraitUseRule.php b/src/Rules/Classes/ExistingClassInTraitUseRule.php index 43e84d80aa..08524a3fd4 100644 --- a/src/Rules/Classes/ExistingClassInTraitUseRule.php +++ b/src/Rules/Classes/ExistingClassInTraitUseRule.php @@ -5,8 +5,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -16,12 +17,13 @@ /** * @implements Rule */ -class ExistingClassInTraitUseRule implements Rule +final class ExistingClassInTraitUseRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, ) { } @@ -33,19 +35,27 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( - array_map(static fn (Node\Name $traitName): ClassNameNodePair => new ClassNameNodePair((string) $traitName, $traitName), $node->traits), - ); - if (!$scope->isInClass()) { throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); + + $messages = $this->classCheck->checkClassNames( + $scope, + array_map(static fn (Node\Name $traitName): ClassNameNodePair => new ClassNameNodePair((string) $traitName, $traitName), $node->traits), + ClassNameUsageLocation::from(ClassNameUsageLocation::TRAIT_USE, [ + 'currentClassName' => $classReflection->isAnonymous() ? null : $classReflection->getName(), + ]), + ); + if ($classReflection->isInterface()) { if (!$scope->isInTrait()) { foreach ($node->traits as $trait) { - $messages[] = RuleErrorBuilder::message(sprintf('Interface %s uses trait %s.', $classReflection->getName(), (string) $trait))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('Interface %s uses trait %s.', $classReflection->getName(), (string) $trait)) + ->identifier('interface.traitUse') + ->nonIgnorable() + ->build(); } } } else { @@ -61,15 +71,32 @@ public function processNode(Node $node, Scope $scope): array foreach ($node->traits as $trait) { $traitName = (string) $trait; if (!$this->reflectionProvider->hasClass($traitName)) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses unknown trait %s.', $currentName, $traitName))->nonIgnorable()->discoveringSymbolsTip()->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('%s uses unknown trait %s.', $currentName, $traitName)) + ->identifier('trait.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } else { $reflection = $this->reflectionProvider->getClass($traitName); if ($reflection->isClass()) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses class %s.', $currentName, $reflection->getDisplayName()))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('%s uses class %s.', $currentName, $reflection->getDisplayName())) + ->identifier('traitUse.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isInterface()) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses interface %s.', $currentName, $reflection->getDisplayName()))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('%s uses interface %s.', $currentName, $reflection->getDisplayName())) + ->identifier('traitUse.interface') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses enum %s.', $currentName, $reflection->getDisplayName()))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('%s uses enum %s.', $currentName, $reflection->getDisplayName())) + ->identifier('traitUse.enum') + ->nonIgnorable() + ->build(); } } } diff --git a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php index fde6d21979..b1e639af29 100644 --- a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php @@ -5,8 +5,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function array_map; @@ -15,12 +16,13 @@ /** * @implements Rule */ -class ExistingClassesInClassImplementsRule implements Rule +final class ExistingClassesInClassImplementsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, ) { } @@ -32,24 +34,36 @@ public function getNodeType(): string 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), - ); - $currentClassName = null; if (isset($node->namespacedName)) { $currentClassName = (string) $node->namespacedName; } + $messages = $this->classCheck->checkClassNames( + $scope, + array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), + ClassNameUsageLocation::from(ClassNameUsageLocation::CLASS_IMPLEMENTS, [ + 'currentClassName' => $currentClassName, + ]), + ); + foreach ($node->implements as $implements) { $implementedClassName = (string) $implements; if (!$this->reflectionProvider->hasClass($implementedClassName)) { if (!$scope->isInClassExists($implementedClassName)) { - $messages[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( '%s implements unknown interface %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $implementedClassName, - ))->nonIgnorable()->discoveringSymbolsTip()->build(); + )) + ->identifier('interface.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($implementedClassName); @@ -58,19 +72,28 @@ public function processNode(Node $node, Scope $scope): array '%s implements class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('classImplements.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s implements trait %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('classImplements.trait') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s implements enum %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('classImplements.enum') + ->nonIgnorable() + ->build(); } } } diff --git a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php index 81f6168eba..1a7f8b2883 100644 --- a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php @@ -5,8 +5,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function array_map; @@ -15,12 +16,13 @@ /** * @implements Rule */ -class ExistingClassesInEnumImplementsRule implements Rule +final class ExistingClassesInEnumImplementsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, ) { } @@ -32,21 +34,32 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $currentEnumName = (string) $node->namespacedName; + $messages = $this->classCheck->checkClassNames( + $scope, array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), + ClassNameUsageLocation::from(ClassNameUsageLocation::ENUM_IMPLEMENTS, [ + 'currentClassName' => $currentEnumName, + ]), ); - $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( + $errorBuilder = RuleErrorBuilder::message(sprintf( 'Enum %s implements unknown interface %s.', $currentEnumName, $implementedClassName, - ))->nonIgnorable()->discoveringSymbolsTip()->build(); + )) + ->identifier('interface.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($implementedClassName); @@ -55,19 +68,28 @@ public function processNode(Node $node, Scope $scope): array 'Enum %s implements class %s.', $currentEnumName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('enumImplements.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Enum %s implements trait %s.', $currentEnumName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('enumImplements.trait') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Enum %s implements enum %s.', $currentEnumName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('enumImplements.enum') + ->nonIgnorable() + ->build(); } } } diff --git a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php index f267550104..8d0d071471 100644 --- a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php +++ b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php @@ -5,8 +5,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function array_map; @@ -15,12 +16,13 @@ /** * @implements Rule */ -class ExistingClassesInInterfaceExtendsRule implements Rule +final class ExistingClassesInInterfaceExtendsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, ) { } @@ -32,20 +34,32 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $currentInterfaceName = (string) $node->namespacedName; + $messages = $this->classCheck->checkClassNames( + $scope, array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->extends), + ClassNameUsageLocation::from(ClassNameUsageLocation::INTERFACE_EXTENDS, [ + 'currentClassName' => $currentInterfaceName, + ]), ); - $currentInterfaceName = (string) $node->namespacedName; foreach ($node->extends as $extends) { $extendedInterfaceName = (string) $extends; if (!$this->reflectionProvider->hasClass($extendedInterfaceName)) { if (!$scope->isInClassExists($extendedInterfaceName)) { - $messages[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( 'Interface %s extends unknown interface %s.', $currentInterfaceName, $extendedInterfaceName, - ))->nonIgnorable()->discoveringSymbolsTip()->build(); + )) + ->identifier('interface.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedInterfaceName); @@ -54,19 +68,28 @@ public function processNode(Node $node, Scope $scope): array 'Interface %s extends class %s.', $currentInterfaceName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('interfaceExtends.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Interface %s extends trait %s.', $currentInterfaceName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('interfaceExtends.trait') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Interface %s extends enum %s.', $currentInterfaceName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('interfaceExtends.enum') + ->nonIgnorable() + ->build(); } } diff --git a/src/Rules/Classes/ImpossibleInstanceOfRule.php b/src/Rules/Classes/ImpossibleInstanceOfRule.php index d047d4b2f5..a4b3cfe70c 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -17,12 +18,13 @@ /** * @implements Rule */ -class ImpossibleInstanceOfRule implements Rule +final class ImpossibleInstanceOfRule implements Rule { public function __construct( - private bool $checkAlwaysTrueInstanceof, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -34,65 +36,78 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $instanceofType = $scope->getType($node); - $expressionType = $scope->getType($node->expr); + $instanceofType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); + if (!$instanceofType instanceof ConstantBooleanType) { + return []; + } if ($node->class instanceof Node\Name) { $className = $scope->resolveName($node->class); $classType = new ObjectType($className); } else { - $classType = $scope->getType($node->class); + $classType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->class) : $scope->getNativeType($node->class); $allowed = TypeCombinator::union( new StringType(), new ObjectWithoutClassType(), ); - if (!$allowed->accepts($classType, true)->yes()) { + if (!$allowed->isSuperTypeOf($classType)->yes()) { return [ RuleErrorBuilder::message(sprintf( 'Instanceof between %s and %s results in an error.', - $expressionType->describe(VerbosityLevel::typeOnly()), + $scope->getType($node->expr)->describe(VerbosityLevel::typeOnly()), $classType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('instanceof.invalidExprType')->build(), ]; } } - if (!$instanceofType instanceof ConstantBooleanType) { - return []; - } - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $instanceofTypeWithoutPhpDocs = $scope->doNotTreatPhpDocTypesAsCertain()->getType($node); + $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) { return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$instanceofType->getValue()) { + $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->expr) : $scope->getNativeType($node->expr); + return [ $addTip(RuleErrorBuilder::message(sprintf( 'Instanceof between %s and %s will always evaluate to false.', - $expressionType->describe(VerbosityLevel::typeOnly()), - $classType->describe(VerbosityLevel::typeOnly()), - )))->build(), - ]; - } elseif ($this->checkAlwaysTrueInstanceof) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Instanceof between %s and %s will always evaluate to true.', - $expressionType->describe(VerbosityLevel::typeOnly()), - $classType->describe(VerbosityLevel::typeOnly()), - )))->build(), + $exprType->describe(VerbosityLevel::typeOnly()), + $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), + )))->identifier('instanceof.alwaysFalse')->build(), ]; } - return []; + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->expr) : $scope->getNativeType($node->expr); + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Instanceof between %s and %s will always evaluate to true.', + $exprType->describe(VerbosityLevel::typeOnly()), + $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('instanceof.alwaysTrue'); + + return [$errorBuilder->build()]; } } diff --git a/src/Rules/Classes/InstantiationCallableRule.php b/src/Rules/Classes/InstantiationCallableRule.php index ed9eae3be8..019d6a2979 100644 --- a/src/Rules/Classes/InstantiationCallableRule.php +++ b/src/Rules/Classes/InstantiationCallableRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class InstantiationCallableRule implements Rule +final class InstantiationCallableRule implements Rule { public function getNodeType(): string @@ -22,7 +22,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { return [ - RuleErrorBuilder::message('Cannot create callable from the new operator.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Cannot create callable from the new operator.') + ->identifier('callable.notSupported') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 2200f7772d..6c5ef87c20 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -6,19 +6,20 @@ use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; +use function array_filter; use function array_map; use function array_merge; use function count; @@ -28,13 +29,14 @@ /** * @implements Rule */ -class InstantiationRule implements Rule +final class InstantiationRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, + private bool $discoveringSymbolsTip, ) { } @@ -55,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Expr\New_ $node - * @return RuleError[] + * @return list */ private function checkClassName(string $class, bool $isName, Node $node, Scope $scope): array { @@ -65,7 +67,9 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ if ($lowercasedClass === 'static') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class)) + ->identifier('outOfClass.static') + ->build(), ]; } @@ -89,14 +93,18 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ } elseif ($lowercasedClass === 'self') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class)) + ->identifier('outOfClass.self') + ->build(), ]; } $classReflection = $scope->getClassReflection(); } elseif ($lowercasedClass === 'parent') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class)) + ->identifier('outOfClass.parent') + ->build(), ]; } if ($scope->getClassReflection()->getParentClass() === null) { @@ -106,7 +114,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $scope->getClassReflection()->getDisplayName(), $scope->getFunctionName(), $scope->getClassReflection()->getDisplayName(), - ))->build(), + ))->identifier('class.noParent')->build(), ]; } $classReflection = $scope->getClassReflection()->getParentClass(); @@ -116,15 +124,22 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ return []; } + $errorBuilder = RuleErrorBuilder::message(sprintf('Instantiated class %s not found.', $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf('Instantiated class %s not found.', $class))->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([ - new ClassNameNodePair($class, $node->class), - ]); } + $messages = $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node->class), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::INSTANTIATION)); + $classReflection = $this->reflectionProvider->getClass($class); } @@ -132,7 +147,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ return [ RuleErrorBuilder::message( sprintf('Cannot instantiate enum %s.', $classReflection->getDisplayName()), - )->build(), + )->identifier('new.enum')->build(), ]; } @@ -140,7 +155,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ return [ RuleErrorBuilder::message( sprintf('Cannot instantiate interface %s.', $classReflection->getDisplayName()), - )->build(), + )->identifier('new.interface')->build(), ]; } @@ -148,7 +163,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ return [ RuleErrorBuilder::message( sprintf('Instantiated class %s is abstract.', $classReflection->getDisplayName()), - )->build(), + )->identifier('new.abstract')->build(), ]; } @@ -162,7 +177,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ RuleErrorBuilder::message(sprintf( 'Class %s does not have a constructor and must be instantiated without any parameters.', $classReflection->getDisplayName(), - ))->build(), + ))->identifier('new.noConstructor')->build(), ]); } @@ -177,7 +192,9 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $constructorReflection->isPrivate() ? 'private' : 'protected', $constructorReflection->getDeclaringClass()->getDisplayName(), $constructorReflection->getName(), - ))->build(); + )) + ->identifier(sprintf('new.%sConstructor', $constructorReflection->isPrivate() ? 'private' : 'protected')) + ->build(); } $classDisplayName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); @@ -187,30 +204,33 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $scope, $node->getArgs(), $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), ), $scope, $constructorReflection->getDeclaringClass()->isBuiltin(), $node, - [ - '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 ' . $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.', - ], + 'new', + $constructorReflection->acceptsNamedArguments(), + '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.', + '%s of class ' . $classDisplayName . ' constructor expects %s, %s given.', + '', // constructor does not have a return type + ' %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.', + '%s of class ' . $classDisplayName . ' constructor contains unresolvable type.', + 'Class ' . $classDisplayName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); } /** - * @param Node\Expr\New_ $node $node + * @param Node\Expr\New_ $node * @return array */ private function getClassNames(Node $node, Scope $scope): array @@ -220,24 +240,41 @@ private function getClassNames(Node $node, Scope $scope): array } if ($node->class instanceof Node\Stmt\Class_) { - $anonymousClassType = $scope->getType($node); - if (!$anonymousClassType instanceof TypeWithClassName) { + $classNames = $scope->getType($node)->getObjectClassNames(); + if ($classNames === []) { throw new ShouldNotHappenException(); } - return [[$anonymousClassType->getClassName(), true]]; + return array_map( + static fn (string $className) => [$className, true], + $classNames, + ); } $type = $scope->getType($node->class); + if ($type->isClassString()->yes()) { + $concretes = array_filter( + $type->getClassStringObjectType()->getObjectClassReflections(), + static fn (ClassReflection $classReflection): bool => !$classReflection->isAbstract() && !$classReflection->isInterface(), + ); + + if (count($concretes) > 0) { + return array_map( + static fn (ClassReflection $classReflection): array => [$classReflection->getName(), true], + $concretes, + ); + } + } + return array_merge( array_map( static fn (ConstantStringType $type): array => [$type->getValue(), true], - TypeUtils::getConstantStrings($type), + $type->getConstantStrings(), ), array_map( static fn (string $name): array => [$name, false], - TypeUtils::getDirectClassNames($type), + $type->getObjectClassNames(), ), ); } diff --git a/src/Rules/Classes/InvalidPromotedPropertiesRule.php b/src/Rules/Classes/InvalidPromotedPropertiesRule.php index b81e387b60..6472285f76 100644 --- a/src/Rules/Classes/InvalidPromotedPropertiesRule.php +++ b/src/Rules/Classes/InvalidPromotedPropertiesRule.php @@ -12,9 +12,9 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ -class InvalidPromotedPropertiesRule implements Rule +final class InvalidPromotedPropertiesRule implements Rule { public function __construct(private PhpVersion $phpVersion) @@ -23,23 +23,20 @@ public function __construct(private PhpVersion $phpVersion) public function getNodeType(): string { - return Node::class; + return Node\FunctionLike::class; } public function processNode(Node $node, Scope $scope): array { - if ( - !$node instanceof Node\Expr\ArrowFunction - && !$node instanceof Node\Stmt\ClassMethod - && !$node instanceof Node\Expr\Closure - && !$node instanceof Node\Stmt\Function_ - ) { - return []; - } - $hasPromotedProperties = false; - foreach ($node->params as $param) { - if ($param->flags === 0) { + + foreach ($node->getParams() as $param) { + if ($param->flags !== 0) { + $hasPromotedProperties = true; + break; + } + + if ($param->hooks === []) { continue; } @@ -55,31 +52,33 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( 'Promoted properties are supported only on PHP 8.0 and later.', - )->nonIgnorable()->build(), + )->identifier('property.promotedNotSupported')->nonIgnorable()->build(), ]; } if ( !$node instanceof Node\Stmt\ClassMethod - || $node->name->toLowerString() !== '__construct' + || ( + $node->name->toLowerString() !== '__construct' + && $node->getAttribute('originalTraitMethodName') !== '__construct') ) { return [ RuleErrorBuilder::message( 'Promoted properties can be in constructor only.', - )->nonIgnorable()->build(), + )->identifier('property.invalidPromoted')->nonIgnorable()->build(), ]; } - if ($node->stmts === null) { + if ($node->getStmts() === null) { return [ RuleErrorBuilder::message( 'Promoted properties are not allowed in abstract constructors.', - )->nonIgnorable()->build(), + )->identifier('property.invalidPromoted')->nonIgnorable()->build(), ]; } $errors = []; - foreach ($node->params as $param) { + foreach ($node->getParams() as $param) { if ($param->flags === 0) { continue; } @@ -95,8 +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), - )->nonIgnorable()->line($param->getLine())->build(); - continue; + )->identifier('property.invalidPromoted')->nonIgnorable()->line($param->getStartLine())->build(); } return $errors; diff --git a/src/Rules/Classes/LocalTypeAliasesCheck.php b/src/Rules/Classes/LocalTypeAliasesCheck.php new file mode 100644 index 0000000000..c62d8abed5 --- /dev/null +++ b/src/Rules/Classes/LocalTypeAliasesCheck.php @@ -0,0 +1,369 @@ + $globalTypeAliases + */ + public function __construct( + private array $globalTypeAliases, + private ReflectionProvider $reflectionProvider, + private TypeNodeResolver $typeNodeResolver, + private MissingTypehintCheck $missingTypehintCheck, + private ClassNameCheck $classCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private bool $checkMissingTypehints, + private bool $checkClassCaseSensitivity, + private bool $discoveringSymbolsTip, + ) + { + } + + /** + * @return list + */ + public function check(Scope $scope, ClassReflection $reflection, ClassLike $node): array + { + $errors = []; + foreach ($this->checkInTraitDefinitionContext($reflection) as $error) { + $errors[] = $error; + } + foreach ($this->checkInTraitUseContext($scope, $reflection, $reflection, $node) as $error) { + $errors[] = $error; + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitDefinitionContext(ClassReflection $reflection): array + { + $phpDoc = $reflection->getResolvedPhpDoc(); + if ($phpDoc === null) { + return []; + } + + $nameScope = $phpDoc->getNullableNameScope(); + $resolveName = static function (string $name) use ($nameScope): string { + if ($nameScope === null) { + return $name; + } + + return $nameScope->resolveStringName($name); + }; + + $errors = []; + $className = $reflection->getDisplayName(); + + $importedAliases = []; + + foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) { + $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias(); + $importedAlias = $typeAliasImportTag->getImportedAlias(); + $importedFromClassName = $typeAliasImportTag->getImportedFrom(); + + if (!$this->reflectionProvider->hasClass($importedFromClassName)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName)) + ->identifier('class.notFound') + ->build(); + continue; + } + + $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); + $typeAliases = $importedFromReflection->getTypeAliases(); + + if (!array_key_exists($importedAlias, $typeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName)) + ->identifier('typeAlias.notFound') + ->build(); + continue; + } + + $resolvedName = $resolveName($aliasName); + if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { + $classReflection = $this->reflectionProvider->getClass($resolvedName); + $classLikeDescription = 'a class'; + if ($classReflection->isInterface()) { + $classLikeDescription = 'an interface'; + } elseif ($classReflection->isTrait()) { + $classLikeDescription = 'a trait'; + } elseif ($classReflection->isEnum()) { + $classLikeDescription = 'an enum'; + } + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className)) + ->identifier('typeAlias.duplicate') + ->build(); + continue; + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->identifier('typeAlias.duplicate')->build(); + continue; + } + + $importedAs = $typeAliasImportTag->getImportedAs(); + if ($importedAs !== null && !$this->isAliasNameValid($importedAs, $nameScope)) { + $errors[] = RuleErrorBuilder::message(sprintf('Imported type alias %s has an invalid name: %s.', $importedAlias, $importedAs))->identifier('typeAlias.invalidName')->build(); + continue; + } + + $importedAliases[] = $aliasName; + } + + foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) { + $aliasName = $typeAliasTag->getAliasName(); + + if (in_array($aliasName, $importedAliases, true)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->identifier('typeAlias.duplicate')->build(); + continue; + } + + $resolvedName = $resolveName($aliasName); + if ($this->reflectionProvider->hasClass($resolvedName)) { + $classReflection = $this->reflectionProvider->getClass($resolvedName); + $classLikeDescription = 'a class'; + if ($classReflection->isInterface()) { + $classLikeDescription = 'an interface'; + } elseif ($classReflection->isTrait()) { + $classLikeDescription = 'a trait'; + } elseif ($classReflection->isEnum()) { + $classLikeDescription = 'an enum'; + } + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->identifier('typeAlias.duplicate')->build(); + continue; + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->identifier('typeAlias.duplicate')->build(); + continue; + } + + if (!$this->isAliasNameValid($aliasName, $nameScope)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias has an invalid name: %s.', $aliasName)) + ->identifier('typeAlias.invalidName') + ->build(); + continue; + } + + $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); + if ($this->hasErrorType($resolvedType, $aliasName, $errors)) { + continue; + } + + if (!$this->checkMissingTypehints) { + continue; + } + + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($resolvedType) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has type alias %s with no value type specified in iterable type %s.', + $reflection->getClassTypeDescription(), + $reflection->getDisplayName(), + $aliasName, + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($resolvedType) as [$name, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has type alias %s with generic %s but does not specify its types: %s', + $reflection->getClassTypeDescription(), + $reflection->getDisplayName(), + $aliasName, + $name, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($resolvedType) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has type alias %s with no signature specified for %s.', + $reflection->getClassTypeDescription(), + $reflection->getDisplayName(), + $aliasName, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitUseContext( + Scope $scope, + ClassReflection $reflection, + ClassReflection $implementingClassReflection, + ClassLike $node, + ): array + { + if ($reflection->getNativeReflection()->getName() === $implementingClassReflection->getName()) { + $phpDoc = $reflection->getResolvedPhpDoc(); + } else { + $phpDoc = $reflection->getTraitContextResolvedPhpDoc($implementingClassReflection); + } + if ($phpDoc === null) { + return []; + } + + $errors = []; + + foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) { + $aliasName = $typeAliasTag->getAliasName(); + $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); + $throwawayErrors = []; + if ($this->hasErrorType($resolvedType, $aliasName, $throwawayErrors)) { + continue; + } + foreach ($resolvedType->getReferencedClasses() as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + $errorBuilder = RuleErrorBuilder::message(sprintf('Type alias %s contains unknown class %s.', $aliasName, $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s contains invalid type %s.', $aliasName, $class)) + ->identifier('typeAlias.trait') + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::TYPE_ALIAS, [ + 'typeAliasName' => $aliasName, + ]), $this->checkClassCaseSensitivity), + ); + } + } + + if ($this->unresolvableTypeHelper->containsUnresolvableType($resolvedType)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s contains unresolvable type.', $aliasName)) + ->identifier('typeAlias.unresolvableType') + ->build(); + } + + $escapedTypeAlias = SprintfHelper::escapeFormatString($aliasName); + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( + $resolvedType, + sprintf( + 'Type alias %s contains generic type %%s but %%s %%s is not generic.', + $escapedTypeAlias, + ), + sprintf( + 'Generic type %%s in type alias %s does not specify all template types of %%s %%s: %%s', + $escapedTypeAlias, + ), + sprintf( + 'Generic type %%s in type alias %s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $escapedTypeAlias, + ), + sprintf( + 'Type %%s in generic type %%s in type alias %s is not subtype of template type %%s of %%s %%s.', + $escapedTypeAlias, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in type alias %s is in conflict with %%s template type %%s of %%s %%s.', + $escapedTypeAlias, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in type alias %s is redundant, template type %%s of %%s %%s has the same variance.', + $escapedTypeAlias, + ), + )); + } + + return $errors; + } + + private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): bool + { + if ($nameScope === null) { + return true; + } + + $aliasNameResolvedType = $this->typeNodeResolver->resolve(new IdentifierTypeNode($aliasName), $nameScope->bypassTypeAliases()); + return ($aliasNameResolvedType->isObject()->yes() && !in_array($aliasName, ['self', 'parent'], true)) + || $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck + } + + /** + * @param list $errors + * @param-out list $errors + */ + private function hasErrorType(Type $type, string $aliasName, array &$errors): bool + { + $foundError = false; + TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type { + if ($foundError) { + return $type; + } + + if ($type instanceof CircularTypeAliasErrorType) { + $errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName)) + ->identifier('typeAlias.circular') + ->build(); + $foundError = true; + return $type; + } + + if ($type instanceof ErrorType) { + $errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName)) + ->identifier('typeAlias.invalidType') + ->build(); + $foundError = true; + return $type; + } + + return $traverse($type); + }); + + return $foundError; + } + +} diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php index cdc76cdb61..9621d3c9ec 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -3,38 +3,17 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; -use PHPStan\Analyser\NameScope; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; -use PHPStan\PhpDoc\TypeNodeResolver; -use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\CircularTypeAliasErrorType; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\TemplateType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\Type; -use PHPStan\Type\TypeTraverser; -use function array_key_exists; -use function in_array; -use function sprintf; /** * @implements Rule */ -class LocalTypeAliasesRule implements Rule +final class LocalTypeAliasesRule implements Rule { - /** - * @param array $globalTypeAliases - */ - public function __construct( - private array $globalTypeAliases, - private ReflectionProvider $reflectionProvider, - private TypeNodeResolver $typeNodeResolver, - ) + public function __construct(private LocalTypeAliasesCheck $check) { } @@ -45,141 +24,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $reflection = $node->getClassReflection(); - $phpDoc = $reflection->getResolvedPhpDoc(); - if ($phpDoc === null) { - return []; - } - - $nameScope = $phpDoc->getNullableNameScope(); - $resolveName = static function (string $name) use ($nameScope): string { - if ($nameScope === null) { - return $name; - } - - return $nameScope->resolveStringName($name); - }; - - $errors = []; - $className = $reflection->getName(); - - $importedAliases = []; - - foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) { - $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias(); - $importedAlias = $typeAliasImportTag->getImportedAlias(); - $importedFromClassName = $typeAliasImportTag->getImportedFrom(); - - if (!$this->reflectionProvider->hasClass($importedFromClassName)) { - $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName))->build(); - continue; - } - - $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); - $typeAliases = $importedFromReflection->getTypeAliases(); - - if (!array_key_exists($importedAlias, $typeAliases)) { - $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName))->build(); - continue; - } - - $resolvedName = $resolveName($aliasName); - if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { - $classReflection = $this->reflectionProvider->getClass($resolvedName); - $classLikeDescription = 'a class'; - if ($classReflection->isInterface()) { - $classLikeDescription = 'an interface'; - } elseif ($classReflection->isTrait()) { - $classLikeDescription = 'a trait'; - } elseif ($classReflection->isEnum()) { - $classLikeDescription = 'an enum'; - } - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->build(); - continue; - } - - if (array_key_exists($aliasName, $this->globalTypeAliases)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build(); - continue; - } - - $importedAs = $typeAliasImportTag->getImportedAs(); - if ($importedAs !== null && !$this->isAliasNameValid($importedAs, $nameScope)) { - $errors[] = RuleErrorBuilder::message(sprintf('Imported type alias %s has an invalid name: %s.', $importedAlias, $importedAs))->build(); - continue; - } - - $importedAliases[] = $aliasName; - } - - foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) { - $aliasName = $typeAliasTag->getAliasName(); - - if (in_array($aliasName, $importedAliases, true)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->build(); - continue; - } - - $resolvedName = $resolveName($aliasName); - if ($this->reflectionProvider->hasClass($resolvedName)) { - $classReflection = $this->reflectionProvider->getClass($resolvedName); - $classLikeDescription = 'a class'; - if ($classReflection->isInterface()) { - $classLikeDescription = 'an interface'; - } elseif ($classReflection->isTrait()) { - $classLikeDescription = 'a trait'; - } elseif ($classReflection->isEnum()) { - $classLikeDescription = 'an enum'; - } - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->build(); - continue; - } - - if (array_key_exists($aliasName, $this->globalTypeAliases)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build(); - continue; - } - - if (!$this->isAliasNameValid($aliasName, $nameScope)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias has an invalid name: %s.', $aliasName))->build(); - continue; - } - - $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); - $foundError = false; - TypeTraverser::map($resolvedType, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type { - if ($foundError) { - return $type; - } - - if ($type instanceof CircularTypeAliasErrorType) { - $errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName))->build(); - $foundError = true; - return $type; - } - - if ($type instanceof ErrorType) { - $errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName))->build(); - $foundError = true; - return $type; - } - - return $traverse($type); - }); - } - - return $errors; - } - - private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): bool - { - if ($nameScope === null) { - return true; - } - - $aliasNameResolvedType = $this->typeNodeResolver->resolve(new IdentifierTypeNode($aliasName), $nameScope->bypassTypeAliases()); - return ($aliasNameResolvedType instanceof ObjectType && !in_array($aliasName, ['self', 'parent'], true)) - || $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck + return $this->check->check($scope, $node->getClassReflection(), $node->getOriginalNode()); } } diff --git a/src/Rules/Classes/LocalTypeTraitAliasesRule.php b/src/Rules/Classes/LocalTypeTraitAliasesRule.php new file mode 100644 index 0000000000..1f7fe5021d --- /dev/null +++ b/src/Rules/Classes/LocalTypeTraitAliasesRule.php @@ -0,0 +1,39 @@ + + */ +final class LocalTypeTraitAliasesRule implements Rule +{ + + public function __construct(private LocalTypeAliasesCheck $check, private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $traitName = $node->namespacedName; + if ($traitName === null) { + return []; + } + + if (!$this->reflectionProvider->hasClass($traitName->toString())) { + return []; + } + + return $this->check->checkInTraitDefinitionContext($this->reflectionProvider->getClass($traitName->toString())); + } + +} diff --git a/src/Rules/Classes/LocalTypeTraitUseAliasesRule.php b/src/Rules/Classes/LocalTypeTraitUseAliasesRule.php new file mode 100644 index 0000000000..8c8eb5cfc1 --- /dev/null +++ b/src/Rules/Classes/LocalTypeTraitUseAliasesRule.php @@ -0,0 +1,35 @@ + + */ +final class LocalTypeTraitUseAliasesRule implements Rule +{ + + public function __construct(private LocalTypeAliasesCheck $check) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->check->checkInTraitUseContext( + $scope, + $node->getTraitReflection(), + $node->getImplementingClassReflection(), + $node->getOriginalNode(), + ); + } + +} diff --git a/src/Rules/Classes/MethodTagCheck.php b/src/Rules/Classes/MethodTagCheck.php new file mode 100644 index 0000000000..a5bc6a5b35 --- /dev/null +++ b/src/Rules/Classes/MethodTagCheck.php @@ -0,0 +1,275 @@ + + */ + public function check( + Scope $scope, + ClassReflection $classReflection, + ClassLike $node, + ): array + { + $errors = []; + foreach ($classReflection->getMethodTags() as $methodName => $methodTag) { + $i = 0; + foreach ($methodTag->getParameters() as $parameterName => $parameterTag) { + $i++; + $parameterDescription = sprintf('parameter #%d $%s', $i, $parameterName); + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $parameterDescription, $parameterTag->getType()) as $error) { + $errors[] = $error; + } + foreach ($this->checkMethodTypeInTraitUseContext($scope, $classReflection, $methodName, $parameterDescription, $parameterTag->getType(), $node) as $error) { + $errors[] = $error; + } + + if ($parameterTag->getDefaultValue() === null) { + continue; + } + + $defaultValueDescription = sprintf('%s default value', $parameterDescription); + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $defaultValueDescription, $parameterTag->getDefaultValue()) as $error) { + $errors[] = $error; + } + foreach ($this->checkMethodTypeInTraitUseContext($scope, $classReflection, $methodName, $defaultValueDescription, $parameterTag->getDefaultValue(), $node) as $error) { + $errors[] = $error; + } + } + + $returnTypeDescription = 'return type'; + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $returnTypeDescription, $methodTag->getReturnType()) as $error) { + $errors[] = $error; + } + foreach ($this->checkMethodTypeInTraitUseContext($scope, $classReflection, $methodName, $returnTypeDescription, $methodTag->getReturnType(), $node) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitDefinitionContext(ClassReflection $classReflection): array + { + $errors = []; + foreach ($classReflection->getMethodTags() as $methodName => $methodTag) { + $i = 0; + foreach ($methodTag->getParameters() as $parameterName => $parameterTag) { + $i++; + $parameterDescription = sprintf('parameter #%d $%s', $i, $parameterName); + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $parameterDescription, $parameterTag->getType()) as $error) { + $errors[] = $error; + } + + if ($parameterTag->getDefaultValue() === null) { + continue; + } + + $defaultValueDescription = sprintf('%s default value', $parameterDescription); + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $defaultValueDescription, $parameterTag->getDefaultValue()) as $error) { + $errors[] = $error; + } + } + + $returnTypeDescription = 'return type'; + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $returnTypeDescription, $methodTag->getReturnType()) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitUseContext( + Scope $scope, + ClassReflection $classReflection, + ClassReflection $implementingClass, + ClassLike $node, + ): array + { + $phpDoc = $classReflection->getTraitContextResolvedPhpDoc($implementingClass); + if ($phpDoc === null) { + return []; + } + + $errors = []; + foreach ($phpDoc->getMethodTags() as $methodName => $methodTag) { + $i = 0; + foreach ($methodTag->getParameters() as $parameterName => $parameterTag) { + $i++; + $parameterDescription = sprintf('parameter #%d $%s', $i, $parameterName); + foreach ($this->checkMethodTypeInTraitUseContext($scope, $classReflection, $methodName, $parameterDescription, $parameterTag->getType(), $node) as $error) { + $errors[] = $error; + } + + if ($parameterTag->getDefaultValue() === null) { + continue; + } + + $defaultValueDescription = sprintf('%s default value', $parameterDescription); + foreach ($this->checkMethodTypeInTraitUseContext($scope, $classReflection, $methodName, $defaultValueDescription, $parameterTag->getDefaultValue(), $node) as $error) { + $errors[] = $error; + } + } + + $returnTypeDescription = 'return type'; + foreach ($this->checkMethodTypeInTraitUseContext($scope, $classReflection, $methodName, $returnTypeDescription, $methodTag->getReturnType(), $node) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function checkMethodTypeInTraitDefinitionContext(ClassReflection $classReflection, string $methodName, string $description, Type $type): array + { + if (!$this->checkMissingTypehints) { + return []; + } + + $errors = []; + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @method for method %s::%s() %s contains generic %s but does not specify its types: %s', + $classReflection->getDisplayName(), + $methodName, + $description, + $innerName, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($type) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has PHPDoc tag @method for method %s() %s with no value type specified in iterable type %s.', + $classReflection->getClassTypeDescription(), + $classReflection->getDisplayName(), + $methodName, + $description, + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($type) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has PHPDoc tag @method for method %s() %s with no signature specified for %s.', + $classReflection->getClassTypeDescription(), + $classReflection->getDisplayName(), + $methodName, + $description, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + + return $errors; + } + + /** + * @return list + */ + private function checkMethodTypeInTraitUseContext(Scope $scope, ClassReflection $classReflection, string $methodName, string $description, Type $type, ClassLike $node): array + { + $errors = []; + foreach ($type->getReferencedClasses() as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + $errorBuilder = RuleErrorBuilder::message(sprintf('PHPDoc tag @method for method %s::%s() %s contains unknown class %s.', $classReflection->getDisplayName(), $methodName, $description, $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @method for method %s::%s() %s contains invalid type %s.', $classReflection->getDisplayName(), $methodName, $description, $class)) + ->identifier('methodTag.trait') + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_METHOD, [ + 'methodTagName' => $methodName, + ]), $this->checkClassCaseSensitivity), + ); + } + } + + if ($this->unresolvableTypeHelper->containsUnresolvableType($type)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @method for method %s::%s() %s contains unresolvable type.', + $classReflection->getDisplayName(), + $methodName, + $description, + ))->identifier('methodTag.unresolvableType')->build(); + } + + $escapedClassName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); + $escapedMethodName = SprintfHelper::escapeFormatString($methodName); + $escapedDescription = SprintfHelper::escapeFormatString($description); + + return array_merge( + $errors, + $this->genericObjectTypeCheck->check( + $type, + sprintf('PHPDoc tag @method for method %s::%s() %s contains generic type %%s but %%s %%s is not generic.', $escapedClassName, $escapedMethodName, $escapedDescription), + sprintf('Generic type %%s in PHPDoc tag @method for method %s::%s() %s does not specify all template types of %%s %%s: %%s', $escapedClassName, $escapedMethodName, $escapedDescription), + sprintf('Generic type %%s in PHPDoc tag @method for method %s::%s() %s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedClassName, $escapedMethodName, $escapedDescription), + sprintf('Type %%s in generic type %%s in PHPDoc tag @method for method %s::%s() %s is not subtype of template type %%s of %%s %%s.', $escapedClassName, $escapedMethodName, $escapedDescription), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @method for method %s::%s() %s is in conflict with %%s template type %%s of %%s %%s.', $escapedClassName, $escapedMethodName, $escapedDescription), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @method for method %s::%s() %s is redundant, template type %%s of %%s %%s has the same variance.', $escapedClassName, $escapedMethodName, $escapedDescription), + ), + ); + } + +} diff --git a/src/Rules/Classes/MethodTagRule.php b/src/Rules/Classes/MethodTagRule.php new file mode 100644 index 0000000000..20b0c004d1 --- /dev/null +++ b/src/Rules/Classes/MethodTagRule.php @@ -0,0 +1,34 @@ + + */ +final class MethodTagRule implements Rule +{ + + public function __construct(private MethodTagCheck $check) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->check->check( + $scope, + $node->getClassReflection(), + $node->getOriginalNode(), + ); + } + +} diff --git a/src/Rules/Classes/MethodTagTraitRule.php b/src/Rules/Classes/MethodTagTraitRule.php new file mode 100644 index 0000000000..157c46f7f4 --- /dev/null +++ b/src/Rules/Classes/MethodTagTraitRule.php @@ -0,0 +1,39 @@ + + */ +final class MethodTagTraitRule implements Rule +{ + + public function __construct(private MethodTagCheck $check, private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $traitName = $node->namespacedName; + if ($traitName === null) { + return []; + } + + if (!$this->reflectionProvider->hasClass($traitName->toString())) { + return []; + } + + return $this->check->checkInTraitDefinitionContext($this->reflectionProvider->getClass($traitName->toString())); + } + +} diff --git a/src/Rules/Classes/MethodTagTraitUseRule.php b/src/Rules/Classes/MethodTagTraitUseRule.php new file mode 100644 index 0000000000..aecce7bdcf --- /dev/null +++ b/src/Rules/Classes/MethodTagTraitUseRule.php @@ -0,0 +1,35 @@ + + */ +final class MethodTagTraitUseRule implements Rule +{ + + public function __construct(private MethodTagCheck $check) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->check->checkInTraitUseContext( + $scope, + $node->getTraitReflection(), + $node->getImplementingClassReflection(), + $node->getOriginalNode(), + ); + } + +} diff --git a/src/Rules/Classes/MixinCheck.php b/src/Rules/Classes/MixinCheck.php new file mode 100644 index 0000000000..2c6417eddd --- /dev/null +++ b/src/Rules/Classes/MixinCheck.php @@ -0,0 +1,178 @@ + + */ + public function check(Scope $scope, ClassReflection $classReflection, ClassLike $node): array + { + $errors = []; + foreach ($this->checkInTraitDefinitionContext($classReflection) as $error) { + $errors[] = $error; + } + + foreach ($this->checkInTraitUseContext($scope, $classReflection, $classReflection, $node) as $error) { + $errors[] = $error; + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitDefinitionContext(ClassReflection $classReflection): array + { + $errors = []; + foreach ($classReflection->getMixinTags() as $mixinTag) { + $type = $mixinTag->getType(); + if (!$type->canCallMethods()->yes() || !$type->canAccessProperties()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('mixin.nonObject') + ->build(); + continue; + } + + if (!$this->checkMissingTypehints) { + continue; + } + + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($type) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has PHPDoc tag @mixin with no value type specified in iterable type %s.', + $classReflection->getClassTypeDescription(), + $classReflection->getDisplayName(), + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + 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, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($type) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has PHPDoc tag @mixin with no signature specified for %s.', + $classReflection->getClassTypeDescription(), + $classReflection->getDisplayName(), + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitUseContext( + Scope $scope, + ClassReflection $reflection, + ClassReflection $implementingClassReflection, + ClassLike $node, + ): array + { + if ($reflection->getNativeReflection()->getName() === $implementingClassReflection->getName()) { + $phpDoc = $reflection->getResolvedPhpDoc(); + } else { + $phpDoc = $reflection->getTraitContextResolvedPhpDoc($implementingClassReflection); + } + if ($phpDoc === null) { + return []; + } + + $errors = []; + foreach ($phpDoc->getMixinTags() as $mixinTag) { + $type = $mixinTag->getType(); + if ( + $this->unresolvableTypeHelper->containsUnresolvableType($type) + ) { + $errors[] = RuleErrorBuilder::message('PHPDoc tag @mixin contains unresolvable type.') + ->identifier('mixin.unresolvableType') + ->build(); + continue; + } + + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( + $type, + '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.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @mixin is in conflict with %s template type %s of %s %s.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @mixin is redundant, template type %s of %s %s has the same variance.', + )); + + foreach ($type->getReferencedClasses() as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + $errorBuilder = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains unknown class %s.', $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains invalid type %s.', $class)) + ->identifier('mixin.trait') + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_MIXIN), $this->checkClassCaseSensitivity), + ); + } + } + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/MixinRule.php b/src/Rules/Classes/MixinRule.php index 93d0f4bfc4..de75fc13a6 100644 --- a/src/Rules/Classes/MixinRule.php +++ b/src/Rules/Classes/MixinRule.php @@ -5,33 +5,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; -use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\Generics\GenericObjectTypeCheck; -use PHPStan\Rules\MissingTypehintCheck; -use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\VerbosityLevel; -use function array_merge; -use function implode; -use function sprintf; /** * @implements Rule */ -class MixinRule implements Rule +final class MixinRule implements Rule { - public function __construct( - private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, - private GenericObjectTypeCheck $genericObjectTypeCheck, - private MissingTypehintCheck $missingTypehintCheck, - private UnresolvableTypeHelper $unresolvableTypeHelper, - private bool $checkClassCaseSensitivity, - ) + public function __construct(private MixinCheck $check) { } @@ -42,59 +24,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); - $mixinTags = $classReflection->getMixinTags(); - $errors = []; - foreach ($mixinTags as $mixinTag) { - $type = $mixinTag->getType(); - if (!$type->canCallMethods()->yes() || !$type->canAccessProperties()->yes()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly())))->build(); - continue; - } - - if ( - $this->unresolvableTypeHelper->containsUnresolvableType($type) - ) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @mixin contains unresolvable type.')->build(); - continue; - } - - $errors = array_merge($errors, $this->genericObjectTypeCheck->check( - $type, - '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), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); - } - - foreach ($type->getReferencedClasses() as $class) { - if (!$this->reflectionProvider->hasClass($class)) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains unknown class %s.', $class))->discoveringSymbolsTip()->build(); - } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains invalid type %s.', $class))->build(); - } elseif ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames([ - new ClassNameNodePair($class, $node), - ]), - ); - } - } - } - - return $errors; + return $this->check->check($scope, $node->getClassReflection(), $node->getOriginalNode()); } } diff --git a/src/Rules/Classes/MixinTraitRule.php b/src/Rules/Classes/MixinTraitRule.php new file mode 100644 index 0000000000..5cb7c1ecd9 --- /dev/null +++ b/src/Rules/Classes/MixinTraitRule.php @@ -0,0 +1,41 @@ + + */ +final class MixinTraitRule implements Rule +{ + + public function __construct(private MixinCheck $check, private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $traitName = $node->namespacedName; + if ($traitName === null) { + return []; + } + + if (!$this->reflectionProvider->hasClass($traitName->toString())) { + return []; + } + + return $this->check->checkInTraitDefinitionContext( + $this->reflectionProvider->getClass($traitName->toString()), + ); + } + +} diff --git a/src/Rules/Classes/MixinTraitUseRule.php b/src/Rules/Classes/MixinTraitUseRule.php new file mode 100644 index 0000000000..7ef205cbaf --- /dev/null +++ b/src/Rules/Classes/MixinTraitUseRule.php @@ -0,0 +1,35 @@ + + */ +final class MixinTraitUseRule implements Rule +{ + + public function __construct(private MixinCheck $check) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->check->checkInTraitUseContext( + $scope, + $node->getTraitReflection(), + $node->getImplementingClassReflection(), + $node->getOriginalNode(), + ); + } + +} diff --git a/src/Rules/Classes/NewStaticRule.php b/src/Rules/Classes/NewStaticRule.php index 3c3ba04562..5540646616 100644 --- a/src/Rules/Classes/NewStaticRule.php +++ b/src/Rules/Classes/NewStaticRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class NewStaticRule implements Rule +final class NewStaticRule implements Rule { public function getNodeType(): string @@ -41,6 +41,7 @@ public function processNode(Node $node, Scope $scope): array $messages = [ RuleErrorBuilder::message('Unsafe usage of new static().') + ->identifier('new.static') ->tip('See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static') ->build(), ]; @@ -53,6 +54,16 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($constructor->getDeclaringClass()->hasConsistentConstructor()) { + return []; + } + + foreach ($classReflection->getImmediateInterfaces() as $interface) { + if ($interface->hasConstructor()) { + return []; + } + } + if ($constructor instanceof PhpMethodReflection) { if ($constructor->isFinal()->yes()) { return []; diff --git a/src/Rules/Classes/NonClassAttributeClassRule.php b/src/Rules/Classes/NonClassAttributeClassRule.php index 0138692224..24f6850492 100644 --- a/src/Rules/Classes/NonClassAttributeClassRule.php +++ b/src/Rules/Classes/NonClassAttributeClassRule.php @@ -5,16 +5,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use function sprintf; +use function strtolower; /** * @implements Rule */ -class NonClassAttributeClassRule implements Rule +final class NonClassAttributeClassRule implements Rule { public function getNodeType(): string @@ -38,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function check(Scope $scope): array { @@ -50,13 +51,17 @@ private function check(Scope $scope): array return [ RuleErrorBuilder::message(sprintf( '%s cannot be an Attribute class.', - $classReflection->isInterface() ? 'Interface' : 'Enum', - ))->build(), + $classReflection->getClassTypeDescription(), + )) + ->identifier(sprintf('attribute.%s', strtolower($classReflection->getClassTypeDescription()))) + ->build(), ]; } if ($classReflection->isAbstract()) { return [ - RuleErrorBuilder::message(sprintf('Abstract class %s cannot be an Attribute class.', $classReflection->getDisplayName()))->build(), + RuleErrorBuilder::message(sprintf('Abstract class %s cannot be an Attribute class.', $classReflection->getDisplayName())) + ->identifier('attribute.abstract') + ->build(), ]; } @@ -66,7 +71,9 @@ private function check(Scope $scope): array if (!$classReflection->getConstructor()->isPublic()) { return [ - RuleErrorBuilder::message(sprintf('Attribute class %s constructor must be public.', $classReflection->getDisplayName()))->build(), + RuleErrorBuilder::message(sprintf('Attribute class %s constructor must be public.', $classReflection->getDisplayName())) + ->identifier('attribute.constructorNotPublic') + ->build(), ]; } diff --git a/src/Rules/Classes/PropertyTagCheck.php b/src/Rules/Classes/PropertyTagCheck.php new file mode 100644 index 0000000000..52ad3e72e2 --- /dev/null +++ b/src/Rules/Classes/PropertyTagCheck.php @@ -0,0 +1,256 @@ + + */ + public function check( + Scope $scope, + ClassReflection $classReflection, + ClassLike $node, + ): array + { + $errors = []; + foreach ($classReflection->getPropertyTags() as $propertyName => $propertyTag) { + [$types, $tagName] = $this->getTypesAndTagName($propertyTag); + foreach ($types as $type) { + foreach ($this->checkPropertyTypeInTraitDefinitionContext($classReflection, $propertyName, $tagName, $type) as $error) { + $errors[] = $error; + } + foreach ($this->checkPropertyTypeInTraitUseContext($scope, $classReflection, $propertyName, $tagName, $type, $node) as $error) { + $errors[] = $error; + } + } + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitDefinitionContext(ClassReflection $classReflection): array + { + $errors = []; + foreach ($classReflection->getPropertyTags() as $propertyName => $propertyTag) { + [$types, $tagName] = $this->getTypesAndTagName($propertyTag); + foreach ($types as $type) { + foreach ($this->checkPropertyTypeInTraitDefinitionContext($classReflection, $propertyName, $tagName, $type) as $error) { + $errors[] = $error; + } + } + } + + return $errors; + } + + /** + * @return list + */ + public function checkInTraitUseContext( + Scope $scope, + ClassReflection $classReflection, + ClassReflection $implementingClass, + ClassLike $node, + ): array + { + $phpDoc = $classReflection->getTraitContextResolvedPhpDoc($implementingClass); + if ($phpDoc === null) { + return []; + } + + $errors = []; + foreach ($phpDoc->getPropertyTags() as $propertyName => $propertyTag) { + [$types, $tagName] = $this->getTypesAndTagName($propertyTag); + foreach ($types as $type) { + foreach ($this->checkPropertyTypeInTraitUseContext($scope, $classReflection, $propertyName, $tagName, $type, $node) as $error) { + $errors[] = $error; + } + } + } + + return $errors; + } + + /** + * @return array{list, string} + */ + private function getTypesAndTagName(PropertyTag $propertyTag): array + { + $readableType = $propertyTag->getReadableType(); + $writableType = $propertyTag->getWritableType(); + + $types = []; + $tagName = '@property'; + if ($readableType !== null) { + if ($writableType !== null) { + if ($writableType->equals($readableType)) { + $types[] = $readableType; + } else { + $types[] = $readableType; + $types[] = $writableType; + } + } else { + $tagName = '@property-read'; + $types[] = $readableType; + } + } elseif ($writableType !== null) { + $tagName = '@property-write'; + $types[] = $writableType; + } else { + throw new ShouldNotHappenException(); + } + + return [$types, $tagName]; + } + + /** + * @return list + */ + private function checkPropertyTypeInTraitDefinitionContext(ClassReflection $classReflection, string $propertyName, string $tagName, Type $type): array + { + if (!$this->checkMissingTypehints) { + return []; + } + + $errors = []; + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for property %s::$%s contains generic %s but does not specify its types: %s', + $tagName, + $classReflection->getDisplayName(), + $propertyName, + $innerName, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($type) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has PHPDoc tag %s for property $%s with no value type specified in iterable type %s.', + $classReflection->getClassTypeDescription(), + $classReflection->getDisplayName(), + $tagName, + $propertyName, + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($type) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has PHPDoc tag %s for property $%s with no signature specified for %s.', + $classReflection->getClassTypeDescription(), + $classReflection->getDisplayName(), + $tagName, + $propertyName, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + + return $errors; + } + + /** + * @return list + */ + private function checkPropertyTypeInTraitUseContext(Scope $scope, ClassReflection $classReflection, string $propertyName, string $tagName, Type $type, ClassLike $node): array + { + $errors = []; + foreach ($type->getReferencedClasses() as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + $errorBuilder = RuleErrorBuilder::message(sprintf('PHPDoc tag %s for property %s::$%s contains unknown class %s.', $tagName, $classReflection->getDisplayName(), $propertyName, $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag %s for property %s::$%s contains invalid type %s.', $tagName, $classReflection->getDisplayName(), $propertyName, $class)) + ->identifier('propertyTag.trait') + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_PROPERTY, [ + 'propertyTagName' => $propertyName, + ]), $this->checkClassCaseSensitivity), + ); + } + } + + if ($this->unresolvableTypeHelper->containsUnresolvableType($type)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for property %s::$%s contains unresolvable type.', + $tagName, + $classReflection->getDisplayName(), + $propertyName, + ))->identifier('propertyTag.unresolvableType')->build(); + } + + $escapedClassName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); + $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); + $escapedTagName = SprintfHelper::escapeFormatString($tagName); + + return array_merge( + $errors, + $this->genericObjectTypeCheck->check( + $type, + sprintf('PHPDoc tag %s for property %s::$%s contains generic type %%s but %%s %%s is not generic.', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Generic type %%s in PHPDoc tag %s for property %s::$%s does not specify all template types of %%s %%s: %%s', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Generic type %%s in PHPDoc tag %s for property %s::$%s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Type %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is not subtype of template type %%s of %%s %%s.', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is in conflict with %%s template type %%s of %%s %%s.', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is redundant, template type %%s of %%s %%s has the same variance.', $escapedTagName, $escapedClassName, $escapedPropertyName), + ), + ); + } + +} diff --git a/src/Rules/Classes/PropertyTagRule.php b/src/Rules/Classes/PropertyTagRule.php new file mode 100644 index 0000000000..0ed1a2918a --- /dev/null +++ b/src/Rules/Classes/PropertyTagRule.php @@ -0,0 +1,30 @@ + + */ +final class PropertyTagRule implements Rule +{ + + public function __construct(private PropertyTagCheck $check) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->check->check($scope, $node->getClassReflection(), $node->getOriginalNode()); + } + +} diff --git a/src/Rules/Classes/PropertyTagTraitRule.php b/src/Rules/Classes/PropertyTagTraitRule.php new file mode 100644 index 0000000000..bd5de407ba --- /dev/null +++ b/src/Rules/Classes/PropertyTagTraitRule.php @@ -0,0 +1,39 @@ + + */ +final class PropertyTagTraitRule implements Rule +{ + + public function __construct(private PropertyTagCheck $check, private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $traitName = $node->namespacedName; + if ($traitName === null) { + return []; + } + + if (!$this->reflectionProvider->hasClass($traitName->toString())) { + return []; + } + + return $this->check->checkInTraitDefinitionContext($this->reflectionProvider->getClass($traitName->toString())); + } + +} diff --git a/src/Rules/Classes/PropertyTagTraitUseRule.php b/src/Rules/Classes/PropertyTagTraitUseRule.php new file mode 100644 index 0000000000..4ac620fbc2 --- /dev/null +++ b/src/Rules/Classes/PropertyTagTraitUseRule.php @@ -0,0 +1,35 @@ + + */ +final class PropertyTagTraitUseRule implements Rule +{ + + public function __construct(private PropertyTagCheck $check) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->check->checkInTraitUseContext( + $scope, + $node->getTraitReflection(), + $node->getImplementingClassReflection(), + $node->getOriginalNode(), + ); + } + +} diff --git a/src/Rules/Classes/ReadOnlyClassRule.php b/src/Rules/Classes/ReadOnlyClassRule.php new file mode 100644 index 0000000000..816e67e7b4 --- /dev/null +++ b/src/Rules/Classes/ReadOnlyClassRule.php @@ -0,0 +1,58 @@ + + */ +final class ReadOnlyClassRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$classReflection->isReadOnly()) { + return []; + } + if ($classReflection->isAnonymous()) { + if ($this->phpVersion->supportsReadOnlyAnonymousClasses()) { + return []; + } + + return [ + RuleErrorBuilder::message('Anonymous readonly classes are supported only on PHP 8.3 and later.') + ->identifier('classConstant.nativeTypeNotSupported') + ->nonIgnorable() + ->build(), + ]; + } + + if ($this->phpVersion->supportsReadOnlyClasses()) { + return []; + } + + return [ + RuleErrorBuilder::message('Readonly classes are supported only on PHP 8.2 and later.') + ->identifier('classConstant.nativeTypeNotSupported') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Classes/RequireExtendsRule.php b/src/Rules/Classes/RequireExtendsRule.php new file mode 100644 index 0000000000..036cf3aa85 --- /dev/null +++ b/src/Rules/Classes/RequireExtendsRule.php @@ -0,0 +1,87 @@ + + */ +final class RequireExtendsRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + if ($classReflection->isInterface()) { + return []; + } + + $errors = []; + foreach ($classReflection->getInterfaces() as $interface) { + $extendsTags = $interface->getRequireExtendsTags(); + foreach ($extendsTags as $extendsTag) { + $type = $extendsTag->getType(); + if (!$type instanceof ObjectType) { + continue; + } + + if ($classReflection->is($type->getClassName())) { + continue; + } + + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Interface %s requires implementing class to extend %s, but %s does not.', + $interface->getDisplayName(), + $type->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + ), + ) + ->identifier('class.missingExtends') + ->build(); + } + } + + foreach ($classReflection->getTraits(true) as $trait) { + $extendsTags = $trait->getRequireExtendsTags(); + foreach ($extendsTags as $extendsTag) { + $type = $extendsTag->getType(); + if (!$type instanceof ObjectType) { + continue; + } + + if ($classReflection->is($type->getClassName())) { + continue; + } + + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Trait %s requires using class to extend %s, but %s does not.', + $trait->getDisplayName(), + $type->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + ), + ) + ->identifier('class.missingExtends') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/RequireImplementsRule.php b/src/Rules/Classes/RequireImplementsRule.php new file mode 100644 index 0000000000..3b1fdca948 --- /dev/null +++ b/src/Rules/Classes/RequireImplementsRule.php @@ -0,0 +1,58 @@ + + */ +final class RequireImplementsRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + $errors = []; + foreach ($classReflection->getTraits(true) as $trait) { + $implementsTags = $trait->getRequireImplementsTags(); + foreach ($implementsTags as $implementsTag) { + $type = $implementsTag->getType(); + if (!$type instanceof ObjectType) { + continue; + } + + if ($classReflection->implementsInterface($type->getClassName())) { + continue; + } + + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Trait %s requires using class to implement %s, but %s does not.', + $trait->getDisplayName(), + $type->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + ), + ) + ->identifier('class.missingImplements') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/TraitAttributeClassRule.php b/src/Rules/Classes/TraitAttributeClassRule.php index 454d063a3a..c73a1f8340 100644 --- a/src/Rules/Classes/TraitAttributeClassRule.php +++ b/src/Rules/Classes/TraitAttributeClassRule.php @@ -10,7 +10,7 @@ /** * @implements Rule */ -class TraitAttributeClassRule implements Rule +final class TraitAttributeClassRule implements Rule { public function getNodeType(): string @@ -25,7 +25,9 @@ public function processNode(Node $node, Scope $scope): array $name = $attr->name->toLowerString(); if ($name === 'attribute') { return [ - RuleErrorBuilder::message('Trait cannot be an Attribute class.')->build(), + RuleErrorBuilder::message('Trait cannot be an Attribute class.') + ->identifier('attribute.trait') + ->build(), ]; } } diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index a6383e9f23..b29ca65bd4 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -8,7 +8,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; use PHPStan\ShouldNotHappenException; @@ -16,14 +15,12 @@ use function array_map; use function array_values; use function count; -use function is_string; use function sprintf; -use function strtolower; /** * @implements Rule */ -class UnusedConstructorParametersRule implements Rule +final class UnusedConstructorParametersRule implements Rule { public function __construct(private UnusedFunctionParametersCheck $check) @@ -37,44 +34,44 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); + $method = $node->getMethodReflection(); + $originalNode = $node->getOriginalNode(); + if (!$method->isConstructor() || $originalNode->stmts === null) { + return []; } - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { + if (count($originalNode->params) === 0) { return []; } - - $originalNode = $node->getOriginalNode(); - if (strtolower($method->getName()) !== '__construct' || $originalNode->stmts === null) { + if ($node->getClassReflection()->isAttributeClass()) { return []; } - if (count($originalNode->params) === 0) { - return []; + foreach ($node->getClassReflection()->getInterfaces() as $interface) { + if ($interface->hasConstructor()) { + return []; + } } $message = sprintf( 'Constructor of class %s has an unused parameter $%%s.', - SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()), + SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName()), ); - if ($scope->getClassReflection()->isAnonymous()) { + if ($node->getClassReflection()->isAnonymous()) { $message = 'Constructor of an anonymous class has an unused parameter $%s.'; } return $this->check->getUnusedParameters( $scope, - array_map(static function (Param $parameter): string { - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + array_map(static function (Param $parameter): Variable { + if (!$parameter->var instanceof Variable) { throw new ShouldNotHappenException(); } - return $parameter->var->name; + return $parameter->var; }, 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 1339155d9e..013e6b4b81 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\BooleanAndNode; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -14,12 +15,14 @@ /** * @implements Rule */ -class BooleanAndConstantConditionRule implements Rule +final class BooleanAndConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -36,10 +39,11 @@ public function processNode( { $errors = []; $originalNode = $node->getOriginalNode(); + $nodeText = $originalNode->getOperatorSigil(); $leftType = $this->helper->getBooleanType($scope, $originalNode->left); - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'logicalAnd'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $tipText, $originalNode): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -48,13 +52,27 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $errors[] = $addTipLeft(RuleErrorBuilder::message(sprintf( - 'Left side of && is always %s.', - $leftType->getValue() ? 'true' : 'false', - )))->line($originalNode->left->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf( + 'Left side of %s is always %s.', + $nodeText, + $leftType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.leftAlways%s', $identifierType, $leftType->getValue() ? 'True' : 'False')) + ->line($originalNode->left->getStartLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } } $rightScope = $node->getRightScope(); @@ -62,48 +80,76 @@ public function processNode( $rightScope, $originalNode->right, ); - if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { + if ($rightType instanceof ConstantBooleanType && !$scope->isInFirstLevelStatement()) { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } $booleanNativeType = $this->helper->getNativeBooleanType( - $rightScope->doNotTreatPhpDocTypesAsCertain(), + $rightScope, $originalNode->right, ); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $errors[] = $addTipRight(RuleErrorBuilder::message(sprintf( - 'Right side of && is always %s.', - $rightType->getValue() ? 'true' : 'false', - )))->line($originalNode->right->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf( + 'Right side of %s is always %s.', + $nodeText, + $rightType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.rightAlways%s', $identifierType, $rightType->getValue() ? 'True' : 'False')) + ->line($originalNode->right->getStartLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } } - if (count($errors) === 0) { - $nodeType = $scope->getType($originalNode); + if (count($errors) === 0 && !$scope->isInFirstLevelStatement()) { + $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($originalNode); + $booleanNativeType = $scope->getNativeType($originalNode); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $errors[] = $addTip(RuleErrorBuilder::message(sprintf( - 'Result of && is always %s.', - $nodeType->getValue() ? 'true' : 'false', - )))->build(); + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$nodeType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Result of %s is always %s.', + $nodeText, + $nodeType->getValue() ? 'true' : 'false', + ))); + if ($nodeType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); + + $errors[] = $errorBuilder->build(); + } } } diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index 9a8ca4ae24..2b04d48f80 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -12,12 +13,14 @@ /** * @implements Rule */ -class BooleanNotConstantConditionRule implements Rule +final class BooleanNotConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -43,16 +46,29 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($exprType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Negated boolean expression is always %s.', $exprType->getValue() ? 'false' : 'true', - )))->line($node->expr->getLine())->build(), - ]; + )))->line($node->expr->getStartLine()); + if (!$exprType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('booleanNot.always%s', $exprType->getValue() ? 'False' : 'True')); + + return [ + $errorBuilder->build(), + ]; + } } return []; diff --git a/src/Rules/Comparison/BooleanOrConstantConditionRule.php b/src/Rules/Comparison/BooleanOrConstantConditionRule.php index 4606b16ef2..b991f45981 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\BooleanOrNode; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -14,12 +15,14 @@ /** * @implements Rule */ -class BooleanOrConstantConditionRule implements Rule +final class BooleanOrConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -35,11 +38,12 @@ public function processNode( ): array { $originalNode = $node->getOriginalNode(); + $nodeText = $originalNode->getOperatorSigil(); $messages = []; $leftType = $this->helper->getBooleanType($scope, $originalNode->left); - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanOr ? 'booleanOr' : 'logicalOr'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -48,13 +52,27 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $messages[] = $addTipLeft(RuleErrorBuilder::message(sprintf( - 'Left side of || is always %s.', - $leftType->getValue() ? 'true' : 'false', - )))->line($originalNode->left->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf( + 'Left side of %s is always %s.', + $nodeText, + $leftType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.leftAlways%s', $identifierType, $leftType->getValue() ? 'True' : 'False')) + ->line($originalNode->left->getStartLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $messages[] = $errorBuilder->build(); + } } $rightScope = $node->getRightScope(); @@ -62,47 +80,76 @@ public function processNode( $rightScope, $originalNode->right, ); - if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { + if ($rightType instanceof ConstantBooleanType && !$scope->isInFirstLevelStatement()) { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } $booleanNativeType = $this->helper->getNativeBooleanType( - $rightScope->doNotTreatPhpDocTypesAsCertain(), + $rightScope, $originalNode->right, ); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $messages[] = $addTipRight(RuleErrorBuilder::message(sprintf( - 'Right side of || is always %s.', - $rightType->getValue() ? 'true' : 'false', - )))->line($originalNode->right->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf( + 'Right side of %s is always %s.', + $nodeText, + $rightType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.rightAlways%s', $identifierType, $rightType->getValue() ? 'True' : 'False')) + ->line($originalNode->right->getStartLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $messages[] = $errorBuilder->build(); + } } - if (count($messages) === 0) { - $nodeType = $scope->getType($originalNode); + if (count($messages) === 0 && !$scope->isInFirstLevelStatement()) { + $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($originalNode); + $booleanNativeType = $scope->getNativeType($originalNode); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $messages[] = $addTip(RuleErrorBuilder::message(sprintf( - 'Result of || is always %s.', - $nodeType->getValue() ? 'true' : 'false', - )))->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$nodeType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Result of %s is always %s.', + $nodeText, + $nodeType->getValue() ? 'true' : 'false', + ))); + if ($nodeType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); + + $messages[] = $errorBuilder->build(); + } } } diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index fd22403af1..36b2c569d8 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -8,7 +8,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Type\BooleanType; -class ConstantConditionRuleHelper +final class ConstantConditionRuleHelper { public function __construct( @@ -18,18 +18,15 @@ public function __construct( { } - public function shouldReportAlwaysTrueByDefault(Expr $expr): bool - { - return $expr instanceof Expr\BooleanNot - || $expr instanceof Expr\BinaryOp\BooleanOr - || $expr instanceof Expr\BinaryOp\BooleanAnd - || $expr instanceof Expr\Ternary - || $expr instanceof Expr\Isset_ - || $expr instanceof Expr\Empty_; - } - public function shouldSkip(Scope $scope, Expr $expr): bool { + if ( + $expr instanceof Expr\BinaryOp\Equal + || $expr instanceof Expr\BinaryOp\NotEqual + ) { + return true; + } + if ( $expr instanceof Expr\Instanceof_ || $expr instanceof Expr\BinaryOp\Identical diff --git a/src/Rules/Comparison/ConstantLooseComparisonRule.php b/src/Rules/Comparison/ConstantLooseComparisonRule.php new file mode 100644 index 0000000000..09961335e7 --- /dev/null +++ b/src/Rules/Comparison/ConstantLooseComparisonRule.php @@ -0,0 +1,91 @@ + + */ +final class ConstantLooseComparisonRule implements Rule +{ + + public function __construct( + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\BinaryOp::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node instanceof Node\Expr\BinaryOp\Equal && !$node instanceof Node\Expr\BinaryOp\NotEqual) { + return []; + } + + $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); + if (!$nodeType instanceof ConstantBooleanType) { + return []; + } + + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); + if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + if (!$nodeType->getValue()) { + return [ + $addTip(RuleErrorBuilder::message(sprintf( + 'Loose comparison using %s between %s and %s will always evaluate to false.', + $node->getOperatorSigil(), + $scope->getType($node->left)->describe(VerbosityLevel::value()), + $scope->getType($node->right)->describe(VerbosityLevel::value()), + )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual'))->build(), + ]; + } + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Loose comparison using %s between %s and %s will always evaluate to true.', + $node->getOperatorSigil(), + $scope->getType($node->left)->describe(VerbosityLevel::value()), + $scope->getType($node->right)->describe(VerbosityLevel::value()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('%s.alwaysTrue', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual')); + + return [$errorBuilder->build()]; + } + +} diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php index 9683f4c608..0de2772219 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -3,7 +3,7 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; use PHPStan\Analyser\Scope; @@ -16,12 +16,13 @@ /** * @implements Rule */ -class DoWhileLoopConstantConditionRule implements Rule +final class DoWhileLoopConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -47,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array if ($statement->num === null) { continue; } - if (!$statement->num instanceof LNumber) { + if (!$statement->num instanceof Int_) { continue; } $value = $statement->num->value; @@ -70,15 +71,21 @@ public function processNode(Node $node, Scope $scope): array if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( 'Do-while loop condition is always %s.', $exprType->getValue() ? 'true' : 'false', - )))->line($node->getCond()->getLine())->build(), + ))) + ->line($node->getCond()->getStartLine()) + ->identifier(sprintf('doWhile.always%s', $exprType->getValue() ? 'True' : 'False')) + ->build(), ]; } diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 31df891540..a3c0bfab64 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -12,12 +13,14 @@ /** * @implements Rule */ -class ElseIfConstantConditionRule implements Rule +final class ElseIfConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -43,22 +46,28 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( + + $isLast = $node->cond->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$exprType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Elseif condition is always %s.', $exprType->getValue() ? 'true' : 'false', - )))->line($node->cond->getLine()) - ->identifier('deadCode.elseifConstantCondition') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - 'value' => $exprType->getValue(), - ]) - ->build(), - ]; + )))->line($node->cond->getStartLine()); + + if ($exprType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('elseif.always%s', $exprType->getValue() ? 'True' : 'False')); + + return [$errorBuilder->build()]; + } } return []; diff --git a/src/Rules/Comparison/IfConstantConditionRule.php b/src/Rules/Comparison/IfConstantConditionRule.php index 0de33d796b..f0d71c3e01 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -12,12 +12,13 @@ /** * @implements Rule */ -class IfConstantConditionRule implements Rule +final class IfConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -43,22 +44,20 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( 'If condition is always %s.', $exprType->getValue() ? 'true' : 'false', - )))->line($node->cond->getLine()) - ->identifier('deadCode.ifConstantCondition') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - 'value' => $exprType->getValue(), - ]) - ->build(), + ))) + ->identifier(sprintf('if.always%s', $exprType->getValue() ? 'True' : 'False')) + ->line($node->cond->getStartLine())->build(), ]; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index e60656e751..a4b522d9f3 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -4,21 +4,22 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; -use function strtolower; /** * @implements Rule */ -class ImpossibleCheckTypeFunctionCallRule implements Rule +final class ImpossibleCheckTypeFunctionCallRule implements Rule { public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - private bool $checkAlwaysTrueCheckTypeFunctionCall, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -35,9 +36,6 @@ public function processNode(Node $node, Scope $scope): array } $functionName = (string) $node->name; - if (strtolower($functionName) === 'is_a') { - return []; - } $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); if ($isAlways === null) { return []; @@ -52,8 +50,11 @@ public function processNode(Node $node, Scope $scope): array if ($isAlways !== null) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$isAlways) { @@ -62,19 +63,27 @@ public function processNode(Node $node, Scope $scope): array 'Call to function %s()%s will always evaluate to false.', $functionName, $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), - ]; - } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to function %s()%s will always evaluate to true.', - $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), + )))->identifier('function.impossibleType')->build(), ]; } - return []; + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to function %s()%s will always evaluate to true.', + $functionName, + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('function.alreadyNarrowedType'); + + return [$errorBuilder->build()]; } } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 5c7c8fc0a8..48eea97d6f 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -7,33 +7,39 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\UnionType; 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 +final class ImpossibleCheckTypeHelper { /** @@ -53,14 +59,16 @@ public function findSpecifiedType( Expr $node, ): ?bool { - if ( - $node instanceof FuncCall - && count($node->getArgs()) > 0 - ) { + if ($node instanceof FuncCall) { + if ($node->isFirstClassCallable()) { + return null; + } + $argsCount = count($node->getArgs()); if ($node->name instanceof Node\Name) { $functionName = strtolower((string) $node->name); - if ($functionName === 'assert') { - $assertValue = $scope->getType($node->getArgs()[0]->value)->toBoolean(); + if ($functionName === 'assert' && $argsCount >= 1) { + $arg = $node->getArgs()[0]->value; + $assertValue = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->toBoolean(); if (!$assertValue instanceof ConstantBooleanType) { return null; } @@ -71,6 +79,7 @@ public function findSpecifiedType( 'class_exists', 'interface_exists', 'trait_exists', + 'enum_exists', ], true)) { return null; } @@ -78,11 +87,11 @@ public function findSpecifiedType( return null; } elseif ($functionName === 'defined') { return null; - } elseif ( - $functionName === 'in_array' - && count($node->getArgs()) >= 3 - ) { - $haystackType = $scope->getType($node->getArgs()[1]->value); + } elseif ($functionName === 'array_search') { + return null; + } elseif ($functionName === 'in_array' && $argsCount >= 2) { + $haystackArg = $node->getArgs()[1]->value; + $haystackType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($haystackArg) : $scope->getNativeType($haystackArg)); if ($haystackType instanceof MixedType) { return null; } @@ -91,14 +100,30 @@ public function findSpecifiedType( return null; } - $constantArrays = TypeUtils::getConstantArrays($haystackType); - $needleType = $scope->getType($node->getArgs()[0]->value); + $needleArg = $node->getArgs()[0]->value; + $needleType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($needleArg) : $scope->getNativeType($needleArg)); + + $isStrictComparison = false; + if ($argsCount >= 3) { + $strictNodeType = $scope->getType($node->getArgs()[2]->value); + $isStrictComparison = $strictNodeType->isTrue()->yes(); + } + + $isStrictComparison = $isStrictComparison + || $needleType->isEnum()->yes() + || $haystackType->getIterableValueType()->isEnum()->yes(); + + if (!$isStrictComparison) { + return null; + } + $valueType = $haystackType->getIterableValueType(); - $constantNeedleTypesCount = count(TypeUtils::getConstantScalars($needleType)); - $constantHaystackTypesCount = count(TypeUtils::getConstantScalars($valueType)); + $constantNeedleTypesCount = count($needleType->getFiniteTypes()); + $constantHaystackTypesCount = count($valueType->getFiniteTypes()); $isNeedleSupertype = $needleType->isSuperTypeOf($valueType); - if (count($constantArrays) === 0) { + if ($haystackType->isConstantArray()->no()) { if ($haystackType->isIterableAtLeastOnce()->yes()) { + // In this case the generic implementation via typeSpecifier fails, because the argument types cannot be narrowed down. if ($constantNeedleTypesCount === 1 && $constantHaystackTypesCount === 1) { if ($isNeedleSupertype->yes()) { return true; @@ -107,21 +132,36 @@ public function findSpecifiedType( return false; } } + + return null; } - return null; } if (!$haystackType instanceof ConstantArrayType || count($haystackType->getValueTypes()) > 0) { - $haystackArrayTypes = TypeUtils::getArrays($haystackType); + $haystackArrayTypes = $haystackType->getArrays(); if (count($haystackArrayTypes) === 1 && $haystackArrayTypes[0]->getIterableValueType() instanceof NeverType) { return null; } if ($isNeedleSupertype->maybe() || $isNeedleSupertype->yes()) { foreach ($haystackArrayTypes as $haystackArrayType) { - foreach (TypeUtils::getConstantScalars($haystackArrayType->getIterableValueType()) as $constantScalarType) { - if ($needleType->isSuperTypeOf($constantScalarType)->yes()) { - continue 2; + if ($haystackArrayType instanceof ConstantArrayType) { + foreach ($haystackArrayType->getValueTypes() as $i => $haystackArrayValueType) { + if ($haystackArrayType->isOptionalKey($i)) { + continue; + } + + foreach ($haystackArrayValueType->getConstantScalarTypes() as $constantScalarType) { + if ($constantScalarType->isSuperTypeOf($needleType)->yes()) { + continue 3; + } + } + } + } else { + foreach ($haystackArrayType->getIterableValueType()->getConstantScalarTypes() as $constantScalarType) { + if ($constantScalarType->isSuperTypeOf($needleType)->yes()) { + continue 2; + } } } @@ -140,9 +180,9 @@ public function findSpecifiedType( } } } - } elseif ($functionName === 'method_exists' && count($node->getArgs()) >= 2) { - $objectType = $scope->getType($node->getArgs()[0]->value); - $methodType = $scope->getType($node->getArgs()[1]->value); + } elseif ($functionName === 'method_exists' && $argsCount >= 2) { + $objectArg = $node->getArgs()[0]->value; + $objectType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($objectArg) : $scope->getNativeType($objectArg)); if ($objectType instanceof ConstantStringType && !$this->reflectionProvider->hasClass($objectType->getValue()) @@ -150,12 +190,15 @@ public function findSpecifiedType( return false; } + $methodArg = $node->getArgs()[1]->value; + $methodType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($methodArg) : $scope->getNativeType($methodArg)); + if ($methodType instanceof ConstantStringType) { if ($objectType instanceof ConstantStringType) { $objectType = new ObjectType($objectType->getValue()); } - if ($objectType instanceof TypeWithClassName) { + if ($objectType->getObjectClassNames() !== []) { if ($objectType->hasMethod($methodType->getValue())->yes()) { return true; } @@ -164,12 +207,41 @@ public function findSpecifiedType( return false; } } + + $genericType = TypeTraverser::map($objectType, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + if ($type instanceof GenericClassStringType) { + return $type->getGenericType(); + } + return new MixedType(); + }); + + if ($genericType instanceof TypeWithClassName) { + if ($genericType->hasMethod($methodType->getValue())->yes()) { + return true; + } + + $classReflection = $genericType->getClassReflection(); + if ( + $classReflection !== null + && $classReflection->isFinal() + && $genericType->hasMethod($methodType->getValue())->no()) { + return false; + } + } } } } } - $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $node, TypeSpecifierContext::createTruthy()); + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $typeSpecifierScope = $this->treatPhpDocTypesAsCertain ? $scope : $scope->doNotTreatPhpDocTypesAsCertain(); + $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($typeSpecifierScope, $node, $this->determineContext($typeSpecifierScope, $node)); // don't validate types on overwrite if ($specifiedTypes->shouldOverwrite()) { @@ -179,26 +251,26 @@ public function findSpecifiedType( $sureTypes = $specifiedTypes->getSureTypes(); $sureNotTypes = $specifiedTypes->getSureNotTypes(); - $isSpecified = static function (Expr $expr) use ($scope, $node): bool { - if ($expr === $node) { - return true; + $rootExpr = $specifiedTypes->getRootExpr(); + if ($rootExpr !== null) { + if (self::isSpecified($typeSpecifierScope, $node, $rootExpr)) { + return null; } - if ($expr instanceof Expr\Variable && is_string($expr->name) && !$scope->hasVariableType($expr->name)->yes()) { - return true; + $rootExprType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($rootExpr) : $scope->getNativeType($rootExpr)); + if ($rootExprType instanceof ConstantBooleanType) { + return $rootExprType->getValue(); } - return ( - $node instanceof FuncCall - || $node instanceof MethodCall - || $node instanceof Expr\StaticCall - ) && $scope->isSpecified($expr); - }; + return null; + } - if (count($sureTypes) === 1 && count($sureNotTypes) === 0) { - $sureType = reset($sureTypes); - if ($isSpecified($sureType[0])) { - return null; + $results = []; + + foreach ($sureTypes as $sureType) { + if (self::isSpecified($typeSpecifierScope, $node, $sureType[0])) { + $results[] = TrinaryLogic::createMaybe(); + continue; } if ($this->treatPhpDocTypesAsCertain) { @@ -210,18 +282,13 @@ public function findSpecifiedType( /** @var Type $resultType */ $resultType = $sureType[1]; - $isSuperType = $resultType->isSuperTypeOf($argumentType); - if ($isSuperType->yes()) { - return true; - } elseif ($isSuperType->no()) { - return false; - } + $results[] = $resultType->isSuperTypeOf($argumentType)->result; + } - return null; - } elseif (count($sureNotTypes) === 1 && count($sureTypes) === 0) { - $sureNotType = reset($sureNotTypes); - if ($isSpecified($sureNotType[0])) { - return null; + foreach ($sureNotTypes as $sureNotType) { + if (self::isSpecified($typeSpecifierScope, $node, $sureNotType[0])) { + $results[] = TrinaryLogic::createMaybe(); + continue; } if ($this->treatPhpDocTypesAsCertain) { @@ -233,45 +300,40 @@ public function findSpecifiedType( /** @var Type $resultType */ $resultType = $sureNotType[1]; - $isSuperType = $resultType->isSuperTypeOf($argumentType); - if ($isSuperType->yes()) { - return false; - } elseif ($isSuperType->no()) { - return true; - } + $results[] = $resultType->isSuperTypeOf($argumentType)->negate()->result; + } + if (count($results) === 0) { return null; } - if (count($sureTypes) > 0) { - foreach ($sureTypes as $sureType) { - if ($isSpecified($sureType[0])) { - return null; - } - } - $types = TypeCombinator::union( - ...array_column($sureTypes, 1), - ); - if ($types instanceof NeverType) { - return false; - } + $result = TrinaryLogic::createYes()->and(...$results); + return $result->maybe() ? null : $result->yes(); + } + + private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool + { + if ($expr === $node) { + return true; } - if (count($sureNotTypes) > 0) { - foreach ($sureNotTypes as $sureNotType) { - if ($isSpecified($sureNotType[0])) { - return null; - } - } - $types = TypeCombinator::union( - ...array_column($sureNotTypes, 1), - ); - if ($types instanceof NeverType) { - return true; - } + if ($expr instanceof Expr\Variable) { + return is_string($expr->name) && !$scope->hasVariableType($expr->name)->yes(); + } + + if ($expr instanceof Expr\BooleanNot) { + return self::isSpecified($scope, $node, $expr->expr); } - return null; + if ($expr instanceof Expr\BinaryOp) { + return self::isSpecified($scope, $node, $expr->left) || self::isSpecified($scope, $node, $expr->right); + } + + return ( + $node instanceof FuncCall + || $node instanceof MethodCall + || $node instanceof Expr\StaticCall + ) && $scope->hasExpressionType($expr)->yes(); } /** @@ -286,7 +348,7 @@ public function getArgumentsDescription( return ''; } - $descriptions = array_map(static fn (Arg $arg): string => $scope->getType($arg->value)->describe(VerbosityLevel::value()), $args); + $descriptions = array_map(fn (Arg $arg): string => ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg->value) : $scope->getNativeType($arg->value))->describe(VerbosityLevel::value()), $args); if (count($descriptions) < 3) { return sprintf(' with %s', implode(' and ', $descriptions)); @@ -315,4 +377,46 @@ public function doNotTreatPhpDocTypesAsCertain(): self ); } + private function determineContext(Scope $scope, Expr $node): TypeSpecifierContext + { + if ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { + return TypeSpecifierContext::createTruthy(); + } + + if ($node instanceof FuncCall && $node->name instanceof Node\Name) { + if ($this->reflectionProvider->hasFunction($node->name, $scope)) { + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + $returnType = TypeUtils::resolveLateResolvableTypes($parametersAcceptor->getReturnType()); + + return $returnType->isVoid()->yes() ? TypeSpecifierContext::createNull() : TypeSpecifierContext::createTruthy(); + } + } elseif ($node instanceof MethodCall && $node->name instanceof Node\Identifier) { + $methodCalledOnType = $scope->getType($node->var); + $methodReflection = $scope->getMethodReflection($methodCalledOnType, $node->name->name); + if ($methodReflection !== null) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + $returnType = TypeUtils::resolveLateResolvableTypes($parametersAcceptor->getReturnType()); + + return $returnType->isVoid()->yes() ? TypeSpecifierContext::createNull() : TypeSpecifierContext::createTruthy(); + } + } elseif ($node instanceof StaticCall && $node->name instanceof Node\Identifier) { + if ($node->class instanceof Node\Name) { + $calleeType = $scope->resolveTypeByName($node->class); + } else { + $calleeType = $scope->getType($node->class); + } + + $staticMethodReflection = $scope->getMethodReflection($calleeType, $node->name->name); + if ($staticMethodReflection !== null) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + $returnType = TypeUtils::resolveLateResolvableTypes($parametersAcceptor->getReturnType()); + + return $returnType->isVoid()->yes() ? TypeSpecifierContext::createNull() : TypeSpecifierContext::createTruthy(); + } + } + + return TypeSpecifierContext::createTruthy(); + } + } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index 6208b0eddf..4b79d98acb 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,13 +15,14 @@ /** * @implements Rule */ -class ImpossibleCheckTypeMethodCallRule implements Rule +final class ImpossibleCheckTypeMethodCallRule implements Rule { public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - private bool $checkAlwaysTrueCheckTypeFunctionCall, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -50,8 +52,11 @@ public function processNode(Node $node, Scope $scope): array if ($isAlways !== null) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$isAlways) { @@ -62,21 +67,29 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), - ]; - } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - $method = $this->getMethod($node->var, $node->name->name, $scope); - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to method %s::%s()%s will always evaluate to true.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), + )))->identifier('method.impossibleType')->build(), ]; } - return []; + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $method = $this->getMethod($node->var, $node->name->name, $scope); + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to method %s::%s()%s will always evaluate to true.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('method.alreadyNarrowedType'); + + return [$errorBuilder->build()]; } private function getMethod( diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index af7bd57afa..e4b3721538 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,13 +15,14 @@ /** * @implements Rule */ -class ImpossibleCheckTypeStaticMethodCallRule implements Rule +final class ImpossibleCheckTypeStaticMethodCallRule implements Rule { public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - private bool $checkAlwaysTrueCheckTypeFunctionCall, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -50,8 +52,11 @@ public function processNode(Node $node, Scope $scope): array if ($isAlways !== null) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$isAlways) { @@ -63,22 +68,29 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), + )))->identifier('staticMethod.impossibleType')->build(), ]; - } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - $method = $this->getMethod($node->class, $node->name->name, $scope); + } - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to static method %s::%s()%s will always evaluate to true.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), - ]; + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; } - return []; + $method = $this->getMethod($node->class, $node->name->name, $scope); + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to static method %s::%s()%s will always evaluate to true.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('staticMethod.alreadyNarrowedType'); + + return [$errorBuilder->build()]; } /** diff --git a/src/Rules/Comparison/LogicalXorConstantConditionRule.php b/src/Rules/Comparison/LogicalXorConstantConditionRule.php new file mode 100644 index 0000000000..c7531c4196 --- /dev/null +++ b/src/Rules/Comparison/LogicalXorConstantConditionRule.php @@ -0,0 +1,109 @@ + + */ +final class LogicalXorConstantConditionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return LogicalXor::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + $leftType = $this->helper->getBooleanType($scope, $node->left); + if ($leftType instanceof ConstantBooleanType) { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->left); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf( + 'Left side of xor is always %s.', + $leftType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('logicalXor.leftAlways%s', $leftType->getValue() ? 'True' : 'False')) + ->line($node->left->getStartLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } + } + + $rightType = $this->helper->getBooleanType($scope, $node->right); + if ($rightType instanceof ConstantBooleanType) { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType( + $scope, + $node->right, + ); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf( + 'Right side of xor is always %s.', + $rightType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('logicalXor.rightAlways%s', $rightType->getValue() ? 'True' : 'False')) + ->line($node->right->getStartLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Comparison/MatchExpressionRule.php b/src/Rules/Comparison/MatchExpressionRule.php index 6e89dde2b4..6edef9747b 100644 --- a/src/Rules/Comparison/MatchExpressionRule.php +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -5,22 +5,31 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MatchExpressionNode; +use PHPStan\Parser\TryCatchTypeVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use UnhandledMatchError; +use function array_map; use function count; use function sprintf; /** * @implements Rule */ -class MatchExpressionRule implements Rule +final class MatchExpressionRule implements Rule { - public function __construct(private bool $checkAlwaysTrueStrictComparison) + public function __construct( + private ConstantConditionRuleHelper $constantConditionRuleHelper, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertain, + ) { } @@ -32,13 +41,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $matchCondition = $node->getCondition(); - $nextArmIsDead = false; + $matchConditionType = $scope->getType($matchCondition); + $nextArmIsDeadForType = false; + $nextArmIsDeadForNativeType = false; $errors = []; $armsCount = count($node->getArms()); $hasDefault = false; foreach ($node->getArms() as $i => $arm) { - if ($nextArmIsDead) { - $errors[] = RuleErrorBuilder::message('Match arm is unreachable because previous comparison is always true.')->line($arm->getLine())->build(); + if ( + $nextArmIsDeadForNativeType + || ($nextArmIsDeadForType && $this->treatPhpDocTypesAsCertain) + ) { continue; } $armConditions = $arm->getConditions(); @@ -51,10 +64,31 @@ public function processNode(Node $node, Scope $scope): array $matchCondition, $armCondition->getCondition(), ); + $armConditionResult = $armConditionScope->getType($armConditionExpr); if (!$armConditionResult instanceof ConstantBooleanType) { continue; } + if ($armConditionResult->getValue()) { + $nextArmIsDeadForType = true; + } + + if (!$this->treatPhpDocTypesAsCertain) { + $armConditionNativeResult = $armConditionScope->getNativeType($armConditionExpr); + if (!$armConditionNativeResult instanceof ConstantBooleanType) { + continue; + } + if ($armConditionNativeResult->getValue()) { + $nextArmIsDeadForNativeType = true; + } + } + + if ($matchConditionType instanceof ConstantBooleanType) { + $armConditionStandaloneResult = $this->constantConditionRuleHelper->getBooleanType($armConditionScope, $armCondition->getCondition()); + if (!$armConditionStandaloneResult instanceof ConstantBooleanType) { + continue; + } + } $armLine = $armCondition->getLine(); if (!$armConditionResult->getValue()) { @@ -62,35 +96,79 @@ public function processNode(Node $node, Scope $scope): array 'Match arm comparison between %s and %s is always false.', $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), - ))->line($armLine)->build(); - } else { - $nextArmIsDead = true; - if ( - $this->checkAlwaysTrueStrictComparison - && ($i !== $armsCount - 1 || $i === 0) - ) { - $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()), - ))->line($armLine)->build(); - } + ))->line($armLine)->identifier('match.alwaysFalse')->build(); + continue; + } + + if ($i === $armsCount - 1 && !$this->reportAlwaysTrueInLastCondition) { + continue; } + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Match arm comparison between %s and %s is always true.', + $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), + $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), + ))->line($armLine); + if ($i !== $armsCount - 1 && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('match.alwaysTrue'); + + $errors[] = $errorBuilder->build(); } } - if (!$hasDefault && !$nextArmIsDead) { + if (!$hasDefault && !$nextArmIsDeadForType) { $remainingType = $node->getEndScope()->getType($matchCondition); - if (!$remainingType instanceof NeverType) { + $cases = $remainingType->getEnumCases(); + $casesCount = count($cases); + if ($casesCount > 1) { + $remainingType = new UnionType($cases); + } + if ($casesCount === 1) { + $remainingType = $cases[0]; + } + if ( + !$remainingType instanceof NeverType + && !$this->isUnhandledMatchErrorCaught($node) + && !$this->hasUnhandledMatchErrorThrowsTag($scope) + ) { $errors[] = RuleErrorBuilder::message(sprintf( 'Match expression does not handle remaining %s: %s', $remainingType instanceof UnionType ? 'values' : 'value', $remainingType->describe(VerbosityLevel::value()), - ))->build(); + ))->identifier('match.unhandled')->build(); } } return $errors; } + private function isUnhandledMatchErrorCaught(Node $node): bool + { + $tryCatchTypes = $node->getAttribute(TryCatchTypeVisitor::ATTRIBUTE_NAME); + if ($tryCatchTypes === null) { + return false; + } + + $tryCatchType = TypeCombinator::union(...array_map(static fn (string $class) => new ObjectType($class), $tryCatchTypes)); + + return $tryCatchType->isSuperTypeOf(new ObjectType(UnhandledMatchError::class))->yes(); + } + + private function hasUnhandledMatchErrorThrowsTag(Scope $scope): bool + { + $function = $scope->getFunction(); + if ($function === null) { + return false; + } + + $throwsType = $function->getThrowType(); + if ($throwsType === null) { + return false; + } + + return $throwsType->isSuperTypeOf(new ObjectType(UnhandledMatchError::class))->yes(); + } + } diff --git a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php index bf8feaaec6..a35a1c740e 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -7,16 +7,25 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\VerbosityLevel; +use function get_class; use function sprintf; /** * @implements Rule */ -class NumberComparisonOperatorsConstantConditionRule implements Rule +final class NumberComparisonOperatorsConstantConditionRule implements Rule { + public function __construct( + private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + public function getNodeType(): string { return BinaryOp::class; @@ -36,16 +45,49 @@ public function processNode( return []; } - $exprType = $scope->getType($node); + $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if ($exprType instanceof ConstantBooleanType) { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $scope->getNativeType($node); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + switch (get_class($node)) { + case BinaryOp\Greater::class: + $nodeType = 'greater'; + break; + case BinaryOp\GreaterOrEqual::class: + $nodeType = 'greaterOrEqual'; + break; + case BinaryOp\Smaller::class: + $nodeType = 'smaller'; + break; + case BinaryOp\SmallerOrEqual::class: + $nodeType = 'smallerOrEqual'; + break; + default: + throw new ShouldNotHappenException(); + } + return [ - RuleErrorBuilder::message(sprintf( + $addTip(RuleErrorBuilder::message(sprintf( 'Comparison operation "%s" between %s and %s is always %s.', $node->getOperatorSigil(), $scope->getType($node->left)->describe(VerbosityLevel::value()), $scope->getType($node->right)->describe(VerbosityLevel::value()), $exprType->getValue() ? 'true' : 'false', - ))->build(), + )))->identifier(sprintf('%s.always%s', $nodeType, $exprType->getValue() ? 'True' : 'False'))->build(), ]; } diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 18200745a0..9ba6ee4797 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -3,20 +3,31 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\VerbosityLevel; +use function count; use function sprintf; /** * @implements Rule */ -class StrictComparisonOfDifferentTypesRule implements Rule +final class StrictComparisonOfDifferentTypesRule implements Rule { - public function __construct(private bool $checkAlwaysTrueStrictComparison) + public function __construct( + private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, + ) { } @@ -27,39 +38,112 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node instanceof Node\Expr\BinaryOp\Identical && !$node instanceof Node\Expr\BinaryOp\NotIdentical) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + if ($node instanceof Node\Expr\BinaryOp\Identical) { + $nodeTypeResult = $this->richerScopeGetTypeHelper->getIdenticalResult($this->treatPhpDocTypesAsCertain ? $scope : $scope->doNotTreatPhpDocTypesAsCertain(), $node); + } elseif ($node instanceof Node\Expr\BinaryOp\NotIdentical) { + $nodeTypeResult = $this->richerScopeGetTypeHelper->getNotIdenticalResult($this->treatPhpDocTypesAsCertain ? $scope : $scope->doNotTreatPhpDocTypesAsCertain(), $node); + } else { return []; } - $nodeType = $scope->getType($node); + $nodeType = $nodeTypeResult->type; if (!$nodeType instanceof ConstantBooleanType) { return []; } - $leftType = $scope->getType($node->left); - $rightType = $scope->getType($node->right); + $leftType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->left) : $scope->getNativeType($node->left); + $rightType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->right) : $scope->getNativeType($node->right); + + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $nodeTypeResult): RuleErrorBuilder { + $reasons = $nodeTypeResult->reasons; + if (count($reasons) > 0) { + return $ruleErrorBuilder->acceptsReasonsTip($reasons); + } + + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); + if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + $verbosity = VerbosityLevel::value(); + + if ( + ( + $leftType->isConstantScalarValue()->yes() + && !$leftType->isString()->no() + && !$rightType->isConstantScalarValue()->yes() + && !$rightType->isString()->no() + && ( + TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + || TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + ) + ) || ( + $rightType->isConstantScalarValue()->yes() + && !$rightType->isString()->no() + && !$leftType->isConstantScalarValue()->yes() + && !$leftType->isString()->no() + && ( + TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + || TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + ) + ) + ) { + $verbosity = VerbosityLevel::precise(); + } if (!$nodeType->getValue()) { return [ - RuleErrorBuilder::message(sprintf( + $addTip(RuleErrorBuilder::message(sprintf( 'Strict comparison using %s between %s and %s will always evaluate to false.', - $node instanceof Node\Expr\BinaryOp\Identical ? '===' : '!==', - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), - ))->build(), - ]; - } elseif ($this->checkAlwaysTrueStrictComparison) { - return [ - RuleErrorBuilder::message(sprintf( - 'Strict comparison using %s between %s and %s will always evaluate to true.', - $node instanceof Node\Expr\BinaryOp\Identical ? '===' : '!==', - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), - ))->build(), + $node->getOperatorSigil(), + $leftType->describe($verbosity), + $rightType->describe($verbosity), + )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(), ]; } - return []; + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Strict comparison using %s between %s and %s will always evaluate to true.', + $node->getOperatorSigil(), + $leftType->describe($verbosity), + $rightType->describe($verbosity), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->addTip('Remove remaining cases below this one and this error will disappear too.'); + } + + if ( + $leftType->isEnum()->yes() + && $rightType->isEnum()->yes() + && $node->getAttribute(LastConditionVisitor::ATTRIBUTE_IS_MATCH_NAME, false) !== true + ) { + $errorBuilder->addTip('Use match expression instead. PHPStan will report unhandled enum cases.'); + } + + $errorBuilder->identifier(sprintf('%s.alwaysTrue', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical')); + + return [ + $errorBuilder->build(), + ]; } } diff --git a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php index e608d7bfbd..10a359ea4b 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -12,12 +12,13 @@ /** * @implements Rule */ -class TernaryOperatorConstantConditionRule implements Rule +final class TernaryOperatorConstantConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -43,23 +44,17 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( 'Ternary operator condition is always %s.', $exprType->getValue() ? 'true' : 'false', - ))) - ->identifier('deadCode.ternaryConstantCondition') - ->metadata([ - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - 'value' => $exprType->getValue(), - ]) - ->build(), + )))->identifier(sprintf('ternary.always%s', $exprType->getValue() ? 'True' : 'False'))->build(), ]; } diff --git a/src/Rules/Comparison/UnreachableIfBranchesRule.php b/src/Rules/Comparison/UnreachableIfBranchesRule.php deleted file mode 100644 index ce9eb2328f..0000000000 --- a/src/Rules/Comparison/UnreachableIfBranchesRule.php +++ /dev/null @@ -1,80 +0,0 @@ - - */ -class UnreachableIfBranchesRule implements Rule -{ - - public function __construct( - private ConstantConditionRuleHelper $helper, - private bool $treatPhpDocTypesAsCertain, - ) - { - } - - public function getNodeType(): string - { - return Node\Stmt\If_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $errors = []; - $condition = $node->cond; - $conditionType = $scope->getType($condition)->toBoolean(); - $nextBranchIsDead = $conditionType instanceof ConstantBooleanType && $conditionType->getValue() && $this->helper->shouldSkip($scope, $node->cond) && !$this->helper->shouldReportAlwaysTrueByDefault($node->cond); - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, &$condition): RuleErrorBuilder { - if (!$this->treatPhpDocTypesAsCertain) { - return $ruleErrorBuilder; - } - - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($condition)->toBoolean(); - if ($booleanNativeType instanceof ConstantBooleanType) { - return $ruleErrorBuilder; - } - - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); - }; - - foreach ($node->elseifs as $elseif) { - if ($nextBranchIsDead) { - $errors[] = $addTip(RuleErrorBuilder::message('Elseif branch is unreachable because previous condition is always true.')->line($elseif->getLine())) - ->identifier('deadCode.unreachableElseif') - ->metadata([ - 'ifDepth' => $node->getAttribute('statementDepth'), - 'ifOrder' => $node->getAttribute('statementOrder'), - 'depth' => $elseif->getAttribute('statementDepth'), - 'order' => $elseif->getAttribute('statementOrder'), - ]) - ->build(); - continue; - } - - $condition = $elseif->cond; - $conditionType = $scope->getType($condition)->toBoolean(); - $nextBranchIsDead = $conditionType instanceof ConstantBooleanType && $conditionType->getValue() && $this->helper->shouldSkip($scope, $elseif->cond) && !$this->helper->shouldReportAlwaysTrueByDefault($elseif->cond); - } - - if ($node->else !== null && $nextBranchIsDead) { - $errors[] = $addTip(RuleErrorBuilder::message('Else branch is unreachable because previous condition is always true.'))->line($node->else->getLine()) - ->identifier('deadCode.unreachableElse') - ->metadata([ - 'ifDepth' => $node->getAttribute('statementDepth'), - 'ifOrder' => $node->getAttribute('statementOrder'), - ]) - ->build(); - } - - return $errors; - } - -} diff --git a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php deleted file mode 100644 index 551f0e63e9..0000000000 --- a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php +++ /dev/null @@ -1,67 +0,0 @@ - - */ -class UnreachableTernaryElseBranchRule implements Rule -{ - - public function __construct( - private ConstantConditionRuleHelper $helper, - private bool $treatPhpDocTypesAsCertain, - ) - { - } - - public function getNodeType(): string - { - return Node\Expr\Ternary::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $conditionType = $scope->getType($node->cond)->toBoolean(); - if ( - $conditionType instanceof ConstantBooleanType - && $conditionType->getValue() - && $this->helper->shouldSkip($scope, $node->cond) - && !$this->helper->shouldReportAlwaysTrueByDefault($node->cond) - ) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { - if (!$this->treatPhpDocTypesAsCertain) { - return $ruleErrorBuilder; - } - - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($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('Else branch is unreachable because ternary operator condition is always true.')) - ->line($node->else->getLine()) - ->identifier('deadCode.unreachableTernaryElse') - ->metadata([ - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - ]) - ->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php index 8494d0c2b2..ac87e2b8a1 100644 --- a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php +++ b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php @@ -6,12 +6,11 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\VoidType; /** * @implements Rule */ -class UsageOfVoidMatchExpressionRule implements Rule +final class UsageOfVoidMatchExpressionRule implements Rule { public function getNodeType(): string @@ -21,12 +20,11 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $matchResultType = $scope->getType($node); - if ( - $matchResultType instanceof VoidType - && !$scope->isInFirstLevelStatement() - ) { - return [RuleErrorBuilder::message('Result of match expression (void) is used.')->build()]; + if (!$scope->isInFirstLevelStatement()) { + $matchResultType = $scope->getKeepVoidType($node); + if ($matchResultType->isVoid()->yes()) { + return [RuleErrorBuilder::message('Result of match expression (void) is used.')->identifier('match.void')->build()]; + } } return []; diff --git a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php index 0c485c7329..2b9fbdbdac 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -12,12 +12,13 @@ /** * @implements Rule */ -class WhileLoopAlwaysFalseConditionRule implements Rule +final class WhileLoopAlwaysFalseConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -33,7 +34,7 @@ public function processNode( ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); - if ($exprType instanceof ConstantBooleanType && !$exprType->getValue()) { + if ($exprType->isFalse()->yes()) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; @@ -43,12 +44,16 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ - $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getLine()) + $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getStartLine()) + ->identifier('while.alwaysFalse') ->build(), ]; } diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php index 3723019229..8f6a1e3cf0 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -3,7 +3,7 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; use PHPStan\Analyser\Scope; @@ -15,12 +15,13 @@ /** * @implements Rule */ -class WhileLoopAlwaysTrueConditionRule implements Rule +final class WhileLoopAlwaysTrueConditionRule implements Rule { public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, ) { } @@ -46,7 +47,7 @@ public function processNode( if ($statement->num === null) { continue; } - if (!$statement->num instanceof LNumber) { + if (!$statement->num instanceof Int_) { continue; } $value = $statement->num->value; @@ -60,7 +61,7 @@ public function processNode( } $originalNode = $node->getOriginalNode(); $exprType = $this->helper->getBooleanType($scope, $originalNode->cond); - if ($exprType instanceof ConstantBooleanType && $exprType->getValue()) { + if ($exprType->isTrue()->yes()) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; @@ -70,12 +71,16 @@ public function processNode( if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ - $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getLine()) + $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getStartLine()) + ->identifier('while.alwaysTrue') ->build(), ]; } diff --git a/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php index 756cd130bf..977bac234b 100644 --- a/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php +++ b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php @@ -2,12 +2,29 @@ namespace PHPStan\Rules\Constants; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ClassConstantReflection; -/** @api */ +/** + * This is the extension interface to implement if you want to describe + * always-used class constant. + * + * To register it in the configuration file use the `phpstan.constants.alwaysUsedClassConstantsExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.constants.alwaysUsedClassConstantsExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/always-used-class-constants + * + * @api + */ interface AlwaysUsedClassConstantsExtension { - public function isAlwaysUsed(ConstantReflection $constant): bool; + public function isAlwaysUsed(ClassConstantReflection $constant): bool; } diff --git a/src/Rules/Constants/ClassAsClassConstantRule.php b/src/Rules/Constants/ClassAsClassConstantRule.php new file mode 100644 index 0000000000..b10d2d0080 --- /dev/null +++ b/src/Rules/Constants/ClassAsClassConstantRule.php @@ -0,0 +1,40 @@ + + */ +final class ClassAsClassConstantRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + + foreach ($node->consts as $const) { + if ($const->name->toLowerString() !== 'class') { + continue; + } + + $errors[] = RuleErrorBuilder::message('A class constant must not be called \'class\'; it is reserved for class name fetching.') + ->line($const->getStartLine()) + ->identifier('classConstant.class') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Constants/ConstantRule.php b/src/Rules/Constants/ConstantRule.php index 1eb08445dd..2bae50132b 100644 --- a/src/Rules/Constants/ConstantRule.php +++ b/src/Rules/Constants/ConstantRule.php @@ -11,9 +11,15 @@ /** * @implements Rule */ -class ConstantRule implements Rule +final class ConstantRule implements Rule { + public function __construct( + private bool $discoveringSymbolsTip, + ) + { + } + public function getNodeType(): string { return Node\Expr\ConstFetch::class; @@ -22,11 +28,18 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->hasConstant($node->name)) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Constant %s not found.', + (string) $node->name, + )) + ->identifier('constant.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf( - 'Constant %s not found.', - (string) $node->name, - ))->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; } diff --git a/src/Rules/Constants/DynamicClassConstantFetchRule.php b/src/Rules/Constants/DynamicClassConstantFetchRule.php new file mode 100644 index 0000000000..40588a1f56 --- /dev/null +++ b/src/Rules/Constants/DynamicClassConstantFetchRule.php @@ -0,0 +1,69 @@ + + */ +final class DynamicClassConstantFetchRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion, private RuleLevelHelper $ruleLevelHelper) + { + } + + public function getNodeType(): string + { + return ClassConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Expr) { + return []; + } + + if (!$this->phpVersion->supportsDynamicClassConstantFetch()) { + return [ + RuleErrorBuilder::message('Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.') + ->identifier('classConstant.dynamicFetch') + ->nonIgnorable() + ->build(), + ]; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->name, + '', + static fn (Type $type): bool => $type->isString()->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return []; + } + if ($type->isString()->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Class constant name in dynamic fetch can only be a string, %s given.', + $type->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.nameType')->build(), + ]; + } + +} diff --git a/src/Rules/Constants/FinalConstantRule.php b/src/Rules/Constants/FinalConstantRule.php index 23fa67ab65..58b13d04e3 100644 --- a/src/Rules/Constants/FinalConstantRule.php +++ b/src/Rules/Constants/FinalConstantRule.php @@ -10,7 +10,7 @@ use PHPStan\Rules\RuleErrorBuilder; /** @implements Rule */ -class FinalConstantRule implements Rule +final class FinalConstantRule implements Rule { public function __construct(private PhpVersion $phpVersion) @@ -33,7 +33,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Final class constants are supported only on PHP 8.1 and later.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Final class constants are supported only on PHP 8.1 and later.') + ->identifier('classConstant.finalNotSupported') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Constants/FinalPrivateConstantRule.php b/src/Rules/Constants/FinalPrivateConstantRule.php new file mode 100644 index 0000000000..2be7d51165 --- /dev/null +++ b/src/Rules/Constants/FinalPrivateConstantRule.php @@ -0,0 +1,49 @@ + */ +final class FinalPrivateConstantRule implements Rule +{ + + public function getNodeType(): string + { + return ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + $classReflection = $scope->getClassReflection(); + + if (!$node->isFinal()) { + return []; + } + + if (!$node->isPrivate()) { + return []; + } + + $errors = []; + foreach ($node->consts as $classConstNode) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private constant %s::%s() cannot be final as it is never overridden by other classes.', + $classReflection->getDisplayName(), + $classConstNode->name->name, + ))->identifier('classConstant.finalPrivate')->nonIgnorable()->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php b/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php index 895fb96360..e91391e8bb 100644 --- a/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php +++ b/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php @@ -4,7 +4,7 @@ use PHPStan\DependencyInjection\Container; -class LazyAlwaysUsedClassConstantsExtensionProvider implements AlwaysUsedClassConstantsExtensionProvider +final class LazyAlwaysUsedClassConstantsExtensionProvider implements AlwaysUsedClassConstantsExtensionProvider { /** @var AlwaysUsedClassConstantsExtension[]|null */ diff --git a/src/Rules/Constants/MagicConstantContextRule.php b/src/Rules/Constants/MagicConstantContextRule.php new file mode 100644 index 0000000000..2de89b7f65 --- /dev/null +++ b/src/Rules/Constants/MagicConstantContextRule.php @@ -0,0 +1,75 @@ + */ +final class MagicConstantContextRule implements Rule +{ + + public function getNodeType(): string + { + return MagicConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // test cases https://3v4l.org/ZUvvr + + if ($node instanceof MagicConst\Class_) { + if ($scope->isInClass()) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty outside a class.', $node->getName()), + )->identifier('magicConstant.outOfClass')->build(), + ]; + } elseif ($node instanceof MagicConst\Trait_) { + if ($scope->isInTrait()) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty outside a trait.', $node->getName()), + )->identifier('magicConstant.outOfTrait')->build(), + ]; + } elseif ($node instanceof MagicConst\Method || $node instanceof MagicConst\Function_) { + if ($scope->getFunctionName() !== null) { + return []; + } + if ($scope->isInAnonymousFunction()) { + return []; + } + + if ((bool) $node->getAttribute(MagicConstantParamDefaultVisitor::ATTRIBUTE_NAME)) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty outside a function.', $node->getName()), + )->identifier('magicConstant.outOfFunction')->build(), + ]; + } elseif ($node instanceof MagicConst\Namespace_) { + if ($scope->getNamespace() === null) { + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty in global namespace.', $node->getName()), + )->identifier('magicConstant.outOfNamespace')->build(), + ]; + } + } + return []; + } + +} diff --git a/src/Rules/Constants/MissingClassConstantTypehintRule.php b/src/Rules/Constants/MissingClassConstantTypehintRule.php index 25f536e43b..e0cbaa844c 100644 --- a/src/Rules/Constants/MissingClassConstantTypehintRule.php +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -5,14 +5,13 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; /** @@ -46,12 +45,15 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processSingleConstant(ClassReflection $classReflection, string $constantName): array { $constantReflection = $classReflection->getConstant($constantName); - $constantType = $constantReflection->getValueType(); + $constantType = $constantReflection->getPhpDocType(); + if ($constantType === null) { + return []; + } $errors = []; foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($constantType) as $iterableType) { @@ -61,7 +63,10 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $iterableTypeDescription, - ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($constantType) as [$name, $genericTypeNames]) { @@ -70,8 +75,10 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $name, - implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($constantType) as $callableType) { @@ -80,7 +87,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $errors; diff --git a/src/Rules/Constants/NativeTypedClassConstantRule.php b/src/Rules/Constants/NativeTypedClassConstantRule.php new file mode 100644 index 0000000000..ad16fe919b --- /dev/null +++ b/src/Rules/Constants/NativeTypedClassConstantRule.php @@ -0,0 +1,44 @@ + + */ +final class NativeTypedClassConstantRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->type === null) { + return []; + } + + if ($this->phpVersion->supportsNativeTypesInClassConstants()) { + return []; + } + + return [ + RuleErrorBuilder::message('Class constants with native types are supported only on PHP 8.3 and later.') + ->identifier('classConstant.nativeTypeNotSupported') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Constants/OverridingConstantRule.php b/src/Rules/Constants/OverridingConstantRule.php index ca60d2c9b8..2b3fa162d1 100644 --- a/src/Rules/Constants/OverridingConstantRule.php +++ b/src/Rules/Constants/OverridingConstantRule.php @@ -6,9 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; @@ -18,7 +17,7 @@ /** * @implements Rule */ -class OverridingConstantRule implements Rule +final class OverridingConstantRule implements Rule { public function __construct( @@ -48,20 +47,16 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processSingleConstant(ClassReflection $classReflection, string $constantName): array { $prototype = $this->findPrototype($classReflection, $constantName); - if (!$prototype instanceof ClassConstantReflection) { + if ($prototype === null) { return []; } $constantReflection = $classReflection->getConstant($constantName); - if (!$constantReflection instanceof ClassConstantReflection) { - return []; - } - $errors = []; if ($prototype->isFinal()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -70,7 +65,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getName(), $prototype->getDeclaringClass()->getDisplayName(), $prototype->getName(), - ))->nonIgnorable()->build(); + ))->identifier('classConstant.final')->nonIgnorable()->build(); } if ($prototype->isPublic()) { @@ -82,7 +77,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getName(), $prototype->getDeclaringClass()->getDisplayName(), $prototype->getName(), - ))->nonIgnorable()->build(); + ))->identifier('classConstant.visibility')->nonIgnorable()->build(); } } elseif ($constantReflection->isPrivate()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -91,13 +86,41 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getName(), $prototype->getDeclaringClass()->getDisplayName(), $prototype->getName(), - ))->nonIgnorable()->build(); + ))->identifier('classConstant.visibility')->nonIgnorable()->build(); } if (!$this->checkPhpDocMethodSignatures) { return $errors; } + $prototypeNativeType = $prototype->getNativeType(); + $constantNativeType = $constantReflection->getNativeType(); + if ($prototypeNativeType !== null) { + if ($constantNativeType !== null) { + if (!$prototypeNativeType->isSuperTypeOf($constantNativeType)->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Native type %s of constant %s::%s is not covariant with native type %s of constant %s::%s.', + $constantNativeType->describe(VerbosityLevel::typeOnly()), + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototypeNativeType->describe(VerbosityLevel::typeOnly()), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->identifier('classConstant.nativeType')->nonIgnorable()->build(); + } + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s overriding constant %s::%s (%s) should also have native type %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + $prototypeNativeType->describe(VerbosityLevel::typeOnly()), + $prototypeNativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.missingNativeType')->nonIgnorable()->build(); + } + } + if (!$prototype->hasPhpDocType()) { return $errors; } @@ -115,13 +138,13 @@ private function processSingleConstant(ClassReflection $classReflection, string $prototype->getValueType()->describe(VerbosityLevel::value()), $prototype->getDeclaringClass()->getDisplayName(), $prototype->getName(), - ))->build(); + ))->identifier('classConstant.type')->build(); } return $errors; } - private function findPrototype(ClassReflection $classReflection, string $constantName): ?ConstantReflection + private function findPrototype(ClassReflection $classReflection, string $constantName): ?ClassConstantReflection { foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { if ($immediateInterface->hasConstant($constantName)) { diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php new file mode 100644 index 0000000000..728b5a039f --- /dev/null +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -0,0 +1,128 @@ + + */ +final class ValueAssignedToClassConstantRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $nativeType = null; + if ($node->type !== null) { + $nativeType = ParserNodeTypeToPHPStanType::resolve($node->type, $scope->getClassReflection()); + } + + $errors = []; + foreach ($node->consts as $const) { + $constantName = $const->name->toString(); + $errors = array_merge($errors, $this->processSingleConstant( + $scope->getClassReflection(), + $constantName, + $scope->getType($const->value), + $nativeType, + )); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleConstant(ClassReflection $classReflection, string $constantName, Type $valueExprType, ?Type $nativeType): array + { + $constantReflection = $classReflection->getConstant($constantName); + $phpDocType = $constantReflection->getPhpDocType(); + if ($phpDocType === null) { + if ($nativeType === null) { + return []; + } + + $accepts = $nativeType->accepts($valueExprType, true); + if ($accepts->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) does not accept value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $nativeType->describe(VerbosityLevel::typeOnly()), + $valueExprType->describe(VerbosityLevel::value()), + ))->acceptsReasonsTip($accepts->reasons)->nonIgnorable()->identifier('classConstant.value')->build(), + ]; + } elseif ($nativeType === null) { + $isSuperType = $phpDocType->isSuperTypeOf($valueExprType); + $verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $valueExprType); + if ($isSuperType->no()) { + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var for constant %s::%s with type %s is incompatible with value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $phpDocType->describe($verbosity), + $valueExprType->describe(VerbosityLevel::value()), + ))->identifier('classConstant.phpDocType')->build(), + ]; + + } elseif ($isSuperType->maybe()) { + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var for constant %s::%s with type %s is not subtype of value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $phpDocType->describe($verbosity), + $valueExprType->describe(VerbosityLevel::value()), + ))->identifier('classConstant.phpDocType')->build(), + ]; + } + + return []; + } + + $type = $constantReflection->getValueType(); + $accepts = $type->accepts($valueExprType, true); + if ($accepts->yes()) { + return []; + } + + $verbosity = VerbosityLevel::getRecommendedLevelByType($type, $valueExprType); + + return [ + RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) does not accept value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $type->describe(VerbosityLevel::typeOnly()), + $valueExprType->describe($verbosity), + ))->acceptsReasonsTip($accepts->reasons)->identifier('classConstant.value')->build(), + ]; + } + +} diff --git a/src/Rules/DateTimeInstantiationRule.php b/src/Rules/DateTimeInstantiationRule.php index 3b09ccef61..3e62e4b68b 100644 --- a/src/Rules/DateTimeInstantiationRule.php +++ b/src/Rules/DateTimeInstantiationRule.php @@ -6,7 +6,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; -use PHPStan\Type\Constant\ConstantStringType; use Throwable; use function count; use function in_array; @@ -16,7 +15,7 @@ /** * @implements Rule */ -class DateTimeInstantiationRule implements Rule +final class DateTimeInstantiationRule implements Rule { public function getNodeType(): string @@ -29,35 +28,40 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if ( - !($node->class instanceof Node\Name) - || count($node->getArgs()) === 0 - || !in_array(strtolower((string) $node->class), ['datetime', 'datetimeimmutable'], true) - ) { + if (!$node->class instanceof Node\Name) { return []; } - $arg = $scope->getType($node->getArgs()[0]->value); - if (!($arg instanceof ConstantStringType)) { + $lowerClassName = strtolower((string) $node->class); + if ( + count($node->getArgs()) === 0 + || !in_array($lowerClassName, ['datetime', 'datetimeimmutable'], true) + ) { return []; } + $arg = $scope->getType($node->getArgs()[0]->value); $errors = []; - $dateString = $arg->getValue(); - try { - new DateTime($dateString); - } catch (Throwable) { - // an exception is thrown for errors only but we want to catch warnings too - } - $lastErrors = DateTime::getLastErrors(); - if ($lastErrors !== false) { + + foreach ($arg->getConstantStrings() as $constantString) { + $dateString = $constantString->getValue(); + try { + new DateTime($dateString); + } catch (Throwable) { + // an exception is thrown for errors only but we want to catch warnings too + } + $lastErrors = DateTime::getLastErrors(); + if ($lastErrors === false) { + continue; + } + foreach ($lastErrors['errors'] as $error) { $errors[] = RuleErrorBuilder::message(sprintf( 'Instantiating %s with %s produces an error: %s', - (string) $node->class, + $lowerClassName === 'datetime' ? 'DateTime' : 'DateTimeImmutable', $dateString, $error, - ))->build(); + ))->identifier(sprintf('new.%s', $lowerClassName === 'datetime' ? 'dateTime' : 'dateTimeImmutable'))->build(); } } diff --git a/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..aa0bf2ec0b --- /dev/null +++ b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php @@ -0,0 +1,54 @@ + + */ +final class CallToConstructorStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classesWithConstructors = []; + foreach ($node->get(ConstructorWithoutImpurePointsCollector::class) as [$class]) { + $classesWithConstructors[strtolower($class)] = $class; + } + + $errors = []; + foreach ($node->get(PossiblyPureNewCollector::class) as $filePath => $data) { + foreach ($data as [$class, $line]) { + $lowerClass = strtolower($class); + if (!array_key_exists($lowerClass, $classesWithConstructors)) { + continue; + } + + $originalClassName = $classesWithConstructors[$lowerClass]; + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to new %s() on a separate line has no effect.', + $originalClassName, + ))->file($filePath) + ->line($line) + ->identifier('new.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..3b123b3e4d --- /dev/null +++ b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php @@ -0,0 +1,54 @@ + + */ +final class CallToFunctionStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $functions = []; + foreach ($node->get(FunctionWithoutImpurePointsCollector::class) as [$functionName]) { + $functions[strtolower($functionName)] = $functionName; + } + + $errors = []; + foreach ($node->get(PossiblyPureFuncCallCollector::class) as $filePath => $data) { + foreach ($data as [$func, $line]) { + $lowerFunc = strtolower($func); + if (!array_key_exists($lowerFunc, $functions)) { + continue; + } + + $originalFunctionName = $functions[$lowerFunc]; + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to function %s() on a separate line has no effect.', + $originalFunctionName, + ))->file($filePath) + ->line($line) + ->identifier('function.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..443b7dcc10 --- /dev/null +++ b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php @@ -0,0 +1,71 @@ + + */ +final class CallToMethodStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methods = []; + foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$className, $methodName, $classDisplayName]) { + $className = strtolower($className); + + if (!array_key_exists($className, $methods)) { + $methods[$className] = []; + } + $methods[$className][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + } + } + + $errors = []; + foreach ($node->get(PossiblyPureMethodCallCollector::class) as $filePath => $data) { + foreach ($data as [$classNames, $method, $line]) { + $originalMethodName = null; + foreach ($classNames as $className) { + $className = strtolower($className); + + if (!array_key_exists($className, $methods)) { + continue 2; + } + + $lowerMethod = strtolower($method); + if (!array_key_exists($lowerMethod, $methods[$className])) { + continue 2; + } + + $originalMethodName = $methods[$className][$lowerMethod]; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to method %s() on a separate line has no effect.', + $originalMethodName, + ))->file($filePath) + ->line($line) + ->identifier('method.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..83d7842813 --- /dev/null +++ b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php @@ -0,0 +1,68 @@ + + */ +final class CallToStaticMethodStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methods = []; + foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$className, $methodName, $classDisplayName]) { + $lowerClassName = strtolower($className); + + if (!array_key_exists($lowerClassName, $methods)) { + $methods[$lowerClassName] = []; + } + $methods[$lowerClassName][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + } + } + + $errors = []; + foreach ($node->get(PossiblyPureStaticCallCollector::class) as $filePath => $data) { + foreach ($data as [$className, $method, $line]) { + $lowerClassName = strtolower($className); + + if (!array_key_exists($lowerClassName, $methods)) { + continue; + } + + $lowerMethod = strtolower($method); + if (!array_key_exists($lowerMethod, $methods[$lowerClassName])) { + continue; + } + + $originalMethodName = $methods[$lowerClassName][$lowerMethod]; + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s() on a separate line has no effect.', + $originalMethodName, + ))->file($filePath) + ->line($line) + ->identifier('staticMethod.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..47b2feb4df --- /dev/null +++ b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php @@ -0,0 +1,56 @@ + + */ +final class ConstructorWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $method = $node->getMethodReflection(); + if (!$method->isConstructor()) { + return null; + } + + if (!$method->isPure()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + foreach ($method->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($method->getAsserts()->getAll()) !== 0) { + return null; + } + + return $method->getDeclaringClass()->getName(); + } + +} diff --git a/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..7283f333d3 --- /dev/null +++ b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php @@ -0,0 +1,55 @@ + + */ +final class FunctionWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $function = $node->getFunctionReflection(); + if (!$function->isPure()->maybe()) { + return null; + } + if (!$function->hasSideEffects()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + foreach ($function->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($function->getAsserts()->getAll()) !== 0) { + return null; + } + + return $function->getName(); + } + +} diff --git a/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..675d150d52 --- /dev/null +++ b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php @@ -0,0 +1,59 @@ + + */ +final class MethodWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $method = $node->getMethodReflection(); + if (!$method->isPure()->maybe()) { + return null; + } + if (!$method->hasSideEffects()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + foreach ($method->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($method->getAsserts()->getAll()) !== 0) { + return null; + } + + if ($method->isConstructor()) { + return null; + } + + return [$method->getDeclaringClass()->getName(), $method->getName(), $method->getDeclaringClass()->getDisplayName()]; + } + +} diff --git a/src/Rules/DeadCode/NoopRule.php b/src/Rules/DeadCode/NoopRule.php index a910a3276b..abc5200a71 100644 --- a/src/Rules/DeadCode/NoopRule.php +++ b/src/Rules/DeadCode/NoopRule.php @@ -3,64 +3,134 @@ namespace PHPStan\Rules\DeadCode; use PhpParser\Node; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Node\NoopExpressionNode; +use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function preg_split; use function sprintf; /** - * @implements Rule + * @implements Rule */ -class NoopRule implements Rule +final class NoopRule implements Rule { - public function __construct(private Standard $printer) + public function __construct(private ExprPrinter $exprPrinter) { } public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - $originalExpr = $node->expr; - $expr = $originalExpr; + $expr = $node->getOriginalExpr(); + if ($expr instanceof Node\Expr\BinaryOp\LogicalXor) { + return [ + RuleErrorBuilder::message( + 'Unused result of "xor" operator.', + )->line($expr->getStartLine()) + ->tip('This operator has unexpected precedence, try disambiguating the logic with parentheses ().') + ->identifier('logicalXor.resultUnused') + ->build(), + ]; + } + + if ($expr instanceof Node\Expr\BinaryOp\LogicalAnd || $expr instanceof Node\Expr\BinaryOp\LogicalOr) { + $identifierType = $expr instanceof Node\Expr\BinaryOp\LogicalAnd ? 'logicalAnd' : 'logicalOr'; + + return [ + RuleErrorBuilder::message(sprintf( + 'Unused result of "%s" operator.', + $expr->getOperatorSigil(), + ))->line($expr->getStartLine()) + ->tip('This operator has unexpected precedence, try disambiguating the logic with parentheses ().') + ->identifier(sprintf('%s.resultUnused', $identifierType)) + ->build(), + ]; + } + + if ($node->hasAssign()) { + return []; + } + + if ($expr instanceof Node\Expr\BinaryOp\BooleanAnd || $expr instanceof Node\Expr\BinaryOp\BooleanOr) { + $identifierType = $expr instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'booleanOr'; + + return [ + RuleErrorBuilder::message(sprintf( + 'Unused result of "%s" operator.', + $expr->getOperatorSigil(), + ))->line($expr->getStartLine()) + ->identifier(sprintf('%s.resultUnused', $identifierType)) + ->build(), + ]; + } + + if ($expr instanceof Node\Expr\Ternary) { + return [ + RuleErrorBuilder::message('Unused result of ternary operator.') + ->line($expr->getStartLine()) + ->identifier('ternary.resultUnused') + ->build(), + ]; + } + + if ($expr instanceof Node\Expr\FuncCall) { + if ($expr->name instanceof Node\Name) { + // handled by CallToFunctionStatementWithoutSideEffectsRule + return []; + } + + $nameType = $scope->getType($expr->name); + if (!$nameType->isCallable()->yes()) { + return []; + } + } + + if ($expr instanceof Node\Expr\New_ && $expr->class instanceof Node\Name) { + // handled by CallToConstructorStatementWithoutSideEffectsRule + return []; + } + if ( - $expr instanceof Node\Expr\Cast - || $expr instanceof Node\Expr\UnaryMinus - || $expr instanceof Node\Expr\UnaryPlus - || $expr instanceof Node\Expr\ErrorSuppress + $expr instanceof Node\Expr\NullsafeMethodCall + || $expr instanceof Node\Expr\MethodCall + || $expr instanceof Node\Expr\StaticCall ) { - $expr = $expr->expr; + // handled by *WithoutSideEffectsRule rules + return []; } + if ( - !$expr instanceof Node\Expr\Variable - && !$expr instanceof Node\Expr\PropertyFetch - && !$expr instanceof Node\Expr\StaticPropertyFetch - && !$expr instanceof Node\Expr\NullsafePropertyFetch - && !$expr instanceof Node\Expr\ArrayDimFetch - && !$expr instanceof Node\Scalar - && !$expr instanceof Node\Expr\Isset_ - && !$expr instanceof Node\Expr\Empty_ - && !$expr instanceof Node\Expr\ConstFetch - && !$expr instanceof Node\Expr\ClassConstFetch + $expr instanceof Node\Expr\Assign + || $expr instanceof Node\Expr\AssignOp + || $expr instanceof Node\Expr\AssignRef ) { return []; } + if ($expr instanceof Node\Expr\Closure) { + return []; + } + + $exprString = $this->exprPrinter->printExpr($expr); + $exprStringLines = preg_split('~\R~', $exprString, 2); + if ($exprStringLines !== false && count($exprStringLines) > 1) { + $exprString = $exprStringLines[0] . '…'; + } + return [ RuleErrorBuilder::message(sprintf( 'Expression "%s" on a separate line does not do anything.', - $this->printer->prettyPrintExpr($originalExpr), - ))->line($expr->getLine()) - ->identifier('deadCode.noopExpression') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - ]) + $exprString, + ))->line($expr->getStartLine()) + ->identifier('expr.resultUnused') ->build(), ]; } diff --git a/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php new file mode 100644 index 0000000000..85c126a00b --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php @@ -0,0 +1,50 @@ + + */ +final class PossiblyPureFuncCallCollector implements Collector +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\FuncCall) { + return null; + } + if (!$node->expr->name instanceof Node\Name) { + return null; + } + + if (!$this->reflectionProvider->hasFunction($node->expr->name, $scope)) { + return null; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->expr->name, $scope); + if (!$functionReflection->isPure()->maybe()) { + return null; + } + if (!$functionReflection->hasSideEffects()->maybe()) { + return null; + } + + return [$functionReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php new file mode 100644 index 0000000000..5869714cf8 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php @@ -0,0 +1,74 @@ +, string, int}> + */ +final class PossiblyPureMethodCallCollector implements Collector +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\MethodCall) { + return null; + } + if (!$node->expr->name instanceof Node\Identifier) { + return null; + } + + $methodName = $node->expr->name->toString(); + $calledOnType = $scope->getType($node->expr->var); + if (!$calledOnType->hasMethod($methodName)->yes()) { + return null; + } + + $classNames = []; + $methodReflection = null; + foreach ($calledOnType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->hasMethod($methodName)) { + return null; + } + + $methodReflection = $classReflection->getMethod($methodName, $scope); + if ( + !$methodReflection->isPrivate() + && !$methodReflection->isFinal()->yes() + && !$methodReflection->getDeclaringClass()->isFinal() + ) { + if (!$classReflection->isFinal()) { + return null; + } + } + if (!$methodReflection->isPure()->maybe()) { + return null; + } + if (!$methodReflection->hasSideEffects()->maybe()) { + return null; + } + + $classNames[] = $methodReflection->getDeclaringClass()->getName(); + } + + if ($methodReflection === null) { + return null; + } + + return [$classNames, $methodReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureNewCollector.php b/src/Rules/DeadCode/PossiblyPureNewCollector.php new file mode 100644 index 0000000000..f76f39c616 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureNewCollector.php @@ -0,0 +1,60 @@ + + */ +final class PossiblyPureNewCollector implements Collector +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\New_) { + return null; + } + + if (!$node->expr->class instanceof Node\Name) { + return null; + } + + $className = $node->expr->class->toString(); + + if (!$this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$classReflection->hasConstructor()) { + return null; + } + + $constructor = $classReflection->getConstructor(); + if (strtolower($constructor->getName()) !== '__construct') { + return null; + } + + if (!$constructor->isPure()->maybe()) { + return null; + } + + return [$constructor->getDeclaringClass()->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php new file mode 100644 index 0000000000..495cdf0248 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php @@ -0,0 +1,55 @@ + + */ +final class PossiblyPureStaticCallCollector implements Collector +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\StaticCall) { + return null; + } + if (!$node->expr->name instanceof Node\Identifier) { + return null; + } + + if (!$node->expr->class instanceof Node\Name) { + return null; + } + + $methodName = $node->expr->name->toString(); + $calledOnType = $scope->resolveTypeByName($node->expr->class); + $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); + + if ($methodReflection === null) { + return null; + } + if (!$methodReflection->isPure()->maybe()) { + return null; + } + if (!$methodReflection->hasSideEffects()->maybe()) { + return null; + } + + return [$methodReflection->getDeclaringClass()->getName(), $methodReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/UnreachableStatementRule.php b/src/Rules/DeadCode/UnreachableStatementRule.php index 8f4793267a..17f166b788 100644 --- a/src/Rules/DeadCode/UnreachableStatementRule.php +++ b/src/Rules/DeadCode/UnreachableStatementRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class UnreachableStatementRule implements Rule +final class UnreachableStatementRule implements Rule { public function getNodeType(): string @@ -21,17 +21,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if ($node->getOriginalStatement() instanceof Node\Stmt\Nop) { - return []; - } - return [ RuleErrorBuilder::message('Unreachable statement - code above always terminates.') - ->identifier('deadCode.unreachableStatement') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - ]) + ->identifier('deadCode.unreachable') ->build(), ]; } diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index c41e3d66eb..2e860a391f 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -8,14 +8,13 @@ use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\ObjectType; use function sprintf; /** * @implements Rule */ -class UnusedPrivateConstantRule implements Rule +final class UnusedPrivateConstantRule implements Rule { public function __construct(private AlwaysUsedClassConstantsExtensionProvider $extensionProvider) @@ -32,11 +31,9 @@ public function processNode(Node $node, Scope $scope): array if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { return []; } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); $constants = []; foreach ($node->getConstants() as $constant) { @@ -60,24 +57,35 @@ public function processNode(Node $node, Scope $scope): array foreach ($node->getFetches() as $fetch) { $fetchNode = $fetch->getNode(); + + $fetchScope = $fetch->getScope(); + if ($fetchNode->class instanceof Node\Name) { + $fetchedOnClass = $fetchScope->resolveTypeByName($fetchNode->class); + } else { + $fetchedOnClass = $fetchScope->getType($fetchNode->class); + } + if (!$fetchNode->name instanceof Node\Identifier) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + $constants = []; + break; + } continue; } - if ($fetchNode->class instanceof Node\Name) { - $fetchScope = $fetch->getScope(); - $fetchedOnClass = $fetchScope->resolveName($fetchNode->class); - if ($fetchedOnClass !== $classReflection->getName()) { - continue; - } - } else { - $classExprType = $fetch->getScope()->getType($fetchNode->class); - if (!$classExprType instanceof TypeWithClassName) { - continue; + $constantReflection = $fetchScope->getConstantReflection($fetchedOnClass, $fetchNode->name->toString()); + if ($constantReflection === null) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + unset($constants[$fetchNode->name->toString()]); } - if ($classExprType->getClassName() !== $classReflection->getName()) { - continue; + continue; + } + + if ($constantReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + unset($constants[$fetchNode->name->toString()]); } + continue; } unset($constants[$fetchNode->name->toString()]); @@ -86,14 +94,8 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($constants as $constantName => $constantNode) { $errors[] = RuleErrorBuilder::message(sprintf('Constant %s::%s is unused.', $classReflection->getDisplayName(), $constantName)) - ->line($constantNode->getLine()) - ->identifier('deadCode.unusedClassConstant') - ->metadata([ - 'classOrder' => $node->getClass()->getAttribute('statementOrder'), - 'classDepth' => $node->getClass()->getAttribute('statementDepth'), - 'classStartLine' => $node->getClass()->getStartLine(), - 'constantName' => $constantName, - ]) + ->line($constantNode->getStartLine()) + ->identifier('classConstant.unused') ->tip(sprintf('See: %s', 'https://phpstan.org/developing-extensions/always-used-class-constants')) ->build(); } diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index b694ebf9de..e5b561af65 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -7,14 +7,11 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\ClassMethodsNode; use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Methods\AlwaysUsedMethodExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeUtils; use function array_map; use function count; use function sprintf; @@ -23,9 +20,13 @@ /** * @implements Rule */ -class UnusedPrivateMethodRule implements Rule +final class UnusedPrivateMethodRule implements Rule { + public function __construct(private AlwaysUsedMethodExtensionProvider $extensionProvider) + { + } + public function getNodeType(): string { return ClassMethodsNode::class; @@ -36,29 +37,37 @@ public function processNode(Node $node, Scope $scope): array if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { return []; } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); $constructor = null; if ($classReflection->hasConstructor()) { $constructor = $classReflection->getConstructor(); } - $classType = new ObjectType($classReflection->getName()); $methods = []; foreach ($node->getMethods() as $method) { - if (!$method->isPrivate()) { + if (!$method->getNode()->isPrivate()) { + continue; + } + if ($method->isDeclaredInTrait()) { continue; } - $methodName = $method->name->toString(); + $methodName = $method->getNode()->name->toString(); if ($constructor !== null && $constructor->getName() === $methodName) { continue; } if (strtolower($methodName) === '__clone') { continue; } - $methods[$method->name->toString()] = $method; + + $methodReflection = $classReflection->getNativeMethod($methodName); + foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysUsed($methodReflection)) { + continue 2; + } + } + + $methods[strtolower($methodName)] = $method; } $arrayCalls = []; @@ -73,9 +82,18 @@ public function processNode(Node $node, Scope $scope): array $methodNames = [$methodCallNode->name->toString()]; } else { $methodNameType = $callScope->getType($methodCallNode->name); - $strings = TypeUtils::getConstantStrings($methodNameType); + $strings = $methodNameType->getConstantStrings(); if (count($strings) === 0) { - return []; + // handle subtractions of a dynamic method call + foreach ($methods as $lowerMethodName => $method) { + if ((new ConstantStringType($method->getNode()->name->toString()))->isSuperTypeOf($methodNameType)->no()) { + continue; + } + + unset($methods[$lowerMethodName]); + } + + continue; } $methodNames = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $strings); @@ -84,27 +102,36 @@ public function processNode(Node $node, Scope $scope): array if ($methodCallNode instanceof Node\Expr\MethodCall) { $calledOnType = $callScope->getType($methodCallNode->var); } else { - if (!$methodCallNode->class instanceof Node\Name) { - continue; + if ($methodCallNode->class instanceof Node\Name) { + $calledOnType = $callScope->resolveTypeByName($methodCallNode->class); + } else { + $calledOnType = $callScope->getType($methodCallNode->class); } - $calledOnType = $scope->resolveTypeByName($methodCallNode->class); - } - if ($classType->isSuperTypeOf($calledOnType)->no()) { - continue; - } - if ($calledOnType instanceof MixedType) { - continue; } + $inMethod = $callScope->getFunction(); if (!$inMethod instanceof MethodReflection) { continue; } foreach ($methodNames as $methodName) { + $methodReflection = $callScope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection === null) { + if (!$classType->isSuperTypeOf($calledOnType)->no()) { + unset($methods[strtolower($methodName)]); + } + continue; + } + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$classType->isSuperTypeOf($calledOnType)->no()) { + unset($methods[strtolower($methodName)]); + } + continue; + } if ($inMethod->getName() === $methodName) { continue; } - unset($methods[$methodName]); + unset($methods[strtolower($methodName)]); } } @@ -113,53 +140,52 @@ public function processNode(Node $node, Scope $scope): array /** @var Node\Expr\Array_ $array */ $array = $arrayCall->getNode(); $arrayScope = $arrayCall->getScope(); - $arrayType = $scope->getType($array); - if (!$arrayType instanceof ConstantArrayType) { - continue; - } - $typeAndMethod = $arrayType->findTypeAndMethodName(); - if ($typeAndMethod === null) { - continue; - } - if ($typeAndMethod->isUnknown()) { - return []; - } - if (!$typeAndMethod->getCertainty()->yes()) { - return []; - } - $calledOnType = $typeAndMethod->getType(); - if ($classType->isSuperTypeOf($calledOnType)->no()) { - continue; - } - if ($calledOnType instanceof MixedType) { - continue; - } - $inMethod = $arrayScope->getFunction(); - if (!$inMethod instanceof MethodReflection) { + $arrayType = $arrayScope->getType($array); + if (!$arrayType->isCallable()->yes()) { continue; } - if ($inMethod->getName() === $typeAndMethod->getMethod()) { - continue; + foreach ($arrayType->getConstantArrays() as $constantArray) { + foreach ($constantArray->findTypeAndMethodNames() as $typeAndMethod) { + if ($typeAndMethod->isUnknown()) { + return []; + } + if (!$typeAndMethod->getCertainty()->yes()) { + return []; + } + + $calledOnType = $typeAndMethod->getType(); + $methodReflection = $arrayScope->getMethodReflection($calledOnType, $typeAndMethod->getMethod()); + if ($methodReflection === null) { + continue; + } + + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } + + $inMethod = $arrayScope->getFunction(); + if (!$inMethod instanceof MethodReflection) { + continue; + } + if ($inMethod->getName() === $typeAndMethod->getMethod()) { + continue; + } + unset($methods[strtolower($typeAndMethod->getMethod())]); + } } - unset($methods[$typeAndMethod->getMethod()]); } } $errors = []; - foreach ($methods as $methodName => $methodNode) { + foreach ($methods as $method) { + $originalMethodName = $method->getNode()->name->toString(); $methodType = 'Method'; - if ($methodNode->isStatic()) { + if ($method->getNode()->isStatic()) { $methodType = 'Static method'; } - $errors[] = RuleErrorBuilder::message(sprintf('%s %s::%s() is unused.', $methodType, $classReflection->getDisplayName(), $methodName)) - ->line($methodNode->getLine()) - ->identifier('deadCode.unusedMethod') - ->metadata([ - 'classOrder' => $node->getClass()->getAttribute('statementOrder'), - 'classDepth' => $node->getClass()->getAttribute('statementDepth'), - 'classStartLine' => $node->getClass()->getStartLine(), - 'methodName' => $methodName, - ]) + $errors[] = RuleErrorBuilder::message(sprintf('%s %s::%s() is unused.', $methodType, $classReflection->getDisplayName(), $originalMethodName)) + ->line($method->getNode()->getStartLine()) + ->identifier('method.unused') ->build(); } diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index add56a0339..239e732056 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -6,24 +6,24 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertiesNode; use PHPStan\Node\Property\PropertyRead; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; 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 is_string; +use function lcfirst; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule */ -class UnusedPrivatePropertyRule implements Rule +final class UnusedPrivatePropertyRule implements Rule { /** @@ -49,24 +49,23 @@ public function processNode(Node $node, Scope $scope): array if (!$node->getClass() instanceof Node\Stmt\Class_) { return []; } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); - $classType = new ObjectType($classReflection->getName()); - + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); $properties = []; foreach ($node->getProperties() as $property) { if (!$property->isPrivate()) { continue; } + if ($property->isDeclaredInTrait()) { + continue; + } - $alwaysRead = false; - $alwaysWritten = false; + $alwaysRead = !$property->isReadable(); + $alwaysWritten = !$property->isWritable(); if ($property->getPhpDoc() !== null) { $text = $property->getPhpDoc(); foreach ($this->alwaysReadTags as $tag) { - if (strpos($text, $tag) === false) { + if (!str_contains($text, $tag)) { continue; } @@ -75,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array } foreach ($this->alwaysWrittenTags as $tag) { - if (strpos($text, $tag) === false) { + if (!str_contains($text, $tag)) { continue; } @@ -113,43 +112,92 @@ public function processNode(Node $node, Scope $scope): array 'read' => $read, 'written' => $written, 'node' => $property, + 'onlyReadable' => $property->isReadable() && !$property->isWritable(), + 'onlyWritable' => $property->isWritable() && !$property->isReadable(), ]; } foreach ($node->getPropertyUsages() as $usage) { + $usageScope = $usage->getScope(); $fetch = $usage->getFetch(); if ($fetch->name instanceof Node\Identifier) { - $propertyNames = [$fetch->name->toString()]; + $propertyName = $fetch->name->toString(); + $propertyNames = [$propertyName]; + if ( + $usageScope->getFunction() !== null + && $fetch instanceof Node\Expr\PropertyFetch + && $fetch->var instanceof Node\Expr\Variable + && is_string($fetch->var->name) + && $fetch->var->name === 'this' + ) { + $methodReflection = $usageScope->getFunction(); + if ( + $methodReflection instanceof PhpMethodFromParserNodeReflection + && $methodReflection->isPropertyHook() + && $methodReflection->getHookedPropertyName() === $propertyName + && ( + $methodReflection->getPropertyHookName() === 'set' + || $usage instanceof PropertyRead + ) + ) { + continue; + } + } } else { - $propertyNameType = $usage->getScope()->getType($fetch->name); - $strings = TypeUtils::getConstantStrings($propertyNameType); + $propertyNameType = $usageScope->getType($fetch->name); + $strings = $propertyNameType->getConstantStrings(); if (count($strings) === 0) { - return []; + // handle subtractions of a dynamic property fetch + foreach ($properties as $propertyName => $data) { + if ((new ConstantStringType($propertyName))->isSuperTypeOf($propertyNameType)->no()) { + continue; + } + + unset($properties[$propertyName]); + } + + continue; } $propertyNames = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $strings); } + if ($fetch instanceof Node\Expr\PropertyFetch) { - $fetchedOnType = $usage->getScope()->getType($fetch->var); + $fetchedOnType = $usageScope->getType($fetch->var); } else { - if (!$fetch->class instanceof Node\Name) { - continue; + if ($fetch->class instanceof Node\Name) { + $fetchedOnType = $usageScope->resolveTypeByName($fetch->class); + } else { + $fetchedOnType = $usageScope->getType($fetch->class); } - - $fetchedOnType = $usage->getScope()->resolveTypeByName($fetch->class); - } - - if ($classType->isSuperTypeOf($fetchedOnType)->no()) { - continue; - } - if ($fetchedOnType instanceof MixedType) { - continue; } foreach ($propertyNames as $propertyName) { if (!array_key_exists($propertyName, $properties)) { continue; } + $propertyReflection = $usageScope->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { + if (!$classType->isSuperTypeOf($fetchedOnType)->no()) { + if ($usage instanceof PropertyRead) { + $properties[$propertyName]['read'] = true; + } else { + $properties[$propertyName]['written'] = true; + } + } + continue; + } + if ($propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$classType->isSuperTypeOf($fetchedOnType)->no()) { + if ($usage instanceof PropertyRead) { + $properties[$propertyName]['read'] = true; + } else { + $properties[$propertyName]['written'] = true; + } + } + continue; + } + if ($usage instanceof PropertyRead) { $properties[$propertyName]['read'] = true; } else { @@ -158,41 +206,51 @@ public function processNode(Node $node, Scope $scope): array } } - $constructors = []; - $classReflection = $scope->getClassReflection(); - if ($classReflection->hasConstructor()) { - $constructors[] = $classReflection->getConstructor()->getName(); - } - - [$uninitializedProperties] = $node->getUninitializedProperties($scope, $constructors, $this->extensionProvider->getExtensions()); + [$uninitializedProperties] = $node->getUninitializedProperties($scope, []); $errors = []; foreach ($properties as $name => $data) { $propertyNode = $data['node']; if ($propertyNode->isStatic()) { - $propertyName = sprintf('Static property %s::$%s', $scope->getClassReflection()->getDisplayName(), $name); + $propertyName = sprintf('Static property %s::$%s', $classReflection->getDisplayName(), $name); } else { - $propertyName = sprintf('Property %s::$%s', $scope->getClassReflection()->getDisplayName(), $name); + $propertyName = sprintf('Property %s::$%s', $classReflection->getDisplayName(), $name); } $tip = sprintf('See: %s', 'https://phpstan.org/developing-extensions/always-read-written-properties'); if (!$data['read']) { if (!$data['written']) { $errors[] = RuleErrorBuilder::message(sprintf('%s is unused.', $propertyName)) ->line($propertyNode->getStartLine()) - ->identifier('deadCode.unusedProperty') - ->metadata([ - 'classOrder' => $node->getClass()->getAttribute('statementOrder'), - 'classDepth' => $node->getClass()->getAttribute('statementDepth'), - 'classStartLine' => $node->getClass()->getStartLine(), - 'propertyName' => $name, - ]) ->tip($tip) + ->identifier('property.unused') ->build(); } else { - $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName))->line($propertyNode->getStartLine())->tip($tip)->build(); + if ($data['onlyReadable']) { + $errors[] = RuleErrorBuilder::message(sprintf('Readable %s is never read.', lcfirst($propertyName))) + ->line($propertyNode->getStartLine()) + ->identifier('property.neverRead') + ->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName)) + ->line($propertyNode->getStartLine()) + ->identifier('property.onlyWritten') + ->tip($tip) + ->build(); + } } } elseif (!$data['written'] && (!array_key_exists($name, $uninitializedProperties) || !$this->checkUninitializedProperties)) { - $errors[] = RuleErrorBuilder::message(sprintf('%s is never written, only read.', $propertyName))->line($propertyNode->getStartLine())->tip($tip)->build(); + if ($data['onlyWritable']) { + $errors[] = RuleErrorBuilder::message(sprintf('Writable %s is never written.', lcfirst($propertyName))) + ->line($propertyNode->getStartLine()) + ->identifier('property.neverWritten') + ->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf('%s is never written, only read.', $propertyName)) + ->line($propertyNode->getStartLine()) + ->identifier('property.onlyRead') + ->tip($tip) + ->build(); + } } } diff --git a/src/Rules/Debug/DebugScopeRule.php b/src/Rules/Debug/DebugScopeRule.php new file mode 100644 index 0000000000..7f6a43930a --- /dev/null +++ b/src/Rules/Debug/DebugScopeRule.php @@ -0,0 +1,66 @@ + + */ +final class DebugScopeRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Expr\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 ($functionName === null) { + return []; + } + + if (strtolower($functionName) !== 'phpstan\debugscope') { + return []; + } + + if (!$scope instanceof MutatingScope) { + return []; + } + + $parts = []; + foreach ($scope->debug() as $key => $row) { + $parts[] = sprintf('%s: %s', $key, $row); + } + + if (count($parts) === 0) { + $parts[] = 'Scope is empty'; + } + + return [ + RuleErrorBuilder::message( + implode("\n", $parts), + )->nonIgnorable()->identifier('phpstan.debugScope')->build(), + ]; + } + +} diff --git a/src/Rules/Debug/DumpPhpDocTypeRule.php b/src/Rules/Debug/DumpPhpDocTypeRule.php new file mode 100644 index 0000000000..be867366b0 --- /dev/null +++ b/src/Rules/Debug/DumpPhpDocTypeRule.php @@ -0,0 +1,59 @@ + + */ +final class DumpPhpDocTypeRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider, private Printer $printer) + { + } + + public function getNodeType(): string + { + return Node\Expr\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 ($functionName === null) { + return []; + } + + if (strtolower($functionName) !== 'phpstan\dumpphpdoctype') { + return []; + } + + if (count($node->getArgs()) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf( + 'Dumped type: %s', + $this->printer->print($scope->getType($node->getArgs()[0]->value)->toPhpDocNode()), + ), + )->nonIgnorable()->identifier('phpstan.dumpPhpDocType')->build(), + ]; + } + +} diff --git a/src/Rules/Debug/DumpTypeRule.php b/src/Rules/Debug/DumpTypeRule.php index ceeeefa0e5..f7d2a6dcb4 100644 --- a/src/Rules/Debug/DumpTypeRule.php +++ b/src/Rules/Debug/DumpTypeRule.php @@ -15,7 +15,7 @@ /** * @implements Rule */ -class DumpTypeRule implements Rule +final class DumpTypeRule implements Rule { public function __construct(private ReflectionProvider $reflectionProvider) @@ -43,11 +43,7 @@ public function processNode(Node $node, Scope $scope): array } if (count($node->getArgs()) === 0) { - return [ - RuleErrorBuilder::message(sprintf('Missing argument for %s() function call.', $functionName)) - ->nonIgnorable() - ->build(), - ]; + return []; } return [ @@ -56,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array 'Dumped type: %s', $scope->getType($node->getArgs()[0]->value)->describe(VerbosityLevel::precise()), ), - )->nonIgnorable()->build(), + )->nonIgnorable()->identifier('phpstan.dumpType')->build(), ]; } diff --git a/src/Rules/Debug/FileAssertRule.php b/src/Rules/Debug/FileAssertRule.php index d8ebdbfa4a..769f37bd1c 100644 --- a/src/Rules/Debug/FileAssertRule.php +++ b/src/Rules/Debug/FileAssertRule.php @@ -6,11 +6,10 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\VerbosityLevel; use function count; use function is_string; @@ -19,7 +18,7 @@ /** * @implements Rule */ -class FileAssertRule implements Rule +final class FileAssertRule implements Rule { public function __construct(private ReflectionProvider $reflectionProvider) @@ -59,7 +58,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Arg[] $args - * @return RuleError[] + * @return list */ private function processAssertType(array $args, Scope $scope): array { @@ -67,26 +66,32 @@ private function processAssertType(array $args, Scope $scope): array return []; } - $expectedTypeString = $scope->getType($args[0]->value); - if (!$expectedTypeString instanceof ConstantStringType) { + $expectedTypeStrings = $scope->getType($args[0]->value)->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { return [ - RuleErrorBuilder::message('Expected type must be a literal string.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Expected type must be a literal string.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), ]; } $expressionType = $scope->getType($args[1]->value)->describe(VerbosityLevel::precise()); - if ($expectedTypeString->getValue() === $expressionType) { + if ($expectedTypeStrings[0]->getValue() === $expressionType) { return []; } return [ - RuleErrorBuilder::message(sprintf('Expected type %s, actual: %s', $expectedTypeString->getValue(), $expressionType))->nonIgnorable()->build(), + RuleErrorBuilder::message(sprintf('Expected type %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType)) + ->nonIgnorable() + ->identifier('phpstan.type') + ->build(), ]; } /** * @param Node\Arg[] $args - * @return RuleError[] + * @return list */ private function processAssertNativeType(array $args, Scope $scope): array { @@ -94,27 +99,32 @@ private function processAssertNativeType(array $args, Scope $scope): array return []; } - $scope = $scope->doNotTreatPhpDocTypesAsCertain(); - $expectedTypeString = $scope->getNativeType($args[0]->value); - if (!$expectedTypeString instanceof ConstantStringType) { + $expectedTypeStrings = $scope->getNativeType($args[0]->value)->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { return [ - RuleErrorBuilder::message('Expected native type must be a literal string.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Expected native type must be a literal string.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), ]; } $expressionType = $scope->getNativeType($args[1]->value)->describe(VerbosityLevel::precise()); - if ($expectedTypeString->getValue() === $expressionType) { + if ($expectedTypeStrings[0]->getValue() === $expressionType) { return []; } return [ - RuleErrorBuilder::message(sprintf('Expected native type %s, actual: %s', $expectedTypeString->getValue(), $expressionType))->nonIgnorable()->build(), + RuleErrorBuilder::message(sprintf('Expected native type %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType)) + ->nonIgnorable() + ->identifier('phpstan.nativeType') + ->build(), ]; } /** * @param Node\Arg[] $args - * @return RuleError[] + * @return list */ private function processAssertVariableCertainty(array $args, Scope $scope): array { @@ -127,6 +137,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('First argument of %s() must be TrinaryLogic call') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -134,6 +145,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid TrinaryLogic call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -142,6 +154,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid TrinaryLogic call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -150,35 +163,39 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid TrinaryLogic call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } - // @phpstan-ignore-next-line + // @phpstan-ignore staticMethod.dynamicName $expectedCertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); $variable = $args[1]->value; - if (!$variable instanceof Node\Expr\Variable) { - return [ - RuleErrorBuilder::message('Invalid assertVariableCertainty call.') - ->nonIgnorable() - ->build(), - ]; - } - if (!is_string($variable->name)) { + if ($variable instanceof Node\Expr\Variable && is_string($variable->name)) { + $actualCertaintyValue = $scope->hasVariableType($variable->name); + $variableDescription = sprintf('variable $%s', $variable->name); + } elseif ($variable instanceof Node\Expr\ArrayDimFetch && $variable->dim !== null) { + $offset = $scope->getType($variable->dim); + $actualCertaintyValue = $scope->getType($variable->var)->hasOffsetValueType($offset); + $variableDescription = sprintf('offset %s', $offset->describe(VerbosityLevel::precise())); + } else { return [ RuleErrorBuilder::message('Invalid assertVariableCertainty call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } - $actualCertaintyValue = $scope->hasVariableType($variable->name); if ($expectedCertaintyValue->equals($actualCertaintyValue)) { return []; } return [ - RuleErrorBuilder::message(sprintf('Expected variable certainty %s, actual: %s', $expectedCertaintyValue->describe(), $actualCertaintyValue->describe()))->nonIgnorable()->build(), + RuleErrorBuilder::message(sprintf('Expected %s certainty %s, actual: %s', $variableDescription, $expectedCertaintyValue->describe(), $actualCertaintyValue->describe())) + ->nonIgnorable() + ->identifier('phpstan.variable') + ->build(), ]; } diff --git a/src/Rules/DirectRegistry.php b/src/Rules/DirectRegistry.php new file mode 100644 index 0000000000..8003bef42a --- /dev/null +++ b/src/Rules/DirectRegistry.php @@ -0,0 +1,56 @@ +rules[$rule->getNodeType()][] = $rule; + } + } + + /** + * @template TNodeType of Node + * @param class-string $nodeType + * @return array> + */ + public function getRules(string $nodeType): array + { + if (!isset($this->cache[$nodeType])) { + $parentNodeTypes = [$nodeType] + class_parents($nodeType) + class_implements($nodeType); + + $rules = []; + foreach ($parentNodeTypes as $parentNodeType) { + foreach ($this->rules[$parentNodeType] ?? [] as $rule) { + $rules[] = $rule; + } + } + + $this->cache[$nodeType] = $rules; + } + + /** + * @var array> $selectedRules + */ + $selectedRules = $this->cache[$nodeType]; + + return $selectedRules; + } + +} diff --git a/src/Rules/EnumCases/EnumCaseAttributesRule.php b/src/Rules/EnumCases/EnumCaseAttributesRule.php index 8d584b72f6..c7e0113764 100644 --- a/src/Rules/EnumCases/EnumCaseAttributesRule.php +++ b/src/Rules/EnumCases/EnumCaseAttributesRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class EnumCaseAttributesRule implements Rule +final class EnumCaseAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) diff --git a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php index 4cbf0a7d92..d7d3b5bc71 100644 --- a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php +++ b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php @@ -14,9 +14,16 @@ /** * @implements Rule */ -class CatchWithUnthrownExceptionRule implements Rule +final class CatchWithUnthrownExceptionRule implements Rule { + public function __construct( + private ExceptionTypeResolver $exceptionTypeResolver, + private bool $reportUncheckedExceptionDeadCatch, + ) + { + } + public function getNodeType(): string { return CatchWithUnthrownExceptionNode::class; @@ -28,14 +35,34 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Dead catch - %s is already caught above.', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly())), - )->line($node->getLine())->build(), + ) + ->line($node->getStartLine()) + ->identifier('catch.alreadyCaught') + ->build(), ]; } + if (!$this->reportUncheckedExceptionDeadCatch) { + $isCheckedException = false; + foreach ($node->getCaughtType()->getObjectClassNames() as $objectClassName) { + if ($this->exceptionTypeResolver->isCheckedException($objectClassName, $scope)) { + $isCheckedException = true; + break; + } + } + + if (!$isCheckedException) { + return []; + } + } + return [ RuleErrorBuilder::message( sprintf('Dead catch - %s is never thrown in the try block.', $node->getCaughtType()->describe(VerbosityLevel::typeOnly())), - )->line($node->getLine())->build(), + ) + ->line($node->getStartLine()) + ->identifier('catch.neverThrown') + ->build(), ]; } diff --git a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php index bbd4b6be44..d873394c20 100644 --- a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php +++ b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php @@ -6,8 +6,9 @@ use PhpParser\Node\Stmt\Catch_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use Throwable; @@ -17,13 +18,14 @@ /** * @implements Rule */ -class CaughtExceptionExistenceRule implements Rule +final class CaughtExceptionExistenceRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkClassCaseSensitivity, + private bool $discoveringSymbolsTip, ) { } @@ -42,22 +44,35 @@ public function processNode(Node $node, Scope $scope): array if ($scope->isInClassExists($className)) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s not found.', $className))->line($class->getLine())->discoveringSymbolsTip()->build(); + + $errorBuilder = RuleErrorBuilder::message(sprintf('Caught class %s not found.', $className)) + ->line($class->getStartLine()) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); continue; } $classReflection = $this->reflectionProvider->getClass($className); if (!$classReflection->isInterface() && !$classReflection->implementsInterface(Throwable::class)) { - $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s is not an exception.', $classReflection->getDisplayName()))->line($class->getLine())->build(); - } - - if (!$this->checkClassCaseSensitivity) { - continue; + $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s is not an exception.', $classReflection->getDisplayName())) + ->line($class->getStartLine()) + ->identifier('catch.notThrowable') + ->build(); } $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]), + $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($className, $class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::EXCEPTION_CATCH), + $this->checkClassCaseSensitivity, + ), ); } diff --git a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php index 8da4705142..f428436b48 100644 --- a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php +++ b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php @@ -7,7 +7,10 @@ use PHPStan\Reflection\ReflectionProvider; use function count; -class DefaultExceptionTypeResolver implements ExceptionTypeResolver +/** + * @api + */ +final class DefaultExceptionTypeResolver implements ExceptionTypeResolver { /** @@ -46,11 +49,7 @@ public function isCheckedException(string $className, Scope $scope): bool $classReflection = $this->reflectionProvider->getClass($className); foreach ($this->uncheckedExceptionClasses as $uncheckedExceptionClass) { - if ($classReflection->getName() === $uncheckedExceptionClass) { - return false; - } - - if (!$classReflection->isSubclassOf($uncheckedExceptionClass)) { + if (!$classReflection->is($uncheckedExceptionClass)) { continue; } @@ -80,11 +79,7 @@ private function isCheckedExceptionInternal(string $className): bool $classReflection = $this->reflectionProvider->getClass($className); foreach ($this->checkedExceptionClasses as $checkedExceptionClass) { - if ($classReflection->getName() === $checkedExceptionClass) { - return true; - } - - if (!$classReflection->isSubclassOf($checkedExceptionClass)) { + if (!$classReflection->is($checkedExceptionClass)) { continue; } diff --git a/src/Rules/Exceptions/ExceptionTypeResolver.php b/src/Rules/Exceptions/ExceptionTypeResolver.php index 83af9366d3..5b7ac7e965 100644 --- a/src/Rules/Exceptions/ExceptionTypeResolver.php +++ b/src/Rules/Exceptions/ExceptionTypeResolver.php @@ -4,7 +4,33 @@ use PHPStan\Analyser\Scope; -/** @api */ +/** + * @api + * + * This interface allows you to write custom logic that can dynamically decide + * whether an exception is checked or unchecked type. + * + * Because the interface accepts a Scope, you can ask about the place in the code where + * it's being decided - a file, a namespace or a class name. + * + * There can only be a single ExceptionTypeResolver per project, and you can register it + * in your configuration file like this: + * + * ``` + * services: + * exceptionTypeResolver!: + * class: PHPStan\Rules\Exceptions\ExceptionTypeResolver + * ``` + * + * You can also take advantage of the `PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver` + * by injecting it into the constructor of your ExceptionTypeResolver + * and delegate the logic of the classes and places you don't care about. + * + * DefaultExceptionTypeResolver decides the type of the exception based on configuration + * parameters like `exceptions.uncheckedExceptionClasses` etc. + * + * Learn more: https://phpstan.org/blog/bring-your-exceptions-under-control + */ interface ExceptionTypeResolver { diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php index c41710b918..f3ed6e08f8 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php @@ -5,16 +5,14 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function sprintf; /** * @implements Rule */ -class MissingCheckedExceptionInFunctionThrowsRule implements Rule +final class MissingCheckedExceptionInFunctionThrowsRule implements Rule { public function __construct(private MissingCheckedExceptionInThrowsCheck $check) @@ -29,26 +27,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { - throw new ShouldNotHappenException(); - } + $functionReflection = $node->getFunctionReflection(); $errors = []; - foreach ($this->check->check($functionReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode, $newCatchPosition]) { + foreach ($this->check->check($functionReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { $errors[] = RuleErrorBuilder::message(sprintf( 'Function %s() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', $functionReflection->getName(), $className, )) - ->line($throwPointNode->getLine()) - ->identifier('exceptions.missingThrowsTag') - ->metadata([ - 'exceptionName' => $className, - 'newCatchPosition' => $newCatchPosition, - 'statementDepth' => $throwPointNode->getAttribute('statementDepth'), - 'statementOrder' => $throwPointNode->getAttribute('statementOrder'), - ]) + ->line($throwPointNode->getStartLine()) + ->identifier('missingType.checkedException') ->build(); } diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php index be58255d26..c564711b2f 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php @@ -5,16 +5,14 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function sprintf; /** * @implements Rule */ -class MissingCheckedExceptionInMethodThrowsRule implements Rule +final class MissingCheckedExceptionInMethodThrowsRule implements Rule { public function __construct(private MissingCheckedExceptionInThrowsCheck $check) @@ -29,27 +27,18 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - throw new ShouldNotHappenException(); - } + $methodReflection = $node->getMethodReflection(); $errors = []; - foreach ($this->check->check($methodReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode, $newCatchPosition]) { + foreach ($this->check->check($methodReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { $errors[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $className, )) - ->line($throwPointNode->getLine()) - ->identifier('exceptions.missingThrowsTag') - ->metadata([ - 'exceptionName' => $className, - 'newCatchPosition' => $newCatchPosition, - 'statementDepth' => $throwPointNode->getAttribute('statementDepth'), - 'statementOrder' => $throwPointNode->getAttribute('statementOrder'), - ]) + ->line($throwPointNode->getStartLine()) + ->identifier('missingType.checkedException') ->build(); } diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRule.php new file mode 100644 index 0000000000..d9b7a6b864 --- /dev/null +++ b/src/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRule.php @@ -0,0 +1,55 @@ + + */ +final class MissingCheckedExceptionInPropertyHookThrowsRule implements Rule +{ + + public function __construct(private MissingCheckedExceptionInThrowsCheck $check) + { + } + + public function getNodeType(): string + { + return PropertyHookReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $hookReflection = $node->getHookReflection(); + + if (!$hookReflection->isPropertyHook()) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($this->check->check($hookReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s hook for property %s::$%s throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', + ucfirst($hookReflection->getPropertyHookName()), + $hookReflection->getDeclaringClass()->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $className, + )) + ->line($throwPointNode->getStartLine()) + ->identifier('missingType.checkedException') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php index f3dfd9d0fb..0756fcac6b 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php @@ -4,17 +4,15 @@ use PhpParser\Node; use PHPStan\Analyser\ThrowPoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; use Throwable; -use function array_map; -class MissingCheckedExceptionInThrowsCheck +final class MissingCheckedExceptionInThrowsCheck { public function __construct(private ExceptionTypeResolver $exceptionTypeResolver) @@ -23,7 +21,7 @@ public function __construct(private ExceptionTypeResolver $exceptionTypeResolver /** * @param ThrowPoint[] $throwPoints - * @return array + * @return array */ public function check(?Type $throwType, array $throwPoints): array { @@ -45,60 +43,19 @@ public function check(?Type $throwType, array $throwPoints): array continue; } - if ( - $throwPointType instanceof TypeWithClassName - && !$this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) - ) { + $isCheckedException = TrinaryLogic::createNo()->lazyOr( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->no()) { continue; } - $classes[] = [$throwPointType->describe(VerbosityLevel::typeOnly()), $throwPoint->getNode(), $this->getNewCatchPosition($throwPointType, $throwPoint->getNode())]; + $classes[] = [$throwPointType->describe(VerbosityLevel::typeOnly()), $throwPoint->getNode()]; } } return $classes; } - private function getNewCatchPosition(Type $throwPointType, Node $throwPointNode): ?int - { - if ($throwPointType instanceof TypeWithClassName) { - // to get rid of type subtraction - $throwPointType = new ObjectType($throwPointType->getClassName()); - } - $tryCatch = $this->findTryCatch($throwPointNode); - if ($tryCatch === null) { - return null; - } - - $position = 0; - foreach ($tryCatch->catches as $catch) { - $type = TypeCombinator::union(...array_map(static fn (Node\Name $class): ObjectType => new ObjectType($class->toString()), $catch->types)); - if (!$throwPointType->isSuperTypeOf($type)->yes()) { - continue; - } - - $position++; - } - - return $position; - } - - private function findTryCatch(Node $node): ?Node\Stmt\TryCatch - { - if ($node instanceof Node\FunctionLike) { - return null; - } - - if ($node instanceof Node\Stmt\TryCatch) { - return $node; - } - - $parent = $node->getAttribute('parent'); - if ($parent === null) { - return null; - } - - return $this->findTryCatch($parent); - } - } diff --git a/src/Rules/Exceptions/NoncapturingCatchRule.php b/src/Rules/Exceptions/NoncapturingCatchRule.php new file mode 100644 index 0000000000..a4d91a9ba2 --- /dev/null +++ b/src/Rules/Exceptions/NoncapturingCatchRule.php @@ -0,0 +1,42 @@ + + */ +final class NoncapturingCatchRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Catch_::class; + } + + /** + * @param Node\Stmt\Catch_ $node + */ + public function processNode(Node $node, Scope $scope): array + { + if ($scope->getPhpVersion()->supportsNoncapturingCatches()->yes()) { + return []; + } + + if ($node->var !== null) { + return []; + } + + return [ + RuleErrorBuilder::message('Non-capturing catch is supported only on PHP 8.0 and later.') + ->nonIgnorable() + ->identifier('catch.nonCapturingNotSupported') + ->build(), + ]; + } + +} diff --git a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php index 4a3b8f5b09..74fa2eb0ae 100644 --- a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php +++ b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php @@ -13,7 +13,7 @@ /** * @implements Rule */ -class OverwrittenExitPointByFinallyRule implements Rule +final class OverwrittenExitPointByFinallyRule implements Rule { public function getNodeType(): string @@ -29,11 +29,17 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($node->getTryCatchExitPoints() as $exitPoint) { - $errors[] = RuleErrorBuilder::message(sprintf('This %s is overwritten by a different one in the finally block below.', $this->describeExitPoint($exitPoint->getStatement())))->line($exitPoint->getStatement()->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('This %s is overwritten by a different one in the finally block below.', $this->describeExitPoint($exitPoint->getStatement()))) + ->line($exitPoint->getStatement()->getStartLine()) + ->identifier('finally.exitPoint') + ->build(); } foreach ($node->getFinallyExitPoints() as $exitPoint) { - $errors[] = RuleErrorBuilder::message(sprintf('The overwriting %s is on this line.', $this->describeExitPoint($exitPoint->getStatement())))->line($exitPoint->getStatement()->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('The overwriting %s is on this line.', $this->describeExitPoint($exitPoint->getStatement()))) + ->line($exitPoint->getStatement()->getStartLine()) + ->identifier('finally.exitPoint') + ->build(); } return $errors; @@ -45,7 +51,7 @@ private function describeExitPoint(Node\Stmt $stmt): string return 'return'; } - if ($stmt instanceof Node\Stmt\Throw_) { + if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Node\Expr\Throw_) { return 'throw'; } diff --git a/src/Rules/Exceptions/ThrowExprTypeRule.php b/src/Rules/Exceptions/ThrowExprTypeRule.php new file mode 100644 index 0000000000..71087449d7 --- /dev/null +++ b/src/Rules/Exceptions/ThrowExprTypeRule.php @@ -0,0 +1,62 @@ + + */ +final class ThrowExprTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\Throw_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $throwableType = new ObjectType(Throwable::class); + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->expr, + 'Throwing object of an unknown class %s.', + static fn (Type $type): bool => $throwableType->isSuperTypeOf($type)->yes(), + ); + + $foundType = $typeResult->getType(); + if ($foundType instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $isSuperType = $throwableType->isSuperTypeOf($foundType); + if ($isSuperType->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Invalid type %s to throw.', + $foundType->describe(VerbosityLevel::typeOnly()), + ))->identifier('throw.notThrowable')->build(), + ]; + } + +} diff --git a/src/Rules/Exceptions/ThrowExpressionRule.php b/src/Rules/Exceptions/ThrowExpressionRule.php index 3cd3d68b5a..9fcc9c9e88 100644 --- a/src/Rules/Exceptions/ThrowExpressionRule.php +++ b/src/Rules/Exceptions/ThrowExpressionRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\StandaloneThrowExprVisitor; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -11,7 +12,7 @@ /** * @implements Rule */ -class ThrowExpressionRule implements Rule +final class ThrowExpressionRule implements Rule { public function __construct(private PhpVersion $phpVersion) @@ -29,8 +30,14 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->getAttribute(StandaloneThrowExprVisitor::ATTRIBUTE_NAME) === true) { + return []; + } + return [ - RuleErrorBuilder::message('Throw expression is supported only on PHP 8.0 and later.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Throw expression is supported only on PHP 8.0 and later.')->nonIgnorable() + ->identifier('throw.notSupported') + ->build(), ]; } diff --git a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php index d0eed19fb9..a2ead680c7 100644 --- a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php @@ -5,20 +5,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; /** * @implements Rule */ -class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule +final class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule { public function __construct( @@ -36,12 +33,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { - throw new ShouldNotHappenException(); - } + $functionReflection = $node->getFunctionReflection(); - if (!$functionReflection->getThrowType() instanceof VoidType) { + if ($functionReflection->getThrowType() === null || !$functionReflection->getThrowType()->isVoid()->yes()) { return []; } @@ -52,11 +46,11 @@ public function processNode(Node $node, Scope $scope): array } foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { - if ( - $throwPointType instanceof TypeWithClassName - && $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) - && $this->missingCheckedExceptionInThrows - ) { + $isCheckedException = TrinaryLogic::createFromBoolean($this->missingCheckedExceptionInThrows)->lazyAnd( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->yes()) { continue; } @@ -64,7 +58,10 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() throws exception %s but the PHPDoc contains @throws void.', $functionReflection->getName(), $throwPointType->describe(VerbosityLevel::typeOnly()), - ))->line($throwPoint->getNode()->getLine())->build(); + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); } } diff --git a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php index 91c97d157b..327b55c202 100644 --- a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php @@ -5,20 +5,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; /** * @implements Rule */ -class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule +final class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule { public function __construct( @@ -36,12 +33,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - throw new ShouldNotHappenException(); - } + $methodReflection = $node->getMethodReflection(); - if (!$methodReflection->getThrowType() instanceof VoidType) { + if ($methodReflection->getThrowType() === null || !$methodReflection->getThrowType()->isVoid()->yes()) { return []; } @@ -52,11 +46,11 @@ public function processNode(Node $node, Scope $scope): array } foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { - if ( - $throwPointType instanceof TypeWithClassName - && $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) - && $this->missingCheckedExceptionInThrows - ) { + $isCheckedException = TrinaryLogic::createFromBoolean($this->missingCheckedExceptionInThrows)->lazyAnd( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->yes()) { continue; } @@ -65,7 +59,10 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $throwPointType->describe(VerbosityLevel::typeOnly()), - ))->line($throwPoint->getNode()->getLine())->build(); + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); } } diff --git a/src/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRule.php new file mode 100644 index 0000000000..71b7cd9c2d --- /dev/null +++ b/src/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRule.php @@ -0,0 +1,79 @@ + + */ +final class ThrowsVoidPropertyHookWithExplicitThrowPointRule implements Rule +{ + + public function __construct( + private ExceptionTypeResolver $exceptionTypeResolver, + private bool $missingCheckedExceptionInThrows, + ) + { + } + + public function getNodeType(): string + { + return PropertyHookReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $hookReflection = $node->getHookReflection(); + + if ($hookReflection->getThrowType() === null || !$hookReflection->getThrowType()->isVoid()->yes()) { + return []; + } + + if ($hookReflection->getPropertyHookName() === null) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($statementResult->getThrowPoints() as $throwPoint) { + if (!$throwPoint->isExplicit()) { + continue; + } + + foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { + $isCheckedException = TrinaryLogic::createFromBoolean($this->missingCheckedExceptionInThrows)->lazyAnd( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->yes()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s hook for property %s::$%s throws exception %s but the PHPDoc contains @throws void.', + ucfirst($hookReflection->getPropertyHookName()), + $hookReflection->getDeclaringClass()->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $throwPointType->describe(VerbosityLevel::typeOnly()), + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php index fff550cd41..6688dec466 100644 --- a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php @@ -5,16 +5,14 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function sprintf; /** * @implements Rule */ -class TooWideFunctionThrowTypeRule implements Rule +final class TooWideFunctionThrowTypeRule implements Rule { public function __construct(private TooWideThrowTypeCheck $check) @@ -29,10 +27,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { - throw new ShouldNotHappenException(); - } + $functionReflection = $node->getFunctionReflection(); $throwType = $functionReflection->getThrowType(); if ($throwType === null) { @@ -46,12 +41,7 @@ public function processNode(Node $node, Scope $scope): array $functionReflection->getName(), $throwClass, )) - ->identifier('exceptions.tooWideThrowType') - ->metadata([ - 'exceptionName' => $throwClass, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - ]) + ->identifier('throws.unusedType') ->build(); } diff --git a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php index 3a26b6843e..55c69ee3a5 100644 --- a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php @@ -5,17 +5,15 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use function sprintf; /** * @implements Rule */ -class TooWideMethodThrowTypeRule implements Rule +final class TooWideMethodThrowTypeRule implements Rule { public function __construct(private FileTypeMapper $fileTypeMapper, private TooWideThrowTypeCheck $check) @@ -29,21 +27,14 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $statementResult = $node->getStatementResult(); - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - throw new ShouldNotHappenException(); - } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $docComment = $node->getDocComment(); if ($docComment === null) { return []; } - $classReflection = $scope->getClassReflection(); + $statementResult = $node->getStatementResult(); + $methodReflection = $node->getMethodReflection(); + $classReflection = $node->getClassReflection(); $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $scope->getFile(), $classReflection->getName(), @@ -66,12 +57,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getName(), $throwClass, )) - ->identifier('exceptions.tooWideThrowType') - ->metadata([ - 'exceptionName' => $throwClass, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - ]) + ->identifier('throws.unusedType') ->build(); } diff --git a/src/Rules/Exceptions/TooWidePropertyHookThrowTypeRule.php b/src/Rules/Exceptions/TooWidePropertyHookThrowTypeRule.php new file mode 100644 index 0000000000..00ed4bacd4 --- /dev/null +++ b/src/Rules/Exceptions/TooWidePropertyHookThrowTypeRule.php @@ -0,0 +1,74 @@ + + */ +final class TooWidePropertyHookThrowTypeRule implements Rule +{ + + public function __construct(private FileTypeMapper $fileTypeMapper, private TooWideThrowTypeCheck $check) + { + } + + public function getNodeType(): string + { + return PropertyHookReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $statementResult = $node->getStatementResult(); + $hookReflection = $node->getHookReflection(); + if ($hookReflection->getPropertyHookName() === null) { + throw new ShouldNotHappenException(); + } + + $classReflection = $node->getClassReflection(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $hookReflection->getName(), + $docComment->getText(), + ); + + if ($resolvedPhpDoc->getThrowsTag() === null) { + return []; + } + + $throwType = $resolvedPhpDoc->getThrowsTag()->getType(); + + $errors = []; + foreach ($this->check->check($throwType, $statementResult->getThrowPoints()) as $throwClass) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s hook for property %s::$%s has %s in PHPDoc @throws tag but it\'s not thrown.', + ucfirst($hookReflection->getPropertyHookName()), + $hookReflection->getDeclaringClass()->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $throwClass, + )) + ->identifier('throws.unusedType') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/TooWideThrowTypeCheck.php b/src/Rules/Exceptions/TooWideThrowTypeCheck.php index 59a9ae1d88..5c5e33c803 100644 --- a/src/Rules/Exceptions/TooWideThrowTypeCheck.php +++ b/src/Rules/Exceptions/TooWideThrowTypeCheck.php @@ -8,24 +8,27 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function array_map; -class TooWideThrowTypeCheck +final class TooWideThrowTypeCheck { + public function __construct(private bool $implicitThrows) + { + } + /** * @param ThrowPoint[] $throwPoints * @return string[] */ public function check(Type $throwType, array $throwPoints): array { - if ($throwType instanceof VoidType) { + if ($throwType->isVoid()->yes()) { return []; } - $throwPointType = TypeCombinator::union(...array_map(static function (ThrowPoint $throwPoint): Type { - if (!$throwPoint->isExplicit()) { + $throwPointType = TypeCombinator::union(...array_map(function (ThrowPoint $throwPoint): Type { + if (!$this->implicitThrows && !$throwPoint->isExplicit()) { return new NeverType(); } diff --git a/src/Rules/FileRuleError.php b/src/Rules/FileRuleError.php index 7f5cd7fc26..612370f9b2 100644 --- a/src/Rules/FileRuleError.php +++ b/src/Rules/FileRuleError.php @@ -2,9 +2,12 @@ namespace PHPStan\Rules; +/** @api */ interface FileRuleError extends RuleError { public function getFile(): string; + public function getFileDescription(): string; + } diff --git a/src/Rules/FoundTypeResult.php b/src/Rules/FoundTypeResult.php index 919fe2adfd..61702c6194 100644 --- a/src/Rules/FoundTypeResult.php +++ b/src/Rules/FoundTypeResult.php @@ -4,13 +4,15 @@ use PHPStan\Type\Type; -/** @api */ -class FoundTypeResult +/** + * @api + */ +final class FoundTypeResult { /** * @param string[] $referencedClasses - * @param RuleError[] $unknownClassErrors + * @param list $unknownClassErrors */ public function __construct( private Type $type, @@ -35,7 +37,7 @@ public function getReferencedClasses(): array } /** - * @return RuleError[] + * @return list */ public function getUnknownClassErrors(): array { diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index f29b301fd3..aed8352077 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -4,35 +4,43 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ResolvedFunctionVariant; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; +use PHPStan\Type\ConditionalType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; +use function array_fill; use function array_key_exists; use function count; +use function implode; +use function in_array; +use function is_int; use function is_string; use function max; use function sprintf; -class FunctionCallParametersCheck +final class FunctionCallParametersCheck { public function __construct( private RuleLevelHelper $ruleLevelHelper, private NullsafeCheck $nullsafeCheck, - private PhpVersion $phpVersion, private UnresolvableTypeHelper $unresolvableTypeHelper, private PropertyReflectionFinder $propertyReflectionFinder, private bool $checkArgumentTypes, @@ -44,16 +52,31 @@ public function __construct( } /** - * @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[] + * @param 'attribute'|'callable'|'method'|'staticMethod'|'function'|'new' $nodeType + * @return list */ public function check( ParametersAcceptor $parametersAcceptor, Scope $scope, bool $isBuiltin, - $funcCall, - array $messages, + Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall, + string $nodeType, + TrinaryLogic $acceptsNamedArguments, + string $singleInsufficientParameterMessage, + string $pluralInsufficientParametersMessage, + string $singleInsufficientParameterInVariadicFunctionMessage, + string $pluralInsufficientParametersInVariadicFunctionMessage, + string $singleInsufficientParameterWithOptionalParametersMessage, + string $pluralInsufficientParametersWithOptionalParametersMessage, + string $wrongArgumentTypeMessage, + string $voidReturnTypeUsed, + string $parameterPassedByReferenceMessage, + string $unresolvableTemplateTypeMessage, + string $missingParameterMessage, + string $unknownParameterMessage, + string $unresolvableReturnTypeMessage, + string $unresolvableParameterTypeMessage, + string $namedArgumentMessage, ): array { $functionParametersMinCount = 0; @@ -70,46 +93,69 @@ public function check( $functionParametersMaxCount = -1; } - /** @var array $arguments */ + /** @var array $arguments */ $arguments = []; /** @var array $args */ $args = $funcCall->getArgs(); $hasNamedArguments = false; $hasUnpackedArgument = false; $errors = []; - foreach ($args as $i => $arg) { - $type = $scope->getType($arg->value); + foreach ($args as $arg) { + $argumentName = null; + if ($arg->name !== null) { + $hasNamedArguments = true; + $argumentName = $arg->name->toString(); + } + if ($hasNamedArguments && $arg->unpack) { - $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.')->line($arg->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.') + ->identifier('argument.unpackAfterNamed') + ->line($arg->getStartLine()) + ->nonIgnorable() + ->build(); } if ($hasUnpackedArgument && !$arg->unpack) { - $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.')->line($arg->getLine())->nonIgnorable()->build(); + if ($argumentName === null || !$scope->getPhpVersion()->supportsNamedArgumentAfterUnpackedArgument()->yes()) { + $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.') + ->identifier('argument.nonUnpackAfterUnpacked') + ->line($arg->getStartLine()) + ->nonIgnorable() + ->build(); + } } if ($arg->unpack) { $hasUnpackedArgument = true; } - $argumentName = null; - if ($arg->name !== null) { - $hasNamedArguments = true; - $argumentName = $arg->name->toString(); - } if ($arg->unpack) { - $arrays = TypeUtils::getConstantArrays($type); + $type = $scope->getType($arg->value); + $arrays = $type->getConstantArrays(); if (count($arrays) > 0) { - $minKeys = null; + $maxKeys = null; foreach ($arrays as $array) { - $keysCount = count($array->getKeyTypes()); - if ($minKeys !== null && $keysCount >= $minKeys) { + $countType = $array->getArraySize(); + if ($countType instanceof ConstantIntegerType) { + $keysCount = $countType->getValue(); + } elseif ($countType instanceof IntegerRangeType) { + $keysCount = $countType->getMax(); + if ($keysCount === null) { + throw new ShouldNotHappenException(); + } + } else { + throw new ShouldNotHappenException(); + } + if ($maxKeys !== null && $keysCount >= $maxKeys) { continue; } - $minKeys = $keysCount; + $maxKeys = $keysCount; } - for ($j = 0; $j < $minKeys; $j++) { + for ($j = 0; $j < $maxKeys; $j++) { $types = []; $commonKey = null; + $isOptionalKey = false; foreach ($arrays as $constantArray) { + $isOptionalKey = in_array($j, $constantArray->getOptionalKeys(), true); $types[] = $constantArray->getValueTypes()[$j]; $keyType = $constantArray->getKeyTypes()[$j]; if ($commonKey === null) { @@ -123,12 +169,16 @@ public function check( $keyArgumentName = $commonKey; $hasNamedArguments = true; } + if ($isOptionalKey) { + continue; + } + $arguments[] = [ $arg->value, TypeCombinator::union(...$types), false, $keyArgumentName, - $arg->getLine(), + $arg->getStartLine(), ]; } } else { @@ -137,7 +187,7 @@ public function check( $type->getIterableValueType(), true, null, - $arg->getLine(), + $arg->getStartLine(), ]; } continue; @@ -145,20 +195,24 @@ public function check( $arguments[] = [ $arg->value, - $type, + null, false, $argumentName, - $arg->getLine(), + $arg->getStartLine(), ]; } - if ($hasNamedArguments && !$this->phpVersion->supportsNamedArguments() && !(bool) $funcCall->getAttribute('isAttribute', false)) { - $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.')->line($funcCall->getLine())->nonIgnorable()->build(); + if ($hasNamedArguments && !$scope->getPhpVersion()->supportsNamedArguments()->yes() && !(bool) $funcCall->getAttribute('isAttribute', false)) { + $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.') + ->identifier('argument.namedNotSupported') + ->line($funcCall->getStartLine()) + ->nonIgnorable() + ->build(); } if (!$hasNamedArguments) { $invokedParametersCount = count($arguments); - foreach ($arguments as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName]) { + foreach ($arguments as [$argumentValue, $argumentValueType, $unpack, $argumentName]) { if ($unpack) { $invokedParametersCount = max($functionParametersMinCount, $functionParametersMaxCount); break; @@ -171,36 +225,48 @@ public function check( ) { if ($functionParametersMinCount === $functionParametersMaxCount) { $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[0] : $messages[1], + $invokedParametersCount === 1 ? $singleInsufficientParameterMessage : $pluralInsufficientParametersMessage, $invokedParametersCount, $functionParametersMinCount, - ))->line($funcCall->getLine())->build(); + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); } elseif ($functionParametersMaxCount === -1 && $invokedParametersCount < $functionParametersMinCount) { $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[2] : $messages[3], + $invokedParametersCount === 1 ? $singleInsufficientParameterInVariadicFunctionMessage : $pluralInsufficientParametersInVariadicFunctionMessage, $invokedParametersCount, $functionParametersMinCount, - ))->line($funcCall->getLine())->build(); + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); } elseif ($functionParametersMaxCount !== -1) { $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[4] : $messages[5], + $invokedParametersCount === 1 ? $singleInsufficientParameterWithOptionalParametersMessage : $pluralInsufficientParametersWithOptionalParametersMessage, $invokedParametersCount, $functionParametersMinCount, $functionParametersMaxCount, - ))->line($funcCall->getLine())->build(); + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); } } } if ( - $scope->getType($funcCall) instanceof VoidType + !$funcCall instanceof Node\Expr\New_ && !$scope->isInFirstLevelStatement() - && !$funcCall instanceof Node\Expr\New_ + && $scope->getKeepVoidType($funcCall)->isVoid()->yes() ) { - $errors[] = RuleErrorBuilder::message($messages[7])->line($funcCall->getLine())->build(); + $errors[] = RuleErrorBuilder::message($voidReturnTypeUsed) + ->identifier(sprintf('%s.void', $nodeType)) + ->line($funcCall->getStartLine()) + ->build(); } - [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCall->getLine(), $isBuiltin, $arguments, $hasNamedArguments, $messages[10], $messages[11]); + [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCall->getStartLine(), $isBuiltin, $arguments, $hasNamedArguments, $missingParameterMessage, $unknownParameterMessage); foreach ($addedErrors as $error) { $errors[] = $error; } @@ -209,7 +275,7 @@ public function check( return $errors; } - foreach ($argumentsWithParameters as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter]) { + foreach ($argumentsWithParameters as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter, $originalParameter]) { if ($this->checkArgumentTypes && $unpack) { $iterableTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, @@ -226,7 +292,7 @@ public function check( 'Only iterables can be unpacked, %s given in argument #%d.', $iterableTypeResultType->describe(VerbosityLevel::typeOnly()), $i + 1, - ))->line($argumentLine)->build(); + ))->identifier('argument.unpackNonIterable')->line($argumentLine)->build(); } } @@ -234,24 +300,86 @@ public function check( continue; } - $parameterType = $parameter->getType(); - if ( - $this->checkArgumentTypes - && !$parameter->passedByReference()->createsNewVariable() - && !$this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()) - ) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); - $errors[] = RuleErrorBuilder::message(sprintf( - $messages[6], - $argumentName === null ? sprintf( - '#%d %s', - $i + 1, - $parameterDescription, - ) : $parameterDescription, - $parameterType->describe($verbosityLevel), - $argumentValueType->describe($verbosityLevel), - ))->line($argumentLine)->build(); + if ($argumentValueType === null) { + if ($scope instanceof MutatingScope) { + $scope = $scope->pushInFunctionCall(null, $parameter); + } + $argumentValueType = $scope->getType($argumentValue); + + if ($scope instanceof MutatingScope) { + $scope = $scope->popInFunctionCall(); + } + } + + if (!$acceptsNamedArguments->yes()) { + if ($argumentName !== null) { + $errors[] = RuleErrorBuilder::message(sprintf($namedArgumentMessage, sprintf('named argument $%s', $argumentName))) + ->identifier('argument.named') + ->line($argumentLine) + ->build(); + } elseif ($unpack) { + $unpackedArrayType = $scope->getType($argumentValue); + $hasStringKey = $unpackedArrayType->getIterableKeyType()->isString(); + if (!$hasStringKey->no()) { + $errors[] = RuleErrorBuilder::message(sprintf($namedArgumentMessage, sprintf('unpacked array with %s', $hasStringKey->yes() ? 'string key' : 'possibly string key'))) + ->identifier('argument.named') + ->line($argumentLine) + ->build(); + } + } + } + + if ($this->checkArgumentTypes) { + $parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType()); + + if ( + !$parameter->passedByReference()->createsNewVariable() + || (!$isBuiltin && !$argumentValueType instanceof ErrorType) + ) { + $accepts = $this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()); + + if (!$accepts->result) { + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); + $errors[] = RuleErrorBuilder::message(sprintf( + $wrongArgumentTypeMessage, + $this->describeParameter($parameter, $argumentName ?? $i + 1), + $parameterType->describe($verbosityLevel), + $argumentValueType->describe($verbosityLevel), + )) + ->identifier('argument.type') + ->line($argumentLine) + ->acceptsReasonsTip($accepts->reasons) + ->build(); + } + } + + if ( + $originalParameter !== null + && !$this->unresolvableTypeHelper->containsUnresolvableType($originalParameter->getType()) + && $this->unresolvableTypeHelper->containsUnresolvableType($parameterType) + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + $unresolvableParameterTypeMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + ))->identifier('argument.unresolvableType')->line($argumentLine)->build(); + } + + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getClosureThisType() !== null + && ($argumentValue instanceof Expr\Closure || $argumentValue instanceof Expr\ArrowFunction) + && $argumentValue->static + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + $wrongArgumentTypeMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + 'bindable closure', + 'static closure', + )) + ->identifier('argument.staticClosure') + ->line($argumentLine) + ->build(); + } } if ( @@ -262,11 +390,13 @@ public function check( } if ($this->nullsafeCheck->containsNullSafe($argumentValue)) { - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( - $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, - ))->line($argumentLine)->build(); + $parameterPassedByReferenceMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + )) + ->identifier('argument.byRef') + ->line($argumentLine) + ->build(); continue; } @@ -279,22 +409,30 @@ public function check( if ($nativePropertyReflection === null) { continue; } - if (!$nativePropertyReflection->isReadOnly()) { - continue; - } - if ($nativePropertyReflection->isStatic()) { - $propertyDescription = sprintf('static readonly property %s::$%s', $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->getName()); + if ($nativePropertyReflection->isReadOnly()) { + if ($nativePropertyReflection->isStatic()) { + $errorFormat = 'static readonly property %s::$%s'; + } else { + $errorFormat = 'readonly property %s::$%s'; + } + } elseif ($nativePropertyReflection->isReadOnlyByPhpDoc()) { + if ($nativePropertyReflection->isStatic()) { + $errorFormat = 'static @readonly property %s::$%s'; + } else { + $errorFormat = '@readonly property %s::$%s'; + } } else { - $propertyDescription = sprintf('readonly property %s::$%s', $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->getName()); + continue; } - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); + $propertyDescription = sprintf($errorFormat, $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->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, + '%s is passed by reference so it does not accept %s.', + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), $propertyDescription, - ))->line($argumentLine)->build(); + ))->identifier('argument.byRef')->line($argumentLine)->build(); } } @@ -305,11 +443,10 @@ public function check( continue; } - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( - $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, - ))->line($argumentLine)->build(); + $parameterPassedByReferenceMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + ))->identifier('argument.byRef')->line($argumentLine)->build(); } if ($this->checkMissingTypehints && $parametersAcceptor instanceof ResolvedFunctionVariant) { @@ -317,19 +454,26 @@ public function check( $resolvedTypes = $parametersAcceptor->getResolvedTemplateTypeMap()->getTypes(); if (count($resolvedTypes) > 0) { $returnTemplateTypes = []; - TypeTraverser::map($originalParametersAcceptor->getReturnType(), static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Type { - if ($type instanceof TemplateType) { - $returnTemplateTypes[$type->getName()] = true; - return $type; - } + TypeTraverser::map( + $parametersAcceptor->getReturnTypeWithUnresolvableTemplateTypes(), + static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Type { + while ($type instanceof ConditionalType && $type->isResolvable()) { + $type = $type->resolve(); + } - return $traverse($type); - }); + if ($type instanceof TemplateType && $type->getDefault() === null) { + $returnTemplateTypes[$type->getName()] = true; + return $type; + } + + return $traverse($type); + }, + ); $parameterTemplateTypes = []; foreach ($originalParametersAcceptor->getParameters() as $parameter) { TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$parameterTemplateTypes): Type { - if ($type instanceof TemplateType) { + if ($type instanceof TemplateType && $type->getDefault() === null) { $parameterTemplateTypes[$type->getName()] = true; return $type; } @@ -357,7 +501,11 @@ public function check( continue; } - $errors[] = RuleErrorBuilder::message(sprintf($messages[9], $name))->line($funcCall->getLine())->tip('See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type')->build(); + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableTemplateTypeMessage, $name)) + ->identifier('argument.templateType') + ->line($funcCall->getStartLine()) + ->tip('See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type') + ->build(); } } @@ -365,7 +513,10 @@ public function check( !$this->unresolvableTypeHelper->containsUnresolvableType($originalParametersAcceptor->getReturnType()) && $this->unresolvableTypeHelper->containsUnresolvableType($parametersAcceptor->getReturnType()) ) { - $errors[] = RuleErrorBuilder::message($messages[12])->line($funcCall->getLine())->build(); + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) + ->identifier(sprintf('%s.unresolvableReturnType', $nodeType)) + ->line($funcCall->getStartLine()) + ->build(); } } @@ -373,8 +524,8 @@ public function check( } /** - * @param array $arguments - * @return array{RuleError[], array} + * @param array $arguments + * @return array{list, array} */ private function processArguments( ParametersAcceptor $parametersAcceptor, @@ -387,11 +538,16 @@ private function processArguments( ): array { $parameters = $parametersAcceptor->getParameters(); + $originalParameters = $parametersAcceptor instanceof ResolvedFunctionVariant + ? $parametersAcceptor->getOriginalParametersAcceptor()->getParameters() + : array_fill(0, count($parameters), null); $parametersByName = []; + $originalParametersByName = []; $unusedParametersByName = []; $errors = []; - foreach ($parametersAcceptor->getParameters() as $parameter) { + foreach ($parameters as $i => $parameter) { $parametersByName[$parameter->getName()] = $parameter; + $originalParametersByName[$parameter->getName()] = $originalParameters[$i]; if ($parameter->isVariadic()) { continue; @@ -407,21 +563,24 @@ private function processArguments( if ($argumentName === null) { if (!isset($parameters[$i])) { if (!$parametersAcceptor->isVariadic() || count($parameters) === 0) { - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null]; + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; break; } $parameter = $parameters[count($parameters) - 1]; + $originalParameter = $originalParameters[count($originalParameters) - 1]; if (!$parameter->isVariadic()) { - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null]; + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; break; // func_get_args } } else { $parameter = $parameters[$i]; + $originalParameter = $originalParameters[$i]; } } elseif (array_key_exists($argumentName, $parametersByName)) { $namedArgumentAlreadyOccurred = true; $parameter = $parametersByName[$argumentName]; + $originalParameter = $originalParametersByName[$argumentName]; } else { $namedArgumentAlreadyOccurred = true; @@ -431,28 +590,39 @@ private function processArguments( || $parametersCount <= 0 || $isBuiltin ) { - $errors[] = RuleErrorBuilder::message(sprintf($unknownParameterMessage, $argumentName))->line($argumentLine)->build(); - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null]; + $errors[] = RuleErrorBuilder::message(sprintf($unknownParameterMessage, $argumentName)) + ->identifier('argument.unknown') + ->line($argumentLine) + ->build(); + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; continue; } $parameter = $parameters[$parametersCount - 1]; + $originalParameter = $originalParameters[$parametersCount - 1]; } if ($namedArgumentAlreadyOccurred && $argumentName === null && !$unpack) { - $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by a positional argument.')->line($argumentLine)->nonIgnorable()->build(); - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null]; + $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by a positional argument.') + ->identifier('argument.positionalAfterNamed') + ->line($argumentLine) + ->nonIgnorable() + ->build(); + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; continue; } - $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter]; + $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter, $originalParameter]; if ( $hasNamedArguments && !$parameter->isVariadic() && !array_key_exists($parameter->getName(), $unusedParametersByName) ) { - $errors[] = RuleErrorBuilder::message(sprintf('Argument for parameter $%s has already been passed.', $parameter->getName()))->line($argumentLine)->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Argument for parameter $%s has already been passed.', $parameter->getName())) + ->identifier('argument.duplicate') + ->line($argumentLine) + ->build(); continue; } @@ -465,11 +635,33 @@ private function processArguments( continue; } - $errors[] = RuleErrorBuilder::message(sprintf($missingParameterMessage, sprintf('%s (%s)', $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly()))))->line($line)->build(); + $errors[] = RuleErrorBuilder::message(sprintf($missingParameterMessage, sprintf('%s (%s)', $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly())))) + ->identifier('argument.missing') + ->line($line) + ->build(); } } return [$errors, $newArguments]; } + private function describeParameter(ParameterReflection $parameter, int|string|null $positionOrNamed): string + { + $parts = []; + if (is_int($positionOrNamed)) { + $parts[] = 'Parameter #' . $positionOrNamed; + } elseif ($parameter->isVariadic() && is_string($positionOrNamed)) { + $parts[] = 'Named argument ' . $positionOrNamed . ' for variadic parameter'; + } else { + $parts[] = 'Parameter'; + } + + $name = $parameter->getName(); + if ($name !== '') { + $parts[] = ($parameter->isVariadic() ? '...$' : '$') . $name; + } + + return implode(' ', $parts); + } + } diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index d311e8f353..af468ab670 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -3,45 +3,53 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PhpParser\Node\ComplexType; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Variable; use PhpParser\Node\FunctionLike; +use PhpParser\Node\Identifier; +use PhpParser\Node\IntersectionType; +use PhpParser\Node\Name; +use PhpParser\Node\NullableType; use PhpParser\Node\Param; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\UnionType; use PHPStan\Analyser\Scope; +use PHPStan\Node\Printer\NodeTypePrinter; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ConditionalTypeForParameter; 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_filter; use function array_keys; use function array_map; use function array_merge; use function count; +use function in_array; use function is_string; use function sprintf; +use function strtolower; -class FunctionDefinitionCheck +final class FunctionDefinitionCheck { public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private PhpVersion $phpVersion, private bool $checkClassCaseSensitivity, @@ -51,11 +59,12 @@ public function __construct( } /** - * @return RuleError[] + * @return list */ public function checkFunction( + Scope $scope, Function_ $function, - FunctionReflection $functionReflection, + PhpFunctionFromParserNodeReflection $functionReflection, string $parameterMessage, string $returnMessage, string $unionTypesMessage, @@ -64,10 +73,9 @@ public function checkFunction( string $unresolvableReturnTypeMessage, ): array { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); - return $this->checkParametersAcceptor( - $parametersAcceptor, + $scope, + $functionReflection, $function, $parameterMessage, $returnMessage, @@ -81,7 +89,7 @@ public function checkFunction( /** * @param Node\Param[] $parameters * @param Node\Identifier|Node\Name|Node\ComplexType|null $returnTypeNode - * @return RuleError[] + * @return list */ public function checkAnonymousFunction( Scope $scope, @@ -96,7 +104,7 @@ public function checkAnonymousFunction( { $errors = []; $unionTypeReported = false; - foreach ($parameters as $param) { + foreach ($parameters as $i => $param) { if ($param->type === null) { continue; } @@ -105,35 +113,75 @@ public function checkAnonymousFunction( && $param->type instanceof UnionType && !$this->phpVersion->supportsNativeUnionTypes() ) { - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($param->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($param->getStartLine()) + ->identifier('parameter.unionTypeNotSupported') + ->nonIgnorable() + ->build(); $unionTypeReported = true; } if (!$param->var instanceof Variable || !is_string($param->var->name)) { throw new ShouldNotHappenException(); } + + $implicitlyNullableTypeError = $this->checkImplicitlyNullableType( + $param->type, + $param->default, + $i + 1, + $param->getStartLine(), + $param->var->name, + ); + if ($implicitlyNullableTypeError !== null) { + $errors[] = $implicitlyNullableTypeError; + } + $type = $scope->getFunctionType($param->type, false, false); - if ($type instanceof VoidType) { - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, 'void'))->line($param->type->getLine())->nonIgnorable()->build(); + if ($type->isVoid()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, 'void')) + ->line($param->type->getStartLine()) + ->identifier('parameter.void') + ->nonIgnorable() + ->build(); } if ( $this->phpVersion->supportsPureIntersectionTypes() && $this->unresolvableTypeHelper->containsUnresolvableType($type) ) { - $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $param->var->name))->line($param->type->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $param->var->name)) + ->line($param->type->getStartLine()) + ->identifier('parameter.unresolvableNativeType') + ->nonIgnorable() + ->build(); } foreach ($type->getReferencedClasses() as $class) { - if (!$this->reflectionProvider->hasClass($class) || $this->reflectionProvider->getClass($class)->isTrait()) { - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, $class))->line($param->type->getLine())->build(); - } elseif ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames([ - new ClassNameNodePair($class, $param->type), - ]), - ); + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, $class)) + ->line($param->type->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + + $classReflection = $this->reflectionProvider->getClass($class); + if ($classReflection->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, $class)) + ->line($param->type->getStartLine()) + ->identifier('parameter.trait') + ->build(); + continue; } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $param->type), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::PARAMETER_TYPE, [ + 'parameterName' => $param->var->name, + 'isInAnonymousFunction' => true, + ]), $this->checkClassCaseSensitivity), + ); } } @@ -150,7 +198,11 @@ public function checkAnonymousFunction( && $returnTypeNode instanceof UnionType && !$this->phpVersion->supportsNativeUnionTypes() ) { - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($returnTypeNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.unionTypeNotSupported') + ->nonIgnorable() + ->build(); } $returnType = $scope->getFunctionType($returnTypeNode, false, false); @@ -158,44 +210,62 @@ public function checkAnonymousFunction( $this->phpVersion->supportsPureIntersectionTypes() && $this->unresolvableTypeHelper->containsUnresolvableType($returnType) ) { - $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage)->line($returnTypeNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.unresolvableNativeType') + ->nonIgnorable() + ->build(); } foreach ($returnType->getReferencedClasses() as $returnTypeClass) { - if (!$this->reflectionProvider->hasClass($returnTypeClass) || $this->reflectionProvider->getClass($returnTypeClass)->isTrait()) { - $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass))->line($returnTypeNode->getLine())->build(); - } elseif ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames([ - new ClassNameNodePair($returnTypeClass, $returnTypeNode), - ]), - ); + if (!$this->reflectionProvider->hasClass($returnTypeClass)) { + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass)) + ->line($returnTypeNode->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + + if ($this->reflectionProvider->getClass($returnTypeClass)->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass)) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.trait') + ->build(); + continue; } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($returnTypeClass, $returnTypeNode), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::RETURN_TYPE, [ + 'isInAnonymousFunction' => true, + ]), $this->checkClassCaseSensitivity), + ); } return $errors; } /** - * @return RuleError[] + * @return list */ public function checkClassMethod( + Scope $scope, PhpMethodFromParserNodeReflection $methodReflection, - ClassMethod $methodNode, + ClassMethod|Node\PropertyHook $methodNode, string $parameterMessage, string $returnMessage, string $unionTypesMessage, string $templateTypeMissingInParameterMessage, string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, + string $selfOutMessage, ): array { - /** @var ParametersAcceptorWithPhpDocs $parametersAcceptor */ - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); - - return $this->checkParametersAcceptor( - $parametersAcceptor, + $errors = $this->checkParametersAcceptor( + $scope, + $methodReflection, $methodNode, $parameterMessage, $returnMessage, @@ -204,13 +274,49 @@ public function checkClassMethod( $unresolvableParameterTypeMessage, $unresolvableReturnTypeMessage, ); + + $selfOutType = $methodReflection->getSelfOutType(); + if ($selfOutType !== null) { + $selfOutTypeReferencedClasses = $selfOutType->getReferencedClasses(); + + foreach ($selfOutTypeReferencedClasses as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf($selfOutMessage, $class)) + ->line($methodNode->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + if (!$this->reflectionProvider->getClass($class)->isTrait()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf($selfOutMessage, $class)) + ->line($methodNode->getStartLine()) + ->identifier('selfOut.trait') + ->build(); + } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + $scope, + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $methodNode), $selfOutTypeReferencedClasses), + ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_SELF_OUT), + $this->checkClassCaseSensitivity, + ), + ); + } + + return $errors; } /** - * @return RuleError[] + * @return list */ private function checkParametersAcceptor( - ParametersAcceptor $parametersAcceptor, + Scope $scope, + PhpMethodFromParserNodeReflection|PhpFunctionFromParserNodeReflection $parametersAcceptor, FunctionLike $functionNode, string $parameterMessage, string $returnMessage, @@ -229,14 +335,34 @@ private function checkParametersAcceptor( continue; } - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($parameterNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($parameterNode->getStartLine()) + ->identifier('parameter.unionTypeNotSupported') + ->nonIgnorable() + ->build(); $unionTypeReported = true; break; } if (!$unionTypeReported && $functionNode->getReturnType() instanceof UnionType) { - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($functionNode->getReturnType()->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($functionNode->getReturnType()->getStartLine()) + ->identifier('return.unionTypeNotSupported') + ->nonIgnorable() + ->build(); + } + } + + foreach ($parameterNodes as $i => $parameterNode) { + if (!$parameterNode->var instanceof Variable || !is_string($parameterNode->var->name)) { + throw new ShouldNotHappenException(); } + $implicitlyNullableTypeError = $this->checkImplicitlyNullableType($parameterNode->type, $parameterNode->default, $i + 1, $parameterNode->getStartLine(), $parameterNode->var->name); + if ($implicitlyNullableTypeError === null) { + continue; + } + + $errors[] = $implicitlyNullableTypeError; } if ($this->phpVersion->deprecatesRequiredParameterAfterOptional()) { @@ -254,23 +380,40 @@ private function checkParametersAcceptor( return $parameterNode; }; - if ($parameter instanceof ParameterReflectionWithPhpDocs) { - $parameterVar = $parameterNodeCallback()->var; - if (!$parameterVar instanceof Variable || !is_string($parameterVar->name)) { - throw new ShouldNotHappenException(); - } - if ($parameter->getNativeType() instanceof VoidType) { - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void'))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build(); - } - if ( - $this->phpVersion->supportsPureIntersectionTypes() - && $this->unresolvableTypeHelper->containsUnresolvableType($parameter->getNativeType()) - ) { - $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $parameterVar->name))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build(); - } + $parameterVar = $parameterNodeCallback()->var; + if (!$parameterVar instanceof Variable || !is_string($parameterVar->name)) { + throw new ShouldNotHappenException(); + } + if ($parameter->getNativeType()->isVoid()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void')) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.void') + ->nonIgnorable() + ->build(); + } + if ( + $this->phpVersion->supportsPureIntersectionTypes() + && $this->unresolvableTypeHelper->containsUnresolvableType($parameter->getNativeType()) + ) { + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $parameterVar->name)) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.unresolvableNativeType') + ->nonIgnorable() + ->build(); } foreach ($referencedClasses as $class) { - if ($this->reflectionProvider->hasClass($class) && !$this->reflectionProvider->getClass($class)->isTrait()) { + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf( + $parameterMessage, + $parameter->getName(), + $class, + )) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + if (!$this->reflectionProvider->getClass($class)->isTrait()) { continue; } @@ -278,47 +421,98 @@ private function checkParametersAcceptor( $parameterMessage, $parameter->getName(), $class, - ))->line($parameterNodeCallback()->getLine())->build(); + )) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.trait') + ->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $parameterNodeCallback()), $referencedClasses)), - ); + $locationData = [ + 'parameterName' => $parameter->getName(), + ]; + if ($parametersAcceptor instanceof PhpMethodFromParserNodeReflection) { + $locationData['method'] = $parametersAcceptor; + if (!$parametersAcceptor->getDeclaringClass()->isAnonymous()) { + $locationData['currentClassName'] = $parametersAcceptor->getDeclaringClass()->getName(); + } + } else { + $locationData['function'] = $parametersAcceptor; } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + $scope, + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $parameterNodeCallback()), $referencedClasses), + ClassNameUsageLocation::from(ClassNameUsageLocation::PARAMETER_TYPE, $locationData), + $this->checkClassCaseSensitivity, + ), + ); if (!($parameter->getType() instanceof NonexistentParentClassType)) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly())))->line($parameterNodeCallback()->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly()))) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.noParent') + ->build(); } if ($this->phpVersion->supportsPureIntersectionTypes() && $functionNode->getReturnType() !== null) { $nativeReturnType = ParserNodeTypeToPHPStanType::resolve($functionNode->getReturnType(), null); if ($this->unresolvableTypeHelper->containsUnresolvableType($nativeReturnType)) { - $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage)->nonIgnorable()->line($returnTypeNode->getLine())->build(); + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) + ->nonIgnorable() + ->line($returnTypeNode->getStartLine()) + ->identifier('return.unresolvableNativeType') + ->build(); } } $returnTypeReferencedClasses = $this->getReturnTypeReferencedClasses($parametersAcceptor); foreach ($returnTypeReferencedClasses as $class) { - if ($this->reflectionProvider->hasClass($class) && !$this->reflectionProvider->getClass($class)->isTrait()) { + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $class)) + ->line($returnTypeNode->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + if (!$this->reflectionProvider->getClass($class)->isTrait()) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $class))->line($returnTypeNode->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $class)) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.trait') + ->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $returnTypeNode), $returnTypeReferencedClasses)), - ); + $locationData = []; + if ($parametersAcceptor instanceof PhpMethodFromParserNodeReflection) { + $locationData['method'] = $parametersAcceptor; + if (!$parametersAcceptor->getDeclaringClass()->isAnonymous()) { + $locationData['currentClassName'] = $parametersAcceptor->getDeclaringClass()->getName(); + } + } else { + $locationData['function'] = $parametersAcceptor; } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + $scope, + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $returnTypeNode), $returnTypeReferencedClasses), + ClassNameUsageLocation::from(ClassNameUsageLocation::RETURN_TYPE, $locationData), + $this->checkClassCaseSensitivity, + ), + ); if ($parametersAcceptor->getReturnType() instanceof NonexistentParentClassType) { - $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $parametersAcceptor->getReturnType()->describe(VerbosityLevel::typeOnly())))->line($returnTypeNode->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $parametersAcceptor->getReturnType()->describe(VerbosityLevel::typeOnly()))) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.noParent') + ->build(); } $templateTypeMap = $parametersAcceptor->getTemplateTypeMap(); @@ -335,8 +529,22 @@ private function checkParametersAcceptor( }); } + $returnType = $parametersAcceptor->getReturnType(); + if ($returnType instanceof ConditionalTypeForParameter && !$returnType->isNegated()) { + TypeTraverser::map($returnType, static function (Type $type, callable $traverse) use (&$templateTypes): Type { + if ($type instanceof TemplateType) { + unset($templateTypes[$type->getName()]); + return $traverse($type); + } + + return $traverse($type); + }); + } + foreach (array_keys($templateTypes) as $templateTypeName) { - $errors[] = RuleErrorBuilder::message(sprintf($templateTypeMissingInParameterMessage, $templateTypeName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf($templateTypeMissingInParameterMessage, $templateTypeName)) + ->identifier('method.templateTypeNotInParameter') + ->build(); } } @@ -345,13 +553,14 @@ private function checkParametersAcceptor( /** * @param Param[] $parameterNodes - * @return RuleError[] + * @return list */ private function checkRequiredParameterAfterOptional(array $parameterNodes): array { /** @var string|null $optionalParameter */ $optionalParameter = null; $errors = []; + $targetPhpVersion = null; foreach ($parameterNodes as $parameterNode) { if (!$parameterNode->var instanceof Variable) { throw new ShouldNotHappenException(); @@ -361,7 +570,17 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr } $parameterName = $parameterNode->var->name; if ($optionalParameter !== null && $parameterNode->default === null && !$parameterNode->variadic) { - $errors[] = RuleErrorBuilder::message(sprintf('Deprecated in PHP 8.0: Required parameter $%s follows optional parameter $%s.', $parameterName, $optionalParameter))->line($parameterNode->getStartLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Deprecated in PHP %s: Required parameter $%s follows optional parameter $%s.', + $targetPhpVersion ?? '8.0', + $parameterName, + $optionalParameter, + ), + )->line($parameterNode->getStartLine()) + ->identifier('parameter.requiredAfterOptional') + ->build(); + $targetPhpVersion = null; continue; } if ($parameterNode->default === null) { @@ -380,7 +599,35 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr $constantName = $defaultValue->name->toLowerString(); if ($constantName === 'null') { - continue; + if (!$this->phpVersion->deprecatesRequiredParameterAfterOptionalNullableAndDefaultNull()) { + continue; + } + + $parameterNodeType = $parameterNode->type; + + if ($parameterNodeType instanceof NullableType) { + $targetPhpVersion = '8.1'; + } + + if ($this->phpVersion->deprecatesRequiredParameterAfterOptionalUnionOrMixed()) { + $types = []; + + if ($parameterNodeType instanceof UnionType) { + $types = $parameterNodeType->types; + } elseif ($parameterNodeType instanceof Identifier) { + $types = [$parameterNodeType]; + } + + $nullOrMixed = array_filter($types, static fn (Identifier|Name|IntersectionType $type): bool => $type instanceof Identifier && (in_array($type->name, ['null', 'mixed'], true))); + + if (0 < count($nullOrMixed)) { + $targetPhpVersion = '8.3'; + } + } + + if ($targetPhpVersion === null) { + continue; + } } $optionalParameter = $parameterName; @@ -419,7 +666,7 @@ private function getParameterNode( */ private function getParameterReferencedClasses(ParameterReflection $parameter): array { - if (!$parameter instanceof ParameterReflectionWithPhpDocs) { + if (!$parameter instanceof ExtendedParameterReflection) { return $parameter->getType()->getReferencedClasses(); } @@ -427,9 +674,18 @@ private function getParameterReferencedClasses(ParameterReflection $parameter): return $parameter->getNativeType()->getReferencedClasses(); } + $moreClasses = []; + if ($parameter->getOutType() !== null) { + $moreClasses = array_merge($moreClasses, $parameter->getOutType()->getReferencedClasses()); + } + if ($parameter->getClosureThisType() !== null) { + $moreClasses = array_merge($moreClasses, $parameter->getClosureThisType()->getReferencedClasses()); + } + return array_merge( $parameter->getNativeType()->getReferencedClasses(), $parameter->getPhpDocType()->getReferencedClasses(), + $moreClasses, ); } @@ -438,7 +694,7 @@ private function getParameterReferencedClasses(ParameterReflection $parameter): */ private function getReturnTypeReferencedClasses(ParametersAcceptor $parametersAcceptor): array { - if (!$parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { + if (!$parametersAcceptor instanceof ExtendedParametersAcceptor) { return $parametersAcceptor->getReturnType()->getReferencedClasses(); } @@ -452,4 +708,61 @@ private function getReturnTypeReferencedClasses(ParametersAcceptor $parametersAc ); } + private function checkImplicitlyNullableType( + Identifier|Name|ComplexType|null $type, + ?Node\Expr $default, + int $order, + int $line, + string $name, + ): ?IdentifierRuleError + { + if (!$default instanceof ConstFetch) { + return null; + } + + if ($default->name->toLowerString() !== 'null') { + return null; + } + + if ($type === null) { + return null; + } + + if ($type instanceof NullableType || $type instanceof IntersectionType) { + return null; + } + + if (!$this->phpVersion->deprecatesImplicitlyNullableParameterTypes()) { + return null; + } + + if ($type instanceof Identifier && strtolower($type->name) === 'mixed') { + return null; + } + + if ($type instanceof Identifier && strtolower($type->name) === 'null') { + return null; + } + if ($type instanceof Name && $type->toLowerString() === 'null') { + return null; + } + + if ($type instanceof UnionType) { + foreach ($type->types as $innerType) { + if ($innerType instanceof Identifier && strtolower($innerType->name) === 'null') { + return null; + } + } + } + + return RuleErrorBuilder::message(sprintf( + 'Deprecated in PHP 8.4: Parameter #%d $%s (%s) is implicitly nullable via default value null.', + $order, + $name, + NodeTypePrinter::printType($type), + ))->line($line) + ->identifier('parameter.implicitlyNullable') + ->build(); + } + } diff --git a/src/Rules/FunctionReturnTypeCheck.php b/src/Rules/FunctionReturnTypeCheck.php index 20995fbc97..994b3943f2 100644 --- a/src/Rules/FunctionReturnTypeCheck.php +++ b/src/Rules/FunctionReturnTypeCheck.php @@ -6,15 +6,14 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -use PHPStan\Type\GenericTypeVariableResolver; +use PHPStan\Type\ErrorType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; -class FunctionReturnTypeCheck +final class FunctionReturnTypeCheck { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -22,7 +21,7 @@ public function __construct(private RuleLevelHelper $ruleLevelHelper) } /** - * @return RuleError[] + * @return list */ public function checkReturnType( Scope $scope, @@ -36,30 +35,25 @@ public function checkReturnType( bool $isGenerator, ): array { + $returnType = TypeUtils::resolveLateResolvableTypes($returnType); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { return [ RuleErrorBuilder::message($neverMessage) - ->line($returnNode->getLine()) + ->line($returnNode->getStartLine()) + ->identifier('return.never') ->build(), ]; } if ($isGenerator) { - if (!$returnType instanceof TypeWithClassName) { - return []; - } - - $returnType = GenericTypeVariableResolver::getType( - $returnType, - Generator::class, - 'TReturn', - ); - if ($returnType === null) { + $returnType = $returnType->getTemplateType(Generator::class, 'TReturn'); + if ($returnType instanceof ErrorType) { return []; } } - $isVoidSuperType = (new VoidType())->isSuperTypeOf($returnType); + $isVoidSuperType = $returnType->isVoid(); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType, null); if ($returnValue === null) { if (!$isVoidSuperType->no()) { @@ -70,10 +64,17 @@ public function checkReturnType( RuleErrorBuilder::message(sprintf( $emptyReturnStatementMessage, $returnType->describe($verbosityLevel), - ))->line($returnNode->getLine())->build(), + )) + ->line($returnNode->getStartLine()) + ->identifier('return.empty') + ->build(), ]; } + if ($returnNode instanceof Expr\Yield_ || $returnNode instanceof Expr\YieldFrom) { + return []; + } + $returnValueType = $scope->getType($returnValue); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType, $returnValueType); @@ -82,17 +83,25 @@ public function checkReturnType( RuleErrorBuilder::message(sprintf( $voidMessage, $returnValueType->describe($verbosityLevel), - ))->line($returnNode->getLine())->build(), + )) + ->line($returnNode->getStartLine()) + ->identifier('return.void') + ->build(), ]; } - if (!$this->ruleLevelHelper->accepts($returnType, $returnValueType, $scope->isDeclareStrictTypes())) { + $accepts = $this->ruleLevelHelper->accepts($returnType, $returnValueType, $scope->isDeclareStrictTypes()); + if (!$accepts->result) { return [ RuleErrorBuilder::message(sprintf( $typeMismatchMessage, $returnType->describe($verbosityLevel), $returnValueType->describe($verbosityLevel), - ))->line($returnNode->getLine())->build(), + )) + ->line($returnNode->getStartLine()) + ->identifier('return.type') + ->acceptsReasonsTip($accepts->reasons) + ->build(), ]; } diff --git a/src/Rules/Functions/ArrayFilterRule.php b/src/Rules/Functions/ArrayFilterRule.php new file mode 100644 index 0000000000..abcc1e9dc8 --- /dev/null +++ b/src/Rules/Functions/ArrayFilterRule.php @@ -0,0 +1,138 @@ + + */ +final class ArrayFilterRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'array_filter') { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + if ($normalizedFuncCall === null) { + return []; + } + + $args = $normalizedFuncCall->getArgs(); + if (count($args) !== 1) { + return []; + } + + if ($this->treatPhpDocTypesAsCertain) { + $arrayType = $scope->getType($args[0]->value); + } else { + $arrayType = $scope->getNativeType($args[0]->value); + } + + if ($arrayType->isIterableAtLeastOnce()->no()) { + $message = 'Parameter #1 $array (%s) to function array_filter is empty, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayFilter.empty'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if ($this->treatPhpDocTypesAsCertainTip && !$nativeArrayType->isIterableAtLeastOnce()->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + return [ + $errorBuilder->build(), + ]; + } + + $falsyType = StaticTypeFactory::falsey(); + $isSuperType = $falsyType->isSuperTypeOf($arrayType->getIterableValueType()); + + if ($isSuperType->no()) { + $message = 'Parameter #1 $array (%s) to function array_filter does not contain falsy values, the array will always stay the same.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayFilter.same'); + + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); + if ($this->treatPhpDocTypesAsCertainTip && !$isNativeSuperType->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + if ($isSuperType->yes()) { + $message = 'Parameter #1 $array (%s) to function array_filter contains falsy values only, the result will always be an empty array.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayFilter.alwaysEmpty'); + + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); + if ($this->treatPhpDocTypesAsCertainTip && !$isNativeSuperType->yes()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Functions/ArrayValuesRule.php b/src/Rules/Functions/ArrayValuesRule.php new file mode 100644 index 0000000000..679bcb4d4a --- /dev/null +++ b/src/Rules/Functions/ArrayValuesRule.php @@ -0,0 +1,114 @@ + + */ +final class ArrayValuesRule implements Rule +{ + + public function __construct( + private readonly ReflectionProvider $reflectionProvider, + private readonly bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'array_values') { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $args = $normalizedFuncCall->getArgs(); + if (count($args) === 0) { + return []; + } + + if ($this->treatPhpDocTypesAsCertain) { + $arrayType = $scope->getType($args[0]->value); + } else { + $arrayType = $scope->getNativeType($args[0]->value); + } + + if ($arrayType->isIterableAtLeastOnce()->no()) { + $message = 'Parameter #1 $array (%s) to function array_values is empty, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayValues.empty'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if ($this->treatPhpDocTypesAsCertainTip && !$nativeArrayType->isIterableAtLeastOnce()->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + if ($arrayType->isList()->yes()) { + $message = 'Parameter #1 $array (%s) of array_values is already a list, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayValues.list'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if ($this->treatPhpDocTypesAsCertainTip && !$nativeArrayType->isList()->yes()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index 1b4e1ddf03..67af9eb5e0 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -5,13 +5,14 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ -class ArrowFunctionAttributesRule implements Rule +final class ArrowFunctionAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Expr\ArrowFunction::class; + return InArrowFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php index 3606f67a71..cc7d673620 100644 --- a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class ArrowFunctionReturnNullsafeByRefRule implements Rule +final class ArrowFunctionReturnNullsafeByRefRule implements Rule { public function __construct(private NullsafeCheck $nullsafeCheck) @@ -34,7 +34,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Nullsafe cannot be returned by reference.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Nullsafe cannot be returned by reference.') + ->nonIgnorable() + ->identifier('nullsafe.byRef') + ->build(), ]; } diff --git a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php index 86bd340d54..045f66913b 100644 --- a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php @@ -9,14 +9,13 @@ use PHPStan\Rules\FunctionReturnTypeCheck; use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\Type; -use PHPStan\Type\VoidType; /** * @implements Rule */ -class ArrowFunctionReturnTypeRule implements Rule +final class ArrowFunctionReturnTypeRule implements Rule { public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) @@ -34,16 +33,25 @@ public function processNode(Node $node, Scope $scope): array throw new ShouldNotHappenException(); } - /** @var Type $returnType */ $returnType = $scope->getAnonymousFunctionReturnType(); $generatorType = new ObjectType(Generator::class); $originalNode = $node->getOriginalNode(); - $isVoidSuperType = (new VoidType())->isSuperTypeOf($returnType); + $isVoidSuperType = $returnType->isVoid(); if ($originalNode->returnType === null && $isVoidSuperType->yes()) { return []; } + $exprType = $scope->getType($originalNode->expr); + if ( + $returnType instanceof NeverType + && $returnType->isExplicit() + && $exprType instanceof NeverType + && $exprType->isExplicit() + ) { + return []; + } + return $this->returnTypeCheck->checkReturnType( $scope, $returnType, diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 0873fdc892..162894e266 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -12,6 +12,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\TrinaryLogic; use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; @@ -24,7 +25,7 @@ /** * @implements Rule */ -class CallCallablesRule implements Rule +final class CallCallablesRule implements Rule { public function __construct( @@ -65,20 +66,25 @@ public function processNode( return [ RuleErrorBuilder::message( sprintf('Trying to invoke %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())), - )->build(), + )->identifier('callable.nonCallable')->build(), ]; } if ($this->reportMaybes && $isCallable->maybe()) { return [ RuleErrorBuilder::message( sprintf('Trying to invoke %s but it might not be a callable.', $type->describe(VerbosityLevel::value())), - )->build(), + )->identifier('callable.nonCallable')->build(), ]; } $parametersAcceptors = $type->getCallableParametersAcceptors($scope); $messages = []; + $acceptsNamedArguments = TrinaryLogic::createYes(); + foreach ($parametersAcceptors as $parametersAcceptor) { + $acceptsNamedArguments = $acceptsNamedArguments->and($parametersAcceptor->acceptsNamedArguments()); + } + if ( count($parametersAcceptors) === 1 && $parametersAcceptors[0] instanceof InaccessibleMethod @@ -89,13 +95,14 @@ public function processNode( $method->isPrivate() ? 'private' : 'protected', $method->getName(), $method->getDeclaringClass()->getDisplayName(), - ))->build(); + ))->identifier('callable.inaccessibleMethod')->build(); } $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $node->getArgs(), $parametersAcceptors, + null, ); if ($type instanceof ClosureType) { @@ -111,21 +118,23 @@ public function processNode( $scope, false, $node, - [ - ucfirst($callableDescription) . ' invoked with %d parameter, %d required.', - ucfirst($callableDescription) . ' invoked with %d parameters, %d required.', - ucfirst($callableDescription) . ' invoked with %d parameter, at least %d required.', - ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', - ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', - ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of ' . $callableDescription . ' expects %s, %s given.', - 'Result of ' . $callableDescription . ' (void) is used.', - 'Parameter %s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to ' . $callableDescription, - 'Missing parameter $%s in call to ' . $callableDescription . '.', - 'Unknown parameter $%s in call to ' . $callableDescription . '.', - 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', - ], + 'callable', + $acceptsNamedArguments, + ucfirst($callableDescription) . ' invoked with %d parameter, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', + '%s of ' . $callableDescription . ' expects %s, %s given.', + 'Result of ' . $callableDescription . ' (void) is used.', + '%s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to ' . $callableDescription, + 'Missing parameter $%s in call to ' . $callableDescription . '.', + 'Unknown parameter $%s in call to ' . $callableDescription . '.', + 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', + '%s of ' . $callableDescription . ' contains unresolvable type.', + ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ), ); } diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 71dd7e06e2..63e1b93c7a 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -14,7 +14,7 @@ /** * @implements Rule */ -class CallToFunctionParametersRule implements Rule +final class CallToFunctionParametersRule implements Rule { public function __construct(private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check) @@ -44,25 +44,28 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getArgs(), $function->getVariants(), + $function->getNamedArgumentsVariants(), ), $scope, $function->isBuiltin(), $node, - [ - '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.', - ], + 'function', + $function->acceptsNamedArguments(), + '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.', + '%s of function ' . $functionName . ' expects %s, %s given.', + 'Result of function ' . $functionName . ' (void) is used.', + '%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.', + '%s of function ' . $functionName . ' contains unresolvable type.', + 'Function ' . $functionName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ); } diff --git a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php index 32b0fb7d77..23338924eb 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -3,21 +3,39 @@ namespace PHPStan\Rules\Functions; use PhpParser\Node; +use PhpParser\Node\Arg; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; -use PHPStan\Type\VoidType; +use PHPStan\Type\Type; use function in_array; use function sprintf; /** * @implements Rule */ -class CallToFunctionStatementWithoutSideEffectsRule implements Rule +final class CallToFunctionStatementWithoutSideEffectsRule implements Rule { + private const SIDE_EFFECT_FLIP_PARAMETERS = [ + // functionName => [name, pos, testName] + 'print_r' => ['return', 1, 'isTruthy'], + 'var_export' => ['return', 1, 'isTruthy'], + 'highlight_string' => ['return', 1, 'isTruthy'], + + ]; + + public const PHPSTAN_TESTING_FUNCTIONS = [ + 'PHPStan\\dumpType', + 'PHPStan\\dumpPhpDocType', + 'PHPStan\\debugScope', + 'PHPStan\\Testing\\assertType', + 'PHPStan\\Testing\\assertNativeType', + 'PHPStan\\Testing\\assertVariableCertainty', + ]; + public function __construct(private ReflectionProvider $reflectionProvider) { } @@ -43,10 +61,62 @@ public function processNode(Node $node, Scope $scope): array } $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); - if ($function->hasSideEffects()->no() || $node->expr->isFirstClassCallable()) { + $functionName = $function->getName(); + $functionHasSideEffects = !$function->hasSideEffects()->no(); + + if (in_array($functionName, self::PHPSTAN_TESTING_FUNCTIONS, true)) { + return []; + } + + if (isset(self::SIDE_EFFECT_FLIP_PARAMETERS[$functionName])) { + [ + $flipParameterName, + $flipParameterPosition, + $testName, + ] = self::SIDE_EFFECT_FLIP_PARAMETERS[$functionName]; + + $sideEffectFlipped = false; + $hasNamedParameter = false; + $checker = [ + 'isNotNull' => static fn (Type $type) => $type->isNull()->no(), + 'isTruthy' => static fn (Type $type) => $type->toBoolean()->isTrue()->yes(), + ][$testName]; + + foreach ($funcCall->getRawArgs() as $i => $arg) { + if (!$arg instanceof Arg) { + return []; + } + + $isFlipParameter = false; + + if ($arg->name !== null) { + $hasNamedParameter = true; + if ($arg->name->name === $flipParameterName) { + $isFlipParameter = true; + } + } + + if (!$hasNamedParameter && $i === $flipParameterPosition) { + $isFlipParameter = true; + } + + if ($isFlipParameter) { + $sideEffectFlipped = $checker($scope->getType($arg->value)); + break; + } + } + + if (!$sideEffectFlipped) { + return []; + } + + $functionHasSideEffects = false; + } + + if (!$functionHasSideEffects || $node->expr->isFirstClassCallable()) { if (!$node->expr->isFirstClassCallable()) { $throwsType = $function->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { + if ($throwsType !== null && !$throwsType->isVoid()->yes()) { return []; } } @@ -56,19 +126,11 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (in_array($function->getName(), [ - 'PHPStan\\Testing\\assertType', - 'PHPStan\\Testing\\assertNativeType', - 'PHPStan\\Testing\\assertVariableCertainty', - ], true)) { - return []; - } - return [ RuleErrorBuilder::message(sprintf( 'Call to function %s() on a separate line has no effect.', $function->getName(), - ))->build(), + ))->identifier('function.resultUnused')->build(), ]; } diff --git a/src/Rules/Functions/CallToNonExistentFunctionRule.php b/src/Rules/Functions/CallToNonExistentFunctionRule.php index 7fc4443838..9805a445b8 100644 --- a/src/Rules/Functions/CallToNonExistentFunctionRule.php +++ b/src/Rules/Functions/CallToNonExistentFunctionRule.php @@ -14,12 +14,13 @@ /** * @implements Rule */ -class CallToNonExistentFunctionRule implements Rule +final class CallToNonExistentFunctionRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, private bool $checkFunctionNameCase, + private bool $discoveringSymbolsTip, ) { } @@ -40,8 +41,15 @@ public function processNode(Node $node, Scope $scope): array return []; } + $errorBuilder = RuleErrorBuilder::message(sprintf('Function %s not found.', (string) $node->name)) + ->identifier('function.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf('Function %s not found.', (string) $node->name))->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; } @@ -60,7 +68,7 @@ public function processNode(Node $node, Scope $scope): array 'Call to function %s() with incorrect case: %s', $function->getName(), $name, - ))->build(), + ))->identifier('function.nameCase')->build(), ]; } } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php new file mode 100644 index 0000000000..5eb21c2128 --- /dev/null +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -0,0 +1,88 @@ + + */ +final class CallUserFuncRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private FunctionCallParametersCheck $check, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + if (count($node->getArgs()) === 0) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'call_user_func') { + return []; + } + + $result = ArgumentsNormalizer::reorderCallUserFuncArguments( + $node, + $scope, + ); + if ($result === null) { + return []; + } + [$parametersAcceptor, $funcCall, $acceptsNamedArguments] = $result; + + $callableDescription = 'callable passed to call_user_func()'; + + return $this->check->check( + $parametersAcceptor, + $scope, + false, + $funcCall, + 'function', + $acceptsNamedArguments, + ucfirst($callableDescription) . ' invoked with %d parameter, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', + '%s of ' . $callableDescription . ' expects %s, %s given.', + 'Result of ' . $callableDescription . ' (void) is used.', + '%s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to ' . $callableDescription, + 'Missing parameter $%s in call to ' . $callableDescription . '.', + 'Unknown parameter $%s in call to ' . $callableDescription . '.', + 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', + '%s of ' . $callableDescription . ' contains unresolvable type.', + ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + ); + } + +} diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index 170d9ad05e..fc206e19d4 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -5,13 +5,14 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClosureNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ -class ClosureAttributesRule implements Rule +final class ClosureAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Expr\Closure::class; + return InClosureNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/ClosureReturnTypeRule.php b/src/Rules/Functions/ClosureReturnTypeRule.php index 22603a10c7..415da4d6bc 100644 --- a/src/Rules/Functions/ClosureReturnTypeRule.php +++ b/src/Rules/Functions/ClosureReturnTypeRule.php @@ -7,14 +7,12 @@ use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Rules\FunctionReturnTypeCheck; use PHPStan\Rules\Rule; -use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use function count; /** * @implements Rule */ -class ClosureReturnTypeRule implements Rule +final class ClosureReturnTypeRule implements Rule { public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) @@ -32,7 +30,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var Type $returnType */ $returnType = $scope->getAnonymousFunctionReturnType(); $containsNull = TypeCombinator::containsNull($returnType); $hasNativeTypehint = $node->getClosureExpr()->returnType !== null; @@ -53,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array 'Anonymous function with return type void returns %s but should not return anything.', 'Anonymous function should return %s but returns %s.', 'Anonymous function should never return but return statement found.', - count($node->getYieldStatements()) > 0, + $node->isGenerator(), ); foreach ($returnMessages as $returnMessage) { diff --git a/src/Rules/Functions/DefineParametersRule.php b/src/Rules/Functions/DefineParametersRule.php index ec5a2590b3..83886f1ea2 100644 --- a/src/Rules/Functions/DefineParametersRule.php +++ b/src/Rules/Functions/DefineParametersRule.php @@ -14,7 +14,7 @@ /** * @implements Rule */ -class DefineParametersRule implements Rule +final class DefineParametersRule implements Rule { public function __construct(private PhpVersion $phpVersion) @@ -47,7 +47,10 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( 'Argument #3 ($case_insensitive) is ignored since declaration of case-insensitive constants is no longer supported.', - )->line($node->getLine())->build(), + ) + ->line($node->getStartLine()) + ->identifier('argument.unused') + ->build(), ]; } diff --git a/src/Rules/Functions/DuplicateFunctionDeclarationRule.php b/src/Rules/Functions/DuplicateFunctionDeclarationRule.php new file mode 100644 index 0000000000..6cf2b6c5c1 --- /dev/null +++ b/src/Rules/Functions/DuplicateFunctionDeclarationRule.php @@ -0,0 +1,59 @@ + + */ +final class DuplicateFunctionDeclarationRule implements Rule +{ + + public function __construct(private Reflector $reflector, private RelativePathHelper $relativePathHelper) + { + } + + public function getNodeType(): string + { + return InFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $thisFunction = $node->getFunctionReflection(); + $allFunctions = $this->reflector->reflectAllFunctions(); + $filteredFunctions = []; + foreach ($allFunctions as $reflectionFunction) { + if ($reflectionFunction->getName() !== $thisFunction->getName()) { + continue; + } + + $filteredFunctions[] = $reflectionFunction; + } + + if (count($filteredFunctions) < 2) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + "Function %s declared multiple times:\n%s", + $thisFunction->getName(), + implode("\n", array_map(fn (ReflectionFunction $function) => sprintf('- %s:%d', $this->relativePathHelper->getRelativePath($function->getFileName() ?? 'unknown'), $function->getStartLine()), $filteredFunctions)), + ))->identifier('function.duplicate')->build(), + ]; + } + +} diff --git a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php index ecaf404573..0b29af004c 100644 --- a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php @@ -4,16 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\NonAcceptingNeverType; +use PHPStan\Type\ParserNodeTypeToPHPStanType; +use function array_merge; /** * @implements Rule */ -class ExistingClassesInArrowFunctionTypehintsRule implements Rule +final class ExistingClassesInArrowFunctionTypehintsRule implements Rule { - public function __construct(private FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check, private PhpVersion $phpVersion) { } @@ -24,7 +29,18 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - return $this->check->checkAnonymousFunction( + $messages = []; + if ($node->returnType !== null && !$this->phpVersion->supportsNeverReturnTypeInArrowFunction()) { + $returnType = ParserNodeTypeToPHPStanType::resolve($node->returnType, $scope->isInClass() ? $scope->getClassReflection() : null); + if ($returnType instanceof NonAcceptingNeverType) { + $messages[] = RuleErrorBuilder::message('Never return type in arrow function is supported only on PHP 8.2 and later.') + ->identifier('return.neverTypeNotSupported') + ->nonIgnorable() + ->build(); + } + } + + return array_merge($messages, $this->check->checkAnonymousFunction( $scope, $node->getParams(), $node->getReturnType(), @@ -33,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', 'Parameter $%s of anonymous function has unresolvable native type.', 'Anonymous function has unresolvable native return type.', - ); + )); } } diff --git a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php index 2c6dbd3ad0..0c5acfbd07 100644 --- a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class ExistingClassesInClosureTypehintsRule implements Rule +final class ExistingClassesInClosureTypehintsRule implements Rule { public function __construct(private FunctionDefinitionCheck $check) diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 4314e6f69d..7f83eea193 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\Rule; use function sprintf; @@ -14,7 +13,7 @@ /** * @implements Rule */ -class ExistingClassesInTypehintsRule implements Rule +final class ExistingClassesInTypehintsRule implements Rule { public function __construct(private FunctionDefinitionCheck $check) @@ -28,15 +27,12 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->getFunction() instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - - $functionName = SprintfHelper::escapeFormatString($scope->getFunction()->getName()); + $functionName = SprintfHelper::escapeFormatString($node->getFunctionReflection()->getName()); return $this->check->checkFunction( + $scope, $node->getOriginalNode(), - $scope->getFunction(), + $node->getFunctionReflection(), sprintf( 'Parameter $%%s of function %s() has invalid type %%s.', $functionName, diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index 2ccf122ac2..9c5ad24d73 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -5,13 +5,14 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InFunctionNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ -class FunctionAttributesRule implements Rule +final class FunctionAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\Function_::class; + return InFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/FunctionCallableRule.php b/src/Rules/Functions/FunctionCallableRule.php index 5905fc6dec..827cf6e876 100644 --- a/src/Rules/Functions/FunctionCallableRule.php +++ b/src/Rules/Functions/FunctionCallableRule.php @@ -20,7 +20,7 @@ /** * @implements Rule */ -class FunctionCallableRule implements Rule +final class FunctionCallableRule implements Rule { public function __construct(private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, private PhpVersion $phpVersion, private bool $checkFunctionNameCase, private bool $reportMaybes) @@ -38,6 +38,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') ->nonIgnorable() + ->identifier('callable.notSupported') ->build(), ]; } @@ -60,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array 'Call to function %s() with incorrect case: %s', $function->getName(), $functionNameName, - ))->build(), + ))->identifier('function.nameCase')->build(), ]; } } @@ -74,6 +75,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf('Function %s not found.', $functionNameName)) + ->identifier('function.notFound') ->build(), ]; } @@ -94,14 +96,14 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Creating callable from %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())), - )->build(), + )->identifier('callable.nonCallable')->build(), ]; } if ($this->reportMaybes && $isCallable->maybe()) { return [ RuleErrorBuilder::message( sprintf('Creating callable from %s but it might not be a callable.', $type->describe(VerbosityLevel::value())), - )->build(), + )->identifier('callable.nonCallable')->build(), ]; } diff --git a/src/Rules/Functions/ImplodeFunctionRule.php b/src/Rules/Functions/ImplodeFunctionRule.php deleted file mode 100644 index 3d75e6d789..0000000000 --- a/src/Rules/Functions/ImplodeFunctionRule.php +++ /dev/null @@ -1,78 +0,0 @@ - - */ -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/ImplodeParameterCastableToStringRule.php b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php new file mode 100644 index 0000000000..724a9ab079 --- /dev/null +++ b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php @@ -0,0 +1,117 @@ + + */ +final class ImplodeParameterCastableToStringRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + if (!in_array($functionName, ['implode', 'join'], true)) { + return []; + } + + $origArgs = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $normalizedArgs = $normalizedFuncCall->getArgs(); + $errorMessage = 'Parameter %s of function %s expects array, %s given.'; + if (count($normalizedArgs) === 1) { + $argsToCheck = [0 => $normalizedArgs[0]]; + } elseif (count($normalizedArgs) === 2) { + $argsToCheck = [1 => $normalizedArgs[1]]; + } else { + return []; + } + + $origNamedArgs = []; + foreach ($origArgs as $arg) { + if ($arg->unpack || $arg->name === null) { + continue; + } + + $origNamedArgs[$arg->name->toString()] = $arg; + } + + $errors = []; + + foreach ($argsToCheck as $argIdx => $arg) { + // implode has weird variants, so $array has to be fixed. It's especially weird with named arguments. + if (array_key_exists('array', $origNamedArgs)) { + $argName = '$array'; + } elseif (array_key_exists('separator', $origNamedArgs) && count($origArgs) === 1) { + $argName = '$separator'; + } else { + $argName = sprintf('#%d $array', $argIdx + 1); + } + + $error = $this->parameterCastableToStringCheck->checkParameter( + $arg, + $scope, + $errorMessage, + static fn (Type $t) => $t->toString(), + $functionName, + $argName, + ); + + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php new file mode 100644 index 0000000000..80ab58e8bb --- /dev/null +++ b/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php @@ -0,0 +1,70 @@ + + */ +final class IncompatibleArrowFunctionDefaultParameterTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InArrowFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $parameters = $node->getClosureType()->getParameters(); + + $errors = []; + foreach ($node->getOriginalNode()->getParams() as $paramI => $param) { + if ($param->default === null) { + continue; + } + if ( + $param->var instanceof Node\Expr\Error + || !is_string($param->var->name) + ) { + throw new ShouldNotHappenException(); + } + + $defaultValueType = $scope->getType($param->default); + $parameterType = $parameters[$paramI]->getType(); + $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); + + $accepts = $parameterType->accepts($defaultValueType, true); + if ($accepts->yes()) { + continue; + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType); + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Default value of the parameter #%d $%s (%s) of anonymous function is incompatible with type %s.', + $paramI + 1, + $param->var->name, + $defaultValueType->describe($verbosityLevel), + $parameterType->describe($verbosityLevel), + )) + ->line($param->getStartLine()) + ->identifier('parameter.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php new file mode 100644 index 0000000000..f4d3965026 --- /dev/null +++ b/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php @@ -0,0 +1,70 @@ + + */ +final class IncompatibleClosureDefaultParameterTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InClosureNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $parameters = $node->getClosureType()->getParameters(); + + $errors = []; + foreach ($node->getOriginalNode()->getParams() as $paramI => $param) { + if ($param->default === null) { + continue; + } + if ( + $param->var instanceof Node\Expr\Error + || !is_string($param->var->name) + ) { + throw new ShouldNotHappenException(); + } + + $defaultValueType = $scope->getType($param->default); + $parameterType = $parameters[$paramI]->getType(); + $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); + + $accepts = $parameterType->accepts($defaultValueType, true); + if ($accepts->yes()) { + continue; + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType); + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Default value of the parameter #%d $%s (%s) of anonymous function is incompatible with type %s.', + $paramI + 1, + $param->var->name, + $defaultValueType->describe($verbosityLevel), + $parameterType->describe($verbosityLevel), + )) + ->line($param->getStartLine()) + ->identifier('parameter.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php index 4d31ea77a4..68f9fffc6d 100644 --- a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php @@ -5,8 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -18,7 +16,7 @@ /** * @implements Rule */ -class IncompatibleDefaultParameterTypeRule implements Rule +final class IncompatibleDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -28,12 +26,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $function = $scope->getFunction(); - if (!$function instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - $parameters = ParametersAcceptorSelector::selectSingle($function->getVariants()); - + $function = $node->getFunctionReflection(); $errors = []; foreach ($node->getOriginalNode()->getParams() as $paramI => $param) { if ($param->default === null) { @@ -47,10 +40,11 @@ public function processNode(Node $node, Scope $scope): array } $defaultValueType = $scope->getType($param->default); - $parameterType = $parameters->getParameters()[$paramI]->getType(); + $parameterType = $function->getParameters()[$paramI]->getType(); $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); - if ($parameterType->accepts($defaultValueType, true)->yes()) { + $accepts = $parameterType->accepts($defaultValueType, true); + if ($accepts->yes()) { continue; } @@ -63,7 +57,11 @@ public function processNode(Node $node, Scope $scope): array $defaultValueType->describe($verbosityLevel), $function->getName(), $parameterType->describe($verbosityLevel), - ))->line($param->getLine())->build(); + )) + ->line($param->getStartLine()) + ->identifier('parameter.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(); } return $errors; diff --git a/src/Rules/Functions/InnerFunctionRule.php b/src/Rules/Functions/InnerFunctionRule.php index 9c58ea233a..04694b4447 100644 --- a/src/Rules/Functions/InnerFunctionRule.php +++ b/src/Rules/Functions/InnerFunctionRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class InnerFunctionRule implements Rule +final class InnerFunctionRule implements Rule { public function getNodeType(): string @@ -28,7 +28,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( 'Inner named functions are not supported by PHPStan. Consider refactoring to an anonymous function, class method, or a top-level-defined function. See issue #165 (https://github.com/phpstan/phpstan/issues/165) for more details.', - )->build(), + )->identifier('function.inner')->build(), ]; } diff --git a/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php new file mode 100644 index 0000000000..1a823e18fe --- /dev/null +++ b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php @@ -0,0 +1,86 @@ + + */ +final class InvalidLexicalVariablesInClosureUseRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\Closure::class; + } + + /** + * @param Node\Expr\Closure $node + */ + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + $params = array_filter(array_map( + static function (Node\Param $param) { + if (!$param->var instanceof Node\Expr\Variable) { + return false; + } + + if (!is_string($param->var->name)) { + return false; + } + + return $param->var->name; + }, + $node->getParams(), + ), static fn ($name) => $name !== false); + + foreach ($node->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $var = $use->var->name; + + if ($var === 'this') { + $errors[] = RuleErrorBuilder::message('Cannot use $this as lexical variable.') + ->line($use->getStartLine()) + ->identifier('closure.useThis') + ->nonIgnorable() + ->build(); + continue; + } + + if (in_array($var, Scope::SUPERGLOBAL_VARIABLES, true)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot use superglobal variable $%s as lexical variable.', $var)) + ->line($use->getStartLine()) + ->identifier('closure.useSuperGlobal') + ->nonIgnorable() + ->build(); + continue; + } + + if (!in_array($var, $params, true)) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf('Cannot use lexical variable $%s since a parameter with the same name already exists.', $var)) + ->line($use->getStartLine()) + ->identifier('closure.useDuplicate') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php index 0a02fbaca7..586ce8727c 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -6,16 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -37,15 +34,25 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - + $functionReflection = $node->getFunctionReflection(); $messages = []; - foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameterReflection) { - foreach ($this->checkFunctionParameter($functionReflection, $parameterReflection) as $parameterMessage) { + foreach ($functionReflection->getParameters() as $parameterReflection) { + foreach ($this->checkFunctionParameter($functionReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + + if ($parameterReflection->getClosureThisType() !== null) { + foreach ($this->checkFunctionParameter($functionReflection, sprintf('@param-closure-this PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getClosureThisType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + } + + if ($parameterReflection->getOutType() === null) { + continue; + } + + foreach ($this->checkFunctionParameter($functionReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) { $messages[] = $parameterMessage; } } @@ -54,19 +61,17 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ - private function checkFunctionParameter(FunctionReflection $functionReflection, ParameterReflection $parameterReflection): array + private function checkFunctionParameter(FunctionReflection $functionReflection, string $parameterMessage, Type $parameterType): array { - $parameterType = $parameterReflection->getType(); - if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no type specified.', + 'Function %s() has %s with no type specified.', $functionReflection->getName(), - $parameterReflection->getName(), - ))->build(), + $parameterMessage, + ))->identifier('missingType.parameter')->build(), ]; } @@ -74,30 +79,35 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no value type specified in iterable type %s.', + 'Function %s() has %s with no value type specified in iterable type %s.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $iterableTypeDescription, - ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with generic %s but does not specify its types: %s', + 'Function %s() has %s with generic %s but does not specify its types: %s', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $name, - implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no signature specified for %s.', + 'Function %s() has %s with no signature specified for %s.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php index 7166ebad1a..648636973e 100644 --- a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php @@ -5,14 +5,11 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -34,26 +31,25 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $functionReflection = $node->getFunctionReflection(); + $returnType = $functionReflection->getReturnType(); if ($returnType instanceof MixedType && !$returnType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( 'Function %s() has no return type specified.', $functionReflection->getName(), - ))->build(), + ))->identifier('missingType.return')->build(), ]; } $messages = []; foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($returnType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); - $messages[] = RuleErrorBuilder::message(sprintf('Function %s() return type has no value type specified in iterable type %s.', $functionReflection->getName(), $iterableTypeDescription))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + $messages[] = RuleErrorBuilder::message(sprintf('Function %s() return type has no value type specified in iterable type %s.', $functionReflection->getName(), $iterableTypeDescription)) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($returnType) as [$name, $genericTypeNames]) { @@ -61,8 +57,10 @@ 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), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($returnType) as $callableType) { @@ -70,7 +68,7 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() return type has no signature specified for %s.', $functionReflection->getName(), $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php index aeadb1cfb6..02006c1619 100644 --- a/src/Rules/Functions/ParamAttributesRule.php +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -7,12 +7,11 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; -use function count; /** * @implements Rule */ -class ParamAttributesRule implements Rule +final class ParamAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) @@ -27,25 +26,16 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $targetName = 'parameter'; + $targetType = Attribute::TARGET_PARAMETER; if ($node->flags !== 0) { $targetName = 'parameter or property'; - - $propertyTargetErrors = $this->attributesCheck->check( - $scope, - $node->attrGroups, - Attribute::TARGET_PROPERTY, - $targetName, - ); - - if (count($propertyTargetErrors) === 0) { - return $propertyTargetErrors; - } + $targetType |= Attribute::TARGET_PROPERTY; } return $this->attributesCheck->check( $scope, $node->attrGroups, - Attribute::TARGET_PARAMETER, + $targetType, $targetName, ); } diff --git a/src/Rules/Functions/ParameterCastableToNumberRule.php b/src/Rules/Functions/ParameterCastableToNumberRule.php new file mode 100644 index 0000000000..640c73a440 --- /dev/null +++ b/src/Rules/Functions/ParameterCastableToNumberRule.php @@ -0,0 +1,84 @@ + + */ +final class ParameterCastableToNumberRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + + if (!in_array($functionName, ['array_sum', 'array_product'], true)) { + return []; + } + + $origArgs = $node->getArgs(); + + if (count($origArgs) !== 1) { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $errorMessage = 'Parameter %s of function %s expects an array of values castable to number, %s given.'; + $functionParameters = $parametersAcceptor->getParameters(); + $error = $this->parameterCastableToStringCheck->checkParameter( + $origArgs[0], + $scope, + $errorMessage, + static fn (Type $t) => $t->toNumber(), + $functionName, + $this->parameterCastableToStringCheck->getParameterName( + $origArgs[0], + 0, + $functionParameters[0] ?? null, + ), + ); + + return $error !== null + ? [$error] + : []; + } + +} diff --git a/src/Rules/Functions/ParameterCastableToStringRule.php b/src/Rules/Functions/ParameterCastableToStringRule.php new file mode 100644 index 0000000000..d76428ff5d --- /dev/null +++ b/src/Rules/Functions/ParameterCastableToStringRule.php @@ -0,0 +1,118 @@ + + */ +final class ParameterCastableToStringRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + $checkAllArgsFunctions = ['array_intersect', 'array_intersect_assoc', 'array_diff', 'array_diff_assoc']; + $checkFirstArgFunctions = [ + 'array_combine', + 'natcasesort', + 'natsort', + 'array_count_values', + 'array_fill_keys', + ]; + + if ( + !in_array($functionName, $checkAllArgsFunctions, true) + && !in_array($functionName, $checkFirstArgFunctions, true) + ) { + return []; + } + + $origArgs = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $errorMessage = 'Parameter %s of function %s expects an array of values castable to string, %s given.'; + $functionParameters = $parametersAcceptor->getParameters(); + + if (in_array($functionName, $checkAllArgsFunctions, true)) { + $argsToCheck = $origArgs; + } elseif (in_array($functionName, $checkFirstArgFunctions, true)) { + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $normalizedArgs = $normalizedFuncCall->getArgs(); + if (!array_key_exists(0, $normalizedArgs)) { + return []; + } + $argsToCheck = [0 => $normalizedArgs[0]]; + } else { + return []; + } + + $errors = []; + + foreach ($argsToCheck as $argIdx => $arg) { + $error = $this->parameterCastableToStringCheck->checkParameter( + $arg, + $scope, + $errorMessage, + static fn (Type $t) => $t->toString(), + $functionName, + $this->parameterCastableToStringCheck->getParameterName( + $arg, + $argIdx, + $functionParameters[$argIdx] ?? null, + ), + ); + + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/PrintfArrayParametersRule.php b/src/Rules/Functions/PrintfArrayParametersRule.php new file mode 100644 index 0000000000..07adcf6b36 --- /dev/null +++ b/src/Rules/Functions/PrintfArrayParametersRule.php @@ -0,0 +1,188 @@ + + */ +final class PrintfArrayParametersRule implements Rule +{ + + public function __construct( + private PrintfHelper $printfHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $name = $functionReflection->getName(); + if (!in_array($name, ['vprintf', 'vsprintf'], true)) { + return []; + } + + $args = $node->getArgs(); + $argsCount = count($args); + if ($argsCount < 1) { + return []; // caught by CallToFunctionParametersRule + } + + $formatArgType = $scope->getType($args[0]->value); + $placeHoldersCounts = []; + foreach ($formatArgType->getConstantStrings() as $formatString) { + $format = $formatString->getValue(); + + $placeHoldersCounts[] = $this->printfHelper->getPrintfPlaceholdersCount($format); + } + + if ($placeHoldersCounts === []) { + return []; + } + + $minCount = min($placeHoldersCounts); + $maxCount = max($placeHoldersCounts); + if ($minCount === $maxCount) { + $placeHoldersCount = new ConstantIntegerType($minCount); + } else { + $placeHoldersCount = IntegerRangeType::fromInterval($minCount, $maxCount); + + if (!$placeHoldersCount instanceof IntegerRangeType && !$placeHoldersCount instanceof ConstantIntegerType) { + return []; + } + } + + $formatArgsCounts = []; + if (isset($args[1])) { + $formatArgsType = $scope->getType($args[1]->value); + + $constantArrays = $formatArgsType->getConstantArrays(); + if ($constantArrays === []) { + $formatArgsCounts[] = new IntegerType(); + } + foreach ($constantArrays as $constantArray) { + $formatArgsCounts[] = $constantArray->getArraySize(); + } + } + + if ($formatArgsCounts === []) { + $formatArgsCount = new ConstantIntegerType(0); + } else { + $formatArgsCount = TypeCombinator::union(...$formatArgsCounts); + + if (!$formatArgsCount instanceof IntegerRangeType && !$formatArgsCount instanceof ConstantIntegerType) { + return []; + } + } + + if (!$this->placeholdersMatchesArgsCount($placeHoldersCount, $formatArgsCount)) { + + if ($placeHoldersCount instanceof IntegerRangeType) { + $placeholders = $this->getIntegerRangeAsString($placeHoldersCount); + $singlePlaceholder = false; + } else { + $placeholders = $placeHoldersCount->getValue(); + $singlePlaceholder = $placeholders === 1; + } + + if ($formatArgsCount instanceof IntegerRangeType) { + $values = $this->getIntegerRangeAsString($formatArgsCount); + $singleValue = false; + } else { + $values = $formatArgsCount->getValue(); + $singleValue = $values === 1; + } + + return [ + RuleErrorBuilder::message(sprintf( + sprintf( + '%s, %s.', + $singlePlaceholder ? 'Call to %s contains %d placeholder' : 'Call to %s contains %s placeholders', + $singleValue ? '%d value given' : '%s values given', + ), + $name, + $placeholders, + $values, + ))->identifier(sprintf('argument.%s', $name))->build(), + ]; + } + + return []; + } + + private function placeholdersMatchesArgsCount(ConstantIntegerType|IntegerRangeType $placeHoldersCount, ConstantIntegerType|IntegerRangeType $formatArgsCount): bool + { + if ($placeHoldersCount instanceof ConstantIntegerType) { + if ($formatArgsCount instanceof ConstantIntegerType) { + return $placeHoldersCount->getValue() === $formatArgsCount->getValue(); + } + + // Zero placeholders + array + if ($placeHoldersCount->getValue() === 0) { + return true; + } + + return false; + } + + if ( + $formatArgsCount instanceof IntegerRangeType + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($placeHoldersCount)->yes() + ) { + if ($formatArgsCount->getMin() !== null && $formatArgsCount->getMax() !== null) { + // constant array + return $placeHoldersCount->isSuperTypeOf($formatArgsCount)->yes(); + } + + // general array + return IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($formatArgsCount)->yes(); + } + + return false; + } + + private function getIntegerRangeAsString(IntegerRangeType $range): string + { + if ($range->getMin() !== null && $range->getMax() !== null) { + return $range->getMin() . '-' . $range->getMax(); + } elseif ($range->getMin() !== null) { + return $range->getMin() . ' or more'; + } elseif ($range->getMax() !== null) { + return $range->getMax() . ' or less'; + } + + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Functions/PrintfHelper.php b/src/Rules/Functions/PrintfHelper.php new file mode 100644 index 0000000000..a5d4571f76 --- /dev/null +++ b/src/Rules/Functions/PrintfHelper.php @@ -0,0 +1,75 @@ +getPlaceholdersCount('(?:[bs%s]|l?[cdeEgfFGouxX])', $format); + } + + public function getScanfPlaceholdersCount(string $format): int + { + return $this->getPlaceholdersCount('(?:[cdDeEfinosuxX%s]|\[[^\]]+\])', $format); + } + + private function getPlaceholdersCount(string $specifiersPattern, string $format): int + { + $addSpecifier = ''; + if ($this->phpVersion->supportsHhPrintfSpecifier()) { + $addSpecifier .= 'hH'; + } + + $specifiers = sprintf($specifiersPattern, $addSpecifier); + + $pattern = '~(?%*)%(?:(?\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?(?\*)?-?\d*(?:\.(?:\d+|(?\*))?)?' . $specifiers . '~'; + + $matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER); + + if (count($matches) === 0) { + return 0; + } + + $placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0); + + if (count($placeholders) === 0) { + return 0; + } + + $maxPositionedNumber = 0; + $maxOrdinaryNumber = 0; + foreach ($placeholders as $placeholder) { + if (isset($placeholder['width']) && $placeholder['width'] !== '') { + $maxOrdinaryNumber++; + } + + if (isset($placeholder['precision']) && $placeholder['precision'] !== '') { + $maxOrdinaryNumber++; + } + + if (isset($placeholder['position']) && $placeholder['position'] !== '') { + $maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber); + } else { + $maxOrdinaryNumber++; + } + } + + return max($maxPositionedNumber, $maxOrdinaryNumber); + } + +} diff --git a/src/Rules/Functions/PrintfParametersRule.php b/src/Rules/Functions/PrintfParametersRule.php index b6f0552b65..a80b44b995 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -2,30 +2,40 @@ 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\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\TypeUtils; -use function array_filter; +use function array_key_exists; use function count; use function in_array; -use function max; use function sprintf; -use function strlen; -use function strtolower; -use const PREG_SET_ORDER; /** * @implements Rule */ -class PrintfParametersRule implements Rule +final class PrintfParametersRule implements Rule { - public function __construct(private PhpVersion $phpVersion) + private const FORMAT_ARGUMENT_POSITIONS = [ + 'printf' => 0, + 'sprintf' => 0, + 'sscanf' => 1, + 'fscanf' => 1, + ]; + private const MINIMUM_NUMBER_OF_ARGUMENTS = [ + 'printf' => 1, + 'sprintf' => 1, + 'sscanf' => 3, + 'fscanf' => 3, + ]; + + public function __construct( + private PrintfHelper $printfHelper, + private ReflectionProvider $reflectionProvider, + ) { } @@ -40,25 +50,17 @@ public function processNode(Node $node, Scope $scope): array return []; } - $functionsArgumentPositions = [ - 'printf' => 0, - 'sprintf' => 0, - 'sscanf' => 1, - 'fscanf' => 1, - ]; - $minimumNumberOfArguments = [ - 'printf' => 1, - 'sprintf' => 1, - 'sscanf' => 3, - 'fscanf' => 3, - ]; - - $name = strtolower((string) $node->name); - if (!isset($functionsArgumentPositions[$name])) { + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { return []; } - $formatArgumentPosition = $functionsArgumentPositions[$name]; + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $name = $functionReflection->getName(); + if (!array_key_exists($name, self::FORMAT_ARGUMENT_POSITIONS)) { + return []; + } + + $formatArgumentPosition = self::FORMAT_ARGUMENT_POSITIONS[$name]; $args = $node->getArgs(); foreach ($args as $arg) { @@ -67,81 +69,50 @@ public function processNode(Node $node, Scope $scope): array } } $argsCount = count($args); - if ($argsCount < $minimumNumberOfArguments[$name]) { + if ($argsCount < self::MINIMUM_NUMBER_OF_ARGUMENTS[$name]) { return []; // caught by CallToFunctionParametersRule } $formatArgType = $scope->getType($args[$formatArgumentPosition]->value); - $placeHoldersCount = null; - foreach (TypeUtils::getConstantStrings($formatArgType) as $formatString) { + $maxPlaceHoldersCount = null; + foreach ($formatArgType->getConstantStrings() as $formatString) { $format = $formatString->getValue(); - $tempPlaceHoldersCount = $this->getPlaceholdersCount($name, $format); - if ($placeHoldersCount === null) { - $placeHoldersCount = $tempPlaceHoldersCount; - } elseif ($tempPlaceHoldersCount > $placeHoldersCount) { - $placeHoldersCount = $tempPlaceHoldersCount; + + if (in_array($name, ['sprintf', 'printf'], true)) { + $tempPlaceHoldersCount = $this->printfHelper->getPrintfPlaceholdersCount($format); + } else { + $tempPlaceHoldersCount = $this->printfHelper->getScanfPlaceholdersCount($format); + } + + if ($maxPlaceHoldersCount === null) { + $maxPlaceHoldersCount = $tempPlaceHoldersCount; + } elseif ($tempPlaceHoldersCount > $maxPlaceHoldersCount) { + $maxPlaceHoldersCount = $tempPlaceHoldersCount; } } - if ($placeHoldersCount === null) { + if ($maxPlaceHoldersCount === null) { return []; } $argsCount -= $formatArgumentPosition; - if ($argsCount !== $placeHoldersCount + 1) { + if ($argsCount !== $maxPlaceHoldersCount + 1) { return [ RuleErrorBuilder::message(sprintf( sprintf( '%s, %s.', - $placeHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders', + $maxPlaceHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders', $argsCount - 1 === 1 ? '%d value given' : '%d values given', ), $name, - $placeHoldersCount, + $maxPlaceHoldersCount, $argsCount - 1, - ))->build(), + ))->identifier(sprintf('argument.%s', $name))->build(), ]; } return []; } - private function getPlaceholdersCount(string $functionName, string $format): int - { - $specifiers = in_array($functionName, ['sprintf', 'printf'], true) ? '[bcdeEfFgGosuxX%s]' : '(?:[cdDeEfinosuxX%s]|\[[^\]]+\])'; - $addSpecifier = ''; - if ($this->phpVersion->supportsHhPrintfSpecifier()) { - $addSpecifier .= 'hH'; - } - - $specifiers = sprintf($specifiers, $addSpecifier); - - $pattern = '~(?%*)%(?:(?\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?-?\d*(?:\.\d*)?' . $specifiers . '~'; - - $matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER); - - if (count($matches) === 0) { - return 0; - } - - $placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0); - - if (count($placeholders) === 0) { - return 0; - } - - $maxPositionedNumber = 0; - $maxOrdinaryNumber = 0; - foreach ($placeholders as $placeholder) { - if (isset($placeholder['position']) && $placeholder['position'] !== '') { - $maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber); - } else { - $maxOrdinaryNumber++; - } - } - - return max($maxPositionedNumber, $maxOrdinaryNumber); - } - } diff --git a/src/Rules/Functions/RandomIntParametersRule.php b/src/Rules/Functions/RandomIntParametersRule.php index f3ce19c174..e35c21a3ed 100644 --- a/src/Rules/Functions/RandomIntParametersRule.php +++ b/src/Rules/Functions/RandomIntParametersRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -18,10 +19,14 @@ /** * @implements Rule */ -class RandomIntParametersRule implements Rule +final class RandomIntParametersRule implements Rule { - public function __construct(private ReflectionProvider $reflectionProvider, private bool $reportMaybes) + public function __construct( + private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, + private bool $reportMaybes, + ) { } @@ -55,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $isSmaller = $maxType->isSmallerThan($minType); + $isSmaller = $maxType->isSmallerThan($minType, $this->phpVersion); if ($isSmaller->yes() || $isSmaller->maybe() && $this->reportMaybes) { $message = 'Parameter #1 $min (%s) of function random_int expects lower number than parameter #2 $max (%s).'; @@ -64,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array $message, $minType->describe(VerbosityLevel::value()), $maxType->describe(VerbosityLevel::value()), - ))->build(), + ))->identifier('argument.type')->build(), ]; } diff --git a/src/Rules/Functions/RedefinedParametersRule.php b/src/Rules/Functions/RedefinedParametersRule.php new file mode 100644 index 0000000000..6e056c6df3 --- /dev/null +++ b/src/Rules/Functions/RedefinedParametersRule.php @@ -0,0 +1,60 @@ + + */ +final class RedefinedParametersRule implements Rule +{ + + public function getNodeType(): string + { + return Node\FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $params = $node->getParams(); + + if (count($params) <= 1) { + return []; + } + + $vars = []; + $errors = []; + + foreach ($params as $param) { + if (!$param->var instanceof Node\Expr\Variable) { + continue; + } + + if (!is_string($param->var->name)) { + continue; + } + + $var = $param->var->name; + + if (!isset($vars[$var])) { + $vars[$var] = true; + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf('Redefinition of parameter $%s.', $var)) + ->identifier('parameter.duplicate') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/ReturnNullsafeByRefRule.php b/src/Rules/Functions/ReturnNullsafeByRefRule.php index d842cf4584..f90bdca70f 100644 --- a/src/Rules/Functions/ReturnNullsafeByRefRule.php +++ b/src/Rules/Functions/ReturnNullsafeByRefRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class ReturnNullsafeByRefRule implements Rule +final class ReturnNullsafeByRefRule implements Rule { public function __construct(private NullsafeCheck $nullsafeCheck) @@ -41,7 +41,11 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message('Nullsafe cannot be returned by reference.')->line($returnNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Nullsafe cannot be returned by reference.') + ->line($returnNode->getStartLine()) + ->identifier('nullsafe.byRef') + ->nonIgnorable() + ->build(); } return $errors; diff --git a/src/Rules/Functions/ReturnTypeRule.php b/src/Rules/Functions/ReturnTypeRule.php index ab9897713b..9fdd33f14a 100644 --- a/src/Rules/Functions/ReturnTypeRule.php +++ b/src/Rules/Functions/ReturnTypeRule.php @@ -5,9 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\FunctionReturnTypeCheck; use PHPStan\Rules\Rule; use function sprintf; @@ -15,7 +13,7 @@ /** * @implements Rule */ -class ReturnTypeRule implements Rule +final class ReturnTypeRule implements Rule { public function __construct( @@ -40,16 +38,13 @@ public function processNode(Node $node, Scope $scope): array } $function = $scope->getFunction(); - if ( - !($function instanceof PhpFunctionFromParserNodeReflection) - || $function instanceof PhpMethodFromParserNodeReflection - ) { + if ($function instanceof MethodReflection) { return []; } return $this->returnTypeCheck->checkReturnType( $scope, - ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(), + $function->getReturnType(), $node->expr, $node, sprintf( diff --git a/src/Rules/Functions/SortParameterCastableToStringRule.php b/src/Rules/Functions/SortParameterCastableToStringRule.php new file mode 100644 index 0000000000..02cb886886 --- /dev/null +++ b/src/Rules/Functions/SortParameterCastableToStringRule.php @@ -0,0 +1,150 @@ + + */ +final class SortParameterCastableToStringRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + + if (!in_array($functionName, ['array_unique', 'sort', 'rsort', 'asort', 'arsort'], true)) { + return []; + } + + $origArgs = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $functionParameters = $parametersAcceptor->getParameters(); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $normalizedArgs = $normalizedFuncCall->getArgs(); + if (!array_key_exists(0, $normalizedArgs)) { + return []; + } + + $argsToCheck = [0 => $normalizedArgs[0]]; + $flags = null; + if (array_key_exists(1, $normalizedArgs)) { + $flags = $scope->getType($normalizedArgs[1]->value); + } elseif (array_key_exists(1, $functionParameters)) { + $flags = $functionParameters[1]->getDefaultValue(); + } + + if ($flags === null || $flags->equals(new ConstantIntegerType(SORT_REGULAR))) { + return []; + } + + $constantIntFlags = TypeUtils::getConstantIntegers($flags); + $mustBeCastableToString = $mustBeCastableToFloat = $constantIntFlags === []; + + foreach ($constantIntFlags as $flag) { + if ($flag->getValue() === SORT_NUMERIC) { + $mustBeCastableToFloat = true; + } elseif (in_array($flag->getValue() & (~SORT_FLAG_CASE), [SORT_STRING, SORT_LOCALE_STRING, SORT_NATURAL], true)) { + $mustBeCastableToString = true; + } + } + + if ($mustBeCastableToString && !$mustBeCastableToFloat) { + $errorMessage = 'Parameter %s of function %s expects an array of values castable to string, %s given.'; + $castFn = static fn (Type $t) => $t->toString(); + } elseif ($mustBeCastableToString) { + $errorMessage = 'Parameter %s of function %s expects an array of values castable to string and float, %s given.'; + $castFn = static function (Type $t): Type { + $float = $t->toFloat(); + + return $float instanceof ErrorType + ? $float + : $t->toString(); + }; + } elseif ($mustBeCastableToFloat) { + $errorMessage = 'Parameter %s of function %s expects an array of values castable to float, %s given.'; + $castFn = static fn (Type $t) => $t->toFloat(); + } else { + return []; + } + + $errors = []; + + foreach ($argsToCheck as $argIdx => $arg) { + $error = $this->parameterCastableToStringCheck->checkParameter( + $arg, + $scope, + $errorMessage, + $castFn, + $functionName, + $this->parameterCastableToStringCheck->getParameterName( + $arg, + $argIdx, + $functionParameters[$argIdx] ?? null, + ), + ); + + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/UnusedClosureUsesRule.php b/src/Rules/Functions/UnusedClosureUsesRule.php index 79b7268c9b..ed9639e41f 100644 --- a/src/Rules/Functions/UnusedClosureUsesRule.php +++ b/src/Rules/Functions/UnusedClosureUsesRule.php @@ -6,15 +6,13 @@ 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 Rule */ -class UnusedClosureUsesRule implements Rule +final class UnusedClosureUsesRule implements Rule { public function __construct(private UnusedFunctionParametersCheck $check) @@ -34,21 +32,10 @@ public function processNode(Node $node, Scope $scope): array return $this->check->getUnusedParameters( $scope, - array_map(static function (Node\Expr\ClosureUse $use): string { - if (!is_string($use->var->name)) { - throw new ShouldNotHappenException(); - } - return $use->var->name; - }, $node->uses), + array_map(static fn (Node\ClosureUse $use): Node\Expr\Variable => $use->var, $node->uses), $node->stmts, 'Anonymous function has an unused use $%s.', - 'anonymousFunction.unusedUse', - [ - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - ], + 'closure.unusedUse', ); } diff --git a/src/Rules/Functions/UselessFunctionReturnValueRule.php b/src/Rules/Functions/UselessFunctionReturnValueRule.php new file mode 100644 index 0000000000..43354dd004 --- /dev/null +++ b/src/Rules/Functions/UselessFunctionReturnValueRule.php @@ -0,0 +1,89 @@ + + */ +final class UselessFunctionReturnValueRule implements Rule +{ + + private const USELESS_FUNCTIONS = [ + 'var_export' => 'null', + 'print_r' => 'true', + 'highlight_string' => 'true', + ]; + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $funcCall, Scope $scope): array + { + if (!($funcCall->name instanceof Node\Name) || $scope->isInFirstLevelStatement()) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($funcCall->name, $scope); + + if (!array_key_exists($functionReflection->getName(), self::USELESS_FUNCTIONS)) { + return []; + } + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $funcCall->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $reorderedFuncCall = ArgumentsNormalizer::reorderFuncArguments( + $parametersAcceptor, + $funcCall, + ); + + if ($reorderedFuncCall === null) { + return []; + } + $reorderedArgs = $reorderedFuncCall->getArgs(); + + if (count($reorderedArgs) === 1 || (count($reorderedArgs) >= 2 && $scope->getType($reorderedArgs[1]->value)->isFalse()->yes())) { + return [RuleErrorBuilder::message( + sprintf( + 'Return value of function %s() is always %s and the result is printed instead of being returned. Pass in true as parameter #%d $%s to return the output instead.', + $functionReflection->getName(), + self::USELESS_FUNCTIONS[$functionReflection->getName()], + 2, + $parametersAcceptor->getParameters()[1]->getName(), + ), + ) + ->identifier('function.uselessReturnValue') + ->line($funcCall->getStartLine()) + ->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Functions/VariadicParametersDeclarationRule.php b/src/Rules/Functions/VariadicParametersDeclarationRule.php new file mode 100644 index 0000000000..f346ec8e72 --- /dev/null +++ b/src/Rules/Functions/VariadicParametersDeclarationRule.php @@ -0,0 +1,51 @@ + + */ +final class VariadicParametersDeclarationRule implements Rule +{ + + public function getNodeType(): string + { + return Node\FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $parameters = $node->getParams(); + $paramCount = count($parameters); + + if ($paramCount === 0) { + return []; + } + + $errors = []; + + foreach ($parameters as $index => $parameter) { + if (!$parameter->variadic) { + continue; + } + + if ($paramCount - 1 === $index) { + continue; + } + + $errors[] = RuleErrorBuilder::message('Only the last parameter can be variadic.') + ->nonIgnorable() + ->identifier('parameter.variadicNotLast') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Generators/YieldFromTypeRule.php b/src/Rules/Generators/YieldFromTypeRule.php index 8926b3f270..5bac445f13 100644 --- a/src/Rules/Generators/YieldFromTypeRule.php +++ b/src/Rules/Generators/YieldFromTypeRule.php @@ -6,21 +6,18 @@ use PhpParser\Node; use PhpParser\Node\Expr\YieldFrom; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\GenericTypeVariableResolver; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; /** * @implements Rule */ -class YieldFromTypeRule implements Rule +final class YieldFromTypeRule implements Rule { public function __construct( @@ -45,7 +42,10 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( $messagePattern, $exprType->describe(VerbosityLevel::typeOnly()), - ))->line($node->expr->getLine())->build(), + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.nonIterable') + ->build(), ]; } elseif ( !$exprType instanceof MixedType @@ -56,7 +56,10 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( $messagePattern, $exprType->describe(VerbosityLevel::typeOnly()), - ))->line($node->expr->getLine())->build(), + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.nonIterable') + ->build(), ]; } @@ -65,7 +68,7 @@ public function processNode(Node $node, Scope $scope): array if ($anonymousFunctionReturnType !== null) { $returnType = $anonymousFunctionReturnType; } elseif ($scopeFunction !== null) { - $returnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); + $returnType = $scopeFunction->getReturnType(); } else { return []; // already reported by YieldInGeneratorRule } @@ -75,21 +78,32 @@ public function processNode(Node $node, Scope $scope): array } $messages = []; - if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes())) { + $acceptsKey = $this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes()); + if (!$acceptsKey->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType(), $exprType->getIterableKeyType()); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects key type %s, %s given.', $returnType->getIterableKeyType()->describe($verbosityLevel), $exprType->getIterableKeyType()->describe($verbosityLevel), - ))->line($node->expr->getLine())->build(); + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.keyType') + ->acceptsReasonsTip($acceptsKey->reasons) + ->build(); } - if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $exprType->getIterableValueType(), $scope->isDeclareStrictTypes())) { + + $acceptsValue = $this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $exprType->getIterableValueType(), $scope->isDeclareStrictTypes()); + if (!$acceptsValue->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType(), $exprType->getIterableValueType()); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects value type %s, %s given.', $returnType->getIterableValueType()->describe($verbosityLevel), $exprType->getIterableValueType()->describe($verbosityLevel), - ))->line($node->expr->getLine())->build(); + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.valueType') + ->acceptsReasonsTip($acceptsValue->reasons) + ->build(); } $scopeFunction = $scope->getFunction(); @@ -97,18 +111,10 @@ public function processNode(Node $node, Scope $scope): array return $messages; } - if (!$exprType instanceof TypeWithClassName) { - return $messages; - } - - $currentReturnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); - if (!$currentReturnType instanceof TypeWithClassName) { - return $messages; - } - - $exprSendType = GenericTypeVariableResolver::getType($exprType, Generator::class, 'TSend'); - $thisSendType = GenericTypeVariableResolver::getType($currentReturnType, Generator::class, 'TSend'); - if ($exprSendType === null || $thisSendType === null) { + $currentReturnType = $scopeFunction->getReturnType(); + $exprSendType = $exprType->getTemplateType(Generator::class, 'TSend'); + $thisSendType = $currentReturnType->getTemplateType(Generator::class, 'TSend'); + if ($exprSendType instanceof ErrorType || $thisSendType instanceof ErrorType) { return $messages; } @@ -118,17 +124,19 @@ public function processNode(Node $node, Scope $scope): array 'Generator expects delegated TSend type %s, %s given.', $exprSendType->describe(VerbosityLevel::typeOnly()), $thisSendType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('generator.sendType')->build(); } elseif ($this->reportMaybes && !$isSuperType->yes()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects delegated TSend type %s, %s given.', $exprSendType->describe(VerbosityLevel::typeOnly()), $thisSendType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('generator.sendType')->build(); } - if ($scope->getType($node) instanceof VoidType && !$scope->isInFirstLevelStatement()) { - $messages[] = RuleErrorBuilder::message('Result of yield from (void) is used.')->build(); + if (!$scope->isInFirstLevelStatement() && $scope->getType($node)->isVoid()->yes()) { + $messages[] = RuleErrorBuilder::message('Result of yield from (void) is used.') + ->identifier('generator.void') + ->build(); } return $messages; diff --git a/src/Rules/Generators/YieldInGeneratorRule.php b/src/Rules/Generators/YieldInGeneratorRule.php index 230853ca38..c6283e8bb5 100644 --- a/src/Rules/Generators/YieldInGeneratorRule.php +++ b/src/Rules/Generators/YieldInGeneratorRule.php @@ -4,7 +4,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; @@ -15,7 +14,7 @@ /** * @implements Rule */ -class YieldInGeneratorRule implements Rule +final class YieldInGeneratorRule implements Rule { public function __construct(private bool $reportMaybes) @@ -38,9 +37,14 @@ public function processNode(Node $node, Scope $scope): array if ($anonymousFunctionReturnType !== null) { $returnType = $anonymousFunctionReturnType; } elseif ($scopeFunction !== null) { - $returnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); + $returnType = $scopeFunction->getReturnType(); } else { - return [RuleErrorBuilder::message('Yield can be used only inside a function.')->build()]; + return [ + RuleErrorBuilder::message('Yield can be used only inside a function.') + ->identifier('generator.outOfFunction') + ->nonIgnorable() + ->build(), + ]; } if ($returnType instanceof MixedType) { @@ -66,7 +70,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Yield can be used only with these return types: %s.', 'Generator, Iterator, Traversable, iterable', - ))->build(), + ))->identifier('generator.returnType')->build(), ]; } diff --git a/src/Rules/Generators/YieldTypeRule.php b/src/Rules/Generators/YieldTypeRule.php index c5678f8a66..eccc960d2a 100644 --- a/src/Rules/Generators/YieldTypeRule.php +++ b/src/Rules/Generators/YieldTypeRule.php @@ -4,7 +4,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -12,13 +11,12 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; /** * @implements Rule */ -class YieldTypeRule implements Rule +final class YieldTypeRule implements Rule { public function __construct( @@ -39,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array if ($anonymousFunctionReturnType !== null) { $returnType = $anonymousFunctionReturnType; } elseif ($scopeFunction !== null) { - $returnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); + $returnType = $scopeFunction->getReturnType(); } else { return []; // already reported by YieldInGeneratorRule } @@ -54,31 +52,42 @@ public function processNode(Node $node, Scope $scope): array $keyType = $scope->getType($node->key); } - if ($node->value === null) { - $valueType = new NullType(); - } else { - $valueType = $scope->getType($node->value); - } - $messages = []; - if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $keyType, $scope->isDeclareStrictTypes())) { + $acceptsKey = $this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $keyType, $scope->isDeclareStrictTypes()); + if (!$acceptsKey->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType(), $keyType); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects key type %s, %s given.', $returnType->getIterableKeyType()->describe($verbosityLevel), $keyType->describe($verbosityLevel), - ))->build(); + )) + ->acceptsReasonsTip($acceptsKey->reasons) + ->identifier('generator.keyType') + ->build(); } - if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes())) { + + if ($node->value === null) { + $valueType = new NullType(); + } else { + $valueType = $scope->getType($node->value); + } + + $acceptsValue = $this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes()); + if (!$acceptsValue->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType(), $valueType); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects value type %s, %s given.', $returnType->getIterableValueType()->describe($verbosityLevel), $valueType->describe($verbosityLevel), - ))->build(); + )) + ->acceptsReasonsTip($acceptsValue->reasons) + ->identifier('generator.valueType') + ->build(); } - if ($scope->getType($node) instanceof VoidType && !$scope->isInFirstLevelStatement()) { - $messages[] = RuleErrorBuilder::message('Result of yield (void) is used.')->build(); + if (!$scope->isInFirstLevelStatement() && $scope->getType($node)->isVoid()->yes()) { + $messages[] = RuleErrorBuilder::message('Result of yield (void) is used.') + ->identifier('generator.void') + ->build(); } return $messages; diff --git a/src/Rules/Generics/ClassAncestorsRule.php b/src/Rules/Generics/ClassAncestorsRule.php index 4c77d8899a..a0a07a2c20 100644 --- a/src/Rules/Generics/ClassAncestorsRule.php +++ b/src/Rules/Generics/ClassAncestorsRule.php @@ -17,7 +17,7 @@ /** * @implements Rule */ -class ClassAncestorsRule implements Rule +final class ClassAncestorsRule implements Rule { public function __construct( @@ -38,10 +38,7 @@ public function processNode(Node $node, Scope $scope): array if (!$originalNode instanceof Node\Stmt\Class_) { return []; } - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); if ($classReflection->isAnonymous()) { return []; } @@ -52,12 +49,14 @@ public function processNode(Node $node, Scope $scope): array $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 @extends tag contains unresolvable type.', $className), 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.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @extends is not allowed.', 'PHPDoc tag @extends has invalid type %s.', sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $escapedClassName), sprintf('in extended type %%s of class %s', $escapedClassName), @@ -67,12 +66,14 @@ public function processNode(Node $node, Scope $scope): array $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 @implements tag contains unresolvable type.', $className), 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.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @implements is not allowed.', 'PHPDoc tag @implements has invalid type %s.', sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $escapedClassName), sprintf('in implemented type %%s of class %s', $escapedClassName), diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index 5e24afd305..f574d76460 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -13,7 +13,7 @@ /** * @implements Rule */ -class ClassTemplateTypeRule implements Rule +final class ClassTemplateTypeRule implements Rule { public function __construct( @@ -29,10 +29,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); if (!$classReflection->isClass()) { return []; } @@ -44,6 +41,7 @@ public function processNode(Node $node, Scope $scope): array } return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($className), $classReflection->getTemplateTags(), @@ -51,6 +49,9 @@ public function processNode(Node $node, Scope $scope): array 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 has invalid default type %%s.', $displayName), + sprintf('Default type %%s in PHPDoc tag @template %%s for %s is not subtype of bound type %%s.', $displayName), + sprintf('PHPDoc tag @template %%s for %s does not have a default type but follows an optional @template %%s.', $displayName), ); } diff --git a/src/Rules/Generics/CrossCheckInterfacesHelper.php b/src/Rules/Generics/CrossCheckInterfacesHelper.php index f272063e24..3e9c477cb3 100644 --- a/src/Rules/Generics/CrossCheckInterfacesHelper.php +++ b/src/Rules/Generics/CrossCheckInterfacesHelper.php @@ -3,17 +3,17 @@ namespace PHPStan\Rules\Generics; use PHPStan\Reflection\ClassReflection; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; use function array_key_exists; use function sprintf; -class CrossCheckInterfacesHelper +final class CrossCheckInterfacesHelper { /** - * @return RuleError[] + * @return list */ public function check(ClassReflection $classReflection): array { @@ -44,7 +44,7 @@ public function check(ClassReflection $classReflection): array $interface->getName(), $type->describe(VerbosityLevel::value()), $otherType->describe(VerbosityLevel::value()), - ))->build(); + ))->identifier('generics.interfaceConflict')->build(); } continue; } diff --git a/src/Rules/Generics/EnumAncestorsRule.php b/src/Rules/Generics/EnumAncestorsRule.php index 63219efb22..71daff135b 100644 --- a/src/Rules/Generics/EnumAncestorsRule.php +++ b/src/Rules/Generics/EnumAncestorsRule.php @@ -17,7 +17,7 @@ /** * @implements Rule */ -class EnumAncestorsRule implements Rule +final class EnumAncestorsRule implements Rule { public function __construct( @@ -38,10 +38,7 @@ public function processNode(Node $node, Scope $scope): array if (!$originalNode instanceof Node\Stmt\Enum_) { return []; } - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); $enumName = $classReflection->getName(); $escapedEnumName = SprintfHelper::escapeFormatString($enumName); @@ -50,6 +47,7 @@ public function processNode(Node $node, Scope $scope): array [], array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), sprintf('Enum %s @extends tag contains incompatible type %%s.', $escapedEnumName), + sprintf('Enum %s @extends tag contains unresolvable type.', $enumName), sprintf('Enum %s has @extends tag, but cannot extend anything.', $escapedEnumName), '', '', @@ -59,18 +57,21 @@ public function processNode(Node $node, Scope $scope): array '', '', '', + '', ); $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 @implements tag contains unresolvable type.', $enumName), 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.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @implements is not allowed.', 'PHPDoc tag @implements has invalid type %s.', sprintf('Enum %s implements generic interface %%s but does not specify its types: %%s', $escapedEnumName), sprintf('in implemented type %%s of enum %s', $escapedEnumName), diff --git a/src/Rules/Generics/EnumTemplateTypeRule.php b/src/Rules/Generics/EnumTemplateTypeRule.php index a52ac23255..6f5b049c7b 100644 --- a/src/Rules/Generics/EnumTemplateTypeRule.php +++ b/src/Rules/Generics/EnumTemplateTypeRule.php @@ -13,7 +13,7 @@ /** * @implements Rule */ -class EnumTemplateTypeRule implements Rule +final class EnumTemplateTypeRule implements Rule { public function getNodeType(): string @@ -23,10 +23,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); if (!$classReflection->isEnum()) { return []; } @@ -39,7 +36,9 @@ public function processNode(Node $node, Scope $scope): array $className = $classReflection->getDisplayName(); return [ - RuleErrorBuilder::message(sprintf('Enum %s has PHPDoc @template tag%s but enums cannot be generic.', $className, $templateTagsCount === 1 ? '' : 's'))->build(), + RuleErrorBuilder::message(sprintf('Enum %s has PHPDoc @template tag%s but enums cannot be generic.', $className, $templateTagsCount === 1 ? '' : 's')) + ->identifier('enum.generic') + ->build(), ]; } diff --git a/src/Rules/Generics/FunctionSignatureVarianceRule.php b/src/Rules/Generics/FunctionSignatureVarianceRule.php index 9b75ab2f57..65e0f7bca3 100644 --- a/src/Rules/Generics/FunctionSignatureVarianceRule.php +++ b/src/Rules/Generics/FunctionSignatureVarianceRule.php @@ -6,14 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use function sprintf; /** * @implements Rule */ -class FunctionSignatureVarianceRule implements Rule +final class FunctionSignatureVarianceRule implements Rule { public function __construct(private VarianceCheck $varianceCheck) @@ -27,19 +26,18 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReflection = $scope->getFunction(); - if ($functionReflection === null) { - return []; - } - + $functionReflection = $node->getFunctionReflection(); $functionName = $functionReflection->getName(); return $this->varianceCheck->checkParametersAcceptor( - ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()), + $functionReflection, sprintf('in parameter %%s of function %s()', SprintfHelper::escapeFormatString($functionName)), + sprintf('in param-out type of parameter %%s of function %s()', SprintfHelper::escapeFormatString($functionName)), sprintf('in return type of function %s()', $functionName), sprintf('in function %s()', $functionName), false, + false, + 'function', ); } diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 5353d0a9cf..d4b56da5f2 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -14,7 +14,7 @@ /** * @implements Rule */ -class FunctionTemplateTypeRule implements Rule +final class FunctionTemplateTypeRule implements Rule { public function __construct( @@ -52,6 +52,7 @@ public function processNode(Node $node, Scope $scope): array $escapedFunctionName = SprintfHelper::escapeFormatString($functionName); return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithFunction($functionName), $resolvedPhpDoc->getTemplateTags(), @@ -59,6 +60,9 @@ public function processNode(Node $node, Scope $scope): array 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), + sprintf('PHPDoc tag @template %%s for function %s() has invalid default type %%s.', $escapedFunctionName), + sprintf('Default type %%s in PHPDoc tag @template %%s for function %s() is not subtype of bound type %%s.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() does not have a default type but follows an optional @template %%s.', $escapedFunctionName), ); } diff --git a/src/Rules/Generics/GenericAncestorsCheck.php b/src/Rules/Generics/GenericAncestorsCheck.php index 4210bf7e29..d55fd57116 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -5,14 +5,17 @@ use PhpParser\Node; use PhpParser\Node\Name; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\MissingTypehintCheck; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TypeProjectionHelper; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_fill_keys; +use function array_filter; use function array_keys; use function array_map; use function array_merge; @@ -21,7 +24,7 @@ use function in_array; use function sprintf; -class GenericAncestorsCheck +final class GenericAncestorsCheck { /** @@ -31,8 +34,9 @@ public function __construct( private ReflectionProvider $reflectionProvider, private GenericObjectTypeCheck $genericObjectTypeCheck, private VarianceCheck $varianceCheck, - private bool $checkGenericClassInNonGenericObjectType, + private UnresolvableTypeHelper $unresolvableTypeHelper, private array $skipCheckGenericClasses, + private bool $checkMissingTypehints, ) { } @@ -40,18 +44,20 @@ public function __construct( /** * @param array $nameNodes * @param array $ancestorTypes - * @return RuleError[] + * @return list */ public function check( array $nameNodes, array $ancestorTypes, string $incompatibleTypeMessage, + string $unresolvableTypeMessage, string $noNamesMessage, string $noRelatedNameMessage, string $classNotGenericMessage, string $notEnoughTypesMessage, string $extraTypesMessage, string $typeIsNotSubtypeMessage, + string $typeProjectionIsNotAllowedMessage, string $invalidTypeMessage, string $genericClassInNonGenericObjectType, string $invalidVarianceMessage, @@ -64,16 +70,22 @@ public function check( $messages = []; foreach ($ancestorTypes as $ancestorType) { if (!$ancestorType instanceof GenericObjectType) { - $messages[] = RuleErrorBuilder::message(sprintf($incompatibleTypeMessage, $ancestorType->describe(VerbosityLevel::typeOnly())))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($incompatibleTypeMessage, $ancestorType->describe(VerbosityLevel::typeOnly()))) + ->identifier('generics.notCompatible') + ->build(); continue; } $ancestorTypeClassName = $ancestorType->getClassName(); if (!isset($names[$ancestorTypeClassName])) { if (count($names) === 0) { - $messages[] = RuleErrorBuilder::message($noNamesMessage)->build(); + $messages[] = RuleErrorBuilder::message($noNamesMessage) + ->identifier('generics.noParent') + ->build(); } else { - $messages[] = RuleErrorBuilder::message(sprintf($noRelatedNameMessage, $ancestorTypeClassName, implode(', ', array_keys($names))))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($noRelatedNameMessage, $ancestorTypeClassName, implode(', ', array_keys($names)))) + ->identifier('generics.wrongParent') + ->build(); } continue; @@ -87,18 +99,40 @@ public function check( $notEnoughTypesMessage, $extraTypesMessage, $typeIsNotSubtypeMessage, + '', + '', ); $messages = array_merge($messages, $genericObjectTypeCheckMessages); + if ($this->unresolvableTypeHelper->containsUnresolvableType($ancestorType)) { + $messages[] = RuleErrorBuilder::message($unresolvableTypeMessage) + ->identifier('generics.unresolvable') + ->build(); + } + foreach ($ancestorType->getReferencedClasses() as $referencedClass) { - if ($this->reflectionProvider->hasClass($referencedClass)) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass)) + ->identifier('class.notFound') + ->build(); + continue; + } + + if ($referencedClass === $ancestorType->getClassName()) { continue; } - $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass))->build(); + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->isTrait()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass)) + ->identifier('generics.trait') + ->build(); } - $variance = TemplateTypeVariance::createInvariant(); + $variance = TemplateTypeVariance::createStatic(); $messageContext = sprintf( $invalidVarianceMessage, $ancestorType->describe(VerbosityLevel::typeOnly()), @@ -106,9 +140,21 @@ public function check( foreach ($this->varianceCheck->check($variance, $ancestorType, $messageContext) as $message) { $messages[] = $message; } + + foreach ($ancestorType->getVariances() as $index => $typeVariance) { + if ($typeVariance->invariant()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf( + $typeProjectionIsNotAllowedMessage, + TypeProjectionHelper::describe($ancestorType->getTypes()[$index], $typeVariance, VerbosityLevel::typeOnly()), + $ancestorType->describe(VerbosityLevel::typeOnly()), + ))->identifier('generics.callSiteVarianceNotAllowed')->build(); + } } - if ($this->checkGenericClassInNonGenericObjectType) { + if ($this->checkMissingTypehints) { foreach (array_keys($unusedNames) as $unusedName) { if (!$this->reflectionProvider->hasClass($unusedName)) { continue; @@ -122,11 +168,25 @@ public function check( continue; } + $templateTypes = $unusedNameClassReflection->getTemplateTypeMap()->getTypes(); + $templateTypesCount = count($templateTypes); + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount === 0) { + continue; + } + + $templateTypesList = implode(', ', array_keys($templateTypes)); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $genericClassInNonGenericObjectType, $unusedName, - implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $templateTypesList, + )) + ->identifier('missingType.generics') + ->build(); } } diff --git a/src/Rules/Generics/GenericObjectTypeCheck.php b/src/Rules/Generics/GenericObjectTypeCheck.php index db2eaf27d6..c0a4936bdc 100644 --- a/src/Rules/Generics/GenericObjectTypeCheck.php +++ b/src/Rules/Generics/GenericObjectTypeCheck.php @@ -2,26 +2,32 @@ namespace PHPStan\Rules\Generics; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Generic\TypeProjectionHelper; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; +use function array_filter; use function array_keys; use function array_values; use function count; use function implode; use function sprintf; +use function strtolower; -class GenericObjectTypeCheck +final class GenericObjectTypeCheck { /** - * @return RuleError[] + * @return list */ public function check( Type $phpDocType, @@ -29,6 +35,8 @@ public function check( string $notEnoughTypesMessage, string $extraTypesMessage, string $typeIsNotSubtypeMessage, + string $typeProjectionHasConflictingVarianceMessage, + string $typeProjectionIsRedundantMessage, ): array { $genericTypes = $this->getGenericTypes($phpDocType); @@ -39,33 +47,40 @@ public function check( continue; } - $classLikeDescription = 'class'; - if ($classReflection->isInterface()) { - $classLikeDescription = 'interface'; - } elseif ($classReflection->isTrait()) { - $classLikeDescription = 'trait'; - } elseif ($classReflection->isEnum()) { - $classLikeDescription = 'enum'; - } + $classLikeDescription = strtolower($classReflection->getClassTypeDescription()); if (!$classReflection->isGeneric()) { - $messages[] = RuleErrorBuilder::message(sprintf($classNotGenericMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName()))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($classNotGenericMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName())) + ->identifier('generics.notGeneric') + ->build(); continue; } $templateTypes = array_values($classReflection->getTemplateTypeMap()->getTypes()); $genericTypeTypes = $genericType->getTypes(); + $genericTypeVariances = $genericType->getVariances(); $templateTypesCount = count($templateTypes); $genericTypeTypesCount = count($genericTypeTypes); - if ($templateTypesCount > $genericTypeTypesCount) { + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount > $genericTypeTypesCount) { + $templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required).', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $notEnoughTypesMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName(false), - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), - ))->build(); + $templateTypesList, + ))->identifier('generics.lessTypes')->build(); } elseif ($templateTypesCount < $genericTypeTypesCount) { + $templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $extraTypesMessage, $genericType->describe(VerbosityLevel::typeOnly()), @@ -73,19 +88,46 @@ public function check( $classLikeDescription, $classReflection->getDisplayName(false), $templateTypesCount, - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), - ))->build(); + $templateTypesList, + ))->identifier('generics.moreTypes')->build(); } - $templateTypesCount = count($templateTypes); for ($i = 0; $i < $templateTypesCount; $i++) { if (!isset($genericTypeTypes[$i])) { continue; } $templateType = $templateTypes[$i]; - $boundType = TemplateTypeHelper::resolveToBounds($templateType); $genericTypeType = $genericTypeTypes[$i]; + + $genericTypeVariance = $genericTypeVariances[$i] ?? TemplateTypeVariance::createInvariant(); + if ($templateType instanceof TemplateType && !$genericTypeVariance->invariant()) { + if ($genericTypeVariance->equals($templateType->getVariance())) { + $messages[] = RuleErrorBuilder::message(sprintf( + $typeProjectionIsRedundantMessage, + TypeProjectionHelper::describe($genericTypeType, $genericTypeVariance, VerbosityLevel::typeOnly()), + $genericType->describe(VerbosityLevel::typeOnly()), + $templateType->describe(VerbosityLevel::typeOnly()), + $classLikeDescription, + $classReflection->getDisplayName(false), + )) + ->identifier('generics.callSiteVarianceRedundant') + ->tip('You can safely remove the call-site variance annotation.') + ->build(); + } elseif (!$genericTypeVariance->validPosition($templateType->getVariance())) { + $messages[] = RuleErrorBuilder::message(sprintf( + $typeProjectionHasConflictingVarianceMessage, + TypeProjectionHelper::describe($genericTypeType, $genericTypeVariance, VerbosityLevel::typeOnly()), + $genericType->describe(VerbosityLevel::typeOnly()), + $templateType->getVariance()->describe(), + $templateType->describe(VerbosityLevel::typeOnly()), + $classLikeDescription, + $classReflection->getDisplayName(false), + ))->identifier('generics.callSiteVarianceConflict')->build(); + } + } + + $boundType = TemplateTypeHelper::resolveToBounds($templateType); if ($boundType->isSuperTypeOf($genericTypeType)->yes()) { if (!$templateType instanceof TemplateType) { continue; @@ -96,11 +138,20 @@ public function check( continue; } - $templateTypes[$j] = TemplateTypeHelper::resolveTemplateTypes($templateTypes[$j], $map); + $templateTypes[$j] = TemplateTypeHelper::resolveTemplateTypes( + $templateTypes[$j], + $map, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createStatic(), + ); } continue; } + if ($genericTypeVariance->bivariant()) { + continue; + } + $messages[] = RuleErrorBuilder::message(sprintf( $typeIsNotSubtypeMessage, $genericTypeType->describe(VerbosityLevel::typeOnly()), @@ -108,7 +159,7 @@ public function check( $templateType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName(false), - ))->build(); + ))->identifier('generics.notSubtype')->build(); } } @@ -116,15 +167,15 @@ public function check( } /** - * @return GenericObjectType[] + * @return list */ private function getGenericTypes(Type $phpDocType): array { $genericObjectTypes = []; TypeTraverser::map($phpDocType, static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type { - if ($type instanceof GenericObjectType) { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { $resolvedType = TemplateTypeHelper::resolveToBounds($type); - if (!$resolvedType instanceof GenericObjectType) { + if (!$resolvedType instanceof GenericObjectType && !$resolvedType instanceof GenericStaticType) { throw new ShouldNotHappenException(); } $genericObjectTypes[] = $resolvedType; diff --git a/src/Rules/Generics/InterfaceAncestorsRule.php b/src/Rules/Generics/InterfaceAncestorsRule.php index bb207f5ae6..c270de3626 100644 --- a/src/Rules/Generics/InterfaceAncestorsRule.php +++ b/src/Rules/Generics/InterfaceAncestorsRule.php @@ -17,7 +17,7 @@ /** * @implements Rule */ -class InterfaceAncestorsRule implements Rule +final class InterfaceAncestorsRule implements Rule { public function __construct( @@ -38,10 +38,7 @@ public function processNode(Node $node, Scope $scope): array if (!$originalNode instanceof Node\Stmt\Interface_) { return []; } - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); $interfaceName = $classReflection->getName(); $escapedInterfaceName = SprintfHelper::escapeFormatString($interfaceName); @@ -50,12 +47,14 @@ public function processNode(Node $node, Scope $scope): array $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 @extends tag contains unresolvable type.', $interfaceName), 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.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @extends is not allowed.', 'PHPDoc tag @extends has invalid type %s.', sprintf('Interface %s extends generic interface %%s but does not specify its types: %%s', $escapedInterfaceName), sprintf('in extended type %%s of interface %s', $escapedInterfaceName), @@ -65,6 +64,7 @@ public function processNode(Node $node, Scope $scope): array [], array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()), sprintf('Interface %s @implements tag contains incompatible type %%s.', $escapedInterfaceName), + sprintf('Interface %s @implements tag contains unresolvable type.', $interfaceName), sprintf('Interface %s has @implements tag, but can not implement any interface, must extend from it.', $escapedInterfaceName), '', '', @@ -74,6 +74,7 @@ public function processNode(Node $node, Scope $scope): array '', '', '', + '', ); foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index dd9198e96b..53adafb43a 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -13,7 +13,7 @@ /** * @implements Rule */ -class InterfaceTemplateTypeRule implements Rule +final class InterfaceTemplateTypeRule implements Rule { public function __construct( @@ -29,10 +29,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); if (!$classReflection->isInterface()) { return []; } @@ -41,6 +38,7 @@ public function processNode(Node $node, Scope $scope): array $escapadInterfaceName = SprintfHelper::escapeFormatString($interfaceName); return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($interfaceName), $classReflection->getTemplateTags(), @@ -48,6 +46,9 @@ public function processNode(Node $node, Scope $scope): array 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), + sprintf('PHPDoc tag @template %%s for interface %s has invalid default type %%s.', $escapadInterfaceName), + sprintf('Default type %%s in PHPDoc tag @template %%s for interface %s is not subtype of bound type %%s.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s does not have a default type but follows an optional @template %%s.', $escapadInterfaceName), ); } diff --git a/src/Rules/Generics/MethodSignatureVarianceRule.php b/src/Rules/Generics/MethodSignatureVarianceRule.php index b2207734d9..44543983dc 100644 --- a/src/Rules/Generics/MethodSignatureVarianceRule.php +++ b/src/Rules/Generics/MethodSignatureVarianceRule.php @@ -6,15 +6,13 @@ 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 Rule */ -class MethodSignatureVarianceRule implements Rule +final class MethodSignatureVarianceRule implements Rule { public function __construct(private VarianceCheck $varianceCheck) @@ -28,17 +26,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { - return []; - } + $method = $node->getMethodReflection(); return $this->varianceCheck->checkParametersAcceptor( - ParametersAcceptorSelector::selectSingle($method->getVariants()), + $method, sprintf('in parameter %%s of method %s::%s()', SprintfHelper::escapeFormatString($method->getDeclaringClass()->getDisplayName()), SprintfHelper::escapeFormatString($method->getName())), + sprintf('in param-out type of parameter %%s of method %s::%s()', SprintfHelper::escapeFormatString($method->getDeclaringClass()->getDisplayName()), SprintfHelper::escapeFormatString($method->getName())), sprintf('in return type of method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), sprintf('in method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), - $method->getName() === '__construct' || $method->isStatic(), + $method->isStatic(), + $method->isPrivate() || $method->getName() === '__construct', + 'method', ); } diff --git a/src/Rules/Generics/MethodTagTemplateTypeCheck.php b/src/Rules/Generics/MethodTagTemplateTypeCheck.php new file mode 100644 index 0000000000..afa672f983 --- /dev/null +++ b/src/Rules/Generics/MethodTagTemplateTypeCheck.php @@ -0,0 +1,83 @@ + + */ + public function check( + ClassReflection $classReflection, + Scope $scope, + ClassLike $node, + string $docComment, + ): array + { + $className = $classReflection->getDisplayName(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + null, + $docComment, + ); + + $messages = []; + $escapedClassName = SprintfHelper::escapeFormatString($className); + $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); + + foreach ($resolvedPhpDoc->getMethodTags() as $methodName => $methodTag) { + $methodTemplateTags = $methodTag->getTemplateTags(); + $escapedMethodName = SprintfHelper::escapeFormatString($methodName); + + $messages = array_merge($messages, $this->templateTypeCheck->check( + $scope, + $node, + TemplateTypeScope::createWithMethod($className, $methodName), + $methodTemplateTags, + sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing class %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid default type %%s', $escapedClassName, $escapedMethodName), + sprintf('Default type %%s in PHPDoc tag @method template %%s for method %s::%s() is not subtype of bound type %%s', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() does not have a default type but follows an optional @template %%s.', $escapedClassName, $escapedMethodName), + )); + + foreach (array_keys($methodTemplateTags) as $name) { + if (!isset($classTemplateTypes[$name])) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @method template %s for method %s::%s() shadows @template %s for class %s.', $name, $className, $methodName, $classTemplateTypes[$name]->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(false))) + ->identifier('methodTag.shadowTemplate') + ->build(); + } + } + + return $messages; + } + +} diff --git a/src/Rules/Generics/MethodTagTemplateTypeRule.php b/src/Rules/Generics/MethodTagTemplateTypeRule.php new file mode 100644 index 0000000000..b2f3e2a08c --- /dev/null +++ b/src/Rules/Generics/MethodTagTemplateTypeRule.php @@ -0,0 +1,42 @@ + + */ +final class MethodTagTemplateTypeRule implements Rule +{ + + public function __construct( + private MethodTagTemplateTypeCheck $check, + ) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + return $this->check->check( + $node->getClassReflection(), + $scope, + $node->getOriginalNode(), + $docComment->getText(), + ); + } + +} diff --git a/src/Rules/Generics/MethodTagTemplateTypeTraitRule.php b/src/Rules/Generics/MethodTagTemplateTypeTraitRule.php new file mode 100644 index 0000000000..d0235e975c --- /dev/null +++ b/src/Rules/Generics/MethodTagTemplateTypeTraitRule.php @@ -0,0 +1,52 @@ + + */ +final class MethodTagTemplateTypeTraitRule implements Rule +{ + + public function __construct( + private MethodTagTemplateTypeCheck $check, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $traitName = $node->namespacedName; + if ($traitName === null) { + return []; + } + + if (!$this->reflectionProvider->hasClass($traitName->toString())) { + return []; + } + + return $this->check->check( + $this->reflectionProvider->getClass($traitName->toString()), + $scope, + $node, + $docComment->getText(), + ); + } + +} diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php index 7f51d9e0d5..65653f833f 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -17,7 +17,7 @@ /** * @implements Rule */ -class MethodTemplateTypeRule implements Rule +final class MethodTemplateTypeRule implements Rule { public function __construct( @@ -58,6 +58,7 @@ public function processNode(Node $node, Scope $scope): array $escapedClassName = SprintfHelper::escapeFormatString($className); $escapedMethodName = SprintfHelper::escapeFormatString($methodName); $messages = $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithMethod($className, $methodName), $methodTemplateTags, @@ -65,6 +66,9 @@ public function processNode(Node $node, Scope $scope): array 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), + sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid default type %%s.', $escapedClassName, $escapedMethodName), + sprintf('Default type %%s in PHPDoc tag @template %%s for method %s::%s() is not subtype of bound type %%s.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() does not have a default type but follows an optional @template %%s.', $escapedClassName, $escapedMethodName), ); $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); @@ -73,7 +77,9 @@ public function processNode(Node $node, Scope $scope): array continue; } - $messages[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @template %s for method %s::%s() shadows @template %s for class %s.', $name, $className, $methodName, $classTemplateTypes[$name]->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(false)))->build(); + $messages[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @template %s for method %s::%s() shadows @template %s for class %s.', $name, $className, $methodName, $classTemplateTypes[$name]->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(false))) + ->identifier('method.shadowTemplate') + ->build(); } return $messages; diff --git a/src/Rules/Generics/PropertyVarianceRule.php b/src/Rules/Generics/PropertyVarianceRule.php new file mode 100644 index 0000000000..f222bd9945 --- /dev/null +++ b/src/Rules/Generics/PropertyVarianceRule.php @@ -0,0 +1,55 @@ + + */ +final class PropertyVarianceRule implements Rule +{ + + public function __construct( + private VarianceCheck $varianceCheck, + ) + { + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + if (!$classReflection->hasNativeProperty($node->getName())) { + return []; + } + + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + + if ($propertyReflection->isPrivate()) { + return []; + } + + $variance = $node->isReadOnly() || $node->isReadOnlyByPhpDoc() + ? TemplateTypeVariance::createCovariant() + : TemplateTypeVariance::createInvariant(); + + return $this->varianceCheck->check( + $variance, + $propertyReflection->getReadableType(), + sprintf('in property %s::$%s', SprintfHelper::escapeFormatString($classReflection->getDisplayName()), SprintfHelper::escapeFormatString($node->getName())), + ); + } + +} diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index c8151a5129..27be48dd50 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -3,22 +3,29 @@ namespace PHPStan\Rules\Generics; use PhpParser\Node; +use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\ClassNameUsageLocation; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; +use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; @@ -30,12 +37,12 @@ use function get_class; use function sprintf; -class TemplateTypeCheck +final class TemplateTypeCheck { public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private GenericObjectTypeCheck $genericObjectTypeCheck, private TypeAliasResolver $typeAliasResolver, private bool $checkClassCaseSensitivity, @@ -45,9 +52,10 @@ public function __construct( /** * @param array $templateTags - * @return RuleError[] + * @return list */ public function check( + Scope $scope, Node $node, TemplateTypeScope $templateTypeScope, array $templateTags, @@ -55,29 +63,38 @@ public function check( string $sameTemplateTypeNameAsTypeMessage, string $invalidBoundTypeMessage, string $notSupportedBoundMessage, + string $invalidDefaultTypeMessage, + string $defaultNotSubtypeOfBoundMessage, + string $requiredTypeAfterOptionalMessage, ): array { $messages = []; + $templateTagWithDefaultType = null; foreach ($templateTags as $templateTag) { - $templateTagName = $templateTag->getName(); + $templateTagName = $scope->resolveName(new Node\Name($templateTag->getName())); if ($this->reflectionProvider->hasClass($templateTagName)) { $messages[] = RuleErrorBuilder::message(sprintf( $sameTemplateTypeNameAsClassMessage, $templateTagName, - ))->build(); + ))->identifier('generics.existingClass')->build(); } if ($this->typeAliasResolver->hasTypeAlias($templateTagName, $templateTypeScope->getClassName())) { $messages[] = RuleErrorBuilder::message(sprintf( $sameTemplateTypeNameAsTypeMessage, $templateTagName, - ))->build(); + ))->identifier('generics.existingTypeAlias')->build(); } $boundType = $templateTag->getBound(); foreach ($boundType->getReferencedClasses() as $referencedClass) { - if ( - $this->reflectionProvider->hasClass($referencedClass) - && !$this->reflectionProvider->getClass($referencedClass)->isTrait() - ) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + $messages[] = RuleErrorBuilder::message(sprintf( + $invalidBoundTypeMessage, + $templateTagName, + $referencedClass, + ))->identifier('class.notFound')->build(); + continue; + } + if (!$this->reflectionProvider->getClass($referencedClass)->isTrait()) { continue; } @@ -85,31 +102,37 @@ public function check( $invalidBoundTypeMessage, $templateTagName, $referencedClass, - ))->build(); + ))->identifier('generics.traitBound')->build(); } - if ($this->checkClassCaseSensitivity) { - $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $boundType->getReferencedClasses()); - $messages = array_merge($messages, $this->classCaseSensitivityCheck->checkClassNames($classNameNodePairs)); - } + $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $boundType->getReferencedClasses()); + $messages = array_merge($messages, $this->classCheck->checkClassNames($scope, $classNameNodePairs, ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_TEMPLATE_BOUND, [ + 'templateTagName' => $templateTagName, + ]), $this->checkClassCaseSensitivity)); - $boundType = $templateTag->getBound(); $boundTypeClass = get_class($boundType); if ( $boundTypeClass !== MixedType::class && $boundTypeClass !== ConstantArrayType::class && $boundTypeClass !== ArrayType::class + && $boundTypeClass !== ConstantStringType::class && $boundTypeClass !== StringType::class + && $boundTypeClass !== ConstantIntegerType::class && $boundTypeClass !== IntegerType::class && $boundTypeClass !== FloatType::class && $boundTypeClass !== BooleanType::class && $boundTypeClass !== ObjectWithoutClassType::class && $boundTypeClass !== ObjectType::class + && $boundTypeClass !== ObjectShapeType::class && $boundTypeClass !== GenericObjectType::class + && $boundTypeClass !== KeyOfType::class && !$boundType instanceof UnionType + && !$boundType instanceof IntersectionType && !$boundType instanceof TemplateType ) { - $messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $boundType->describe(VerbosityLevel::typeOnly())))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $boundType->describe(VerbosityLevel::typeOnly()))) + ->identifier('generics.notSupportedBound') + ->build(); } $escapedTemplateTagName = SprintfHelper::escapeFormatString($templateTagName); @@ -119,10 +142,73 @@ public function check( sprintf('PHPDoc tag @template %s bound has type %%s which does not specify all template types of %%s %%s: %%s', $escapedTemplateTagName), sprintf('PHPDoc tag @template %s bound has type %%s which specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedTemplateTagName), sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s is not subtype of template type %%s of %%s %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s is in conflict with %%s template type %%s of %%s %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s is redundant, template type %%s of %%s %%s has the same variance.', $escapedTemplateTagName), ); foreach ($genericObjectErrors as $genericObjectError) { $messages[] = $genericObjectError; } + + $defaultType = $templateTag->getDefault(); + if ($defaultType === null) { + if ($templateTagWithDefaultType !== null) { + $messages[] = RuleErrorBuilder::message(sprintf( + $requiredTypeAfterOptionalMessage, + $templateTagName, + $templateTagWithDefaultType, + ))->identifier('generics.requiredTypeAfterOptional')->build(); + } + + continue; + } + + $templateTagWithDefaultType = $templateTagName; + + foreach ($defaultType->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + $messages[] = RuleErrorBuilder::message(sprintf( + $invalidDefaultTypeMessage, + $templateTagName, + $referencedClass, + ))->identifier('class.notFound')->build(); + continue; + } + if (!$this->reflectionProvider->getClass($referencedClass)->isTrait()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf( + $invalidDefaultTypeMessage, + $templateTagName, + $referencedClass, + ))->identifier('generics.traitBound')->build(); + } + + $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $defaultType->getReferencedClasses()); + $messages = array_merge($messages, $this->classCheck->checkClassNames($scope, $classNameNodePairs, ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_TEMPLATE_DEFAULT, [ + 'templateTagName' => $templateTagName, + ]), $this->checkClassCaseSensitivity)); + + $genericDefaultErrors = $this->genericObjectTypeCheck->check( + $defaultType, + sprintf('PHPDoc tag @template %s default contains generic type %%s but class %%s is not generic.', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s default has type %%s which does not specify all template types of class %%s: %%s', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s default has type %%s which specifies %%d template types, but class %%s supports only %%d: %%s', $escapedTemplateTagName), + sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s default is not subtype of template type %%s of class %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s default is in conflict with %%s template type %%s of %%s %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s default is redundant, template type %%s of %%s %%s has the same variance.', $escapedTemplateTagName), + ); + foreach ($genericDefaultErrors as $genericDefaultError) { + $messages[] = $genericDefaultError; + } + + if (!$boundType->accepts($defaultType, $scope->isDeclareStrictTypes())->no()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf($defaultNotSubtypeOfBoundMessage, $defaultType->describe(VerbosityLevel::typeOnly()), $templateTagName, $boundType->describe(VerbosityLevel::typeOnly()))) + ->identifier('generics.templateDefaultOutOfBounds') + ->build(); } return $messages; diff --git a/src/Rules/Generics/TraitTemplateTypeRule.php b/src/Rules/Generics/TraitTemplateTypeRule.php index 5294ae9eaa..27ce74e298 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -14,7 +14,7 @@ /** * @implements Rule */ -class TraitTemplateTypeRule implements Rule +final class TraitTemplateTypeRule implements Rule { public function __construct( @@ -52,6 +52,7 @@ public function processNode(Node $node, Scope $scope): array $escapedTraitName = SprintfHelper::escapeFormatString($traitName); return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($traitName), $resolvedPhpDoc->getTemplateTags(), @@ -59,6 +60,9 @@ public function processNode(Node $node, Scope $scope): array 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), + sprintf('PHPDoc tag @template %%s for trait %s has invalid default type %%s.', $escapedTraitName), + sprintf('Default type %%s in PHPDoc tag @template %%s for trait %s is not subtype of bound type %%s.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s does not have a default type but follows an optional @template %%s.', $escapedTraitName), ); } diff --git a/src/Rules/Generics/UsedTraitsRule.php b/src/Rules/Generics/UsedTraitsRule.php index fa8ec17a47..ef698a4061 100644 --- a/src/Rules/Generics/UsedTraitsRule.php +++ b/src/Rules/Generics/UsedTraitsRule.php @@ -12,12 +12,13 @@ use PHPStan\Type\Type; use function array_map; use function sprintf; +use function strtolower; use function ucfirst; /** * @implements Rule */ -class UsedTraitsRule implements Rule +final class UsedTraitsRule implements Rule { public function __construct( @@ -56,26 +57,32 @@ public function processNode(Node $node, Scope $scope): array $useTags = $resolvedPhpDoc->getUsesTags(); } - $description = sprintf('class %s', SprintfHelper::escapeFormatString($className)); - $typeDescription = 'class'; + $typeDescription = strtolower($scope->getClassReflection()->getClassTypeDescription()); + $description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($className)); if ($traitName !== null) { - $description = sprintf('trait %s', SprintfHelper::escapeFormatString($traitName)); $typeDescription = 'trait'; + $description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($traitName)); } + $escapedDescription = SprintfHelper::escapeFormatString($description); + $upperCaseDescription = ucfirst($description); + $escapedUpperCaseDescription = SprintfHelper::escapeFormatString($upperCaseDescription); + return $this->genericAncestorsCheck->check( $node->traits, 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), + sprintf('%s @use tag contains incompatible type %%s.', $escapedUpperCaseDescription), + sprintf('%s @use tag contains unresolvable type.', $upperCaseDescription), + sprintf('%s has @use tag, but does not use any trait.', $upperCaseDescription), + sprintf('The @use tag of %s describes %%s but the %s uses %%s.', $escapedDescription, $typeDescription), '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.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @use is not allowed.', 'PHPDoc tag @use has invalid type %s.', - sprintf('%s uses generic trait %%s but does not specify its types: %%s', ucfirst($description)), - sprintf('in used type %%s of %s', $description), + sprintf('%s uses generic trait %%s but does not specify its types: %%s', $escapedUpperCaseDescription), + sprintf('in used type %%s of %s', $escapedDescription), ); } diff --git a/src/Rules/Generics/VarianceCheck.php b/src/Rules/Generics/VarianceCheck.php index 34f2e4ffe6..d01dbf75a3 100644 --- a/src/Rules/Generics/VarianceCheck.php +++ b/src/Rules/Generics/VarianceCheck.php @@ -2,39 +2,34 @@ namespace PHPStan\Rules\Generics; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Rules\RuleError; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; use function sprintf; -class VarianceCheck +final class VarianceCheck { - /** @return RuleError[] */ + /** + * @param 'function'|'method' $identifier + * @return list + */ public function checkParametersAcceptor( - ParametersAcceptor $parametersAcceptor, + ExtendedParametersAcceptor $parametersAcceptor, string $parameterTypeMessage, + string $parameterOutTypeMessage, string $returnTypeMessage, string $generalMessage, bool $isStatic, + bool $isPrivate, + string $identifier, ): array { $errors = []; - foreach ($parametersAcceptor->getParameters() as $parameterReflection) { - $variance = $isStatic - ? TemplateTypeVariance::createStatic() - : TemplateTypeVariance::createContravariant(); - $type = $parameterReflection->getType(); - $message = sprintf($parameterTypeMessage, $parameterReflection->getName()); - foreach ($this->check($variance, $type, $message) as $error) { - $errors[] = $error; - } - } - foreach ($parametersAcceptor->getTemplateTypeMap()->getTypes() as $templateType) { if (!$templateType instanceof TemplateType || $templateType->getScope()->getFunctionName() === null @@ -47,19 +42,43 @@ public function checkParametersAcceptor( 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type %s in %s.', $templateType->getName(), $generalMessage, - ))->build(); + ))->identifier(sprintf('%s.variance', $identifier))->build(); + } + + if ($isPrivate) { + return $errors; + } + + $covariant = TemplateTypeVariance::createCovariant(); + $parameterVariance = TemplateTypeVariance::createContravariant(); + + foreach ($parametersAcceptor->getParameters() as $parameterReflection) { + $type = $parameterReflection->getType(); + $message = sprintf($parameterTypeMessage, $parameterReflection->getName()); + foreach ($this->check($parameterVariance, $type, $message) as $error) { + $errors[] = $error; + } + + $paramOutType = $parameterReflection->getOutType(); + if ($paramOutType === null) { + continue; + } + + $outMessage = sprintf($parameterOutTypeMessage, $parameterReflection->getName()); + foreach ($this->check($covariant, $paramOutType, $outMessage) as $error) { + $errors[] = $error; + } } - $variance = TemplateTypeVariance::createCovariant(); $type = $parametersAcceptor->getReturnType(); - foreach ($this->check($variance, $type, $returnTypeMessage) as $error) { + foreach ($this->check($covariant, $type, $returnTypeMessage) as $error) { $errors[] = $error; } return $errors; } - /** @return RuleError[] */ + /** @return list */ public function check(TemplateTypeVariance $positionVariance, Type $type, string $messageContext): array { $errors = []; @@ -77,7 +96,7 @@ public function check(TemplateTypeVariance $positionVariance, Type $type, string $referredType->getVariance()->describe(), $reference->getPositionVariance()->describe(), $messageContext, - ))->build(); + ))->identifier('generics.variance')->build(); } return $errors; diff --git a/src/Rules/IdentifierRuleError.php b/src/Rules/IdentifierRuleError.php index fb556fc875..b7e32e019a 100644 --- a/src/Rules/IdentifierRuleError.php +++ b/src/Rules/IdentifierRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface IdentifierRuleError extends RuleError { diff --git a/src/Rules/Ignore/IgnoreParseErrorRule.php b/src/Rules/Ignore/IgnoreParseErrorRule.php new file mode 100644 index 0000000000..e44f5de4c9 --- /dev/null +++ b/src/Rules/Ignore/IgnoreParseErrorRule.php @@ -0,0 +1,47 @@ + + */ +final class IgnoreParseErrorRule implements Rule +{ + + public function getNodeType(): string + { + return FileNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $nodes = $node->getNodes(); + if (count($nodes) === 0) { + return []; + } + + $firstNode = $nodes[0]; + $parseErrors = $firstNode->getAttribute('linesToIgnoreParseErrors', []); + $errors = []; + foreach ($parseErrors as $line => $lineParseErrors) { + foreach ($lineParseErrors as $parseError) { + $errors[] = RuleErrorBuilder::message(sprintf('Parse error in @phpstan-ignore: %s', $parseError)) + ->line($line) + ->identifier('ignore.parseError') + ->nonIgnorable() + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/InternalTag/RestrictedInternalClassConstantUsageExtension.php b/src/Rules/InternalTag/RestrictedInternalClassConstantUsageExtension.php new file mode 100644 index 0000000000..6971c7b82e --- /dev/null +++ b/src/Rules/InternalTag/RestrictedInternalClassConstantUsageExtension.php @@ -0,0 +1,92 @@ +isInternal()->yes(); + $declaringClass = $constantReflection->getDeclaringClass(); + $isDeclaringClassInternal = $declaringClass->isInternal(); + if (!$isConstantInternal && !$isDeclaringClassInternal) { + return null; + } + + $declaringClassName = $declaringClass->getName(); + if (!$this->helper->shouldBeReported($scope, $declaringClassName)) { + return null; + } + + $namespace = array_slice(explode('\\', $declaringClassName), 0, -1)[0] ?? null; + if ($namespace === null) { + if (!$isConstantInternal) { + return RestrictedUsage::create( + sprintf( + 'Access to constant %s of internal %s %s.', + $constantReflection->getName(), + strtolower($constantReflection->getDeclaringClass()->getClassTypeDescription()), + $constantReflection->getDeclaringClass()->getDisplayName(), + ), + sprintf( + 'classConstant.internal%s', + $constantReflection->getDeclaringClass()->getClassTypeDescription(), + ), + ); + } + + return RestrictedUsage::create( + sprintf( + 'Access to internal constant %s::%s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + ), + 'classConstant.internal', + ); + } + + if (!$isConstantInternal) { + return RestrictedUsage::create( + sprintf( + 'Access to constant %s of internal %s %s from outside its root namespace %s.', + $constantReflection->getName(), + strtolower($constantReflection->getDeclaringClass()->getClassTypeDescription()), + $constantReflection->getDeclaringClass()->getDisplayName(), + $namespace, + ), + sprintf( + 'classConstant.internal%s', + $constantReflection->getDeclaringClass()->getClassTypeDescription(), + ), + ); + } + + return RestrictedUsage::create( + sprintf( + 'Access to internal constant %s::%s from outside its root namespace %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $namespace, + ), + 'classConstant.internal', + ); + } + +} diff --git a/src/Rules/InternalTag/RestrictedInternalClassNameUsageExtension.php b/src/Rules/InternalTag/RestrictedInternalClassNameUsageExtension.php new file mode 100644 index 0000000000..d8b3a32e9a --- /dev/null +++ b/src/Rules/InternalTag/RestrictedInternalClassNameUsageExtension.php @@ -0,0 +1,69 @@ +isInternal()) { + return null; + } + + if (!$this->helper->shouldBeReported($scope, $classReflection->getName())) { + return null; + } + + if ($location->value === ClassNameUsageLocation::STATIC_METHOD_CALL) { + $method = $location->getMethod(); + if ($method !== null) { + if ($method->isInternal()->yes() || $method->getDeclaringClass()->isInternal()) { + return null; + } + } + } + + if ($location->value === ClassNameUsageLocation::STATIC_PROPERTY_ACCESS) { + $property = $location->getProperty(); + if ($property !== null) { + if ($property->isInternal()->yes() || $property->getDeclaringClass()->isInternal()) { + return null; + } + } + } + + if ($location->value === ClassNameUsageLocation::CLASS_CONSTANT_ACCESS) { + $constant = $location->getClassConstant(); + if ($constant !== null) { + if ($constant->isInternal()->yes() || $constant->getDeclaringClass()->isInternal()) { + return null; + } + } + } + + return RestrictedUsage::create( + $location->createMessage(sprintf('internal %s %s', strtolower($classReflection->getClassTypeDescription()), $classReflection->getDisplayName())), + $location->createIdentifier(sprintf('internal%s', $classReflection->getClassTypeDescription())), + ); + } + +} diff --git a/src/Rules/InternalTag/RestrictedInternalFunctionUsageExtension.php b/src/Rules/InternalTag/RestrictedInternalFunctionUsageExtension.php new file mode 100644 index 0000000000..1ab605cdd9 --- /dev/null +++ b/src/Rules/InternalTag/RestrictedInternalFunctionUsageExtension.php @@ -0,0 +1,51 @@ +isInternal()->yes()) { + return null; + } + + if (!$this->helper->shouldBeReported($scope, $functionReflection->getName())) { + return null; + } + + $namespace = array_slice(explode('\\', $functionReflection->getName()), 0, -1)[0] ?? null; + if ($namespace === null) { + return RestrictedUsage::create( + sprintf( + 'Call to internal function %s().', + $functionReflection->getName(), + ), + 'function.internal', + ); + } + + return RestrictedUsage::create( + sprintf( + 'Call to internal function %s() from outside its root namespace %s.', + $functionReflection->getName(), + $namespace, + ), + 'function.internal', + ); + } + +} diff --git a/src/Rules/InternalTag/RestrictedInternalMethodUsageExtension.php b/src/Rules/InternalTag/RestrictedInternalMethodUsageExtension.php new file mode 100644 index 0000000000..fe611165d0 --- /dev/null +++ b/src/Rules/InternalTag/RestrictedInternalMethodUsageExtension.php @@ -0,0 +1,98 @@ +isInternal()->yes(); + $declaringClass = $methodReflection->getDeclaringClass(); + $isDeclaringClassInternal = $declaringClass->isInternal(); + if (!$isMethodInternal && !$isDeclaringClassInternal) { + return null; + } + + $declaringClassName = $declaringClass->getName(); + if (!$this->helper->shouldBeReported($scope, $declaringClassName)) { + return null; + } + + $namespace = array_slice(explode('\\', $declaringClassName), 0, -1)[0] ?? null; + if ($namespace === null) { + if (!$isMethodInternal) { + return RestrictedUsage::create( + sprintf( + 'Call to %smethod %s() of internal %s %s.', + $methodReflection->isStatic() ? 'static ' : '', + $methodReflection->getName(), + strtolower($methodReflection->getDeclaringClass()->getClassTypeDescription()), + $methodReflection->getDeclaringClass()->getDisplayName(), + ), + sprintf( + '%s.internal%s', + $methodReflection->isStatic() ? 'staticMethod' : 'method', + $methodReflection->getDeclaringClass()->getClassTypeDescription(), + ), + ); + } + + return RestrictedUsage::create( + sprintf( + 'Call to internal %smethod %s::%s().', + $methodReflection->isStatic() ? 'static ' : '', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName(), + ), + sprintf('%s.internal', $methodReflection->isStatic() ? 'staticMethod' : 'method'), + ); + } + + if (!$isMethodInternal) { + return RestrictedUsage::create( + sprintf( + 'Call to %smethod %s() of internal %s %s from outside its root namespace %s.', + $methodReflection->isStatic() ? 'static ' : '', + $methodReflection->getName(), + strtolower($methodReflection->getDeclaringClass()->getClassTypeDescription()), + $methodReflection->getDeclaringClass()->getDisplayName(), + $namespace, + ), + sprintf( + '%s.internal%s', + $methodReflection->isStatic() ? 'staticMethod' : 'method', + $methodReflection->getDeclaringClass()->getClassTypeDescription(), + ), + ); + } + + return RestrictedUsage::create( + sprintf( + 'Call to internal %smethod %s::%s() from outside its root namespace %s.', + $methodReflection->isStatic() ? 'static ' : '', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName(), + $namespace, + ), + sprintf('%s.internal', $methodReflection->isStatic() ? 'staticMethod' : 'method'), + ); + } + +} diff --git a/src/Rules/InternalTag/RestrictedInternalPropertyUsageExtension.php b/src/Rules/InternalTag/RestrictedInternalPropertyUsageExtension.php new file mode 100644 index 0000000000..e6bbf4f3b9 --- /dev/null +++ b/src/Rules/InternalTag/RestrictedInternalPropertyUsageExtension.php @@ -0,0 +1,98 @@ +isInternal()->yes(); + $declaringClass = $propertyReflection->getDeclaringClass(); + $isDeclaringClassInternal = $declaringClass->isInternal(); + if (!$isPropertyInternal && !$isDeclaringClassInternal) { + return null; + } + + $declaringClassName = $declaringClass->getName(); + if (!$this->helper->shouldBeReported($scope, $declaringClassName)) { + return null; + } + + $namespace = array_slice(explode('\\', $declaringClassName), 0, -1)[0] ?? null; + if ($namespace === null) { + if (!$isPropertyInternal) { + return RestrictedUsage::create( + sprintf( + 'Access to %sproperty $%s of internal %s %s.', + $propertyReflection->isStatic() ? 'static ' : '', + $propertyReflection->getName(), + strtolower($propertyReflection->getDeclaringClass()->getClassTypeDescription()), + $propertyReflection->getDeclaringClass()->getDisplayName(), + ), + sprintf( + '%s.internal%s', + $propertyReflection->isStatic() ? 'staticProperty' : 'property', + $propertyReflection->getDeclaringClass()->getClassTypeDescription(), + ), + ); + } + + return RestrictedUsage::create( + sprintf( + 'Access to internal %sproperty %s::$%s.', + $propertyReflection->isStatic() ? 'static ' : '', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $propertyReflection->getName(), + ), + sprintf('%s.internal', $propertyReflection->isStatic() ? 'staticProperty' : 'property'), + ); + } + + if (!$isPropertyInternal) { + return RestrictedUsage::create( + sprintf( + 'Access to %sproperty $%s of internal %s %s from outside its root namespace %s.', + $propertyReflection->isStatic() ? 'static ' : '', + $propertyReflection->getName(), + strtolower($propertyReflection->getDeclaringClass()->getClassTypeDescription()), + $propertyReflection->getDeclaringClass()->getDisplayName(), + $namespace, + ), + sprintf( + '%s.internal%s', + $propertyReflection->isStatic() ? 'staticProperty' : 'property', + $propertyReflection->getDeclaringClass()->getClassTypeDescription(), + ), + ); + } + + return RestrictedUsage::create( + sprintf( + 'Access to internal %sproperty %s::$%s from outside its root namespace %s.', + $propertyReflection->isStatic() ? 'static ' : '', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $propertyReflection->getName(), + $namespace, + ), + sprintf('%s.internal', $propertyReflection->isStatic() ? 'staticProperty' : 'property'), + ); + } + +} diff --git a/src/Rules/InternalTag/RestrictedInternalUsageHelper.php b/src/Rules/InternalTag/RestrictedInternalUsageHelper.php new file mode 100644 index 0000000000..1767c02fbb --- /dev/null +++ b/src/Rules/InternalTag/RestrictedInternalUsageHelper.php @@ -0,0 +1,26 @@ +getNamespace(); + $namespace = array_slice(explode('\\', $name), 0, -1)[0] ?? null; + if ($currentNamespace === null) { + return true; + } + + $currentNamespace = explode('\\', $currentNamespace)[0]; + + return !str_starts_with($namespace . '\\', $currentNamespace . '\\'); + } + +} diff --git a/src/Rules/IssetCheck.php b/src/Rules/IssetCheck.php index 6b5fec3210..9669fccc8a 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -5,15 +5,20 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\PropertyInitializationExpr; use PHPStan\Rules\Properties\PropertyDescriptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; -use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function is_string; use function sprintf; +use function str_starts_with; -class IssetCheck +/** + * @phpstan-type ErrorIdentifier = 'empty'|'isset'|'nullCoalesce' + */ +final class IssetCheck { public function __construct( @@ -26,9 +31,10 @@ public function __construct( } /** + * @param ErrorIdentifier $identifier * @param callable(Type): ?string $typeMessageCallback */ - public function check(Expr $expr, Scope $scope, string $operatorDescription, callable $typeMessageCallback, ?RuleError $error = null): ?RuleError + public function check(Expr $expr, Scope $scope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?IdentifierRuleError $error = null): ?IdentifierRuleError { // mirrored in PHPStan\Analyser\MutatingScope::issetCheck() if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { @@ -43,14 +49,21 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal return null; } - return $this->generateError( - $scope->getVariableType($expr->name), - sprintf('Variable $%s %s always exists and', $expr->name, $operatorDescription), - $typeMessageCallback, - ); + $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr) : $scope->getNativeType($expr); + if (!$type instanceof NeverType) { + return $this->generateError( + $type, + sprintf('Variable $%s %s always exists and', $expr->name, $operatorDescription), + $typeMessageCallback, + $identifier, + 'variable', + ); + } } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription))->build(); + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + ->identifier(sprintf('%s.variable', $identifier)) + ->build(); } return $error; @@ -58,19 +71,15 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->var) : $scope->getNativeType($expr->var); + if (!$type->isOffsetAccessible()->yes()) { + return $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + } + $dimType = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->dim) : $scope->getNativeType($expr->dim); $hasOffsetValue = $type->hasOffsetValueType($dimType); - if (!$type->isOffsetAccessible()->yes()) { - return $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription); - } - if ($hasOffsetValue->no()) { - if ($error !== null) { - return $error; - } - if (!$this->checkAdvancedIsset) { return null; } @@ -82,33 +91,25 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $type->describe(VerbosityLevel::value()), $operatorDescription, ), - )->build(); - } - - if ($hasOffsetValue->maybe()) { - return null; + )->identifier(sprintf('%s.offset', $identifier))->build(); } - // If offset is cannot be null, store this error message and see if one of the earlier offsets is. + // If offset cannot be null, store this error message and see if one of the earlier offsets is. // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null. - if ($hasOffsetValue->yes()) { - if ($error !== null) { - return $error; - } - + if ($hasOffsetValue->yes() || $scope->hasExpressionType($expr)->yes()) { if (!$this->checkAdvancedIsset) { return null; } - $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, - ), $typeMessageCallback); + ), $typeMessageCallback, $identifier, 'offset'); if ($error !== null) { - return $this->check($expr->var, $scope, $operatorDescription, $typeMessageCallback, $error); + return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } @@ -121,11 +122,11 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal if ($propertyReflection === null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -133,43 +134,73 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal if (!$propertyReflection->isNative()) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; } - $nativeType = $propertyReflection->getNativeType(); - if (!$nativeType instanceof MixedType) { - if (!$scope->isSpecified($expr)) { + if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { + if ( + $expr instanceof Node\Expr\PropertyFetch + && $expr->name instanceof Node\Identifier + && $expr->var instanceof Expr\Variable + && $expr->var->name === 'this' + && $scope->hasExpressionType(new PropertyInitializationExpr($propertyReflection->getName()))->yes() + ) { + return $this->generateError( + $propertyReflection->getNativeType(), + sprintf( + '%s %s', + $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr), + $operatorDescription, + ), + static function (Type $type) use ($typeMessageCallback): ?string { + $originalMessage = $typeMessageCallback($type); + if ($originalMessage === null) { + return null; + } + + if (str_starts_with($originalMessage, 'is not')) { + return sprintf('%s nor uninitialized', $originalMessage); + } + + return sprintf('%s and initialized', $originalMessage); + }, + $identifier, + 'initializedProperty', + ); + } + + if (!$scope->hasExpressionType($expr)->yes()) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; } } - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $expr); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr); $propertyType = $propertyReflection->getWritableType(); if ($error !== null) { return $error; } if (!$this->checkAdvancedIsset) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -179,15 +210,17 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $propertyReflection->getWritableType(), sprintf('%s (%s) %s', $propertyDescription, $propertyType->describe(VerbosityLevel::typeOnly()), $operatorDescription), $typeMessageCallback, + $identifier, + 'property', ); if ($error !== null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->check($expr->var, $scope, $operatorDescription, $typeMessageCallback, $error); + return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } if ($expr->class instanceof Expr) { - return $this->check($expr->class, $scope, $operatorDescription, $typeMessageCallback, $error); + return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } @@ -202,10 +235,36 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal return null; } - return $this->generateError($scope->getType($expr), sprintf('Expression %s', $operatorDescription), $typeMessageCallback); + $error = $this->generateError( + $this->treatPhpDocTypesAsCertain ? $scope->getType($expr) : $scope->getNativeType($expr), + sprintf('Expression %s', $operatorDescription), + $typeMessageCallback, + $identifier, + 'expr', + ); + if ($error !== null) { + return $error; + } + + if ($expr instanceof Expr\NullsafePropertyFetch) { + if ($expr->name instanceof Node\Identifier) { + return RuleErrorBuilder::message(sprintf('Using nullsafe property access "?->%s" %s is unnecessary. Use -> instead.', $expr->name->name, $operatorDescription)) + ->identifier('nullsafe.neverNull') + ->build(); + } + + return RuleErrorBuilder::message(sprintf('Using nullsafe property access "?->(Expression)" %s is unnecessary. Use -> instead.', $operatorDescription)) + ->identifier('nullsafe.neverNull') + ->build(); + } + + return null; } - private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription): ?RuleError + /** + * @param ErrorIdentifier $identifier + */ + private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription, string $identifier): ?IdentifierRuleError { if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { $hasVariable = $scope->hasVariableType($expr->name); @@ -213,19 +272,21 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri return null; } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription))->build(); + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + ->identifier(sprintf('%s.variable', $identifier)) + ->build(); } if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - $type = $scope->getType($expr->var); - $dimType = $scope->getType($expr->dim); + $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->var) : $scope->getNativeType($expr->var); + $dimType = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->dim) : $scope->getNativeType($expr->dim); $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$type->isOffsetAccessible()->yes()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if (!$hasOffsetValue->no()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } return RuleErrorBuilder::message( @@ -235,15 +296,15 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri $type->describe(VerbosityLevel::value()), $operatorDescription, ), - )->build(); + )->identifier(sprintf('%s.offset', $identifier))->build(); } if ($expr instanceof Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -251,8 +312,10 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri /** * @param callable(Type): ?string $typeMessageCallback + * @param ErrorIdentifier $identifier + * @param 'variable'|'offset'|'property'|'expr'|'initializedProperty' $identifierSecondPart */ - private function generateError(Type $type, string $message, callable $typeMessageCallback): ?RuleError + private function generateError(Type $type, string $message, callable $typeMessageCallback, string $identifier, string $identifierSecondPart): ?IdentifierRuleError { $typeMessage = $typeMessageCallback($type); if ($typeMessage === null) { @@ -261,7 +324,7 @@ private function generateError(Type $type, string $message, callable $typeMessag return RuleErrorBuilder::message( sprintf('%s %s.', $message, $typeMessage), - )->build(); + )->identifier(sprintf('%s.%s', $identifier, $identifierSecondPart))->build(); } } diff --git a/src/Rules/Keywords/ContinueBreakInLoopRule.php b/src/Rules/Keywords/ContinueBreakInLoopRule.php index 0ccfea7527..4f421e5a6c 100644 --- a/src/Rules/Keywords/ContinueBreakInLoopRule.php +++ b/src/Rules/Keywords/ContinueBreakInLoopRule.php @@ -5,15 +5,16 @@ use PhpParser\Node; use PhpParser\Node\Stmt; use PHPStan\Analyser\Scope; +use PHPStan\Parser\ParentStmtTypesVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; +use function array_reverse; use function sprintf; /** * @implements Rule */ -class ContinueBreakInLoopRule implements Rule +final class ContinueBreakInLoopRule implements Rule { public function getNodeType(): string @@ -27,44 +28,52 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$node->num instanceof Node\Scalar\LNumber) { + if (!$node->num instanceof Node\Scalar\Int_) { $value = 1; } else { $value = $node->num->value; } - $parent = $node->getAttribute('parent'); - while ($value > 0) { - if ( - $parent === null - || $parent instanceof Stmt\Function_ - || $parent instanceof Stmt\ClassMethod - || $parent instanceof Node\Expr\Closure - ) { + $parentStmtTypes = array_reverse($node->getAttribute(ParentStmtTypesVisitor::ATTRIBUTE_NAME)); + foreach ($parentStmtTypes as $parentStmtType) { + if ($parentStmtType === Stmt\Case_::class) { + continue; + } + if ($parentStmtType === Node\Expr\Closure::class) { return [ RuleErrorBuilder::message(sprintf( 'Keyword %s used outside of a loop or a switch statement.', $node instanceof Stmt\Continue_ ? 'continue' : 'break', - ))->nonIgnorable()->build(), + )) + ->nonIgnorable() + ->identifier(sprintf('%s.outOfLoop', $node instanceof Stmt\Continue_ ? 'continue' : 'break')) + ->build(), ]; } if ( - $parent instanceof Stmt\For_ - || $parent instanceof Stmt\Foreach_ - || $parent instanceof Stmt\Do_ - || $parent instanceof Stmt\While_ + $parentStmtType === Stmt\For_::class + || $parentStmtType === Stmt\Foreach_::class + || $parentStmtType === Stmt\Do_::class + || $parentStmtType === Stmt\While_::class + || $parentStmtType === Stmt\Switch_::class ) { $value--; } - if ($parent instanceof Stmt\Case_) { - $value--; - $parent = $parent->getAttribute('parent'); - if (!$parent instanceof Stmt\Switch_) { - throw new ShouldNotHappenException(); - } + if ($value === 0) { + break; } + } - $parent = $parent->getAttribute('parent'); + if ($value > 0) { + return [ + RuleErrorBuilder::message(sprintf( + 'Keyword %s used outside of a loop or a switch statement.', + $node instanceof Stmt\Continue_ ? 'continue' : 'break', + )) + ->nonIgnorable() + ->identifier(sprintf('%s.outOfLoop', $node instanceof Stmt\Continue_ ? 'continue' : 'break')) + ->build(), + ]; } return []; diff --git a/src/Rules/Keywords/DeclareStrictTypesRule.php b/src/Rules/Keywords/DeclareStrictTypesRule.php new file mode 100644 index 0000000000..66aaa94026 --- /dev/null +++ b/src/Rules/Keywords/DeclareStrictTypesRule.php @@ -0,0 +1,80 @@ + + */ +final class DeclareStrictTypesRule implements Rule +{ + + public function __construct( + private readonly ExprPrinter $exprPrinter, + ) + { + } + + public function getNodeType(): string + { + return Stmt\Declare_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $declaresStrictTypes = false; + foreach ($node->declares as $declare) { + if ( + $declare->key->name !== 'strict_types' + ) { + continue; + } + + if ( + !$declare->value instanceof Node\Scalar\Int_ + || !in_array($declare->value->value, [0, 1], true) + ) { + return [ + RuleErrorBuilder::message(sprintf( + sprintf( + 'Declare strict_types must have 0 or 1 as its value, %s given.', + $this->exprPrinter->printExpr($declare->value), + ), + ))->identifier('declareStrictTypes.value')->nonIgnorable()->build(), + ]; + } + + $declaresStrictTypes = true; + break; + } + + if ($declaresStrictTypes === false) { + return []; + } + + if (!$node->hasAttribute(DeclarePositionVisitor::ATTRIBUTE_NAME)) { + return []; + } + + $isFirstStatement = (bool) $node->getAttribute(DeclarePositionVisitor::ATTRIBUTE_NAME); + if ($isFirstStatement) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Declare strict_types must be the very first statement.', + ))->identifier('declareStrictTypes.notFirst')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Keywords/RequireFileExistsRule.php b/src/Rules/Keywords/RequireFileExistsRule.php new file mode 100644 index 0000000000..8ccf92e3df --- /dev/null +++ b/src/Rules/Keywords/RequireFileExistsRule.php @@ -0,0 +1,137 @@ + + */ +final class RequireFileExistsRule implements Rule +{ + + public function __construct(private string $currentWorkingDirectory) + { + } + + public function getNodeType(): string + { + return Include_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + $paths = $this->resolveFilePaths($node, $scope); + + foreach ($paths as $path) { + if ($this->doesFileExist($path, $scope)) { + continue; + } + + $errors[] = $this->getErrorMessage($node, $path); + } + + return $errors; + } + + /** + * We cannot use `stream_resolve_include_path` as it works based on the calling script. + * This method simulates the behavior of `stream_resolve_include_path` but for the given scope. + * The priority order is the following: + * 1. The current working directory. + * 2. The include path. + * 3. The path of the script that is being executed. + */ + private function doesFileExist(string $path, Scope $scope): bool + { + $directories = array_merge( + [$this->currentWorkingDirectory], + explode(PATH_SEPARATOR, get_include_path()), + [dirname($scope->getFile())], + ); + + foreach ($directories as $directory) { + if ($this->doesFileExistForDirectory($path, $directory)) { + return true; + } + } + + return false; + } + + private function doesFileExistForDirectory(string $path, string $workingDirectory): bool + { + $fileHelper = new FileHelper($workingDirectory); + $absolutePath = $fileHelper->absolutizePath($path); + + return is_file($absolutePath); + } + + private function getErrorMessage(Include_ $node, string $filePath): IdentifierRuleError + { + $message = 'Path in %s() "%s" is not a file or it does not exist.'; + + switch ($node->type) { + case Include_::TYPE_REQUIRE: + $type = 'require'; + $identifierType = 'require'; + break; + case Include_::TYPE_REQUIRE_ONCE: + $type = 'require_once'; + $identifierType = 'requireOnce'; + break; + case Include_::TYPE_INCLUDE: + $type = 'include'; + $identifierType = 'include'; + break; + case Include_::TYPE_INCLUDE_ONCE: + $type = 'include_once'; + $identifierType = 'includeOnce'; + break; + default: + throw new ShouldNotHappenException('Rule should have already validated the node type.'); + } + + $identifier = sprintf('%s.fileNotFound', $identifierType); + + return RuleErrorBuilder::message( + sprintf( + $message, + $type, + $filePath, + ), + )->identifier($identifier)->build(); + } + + /** + * @return array + */ + private function resolveFilePaths(Include_ $node, Scope $scope): array + { + $paths = []; + $type = $scope->getType($node->expr); + $constantStrings = $type->getConstantStrings(); + + foreach ($constantStrings as $constantString) { + $paths[] = $constantString->getValue(); + } + + return $paths; + } + +} diff --git a/src/Rules/LazyRegistry.php b/src/Rules/LazyRegistry.php new file mode 100644 index 0000000000..ec5b1dc13b --- /dev/null +++ b/src/Rules/LazyRegistry.php @@ -0,0 +1,71 @@ + $nodeType + * @return array> + */ + public function getRules(string $nodeType): array + { + if (!isset($this->cache[$nodeType])) { + $parentNodeTypes = [$nodeType] + class_parents($nodeType) + class_implements($nodeType); + + $rules = []; + $rulesFromContainer = $this->getRulesFromContainer(); + foreach ($parentNodeTypes as $parentNodeType) { + foreach ($rulesFromContainer[$parentNodeType] ?? [] as $rule) { + $rules[] = $rule; + } + } + + $this->cache[$nodeType] = $rules; + } + + /** + * @var array> $selectedRules + */ + $selectedRules = $this->cache[$nodeType]; + + return $selectedRules; + } + + /** + * @return Rule[][] + */ + private function getRulesFromContainer(): array + { + if ($this->rules !== null) { + return $this->rules; + } + + $rules = []; + foreach ($this->container->getServicesByTag(self::RULE_TAG) as $rule) { + $rules[$rule->getNodeType()][] = $rule; + } + + return $this->rules = $rules; + } + +} diff --git a/src/Rules/LineRuleError.php b/src/Rules/LineRuleError.php index 0388b7fca7..986840eff2 100644 --- a/src/Rules/LineRuleError.php +++ b/src/Rules/LineRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface LineRuleError extends RuleError { diff --git a/src/Rules/MetadataRuleError.php b/src/Rules/MetadataRuleError.php index 01b6d15515..5123d37c80 100644 --- a/src/Rules/MetadataRuleError.php +++ b/src/Rules/MetadataRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface MetadataRuleError extends RuleError { diff --git a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php index da1a3950b7..82c92a4ecd 100644 --- a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php +++ b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php @@ -7,12 +7,13 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; +use function in_array; use function sprintf; /** * @implements Rule */ -class AbstractMethodInNonAbstractClassRule implements Rule +final class AbstractMethodInNonAbstractClassRule implements Rule { public function getNodeType(): string @@ -27,17 +28,48 @@ public function processNode(Node $node, Scope $scope): array } $class = $scope->getClassReflection(); - if ($class->isAbstract()) { - return []; + + if (!$class->isAbstract() && $node->isAbstract()) { + if ($class->isEnum()) { + $lowercasedMethodName = $node->name->toLowerString(); + if ($lowercasedMethodName === 'cases') { + return []; + } + if ($class->isBackedEnum()) { + if (in_array($lowercasedMethodName, ['from', 'tryfrom'], true)) { + return []; + } + } + } + + $description = $class->getClassTypeDescription(); + return [ + RuleErrorBuilder::message(sprintf( + '%s %s contains abstract method %s().', + $description === 'Class' ? 'Non-abstract class' : $description, + $class->getDisplayName(), + $node->name->toString(), + )) + ->nonIgnorable() + ->identifier('method.abstract') + ->build(), + ]; } - if (!$node->isAbstract()) { - return []; + if (!$class->isAbstract() && !$class->isInterface() && $node->getStmts() === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Non-abstract method %s::%s() must contain a body.', + $class->getDisplayName(), + $node->name->toString(), + )) + ->nonIgnorable() + ->identifier('method.nonAbstract') + ->build(), + ]; } - return [ - RuleErrorBuilder::message(sprintf('Non-abstract class %s contains abstract method %s().', $class->getDisplayName(), $node->name->toString()))->nonIgnorable()->build(), - ]; + return []; } } diff --git a/src/Rules/Methods/AbstractPrivateMethodRule.php b/src/Rules/Methods/AbstractPrivateMethodRule.php new file mode 100644 index 0000000000..d087a19ce1 --- /dev/null +++ b/src/Rules/Methods/AbstractPrivateMethodRule.php @@ -0,0 +1,58 @@ + */ +final class AbstractPrivateMethodRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + + if (!$method->isPrivate()) { + return []; + } + + if (!$method->isAbstract()->yes()) { + return []; + } + + if ($scope->isInTrait()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return []; + } + + if (!$classReflection->isAbstract() && !$classReflection->isInterface()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Private method %s::%s() cannot be abstract.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + )) + ->identifier('method.abstractPrivate') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Methods/AlwaysUsedMethodExtension.php b/src/Rules/Methods/AlwaysUsedMethodExtension.php new file mode 100644 index 0000000000..f52b6a9c2b --- /dev/null +++ b/src/Rules/Methods/AlwaysUsedMethodExtension.php @@ -0,0 +1,27 @@ + */ -class CallMethodsRule implements Rule +final class CallMethodsRule implements Rule { public function __construct( @@ -31,13 +34,35 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; + $errors = []; + if ($node->name instanceof Node\Identifier) { + $methodNameScopes = [$node->name->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $methodNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } } - $methodName = $node->name->name; + foreach ($methodNameScopes as $methodName => $methodScope) { + $errors = array_merge($errors, $this->processSingleMethodCall( + $methodScope, + $node, + (string) $methodName, // @phpstan-ignore cast.useless + )); + } + + return $errors; + } - [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodName, $node->var); + /** + * @return list + */ + private function processSingleMethodCall(Scope $scope, MethodCall $node, string $methodName): array + { + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodName, $node->var, $node->name); if ($methodReflection === null) { return $errors; } @@ -50,25 +75,28 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getArgs(), $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ), $scope, $declaringClass->isBuiltin(), $node, - [ - 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameter, at least %d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameters, at least %d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d-%d required.', - 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of method ' . $messagesMethodName . ' expects %s, %s given.', - 'Result of method ' . $messagesMethodName . ' (void) is used.', - 'Parameter %s of method ' . $messagesMethodName . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to method ' . $messagesMethodName, - '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.', - ], + 'method', + $methodReflection->acceptsNamedArguments(), + 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameter, at least %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameters, at least %d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d-%d required.', + 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d-%d required.', + '%s of method ' . $messagesMethodName . ' expects %s, %s given.', + 'Result of method ' . $messagesMethodName . ' (void) is used.', + '%s of method ' . $messagesMethodName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to method ' . $messagesMethodName, + '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.', + '%s of method ' . $messagesMethodName . ' contains unresolvable type.', + 'Method ' . $messagesMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); } diff --git a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php index 8fd13a19e5..d381cdc659 100644 --- a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php +++ b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php @@ -13,7 +13,7 @@ /** * @implements Rule */ -class CallPrivateMethodThroughStaticRule implements Rule +final class CallPrivateMethodThroughStaticRule implements Rule { public function getNodeType(): string @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array 'Unsafe call to private method %s::%s() through static::.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - ))->build(), + ))->identifier('staticClassAccess.privateMethod')->build(), ]; } diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index c53822e7a2..04ffc64325 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -3,11 +3,14 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use function array_merge; use function sprintf; @@ -15,7 +18,7 @@ /** * @implements Rule */ -class CallStaticMethodsRule implements Rule +final class CallStaticMethodsRule implements Rule { public function __construct( @@ -32,11 +35,34 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; + $errors = []; + if ($node->name instanceof Node\Identifier) { + $methodNameScopes = [$node->name->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $methodNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } } - $methodName = $node->name->name; + foreach ($methodNameScopes as $methodName => $methodScope) { + $errors = array_merge($errors, $this->processSingleMethodCall( + $methodScope, + $node, + (string) $methodName, // @phpstan-ignore cast.useless + )); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleMethodCall(Scope $scope, StaticCall $node, string $methodName): array + { [$errors, $method] = $this->methodCallCheck->check($scope, $methodName, $node->class); if ($method === null) { return $errors; @@ -58,25 +84,28 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getArgs(), $method->getVariants(), + $method->getNamedArgumentsVariants(), ), $scope, $method->getDeclaringClass()->isBuiltin(), $node, - [ - $displayMethodName . ' invoked with %d parameter, %d required.', - $displayMethodName . ' invoked with %d parameters, %d required.', - $displayMethodName . ' invoked with %d parameter, at least %d required.', - $displayMethodName . ' invoked with %d parameters, at least %d required.', - $displayMethodName . ' invoked with %d parameter, %d-%d required.', - $displayMethodName . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of ' . $lowercasedMethodName . ' expects %s, %s given.', - 'Result of ' . $lowercasedMethodName . ' (void) is used.', - 'Parameter %s of ' . $lowercasedMethodName . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to method ' . $lowercasedMethodName, - 'Missing parameter $%s in call to ' . $lowercasedMethodName . '.', - 'Unknown parameter $%s in call to ' . $lowercasedMethodName . '.', - 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', - ], + 'staticMethod', + $method->acceptsNamedArguments(), + $displayMethodName . ' invoked with %d parameter, %d required.', + $displayMethodName . ' invoked with %d parameters, %d required.', + $displayMethodName . ' invoked with %d parameter, at least %d required.', + $displayMethodName . ' invoked with %d parameters, at least %d required.', + $displayMethodName . ' invoked with %d parameter, %d-%d required.', + $displayMethodName . ' invoked with %d parameters, %d-%d required.', + '%s of ' . $lowercasedMethodName . ' expects %s, %s given.', + 'Result of ' . $lowercasedMethodName . ' (void) is used.', + '%s of ' . $lowercasedMethodName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to method ' . $lowercasedMethodName, + 'Missing parameter $%s in call to ' . $lowercasedMethodName . '.', + 'Unknown parameter $%s in call to ' . $lowercasedMethodName . '.', + 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', + '%s of ' . $lowercasedMethodName . ' contains unresolvable type.', + $displayMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); return $errors; diff --git a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php index 713f3996af..f214edf960 100644 --- a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php @@ -4,35 +4,37 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\NoopExpressionNode; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; -use PHPStan\Type\VoidType; use function sprintf; /** - * @implements Rule + * @implements Rule */ -class CallToConstructorStatementWithoutSideEffectsRule implements Rule +final class CallToConstructorStatementWithoutSideEffectsRule implements Rule { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + ) { } public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->expr instanceof Node\Expr\New_) { + $instantiation = $node->getOriginalExpr(); + if (!$instantiation instanceof Node\Expr\New_) { return []; } - $instantiation = $node->expr; if (!$instantiation->class instanceof Node\Name) { return []; } @@ -44,31 +46,27 @@ public function processNode(Node $node, Scope $scope): array $classReflection = $this->reflectionProvider->getClass($className); if (!$classReflection->hasConstructor()) { - return []; - } - - $constructor = $classReflection->getConstructor(); - if ($constructor->hasSideEffects()->no()) { - $throwsType = $constructor->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { - return []; - } - - $methodResult = $scope->getType($instantiation); - if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { - return []; - } - return [ RuleErrorBuilder::message(sprintf( - 'Call to %s::%s() on a separate line has no effect.', + 'Call to new %s() on a separate line has no effect.', $classReflection->getDisplayName(), - $constructor->getName(), - ))->build(), + ))->identifier('new.resultUnused')->build(), ]; } - return []; + $constructor = $classReflection->getConstructor(); + $methodResult = $scope->getType($instantiation); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s::%s() on a separate line has no effect.', + $classReflection->getDisplayName(), + $constructor->getName(), + ))->identifier('new.resultUnused')->build(), + ]; } } diff --git a/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php index 93b9202984..c8ead1d217 100644 --- a/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php @@ -5,19 +5,19 @@ use PhpParser\Node; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Node\NoopExpressionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\VoidType; use function sprintf; /** - * @implements Rule + * @implements Rule */ -class CallToMethodStatementWithoutSideEffectsRule implements Rule +final class CallToMethodStatementWithoutSideEffectsRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -26,18 +26,18 @@ public function __construct(private RuleLevelHelper $ruleLevelHelper) public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - if ($node->expr instanceof Node\Expr\NullsafeMethodCall) { - $scope = $scope->filterByTruthyValue(new Node\Expr\BinaryOp\NotIdentical($node->expr->var, new Node\Expr\ConstFetch(new Node\Name('null')))); - } elseif (!$node->expr instanceof Node\Expr\MethodCall) { + $methodCall = $node->getOriginalExpr(); + if ($methodCall instanceof Node\Expr\NullsafeMethodCall) { + $scope = $scope->filterByTruthyValue(new Node\Expr\BinaryOp\NotIdentical($methodCall->var, new Node\Expr\ConstFetch(new Node\Name('null')))); + } elseif (!$methodCall instanceof Node\Expr\MethodCall) { return []; } - $methodCall = $node->expr; if (!$methodCall->name instanceof Node\Identifier) { return []; } @@ -61,31 +61,21 @@ public function processNode(Node $node, Scope $scope): array return []; } - $method = $calledOnType->getMethod($methodName, $scope); - 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); - if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Call to %s %s::%s() on a separate line has no effect.', - $method->isStatic() ? 'static method' : 'method', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - ))->build(), - ]; + $methodResult = $scope->getType($methodCall); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return []; } - return []; + $method = $calledOnType->getMethod($methodName, $scope); + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line has no effect.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('method.resultUnused')->build(), + ]; } } diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php index 593846a1d2..550ea9e019 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Node\NoopExpressionNode; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -13,14 +14,13 @@ use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\VoidType; use function sprintf; use function strtolower; /** - * @implements Rule + * @implements Rule */ -class CallToStaticMethodStatementWithoutSideEffectsRule implements Rule +final class CallToStaticMethodStatementWithoutSideEffectsRule implements Rule { public function __construct( @@ -32,16 +32,16 @@ public function __construct( public function getNodeType(): string { - return Node\Stmt\Expression::class; + return NoopExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->expr instanceof Node\Expr\StaticCall) { + $staticCall = $node->getOriginalExpr(); + if (!$staticCall instanceof Node\Expr\StaticCall) { return []; } - $staticCall = $node->expr; if (!$staticCall->name instanceof Node\Identifier) { return []; } @@ -85,30 +85,19 @@ public function processNode(Node $node, Scope $scope): array 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); - if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Call to %s %s::%s() on a separate line has no effect.', - $method->isStatic() ? 'static method' : 'method', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - ))->build(), - ]; + $methodResult = $scope->getType($staticCall); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return []; } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line has no effect.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('staticMethod.resultUnused')->build(), + ]; } } diff --git a/src/Rules/Methods/ConsistentConstructorRule.php b/src/Rules/Methods/ConsistentConstructorRule.php new file mode 100644 index 0000000000..16226a1074 --- /dev/null +++ b/src/Rules/Methods/ConsistentConstructorRule.php @@ -0,0 +1,58 @@ + */ +final class ConsistentConstructorRule implements Rule +{ + + public function __construct( + private MethodParameterComparisonHelper $methodParameterComparisonHelper, + private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper, + ) + { + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + if (strtolower($method->getName()) !== '__construct') { + return []; + } + + $parent = $method->getDeclaringClass()->getParentClass(); + + if ($parent === null) { + return []; + } + + if ($parent->hasConstructor()) { + $parentConstructor = $parent->getConstructor(); + } else { + $parentConstructor = new DummyConstructorReflection($parent); + } + + if (! $parentConstructor->getDeclaringClass()->hasConsistentConstructor()) { + return []; + } + + return array_merge( + $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, true), + $this->methodVisibilityComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method), + ); + } + +} diff --git a/src/Rules/Methods/ConstructorReturnTypeRule.php b/src/Rules/Methods/ConstructorReturnTypeRule.php new file mode 100644 index 0000000000..0f1388314d --- /dev/null +++ b/src/Rules/Methods/ConstructorReturnTypeRule.php @@ -0,0 +1,63 @@ + + */ +final class ConstructorReturnTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $methodNode = $node->getOriginalNode(); + if ($scope->isInTrait()) { + $originalMethodName = $methodNode->getAttribute('originalTraitMethodName'); + if ( + $originalMethodName === '__construct' + && $methodNode->returnType !== null + ) { + return [ + RuleErrorBuilder::message(sprintf('Original constructor of trait %s has a return type.', $scope->getTraitReflection()->getDisplayName())) + ->identifier('constructor.returnType') + ->nonIgnorable() + ->build(), + ]; + } + } + if (!$classReflection->hasConstructor()) { + return []; + } + + $constructorReflection = $classReflection->getConstructor(); + $methodReflection = $node->getMethodReflection(); + if ($methodReflection->getName() !== $constructorReflection->getName()) { + return []; + } + + if ($methodNode->returnType === null) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Constructor of class %s has a return type.', $classReflection->getDisplayName())) + ->identifier('constructor.returnType') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php b/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php new file mode 100644 index 0000000000..3012ef21f2 --- /dev/null +++ b/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php @@ -0,0 +1,20 @@ +extensions; + } + +} diff --git a/src/Rules/Methods/ExistingClassesInTypehintsRule.php b/src/Rules/Methods/ExistingClassesInTypehintsRule.php index 0f3ee88ed5..6127bac985 100644 --- a/src/Rules/Methods/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Methods/ExistingClassesInTypehintsRule.php @@ -6,16 +6,14 @@ 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 Rule */ -class ExistingClassesInTypehintsRule implements Rule +final class ExistingClassesInTypehintsRule implements Rule { public function __construct(private FunctionDefinitionCheck $check) @@ -29,18 +27,12 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { - throw new ShouldNotHappenException(); - } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $className = SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()); + $methodReflection = $node->getMethodReflection(); + $className = SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName()); $methodName = SprintfHelper::escapeFormatString($methodReflection->getName()); return $this->check->checkClassMethod( + $scope, $methodReflection, $node->getOriginalNode(), sprintf( @@ -65,6 +57,11 @@ public function processNode(Node $node, Scope $scope): array $className, $methodName, ), + sprintf( + 'Method %s::%s() has invalid @phpstan-self-out type %%s.', + $className, + $methodName, + ), ); } diff --git a/src/Rules/Methods/FinalPrivateMethodRule.php b/src/Rules/Methods/FinalPrivateMethodRule.php new file mode 100644 index 0000000000..7331e21bc1 --- /dev/null +++ b/src/Rules/Methods/FinalPrivateMethodRule.php @@ -0,0 +1,45 @@ + */ +final class FinalPrivateMethodRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + if ($scope->getPhpVersion()->producesWarningForFinalPrivateMethods()->no()) { + return []; + } + + if ($method->getName() === '__construct') { + return []; + } + + if (!$method->isFinal()->yes() || !$method->isPrivate()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Private method %s::%s() cannot be final as it is never overridden by other classes.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('method.finalPrivate')->build(), + ]; + } + +} diff --git a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php index 19aa3e2947..85059abcac 100644 --- a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php @@ -5,8 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -18,7 +16,7 @@ /** * @implements Rule */ -class IncompatibleDefaultParameterTypeRule implements Rule +final class IncompatibleDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -28,13 +26,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof PhpMethodFromParserNodeReflection) { - return []; - } - - $parameters = ParametersAcceptorSelector::selectSingle($method->getVariants()); - + $method = $node->getMethodReflection(); $errors = []; foreach ($node->getOriginalNode()->getParams() as $paramI => $param) { if ($param->default === null) { @@ -48,10 +40,12 @@ public function processNode(Node $node, Scope $scope): array } $defaultValueType = $scope->getType($param->default); - $parameterType = $parameters->getParameters()[$paramI]->getType(); + $parameter = $method->getParameters()[$paramI]; + $parameterType = $parameter->getType(); $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); - if ($parameterType->accepts($defaultValueType, true)->yes()) { + $accepts = $parameterType->accepts($defaultValueType, true); + if ($accepts->yes()) { continue; } @@ -65,7 +59,11 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $parameterType->describe($verbosityLevel), - ))->line($param->getLine())->build(); + )) + ->line($param->getStartLine()) + ->identifier('parameter.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(); } return $errors; diff --git a/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php b/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php new file mode 100644 index 0000000000..6fca3226a5 --- /dev/null +++ b/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php @@ -0,0 +1,22 @@ +extensions ??= $this->container->getServicesByTag(static::EXTENSION_TAG); + } + +} diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index ae220df96e..433931baf3 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -5,13 +5,14 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ -class MethodAttributesRule implements Rule +final class MethodAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\ClassMethod::class; + return InClassMethodNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_METHOD, 'method', ); diff --git a/src/Rules/Methods/MethodCallCheck.php b/src/Rules/Methods/MethodCallCheck.php index bd46e50708..06cbf2e9ca 100644 --- a/src/Rules/Methods/MethodCallCheck.php +++ b/src/Rules/Methods/MethodCallCheck.php @@ -2,23 +2,27 @@ namespace PHPStan\Rules\Methods; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; +use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function count; use function sprintf; use function strtolower; -class MethodCallCheck +final class MethodCallCheck { public function __construct( @@ -31,12 +35,13 @@ public function __construct( } /** - * @return array{RuleError[], MethodReflection|null} + * @return array{list, ExtendedMethodReflection|null} */ public function check( Scope $scope, string $methodName, Expr $var, + Identifier|Expr $astName, ): array { $typeResult = $this->ruleLevelHelper->findTypeToCheck( @@ -50,14 +55,19 @@ public function check( if ($type instanceof ErrorType) { return [$typeResult->getUnknownClassErrors(), null]; } - if (!$type->canCallMethods()->yes()) { + + $typeForDescribe = $type; + if ($type instanceof StaticType) { + $typeForDescribe = $type->getStaticObjectType(); + } + if (!$type->canCallMethods()->yes() || $type->isClassString()->yes()) { return [ [ RuleErrorBuilder::message(sprintf( 'Cannot call method %s() on %s.', $methodName, - $type->describe(VerbosityLevel::typeOnly()), - ))->build(), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->identifier('method.nonObject')->build(), ], null, ]; @@ -91,7 +101,7 @@ public function check( 'Call to private method %s() of parent class %s.', $methodReflection->getName(), $parentClassReflection->getDisplayName(), - ))->build(), + ))->identifier('method.private')->build(), ], $methodReflection, ]; @@ -101,13 +111,24 @@ public function check( } } + if ($astName instanceof Expr) { + $methodExistsExpr = new Expr\FuncCall(new FullyQualified('method_exists'), [ + new Arg($var), + new Arg($astName), + ]); + + if ($scope->getType($methodExistsExpr)->isTrue()->yes()) { + return [[], null]; + } + } + return [ [ RuleErrorBuilder::message(sprintf( 'Call to an undefined method %s::%s().', - $type->describe(VerbosityLevel::typeOnly()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), $methodName, - ))->build(), + ))->identifier('method.notFound')->build(), ], null, ]; @@ -123,7 +144,9 @@ public function check( $methodReflection->isPrivate() ? 'private' : 'protected', $methodReflection->getName(), $declaringClass->getDisplayName(), - ))->build(); + )) + ->identifier(sprintf('method.%s', $methodReflection->isPrivate() ? 'private' : 'protected')) + ->build(); } if ( @@ -133,7 +156,7 @@ public function check( ) { $errors[] = RuleErrorBuilder::message( sprintf('Call to method %s with incorrect case: %s', $messagesMethodName, $methodName), - )->build(); + )->identifier('method.nameCase')->build(); } return [$errors, $methodReflection]; diff --git a/src/Rules/Methods/MethodCallableRule.php b/src/Rules/Methods/MethodCallableRule.php index 8cd5c3f14b..b91b0537bf 100644 --- a/src/Rules/Methods/MethodCallableRule.php +++ b/src/Rules/Methods/MethodCallableRule.php @@ -14,7 +14,7 @@ /** * @implements Rule */ -class MethodCallableRule implements Rule +final class MethodCallableRule implements Rule { public function __construct(private MethodCallCheck $methodCallCheck, private PhpVersion $phpVersion) @@ -32,6 +32,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') ->nonIgnorable() + ->identifier('callable.notSupported') ->build(), ]; } @@ -43,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array $methodNameName = $methodName->toString(); - [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getVar()); + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getVar(), $node->getName()); if ($methodReflection === null) { return $errors; } @@ -55,7 +56,9 @@ public function processNode(Node $node, Scope $scope): array $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); - $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native method %s.', $messagesMethodName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native method %s.', $messagesMethodName)) + ->identifier('callable.nonNativeMethod') + ->build(); return $errors; } diff --git a/src/Rules/Methods/MethodParameterComparisonHelper.php b/src/Rules/Methods/MethodParameterComparisonHelper.php new file mode 100644 index 0000000000..34f66bb8bb --- /dev/null +++ b/src/Rules/Methods/MethodParameterComparisonHelper.php @@ -0,0 +1,410 @@ + + */ + public function compare(ExtendedMethodReflection $prototype, ClassReflection $prototypeDeclaringClass, PhpMethodFromParserNodeReflection $method, bool $ignorable): array + { + /** @var list $messages */ + $messages = []; + $prototypeVariant = $prototype->getVariants()[0]; + + $methodParameters = $method->getParameters(); + + $prototypeAfterVariadic = false; + foreach ($prototypeVariant->getParameters() as $i => $prototypeParameter) { + if (!array_key_exists($i, $methodParameters)) { + $error = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() overrides method %s::%s() but misses parameter #%d $%s.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + $i + 1, + $prototypeParameter->getName(), + ))->identifier('parameter.missing'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + + continue; + } + + $methodParameter = $methodParameters[$i]; + if ($prototypeParameter->passedByReference()->no()) { + if (!$methodParameter->passedByReference()->no()) { + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s of method %s::%s() is passed by reference but parameter #%d $%s of method %s::%s() is not passed by reference.', + $i + 1, + $methodParameter->getName(), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $i + 1, + $prototypeParameter->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('parameter.byRef'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + } + } elseif ($methodParameter->passedByReference()->no()) { + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s of method %s::%s() is not passed by reference but parameter #%d $%s of method %s::%s() is passed by reference.', + $i + 1, + $methodParameter->getName(), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $i + 1, + $prototypeParameter->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('parameter.notByRef'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + } + + if ($prototypeParameter->isVariadic()) { + $prototypeAfterVariadic = true; + if (!$methodParameter->isVariadic()) { + if (!$methodParameter->isOptional()) { + if (count($methodParameters) !== $i + 1) { + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s of method %s::%s() is not optional.', + $i + 1, + $methodParameter->getName(), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('parameter.notOptional'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + + continue; + } + + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s of method %s::%s() is not variadic but parameter #%d $%s of method %s::%s() is variadic.', + $i + 1, + $methodParameter->getName(), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $i + 1, + $prototypeParameter->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('parameter.notVariadic'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + + continue; + } elseif (count($methodParameters) === $i + 1) { + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s of method %s::%s() is not variadic.', + $i + 1, + $methodParameter->getName(), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('parameter.notVariadic'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + } + } + } elseif ($methodParameter->isVariadic()) { + if ($this->phpVersion->supportsLessOverridenParametersWithVariadic()) { + $remainingPrototypeParameters = array_slice($prototypeVariant->getParameters(), $i); + foreach ($remainingPrototypeParameters as $j => $remainingPrototypeParameter) { + if ($methodParameter->getNativeType()->isSuperTypeOf($remainingPrototypeParameter->getNativeType())->yes()) { + continue; + } + + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d ...$%s (%s) of method %s::%s() is not contravariant with parameter #%d $%s (%s) of method %s::%s().', + $i + 1, + $methodParameter->getName(), + $methodParameter->getNativeType()->describe(VerbosityLevel::typeOnly()), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $i + $j + 1, + $remainingPrototypeParameter->getName(), + $remainingPrototypeParameter->getNativeType()->describe(VerbosityLevel::typeOnly()), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('method.childParameterType'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + } + break; + } + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s of method %s::%s() is variadic but parameter #%d $%s of method %s::%s() is not variadic.', + $i + 1, + $methodParameter->getName(), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $i + 1, + $prototypeParameter->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('parameter.variadic'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + + continue; + } + + if ($prototypeParameter->isOptional() && !$methodParameter->isOptional()) { + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s of method %s::%s() is required but parameter #%d $%s of method %s::%s() is optional.', + $i + 1, + $methodParameter->getName(), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $i + 1, + $prototypeParameter->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('parameter.notOptional'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + } + + $methodParameterType = $methodParameter->getNativeType(); + + $prototypeParameterType = $prototypeParameter->getNativeType(); + if (!$this->phpVersion->supportsParameterTypeWidening()) { + if (!$methodParameterType->equals($prototypeParameterType)) { + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s (%s) of method %s::%s() does not match parameter #%d $%s (%s) of method %s::%s().', + $i + 1, + $methodParameter->getName(), + $methodParameterType->describe(VerbosityLevel::typeOnly()), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $i + 1, + $prototypeParameter->getName(), + $prototypeParameterType->describe(VerbosityLevel::typeOnly()), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('method.childParameterType'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + } + continue; + } + + if ($this->isParameterTypeCompatible($methodParameterType, $prototypeParameterType, $this->phpVersion->supportsParameterContravariance())) { + continue; + } + + if ($this->phpVersion->supportsParameterContravariance()) { + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s (%s) of method %s::%s() is not contravariant with parameter #%d $%s (%s) of method %s::%s().', + $i + 1, + $methodParameter->getName(), + $methodParameterType->describe(VerbosityLevel::typeOnly()), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $i + 1, + $prototypeParameter->getName(), + $prototypeParameterType->describe(VerbosityLevel::typeOnly()), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('method.childParameterType'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + } else { + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s (%s) of method %s::%s() is not compatible with parameter #%d $%s (%s) of method %s::%s().', + $i + 1, + $methodParameter->getName(), + $methodParameterType->describe(VerbosityLevel::typeOnly()), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $i + 1, + $prototypeParameter->getName(), + $prototypeParameterType->describe(VerbosityLevel::typeOnly()), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('method.childParameterType'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + } + } + + if (!isset($i)) { + $i = -1; + } + + foreach ($methodParameters as $j => $methodParameter) { + if ($j <= $i) { + continue; + } + + if ( + $j === count($methodParameters) - 1 + && $prototypeAfterVariadic + && !$methodParameter->isVariadic() + ) { + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s of method %s::%s() is not variadic.', + $j + 1, + $methodParameter->getName(), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('parameter.notVariadic'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + + continue; + } + + if ($methodParameter->isOptional()) { + continue; + } + + $error = RuleErrorBuilder::message(sprintf( + 'Parameter #%d $%s of method %s::%s() is not optional.', + $j + 1, + $methodParameter->getName(), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('parameter.notOptional'); + + if (! $ignorable) { + $error->nonIgnorable(); + } + + $messages[] = $error->build(); + + continue; + } + + return $messages; + } + + public function isParameterTypeCompatible(Type $methodParameterType, Type $prototypeParameterType, bool $supportsContravariance): bool + { + return $this->isTypeCompatible($methodParameterType, $prototypeParameterType, $supportsContravariance, false); + } + + public function isReturnTypeCompatible(Type $methodParameterType, Type $prototypeParameterType, bool $supportsCovariance): bool + { + return $this->isTypeCompatible($methodParameterType, $prototypeParameterType, $supportsCovariance, true); + } + + private function isTypeCompatible(Type $methodParameterType, Type $prototypeParameterType, bool $supportsContravariance, bool $considerMixedExplicitness): bool + { + if ($methodParameterType instanceof MixedType) { + if ($considerMixedExplicitness && $prototypeParameterType instanceof MixedType) { + return !$methodParameterType->isExplicitMixed() || $prototypeParameterType->isExplicitMixed(); + } + + return true; + } + + if (!$supportsContravariance) { + if (TypeCombinator::containsNull($methodParameterType)) { + $prototypeParameterType = TypeCombinator::removeNull($prototypeParameterType); + } + $methodParameterType = TypeCombinator::removeNull($methodParameterType); + if ($methodParameterType->equals($prototypeParameterType)) { + return true; + } + + if ($methodParameterType instanceof IterableType) { + if ($prototypeParameterType instanceof ArrayType) { + return true; + } + if ($prototypeParameterType instanceof ConstantArrayType) { + return true; + } + if ($prototypeParameterType->isObject()->yes() && $prototypeParameterType->getObjectClassNames() === [Traversable::class]) { + return true; + } + } + + return false; + } + + return $methodParameterType->isSuperTypeOf($prototypeParameterType)->yes(); + } + +} diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index a02e974f4c..2e585ff43e 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -6,15 +6,21 @@ 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\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\ArrayType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; @@ -22,18 +28,19 @@ use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function count; use function min; use function sprintf; +use function strtolower; /** * @implements Rule */ -class MethodSignatureRule implements Rule +final class MethodSignatureRule implements Rule { public function __construct( + private PhpClassReflectionExtension $phpClassReflectionExtension, private bool $reportMaybes, private bool $reportStatic, ) @@ -47,11 +54,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof PhpMethodFromParserNodeReflection) { - return []; - } - + $method = $node->getMethodReflection(); $methodName = $method->getName(); if ($methodName === '__construct') { return []; @@ -62,35 +65,54 @@ public function processNode(Node $node, Scope $scope): array if ($method->isPrivate()) { return []; } - $parameters = ParametersAcceptorSelector::selectSingle($method->getVariants()); - $errors = []; $declaringClass = $method->getDeclaringClass(); - foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as $parentMethod) { + foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as [$parentMethod, $parentMethodDeclaringClass]) { $parentVariants = $parentMethod->getVariants(); if (count($parentVariants) !== 1) { continue; } - $parentParameters = $parentVariants[0]; - if (!$parentParameters instanceof ParametersAcceptorWithPhpDocs) { - continue; - } - - [$returnTypeCompatibility, $returnType, $parentReturnType] = $this->checkReturnTypeCompatibility($declaringClass, $parameters, $parentParameters); + $parentVariant = $parentVariants[0]; + [$returnTypeCompatibility, $returnType, $parentReturnType] = $this->checkReturnTypeCompatibility($declaringClass, $method, $parentVariant); if ($returnTypeCompatibility->no() || (!$returnTypeCompatibility->yes() && $this->reportMaybes)) { - $errors[] = RuleErrorBuilder::message(sprintf( + $builder = RuleErrorBuilder::message(sprintf( 'Return type (%s) of method %s::%s() should be %s with return type (%s) of method %s::%s()', $returnType->describe(VerbosityLevel::value()), $method->getDeclaringClass()->getDisplayName(), $method->getName(), $returnTypeCompatibility->no() ? 'compatible' : 'covariant', $parentReturnType->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), + $parentMethodDeclaringClass->getDisplayName(), $parentMethod->getName(), - ))->build(); + ))->identifier('method.childReturnType'); + if ( + $parentMethod->getDeclaringClass()->getName() === Rule::class + && strtolower($methodName) === 'processnode' + ) { + $ruleErrorType = new ObjectType(RuleError::class); + $identifierRuleErrorType = new ObjectType(IdentifierRuleError::class); + $listOfIdentifierRuleErrors = new IntersectionType([ + new ArrayType(IntegerRangeType::fromInterval(0, null), $identifierRuleErrorType), + new AccessoryArrayListType(), + ]); + if ($listOfIdentifierRuleErrors->isSuperTypeOf($parentReturnType)->yes()) { + $returnValueType = $returnType->getIterableValueType(); + if (!$returnValueType->isString()->no()) { + $builder->tip('Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder'); + } elseif ( + $ruleErrorType->isSuperTypeOf($returnValueType)->yes() + && !$identifierRuleErrorType->isSuperTypeOf($returnValueType)->yes() + ) { + $builder->tip('Errors are missing identifiers. See: https://phpstan.org/blog/using-rule-error-builder'); + } elseif (!$returnType->isList()->yes()) { + $builder->tip('Return type must be a list. See: https://phpstan.org/blog/using-rule-error-builder'); + } + } + } + $errors[] = $builder->build(); } - $parameterResults = $this->checkParameterTypeCompatibility($declaringClass, $parameters->getParameters(), $parentParameters->getParameters()); + $parameterResults = $this->checkParameterTypeCompatibility($declaringClass, $method->getParameters(), $parentVariant->getParameters()); foreach ($parameterResults as $parameterIndex => [$parameterResult, $parameterType, $parentParameterType]) { if ($parameterResult->yes()) { continue; @@ -98,8 +120,8 @@ public function processNode(Node $node, Scope $scope): array if (!$parameterResult->no() && !$this->reportMaybes) { continue; } - $parameter = $parameters->getParameters()[$parameterIndex]; - $parentParameter = $parentParameters->getParameters()[$parameterIndex]; + $parameter = $method->getParameters()[$parameterIndex]; + $parentParameter = $parentVariant->getParameters()[$parameterIndex]; $errors[] = RuleErrorBuilder::message(sprintf( 'Parameter #%d $%s (%s) of method %s::%s() should be %s with parameter $%s (%s) of method %s::%s()', $parameterIndex + 1, @@ -110,9 +132,9 @@ public function processNode(Node $node, Scope $scope): array $parameterResult->no() ? 'compatible' : 'contravariant', $parentParameter->getName(), $parentParameterType->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), + $parentMethodDeclaringClass->getDisplayName(), $parentMethod->getName(), - ))->build(); + ))->identifier('method.childParameterType')->build(); } } @@ -120,7 +142,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return MethodReflection[] + * @return list */ private function collectParentMethods(string $methodName, ClassReflection $class): array { @@ -130,7 +152,7 @@ private function collectParentMethods(string $methodName, ClassReflection $class if ($parentClass !== null && $parentClass->hasNativeMethod($methodName)) { $parentMethod = $parentClass->getNativeMethod($methodName); if (!$parentMethod->isPrivate()) { - $parentMethods[] = $parentMethod; + $parentMethods[] = [$parentMethod, $parentMethod->getDeclaringClass()]; } } @@ -139,7 +161,32 @@ private function collectParentMethods(string $methodName, ClassReflection $class continue; } - $parentMethods[] = $interface->getNativeMethod($methodName); + $method = $interface->getNativeMethod($methodName); + $parentMethods[] = [$method, $method->getDeclaringClass()]; + } + + foreach ($class->getTraits(true) as $trait) { + $nativeTraitReflection = $trait->getNativeReflection(); + if (!$nativeTraitReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $nativeTraitReflection->getMethod($methodName); + $isAbstract = $methodReflection->isAbstract(); + if (!$isAbstract) { + continue; + } + + $declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass(); + $parentMethods[] = [ + $this->phpClassReflectionExtension->createUserlandMethodReflection( + $trait, + $class, + $methodReflection, + $declaringTrait->getName(), + ), + $declaringTrait, + ]; } return $parentMethods; @@ -150,8 +197,8 @@ private function collectParentMethods(string $methodName, ClassReflection $class */ private function checkReturnTypeCompatibility( ClassReflection $declaringClass, - ParametersAcceptorWithPhpDocs $currentVariant, - ParametersAcceptorWithPhpDocs $parentVariant, + ExtendedParametersAcceptor $currentVariant, + ExtendedParametersAcceptor $parentVariant, ): array { $returnType = TypehintHelper::decideType( @@ -164,24 +211,24 @@ private function checkReturnTypeCompatibility( ); $parentReturnType = $this->transformStaticType($declaringClass, $originalParentReturnType); // Allow adding `void` return type hints when the parent defines no return type - if ($returnType instanceof VoidType && $parentReturnType instanceof MixedType) { + if ($returnType->isVoid()->yes() && $parentReturnType instanceof MixedType) { return [TrinaryLogic::createYes(), $returnType, $parentReturnType]; } // We can return anything - if ($parentReturnType instanceof VoidType) { + if ($parentReturnType->isVoid()->yes()) { return [TrinaryLogic::createYes(), $returnType, $parentReturnType]; } - return [$parentReturnType->isSuperTypeOf($returnType), TypehintHelper::decideType( + return [$parentReturnType->isSuperTypeOf($returnType)->result, TypehintHelper::decideType( $currentVariant->getNativeReturnType(), $currentVariant->getPhpDocReturnType(), ), $originalParentReturnType]; } /** - * @param ParameterReflectionWithPhpDocs[] $parameters - * @param ParameterReflectionWithPhpDocs[] $parentParameters + * @param ExtendedParameterReflection[] $parameters + * @param ExtendedParameterReflection[] $parentParameters * @return array */ private function checkParameterTypeCompatibility( @@ -207,7 +254,7 @@ private function checkParameterTypeCompatibility( ); $parentParameterType = $this->transformStaticType($declaringClass, $originalParameterType); - $parameterResults[] = [$parameterType->isSuperTypeOf($parentParameterType), TypehintHelper::decideType( + $parameterResults[] = [$parameterType->isSuperTypeOf($parentParameterType)->result, TypehintHelper::decideType( $parameter->getNativeType(), $parameter->getPhpDocType(), ), $originalParameterType]; @@ -219,6 +266,15 @@ private function checkParameterTypeCompatibility( private function transformStaticType(ClassReflection $declaringClass, Type $type): Type { return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type { + if ($type instanceof GenericStaticType) { + if ($declaringClass->isFinal()) { + $changedType = $type->changeBaseClass($declaringClass)->getStaticObjectType(); + } else { + $changedType = $type->changeBaseClass($declaringClass); + } + return $traverse($changedType); + } + if ($type instanceof StaticType) { if ($declaringClass->isFinal()) { $changedType = new ObjectType($declaringClass->getName()); diff --git a/src/Rules/Methods/MethodVisibilityComparisonHelper.php b/src/Rules/Methods/MethodVisibilityComparisonHelper.php new file mode 100755 index 0000000000..4807453f2a --- /dev/null +++ b/src/Rules/Methods/MethodVisibilityComparisonHelper.php @@ -0,0 +1,51 @@ + */ + public function compare(ExtendedMethodReflection $prototype, ClassReflection $prototypeDeclaringClass, PhpMethodFromParserNodeReflection $method): array + { + /** @var list $messages */ + $messages = []; + + if ($prototype->isPublic()) { + if (!$method->isPublic()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s method %s::%s() overriding public method %s::%s() should also be public.', + $method->isPrivate() ? 'Private' : 'Protected', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->nonIgnorable() + ->identifier('method.visibility') + ->build(); + } + } elseif ($method->isPrivate()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Private method %s::%s() overriding protected method %s::%s() should be protected or public.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->nonIgnorable() + ->identifier('method.visibility') + ->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Methods/MethodVisibilityInInterfaceRule.php b/src/Rules/Methods/MethodVisibilityInInterfaceRule.php new file mode 100644 index 0000000000..28dbd1e368 --- /dev/null +++ b/src/Rules/Methods/MethodVisibilityInInterfaceRule.php @@ -0,0 +1,47 @@ + */ +final class MethodVisibilityInInterfaceRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + + if ($method->isPublic()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return []; + } + + if (!$classReflection->isInterface()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Method %s::%s() cannot use non-public visibility in interface.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('method.visibility')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Methods/MissingMagicSerializationMethodsRule.php b/src/Rules/Methods/MissingMagicSerializationMethodsRule.php new file mode 100644 index 0000000000..4f7df1a538 --- /dev/null +++ b/src/Rules/Methods/MissingMagicSerializationMethodsRule.php @@ -0,0 +1,87 @@ + + */ +final class MissingMagicSerializationMethodsRule implements Rule +{ + + public function __construct(private PhpVersion $phpversion) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$this->phpversion->serializableRequiresMagicMethods()) { + return []; + } + if (!$classReflection->implementsInterface(Serializable::class)) { + return []; + } + if ($classReflection->isAbstract() || $classReflection->isInterface() || $classReflection->isEnum()) { + return []; + } + + $messages = []; + + try { + $nativeMethods = $classReflection->getNativeReflection()->getMethods(); + } catch (IdentifierNotFound) { + return []; + } + + $missingMagicSerialize = true; + $missingMagicUnserialize = true; + foreach ($nativeMethods as $method) { + if (strtolower($method->getName()) === '__serialize') { + $missingMagicSerialize = false; + } + if (strtolower($method->getName()) !== '__unserialize') { + continue; + } + + $missingMagicUnserialize = false; + } + + if ($missingMagicSerialize) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Non-abstract class %s implements the Serializable interface, but does not implement __serialize().', + $classReflection->getDisplayName(), + )) + ->tip('See https://wiki.php.net/rfc/phase_out_serializable') + ->identifier('class.serializable') + ->build(); + } + if ($missingMagicUnserialize) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Non-abstract class %s implements the Serializable interface, but does not implement __unserialize().', + $classReflection->getDisplayName(), + )) + ->tip('See https://wiki.php.net/rfc/phase_out_serializable') + ->identifier('class.serializable') + ->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Methods/MissingMethodImplementationRule.php b/src/Rules/Methods/MissingMethodImplementationRule.php index 4ccab0d1e1..ec6d40f0a3 100644 --- a/src/Rules/Methods/MissingMethodImplementationRule.php +++ b/src/Rules/Methods/MissingMethodImplementationRule.php @@ -13,7 +13,7 @@ /** * @implements Rule */ -class MissingMethodImplementationRule implements Rule +final class MissingMethodImplementationRule implements Rule { public function getNodeType(): string @@ -57,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array $method->getName(), $declaringClass->isInterface() ? 'interface' : 'class', $declaringClass->getName(), - ))->nonIgnorable()->build(); + ))->nonIgnorable()->identifier('method.abstract')->build(); } return $messages; diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index b94ccdb832..5c6c204760 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -6,15 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -23,7 +21,9 @@ final class MissingMethodParameterTypehintRule implements Rule { - public function __construct(private MissingTypehintCheck $missingTypehintCheck) + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + ) { } @@ -34,15 +34,25 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - return []; - } - + $methodReflection = $node->getMethodReflection(); $messages = []; - foreach (ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getParameters() as $parameterReflection) { - foreach ($this->checkMethodParameter($methodReflection, $parameterReflection) as $parameterMessage) { + foreach ($methodReflection->getParameters() as $parameterReflection) { + foreach ($this->checkMethodParameter($methodReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + + if ($parameterReflection->getClosureThisType() !== null) { + foreach ($this->checkMethodParameter($methodReflection, sprintf('@param-closure-this PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getClosureThisType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + } + + if ($parameterReflection->getOutType() === null) { + continue; + } + + foreach ($this->checkMethodParameter($methodReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) { $messages[] = $parameterMessage; } } @@ -51,20 +61,18 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ - private function checkMethodParameter(MethodReflection $methodReflection, ParameterReflection $parameterReflection): array + private function checkMethodParameter(MethodReflection $methodReflection, string $parameterMessage, Type $parameterType): array { - $parameterType = $parameterReflection->getType(); - if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no type specified.', + 'Method %s::%s() has %s with no type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), - ))->build(), + $parameterMessage, + ))->identifier('missingType.parameter')->build(), ]; } @@ -72,33 +80,38 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no value type specified in iterable type %s.', + 'Method %s::%s() has %s with no value type specified in iterable type %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $iterableTypeDescription, - ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with generic %s but does not specify its types: %s', + 'Method %s::%s() has %s with generic %s but does not specify its types: %s', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $name, - implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no signature specified for %s.', + 'Method %s::%s() has %s with no signature specified for %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Methods/MissingMethodReturnTypehintRule.php b/src/Rules/Methods/MissingMethodReturnTypehintRule.php index 7b891b9f62..2b3c563f2d 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -5,14 +5,11 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -32,12 +29,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - return []; + $methodReflection = $node->getMethodReflection(); + if ($scope->isInTrait()) { + $methodNode = $node->getOriginalNode(); + $originalMethodName = $methodNode->getAttribute('originalTraitMethodName'); + if ($originalMethodName === '__construct') { + return []; + } } - - $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $returnType = $methodReflection->getReturnType(); if ($returnType instanceof MixedType && !$returnType->isExplicitMixed()) { return [ @@ -45,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() has no return type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - ))->build(), + ))->identifier('missingType.return')->build(), ]; } @@ -57,7 +57,10 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $iterableTypeDescription, - ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($returnType) as [$name, $genericTypeNames]) { @@ -66,8 +69,10 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $name, - implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($returnType) as $callableType) { @@ -76,7 +81,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Methods/MissingMethodSelfOutTypeRule.php b/src/Rules/Methods/MissingMethodSelfOutTypeRule.php new file mode 100644 index 0000000000..e4e023ce21 --- /dev/null +++ b/src/Rules/Methods/MissingMethodSelfOutTypeRule.php @@ -0,0 +1,84 @@ + + */ +final class MissingMethodSelfOutTypeRule implements Rule +{ + + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + ) + { + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methodReflection = $node->getMethodReflection(); + $selfOutType = $methodReflection->getSelfOutType(); + + if ($selfOutType === null) { + return []; + } + + $classReflection = $methodReflection->getDeclaringClass(); + $phpDocTagMessage = 'PHPDoc tag @phpstan-self-out'; + + $messages = []; + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($selfOutType) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() has %s with no value type specified in iterable type %s.', + $classReflection->getDisplayName(), + $methodReflection->getName(), + $phpDocTagMessage, + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($selfOutType) as [$name, $genericTypeNames]) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() has %s with generic %s but does not specify its types: %s', + $classReflection->getDisplayName(), + $methodReflection->getName(), + $phpDocTagMessage, + $name, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($selfOutType) as $callableType) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() has %s with no signature specified for %s.', + $classReflection->getDisplayName(), + $methodReflection->getName(), + $phpDocTagMessage, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Methods/NullsafeMethodCallRule.php b/src/Rules/Methods/NullsafeMethodCallRule.php index d060485df7..e950e4cb9a 100644 --- a/src/Rules/Methods/NullsafeMethodCallRule.php +++ b/src/Rules/Methods/NullsafeMethodCallRule.php @@ -6,14 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; use function sprintf; /** * @implements Rule */ -class NullsafeMethodCallRule implements Rule +final class NullsafeMethodCallRule implements Rule { public function getNodeType(): string @@ -23,18 +22,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $nullType = new NullType(); $calledOnType = $scope->getType($node->var); - if ($calledOnType->equals($nullType)) { - return []; - } - - if (!$calledOnType->isSuperTypeOf($nullType)->no()) { + if (!$calledOnType->isNull()->no()) { return []; } return [ - RuleErrorBuilder::message(sprintf('Using nullsafe method call on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly())))->build(), + RuleErrorBuilder::message(sprintf('Using nullsafe method call on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly()))) + ->identifier('nullsafe.neverNull') + ->build(), ]; } diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 3d38e95a22..2dcb9d3cc3 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -6,39 +6,38 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodPrototypeReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use PHPStan\Reflection\Native\NativeMethodReflection; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Reflection\Php\PhpMethodReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ArrayType; -use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; -use Traversable; -use function array_key_exists; -use function array_slice; +use function array_merge; use function count; +use function is_bool; use function sprintf; use function strtolower; /** * @implements Rule */ -class OverridingMethodRule implements Rule +final class OverridingMethodRule implements Rule { public function __construct( private PhpVersion $phpVersion, private MethodSignatureRule $methodSignatureRule, private bool $checkPhpDocMethodSignatures, + private MethodParameterComparisonHelper $methodParameterComparisonHelper, + private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper, + private PhpClassReflectionExtension $phpClassReflectionExtension, + private bool $checkMissingOverrideMethodAttribute, ) { } @@ -50,47 +49,95 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof PhpMethodFromParserNodeReflection) { - throw new ShouldNotHappenException(); - } - - $prototype = $method->getPrototype(); - if ($prototype->getDeclaringClass()->getName() === $method->getDeclaringClass()->getName()) { + $method = $node->getMethodReflection(); + $prototypeData = $this->findPrototype($node->getClassReflection(), $method->getName()); + if ($prototypeData === null) { if (strtolower($method->getName()) === '__construct') { $parent = $method->getDeclaringClass()->getParentClass(); if ($parent !== null && $parent->hasConstructor()) { $parentConstructor = $parent->getConstructor(); - if ($parentConstructor->isFinal()->yes()) { + if ($parentConstructor->isFinalByKeyword()->yes()) { return $this->addErrors([ RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides final method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $parent->getDisplayName(), + $parent->getDisplayName(true), + $parentConstructor->getName(), + )) + ->nonIgnorable() + ->identifier('method.parentMethodFinal') + ->build(), + ], $node, $scope); + } + if ($parentConstructor->isFinal()->yes()) { + return $this->addErrors([ + RuleErrorBuilder::message(sprintf( + 'Method %s::%s() overrides @final method %s::%s().', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $parent->getDisplayName(true), $parentConstructor->getName(), - ))->nonIgnorable()->build(), + ))->identifier('method.parentMethodFinalByPhpDoc') + ->build(), ], $node, $scope); } } } - return []; - } + if ($this->phpVersion->supportsOverrideAttribute() && $this->hasOverrideAttribute($node->getOriginalNode())) { + return [ + RuleErrorBuilder::message(sprintf( + 'Method %s::%s() has #[\Override] attribute but does not override any method.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + )) + ->nonIgnorable() + ->identifier('method.override') + ->build(), + ]; + } - if (!$prototype instanceof MethodPrototypeReflection) { return []; } + [$prototype, $prototypeDeclaringClass, $checkVisibility] = $prototypeData; + $messages = []; - if ($prototype->isFinal()) { + if ( + $this->phpVersion->supportsOverrideAttribute() + && $this->checkMissingOverrideMethodAttribute + && !$scope->isInTrait() + && !$this->hasOverrideAttribute($node->getOriginalNode()) + ) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() overrides method %s::%s() but is missing the #[\Override] attribute.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + ))->identifier('method.missingOverride')->build(); + } + if ($prototype->isFinalByKeyword()->yes()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides final method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->nonIgnorable() + ->identifier('method.parentMethodFinal') + ->build(); + } elseif ($prototype->isFinal()->yes()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() overrides @final method %s::%s().', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), - ))->nonIgnorable()->build(); + ))->identifier('method.parentMethodFinalByPhpDoc') + ->build(); } if ($prototype->isStatic()) { @@ -99,39 +146,28 @@ public function processNode(Node $node, Scope $scope): array 'Non-static method %s::%s() overrides static method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), - ))->nonIgnorable()->build(); + )) + ->nonIgnorable() + ->identifier('method.nonStatic') + ->build(); } } elseif ($method->isStatic()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Static method %s::%s() overrides non-static method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), - ))->nonIgnorable()->build(); + )) + ->nonIgnorable() + ->identifier('method.static') + ->build(); } - if ($prototype->isPublic()) { - if (!$method->isPublic()) { - $messages[] = RuleErrorBuilder::message(sprintf( - '%s method %s::%s() overriding public method %s::%s() should also be public.', - $method->isPrivate() ? 'Private' : 'Protected', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); - } - } elseif ($method->isPrivate()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Private method %s::%s() overriding protected method %s::%s() should be protected or public.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); + if ($checkVisibility) { + $messages = array_merge($messages, $this->methodVisibilityComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method)); } $prototypeVariants = $prototype->getVariants(); @@ -141,269 +177,75 @@ public function processNode(Node $node, Scope $scope): array $prototypeVariant = $prototypeVariants[0]; - $methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants()); - $methodReturnType = $methodVariant->getNativeReturnType(); - $methodParameters = $methodVariant->getParameters(); + $methodReturnType = $method->getNativeReturnType(); + + $realPrototype = $method->getPrototype(); if ( - $this->phpVersion->hasTentativeReturnTypes() - && $prototype->getTentativeReturnType() !== null + $realPrototype instanceof MethodPrototypeReflection + && $this->phpVersion->hasTentativeReturnTypes() + && $realPrototype->getTentativeReturnType() !== null && !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()) + && count($prototypeDeclaringClass->getNativeReflection()->getMethod($prototype->getName())->getAttributes('ReturnTypeWillChange')) === 0 ) { - - if (!$this->isTypeCompatible($prototype->getTentativeReturnType(), $methodVariant->getNativeReturnType(), true)) { + if (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($realPrototype->getTentativeReturnType(), $method->getNativeReturnType(), true)) { $messages[] = RuleErrorBuilder::message(sprintf( 'Return type %s of method %s::%s() is not covariant with tentative return type %s of method %s::%s().', $methodReturnType->describe(VerbosityLevel::typeOnly()), $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getTentativeReturnType()->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->tip('Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.')->nonIgnorable()->build(); + $realPrototype->getTentativeReturnType()->describe(VerbosityLevel::typeOnly()), + $realPrototype->getDeclaringClass()->getDisplayName(true), + $realPrototype->getName(), + )) + ->tip('Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.') + ->nonIgnorable() + ->identifier('method.tentativeReturnType') + ->build(); } } - $prototypeAfterVariadic = false; - foreach ($prototypeVariant->getParameters() as $i => $prototypeParameter) { - if (!array_key_exists($i, $methodParameters)) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() overrides method %s::%s() but misses parameter #%d $%s.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - $i + 1, - $prototypeParameter->getName(), - ))->nonIgnorable()->build(); - continue; - } - - $methodParameter = $methodParameters[$i]; - if ($prototypeParameter->passedByReference()->no()) { - if (!$methodParameter->passedByReference()->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is passed by reference but parameter #%d $%s of method %s::%s() is not passed by reference.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); - } - } elseif ($methodParameter->passedByReference()->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not passed by reference but parameter #%d $%s of method %s::%s() is passed by reference.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); - } + $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method, false)); - if ($prototypeParameter->isVariadic()) { - $prototypeAfterVariadic = true; - if (!$methodParameter->isVariadic()) { - if (!$methodParameter->isOptional()) { - if (count($methodParameters) !== $i + 1) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not optional.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - ))->nonIgnorable()->build(); - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not variadic but parameter #%d $%s of method %s::%s() is variadic.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); - continue; - } elseif (count($methodParameters) === $i + 1) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not variadic.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - ))->nonIgnorable()->build(); - } - } - } elseif ($methodParameter->isVariadic()) { - if ($this->phpVersion->supportsLessOverridenParametersWithVariadic()) { - $remainingPrototypeParameters = array_slice($prototypeVariant->getParameters(), $i); - foreach ($remainingPrototypeParameters as $j => $remainingPrototypeParameter) { - if (!$remainingPrototypeParameter instanceof ParameterReflectionWithPhpDocs) { - continue; - } - if ($methodParameter->getNativeType()->isSuperTypeOf($remainingPrototypeParameter->getNativeType())->yes()) { - continue; - } + if (!$prototypeVariant instanceof ExtendedFunctionVariant) { + return $this->addErrors($messages, $node, $scope); + } - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d ...$%s (%s) of method %s::%s() is not contravariant with parameter #%d $%s (%s) of method %s::%s().', - $i + 1, - $methodParameter->getName(), - $methodParameter->getNativeType()->describe(VerbosityLevel::typeOnly()), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + $j + 1, - $remainingPrototypeParameter->getName(), - $remainingPrototypeParameter->getNativeType()->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); + $prototypeReturnType = $prototypeVariant->getNativeReturnType(); + $reportReturnType = true; + if ($this->phpVersion->hasTentativeReturnTypes()) { + $reportReturnType = !$realPrototype instanceof MethodPrototypeReflection + || $realPrototype->getTentativeReturnType() === null + || (is_bool($prototype->isBuiltin()) ? !$prototype->isBuiltin() : $prototype->isBuiltin()->no()); + } else { + if ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->isInternal()) { + if ( + (is_bool($prototype->isBuiltin()) ? $prototype->isBuiltin() : $prototype->isBuiltin()->yes()) + && $prototypeDeclaringClass->getName() !== $realPrototype->getDeclaringClass()->getName() + ) { + $realPrototypeVariant = $realPrototype->getVariants()[0]; + if ( + $prototypeReturnType instanceof MixedType + && !$prototypeReturnType->isExplicitMixed() + && (!$realPrototypeVariant->getReturnType() instanceof MixedType || $realPrototypeVariant->getReturnType()->isExplicitMixed()) + ) { + $reportReturnType = false; } - break; } - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is variadic but parameter #%d $%s of method %s::%s() is not variadic.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); - continue; - } - if ($prototypeParameter->isOptional() && !$methodParameter->isOptional()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is required but parameter #%d $%s of method %s::%s() is optional.', - $i + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); - } - - $methodParameterType = $methodParameter->getNativeType(); - - if (!$prototypeParameter instanceof ParameterReflectionWithPhpDocs) { - continue; - } - - $prototypeParameterType = $prototypeParameter->getNativeType(); - if (!$this->phpVersion->supportsParameterTypeWidening()) { - if (!$methodParameterType->equals($prototypeParameterType)) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s (%s) of method %s::%s() does not match parameter #%d $%s (%s) of method %s::%s().', - $i + 1, - $methodParameter->getName(), - $methodParameterType->describe(VerbosityLevel::typeOnly()), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); + if ( + $reportReturnType + && (is_bool($prototype->isBuiltin()) ? $prototype->isBuiltin() : $prototype->isBuiltin()->yes()) + ) { + $reportReturnType = !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()); } - continue; - } - - if ($this->isTypeCompatible($methodParameterType, $prototypeParameterType, $this->phpVersion->supportsParameterContravariance())) { - continue; } - - if ($this->phpVersion->supportsParameterContravariance()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s (%s) of method %s::%s() is not contravariant with parameter #%d $%s (%s) of method %s::%s().', - $i + 1, - $methodParameter->getName(), - $methodParameterType->describe(VerbosityLevel::typeOnly()), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); - } else { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s (%s) of method %s::%s() is not compatible with parameter #%d $%s (%s) of method %s::%s().', - $i + 1, - $methodParameter->getName(), - $methodParameterType->describe(VerbosityLevel::typeOnly()), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $i + 1, - $prototypeParameter->getName(), - $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); - } - } - - if (!isset($i)) { - $i = -1; - } - - foreach ($methodParameters as $j => $methodParameter) { - if ($j <= $i) { - continue; - } - - if ( - $j === count($methodParameters) - 1 - && $prototypeAfterVariadic - && !$methodParameter->isVariadic() - ) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not variadic.', - $j + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - ))->nonIgnorable()->build(); - continue; - } - - if (!$methodParameter->isOptional()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Parameter #%d $%s of method %s::%s() is not optional.', - $j + 1, - $methodParameter->getName(), - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - ))->nonIgnorable()->build(); - continue; - } - } - - if (!$prototypeVariant instanceof FunctionVariantWithPhpDocs) { - return $this->addErrors($messages, $node, $scope); } - $prototypeReturnType = $prototypeVariant->getNativeReturnType(); - - if (!$this->isTypeCompatible($prototypeReturnType, $methodReturnType, $this->phpVersion->supportsReturnCovariance())) { + if ( + $reportReturnType + && !$this->methodParameterComparisonHelper->isReturnTypeCompatible($prototypeReturnType, $methodReturnType, $this->phpVersion->supportsReturnCovariance()) + ) { if ($this->phpVersion->supportsReturnCovariance()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Return type %s of method %s::%s() is not covariant with return type %s of method %s::%s().', @@ -411,9 +253,12 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), - ))->nonIgnorable()->build(); + )) + ->nonIgnorable() + ->identifier('method.childReturnType') + ->build(); } else { $messages[] = RuleErrorBuilder::message(sprintf( 'Return type %s of method %s::%s() is not compatible with return type %s of method %s::%s().', @@ -421,48 +266,21 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName(true), $prototype->getName(), - ))->nonIgnorable()->build(); + )) + ->nonIgnorable() + ->identifier('method.childReturnType') + ->build(); } } return $this->addErrors($messages, $node, $scope); } - private function isTypeCompatible(Type $methodParameterType, Type $prototypeParameterType, bool $supportsContravariance): bool - { - if ($methodParameterType instanceof MixedType) { - return true; - } - - if (!$supportsContravariance) { - if (TypeCombinator::containsNull($methodParameterType)) { - $prototypeParameterType = TypeCombinator::removeNull($prototypeParameterType); - } - $methodParameterType = TypeCombinator::removeNull($methodParameterType); - if ($methodParameterType->equals($prototypeParameterType)) { - return true; - } - - if ($methodParameterType instanceof IterableType) { - if ($prototypeParameterType instanceof ArrayType) { - return true; - } - if ($prototypeParameterType instanceof ObjectType && $prototypeParameterType->getClassName() === Traversable::class) { - return true; - } - } - - return false; - } - - return $methodParameterType->isSuperTypeOf($prototypeParameterType)->yes(); - } - /** - * @param RuleError[] $errors - * @return (string|RuleError)[] + * @param list $errors + * @return list */ private function addErrors( array $errors, @@ -494,4 +312,90 @@ private function hasReturnTypeWillChangeAttribute(Node\Stmt\ClassMethod $method) return false; } + private function hasOverrideAttribute(Node\Stmt\ClassMethod $method): bool + { + foreach ($method->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'override') { + return true; + } + } + } + + return false; + } + + /** + * @return array{ExtendedMethodReflection, ClassReflection, bool}|null + */ + private function findPrototype(ClassReflection $classReflection, string $methodName): ?array + { + foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { + if ($immediateInterface->hasNativeMethod($methodName)) { + $method = $immediateInterface->getNativeMethod($methodName); + return [$method, $method->getDeclaringClass(), true]; + } + } + + if ($this->phpVersion->supportsAbstractTraitMethods()) { + foreach ($classReflection->getTraits(true) as $trait) { + $nativeTraitReflection = $trait->getNativeReflection(); + if (!$nativeTraitReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $nativeTraitReflection->getMethod($methodName); + $isAbstract = $methodReflection->isAbstract(); + if ($isAbstract) { + $declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass(); + return [ + $this->phpClassReflectionExtension->createUserlandMethodReflection( + $trait, + $classReflection, + $methodReflection, + $declaringTrait->getName(), + ), + $declaringTrait, + false, + ]; + } + } + } + + $parentClass = $classReflection->getParentClass(); + if ($parentClass === null) { + return null; + } + + if (!$parentClass->hasNativeMethod($methodName)) { + return null; + } + + $method = $parentClass->getNativeMethod($methodName); + if ($method->isPrivate()) { + return null; + } + + $declaringClass = $method->getDeclaringClass(); + if ($declaringClass->hasConstructor()) { + if ($method->getName() === $declaringClass->getConstructor()->getName()) { + $prototype = $method->getPrototype(); + if ($prototype instanceof PhpMethodReflection || $prototype instanceof MethodPrototypeReflection || $prototype instanceof NativeMethodReflection) { + $abstract = $prototype->isAbstract(); + if (is_bool($abstract)) { + if (!$abstract) { + return null; + } + } elseif (!$abstract->yes()) { + return null; + } + } + } elseif (strtolower($methodName) === '__construct') { + return null; + } + } + + return [$method, $method->getDeclaringClass(), true]; + } + } diff --git a/src/Rules/Methods/ReturnTypeRule.php b/src/Rules/Methods/ReturnTypeRule.php index e1fae76875..d580b0c2b9 100644 --- a/src/Rules/Methods/ReturnTypeRule.php +++ b/src/Rules/Methods/ReturnTypeRule.php @@ -5,16 +5,28 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\TipRuleError; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\ArrayType; +use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; +use PHPStan\Type\ObjectType; +use function count; use function sprintf; +use function strtolower; +use function ucfirst; /** * @implements Rule */ -class ReturnTypeRule implements Rule +final class ReturnTypeRule implements Rule { public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) @@ -41,33 +53,78 @@ public function processNode(Node $node, Scope $scope): array return []; } - return $this->returnTypeCheck->checkReturnType( + if ($method->isPropertyHook()) { + $methodDescription = sprintf( + '%s hook for property %s::$%s', + ucfirst($method->getPropertyHookName()), + $method->getDeclaringClass()->getDisplayName(), + $method->getHookedPropertyName(), + ); + } else { + $methodDescription = sprintf('Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()); + } + + $returnType = $method->getReturnType(); + $errors = $this->returnTypeCheck->checkReturnType( $scope, - ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(), + $returnType, $node->expr, $node, sprintf( - 'Method %s::%s() should return %%s but empty return statement found.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), + '%s should return %%s but empty return statement found.', + $methodDescription, ), sprintf( - 'Method %s::%s() with return type void returns %%s but should not return anything.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), + '%s with return type void returns %%s but should not return anything.', + $methodDescription, ), sprintf( - 'Method %s::%s() should return %%s but returns %%s.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), + '%s should return %%s but returns %%s.', + $methodDescription, ), sprintf( - 'Method %s::%s() should never return but return statement found.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), + '%s should never return but return statement found.', + $methodDescription, ), $method->isGenerator(), ); + + if ( + count($errors) === 1 + && $errors[0]->getIdentifier() === 'return.type' + && !$errors[0] instanceof TipRuleError + && $errors[0] instanceof LineRuleError + && $method->getDeclaringClass()->is(Rule::class) + && strtolower($method->getName()) === 'processnode' + && $node->expr !== null + ) { + $ruleErrorType = new ObjectType(RuleError::class); + $identifierRuleErrorType = new ObjectType(IdentifierRuleError::class); + $listOfIdentifierRuleErrors = new IntersectionType([ + new ArrayType(IntegerRangeType::fromInterval(0, null), $identifierRuleErrorType), + new AccessoryArrayListType(), + ]); + if (!$listOfIdentifierRuleErrors->isSuperTypeOf($returnType)->yes()) { + return $errors; + } + + $returnValueType = $scope->getType($node->expr)->getIterableValueType(); + $builder = RuleErrorBuilder::message($errors[0]->getMessage()) + ->line($errors[0]->getLine()) + ->identifier($errors[0]->getIdentifier()); + if (!$returnValueType->isString()->no()) { + $builder->tip('Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder'); + } elseif ( + $ruleErrorType->isSuperTypeOf($returnValueType)->yes() + && !$identifierRuleErrorType->isSuperTypeOf($returnValueType)->yes() + ) { + $builder->tip('Error is missing an identifier. See: https://phpstan.org/blog/using-rule-error-builder'); + } + + $errors = [$builder->build()]; + } + + return $errors; } } diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index e4d4ae017e..373e41ddd9 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -2,51 +2,54 @@ namespace PHPStan\Rules\Methods; +use DOMDocument; use PhpParser\Node\Expr; use PhpParser\Node\Name; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\ClassNameUsageLocation; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StaticType; use PHPStan\Type\StringType; -use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; use function array_merge; use function in_array; use function sprintf; use function strtolower; -class StaticMethodCallCheck +final class StaticMethodCallCheck { public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, + private bool $discoveringSymbolsTip, private bool $reportMagicMethods, ) { } + /** * @param Name|Expr $class - * @return array{RuleError[], MethodReflection|null} + * @return array{list, ExtendedMethodReflection|null} */ public function check( Scope $scope, @@ -57,6 +60,11 @@ public function check( $errors = []; $isAbstract = false; if ($class instanceof Name) { + $classStringType = $scope->getType(new Expr\ClassConstFetch($class, 'class')); + if ($classStringType->hasMethod($methodName)->yes()) { + return [[], null]; + } + $className = (string) $class; $lowercasedClassName = strtolower($className); if (in_array($lowercasedClassName, ['self', 'static'], true)) { @@ -67,7 +75,7 @@ public function check( 'Calling %s::%s() outside of class scope.', $className, $methodName, - ))->build(), + ))->identifier(sprintf('outOfClass.%s', $lowercasedClassName))->build(), ], null, ]; @@ -81,7 +89,7 @@ public function check( 'Calling %s::%s() outside of class scope.', $className, $methodName, - ))->build(), + ))->identifier(sprintf('outOfClass.parent'))->build(), ], null, ]; @@ -96,7 +104,7 @@ public function check( $scope->getFunctionName(), $methodName, $scope->getClassReflection()->getDisplayName(), - ))->build(), + ))->identifier('class.noParent')->build(), ], null, ]; @@ -113,20 +121,37 @@ public function check( return [[], null]; } + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Call to static method %s() on an unknown class %s.', + $methodName, + $className, + )) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ [ - RuleErrorBuilder::message(sprintf( - 'Call to static method %s() on an unknown class %s.', - $methodName, - $className, - ))->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ], null, ]; - } else { - $errors = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); } + $locationData = []; + $locationClassReflection = $this->reflectionProvider->getClass($className); + if ($locationClassReflection->hasMethod($methodName)) { + $locationData['method'] = $locationClassReflection->getMethod($methodName, $scope); + } + + $errors = $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($className, $class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::STATIC_METHOD_CALL, $locationData), + ); + $classType = $scope->resolveTypeByName($class); } @@ -135,6 +160,9 @@ public function check( $nativeMethodReflection = $classReflection->getNativeMethod($methodName); if ($nativeMethodReflection instanceof PhpMethodReflection || $nativeMethodReflection instanceof NativeMethodReflection) { $isAbstract = $nativeMethodReflection->isAbstract(); + if ($isAbstract instanceof TrinaryLogic) { + $isAbstract = $isAbstract->yes(); + } } } } else { @@ -152,15 +180,15 @@ public function check( if ($classType instanceof GenericClassStringType) { $classType = $classType->getGenericType(); - if (!(new ObjectWithoutClassType())->isSuperTypeOf($classType)->yes()) { + if (!$classType->isObject()->yes()) { return [[], null]; } - } elseif ((new StringType())->isSuperTypeOf($classType)->yes()) { + } elseif ($classType->isString()->yes()) { return [[], null]; } $typeForDescribe = $classType; - if ($classType instanceof ThisType) { + if ($classType instanceof StaticType) { $typeForDescribe = $classType->getStaticObjectType(); } $classType = TypeCombinator::remove($classType, new StringType()); @@ -172,7 +200,7 @@ public function check( 'Cannot call static method %s() on %s.', $methodName, $typeForDescribe->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('staticMethod.nonObject')->build(), ]), null, ]; @@ -180,8 +208,7 @@ public function check( if (!$classType->hasMethod($methodName)->yes()) { if (!$this->reportMagicMethods) { - $directClassNames = TypeUtils::getDirectClassNames($classType); - foreach ($directClassNames as $className) { + foreach ($classType->getObjectClassNames() as $className) { if (!$this->reflectionProvider->hasClass($className)) { continue; } @@ -199,7 +226,7 @@ public function check( 'Call to an undefined static method %s::%s().', $typeForDescribe->describe(VerbosityLevel::typeOnly()), $methodName, - ))->build(), + ))->identifier('staticMethod.notFound')->build(), ]), null, ]; @@ -208,23 +235,31 @@ public function check( $method = $classType->getMethod($methodName, $scope); if (!$method->isStatic()) { $function = $scope->getFunction(); + + $scopeIsInMethodClassOrSubClass = TrinaryLogic::createFromBoolean($scope->isInClass())->lazyAnd( + $classType->getObjectClassNames(), + static fn (string $objectClassName) => TrinaryLogic::createFromBoolean( + $scope->isInClass() + && $scope->getClassReflection()->is($objectClassName), + ), + ); if ( !$function instanceof MethodReflection || $function->isStatic() - || !$scope->isInClass() - || ( - $classType instanceof TypeWithClassName - && $scope->getClassReflection()->getName() !== $classType->getClassName() - && !$scope->getClassReflection()->isSubclassOf($classType->getClassName()) - ) + || $scopeIsInMethodClassOrSubClass->no() ) { + // per php-src docs, this method can be called statically, even if declared non-static + if (strtolower($method->getName()) === 'loadhtml' && $method->getDeclaringClass()->getName() === DOMDocument::class) { + return [[], null]; + } + return [ array_merge($errors, [ RuleErrorBuilder::message(sprintf( 'Static call to instance method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - ))->build(), + ))->identifier('method.staticCall')->build(), ]), $method, ]; @@ -239,7 +274,9 @@ public function check( $method->isStatic() ? 'static method' : 'method', $method->getName(), $method->getDeclaringClass()->getDisplayName(), - ))->build(), + )) + ->identifier(sprintf('staticMethod.%s', $method->isPrivate() ? 'private' : 'protected')) + ->build(), ]); } @@ -251,6 +288,9 @@ public function check( $method->isStatic() ? ' static' : '', $method->getDeclaringClass()->getDisplayName(), $method->getName(), + ))->identifier(sprintf( + '%s.callToAbstract', + $method->isStatic() ? 'staticMethod' : 'method', ))->build(), ], $method, @@ -271,7 +311,7 @@ public function check( 'Call to %s with incorrect case: %s', $lowercasedMethodName, $methodName, - ))->build(); + ))->identifier('staticMethod.nameCase')->build(); } return [$errors, $method]; diff --git a/src/Rules/Methods/StaticMethodCallableRule.php b/src/Rules/Methods/StaticMethodCallableRule.php index 72ddeda460..815fdce793 100644 --- a/src/Rules/Methods/StaticMethodCallableRule.php +++ b/src/Rules/Methods/StaticMethodCallableRule.php @@ -14,7 +14,7 @@ /** * @implements Rule */ -class StaticMethodCallableRule implements Rule +final class StaticMethodCallableRule implements Rule { public function __construct(private StaticMethodCallCheck $methodCallCheck, private PhpVersion $phpVersion) @@ -32,6 +32,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') ->nonIgnorable() + ->identifier('callable.notSupported') ->build(), ]; } @@ -55,7 +56,9 @@ public function processNode(Node $node, Scope $scope): array $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); - $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native static method %s.', $messagesMethodName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native static method %s.', $messagesMethodName)) + ->identifier('callable.nonNativeMethod') + ->build(); return $errors; } diff --git a/src/Rules/Missing/MissingReturnRule.php b/src/Rules/Missing/MissingReturnRule.php index e7109da073..ef53fa16c8 100644 --- a/src/Rules/Missing/MissingReturnRule.php +++ b/src/Rules/Missing/MissingReturnRule.php @@ -6,25 +6,25 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ExecutionEndNode; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateMixedType; -use PHPStan\Type\GenericTypeVariableResolver; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; use function sprintf; +use function ucfirst; /** * @implements Rule */ -class MissingReturnRule implements Rule +final class MissingReturnRule implements Rule { public function __construct( @@ -55,9 +55,13 @@ public function processNode(Node $node, Scope $scope): array return []; } } elseif ($scopeFunction !== null) { - $returnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); - if ($scopeFunction instanceof MethodReflection) { - $description = sprintf('Method %s::%s()', $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getName()); + $returnType = $scopeFunction->getReturnType(); + if ($scopeFunction instanceof PhpMethodFromParserNodeReflection) { + if (!$scopeFunction->isPropertyHook()) { + $description = sprintf('Method %s::%s()', $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getName()); + } else { + $description = sprintf('%s hook for property %s::$%s', ucfirst($scopeFunction->getPropertyHookName()), $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getHookedPropertyName()); + } } else { $description = sprintf('Function %s()', $scopeFunction->getName()); } @@ -65,28 +69,29 @@ public function processNode(Node $node, Scope $scope): array throw new ShouldNotHappenException(); } + $returnType = TypeUtils::resolveLateResolvableTypes($returnType); + $isVoidSuperType = $returnType->isSuperTypeOf(new VoidType()); if ($isVoidSuperType->yes() && !$returnType instanceof MixedType) { return []; } if ($statementResult->hasYield()) { - if ($returnType instanceof TypeWithClassName && $this->checkPhpDocMissingReturn) { - $generatorReturnType = GenericTypeVariableResolver::getType( - $returnType, - Generator::class, - 'TReturn', - ); - if ($generatorReturnType !== null) { + if ($this->checkPhpDocMissingReturn) { + $generatorReturnType = $returnType->getTemplateType(Generator::class, 'TReturn'); + if (!$generatorReturnType instanceof ErrorType) { $returnType = $generatorReturnType; - if ($returnType instanceof VoidType) { + if ($returnType->isVoid()->yes()) { return []; } if (!$returnType instanceof MixedType) { return [ RuleErrorBuilder::message( sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())), - )->line($node->getNode()->getStartLine())->build(), + ) + ->line($node->getNode()->getStartLine()) + ->identifier('return.missing') + ->build(), ]; } } @@ -109,6 +114,8 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->nonIgnorable(); } + $errorBuilder->identifier('return.never'); + return [ $errorBuilder->build(), ]; @@ -134,6 +141,8 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->nonIgnorable(); } + $errorBuilder->identifier('return.missing'); + return [ $errorBuilder->build(), ]; diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index b360872811..f6910907e1 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -6,11 +6,14 @@ use Generator; use Iterator; use IteratorAggregate; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\CallableType; +use PHPStan\Type\ClosureType; +use PHPStan\Type\ConditionalType; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\IntersectionType; @@ -18,18 +21,20 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; use Traversable; +use function array_filter; use function array_keys; +use function array_merge; +use function count; +use function implode; use function in_array; use function sprintf; +use function strtolower; -class MissingTypehintCheck +final class MissingTypehintCheck { - public const TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP = 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type'; - - public const TURN_OFF_NON_GENERIC_CHECK_TIP = 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.'; + public const MISSING_ITERABLE_VALUE_TYPE_TIP = 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type'; private const ITERABLE_GENERIC_CLASS_NAMES = [ Traversable::class, @@ -42,9 +47,6 @@ class MissingTypehintCheck * @param string[] $skipCheckGenericClasses */ public function __construct( - private ReflectionProvider $reflectionProvider, - private bool $checkMissingIterableValueType, - private bool $checkGenericClassInNonGenericObjectType, private bool $checkMissingCallableSignature, private array $skipCheckGenericClasses, ) @@ -56,10 +58,6 @@ public function __construct( */ public function getIterableTypesWithMissingValueTypehint(Type $type): array { - if (!$this->checkMissingIterableValueType) { - return []; - } - $iterablesWithMissingValueTypehint = []; TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$iterablesWithMissingValueTypehint): Type { if ($type instanceof TemplateType) { @@ -68,26 +66,27 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array if ($type instanceof AccessoryType) { return $type; } + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $iterablesWithMissingValueTypehint = array_merge( + $iterablesWithMissingValueTypehint, + $this->getIterableTypesWithMissingValueTypehint($type->getIf()), + $this->getIterableTypesWithMissingValueTypehint($type->getElse()), + ); + + return $type; + } if ($type->isIterable()->yes()) { $iterableValue = $type->getIterableValueType(); if ($iterableValue instanceof MixedType && !$iterableValue->isExplicitMixed()) { - if ( - $type instanceof TypeWithClassName - && !in_array($type->getClassName(), self::ITERABLE_GENERIC_CLASS_NAMES, true) - && $this->reflectionProvider->hasClass($type->getClassName()) - ) { - $classReflection = $this->reflectionProvider->getClass($type->getClassName()); - if ($classReflection->isGeneric()) { - return $type; - } - } $iterablesWithMissingValueTypehint[] = $type; } - if (!$type instanceof IntersectionType) { - return $traverse($type); - } + if ($type instanceof IntersectionType) { + if ($type->isList()->yes()) { + return $traverse($iterableValue); + } - return $type; + return $type; + } } return $traverse($type); }); @@ -96,17 +95,13 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array } /** - * @return array + * @return array */ public function getNonGenericObjectTypesWithGenericClass(Type $type): array { - if (!$this->checkGenericClassInNonGenericObjectType) { - return []; - } - $objectTypes = []; TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$objectTypes): Type { - if ($type instanceof GenericObjectType) { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { $traverse($type); return $type; } @@ -136,9 +131,22 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array if (!$resolvedType instanceof ObjectType) { throw new ShouldNotHappenException(); } + + $templateTypes = $classReflection->getTemplateTypeMap()->getTypes(); + $templateTypesCount = count($templateTypes); + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount === 0) { + return $type; + } + + $templateTypesList = implode(', ', array_keys($templateTypes)); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $objectTypes[] = [ - sprintf('%s %s', $classReflection->isInterface() ? 'interface' : 'class', $classReflection->getDisplayName(false)), - array_keys($classReflection->getTemplateTypeMap()->getTypes()), + sprintf('%s %s', strtolower($classReflection->getClassTypeDescription()), $classReflection->getDisplayName(false)), + $templateTypesList, ]; return $type; } @@ -161,8 +169,10 @@ public function getCallablesWithMissingSignature(Type $type): array $result = []; TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type { if ( - ($type instanceof CallableType && $type->isCommonCallable()) || - ($type instanceof ObjectType && $type->getClassName() === Closure::class)) { + ($type instanceof CallableType && $type->isCommonCallable()) + || ($type instanceof ClosureType && $type->isCommonCallable()) + || ($type instanceof ObjectType && $type->getClassName() === Closure::class) + ) { $result[] = $type; } return $traverse($type); diff --git a/src/Rules/Names/UsedNamesRule.php b/src/Rules/Names/UsedNamesRule.php new file mode 100644 index 0000000000..5462137e5d --- /dev/null +++ b/src/Rules/Names/UsedNamesRule.php @@ -0,0 +1,149 @@ + + */ +final class UsedNamesRule implements Rule +{ + + public function getNodeType(): string + { + return FileNode::class; + } + + /** + * @param FileNode $node + */ + public function processNode(Node $node, Scope $scope): array + { + $usedNames = []; + $errors = []; + foreach ($node->getNodes() as $oneNode) { + if ($oneNode instanceof Namespace_) { + $namespaceName = $oneNode->name !== null ? $oneNode->name->toString() : ''; + foreach ($oneNode->stmts as $stmt) { + foreach ($this->findErrorsForNode($stmt, $namespaceName, $usedNames) as $error) { + $errors[] = $error; + } + } + continue; + } + + foreach ($this->findErrorsForNode($oneNode, '', $usedNames) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @param array $usedNames + * @return list + */ + private function findErrorsForNode(Node $node, string $namespace, array &$usedNames): array + { + $lowerNamespace = strtolower($namespace); + if ($node instanceof Use_) { + if ($this->shouldBeIgnored($node)) { + return []; + } + return $this->findErrorsInUses($node->uses, '', $lowerNamespace, $usedNames); + } + + if ($node instanceof GroupUse) { + if ($this->shouldBeIgnored($node)) { + return []; + } + $useGroupPrefix = $node->prefix->toString(); + return $this->findErrorsInUses($node->uses, $useGroupPrefix, $lowerNamespace, $usedNames); + } + + if ($node instanceof ClassLike) { + if ($node->name === null) { + return []; + } + $type = 'class'; + if ($node instanceof Interface_) { + $type = 'interface'; + } elseif ($node instanceof Trait_) { + $type = 'trait'; + } elseif ($node instanceof Enum_) { + $type = 'enum'; + } + $name = $node->name->toLowerString(); + if (in_array($name, $usedNames[$lowerNamespace] ?? [], true)) { + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot declare %s %s because the name is already in use.', + $type, + $namespace !== '' ? $namespace . '\\' . $node->name->toString() : $node->name->toString(), + )) + ->identifier(sprintf('%s.nameInUse', $type)) + ->line($node->getStartLine()) + ->nonIgnorable() + ->build(), + ]; + } + $usedNames[$lowerNamespace][] = $name; + return []; + } + + return []; + } + + /** + * @param Node\UseItem[] $uses + * @param array $usedNames + * @return list + */ + private function findErrorsInUses(array $uses, string $useGroupPrefix, string $lowerNamespace, array &$usedNames): array + { + $errors = []; + foreach ($uses as $use) { + if ($this->shouldBeIgnored($use)) { + continue; + } + $useAlias = $use->getAlias()->toLowerString(); + if (in_array($useAlias, $usedNames[$lowerNamespace] ?? [], true)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot use %s as %s because the name is already in use.', + $useGroupPrefix !== '' ? $useGroupPrefix . '\\' . $use->name->toString() : $use->name->toString(), + $use->getAlias()->toString(), + )) + ->identifier('use.nameInUse') + ->line($use->getStartLine()) + ->nonIgnorable() + ->build(); + continue; + } + $usedNames[$lowerNamespace][] = $useAlias; + } + return $errors; + } + + private function shouldBeIgnored(Use_|GroupUse|Node\UseItem $use): bool + { + return in_array($use->type, [Use_::TYPE_FUNCTION, Use_::TYPE_CONSTANT], true); + } + +} diff --git a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php index c4cd4231cf..b167becd52 100644 --- a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php @@ -6,10 +6,10 @@ use PhpParser\Node\Stmt\Use_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use function count; @@ -19,13 +19,14 @@ /** * @implements Rule */ -class ExistingNamesInGroupUseRule implements Rule +final class ExistingNamesInGroupUseRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, + private bool $discoveringSymbolsTip, ) { } @@ -42,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array $error = null; /** @var Node\Name $name */ - $name = Node\Name::concat($node->prefix, $use->name, ['startLine' => $use->getLine()]); + $name = Node\Name::concat($node->prefix, $use->name, ['startLine' => $use->getStartLine()]); if ( $node->type === Use_::TYPE_CONSTANT || $use->type === Use_::TYPE_CONSTANT @@ -54,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array ) { $error = $this->checkFunction($name); } elseif ($use->type === Use_::TYPE_NORMAL) { - $error = $this->checkClass($name); + $error = $this->checkClass($scope, $name); } else { throw new ShouldNotHappenException(); } @@ -69,19 +70,35 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - private function checkConstant(Node\Name $name): ?RuleError + private function checkConstant(Node\Name $name): ?IdentifierRuleError { if (!$this->reflectionProvider->hasConstant($name, null)) { - return RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $name))->discoveringSymbolsTip()->line($name->getLine())->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $name)) + ->line($name->getStartLine()) + ->identifier('constant.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + return $errorBuilder->build(); } return null; } - private function checkFunction(Node\Name $name): ?RuleError + private function checkFunction(Node\Name $name): ?IdentifierRuleError { if (!$this->reflectionProvider->hasFunction($name, null)) { - return RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $name))->discoveringSymbolsTip()->line($name->getLine())->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $name)) + ->line($name->getStartLine()) + ->identifier('function.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + return $errorBuilder->build(); } if ($this->checkFunctionNameCase) { @@ -96,18 +113,21 @@ private function checkFunction(Node\Name $name): ?RuleError 'Function %s used with incorrect case: %s.', $realName, $usedName, - ))->line($name->getLine())->build(); + )) + ->line($name->getStartLine()) + ->identifier('function.nameCase') + ->build(); } } return null; } - private function checkClass(Node\Name $name): ?RuleError + private function checkClass(Scope $scope, Node\Name $name): ?IdentifierRuleError { - $errors = $this->classCaseSensitivityCheck->checkClassNames([ + $errors = $this->classCheck->checkClassNames($scope, [ new ClassNameNodePair((string) $name, $name), - ]); + ], null); if (count($errors) === 0) { return null; } elseif (count($errors) === 1) { diff --git a/src/Rules/Namespaces/ExistingNamesInUseRule.php b/src/Rules/Namespaces/ExistingNamesInUseRule.php index 3bb023ad25..daf1ee2ce1 100644 --- a/src/Rules/Namespaces/ExistingNamesInUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInUseRule.php @@ -5,10 +5,10 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use function array_map; @@ -18,13 +18,14 @@ /** * @implements Rule */ -class ExistingNamesInUseRule implements Rule +final class ExistingNamesInUseRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, + private bool $discoveringSymbolsTip, ) { } @@ -54,12 +55,12 @@ public function processNode(Node $node, Scope $scope): array return $this->checkFunctions($node->uses); } - return $this->checkClasses($node->uses); + return $this->checkClasses($scope, $node->uses); } /** - * @param Node\Stmt\UseUse[] $uses - * @return RuleError[] + * @param Node\UseItem[] $uses + * @return list */ private function checkConstants(array $uses): array { @@ -69,22 +70,38 @@ private function checkConstants(array $uses): array continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $use->name))->line($use->name->getLine())->discoveringSymbolsTip()->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $use->name)) + ->line($use->name->getStartLine()) + ->identifier('constant.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } return $errors; } /** - * @param Node\Stmt\UseUse[] $uses - * @return RuleError[] + * @param Node\UseItem[] $uses + * @return list */ private function checkFunctions(array $uses): array { $errors = []; foreach ($uses as $use) { if (!$this->reflectionProvider->hasFunction($use->name, null)) { - $errors[] = RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $use->name))->line($use->name->getLine())->discoveringSymbolsTip()->build(); + $errorBuilder = RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $use->name)) + ->line($use->name->getStartLine()) + ->identifier('function.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } elseif ($this->checkFunctionNameCase) { $functionReflection = $this->reflectionProvider->getFunction($use->name, null); $realName = $functionReflection->getName(); @@ -97,7 +114,10 @@ private function checkFunctions(array $uses): array 'Function %s used with incorrect case: %s.', $realName, $usedName, - ))->line($use->name->getLine())->build(); + )) + ->line($use->name->getStartLine()) + ->identifier('function.nameCase') + ->build(); } } } @@ -106,13 +126,15 @@ private function checkFunctions(array $uses): array } /** - * @param Node\Stmt\UseUse[] $uses - * @return RuleError[] + * @param Node\UseItem[] $uses + * @return list */ - private function checkClasses(array $uses): array + private function checkClasses(Scope $scope, array $uses): array { - return $this->classCaseSensitivityCheck->checkClassNames( - array_map(static fn (Node\Stmt\UseUse $use): ClassNameNodePair => new ClassNameNodePair((string) $use->name, $use->name), $uses), + return $this->classCheck->checkClassNames( + $scope, + array_map(static fn (Node\UseItem $use): ClassNameNodePair => new ClassNameNodePair((string) $use->name, $use->name), $uses), + null, ); } diff --git a/src/Rules/NonIgnorableRuleError.php b/src/Rules/NonIgnorableRuleError.php index 4b79dac56d..f0a4fbeee3 100644 --- a/src/Rules/NonIgnorableRuleError.php +++ b/src/Rules/NonIgnorableRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface NonIgnorableRuleError extends RuleError { diff --git a/src/Rules/NullsafeCheck.php b/src/Rules/NullsafeCheck.php index fd17b1f850..a4424b69ac 100644 --- a/src/Rules/NullsafeCheck.php +++ b/src/Rules/NullsafeCheck.php @@ -4,7 +4,7 @@ use PhpParser\Node\Expr; -class NullsafeCheck +final class NullsafeCheck { public function containsNullSafe(Expr $expr): bool @@ -36,7 +36,7 @@ public function containsNullSafe(Expr $expr): bool return $this->containsNullSafe($expr->class); } - if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) { + if ($expr instanceof Expr\List_) { foreach ($expr->items as $item) { if ($item === null) { continue; diff --git a/src/Rules/Operators/InvalidAssignVarRule.php b/src/Rules/Operators/InvalidAssignVarRule.php index 7495c81e27..a5ae2ac291 100644 --- a/src/Rules/Operators/InvalidAssignVarRule.php +++ b/src/Rules/Operators/InvalidAssignVarRule.php @@ -15,7 +15,7 @@ /** * @implements Rule */ -class InvalidAssignVarRule implements Rule +final class InvalidAssignVarRule implements Rule { public function __construct(private NullsafeCheck $nullsafeCheck) @@ -39,26 +39,34 @@ public function processNode(Node $node, Scope $scope): array if ($this->nullsafeCheck->containsNullSafe($node->var)) { return [ - RuleErrorBuilder::message('Nullsafe operator cannot be on left side of assignment.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Nullsafe operator cannot be on left side of assignment.') + ->identifier('nullsafe.assign') + ->nonIgnorable() + ->build(), ]; } if ($node instanceof AssignRef && $this->nullsafeCheck->containsNullSafe($node->expr)) { return [ - RuleErrorBuilder::message('Nullsafe operator cannot be on right side of assignment by reference.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Nullsafe operator cannot be on right side of assignment by reference.') + ->identifier('nullsafe.byRef') + ->nonIgnorable() + ->build(), ]; } if ($this->containsNonAssignableExpression($node->var)) { return [ - RuleErrorBuilder::message('Expression on left side of assignment is not assignable.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Expression on left side of assignment is not assignable.') + ->identifier('assign.invalidExpr') + ->nonIgnorable() + ->build(), ]; } return []; } - private function containsNonAssignableExpression(Expr $expr): bool { if ($expr instanceof Expr\Variable) { @@ -77,7 +85,7 @@ private function containsNonAssignableExpression(Expr $expr): bool return false; } - if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) { + if ($expr instanceof Expr\List_) { foreach ($expr->items as $item) { if ($item === null) { continue; diff --git a/src/Rules/Operators/InvalidBinaryOperationRule.php b/src/Rules/Operators/InvalidBinaryOperationRule.php index f9faf4a8f0..e44b2178d5 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -3,13 +3,14 @@ namespace PHPStan\Rules\Operators; use PhpParser\Node; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; +use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; @@ -20,11 +21,11 @@ /** * @implements Rule */ -class InvalidBinaryOperationRule implements Rule +final class InvalidBinaryOperationRule implements Rule { public function __construct( - private Standard $printer, + private ExprPrinter $exprPrinter, private RuleLevelHelper $ruleLevelHelper, ) { @@ -44,74 +45,79 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->getType($node) instanceof ErrorType) { - $leftName = '__PHPSTAN__LEFT__'; - $rightName = '__PHPSTAN__RIGHT__'; - $leftVariable = new Node\Expr\Variable($leftName); - $rightVariable = new Node\Expr\Variable($rightName); - if ($node instanceof Node\Expr\AssignOp) { - $newNode = clone $node; - $left = $node->var; - $right = $node->expr; - $newNode->var = $leftVariable; - $newNode->expr = $rightVariable; - } else { - $newNode = clone $node; - $left = $node->left; - $right = $node->right; - $newNode->left = $leftVariable; - $newNode->right = $rightVariable; - } - - if ($node instanceof Node\Expr\AssignOp\Concat || $node instanceof Node\Expr\BinaryOp\Concat) { - $callback = static fn (Type $type): bool => !$type->toString() instanceof ErrorType; - } else { - $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; - } + $leftName = '__PHPSTAN__LEFT__'; + $rightName = '__PHPSTAN__RIGHT__'; + $leftVariable = new Node\Expr\Variable($leftName); + $rightVariable = new Node\Expr\Variable($rightName); + if ($node instanceof Node\Expr\AssignOp) { + $identifier = 'assignOp'; + $newNode = clone $node; + $newNode->setAttribute('phpstan_cache_printer', null); + $left = $node->var; + $right = $node->expr; + $newNode->var = $leftVariable; + $newNode->expr = $rightVariable; + } else { + $identifier = 'binaryOp'; + $newNode = clone $node; + $newNode->setAttribute('phpstan_cache_printer', null); + $left = $node->left; + $right = $node->right; + $newNode->left = $leftVariable; + $newNode->right = $rightVariable; + } - $leftType = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $left, - '', - $callback, - )->getType(); - if ($leftType instanceof ErrorType) { - return []; - } + if ($node instanceof Node\Expr\AssignOp\Concat || $node instanceof Node\Expr\BinaryOp\Concat) { + $callback = static fn (Type $type): bool => !$type->toString() instanceof ErrorType; + } elseif ($node instanceof Node\Expr\AssignOp\Plus || $node instanceof Node\Expr\BinaryOp\Plus) { + $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType || $type->isArray()->yes(); + } else { + $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; + } - $rightType = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $right, - '', - $callback, - )->getType(); - if ($rightType instanceof ErrorType) { - return []; - } + $leftType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $left, + '', + $callback, + )->getType(); + if ($leftType instanceof ErrorType) { + return []; + } - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } + $rightType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $right, + '', + $callback, + )->getType(); + if ($rightType instanceof ErrorType) { + return []; + } - $scope = $scope - ->assignVariable($leftName, $leftType) - ->assignVariable($rightName, $rightType); + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } - if (!$scope->getType($newNode) instanceof ErrorType) { - return []; - } + $scope = $scope + ->assignVariable($leftName, $leftType, $leftType, TrinaryLogic::createYes()) + ->assignVariable($rightName, $rightType, $rightType, TrinaryLogic::createYes()); - return [ - RuleErrorBuilder::message(sprintf( - '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()), - ))->line($left->getLine())->build(), - ]; + if (!$scope->getType($newNode) instanceof ErrorType) { + return []; } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'Binary operation "%s" between %s and %s results in an error.', + substr(substr($this->exprPrinter->printExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)), + $scope->getType($left)->describe(VerbosityLevel::value()), + $scope->getType($right)->describe(VerbosityLevel::value()), + )) + ->line($left->getStartLine()) + ->identifier(sprintf('%s.invalid', $identifier)) + ->build(), + ]; } } diff --git a/src/Rules/Operators/InvalidComparisonOperationRule.php b/src/Rules/Operators/InvalidComparisonOperationRule.php index 24110f32dd..8dc06429e5 100644 --- a/src/Rules/Operators/InvalidComparisonOperationRule.php +++ b/src/Rules/Operators/InvalidComparisonOperationRule.php @@ -7,22 +7,23 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; -use PHPStan\Type\NullType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function get_class; use function sprintf; /** * @implements Rule */ -class InvalidComparisonOperationRule implements Rule +final class InvalidComparisonOperationRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -48,6 +49,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($this->isNumberType($scope, $node->left) && $this->isNumberType($scope, $node->right)) { + return []; + } + if ( ($this->isNumberType($scope, $node->left) && ( $this->isPossiblyNullableObjectType($scope, $node->right) || $this->isPossiblyNullableArrayType($scope, $node->right) @@ -56,13 +61,42 @@ public function processNode(Node $node, Scope $scope): array $this->isPossiblyNullableObjectType($scope, $node->left) || $this->isPossiblyNullableArrayType($scope, $node->left) )) ) { + switch (get_class($node)) { + case Node\Expr\BinaryOp\Equal::class: + $nodeType = 'equal'; + break; + case Node\Expr\BinaryOp\NotEqual::class: + $nodeType = 'notEqual'; + break; + case Node\Expr\BinaryOp\Greater::class: + $nodeType = 'greater'; + break; + case Node\Expr\BinaryOp\GreaterOrEqual::class: + $nodeType = 'greaterOrEqual'; + break; + case Node\Expr\BinaryOp\Smaller::class: + $nodeType = 'smaller'; + break; + case Node\Expr\BinaryOp\SmallerOrEqual::class: + $nodeType = 'smallerOrEqual'; + break; + case Node\Expr\BinaryOp\Spaceship::class: + $nodeType = 'spaceship'; + break; + default: + throw new ShouldNotHappenException(); + } + return [ RuleErrorBuilder::message(sprintf( 'Comparison operation "%s" between %s and %s results in an error.', $node->getOperatorSigil(), $scope->getType($node->left)->describe(VerbosityLevel::value()), $scope->getType($node->right)->describe(VerbosityLevel::value()), - ))->line($node->left->getLine())->build(), + )) + ->line($node->left->getStartLine()) + ->identifier(sprintf('%s.invalid', $nodeType)) + ->build(), ]; } @@ -72,7 +106,7 @@ public function processNode(Node $node, Scope $scope): array private function isNumberType(Scope $scope, Node\Expr $expr): bool { $acceptedType = new UnionType([new IntegerType(), new FloatType()]); - $onlyNumber = static fn (Type $type): bool => $acceptedType->accepts($type, true)->yes(); + $onlyNumber = static fn (Type $type): bool => $acceptedType->isSuperTypeOf($type)->yes(); $type = $this->ruleLevelHelper->findTypeToCheck($scope, $expr, '', $onlyNumber)->getType(); @@ -83,7 +117,8 @@ private function isNumberType(Scope $scope, Node\Expr $expr): bool return false; } - return !$acceptedType->isSuperTypeOf($type)->no(); + // SimpleXMLElement can be cast to number union type + return !$acceptedType->isSuperTypeOf($type)->no() || $acceptedType->equals($type->toNumber()); } private function isPossiblyNullableObjectType(Scope $scope, Node\Expr $expr): bool @@ -101,7 +136,7 @@ private function isPossiblyNullableObjectType(Scope $scope, Node\Expr $expr): bo return false; } - if (TypeCombinator::containsNull($type) && !$type instanceof NullType) { + if (TypeCombinator::containsNull($type) && !$type->isNull()->yes()) { $type = TypeCombinator::removeNull($type); } @@ -122,7 +157,7 @@ private function isPossiblyNullableArrayType(Scope $scope, Node\Expr $expr): boo static fn (Type $type): bool => $type->isArray()->yes(), )->getType(); - if (TypeCombinator::containsNull($type) && !$type instanceof NullType) { + if (TypeCombinator::containsNull($type) && !$type->isNull()->yes()) { $type = TypeCombinator::removeNull($type); } diff --git a/src/Rules/Operators/InvalidIncDecOperationRule.php b/src/Rules/Operators/InvalidIncDecOperationRule.php index 1bd9d46d0e..8283936e73 100644 --- a/src/Rules/Operators/InvalidIncDecOperationRule.php +++ b/src/Rules/Operators/InvalidIncDecOperationRule.php @@ -6,17 +6,30 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\BooleanType; use PHPStan\Type\ErrorType; +use PHPStan\Type\FloatType; +use PHPStan\Type\IntegerType; +use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function get_class; use function sprintf; /** * @implements Rule */ -class InvalidIncDecOperationRule implements Rule +final class InvalidIncDecOperationRule implements Rule { - public function __construct(private bool $checkThisOnly) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) { } @@ -36,6 +49,23 @@ public function processNode(Node $node, Scope $scope): array return []; } + switch (get_class($node)) { + case Node\Expr\PreInc::class: + $nodeType = 'preInc'; + break; + case Node\Expr\PostInc::class: + $nodeType = 'postInc'; + break; + case Node\Expr\PreDec::class: + $nodeType = 'preDec'; + break; + case Node\Expr\PostDec::class: + $nodeType = 'postDec'; + break; + default: + throw new ShouldNotHappenException(); + } + $operatorString = $node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc ? '++' : '--'; if ( @@ -48,29 +78,35 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Cannot use %s on a non-variable.', $operatorString, - ))->line($node->var->getLine())->build(), + )) + ->line($node->var->getStartLine()) + ->identifier(sprintf('%s.expr', $nodeType)) + ->build(), ]; } - if (!$this->checkThisOnly) { - $varType = $scope->getType($node->var); - if (!$varType->toString() instanceof ErrorType) { - return []; - } - if (!$varType->toNumber() instanceof ErrorType) { - return []; - } + $allowedTypes = new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType(), new NullType(), new ObjectType('SimpleXMLElement')]); + $varType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->var, + '', + static fn (Type $type): bool => $allowedTypes->isSuperTypeOf($type)->yes(), + )->getType(); - return [ - RuleErrorBuilder::message(sprintf( - 'Cannot use %s on %s.', - $operatorString, - $varType->describe(VerbosityLevel::value()), - ))->line($node->var->getLine())->build(), - ]; + if ($varType instanceof ErrorType || $allowedTypes->isSuperTypeOf($varType)->yes()) { + return []; } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot use %s on %s.', + $operatorString, + $varType->describe(VerbosityLevel::value()), + )) + ->line($node->var->getStartLine()) + ->identifier(sprintf('%s.type', $nodeType)) + ->build(), + ]; } } diff --git a/src/Rules/Operators/InvalidUnaryOperationRule.php b/src/Rules/Operators/InvalidUnaryOperationRule.php index 7c3ac12ecf..6600cce4ad 100644 --- a/src/Rules/Operators/InvalidUnaryOperationRule.php +++ b/src/Rules/Operators/InvalidUnaryOperationRule.php @@ -3,19 +3,30 @@ namespace PHPStan\Rules\Operators; use PhpParser\Node; +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\TrinaryLogic; use PHPStan\Type\ErrorType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function sprintf; /** * @implements Rule */ -class InvalidUnaryOperationRule implements Rule +final class InvalidUnaryOperationRule implements Rule { + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + public function getNodeType(): string { return Node\Expr::class; @@ -31,25 +42,54 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->getType($node) instanceof ErrorType) { - - if ($node instanceof Node\Expr\UnaryPlus) { - $operator = '+'; - } elseif ($node instanceof Node\Expr\UnaryMinus) { - $operator = '-'; - } else { - $operator = '~'; - } - return [ - RuleErrorBuilder::message(sprintf( - 'Unary operation "%s" on %s results in an error.', - $operator, - $scope->getType($node->expr)->describe(VerbosityLevel::value()), - ))->line($node->expr->getLine())->build(), - ]; + $varName = '__PHPSTAN__LEFT__'; + $variable = new Node\Expr\Variable($varName); + $newNode = clone $node; + $newNode->setAttribute('phpstan_cache_printer', null); + $newNode->expr = $variable; + + if ($node instanceof Node\Expr\BitwiseNot) { + $callback = static fn (Type $type): bool => $type->isString()->yes() || $type->isInteger()->yes() || $type->isFloat()->yes(); + } else { + $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; + } + + $exprType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->expr, + '', + $callback, + )->getType(); + if ($exprType instanceof ErrorType) { + return []; } - return []; + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $scope = $scope->assignVariable($varName, $exprType, $exprType, TrinaryLogic::createYes()); + if (!$scope->getType($newNode) instanceof ErrorType) { + return []; + } + + if ($node instanceof Node\Expr\UnaryPlus) { + $operator = '+'; + } elseif ($node instanceof Node\Expr\UnaryMinus) { + $operator = '-'; + } else { + $operator = '~'; + } + return [ + RuleErrorBuilder::message(sprintf( + 'Unary operation "%s" on %s results in an error.', + $operator, + $scope->getType($node->expr)->describe(VerbosityLevel::value()), + )) + ->line($node->expr->getStartLine()) + ->identifier('unaryOp.invalid') + ->build(), + ]; } } diff --git a/src/Rules/ParameterCastableToStringCheck.php b/src/Rules/ParameterCastableToStringCheck.php new file mode 100644 index 0000000000..9753863557 --- /dev/null +++ b/src/Rules/ParameterCastableToStringCheck.php @@ -0,0 +1,72 @@ +unpack) { + return null; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $parameter->value, + '', + static fn (Type $type): bool => $type->isArray()->yes() && !$castFn($type->getIterableValueType()) instanceof ErrorType, + ); + + if ( + ! $typeResult->getType()->isArray()->yes() + || !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType + ) { + return null; + } + + return RuleErrorBuilder::message( + sprintf($errorMessageTemplate, $parameterName, $functionName, $typeResult->getType()->describe(VerbosityLevel::typeOnly())), + )->identifier('argument.type')->build(); + } + + public function getParameterName(Arg $parameter, int $parameterIdx, ?ParameterReflection $parameterReflection): string + { + if ($parameterReflection === null) { + return sprintf('#%d', $parameterIdx + 1); + } + + $paramName = $parameterReflection->getName(); + $origParameter = $parameter->getAttributes()[ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE] ?? null; + + if (!$origParameter instanceof Arg) { + $origParameter = $parameter; + } + + return $origParameter->name !== null + ? sprintf('$%s', $paramName) + : sprintf('#%d $%s', $parameterIdx + 1, $paramName); + } + +} diff --git a/src/Rules/PhpDoc/AssertRuleHelper.php b/src/Rules/PhpDoc/AssertRuleHelper.php new file mode 100644 index 0000000000..473036edb1 --- /dev/null +++ b/src/Rules/PhpDoc/AssertRuleHelper.php @@ -0,0 +1,215 @@ + + */ + public function check( + Scope $scope, + Function_|ClassMethod $node, + ExtendedMethodReflection|FunctionReflection $reflection, + ParametersAcceptor $acceptor, + ): array + { + $parametersByName = []; + foreach ($acceptor->getParameters() as $parameter) { + $parametersByName[$parameter->getName()] = $parameter->getType(); + } + + if ($reflection instanceof ExtendedMethodReflection && !$reflection->isStatic()) { + $class = $reflection->getDeclaringClass(); + $parametersByName['this'] = new ObjectType($class->getName(), null, $class); + } + + $context = InitializerExprContext::createEmpty(); + + $errors = []; + foreach ($reflection->getAsserts()->getAll() as $assert) { + $parameterName = substr($assert->getParameter()->getParameterName(), 1); + if (!array_key_exists($parameterName, $parametersByName)) { + $errors[] = RuleErrorBuilder::message(sprintf('Assert references unknown parameter $%s.', $parameterName)) + ->identifier('parameter.notFound') + ->build(); + continue; + } + + if (!$assert->isExplicit()) { + continue; + } + + $assertedExpr = $assert->getParameter()->getExpr(new TypeExpr($parametersByName[$parameterName])); + $assertedExprType = $this->initializerExprTypeResolver->getType($assertedExpr, $context); + $assertedExprString = $assert->getParameter()->describe(); + if ($assertedExprType instanceof ErrorType) { + $errors[] = RuleErrorBuilder::message(sprintf('Assert references unknown %s.', $assertedExprString)) + ->identifier('assert.unknownExpr') + ->build(); + continue; + } + + $assertedType = $assert->getType(); + + $tagName = [ + AssertTag::NULL => '@phpstan-assert', + AssertTag::IF_TRUE => '@phpstan-assert-if-true', + AssertTag::IF_FALSE => '@phpstan-assert-if-false', + ][$assert->getIf()]; + + if ($this->unresolvableTypeHelper->containsUnresolvableType($assertedType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for %s contains unresolvable type.', + $tagName, + $assertedExprString, + ))->identifier('assert.unresolvableType')->build(); + continue; + } + + $isSuperType = $assertedType->isSuperTypeOf($assertedExprType); + if (!$isSuperType->maybe()) { + if ($assert->isNegated() ? $isSuperType->yes() : $isSuperType->no()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Asserted %stype %s for %s with type %s can never happen.', + $assert->isNegated() ? 'negated ' : '', + $assertedType->describe(VerbosityLevel::precise()), + $assertedExprString, + $assertedExprType->describe(VerbosityLevel::precise()), + ))->identifier('assert.impossibleType')->build(); + } elseif ($assert->isNegated() ? $isSuperType->no() : $isSuperType->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Asserted %stype %s for %s with type %s does not narrow down the type.', + $assert->isNegated() ? 'negated ' : '', + $assertedType->describe(VerbosityLevel::precise()), + $assertedExprString, + $assertedExprType->describe(VerbosityLevel::precise()), + ))->identifier('assert.alreadyNarrowedType')->build(); + } + } + + foreach ($assertedType->getReferencedClasses() as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for %s contains unknown class %s.', + $tagName, + $assertedExprString, + $class, + ))->identifier('class.notFound')->build(); + continue; + } + + $classReflection = $this->reflectionProvider->getClass($class); + if ($classReflection->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for %s contains invalid type %s.', + $tagName, + $assertedExprString, + $class, + ))->identifier('assert.trait')->build(); + continue; + } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_ASSERT, [ + 'phpDocTagName' => $tagName, + 'assertedExprString' => $assertedExprString, + ]), $this->checkClassCaseSensitivity), + ); + } + + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( + $assertedType, + sprintf('PHPDoc tag %s for %s contains generic type %%s but %%s %%s is not generic.', $tagName, $assertedExprString), + sprintf('Generic type %%s in PHPDoc tag %s for %s does not specify all template types of %%s %%s: %%s', $tagName, $assertedExprString), + sprintf('Generic type %%s in PHPDoc tag %s for %s specifies %%d template types, but %%s %%s supports only %%d: %%s', $tagName, $assertedExprString), + sprintf('Type %%s in generic type %%s in PHPDoc tag %s for %s is not subtype of template type %%s of %%s %%s.', $tagName, $assertedExprString), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for %s is in conflict with %%s template type %%s of %%s %%s.', $tagName, $assertedExprString), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for %s is redundant, template type %%s of %%s %%s has the same variance.', $tagName, $assertedExprString), + )); + + if (!$this->checkMissingTypehints) { + continue; + } + + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($assertedType) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for %s has no value type specified in iterable type %s.', + $tagName, + $assertedExprString, + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($assertedType) as [$innerName, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for %s contains generic %s but does not specify its types: %s', + $tagName, + $assertedExprString, + $innerName, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($assertedType) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for %s has no signature specified for %s.', + $tagName, + $assertedExprString, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + } + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php new file mode 100644 index 0000000000..f48a8abc7a --- /dev/null +++ b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php @@ -0,0 +1,128 @@ + + */ + public function check(ExtendedParametersAcceptor $acceptor): array + { + $conditionalTypes = []; + $parametersByName = []; + foreach ($acceptor->getParameters() as $parameter) { + TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + + if ($parameter->getOutType() !== null) { + TypeTraverser::map($parameter->getOutType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + } + + if ($parameter->getClosureThisType() !== null) { + TypeTraverser::map($parameter->getClosureThisType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + } + + $parametersByName[$parameter->getName()] = $parameter; + } + + TypeTraverser::map($acceptor->getReturnType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + + $errors = []; + foreach ($conditionalTypes as $conditionalType) { + if ($conditionalType instanceof ConditionalType) { + $subjectType = $conditionalType->getSubject(); + if ($subjectType instanceof StaticType) { + continue; + } + $templateTypes = []; + TypeTraverser::map($subjectType, static function (Type $type, callable $traverse) use (&$templateTypes): Type { + if ($type instanceof TemplateType) { + $templateTypes[] = $type; + return $type; + } + + return $traverse($type); + }); + + if (count($templateTypes) === 0) { + $errors[] = RuleErrorBuilder::message(sprintf('Conditional return type uses subject type %s which is not part of PHPDoc @template tags.', $subjectType->describe(VerbosityLevel::typeOnly()))) + ->identifier('conditionalType.subjectNotFound') + ->build(); + continue; + } + } else { + $parameterName = substr($conditionalType->getParameterName(), 1); + if (!array_key_exists($parameterName, $parametersByName)) { + $errors[] = RuleErrorBuilder::message(sprintf('Conditional return type references unknown parameter $%s.', $parameterName)) + ->identifier('parameter.notFound') + ->build(); + continue; + } + $subjectType = $parametersByName[$parameterName]->getType(); + } + + $targetType = $conditionalType->getTarget(); + $isTargetSuperType = $targetType->isSuperTypeOf($subjectType); + if ($isTargetSuperType->maybe()) { + continue; + } + + $verbosity = VerbosityLevel::getRecommendedLevelByType($subjectType, $targetType); + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Condition "%s" in conditional return type is always %s.', + sprintf('%s %s %s', $subjectType->describe($verbosity), $conditionalType->isNegated() ? 'is not' : 'is', $targetType->describe($verbosity)), + $conditionalType->isNegated() + ? ($isTargetSuperType->yes() ? 'false' : 'true') + : ($isTargetSuperType->yes() ? 'true' : 'false'), + )) + ->identifier(sprintf('conditionalType.always%s', $conditionalType->isNegated() + ? ($isTargetSuperType->yes() ? 'False' : 'True') + : ($isTargetSuperType->yes() ? 'True' : 'False'))) + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/FunctionAssertRule.php b/src/Rules/PhpDoc/FunctionAssertRule.php new file mode 100644 index 0000000000..cf91c5ece6 --- /dev/null +++ b/src/Rules/PhpDoc/FunctionAssertRule.php @@ -0,0 +1,37 @@ + + */ +final class FunctionAssertRule implements Rule +{ + + public function __construct(private AssertRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return InFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $function = $node->getFunctionReflection(); + $variants = $function->getVariants(); + if (count($variants) !== 1) { + return []; + } + + return $this->helper->check($scope, $node->getOriginalNode(), $function, $variants[0]); + } + +} diff --git a/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php b/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php new file mode 100644 index 0000000000..56ed3c3cf7 --- /dev/null +++ b/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php @@ -0,0 +1,37 @@ + + */ +final class FunctionConditionalReturnTypeRule implements Rule +{ + + public function __construct(private ConditionalReturnTypeRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return InFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $function = $node->getFunctionReflection(); + $variants = $function->getVariants(); + if (count($variants) !== 1) { + return []; + } + + return $this->helper->check($variants[0]); + } + +} diff --git a/src/Rules/PhpDoc/GenericCallableRuleHelper.php b/src/Rules/PhpDoc/GenericCallableRuleHelper.php new file mode 100644 index 0000000000..c32491fe42 --- /dev/null +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -0,0 +1,120 @@ + $functionTemplateTags + * + * @return list + */ + public function check( + Node $node, + Scope $scope, + string $location, + Type $callableType, + ?string $functionName, + array $functionTemplateTags, + ?ClassReflection $classReflection, + ): array + { + $errors = []; + + TypeTraverser::map($callableType, function (Type $type, callable $traverse) use (&$errors, $node, $scope, $location, $functionName, $functionTemplateTags, $classReflection) { + if (!($type instanceof CallableType || $type instanceof ClosureType)) { + return $traverse($type); + } + + $typeDescription = $type->describe(VerbosityLevel::precise()); + + $errors = $this->templateTypeCheck->check( + $scope, + $node, + TemplateTypeScope::createWithAnonymousFunction(), + $type->getTemplateTags(), + sprintf('PHPDoc tag %s template of %s cannot have existing class %%s as its name.', $location, $typeDescription), + sprintf('PHPDoc tag %s template of %s cannot have existing type alias %%s as its name.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s has invalid bound type %%s.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s with bound type %%s is not supported.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s has invalid default type %%s.', $location, $typeDescription), + sprintf('Default type %%s in PHPDoc tag %s template %%s of %s is not subtype of bound type %%s.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s does not have a default type but follows an optional template %%s.', $location, $typeDescription), + ); + + $templateTags = $type->getTemplateTags(); + + $classDescription = null; + if ($classReflection !== null) { + $classDescription = $classReflection->getDisplayName(); + } + + if ($functionName !== null) { + $functionDescription = sprintf('function %s', $functionName); + if ($classReflection !== null) { + $functionDescription = sprintf('method %s::%s', $classDescription, $functionName); + } + + foreach (array_keys($functionTemplateTags) as $name) { + if (!isset($templateTags[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s template %s of %s shadows @template %s for %s.', + $location, + $name, + $typeDescription, + $name, + $functionDescription, + ))->identifier('callable.shadowTemplate')->build(); + } + } + + if ($classReflection !== null) { + foreach (array_keys($classReflection->getTemplateTags()) as $name) { + if (!isset($templateTags[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s template %s of %s shadows @template %s for class %s.', + $location, + $name, + $typeDescription, + $name, + $classDescription, + ))->identifier('callable.shadowTemplate')->build(); + } + } + + return $traverse($type); + }); + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php index ace957a7ba..0008644c1a 100644 --- a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php @@ -5,14 +5,14 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; -use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ConstantTypeHelper; +use PHPStan\Type\ParserNodeTypeToPHPStanType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_merge; use function sprintf; @@ -20,7 +20,7 @@ /** * @implements Rule */ -class IncompatibleClassConstantPhpDocTypeRule implements Rule +final class IncompatibleClassConstantPhpDocTypeRule implements Rule { public function __construct( @@ -41,31 +41,31 @@ public function processNode(Node $node, Scope $scope): array throw new ShouldNotHappenException(); } + $nativeType = null; + if ($node->type !== null) { + $nativeType = ParserNodeTypeToPHPStanType::resolve($node->type, $scope->getClassReflection()); + } + $errors = []; foreach ($node->consts as $const) { $constantName = $const->name->toString(); - $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); + $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $nativeType, $constantName)); } return $errors; } /** - * @return RuleError[] + * @return list */ - private function processSingleConstant(ClassReflection $classReflection, string $constantName): array + private function processSingleConstant(ClassReflection $classReflection, ?Type $nativeType, string $constantName): array { $constantReflection = $classReflection->getConstant($constantName); - if (!$constantReflection instanceof ClassConstantReflection) { + $phpDocType = $constantReflection->getPhpDocType(); + if ($phpDocType === null) { return []; } - if (!$constantReflection->hasPhpDocType()) { - return []; - } - - $phpDocType = $constantReflection->getValueType(); - $errors = []; if ( $this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) @@ -74,28 +74,26 @@ private function processSingleConstant(ClassReflection $classReflection, string 'PHPDoc tag @var for constant %s::%s contains unresolvable type.', $constantReflection->getDeclaringClass()->getName(), $constantName, - ))->build(); - } else { - $nativeType = ConstantTypeHelper::getTypeFromValue($constantReflection->getValue()); - $isSuperType = $phpDocType->isSuperTypeOf($nativeType); - $verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $nativeType); + ))->identifier('classConstant.unresolvableType')->build(); + } elseif ($nativeType !== null) { + $isSuperType = $nativeType->isSuperTypeOf($phpDocType); if ($isSuperType->no()) { $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @var for constant %s::%s with type %s is incompatible with value %s.', + 'PHPDoc tag @var for constant %s::%s with type %s is incompatible with native type %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, - $phpDocType->describe($verbosity), - $nativeType->describe(VerbosityLevel::value()), - ))->build(); + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.phpDocType')->build(); } elseif ($isSuperType->maybe()) { $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @var for constant %s::%s with type %s is not subtype of value %s.', + 'PHPDoc tag @var for constant %s::%s with type %s is not subtype of native type %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, - $phpDocType->describe($verbosity), - $nativeType->describe(VerbosityLevel::value()), - ))->build(); + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.phpDocType')->build(); } } @@ -124,6 +122,16 @@ private function processSingleConstant(ClassReflection $classReflection, string $className, $escapedConstantName, ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is in conflict with %%s template type %%s of %%s %%s.', + $className, + $escapedConstantName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is redundant, template type %%s of %%s %%s has the same variance.', + $className, + $escapedConstantName, + ), )); } diff --git a/src/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRule.php b/src/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRule.php new file mode 100644 index 0000000000..fa6f36463c --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRule.php @@ -0,0 +1,94 @@ + + */ +final class IncompatibleParamImmediatelyInvokedCallableRule implements Rule +{ + + public function __construct( + private FileTypeMapper $fileTypeMapper, + ) + { + } + + public function getNodeType(): string + { + return FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node instanceof Node\Stmt\ClassMethod) { + $functionName = $node->name->name; + } elseif ($node instanceof Node\Stmt\Function_) { + $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\'); + } else { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $functionName, + $docComment->getText(), + ); + $nativeParameterTypes = []; + foreach ($node->getParams() as $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + $nativeParameterTypes[$parameter->var->name] = $scope->getFunctionType( + $parameter->type, + $scope->isParameterValueNullable($parameter), + false, + ); + } + + $errors = []; + foreach ($resolvedPhpDoc->getParamsImmediatelyInvokedCallable() as $parameterName => $immediately) { + $tagName = $immediately ? '@param-immediately-invoked-callable' : '@param-later-invoked-callable'; + if (!isset($nativeParameterTypes[$parameterName])) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s references unknown parameter: $%s', + $tagName, + $parameterName, + ))->identifier('parameter.notFound')->build(); + } elseif ($nativeParameterTypes[$parameterName]->isCallable()->no()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s is for parameter $%s with non-callable type %s.', + $tagName, + $parameterName, + $nativeParameterTypes[$parameterName]->describe(VerbosityLevel::typeOnly()), + ))->identifier(sprintf( + '%s.nonCallable', + $immediately ? 'paramImmediatelyInvokedCallable' : 'paramLaterInvokedCallable', + ))->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/IncompatiblePhpDocTypeCheck.php b/src/Rules/PhpDoc/IncompatiblePhpDocTypeCheck.php new file mode 100644 index 0000000000..56c0ac529e --- /dev/null +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeCheck.php @@ -0,0 +1,236 @@ + $nativeParameterTypes + * @param array $byRefParameters + * @return list + */ + public function check( + Scope $scope, + Node $node, + ResolvedPhpDocBlock $resolvedPhpDoc, + string $functionName, + array $nativeParameterTypes, + array $byRefParameters, + Type $nativeReturnType, + ): array + { + $errors = []; + + foreach (['@param' => $resolvedPhpDoc->getParamTags(), '@param-out' => $resolvedPhpDoc->getParamOutTags(), '@param-closure-this' => $resolvedPhpDoc->getParamClosureThisTags()] as $tagName => $parameters) { + foreach ($parameters as $parameterName => $phpDocParamTag) { + $phpDocParamType = $phpDocParamTag->getType(); + + if (!isset($nativeParameterTypes[$parameterName])) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s references unknown parameter: $%s', + $tagName, + $parameterName, + ))->identifier('parameter.notFound')->build(); + + } elseif ( + $this->unresolvableTypeHelper->containsUnresolvableType($phpDocParamType) + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for parameter $%s contains unresolvable type.', + $tagName, + $parameterName, + ))->identifier('parameter.unresolvableType')->build(); + + } else { + $nativeParamType = $nativeParameterTypes[$parameterName]; + if ( + $phpDocParamTag instanceof ParamTag + && $phpDocParamTag->isVariadic() + && $phpDocParamType->isArray()->yes() + && $nativeParamType->isArray()->no() + ) { + $phpDocParamType = $phpDocParamType->getIterableValueType(); + } + + $escapedParameterName = SprintfHelper::escapeFormatString($parameterName); + $escapedTagName = SprintfHelper::escapeFormatString($tagName); + + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( + $phpDocParamType, + sprintf( + 'PHPDoc tag %s for parameter $%s contains generic type %%s but %%s %%s is not generic.', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag %s for parameter $%s does not specify all template types of %%s %%s: %%s', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag %s for parameter $%s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Type %%s in generic type %%s in PHPDoc tag %s for parameter $%s is not subtype of template type %%s of %%s %%s.', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag %s for parameter $%s is in conflict with %%s template type %%s of %%s %%s.', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag %s for parameter $%s is redundant, template type %%s of %%s %%s has the same variance.', + $escapedTagName, + $escapedParameterName, + ), + )); + + $errors = array_merge($errors, $this->genericCallableRuleHelper->check( + $node, + $scope, + sprintf('%s for parameter $%s', $escapedTagName, $escapedParameterName), + $phpDocParamType, + $functionName, + $resolvedPhpDoc->getTemplateTags(), + $scope->isInClass() ? $scope->getClassReflection() : null, + )); + + if ($phpDocParamTag instanceof ParamOutTag) { + if (!$byRefParameters[$parameterName]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Parameter $%s for PHPDoc tag %s is not passed by reference.', + $parameterName, + $tagName, + ))->identifier('parameter.notByRef')->build(); + + } + continue; + } + + if (in_array($tagName, ['@param', '@param-out'], true)) { + $isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType); + if ($isParamSuperType->no()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for parameter $%s with type %s is incompatible with native type %s.', + $tagName, + $parameterName, + $phpDocParamType->describe(VerbosityLevel::typeOnly()), + $nativeParamType->describe(VerbosityLevel::typeOnly()), + ))->identifier('parameter.phpDocType')->build(); + + } elseif ($isParamSuperType->maybe()) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for parameter $%s with type %s is not subtype of native type %s.', + $tagName, + $parameterName, + $phpDocParamType->describe(VerbosityLevel::typeOnly()), + $nativeParamType->describe(VerbosityLevel::typeOnly()), + ))->identifier('parameter.phpDocType'); + if ($phpDocParamType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly()))); + } + + $errors[] = $errorBuilder->build(); + } + } + + if ($tagName === '@param-closure-this') { + $isNonClosure = (new ClosureType())->isSuperTypeOf($nativeParamType)->no(); + if ($isNonClosure) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s is for parameter $%s with non-Closure type %s.', + $tagName, + $parameterName, + $nativeParamType->describe(VerbosityLevel::typeOnly()), + ))->identifier('paramClosureThis.nonClosure')->build(); + } + } + } + } + } + + if ($resolvedPhpDoc->getReturnTag() !== null) { + $phpDocReturnType = $resolvedPhpDoc->getReturnTag()->getType(); + + if ( + $this->unresolvableTypeHelper->containsUnresolvableType($phpDocReturnType) + ) { + $errors[] = RuleErrorBuilder::message('PHPDoc tag @return contains unresolvable type.')->identifier('return.unresolvableType')->build(); + + } else { + $isReturnSuperType = $nativeReturnType->isSuperTypeOf($phpDocReturnType); + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( + $phpDocReturnType, + '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.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @return is in conflict with %s template type %s of %s %s.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @return is redundant, template type %s of %s %s has the same variance.', + )); + if ($isReturnSuperType->no()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @return with type %s is incompatible with native type %s.', + $phpDocReturnType->describe(VerbosityLevel::typeOnly()), + $nativeReturnType->describe(VerbosityLevel::typeOnly()), + ))->identifier('return.phpDocType')->build(); + + } elseif ($isReturnSuperType->maybe()) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @return with type %s is not subtype of native type %s.', + $phpDocReturnType->describe(VerbosityLevel::typeOnly()), + $nativeReturnType->describe(VerbosityLevel::typeOnly()), + ))->identifier('return.phpDocType'); + if ($phpDocReturnType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocReturnType->getName(), $nativeReturnType->describe(VerbosityLevel::typeOnly()))); + } + + $errors[] = $errorBuilder->build(); + } + + $errors = array_merge($errors, $this->genericCallableRuleHelper->check( + $node, + $scope, + '@return', + $phpDocReturnType, + $functionName, + $resolvedPhpDoc->getTemplateTags(), + $scope->isInClass() ? $scope->getClassReflection() : null, + )); + } + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php index 8c8a870b65..47b3a78248 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -5,31 +5,22 @@ 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\TemplateType; use PHPStan\Type\Type; -use PHPStan\Type\VerbosityLevel; -use function array_merge; use function is_string; -use function sprintf; use function trim; /** * @implements Rule */ -class IncompatiblePhpDocTypeRule implements Rule +final class IncompatiblePhpDocTypeRule implements Rule { public function __construct( private FileTypeMapper $fileTypeMapper, - private GenericObjectTypeCheck $genericObjectTypeCheck, - private UnresolvableTypeHelper $unresolvableTypeHelper, + private IncompatiblePhpDocTypeCheck $check, ) { } @@ -41,12 +32,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $docComment = $node->getDocComment(); - if ($docComment === null) { - return []; - } - - $functionName = null; if ($node instanceof Node\Stmt\ClassMethod) { $functionName = $node->name->name; } elseif ($node instanceof Node\Stmt\Function_) { @@ -55,6 +40,11 @@ public function processNode(Node $node, Scope $scope): array return []; } + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $scope->getFile(), $scope->isInClass() ? $scope->getClassReflection()->getName() : null, @@ -62,128 +52,20 @@ public function processNode(Node $node, Scope $scope): array $functionName, $docComment->getText(), ); - $nativeParameterTypes = $this->getNativeParameterTypes($node, $scope); - $nativeReturnType = $this->getNativeReturnType($node, $scope); - - $errors = []; - - foreach ($resolvedPhpDoc->getParamTags() as $parameterName => $phpDocParamTag) { - $phpDocParamType = $phpDocParamTag->getType(); - if (!isset($nativeParameterTypes[$parameterName])) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param references unknown parameter: $%s', - $parameterName, - ))->identifier('phpDoc.unknownParameter')->metadata(['parameterName' => $parameterName])->build(); - - } elseif ( - $this->unresolvableTypeHelper->containsUnresolvableType($phpDocParamType) - ) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s contains unresolvable type.', - $parameterName, - ))->build(); - - } else { - $nativeParamType = $nativeParameterTypes[$parameterName]; - if ( - $phpDocParamTag->isVariadic() - && $phpDocParamType instanceof ArrayType - && !$nativeParamType instanceof ArrayType - ) { - $phpDocParamType = $phpDocParamType->getItemType(); - } - $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 %%s %%s is not generic.', - $escapedParameterName, - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s does not specify all template types of %%s %%s: %%s', - $escapedParameterName, - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s specifies %%d template types, but %%s %%s supports only %%d: %%s', - $escapedParameterName, - ), - sprintf( - 'Type %%s in generic type %%s in PHPDoc tag @param for parameter $%s is not subtype of template type %%s of %%s %%s.', - $escapedParameterName, - ), - )); - - if ($isParamSuperType->no()) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s with type %s is incompatible with native type %s.', - $parameterName, - $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()), - ))->build(); - - } elseif ($isParamSuperType->maybe()) { - $errorBuilder = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s with type %s is not subtype of native type %s.', - $parameterName, - $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()), - )); - 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 = $resolvedPhpDoc->getReturnTag()->getType(); - if ( - $this->unresolvableTypeHelper->containsUnresolvableType($phpDocReturnType) - ) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @return contains unresolvable type.')->build(); - - } else { - $isReturnSuperType = $nativeReturnType->isSuperTypeOf($phpDocReturnType); - $errors = array_merge($errors, $this->genericObjectTypeCheck->check( - $phpDocReturnType, - '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()), - ))->build(); - - } elseif ($isReturnSuperType->maybe()) { - $errorBuilder = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @return with type %s is not subtype of native type %s.', - $phpDocReturnType->describe(VerbosityLevel::typeOnly()), - $nativeReturnType->describe(VerbosityLevel::typeOnly()), - )); - if ($phpDocReturnType instanceof TemplateType) { - $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocReturnType->getName(), $nativeReturnType->describe(VerbosityLevel::typeOnly()))); - } - - $errors[] = $errorBuilder->build(); - } - } - } - - return $errors; + return $this->check->check( + $scope, + $node, + $resolvedPhpDoc, + $functionName, + $this->getNativeParameterTypes($node, $scope), + $this->getByRefParameters($node), + $this->getNativeReturnType($node, $scope), + ); } /** - * @return Type[] + * @return array */ private function getNativeParameterTypes(Node\FunctionLike $node, Scope $scope): array { @@ -203,6 +85,22 @@ private function getNativeParameterTypes(Node\FunctionLike $node, Scope $scope): return $nativeParameterTypes; } + /** + * @return array + */ + private function getByRefParameters(Node\FunctionLike $node): array + { + $nativeParameterTypes = []; + foreach ($node->getParams() as $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + $nativeParameterTypes[$parameter->var->name] = $parameter->byRef; + } + + return $nativeParameterTypes; + } + private function getNativeReturnType(Node\FunctionLike $node, Scope $scope): Type { return $scope->getFunctionType($node->getReturnType(), false, false); diff --git a/src/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRule.php new file mode 100644 index 0000000000..dffebfa1c8 --- /dev/null +++ b/src/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRule.php @@ -0,0 +1,85 @@ + + */ +final class IncompatiblePropertyHookPhpDocTypeRule implements Rule +{ + + public function __construct( + private FileTypeMapper $fileTypeMapper, + private IncompatiblePhpDocTypeCheck $check, + ) + { + } + + public function getNodeType(): string + { + return InPropertyHookNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $hookReflection = $node->getHookReflection(); + + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $node->getClassReflection()->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $hookReflection->getName(), + $docComment->getText(), + ); + + return $this->check->check( + $scope, + $node, + $resolvedPhpDoc, + $hookReflection->getName(), + $this->getNativeParameterTypes($hookReflection), + $this->getByRefParameters($hookReflection), + $hookReflection->getNativeReturnType(), + ); + } + + /** + * @return array + */ + private function getNativeParameterTypes(PhpMethodFromParserNodeReflection $node): array + { + $parameters = []; + foreach ($node->getParameters() as $parameter) { + $parameters[$parameter->getName()] = $parameter->getNativeType(); + } + + return $parameters; + } + + /** + * @return array + */ + private function getByRefParameters(PhpMethodFromParserNodeReflection $node): array + { + $parameters = []; + foreach ($node->getParameters() as $parameter) { + $parameters[$parameter->getName()] = false; + } + + return $parameters; + } + +} diff --git a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php index bc053796de..a202f67bcd 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -9,7 +9,6 @@ use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\VerbosityLevel; use function array_merge; @@ -18,12 +17,13 @@ /** * @implements Rule */ -class IncompatiblePropertyPhpDocTypeRule implements Rule +final class IncompatiblePropertyPhpDocTypeRule implements Rule { public function __construct( private GenericObjectTypeCheck $genericObjectTypeCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericCallableRuleHelper $genericCallableRuleHelper, ) { } @@ -35,23 +35,20 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); + $phpDocType = $node->getPhpDocType(); + if ($phpDocType === null) { + return []; } $propertyName = $node->getName(); - $propertyReflection = $scope->getClassReflection()->getNativeProperty($propertyName); - - if (!$propertyReflection->hasPhpDocType()) { - return []; - } - $phpDocType = $propertyReflection->getPhpDocType(); $description = 'PHPDoc tag @var'; - if ($propertyReflection->isPromoted()) { + if ($node->isPromoted()) { $description = 'PHPDoc type'; } + $classReflection = $node->getClassReflection(); + $messages = []; if ( $this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) @@ -59,43 +56,57 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( '%s for property %s::$%s contains unresolvable type.', $description, - $propertyReflection->getDeclaringClass()->getName(), + $classReflection->getDisplayName(), $propertyName, - ))->build(); + ))->identifier('property.unresolvableType')->build(); } - $nativeType = $propertyReflection->getNativeType(); - $isSuperType = $nativeType->isSuperTypeOf($phpDocType); - if ($isSuperType->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - '%s for property %s::$%s with type %s is incompatible with native type %s.', - $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName, - $phpDocType->describe(VerbosityLevel::typeOnly()), - $nativeType->describe(VerbosityLevel::typeOnly()), - ))->build(); - - } elseif ($isSuperType->maybe()) { - $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()), - )); - - if ($phpDocType instanceof TemplateType) { - $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocType->getName(), $nativeType->describe(VerbosityLevel::typeOnly()))); + $nativeType = $node->getNativeType(); + if ($nativeType !== null) { + $isSuperType = $nativeType->isSuperTypeOf($phpDocType); + if ($isSuperType->no()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s for property %s::$%s with type %s is incompatible with native type %s.', + $description, + $classReflection->getDisplayName(), + $propertyName, + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('property.phpDocType')->build(); + + } elseif ($isSuperType->maybe()) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + '%s for property %s::$%s with type %s is not subtype of native type %s.', + $description, + $classReflection->getDisplayName(), + $propertyName, + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('property.phpDocType'); + + if ($phpDocType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocType->getName(), $nativeType->describe(VerbosityLevel::typeOnly()))); + } + + $messages[] = $errorBuilder->build(); } - - $messages[] = $errorBuilder->build(); } - $className = SprintfHelper::escapeFormatString($propertyReflection->getDeclaringClass()->getDisplayName()); + $className = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); + if ($node->isPromoted() === false) { + $messages = array_merge($messages, $this->genericCallableRuleHelper->check( + $node, + $scope, + '@var', + $phpDocType, + null, + [], + $classReflection, + )); + } + $messages = array_merge($messages, $this->genericObjectTypeCheck->check( $phpDocType, sprintf( @@ -122,6 +133,18 @@ public function processNode(Node $node, Scope $scope): array $className, $escapedPropertyName, ), + sprintf( + 'Call-site variance of %%s in generic type %%s in %s for property %s::$%s is in conflict with %%s template type %%s of %%s %%s.', + $description, + $className, + $escapedPropertyName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in %s for property %s::$%s is redundant, template type %%s of %%s %%s has the same variance.', + $description, + $className, + $escapedPropertyName, + ), )); return $messages; diff --git a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php new file mode 100644 index 0000000000..f7907395ad --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php @@ -0,0 +1,103 @@ + + */ +final class IncompatibleSelfOutTypeRule implements Rule +{ + + public function __construct( + private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericObjectTypeCheck $genericObjectTypeCheck, + ) + { + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + $selfOutType = $method->getSelfOutType(); + + if ($selfOutType === null) { + return []; + } + + $classReflection = $method->getDeclaringClass(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); + + $errors = []; + if (!$classType->isSuperTypeOf($selfOutType)->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Self-out type %s of method %s::%s is not subtype of %s.', + $selfOutType->describe(VerbosityLevel::precise()), + $classReflection->getDisplayName(), + $method->getName(), + $classType->describe(VerbosityLevel::precise()), + ))->identifier('selfOut.type')->build(); + } + + if ($method->isStatic()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-self-out is not supported above static method %s::%s().', $classReflection->getName(), $method->getName())) + ->identifier('selfOut.static') + ->build(); + } + + if ($this->unresolvableTypeHelper->containsUnresolvableType($selfOutType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @phpstan-self-out for method %s::%s() contains unresolvable type.', + $classReflection->getDisplayName(), + $method->getName(), + ))->identifier('selfOut.unresolvableType')->build(); + } + + $escapedTagName = SprintfHelper::escapeFormatString('@phpstan-self-out'); + + return array_merge($errors, $this->genericObjectTypeCheck->check( + $selfOutType, + sprintf( + 'PHPDoc tag %s contains generic type %%s but %%s %%s is not generic.', + $escapedTagName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag %s does not specify all template types of %%s %%s: %%s', + $escapedTagName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag %s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $escapedTagName, + ), + sprintf( + 'Type %%s in generic type %%s in PHPDoc tag %s is not subtype of template type %%s of %%s %%s.', + $escapedTagName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag %s is in conflict with %%s template type %%s of %%s %%s.', + $escapedTagName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag %s is redundant, template type %%s of %%s %%s has the same variance.', + $escapedTagName, + ), + )); + } + +} diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index 2b3d22b15c..c9e27ca74a 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\PhpDoc; use PhpParser\Node; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Node\VirtualNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; @@ -11,58 +13,80 @@ use PHPStan\Rules\RuleErrorBuilder; use function in_array; use function sprintf; -use function strpos; +use function str_starts_with; /** - * @implements Rule + * @implements Rule */ -class InvalidPHPStanDocTagRule implements Rule +final class InvalidPHPStanDocTagRule implements Rule { private const POSSIBLE_PHPSTAN_TAGS = [ '@phpstan-param', + '@phpstan-param-out', '@phpstan-var', - '@phpstan-template', '@phpstan-extends', '@phpstan-implements', '@phpstan-use', '@phpstan-template', + '@phpstan-template-contravariant', '@phpstan-template-covariant', '@phpstan-return', '@phpstan-throws', + '@phpstan-ignore', '@phpstan-ignore-next-line', '@phpstan-ignore-line', '@phpstan-method', '@phpstan-pure', '@phpstan-impure', + '@phpstan-immutable', '@phpstan-type', '@phpstan-import-type', '@phpstan-property', '@phpstan-property-read', '@phpstan-property-write', + '@phpstan-consistent-constructor', + '@phpstan-assert', + '@phpstan-assert-if-true', + '@phpstan-assert-if-false', + '@phpstan-self-out', + '@phpstan-this-out', + '@phpstan-allow-private-mutation', + '@phpstan-readonly', + '@phpstan-readonly-allow-private-mutation', + '@phpstan-require-extends', + '@phpstan-require-implements', + '@phpstan-param-immediately-invoked-callable', + '@phpstan-param-later-invoked-callable', + '@phpstan-param-closure-this', ]; - public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) + public function __construct( + private Lexer $phpDocLexer, + private PhpDocParser $phpDocParser, + ) { } public function getNodeType(): string { - return Node::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array { - if ( - !$node instanceof Node\Stmt\ClassLike - && !$node instanceof Node\FunctionLike - && !$node instanceof Node\Stmt\Foreach_ - && !$node instanceof Node\Stmt\Property - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - ) { + // mirrored with InvalidPhpDocTagValueRule + if ($node instanceof VirtualNode) { + return []; + } + if (!$node instanceof Node\Stmt && !$node instanceof Node\PropertyHook) { return []; } + if ($node instanceof Node\Stmt\Expression) { + if (!$node->expr instanceof Node\Expr\Assign && !$node->expr instanceof Node\Expr\AssignRef) { + return []; + } + } $docComment = $node->getDocComment(); if ($docComment === null) { @@ -74,7 +98,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($phpDocNode->getTags() as $phpDocTag) { - if (strpos($phpDocTag->name, '@phpstan-') !== 0 + if (!str_starts_with($phpDocTag->name, '@phpstan-') || in_array($phpDocTag->name, self::POSSIBLE_PHPSTAN_TAGS, true) ) { continue; @@ -83,7 +107,9 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Unknown PHPDoc tag: %s', $phpDocTag->name, - ))->build(); + )) + ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) + ->identifier('phpDoc.phpstanTag')->build(); } return $errors; diff --git a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php index bcf90bec7e..5e99af64f5 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -3,44 +3,52 @@ namespace PHPStan\Rules\PhpDoc; use PhpParser\Node; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Node\VirtualNode; use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; -use function strpos; +use function str_starts_with; /** - * @implements Rule + * @implements Rule */ -class InvalidPhpDocTagValueRule implements Rule +final class InvalidPhpDocTagValueRule implements Rule { - public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) + public function __construct( + private Lexer $phpDocLexer, + private PhpDocParser $phpDocParser, + ) { } public function getNodeType(): string { - return Node::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array { - if ( - !$node instanceof Node\Stmt\ClassLike - && !$node instanceof Node\FunctionLike - && !$node instanceof Node\Stmt\Foreach_ - && !$node instanceof Node\Stmt\Property - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - && !$node instanceof Node\Stmt\ClassConst - ) { + // mirrored with InvalidPHPStanDocTagRule + if ($node instanceof VirtualNode) { return []; } + if (!$node instanceof Node\Stmt && !$node instanceof Node\PropertyHook) { + return []; + } + if ($node instanceof Node\Stmt\Expression) { + if (!$node->expr instanceof Node\Expr\Assign && !$node->expr instanceof Node\Expr\AssignRef) { + return []; + } + } $docComment = $node->getDocComment(); if ($docComment === null) { @@ -53,11 +61,26 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($phpDocNode->getTags() as $phpDocTag) { - if (!($phpDocTag->value instanceof InvalidTagValueNode)) { + if (str_starts_with($phpDocTag->name, '@phan-') || str_starts_with($phpDocTag->name, '@psalm-')) { continue; } - if (strpos($phpDocTag->name, '@psalm-') === 0) { + if ($phpDocTag->value instanceof TypeAliasTagValueNode) { + if (!$phpDocTag->value->type instanceof InvalidTypeNode) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s %s has invalid value: %s', + $phpDocTag->name, + $phpDocTag->value->alias, + $phpDocTag->value->type->getException()->getMessage(), + )) + ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) + ->identifier('phpDoc.parseError')->build(); + + continue; + } elseif (!($phpDocTag->value instanceof InvalidTagValueNode)) { continue; } @@ -66,7 +89,9 @@ public function processNode(Node $node, Scope $scope): array $phpDocTag->name, $phpDocTag->value->value, $phpDocTag->value->exception->getMessage(), - ))->build(); + )) + ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) + ->identifier('phpDoc.parseError')->build(); } return $errors; diff --git a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php index ea97fa34ef..7dc718889e 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -6,8 +6,9 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; @@ -16,25 +17,25 @@ use PHPStan\Type\VerbosityLevel; use function array_map; use function array_merge; -use function implode; use function is_string; use function sprintf; /** * @implements Rule */ -class InvalidPhpDocVarTagTypeRule implements Rule +final class InvalidPhpDocVarTagTypeRule implements Rule { public function __construct( private FileTypeMapper $fileTypeMapper, private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private GenericObjectTypeCheck $genericObjectTypeCheck, private MissingTypehintCheck $missingTypehintCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private bool $checkClassCaseSensitivity, private bool $checkMissingVarTagTypehint, + private bool $discoveringSymbolsTip, ) { } @@ -48,7 +49,6 @@ 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_ ) { @@ -79,7 +79,10 @@ public function processNode(Node $node, Scope $scope): array if ( $this->unresolvableTypeHelper->containsUnresolvableType($varTagType) ) { - $errors[] = RuleErrorBuilder::message(sprintf('%s contains unresolvable type.', $identifier))->line($docComment->getStartLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s contains unresolvable type.', $identifier)) + ->line($docComment->getStartLine()) + ->identifier('varTag.unresolvableType') + ->build(); continue; } @@ -90,7 +93,21 @@ public function processNode(Node $node, Scope $scope): array '%s has no value type specified in iterable type %s.', $identifier, $iterableTypeDescription, - ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($varTagType) as [$innerName, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s contains generic %s but does not specify its types: %s', + $identifier, + $innerName, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } } @@ -101,17 +118,10 @@ public function processNode(Node $node, Scope $scope): array sprintf('Generic type %%s in %s does not specify all template types of %%s %%s: %%s', $escapedIdentifier), sprintf('Generic type %%s in %s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedIdentifier), sprintf('Type %%s in generic type %%s in %s is not subtype of template type %%s of %%s %%s.', $escapedIdentifier), + sprintf('Call-site variance of %%s in generic type %%s in %s is in conflict with %%s template type %%s of %%s %%s.', $escapedIdentifier), + sprintf('Call-site variance of %%s in generic type %%s in %s is redundant, template type %%s of %%s %%s has the same variance.', $escapedIdentifier), )); - foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($varTagType) as [$innerName, $genericTypeNames]) { - $errors[] = RuleErrorBuilder::message(sprintf( - '%s contains generic %s but does not specify its types: %s', - $identifier, - $innerName, - implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); - } - $referencedClasses = $varTagType->getReferencedClasses(); foreach ($referencedClasses as $referencedClass) { if ($this->reflectionProvider->hasClass($referencedClass)) { @@ -119,24 +129,36 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( sprintf('%s has invalid type %%s.', $identifier), $referencedClass, - ))->build(); + ))->identifier('varTag.trait')->build(); } continue; } - $errors[] = RuleErrorBuilder::message(sprintf( + if ($scope->isInClassExists($referencedClass)) { + continue; + } + + $errorBuilder = RuleErrorBuilder::message(sprintf( sprintf('%s contains unknown class %%s.', $identifier), $referencedClass, - ))->discoveringSymbolsTip()->build(); - } + )) + ->identifier('class.notFound'); - if (!$this->checkClassCaseSensitivity) { - continue; + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses)), + $this->classCheck->checkClassNames( + $scope, + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses), + ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_VAR), + $this->checkClassCaseSensitivity, + ), ); } diff --git a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php index f3d40be686..33a2e120c3 100644 --- a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php +++ b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php @@ -3,20 +3,24 @@ namespace PHPStan\Rules\PhpDoc; use PhpParser\Node; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Node\InPropertyHookNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use Throwable; use function sprintf; /** - * @implements Rule + * @implements Rule */ -class InvalidThrowsPhpDocValueRule implements Rule +final class InvalidThrowsPhpDocValueRule implements Rule { public function __construct(private FileTypeMapper $fileTypeMapper) @@ -25,18 +29,22 @@ public function __construct(private FileTypeMapper $fileTypeMapper) public function getNodeType(): string { - return Node\Stmt::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array { - $docComment = $node->getDocComment(); - if ($docComment === null) { + if ($node instanceof Node\Stmt) { + if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { + return []; // is handled by virtual nodes + } + } elseif (!$node instanceof InPropertyHookNode) { return []; } - if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { - return []; // is handled by virtual nodes + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; } $functionName = null; @@ -57,12 +65,11 @@ public function processNode(Node $node, Scope $scope): array } $phpDocThrowsType = $resolvedPhpDoc->getThrowsTag()->getType(); - if ((new VoidType())->isSuperTypeOf($phpDocThrowsType)->yes()) { + if ($phpDocThrowsType->isVoid()->yes()) { return []; } - $isThrowsSuperType = (new ObjectType(Throwable::class))->isSuperTypeOf($phpDocThrowsType); - if ($isThrowsSuperType->yes()) { + if ($this->isThrowsValid($phpDocThrowsType)) { return []; } @@ -70,8 +77,36 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'PHPDoc tag @throws with type %s is not subtype of Throwable', $phpDocThrowsType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('throws.notThrowable')->build(), ]; } + private function isThrowsValid(Type $phpDocThrowsType): bool + { + $throwType = new ObjectType(Throwable::class); + if ($phpDocThrowsType instanceof UnionType) { + foreach ($phpDocThrowsType->getTypes() as $innerType) { + if (!$this->isThrowsValid($innerType)) { + return false; + } + } + + return true; + } + + $toIntersectWith = []; + foreach ($phpDocThrowsType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->isInterface()) { + continue; + } + foreach ($classReflection->getRequireExtendsTags() as $requireExtendsTag) { + $toIntersectWith[] = $requireExtendsTag->getType(); + } + } + + return $throwType->isSuperTypeOf( + TypeCombinator::intersect($phpDocThrowsType, ...$toIntersectWith), + )->yes(); + } + } diff --git a/src/Rules/PhpDoc/MethodAssertRule.php b/src/Rules/PhpDoc/MethodAssertRule.php new file mode 100644 index 0000000000..47279e6e4e --- /dev/null +++ b/src/Rules/PhpDoc/MethodAssertRule.php @@ -0,0 +1,37 @@ + + */ +final class MethodAssertRule implements Rule +{ + + public function __construct(private AssertRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + $variants = $method->getVariants(); + if (count($variants) !== 1) { + return []; + } + + return $this->helper->check($scope, $node->getOriginalNode(), $method, $variants[0]); + } + +} diff --git a/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php b/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php new file mode 100644 index 0000000000..56746b7f7b --- /dev/null +++ b/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php @@ -0,0 +1,37 @@ + + */ +final class MethodConditionalReturnTypeRule implements Rule +{ + + public function __construct(private ConditionalReturnTypeRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + $variants = $method->getVariants(); + if (count($variants) !== 1) { + return []; + } + + return $this->helper->check($variants[0]); + } + +} diff --git a/src/Rules/PhpDoc/PhpDocLineHelper.php b/src/Rules/PhpDoc/PhpDocLineHelper.php new file mode 100644 index 0000000000..a7894f762f --- /dev/null +++ b/src/Rules/PhpDoc/PhpDocLineHelper.php @@ -0,0 +1,28 @@ +getAttribute('startLine'); + $phpDoc = $node->getDocComment(); + + if ($phpDocTagLine === null || $phpDoc === null) { + return $node->getStartLine(); + } + + return $phpDoc->getStartLine() + $phpDocTagLine - 1; + } + +} diff --git a/src/Rules/PhpDoc/RequireExtendsCheck.php b/src/Rules/PhpDoc/RequireExtendsCheck.php new file mode 100644 index 0000000000..233082b3a9 --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsCheck.php @@ -0,0 +1,90 @@ + $extendsTags + * @return list + */ + public function checkExtendsTags(Scope $scope, Node $node, array $extendsTags): array + { + $errors = []; + + if (count($extendsTags) > 1) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends can only be used once.')) + ->identifier('requireExtends.duplicate') + ->build(); + } + + foreach ($extendsTags as $extendsTag) { + $type = $extendsTag->getType(); + if (!$type instanceof ObjectType) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('requireExtends.nonObject') + ->build(); + continue; + } + + $class = $type->getClassName(); + $referencedClassReflection = $type->getClassReflection(); + + if ($referencedClassReflection === null) { + $errorBuilder = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains unknown class %s.', $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + continue; + } + + if (!$referencedClassReflection->isClass()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain non-class type %s.', $class)) + ->identifier(sprintf('requireExtends.%s', strtolower($referencedClassReflection->getClassTypeDescription()))) + ->build(); + } elseif ($referencedClassReflection->isFinal()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain final class %s.', $class)) + ->identifier('requireExtends.finalClass') + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_REQUIRE_EXTENDS), $this->checkClassCaseSensitivity), + ); + } + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php new file mode 100644 index 0000000000..7b5779fa9f --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php @@ -0,0 +1,50 @@ + + */ +final class RequireExtendsDefinitionClassRule implements Rule +{ + + public function __construct( + private RequireExtendsCheck $requireExtendsCheck, + ) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $extendsTags = $classReflection->getRequireExtendsTags(); + + if (count($extendsTags) === 0) { + return []; + } + + if (!$classReflection->isInterface()) { + return [ + RuleErrorBuilder::message('PHPDoc tag @phpstan-require-extends is only valid on trait or interface.') + ->identifier(sprintf('requireExtends.on%s', $classReflection->getClassTypeDescription())) + ->build(), + ]; + } + + return $this->requireExtendsCheck->checkExtendsTags($scope, $node, $extendsTags); + } + +} diff --git a/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php b/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php new file mode 100644 index 0000000000..468e0a709f --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php @@ -0,0 +1,43 @@ + + */ +final class RequireExtendsDefinitionTraitRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private RequireExtendsCheck $requireExtendsCheck, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ( + $node->namespacedName === null + || !$this->reflectionProvider->hasClass($node->namespacedName->toString()) + ) { + return []; + } + + $traitReflection = $this->reflectionProvider->getClass($node->namespacedName->toString()); + $extendsTags = $traitReflection->getRequireExtendsTags(); + + return $this->requireExtendsCheck->checkExtendsTags($scope, $node, $extendsTags); + } + +} diff --git a/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php new file mode 100644 index 0000000000..9a33044401 --- /dev/null +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php @@ -0,0 +1,40 @@ + + */ +final class RequireImplementsDefinitionClassRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $implementsTags = $classReflection->getRequireImplementsTags(); + + if (count($implementsTags) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message('PHPDoc tag @phpstan-require-implements is only valid on trait.') + ->identifier(sprintf('requireImplements.on%s', $classReflection->getClassTypeDescription())) + ->build(), + ]; + } + +} diff --git a/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php new file mode 100644 index 0000000000..616ce79827 --- /dev/null +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php @@ -0,0 +1,92 @@ + + */ +final class RequireImplementsDefinitionTraitRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private bool $checkClassCaseSensitivity, + private bool $discoveringSymbolsTip, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ( + $node->namespacedName === null + || !$this->reflectionProvider->hasClass($node->namespacedName->toString()) + ) { + return []; + } + + $traitReflection = $this->reflectionProvider->getClass($node->namespacedName->toString()); + $implementsTags = $traitReflection->getRequireImplementsTags(); + + $errors = []; + foreach ($implementsTags as $implementsTag) { + $type = $implementsTag->getType(); + if (!$type instanceof ObjectType) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('requireImplements.nonObject') + ->build(); + continue; + } + + $class = $type->getClassName(); + $referencedClassReflection = $type->getClassReflection(); + if ($referencedClassReflection === null) { + $errorBuilder = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements contains unknown class %s.', $class)) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + continue; + } + + if (!$referencedClassReflection->isInterface()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements cannot contain non-interface type %s.', $class)) + ->identifier(sprintf('requireImplements.%s', strtolower($referencedClassReflection->getClassTypeDescription()))) + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames($scope, [ + new ClassNameNodePair($class, $node), + ], ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_REQUIRE_IMPLEMENTS), $this->checkClassCaseSensitivity), + ); + } + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/UnresolvableTypeHelper.php b/src/Rules/PhpDoc/UnresolvableTypeHelper.php index 8b53af21d5..25b485dfad 100644 --- a/src/Rules/PhpDoc/UnresolvableTypeHelper.php +++ b/src/Rules/PhpDoc/UnresolvableTypeHelper.php @@ -7,7 +7,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -class UnresolvableTypeHelper +final class UnresolvableTypeHelper { public function containsUnresolvableType(Type $type): bool diff --git a/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php b/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php new file mode 100644 index 0000000000..2ce5e70cce --- /dev/null +++ b/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php @@ -0,0 +1,30 @@ + + */ +final class VarTagChangedExpressionTypeRule implements Rule +{ + + public function __construct(private VarTagTypeRuleHelper $varTagTypeRuleHelper) + { + } + + public function getNodeType(): string + { + return VarTagChangedExpressionTypeNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->varTagTypeRuleHelper->checkExprType($scope, $node->getExpr(), $node->getVarTag()->getType()); + } + +} diff --git a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php new file mode 100644 index 0000000000..4ad442ac42 --- /dev/null +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -0,0 +1,253 @@ + + */ + public function checkVarType(Scope $scope, Node\Expr $var, Node\Expr $expr, array $varTags, array $assignedVariables): array + { + $errors = []; + + if ($var instanceof Expr\Variable && is_string($var->name)) { + if (array_key_exists($var->name, $varTags)) { + $varTagType = $varTags[$var->name]->getType(); + } elseif (count($assignedVariables) === 1 && array_key_exists(0, $varTags)) { + $varTagType = $varTags[0]->getType(); + } else { + return []; + } + + return $this->checkExprType($scope, $expr, $varTagType); + } elseif ($var instanceof Expr\List_ || $var instanceof Expr\Array_) { + foreach ($var->items as $i => $arrayItem) { + if ($arrayItem === null) { + continue; + } + if ($arrayItem->key === null) { + $dimExpr = new Node\Scalar\Int_($i); + } else { + $dimExpr = $arrayItem->key; + } + + $itemErrors = $this->checkVarType($scope, $arrayItem->value, new GetOffsetValueTypeExpr($expr, $dimExpr), $varTags, $assignedVariables); + foreach ($itemErrors as $error) { + $errors[] = $error; + } + } + } + + return $errors; + } + + /** + * @return list + */ + public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): array + { + $errors = []; + $exprNativeType = $scope->getNativeType($expr); + $containsPhpStanType = $this->containsPhpStanType($varTagType); + if ($this->shouldVarTagTypeBeReported($scope, $expr, $exprNativeType, $varTagType)) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($exprNativeType, $varTagType); + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var with type %s is not subtype of native type %s.', + $varTagType->describe($verbosity), + $exprNativeType->describe($verbosity), + ))->identifier('varTag.nativeType')->build(); + } else { + $exprType = $scope->getType($expr); + if ( + $this->shouldVarTagTypeBeReported($scope, $expr, $exprType, $varTagType) + && ($this->checkTypeAgainstPhpDocType || $containsPhpStanType) + ) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($exprType, $varTagType); + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var with type %s is not subtype of type %s.', + $varTagType->describe($verbosity), + $exprType->describe($verbosity), + ))->identifier('varTag.type')->build(); + } + } + + if (count($errors) === 0 && $containsPhpStanType) { + $exprType = $scope->getType($expr); + if (!$exprType->equals($varTagType)) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($exprType, $varTagType); + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var assumes the expression with type %s is always %s but it\'s error-prone and dangerous.', + $exprType->describe($verbosity), + $varTagType->describe($verbosity), + ))->identifier('phpstanApi.varTagAssumption')->build(); + } + } + + return $errors; + } + + private function containsPhpStanType(Type $type): bool + { + $classReflections = TypeUtils::toBenevolentUnion($type)->getObjectClassReflections(); + if (!$this->reflectionProvider->hasClass(Type::class)) { + return false; + } + + $typeClass = $this->reflectionProvider->getClass(Type::class); + foreach ($classReflections as $classReflection) { + if (!$classReflection->isSubclassOfClass($typeClass)) { + continue; + } + + return true; + } + + return false; + } + + private function shouldVarTagTypeBeReported(Scope $scope, Node\Expr $expr, Type $type, Type $varTagType): bool + { + if ($expr instanceof Expr\Array_) { + if ($expr->items === []) { + $type = new ArrayType(new MixedType(), new MixedType()); + } + + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); + } + + if ($expr instanceof Expr\ConstFetch) { + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); + } + + if ($expr instanceof Node\Scalar) { + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); + } + + if ($expr instanceof Expr\New_) { + if ($type instanceof GenericObjectType) { + $type = new ObjectType($type->getClassName()); + } + } + + return $this->checkType($scope, $type, $varTagType); + } + + private function checkType(Scope $scope, Type $type, Type $varTagType, int $depth = 0): bool + { + if ($this->strictWideningCheck) { + return !$this->isSuperTypeOfVarType($scope, $type, $varTagType); + } + + if ($type->isConstantArray()->yes()) { + if ($type->isIterableAtLeastOnce()->no()) { + $type = new ArrayType(new MixedType(), new MixedType()); + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); + } + } + + if ($type->isIterable()->yes() && $varTagType->isIterable()->yes()) { + if (!$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType)) { + return true; + } + + $innerType = $type->getIterableValueType(); + $innerVarTagType = $varTagType->getIterableValueType(); + + if ($type->equals($innerType) || $varTagType->equals($innerVarTagType)) { + return !$this->isSuperTypeOfVarType($scope, $innerType, $innerVarTagType); + } + + return $this->checkType($scope, $innerType, $innerVarTagType, $depth + 1); + } + + if ($depth === 0 && $type->isConstantValue()->yes()) { + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); + } + + return !$this->isSuperTypeOfVarType($scope, $type, $varTagType); + } + + private function isSuperTypeOfVarType(Scope $scope, Type $type, Type $varTagType): bool + { + if ($type->isSuperTypeOf($varTagType)->yes()) { + return true; + } + + try { + $type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), $this->createNameScope($scope)); + } catch (NameScopeAlreadyBeingCreatedException) { + return true; + } + + return $type->isSuperTypeOf($varTagType)->yes(); + } + + private function isAtLeastMaybeSuperTypeOfVarType(Scope $scope, Type $type, Type $varTagType): bool + { + if (!$type->isSuperTypeOf($varTagType)->no()) { + return true; + } + + try { + $type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), $this->createNameScope($scope)); + } catch (NameScopeAlreadyBeingCreatedException) { + return true; + } + + return !$type->isSuperTypeOf($varTagType)->no(); + } + + /** + * @throws NameScopeAlreadyBeingCreatedException + */ + private function createNameScope(Scope $scope): NameScope + { + $function = $scope->getFunction(); + + return $this->fileTypeMapper->getNameScope( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + )->withoutNamespaceAndUses(); + } + +} diff --git a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php index c1f3dac121..72e61ffdb6 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -6,13 +6,15 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\GetIterableKeyTypeExpr; +use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\InClassMethodNode; use PHPStan\Node\InClassNode; use PHPStan\Node\InFunctionNode; use PHPStan\Node\VirtualNode; use PHPStan\PhpDoc\Tag\VarTag; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; @@ -29,11 +31,12 @@ /** * @implements Rule */ -class WrongVariableNameInVarTagRule implements Rule +final class WrongVariableNameInVarTagRule implements Rule { public function __construct( private FileTypeMapper $fileTypeMapper, + private VarTagTypeRuleHelper $varTagTypeRuleHelper, ) { } @@ -47,7 +50,6 @@ 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_ || ($node instanceof VirtualNode && !$node instanceof InFunctionNode && !$node instanceof InClassMethodNode && !$node instanceof InClassNode) @@ -78,18 +80,21 @@ public function processNode(Node $node, Scope $scope): array } if ($node instanceof Node\Stmt\Foreach_) { - return $this->processForeach($node->expr, $node->keyVar, $node->valueVar, $varTags); + return $this->processForeach($scope, $node->expr, $node->keyVar, $node->valueVar, $varTags); } if ($node instanceof Node\Stmt\Static_) { - return $this->processStatic($node->vars, $varTags); + return $this->processStatic($scope, $node->vars, $varTags); } if ($node instanceof Node\Stmt\Expression) { + if ($node->expr instanceof Expr\Throw_) { + return $this->processStmt($scope, $varTags, $node->expr); + } return $this->processExpression($scope, $node->expr, $varTags); } - if ($node instanceof Node\Stmt\Throw_ || $node instanceof Node\Stmt\Return_) { + if ($node instanceof Node\Stmt\Return_) { return $this->processStmt($scope, $varTags, $node->expr); } @@ -116,7 +121,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'PHPDoc tag @var above %s has no effect.', $description, - ))->build(), + ))->identifier('varTag.misplaced')->build(), ]; } @@ -125,9 +130,9 @@ public function processNode(Node $node, Scope $scope): array /** * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ - private function processAssign(Scope $scope, Node\Expr $var, array $varTags): array + private function processAssign(Scope $scope, Node\Expr $var, Node\Expr $expr, array $varTags): array { $errors = []; $hasMultipleMessage = false; @@ -136,13 +141,15 @@ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): ar if (is_int($key)) { if (count($varTags) !== 1) { if (!$hasMultipleMessage) { - $errors[] = RuleErrorBuilder::message('Multiple PHPDoc @var tags above single variable assignment are not supported.')->build(); + $errors[] = RuleErrorBuilder::message('Multiple PHPDoc @var tags above single variable assignment are not supported.') + ->identifier('varTag.multipleTags') + ->build(); $hasMultipleMessage = true; } } elseif (count($assignedVariables) !== 1) { $errors[] = RuleErrorBuilder::message( 'PHPDoc tag @var above assignment does not specify variable name.', - )->build(); + )->identifier('varTag.noVariable')->build(); } continue; } @@ -160,9 +167,17 @@ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): ar 'Variable $%s in PHPDoc tag @var does not match assigned variable $%s.', $key, $assignedVariables[0], - ))->build(); + ))->identifier('varTag.differentVariable')->build(); } else { - $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $key))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $key)) + ->identifier('varTag.variableNotFound') + ->build(); + } + } + + if (count($errors) === 0) { + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var, $expr, $varTags, $assignedVariables) as $error) { + $errors[] = $error; } } @@ -182,7 +197,7 @@ private function getAssignedVariables(Expr $expr): array return []; } - if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) { + if ($expr instanceof Expr\List_) { $names = []; foreach ($expr->items as $item) { if ($item === null) { @@ -200,9 +215,9 @@ private function getAssignedVariables(Expr $expr): array /** * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ - private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Node\Expr $valueVar, array $varTags): array + private function processForeach(Scope $scope, Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Node\Expr $valueVar, array $varTags): array { $variableNames = []; if ($iterateeExpr instanceof Node\Expr\Variable && is_string($iterateeExpr->name)) { @@ -221,7 +236,7 @@ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Nod } $errors[] = RuleErrorBuilder::message( 'PHPDoc tag @var above foreach loop does not specify variable name.', - )->build(); + )->identifier('varTag.noVariable')->build(); continue; } @@ -233,18 +248,43 @@ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Nod 'Variable $%s in PHPDoc tag @var does not match any variable in the foreach loop: %s', $name, implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), $variableNames)), - ))->build(); + ))->identifier('varTag.differentVariable')->build(); + } + + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $iterateeExpr, $iterateeExpr, $varTags, $variableNames) as $error) { + $errors[] = $error; + } + if ($keyVar !== null) { + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $keyVar, new GetIterableKeyTypeExpr($iterateeExpr), $varTags, $variableNames) as $error) { + $errors[] = $error; + } + } + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $valueVar, new GetIterableValueTypeExpr($iterateeExpr), $varTags, $variableNames) as $error) { + $errors[] = $error; } return $errors; } + /** + * @param VarTag[] $varTags + * @return list + */ + private function processExpression(Scope $scope, Expr $expr, array $varTags): array + { + if ($expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignRef) { + return $this->processAssign($scope, $expr->var, $expr->expr, $varTags); + } + + return $this->processStmt($scope, $varTags, null); + } + /** * @param Node\Stmt\StaticVar[] $vars * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ - private function processStatic(array $vars, array $varTags): array + private function processStatic(Scope $scope, array $vars, array $varTags): array { $variableNames = []; foreach ($vars as $var) { @@ -252,7 +292,7 @@ private function processStatic(array $vars, array $varTags): array continue; } - $variableNames[$var->var->name] = true; + $variableNames[] = $var->var->name; } $errors = []; @@ -264,40 +304,36 @@ private function processStatic(array $vars, array $varTags): array $errors[] = RuleErrorBuilder::message( 'PHPDoc tag @var above multiple static variables does not specify variable name.', - )->build(); + )->identifier('varTag.noVariable')->build(); continue; } - if (isset($variableNames[$name])) { + if (in_array($name, $variableNames, true)) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( 'Variable $%s in PHPDoc tag @var does not match any static variable: %s', $name, - implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), array_keys($variableNames))), - ))->build(); + implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), $variableNames)), + ))->identifier('varTag.differentVariable')->build(); } - return $errors; - } - - /** - * @param VarTag[] $varTags - * @return RuleError[] - */ - private function processExpression(Scope $scope, Expr $expr, array $varTags): array - { - if ($expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignRef) { - return $this->processAssign($scope, $expr->var, $varTags); + foreach ($vars as $var) { + if ($var->default === null) { + continue; + } + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var->var, $var->default, $varTags, $variableNames) as $error) { + $errors[] = $error; + } } - return $this->processStmt($scope, $varTags, null); + return $errors; } /** * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): array { @@ -314,12 +350,16 @@ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $name))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $name)) + ->identifier('varTag.variableNotFound') + ->build(); } if (count($variableLessVarTags) !== 1 || $defaultExpr === null) { if (count($variableLessVarTags) > 0) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @var does not specify variable name.')->build(); + $errors[] = RuleErrorBuilder::message('PHPDoc tag @var does not specify variable name.') + ->identifier('varTag.noVariable') + ->build(); } } @@ -328,7 +368,7 @@ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): /** * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $varTags): array { @@ -353,7 +393,7 @@ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $var $errors[] = RuleErrorBuilder::message( 'PHPDoc tag @var above multiple global variables does not specify variable name.', - )->build(); + )->identifier('varTag.noVariable')->build(); continue; } @@ -365,7 +405,7 @@ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $var 'Variable $%s in PHPDoc tag @var does not match any global variable: %s', $name, implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), array_keys($variableNames))), - ))->build(); + ))->identifier('varTag.differentVariable')->build(); } return $errors; diff --git a/src/Rules/Playground/FunctionNeverRule.php b/src/Rules/Playground/FunctionNeverRule.php new file mode 100644 index 0000000000..5bb4714521 --- /dev/null +++ b/src/Rules/Playground/FunctionNeverRule.php @@ -0,0 +1,51 @@ + + */ +final class FunctionNeverRule implements Rule +{ + + public function __construct(private NeverRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (count($node->getReturnStatements()) > 0) { + return []; + } + + $function = $node->getFunctionReflection(); + + $returnType = $function->getReturnType(); + $helperResult = $this->helper->shouldReturnNever($node, $returnType); + if ($helperResult === false) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Function %s() always %s, it should have return type "never".', + $function->getName(), + count($helperResult) === 0 ? 'throws an exception' : 'terminates script execution', + ))->identifier('phpstanPlayground.never')->build(), + ]; + } + +} diff --git a/src/Rules/Playground/MethodNeverRule.php b/src/Rules/Playground/MethodNeverRule.php new file mode 100644 index 0000000000..fdef4e9396 --- /dev/null +++ b/src/Rules/Playground/MethodNeverRule.php @@ -0,0 +1,52 @@ + + */ +final class MethodNeverRule implements Rule +{ + + public function __construct(private NeverRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (count($node->getReturnStatements()) > 0) { + return []; + } + + $method = $node->getMethodReflection(); + + $returnType = $method->getReturnType(); + $helperResult = $this->helper->shouldReturnNever($node, $returnType); + if ($helperResult === false) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Method %s::%s() always %s, it should have return type "never".', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + count($helperResult) === 0 ? 'throws an exception' : 'terminates script execution', + ))->identifier('phpstanPlayground.never')->build(), + ]; + } + +} diff --git a/src/Rules/Playground/NeverRuleHelper.php b/src/Rules/Playground/NeverRuleHelper.php new file mode 100644 index 0000000000..9865d3d1ce --- /dev/null +++ b/src/Rules/Playground/NeverRuleHelper.php @@ -0,0 +1,49 @@ +|false + */ + public function shouldReturnNever(ReturnStatementsNode $node, Type $returnType): array|false + { + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + return false; + } + + if ($node->isGenerator()) { + return false; + } + + $other = []; + foreach ($node->getExecutionEnds() as $executionEnd) { + if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { + $executionEndNode = $executionEnd->getNode(); + if (!$executionEndNode instanceof Node\Stmt\Expression) { + $other[] = $executionEnd->getNode(); + continue; + } + + if ($executionEndNode->expr instanceof Node\Expr\Throw_) { + continue; + } + + $other[] = $executionEnd->getNode(); + continue; + } + + return false; + } + + return $other; + } + +} diff --git a/src/Rules/Playground/NoPhpCodeRule.php b/src/Rules/Playground/NoPhpCodeRule.php new file mode 100644 index 0000000000..c8d0646083 --- /dev/null +++ b/src/Rules/Playground/NoPhpCodeRule.php @@ -0,0 +1,41 @@ + + */ +final class NoPhpCodeRule implements Rule +{ + + public function getNodeType(): string + { + return FileNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (count($node->getNodes()) !== 1) { + return []; + } + + $html = $node->getNodes()[0]; + if (!$html instanceof Node\Stmt\InlineHTML) { + return []; + } + + return [ + RuleErrorBuilder::message('The example does not contain any PHP code. Did you forget the opening identifier('phpstanPlayground.noPhp') + ->build(), + ]; + } + +} diff --git a/src/Rules/Playground/NotAnalysedTraitRule.php b/src/Rules/Playground/NotAnalysedTraitRule.php new file mode 100644 index 0000000000..c9ba6e0a24 --- /dev/null +++ b/src/Rules/Playground/NotAnalysedTraitRule.php @@ -0,0 +1,62 @@ + + */ +final class NotAnalysedTraitRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $traitDeclarationData = $node->get(TraitDeclarationCollector::class); + $traitUseData = $node->get(TraitUseCollector::class); + + $declaredTraits = []; + foreach ($traitDeclarationData as $file => $declaration) { + foreach ($declaration as [$name, $line]) { + $declaredTraits[strtolower($name)] = [$file, $name, $line]; + } + } + + foreach ($traitUseData as $usedNamesData) { + foreach ($usedNamesData as $usedNames) { + foreach ($usedNames as $usedName) { + unset($declaredTraits[strtolower($usedName)]); + } + } + } + + $errors = []; + foreach ($declaredTraits as [$file, $name, $line]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Trait %s is used zero times and is not analysed.', + $name, + )) + ->identifier('phpstanPlayground.traitUnused') + ->file($file) + ->line($line) + ->tip('See: https://phpstan.org/blog/how-phpstan-analyses-traits') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Playground/PromoteParameterRule.php b/src/Rules/Playground/PromoteParameterRule.php new file mode 100644 index 0000000000..cee351e3ff --- /dev/null +++ b/src/Rules/Playground/PromoteParameterRule.php @@ -0,0 +1,61 @@ + + */ +final class PromoteParameterRule implements Rule +{ + + /** + * @param Rule $rule + * @param class-string $nodeType + */ + public function __construct( + private Rule $rule, + private string $nodeType, + private bool $parameterValue, + private string $parameterName, + ) + { + } + + public function getNodeType(): string + { + return $this->nodeType; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->parameterValue) { + return []; + } + + if ($this->nodeType !== $this->rule->getNodeType()) { + return []; + } + + $errors = []; + foreach ($this->rule->processNode($node, $scope) as $error) { + $builder = RuleErrorBuilder::message($error->getMessage()) + ->identifier('phpstanPlayground.configParameter') + ->tip(sprintf('This error would be reported if the %s: true parameter was enabled in your %%configurationFile%%.', $this->parameterName)); + if ($error instanceof LineRuleError) { + $builder->line($error->getLine()); + } + $errors[] = $builder->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Playground/StaticVarWithoutTypeRule.php b/src/Rules/Playground/StaticVarWithoutTypeRule.php new file mode 100644 index 0000000000..28f5369952 --- /dev/null +++ b/src/Rules/Playground/StaticVarWithoutTypeRule.php @@ -0,0 +1,81 @@ + + */ +final class StaticVarWithoutTypeRule implements Rule +{ + + public function __construct( + private FileTypeMapper $fileTypeMapper, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Static_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + $ruleError = RuleErrorBuilder::message('Static variable needs to be typed with PHPDoc @var tag.') + ->identifier('phpstanPlayground.staticWithoutType') + ->build(); + if ($docComment === null) { + return [$ruleError]; + } + $variableNames = []; + foreach ($node->vars as $var) { + if (!is_string($var->var->name)) { + throw new ShouldNotHappenException(); + } + + $variableNames[] = $var->var->name; + } + + $function = $scope->getFunction(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $docComment->getText(), + ); + $varTags = []; + foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) { + $varTags[$key] = $varTag; + } + + if (count($varTags) === 0) { + return [$ruleError]; + } + + if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) { + return []; + } + + foreach ($variableNames as $variableName) { + if (isset($varTags[$variableName])) { + continue; + } + + return [$ruleError]; + } + + return []; + } + +} diff --git a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php index 5f7e92fbac..548e176c1d 100644 --- a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php +++ b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class AccessPrivatePropertyThroughStaticRule implements Rule +final class AccessPrivatePropertyThroughStaticRule implements Rule { public function getNodeType(): string @@ -57,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array 'Unsafe access to private property %s::$%s through static::.', $property->getDeclaringClass()->getDisplayName(), $propertyName, - ))->build(), + ))->identifier('staticClassAccess.privateProperty')->build(), ]; } diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php new file mode 100644 index 0000000000..467cf98a99 --- /dev/null +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -0,0 +1,225 @@ + + */ + public function check(PropertyFetch $node, Scope $scope, bool $write): array + { + if ($node->name instanceof Identifier) { + $names = [$node->name->name]; + } else { + $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); + } + + $errors = []; + foreach ($names as $name) { + $errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name, $write)); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleProperty(Scope $scope, PropertyFetch $node, string $name, bool $write): array + { + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + 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) { + return $typeResult->getUnknownClassErrors(); + } + + if ($scope->isInExpressionAssign($node)) { + return []; + } + + $typeForDescribe = $type; + if ($type instanceof StaticType) { + $typeForDescribe = $type->getStaticObjectType(); + } + + if ($type->canAccessProperties()->no() || $type->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) { + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot access property $%s on %s.', + $name, + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->identifier('property.nonObject')->build(), + ]; + } + + $has = $type->hasProperty($name); + if (!$has->no() && $this->canAccessUndefinedProperties($scope, $node)) { + return []; + } + + if (!$has->yes()) { + if ($scope->hasExpressionType($node)->yes()) { + return []; + } + + $classNames = $type->getObjectClassNames(); + if (!$this->reportMagicProperties) { + foreach ($classNames as $className) { + if (!$this->reflectionProvider->hasClass($className)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if ( + $classReflection->hasNativeMethod('__get') + || $classReflection->hasNativeMethod('__set') + ) { + return []; + } + } + } + + if (count($classNames) === 1) { + $propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]); + $parentClassReflection = $propertyClassReflection->getParentClass(); + while ($parentClassReflection !== null) { + if ($parentClassReflection->hasProperty($name)) { + if ($write) { + if ($scope->canWriteProperty($parentClassReflection->getProperty($name, $scope))) { + return []; + } + } elseif ($scope->canReadProperty($parentClassReflection->getProperty($name, $scope))) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Access to private property $%s of parent class %s.', + $name, + $parentClassReflection->getDisplayName(), + ))->identifier('property.private')->build(), + ]; + } + + $parentClassReflection = $parentClassReflection->getParentClass(); + } + } + + if ($node->name instanceof Expr) { + $propertyExistsExpr = new FuncCall(new FullyQualified('property_exists'), [ + new Arg($node->var), + new Arg($node->name), + ]); + + if ($scope->getType($propertyExistsExpr)->isTrue()->yes()) { + return []; + } + } + + $ruleErrorBuilder = RuleErrorBuilder::message(sprintf( + 'Access to an undefined property %s::$%s.', + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + $name, + ))->identifier('property.notFound'); + if ($typeResult->getTip() !== null) { + $ruleErrorBuilder->tip($typeResult->getTip()); + } else { + $ruleErrorBuilder->tip('Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'); + } + + return [ + $ruleErrorBuilder->build(), + ]; + } + + $propertyReflection = $type->getProperty($name, $scope); + if ($propertyReflection->isStatic()) { + return [ + RuleErrorBuilder::message(sprintf( + 'Non-static access to static property %s::$%s.', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $name, + ))->identifier('staticProperty.nonStaticAccess')->build(), + ]; + } + + if ($write) { + if ($scope->canWriteProperty($propertyReflection)) { + return []; + } + } elseif ($scope->canReadProperty($propertyReflection)) { + return []; + } + + if ( + !$this->phpVersion->supportsAsymmetricVisibility() + || !$write + || (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) + ) { + return [ + RuleErrorBuilder::message(sprintf( + 'Access to %s property %s::$%s.', + $propertyReflection->isPrivate() ? 'private' : 'protected', + $type->describe(VerbosityLevel::typeOnly()), + $name, + ))->identifier(sprintf('property.%s', $propertyReflection->isPrivate() ? 'private' : 'protected'))->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Assign to %s property %s::$%s.', + $propertyReflection->isPrivateSet() ? 'private(set)' : 'protected(set)', + $type->describe(VerbosityLevel::typeOnly()), + $name, + ))->identifier(sprintf('assign.property%s', $propertyReflection->isPrivateSet() ? 'PrivateSet' : 'ProtectedSet'))->build(), + ]; + } + + private function canAccessUndefinedProperties(Scope $scope, Expr $node): bool + { + return $scope->isUndefinedExpressionAllowed($node) && !$this->checkDynamicProperties; + } + +} diff --git a/src/Rules/Properties/AccessPropertiesInAssignRule.php b/src/Rules/Properties/AccessPropertiesInAssignRule.php index e5d3d08b4e..6577820611 100644 --- a/src/Rules/Properties/AccessPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessPropertiesInAssignRule.php @@ -10,10 +10,10 @@ /** * @implements Rule */ -class AccessPropertiesInAssignRule implements Rule +final class AccessPropertiesInAssignRule implements Rule { - public function __construct(private AccessPropertiesRule $accessPropertiesRule) + public function __construct(private AccessPropertiesCheck $check) { } @@ -32,7 +32,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - return $this->accessPropertiesRule->processNode($node->getPropertyFetch(), $scope); + return $this->check->check($node->getPropertyFetch(), $scope, true); } } diff --git a/src/Rules/Properties/AccessPropertiesRule.php b/src/Rules/Properties/AccessPropertiesRule.php index 5b3d81776a..e9b382c7f2 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -4,36 +4,16 @@ 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; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ErrorType; -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 Rule */ -class AccessPropertiesRule implements Rule +final class AccessPropertiesRule implements Rule { - public function __construct( - private ReflectionProvider $reflectionProvider, - private RuleLevelHelper $ruleLevelHelper, - private bool $reportMagicProperties, - ) + public function __construct(private AccessPropertiesCheck $check) { } @@ -44,118 +24,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if ($node->name instanceof Identifier) { - $names = [$node->name->name]; - } else { - $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), TypeUtils::getConstantStrings($scope->getType($node->name))); - } - - $errors = []; - foreach ($names as $name) { - $errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name)); - } - - return $errors; - } - - /** - * @return RuleError[] - */ - private function processSingleProperty(Scope $scope, PropertyFetch $node, string $name): array - { - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - 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) { - return $typeResult->getUnknownClassErrors(); - } - - if ($scope->isInExpressionAssign($node)) { - return []; - } - - if (!$type->canAccessProperties()->yes()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Cannot access property $%s on %s.', - $name, - $type->describe(VerbosityLevel::typeOnly()), - ))->build(), - ]; - } - - if (!$type->hasProperty($name)->yes()) { - if ($scope->isSpecified($node)) { - return []; - } - - $classNames = $typeResult->getReferencedClasses(); - if (!$this->reportMagicProperties) { - foreach ($classNames as $className) { - if (!$this->reflectionProvider->hasClass($className)) { - continue; - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ( - $classReflection->hasNativeMethod('__get') - || $classReflection->hasNativeMethod('__set') - ) { - return []; - } - } - } - - if (count($classNames) === 1) { - $referencedClass = $typeResult->getReferencedClasses()[0]; - $propertyClassReflection = $this->reflectionProvider->getClass($referencedClass); - $parentClassReflection = $propertyClassReflection->getParentClass(); - while ($parentClassReflection !== null) { - if ($parentClassReflection->hasProperty($name)) { - return [ - RuleErrorBuilder::message(sprintf( - 'Access to private property $%s of parent class %s.', - $name, - $parentClassReflection->getDisplayName(), - ))->build(), - ]; - } - - $parentClassReflection = $parentClassReflection->getParentClass(); - } - } - - $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->build(), - ]; - } - - $propertyReflection = $type->getProperty($name, $scope); - if (!$scope->canAccessProperty($propertyReflection)) { - return [ - RuleErrorBuilder::message(sprintf( - 'Access to %s property %s::$%s.', - $propertyReflection->isPrivate() ? 'private' : 'protected', - $type->describe(VerbosityLevel::typeOnly()), - $name, - ))->build(), - ]; - } - - return []; + return $this->check->check($node, $scope, false); } } diff --git a/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php b/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php index 21d6ff3e1f..f9a6e61602 100644 --- a/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php @@ -10,7 +10,7 @@ /** * @implements Rule */ -class AccessStaticPropertiesInAssignRule implements Rule +final class AccessStaticPropertiesInAssignRule implements Rule { public function __construct(private AccessStaticPropertiesRule $accessStaticPropertiesRule) diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 81da5c51ee..b15808ad5a 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -9,13 +9,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; +use PHPStan\Rules\IdentifierRuleError; 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; @@ -26,6 +26,7 @@ use PHPStan\Type\VerbosityLevel; use function array_map; use function array_merge; +use function count; use function in_array; use function sprintf; use function strtolower; @@ -33,13 +34,14 @@ /** * @implements Rule */ -class AccessStaticPropertiesRule implements Rule +final class AccessStaticPropertiesRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, + private bool $discoveringSymbolsTip, ) { } @@ -54,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array if ($node->name instanceof Node\VarLikeIdentifier) { $names = [$node->name->name]; } else { - $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), TypeUtils::getConstantStrings($scope->getType($node->name))); + $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); } $errors = []; @@ -66,7 +68,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, string $name): array { @@ -81,7 +83,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, 'Accessing %s::$%s outside of class scope.', $class, $name, - ))->build(), + ))->identifier(sprintf('outOfClass.%s', $lowercasedClass))->build(), ]; } $classType = $scope->resolveTypeByName($node->class); @@ -92,7 +94,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, 'Accessing %s::$%s outside of class scope.', $class, $name, - ))->build(), + ))->identifier('outOfClass.parent')->build(), ]; } if ($scope->getClassReflection()->getParentClass() === null) { @@ -103,20 +105,10 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $scope->getFunctionName(), $name, $scope->getClassReflection()->getDisplayName(), - ))->build(), + ))->identifier('class.noParent')->build(), ]; } - if ($scope->getFunctionName() === null) { - throw new ShouldNotHappenException(); - } - - $currentMethodReflection = $scope->getClassReflection()->getNativeMethod($scope->getFunctionName()); - if (!$currentMethodReflection->isStatic()) { - // calling parent::method() from instance method - return []; - } - $classType = $scope->resolveTypeByName($node->class); } else { if (!$this->reflectionProvider->hasClass($class)) { @@ -124,17 +116,34 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, return []; } + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Access to static property $%s on an unknown class %s.', + $name, + $class, + )) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + return [ - RuleErrorBuilder::message(sprintf( - 'Access to static property $%s on an unknown class %s.', - $name, - $class, - ))->discoveringSymbolsTip()->build(), + $errorBuilder->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($class, $node->class)]); } + $locationData = []; + $locationClassReflection = $this->reflectionProvider->getClass($class); + if ($locationClassReflection->hasProperty($name)) { + $locationData['property'] = $locationClassReflection->getProperty($name, $scope); + } + + $messages = $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($class, $node->class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::STATIC_PROPERTY_ACCESS, $locationData), + ); + $classType = $scope->resolveTypeByName($node->class); } } else { @@ -150,7 +159,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, } } - if ((new StringType())->isSuperTypeOf($classType)->yes()) { + if ($classType->isString()->yes()) { return []; } @@ -164,27 +173,55 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, return []; } - if (!$classType->canAccessProperties()->yes()) { + if ($classType->canAccessProperties()->no() || $classType->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) { return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Cannot access static property $%s on %s.', $name, $typeForDescribe->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('staticProperty.nonObject')->build(), ]); } - if (!$classType->hasProperty($name)->yes()) { - if ($scope->isSpecified($node)) { + $has = $classType->hasProperty($name); + if (!$has->no() && $scope->isUndefinedExpressionAllowed($node)) { + return []; + } + + if (!$has->yes()) { + if ($scope->hasExpressionType($node)->yes()) { return $messages; } + $classNames = $classType->getObjectClassNames(); + if (count($classNames) === 1) { + $propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]); + $parentClassReflection = $propertyClassReflection->getParentClass(); + + while ($parentClassReflection !== null) { + if ($parentClassReflection->hasProperty($name)) { + if ($scope->canReadProperty($parentClassReflection->getProperty($name, $scope))) { + return []; + } + return [ + RuleErrorBuilder::message(sprintf( + 'Access to private static property $%s of parent class %s.', + $name, + $parentClassReflection->getDisplayName(), + ))->identifier('staticProperty.private')->build(), + ]; + } + + $parentClassReflection = $parentClassReflection->getParentClass(); + } + } + return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Access to an undefined static property %s::$%s.', $typeForDescribe->describe(VerbosityLevel::typeOnly()), $name, - ))->build(), + ))->identifier('staticProperty.notFound')->build(), ]); } @@ -202,18 +239,18 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, 'Static access to instance property %s::$%s.', $property->getDeclaringClass()->getDisplayName(), $name, - ))->build(), + ))->identifier('property.staticAccess')->build(), ]); } - if (!$scope->canAccessProperty($property)) { + if (!$scope->canReadProperty($property)) { return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Access to %s property $%s of class %s.', $property->isPrivate() ? 'private' : 'protected', $name, $property->getDeclaringClass()->getDisplayName(), - ))->build(), + ))->identifier(sprintf('staticProperty.%s', $property->isPrivate() ? 'private' : 'protected'))->build(), ]); } diff --git a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php index b27e01de6b..cb68412aa4 100644 --- a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php @@ -8,15 +8,13 @@ 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 Rule */ -class DefaultValueTypesAssignedToPropertiesRule implements Rule +final class DefaultValueTypesAssignedToPropertiesRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -30,25 +28,23 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $classReflection = $scope->getClassReflection(); $default = $node->getDefault(); if ($default === null) { return []; } + $classReflection = $node->getClassReflection(); + $propertyReflection = $classReflection->getNativeProperty($node->getName()); $propertyType = $propertyReflection->getWritableType(); - if ($propertyReflection->getNativeType() instanceof MixedType) { - if ($default instanceof Node\Expr\ConstFetch && (string) $default->name === 'null') { + if (!$propertyReflection->hasNativeType()) { + if ($default instanceof Node\Expr\ConstFetch && $default->name->toLowerString() === 'null') { return []; } } $defaultValueType = $scope->getType($default); - if ($this->ruleLevelHelper->accepts($propertyType, $defaultValueType, true)) { + $accepts = $this->ruleLevelHelper->accepts($propertyType, $defaultValueType, true); + if ($accepts->result) { return []; } @@ -62,7 +58,10 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $propertyType->describe($verbosityLevel), $defaultValueType->describe($verbosityLevel), - ))->build(), + )) + ->identifier('property.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(), ]; } diff --git a/tests/PHPStan/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php b/src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php similarity index 76% rename from tests/PHPStan/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php rename to src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php index 6ce75189d7..0130c2f633 100644 --- a/tests/PHPStan/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php +++ b/src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\Properties; -class DirectReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider +final class DirectReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider { /** diff --git a/src/Rules/Properties/ExistingClassesInPropertiesRule.php b/src/Rules/Properties/ExistingClassesInPropertiesRule.php index 1f4780b33b..869074e358 100644 --- a/src/Rules/Properties/ExistingClassesInPropertiesRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertiesRule.php @@ -7,12 +7,12 @@ use PHPStan\Node\ClassPropertyNode; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\ClassNameUsageLocation; 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; @@ -20,16 +20,17 @@ /** * @implements Rule */ -class ExistingClassesInPropertiesRule implements Rule +final class ExistingClassesInPropertiesRule implements Rule { public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private PhpVersion $phpVersion, private bool $checkClassCaseSensitivity, private bool $checkThisOnly, + private bool $discoveringSymbolsTip, ) { } @@ -41,11 +42,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $propertyReflection = $scope->getClassReflection()->getNativeProperty($node->getName()); + $propertyReflection = $node->getClassReflection()->getNativeProperty($node->getName()); if ($this->checkThisOnly) { $referencedClasses = $propertyReflection->getNativeType()->getReferencedClasses(); } else { @@ -64,26 +61,38 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $referencedClass, - ))->build(); + ))->identifier('property.trait')->build(); } continue; } - $errors[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( 'Property %s::$%s has unknown class %s as its type.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $referencedClass, - ))->discoveringSymbolsTip()->build(); - } + )) + ->identifier('class.notFound'); - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses)), - ); + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + $scope, + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses), + ClassNameUsageLocation::from(ClassNameUsageLocation::PROPERTY_TYPE, [ + 'property' => $propertyReflection, + ]), + $this->checkClassCaseSensitivity, + ), + ); + if ( $this->phpVersion->supportsPureIntersectionTypes() && $this->unresolvableTypeHelper->containsUnresolvableType($propertyReflection->getNativeType()) @@ -92,7 +101,7 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s has unresolvable native type.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->build(); + ))->identifier('property.unresolvableNativeType')->build(); } return $errors; diff --git a/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php new file mode 100644 index 0000000000..dff1491617 --- /dev/null +++ b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php @@ -0,0 +1,88 @@ + + */ +final class ExistingClassesInPropertyHookTypehintsRule implements Rule +{ + + public function __construct(private FunctionDefinitionCheck $check) + { + } + + public function getNodeType(): string + { + return InPropertyHookNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $hookReflection = $node->getHookReflection(); + if (!$hookReflection->isPropertyHook()) { + throw new ShouldNotHappenException(); + } + $className = SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName()); + $hookName = $hookReflection->getPropertyHookName(); + $propertyName = SprintfHelper::escapeFormatString($hookReflection->getHookedPropertyName()); + + $originalHookNode = $node->getOriginalNode(); + if ($hookReflection->getPropertyHookName() === 'set' && $originalHookNode->params === []) { + $originalHookNode = clone $originalHookNode; + $originalHookNode->params = [ + new Node\Param(new Variable('value'), null, null), + ]; + } + + return $this->check->checkClassMethod( + $scope, + $hookReflection, + $originalHookNode, + sprintf( + 'Parameter $%%s of %s hook for property %s::$%s has invalid type %%s.', + $hookName, + $className, + $propertyName, + ), + sprintf( + '%s hook for property %s::$%s has invalid return type %%s.', + ucfirst($hookName), + $className, + $propertyName, + ), + sprintf('%s hook for property %s::$%s uses native union types but they\'re supported only on PHP 8.0 and later.', $hookName, $className, $propertyName), + sprintf('Template type %%s of %s hook for property %s::$%s is not referenced in a parameter.', $hookName, $className, $propertyName), + sprintf( + 'Parameter $%%s of %s hook for property %s::$%s has unresolvable native type.', + $hookName, + $className, + $propertyName, + ), + sprintf( + '%s hook for property %s::$%s has unresolvable native return type.', + ucfirst($hookName), + $className, + $propertyName, + ), + sprintf( + '%s hook for property %s::$%s has invalid @phpstan-self-out type %%s.', + ucfirst($hookName), + $className, + $propertyName, + ), + ); + } + +} diff --git a/src/Rules/Properties/FoundPropertyReflection.php b/src/Rules/Properties/FoundPropertyReflection.php index 0132ac0d3b..1b14b785aa 100644 --- a/src/Rules/Properties/FoundPropertyReflection.php +++ b/src/Rules/Properties/FoundPropertyReflection.php @@ -4,17 +4,18 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Php\PhpPropertyReflection; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\WrapperPropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class FoundPropertyReflection implements PropertyReflection +final class FoundPropertyReflection implements ExtendedPropertyReflection { public function __construct( - private PropertyReflection $originalPropertyReflection, + private ExtendedPropertyReflection $originalPropertyReflection, private Scope $scope, private string $propertyName, private Type $readableType, @@ -58,6 +59,26 @@ public function getDocComment(): ?string return $this->originalPropertyReflection->getDocComment(); } + public function hasPhpDocType(): bool + { + return $this->originalPropertyReflection->hasPhpDocType(); + } + + public function getPhpDocType(): Type + { + return $this->originalPropertyReflection->getPhpDocType(); + } + + public function hasNativeType(): bool + { + return $this->originalPropertyReflection->hasNativeType(); + } + + public function getNativeType(): Type + { + return $this->originalPropertyReflection->getNativeType(); + } + public function getReadableType(): Type { return $this->readableType; @@ -103,16 +124,6 @@ public function isNative(): bool return $this->getNativeReflection() !== null; } - public function getNativeType(): ?Type - { - $reflection = $this->getNativeReflection(); - if ($reflection === null) { - return null; - } - - return $reflection->getNativeType(); - } - public function getNativeReflection(): ?PhpPropertyReflection { $reflection = $this->originalPropertyReflection; @@ -127,4 +138,44 @@ public function getNativeReflection(): ?PhpPropertyReflection return $reflection; } + public function isAbstract(): TrinaryLogic + { + return $this->originalPropertyReflection->isAbstract(); + } + + public function isFinal(): TrinaryLogic + { + return $this->originalPropertyReflection->isFinal(); + } + + public function isVirtual(): TrinaryLogic + { + return $this->originalPropertyReflection->isVirtual(); + } + + public function hasHook(string $hookType): bool + { + return $this->originalPropertyReflection->hasHook($hookType); + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + return $this->originalPropertyReflection->getHook($hookType); + } + + public function isProtectedSet(): bool + { + return $this->originalPropertyReflection->isProtectedSet(); + } + + public function isPrivateSet(): bool + { + return $this->originalPropertyReflection->isPrivateSet(); + } + + public function getAttributes(): array + { + return $this->originalPropertyReflection->getAttributes(); + } + } diff --git a/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php b/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php new file mode 100644 index 0000000000..a8a9554062 --- /dev/null +++ b/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php @@ -0,0 +1,132 @@ + + */ +final class GetNonVirtualPropertyHookReadRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertiesNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $reads = []; + $classReflection = $node->getClassReflection(); + foreach ($node->getPropertyUsages() as $propertyUsage) { + if (!$propertyUsage instanceof PropertyRead) { + continue; + } + + $fetch = $propertyUsage->getFetch(); + if (!$fetch instanceof Node\Expr\PropertyFetch) { + continue; + } + + if (!$fetch->name instanceof Node\Identifier) { + continue; + } + + $propertyName = $fetch->name->toString(); + if (!$fetch->var instanceof Node\Expr\Variable || $fetch->var->name !== 'this') { + continue; + } + + $usageScope = $propertyUsage->getScope(); + $inFunction = $usageScope->getFunction(); + if (!$inFunction instanceof PhpMethodFromParserNodeReflection) { + continue; + } + + if (!$inFunction->isPropertyHook()) { + continue; + } + + if ($inFunction->getPropertyHookName() !== 'get') { + continue; + } + + if ($propertyName !== $inFunction->getHookedPropertyName()) { + continue; + } + + $reads[$propertyName] = true; + } + + $errors = []; + foreach ($node->getProperties() as $propertyNode) { + $hasGetHook = false; + foreach ($propertyNode->getHooks() as $hook) { + if ($hook->name->toLowerString() !== 'get') { + continue; + } + + if ($hook->body === null) { + continue; + } + + $hasGetHook = true; + break; + } + + if (!$hasGetHook) { + continue; + } + + if (array_key_exists($propertyNode->getName(), $reads)) { + continue; + } + + $propertyReflection = $classReflection->getNativeProperty($propertyNode->getName()); + if ($propertyReflection->isVirtual()->yes()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Get hook for non-virtual property %s::$%s does not read its value.', + $classReflection->getDisplayName(), + $propertyNode->getName(), + )) + ->line($this->getGetHookLine($propertyNode)) + ->identifier('propertyGetHook.noRead') + ->build(); + } + + return $errors; + } + + private function getGetHookLine(ClassPropertyNode $propertyNode): int + { + $getHook = null; + foreach ($propertyNode->getHooks() as $hook) { + if ($hook->name->toLowerString() !== 'get') { + continue; + } + + $getHook = $hook; + break; + } + + if ($getHook === null) { + return $propertyNode->getStartLine(); + } + + return $getHook->getStartLine(); + } + +} diff --git a/src/Rules/Properties/InvalidCallablePropertyTypeRule.php b/src/Rules/Properties/InvalidCallablePropertyTypeRule.php new file mode 100644 index 0000000000..4c4a2aa655 --- /dev/null +++ b/src/Rules/Properties/InvalidCallablePropertyTypeRule.php @@ -0,0 +1,65 @@ + + */ +final class InvalidCallablePropertyTypeRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + + if (!$propertyReflection->hasNativeType()) { + return []; + } + + $nativeType = $propertyReflection->getNativeType(); + $callableTypes = []; + + TypeTraverser::map($nativeType, static function (Type $type, callable $traverse) use (&$callableTypes): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof CallableType) { + $callableTypes[] = $type; + } + + return $type; + }); + + if ($callableTypes === []) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Property %s::$%s cannot have callable in its type declaration.', + $classReflection->getDisplayName(), + $node->getName(), + ))->identifier('property.callableType')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php b/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php index 3f61fd0fb8..f42cb7e7d5 100644 --- a/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php +++ b/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php @@ -4,7 +4,7 @@ use PHPStan\DependencyInjection\Container; -class LazyReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider +final class LazyReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider { /** @var ReadWritePropertiesExtension[]|null */ diff --git a/src/Rules/Properties/MissingPropertyTypehintRule.php b/src/Rules/Properties/MissingPropertyTypehintRule.php index 6831c3ca37..84c8a20325 100644 --- a/src/Rules/Properties/MissingPropertyTypehintRule.php +++ b/src/Rules/Properties/MissingPropertyTypehintRule.php @@ -8,10 +8,8 @@ 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; /** @@ -31,19 +29,21 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); + $propertyReflection = $node->getClassReflection()->getNativeProperty($node->getName()); + + if ($propertyReflection->isPromoted()) { + return []; } - $propertyReflection = $scope->getClassReflection()->getNativeProperty($node->getName()); $propertyType = $propertyReflection->getReadableType(); + if ($propertyType instanceof MixedType && !$propertyType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( 'Property %s::$%s has no type specified.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->build(), + ))->identifier('missingType.property')->build(), ]; } @@ -55,7 +55,10 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $iterableTypeDescription, - ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($propertyType) as [$name, $genericTypeNames]) { @@ -64,8 +67,10 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $name, - implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($propertyType) as $callableType) { @@ -74,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php new file mode 100644 index 0000000000..bbb53f1089 --- /dev/null +++ b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php @@ -0,0 +1,79 @@ + + */ +final class MissingReadOnlyByPhpDocPropertyAssignRule implements Rule +{ + + public function __construct( + private ConstructorsHelper $constructorsHelper, + ) + { + } + + public function getNodeType(): string + { + return ClassPropertiesNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + [$properties, $prematureAccess, $additionalAssigns] = $node->getUninitializedProperties($scope, $this->constructorsHelper->getConstructors($classReflection)); + + $errors = []; + foreach ($properties as $propertyName => $propertyNode) { + if (!$propertyNode->isReadOnlyByPhpDoc() || $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->getStartLine()) + ->identifier('property.uninitializedReadonlyByPhpDoc') + ->build(); + } + + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { + if (!$propertyNode->isReadOnlyByPhpDoc() || $propertyNode->isReadOnly()) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( + 'Access to an uninitialized @readonly property %s::$%s.', + $classReflection->getDisplayName(), + $propertyName, + )) + ->identifier('property.uninitializedReadonlyByPhpDoc') + ->line($line) + ->file($file, $fileDescription) + ->build(); + } + + foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { + if (!$propertyNode->isReadOnlyByPhpDoc() || $propertyNode->isReadOnly()) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( + '@readonly property %s::$%s is already assigned.', + $classReflection->getDisplayName(), + $propertyName, + ))->identifier('assign.readOnlyPropertyByPhpDoc')->line($line)->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php index ebe5d6231d..de05aaaab9 100644 --- a/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php +++ b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php @@ -5,29 +5,19 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertiesNode; -use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstructorsHelper; 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 */ -class MissingReadOnlyPropertyAssignRule implements Rule +final class MissingReadOnlyPropertyAssignRule implements Rule { - /** @var array */ - private array $additionalConstructorsCache = []; - - /** - * @param string[] $additionalConstructors - */ public function __construct( - private array $additionalConstructors, + private ConstructorsHelper $constructorsHelper, ) { } @@ -39,11 +29,8 @@ public function getNodeType(): string 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), []); + $classReflection = $node->getClassReflection(); + [$properties, $prematureAccess, $additionalAssigns] = $node->getUninitializedProperties($scope, $this->constructorsHelper->getConstructors($classReflection)); $errors = []; foreach ($properties as $propertyName => $propertyNode) { @@ -54,10 +41,13 @@ public function processNode(Node $node, Scope $scope): array 'Class %s has an uninitialized readonly property $%s. Assign it in the constructor.', $classReflection->getDisplayName(), $propertyName, - ))->line($propertyNode->getLine())->build(); + )) + ->line($propertyNode->getStartLine()) + ->identifier('property.uninitializedReadonly') + ->build(); } - foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { if (!$propertyNode->isReadOnly()) { continue; } @@ -65,7 +55,11 @@ public function processNode(Node $node, Scope $scope): array 'Access to an uninitialized readonly property %s::$%s.', $classReflection->getDisplayName(), $propertyName, - ))->line($line)->build(); + )) + ->line($line) + ->file($file, $fileDescription) + ->identifier('property.uninitializedReadonly') + ->build(); } foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { @@ -76,52 +70,13 @@ public function processNode(Node $node, Scope $scope): array 'Readonly property %s::$%s is already assigned.', $classReflection->getDisplayName(), $propertyName, - ))->line($line)->build(); + )) + ->line($line) + ->identifier('assign.readOnlyProperty') + ->build(); } return $errors; } - /** - * @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 a71d24f45d..6b72cf6df7 100644 --- a/src/Rules/Properties/NullsafePropertyFetchRule.php +++ b/src/Rules/Properties/NullsafePropertyFetchRule.php @@ -6,16 +6,19 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; use function sprintf; /** * @implements Rule */ -class NullsafePropertyFetchRule implements Rule +final class NullsafePropertyFetchRule implements Rule { + public function __construct() + { + } + public function getNodeType(): string { return Node\Expr\NullsafePropertyFetch::class; @@ -23,18 +26,19 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $nullType = new NullType(); $calledOnType = $scope->getType($node->var); - if ($calledOnType->equals($nullType)) { + if (!$calledOnType->isNull()->no()) { return []; } - if (!$calledOnType->isSuperTypeOf($nullType)->no()) { + if ($scope->isUndefinedExpressionAllowed($node)) { return []; } return [ - RuleErrorBuilder::message(sprintf('Using nullsafe property access on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly())))->build(), + RuleErrorBuilder::message(sprintf('Using nullsafe property access on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly()))) + ->identifier('nullsafe.neverNull') + ->build(), ]; } diff --git a/src/Rules/Properties/OverridingPropertyRule.php b/src/Rules/Properties/OverridingPropertyRule.php index e8749b62f0..c1806565e2 100644 --- a/src/Rules/Properties/OverridingPropertyRule.php +++ b/src/Rules/Properties/OverridingPropertyRule.php @@ -5,12 +5,11 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertyNode; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\VerbosityLevel; use function array_merge; use function count; @@ -19,10 +18,11 @@ /** * @implements Rule */ -class OverridingPropertyRule implements Rule +final class OverridingPropertyRule implements Rule { public function __construct( + private PhpVersion $phpVersion, private bool $checkPhpDocMethodSignatures, private bool $reportMaybes, ) @@ -36,11 +36,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); $prototype = $this->findPrototype($classReflection, $node->getName()); if ($prototype === null) { return []; @@ -55,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.nonStatic')->nonIgnorable()->build(); } } elseif ($node->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -64,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.static')->nonIgnorable()->build(); } if ($prototype->isReadOnly()) { @@ -75,16 +71,47 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.readWrite')->nonIgnorable()->build(); } } elseif ($node->isReadOnly()) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Readonly property %s::$%s overrides readwrite property %s::$%s.', - $classReflection->getDisplayName(), - $node->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $node->getName(), - ))->nonIgnorable()->build(); + if ( + !$this->phpVersion->supportsPropertyHooks() + || $prototype->isWritable() + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Readonly property %s::$%s overrides readwrite property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.readOnly')->nonIgnorable()->build(); + } + } + + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + if ($this->phpVersion->supportsPropertyHooks()) { + if ($prototype->isReadable()) { + if (!$propertyReflection->isReadable()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s overriding readable property %s::$%s also has to be readable.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.notReadable')->nonIgnorable()->build(); + } + } + if ($prototype->isWritable()) { + if (!$propertyReflection->isWritable()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s overriding writable property %s::$%s also has to be writable.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.notWritable')->nonIgnorable()->build(); + } + } } if ($prototype->isPublic()) { @@ -96,7 +123,7 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.visibility')->nonIgnorable()->build(); } } elseif ($node->isPrivate()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -105,12 +132,25 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.visibility')->nonIgnorable()->build(); + } + + if ($prototype->isFinal()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s overrides final property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.parentPropertyFinal') + ->nonIgnorable() + ->build(); } $typeErrors = []; + $nativeType = $node->getNativeType(); if ($prototype->hasNativeType()) { - if ($node->getNativeType() === null) { + if ($nativeType === null) { $typeErrors[] = RuleErrorBuilder::message(sprintf( 'Property %s::$%s overriding property %s::$%s (%s) should also have native type %s.', $classReflection->getDisplayName(), @@ -119,30 +159,59 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), - ))->nonIgnorable()->build(); + ))->identifier('property.missingNativeType')->nonIgnorable()->build(); } else { - $nativeType = ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection()); 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(); + if ( + $this->phpVersion->supportsPropertyHooks() + && ($prototype->isVirtual()->yes() || $prototype->isAbstract()->yes()) + && (!$prototype->isReadable() || !$prototype->isWritable()) + ) { + if (!$prototype->isReadable()) { + if (!$nativeType->isSuperTypeOf($prototype->getNativeType())->yes()) { + $typeErrors[] = RuleErrorBuilder::message(sprintf( + 'Type %s of property %s::$%s is not contravariant with 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(), + ))->identifier('property.nativeType')->nonIgnorable()->build(); + } + } elseif (!$prototype->getNativeType()->isSuperTypeOf($nativeType)->yes()) { + $typeErrors[] = RuleErrorBuilder::message(sprintf( + 'Type %s of property %s::$%s is not covariant with 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(), + ))->identifier('property.nativeType')->nonIgnorable()->build(); + } + } else { + $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(), + ))->identifier('property.nativeType')->nonIgnorable()->build(); + } } } - } elseif ($node->getNativeType() !== null) { + } elseif ($nativeType !== 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()), + $nativeType->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.extraNativeType')->nonIgnorable()->build(); } $errors = array_merge($errors, $typeErrors); @@ -155,12 +224,50 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - $propertyReflection = $classReflection->getNativeProperty($node->getName()); if ($prototype->getReadableType()->equals($propertyReflection->getReadableType())) { return $errors; } $verbosity = VerbosityLevel::getRecommendedLevelByType($prototype->getReadableType(), $propertyReflection->getReadableType()); + + if ( + $this->phpVersion->supportsPropertyHooks() + && ($prototype->isVirtual()->yes() || $prototype->isAbstract()->yes()) + && (!$prototype->isReadable() || !$prototype->isWritable()) + ) { + if (!$prototype->isReadable()) { + if (!$propertyReflection->getReadableType()->isSuperTypeOf($prototype->getReadableType())->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc type %s of property %s::$%s is not contravariant with 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(), + ))->identifier('property.phpDocType')->tip(sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ))->build(); + } + } elseif (!$prototype->getReadableType()->isSuperTypeOf($propertyReflection->getReadableType())->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc type %s of property %s::$%s is not covariant with 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(), + ))->identifier('property.phpDocType')->tip(sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ))->build(); + } + + return $errors; + } + $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.', @@ -170,7 +277,7 @@ public function processNode(Node $node, Scope $scope): array $prototype->getReadableType()->describe($verbosity), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->tip(sprintf( + ))->identifier('property.phpDocType')->tip(sprintf( "You can fix 3rd party PHPDoc types with stub files:\n %s\n This error can be turned off by setting\n %s", 'https://phpstan.org/user-guide/stub-files', 'reportMaybesInPropertyPhpDocTypes: false in your %configurationFile%.', @@ -184,7 +291,7 @@ public function processNode(Node $node, Scope $scope): array $prototype->getReadableType()->describe($verbosity), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->tip(sprintf( + ))->identifier('property.phpDocType')->tip(sprintf( "You can fix 3rd party PHPDoc types with stub files:\n %s", 'https://phpstan.org/user-guide/stub-files', ))->build(); @@ -207,19 +314,32 @@ private function findPrototype(ClassReflection $classReflection, string $propert { $parentClass = $classReflection->getParentClass(); if ($parentClass === null) { - return null; + return $this->findPrototypeInInterfaces($classReflection, $propertyName); } if (!$parentClass->hasNativeProperty($propertyName)) { - return null; + return $this->findPrototypeInInterfaces($classReflection, $propertyName); } $property = $parentClass->getNativeProperty($propertyName); if ($property->isPrivate()) { - return null; + return $this->findPrototypeInInterfaces($classReflection, $propertyName); } return $property; } + private function findPrototypeInInterfaces(ClassReflection $classReflection, string $propertyName): ?PhpPropertyReflection + { + foreach ($classReflection->getInterfaces() as $interface) { + if (!$interface->hasNativeProperty($propertyName)) { + continue; + } + + return $interface->getNativeProperty($propertyName); + } + + return null; + } + } diff --git a/src/Rules/Properties/PropertiesInInterfaceRule.php b/src/Rules/Properties/PropertiesInInterfaceRule.php new file mode 100644 index 0000000000..6126625da6 --- /dev/null +++ b/src/Rules/Properties/PropertiesInInterfaceRule.php @@ -0,0 +1,132 @@ + + */ +final class PropertiesInInterfaceRule 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->getClassReflection()->isInterface()) { + return []; + } + + if (!$this->phpVersion->supportsPropertyHooks()) { + return [ + RuleErrorBuilder::message('Interfaces can include properties only on PHP 8.4 and later.') + ->nonIgnorable() + ->identifier('property.inInterface') + ->build(), + ]; + } + + if (!$node->hasHooks()) { + return [ + RuleErrorBuilder::message('Interfaces can only include hooked properties.') + ->nonIgnorable() + ->identifier('property.nonHookedInInterface') + ->build(), + ]; + } + + if (!$node->isPublic()) { + return [ + RuleErrorBuilder::message('Interfaces cannot include non-public properties.') + ->nonIgnorable() + ->identifier('property.nonPublicInInterface') + ->build(), + ]; + } + + if ($node->isReadOnly()) { + return [ + RuleErrorBuilder::message('Interfaces cannot include readonly hooked properties.') + ->nonIgnorable() + ->identifier('property.readOnlyInInterface') + ->build(), + ]; + } + + if ($node->isStatic()) { + return [ + RuleErrorBuilder::message('Hooked properties cannot be static.') + ->nonIgnorable() + ->identifier('property.hookedStatic') + ->build(), + ]; + } + + if ($node->isAbstract()) { + return [ + RuleErrorBuilder::message('Property in interface cannot be explicitly abstract.') + ->nonIgnorable() + ->identifier('property.abstractInInterface') + ->build(), + ]; + } + + if ($node->isFinal()) { + return [ + RuleErrorBuilder::message('Interfaces cannot include final properties.') + ->nonIgnorable() + ->identifier('property.finalInInterface') + ->build(), + ]; + } + + foreach ($node->getHooks() as $hook) { + if (!$hook->isFinal()) { + continue; + } + + return [ + RuleErrorBuilder::message('Property hook cannot be both abstract and final.') + ->nonIgnorable() + ->identifier('property.abstractFinalHook') + ->build(), + ]; + } + + if ($this->hasAnyHookBody($node)) { + return [ + RuleErrorBuilder::message('Interfaces cannot include property hooks with bodies.') + ->nonIgnorable() + ->identifier('property.hookBodyInInterface') + ->build(), + ]; + } + + return []; + } + + private function hasAnyHookBody(ClassPropertyNode $node): bool + { + foreach ($node->getHooks() as $hook) { + if ($hook->body !== null) { + return true; + } + } + + return false; + } + +} diff --git a/src/Rules/Properties/PropertyAssignRefRule.php b/src/Rules/Properties/PropertyAssignRefRule.php new file mode 100644 index 0000000000..f6d3cc0cd0 --- /dev/null +++ b/src/Rules/Properties/PropertyAssignRefRule.php @@ -0,0 +1,71 @@ + + */ +final class PropertyAssignRefRule implements Rule +{ + + public function __construct( + private PhpVersion $phpVersion, + private PropertyReflectionFinder $propertyReflectionFinder, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\AssignRef::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsAsymmetricVisibility()) { + return []; + } + + if (!$node->expr instanceof Node\Expr\PropertyFetch) { + return []; + } + + $propertyFetch = $node->expr; + + $errors = []; + $reflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); + foreach ($reflections as $propertyReflection) { + $nativeReflection = $propertyReflection->getNativeReflection(); + if ($nativeReflection === null) { + continue; + } + if ($scope->canWriteProperty($propertyReflection)) { + continue; + } + + $declaringClass = $nativeReflection->getDeclaringClass(); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s with %s visibility is assigned by reference.', + $declaringClass->getDisplayName(), + $propertyReflection->getName(), + $propertyReflection->isPrivateSet() ? 'private(set)' : ( + $propertyReflection->isProtectedSet() ? 'protected(set)' : ( + $propertyReflection->isPrivate() ? 'private' : 'protected' + ) + ), + )) + ->identifier('property.assignByRef') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/PropertyAttributesRule.php b/src/Rules/Properties/PropertyAttributesRule.php index b0d9cf2812..375eb439cf 100644 --- a/src/Rules/Properties/PropertyAttributesRule.php +++ b/src/Rules/Properties/PropertyAttributesRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class PropertyAttributesRule implements Rule +final class PropertyAttributesRule implements Rule { public function __construct(private AttributesCheck $attributesCheck) diff --git a/src/Rules/Properties/PropertyDescriptor.php b/src/Rules/Properties/PropertyDescriptor.php index 4bd700c718..8588a1a57a 100644 --- a/src/Rules/Properties/PropertyDescriptor.php +++ b/src/Rules/Properties/PropertyDescriptor.php @@ -3,33 +3,39 @@ namespace PHPStan\Rules\Properties; use PhpParser\Node; +use PHPStan\Analyser\Scope; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Type\ObjectType; +use PHPStan\Type\VerbosityLevel; use function sprintf; -class PropertyDescriptor +final class PropertyDescriptor { - public function describePropertyByName(PropertyReflection $property, string $propertyName): string - { - if (!$property->isStatic()) { - return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); - } - - return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); - } - /** * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - public function describeProperty(PropertyReflection $property, $propertyFetch): string + public function describeProperty(PropertyReflection $property, Scope $scope, $propertyFetch): string { + if ($propertyFetch instanceof Node\Expr\PropertyFetch) { + $fetchedOnType = $scope->getType($propertyFetch->var); + $declaringClassType = new ObjectType($property->getDeclaringClass()->getName()); + if ($declaringClassType->isSuperTypeOf($fetchedOnType)->yes()) { + $classDescription = $property->getDeclaringClass()->getDisplayName(); + } else { + $classDescription = $fetchedOnType->describe(VerbosityLevel::typeOnly()); + } + } else { + $classDescription = $property->getDeclaringClass()->getDisplayName(); + } + /** @var Node\Identifier $name */ $name = $propertyFetch->name; if (!$property->isStatic()) { - return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $name->name); + return sprintf('Property %s::$%s', $classDescription, $name->name); } - return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $name->name); + return sprintf('Static property %s::$%s', $classDescription, $name->name); } } diff --git a/src/Rules/Properties/PropertyHookAttributesRule.php b/src/Rules/Properties/PropertyHookAttributesRule.php new file mode 100644 index 0000000000..bd4968e8bf --- /dev/null +++ b/src/Rules/Properties/PropertyHookAttributesRule.php @@ -0,0 +1,37 @@ + + */ +final class PropertyHookAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return InPropertyHookNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->attributesCheck->check( + $scope, + $node->getOriginalNode()->attrGroups, + Attribute::TARGET_METHOD, + 'method', + ); + } + +} diff --git a/src/Rules/Properties/PropertyInClassRule.php b/src/Rules/Properties/PropertyInClassRule.php new file mode 100644 index 0000000000..18190223b2 --- /dev/null +++ b/src/Rules/Properties/PropertyInClassRule.php @@ -0,0 +1,215 @@ + + */ +final class PropertyInClassRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + if (!$classReflection->isClass()) { + return []; + } + + if ( + $node->isFinal() + && !$this->phpVersion->supportsFinalProperties() + ) { + return [ + RuleErrorBuilder::message('Final properties are supported only on PHP 8.4 and later.') + ->nonIgnorable() + ->identifier('property.final') + ->build(), + ]; + } + + if (!$this->phpVersion->supportsPropertyHooks()) { + if ($node->hasHooks()) { + return [ + RuleErrorBuilder::message('Property hooks are supported only on PHP 8.4 and later.') + ->nonIgnorable() + ->identifier('property.hooksNotSupported') + ->build(), + ]; + } + + return []; + } + + if ($node->isAbstract()) { + if (!$node->hasHooks()) { + return [ + RuleErrorBuilder::message('Only hooked properties can be declared abstract.') + ->nonIgnorable() + ->identifier('property.abstractNonHooked') + ->build(), + ]; + } + + if (!$this->isAtLeastOneHookBodyEmpty($node)) { + return [ + RuleErrorBuilder::message('Abstract properties must specify at least one abstract hook.') + ->nonIgnorable() + ->identifier('property.abstractWithoutAbstractHook') + ->build(), + ]; + } + + if (!$classReflection->isAbstract()) { + return [ + RuleErrorBuilder::message('Non-abstract classes cannot include abstract properties.') + ->nonIgnorable() + ->identifier('property.abstract') + ->build(), + ]; + } + } elseif (!$this->doAllHooksHaveBody($node)) { + return [ + RuleErrorBuilder::message('Non-abstract properties cannot include hooks without bodies.') + ->nonIgnorable() + ->identifier('property.hookWithoutBody') + ->build(), + ]; + } + + if ($node->isPrivate()) { + if ($node->isAbstract()) { + return [ + RuleErrorBuilder::message('Property cannot be both abstract and private.') + ->nonIgnorable() + ->identifier('property.abstractPrivate') + ->build(), + ]; + } + + if ($node->isFinal()) { + return [ + RuleErrorBuilder::message('Property cannot be both final and private.') + ->nonIgnorable() + ->identifier('property.finalPrivate') + ->build(), + ]; + } + + foreach ($node->getHooks() as $hook) { + if (!$hook->isFinal()) { + continue; + } + + return [ + RuleErrorBuilder::message('Private property cannot have a final hook.') + ->nonIgnorable() + ->identifier('property.finalPrivateHook') + ->build(), + ]; + } + } + + if ($node->isAbstract()) { + if ($node->isFinal()) { + return [ + RuleErrorBuilder::message('Property cannot be both abstract and final.') + ->nonIgnorable() + ->identifier('property.abstractFinal') + ->build(), + ]; + } + + foreach ($node->getHooks() as $hook) { + if ($hook->body !== null) { + continue; + } + + if (!$hook->isFinal()) { + continue; + } + + return [ + RuleErrorBuilder::message('Property cannot be both abstract and final.') + ->nonIgnorable() + ->identifier('property.abstractFinal') + ->build(), + ]; + } + } + + if ($node->isReadOnly()) { + if ($node->hasHooks()) { + return [ + RuleErrorBuilder::message('Hooked properties cannot be readonly.') + ->nonIgnorable() + ->identifier('property.hookReadOnly') + ->build(), + ]; + } + } + + if ($node->isStatic()) { + if ($node->hasHooks()) { + return [ + RuleErrorBuilder::message('Hooked properties cannot be static.') + ->nonIgnorable() + ->identifier('property.hookedStatic') + ->build(), + ]; + } + } + + if ($node->isVirtual()) { + if ($node->getDefault() !== null) { + return [ + RuleErrorBuilder::message('Virtual hooked properties cannot have a default value.') + ->nonIgnorable() + ->identifier('property.virtualDefault') + ->build(), + ]; + } + } + + return []; + } + + private function doAllHooksHaveBody(ClassPropertyNode $node): bool + { + foreach ($node->getHooks() as $hook) { + if ($hook->body === null) { + return false; + } + } + + return true; + } + + private function isAtLeastOneHookBodyEmpty(ClassPropertyNode $node): bool + { + foreach ($node->getHooks() as $hook) { + if ($hook->body === null) { + return true; + } + } + + return false; + } + +} diff --git a/src/Rules/Properties/PropertyReflectionFinder.php b/src/Rules/Properties/PropertyReflectionFinder.php index 1dd0cc4748..67b2785fa9 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -9,10 +9,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; use function array_map; +use function count; -class PropertyReflectionFinder +final class PropertyReflectionFinder { /** @@ -25,7 +25,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a if ($propertyFetch->name instanceof Node\Identifier) { $names = [$propertyFetch->name->name]; } else { - $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), $scope->getType($propertyFetch->name)->getConstantStrings()); } $reflections = []; @@ -58,7 +58,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a if ($propertyFetch->name instanceof VarLikeIdentifier) { $names = [$propertyFetch->name->name]; } else { - $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), $scope->getType($propertyFetch->name)->getConstantStrings()); } $reflections = []; @@ -87,11 +87,18 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?FoundPropertyReflection { 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 Node\Identifier) { + return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + } + + $nameType = $scope->getType($propertyFetch->name); + $nameTypeConstantStrings = $nameType->getConstantStrings(); + if (count($nameTypeConstantStrings) === 1) { + return $this->findPropertyReflection($propertyHolderType, $nameTypeConstantStrings[0]->getValue(), $scope); + } + + return null; } if (!$propertyFetch->name instanceof Node\Identifier) { diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php new file mode 100644 index 0000000000..52637bb509 --- /dev/null +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php @@ -0,0 +1,57 @@ + + */ +final class ReadOnlyByPhpDocPropertyAssignRefRule 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->canWriteProperty($propertyReflection)) { + continue; + } + if (!$nativeReflection->isReadOnlyByPhpDoc() || $nativeReflection->isReadOnly()) { + continue; + } + + $declaringClass = $nativeReflection->getDeclaringClass(); + $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned by reference.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyByPhpDocAssignByRef') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php new file mode 100644 index 0000000000..4c475096ef --- /dev/null +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php @@ -0,0 +1,129 @@ + + */ +final class ReadOnlyByPhpDocPropertyAssignRule implements Rule +{ + + public function __construct( + private PropertyReflectionFinder $propertyReflectionFinder, + private ConstructorsHelper $constructorsHelper, + ) + { + } + + 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 []; + } + + $inFunction = $scope->getFunction(); + if ( + $inFunction instanceof PhpMethodFromParserNodeReflection + && $inFunction->isPropertyHook() + && $propertyFetch->var instanceof Node\Expr\Variable + && $propertyFetch->var->name === 'this' + && $propertyFetch->name instanceof Node\Identifier + && $inFunction->getHookedPropertyName() === $propertyFetch->name->toString() + ) { + return []; + } + + $errors = []; + $reflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); + foreach ($reflections as $propertyReflection) { + $nativeReflection = $propertyReflection->getNativeReflection(); + if ($nativeReflection === null) { + continue; + } + if (!$scope->canWriteProperty($propertyReflection)) { + continue; + } + if (!$nativeReflection->isReadOnlyByPhpDoc() || $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())) + ->identifier('property.readOnlyByPhpDocAssignOutOfClass') + ->build(); + continue; + } + + $scopeClassReflection = $scope->getClassReflection(); + if ($scopeClassReflection->getName() !== $declaringClass->getName()) { + $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyByPhpDocAssignOutOfClass') + ->build(); + continue; + } + + $scopeMethod = $scope->getFunction(); + if (!$scopeMethod instanceof MethodReflection) { + throw new ShouldNotHappenException(); + } + + if ( + in_array($scopeMethod->getName(), $this->constructorsHelper->getConstructors($scopeClassReflection), true) + || strtolower($scopeMethod->getName()) === '__unserialize' + ) { + if (TypeUtils::findThisType($scope->getType($propertyFetch->var)) === null) { + $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is not assigned on $this.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyByPhpDocAssignNotOnThis') + ->build(); + } + + continue; + } + + if ($nativeReflection->isAllowedPrivateMutation()) { + continue; + } + + $assignedExpr = $node->getAssignedExpr(); + if ( + ($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr) + && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes() + ) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyByPhpDocAssignNotInConstructor') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php new file mode 100644 index 0000000000..93e7a0fb4d --- /dev/null +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php @@ -0,0 +1,38 @@ + + */ +final class ReadOnlyByPhpDocPropertyRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->isReadOnlyByPhpDoc() && !$node->isAllowedPrivateMutation()) || $node->isReadOnly()) { + return []; + } + + $errors = []; + if ($node->getDefault() !== null) { + $errors[] = RuleErrorBuilder::message('@readonly property cannot have a default value.') + ->identifier('property.readOnlyByPhpDocDefaultValue') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php index 97c5686c69..d5079dc353 100644 --- a/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php @@ -11,7 +11,7 @@ /** * @implements Rule */ -class ReadOnlyPropertyAssignRefRule implements Rule +final class ReadOnlyPropertyAssignRefRule implements Rule { public function __construct(private PropertyReflectionFinder $propertyReflectionFinder) @@ -25,7 +25,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->expr instanceof Node\Expr\PropertyFetch && !$node->expr instanceof Node\Expr\StaticPropertyFetch) { + if (!$node->expr instanceof Node\Expr\PropertyFetch) { return []; } @@ -38,7 +38,7 @@ public function processNode(Node $node, Scope $scope): array if ($nativeReflection === null) { continue; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canWriteProperty($propertyReflection)) { continue; } if (!$nativeReflection->isReadOnly()) { @@ -46,7 +46,9 @@ public function processNode(Node $node, Scope $scope): array } $declaringClass = $nativeReflection->getDeclaringClass(); - $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned by reference.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned by reference.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignByRef') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php index e63953bc1d..eac07303a2 100644 --- a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php @@ -2,24 +2,33 @@ namespace PHPStan\Rules\Properties; +use ArrayAccess; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\SetOffsetValueTypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Node\PropertyAssignNode; +use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ThisType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\TypeUtils; +use function in_array; use function sprintf; use function strtolower; /** * @implements Rule */ -class ReadOnlyPropertyAssignRule implements Rule +final class ReadOnlyPropertyAssignRule implements Rule { - public function __construct(private PropertyReflectionFinder $propertyReflectionFinder) + public function __construct( + private PropertyReflectionFinder $propertyReflectionFinder, + private ConstructorsHelper $constructorsHelper, + ) { } @@ -42,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array if ($nativeReflection === null) { continue; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canWriteProperty($propertyReflection)) { continue; } if (!$nativeReflection->isReadOnly()) { @@ -52,13 +61,17 @@ public function processNode(Node $node, Scope $scope): array $declaringClass = $nativeReflection->getDeclaringClass(); if (!$scope->isInClass()) { - $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignOutOfClass') + ->build(); continue; } $scopeClassReflection = $scope->getClassReflection(); if ($scopeClassReflection->getName() !== $declaringClass->getName()) { - $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignOutOfClass') + ->build(); continue; } @@ -67,15 +80,30 @@ public function processNode(Node $node, Scope $scope): array 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(); + if ( + in_array($scopeMethod->getName(), $this->constructorsHelper->getConstructors($scopeClassReflection), true) + || strtolower($scopeMethod->getName()) === '__unserialize' + ) { + if (TypeUtils::findThisType($scope->getType($propertyFetch->var)) === null) { + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is not assigned on $this.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignNotOnThis') + ->build(); } continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $assignedExpr = $node->getAssignedExpr(); + if ( + ($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr) + && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes() + ) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignNotInConstructor') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadOnlyPropertyRule.php b/src/Rules/Properties/ReadOnlyPropertyRule.php index 5c80d413ea..5e777ae164 100644 --- a/src/Rules/Properties/ReadOnlyPropertyRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class ReadOnlyPropertyRule implements Rule +final class ReadOnlyPropertyRule implements Rule { public function __construct(private PhpVersion $phpVersion) @@ -32,15 +32,28 @@ public function processNode(Node $node, Scope $scope): array $errors = []; if (!$this->phpVersion->supportsReadOnlyProperties()) { - $errors[] = RuleErrorBuilder::message('Readonly properties are supported only on PHP 8.1 and later.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly properties are supported only on PHP 8.1 and later.')->nonIgnorable() + ->identifier('property.readOnlyNotSupported') + ->build(); } if ($node->getNativeType() === null) { - $errors[] = RuleErrorBuilder::message('Readonly property must have a native type.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly property must have a native type.') + ->identifier('property.readOnlyNoNativeType') + ->nonIgnorable() + ->build(); } if ($node->getDefault() !== null) { - $errors[] = RuleErrorBuilder::message('Readonly property cannot have a default value.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly property cannot have a default value.')->nonIgnorable() + ->identifier('property.readOnlyDefaultValue') + ->build(); + } + + if ($node->isStatic()) { + $errors[] = RuleErrorBuilder::message('Readonly property cannot be static.')->nonIgnorable() + ->identifier('property.readOnlyStatic') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadWritePropertiesExtension.php b/src/Rules/Properties/ReadWritePropertiesExtension.php index b8f26e60c7..804619781c 100644 --- a/src/Rules/Properties/ReadWritePropertiesExtension.php +++ b/src/Rules/Properties/ReadWritePropertiesExtension.php @@ -2,16 +2,33 @@ namespace PHPStan\Rules\Properties; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; -/** @api */ +/** + * This is the extension interface to implement if you want to describe + * always-read or always-written properties. + * + * To register it in the configuration file use the `phpstan.properties.readWriteExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.properties.readWriteExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/always-read-written-properties + * + * @api + */ interface ReadWritePropertiesExtension { - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool; + public function isAlwaysRead(ExtendedPropertyReflection $property, string $propertyName): bool; - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool; + public function isAlwaysWritten(ExtendedPropertyReflection $property, string $propertyName): bool; - public function isInitialized(PropertyReflection $property, string $propertyName): bool; + public function isInitialized(ExtendedPropertyReflection $property, string $propertyName): bool; } diff --git a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php index 315be25535..a3ce3325f0 100644 --- a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php +++ b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php @@ -12,7 +12,7 @@ /** * @implements Rule */ -class ReadingWriteOnlyPropertiesRule implements Rule +final class ReadingWriteOnlyPropertiesRule implements Rule { public function __construct( @@ -54,18 +54,20 @@ public function processNode(Node $node, Scope $scope): array if ($propertyReflection === null) { return []; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canReadProperty($propertyReflection)) { return []; } if (!$propertyReflection->isReadable()) { - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $node); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $node); return [ RuleErrorBuilder::message(sprintf( '%s is not readable.', $propertyDescription, - ))->build(), + )) + ->identifier('property.writeOnly') + ->build(), ]; } diff --git a/src/Rules/Properties/SetNonVirtualPropertyHookAssignRule.php b/src/Rules/Properties/SetNonVirtualPropertyHookAssignRule.php new file mode 100644 index 0000000000..aeedaeb4a9 --- /dev/null +++ b/src/Rules/Properties/SetNonVirtualPropertyHookAssignRule.php @@ -0,0 +1,93 @@ + + */ +final class SetNonVirtualPropertyHookAssignRule implements Rule +{ + + public function getNodeType(): string + { + return PropertyHookReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $hookNode = $node->getPropertyHookNode(); + if ($hookNode->name->toLowerString() !== 'set') { + return []; + } + + $hookReflection = $node->getHookReflection(); + if (!$hookReflection->isPropertyHook()) { + throw new ShouldNotHappenException(); + } + + $propertyName = $hookReflection->getHookedPropertyName(); + $classReflection = $node->getClassReflection(); + $propertyReflection = $node->getPropertyReflection(); + if ($propertyReflection->isVirtual()->yes()) { + return []; + } + + $finalHookScope = null; + foreach ($node->getExecutionEnds() as $executionEnd) { + $statementResult = $executionEnd->getStatementResult(); + $endNode = $executionEnd->getNode(); + if ($statementResult->isAlwaysTerminating()) { + if ($endNode instanceof Node\Stmt\Expression) { + $exprType = $statementResult->getScope()->getType($endNode->expr); + if ($exprType instanceof NeverType && $exprType->isExplicit()) { + continue; + } + } + } + if ($finalHookScope === null) { + $finalHookScope = $statementResult->getScope(); + continue; + } + + $finalHookScope = $finalHookScope->mergeWith($statementResult->getScope()); + } + + foreach ($node->getReturnStatements() as $returnStatement) { + if ($finalHookScope === null) { + $finalHookScope = $returnStatement->getScope(); + continue; + } + $finalHookScope = $finalHookScope->mergeWith($returnStatement->getScope()); + } + + if ($finalHookScope === null) { + return []; + } + + $initExpr = new PropertyInitializationExpr($propertyName); + $hasInit = $finalHookScope->hasExpressionType($initExpr); + if ($hasInit->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Set hook for non-virtual property %s::$%s does not %sassign value to it.', + $classReflection->getDisplayName(), + $propertyName, + $hasInit->maybe() ? 'always ' : '', + ))->identifier('propertySetHook.noAssign')->build(), + ]; + } + +} diff --git a/src/Rules/Properties/SetPropertyHookParameterRule.php b/src/Rules/Properties/SetPropertyHookParameterRule.php new file mode 100644 index 0000000000..e8de30667e --- /dev/null +++ b/src/Rules/Properties/SetPropertyHookParameterRule.php @@ -0,0 +1,157 @@ + + */ +final class SetPropertyHookParameterRule implements Rule +{ + + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + private bool $checkPhpDocMethodSignatures, + private bool $checkMissingTypehints, + ) + { + } + + public function getNodeType(): string + { + return InPropertyHookNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $hookReflection = $node->getHookReflection(); + if (!$hookReflection->isPropertyHook()) { + return []; + } + + if ($hookReflection->getPropertyHookName() !== 'set') { + return []; + } + + $propertyReflection = $node->getPropertyReflection(); + $parameters = $hookReflection->getParameters(); + if (!isset($parameters[0])) { + throw new ShouldNotHappenException(); + } + + $classReflection = $node->getClassReflection(); + + $errors = []; + $parameter = $parameters[0]; + if (!$propertyReflection->hasNativeType()) { + if ($parameter->hasNativeType()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Parameter $%s of set hook has a native type but the property %s::$%s does not.', + $parameter->getName(), + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + ))->identifier('propertySetHook.nativeParameterType') + ->nonIgnorable() + ->build(); + } + } elseif (!$parameter->hasNativeType()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Parameter $%s of set hook does not have a native type but the property %s::$%s does.', + $parameter->getName(), + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + ))->identifier('propertySetHook.nativeParameterType') + ->nonIgnorable() + ->build(); + } else { + if (!$parameter->getNativeType()->isSuperTypeOf($propertyReflection->getNativeType())->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Native type %s of set hook parameter $%s is not contravariant with native type %s of property %s::$%s.', + $parameter->getNativeType()->describe(VerbosityLevel::typeOnly()), + $parameter->getName(), + $propertyReflection->getNativeType()->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + ))->identifier('propertySetHook.nativeParameterType') + ->nonIgnorable() + ->build(); + } + } + + if (!$this->checkPhpDocMethodSignatures || count($errors) > 0) { + return $errors; + } + + $parameterType = $parameter->getType(); + + if (!$parameterType->isSuperTypeOf($propertyReflection->getReadableType())->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Type %s of set hook parameter $%s is not contravariant with type %s of property %s::$%s.', + $parameterType->describe(VerbosityLevel::value()), + $parameter->getName(), + $propertyReflection->getReadableType()->describe(VerbosityLevel::value()), + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + ))->identifier('propertySetHook.parameterType') + ->build(); + } + + if (!$this->checkMissingTypehints) { + return $errors; + } + + if ($parameter->getNativeType()->equals($propertyReflection->getReadableType())) { + return $errors; + } + + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Set hook for property %s::$%s has parameter $%s with no value type specified in iterable type %s.', + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $parameter->getName(), + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Set hook for property %s::$%s has parameter $%s with generic %s but does not specify its types: %s', + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $parameter->getName(), + $name, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Set hook for property %s::$%s has parameter $%s with no signature specified for %s.', + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $parameter->getName(), + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/TypesAssignedToPropertiesRule.php b/src/Rules/Properties/TypesAssignedToPropertiesRule.php index e86865cb29..50d1502401 100644 --- a/src/Rules/Properties/TypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/TypesAssignedToPropertiesRule.php @@ -3,25 +3,29 @@ namespace PHPStan\Rules\Properties; use PhpParser\Node; +use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Analyser\Scope; use PHPStan\Node\PropertyAssignNode; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use PHPStan\Reflection\PropertyReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\VerbosityLevel; use function array_merge; +use function is_string; use function sprintf; /** * @implements Rule */ -class TypesAssignedToPropertiesRule implements Rule +final class TypesAssignedToPropertiesRule implements Rule { public function __construct( private RuleLevelHelper $ruleLevelHelper, - private PropertyDescriptor $propertyDescriptor, private PropertyReflectionFinder $propertyReflectionFinder, ) { @@ -34,12 +38,14 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($node->getPropertyFetch(), $scope); + $propertyFetch = $node->getPropertyFetch(); + $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); $errors = []; foreach ($propertyReflections as $propertyReflection) { $errors = array_merge($errors, $this->processSingleProperty( $propertyReflection, + $propertyFetch, $node->getAssignedExpr(), )); } @@ -48,19 +54,40 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processSingleProperty( FoundPropertyReflection $propertyReflection, + PropertyFetch|StaticPropertyFetch $fetch, Node\Expr $assignedExpr, ): array { - $propertyType = $propertyReflection->getWritableType(); + if (!$propertyReflection->isWritable()) { + return []; + } + $scope = $propertyReflection->getScope(); + $inFunction = $scope->getFunction(); + if ( + $fetch instanceof PropertyFetch + && $fetch->var instanceof Node\Expr\Variable + && is_string($fetch->var->name) + && $fetch->var->name === 'this' + && $fetch->name instanceof Node\Identifier + && $inFunction instanceof PhpMethodFromParserNodeReflection + && $inFunction->isPropertyHook() + && $inFunction->getHookedPropertyName() === $fetch->name->toString() + ) { + $propertyType = $propertyReflection->getReadableType(); + } else { + $propertyType = $propertyReflection->getWritableType(); + } + $assignedValueType = $scope->getType($assignedExpr); - if (!$this->ruleLevelHelper->accepts($propertyType, $assignedValueType, $scope->isDeclareStrictTypes())) { - $propertyDescription = $this->propertyDescriptor->describePropertyByName($propertyReflection, $propertyReflection->getName()); + $accepts = $this->ruleLevelHelper->accepts($propertyType, $assignedValueType, $scope->isDeclareStrictTypes()); + if (!$accepts->result) { + $propertyDescription = $this->describePropertyByName($propertyReflection, $propertyReflection->getName()); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedValueType); return [ @@ -69,11 +96,23 @@ private function processSingleProperty( $propertyDescription, $propertyType->describe($verbosityLevel), $assignedValueType->describe($verbosityLevel), - ))->build(), + )) + ->identifier('assign.propertyType') + ->acceptsReasonsTip($accepts->reasons) + ->build(), ]; } return []; } + private function describePropertyByName(PropertyReflection $property, string $propertyName): string + { + if (!$property->isStatic()) { + return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); + } + + return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); + } + } diff --git a/src/Rules/Properties/UninitializedPropertyRule.php b/src/Rules/Properties/UninitializedPropertyRule.php index 538bd5ef55..525a9ecbb6 100644 --- a/src/Rules/Properties/UninitializedPropertyRule.php +++ b/src/Rules/Properties/UninitializedPropertyRule.php @@ -5,30 +5,19 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertiesNode; -use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstructorsHelper; 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 */ -class UninitializedPropertyRule implements Rule +final class UninitializedPropertyRule implements Rule { - /** @var array */ - private array $additionalConstructorsCache = []; - - /** - * @param string[] $additionalConstructors - */ public function __construct( - private ReadWritePropertiesExtensionProvider $extensionProvider, - private array $additionalConstructors, + private ConstructorsHelper $constructorsHelper, ) { } @@ -40,78 +29,40 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); - [$properties, $prematureAccess] = $node->getUninitializedProperties($scope, $this->getConstructors($classReflection), $this->extensionProvider->getExtensions()); + $classReflection = $node->getClassReflection(); + [$properties, $prematureAccess] = $node->getUninitializedProperties($scope, $this->constructorsHelper->getConstructors($classReflection)); $errors = []; foreach ($properties as $propertyName => $propertyNode) { - if ($propertyNode->isReadOnly()) { + if ($propertyNode->isReadOnly() || $propertyNode->isReadOnlyByPhpDoc()) { 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, - ))->line($propertyNode->getLine())->build(); + )) + ->line($propertyNode->getStartLine()) + ->identifier('property.uninitialized') + ->build(); } - foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { - if ($propertyNode->isReadOnly()) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { + if ($propertyNode->isReadOnly() || $propertyNode->isReadOnlyByPhpDoc()) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( 'Access to an uninitialized property %s::$%s.', $classReflection->getDisplayName(), $propertyName, - ))->line($line)->build(); + )) + ->line($line) + ->file($file, $fileDescription) + ->identifier('property.uninitialized') + ->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/WritingToReadOnlyPropertiesRule.php b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php index f25cd413c7..137caf24ce 100644 --- a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php +++ b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php @@ -4,15 +4,16 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use function sprintf; /** - * @implements Rule + * @implements Rule */ -class WritingToReadOnlyPropertiesRule implements Rule +final class WritingToReadOnlyPropertiesRule implements Rule { public function __construct( @@ -26,53 +27,37 @@ public function __construct( public function getNodeType(): string { - return Node\Expr::class; + return PropertyAssignNode::class; } public function processNode(Node $node, Scope $scope): array { + $propertyFetch = $node->getPropertyFetch(); if ( - !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignOp - && !$node instanceof Node\Expr\AssignRef - ) { - return []; - } - - if ( - !($node->var instanceof Node\Expr\PropertyFetch) - && !($node->var instanceof Node\Expr\StaticPropertyFetch) - ) { - return []; - } - - if ( - $node->var instanceof Node\Expr\PropertyFetch + $propertyFetch instanceof Node\Expr\PropertyFetch && $this->checkThisOnly - && !$this->ruleLevelHelper->isThis($node->var->var) + && !$this->ruleLevelHelper->isThis($propertyFetch->var) ) { return []; } - /** @var Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - $propertyFetch = $node->var; $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $scope); if ($propertyReflection === null) { return []; } - if (!$scope->canAccessProperty($propertyReflection)) { + if (!$scope->canWriteProperty($propertyReflection)) { return []; } if (!$propertyReflection->isWritable()) { - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $propertyFetch); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $propertyFetch); return [ RuleErrorBuilder::message(sprintf( '%s is not writable.', $propertyDescription, - ))->build(), + ))->identifier('assign.propertyReadOnly')->build(), ]; } diff --git a/src/Rules/Pure/FunctionPurityCheck.php b/src/Rules/Pure/FunctionPurityCheck.php new file mode 100644 index 0000000000..817b6a3273 --- /dev/null +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -0,0 +1,143 @@ + + */ + public function check( + string $functionDescription, + string $identifier, + FunctionReflection|ExtendedMethodReflection $functionReflection, + array $parameters, + Type $returnType, + array $impurePoints, + array $throwPoints, + array $statements, + bool $isConstructor, + ): array + { + $errors = []; + $isPure = $functionReflection->isPure(); + + if ($isPure->yes()) { + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked as pure but parameter $%s is passed by reference.', + $functionDescription, + $parameter->getName(), + ))->identifier(sprintf('pure%s.parameterByRef', $identifier))->build(); + } + + if ($returnType->isVoid()->yes() && !$isConstructor) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked as pure but returns void.', + $functionDescription, + ))->identifier(sprintf('pure%s.void', $identifier))->build(); + } + + foreach ($impurePoints as $impurePoint) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s in pure %s.', + $impurePoint->isCertain() ? 'Impure' : 'Possibly impure', + $impurePoint->getDescription(), + lcfirst($functionDescription), + )) + ->line($impurePoint->getNode()->getStartLine()) + ->identifier(sprintf( + '%s.%s', + $impurePoint->isCertain() ? 'impure' : 'possiblyImpure', + $impurePoint->getIdentifier(), + )) + ->build(); + } + } elseif ($isPure->no()) { + if ( + count($throwPoints) === 0 + && count($impurePoints) === 0 + && count($functionReflection->getAsserts()->getAll()) === 0 + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked as impure but does not have any side effects.', + $functionDescription, + ))->identifier(sprintf('impure%s.pure', $identifier))->build(); + } + } elseif ($returnType->isVoid()->yes()) { + if ( + count($throwPoints) === 0 + && count($impurePoints) === 0 + && !$isConstructor + && (!$functionReflection instanceof ExtendedMethodReflection || $functionReflection->isPrivate()) + && count($functionReflection->getAsserts()->getAll()) === 0 + ) { + $hasByRef = false; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + $hasByRef = true; + break; + } + + $statements = array_filter($statements, static function (Stmt $stmt): bool { + if ($stmt instanceof Stmt\Nop) { + return false; + } + + if (!$stmt instanceof Stmt\Expression) { + return true; + } + if (!$stmt->expr instanceof FuncCall) { + return true; + } + if (!$stmt->expr->name instanceof Name) { + return true; + } + + return !in_array($stmt->expr->name->toString(), CallToFunctionStatementWithoutSideEffectsRule::PHPSTAN_TESTING_FUNCTIONS, true); + }); + + if (!$hasByRef && count($statements) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s returns void but does not have any side effects.', + $functionDescription, + ))->identifier('void.pure')->build(); + } + } + } + + return $errors; + } + +} diff --git a/src/Rules/Pure/PureFunctionRule.php b/src/Rules/Pure/PureFunctionRule.php new file mode 100644 index 0000000000..e05a0be902 --- /dev/null +++ b/src/Rules/Pure/PureFunctionRule.php @@ -0,0 +1,43 @@ + + */ +final class PureFunctionRule implements Rule +{ + + public function __construct(private FunctionPurityCheck $check) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $function = $node->getFunctionReflection(); + + return $this->check->check( + sprintf('Function %s()', $function->getName()), + 'Function', + $function, + $function->getParameters(), + $function->getReturnType(), + $node->getImpurePoints(), + $node->getStatementResult()->getThrowPoints(), + $node->getStatements(), + false, + ); + } + +} diff --git a/src/Rules/Pure/PureMethodRule.php b/src/Rules/Pure/PureMethodRule.php new file mode 100644 index 0000000000..8ef6f87c66 --- /dev/null +++ b/src/Rules/Pure/PureMethodRule.php @@ -0,0 +1,43 @@ + + */ +final class PureMethodRule implements Rule +{ + + public function __construct(private FunctionPurityCheck $check) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + + return $this->check->check( + sprintf('Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), + 'Method', + $method, + $method->getParameters(), + $method->getReturnType(), + $node->getImpurePoints(), + $node->getStatementResult()->getThrowPoints(), + $node->getStatements(), + $method->isConstructor(), + ); + } + +} diff --git a/src/Rules/Regexp/RegularExpressionPatternRule.php b/src/Rules/Regexp/RegularExpressionPatternRule.php index 124d75a8b5..e15bdeb13b 100644 --- a/src/Rules/Regexp/RegularExpressionPatternRule.php +++ b/src/Rules/Regexp/RegularExpressionPatternRule.php @@ -9,8 +9,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\Regex\RegexExpressionHelper; use function in_array; use function sprintf; use function str_starts_with; @@ -19,9 +18,15 @@ /** * @implements Rule */ -class RegularExpressionPatternRule implements Rule +final class RegularExpressionPatternRule implements Rule { + public function __construct( + private RegexExpressionHelper $regexExpressionHelper, + ) + { + } + public function getNodeType(): string { return FuncCall::class; @@ -38,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->identifier('regexp.pattern')->build(); } return $errors; @@ -65,51 +70,48 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array $patternStrings = []; - foreach (TypeUtils::getConstantStrings($patternType) as $constantStringType) { - if ( - !in_array($functionName, [ - 'preg_match', - 'preg_match_all', - 'preg_split', - 'preg_grep', - 'preg_replace', - 'preg_replace_callback', - 'preg_filter', - ], true) - ) { - continue; + if ( + in_array($functionName, [ + 'preg_match', + 'preg_match_all', + 'preg_split', + 'preg_grep', + 'preg_replace', + 'preg_replace_callback', + 'preg_filter', + ], true) + ) { + if ($patternNode instanceof Node\Expr\BinaryOp\Concat) { + $patternType = $this->regexExpressionHelper->resolvePatternConcat($patternNode, $scope); + } + foreach ($patternType->getConstantStrings() as $constantStringType) { + $patternStrings[] = $constantStringType->getValue(); } - - $patternStrings[] = $constantStringType->getValue(); } - foreach (TypeUtils::getConstantArrays($patternType) as $constantArrayType) { - if ( - in_array($functionName, [ - 'preg_replace', - 'preg_replace_callback', - 'preg_filter', - ], true) - ) { + if ( + in_array($functionName, [ + 'preg_replace', + 'preg_replace_callback', + 'preg_filter', + ], true) + ) { + foreach ($patternType->getConstantArrays() as $constantArrayType) { foreach ($constantArrayType->getValueTypes() as $arrayKeyType) { - if (!$arrayKeyType instanceof ConstantStringType) { - continue; + foreach ($arrayKeyType->getConstantStrings() as $constantString) { + $patternStrings[] = $constantString->getValue(); } - - $patternStrings[] = $arrayKeyType->getValue(); } } + } - if ($functionName !== 'preg_replace_callback_array') { - continue; - } - - foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { - if (!$arrayKeyType instanceof ConstantStringType) { - continue; + if ($functionName === 'preg_replace_callback_array') { + foreach ($patternType->getConstantArrays() as $constantArrayType) { + foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { + foreach ($arrayKeyType->getConstantStrings() as $constantString) { + $patternStrings[] = $constantString->getValue(); + } } - - $patternStrings[] = $arrayKeyType->getValue(); } } diff --git a/src/Rules/Regexp/RegularExpressionQuotingRule.php b/src/Rules/Regexp/RegularExpressionQuotingRule.php new file mode 100644 index 0000000000..83eddaa159 --- /dev/null +++ b/src/Rules/Regexp/RegularExpressionQuotingRule.php @@ -0,0 +1,242 @@ + + */ +final class RegularExpressionQuotingRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private RegexExpressionHelper $regexExpressionHelper, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ( + !in_array($functionReflection->getName(), [ + 'preg_match', + 'preg_match_all', + 'preg_filter', + 'preg_grep', + 'preg_replace', + 'preg_replace_callback', + 'preg_split', + ], true) + ) { + return []; + } + + $normalizedArgs = $this->getNormalizedArgs($node, $scope, $functionReflection); + if ($normalizedArgs === null) { + return []; + } + if (!isset($normalizedArgs[0])) { + return []; + } + if (!$normalizedArgs[0]->value instanceof Concat) { + return []; + } + + $patternDelimiters = $this->regexExpressionHelper->getPatternDelimiters($normalizedArgs[0]->value, $scope); + return $this->validateQuoteDelimiters($normalizedArgs[0]->value, $scope, $patternDelimiters); + } + + /** + * @param string[] $patternDelimiters + * + * @return list + */ + private function validateQuoteDelimiters(Concat $concat, Scope $scope, array $patternDelimiters): array + { + if ($patternDelimiters === []) { + return []; + } + + $errors = []; + if ( + $concat->left instanceof FuncCall + && $concat->left->name instanceof Name + && $concat->left->name->toLowerString() === 'preg_quote' + ) { + $pregError = $this->validatePregQuote($concat->left, $scope, $patternDelimiters); + if ($pregError !== null) { + $errors[] = $pregError; + } + } elseif ($concat->left instanceof Concat) { + $errors = array_merge($errors, $this->validateQuoteDelimiters($concat->left, $scope, $patternDelimiters)); + } + + if ( + $concat->right instanceof FuncCall + && $concat->right->name instanceof Name + && $concat->right->name->toLowerString() === 'preg_quote' + ) { + $pregError = $this->validatePregQuote($concat->right, $scope, $patternDelimiters); + if ($pregError !== null) { + $errors[] = $pregError; + } + } elseif ($concat->right instanceof Concat) { + $errors = array_merge($errors, $this->validateQuoteDelimiters($concat->right, $scope, $patternDelimiters)); + } + + return $errors; + } + + /** + * @param string[] $patternDelimiters + */ + private function validatePregQuote(FuncCall $pregQuote, Scope $scope, array $patternDelimiters): ?IdentifierRuleError + { + if (!$pregQuote->name instanceof Node\Name) { + return null; + } + + if (!$this->reflectionProvider->hasFunction($pregQuote->name, $scope)) { + return null; + } + $functionReflection = $this->reflectionProvider->getFunction($pregQuote->name, $scope); + + $args = $this->getNormalizedArgs($pregQuote, $scope, $functionReflection); + if ($args === null) { + return null; + } + + $patternDelimiters = $this->removeDefaultEscapedDelimiters($patternDelimiters); + if ($patternDelimiters === []) { + return null; + } + + if (count($args) === 1) { + if (count($patternDelimiters) === 1) { + return RuleErrorBuilder::message(sprintf('Call to preg_quote() is missing delimiter %s to be effective.', $patternDelimiters[0])) + ->line($pregQuote->getStartLine()) + ->identifier('argument.invalidPregQuote') + ->build(); + } + + return RuleErrorBuilder::message('Call to preg_quote() is missing delimiter parameter to be effective.') + ->line($pregQuote->getStartLine()) + ->identifier('argument.invalidPregQuote') + ->build(); + } + + if (count($args) >= 2) { + + foreach ($scope->getType($args[1]->value)->getConstantStrings() as $quoteDelimiterType) { + $quoteDelimiter = $quoteDelimiterType->getValue(); + + $quoteDelimiters = $this->removeDefaultEscapedDelimiters([$quoteDelimiter]); + if ($quoteDelimiters === []) { + continue; + } + + if (count($quoteDelimiters) !== 1) { + throw new ShouldNotHappenException(); + } + $quoteDelimiter = $quoteDelimiters[0]; + + if (!in_array($quoteDelimiter, $patternDelimiters, true)) { + if (count($patternDelimiters) === 1) { + return RuleErrorBuilder::message(sprintf('Call to preg_quote() uses invalid delimiter %s while pattern uses %s.', $quoteDelimiter, $patternDelimiters[0])) + ->line($pregQuote->getStartLine()) + ->identifier('argument.invalidPregQuote') + ->build(); + } + + return RuleErrorBuilder::message(sprintf('Call to preg_quote() uses invalid delimiter %s.', $quoteDelimiter)) + ->line($pregQuote->getStartLine()) + ->identifier('argument.invalidPregQuote') + ->build(); + } + } + } + + return null; + } + + /** + * @param string[] $delimiters + * + * @return list + */ + private function removeDefaultEscapedDelimiters(array $delimiters): array + { + return array_values(array_filter($delimiters, fn (string $delimiter): bool => !$this->isDefaultEscaped($delimiter))); + } + + private function isDefaultEscaped(string $delimiter): bool + { + if (strlen($delimiter) !== 1) { + return false; + } + + return in_array( + $delimiter, + // these delimiters are escaped, no matter what preg_quote() 2nd arg looks like + ['.', '\\', '+', '*', '?', '[', '^', ']', '$', '(', ')', '{', '}', '=', '!', '<', '>', '|', ':', '-', '#'], + true, + ); + } + + /** + * @return Node\Arg[]|null + */ + private function getNormalizedArgs(FuncCall $functionCall, Scope $scope, FunctionReflection $functionReflection): ?array + { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $functionCall); + if ($normalizedFuncCall === null) { + return null; + } + + return $normalizedFuncCall->getArgs(); + } + +} diff --git a/src/Rules/Registry.php b/src/Rules/Registry.php index c13d06d02c..792f4428f0 100644 --- a/src/Rules/Registry.php +++ b/src/Rules/Registry.php @@ -3,57 +3,15 @@ namespace PHPStan\Rules; use PhpParser\Node; -use function class_implements; -use function class_parents; -class Registry +interface Registry { - /** @var Rule[][] */ - private array $rules = []; - - /** @var Rule[][] */ - private array $cache = []; - - /** - * @param Rule[] $rules - */ - public function __construct(array $rules) - { - foreach ($rules as $rule) { - $this->rules[$rule->getNodeType()][] = $rule; - } - } - /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ - public function getRules(string $nodeType): array - { - if (!isset($this->cache[$nodeType])) { - $parentNodeTypes = [$nodeType] + class_parents($nodeType) + class_implements($nodeType); - - $rules = []; - foreach ($parentNodeTypes as $parentNodeType) { - foreach ($this->rules[$parentNodeType] ?? [] as $rule) { - $rules[] = $rule; - } - } - - $this->cache[$nodeType] = $rules; - } - - /** - * @phpstan-var array> $selectedRules - * @var Rule[] $selectedRules - */ - $selectedRules = $this->cache[$nodeType]; - - return $selectedRules; - } + public function getRules(string $nodeType): array; } diff --git a/src/Rules/RegistryFactory.php b/src/Rules/RegistryFactory.php deleted file mode 100644 index ebf00801d1..0000000000 --- a/src/Rules/RegistryFactory.php +++ /dev/null @@ -1,23 +0,0 @@ -container->getServicesByTag(self::RULE_TAG), - ); - } - -} diff --git a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageExtension.php new file mode 100644 index 0000000000..ebb5d989d1 --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageExtension.php @@ -0,0 +1,38 @@ + + */ +final class RestrictedClassConstantUsageRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\ClassConstFetch::class; + } + + /** + * @api + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Identifier) { + return []; + } + + /** @var RestrictedClassConstantUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedClassConstantUsageExtension::CLASS_CONSTANT_EXTENSION_TAG); + if ($extensions === []) { + return []; + } + + $constantName = $node->name->name; + $referencedClasses = []; + + if ($node->class instanceof Name) { + $referencedClasses[] = $scope->resolveName($node->class); + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->class, + '', // We don't care about the error message + static fn (Type $type): bool => $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(), + ); + + if ($classTypeResult->getType() instanceof ErrorType) { + return []; + } + + $referencedClasses = $classTypeResult->getReferencedClasses(); + } + + $errors = []; + foreach ($referencedClasses as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->hasConstant($constantName)) { + continue; + } + + $constantReflection = $classReflection->getConstant($constantName); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedClassConstantUsage($constantReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedClassNameUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedClassNameUsageExtension.php new file mode 100644 index 0000000000..1d1f619c99 --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedClassNameUsageExtension.php @@ -0,0 +1,42 @@ + + */ +final class RestrictedFunctionCallableUsageRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return FunctionCallableNode::class; + } + + /** + * @api + */ + public function processNode(Node $node, Scope $scope): array + { + if (!($node->getName() instanceof Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->getName(), $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->getName(), $scope); + + /** @var RestrictedFunctionUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedFunctionUsageExtension::FUNCTION_EXTENSION_TAG); + $errors = []; + + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedFunctionUsage($functionReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedFunctionUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedFunctionUsageExtension.php new file mode 100644 index 0000000000..f43c357190 --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedFunctionUsageExtension.php @@ -0,0 +1,38 @@ + + */ +final class RestrictedFunctionUsageRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\FuncCall::class; + } + + /** + * @api + */ + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + + /** @var RestrictedFunctionUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedFunctionUsageExtension::FUNCTION_EXTENSION_TAG); + $errors = []; + + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedFunctionUsage($functionReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php new file mode 100644 index 0000000000..66eb85f906 --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php @@ -0,0 +1,79 @@ + + */ +final class RestrictedMethodCallableUsageRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return MethodCallableNode::class; + } + + /** + * @api + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->getName() instanceof Identifier) { + return []; + } + + /** @var RestrictedMethodUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG); + if ($extensions === []) { + return []; + } + + $methodName = $node->getName()->name; + $methodCalledOnType = $scope->getType($node->getVar()); + $referencedClasses = $methodCalledOnType->getObjectClassNames(); + + $errors = []; + + foreach ($referencedClasses as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $classReflection->getMethod($methodName, $scope); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedMethodUsage($methodReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedMethodUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedMethodUsageExtension.php new file mode 100644 index 0000000000..8de9bf5ffc --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedMethodUsageExtension.php @@ -0,0 +1,38 @@ + + */ +final class RestrictedMethodUsageRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + /** + * @api + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Identifier) { + return []; + } + + /** @var RestrictedMethodUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG); + if ($extensions === []) { + return []; + } + + $methodName = $node->name->name; + $methodCalledOnType = $scope->getType($node->var); + $referencedClasses = $methodCalledOnType->getObjectClassNames(); + + $errors = []; + + foreach ($referencedClasses as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $classReflection->getMethod($methodName, $scope); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedMethodUsage($methodReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedPropertyUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedPropertyUsageExtension.php new file mode 100644 index 0000000000..2216c7435b --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedPropertyUsageExtension.php @@ -0,0 +1,38 @@ + + */ +final class RestrictedPropertyUsageRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\PropertyFetch::class; + } + + /** + * @api + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Identifier) { + return []; + } + + /** @var RestrictedPropertyUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedPropertyUsageExtension::PROPERTY_EXTENSION_TAG); + if ($extensions === []) { + return []; + } + + $propertyName = $node->name->name; + $propertyCalledOnType = $scope->getType($node->var); + $referencedClasses = $propertyCalledOnType->getObjectClassNames(); + + $errors = []; + + foreach ($referencedClasses as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->hasProperty($propertyName)) { + continue; + } + + $propertyReflection = $classReflection->getProperty($propertyName, $scope); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedPropertyUsage($propertyReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php new file mode 100644 index 0000000000..a6172e69dd --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php @@ -0,0 +1,99 @@ + + */ +final class RestrictedStaticMethodCallableUsageRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return StaticMethodCallableNode::class; + } + + /** + * @api + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->getName() instanceof Identifier) { + return []; + } + + /** @var RestrictedMethodUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG); + if ($extensions === []) { + return []; + } + + $methodName = $node->getName()->name; + $referencedClasses = []; + + if ($node->getClass() instanceof Name) { + $referencedClasses[] = $scope->resolveName($node->getClass()); + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->getClass(), + '', // We don't care about the error message + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + + if ($classTypeResult->getType() instanceof ErrorType) { + return []; + } + + $referencedClasses = $classTypeResult->getReferencedClasses(); + } + + $errors = []; + foreach ($referencedClasses as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $classReflection->getMethod($methodName, $scope); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedMethodUsage($methodReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php new file mode 100644 index 0000000000..b9f061bc3b --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php @@ -0,0 +1,98 @@ + + */ +final class RestrictedStaticMethodUsageRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\StaticCall::class; + } + + /** + * @api + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Identifier) { + return []; + } + + /** @var RestrictedMethodUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG); + if ($extensions === []) { + return []; + } + + $methodName = $node->name->name; + $referencedClasses = []; + + if ($node->class instanceof Name) { + $referencedClasses[] = $scope->resolveName($node->class); + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->class, + '', // We don't care about the error message + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + + if ($classTypeResult->getType() instanceof ErrorType) { + return []; + } + + $referencedClasses = $classTypeResult->getReferencedClasses(); + } + + $errors = []; + foreach ($referencedClasses as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $classReflection->getMethod($methodName, $scope); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedMethodUsage($methodReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php new file mode 100644 index 0000000000..260672d0d5 --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php @@ -0,0 +1,98 @@ + + */ +final class RestrictedStaticPropertyUsageRule implements Rule +{ + + public function __construct( + private Container $container, + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\StaticPropertyFetch::class; + } + + /** + * @api + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Identifier) { + return []; + } + + /** @var RestrictedPropertyUsageExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(RestrictedPropertyUsageExtension::PROPERTY_EXTENSION_TAG); + if ($extensions === []) { + return []; + } + + $propertyName = $node->name->name; + $referencedClasses = []; + + if ($node->class instanceof Name) { + $referencedClasses[] = $scope->resolveName($node->class); + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->class, + '', // We don't care about the error message + static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($propertyName)->yes(), + ); + + if ($classTypeResult->getType() instanceof ErrorType) { + return []; + } + + $referencedClasses = $classTypeResult->getReferencedClasses(); + } + + $errors = []; + foreach ($referencedClasses as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->hasProperty($propertyName)) { + continue; + } + + $propertyReflection = $classReflection->getProperty($propertyName, $scope); + foreach ($extensions as $extension) { + $restrictedUsage = $extension->isRestrictedPropertyUsage($propertyReflection, $scope); + if ($restrictedUsage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedUsage.php b/src/Rules/RestrictedUsage/RestrictedUsage.php new file mode 100644 index 0000000000..d632582981 --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedUsage.php @@ -0,0 +1,26 @@ + + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node - * @return (string|RuleError)[] errors + * @param TNodeType $node + * @return list */ public function processNode(Node $node, Scope $scope): array; diff --git a/src/Rules/RuleError.php b/src/Rules/RuleError.php index 6fadd1cd7e..ee2e0f5f6d 100644 --- a/src/Rules/RuleError.php +++ b/src/Rules/RuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface RuleError { diff --git a/src/Rules/RuleErrorBuilder.php b/src/Rules/RuleErrorBuilder.php index 091ffbf9db..cb714dfd2c 100644 --- a/src/Rules/RuleErrorBuilder.php +++ b/src/Rules/RuleErrorBuilder.php @@ -2,12 +2,20 @@ namespace PHPStan\Rules; +use PHPStan\Analyser\Error; use PHPStan\ShouldNotHappenException; +use function array_map; use function class_exists; +use function count; +use function implode; +use function is_file; use function sprintf; -/** @api */ -class RuleErrorBuilder +/** + * @api + * @template-covariant T of RuleError + */ +final class RuleErrorBuilder { private const TYPE_MESSAGE = 1; @@ -23,6 +31,9 @@ class RuleErrorBuilder /** @var mixed[] */ private array $properties; + /** @var list */ + private array $tips = []; + private function __construct(string $message) { $this->properties['message'] = $message; @@ -30,61 +41,95 @@ private function __construct(string $message) } /** - * @return array + * @return array}> */ public static function getRuleErrorTypes(): array { return [ self::TYPE_MESSAGE => [ RuleError::class, - 'message', - 'string', - 'string', + [ + [ + 'message', + 'string', + 'string', + ], + ], ], self::TYPE_LINE => [ LineRuleError::class, - 'line', - 'int', - 'int', + [ + [ + 'line', + 'int', + 'int', + ], + ], ], self::TYPE_FILE => [ FileRuleError::class, - 'file', - 'string', - 'string', + [ + [ + 'file', + 'string', + 'string', + ], + [ + 'fileDescription', + 'string', + 'string', + ], + ], ], self::TYPE_TIP => [ TipRuleError::class, - 'tip', - 'string', - 'string', + [ + [ + 'tip', + 'string', + 'string', + ], + ], ], self::TYPE_IDENTIFIER => [ IdentifierRuleError::class, - 'identifier', - 'string', - 'string', + [ + [ + 'identifier', + 'string', + 'string', + ], + ], ], self::TYPE_METADATA => [ MetadataRuleError::class, - 'metadata', - 'array', - 'mixed[]', + [ + [ + 'metadata', + 'array', + 'mixed[]', + ], + ], ], self::TYPE_NON_IGNORABLE => [ NonIgnorableRuleError::class, - null, - null, - null, + [], ], ]; } + /** + * @return self + */ public static function message(string $message): self { return new self($message); } + /** + * @phpstan-this-out self + * @return self + */ public function line(int $line): self { $this->properties['line'] = $line; @@ -93,29 +138,92 @@ public function line(int $line): self return $this; } - public function file(string $file): self + /** + * @phpstan-this-out self + * @return self + */ + public function file(string $file, ?string $fileDescription = null): self { + if (!is_file($file)) { + throw new ShouldNotHappenException(sprintf('File %s does not exist.', $file)); + } $this->properties['file'] = $file; + $this->properties['fileDescription'] = $fileDescription ?? $file; $this->type |= self::TYPE_FILE; return $this; } + /** + * @phpstan-this-out self + * @return self + */ public function tip(string $tip): self { - $this->properties['tip'] = $tip; + $this->tips = [$tip]; + $this->type |= self::TYPE_TIP; + + return $this; + } + + /** + * @phpstan-this-out self + * @return self + */ + public function addTip(string $tip): self + { + $this->tips[] = $tip; $this->type |= self::TYPE_TIP; return $this; } + /** + * @phpstan-this-out self + * @return self + */ public function discoveringSymbolsTip(): self { return $this->tip('Learn more at https://phpstan.org/user-guide/discovering-symbols'); } + /** + * @param list $reasons + * @phpstan-this-out self + * @return self + */ + public function acceptsReasonsTip(array $reasons): self + { + foreach ($reasons as $reason) { + $this->addTip($reason); + } + + return $this; + } + + /** + * @phpstan-this-out self + * @return self + */ + public function treatPhpDocTypesAsCertainTip(): self + { + return $this->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + } + + /** + * Sets an error identifier. + * + * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers + * + * @phpstan-this-out self + * @return self + */ public function identifier(string $identifier): self { + if (!Error::validateIdentifier($identifier)) { + throw new ShouldNotHappenException(sprintf('Invalid identifier: %s, error identifiers must match /%s/', $identifier, Error::PATTERN_IDENTIFIER)); + } + $this->properties['identifier'] = $identifier; $this->type |= self::TYPE_IDENTIFIER; @@ -124,6 +232,8 @@ public function identifier(string $identifier): self /** * @param mixed[] $metadata + * @phpstan-this-out self + * @return self */ public function metadata(array $metadata): self { @@ -133,6 +243,10 @@ public function metadata(array $metadata): self return $this; } + /** + * @phpstan-this-out self + * @return self + */ public function nonIgnorable(): self { $this->type |= self::TYPE_NON_IGNORABLE; @@ -140,9 +254,12 @@ public function nonIgnorable(): self return $this; } + /** + * @return T + */ public function build(): RuleError { - /** @var class-string $className */ + /** @var class-string $className */ $className = sprintf('PHPStan\\Rules\\RuleErrors\\RuleError%d', $this->type); if (!class_exists($className)) { throw new ShouldNotHappenException(sprintf('Class %s does not exist.', $className)); @@ -153,6 +270,14 @@ public function build(): RuleError $ruleError->{$propertyName} = $value; } + if (count($this->tips) > 0) { + if (count($this->tips) === 1) { + $ruleError->tip = $this->tips[0]; + } else { + $ruleError->tip = implode("\n", array_map(static fn (string $tip) => sprintf('• %s', $tip), $this->tips)); + } + } + return $ruleError; } diff --git a/src/Rules/RuleErrors/RuleError1.php b/src/Rules/RuleErrors/RuleError1.php index ef7771dea3..c7a2338b30 100644 --- a/src/Rules/RuleErrors/RuleError1.php +++ b/src/Rules/RuleErrors/RuleError1.php @@ -7,7 +7,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError1 implements RuleError +final class RuleError1 implements RuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError101.php b/src/Rules/RuleErrors/RuleError101.php index a4c08ae140..ff16ad5339 100644 --- a/src/Rules/RuleErrors/RuleError101.php +++ b/src/Rules/RuleErrors/RuleError101.php @@ -10,13 +10,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError101 implements RuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError101 implements RuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -30,6 +32,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + /** * @return mixed[] */ diff --git a/src/Rules/RuleErrors/RuleError103.php b/src/Rules/RuleErrors/RuleError103.php index 04c4ae5083..6c125de9b7 100644 --- a/src/Rules/RuleErrors/RuleError103.php +++ b/src/Rules/RuleErrors/RuleError103.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError103 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError103 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; @@ -20,6 +20,8 @@ class RuleError103 implements RuleError, LineRuleError, FileRuleError, MetadataR public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -38,6 +40,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + /** * @return mixed[] */ diff --git a/src/Rules/RuleErrors/RuleError105.php b/src/Rules/RuleErrors/RuleError105.php index 0fb7b8bc41..a0b8945f52 100644 --- a/src/Rules/RuleErrors/RuleError105.php +++ b/src/Rules/RuleErrors/RuleError105.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError105 implements RuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError +final 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 35b081c092..a0b9b85c84 100644 --- a/src/Rules/RuleErrors/RuleError107.php +++ b/src/Rules/RuleErrors/RuleError107.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError107 implements RuleError, LineRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError +final 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 64d81d29db..a4f81cce53 100644 --- a/src/Rules/RuleErrors/RuleError109.php +++ b/src/Rules/RuleErrors/RuleError109.php @@ -11,13 +11,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError109 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError109 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -33,6 +35,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError11.php b/src/Rules/RuleErrors/RuleError11.php index 50d8bdb997..be6bc0923a 100644 --- a/src/Rules/RuleErrors/RuleError11.php +++ b/src/Rules/RuleErrors/RuleError11.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError11 implements RuleError, LineRuleError, TipRuleError +final class RuleError11 implements RuleError, LineRuleError, TipRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError111.php b/src/Rules/RuleErrors/RuleError111.php index f3ad512dbc..ac0b980e01 100644 --- a/src/Rules/RuleErrors/RuleError111.php +++ b/src/Rules/RuleErrors/RuleError111.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError111 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError111 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; @@ -21,6 +21,8 @@ class RuleError111 implements RuleError, LineRuleError, FileRuleError, TipRuleEr public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -41,6 +43,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError113.php b/src/Rules/RuleErrors/RuleError113.php index ee74c1f0ac..5d602a2fe5 100644 --- a/src/Rules/RuleErrors/RuleError113.php +++ b/src/Rules/RuleErrors/RuleError113.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError113 implements RuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final 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 7b5f1af9e5..f6d020a9e2 100644 --- a/src/Rules/RuleErrors/RuleError115.php +++ b/src/Rules/RuleErrors/RuleError115.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError115 implements RuleError, LineRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final 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 d46b42784f..80b8bd5fb2 100644 --- a/src/Rules/RuleErrors/RuleError117.php +++ b/src/Rules/RuleErrors/RuleError117.php @@ -11,13 +11,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError117 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError117 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -33,6 +35,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError119.php b/src/Rules/RuleErrors/RuleError119.php index 67ed0183e6..ddee015752 100644 --- a/src/Rules/RuleErrors/RuleError119.php +++ b/src/Rules/RuleErrors/RuleError119.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError119 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError119 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; @@ -21,6 +21,8 @@ class RuleError119 implements RuleError, LineRuleError, FileRuleError, Identifie public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -41,6 +43,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError121.php b/src/Rules/RuleErrors/RuleError121.php index 2c8995a0b3..3a05d8d3c3 100644 --- a/src/Rules/RuleErrors/RuleError121.php +++ b/src/Rules/RuleErrors/RuleError121.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError121 implements RuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final 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 fedd8de5d9..4bae22b6a5 100644 --- a/src/Rules/RuleErrors/RuleError123.php +++ b/src/Rules/RuleErrors/RuleError123.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError123 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final 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 b8d1602fef..a24ea70b44 100644 --- a/src/Rules/RuleErrors/RuleError125.php +++ b/src/Rules/RuleErrors/RuleError125.php @@ -12,13 +12,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError125 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError125 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -36,6 +38,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError127.php b/src/Rules/RuleErrors/RuleError127.php index a32ffdb849..0c2dea58a7 100644 --- a/src/Rules/RuleErrors/RuleError127.php +++ b/src/Rules/RuleErrors/RuleError127.php @@ -13,7 +13,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError127 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError127 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; @@ -22,6 +22,8 @@ class RuleError127 implements RuleError, LineRuleError, FileRuleError, TipRuleEr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -44,6 +46,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError13.php b/src/Rules/RuleErrors/RuleError13.php index 51568e83da..a606a29b87 100644 --- a/src/Rules/RuleErrors/RuleError13.php +++ b/src/Rules/RuleErrors/RuleError13.php @@ -9,13 +9,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError13 implements RuleError, FileRuleError, TipRuleError +final class RuleError13 implements RuleError, FileRuleError, TipRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -28,6 +30,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError15.php b/src/Rules/RuleErrors/RuleError15.php index 9c57396605..b952f9a3c5 100644 --- a/src/Rules/RuleErrors/RuleError15.php +++ b/src/Rules/RuleErrors/RuleError15.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError15 implements RuleError, LineRuleError, FileRuleError, TipRuleError +final class RuleError15 implements RuleError, LineRuleError, FileRuleError, TipRuleError { public string $message; @@ -19,6 +19,8 @@ class RuleError15 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -36,6 +38,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError17.php b/src/Rules/RuleErrors/RuleError17.php index 827f6a8724..8cdf151a33 100644 --- a/src/Rules/RuleErrors/RuleError17.php +++ b/src/Rules/RuleErrors/RuleError17.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError17 implements RuleError, IdentifierRuleError +final class RuleError17 implements RuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError19.php b/src/Rules/RuleErrors/RuleError19.php index 3732da9802..d7a3a4388b 100644 --- a/src/Rules/RuleErrors/RuleError19.php +++ b/src/Rules/RuleErrors/RuleError19.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError19 implements RuleError, LineRuleError, IdentifierRuleError +final class RuleError19 implements RuleError, LineRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError21.php b/src/Rules/RuleErrors/RuleError21.php index b1bb82ca7a..91516979e0 100644 --- a/src/Rules/RuleErrors/RuleError21.php +++ b/src/Rules/RuleErrors/RuleError21.php @@ -9,13 +9,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError21 implements RuleError, FileRuleError, IdentifierRuleError +final class RuleError21 implements RuleError, FileRuleError, IdentifierRuleError { public string $message; public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -28,6 +30,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError23.php b/src/Rules/RuleErrors/RuleError23.php index 1aa68678de..4dcb3e0bae 100644 --- a/src/Rules/RuleErrors/RuleError23.php +++ b/src/Rules/RuleErrors/RuleError23.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError23 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError +final class RuleError23 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError { public string $message; @@ -19,6 +19,8 @@ class RuleError23 implements RuleError, LineRuleError, FileRuleError, Identifier public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -36,6 +38,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError25.php b/src/Rules/RuleErrors/RuleError25.php index 1c5c6001e8..429a1ed0ef 100644 --- a/src/Rules/RuleErrors/RuleError25.php +++ b/src/Rules/RuleErrors/RuleError25.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError25 implements RuleError, TipRuleError, IdentifierRuleError +final class RuleError25 implements RuleError, TipRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError27.php b/src/Rules/RuleErrors/RuleError27.php index e592c0d98e..6910c787fd 100644 --- a/src/Rules/RuleErrors/RuleError27.php +++ b/src/Rules/RuleErrors/RuleError27.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError27 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError +final 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 9f10ef1f20..85a6c85960 100644 --- a/src/Rules/RuleErrors/RuleError29.php +++ b/src/Rules/RuleErrors/RuleError29.php @@ -10,13 +10,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError29 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError +final class RuleError29 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -31,6 +33,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError3.php b/src/Rules/RuleErrors/RuleError3.php index ce5c8fffcc..17ab507d2a 100644 --- a/src/Rules/RuleErrors/RuleError3.php +++ b/src/Rules/RuleErrors/RuleError3.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError3 implements RuleError, LineRuleError +final class RuleError3 implements RuleError, LineRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError31.php b/src/Rules/RuleErrors/RuleError31.php index 2df2e100c5..d9e7665e9b 100644 --- a/src/Rules/RuleErrors/RuleError31.php +++ b/src/Rules/RuleErrors/RuleError31.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError31 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError +final class RuleError31 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError { public string $message; @@ -20,6 +20,8 @@ class RuleError31 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -39,6 +41,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError33.php b/src/Rules/RuleErrors/RuleError33.php index 0f37cede7c..692da9a71a 100644 --- a/src/Rules/RuleErrors/RuleError33.php +++ b/src/Rules/RuleErrors/RuleError33.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError33 implements RuleError, MetadataRuleError +final class RuleError33 implements RuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError35.php b/src/Rules/RuleErrors/RuleError35.php index 65868071a8..91c52036bb 100644 --- a/src/Rules/RuleErrors/RuleError35.php +++ b/src/Rules/RuleErrors/RuleError35.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError35 implements RuleError, LineRuleError, MetadataRuleError +final class RuleError35 implements RuleError, LineRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError37.php b/src/Rules/RuleErrors/RuleError37.php index ae5adf983c..a92ded0e4f 100644 --- a/src/Rules/RuleErrors/RuleError37.php +++ b/src/Rules/RuleErrors/RuleError37.php @@ -9,13 +9,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError37 implements RuleError, FileRuleError, MetadataRuleError +final class RuleError37 implements RuleError, FileRuleError, MetadataRuleError { public string $message; public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -29,6 +31,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + /** * @return mixed[] */ diff --git a/src/Rules/RuleErrors/RuleError39.php b/src/Rules/RuleErrors/RuleError39.php index a3699d7c68..7b74800753 100644 --- a/src/Rules/RuleErrors/RuleError39.php +++ b/src/Rules/RuleErrors/RuleError39.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError39 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError +final class RuleError39 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError { public string $message; @@ -19,6 +19,8 @@ class RuleError39 implements RuleError, LineRuleError, FileRuleError, MetadataRu public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -37,6 +39,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + /** * @return mixed[] */ diff --git a/src/Rules/RuleErrors/RuleError41.php b/src/Rules/RuleErrors/RuleError41.php index 528a20c731..7cc55fdeb1 100644 --- a/src/Rules/RuleErrors/RuleError41.php +++ b/src/Rules/RuleErrors/RuleError41.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError41 implements RuleError, TipRuleError, MetadataRuleError +final class RuleError41 implements RuleError, TipRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError43.php b/src/Rules/RuleErrors/RuleError43.php index 9992c86c9d..a251840dd9 100644 --- a/src/Rules/RuleErrors/RuleError43.php +++ b/src/Rules/RuleErrors/RuleError43.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError43 implements RuleError, LineRuleError, TipRuleError, MetadataRuleError +final 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 b81bfcd3b7..aceabd9f78 100644 --- a/src/Rules/RuleErrors/RuleError45.php +++ b/src/Rules/RuleErrors/RuleError45.php @@ -10,13 +10,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError45 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError +final class RuleError45 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -32,6 +34,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError47.php b/src/Rules/RuleErrors/RuleError47.php index d4f0af6e2a..866bbc3ddf 100644 --- a/src/Rules/RuleErrors/RuleError47.php +++ b/src/Rules/RuleErrors/RuleError47.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError47 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError +final class RuleError47 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError { public string $message; @@ -20,6 +20,8 @@ class RuleError47 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -40,6 +42,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError49.php b/src/Rules/RuleErrors/RuleError49.php index 6b8aa73a5b..81b0015029 100644 --- a/src/Rules/RuleErrors/RuleError49.php +++ b/src/Rules/RuleErrors/RuleError49.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError49 implements RuleError, IdentifierRuleError, MetadataRuleError +final class RuleError49 implements RuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError5.php b/src/Rules/RuleErrors/RuleError5.php index 58f47a9053..0dbad8299b 100644 --- a/src/Rules/RuleErrors/RuleError5.php +++ b/src/Rules/RuleErrors/RuleError5.php @@ -8,13 +8,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError5 implements RuleError, FileRuleError +final class RuleError5 implements RuleError, FileRuleError { public string $message; public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -25,4 +27,9 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + } diff --git a/src/Rules/RuleErrors/RuleError51.php b/src/Rules/RuleErrors/RuleError51.php index a008143cd2..96d93510c9 100644 --- a/src/Rules/RuleErrors/RuleError51.php +++ b/src/Rules/RuleErrors/RuleError51.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError51 implements RuleError, LineRuleError, IdentifierRuleError, MetadataRuleError +final 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 e11bdaf0cb..1e11f5e641 100644 --- a/src/Rules/RuleErrors/RuleError53.php +++ b/src/Rules/RuleErrors/RuleError53.php @@ -10,13 +10,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError53 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError53 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError { public string $message; public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -32,6 +34,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError55.php b/src/Rules/RuleErrors/RuleError55.php index 3b9aefc997..3bf4a22ccf 100644 --- a/src/Rules/RuleErrors/RuleError55.php +++ b/src/Rules/RuleErrors/RuleError55.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError55 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError55 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError { public string $message; @@ -20,6 +20,8 @@ class RuleError55 implements RuleError, LineRuleError, FileRuleError, Identifier public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -40,6 +42,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError57.php b/src/Rules/RuleErrors/RuleError57.php index 4fabd64eac..22c77fc545 100644 --- a/src/Rules/RuleErrors/RuleError57.php +++ b/src/Rules/RuleErrors/RuleError57.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError57 implements RuleError, TipRuleError, IdentifierRuleError, MetadataRuleError +final 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 837c62602d..a7659febe1 100644 --- a/src/Rules/RuleErrors/RuleError59.php +++ b/src/Rules/RuleErrors/RuleError59.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError59 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError +final 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 a263eafb22..723a0aa79b 100644 --- a/src/Rules/RuleErrors/RuleError61.php +++ b/src/Rules/RuleErrors/RuleError61.php @@ -11,13 +11,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError61 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError61 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -35,6 +37,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError63.php b/src/Rules/RuleErrors/RuleError63.php index 80a680ec0d..1c88f9fbc2 100644 --- a/src/Rules/RuleErrors/RuleError63.php +++ b/src/Rules/RuleErrors/RuleError63.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError63 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError +final class RuleError63 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; @@ -21,6 +21,8 @@ class RuleError63 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -43,6 +45,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError65.php b/src/Rules/RuleErrors/RuleError65.php index 095f49475d..fc2593bbfa 100644 --- a/src/Rules/RuleErrors/RuleError65.php +++ b/src/Rules/RuleErrors/RuleError65.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError65 implements RuleError, NonIgnorableRuleError +final class RuleError65 implements RuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError67.php b/src/Rules/RuleErrors/RuleError67.php index 08a214f997..b2218268c3 100644 --- a/src/Rules/RuleErrors/RuleError67.php +++ b/src/Rules/RuleErrors/RuleError67.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError67 implements RuleError, LineRuleError, NonIgnorableRuleError +final class RuleError67 implements RuleError, LineRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError69.php b/src/Rules/RuleErrors/RuleError69.php index afa9983d5d..7f5e130f09 100644 --- a/src/Rules/RuleErrors/RuleError69.php +++ b/src/Rules/RuleErrors/RuleError69.php @@ -9,13 +9,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError69 implements RuleError, FileRuleError, NonIgnorableRuleError +final class RuleError69 implements RuleError, FileRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -26,4 +28,9 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + } diff --git a/src/Rules/RuleErrors/RuleError7.php b/src/Rules/RuleErrors/RuleError7.php index 4ce7c6b45e..203696b2fd 100644 --- a/src/Rules/RuleErrors/RuleError7.php +++ b/src/Rules/RuleErrors/RuleError7.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError7 implements RuleError, LineRuleError, FileRuleError +final class RuleError7 implements RuleError, LineRuleError, FileRuleError { public string $message; @@ -18,6 +18,8 @@ class RuleError7 implements RuleError, LineRuleError, FileRuleError public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -33,4 +35,9 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + } diff --git a/src/Rules/RuleErrors/RuleError71.php b/src/Rules/RuleErrors/RuleError71.php index 06db73390a..d78d2813e3 100644 --- a/src/Rules/RuleErrors/RuleError71.php +++ b/src/Rules/RuleErrors/RuleError71.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError71 implements RuleError, LineRuleError, FileRuleError, NonIgnorableRuleError +final class RuleError71 implements RuleError, LineRuleError, FileRuleError, NonIgnorableRuleError { public string $message; @@ -19,6 +19,8 @@ class RuleError71 implements RuleError, LineRuleError, FileRuleError, NonIgnorab public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -34,4 +36,9 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + } diff --git a/src/Rules/RuleErrors/RuleError73.php b/src/Rules/RuleErrors/RuleError73.php index 8cdaeaa36d..fd81121a2d 100644 --- a/src/Rules/RuleErrors/RuleError73.php +++ b/src/Rules/RuleErrors/RuleError73.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError73 implements RuleError, TipRuleError, NonIgnorableRuleError +final class RuleError73 implements RuleError, TipRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError75.php b/src/Rules/RuleErrors/RuleError75.php index 3195db7454..d249eef75e 100644 --- a/src/Rules/RuleErrors/RuleError75.php +++ b/src/Rules/RuleErrors/RuleError75.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError75 implements RuleError, LineRuleError, TipRuleError, NonIgnorableRuleError +final 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 12a34ab040..e0e8547a32 100644 --- a/src/Rules/RuleErrors/RuleError77.php +++ b/src/Rules/RuleErrors/RuleError77.php @@ -10,13 +10,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError77 implements RuleError, FileRuleError, TipRuleError, NonIgnorableRuleError +final class RuleError77 implements RuleError, FileRuleError, TipRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -29,6 +31,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError79.php b/src/Rules/RuleErrors/RuleError79.php index ac103ea3f5..3a07eea396 100644 --- a/src/Rules/RuleErrors/RuleError79.php +++ b/src/Rules/RuleErrors/RuleError79.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError79 implements RuleError, LineRuleError, FileRuleError, TipRuleError, NonIgnorableRuleError +final class RuleError79 implements RuleError, LineRuleError, FileRuleError, TipRuleError, NonIgnorableRuleError { public string $message; @@ -20,6 +20,8 @@ class RuleError79 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -37,6 +39,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError81.php b/src/Rules/RuleErrors/RuleError81.php index 1412335a8a..fe96c09839 100644 --- a/src/Rules/RuleErrors/RuleError81.php +++ b/src/Rules/RuleErrors/RuleError81.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError81 implements RuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError81 implements RuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError83.php b/src/Rules/RuleErrors/RuleError83.php index ceb2141456..9570715ebe 100644 --- a/src/Rules/RuleErrors/RuleError83.php +++ b/src/Rules/RuleErrors/RuleError83.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError83 implements RuleError, LineRuleError, IdentifierRuleError, NonIgnorableRuleError +final 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 48a2c071f1..5af535902a 100644 --- a/src/Rules/RuleErrors/RuleError85.php +++ b/src/Rules/RuleErrors/RuleError85.php @@ -10,13 +10,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError85 implements RuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError85 implements RuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -29,6 +31,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError87.php b/src/Rules/RuleErrors/RuleError87.php index 04ccf29f73..44028fe427 100644 --- a/src/Rules/RuleErrors/RuleError87.php +++ b/src/Rules/RuleErrors/RuleError87.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError87 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError87 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; @@ -20,6 +20,8 @@ class RuleError87 implements RuleError, LineRuleError, FileRuleError, Identifier public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -37,6 +39,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError89.php b/src/Rules/RuleErrors/RuleError89.php index 3b207cf2ad..e69b4058a6 100644 --- a/src/Rules/RuleErrors/RuleError89.php +++ b/src/Rules/RuleErrors/RuleError89.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError89 implements RuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError +final 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 e93934d20b..c8454faf62 100644 --- a/src/Rules/RuleErrors/RuleError9.php +++ b/src/Rules/RuleErrors/RuleError9.php @@ -8,7 +8,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError9 implements RuleError, TipRuleError +final class RuleError9 implements RuleError, TipRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError91.php b/src/Rules/RuleErrors/RuleError91.php index a0fc78df5a..8c11c1816f 100644 --- a/src/Rules/RuleErrors/RuleError91.php +++ b/src/Rules/RuleErrors/RuleError91.php @@ -11,7 +11,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError91 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError +final 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 a9cdb69c6e..8c5b9c5a64 100644 --- a/src/Rules/RuleErrors/RuleError93.php +++ b/src/Rules/RuleErrors/RuleError93.php @@ -11,13 +11,15 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError93 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError93 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -32,6 +34,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError95.php b/src/Rules/RuleErrors/RuleError95.php index 117c4b574d..68b993db00 100644 --- a/src/Rules/RuleErrors/RuleError95.php +++ b/src/Rules/RuleErrors/RuleError95.php @@ -12,7 +12,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError95 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError +final class RuleError95 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; @@ -21,6 +21,8 @@ class RuleError95 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -40,6 +42,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError97.php b/src/Rules/RuleErrors/RuleError97.php index da07902233..da13e04d88 100644 --- a/src/Rules/RuleErrors/RuleError97.php +++ b/src/Rules/RuleErrors/RuleError97.php @@ -9,7 +9,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError97 implements RuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError97 implements RuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError99.php b/src/Rules/RuleErrors/RuleError99.php index b26457a1e1..60c3af565f 100644 --- a/src/Rules/RuleErrors/RuleError99.php +++ b/src/Rules/RuleErrors/RuleError99.php @@ -10,7 +10,7 @@ /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError99 implements RuleError, LineRuleError, MetadataRuleError, NonIgnorableRuleError +final class RuleError99 implements RuleError, LineRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index bc1b80ebe4..2f9dd97903 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -6,26 +6,25 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\BenevolentUnionType; -use PHPStan\Type\CompoundType; +use PHPStan\Type\CallableType; +use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateMixedType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StaticType; +use PHPStan\Type\ObjectType; 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 +final class RuleLevelHelper { public function __construct( @@ -34,6 +33,9 @@ public function __construct( private bool $checkThisOnly, private bool $checkUnionTypes, private bool $checkExplicitMixed, + private bool $checkImplicitMixed, + private bool $checkBenevolentUnionTypes, + private bool $discoveringSymbolsTip, ) { } @@ -44,68 +46,110 @@ public function isThis(Expr $expression): bool return $expression instanceof Expr\Variable && $expression->name === 'this'; } - /** @api */ - public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): bool + private function transformCommonType(Type $type): Type { - if ( - $this->checkExplicitMixed - ) { - $traverse = static function (Type $type, callable $traverse): Type { - if ($type instanceof TemplateMixedType) { + if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { + return $type; + } + + return TypeTraverser::map($type, function (Type $type, callable $traverse) { + if ($type instanceof TemplateMixedType) { + if ($this->checkExplicitMixed) { return $type->toStrictMixedType(); } - if ( - $type instanceof MixedType - && $type->isExplicitMixed() - ) { - return new StrictMixedType(); + } + if ( + $type instanceof MixedType + && ( + ($type->isExplicitMixed() && $this->checkExplicitMixed) + || (!$type->isExplicitMixed() && $this->checkImplicitMixed) + ) + ) { + return new StrictMixedType(); + } + + return $traverse($type); + }); + } + + /** + * @return array{Type, bool} + */ + private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array + { + $checkForUnion = $this->checkUnionTypes; + $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { + if ($acceptedType instanceof CallableType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; } - return $traverse($type); - }; - $acceptingType = TypeTraverser::map($acceptingType, $traverse); - $acceptedType = TypeTraverser::map($acceptedType, $traverse); - } + return new CallableType( + $acceptedType->getParameters(), + $traverse($this->transformCommonType($acceptedType->getReturnType())), + $acceptedType->isVariadic(), + $acceptedType->getTemplateTypeMap(), + $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getTemplateTags(), + $acceptedType->isPure(), + ); + } - if ( - !$this->checkNullables - && !$acceptingType instanceof NullType - && !$acceptedType instanceof NullType - && !$acceptedType instanceof BenevolentUnionType - ) { - $acceptedType = TypeCombinator::removeNull($acceptedType); - } + if ($acceptedType instanceof ClosureType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; + } - $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; + return new ClosureType( + $acceptedType->getParameters(), + $traverse($this->transformCommonType($acceptedType->getReturnType())), + $acceptedType->isVariadic(), + $acceptedType->getTemplateTypeMap(), + $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getCallSiteVarianceMap(), + $acceptedType->getTemplateTags(), + $acceptedType->getThrowPoints(), + $acceptedType->getImpurePoints(), + $acceptedType->getInvalidateExpressions(), + $acceptedType->getUsedVariables(), + $acceptedType->acceptsNamedArguments(), + ); + } + + if ( + !$this->checkNullables + && !$acceptingType instanceof NullType + && !$acceptedType instanceof NullType + && !$acceptedType instanceof BenevolentUnionType + ) { + return $traverse(TypeCombinator::removeNull($acceptedType)); + } + + if ($this->checkBenevolentUnionTypes) { + if ($acceptedType instanceof BenevolentUnionType) { + $checkForUnion = true; + return $traverse(TypeUtils::toStrictUnion($acceptedType)); } } - return false; - } + return $traverse($this->transformCommonType($acceptedType)); + }); - if ( - $acceptedType->isArray()->yes() - && $acceptingType->isArray()->yes() - && !$acceptingType->isIterableAtLeastOnce()->yes() - && count(TypeUtils::getConstantArrays($acceptedType)) === 0 - && count(TypeUtils::getConstantArrays($acceptingType)) === 0 - ) { - return self::accepts( - $acceptingType->getIterableKeyType(), - $acceptedType->getIterableKeyType(), - $strictTypes, - ) && self::accepts( - $acceptingType->getIterableValueType(), - $acceptedType->getIterableValueType(), - $strictTypes, - ); - } + return [$acceptedType, $checkForUnion]; + } + + /** @api */ + public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult + { + [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); + $acceptingType = $this->transformCommonType($acceptingType); - return $this->checkUnionTypes ? $accepts->yes() : !$accepts->no(); + $accepts = $acceptingType->accepts($acceptedType, $strictTypes); + + return new RuleLevelHelperAcceptsResult( + $checkForUnion ? $accepts->yes() : !$accepts->no(), + $accepts->reasons, + ); } /** @@ -123,71 +167,154 @@ public function findTypeToCheck( return new FoundTypeResult(new ErrorType(), [], [], null); } $type = $scope->getType($var); - if (!$this->checkNullables && !$type instanceof NullType) { + + return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true); + } + + /** @param callable(Type $type): bool $unionTypeCriteriaCallback */ + private function findTypeToCheckImplementation( + Scope $scope, + Expr $var, + Type $type, + string $unknownClassErrorPattern, + callable $unionTypeCriteriaCallback, + bool $isTopLevel = false, + ): FoundTypeResult + { + if (!$this->checkNullables && !$type->isNull()->yes()) { $type = TypeCombinator::removeNull($type); } if ( - $this->checkExplicitMixed + ($this->checkExplicitMixed || $this->checkImplicitMixed) && $type instanceof MixedType - && !$type instanceof TemplateMixedType - && $type->isExplicitMixed() + && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed) ) { - return new FoundTypeResult(new StrictMixedType(), [], [], null); + return new FoundTypeResult( + $type instanceof TemplateMixedType + ? $type->toStrictMixedType() + : new StrictMixedType(), + [], + [], + null, + ); } if ($type instanceof MixedType || $type instanceof NeverType) { return new FoundTypeResult(new ErrorType(), [], [], null); } - if ($type instanceof StaticType) { - $type = $type->getStaticObjectType(); - } $errors = []; - $directClassNames = TypeUtils::getDirectClassNames($type); $hasClassExistsClass = false; - foreach ($directClassNames as $referencedClass) { - if ($this->reflectionProvider->hasClass($referencedClass)) { - $classReflection = $this->reflectionProvider->getClass($referencedClass); - if (!$classReflection->isTrait()) { + $directClassNames = []; + + if ($isTopLevel) { + $directClassNames = $type->getObjectClassNames(); + foreach ($directClassNames as $referencedClass) { + if ($this->reflectionProvider->hasClass($referencedClass)) { + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->isTrait()) { + continue; + } + } + + if ($scope->isInClassExists($referencedClass)) { + $hasClassExistsClass = true; continue; } - } - if ($scope->isInClassExists($referencedClass)) { - $hasClassExistsClass = true; - continue; - } + $errorBuilder = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) + ->line($var->getStartLine()) + ->identifier('class.notFound'); - $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))->line($var->getLine())->discoveringSymbolsTip()->build(); + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + } } if (count($errors) > 0 || $hasClassExistsClass) { return new FoundTypeResult(new ErrorType(), [], $errors, null); } - if (!$this->checkUnionTypes) { - if ($type instanceof ObjectWithoutClassType) { - return new FoundTypeResult(new ErrorType(), [], [], null); + if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) { + return new FoundTypeResult(new ErrorType(), [], [], null); + } + + if ($type instanceof UnionType) { + $shouldFilterUnion = ( + !$this->checkUnionTypes + && !$type instanceof BenevolentUnionType + ) || ( + !$this->checkBenevolentUnionTypes + && $type instanceof BenevolentUnionType + ); + + $newTypes = []; + + foreach ($type->getTypes() as $innerType) { + if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { + continue; + } + + $newTypes[] = $this->findTypeToCheckImplementation( + $scope, + $var, + $innerType, + $unknownClassErrorPattern, + $unionTypeCriteriaCallback, + )->getType(); } - if ($type instanceof UnionType) { - $newTypes = []; - foreach ($type->getTypes() as $innerType) { - if (!$unionTypeCriteriaCallback($innerType)) { - continue; - } - $newTypes[] = $innerType; + if (count($newTypes) > 0) { + $newUnion = TypeCombinator::union(...$newTypes); + if ( + !$this->checkBenevolentUnionTypes + && $type instanceof BenevolentUnionType + ) { + $newUnion = TypeUtils::toBenevolentUnion($newUnion); } - if (count($newTypes) > 0) { - return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); + return new FoundTypeResult($newUnion, $directClassNames, [], null); + } + } + + if ($type instanceof IntersectionType) { + $newTypes = []; + + $changed = false; + foreach ($type->getTypes() as $innerType) { + if ($innerType instanceof TemplateMixedType) { + $changed = true; + $newTypes[] = $this->findTypeToCheckImplementation( + $scope, + $var, + $innerType->toStrictMixedType(), + $unknownClassErrorPattern, + $unionTypeCriteriaCallback, + )->getType(); + continue; } + $newTypes[] = $innerType; + } + + if ($changed) { + return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); } } $tip = null; - if (strpos($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') !== false && !$unionTypeCriteriaCallback($type)) { + if ( + $type instanceof UnionType + && count($type->getTypes()) === 2 + && $type->getTypes()[0] instanceof ObjectType + && $type->getTypes()[1] instanceof ObjectType + && $type->getTypes()[0]->getClassName() === 'PhpParser\\Node\\Arg' + && $type->getTypes()[1]->getClassName() === 'PhpParser\\Node\\VariadicPlaceholder' + && !$unionTypeCriteriaCallback($type) + ) { $tip = 'Use ->getArgs() instead of ->args.'; } diff --git a/src/Rules/RuleLevelHelperAcceptsResult.php b/src/Rules/RuleLevelHelperAcceptsResult.php new file mode 100644 index 0000000000..1b421c60a4 --- /dev/null +++ b/src/Rules/RuleLevelHelperAcceptsResult.php @@ -0,0 +1,44 @@ + $reasons + */ + public function __construct( + public readonly bool $result, + public readonly array $reasons, + ) + { + } + + public function and(self $other): self + { + return new self( + $this->result && $other->result, + array_merge($this->reasons, $other->reasons), + ); + } + + /** + * @param callable(string): string $cb + */ + public function decorateReasons(callable $cb): self + { + $reasons = []; + foreach ($this->reasons as $reason) { + $reasons[] = $cb($reason); + } + + return new self($this->result, $reasons); + } + +} diff --git a/src/Rules/TipRuleError.php b/src/Rules/TipRuleError.php index fa9f8f9885..ac518ddce7 100644 --- a/src/Rules/TipRuleError.php +++ b/src/Rules/TipRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface TipRuleError extends RuleError { diff --git a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php index 01b4e33160..74c5328ba4 100644 --- a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php @@ -7,7 +7,6 @@ use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -15,7 +14,7 @@ /** * @implements Rule */ -class TooWideArrowFunctionReturnTypehintRule implements Rule +final class TooWideArrowFunctionReturnTypehintRule implements Rule { public function getNodeType(): string @@ -25,22 +24,23 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReturnType = $scope->getAnonymousFunctionReturnType(); - if ($functionReturnType === null || !$functionReturnType instanceof UnionType) { - return []; - } - $arrowFunction = $node->getOriginalNode(); if ($arrowFunction->returnType === null) { return []; } + $expr = $arrowFunction->expr; if ($expr instanceof Node\Expr\YieldFrom || $expr instanceof Node\Expr\Yield_) { return []; } + $functionReturnType = $scope->getFunctionType($arrowFunction->returnType, false, false); + if (!$functionReturnType instanceof UnionType) { + return []; + } + $returnType = $scope->getType($expr); - if ($returnType instanceof NullType) { + if ($returnType->isNull()->yes()) { return []; } $messages = []; @@ -52,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Anonymous function never returns %s so it can be removed from the return type.', $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->build(); + ))->identifier('return.unusedType')->build(); } return $messages; diff --git a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php index 60cd322ce6..bf49b02765 100644 --- a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php @@ -7,7 +7,6 @@ use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -17,7 +16,7 @@ /** * @implements Rule */ -class TooWideClosureReturnTypehintRule implements Rule +final class TooWideClosureReturnTypehintRule implements Rule { public function getNodeType(): string @@ -27,11 +26,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $closureReturnType = $scope->getAnonymousFunctionReturnType(); - if ($closureReturnType === null || !$closureReturnType instanceof UnionType) { - return []; - } - $closureExpr = $node->getClosureExpr(); if ($closureExpr->returnType === null) { return []; @@ -47,6 +41,11 @@ public function processNode(Node $node, Scope $scope): array return []; } + $closureReturnType = $scope->getFunctionType($closureExpr->returnType, false, false); + if (!$closureReturnType instanceof UnionType) { + return []; + } + $returnTypes = []; foreach ($returnStatements as $returnStatement) { $returnNode = $returnStatement->getReturnNode(); @@ -62,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array } $returnType = TypeCombinator::union(...$returnTypes); - if ($returnType instanceof NullType) { + if ($returnType->isNull()->yes()) { return []; } @@ -75,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Anonymous function never returns %s so it can be removed from the return type.', $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->build(); + ))->identifier('return.unusedType')->build(); } return $messages; diff --git a/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php new file mode 100644 index 0000000000..8e2c1f57e8 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php @@ -0,0 +1,40 @@ + + */ +final class TooWideFunctionParameterOutTypeRule implements Rule +{ + + public function __construct( + private TooWideParameterOutTypeCheck $check, + ) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $node->getFunctionReflection(); + + return $this->check->check( + $node->getExecutionEnds(), + $node->getReturnStatements(), + $inFunction->getParameters(), + sprintf('Function %s()', $inFunction->getName()), + ); + } + +} diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index 35f4080685..3b52a47aad 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -5,22 +5,20 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPStan\Type\VoidType; use function count; use function sprintf; /** * @implements Rule */ -class TooWideFunctionReturnTypehintRule implements Rule +final class TooWideFunctionReturnTypehintRule implements Rule { public function getNodeType(): string @@ -30,12 +28,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $function = $scope->getFunction(); - if (!$function instanceof FunctionReflection) { - throw new ShouldNotHappenException(); - } + $function = $node->getFunctionReflection(); - $functionReturnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); + $functionReturnType = $function->getReturnType(); + $functionReturnType = TypeUtils::resolveLateResolvableTypes($functionReturnType); if (!$functionReturnType instanceof UnionType) { return []; } @@ -53,14 +49,15 @@ public function processNode(Node $node, Scope $scope): array foreach ($returnStatements as $returnStatement) { $returnNode = $returnStatement->getReturnNode(); if ($returnNode->expr === null) { + $returnTypes[] = new VoidType(); continue; } $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); } - if (count($returnTypes) === 0) { - return []; + if (!$statementResult->isAlwaysTerminating()) { + $returnTypes[] = new VoidType(); } $returnType = TypeCombinator::union(...$returnTypes); @@ -71,7 +68,7 @@ public function processNode(Node $node, Scope $scope): array continue; } - if ($type instanceof NullType && !$node->hasNativeReturnTypehint()) { + if ($type->isNull()->yes() && !$node->hasNativeReturnTypehint()) { foreach ($node->getExecutionEnds() as $executionEnd) { if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { continue; @@ -85,7 +82,7 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() never returns %s so it can be removed from the return type.', $function->getName(), $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->build(); + ))->identifier('return.unusedType')->build(); } return $messages; diff --git a/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php new file mode 100644 index 0000000000..9c098e27b2 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php @@ -0,0 +1,40 @@ + + */ +final class TooWideMethodParameterOutTypeRule implements Rule +{ + + public function __construct( + private TooWideParameterOutTypeCheck $check, + ) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inMethod = $node->getMethodReflection(); + + return $this->check->check( + $node->getExecutionEnds(), + $node->getReturnStatements(), + $inMethod->getParameters(), + sprintf('Method %s::%s()', $inMethod->getDeclaringClass()->getDisplayName(), $inMethod->getName()), + ); + } + +} diff --git a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php index 259487e3bf..1c40c4a9ce 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -5,23 +5,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPStan\Type\VoidType; use function count; use function sprintf; /** * @implements Rule */ -class TooWideMethodReturnTypehintRule implements Rule +final class TooWideMethodReturnTypehintRule implements Rule { public function __construct(private bool $checkProtectedAndPublicMethods) @@ -35,21 +33,25 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { - throw new ShouldNotHappenException(); + if ($scope->isInTrait()) { + return []; } + $method = $node->getMethodReflection(); $isFirstDeclaration = $method->getPrototype()->getDeclaringClass() === $method->getDeclaringClass(); if (!$method->isPrivate()) { - if (!$this->checkProtectedAndPublicMethods) { - return []; - } - if ($isFirstDeclaration && !$method->getDeclaringClass()->isFinal() && !$method->isFinal()->yes()) { - return []; + if (!$method->getDeclaringClass()->isFinal() && !$method->isFinal()->yes()) { + if (!$this->checkProtectedAndPublicMethods) { + return []; + } + + if ($isFirstDeclaration) { + return []; + } } } - $methodReturnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); + $methodReturnType = $method->getReturnType(); + $methodReturnType = TypeUtils::resolveLateResolvableTypes($methodReturnType); if (!$methodReturnType instanceof UnionType) { return []; } @@ -67,20 +69,21 @@ public function processNode(Node $node, Scope $scope): array foreach ($returnStatements as $returnStatement) { $returnNode = $returnStatement->getReturnNode(); if ($returnNode->expr === null) { + $returnTypes[] = new VoidType(); continue; } $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); } - if (count($returnTypes) === 0) { - return []; + if (!$statementResult->isAlwaysTerminating()) { + $returnTypes[] = new VoidType(); } $returnType = TypeCombinator::union(...$returnTypes); if ( !$method->isPrivate() - && ($returnType instanceof NullType || $returnType instanceof ConstantBooleanType) + && ($returnType->isNull()->yes() || $returnType instanceof ConstantBooleanType) && !$isFirstDeclaration ) { return []; @@ -92,7 +95,7 @@ public function processNode(Node $node, Scope $scope): array continue; } - if ($type instanceof NullType && !$node->hasNativeReturnTypehint()) { + if ($type->isNull()->yes() && !$node->hasNativeReturnTypehint()) { foreach ($node->getExecutionEnds() as $executionEnd) { if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { continue; @@ -107,7 +110,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->build(); + ))->identifier('return.unusedType')->build(); } return $messages; diff --git a/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php new file mode 100644 index 0000000000..e891b65fb6 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php @@ -0,0 +1,118 @@ + $executionEnds + * @param list $returnStatements + * @param ExtendedParameterReflection[] $parameters + * @return list + */ + public function check( + array $executionEnds, + array $returnStatements, + array $parameters, + string $functionDescription, + ): array + { + $finalScope = null; + foreach ($executionEnds as $executionEnd) { + $endScope = $executionEnd->getStatementResult()->getScope(); + if ($finalScope === null) { + $finalScope = $endScope; + continue; + } + + $finalScope = $finalScope->mergeWith($endScope); + } + + foreach ($returnStatements as $statement) { + if ($finalScope === null) { + $finalScope = $statement->getScope(); + continue; + } + + $finalScope = $finalScope->mergeWith($statement->getScope()); + } + + if ($finalScope === null) { + return []; + } + + $errors = []; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + foreach ($this->processSingleParameter($finalScope, $functionDescription, $parameter) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleParameter( + Scope $scope, + string $functionDescription, + ExtendedParameterReflection $parameter, + ): array + { + $isParamOutType = true; + $outType = $parameter->getOutType(); + if ($outType === null) { + $isParamOutType = false; + $outType = $parameter->getType(); + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + if (!$outType instanceof UnionType) { + return []; + } + + $variableExpr = new Variable($parameter->getName()); + $variableType = $scope->getType($variableExpr); + + $messages = []; + foreach ($outType->getTypes() as $type) { + if (!$type->isSuperTypeOf($variableType)->no()) { + continue; + } + + $errorBuilder = RuleErrorBuilder::message(sprintf( + '%s never assigns %s to &$%s so it can be removed from the %s.', + $functionDescription, + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + $parameter->getName(), + $isParamOutType ? '@param-out type' : 'by-ref type', + ))->identifier(sprintf('%s.unusedType', $isParamOutType ? 'paramOut' : 'parameterByRef')); + if (!$isParamOutType) { + $errorBuilder->tip('You can narrow the parameter out type with @param-out PHPDoc tag.'); + } + + $messages[] = $errorBuilder->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php new file mode 100644 index 0000000000..8d77cb01b4 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php @@ -0,0 +1,135 @@ + + */ +final class TooWidePropertyTypeRule implements Rule +{ + + public function __construct( + private ReadWritePropertiesExtensionProvider $extensionProvider, + private PropertyReflectionFinder $propertyReflectionFinder, + ) + { + } + + public function getNodeType(): string + { + return ClassPropertiesNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + $classReflection = $node->getClassReflection(); + + foreach ($node->getProperties() as $property) { + if (!$property->isPrivate()) { + continue; + } + if ($property->isDeclaredInTrait()) { + continue; + } + if ($property->isPromoted()) { + continue; + } + $propertyName = $property->getName(); + if (!$classReflection->hasNativeProperty($propertyName)) { + continue; + } + + $propertyReflection = $classReflection->getNativeProperty($propertyName); + $propertyType = $propertyReflection->getWritableType(); + if (!$propertyType instanceof UnionType) { + continue; + } + foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysRead($propertyReflection, $propertyName)) { + continue 2; + } + if ($extension->isAlwaysWritten($propertyReflection, $propertyName)) { + continue 2; + } + if ($extension->isInitialized($propertyReflection, $propertyName)) { + continue 2; + } + } + + $assignedTypes = []; + foreach ($node->getPropertyAssigns() as $assign) { + $assignNode = $assign->getAssign(); + $assignPropertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($assignNode->getPropertyFetch(), $assign->getScope()); + foreach ($assignPropertyReflections as $assignPropertyReflection) { + if ($propertyName !== $assignPropertyReflection->getName()) { + continue; + } + if ($propertyReflection->getDeclaringClass()->getName() !== $assignPropertyReflection->getDeclaringClass()->getName()) { + continue; + } + + $assignedTypes[] = $assignPropertyReflection->getScope()->getType($assignNode->getAssignedExpr()); + } + } + + if ($property->getDefault() !== null) { + $assignedTypes[] = $scope->getType($property->getDefault()); + } + + if (count($assignedTypes) === 0) { + continue; + } + + $assignedType = TypeCombinator::union(...$assignedTypes); + $propertyDescription = $this->describePropertyByName($propertyReflection, $propertyName); + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedType); + foreach ($propertyType->getTypes() as $type) { + if (!$type->isSuperTypeOf($assignedType)->no()) { + continue; + } + + if ($property->getNativeType() === null && (new NullType())->isSuperTypeOf($type)->yes()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s (%s) is never assigned %s so it can be removed from the property type.', + $propertyDescription, + $propertyType->describe($verbosityLevel), + $type->describe($verbosityLevel), + )) + ->identifier('property.unusedType') + ->line($property->getStartLine()) + ->build(); + } + + } + return $errors; + } + + private function describePropertyByName(PropertyReflection $property, string $propertyName): string + { + if (!$property->isStatic()) { + return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); + } + + return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); + } + +} diff --git a/src/Rules/Traits/ConflictingTraitConstantsRule.php b/src/Rules/Traits/ConflictingTraitConstantsRule.php new file mode 100644 index 0000000000..4e38e44bcb --- /dev/null +++ b/src/Rules/Traits/ConflictingTraitConstantsRule.php @@ -0,0 +1,252 @@ + + */ +final class ConflictingTraitConstantsRule implements Rule +{ + + public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + $traitConstants = []; + foreach ($classReflection->getTraits(true) as $trait) { + foreach ($trait->getNativeReflection()->getReflectionConstants() as $constant) { + $traitConstants[] = $constant; + } + } + + $errors = []; + foreach ($node->consts as $const) { + foreach ($traitConstants as $traitConstant) { + if ($traitConstant->getName() !== $const->name->toString()) { + continue; + } + + foreach ($this->processSingleConstant($classReflection, $traitConstant, $node, $const->value) as $error) { + $errors[] = $error; + } + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleConstant(ClassReflection $classReflection, ReflectionClassConstant $traitConstant, Node\Stmt\ClassConst $classConst, Node\Expr $valueExpr): array + { + $errors = []; + if ($traitConstant->isPublic()) { + if ($classConst->isProtected()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Protected constant %s::%s overriding public constant %s::%s should also be public.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } elseif ($classConst->isPrivate()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private constant %s::%s overriding public constant %s::%s should also be public.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } + } elseif ($traitConstant->isProtected()) { + if ($classConst->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Public constant %s::%s overriding protected constant %s::%s should also be protected.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } elseif ($classConst->isPrivate()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private constant %s::%s overriding protected constant %s::%s should also be protected.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } + } elseif ($traitConstant->isPrivate()) { + if ($classConst->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Public constant %s::%s overriding private constant %s::%s should also be private.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } elseif ($classConst->isProtected()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Protected constant %s::%s overriding private constant %s::%s should also be private.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } + } + + if ($traitConstant->isFinal()) { + if (!$classConst->isFinal()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Non-final constant %s::%s overriding final constant %s::%s should also be final.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.nonFinal') + ->build(); + } + } elseif ($classConst->isFinal()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Final constant %s::%s overriding non-final constant %s::%s should also be non-final.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.final') + ->build(); + } + + $traitNativeType = $traitConstant->getType(); + $constantNativeType = $classConst->type; + $traitDeclaringClass = $traitConstant->getDeclaringClass(); + if ($traitNativeType === null) { + if ($constantNativeType !== null) { + $constantNativeTypeType = ParserNodeTypeToPHPStanType::resolve($constantNativeType, $classReflection); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) overriding constant %s::%s should not have a native type.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $constantNativeTypeType->describe(VerbosityLevel::typeOnly()), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.nativeType') + ->build(); + } + } elseif ($constantNativeType === null) { + $traitNativeTypeType = TypehintHelper::decideTypeFromReflection($traitNativeType, null, $this->reflectionProvider->getClass($traitDeclaringClass->getName())); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s overriding constant %s::%s (%s) should also have native type %s.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + $traitNativeTypeType->describe(VerbosityLevel::typeOnly()), + $traitNativeTypeType->describe(VerbosityLevel::typeOnly()), + )) + ->nonIgnorable() + ->identifier('classConstant.missingNativeType') + ->build(); + } else { + $traitNativeTypeType = TypehintHelper::decideTypeFromReflection($traitNativeType, null, $this->reflectionProvider->getClass($traitDeclaringClass->getName())); + $constantNativeTypeType = ParserNodeTypeToPHPStanType::resolve($constantNativeType, $classReflection); + if (!$traitNativeTypeType->equals($constantNativeTypeType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) overriding constant %s::%s (%s) should have the same native type %s.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $constantNativeTypeType->describe(VerbosityLevel::typeOnly()), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + $traitNativeTypeType->describe(VerbosityLevel::typeOnly()), + $traitNativeTypeType->describe(VerbosityLevel::typeOnly()), + )) + ->nonIgnorable() + ->identifier('classConstant.nativeType') + ->build(); + } + } + + $classConstantValueType = $this->initializerExprTypeResolver->getType($valueExpr, InitializerExprContext::fromClassReflection($classReflection)); + $traitConstantValueType = $this->initializerExprTypeResolver->getType( + $traitConstant->getValueExpression(), + InitializerExprContext::fromClass( + $traitDeclaringClass->getName(), + $traitDeclaringClass->getFileName() !== false ? $traitDeclaringClass->getFileName() : null, + ), + ); + if (!$classConstantValueType->equals($traitConstantValueType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s with value %s overriding constant %s::%s with different value %s should have the same value.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $classConstantValueType->describe(VerbosityLevel::value()), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + $traitConstantValueType->describe(VerbosityLevel::value()), + )) + ->nonIgnorable() + ->identifier('classConstant.value') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Traits/ConstantsInTraitsRule.php b/src/Rules/Traits/ConstantsInTraitsRule.php new file mode 100644 index 0000000000..177af08c6e --- /dev/null +++ b/src/Rules/Traits/ConstantsInTraitsRule.php @@ -0,0 +1,46 @@ + + */ +final class ConstantsInTraitsRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + /** + * @param Node\Stmt\ClassConst $node + */ + public function processNode(Node $node, Scope $scope): array + { + if ($this->phpVersion->supportsConstantsInTraits()) { + return []; + } + + if (!$scope->isInTrait()) { + return []; + } + + return [ + RuleErrorBuilder::message( + 'Constant is declared inside a trait but is only supported on PHP 8.2 and later.', + )->identifier('classConstant.inTrait')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Traits/NotAnalysedTraitRule.php b/src/Rules/Traits/NotAnalysedTraitRule.php new file mode 100644 index 0000000000..6abc321550 --- /dev/null +++ b/src/Rules/Traits/NotAnalysedTraitRule.php @@ -0,0 +1,64 @@ + + */ +final class NotAnalysedTraitRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->isOnlyFilesAnalysis()) { + return []; + } + + $traitDeclarationData = $node->get(TraitDeclarationCollector::class); + $traitUseData = $node->get(TraitUseCollector::class); + + $declaredTraits = []; + foreach ($traitDeclarationData as $file => $declaration) { + foreach ($declaration as [$name, $line]) { + $declaredTraits[strtolower($name)] = [$file, $name, $line]; + } + } + + foreach ($traitUseData as $usedNamesData) { + foreach ($usedNamesData as $usedNames) { + foreach ($usedNames as $usedName) { + unset($declaredTraits[strtolower($usedName)]); + } + } + } + + $errors = []; + foreach ($declaredTraits as [$file, $name, $line]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Trait %s is used zero times and is not analysed.', + $name, + )) + ->file($file) + ->line($line) + ->identifier('trait.unused') + ->tip('See: https://phpstan.org/blog/how-phpstan-analyses-traits') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Traits/TraitAttributesRule.php b/src/Rules/Traits/TraitAttributesRule.php new file mode 100644 index 0000000000..2b9fa768d0 --- /dev/null +++ b/src/Rules/Traits/TraitAttributesRule.php @@ -0,0 +1,51 @@ + + */ +final class TraitAttributesRule implements Rule +{ + + public function __construct( + private AttributesCheck $attributesCheck, + ) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + $errors = $this->attributesCheck->check( + $scope, + $originalNode->attrGroups, + Attribute::TARGET_CLASS, + 'class', + ); + + if (count($node->getTraitReflection()->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { + $errors[] = RuleErrorBuilder::message('Attribute class AllowDynamicProperties cannot be used with trait.') + ->identifier('trait.allowDynamicProperties') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Traits/TraitDeclarationCollector.php b/src/Rules/Traits/TraitDeclarationCollector.php new file mode 100644 index 0000000000..5ccb33a736 --- /dev/null +++ b/src/Rules/Traits/TraitDeclarationCollector.php @@ -0,0 +1,29 @@ + + */ +final class TraitDeclarationCollector implements Collector +{ + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope) + { + if ($node->namespacedName === null) { + return null; + } + + return [$node->namespacedName->toString(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/Traits/TraitUseCollector.php b/src/Rules/Traits/TraitUseCollector.php new file mode 100644 index 0000000000..cd97f3b9b4 --- /dev/null +++ b/src/Rules/Traits/TraitUseCollector.php @@ -0,0 +1,30 @@ +> + */ +final class TraitUseCollector implements Collector +{ + + public function getNodeType(): string + { + return Node\Stmt\TraitUse::class; + } + + /** + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + return array_values(array_map(static fn (Node\Name $traitName) => $traitName->toString(), $node->traits)); + } + +} diff --git a/src/Rules/Types/InvalidTypesInUnionRule.php b/src/Rules/Types/InvalidTypesInUnionRule.php new file mode 100644 index 0000000000..39379b6663 --- /dev/null +++ b/src/Rules/Types/InvalidTypesInUnionRule.php @@ -0,0 +1,125 @@ + + */ +final class InvalidTypesInUnionRule implements Rule +{ + + private const ONLY_STANDALONE_TYPES = [ + 'mixed', + 'never', + 'void', + ]; + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node instanceof Node\FunctionLike && !$node instanceof ClassPropertyNode) { + return []; + } + + if ($node instanceof Node\FunctionLike) { + return $this->processFunctionLikeNode($node); + } + + return $this->processClassPropertyNode($node); + } + + /** + * @return list + */ + private function processFunctionLikeNode(Node\FunctionLike $functionLike): array + { + $errors = []; + + foreach ($functionLike->getParams() as $param) { + if (!$param->type instanceof Node\ComplexType) { + continue; + } + + $errors = array_merge($errors, $this->processComplexType($param->type)); + } + + if ($functionLike->getReturnType() instanceof Node\ComplexType) { + $errors = array_merge($errors, $this->processComplexType($functionLike->getReturnType())); + } + + return $errors; + } + + /** + * @return list + */ + private function processClassPropertyNode(ClassPropertyNode $classPropertyNode): array + { + if (!$classPropertyNode->getNativeTypeNode() instanceof Node\ComplexType) { + return []; + } + + return $this->processComplexType($classPropertyNode->getNativeTypeNode()); + } + + /** + * @return list + */ + private function processComplexType(Node\ComplexType $complexType): array + { + if (!$complexType instanceof Node\UnionType && !$complexType instanceof Node\NullableType) { + return []; + } + + if ($complexType instanceof Node\UnionType) { + foreach ($complexType->types as $type) { + if (!$type instanceof Node\Identifier) { + continue; + } + + $typeString = $type->toLowerString(); + if (in_array($typeString, self::ONLY_STANDALONE_TYPES, true)) { + return [ + RuleErrorBuilder::message(sprintf('Type %s cannot be part of a union type declaration.', $type->toString())) + ->line($complexType->getStartLine()) + ->identifier(sprintf('unionType.%s', $typeString)) + ->nonIgnorable() + ->build(), + ]; + } + } + + return []; + } + + if ($complexType->type instanceof Node\Identifier) { + $complexTypeString = $complexType->type->toLowerString(); + if (in_array($complexTypeString, self::ONLY_STANDALONE_TYPES, true)) { + return [ + RuleErrorBuilder::message(sprintf('Type %s cannot be part of a nullable type declaration.', $complexType->type->toString())) + ->line($complexType->getStartLine()) + ->identifier(sprintf('nullableType.%s', $complexTypeString)) + ->nonIgnorable() + ->build(), + ]; + } + } + + return []; + } + +} diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php index 890fc5d3f6..628041a032 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -3,39 +3,49 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; -use function array_fill_keys; -use function array_keys; +use function array_combine; +use function array_map; use function array_merge; use function is_array; use function is_string; use function sprintf; -class UnusedFunctionParametersCheck +final class UnusedFunctionParametersCheck { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private bool $reportExactLine, + ) { } /** - * @param string[] $parameterNames + * @param Variable[] $parameterVars * @param Node[] $statements - * @param mixed[] $additionalMetadata - * @return RuleError[] + * @param 'constructor.unusedParameter'|'closure.unusedUse' $identifier + * @return list */ public function getUnusedParameters( Scope $scope, - array $parameterNames, + array $parameterVars, array $statements, string $unusedParameterMessage, string $identifier, - array $additionalMetadata, ): array { - $unusedParameters = array_fill_keys($parameterNames, true); + $parameterNames = array_map(static function (Variable $variable): string { + if (!is_string($variable->name)) { + throw new ShouldNotHappenException(); + } + return $variable->name; + }, $parameterVars); + $unusedParameters = array_combine($parameterNames, $parameterVars); foreach ($this->getUsedVariables($scope, $statements) as $variableName) { if (!isset($unusedParameters[$variableName])) { continue; @@ -44,17 +54,19 @@ public function getUnusedParameters( unset($unusedParameters[$variableName]); } $errors = []; - foreach (array_keys($unusedParameters) as $name) { - $errors[] = RuleErrorBuilder::message( - sprintf($unusedParameterMessage, $name), - )->identifier($identifier)->metadata($additionalMetadata + ['variableName' => $name])->build(); + foreach ($unusedParameters as $name => $variable) { + $errorBuilder = RuleErrorBuilder::message(sprintf($unusedParameterMessage, $name))->identifier($identifier); + if ($this->reportExactLine) { + $errorBuilder->line($variable->getStartLine()); + } + $errors[] = $errorBuilder->build(); } return $errors; } /** - * @param Node[]|Node|scalar $node + * @param Node[]|Node|scalar|null $node * @return string[] */ private function getUsedVariables(Scope $scope, $node): array @@ -63,14 +75,14 @@ private function getUsedVariables(Scope $scope, $node): array if ($node instanceof Node) { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); - if ($functionName === 'func_get_args') { + if ($functionName === 'func_get_args' || $functionName === 'get_defined_vars') { return $scope->getDefinedVariables(); } } - if ($node instanceof Node\Expr\Variable && is_string($node->name) && $node->name !== 'this') { + if ($node instanceof Variable && is_string($node->name) && $node->name !== 'this') { return [$node->name]; } - if ($node instanceof Node\Expr\ClosureUse && is_string($node->var->name)) { + if ($node instanceof Node\ClosureUse && is_string($node->var->name)) { return [$node->var->name]; } if ( diff --git a/src/Rules/Variables/CompactVariablesRule.php b/src/Rules/Variables/CompactVariablesRule.php index 454082e900..c6324f7678 100644 --- a/src/Rules/Variables/CompactVariablesRule.php +++ b/src/Rules/Variables/CompactVariablesRule.php @@ -53,11 +53,11 @@ public function processNode(Node $node, Scope $scope): array if ($scopeHasVariable->no()) { $messages[] = RuleErrorBuilder::message( sprintf('Call to function compact() contains undefined variable $%s.', $variableName), - )->line($argument->getLine())->build(); + )->identifier('variable.undefined')->line($argument->getStartLine())->build(); } elseif ($this->checkMaybeUndefinedVariables && $scopeHasVariable->maybe()) { $messages[] = RuleErrorBuilder::message( sprintf('Call to function compact() contains possibly undefined variable $%s.', $variableName), - )->line($argument->getLine())->build(); + )->identifier('variable.undefined')->line($argument->getStartLine())->build(); } } } diff --git a/src/Rules/Variables/DefinedVariableRule.php b/src/Rules/Variables/DefinedVariableRule.php index 9a69183eaa..2056a89dbe 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -3,10 +3,14 @@ namespace PHPStan\Rules\Variables; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function array_merge; use function in_array; use function is_string; use function sprintf; @@ -14,7 +18,7 @@ /** * @implements Rule */ -class DefinedVariableRule implements Rule +final class DefinedVariableRule implements Rule { public function __construct( @@ -31,11 +35,35 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!is_string($node->name)) { - return []; + $errors = []; + if (is_string($node->name)) { + $variableNameScopes = [$node->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $variableNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $variableNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } } - if ($this->cliArgumentsVariablesRegistered && in_array($node->name, [ + foreach ($variableNameScopes as $name => $variableScope) { + $errors = array_merge($errors, $this->processSingleVariable( + $variableScope, + $node, + (string) $name, // @phpstan-ignore cast.useless + )); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleVariable(Scope $scope, Variable $node, string $variableName): array + { + if ($this->cliArgumentsVariablesRegistered && in_array($variableName, [ 'argc', 'argv', ], true)) { @@ -45,41 +73,23 @@ public function processNode(Node $node, Scope $scope): array } } - if ($scope->isInExpressionAssign($node)) { + if ($scope->isInExpressionAssign($node) || $scope->isUndefinedExpressionAllowed($node)) { return []; } - if ($scope->hasVariableType($node->name)->no()) { + if ($scope->hasVariableType($variableName)->no()) { return [ - RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $node->name)) + RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $variableName)) ->identifier('variable.undefined') - ->metadata([ - 'variableName' => $node->name, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - 'variables' => $scope->getDefinedVariables(), - 'parentVariables' => $this->getParentVariables($scope), - ]) ->build(), ]; } elseif ( $this->checkMaybeUndefinedVariables - && !$scope->hasVariableType($node->name)->yes() + && !$scope->hasVariableType($variableName)->yes() ) { return [ - RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $node->name)) - ->identifier('variable.maybeUndefined') - ->metadata([ - 'variableName' => $node->name, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - 'variables' => $scope->getDefinedVariables(), - 'parentVariables' => $this->getParentVariables($scope), - ]) + RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $variableName)) + ->identifier('variable.undefined') ->build(), ]; } @@ -87,19 +97,4 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** - * @return array> - */ - private function getParentVariables(Scope $scope): array - { - $variables = []; - $parent = $scope->getParentScope(); - while ($parent !== null) { - $variables[] = $parent->getDefinedVariables(); - $parent = $parent->getParentScope(); - } - - return $variables; - } - } diff --git a/src/Rules/Variables/EmptyRule.php b/src/Rules/Variables/EmptyRule.php index 9a48592cb9..12d3fadf59 100644 --- a/src/Rules/Variables/EmptyRule.php +++ b/src/Rules/Variables/EmptyRule.php @@ -6,14 +6,12 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Rule; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; /** * @implements Rule */ -class EmptyRule implements Rule +final class EmptyRule implements Rule { public function __construct(private IssetCheck $issetCheck) @@ -27,12 +25,12 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $error = $this->issetCheck->check($node->expr, $scope, 'in empty()', static function (Type $type): ?string { - $isNull = (new NullType())->isSuperTypeOf($type); - $isFalsey = (new ConstantBooleanType(false))->isSuperTypeOf($type->toBoolean()); + $error = $this->issetCheck->check($node->expr, $scope, 'in empty()', 'empty', static function (Type $type): ?string { + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } + $isFalsey = $type->toBoolean()->isFalse(); if ($isFalsey->maybe()) { return null; } diff --git a/src/Rules/Variables/IssetRule.php b/src/Rules/Variables/IssetRule.php index 55cd5dc05f..69ed263479 100644 --- a/src/Rules/Variables/IssetRule.php +++ b/src/Rules/Variables/IssetRule.php @@ -6,13 +6,12 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Rule; -use PHPStan\Type\NullType; use PHPStan\Type\Type; /** * @implements Rule */ -class IssetRule implements Rule +final class IssetRule implements Rule { public function __construct(private IssetCheck $issetCheck) @@ -28,8 +27,8 @@ public function processNode(Node $node, Scope $scope): array { $messages = []; foreach ($node->vars as $var) { - $error = $this->issetCheck->check($var, $scope, 'in isset()', static function (Type $type): ?string { - $isNull = (new NullType())->isSuperTypeOf($type); + $error = $this->issetCheck->check($var, $scope, 'in isset()', 'isset', static function (Type $type): ?string { + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } diff --git a/src/Rules/Variables/NullCoalesceRule.php b/src/Rules/Variables/NullCoalesceRule.php index 8666ab4f7e..563bec59f7 100644 --- a/src/Rules/Variables/NullCoalesceRule.php +++ b/src/Rules/Variables/NullCoalesceRule.php @@ -6,13 +6,12 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Rule; -use PHPStan\Type\NullType; use PHPStan\Type\Type; /** * @implements Rule */ -class NullCoalesceRule implements Rule +final class NullCoalesceRule implements Rule { public function __construct(private IssetCheck $issetCheck) @@ -27,7 +26,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $typeMessageCallback = static function (Type $type): ?string { - $isNull = (new NullType())->isSuperTypeOf($type); + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } @@ -40,9 +39,9 @@ public function processNode(Node $node, Scope $scope): array }; if ($node instanceof Node\Expr\BinaryOp\Coalesce) { - $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', $typeMessageCallback); + $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', 'nullCoalesce', $typeMessageCallback); } elseif ($node instanceof Node\Expr\AssignOp\Coalesce) { - $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', $typeMessageCallback); + $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', 'nullCoalesce', $typeMessageCallback); } else { return []; } diff --git a/src/Rules/Variables/ParameterOutAssignedTypeRule.php b/src/Rules/Variables/ParameterOutAssignedTypeRule.php new file mode 100644 index 0000000000..e24a938064 --- /dev/null +++ b/src/Rules/Variables/ParameterOutAssignedTypeRule.php @@ -0,0 +1,120 @@ + + */ +final class ParameterOutAssignedTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return VariableAssignNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $scope->getFunction(); + if ($inFunction === null) { + return []; + } + + if ($scope->isInAnonymousFunction()) { + return []; + } + + $variable = $node->getVariable(); + if (!is_string($variable->name)) { + return []; + } + + $parameters = $inFunction->getParameters(); + $foundParameter = null; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + if ($parameter->getName() !== $variable->name) { + continue; + } + + $foundParameter = $parameter; + break; + } + + if ($foundParameter === null) { + return []; + } + + $isParamOutType = true; + $outType = $foundParameter->getOutType(); + if ($outType === null) { + $isParamOutType = false; + $outType = $foundParameter->getType(); + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->getAssignedExpr(), + '', + static fn (Type $type): bool => $outType->isSuperTypeOf($type)->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $assignedExprType = $scope->getType($node->getAssignedExpr()); + if ($outType->isSuperTypeOf($assignedExprType)->yes()) { + return []; + } + + if ($inFunction instanceof ExtendedMethodReflection) { + $functionDescription = sprintf('method %s::%s()', $inFunction->getDeclaringClass()->getDisplayName(), $inFunction->getName()); + } else { + $functionDescription = sprintf('function %s()', $inFunction->getName()); + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($outType, $assignedExprType); + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Parameter &$%s %s of %s expects %s, %s given.', + $foundParameter->getName(), + $isParamOutType ? '@param-out type' : 'by-ref type', + $functionDescription, + $outType->describe($verbosityLevel), + $assignedExprType->describe($verbosityLevel), + ))->identifier(sprintf('%s.type', $isParamOutType ? 'paramOut' : 'parameterByRef')); + + if (!$isParamOutType) { + $errorBuilder->tip('You can change the parameter out type with @param-out PHPDoc tag.'); + } + + return [ + $errorBuilder->build(), + ]; + } + +} diff --git a/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php b/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php new file mode 100644 index 0000000000..d8337c97a0 --- /dev/null +++ b/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php @@ -0,0 +1,132 @@ + + */ +final class ParameterOutExecutionEndTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return ExecutionEndNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $scope->getFunction(); + if ($inFunction === null) { + return []; + } + + if ($scope->isInAnonymousFunction()) { + return []; + } + + $endNode = $node->getNode(); + if ($endNode instanceof Node\Stmt\Expression) { + $endNodeExpr = $endNode->expr; + $endNodeExprType = $scope->getType($endNodeExpr); + if ($endNodeExprType instanceof NeverType && $endNodeExprType->isExplicit()) { + return []; + } + } + + $parameters = $inFunction->getParameters(); + $errors = []; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + foreach ($this->processSingleParameter($scope, $inFunction, $parameter) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleParameter( + Scope $scope, + FunctionReflection|ExtendedMethodReflection $inFunction, + ExtendedParameterReflection $parameter, + ): array + { + $outType = $parameter->getOutType(); + if ($outType === null) { + return []; + } + + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($parameter->getName()))->no()) { + return []; + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + + $variableExpr = new Node\Expr\Variable($parameter->getName()); + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $variableExpr, + '', + static fn (Type $type): bool => $outType->isSuperTypeOf($type)->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $assignedExprType = $scope->getType($variableExpr); + if ($outType->isSuperTypeOf($assignedExprType)->yes()) { + return []; + } + + if ($inFunction instanceof ExtendedMethodReflection) { + $functionDescription = sprintf('method %s::%s()', $inFunction->getDeclaringClass()->getDisplayName(), $inFunction->getName()); + } else { + $functionDescription = sprintf('function %s()', $inFunction->getName()); + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($outType, $assignedExprType); + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Parameter &$%s @param-out type of %s expects %s, %s given.', + $parameter->getName(), + $functionDescription, + $outType->describe($verbosityLevel), + $assignedExprType->describe($verbosityLevel), + ))->identifier(sprintf('paramOut.type')); + + return [ + $errorBuilder->build(), + ]; + } + +} diff --git a/src/Rules/Variables/ThrowTypeRule.php b/src/Rules/Variables/ThrowTypeRule.php deleted file mode 100644 index deb7a22856..0000000000 --- a/src/Rules/Variables/ThrowTypeRule.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ -class ThrowTypeRule implements Rule -{ - - public function __construct( - private RuleLevelHelper $ruleLevelHelper, - ) - { - } - - public function getNodeType(): string - { - return Node\Stmt\Throw_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $throwableType = new ObjectType(Throwable::class); - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $node->expr, - 'Throwing object of an unknown class %s.', - static fn (Type $type): bool => $throwableType->isSuperTypeOf($type)->yes(), - ); - - $foundType = $typeResult->getType(); - if ($foundType instanceof ErrorType) { - return $typeResult->getUnknownClassErrors(); - } - - $isSuperType = $throwableType->isSuperTypeOf($foundType); - if ($isSuperType->yes()) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Invalid type %s to throw.', - $foundType->describe(VerbosityLevel::typeOnly()), - ))->build(), - ]; - } - -} diff --git a/src/Rules/Variables/UnsetRule.php b/src/Rules/Variables/UnsetRule.php index 2484802dc1..91e5260488 100644 --- a/src/Rules/Variables/UnsetRule.php +++ b/src/Rules/Variables/UnsetRule.php @@ -4,8 +4,10 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; use function is_string; @@ -14,9 +16,16 @@ /** * @implements Rule */ -class UnsetRule implements Rule +final class UnsetRule implements Rule { + public function __construct( + private PropertyReflectionFinder $propertyReflectionFinder, + private PhpVersion $phpVersion, + ) + { + } + public function getNodeType(): string { return Node\Stmt\Unset_::class; @@ -28,6 +37,67 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($functionArguments as $argument) { + if ( + $argument instanceof Node\Expr\PropertyFetch + && $argument->name instanceof Node\Identifier + ) { + $foundPropertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($argument, $scope); + if ($foundPropertyReflection === null) { + continue; + } + + $propertyReflection = $foundPropertyReflection->getNativeReflection(); + if ($propertyReflection === null) { + continue; + } + + if ($propertyReflection->isReadOnly() || $propertyReflection->isReadOnlyByPhpDoc()) { + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Cannot unset %s %s::$%s property.', + $propertyReflection->isReadOnly() ? 'readonly' : '@readonly', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $foundPropertyReflection->getName(), + ), + ) + ->line($argument->getStartLine()) + ->identifier($propertyReflection->isReadOnly() ? 'unset.readOnlyProperty' : 'unset.readOnlyPropertyByPhpDoc') + ->build(); + continue; + } + + if ($propertyReflection->isHooked()) { + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Cannot unset hooked %s::$%s property.', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $foundPropertyReflection->getName(), + ), + ) + ->line($argument->getStartLine()) + ->identifier('unset.hookedProperty') + ->build(); + continue; + } elseif ($this->phpVersion->supportsPropertyHooks()) { + if ( + !$propertyReflection->isPrivate() + && !$propertyReflection->isFinal()->yes() + && !$propertyReflection->getDeclaringClass()->isFinal() + ) { + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Cannot unset property %s::$%s because it might have hooks in a subclass.', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $foundPropertyReflection->getName(), + ), + ) + ->line($argument->getStartLine()) + ->identifier('unset.possiblyHookedProperty') + ->build(); + continue; + } + } + } $error = $this->canBeUnset($argument, $scope); if ($error === null) { continue; @@ -39,14 +109,17 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - private function canBeUnset(Node $node, Scope $scope): ?RuleError + private function canBeUnset(Node $node, Scope $scope): ?IdentifierRuleError { if ($node instanceof Node\Expr\Variable && is_string($node->name)) { $hasVariable = $scope->hasVariableType($node->name); if ($hasVariable->no()) { return RuleErrorBuilder::message( sprintf('Call to function unset() contains undefined variable $%s.', $node->name), - )->line($node->getLine())->build(); + ) + ->line($node->getStartLine()) + ->identifier('unset.variable') + ->build(); } } elseif ($node instanceof Node\Expr\ArrayDimFetch && $node->dim !== null) { $type = $scope->getType($node->var); @@ -59,7 +132,10 @@ private function canBeUnset(Node $node, Scope $scope): ?RuleError $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()), ), - )->line($node->getLine())->build(); + ) + ->line($node->getStartLine()) + ->identifier('unset.offset') + ->build(); } return $this->canBeUnset($node->var, $scope); diff --git a/src/Rules/Variables/VariableCloningRule.php b/src/Rules/Variables/VariableCloningRule.php index 81bfbcb253..da72e99ebe 100644 --- a/src/Rules/Variables/VariableCloningRule.php +++ b/src/Rules/Variables/VariableCloningRule.php @@ -18,7 +18,7 @@ /** * @implements Rule */ -class VariableCloningRule implements Rule +final class VariableCloningRule implements Rule { public function __construct(private RuleLevelHelper $ruleLevelHelper) @@ -52,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array 'Cannot clone non-object variable $%s of type %s.', $node->expr->name, $type->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('clone.nonObject')->build(), ]; } @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Cannot clone %s.', $type->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('clone.nonObject')->build(), ]; } diff --git a/src/Rules/Whitespace/FileWhitespaceRule.php b/src/Rules/Whitespace/FileWhitespaceRule.php index 9a48d9293a..3fb3cbf239 100644 --- a/src/Rules/Whitespace/FileWhitespaceRule.php +++ b/src/Rules/Whitespace/FileWhitespaceRule.php @@ -5,6 +5,7 @@ use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\Scope; use PHPStan\Node\FileNode; @@ -15,7 +16,7 @@ /** * @implements Rule */ -class FileWhitespaceRule implements Rule +final class FileWhitespaceRule implements Rule { public function getNodeType(): string @@ -33,7 +34,9 @@ public function processNode(Node $node, Scope $scope): array $firstNode = $nodes[0]; $messages = []; if ($firstNode instanceof Node\Stmt\InlineHTML && $firstNode->value === "\xef\xbb\xbf") { - $messages[] = RuleErrorBuilder::message('File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.')->build(); + $messages[] = RuleErrorBuilder::message('File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.') + ->identifier('whitespace.bom') + ->build(); } $nodeTraverser = new NodeTraverser(); @@ -43,7 +46,7 @@ public function processNode(Node $node, Scope $scope): array private array $lastNodes = []; /** - * @return int|Node|null + * @return int|null */ public function enterNode(Node $node) { @@ -59,7 +62,7 @@ public function enterNode(Node $node) } return null; } - return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } /** @@ -81,7 +84,9 @@ public function getLastNodes(): array continue; } - $messages[] = RuleErrorBuilder::message('File ends with a trailing whitespace. This may cause problems when running the code in the web browser. Remove the closing ?> mark or remove the whitespace.')->line($lastNode->getStartLine())->build(); + $messages[] = RuleErrorBuilder::message('File ends with a trailing whitespace. This may cause problems when running the code in the web browser. Remove the closing ?> mark or remove the whitespace.')->line($lastNode->getStartLine()) + ->identifier('whitespace.fileEnd') + ->build(); } return $messages; diff --git a/src/Testing/DelayedRule.php b/src/Testing/DelayedRule.php new file mode 100644 index 0000000000..a3ae370111 --- /dev/null +++ b/src/Testing/DelayedRule.php @@ -0,0 +1,57 @@ + + */ +final class DelayedRule implements Rule +{ + + private Registry $registry; + + /** @var list */ + private array $errors = []; + + /** + * @param Rule $rule + */ + public function __construct(Rule $rule) + { + $this->registry = new DirectRegistry([$rule]); + } + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return list + */ + public function getDelayedErrors(): array + { + return $this->errors; + } + + public function processNode(Node $node, Scope $scope): array + { + $nodeType = get_class($node); + foreach ($this->registry->getRules($nodeType) as $rule) { + foreach ($rule->processNode($node, $scope) as $error) { + $this->errors[] = $error; + } + } + + return []; + } + +} diff --git a/src/Testing/ErrorFormatterTestCase.php b/src/Testing/ErrorFormatterTestCase.php index 40699ee725..1009a53a0c 100644 --- a/src/Testing/ErrorFormatterTestCase.php +++ b/src/Testing/ErrorFormatterTestCase.php @@ -16,6 +16,9 @@ use function explode; use function fopen; use function implode; +use function in_array; +use function is_int; +use function range; use function rewind; use function rtrim; use function stream_get_contents; @@ -25,38 +28,53 @@ abstract class ErrorFormatterTestCase extends PHPStanTestCase protected const DIRECTORY_PATH = '/data/folder/with space/and unicode 😃/project'; - private ?StreamOutput $outputStream = null; + private const KIND_DECORATED = 'decorated'; + private const KIND_PLAIN = 'plain'; + private const KIND_VERBOSE = '+verbose'; + private const KIND_NOT_VERBOSE = '+not-verbose'; - private ?Output $output = null; + /** @var array */ + private array $outputStream = []; - private function getOutputStream(): StreamOutput + /** @var array */ + private array $output = []; + + private function getOutputStream(bool $decorated = false, bool $verbose = false): StreamOutput { - if ($this->outputStream === null) { + $kind = $decorated ? self::KIND_DECORATED : self::KIND_PLAIN; + $kind .= $verbose ? self::KIND_VERBOSE : self::KIND_NOT_VERBOSE; + + if (!isset($this->outputStream[$kind])) { $resource = fopen('php://memory', 'w', false); if ($resource === false) { throw new ShouldNotHappenException(); } - $this->outputStream = new StreamOutput($resource, StreamOutput::VERBOSITY_NORMAL, false); + $verbosity = $verbose ? StreamOutput::VERBOSITY_VERBOSE : StreamOutput::VERBOSITY_NORMAL; + $this->outputStream[$kind] = new StreamOutput($resource, $verbosity, $decorated); } - return $this->outputStream; + return $this->outputStream[$kind]; } - protected function getOutput(): Output + protected function getOutput(bool $decorated = false, bool $verbose = false): Output { - if ($this->output === null) { - $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $this->getOutputStream()); - $this->output = new SymfonyOutput($this->getOutputStream(), new SymfonyStyle($errorConsoleStyle)); + $kind = $decorated ? self::KIND_DECORATED : self::KIND_PLAIN; + $kind .= $verbose ? self::KIND_VERBOSE : self::KIND_NOT_VERBOSE; + + if (!isset($this->output[$kind])) { + $outputStream = $this->getOutputStream($decorated, $verbose); + $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $outputStream); + $this->output[$kind] = new SymfonyOutput($outputStream, new SymfonyStyle($errorConsoleStyle)); } - return $this->output; + return $this->output[$kind]; } - protected function getOutputContent(): string + protected function getOutputContent(bool $decorated = false, bool $verbose = false): string { - rewind($this->getOutputStream()->getStream()); + rewind($this->getOutputStream($decorated, $verbose)->getStream()); - $contents = stream_get_contents($this->getOutputStream()->getStream()); + $contents = stream_get_contents($this->getOutputStream($decorated, $verbose)->getStream()); if ($contents === false) { throw new ShouldNotHappenException(); } @@ -64,23 +82,36 @@ protected function getOutputContent(): string return $this->rtrimMultiline($contents); } - protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): AnalysisResult + /** + * @param array{int, int}|int $numFileErrors + */ + protected function getAnalysisResult(array|int $numFileErrors, int $numGenericErrors): AnalysisResult { - if ($numFileErrors > 5 || $numFileErrors < 0 || $numGenericErrors > 2 || $numGenericErrors < 0) { + if (is_int($numFileErrors)) { + $offsetFileErrors = 0; + } else { + [$offsetFileErrors, $numFileErrors] = $numFileErrors; + } + + if (!in_array($numFileErrors, range(0, 6), true) || + !in_array($offsetFileErrors, range(0, 6), true) || + !in_array($numGenericErrors, range(0, 2), true) + ) { throw new ShouldNotHappenException(); } $fileErrors = array_slice([ new Error('Foo', self::DIRECTORY_PATH . '/folder with unicode 😃/file name with "spaces" and unicode 😃.php', 4), - new Error('Foo', self::DIRECTORY_PATH . '/foo.php', 1), - new Error("Bar\nBar2", self::DIRECTORY_PATH . '/foo.php', 5), + new Error('Foo', self::DIRECTORY_PATH . '/foo.php', 1), + new Error("Bar\nBar2", self::DIRECTORY_PATH . '/foo.php', 5, true, null, null, 'a tip'), 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); + new Error('Foobar\\Buz', self::DIRECTORY_PATH . '/foo.php', 5, true, null, null, 'a tip', null, null, 'foobar.buz'), + ], $offsetFileErrors, $numFileErrors); $genericErrors = array_slice([ 'first generic error', - 'second generic error', + 'second generic', ], 0, $numGenericErrors); return new AnalysisResult( @@ -88,9 +119,13 @@ protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): $genericErrors, [], [], + [], false, null, true, + 0, + false, + [], ); } diff --git a/src/Testing/LevelsTestCase.php b/src/Testing/LevelsTestCase.php index ca901a0dce..499277dc8b 100644 --- a/src/Testing/LevelsTestCase.php +++ b/src/Testing/LevelsTestCase.php @@ -16,6 +16,7 @@ use function exec; use function implode; use function method_exists; +use function putenv; use function range; use function sprintf; use function unlink; @@ -29,7 +30,7 @@ abstract class LevelsTestCase extends TestCase /** * @return array> */ - abstract public function dataTopics(): array; + abstract public static function dataTopics(): array; abstract public function getDataPath(): string; @@ -63,12 +64,15 @@ public function testLevels( $exceptions = []; - foreach (range(0, 9) as $level) { + 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 ShouldNotHappenException('Could not clear result cache: ' . implode("\n", $clearResultCacheOutputLines)); + } + + putenv('__PHPSTAN_FORCE_VALIDATE_STUB_FILES=1'); + + foreach (range(0, 10) 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 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); @@ -109,6 +113,9 @@ public function testLevels( } } + unset($message['tip']); + unset($message['identifier']); + $messages[] = $message; } @@ -123,6 +130,8 @@ public function testLevels( } } + unset($previousMessage['tip']); + $missingMessages[] = $previousMessage; } @@ -163,7 +172,7 @@ private function compareFiles(string $expectedJsonFile, array $expectedMessages) { if (count($expectedMessages) === 0) { try { - self::assertFileDoesNotExist($expectedJsonFile); + self::ourCustomAssertFileDoesNotExist($expectedJsonFile); return null; } catch (AssertionFailedError $e) { unlink($expectedJsonFile); @@ -186,8 +195,9 @@ private function compareFiles(string $expectedJsonFile, array $expectedMessages) return null; } - public static function assertFileDoesNotExist(string $filename, string $message = ''): void + public static function ourCustomAssertFileDoesNotExist(string $filename, string $message = ''): void { + // this method is no longer called assertFileDoesNotExist because this method is final in PHPUnit 10 if (!method_exists(parent::class, 'assertFileDoesNotExist')) { parent::assertFileNotExists($filename, $message); return; diff --git a/src/Testing/NonexistentAnalysedClassRule.php b/src/Testing/NonexistentAnalysedClassRule.php new file mode 100644 index 0000000000..25c7a11459 --- /dev/null +++ b/src/Testing/NonexistentAnalysedClassRule.php @@ -0,0 +1,47 @@ + + */ +final class NonexistentAnalysedClassRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $className = $node->getClassReflection()->getName(); + if ($this->reflectionProvider->hasClass($className)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + '%s %s not found in ReflectionProvider. Configure "autoload-dev" section in composer.json to include your tests directory.', + $node->getClassReflection()->getClassTypeDescription(), + $node->getClassReflection()->getName(), + )) + ->identifier('phpstan.classNotFound') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Testing/NonexistentAnalysedTraitRule.php b/src/Testing/NonexistentAnalysedTraitRule.php new file mode 100644 index 0000000000..8593429e98 --- /dev/null +++ b/src/Testing/NonexistentAnalysedTraitRule.php @@ -0,0 +1,49 @@ + + */ +final class NonexistentAnalysedTraitRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->namespacedName === null) { + throw new ShouldNotHappenException(); + } + $traitName = $node->namespacedName->toString(); + if ($this->reflectionProvider->hasClass($traitName)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Trait %s not found in ReflectionProvider. Configure "autoload-dev" section in composer.json to include your tests directory.', + $traitName, + )) + ->identifier('phpstan.traitNotFound') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 3eb74e888c..7587ac8d05 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -2,38 +2,42 @@ namespace PHPStan\Testing; -use PhpParser\PrettyPrinter\Standard; -use PHPStan\Analyser\DirectScopeFactory; +use PHPStan\Analyser\ConstantResolver; +use PHPStan\Analyser\DirectInternalScopeFactory; use PHPStan\Analyser\Error; -use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\ScopeFactory; use PHPStan\Analyser\TypeSpecifier; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; -use PHPStan\BetterReflection\Reflector\FunctionReflector; use PHPStan\BetterReflection\Reflector\Reflector; -use PHPStan\Broker\Broker; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ContainerFactory; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; +use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\File\FileHelper; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Parser\Parser; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Type\Constant\OversizedArrayBuilder; use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\UsefulTypeAliasResolver; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use function array_merge; +use function count; use function implode; -use function is_dir; -use function mkdir; use function rtrim; use function sha1; use function sprintf; @@ -45,8 +49,6 @@ abstract class PHPStanTestCase extends TestCase { - public static bool $useStaticReflectionProvider = false; - /** @var array */ private static array $containers = []; @@ -55,15 +57,14 @@ 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)); + try { + DirectoryCreator::ensureDirectoryExists($tmpDir, 0777); + } catch (DirectoryCreatorException $e) { + self::fail($e->getMessage()); } $rootDir = __DIR__ . '/../..'; @@ -88,6 +89,8 @@ public static function getContainer(): Container require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumUnitCase.php'; require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumBackedCase.php'; } + } else { + ContainerFactory::postInitializeContainer(self::$containers[$cacheKey]); } return self::$containers[$cacheKey]; @@ -101,24 +104,15 @@ public static function getAdditionalConfigFiles(): array return []; } - public function getParser(): Parser + public static 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 + public static function createReflectionProvider(): ReflectionProvider { return self::getContainer()->getByType(ReflectionProvider::class); } @@ -128,49 +122,59 @@ 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 + public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider { return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class); } - public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier): ScopeFactory + /** + * @param string[] $dynamicConstantNames + */ + public static function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier, array $dynamicConstantNames = []): 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, + if (count($dynamicConstantNames) === 0) { + $dynamicConstantNames = $container->getParameter('dynamicConstantNames'); + } + + $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); + $composerPhpVersionFactory = $container->getByType(ComposerPhpVersionFactory::class); + $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, null, $composerPhpVersionFactory); + + $initializerExprTypeResolver = new InitializerExprTypeResolver( + $constantResolver, + $reflectionProviderProvider, $container->getByType(PhpVersion::class), - $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], + $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), + new OversizedArrayBuilder(), + $container->getParameter('usePathConstantsAsConstantString'), + ); + + return new ScopeFactory( + new DirectInternalScopeFactory( + $reflectionProvider, + $initializerExprTypeResolver, + $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), + $container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class), + $container->getByType(ExprPrinter::class), + $typeSpecifier, + new PropertyReflectionFinder(), + self::getParser(), + $container->getByType(NodeScopeResolver::class), + new RicherScopeGetTypeHelper($initializerExprTypeResolver), + $container->getByType(PhpVersion::class), + $container->getByType(AttributeReflectionFactory::class), + $container->getParameter('phpVersion'), + $constantResolver, + ), ); } /** * @param array $globalTypeAliases */ - public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver + public static function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver { $container = self::getContainer(); @@ -187,7 +191,7 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return true; } - public function getFileHelper(): FileHelper + public static function getFileHelper(): FileHelper { return self::getContainer()->getByType(FileHelper::class); } diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 9751ad1fb1..026069146a 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -2,21 +2,40 @@ namespace PHPStan\Testing; +use PhpParser\Node; use PHPStan\Analyser\Analyser; +use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\IgnoreErrorExtensionProvider; +use PHPStan\Analyser\InternalError; +use PHPStan\Analyser\LocalIgnoresProcessor; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\RuleErrorTransformer; use PHPStan\Analyser\TypeSpecifier; +use PHPStan\Collectors\Collector; +use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; -use PHPStan\Rules\Registry; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Properties\DirectReadWritePropertiesExtensionProvider; +use PHPStan\Rules\Properties\ReadWritePropertiesExtension; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; use function array_map; +use function array_merge; use function count; use function implode; use function sprintf; @@ -31,52 +50,83 @@ abstract class RuleTestCase extends PHPStanTestCase private ?Analyser $analyser = null; /** - * @phpstan-return TRule + * @return TRule */ abstract protected function getRule(): Rule; + /** + * @return array> + */ + protected function getCollectors(): array + { + return []; + } + + /** + * @return ReadWritePropertiesExtension[] + */ + protected function getReadWritePropertiesExtensions(): array + { + return []; + } + protected function getTypeSpecifier(): TypeSpecifier { return self::getContainer()->getService('typeSpecifier'); } - private function getAnalyser(): Analyser + private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser { if ($this->analyser === null) { - $registry = new Registry([ - $this->getRule(), - ]); + $collectorRegistry = new CollectorRegistry($this->getCollectors()); $reflectionProvider = $this->createReflectionProvider(); $typeSpecifier = $this->getTypeSpecifier(); + + $readWritePropertiesExtensions = $this->getReadWritePropertiesExtensions(); $nodeScopeResolver = new NodeScopeResolver( $reflectionProvider, + self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), - $this->getClassReflectionExtensionRegistryProvider(), + self::getClassReflectionExtensionRegistryProvider(), + self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class), $this->getParser(), self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(SignatureMapProvider::class), + self::getContainer()->getByType(DeprecationProvider::class), + self::getContainer()->getByType(AttributeReflectionFactory::class), self::getContainer()->getByType(PhpDocInheritanceResolver::class), self::getContainer()->getByType(FileHelper::class), $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), + $readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), + self::createScopeFactory($reflectionProvider, $typeSpecifier), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), + self::getContainer()->getParameter('polluteScopeWithBlock'), [], [], - true, + self::getContainer()->getParameter('universalObjectCratesClasses'), + self::getContainer()->getParameter('exceptions')['implicitThrows'], + $this->shouldTreatPhpDocTypesAsCertain(), + $this->shouldNarrowMethodScopeFromConstructor(), ); $fileAnalyser = new FileAnalyser( $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, $this->getParser(), self::getContainer()->getByType(DependencyResolver::class), - true, + new IgnoreErrorExtensionProvider(self::getContainer()), + new RuleErrorTransformer(), + new LocalIgnoresProcessor(), ); $this->analyser = new Analyser( $fileAnalyser, - $registry, + $ruleRegistry, + $collectorRegistry, $nodeScopeResolver, 50, ); @@ -87,22 +137,11 @@ private function getAnalyser(): Analyser /** * @param string[] $files - * @param list $expectedErrors + * @param list $expectedErrors */ public function analyse(array $files, array $expectedErrors): void { - $files = array_map([$this->getFileHelper(), 'normalizePath'], $files); - $analyserResult = $this->getAnalyser()->analyse( - $files, - null, - null, - true, - ); - if (count($analyserResult->getInternalErrors()) > 0) { - $this->fail(implode("\n", $analyserResult->getInternalErrors())); - } - $actualErrors = $analyserResult->getUnorderedErrors(); - + [$actualErrors, $delayedErrors] = $this->gatherAnalyserErrorsWithDelayedErrors($files); $strictlyTypedSprintf = static function (int $line, string $message, ?string $tip): string { $message = sprintf('%02d: %s', $line, $message); if ($tip !== null) { @@ -128,12 +167,91 @@ static function (Error $error) use ($strictlyTypedSprintf): string { $actualErrors, ); - $this->assertSame(implode("\n", $expectedErrors) . "\n", implode("\n", $actualErrors) . "\n"); + $expectedErrorsString = implode("\n", $expectedErrors) . "\n"; + $actualErrorsString = implode("\n", $actualErrors) . "\n"; + + if (count($delayedErrors) === 0) { + $this->assertSame($expectedErrorsString, $actualErrorsString); + return; + } + + if ($expectedErrorsString === $actualErrorsString) { + $this->assertSame($expectedErrorsString, $actualErrorsString); + return; + } + + $actualErrorsString .= sprintf( + "\n%s might be reported because of the following misconfiguration %s:\n\n", + count($actualErrors) === 1 ? 'This error' : 'These errors', + count($delayedErrors) === 1 ? 'issue' : 'issues', + ); + + foreach ($delayedErrors as $delayedError) { + $actualErrorsString .= sprintf("* %s\n", $delayedError->getMessage()); + } + + $this->assertSame($expectedErrorsString, $actualErrorsString); + } + + /** + * @param string[] $files + * @return list + */ + public function gatherAnalyserErrors(array $files): array + { + return $this->gatherAnalyserErrorsWithDelayedErrors($files)[0]; + } + + /** + * @param string[] $files + * @return array{list, list} + */ + private function gatherAnalyserErrorsWithDelayedErrors(array $files): array + { + $reflectionProvider = $this->createReflectionProvider(); + $classRule = new DelayedRule(new NonexistentAnalysedClassRule($reflectionProvider)); + $traitRule = new DelayedRule(new NonexistentAnalysedTraitRule($reflectionProvider)); + $ruleRegistry = new DirectRuleRegistry([ + $this->getRule(), + $classRule, + $traitRule, + ]); + $files = array_map([$this->getFileHelper(), 'normalizePath'], $files); + $analyserResult = $this->getAnalyser($ruleRegistry)->analyse( + $files, + null, + null, + true, + ); + if (count($analyserResult->getInternalErrors()) > 0) { + $this->fail(implode("\n", array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $analyserResult->getInternalErrors()))); + } + + if ($this->shouldFailOnPhpErrors() && count($analyserResult->getAllPhpErrors()) > 0) { + $this->fail(implode("\n", array_map( + static fn (Error $error): string => sprintf('%s on %s:%d', $error->getMessage(), $error->getFile(), $error->getLine()), + $analyserResult->getAllPhpErrors(), + ))); + } + + $finalizer = new AnalyserResultFinalizer( + $ruleRegistry, + new IgnoreErrorExtensionProvider(self::getContainer()), + new RuleErrorTransformer(), + $this->createScopeFactory($reflectionProvider, $this->getTypeSpecifier()), + new LocalIgnoresProcessor(), + true, + ); + + return [ + $finalizer->finalize($analyserResult, false, true)->getAnalyserResult()->getUnorderedErrors(), + array_merge($classRule->getDelayedErrors(), $traitRule->getDelayedErrors()), + ]; } protected function shouldPolluteScopeWithLoopInitialAssignments(): bool { - return false; + return true; } protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool @@ -141,6 +259,16 @@ protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool return true; } + protected function shouldFailOnPhpErrors(): bool + { + return true; + } + + protected function shouldNarrowMethodScopeFromConstructor(): bool + { + return false; + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/src/Testing/TestCase-staticReflection.neon b/src/Testing/TestCase-staticReflection.neon deleted file mode 100644 index 6da322a517..0000000000 --- a/src/Testing/TestCase-staticReflection.neon +++ /dev/null @@ -1,24 +0,0 @@ -services: - - - class: PHPStan\Testing\TestCaseSourceLocatorFactory - arguments: - phpParser: @phpParserDecorator - php8Parser: @php8PhpParser - - currentPhpVersionLexer: - class: PhpParser\Lexer - factory: PhpParser\Lexer\Emulative - arguments: - options: - usedAttributes: [comments, startLine, endLine, startTokenPos, endTokenPos] - - betterReflectionSourceLocator: - class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator - factory: @PHPStan\Testing\TestCaseSourceLocatorFactory::create() - autowired: false - - reflectionProvider: - factory: @betterReflectionProvider - arguments!: [] - autowired: - - PHPStan\Reflection\ReflectionProvider diff --git a/src/Testing/TestCase.neon b/src/Testing/TestCase.neon index de27420e8b..c5ef275d1f 100644 --- a/src/Testing/TestCase.neon +++ b/src/Testing/TestCase.neon @@ -1,8 +1,33 @@ parameters: inferPrivatePropertyTypeFromConstructor: true + services: + - + class: PHPStan\Testing\TestCaseSourceLocatorFactory + arguments: + phpParser: @phpParserDecorator + php8Parser: @php8PhpParser + fileExtensions: %fileExtensions% + excludePaths: %excludePaths% + cacheStorage: class: PHPStan\Cache\MemoryCacheStorage arguments!: [] + currentPhpVersionSimpleParser!: factory: @currentPhpVersionRichParser + + currentPhpVersionLexer: + class: PhpParser\Lexer + factory: @PHPStan\Parser\LexerFactory::createEmulative() + + betterReflectionSourceLocator: + class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator + factory: @PHPStan\Testing\TestCaseSourceLocatorFactory::create() + autowired: false + + reflectionProvider: + factory: @betterReflectionProvider + arguments!: [] + autowired: + - PHPStan\Reflection\ReflectionProvider diff --git a/src/Testing/TestCaseSourceLocatorFactory.php b/src/Testing/TestCaseSourceLocatorFactory.php index 16b94cae23..724380955e 100644 --- a/src/Testing/TestCaseSourceLocatorFactory.php +++ b/src/Testing/TestCaseSourceLocatorFactory.php @@ -12,23 +12,37 @@ use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker; +use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; use PHPStan\Reflection\BetterReflection\SourceLocator\PhpVersionBlacklistSourceLocator; use ReflectionClass; use function dirname; use function is_file; +use function serialize; +use function sha1; -class TestCaseSourceLocatorFactory +final class TestCaseSourceLocatorFactory { + /** @var array> */ + private static array $composerSourceLocatorsCache = []; + + /** + * @param string[] $fileExtensions + * @param array{analyse?: array, analyseAndScan?: array}|null $excludePaths + */ public function __construct( private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, - private AutoloadSourceLocator $autoloadSourceLocator, private Parser $phpParser, private Parser $php8Parser, + private FileNodesFetcher $fileNodesFetcher, private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, private ReflectionSourceStubber $reflectionSourceStubber, + private PhpVersion $phpVersion, + private array $fileExtensions, + private ?array $excludePaths, ) { } @@ -37,8 +51,13 @@ public function create(): SourceLocator { $classLoaders = ClassLoader::getRegisteredLoaders(); $classLoaderReflection = new ReflectionClass(ClassLoader::class); - $locators = []; - if ($classLoaderReflection->hasProperty('vendorDir')) { + $cacheKey = sha1(serialize([ + $this->phpVersion->getVersionId(), + $this->fileExtensions, + $this->excludePaths, + ])); + if ($classLoaderReflection->hasProperty('vendorDir') && ! isset(self::$composerSourceLocatorsCache[$cacheKey])) { + $composerLocators = []; $vendorDirProperty = $classLoaderReflection->getProperty('vendorDir'); $vendorDirProperty->setAccessible(true); foreach ($classLoaders as $classLoader) { @@ -51,15 +70,18 @@ public function create(): SourceLocator if ($composerSourceLocator === null) { continue; } - $locators[] = $composerSourceLocator; + $composerLocators[] = $composerSourceLocator; } + + self::$composerSourceLocatorsCache[$cacheKey] = $composerLocators; } + $locators = self::$composerSourceLocatorsCache[$cacheKey] ?? []; $astLocator = new Locator($this->phpParser); $astPhp8Locator = new Locator($this->php8Parser); $locators[] = new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber); - $locators[] = $this->autoloadSourceLocator; + $locators[] = new AutoloadSourceLocator($this->fileNodesFetcher, true); $locators[] = new PhpVersionBlacklistSourceLocator(new PhpInternalSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); $locators[] = new PhpVersionBlacklistSourceLocator(new EvaledCodeSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 7b4d63da20..0d7e0306d1 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -5,24 +5,47 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; -use PHPStan\Analyser\DirectScopeFactory; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\ScopeContext; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; +use PHPStan\File\SystemAgnosticSimpleRelativePathHelper; +use PHPStan\Node\InClassNode; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use ReflectionProperty; +use Symfony\Component\Finder\Finder; use function array_map; use function array_merge; use function count; +use function fclose; +use function fgets; +use function fopen; +use function in_array; +use function is_dir; use function is_string; +use function preg_match; use function sprintf; +use function stripos; +use function strpos; +use function strtolower; +use function version_compare; +use const PHP_VERSION; /** @api */ abstract class TypeInferenceTestCase extends PHPStanTestCase @@ -32,45 +55,52 @@ abstract class TypeInferenceTestCase extends PHPStanTestCase * @param callable(Node , Scope ): void $callback * @param string[] $dynamicConstantNames */ - public function processFile( + public static function processFile( string $file, callable $callback, array $dynamicConstantNames = [], ): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeSpecifier = self::getContainer()->getService('typeSpecifier'); $fileHelper = self::getContainer()->getByType(FileHelper::class); $resolver = new NodeScopeResolver( $reflectionProvider, + self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), - $this->getClassReflectionExtensionRegistryProvider(), - $this->getParser(), + self::getClassReflectionExtensionRegistryProvider(), + self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class), + self::getParser(), self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(SignatureMapProvider::class), + self::getContainer()->getByType(DeprecationProvider::class), + self::getContainer()->getByType(AttributeReflectionFactory::class), self::getContainer()->getByType(PhpDocInheritanceResolver::class), self::getContainer()->getByType(FileHelper::class), $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), - true, - true, - $this->getEarlyTerminatingMethodCalls(), - $this->getEarlyTerminatingFunctionCalls(), + self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), + self::createScopeFactory($reflectionProvider, $typeSpecifier), + self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'), + self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach'), + self::getContainer()->getParameter('polluteScopeWithBlock'), + static::getEarlyTerminatingMethodCalls(), + static::getEarlyTerminatingFunctionCalls(), + self::getContainer()->getParameter('universalObjectCratesClasses'), + self::getContainer()->getParameter('exceptions')['implicitThrows'], + self::getContainer()->getParameter('treatPhpDocTypesAsCertain'), true, ); - $resolver->setAnalysedFiles(array_map(static fn (string $file): string => $fileHelper->normalizePath($file), array_merge([$file], $this->getAdditionalAnalysedFiles()))); + $resolver->setAnalysedFiles(array_map(static fn (string $file): string => $fileHelper->normalizePath($file), array_merge([$file], static::getAdditionalAnalysedFiles()))); - $scopeFactory = $this->createScopeFactory($reflectionProvider, $typeSpecifier); - if (count($dynamicConstantNames) > 0) { - $reflectionProperty = new ReflectionProperty(DirectScopeFactory::class, 'dynamicConstantNames'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($scopeFactory, $dynamicConstantNames); - } + $scopeFactory = self::createScopeFactory($reflectionProvider, $typeSpecifier, $dynamicConstantNames); $scope = $scopeFactory->create(ScopeContext::create($file)); $resolver->processNodes( - $this->getParser()->parseFile($file), + self::getParser()->parseFile($file), $scope, $callback, ); @@ -87,22 +117,56 @@ public function assertFileAsserts( ): void { if ($assertType === 'type') { - $expectedType = $args[0]; - $expected = $expectedType->getValue(); - $actualType = $args[1]; - $actual = $actualType->describe(VerbosityLevel::precise()); + if ($args[0] instanceof Type) { + // backward compatibility + $expectedType = $args[0]; + $this->assertInstanceOf(ConstantScalarType::class, $expectedType); + $expected = $expectedType->getValue(); + $actualType = $args[1]; + $actual = $actualType->describe(VerbosityLevel::precise()); + } else { + $expected = $args[0]; + $actual = $args[1]; + } + + $failureMessage = sprintf('Expected type %s, got type %s in %s on line %d.', $expected, $actual, $file, $args[2]); + + $delayedErrors = $args[3] ?? []; + if (count($delayedErrors) > 0) { + $failureMessage .= sprintf( + "\n\nThis failure might be reported because of the following misconfiguration %s:\n\n", + count($delayedErrors) === 1 ? 'issue' : 'issues', + ); + foreach ($delayedErrors as $delayedError) { + $failureMessage .= sprintf("* %s\n", $delayedError); + } + } + $this->assertSame( $expected, $actual, - sprintf('Expected type %s, got type %s in %s on line %d.', $expected, $actual, $file, $args[2]), + $failureMessage, ); } elseif ($assertType === 'variableCertainty') { $expectedCertainty = $args[0]; $actualCertainty = $args[1]; $variableName = $args[2]; + + $failureMessage = sprintf('Expected %s, actual certainty of %s is %s in %s on line %d.', $expectedCertainty->describe(), $variableName, $actualCertainty->describe(), $file, $args[3]); + $delayedErrors = $args[4] ?? []; + if (count($delayedErrors) > 0) { + $failureMessage .= sprintf( + "\n\nThis failure might be reported because of the following misconfiguration %s:\n\n", + count($delayedErrors) === 1 ? 'issue' : 'issues', + ); + foreach ($delayedErrors as $delayedError) { + $failureMessage .= sprintf("* %s\n", $delayedError); + } + } + $this->assertTrue( $expectedCertainty->equals($actualCertainty), - sprintf('Expected %s, actual certainty of variable $%s is %s', $expectedCertainty->describe(), $variableName, $actualCertainty->describe()), + $failureMessage, ); } } @@ -111,10 +175,35 @@ public function assertFileAsserts( * @api * @return array */ - public function gatherAssertTypes(string $file): array + public static function gatherAssertTypes(string $file): array { + $fileHelper = self::getContainer()->getByType(FileHelper::class); + + $relativePathHelper = new SystemAgnosticSimpleRelativePathHelper($fileHelper); + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + $file = $fileHelper->normalizePath($file); + $asserts = []; - $this->processFile($file, function (Node $node, Scope $scope) use (&$asserts, $file): void { + $delayedErrors = []; + self::processFile($file, static function (Node $node, Scope $scope) use (&$asserts, &$delayedErrors, $file, $relativePathHelper, $reflectionProvider): void { + if ($node instanceof InClassNode) { + if (!$reflectionProvider->hasClass($node->getClassReflection()->getName())) { + $delayedErrors[] = sprintf( + '%s %s in %s not found in ReflectionProvider. Configure "autoload-dev" section in composer.json to include your tests directory.', + $node->getClassReflection()->getClassTypeDescription(), + $node->getClassReflection()->getName(), + $file, + ); + } + } elseif ($node instanceof Node\Stmt\Trait_) { + if ($node->namespacedName === null) { + throw new ShouldNotHappenException(); + } + if (!$reflectionProvider->hasClass($node->namespacedName->toString())) { + $delayedErrors[] = sprintf('Trait %s not found in ReflectionProvider. Configure "autoload-dev" section in composer.json to include your tests directory.', $node->namespacedName->toString()); + } + } if (!$node instanceof Node\Expr\FuncCall) { return; } @@ -125,76 +214,212 @@ public function gatherAssertTypes(string $file): array } $functionName = $nameNode->toString(); - if ($functionName === 'PHPStan\\Testing\\assertType') { + if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertvariablecertainty'], true)) { + self::fail(sprintf( + 'Missing use statement for %s() in %s on line %d.', + $functionName, + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); + } elseif ($functionName === 'PHPStan\\Testing\\assertType') { $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf( + 'Expected type must be a literal string, %s given in %s on line %d.', + $expectedType->describe(VerbosityLevel::precise()), + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); + } $actualType = $scope->getType($node->getArgs()[1]->value); - $assert = ['type', $file, $expectedType, $actualType, $node->getLine()]; + $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertNativeType') { - $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); - $expectedType = $nativeScope->getNativeType($node->getArgs()[0]->value); - $actualType = $nativeScope->getNativeType($node->getArgs()[1]->value); - $assert = ['type', $file, $expectedType, $actualType, $node->getLine()]; + $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf( + 'Expected type must be a literal string, %s given in %s on line %d.', + $expectedType->describe(VerbosityLevel::precise()), + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); + } + + $actualType = $scope->getNativeType($node->getArgs()[1]->value); + $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertVariableCertainty') { $certainty = $node->getArgs()[0]->value; if (!$certainty instanceof StaticCall) { - $this->fail(sprintf('First argument of %s() must be TrinaryLogic call', $functionName)); + self::fail(sprintf('First argument of %s() must be TrinaryLogic call', $functionName)); } if (!$certainty->class instanceof Node\Name) { - $this->fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); } if ($certainty->class->toString() !== 'PHPStan\\TrinaryLogic') { - $this->fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); } if (!$certainty->name instanceof Node\Identifier) { - $this->fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); } - // @phpstan-ignore-next-line + // @phpstan-ignore staticMethod.dynamicName $expectedertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); $variable = $node->getArgs()[1]->value; - if (!$variable instanceof Node\Expr\Variable) { - $this->fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); - } - if (!is_string($variable->name)) { - $this->fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); + if ($variable instanceof Node\Expr\Variable && is_string($variable->name)) { + $actualCertaintyValue = $scope->hasVariableType($variable->name); + $variableDescription = sprintf('variable $%s', $variable->name); + } elseif ($variable instanceof Node\Expr\ArrayDimFetch && $variable->dim !== null) { + $offset = $scope->getType($variable->dim); + $actualCertaintyValue = $scope->getType($variable->var)->hasOffsetValueType($offset); + $variableDescription = sprintf('offset %s', $offset->describe(VerbosityLevel::precise())); + } else { + self::fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); } - $actualCertaintyValue = $scope->hasVariableType($variable->name); - $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variable->name]; + $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variableDescription, $node->getStartLine()]; } else { - return; + $correctFunction = null; + + $assertFunctions = [ + 'assertType' => 'PHPStan\\Testing\\assertType', + 'assertNativeType' => 'PHPStan\\Testing\\assertNativeType', + 'assertVariableCertainty' => 'PHPStan\\Testing\\assertVariableCertainty', + ]; + foreach ($assertFunctions as $assertFn => $fqFunctionName) { + if (stripos($functionName, $assertFn) === false) { + continue; + } + + $correctFunction = $fqFunctionName; + } + + if ($correctFunction === null) { + return; + } + + self::fail(sprintf( + 'Function %s imported with wrong namespace %s called in %s on line %d.', + $correctFunction, + $functionName, + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); } if (count($node->getArgs()) !== 2) { - $this->fail(sprintf( - 'ERROR: Wrong %s() call on line %d.', + self::fail(sprintf( + 'ERROR: Wrong %s() call in %s on line %d.', $functionName, - $node->getLine(), + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), )); } - $asserts[$file . ':' . $node->getLine()] = $assert; + $asserts[$file . ':' . $node->getStartLine()] = $assert; }); + if (count($asserts) === 0) { + self::fail(sprintf('File %s does not contain any asserts', $file)); + } + + if (count($delayedErrors) === 0) { + return $asserts; + } + + foreach ($asserts as $i => $assert) { + $assert[] = $delayedErrors; + $asserts[$i] = $assert; + } + + return $asserts; + } + + /** + * @api + * @return array + */ + public static function gatherAssertTypesFromDirectory(string $directory): array + { + $asserts = []; + foreach (self::findTestDataFilesFromDirectory($directory) as $path) { + foreach (self::gatherAssertTypes($path) as $key => $assert) { + $asserts[$key] = $assert; + } + } + return $asserts; } + /** + * @return list + */ + public static function findTestDataFilesFromDirectory(string $directory): array + { + if (!is_dir($directory)) { + self::fail(sprintf('Directory %s does not exist.', $directory)); + } + + $finder = new Finder(); + $finder->followLinks(); + $files = []; + foreach ($finder->files()->name('*.php')->in($directory) as $fileInfo) { + $path = $fileInfo->getPathname(); + if (self::isFileLintSkipped($path)) { + continue; + } + $files[] = $path; + } + + return $files; + } + + /** + * From https://github.com/php-parallel-lint/PHP-Parallel-Lint/blob/0c2706086ac36dce31967cb36062ff8915fe03f7/bin/skip-linting.php + * + * Copyright (c) 2012, Jakub Onderka + */ + private static function isFileLintSkipped(string $file): bool + { + $f = @fopen($file, 'r'); + if ($f !== false) { + $firstLine = fgets($f); + if ($firstLine === false) { + return false; + } + + // ignore shebang line + if (strpos($firstLine, '#!') === 0) { + $firstLine = fgets($f); + if ($firstLine === false) { + return false; + } + } + + @fclose($f); + + if (preg_match('~value; - return self::create(min($operandValues)); + $min = $this->value; + foreach ($operands as $operand) { + if ($operand->value >= $min) { + continue; + } + + $min = $operand->value; + } + return self::create($min); + } + + /** + * @template T + * @param T[] $objects + * @param callable(T): self $callback + */ + public function lazyAnd( + array $objects, + callable $callback, + ): self + { + if ($this->no()) { + return $this; + } + + $results = []; + foreach ($objects as $object) { + $result = $callback($object); + if ($result->no()) { + return $result; + } + + $results[] = $result; + } + + return $this->and(...$results); } public function or(self ...$operands): self @@ -90,6 +124,33 @@ public function or(self ...$operands): self return self::create(max($operandValues)); } + /** + * @template T + * @param T[] $objects + * @param callable(T): self $callback + */ + public function lazyOr( + array $objects, + callable $callback, + ): self + { + if ($this->yes()) { + return $this; + } + + $results = []; + foreach ($objects as $object) { + $result = $callback($object); + if ($result->yes()) { + return $result; + } + + $results[] = $result; + } + + return $this->or(...$results); + } + public static function extremeIdentity(self ...$operands): self { if ($operands === []) { @@ -101,13 +162,67 @@ public static function extremeIdentity(self ...$operands): self return self::create($min === $max ? $min : self::MAYBE); } + /** + * @template T + * @param T[] $objects + * @param callable(T): self $callback + */ + public static function lazyExtremeIdentity( + array $objects, + callable $callback, + ): self + { + if ($objects === []) { + throw new ShouldNotHappenException(); + } + + $lastResult = null; + foreach ($objects as $object) { + $result = $callback($object); + if ($lastResult === null) { + $lastResult = $result; + continue; + } + if ($lastResult->equals($result)) { + continue; + } + + return self::createMaybe(); + } + + return $lastResult; + } + 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)); + return self::create(max($operandValues) > 0 ? 1 : min($operandValues)); + } + + /** + * @template T + * @param T[] $objects + * @param callable(T): self $callback + */ + public static function lazyMaxMin( + array $objects, + callable $callback, + ): self + { + $results = []; + foreach ($objects as $object) { + $result = $callback($object); + if ($result->yes()) { + return $result; + } + + $results[] = $result; + } + + return self::maxMin(...$results); } public function negate(): self @@ -142,12 +257,4 @@ public function describe(): string return $labels[$this->value]; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return self::create($properties['value']); - } - } diff --git a/src/Type/AcceptsResult.php b/src/Type/AcceptsResult.php new file mode 100644 index 0000000000..2358285f16 --- /dev/null +++ b/src/Type/AcceptsResult.php @@ -0,0 +1,130 @@ + $reasons + */ + public function __construct( + public readonly TrinaryLogic $result, + public readonly array $reasons, + ) + { + } + + public function yes(): bool + { + return $this->result->yes(); + } + + public function maybe(): bool + { + return $this->result->maybe(); + } + + public function no(): bool + { + return $this->result->no(); + } + + public static function createYes(): self + { + return new self(TrinaryLogic::createYes(), []); + } + + /** + * @param list $reasons + */ + public static function createNo(array $reasons = []): self + { + return new self(TrinaryLogic::createNo(), $reasons); + } + + public static function createMaybe(): self + { + return new self(TrinaryLogic::createMaybe(), []); + } + + public static function createFromBoolean(bool $value): self + { + return new self(TrinaryLogic::createFromBoolean($value), []); + } + + public function and(self $other): self + { + return new self( + $this->result->and($other->result), + array_values(array_unique(array_merge($this->reasons, $other->reasons))), + ); + } + + public function or(self $other): self + { + return new self( + $this->result->or($other->result), + array_values(array_unique(array_merge($this->reasons, $other->reasons))), + ); + } + + /** + * @param callable(string): string $cb + */ + public function decorateReasons(callable $cb): self + { + $reasons = []; + foreach ($this->reasons as $reason) { + $reasons[] = $cb($reason); + } + + return new self($this->result, $reasons); + } + + public static function extremeIdentity(self ...$operands): self + { + if ($operands === []) { + throw new ShouldNotHappenException(); + } + + $result = TrinaryLogic::extremeIdentity(...array_map(static fn (self $result) => $result->result, $operands)); + $reasons = []; + foreach ($operands as $operand) { + foreach ($operand->reasons as $reason) { + $reasons[] = $reason; + } + } + + return new self($result, array_values(array_unique($reasons))); + } + + public static function maxMin(self ...$operands): self + { + if ($operands === []) { + throw new ShouldNotHappenException(); + } + + $result = TrinaryLogic::maxMin(...array_map(static fn (self $result) => $result->result, $operands)); + $reasons = []; + foreach ($operands as $operand) { + foreach ($operand->reasons as $reason) { + $reasons[] = $reason; + } + } + + return new self($result, array_values(array_unique($reasons))); + } + +} diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php new file mode 100644 index 0000000000..5f60fe8eb7 --- /dev/null +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -0,0 +1,502 @@ +isAcceptedBy($this, $strictTypes); + } + + $isArray = $type->isArray(); + $isList = $type->isList(); + + return new AcceptsResult($isArray->and($isList), []); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return new IsSuperTypeOfResult($type->isArray()->and($type->isList()), []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return (new IsSuperTypeOfResult($otherType->isArray()->and($otherType->isList()), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'list'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $this->getIterableKeyType()->isSuperTypeOf($offsetType)->result->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + return new MixedType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + if ($offsetType === null || (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { + return $this; + } + + return new ErrorType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { + return $this; + } + + return new ErrorType(); + } + + public function unsetOffset(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return $this; + } + + return new ErrorType(); + } + + public function getKeysArray(): Type + { + return $this; + } + + public function getValuesArray(): Type + { + return $this; + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function fillKeysArray(Type $valueType): Type + { + return new MixedType(); + } + + public function flipArray(): Type + { + return new MixedType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + if ($otherArraysType->isList()->yes()) { + return $this; + } + + return new MixedType(); + } + + public function popArray(): Type + { + return $this; + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + if ($preserveKeys->no()) { + return $this; + } + + return new MixedType(); + } + + public function searchArray(Type $needleType): Type + { + return new MixedType(); + } + + public function shiftArray(): Type + { + return $this; + } + + public function shuffleArray(): Type + { + return $this; + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ($preserveKeys->no()) { + return $this; + } + + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { + return $this; + } + + return new MixedType(); + } + + public function isIterable(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + + public function getIterableKeyType(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + + public function getFirstIterableKeyType(): Type + { + return new ConstantIntegerType(0); + } + + public function getLastIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getIterableValueType(): Type + { + return new MixedType(); + } + + public function getFirstIterableValueType(): Type + { + return new MixedType(); + } + + public function getLastIterableValueType(): Type + { + return new MixedType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isList(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ); + } + + public function toFloat(): Type + { + return TypeCombinator::union( + new ConstantFloatType(0.0), + new ConstantFloatType(1.0), + ); + } + + public function toString(): Type + { + return new ErrorType(); + } + + public function toArray(): Type + { + return $this; + } + + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('list'); + } + +} diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 1c8a9bc81d..8bcc663327 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -2,7 +2,12 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; @@ -12,15 +17,19 @@ use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -28,6 +37,7 @@ class AccessoryLiteralStringType implements CompoundType, AccessoryType { use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; use UndecidedComparisonCompoundTypeTrait; @@ -44,44 +54,59 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof MixedType) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isLiteralString(); + return new AcceptsResult($type->isLiteralString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return $type->isLiteralString(); + return new IsSuperTypeOfResult($type->isLiteralString(), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isLiteralString() - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isLiteralString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -99,9 +124,14 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -114,6 +144,21 @@ public function getOffsetValueType(Type $offsetType): Type } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + if ($valueType->isLiteralString()->yes()) { + return $this; + } + + return new StringType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { return $this; } @@ -123,12 +168,12 @@ public function unsetOffset(Type $offsetType): Type return new ErrorType(); } - public function isArray(): TrinaryLogic + public function toNumber(): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); } - public function toNumber(): Type + public function toAbsoluteNumber(): Type { return new ErrorType(); } @@ -158,10 +203,76 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -177,24 +288,92 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new StringType(); } - public static function __set_state(array $properties): Type + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('literal-string'); } } diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php new file mode 100644 index 0000000000..1e3b55b0ee --- /dev/null +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -0,0 +1,384 @@ +isAcceptedBy($this, $strictTypes); + } + + return new AcceptsResult($type->isLowercaseString(), []); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + return new IsSuperTypeOfResult($type->isLowercaseString(), []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return (new IsSuperTypeOfResult($otherType->isLowercaseString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'lowercase-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return new ErrorType(); + } + + return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + if ($valueType->isLowercaseString()->yes()) { + return $this; + } + + return new StringType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): 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], + [], + TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ( + $type->isString()->yes() + && $type->isLowercaseString()->no() + && ($type->isNumericString()->no() || $this->isNumericString()->no()) + ) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('lowercase-string'); + } + +} diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 7e17a4f524..f9fce63d94 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -2,24 +2,35 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; -use PHPStan\Type\Traits\NonRemoveableTypeTrait; -use PHPStan\Type\Traits\TruthyBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -27,12 +38,12 @@ class AccessoryNonEmptyStringType implements CompoundType, AccessoryType { use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; - use TruthyBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonGenericTypeTrait; - use NonRemoveableTypeTrait; + use UndecidedBooleanTypeTrait; /** @api */ public function __construct() @@ -44,41 +55,60 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isNonEmptyString(); + return new AcceptsResult($type->isNonEmptyString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); + } + + if ($type->isNonFalsyString()->yes()) { + return IsSuperTypeOfResult::createYes(); } - return $type->isNonEmptyString(); + return new IsSuperTypeOfResult($type->isNonEmptyString(), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isNonEmptyString() - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isNonEmptyString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -96,9 +126,14 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -115,6 +150,17 @@ public function getOffsetValueType(Type $offsetType): Type } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + return $this; + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { return $this; } @@ -124,12 +170,12 @@ public function unsetOffset(Type $offsetType): Type return new ErrorType(); } - public function isArray(): TrinaryLogic + public function toNumber(): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); } - public function toNumber(): Type + public function toAbsoluteNumber(): Type { return new ErrorType(); } @@ -154,10 +200,76 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -173,24 +285,104 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isNull()->yes()) { + return new ConstantBooleanType(false); + } + + if ($type->isString()->yes() && $type->isNonEmptyString()->no()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new StringType(); } - public static function __set_state(array $properties): Type + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '0') { + return TypeCombinator::intersect($this, new AccessoryNonFalsyStringType()); + } + + return null; + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('non-empty-string'); } } diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php new file mode 100644 index 0000000000..6600512da1 --- /dev/null +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -0,0 +1,378 @@ +isAcceptedBy($this, $strictTypes); + } + + return new AcceptsResult($type->isNonFalsyString(), []); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + return new IsSuperTypeOfResult($type->isNonFalsyString(), []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + if ($otherType instanceof AccessoryNonEmptyStringType) { + return IsSuperTypeOfResult::createYes(); + } + + return (new IsSuperTypeOfResult($otherType->isNonFalsyString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'non-falsy-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $offsetType->isInteger()->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 + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + if ($valueType->isNonFalsyString()->yes()) { + return $this; + } + + return new StringType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return TypeCombinator::remove(new IntegerType(), new ConstantIntegerType(0)); + } + + 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], + [], + TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + $falseyTypes = StaticTypeFactory::falsey(); + if ($falseyTypes->isSuperTypeOf($type)->yes()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('non-falsy-string'); + } + +} diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 28438a2a37..72f81cabdb 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -2,37 +2,47 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\StringType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; 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\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; class AccessoryNumericStringType implements CompoundType, AccessoryType { + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; use UndecidedBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonGenericTypeTrait; - use NonRemoveableTypeTrait; /** @api */ public function __construct() @@ -44,41 +54,64 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isNumericString(); + return new AcceptsResult($type->isNumericString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return $type->isNumericString(); + return new IsSuperTypeOfResult($type->isNumericString(), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isNumericString() - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isNumericString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + if ($acceptingType->isNonFalsyString()->yes()) { + return AcceptsResult::createMaybe(); + } + + if ($acceptingType->isNonEmptyString()->yes()) { + return AcceptsResult::createYes(); + } + + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -96,9 +129,14 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -112,17 +150,23 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + return $this; } - public function unsetOffset(Type $offsetType): Type + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return new ErrorType(); + return $this; } - public function isArray(): TrinaryLogic + public function unsetOffset(Type $offsetType): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); } public function toNumber(): Type @@ -133,6 +177,11 @@ public function toNumber(): Type ]); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new IntegerType(); @@ -153,10 +202,76 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return new IntegerType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -172,24 +287,104 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isNull()->yes()) { + return new ConstantBooleanType(false); + } + + if ($type->isString()->yes() && $type->isNumericString()->no()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new StringType(); } - public static function __set_state(array $properties): Type + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '0') { + return TypeCombinator::intersect($this, new AccessoryNonFalsyStringType()); + } + + return null; + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('numeric-string'); } } diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php new file mode 100644 index 0000000000..3fee19deb3 --- /dev/null +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -0,0 +1,384 @@ +isAcceptedBy($this, $strictTypes); + } + + return new AcceptsResult($type->isUppercaseString(), []); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + return new IsSuperTypeOfResult($type->isUppercaseString(), []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return (new IsSuperTypeOfResult($otherType->isUppercaseString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'uppercase-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return new ErrorType(); + } + + return new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + if ($valueType->isUppercaseString()->yes()) { + return $this; + } + + return new StringType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): 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], + [], + TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ( + $type->isString()->yes() + && $type->isUppercaseString()->no() + && ($type->isNumericString()->no() || $this->isNumericString()->no()) + ) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('uppercase-string'); + } + +} diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index e63285cdeb..94364bb21f 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -2,15 +2,21 @@ namespace PHPStan\Type\Accessory; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\Dummy\DummyMethodReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\StringType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; @@ -41,39 +47,57 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + private function getCanonicalMethodName(): string { return strtolower($this->methodName); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { - return TrinaryLogic::createFromBoolean($this->equals($type)); + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + return AcceptsResult::createFromBoolean($this->equals($type)); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - return $type->hasMethod($this->methodName); + return new IsSuperTypeOfResult($type->hasMethod($this->methodName), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } + if ($this->isCallable()->yes() && $otherType->isCallable()->yes()) { + return IsSuperTypeOfResult::createYes(); + } + if ($otherType instanceof self) { - $limit = TrinaryLogic::createYes(); + $limit = IsSuperTypeOfResult::createYes(); } else { - $limit = TrinaryLogic::createMaybe(); + $limit = IsSuperTypeOfResult::createMaybe(); } - return $limit->and($otherType->hasMethod($this->methodName)); + return $limit->and(new IsSuperTypeOfResult($otherType->hasMethod($this->methodName), [])); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -87,6 +111,11 @@ public function describe(VerbosityLevel $level): string return sprintf('hasMethod(%s)', $this->methodName); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function hasMethod(string $methodName): TrinaryLogic { if ($this->getCanonicalMethodName() === strtolower($methodName)) { @@ -96,7 +125,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -121,6 +150,15 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function toString(): Type + { + if ($this->getCanonicalMethodName() === '__tostring') { + return new StringType(); + } + + return new ErrorType(); + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [ @@ -128,14 +166,34 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) ]; } + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { return $this; } - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['methodName']); + return new IdentifierTypeNode(''); // no PHPDoc representation } } diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index cbe2da116f..455f0de86e 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -2,12 +2,22 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; -use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\Traits\MaybeArrayTypeTrait; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; @@ -17,6 +27,7 @@ use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -24,6 +35,7 @@ class HasOffsetType implements CompoundType, AccessoryType { + use MaybeArrayTypeTrait; use MaybeCallableTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; @@ -33,11 +45,16 @@ class HasOffsetType implements CompoundType, AccessoryType use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; - /** @api */ - public function __construct(private Type $offsetType) + /** + * @api + */ + public function __construct(private ConstantStringType|ConstantIntegerType $offsetType) { } + /** + * @return ConstantStringType|ConstantIntegerType + */ public function getOffsetType(): Type { return $this->offsetType; @@ -48,39 +65,53 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isOffsetAccessible() - ->and($type->hasOffsetValueType($this->offsetType)); + return new AcceptsResult($type->isOffsetAccessible()->and($type->hasOffsetValueType($this->offsetType)), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return $type->isOffsetAccessible() - ->and($type->hasOffsetValueType($this->offsetType)); + return new IsSuperTypeOfResult($type->isOffsetAccessible()->and($type->hasOffsetValueType($this->offsetType)), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isOffsetAccessible() - ->and($otherType->hasOffsetValueType($this->offsetType)) - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + $result = new IsSuperTypeOfResult($otherType->isOffsetAccessible()->and($otherType->hasOffsetValueType($this->offsetType)), []); + + return $result + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -99,9 +130,14 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - if ($offsetType instanceof ConstantScalarType && $offsetType->equals($this->offsetType)) { + if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { return TrinaryLogic::createYes(); } @@ -118,6 +154,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) { @@ -126,16 +167,117 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new NonEmptyArrayType(); + } + + public function fillKeysArray(Type $valueType): Type + { + return new NonEmptyArrayType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + if ($otherArraysType->hasOffsetValueType($this->offsetType)->yes()) { + return $this; + } + + return new MixedType(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + if ($preserveKeys->yes()) { + return $this; + } + + return new NonEmptyArrayType(); + } + + public function shuffleArray(): Type + { + return new NonEmptyArrayType(); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ( + $this->offsetType->isSuperTypeOf($offsetType)->yes() + && ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()) + ) { + return $preserveKeys->yes() + ? TypeCombinator::intersect($this, new NonEmptyArrayType()) + : new NonEmptyArrayType(); + } + + return new MixedType(); + } + public function isIterableAtLeastOnce(): TrinaryLogic { return TrinaryLogic::createYes(); } - public function isArray(): TrinaryLogic + public function isList(): TrinaryLogic { + if ($this->offsetType->isString()->yes()) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createMaybe(); } + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -151,16 +293,76 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getKeysArray(): Type + { + return new NonEmptyArrayType(); + } + + public function getValuesArray(): Type + { + return new NonEmptyArrayType(); + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); @@ -181,14 +383,44 @@ public function toArray(): Type return new MixedType(); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { return $this; } - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['offsetType']); + return new IdentifierTypeNode(''); // no PHPDoc representation } } diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php new file mode 100644 index 0000000000..ec6e822a31 --- /dev/null +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -0,0 +1,486 @@ +offsetType; + } + + public function getValueType(): Type + { + return $this->valueType; + } + + public function getReferencedClasses(): array + { + return []; + } + + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + return new AcceptsResult( + $type->isOffsetAccessible() + ->and($type->hasOffsetValueType($this->offsetType)) + ->and($this->valueType->accepts($type->getOffsetValueType($this->offsetType), $strictTypes)->result), + [], + ); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + $result = new IsSuperTypeOfResult($type->isOffsetAccessible()->and($type->hasOffsetValueType($this->offsetType)), []); + + return $result + ->and($this->valueType->isSuperTypeOf($type->getOffsetValueType($this->offsetType))); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + $result = new IsSuperTypeOfResult($otherType->isOffsetAccessible()->and($otherType->hasOffsetValueType($this->offsetType)), []); + + return $result + ->and($otherType->getOffsetValueType($this->offsetType)->isSuperTypeOf($this->valueType)) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->offsetType->equals($type->offsetType) + && $this->valueType->equals($type->valueType); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('hasOffsetValue(%s, %s)', $this->offsetType->describe($level), $this->valueType->describe($level)); + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createMaybe(); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { + return $this->valueType; + } + + return new MixedType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + if ($offsetType === null) { + return $this; + } + + if (!$offsetType->equals($this->offsetType)) { + return $this; + } + + if (!$offsetType instanceof ConstantIntegerType && !$offsetType instanceof ConstantStringType) { + throw new ShouldNotHappenException(); + } + + return new self($offsetType, $valueType); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new self($this->offsetType, $valueType); + } + + public function unsetOffset(Type $offsetType): Type + { + if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) { + return new ErrorType(); + } + return $this; + } + + public function getKeysArray(): Type + { + return new NonEmptyArrayType(); + } + + public function getValuesArray(): Type + { + return new NonEmptyArrayType(); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new NonEmptyArrayType(); + } + + public function fillKeysArray(Type $valueType): Type + { + return new NonEmptyArrayType(); + } + + public function flipArray(): Type + { + $valueType = $this->valueType->toArrayKey(); + if ($valueType instanceof ConstantIntegerType || $valueType instanceof ConstantStringType) { + return new self($valueType, $this->offsetType); + } + + return new MixedType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + if ($otherArraysType->hasOffsetValueType($this->offsetType)->yes()) { + return $this; + } + + return new MixedType(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + if ($preserveKeys->yes()) { + return $this; + } + + return new NonEmptyArrayType(); + } + + public function searchArray(Type $needleType): Type + { + if ( + $needleType instanceof ConstantScalarType && $this->valueType instanceof ConstantScalarType + && $needleType->getValue() === $this->valueType->getValue() + ) { + return $this->offsetType; + } + + return new MixedType(); + } + + public function shuffleArray(): Type + { + return new NonEmptyArrayType(); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ( + $this->offsetType->isSuperTypeOf($offsetType)->yes() + && ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()) + ) { + return $preserveKeys->yes() + ? TypeCombinator::intersect($this, new NonEmptyArrayType()) + : new NonEmptyArrayType(); + } + + return new MixedType(); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isList(): TrinaryLogic + { + if ($this->offsetType->isString()->yes()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return new ErrorType(); + } + + public function toFloat(): Type + { + return new ErrorType(); + } + + public function toString(): Type + { + return new ErrorType(); + } + + public function toArray(): Type + { + return new MixedType(); + } + + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + + public function getEnumCases(): array + { + return []; + } + + public function traverse(callable $cb): Type + { + $newValueType = $cb($this->valueType); + if ($newValueType === $this->valueType) { + return $this; + } + + return new self($this->offsetType, $newValueType); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $newValueType = $cb($this->valueType, $right->getOffsetValueType($this->offsetType)); + if ($newValueType === $this->valueType) { + return $this; + } + + return new self($this->offsetType, $newValueType); + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode(''); // no PHPDoc representation + } + +} diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 1d65057878..71b6b42759 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -2,11 +2,16 @@ namespace PHPStan\Type\Accessory; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; @@ -31,47 +36,63 @@ public function __construct(private string $propertyName) { } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function getPropertyName(): string { return $this->propertyName; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { - return TrinaryLogic::createFromBoolean($this->equals($type)); + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + return AcceptsResult::createFromBoolean($this->equals($type)); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - return $type->hasProperty($this->propertyName); + return new IsSuperTypeOfResult($type->hasProperty($this->propertyName), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } if ($otherType instanceof self) { - $limit = TrinaryLogic::createYes(); + $limit = IsSuperTypeOfResult::createYes(); } else { - $limit = TrinaryLogic::createMaybe(); + $limit = IsSuperTypeOfResult::createMaybe(); } - return $limit->and($otherType->hasProperty($this->propertyName)); + return $limit->and(new IsSuperTypeOfResult($otherType->hasProperty($this->propertyName), [])); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -85,6 +106,11 @@ public function describe(VerbosityLevel $level): string return sprintf('hasProperty(%s)', $this->propertyName); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function hasProperty(string $propertyName): TrinaryLogic { if ($this->propertyName === $propertyName) { @@ -99,14 +125,34 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return [new TrivialParametersAcceptor()]; } + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { return $this; } - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['propertyName']); + return new IdentifierTypeNode(''); // no PHPDoc representation } } diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 9ed064559c..2fbea580ca 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -2,12 +2,20 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -41,44 +49,69 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getArrays(): array + { + return []; + } + + public function getConstantArrays(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $type->isArray() - ->and($type->isIterableAtLeastOnce()); + $isArray = $type->isArray(); + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + + return new AcceptsResult($isArray->and($isIterableAtLeastOnce), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($this->equals($type)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return $type->isArray() - ->and($type->isIterableAtLeastOnce()); + return new IsSuperTypeOfResult($type->isArray()->and($type->isIterableAtLeastOnce()), []); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isArray() - ->and($otherType->isIterableAtLeastOnce()) - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isArray()->and($otherType->isIterableAtLeastOnce()), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -96,6 +129,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -111,11 +149,80 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); } + public function getKeysArray(): Type + { + return $this; + } + + public function getValuesArray(): Type + { + return $this; + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function fillKeysArray(Type $valueType): Type + { + return $this; + } + + public function flipArray(): Type + { + return $this; + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return new MixedType(); + } + + public function popArray(): Type + { + return new MixedType(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function searchArray(Type $needleType): Type + { + return new MixedType(); + } + + public function shiftArray(): Type + { + return new MixedType(); + } + + public function shuffleArray(): Type + { + return $this; + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() && $lengthType->isNull()->yes()) { + return $this; + } + + return new MixedType(); + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -126,21 +233,111 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createYes(); } + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(1, null); + } + public function getIterableKeyType(): Type { return new MixedType(); } + public function getFirstIterableKeyType(): Type + { + return new MixedType(); + } + + public function getLastIterableKeyType(): Type + { + return new MixedType(); + } + public function getIterableValueType(): Type { return new MixedType(); } + public function getFirstIterableValueType(): Type + { + return new MixedType(); + } + + public function getLastIterableValueType(): Type + { + return new MixedType(); + } + public function isArray(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isList(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -156,16 +353,70 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ConstantIntegerType(1); @@ -183,7 +434,17 @@ public function toString(): Type public function toArray(): Type { - return new MixedType(); + return $this; + } + + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; } public function traverse(callable $cb): Type @@ -191,9 +452,24 @@ public function traverse(callable $cb): Type return $this; } - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('non-empty-array'); } } diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php new file mode 100644 index 0000000000..c43c86a903 --- /dev/null +++ b/src/Type/Accessory/OversizedArrayType.php @@ -0,0 +1,462 @@ +isAcceptedBy($this, $strictTypes); + } + + return new AcceptsResult($type->isArray()->and($type->isIterableAtLeastOnce()), []); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return new IsSuperTypeOfResult($type->isArray()->and($type->isOversizedArray()), []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return (new IsSuperTypeOfResult($otherType->isArray()->and($otherType->isOversizedArray()), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'oversized-array'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getOffsetValueType(Type $offsetType): Type + { + return new MixedType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + return $this; + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function getKeysArray(): Type + { + return $this; + } + + public function getValuesArray(): Type + { + return $this; + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function fillKeysArray(Type $valueType): Type + { + return $this; + } + + public function flipArray(): Type + { + return $this; + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this; + } + + public function popArray(): Type + { + return $this; + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function searchArray(Type $needleType): Type + { + return new MixedType(); + } + + public function shiftArray(): Type + { + return $this; + } + + public function shuffleArray(): Type + { + return $this; + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function isIterable(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + + public function getIterableKeyType(): Type + { + return new MixedType(); + } + + public function getFirstIterableKeyType(): Type + { + return new MixedType(); + } + + public function getLastIterableKeyType(): Type + { + return new MixedType(); + } + + public function getIterableValueType(): Type + { + return new MixedType(); + } + + public function getFirstIterableValueType(): Type + { + return new MixedType(); + } + + public function getLastIterableValueType(): Type + { + return new MixedType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isList(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return new ConstantIntegerType(1); + } + + public function toFloat(): Type + { + return new ConstantFloatType(1.0); + } + + public function toString(): Type + { + return new ErrorType(); + } + + public function toArray(): Type + { + return new MixedType(); + } + + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode(''); // no PHPDoc representation + } + +} diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 8d7d323b20..e6c0097db7 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -2,36 +2,42 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; -use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateMixedType; -use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateStrictMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\ArrayTypeTrait; 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 count; use function sprintf; /** @api */ class ArrayType implements Type { + use ArrayTypeTrait; use MaybeCallableTypeTrait; use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; @@ -46,6 +52,10 @@ public function __construct(Type $keyType, private Type $itemType) if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') { $keyType = new MixedType(); } + if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) { + $keyType = new UnionType([new StringType(), new IntegerType()]); + } + $this->keyType = $keyType; } @@ -59,9 +69,6 @@ public function getItemType(): Type return $this->itemType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return array_merge( @@ -70,19 +77,26 @@ public function getReferencedClasses(): array ); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getConstantArrays(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof ConstantArrayType) { - $result = TrinaryLogic::createYes(); + $result = AcceptsResult::createYes(); $thisKeyType = $this->keyType; $itemType = $this->getItemType(); foreach ($type->getKeyTypes() as $i => $keyType) { $valueType = $type->getValueTypes()[$i]; - $result = $result->and($thisKeyType->accepts($keyType, $strictTypes))->and($itemType->accepts($valueType, $strictTypes)); + $acceptsKey = $thisKeyType->accepts($keyType, $strictTypes); + $acceptsValue = $itemType->accepts($valueType, $strictTypes); + $result = $result->and($acceptsKey)->and($acceptsValue); } return $result; @@ -93,35 +107,34 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic ->and($this->keyType->accepts($type->keyType, $strictTypes)); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - if ($type instanceof self) { + if ($type instanceof self || $type instanceof ConstantArrayType) { return $this->getItemType()->isSuperTypeOf($type->getItemType()) - ->and($this->keyType->isSuperTypeOf($type->keyType)); + ->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType())); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool { return $type instanceof self - && !$type instanceof ConstantArrayType - && $this->getItemType()->equals($type->getItemType()) + && $this->getItemType()->equals($type->getIterableValueType()) && $this->keyType->equals($type->keyType); } public function describe(VerbosityLevel $level): string { - $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; - $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->keyType->isExplicitMixed(); + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->itemType->isExplicitMixed(); $valueHandler = function () use ($level, $isMixedKeyType, $isMixedItemType): string { if ($isMixedKeyType || $this->keyType instanceof NeverType) { @@ -157,24 +170,24 @@ public function generalizeValues(): self return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific())); } - public function getKeysArray(): self + public function getKeysArray(): Type { - return new self(new IntegerType(), $this->keyType); + return TypeCombinator::intersect(new self(new IntegerType(), $this->getIterableKeyType()), new AccessoryArrayListType()); } - public function getValuesArray(): self + public function getValuesArray(): Type { - return new self(new IntegerType(), $this->itemType); + return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType()); } - public function isIterable(): TrinaryLogic + public function isIterableAtLeastOnce(): TrinaryLogic { - return TrinaryLogic::createYes(); + return TrinaryLogic::createMaybe(); } - public function isIterableAtLeastOnce(): TrinaryLogic + public function getArraySize(): Type { - return TrinaryLogic::createMaybe(); + return IntegerRangeType::fromInterval(0, null); } public function getIterableKeyType(): Type @@ -190,45 +203,66 @@ public function getIterableKeyType(): Type return $keyType; } + public function getFirstIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getLastIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + public function getIterableValueType(): Type { return $this->getItemType(); } - public function isArray(): TrinaryLogic + public function getFirstIterableValueType(): Type { - return TrinaryLogic::createYes(); + return $this->getItemType(); } - public function isString(): TrinaryLogic + public function getLastIterableValueType(): Type { - return TrinaryLogic::createNo(); + return $this->getItemType(); } - public function isNumericString(): TrinaryLogic + public function isConstantArray(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isNonEmptyString(): TrinaryLogic + public function isList(): TrinaryLogic { - return TrinaryLogic::createNo(); + if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); } - public function isLiteralString(): TrinaryLogic + public function isConstantValue(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOffsetAccessible(): TrinaryLogic + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return TrinaryLogic::createYes(); + if ($type->isInteger()->yes()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); } public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - $offsetType = self::castToArrayKeyType($offsetType); - if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) { + $offsetType = $offsetType->toArrayKey(); + + if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) + ) { return TrinaryLogic::createNo(); } @@ -237,8 +271,10 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - $offsetType = self::castToArrayKeyType($offsetType); - if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) { + $offsetType = $offsetType->toArrayKey(); + if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) + ) { return new ErrorType(); } @@ -253,112 +289,198 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { if ($offsetType === null) { - $offsetType = new IntegerType(); + $isKeyTypeInteger = $this->keyType->isInteger(); + if ($isKeyTypeInteger->no()) { + $offsetType = new IntegerType(); + } elseif ($isKeyTypeInteger->yes()) { + /** @var list $constantScalars */ + $constantScalars = $this->keyType->getConstantScalarTypes(); + if (count($constantScalars) > 0) { + foreach ($constantScalars as $constantScalar) { + $constantScalars[] = ConstantTypeHelper::getTypeFromValue($constantScalar->getValue() + 1); + } + + $offsetType = TypeCombinator::union(...$constantScalars); + } else { + $offsetType = $this->keyType; + } + } else { + $integerTypes = []; + TypeTraverser::map($this->keyType, static function (Type $type, callable $traverse) use (&$integerTypes): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + $isInteger = $type->isInteger(); + if ($isInteger->yes()) { + $integerTypes[] = $type; + } + + return $type; + }); + if (count($integerTypes) === 0) { + $offsetType = $this->keyType; + } else { + $offsetType = TypeCombinator::union(...$integerTypes); + } + } + } else { + $offsetType = $offsetType->toArrayKey(); + } + + if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { + if ($offsetType->isSuperTypeOf($this->keyType)->yes()) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType($offsetType, $valueType); + return $builder->getArray(); + } + + return TypeCombinator::intersect( + new self( + TypeCombinator::union($this->keyType, $offsetType), + TypeCombinator::union($this->itemType, $valueType), + ), + new HasOffsetValueType($offsetType, $valueType), + new NonEmptyArrayType(), + ); } - return TypeCombinator::intersect(new self( - TypeCombinator::union($this->keyType, self::castToArrayKeyType($offsetType)), - $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType, - ), new NonEmptyArrayType()); + return TypeCombinator::intersect( + new self( + TypeCombinator::union($this->keyType, $offsetType), + $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType, + ), + new NonEmptyArrayType(), + ); } - public function unsetOffset(Type $offsetType): Type + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return $this; + return new self( + $this->keyType, + TypeCombinator::union($this->itemType, $valueType), + ); } - public function isCallable(): TrinaryLogic + public function unsetOffset(Type $offsetType): Type { - return TrinaryLogic::createMaybe()->and((new StringType())->isSuperTypeOf($this->itemType)); + $offsetType = $offsetType->toArrayKey(); + + if ( + ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) + && !$this->keyType->isSuperTypeOf($offsetType)->no() + ) { + $keyType = TypeCombinator::remove($this->keyType, $offsetType); + if ($keyType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + return new self($keyType, $this->itemType); + } + + return $this; } - /** - * @return ParametersAcceptor[] - */ - public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + public function fillKeysArray(Type $valueType): Type { - if ($this->isCallable()->no()) { - throw new ShouldNotHappenException(); + $itemType = $this->getItemType(); + if ($itemType->isInteger()->no()) { + $stringKeyType = $itemType->toString(); + if ($stringKeyType instanceof ErrorType) { + return $stringKeyType; + } + + return new ArrayType($stringKeyType, $valueType); } - return [new TrivialParametersAcceptor()]; + return new ArrayType($itemType, $valueType); } - public function toNumber(): Type + public function flipArray(): Type { - return new ErrorType(); + return new self($this->getIterableValueType()->toArrayKey(), $this->getIterableKeyType()); } - public function toString(): Type + public function intersectKeyArray(Type $otherArraysType): Type { - return new ErrorType(); + $isKeySuperType = $otherArraysType->getIterableKeyType()->isSuperTypeOf($this->getIterableKeyType()); + if ($isKeySuperType->no()) { + return ConstantArrayTypeBuilder::createEmpty()->getArray(); + } + + if ($isKeySuperType->yes()) { + return $this; + } + + return new self($otherArraysType->getIterableKeyType(), $this->getIterableValueType()); } - public function toInteger(): Type + public function popArray(): Type { - return TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantIntegerType(1), - ); + return $this; } - public function toFloat(): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type { - return TypeCombinator::union( - new ConstantFloatType(0.0), - new ConstantFloatType(1.0), - ); + return $this; + } + + public function searchArray(Type $needleType): Type + { + return TypeCombinator::union($this->getIterableKeyType(), new ConstantBooleanType(false)); } - public function toArray(): Type + public function shiftArray(): Type { return $this; } - public function count(): Type + public function shuffleArray(): Type { - return IntegerRangeType::fromInterval(0, null); + return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType()); } - public static function castToArrayKeyType(Type $offsetType): Type + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { - return TypeTraverser::map($offsetType, static function (Type $offsetType, callable $traverse): Type { - if ($offsetType instanceof TemplateType) { - return $offsetType; - } + if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) { + return new ConstantArrayType([], []); + } - if ($offsetType instanceof ConstantScalarType) { - $keyValue = $offsetType->getValue(); - if (is_float($keyValue)) { - $keyValue = (int) $keyValue; - } - /** @var int|string $offsetValue */ - $offsetValue = key([$keyValue => null]); - return is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue); - } + if ($preserveKeys->no() && $this->keyType->isInteger()->yes()) { + return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType()); + } - if ($offsetType instanceof IntegerType) { - return $offsetType; - } + return $this; + } - if ($offsetType instanceof BooleanType) { - return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); - } + public function isCallable(): TrinaryLogic + { + return TrinaryLogic::createMaybe()->and($this->itemType->isString()); + } - if ($offsetType instanceof FloatType || $offsetType->isNumericString()->yes()) { - return new IntegerType(); - } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + { + if ($this->isCallable()->no()) { + throw new ShouldNotHappenException(); + } - if ($offsetType instanceof StringType || $offsetType->isNonEmptyString()->yes()) { - return $offsetType; - } + return [new TrivialParametersAcceptor()]; + } - if ($offsetType instanceof UnionType || $offsetType instanceof IntersectionType) { - return $traverse($offsetType); - } + public function toInteger(): Type + { + return TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ); + } - return new UnionType([new IntegerType(), new StringType()]); - }); + public function toFloat(): Type + { + return TypeCombinator::union( + new ConstantFloatType(0.0), + new ConstantFloatType(1.0), + ); } public function inferTemplateTypes(Type $receivedType): TemplateTypeMap @@ -368,7 +490,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap } if ($receivedType->isArray()->yes()) { - $keyTypeMap = $this->getKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); + $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getIterableValueType()); return $keyTypeMap->union($itemTypeMap); @@ -379,31 +501,61 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { - $keyVariance = $positionVariance; - $itemVariance = $positionVariance; + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); - if (!$positionVariance->contravariant()) { - $keyType = $this->getKeyType(); - if ($keyType instanceof TemplateType) { - $keyVariance = $keyType->getVariance(); + return array_merge( + $this->getIterableKeyType()->getReferencedTemplateTypes($variance), + $this->getItemType()->getReferencedTemplateTypes($variance), + ); + } + + public function traverse(callable $cb): Type + { + $keyType = $cb($this->keyType); + $itemType = $cb($this->itemType); + + if ($keyType !== $this->keyType || $itemType !== $this->itemType) { + if ($keyType instanceof NeverType && $itemType instanceof NeverType) { + return new ConstantArrayType([], []); } - $itemType = $this->getItemType(); - if ($itemType instanceof TemplateType) { - $itemVariance = $itemType->getVariance(); + return new self($keyType, $itemType); + } + + return $this; + } + + public function toPhpDocNode(): TypeNode + { + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->keyType->isExplicitMixed(); + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->itemType->isExplicitMixed(); + + if ($isMixedKeyType) { + if ($isMixedItemType) { + return new IdentifierTypeNode('array'); } + + return new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + $this->itemType->toPhpDocNode(), + ], + ); } - return array_merge( - $this->getKeyType()->getReferencedTemplateTypes($keyVariance), - $this->getItemType()->getReferencedTemplateTypes($itemVariance), + return new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + $this->keyType->toPhpDocNode(), + $this->itemType->toPhpDocNode(), + ], ); } - public function traverse(callable $cb): Type + public function traverseSimultaneously(Type $right, callable $cb): Type { - $keyType = $cb($this->keyType); - $itemType = $cb($this->itemType); + $keyType = $cb($this->keyType, $right->getIterableKeyType()); + $itemType = $cb($this->itemType, $right->getIterableValueType()); if ($keyType !== $this->keyType || $itemType !== $this->itemType) { if ($keyType instanceof NeverType && $itemType instanceof NeverType) { @@ -418,7 +570,7 @@ public function traverse(callable $cb): Type public function tryRemove(Type $typeToRemove): ?Type { - if ($typeToRemove instanceof ConstantArrayType && $typeToRemove->isIterableAtLeastOnce()->no()) { + if ($typeToRemove->isConstantArray()->yes() && $typeToRemove->isIterableAtLeastOnce()->no()) { return TypeCombinator::intersect($this, new NonEmptyArrayType()); } @@ -426,22 +578,12 @@ public function tryRemove(Type $typeToRemove): ?Type return new ConstantArrayType([], []); } - if ($this instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetType) { - return $this->unsetOffset($typeToRemove->getOffsetType()); - } - return null; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function getFiniteTypes(): array { - return new self( - $properties['keyType'], - $properties['itemType'], - ); + return []; } } diff --git a/src/Type/BenevolentUnionType.php b/src/Type/BenevolentUnionType.php index 384488ff21..6a2d28dc1c 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -4,7 +4,6 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; -use function array_map; use function count; /** @api */ @@ -15,9 +14,19 @@ class BenevolentUnionType extends UnionType * @api * @param Type[] $types */ - public function __construct(array $types) + public function __construct(array $types, bool $normalized = false) { - parent::__construct($types); + parent::__construct($types, $normalized); + } + + public function filterTypes(callable $filterCb): Type + { + $result = parent::filterTypes($filterCb); + if (!$result instanceof self && $result instanceof UnionType) { + return TypeUtils::toBenevolentUnion($result); + } + + return $result; } public function describe(VerbosityLevel $level): string @@ -44,19 +53,58 @@ protected function unionTypes(callable $getType): Type return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$resultTypes)); } + protected function pickFromTypes( + callable $getValues, + callable $criteria, + ): array + { + $values = []; + foreach ($this->getTypes() as $type) { + $innerValues = $getValues($type); + if ($innerValues === [] && $criteria($type)) { + return []; + } + + foreach ($innerValues as $innerType) { + $values[] = $innerType; + } + } + + return $values; + } + + public function getOffsetValueType(Type $offsetType): Type + { + $types = []; + foreach ($this->getTypes() as $innerType) { + $valueType = $innerType->getOffsetValueType($offsetType); + if ($valueType instanceof ErrorType) { + continue; + } + + $types[] = $valueType; + } + + if (count($types) === 0) { + return new ErrorType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + } + protected function unionResults(callable $getResult): TrinaryLogic { - return TrinaryLogic::createNo()->or(...array_map($getResult, $this->getTypes())); + return TrinaryLogic::createNo()->lazyOr($this->getTypes(), $getResult); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - $results = []; + $result = AcceptsResult::createNo(); foreach ($this->getTypes() as $innerType) { - $results[] = $acceptingType->accepts($innerType, $strictTypes); + $result = $result->or($acceptingType->accepts($innerType, $strictTypes)); } - return TrinaryLogic::createNo()->or(...$results); + return $result; } public function inferTemplateTypes(Type $receivedType): TemplateTypeMap @@ -101,12 +149,33 @@ public function traverse(callable $cb): Type return $this; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type { - return new self($properties['types']); + $types = []; + $changed = false; + + if (!$right instanceof UnionType) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + } + + return $this; } } diff --git a/src/Type/BitwiseFlagHelper.php b/src/Type/BitwiseFlagHelper.php new file mode 100644 index 0000000000..7d5e4c3db9 --- /dev/null +++ b/src/Type/BitwiseFlagHelper.php @@ -0,0 +1,107 @@ +name) === $constName) { + return TrinaryLogic::createYes(); + } + + $resolveConstantName = $this->reflectionProvider->resolveConstantName($expr->name, $scope); + if ($resolveConstantName !== null) { + if ($resolveConstantName === $constName) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createNo(); + } + } + + if ($expr instanceof BitwiseOr) { + return TrinaryLogic::createFromBoolean($this->bitwiseOrContainsConstant($expr->left, $scope, $constName)->yes() || + $this->bitwiseOrContainsConstant($expr->right, $scope, $constName)->yes()); + } + + $fqcn = new FullyQualified($constName); + if ($this->reflectionProvider->hasConstant($fqcn, $scope)) { + $constant = $this->reflectionProvider->getConstant($fqcn, $scope); + + $valueType = $constant->getValueType(); + + if ($valueType instanceof ConstantIntegerType) { + return $this->exprContainsIntFlag($expr, $scope, $valueType->getValue()); + } + } + + return TrinaryLogic::createNo(); + } + + private function exprContainsIntFlag(Expr $expr, Scope $scope, int $flag): TrinaryLogic + { + $exprType = $scope->getType($expr); + + if ($exprType instanceof UnionType) { + $allTypesContainFlag = true; + $someTypesContainFlag = false; + foreach ($exprType->getTypes() as $type) { + $containsFlag = $this->typeContainsIntFlag($type, $flag); + if (!$containsFlag->yes()) { + $allTypesContainFlag = false; + } + + if (!$containsFlag->yes() && !$containsFlag->maybe()) { + continue; + } + + $someTypesContainFlag = true; + } + + if ($allTypesContainFlag) { + return TrinaryLogic::createYes(); + } + if ($someTypesContainFlag) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createNo(); + } + + return $this->typeContainsIntFlag($exprType, $flag); + } + + private function typeContainsIntFlag(Type $type, int $flag): TrinaryLogic + { + if ($type instanceof ConstantIntegerType) { + if (($type->getValue() & $flag) === $flag) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createNo(); + } + + if ($type->isInteger()->yes() || $type instanceof MixedType) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createNo(); + } + +} diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 5521b2bacb..a703decac4 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -2,11 +2,16 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -21,6 +26,7 @@ class BooleanType implements Type { use JustNullableTypeTrait; + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -35,6 +41,21 @@ public function __construct() { } + public function getConstantStrings(): array + { + return []; + } + + public function getConstantScalarTypes(): array + { + return [new ConstantBooleanType(true), new ConstantBooleanType(false)]; + } + + public function getConstantScalarValues(): array + { + return [true, false]; + } + public function describe(VerbosityLevel $level): string { return 'bool'; @@ -45,6 +66,11 @@ public function toNumber(): Type return $this->toInteger(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return TypeCombinator::union( @@ -74,10 +100,61 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this); + } + + return $this; + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function tryRemove(Type $typeToRemove): ?Type { if ($typeToRemove instanceof ConstantBooleanType) { @@ -87,12 +164,34 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function getFiniteTypes(): array + { + return [ + new ConstantBooleanType(true), + new ConstantBooleanType(false), + ]; + } + + public function exponentiate(Type $exponent): Type + { + return ExponentiateHelper::exponentiate($this, $exponent); + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('bool'); + } + + public function toTrinaryLogic(): TrinaryLogic + { + if ($this->isTrue()->yes()) { + return TrinaryLogic::createYes(); + } + if ($this->isFalse()->yes()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); } } diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index cf05b87486..568c847711 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -2,17 +2,35 @@ namespace PHPStan\Type; +use Closure; use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Printer\Printer; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\Php\DummyParameter; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Traits\MaybeArrayTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; @@ -22,13 +40,13 @@ use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use function array_map; use function array_merge; -use function implode; -use function sprintf; +use function count; /** @api */ -class CallableType implements CompoundType, ParametersAcceptor +class CallableType implements CompoundType, CallableParametersAcceptor { + use MaybeArrayTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; use MaybeOffsetAccessibleTypeTrait; @@ -37,31 +55,55 @@ class CallableType implements CompoundType, ParametersAcceptor use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; - /** @var array */ + /** @var list */ private array $parameters; private Type $returnType; private bool $isCommonCallable; + private TemplateTypeMap $templateTypeMap; + + private TemplateTypeMap $resolvedTemplateTypeMap; + + private TrinaryLogic $isPure; + /** * @api - * @param array $parameters + * @param list|null $parameters + * @param array $templateTags */ public function __construct( ?array $parameters = null, ?Type $returnType = null, private bool $variadic = true, + ?TemplateTypeMap $templateTypeMap = null, + ?TemplateTypeMap $resolvedTemplateTypeMap = null, + private array $templateTags = [], + ?TrinaryLogic $isPure = null, ) { $this->parameters = $parameters ?? []; $this->returnType = $returnType ?? new MixedType(); $this->isCommonCallable = $parameters === null && $returnType === null; + $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->isPure = $isPure ?? TrinaryLogic::createMaybe(); } /** - * @return string[] + * @return array */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + + public function isPure(): TrinaryLogic + { + return $this->isPure; + } + public function getReferencedClasses(): array { $classes = []; @@ -72,24 +114,43 @@ public function getReferencedClasses(): array return array_merge($classes, $this->returnType->getReferencedClasses()); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType && !$type instanceof self) { return $type->isAcceptedBy($this, $strictTypes); } - return $this->isSuperTypeOfInternal($type, true); + return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { + if ($type instanceof CompoundType && !$type instanceof self) { + return $type->isSubTypeOf($this); + } + return $this->isSuperTypeOfInternal($type, false); } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): TrinaryLogic + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult { - $isCallable = $type->isCallable(); - if ($isCallable->no() || $this->isCommonCallable) { + $isCallable = new IsSuperTypeOfResult($type->isCallable(), []); + if ($isCallable->no()) { return $isCallable; } @@ -98,8 +159,27 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Trina $scope = new OutOfClassScope(); } + if ($this->isCommonCallable) { + if ($this->isPure()->yes()) { + $typePure = TrinaryLogic::createYes(); + foreach ($type->getCallableParametersAcceptors($scope) as $variant) { + $typePure = $typePure->and($variant->isPure()); + } + + return $isCallable->and(new IsSuperTypeOfResult($typePure, [])); + } + + return $isCallable; + } + + $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); + $variantsResult = null; foreach ($type->getCallableParametersAcceptors($scope) as $variant) { + $variant = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$variant], false); + if (!$variant instanceof CallableParametersAcceptor) { + return IsSuperTypeOfResult::createNo([]); + } $isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny); if ($variantsResult === null) { $variantsResult = $isSuperType; @@ -115,38 +195,55 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Trina return $isCallable->and($variantsResult); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) { return $otherType->isSuperTypeOf($this); } - return $otherType->isCallable() - ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + return (new IsSuperTypeOfResult($otherType->isCallable(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool { - return $type instanceof self; + if (!$type instanceof self) { + return false; + } + + return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise()); } public function describe(VerbosityLevel $level): string { return $level->handle( static fn (): string => 'callable', - fn (): string => sprintf( - 'callable(%s): %s', - implode(', ', array_map( - static fn (ParameterReflection $param): string => sprintf('%s%s', $param->isVariadic() ? '...' : '', $param->getType()->describe($level)), - $this->getParameters(), - )), - $this->returnType->describe($level), - ), + function (): string { + $printer = new Printer(); + $selfWithoutParameterNames = new self( + array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter( + '', + $p->getType(), + $p->isOptional() && !$p->isVariadic(), + PassedByReference::createNo(), + $p->isVariadic(), + $p->getDefaultValue(), + ), $this->parameters), + $this->returnType, + $this->variadic, + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, + ); + + return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + }, ); } @@ -155,19 +252,59 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [$this]; } + public function getThrowPoints(): array + { + return [ + SimpleThrowPoint::createImplicit(), + ]; + } + + public function getImpurePoints(): array + { + $pure = $this->isPure(); + if ($pure->yes()) { + return []; + } + + return [ + new SimpleImpurePoint( + 'functionCall', + 'call to a callable', + $pure->no(), + ), + ]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -188,18 +325,43 @@ public function toArray(): Type return new ArrayType(new MixedType(), new MixedType()); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return TypeCombinator::union( + $this, + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + new ArrayType(new MixedType(true), new MixedType(true)), + new ObjectType(Closure::class), + ); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->templateTypeMap; } public function getResolvedTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->resolvedTemplateTypeMap; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); } /** - * @return array + * @return list */ public function getParameters(): array { @@ -239,10 +401,12 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $parametersAcceptor): TemplateTypeMap { - $typeMap = TemplateTypeMap::createEmpty(); + $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$parametersAcceptor], false); $args = $parametersAcceptor->getParameters(); $returnType = $parametersAcceptor->getReturnType(); + $typeMap = TemplateTypeMap::createEmpty(); foreach ($this->getParameters() as $i => $param) { $paramType = $param->getType(); if (isset($args[$i])) { @@ -298,12 +462,116 @@ public function traverse(callable $cb): Type $parameters, $cb($this->getReturnType()), $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, ); } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type { - return TrinaryLogic::createMaybe(); + if ($this->isCommonCallable) { + return $this; + } + + if (!$right->isCallable()->yes()) { + return $this; + } + + $rightAcceptors = $right->getCallableParametersAcceptors(new OutOfClassScope()); + if (count($rightAcceptors) !== 1) { + return $this; + } + + $rightParameters = $rightAcceptors[0]->getParameters(); + if (count($this->getParameters()) !== count($rightParameters)) { + return $this; + } + + $parameters = []; + foreach ($this->getParameters() as $i => $leftParam) { + $rightParam = $rightParameters[$i]; + $leftDefaultValue = $leftParam->getDefaultValue(); + $rightDefaultValue = $rightParam->getDefaultValue(); + $defaultValue = $leftDefaultValue; + if ($leftDefaultValue !== null && $rightDefaultValue !== null) { + $defaultValue = $cb($leftDefaultValue, $rightDefaultValue); + } + $parameters[] = new NativeParameterReflection( + $leftParam->getName(), + $leftParam->isOptional(), + $cb($leftParam->getType(), $rightParam->getType()), + $leftParam->passedByReference(), + $leftParam->isVariadic(), + $defaultValue, + ); + } + + return new self( + $parameters, + $cb($this->getReturnType(), $rightAcceptors[0]->getReturnType()), + $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, + ); + } + + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); } public function isString(): TrinaryLogic @@ -321,25 +589,107 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getEnumCases(): array + { + return []; + } + public function isCommonCallable(): bool { return $this->isCommonCallable; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function exponentiate(Type $exponent): Type { - return new self( - (bool) $properties['isCommonCallable'] ? null : $properties['parameters'], - (bool) $properties['isCommonCallable'] ? null : $properties['returnType'], - $properties['variadic'], + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + if ($this->isCommonCallable) { + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable'); + } + + $parameters = []; + foreach ($this->parameters as $parameter) { + $parameters[] = new CallableTypeParameterNode( + $parameter->getType()->toPhpDocNode(), + !$parameter->passedByReference()->no(), + $parameter->isVariadic(), + $parameter->getName() === '' ? '' : '$' . $parameter->getName(), + $parameter->isOptional(), + ); + } + + $templateTags = []; + foreach ($this->templateTags as $templateName => $templateTag) { + $templateTags[] = new TemplateTagValueNode( + $templateName, + $templateTag->getBound()->toPhpDocNode(), + '', + ); + } + + return new CallableTypeNode( + new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'), + $parameters, + $this->returnType->toPhpDocNode(), + $templateTags, ); } diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index 94e903da5e..4e99e94cc9 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -2,58 +2,117 @@ namespace PHPStan\Type; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\TrinaryLogic; +use function array_key_exists; +use function array_merge; +use function count; +use function sprintf; -class CallableTypeHelper +final class CallableTypeHelper { public static function isParametersAcceptorSuperTypeOf( - ParametersAcceptor $ours, - ParametersAcceptor $theirs, + CallableParametersAcceptor $ours, + CallableParametersAcceptor $theirs, bool $treatMixedAsAny, - ): TrinaryLogic + ): IsSuperTypeOfResult { $theirParameters = $theirs->getParameters(); $ourParameters = $ours->getParameters(); - $result = null; + $lastParameter = null; + foreach ($theirParameters as $theirParameter) { + $lastParameter = $theirParameter; + } + $theirParameterCount = count($theirParameters); + $ourParameterCount = count($ourParameters); + if ( + $lastParameter !== null + && $lastParameter->isVariadic() + && $theirParameterCount < $ourParameterCount + ) { + foreach ($ourParameters as $i => $ourParameter) { + if (array_key_exists($i, $theirParameters)) { + continue; + } + $theirParameters[] = $lastParameter; + } + } + + $result = IsSuperTypeOfResult::createYes(); foreach ($theirParameters as $i => $theirParameter) { + $parameterDescription = $theirParameter->getName() === '' ? sprintf('#%d', $i + 1) : sprintf('#%d $%s', $i + 1, $theirParameter->getName()); if (!isset($ourParameters[$i])) { if ($theirParameter->isOptional()) { continue; } - return TrinaryLogic::createNo(); + $accepts = new IsSuperTypeOfResult(TrinaryLogic::createNo(), [ + sprintf( + 'Parameter %s of passed callable is required but accepting callable does not have that parameter. It will be called without it.', + $parameterDescription, + ), + ]); + $result = $result->and($accepts); + continue; } $ourParameter = $ourParameters[$i]; $ourParameterType = $ourParameter->getType(); + + if ($ourParameter->isOptional() && !$theirParameter->isOptional()) { + $accepts = new IsSuperTypeOfResult(TrinaryLogic::createNo(), [ + sprintf( + 'Parameter %s of passed callable is required but the parameter of accepting callable is optional. It might be called without it.', + $parameterDescription, + ), + ]); + $result = $result->and($accepts); + } + if ($treatMixedAsAny) { $isSuperType = $theirParameter->getType()->accepts($ourParameterType, true); + $isSuperType = new IsSuperTypeOfResult($isSuperType->result, $isSuperType->reasons); } else { $isSuperType = $theirParameter->getType()->isSuperTypeOf($ourParameterType); } - if ($result === null) { - $result = $isSuperType; - } else { - $result = $result->and($isSuperType); + + if ($isSuperType->maybe()) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($theirParameter->getType(), $ourParameterType); + $isSuperType = new IsSuperTypeOfResult($isSuperType->result, array_merge($isSuperType->reasons, [ + sprintf( + 'Type %s of parameter %s of passed callable needs to be same or wider than parameter type %s of accepting callable.', + $theirParameter->getType()->describe($verbosity), + $parameterDescription, + $ourParameterType->describe($verbosity), + ), + ])); } + + $result = $result->and($isSuperType); + } + + if (!$treatMixedAsAny && $theirParameterCount < $ourParameterCount) { + $result = $result->and(IsSuperTypeOfResult::createMaybe()); } $theirReturnType = $theirs->getReturnType(); if ($treatMixedAsAny) { $isReturnTypeSuperType = $ours->getReturnType()->accepts($theirReturnType, true); + $isReturnTypeSuperType = new IsSuperTypeOfResult($isReturnTypeSuperType->result, $isReturnTypeSuperType->reasons); } else { $isReturnTypeSuperType = $ours->getReturnType()->isSuperTypeOf($theirReturnType); } - if ($result === null) { - $result = $isReturnTypeSuperType; - } else { - $result = $result->and($isReturnTypeSuperType); + + $pure = $ours->isPure(); + if ($pure->yes()) { + $result = $result->and(new IsSuperTypeOfResult($theirs->isPure(), [])); + } elseif ($pure->no()) { + $result = $result->and(new IsSuperTypeOfResult($theirs->isPure()->negate(), [])); } - return $result; + return $result->and($isReturnTypeSuperType); } } diff --git a/src/Type/CircularTypeAliasDefinitionException.php b/src/Type/CircularTypeAliasDefinitionException.php index ad4bbfa25e..d0502cb9a1 100644 --- a/src/Type/CircularTypeAliasDefinitionException.php +++ b/src/Type/CircularTypeAliasDefinitionException.php @@ -4,7 +4,7 @@ use Exception; -class CircularTypeAliasDefinitionException extends Exception +final class CircularTypeAliasDefinitionException extends Exception { } diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index f37d5d3ff2..c5dae4f195 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -2,8 +2,9 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantStringType; /** @api */ class ClassStringType extends StringType @@ -20,46 +21,22 @@ public function describe(VerbosityLevel $level): string return 'class-string'; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - if ($type instanceof ConstantStringType) { - return TrinaryLogic::createFromBoolean($type->isClassString()); - } - - if ($type instanceof self) { - return TrinaryLogic::createYes(); - } - - if ($type instanceof StringType) { - return TrinaryLogic::createMaybe(); - } - - return TrinaryLogic::createNo(); + return new AcceptsResult($type->isClassString(), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - if ($type instanceof ConstantStringType) { - return TrinaryLogic::createFromBoolean($type->isClassString()); - } - - if ($type instanceof self) { - return TrinaryLogic::createYes(); - } - - if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); - } - if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return new IsSuperTypeOfResult($type->isClassString(), []); } public function isString(): TrinaryLogic @@ -77,17 +54,44 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('class-string'); } } diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index a28df65cc1..9a12b00fbb 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -4,15 +4,30 @@ use Closure; use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Node\InvalidateExprNode; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Printer\Printer; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\ClosureCallUnresolvedMethodPrototypeReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; @@ -22,47 +37,118 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; -use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use function array_map; use function array_merge; -use function implode; -use function sprintf; +use function count; /** @api */ -class ClosureType implements TypeWithClassName, ParametersAcceptor +class ClosureType implements TypeWithClassName, CallableParametersAcceptor { - use NonGenericTypeTrait; + use NonArrayTypeTrait; + use NonIterableTypeTrait; use UndecidedComparisonTypeTrait; use NonOffsetAccessibleTypeTrait; use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; + /** @var list */ + private array $parameters; + + private Type $returnType; + + private bool $isCommonCallable; + private ObjectType $objectType; private TemplateTypeMap $templateTypeMap; private TemplateTypeMap $resolvedTemplateTypeMap; + private TemplateTypeVarianceMap $callSiteVarianceMap; + + /** @var SimpleImpurePoint[] */ + private array $impurePoints; + + private TrinaryLogic $acceptsNamedArguments; + /** * @api - * @param array $parameters + * @param list|null $parameters + * @param array $templateTags + * @param SimpleThrowPoint[] $throwPoints + * @param ?SimpleImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + * @param string[] $usedVariables */ public function __construct( - private array $parameters, - private Type $returnType, - private bool $variadic, + ?array $parameters = null, + ?Type $returnType = null, + private bool $variadic = true, ?TemplateTypeMap $templateTypeMap = null, ?TemplateTypeMap $resolvedTemplateTypeMap = null, + ?TemplateTypeVarianceMap $callSiteVarianceMap = null, + private array $templateTags = [], + private array $throwPoints = [], + ?array $impurePoints = null, + private array $invalidateExpressions = [], + private array $usedVariables = [], + ?TrinaryLogic $acceptsNamedArguments = null, ) { + if ($acceptsNamedArguments === null) { + $acceptsNamedArguments = TrinaryLogic::createYes(); + } + $this->acceptsNamedArguments = $acceptsNamedArguments; + + $this->parameters = $parameters ?? []; + $this->returnType = $returnType ?? new MixedType(); + $this->isCommonCallable = $parameters === null && $returnType === null; $this->objectType = new ObjectType(Closure::class); $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty(); + $this->impurePoints = $impurePoints ?? [new SimpleImpurePoint('functionCall', 'call to an unknown Closure', false)]; + } + + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + + public static function createPure(): self + { + return new self(null, null, true, null, null, null, [], [], []); + } + + public function isPure(): TrinaryLogic + { + $impurePoints = $this->getImpurePoints(); + if (count($impurePoints) === 0) { + return TrinaryLogic::createYes(); + } + + $certainCount = 0; + foreach ($impurePoints as $impurePoint) { + if (!$impurePoint->isCertain()) { + continue; + } + + $certainCount++; + } + + return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(); } public function getClassName(): string @@ -80,9 +166,6 @@ public function getAncestorWithClassName(string $className): ?TypeWithClassName return $this->objectType->getAncestorWithClassName($className); } - /** - * @return string[] - */ public function getReferencedClasses(): array { $classes = $this->objectType->getReferencedClasses(); @@ -93,7 +176,17 @@ public function getReferencedClasses(): array return array_merge($classes, $this->returnType->getReferencedClasses()); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return $this->objectType->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->objectType->getObjectClassReflections(); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); @@ -103,29 +196,35 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic return $this->objectType->accepts($type, $strictTypes); } - return $this->isSuperTypeOfInternal($type, true); + return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + return $this->isSuperTypeOfInternal($type, false); } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): TrinaryLogic + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult { if ($type instanceof self) { + $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); + $variant = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$type], false); + if (!$variant instanceof CallableParametersAcceptor) { + return IsSuperTypeOfResult::createNo([]); + } return CallableTypeHelper::isParametersAcceptorSuperTypeOf( $this, - $type, + $variant, $treatMixedAsAny, ); } - if ( - $type instanceof TypeWithClassName - && $type->getClassName() === Closure::class - ) { - return TrinaryLogic::createMaybe(); + if ($type->getObjectClassNames() === [Closure::class]) { + return IsSuperTypeOfResult::createMaybe(); } return $this->objectType->isSuperTypeOf($type); @@ -137,21 +236,65 @@ public function equals(Type $type): bool return false; } - return $this->returnType->equals($type->returnType); + return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise()); } public function describe(VerbosityLevel $level): string { return $level->handle( static fn (): string => 'Closure', - fn (): string => sprintf( - 'Closure(%s): %s', - implode(', ', array_map(static fn (ParameterReflection $parameter): string => sprintf('%s%s', $parameter->isVariadic() ? '...' : '', $parameter->getType()->describe($level)), $this->parameters)), - $this->returnType->describe($level), - ), + function (): string { + if ($this->isCommonCallable) { + return $this->isPure()->yes() ? 'pure-Closure' : 'Closure'; + } + + $printer = new Printer(); + $selfWithoutParameterNames = new self( + array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter( + '', + $p->getType(), + $p->isOptional() && !$p->isVariadic(), + PassedByReference::createNo(), + $p->isVariadic(), + $p->getDefaultValue(), + ), $this->parameters), + $this->returnType, + $this->variadic, + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + $this->templateTags, + $this->throwPoints, + $this->impurePoints, + $this->invalidateExpressions, + $this->usedVariables, + ); + + return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + }, ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isObject(): TrinaryLogic + { + return $this->objectType->isObject(); + } + + public function isEnum(): TrinaryLogic + { + return $this->objectType->isEnum(); + } + + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->objectType->getTemplateType($ancestorClassName, $templateTypeName); + } + public function canAccessProperties(): TrinaryLogic { return $this->objectType->canAccessProperties(); @@ -162,7 +305,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return $this->objectType->hasProperty($propertyName); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->objectType->getProperty($propertyName, $scope); } @@ -182,7 +325,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->objectType->hasMethod($methodName); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -209,11 +352,16 @@ public function hasConstant(string $constantName): TrinaryLogic return $this->objectType->hasConstant($constantName); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { return $this->objectType->getConstant($constantName); } + public function getConstantStrings(): array + { + return []; + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -224,29 +372,51 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createNo(); } - public function getIterableKeyType(): Type + public function isCallable(): TrinaryLogic { - return new ErrorType(); + return TrinaryLogic::createYes(); } - public function getIterableValueType(): Type + public function getEnumCases(): array { - return new ErrorType(); + return []; } - public function isCallable(): TrinaryLogic + public function isCommonCallable(): bool { - return TrinaryLogic::createYes(); + return $this->isCommonCallable; } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [$this]; } + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + + public function getUsedVariables(): array + { + return $this->usedVariables; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->acceptsNamedArguments; + } + public function isCloneable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -262,6 +432,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); @@ -282,10 +457,22 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return TypeCombinator::union($this, new CallableType()); + } + public function getTemplateTypeMap(): TemplateTypeMap { return $this->templateTypeMap; @@ -296,8 +483,13 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap; } + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; + } + /** - * @return array + * @return list */ public function getParameters(): array { @@ -337,10 +529,12 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $parametersAcceptor): TemplateTypeMap { - $typeMap = TemplateTypeMap::createEmpty(); + $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$parametersAcceptor], false); $args = $parametersAcceptor->getParameters(); $returnType = $parametersAcceptor->getReturnType(); + $typeMap = TemplateTypeMap::createEmpty(); foreach ($this->getParameters() as $i => $param) { $paramType = $param->getType(); if (isset($args[$i])) { @@ -357,8 +551,29 @@ private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $para return $typeMap->union($this->getReturnType()->inferTemplateTypes($returnType)); } + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + $references = $this->getReturnType()->getReferencedTemplateTypes( + $positionVariance->compose(TemplateTypeVariance::createCovariant()), + ); + + $paramVariance = $positionVariance->compose(TemplateTypeVariance::createContravariant()); + + foreach ($this->getParameters() as $param) { + foreach ($param->getType()->getReferencedTemplateTypes($paramVariance) as $reference) { + $references[] = $reference; + } + } + + return $references; + } + public function traverse(callable $cb): Type { + if ($this->isCommonCallable) { + return $this; + } + return new self( array_map(static function (ParameterReflection $param) use ($cb): NativeParameterReflection { $defaultValue = $param->getDefaultValue(); @@ -375,10 +590,112 @@ public function traverse(callable $cb): Type $this->isVariadic(), $this->templateTypeMap, $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + $this->templateTags, + $this->throwPoints, + $this->impurePoints, + $this->invalidateExpressions, + $this->usedVariables, + $this->acceptsNamedArguments, ); } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->isCommonCallable) { + return $this; + } + + if (!$right instanceof self) { + return $this; + } + + $rightParameters = $right->getParameters(); + if (count($this->getParameters()) !== count($rightParameters)) { + return $this; + } + + $parameters = []; + foreach ($this->getParameters() as $i => $leftParam) { + $rightParam = $rightParameters[$i]; + $leftDefaultValue = $leftParam->getDefaultValue(); + $rightDefaultValue = $rightParam->getDefaultValue(); + $defaultValue = $leftDefaultValue; + if ($leftDefaultValue !== null && $rightDefaultValue !== null) { + $defaultValue = $cb($leftDefaultValue, $rightDefaultValue); + } + $parameters[] = new NativeParameterReflection( + $leftParam->getName(), + $leftParam->isOptional(), + $cb($leftParam->getType(), $rightParam->getType()), + $leftParam->passedByReference(), + $leftParam->isVariadic(), + $defaultValue, + ); + } + + return new self( + $parameters, + $cb($this->getReturnType(), $right->getReturnType()), + $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + $this->templateTags, + $this->throwPoints, + $this->impurePoints, + $this->invalidateExpressions, + $this->usedVariables, + $this->acceptsNamedArguments, + ); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -398,22 +715,97 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function isLowercaseString(): TrinaryLogic { - return new self( - $properties['parameters'], - $properties['returnType'], - $properties['variadic'], - $properties['templateTypeMap'], - $properties['resolvedTemplateTypeMap'], + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + if ($this->isCommonCallable) { + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-Closure' : 'Closure'); + } + + $parameters = []; + foreach ($this->parameters as $parameter) { + $parameters[] = new CallableTypeParameterNode( + $parameter->getType()->toPhpDocNode(), + !$parameter->passedByReference()->no(), + $parameter->isVariadic(), + $parameter->getName() === '' ? '' : '$' . $parameter->getName(), + $parameter->isOptional(), + ); + } + + $templateTags = []; + foreach ($this->templateTags as $templateName => $templateTag) { + $templateTags[] = new TemplateTagValueNode( + $templateName, + $templateTag->getBound()->toPhpDocNode(), + '', + ); + } + + return new CallableTypeNode( + new IdentifierTypeNode('Closure'), + $parameters, + $this->returnType->toPhpDocNode(), + $templateTags, ); } diff --git a/src/Type/ClosureTypeFactory.php b/src/Type/ClosureTypeFactory.php new file mode 100644 index 0000000000..fb36d04c44 --- /dev/null +++ b/src/Type/ClosureTypeFactory.php @@ -0,0 +1,119 @@ +reflectionSourceStubber->generateFunctionStubFromReflection(new ReflectionFunction($closure)); + if ($stubData === null) { + throw new ShouldNotHappenException('Closure reflection not found.'); + } + $source = $stubData->getStub(); + $source = str_replace('{closure}', 'foo', $source); + $locatedSource = new LocatedSource($source, '{closure}', $stubData->getFileName()); + $find = new FindReflectionsInTree(new NodeToReflection()); + $ast = $this->parser->parse($locatedSource->getSource()); + if ($ast === null) { + throw new ShouldNotHappenException('Closure reflection not found.'); + } + + /** @var list<\PHPStan\BetterReflection\Reflection\ReflectionFunction> $reflections */ + $reflections = $find($this->reflector, $ast, new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), $locatedSource); + if (count($reflections) !== 1) { + throw new ShouldNotHappenException('Closure reflection not found.'); + } + + $betterReflectionFunction = $reflections[0]; + + $parameters = array_map(fn (BetterReflectionParameter $parameter) => new class($parameter, $this->initializerExprTypeResolver) implements ParameterReflection { + + public function __construct(private BetterReflectionParameter $reflection, private InitializerExprTypeResolver $initializerExprTypeResolver) + { + } + + public function getName(): string + { + return $this->reflection->getName(); + } + + public function isOptional(): bool + { + return $this->reflection->isOptional(); + } + + public function getType(): Type + { + return TypehintHelper::decideTypeFromReflection(ReflectionType::fromTypeOrNull($this->reflection->getType()), null, null, $this->reflection->isVariadic()); + } + + public function passedByReference(): PassedByReference + { + return $this->reflection->isPassedByReference() + ? PassedByReference::createCreatesNewVariable() + : PassedByReference::createNo(); + } + + public function isVariadic(): bool + { + return $this->reflection->isVariadic(); + } + + public function getDefaultValue(): ?Type + { + if (! $this->reflection->isDefaultValueAvailable()) { + return null; + } + + $defaultExpr = $this->reflection->getDefaultValueExpression(); + if ($defaultExpr === null) { + return null; + } + + return $this->initializerExprTypeResolver->getType($defaultExpr, InitializerExprContext::fromReflectionParameter(new ReflectionParameter($this->reflection))); + } + + }, $betterReflectionFunction->getParameters()); + + return new ClosureType($parameters, TypehintHelper::decideTypeFromReflection(ReflectionType::fromTypeOrNull($betterReflectionFunction->getReturnType())), $betterReflectionFunction->isVariadic()); + } + +} diff --git a/src/Type/CompoundType.php b/src/Type/CompoundType.php index 199c516c4b..f66e10c091 100644 --- a/src/Type/CompoundType.php +++ b/src/Type/CompoundType.php @@ -2,18 +2,19 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; /** @api */ interface CompoundType extends Type { - public function isSubTypeOf(Type $otherType): TrinaryLogic; + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult; - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic; + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult; - public function isGreaterThan(Type $otherType): TrinaryLogic; + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic; + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; } diff --git a/src/Type/ConditionalType.php b/src/Type/ConditionalType.php new file mode 100644 index 0000000000..f154fb5368 --- /dev/null +++ b/src/Type/ConditionalType.php @@ -0,0 +1,220 @@ +subject; + } + + public function getTarget(): Type + { + return $this->target; + } + + public function getIf(): Type + { + return $this->if; + } + + public function getElse(): Type + { + return $this->else; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof self) { + return $this->if->isSuperTypeOf($type->if) + ->and($this->else->isSuperTypeOf($type->else)); + } + + return $this->isSuperTypeOfDefault($type); + } + + public function getReferencedClasses(): array + { + return array_merge( + $this->subject->getReferencedClasses(), + $this->target->getReferencedClasses(), + $this->if->getReferencedClasses(), + $this->else->getReferencedClasses(), + ); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return array_merge( + $this->subject->getReferencedTemplateTypes($positionVariance), + $this->target->getReferencedTemplateTypes($positionVariance), + $this->if->getReferencedTemplateTypes($positionVariance), + $this->else->getReferencedTemplateTypes($positionVariance), + ); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->subject->equals($type->subject) + && $this->target->equals($type->target) + && $this->if->equals($type->if) + && $this->else->equals($type->else); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf( + '(%s %s %s ? %s : %s)', + $this->subject->describe($level), + $this->negated ? 'is not' : 'is', + $this->target->describe($level), + $this->if->describe($level), + $this->else->describe($level), + ); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target); + } + + protected function getResult(): Type + { + $isSuperType = $this->target->isSuperTypeOf($this->subject); + + if ($isSuperType->yes()) { + return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse(); + } + + if ($isSuperType->no()) { + return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf(); + } + + return TypeCombinator::union( + $this->getNormalizedIf(), + $this->getNormalizedElse(), + ); + } + + public function traverse(callable $cb): Type + { + $subject = $cb($this->subject); + $target = $cb($this->target); + $if = $cb($this->getNormalizedIf()); + $else = $cb($this->getNormalizedElse()); + + if ( + $this->subject === $subject + && $this->target === $target + && $this->getNormalizedIf() === $if + && $this->getNormalizedElse() === $else + ) { + return $this; + } + + return new self($subject, $target, $if, $else, $this->negated); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $subject = $cb($this->subject, $right->subject); + $target = $cb($this->target, $right->target); + $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf()); + $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse()); + + if ( + $this->subject === $subject + && $this->target === $target + && $this->getNormalizedIf() === $if + && $this->getNormalizedElse() === $else + ) { + return $this; + } + + return new self($subject, $target, $if, $else, $this->negated); + } + + public function toPhpDocNode(): TypeNode + { + return new ConditionalTypeNode( + $this->subject->toPhpDocNode(), + $this->target->toPhpDocNode(), + $this->if->toPhpDocNode(), + $this->else->toPhpDocNode(), + $this->negated, + ); + } + + private function getNormalizedIf(): Type + { + return $this->normalizedIf ??= TypeTraverser::map( + $this->if, + fn (Type $type, callable $traverse) => $type === $this->subject + ? (!$this->negated ? $this->getSubjectWithTargetIntersectedType() : $this->getSubjectWithTargetRemovedType()) + : $traverse($type), + ); + } + + private function getNormalizedElse(): Type + { + return $this->normalizedElse ??= TypeTraverser::map( + $this->else, + fn (Type $type, callable $traverse) => $type === $this->subject + ? (!$this->negated ? $this->getSubjectWithTargetRemovedType() : $this->getSubjectWithTargetIntersectedType()) + : $traverse($type), + ); + } + + private function getSubjectWithTargetIntersectedType(): Type + { + return $this->subjectWithTargetIntersectedType ??= TypeCombinator::intersect($this->subject, $this->target); + } + + private function getSubjectWithTargetRemovedType(): Type + { + return $this->subjectWithTargetRemovedType ??= TypeCombinator::remove($this->subject, $this->target); + } + +} diff --git a/src/Type/ConditionalTypeForParameter.php b/src/Type/ConditionalTypeForParameter.php new file mode 100644 index 0000000000..0fd8cf4475 --- /dev/null +++ b/src/Type/ConditionalTypeForParameter.php @@ -0,0 +1,177 @@ +parameterName; + } + + public function getTarget(): Type + { + return $this->target; + } + + public function getIf(): Type + { + return $this->if; + } + + public function getElse(): Type + { + return $this->else; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function changeParameterName(string $parameterName): self + { + return new self( + $parameterName, + $this->target, + $this->if, + $this->else, + $this->negated, + ); + } + + public function toConditional(Type $subject): Type + { + return new ConditionalType( + $subject, + $this->target, + $this->if, + $this->else, + $this->negated, + ); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof self) { + return $this->if->isSuperTypeOf($type->if) + ->and($this->else->isSuperTypeOf($type->else)); + } + + return $this->isSuperTypeOfDefault($type); + } + + public function getReferencedClasses(): array + { + return array_merge( + $this->target->getReferencedClasses(), + $this->if->getReferencedClasses(), + $this->else->getReferencedClasses(), + ); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return array_merge( + $this->target->getReferencedTemplateTypes($positionVariance), + $this->if->getReferencedTemplateTypes($positionVariance), + $this->else->getReferencedTemplateTypes($positionVariance), + ); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->parameterName === $type->parameterName + && $this->target->equals($type->target) + && $this->if->equals($type->if) + && $this->else->equals($type->else); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf( + '(%s %s %s ? %s : %s)', + $this->parameterName, + $this->negated ? 'is not' : 'is', + $this->target->describe($level), + $this->if->describe($level), + $this->else->describe($level), + ); + } + + public function isResolvable(): bool + { + return false; + } + + protected function getResult(): Type + { + return TypeCombinator::union($this->if, $this->else); + } + + public function traverse(callable $cb): Type + { + $target = $cb($this->target); + $if = $cb($this->if); + $else = $cb($this->else); + + if ($this->target === $target && $this->if === $if && $this->else === $else) { + return $this; + } + + return new self($this->parameterName, $target, $if, $else, $this->negated); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $target = $cb($this->target, $right->target); + $if = $cb($this->if, $right->if); + $else = $cb($this->else, $right->else); + + if ($this->target === $target && $this->if === $if && $this->else === $else) { + return $this; + } + + return new self($this->parameterName, $target, $if, $else, $this->negated); + } + + public function toPhpDocNode(): TypeNode + { + return new ConditionalTypeForParameterNode( + $this->parameterName, + $this->target->toPhpDocNode(), + $this->if->toPhpDocNode(), + $this->else->toPhpDocNode(), + $this->negated, + ); + } + +} diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 761390ce05..a24df69710 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -2,31 +2,47 @@ namespace PHPStan\Type\Constant; +use Nette\Utils\Strings; +use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Internal\CombinationsHelper; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\FunctionCallableVariant; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\InaccessibleMethod; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; -use PHPStan\Type\ConstantType; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; use PHPStan\Type\GeneralizePrecision; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StringType; +use PHPStan\Type\NullType; +use PHPStan\Type\Traits\ArrayTypeTrait; +use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; @@ -35,6 +51,7 @@ use function array_map; use function array_merge; use function array_pop; +use function array_push; use function array_slice; use function array_unique; use function array_values; @@ -43,51 +60,136 @@ use function implode; use function in_array; use function is_string; -use function max; +use function min; use function pow; +use function range; +use function sort; use function sprintf; -use function strpos; +use function str_contains; /** * @api */ -class ConstantArrayType extends ArrayType implements ConstantType +class ConstantArrayType implements Type { + use ArrayTypeTrait { + chunkArray as traitChunkArray; + } + use NonObjectTypeTrait; + use UndecidedComparisonTypeTrait; + private const DESCRIBE_LIMIT = 8; + private const CHUNK_FINITE_TYPES_LIMIT = 5; + + private TrinaryLogic $isList; /** @var self[]|null */ private ?array $allArrays = null; + private ?Type $iterableKeyType = null; + + private ?Type $iterableValueType = null; + /** * @api * @param array $keyTypes * @param array $valueTypes + * @param non-empty-list $nextAutoIndexes * @param int[] $optionalKeys */ public function __construct( private array $keyTypes, private array $valueTypes, - private int $nextAutoIndex = 0, + private array $nextAutoIndexes = [0], private array $optionalKeys = [], + ?TrinaryLogic $isList = null, ) { assert(count($keyTypes) === count($valueTypes)); - parent::__construct( - count($keyTypes) > 0 ? TypeCombinator::union(...$keyTypes) : new NeverType(true), - count($valueTypes) > 0 ? TypeCombinator::union(...$valueTypes) : new NeverType(true), - ); + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + $isList = TrinaryLogic::createYes(); + } + + if ($isList === null) { + $isList = TrinaryLogic::createNo(); + } + $this->isList = $isList; + } + + public function getConstantArrays(): array + { + return [$this]; + } + + public function getReferencedClasses(): array + { + $referencedClasses = []; + foreach ($this->getKeyTypes() as $keyType) { + foreach ($keyType->getReferencedClasses() as $referencedClass) { + $referencedClasses[] = $referencedClass; + } + } + + foreach ($this->getValueTypes() as $valueType) { + foreach ($valueType->getReferencedClasses() as $referencedClass) { + $referencedClasses[] = $referencedClass; + } + } + + return $referencedClasses; + } + + public function getIterableKeyType(): Type + { + if ($this->iterableKeyType !== null) { + return $this->iterableKeyType; + } + + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + $keyType = new NeverType(true); + } elseif ($keyTypesCount === 1) { + $keyType = $this->keyTypes[0]; + } else { + $keyType = new UnionType($this->keyTypes); + } + + return $this->iterableKeyType = $keyType; + } + + public function getIterableValueType(): Type + { + if ($this->iterableValueType !== null) { + return $this->iterableValueType; + } + + return $this->iterableValueType = count($this->valueTypes) > 0 ? TypeCombinator::union(...$this->valueTypes) : new NeverType(true); + } + + public function getKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getItemType(): Type + { + return $this->getIterableValueType(); } - public function isEmpty(): bool + public function isConstantValue(): TrinaryLogic { - return count($this->keyTypes) === 0; + return TrinaryLogic::createYes(); } - public function getNextAutoIndex(): int + /** + * @return non-empty-list + */ + public function getNextAutoIndexes(): array { - return $this->nextAutoIndex; + return $this->nextAutoIndexes; } /** @@ -127,13 +229,19 @@ public function getAllArrays(): array $arrays = []; foreach ($optionalKeysCombinations as $combination) { $keys = array_merge($requiredKeys, $combination); + sort($keys); + + if ($this->isList->yes() && array_keys($keys) !== $keys) { + continue; + } + $builder = ConstantArrayTypeBuilder::createEmpty(); foreach ($keys as $i) { $builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i]); } $array = $builder->getArray(); - if (!$array instanceof ConstantArrayType) { + if (!$array instanceof self) { throw new ShouldNotHappenException(); } @@ -169,15 +277,6 @@ private function powerSet(array $in): array return $return; } - public function getKeyType(): Type - { - if (count($this->keyTypes) > 1) { - return new UnionType($this->keyTypes); - } - - return parent::getKeyType(); - } - /** * @return array */ @@ -199,20 +298,24 @@ public function isOptionalKey(int $i): bool return in_array($i, $this->optionalKeys, true); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { - if ($type instanceof MixedType && !$type instanceof TemplateMixedType) { + if ($type instanceof CompoundType && !$type instanceof IntersectionType) { return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof self && count($this->keyTypes) === 0) { - return TrinaryLogic::createFromBoolean(count($type->keyTypes) === 0); + return AcceptsResult::createFromBoolean(count($type->keyTypes) === 0); } - $result = TrinaryLogic::createYes(); + $result = AcceptsResult::createYes(); foreach ($this->keyTypes as $i => $keyType) { $valueType = $this->valueTypes[$i]; - $hasOffset = $type->hasOffsetValueType($keyType); + $hasOffsetValueType = $type->hasOffsetValueType($keyType); + $hasOffset = new AcceptsResult( + $hasOffsetValueType, + $hasOffsetValueType->yes() || !$type->isConstantArray()->yes() ? [] : [sprintf('Array %s have offset %s.', $hasOffsetValueType->no() ? 'does not' : 'might not', $keyType->describe(VerbosityLevel::value()))], + ); if ($hasOffset->no()) { if ($this->isOptionalKey($i)) { continue; @@ -220,33 +323,52 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic return $hasOffset; } if ($hasOffset->maybe() && $this->isOptionalKey($i)) { - $hasOffset = TrinaryLogic::createYes(); + $hasOffset = AcceptsResult::createYes(); } $result = $result->and($hasOffset); $otherValueType = $type->getOffsetValueType($keyType); - $acceptsValue = $valueType->accepts($otherValueType, $strictTypes); + $verbosity = VerbosityLevel::getRecommendedLevelByType($valueType, $otherValueType); + $acceptsValue = $valueType->accepts($otherValueType, $strictTypes)->decorateReasons( + static fn (string $reason) => sprintf( + 'Offset %s (%s) does not accept type %s: %s', + $keyType->describe(VerbosityLevel::precise()), + $valueType->describe($verbosity), + $otherValueType->describe($verbosity), + $reason, + ), + ); + if (!$acceptsValue->yes() && count($acceptsValue->reasons) === 0 && $type->isConstantArray()->yes()) { + $acceptsValue = new AcceptsResult($acceptsValue->result, [ + sprintf( + 'Offset %s (%s) does not accept type %s.', + $keyType->describe(VerbosityLevel::precise()), + $valueType->describe($verbosity), + $otherValueType->describe($verbosity), + ), + ]); + } if ($acceptsValue->no()) { return $acceptsValue; } $result = $result->and($acceptsValue); } - return $result->and($type->isArray()); + $result = $result->and(new AcceptsResult($type->isArray(), [])); + if ($type->isOversizedArray()->yes()) { + if (!$result->no()) { + return AcceptsResult::createYes(); + } + } + + return $result; } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { if (count($this->keyTypes) === 0) { - if (count($type->keyTypes) > 0) { - if (count($type->optionalKeys) > 0) { - return TrinaryLogic::createMaybe(); - } - return TrinaryLogic::createNo(); - } - - return TrinaryLogic::createYes(); + return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []); } $results = []; @@ -254,35 +376,70 @@ public function isSuperTypeOf(Type $type): TrinaryLogic $hasOffset = $type->hasOffsetValueType($keyType); if ($hasOffset->no()) { if (!$this->isOptionalKey($i)) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } - $results[] = TrinaryLogic::createMaybe(); + $results[] = IsSuperTypeOfResult::createYes(); continue; + } elseif ($hasOffset->maybe() && !$this->isOptionalKey($i)) { + $results[] = IsSuperTypeOfResult::createMaybe(); } - $results[] = $this->valueTypes[$i]->isSuperTypeOf($type->getOffsetValueType($keyType)); + + $isValueSuperType = $this->valueTypes[$i]->isSuperTypeOf($type->getOffsetValueType($keyType)); + if ($isValueSuperType->no()) { + return $isValueSuperType->decorateReasons(static fn (string $reason) => sprintf('Offset %s: %s', $keyType->describe(VerbosityLevel::value()), $reason)); + } + $results[] = $isValueSuperType; } - return TrinaryLogic::createYes()->and(...$results); + return IsSuperTypeOfResult::createYes()->and(...$results); } if ($type instanceof ArrayType) { - $result = TrinaryLogic::createMaybe(); + $result = IsSuperTypeOfResult::createMaybe(); if (count($this->keyTypes) === 0) { return $result; } - return $result->and( - $this->getKeyType()->isSuperTypeOf($type->getKeyType()), - $this->getItemType()->isSuperTypeOf($type->getItemType()), - ); + $isKeySuperType = $this->getKeyType()->isSuperTypeOf($type->getKeyType()); + if ($isKeySuperType->no()) { + return $isKeySuperType; + } + + return $result->and($isKeySuperType, $this->getItemType()->isSuperTypeOf($type->getItemType())); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isInteger()->yes()) { + return new ConstantBooleanType(false); + } + + if ($this->isIterableAtLeastOnce()->no()) { + if ($type->isIterableAtLeastOnce()->yes()) { + return new ConstantBooleanType(false); + } + + $constantScalarValues = $type->getConstantScalarValues(); + if (count($constantScalarValues) > 0) { + $results = []; + foreach ($constantScalarValues as $constantScalarValue) { + // @phpstan-ignore equal.invalid, equal.notAllowed + $results[] = TrinaryLogic::createFromBoolean($constantScalarValue == []); // phpcs:ignore + } + + return TrinaryLogic::extremeIdentity(...$results)->toBooleanType(); + } + } + + return new BooleanType(); } public function equals(Type $type): bool @@ -314,87 +471,116 @@ public function equals(Type $type): bool public function isCallable(): TrinaryLogic { - $typeAndMethod = $this->findTypeAndMethodName(); - if ($typeAndMethod === null) { + $typeAndMethods = $this->findTypeAndMethodNames(); + if ($typeAndMethods === []) { return TrinaryLogic::createNo(); } - return $typeAndMethod->getCertainty(); + $results = array_map( + static fn (ConstantArrayTypeAndMethod $typeAndMethod): TrinaryLogic => $typeAndMethod->getCertainty(), + $typeAndMethods, + ); + + return TrinaryLogic::createYes()->and(...$results); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - $typeAndMethodName = $this->findTypeAndMethodName(); - if ($typeAndMethodName === null) { + $typeAndMethodNames = $this->findTypeAndMethodNames(); + if ($typeAndMethodNames === []) { throw new ShouldNotHappenException(); } - if ($typeAndMethodName->isUnknown() || !$typeAndMethodName->getCertainty()->yes()) { - return [new TrivialParametersAcceptor()]; - } + $acceptors = []; + foreach ($typeAndMethodNames as $typeAndMethodName) { + if ($typeAndMethodName->isUnknown() || !$typeAndMethodName->getCertainty()->yes()) { + $acceptors[] = new TrivialParametersAcceptor(); + continue; + } + + $method = $typeAndMethodName->getType() + ->getMethod($typeAndMethodName->getMethod(), $scope); - $method = $typeAndMethodName->getType() - ->getMethod($typeAndMethodName->getMethod(), $scope); + if (!$scope->canCallMethod($method)) { + $acceptors[] = new InaccessibleMethod($method); + continue; + } - if (!$scope->canCallMethod($method)) { - return [new InaccessibleMethod($method)]; + array_push($acceptors, ...FunctionCallableVariant::createFromVariants($method, $method->getVariants())); } - return $method->getVariants(); + return $acceptors; } - public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod + /** @return ConstantArrayTypeAndMethod[] */ + public function findTypeAndMethodNames(): array { if (count($this->keyTypes) !== 2) { - return null; + return []; } - if ($this->keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) { - return null; + $classOrObject = null; + $method = null; + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->isSuperTypeOf(new ConstantIntegerType(0))->yes()) { + $classOrObject = $this->valueTypes[$i]; + continue; + } + + if (!$keyType->isSuperTypeOf(new ConstantIntegerType(1))->yes()) { + continue; + } + + $method = $this->valueTypes[$i]; } - if ($this->keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no()) { - return null; + if ($classOrObject === null || $method === null) { + return []; } - [$classOrObject, $method] = $this->valueTypes; + $callableArray = [$classOrObject, $method]; - if (!$method instanceof ConstantStringType) { - return ConstantArrayTypeAndMethod::createUnknown(); + [$classOrObject, $methods] = $callableArray; + if (count($methods->getConstantStrings()) === 0) { + return [ConstantArrayTypeAndMethod::createUnknown()]; } - if ($classOrObject instanceof ConstantStringType) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($classOrObject->getValue())) { - return ConstantArrayTypeAndMethod::createUnknown(); - } - $type = new ObjectType($reflectionProvider->getClass($classOrObject->getValue())->getName()); - } elseif ($classOrObject instanceof GenericClassStringType) { - $type = $classOrObject->getGenericType(); - } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($classOrObject)->yes()) { - $type = $classOrObject; - } else { - return ConstantArrayTypeAndMethod::createUnknown(); + $type = $classOrObject->getObjectTypeOrClassStringObjectType(); + if (!$type->isObject()->yes()) { + return [ConstantArrayTypeAndMethod::createUnknown()]; } - $has = $type->hasMethod($method->getValue()); - if (!$has->no()) { + $typeAndMethods = []; + $phpVersion = PhpVersionStaticAccessor::getInstance(); + foreach ($methods->getConstantStrings() as $methodName) { + $has = $type->hasMethod($methodName->getValue()); + if ($has->no()) { + continue; + } + + if ( + $has->yes() + && !$phpVersion->supportsCallableInstanceMethods() + ) { + $methodReflection = $type->getMethod($methodName->getValue(), new OutOfClassScope()); + if ($classOrObject->isString()->yes() && !$methodReflection->isStatic()) { + continue; + } + } + if ($this->isOptionalKey(0) || $this->isOptionalKey(1)) { $has = $has->and(TrinaryLogic::createMaybe()); } - return ConstantArrayTypeAndMethod::createConcrete($type, $method->getValue(), $has); + $typeAndMethods[] = ConstantArrayTypeAndMethod::createConcrete($type, $methodName->getValue(), $has); } - return null; + return $typeAndMethods; } public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); if ($offsetType instanceof UnionType) { $results = []; foreach ($offsetType->getTypes() as $innerType) { @@ -403,13 +589,24 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic return TrinaryLogic::extremeIdentity(...$results); } + if ($offsetType instanceof IntegerRangeType) { + $finiteTypes = $offsetType->getFiniteTypes(); + if ($finiteTypes !== []) { + $results = []; + foreach ($finiteTypes as $innerType) { + $results[] = $this->hasOffsetValueType($innerType); + } + + return TrinaryLogic::extremeIdentity(...$results); + } + } $result = TrinaryLogic::createNo(); foreach ($this->keyTypes as $i => $keyType) { if ( $keyType instanceof ConstantIntegerType - && $offsetType instanceof StringType - && !$offsetType instanceof ConstantStringType + && !$offsetType->isString()->no() + && $offsetType->isConstantScalarValue()->no() ) { return TrinaryLogic::createMaybe(); } @@ -433,16 +630,36 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + if (count($this->keyTypes) === 0) { + return new ErrorType(); + } + + $offsetType = $offsetType->toArrayKey(); $matchingValueTypes = []; + $all = true; + $maybeAll = true; foreach ($this->keyTypes as $i => $keyType) { if ($keyType->isSuperTypeOf($offsetType)->no()) { + $all = false; + + if ( + $keyType instanceof ConstantIntegerType + && !$offsetType->isString()->no() + && $offsetType->isConstantScalarValue()->no() + ) { + continue; + } + $maybeAll = false; continue; } $matchingValueTypes[] = $this->valueTypes[$i]; } + if ($all) { + return $this->getIterableValueType(); + } + if (count($matchingValueTypes) > 0) { $type = TypeCombinator::union(...$matchingValueTypes); if ($type instanceof ErrorType) { @@ -452,6 +669,10 @@ public function getOffsetValueType(Type $offsetType): Type return $type; } + if ($maybeAll) { + return $this->getIterableValueType(); + } + return new ErrorType(); // undefined offset } @@ -463,255 +684,639 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $builder->getArray(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + $offsetType = $offsetType->toArrayKey(); + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + foreach ($this->keyTypes as $keyType) { + if ($offsetType->isSuperTypeOf($keyType)->no()) { + continue; + } + + $builder->setOffsetValueType($keyType, $valueType); + } + + return $builder->getArray(); + } + public function unsetOffset(Type $offsetType): Type { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) { foreach ($this->keyTypes as $i => $keyType) { - if ($keyType->getValue() === $offsetType->getValue()) { - $keyTypes = $this->keyTypes; - unset($keyTypes[$i]); - $valueTypes = $this->valueTypes; - unset($valueTypes[$i]); - - $newKeyTypes = []; - $newValueTypes = []; - $newOptionalKeys = []; - - $k = 0; - foreach ($keyTypes as $j => $newKeyType) { - $newKeyTypes[] = $newKeyType; - $newValueTypes[] = $valueTypes[$j]; - if (in_array($j, $this->optionalKeys, true)) { - $newOptionalKeys[] = $k; - } - $k++; + if ($keyType->getValue() !== $offsetType->getValue()) { + continue; + } + + $keyTypes = $this->keyTypes; + unset($keyTypes[$i]); + $valueTypes = $this->valueTypes; + unset($valueTypes[$i]); + + $newKeyTypes = []; + $newValueTypes = []; + $newOptionalKeys = []; + + $k = 0; + foreach ($keyTypes as $j => $newKeyType) { + $newKeyTypes[] = $newKeyType; + $newValueTypes[] = $valueTypes[$j]; + if (in_array($j, $this->optionalKeys, true)) { + $newOptionalKeys[] = $k; + } + $k++; + } + + return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, TrinaryLogic::createNo()); + } + + return $this; + } + + $constantScalars = $offsetType->getConstantScalarTypes(); + if (count($constantScalars) > 0) { + $optionalKeys = $this->optionalKeys; + + foreach ($constantScalars as $constantScalar) { + $constantScalar = $constantScalar->toArrayKey(); + if (!$constantScalar instanceof ConstantIntegerType && !$constantScalar instanceof ConstantStringType) { + continue; + } + + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->getValue() !== $constantScalar->getValue()) { + continue; + } + + if (in_array($i, $optionalKeys, true)) { + continue 2; } - return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndex, $newOptionalKeys); + $optionalKeys[] = $i; } } + + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); } - $arrays = []; - foreach ($this->getAllArrays() as $tmp) { - $arrays[] = new self($tmp->keyTypes, $tmp->valueTypes, $tmp->nextAutoIndex, array_keys($tmp->keyTypes)); + $optionalKeys = $this->optionalKeys; + $isList = $this->isList; + foreach ($this->keyTypes as $i => $keyType) { + if (!$offsetType->isSuperTypeOf($keyType)->yes()) { + continue; + } + $optionalKeys[] = $i; + $isList = TrinaryLogic::createNo(); } + $optionalKeys = array_values(array_unique($optionalKeys)); - return TypeCombinator::union(...$arrays)->generalize(GeneralizePrecision::moreSpecific()); + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $isList); } - public function isIterableAtLeastOnce(): TrinaryLogic + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type { - $keysCount = count($this->keyTypes); - if ($keysCount === 0) { - return TrinaryLogic::createNo(); - } + $biggerOne = IntegerRangeType::fromInterval(1, null); + $finiteTypes = $lengthType->getFiniteTypes(); + if ($biggerOne->isSuperTypeOf($lengthType)->yes() && count($finiteTypes) < self::CHUNK_FINITE_TYPES_LIMIT) { + $results = []; + foreach ($finiteTypes as $finiteType) { + if (!$finiteType instanceof ConstantIntegerType || $finiteType->getValue() < 1) { + return $this->traitChunkArray($lengthType, $preserveKeys); + } - $optionalKeysCount = count($this->optionalKeys); - if ($optionalKeysCount < $keysCount) { - return TrinaryLogic::createYes(); + $length = $finiteType->getValue(); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $keyTypesCount = count($this->keyTypes); + for ($i = 0; $i < $keyTypesCount; $i += $length) { + $chunk = $this->sliceArray(new ConstantIntegerType($i), new ConstantIntegerType($length), TrinaryLogic::createYes()); + $builder->setOffsetValueType(null, $preserveKeys->yes() ? $chunk : $chunk->getValuesArray()); + } + + $results[] = $builder->getArray(); + } + + return TypeCombinator::union(...$results); } - return TrinaryLogic::createMaybe(); + return $this->traitChunkArray($lengthType, $preserveKeys); } - public function removeLast(): self + public function fillKeysArray(Type $valueType): Type { - if (count($this->keyTypes) === 0) { - return $this; + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($this->valueTypes as $i => $keyType) { + if ($keyType->isInteger()->no()) { + $stringKeyType = $keyType->toString(); + if ($stringKeyType instanceof ErrorType) { + return $stringKeyType; + } + + $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i)); + } else { + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i)); + } } - $i = count($this->keyTypes) - 1; + return $builder->getArray(); + } - $keyTypes = $this->keyTypes; - $valueTypes = $this->valueTypes; - $optionalKeys = $this->optionalKeys; - unset($optionalKeys[$i]); + public function flipArray(): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); - $removedKeyType = array_pop($keyTypes); - array_pop($valueTypes); - $nextAutoindex = $removedKeyType instanceof ConstantIntegerType - ? $removedKeyType->getValue() - : $this->nextAutoIndex; + foreach ($this->keyTypes as $i => $keyType) { + $valueType = $this->valueTypes[$i]; + $builder->setOffsetValueType( + $valueType->toArrayKey(), + $keyType, + $this->isOptionalKey($i), + ); + } - return new self( - $keyTypes, - $valueTypes, - $nextAutoindex, - array_values($optionalKeys), - ); + return $builder->getArray(); } - public function removeFirst(): Type + public function intersectKeyArray(Type $otherArraysType): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($this->keyTypes as $i => $keyType) { - if ($i === 0) { + $valueType = $this->valueTypes[$i]; + $has = $otherArraysType->hasOffsetValueType($keyType); + if ($has->no()) { continue; } + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || !$has->yes()); + } - $valueType = $this->valueTypes[$i]; - if ($keyType instanceof ConstantIntegerType) { - $keyType = null; - } + return $builder->getArray(); + } - $builder->setOffsetValueType($keyType, $valueType); + public function popArray(): Type + { + return $this->removeLastElements(1); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no() + ? $this->keyTypes[$i] + : null; + $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $this->isOptionalKey($i)); } return $builder->getArray(); } - public function slice(int $offset, ?int $limit, bool $preserveKeys = false): self + public function searchArray(Type $needleType): Type { - if (count($this->keyTypes) === 0) { - return $this; - } + $matches = []; + $hasIdenticalValue = false; - $keyTypes = array_slice($this->keyTypes, $offset, $limit); - $valueTypes = array_slice($this->valueTypes, $offset, $limit); + foreach ($this->valueTypes as $index => $valueType) { + $isNeedleSuperType = $valueType->isSuperTypeOf($needleType); + if ($isNeedleSuperType->no()) { + continue; + } - if (!$preserveKeys) { - $i = 0; - /** @var array $keyTypes */ - $keyTypes = array_map(static function ($keyType) use (&$i) { - if ($keyType instanceof ConstantIntegerType) { - $i++; - return new ConstantIntegerType($i - 1); - } + if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType + && $needleType->getValue() === $valueType->getValue() + && !$this->isOptionalKey($index) + ) { + $hasIdenticalValue = true; + } - return $keyType; - }, $keyTypes); + $matches[] = $this->keyTypes[$index]; } - /** @var int|float $nextAutoIndex */ - $nextAutoIndex = 0; - foreach ($keyTypes as $keyType) { - if (!$keyType instanceof ConstantIntegerType) { - continue; + if (count($matches) > 0) { + if ($hasIdenticalValue) { + return TypeCombinator::union(...$matches); } - /** @var int|float $nextAutoIndex */ - $nextAutoIndex = max($nextAutoIndex, $keyType->getValue() + 1); + return TypeCombinator::union(new ConstantBooleanType(false), ...$matches); } - return new self( - $keyTypes, - $valueTypes, - (int) $nextAutoIndex, - [], - ); + return new ConstantBooleanType(false); } - public function toBoolean(): BooleanType + public function shiftArray(): Type { - return $this->count()->toBoolean(); + return $this->removeFirstElements(1); } - public function toInteger(): Type + public function shuffleArray(): Type { - return $this->toBoolean()->toInteger(); - } + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this->getValuesArray()); + $builder->degradeToGeneralArray(); - public function toFloat(): Type - { - return $this->toBoolean()->toFloat(); + return $builder->getArray(); } - public function generalize(GeneralizePrecision $precision): Type + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { - if (count($this->keyTypes) === 0) { + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { return $this; } - if ($precision->isTemplateArgument()) { - return $this->traverse(static fn (Type $type) => $type->generalize($precision)); + $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null; + + if ($lengthType instanceof ConstantIntegerType) { + $length = $lengthType->getValue(); + } elseif ($lengthType->isNull()->yes()) { + $length = $keyTypesCount; + } else { + $length = null; } - $arrayType = new ArrayType( - $this->getKeyType()->generalize($precision), - $this->getItemType()->generalize($precision), - ); + if ($offset === null || $length === null) { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->degradeToGeneralArray(); - if (count($this->keyTypes) > count($this->optionalKeys)) { - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + return $builder->getArray() + ->sliceArray($offsetType, $lengthType, $preserveKeys); } - return $arrayType; - } + if ($keyTypesCount + $offset <= 0) { + // A negative offset cannot reach left outside the array twice + $offset = 0; + } - /** - * @return self - */ - public function generalizeValues(): ArrayType - { - $valueTypes = []; - foreach ($this->valueTypes as $valueType) { - $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific()); + if ($keyTypesCount + $length <= 0) { + // A negative length cannot reach left outside the array twice + $length = 0; + } + + if ($length === 0 || ($offset < 0 && $length < 0 && $offset - $length >= 0)) { + // 0 / 0, 3 / 0 or e.g. -3 / -3 or -3 / -4 and so on never extract anything + return new self([], []); + } + + if ($length < 0) { + // Negative lengths prevent access to the most right n elements + return $this->removeLastElements($length * -1) + ->sliceArray($offsetType, new NullType(), $preserveKeys); + } + + if ($offset < 0) { + /* + * Transforms the problem with the negative offset in one with a positive offset using array reversion. + * The reason is belows handling of optional keys which works only from left to right. + * + * e.g. + * array{a: 0, b: 1, c: 2, d: 3, e: 4} + * with offset -4 and length 2 (which would be sliced to array{b: 1, c: 2}) + * + * is transformed via reversion to + * + * array{e: 4, d: 3, c: 2, b: 1, a: 0} + * with offset 2 and length 2 (which will be sliced to array{c: 2, b: 1} and then reversed again) + */ + $offset *= -1; + $reversedLength = min($length, $offset); + $reversedOffset = $offset - $reversedLength; + return $this->reverseArray(TrinaryLogic::createYes()) + ->sliceArray(new ConstantIntegerType($reversedOffset), new ConstantIntegerType($reversedLength), $preserveKeys) + ->reverseArray(TrinaryLogic::createYes()); + } + + if ($offset > 0) { + return $this->removeFirstElements($offset, false) + ->sliceArray(new ConstantIntegerType(0), $lengthType, $preserveKeys); + } + + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $nonOptionalElementsCount = 0; + $hasOptional = false; + for ($i = 0; $nonOptionalElementsCount < $length && $i < $keyTypesCount; $i++) { + $isOptional = $this->isOptionalKey($i); + if (!$isOptional) { + $nonOptionalElementsCount++; + } else { + $hasOptional = true; + } + + $isLastElement = $nonOptionalElementsCount >= $length || $i + 1 >= $keyTypesCount; + if ($isLastElement && $length < $keyTypesCount && $hasOptional) { + // If the slice is not full yet, but has at least one optional key + // the last non-optional element is going to be optional. + // Otherwise, it would not fit into the slice if previous non-optional keys are there. + $isOptional = true; + } + + $offsetType = $preserveKeys->yes() || $this->keyTypes[$i]->isInteger()->no() + ? $this->keyTypes[$i] + : null; + + $builder->setOffsetValueType($offsetType, $this->valueTypes[$i], $isOptional); } - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return $builder->getArray(); } - /** - * @return self - */ - public function getKeysArray(): ArrayType + public function isIterableAtLeastOnce(): TrinaryLogic { - $keyTypes = []; - $valueTypes = []; - $optionalKeys = []; - $autoIndex = 0; + $keysCount = count($this->keyTypes); + if ($keysCount === 0) { + return TrinaryLogic::createNo(); + } - foreach ($this->keyTypes as $i => $keyType) { - $keyTypes[] = new ConstantIntegerType($i); - $valueTypes[] = $keyType; - $autoIndex++; + $optionalKeysCount = count($this->optionalKeys); + if ($optionalKeysCount < $keysCount) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); + } + + public function getArraySize(): Type + { + $optionalKeysCount = count($this->optionalKeys); + $totalKeysCount = count($this->getKeyTypes()); + if ($optionalKeysCount === 0) { + return new ConstantIntegerType($totalKeysCount); + } + + return IntegerRangeType::fromInterval($totalKeysCount - $optionalKeysCount, $totalKeysCount); + } + + public function getFirstIterableKeyType(): Type + { + $keyTypes = []; + foreach ($this->keyTypes as $i => $keyType) { + $keyTypes[] = $keyType; if (!$this->isOptionalKey($i)) { - continue; + break; } - - $optionalKeys[] = $i; } - return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys); + return TypeCombinator::union(...$keyTypes); } - /** - * @return self - */ - public function getValuesArray(): ArrayType + public function getLastIterableKeyType(): Type { $keyTypes = []; - $valueTypes = []; - $optionalKeys = []; - $autoIndex = 0; + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $keyTypes[] = $this->keyTypes[$i]; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$keyTypes); + } + public function getFirstIterableValueType(): Type + { + $valueTypes = []; foreach ($this->valueTypes as $i => $valueType) { - $keyTypes[] = new ConstantIntegerType($i); $valueTypes[] = $valueType; - $autoIndex++; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$valueTypes); + } + public function getLastIterableValueType(): Type + { + $valueTypes = []; + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $valueTypes[] = $this->valueTypes[$i]; if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$valueTypes); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isList(): TrinaryLogic + { + return $this->isList; + } + + /** @param positive-int $length */ + private function removeLastElements(int $length): self + { + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + return $this; + } + + $keyTypes = $this->keyTypes; + $valueTypes = $this->valueTypes; + $optionalKeys = $this->optionalKeys; + $nextAutoindexes = $this->nextAutoIndexes; + + $optionalKeysRemoved = 0; + $newLength = $keyTypesCount - $length; + for ($i = $keyTypesCount - 1; $i >= 0; $i--) { + $isOptional = $this->isOptionalKey($i); + + if ($i >= $newLength) { + if ($isOptional) { + $optionalKeysRemoved++; + foreach ($optionalKeys as $key => $value) { + if ($value === $i) { + unset($optionalKeys[$key]); + break; + } + } + } + + $removedKeyType = array_pop($keyTypes); + array_pop($valueTypes); + $nextAutoindexes = $removedKeyType instanceof ConstantIntegerType + ? [$removedKeyType->getValue()] + : $this->nextAutoIndexes; + continue; + } + + if ($isOptional || $optionalKeysRemoved <= 0) { continue; } $optionalKeys[] = $i; + $optionalKeysRemoved--; + } + + return new self( + $keyTypes, + $valueTypes, + $nextAutoindexes, + array_values($optionalKeys), + $this->isList, + ); + } + + /** @param positive-int $length */ + private function removeFirstElements(int $length, bool $reindex = true): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $optionalKeysIgnored = 0; + foreach ($this->keyTypes as $i => $keyType) { + $isOptional = $this->isOptionalKey($i); + if ($i <= $length - 1) { + if ($isOptional) { + $optionalKeysIgnored++; + } + continue; + } + + if (!$isOptional && $optionalKeysIgnored > 0) { + $isOptional = true; + $optionalKeysIgnored--; + } + + $valueType = $this->valueTypes[$i]; + if ($reindex && $keyType instanceof ConstantIntegerType) { + $keyType = null; + } + + $builder->setOffsetValueType($keyType, $valueType, $isOptional); } - return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys); + return $builder->getArray(); + } + + public function toBoolean(): BooleanType + { + return $this->getArraySize()->toBoolean(); + } + + public function toInteger(): Type + { + return $this->toBoolean()->toInteger(); + } + + public function toFloat(): Type + { + return $this->toBoolean()->toFloat(); } - public function count(): Type + 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( + $this->getIterableKeyType()->generalize($precision), + $this->getIterableValueType()->generalize($precision), + ); + + $keyTypesCount = count($this->keyTypes); $optionalKeysCount = count($this->optionalKeys); - $totalKeysCount = count($this->getKeyTypes()); - if ($optionalKeysCount === 0) { - return new ConstantIntegerType($totalKeysCount); + + $accessoryTypes = []; + if ($precision->isMoreSpecific() && ($keyTypesCount - $optionalKeysCount) < 32) { + foreach ($this->keyTypes as $i => $keyType) { + if ($this->isOptionalKey($i)) { + continue; + } + + $accessoryTypes[] = new HasOffsetValueType($keyType, $this->valueTypes[$i]->generalize($precision)); + } + } elseif ($keyTypesCount > $optionalKeysCount) { + $accessoryTypes[] = new NonEmptyArrayType(); } - return IntegerRangeType::fromInterval($totalKeysCount - $optionalKeysCount, $totalKeysCount); + if ($this->isList()->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } + + if (count($accessoryTypes) > 0) { + return TypeCombinator::intersect($arrayType, ...$accessoryTypes); + } + + return $arrayType; + } + + public function generalizeValues(): self + { + $valueTypes = []; + foreach ($this->valueTypes as $valueType) { + $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific()); + } + + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); + } + + public function getKeysArray(): self + { + return $this->getKeysOrValuesArray($this->keyTypes); + } + + public function getValuesArray(): self + { + return $this->getKeysOrValuesArray($this->valueTypes); + } + + /** + * @param array $types + */ + private function getKeysOrValuesArray(array $types): self + { + $count = count($types); + $autoIndexes = range($count - count($this->optionalKeys), $count); + assert($autoIndexes !== []); + + if ($this->isList->yes()) { + // Optimized version for lists: Assume that if a later key exists, then earlier keys also exist. + $keyTypes = array_map( + static fn (int $i): ConstantIntegerType => new ConstantIntegerType($i), + array_keys($types), + ); + return new self($keyTypes, $types, $autoIndexes, $this->optionalKeys, TrinaryLogic::createYes()); + } + + $keyTypes = []; + $valueTypes = []; + $optionalKeys = []; + $maxIndex = 0; + + foreach ($types as $i => $type) { + $keyTypes[] = new ConstantIntegerType($i); + + if ($this->isOptionalKey($maxIndex)) { + // move $maxIndex to next non-optional key + do { + $maxIndex++; + } while ($maxIndex < $count && $this->isOptionalKey($maxIndex)); + } + + if ($i === $maxIndex) { + $valueTypes[] = $type; + } else { + $valueTypes[] = TypeCombinator::union(...array_slice($types, $i, $maxIndex - $i + 1)); + if ($maxIndex >= $count) { + $optionalKeys[] = $i; + } + } + $maxIndex++; + } + + return new self($keyTypes, $valueTypes, $autoIndexes, $optionalKeys, TrinaryLogic::createYes()); } public function describe(VerbosityLevel $level): string @@ -733,15 +1338,16 @@ public function describe(VerbosityLevel $level): string $keyDescription = $keyType->getValue(); if (is_string($keyDescription)) { - if (strpos($keyDescription, '"') !== false) { + if (str_contains($keyDescription, '"')) { $keyDescription = sprintf('\'%s\'', $keyDescription); - } elseif (strpos($keyDescription, '\'') !== false) { + } elseif (str_contains($keyDescription, '\'')) { $keyDescription = sprintf('"%s"', $keyDescription); } } - $items[] = sprintf('%s%s: %s', $keyDescription, $isOptional ? '?' : '', $valueType->describe($level)); - $values[] = $valueType->describe($level); + $valueTypeDescription = $valueType->describe($level); + $items[] = sprintf('%s%s: %s', $keyDescription, $isOptional ? '?' : '', $valueTypeDescription); + $values[] = $valueTypeDescription; } $append = ''; @@ -758,7 +1364,7 @@ public function describe(VerbosityLevel $level): string ); }; return $level->handle( - fn (): string => parent::describe($level), + fn (): string => $this->isIterableAtLeastOnce()->no() ? 'array' : sprintf('array<%s, %s>', $this->getIterableKeyType()->describe($level), $this->getIterableValueType()->describe($level)), static fn (): string => $describeValue(true), static fn (): string => $describeValue(false), ); @@ -784,12 +1390,19 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $typeMap; } - return parent::inferTemplateTypes($receivedType); + if ($receivedType->isArray()->yes()) { + $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); + $itemTypeMap = $this->getIterableValueType()->inferTemplateTypes($receivedType->getIterableValueType()); + + return $keyTypeMap->union($itemTypeMap); + } + + return TemplateTypeMap::createEmpty(); } public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { - $variance = $positionVariance->compose(TemplateTypeVariance::createInvariant()); + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); $references = []; foreach ($this->keyTypes as $type) { @@ -807,6 +1420,27 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc return $references; } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove->isConstantArray()->yes() && $typeToRemove->isIterableAtLeastOnce()->no()) { + return TypeCombinator::intersect($this, new NonEmptyArrayType()); + } + + if ($typeToRemove instanceof NonEmptyArrayType) { + return new ConstantArrayType([], []); + } + + if ($typeToRemove instanceof HasOffsetType) { + return $this->unsetOffset($typeToRemove->getOffsetType()); + } + + if ($typeToRemove instanceof HasOffsetValueType) { + return $this->unsetOffset($typeToRemove->getOffsetType()); + } + + return null; + } + public function traverse(callable $cb): Type { $valueTypes = []; @@ -825,37 +1459,90 @@ public function traverse(callable $cb): Type return $this; } - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } - public function isKeysSupersetOf(self $otherArray): bool + public function traverseSimultaneously(Type $right, callable $cb): Type { - if (count($this->keyTypes) === 0) { - return count($otherArray->keyTypes) === 0; + if (!$right->isArray()->yes()) { + return $this; } - if (count($otherArray->keyTypes) === 0) { + $valueTypes = []; + + $stillOriginal = true; + foreach ($this->valueTypes as $i => $valueType) { + $keyType = $this->keyTypes[$i]; + $transformedValueType = $cb($valueType, $right->getOffsetValueType($keyType)); + if ($transformedValueType !== $valueType) { + $stillOriginal = false; + } + + $valueTypes[] = $transformedValueType; + } + + if ($stillOriginal) { + return $this; + } + + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); + } + + public function isKeysSupersetOf(self $otherArray): bool + { + $keyTypesCount = count($this->keyTypes); + $otherKeyTypesCount = count($otherArray->keyTypes); + + if ($keyTypesCount < $otherKeyTypesCount) { return false; } - $otherKeys = $otherArray->keyTypes; - foreach ($this->keyTypes as $keyType) { - foreach ($otherArray->keyTypes as $j => $otherKeyType) { - if (!$keyType->equals($otherKeyType)) { - continue; - } + if ($otherKeyTypesCount === 0) { + return $keyTypesCount === 0; + } + + $failOnDifferentValueType = $keyTypesCount !== $otherKeyTypesCount || $keyTypesCount < 2; + + $keyTypes = $this->keyTypes; + + foreach ($otherArray->keyTypes as $j => $keyType) { + $i = self::findKeyIndex($keyType, $keyTypes); + if ($i === null) { + return false; + } + + unset($keyTypes[$i]); + + $valueType = $this->valueTypes[$i]; + $otherValueType = $otherArray->valueTypes[$j]; + if (!$otherValueType->isSuperTypeOf($valueType)->no()) { + continue; + } - unset($otherKeys[$j]); - continue 2; + if ($failOnDifferentValueType) { + return false; } + $failOnDifferentValueType = true; } - return count($otherKeys) === 0; + $requiredKeyCount = 0; + foreach (array_keys($keyTypes) as $i) { + if ($this->isOptionalKey($i)) { + continue; + } + + $requiredKeyCount++; + if ($requiredKeyCount > 1) { + return false; + } + } + + return true; } public function mergeWith(self $otherArray): self { - // only call this after verifying isKeysSupersetOf + // only call this after verifying isKeysSupersetOf, or if losing tagged unions is not an issue $valueTypes = $this->valueTypes; $optionalKeys = $this->optionalKeys; foreach ($this->keyTypes as $i => $keyType) { @@ -873,7 +1560,10 @@ public function mergeWith(self $otherArray): self $optionalKeys = array_values(array_unique($optionalKeys)); - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $optionalKeys); + $nextAutoIndexes = array_values(array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes))); + sort($nextAutoIndexes); + + return new self($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList->and($otherArray->isList)); } /** @@ -881,7 +1571,16 @@ public function mergeWith(self $otherArray): self */ private function getKeyIndex($otherKeyType): ?int { - foreach ($this->keyTypes as $i => $keyType) { + return self::findKeyIndex($otherKeyType, $this->keyTypes); + } + + /** + * @param ConstantIntegerType|ConstantStringType $otherKeyType + * @param array $keyTypes + */ + private static function findKeyIndex($otherKeyType, array $keyTypes): ?int + { + foreach ($keyTypes as $i => $keyType) { if ($keyType->equals($otherKeyType)) { return $i; } @@ -892,7 +1591,7 @@ private function getKeyIndex($otherKeyType): ?int public function makeOffsetRequired(Type $offsetType): self { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); $optionalKeys = $this->optionalKeys; foreach ($this->keyTypes as $i => $keyType) { if (!$keyType->equals($offsetType)) { @@ -902,7 +1601,7 @@ public function makeOffsetRequired(Type $offsetType): self foreach ($optionalKeys as $j => $key) { if ($i === $key) { unset($optionalKeys[$j]); - return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndex, array_values($optionalKeys)); + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys), $this->isList); } } @@ -912,12 +1611,93 @@ public function makeOffsetRequired(Type $offsetType): self return $this; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function toPhpDocNode(): TypeNode + { + $items = []; + $values = []; + $exportValuesOnly = true; + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->getValue() !== $i) { + $exportValuesOnly = false; + } + $keyPhpDocNode = $keyType->toPhpDocNode(); + if (!$keyPhpDocNode instanceof ConstTypeNode) { + continue; + } + $valueType = $this->valueTypes[$i]; + + /** @var ConstExprStringNode|ConstExprIntegerNode $keyNode */ + $keyNode = $keyPhpDocNode->constExpr; + if ($keyNode instanceof ConstExprStringNode) { + $value = $keyNode->value; + if (self::isValidIdentifier($value)) { + $keyNode = new IdentifierTypeNode($value); + } + } + + $isOptional = $this->isOptionalKey($i); + if ($isOptional) { + $exportValuesOnly = false; + } + $items[] = new ArrayShapeItemNode( + $keyNode, + $isOptional, + $valueType->toPhpDocNode(), + ); + $values[] = new ArrayShapeItemNode( + null, + $isOptional, + $valueType->toPhpDocNode(), + ); + } + + return ArrayShapeNode::createSealed($exportValuesOnly ? $values : $items); + } + + public static function isValidIdentifier(string $value): bool { - return new self($properties['keyTypes'], $properties['valueTypes'], $properties['nextAutoIndex'], $properties['optionalKeys'] ?? []); + $result = Strings::match($value, '~^(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++$~si'); + + return $result !== null; + } + + public function getFiniteTypes(): array + { + $arraysArraysForCombinations = []; + $count = 0; + foreach ($this->getAllArrays() as $array) { + $values = $array->getValueTypes(); + $arraysForCombinations = []; + $combinationCount = 1; + foreach ($values as $valueType) { + $finiteTypes = $valueType->getFiniteTypes(); + if ($finiteTypes === []) { + return []; + } + $arraysForCombinations[] = $finiteTypes; + $combinationCount *= count($finiteTypes); + } + $arraysArraysForCombinations[] = $arraysForCombinations; + $count += $combinationCount; + } + + if ($count > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + $finiteTypes = []; + foreach ($arraysArraysForCombinations as $arraysForCombinations) { + $combinations = CombinationsHelper::combinations($arraysForCombinations); + foreach ($combinations as $combination) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($combination as $i => $v) { + $builder->setOffsetValueType($this->keyTypes[$i], $v); + } + $finiteTypes[] = $builder->getArray(); + } + } + + return $finiteTypes; } } diff --git a/src/Type/Constant/ConstantArrayTypeAndMethod.php b/src/Type/Constant/ConstantArrayTypeAndMethod.php index e5bd1caf7b..07f4156550 100644 --- a/src/Type/Constant/ConstantArrayTypeAndMethod.php +++ b/src/Type/Constant/ConstantArrayTypeAndMethod.php @@ -6,8 +6,10 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -/** @api */ -class ConstantArrayTypeAndMethod +/** + * @api + */ +final class ConstantArrayTypeAndMethod { private function __construct( diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 6af191026b..a639bf6c0e 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -3,43 +3,56 @@ namespace PHPStan\Type\Constant; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function array_filter; +use function array_map; +use function array_unique; use function array_values; use function count; +use function in_array; use function is_float; use function max; +use function min; use function range; -/** @api */ -class ConstantArrayTypeBuilder +/** + * @api + */ +final class ConstantArrayTypeBuilder { public const ARRAY_COUNT_LIMIT = 256; private bool $degradeToGeneralArray = false; + private bool $oversized = false; + /** * @param array $keyTypes * @param array $valueTypes + * @param non-empty-list $nextAutoIndexes * @param array $optionalKeys */ private function __construct( private array $keyTypes, private array $valueTypes, - private int $nextAutoIndex, + private array $nextAutoIndexes, private array $optionalKeys, + private TrinaryLogic $isList, ) { } public static function createEmpty(): self { - return new self([], [], 0, []); + return new self([], [], [0], [], TrinaryLogic::createYes()); } public static function createFromConstantArray(ConstantArrayType $startArrayType): self @@ -47,12 +60,13 @@ public static function createFromConstantArray(ConstantArrayType $startArrayType $builder = new self( $startArrayType->getKeyTypes(), $startArrayType->getValueTypes(), - $startArrayType->getNextAutoIndex(), + $startArrayType->getNextAutoIndexes(), $startArrayType->getOptionalKeys(), + $startArrayType->isList(), ); if (count($startArrayType->getKeyTypes()) > self::ARRAY_COUNT_LIMIT) { - $builder->degradeToGeneralArray(); + $builder->degradeToGeneralArray(true); } return $builder; @@ -60,46 +74,139 @@ public static function createFromConstantArray(ConstantArrayType $startArrayType public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $optional = false): void { - if ($offsetType === null) { - $offsetType = new ConstantIntegerType($this->nextAutoIndex); - } else { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + if ($offsetType !== null) { + $offsetType = $offsetType->toArrayKey(); } if (!$this->degradeToGeneralArray) { + if ($offsetType === null) { + $newAutoIndexes = $optional ? $this->nextAutoIndexes : []; + $hasOptional = false; + foreach ($this->keyTypes as $i => $keyType) { + if (!$keyType instanceof ConstantIntegerType) { + continue; + } + + if (!in_array($keyType->getValue(), $this->nextAutoIndexes, true)) { + continue; + } + + $this->valueTypes[$i] = TypeCombinator::union($this->valueTypes[$i], $valueType); + + if (!$hasOptional && !$optional) { + $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i)); + } + + /** @var int|float $newAutoIndex */ + $newAutoIndex = $keyType->getValue() + 1; + if (is_float($newAutoIndex)) { + $newAutoIndex = $keyType->getValue(); + } + + $newAutoIndexes[] = $newAutoIndex; + $hasOptional = true; + } + + $max = max($this->nextAutoIndexes); + + $this->keyTypes[] = new ConstantIntegerType($max); + $this->valueTypes[] = $valueType; + + /** @var int|float $newAutoIndex */ + $newAutoIndex = $max + 1; + if (is_float($newAutoIndex)) { + $newAutoIndex = $max; + } + + $newAutoIndexes[] = $newAutoIndex; + $this->nextAutoIndexes = array_values(array_unique($newAutoIndexes)); + + if ($optional || $hasOptional) { + $this->optionalKeys[] = count($this->keyTypes) - 1; + } + + if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { + $this->degradeToGeneralArray = true; + $this->oversized = true; + } + + return; + } + 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; + if ($keyType->getValue() !== $offsetType->getValue()) { + continue; + } + + if ($optional) { + $valueType = TypeCombinator::union($valueType, $this->valueTypes[$i]); + } + + $this->valueTypes[$i] = $valueType; + + if (!$optional) { $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i)); - return; + if ($keyType instanceof ConstantIntegerType) { + $nextAutoIndexes = array_values(array_filter($this->nextAutoIndexes, static fn (int $index) => $index > $keyType->getValue())); + if (count($nextAutoIndexes) === 0) { + throw new ShouldNotHappenException(); + } + $this->nextAutoIndexes = $nextAutoIndexes; + } } + return; } $this->keyTypes[] = $offsetType; $this->valueTypes[] = $valueType; - if ($optional) { - $this->optionalKeys[] = count($this->keyTypes) - 1; + if ($offsetType instanceof ConstantIntegerType) { + $min = min($this->nextAutoIndexes); + $max = max($this->nextAutoIndexes); + $offsetValue = $offsetType->getValue(); + if ($offsetValue >= 0) { + if ($offsetValue > $min) { + if ($offsetValue <= $max) { + $this->isList = $this->isList->and(TrinaryLogic::createMaybe()); + } else { + $this->isList = TrinaryLogic::createNo(); + } + } + } else { + $this->isList = TrinaryLogic::createNo(); + } + + if ($offsetValue >= $max) { + /** @var int|float $newAutoIndex */ + $newAutoIndex = $offsetValue + 1; + if (is_float($newAutoIndex)) { + $newAutoIndex = $max; + } + if (!$optional) { + $this->nextAutoIndexes = [$newAutoIndex]; + } else { + $this->nextAutoIndexes[] = $newAutoIndex; + } + } + } else { + $this->isList = TrinaryLogic::createNo(); } - /** @var int|float $newNextAutoIndex */ - $newNextAutoIndex = $offsetType instanceof ConstantIntegerType - ? max($this->nextAutoIndex, $offsetType->getValue() + 1) - : $this->nextAutoIndex; - if (!is_float($newNextAutoIndex)) { - $this->nextAutoIndex = $newNextAutoIndex; + if ($optional) { + $this->optionalKeys[] = count($this->keyTypes) - 1; } if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { $this->degradeToGeneralArray = true; + $this->oversized = true; } return; } - $scalarTypes = TypeUtils::getConstantScalars($offsetType); + $scalarTypes = $offsetType->getConstantScalarTypes(); if (count($scalarTypes) === 0) { $integerRanges = TypeUtils::getIntegerRanges($offsetType); if (count($integerRanges) > 0) { @@ -127,7 +234,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt $match = true; $valueTypes = $this->valueTypes; foreach ($scalarTypes as $scalarType) { - $scalarOffsetType = ArrayType::castToArrayKeyType($scalarType); + $scalarOffsetType = $scalarType->toArrayKey(); if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) { throw new ShouldNotHappenException(); } @@ -155,6 +262,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt return; } } + + $this->isList = TrinaryLogic::createNo(); + } + + if ($offsetType === null) { + $offsetType = TypeCombinator::union(...array_map(static fn (int $index) => new ConstantIntegerType($index), $this->nextAutoIndexes)); + } else { + $this->isList = TrinaryLogic::createNo(); } $this->keyTypes[] = $offsetType; @@ -165,9 +280,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt $this->degradeToGeneralArray = true; } - public function degradeToGeneralArray(): void + public function degradeToGeneralArray(bool $oversized = false): void { $this->degradeToGeneralArray = true; + $this->oversized = $this->oversized || $oversized; } public function getArray(): Type @@ -180,7 +296,7 @@ public function getArray(): Type if (!$this->degradeToGeneralArray) { /** @var array $keyTypes */ $keyTypes = $this->keyTypes; - return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } $array = new ArrayType( @@ -189,10 +305,23 @@ public function getArray(): Type ); if (count($this->optionalKeys) < $keyTypesCount) { - return TypeCombinator::intersect($array, new NonEmptyArrayType()); + $array = TypeCombinator::intersect($array, new NonEmptyArrayType()); + } + + if ($this->oversized) { + $array = TypeCombinator::intersect($array, new OversizedArrayType()); + } + + if ($this->isList->yes()) { + $array = TypeCombinator::intersect($array, new AccessoryArrayListType()); } return $array; } + public function isList(): bool + { + return $this->isList->yes(); + } + } diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 0317f30667..282b005c15 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -2,20 +2,28 @@ namespace PHPStan\Type\Constant; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\TrinaryLogic; use PHPStan\Type\BooleanType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; /** @api */ class ConstantBooleanType extends BooleanType implements ConstantScalarType { - use ConstantScalarTypeTrait; + use ConstantScalarTypeTrait { + looseCompare as private scalarLooseCompare; + } /** @api */ public function __construct(private bool $value) @@ -33,7 +41,7 @@ public function describe(VerbosityLevel $level): string return $this->value ? 'true' : 'false'; } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { if ($this->value) { return StaticTypeFactory::falsey(); @@ -41,7 +49,7 @@ public function getSmallerType(): Type return new NeverType(); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { if ($this->value) { return new MixedType(); @@ -49,7 +57,7 @@ public function getSmallerOrEqualType(): Type return StaticTypeFactory::falsey(); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { if ($this->value) { return new NeverType(); @@ -57,7 +65,7 @@ public function getGreaterType(): Type return StaticTypeFactory::truthy(); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { if ($this->value) { return StaticTypeFactory::truthy(); @@ -75,6 +83,11 @@ public function toNumber(): Type return new ConstantIntegerType((int) $this->value); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return new ConstantStringType((string) $this->value); @@ -90,12 +103,47 @@ public function toFloat(): Type return new ConstantFloatType((float) $this->value); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function toArrayKey(): Type + { + return new ConstantIntegerType((int) $this->value); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this); + } + + return $this; + } + + public function isTrue(): TrinaryLogic { - return new self($properties['value']); + return TrinaryLogic::createFromBoolean($this->value === true); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->value === false); + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new BooleanType(); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->value ? 'true' : 'false'); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isObject()->yes()) { + return $this; + } + + return $this->scalarLooseCompare($type, $phpVersion); } } diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 2953a60d0a..0ac763af76 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -2,15 +2,22 @@ namespace PHPStan\Type\Constant; -use PHPStan\TrinaryLogic; -use PHPStan\Type\CompoundType; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\FloatType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function strpos; +use function abs; +use function ini_get; +use function ini_set; +use function is_finite; +use function is_nan; +use function str_contains; /** @api */ class ConstantFloatType extends FloatType implements ConstantScalarType @@ -31,44 +38,33 @@ public function getValue(): float return $this->value; } - public function describe(VerbosityLevel $level): string + public function equals(Type $type): bool { - return $level->handle( - static fn (): string => 'float', - function (): string { - $formatted = (string) $this->value; - if (strpos($formatted, '.') === false) { - $formatted .= '.0'; - } - - return $formatted; - }, - ); + return $type instanceof self && ($this->value === $type->value || is_nan($this->value) && is_nan($type->value)); } - public function isSuperTypeOf(Type $type): TrinaryLogic + private function castFloatToString(float $value): string { - if ($type instanceof self) { - if (!$this->equals($type)) { - if ($this->describe(VerbosityLevel::value()) === $type->describe(VerbosityLevel::value())) { - return TrinaryLogic::createMaybe(); - } - - return TrinaryLogic::createNo(); + $precisionBackup = ini_get('precision'); + ini_set('precision', '-1'); + try { + $valueStr = (string) $value; + if (is_finite($value) && !str_contains($valueStr, '.')) { + $valueStr .= '.0'; } - return TrinaryLogic::createYes(); - } - - if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); - } - - if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); + return $valueStr; + } finally { + ini_set('precision', $precisionBackup); } + } - return TrinaryLogic::createNo(); + public function describe(VerbosityLevel $level): string + { + return $level->handle( + static fn (): string => 'float', + fn (): string => $this->castFloatToString($this->value), + ); } public function toString(): Type @@ -81,12 +77,27 @@ public function toInteger(): Type return new ConstantIntegerType((int) $this->value); } + public function toAbsoluteNumber(): Type + { + return new self(abs($this->value)); + } + + public function toArrayKey(): Type + { + return new ConstantIntegerType((int) $this->value); + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new FloatType(); + } + /** - * @param mixed[] $properties + * @return ConstTypeNode */ - public static function __set_state(array $properties): Type + public function toPhpDocNode(): TypeNode { - return new self($properties['value']); + return new ConstTypeNode(new ConstExprFloatNode($this->castFloatToString($this->value))); } } diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 9fb760b96e..6b482c62e6 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -2,15 +2,21 @@ namespace PHPStan\Type\Constant; -use PHPStan\TrinaryLogic; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function abs; use function sprintf; /** @api */ @@ -32,32 +38,31 @@ public function getValue(): int return $this->value; } - - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return $this->value === $type->value ? TrinaryLogic::createYes() : TrinaryLogic::createNo(); + return $this->value === $type->value ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createNo(); } if ($type instanceof IntegerRangeType) { $min = $type->getMin(); $max = $type->getMax(); if (($min === null || $min <= $this->value) && ($max === null || $this->value <= $max)) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function describe(VerbosityLevel $level): string @@ -73,17 +78,41 @@ public function toFloat(): Type return new ConstantFloatType($this->value); } + public function toAbsoluteNumber(): Type + { + return new self(abs($this->value)); + } + public function toString(): Type { return new ConstantStringType((string) $this->value); } + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean()); + } + + return TypeCombinator::union($this, $this->toFloat()); + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new IntegerType(); + } + /** - * @param mixed[] $properties + * @return ConstTypeNode */ - public static function __set_state(array $properties): Type + public function toPhpDocNode(): TypeNode { - return new self($properties['value']); + return new ConstTypeNode(new ConstExprIntegerNode((string) $this->value)); } } diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 1407a7f66e..e4c34e609f 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -5,15 +5,26 @@ use Nette\Utils\RegexpException; use Nette\Utils\Strings; use PhpParser\Node\Name; +use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\FunctionCallableVariant; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\InaccessibleMethod; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; @@ -23,6 +34,7 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; @@ -32,11 +44,17 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function addcslashes; +use function in_array; use function is_float; +use function is_int; use function is_numeric; +use function key; use function strlen; +use function strtolower; +use function strtoupper; use function substr; -use function var_export; +use function substr_count; /** @api */ class ConstantStringType extends StringType implements ConstantScalarType @@ -47,6 +65,10 @@ class ConstantStringType extends StringType implements ConstantScalarType use ConstantScalarTypeTrait; use ConstantScalarToBooleanTrait; + private ?ObjectType $objectType = null; + + private ?Type $arrayKeyType = null; + /** @api */ public function __construct(private string $value, private bool $isClassString = false) { @@ -58,15 +80,34 @@ public function getValue(): string return $this->value; } - public function isClassString(): bool + public function getConstantStrings(): array + { + return [$this]; + } + + public function isClassString(): TrinaryLogic { if ($this->isClassString) { - return true; + return TrinaryLogic::createYes(); } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return $reflectionProvider->hasClass($this->value); + return TrinaryLogic::createFromBoolean($reflectionProvider->hasClass($this->value)); + } + + public function getClassStringObjectType(): Type + { + if ($this->isClassString()->yes()) { + return new ObjectType($this->value); + } + + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->getClassStringObjectType(); } public function describe(VerbosityLevel $level): string @@ -74,31 +115,38 @@ public function describe(VerbosityLevel $level): string return $level->handle( static fn (): string => 'string', function (): string { - if ($this->isClassString) { - return var_export($this->value, true); + $value = $this->value; + + if (!$this->isClassString) { + try { + $value = Strings::truncate($value, self::DESCRIBE_LIMIT); + } catch (RegexpException) { + $value = substr($value, 0, self::DESCRIBE_LIMIT) . "\u{2026}"; + } } - try { - $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, - ); + return self::export($value); }, - fn (): string => var_export($this->value, true), + fn (): string => self::export($this->value), ); } - public function isSuperTypeOf(Type $type): TrinaryLogic + private function export(string $value): string + { + $escapedValue = addcslashes($value, "\0..\37"); + if ($escapedValue !== $value) { + return '"' . addcslashes($value, "\0..\37\\\"") . '"'; + } + + return "'" . addcslashes($value, '\\\'') . "'"; + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof GenericClassStringType) { $genericType = $type->getGenericType(); if ($genericType instanceof MixedType) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($genericType instanceof StaticType) { $genericType = $genericType->getStaticObjectType(); @@ -106,7 +154,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic // We are transforming constant class-string to ObjectType. But we need to filter out // an uncertainty originating in possible ObjectType's class subtypes. - $objectType = new ObjectType($this->getValue()); + $objectType = $this->getObjectType(); // Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType // uncertainty into account. @@ -118,27 +166,27 @@ public function isSuperTypeOf(Type $type): TrinaryLogic // Explicitly handle the uncertainty for Yes & Maybe. if ($isSuperType->yes()) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if ($type instanceof ClassStringType) { - return $this->isClassString() ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(); + return $this->isClassString()->yes() ? IsSuperTypeOfResult::createMaybe() : IsSuperTypeOfResult::createNo(); } if ($type instanceof self) { - return $this->value === $type->value ? TrinaryLogic::createYes() : TrinaryLogic::createNo(); + return $this->value === $type->value ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createNo(); } if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function isCallable(): TrinaryLogic @@ -161,12 +209,21 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe(); } + $phpVersion = PhpVersionStaticAccessor::getInstance(); $classRef = $reflectionProvider->getClass($matches[1]); if ($classRef->hasMethod($matches[2])) { + $method = $classRef->getMethod($matches[2], new OutOfClassScope()); + if ( + !$phpVersion->supportsCallableInstanceMethods() + && !$method->isStatic() + ) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createYes(); } - if (!$classRef->getNativeReflection()->isFinal()) { + if (!$classRef->isFinalByKeyword()) { return TrinaryLogic::createMaybe(); } @@ -176,17 +233,19 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createNo(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { + if ($this->value === '') { + return []; + } + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); // 'my_function' $functionName = new Name($this->value); if ($reflectionProvider->hasFunction($functionName, null)) { - return $reflectionProvider->getFunction($functionName, null)->getVariants(); + $function = $reflectionProvider->getFunction($functionName, null); + return FunctionCallableVariant::createFromVariants($function, $function->getVariants()); } // 'MyClass::myStaticFunction' @@ -203,10 +262,10 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return [new InaccessibleMethod($method)]; } - return $method->getVariants(); + return FunctionCallableVariant::createFromVariants($method, $method->getVariants()); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } } @@ -217,7 +276,6 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) public function toNumber(): Type { if (is_numeric($this->value)) { - /** @var mixed $value */ $value = $this->value; $value = +$value; if (is_float($value)) { @@ -230,6 +288,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new ConstantIntegerType((int) $this->value); @@ -240,6 +303,17 @@ public function toFloat(): Type return new ConstantFloatType((float) $this->value); } + public function toArrayKey(): Type + { + if ($this->arrayKeyType !== null) { + return $this->arrayKeyType; + } + + /** @var int|string $offsetValue */ + $offsetValue = key([$this->value => null]); + return $this->arrayKeyType = is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -255,17 +329,32 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->getValue() !== ''); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(!in_array($this->getValue(), ['', '0'], true)); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(strtolower($this->value) === $this->value); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(strtoupper($this->value) === $this->value); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - if ($offsetType instanceof ConstantIntegerType) { - return TrinaryLogic::createFromBoolean( - $offsetType->getValue() < strlen($this->value), - ); + if ($offsetType->isInteger()->yes()) { + $strlen = strlen($this->value); + $strLenType = IntegerRangeType::fromInterval(-$strlen, $strlen - 1); + return $strLenType->isSuperTypeOf($offsetType)->result; } return parent::hasOffsetValueType($offsetType); @@ -273,12 +362,35 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - if ($offsetType instanceof ConstantIntegerType) { - if ($offsetType->getValue() < strlen($this->value)) { - return new self($this->value[$offsetType->getValue()]); + if ($offsetType->isInteger()->yes()) { + $strlen = strlen($this->value); + $strLenType = IntegerRangeType::fromInterval(-$strlen, $strlen - 1); + + if ($offsetType instanceof ConstantIntegerType) { + if ($strLenType->isSuperTypeOf($offsetType)->yes()) { + return new self($this->value[$offsetType->getValue()]); + } + + return new ErrorType(); } - return new ErrorType(); + $intersected = TypeCombinator::intersect($strLenType, $offsetType); + if ($intersected instanceof IntegerRangeType) { + $finiteTypes = $intersected->getFiniteTypes(); + if ($finiteTypes === []) { + return parent::getOffsetValueType($offsetType); + } + + $chars = []; + foreach ($finiteTypes as $constantInteger) { + $chars[] = new self($this->value[$constantInteger->getValue()]); + } + if (!$strLenType->isSuperTypeOf($offsetType)->yes()) { + $chars[] = new self(''); + } + + return TypeCombinator::union(...$chars); + } } return parent::getOffsetValueType($offsetType); @@ -311,6 +423,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return parent::setOffsetValueType($offsetType, $valueType); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return parent::setOffsetValueType($offsetType, $valueType); + } + public function append(self $otherString): self { return new self($this->getValue() . $otherString->getValue()); @@ -327,11 +444,30 @@ public function generalize(GeneralizePrecision $precision): Type } if ($this->getValue() !== '' && $precision->isMoreSpecific()) { - return new IntersectionType([ + $accessories = [ new StringType(), - new AccessoryNonEmptyStringType(), new AccessoryLiteralStringType(), - ]); + ]; + + if (is_numeric($this->getValue())) { + $accessories[] = new AccessoryNumericStringType(); + } + + if ($this->getValue() !== '0') { + $accessories[] = new AccessoryNonFalsyStringType(); + } else { + $accessories[] = new AccessoryNonEmptyStringType(); + } + + if (strtolower($this->getValue()) === $this->getValue()) { + $accessories[] = new AccessoryLowercaseStringType(); + } + + if (strtoupper($this->getValue()) === $this->getValue()) { + $accessories[] = new AccessoryUppercaseStringType(); + } + + return new IntersectionType($accessories); } if ($precision->isMoreSpecific()) { @@ -344,7 +480,7 @@ public function generalize(GeneralizePrecision $precision): Type return new StringType(); } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(true), @@ -363,7 +499,7 @@ public function getSmallerType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ IntegerRangeType::createAllGreaterThan((float) $this->value), @@ -376,7 +512,7 @@ public function getSmallerOrEqualType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(false), @@ -390,7 +526,7 @@ public function getGreaterType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ IntegerRangeType::createAllSmallerThan((float) $this->value), @@ -403,12 +539,33 @@ public function getGreaterOrEqualType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function canAccessConstants(): TrinaryLogic + { + return $this->isClassString(); + } + + public function hasConstant(string $constantName): TrinaryLogic { - return new self($properties['value'], $properties['isClassString'] ?? false); + return $this->getObjectType()->hasConstant($constantName); + } + + public function getConstant(string $constantName): ClassConstantReflection + { + return $this->getObjectType()->getConstant($constantName); + } + + private function getObjectType(): ObjectType + { + return $this->objectType ??= new ObjectType($this->value); + } + + public function toPhpDocNode(): TypeNode + { + if (substr_count($this->value, "\n") > 0) { + return $this->generalize(GeneralizePrecision::moreSpecific())->toPhpDocNode(); + } + + return new ConstTypeNode(new ConstExprStringNode($this->value, ConstExprStringNode::SINGLE_QUOTED)); } } diff --git a/src/Type/Constant/OversizedArrayBuilder.php b/src/Type/Constant/OversizedArrayBuilder.php new file mode 100644 index 0000000000..026e305361 --- /dev/null +++ b/src/Type/Constant/OversizedArrayBuilder.php @@ -0,0 +1,101 @@ +items; + for ($i = 0; $i < count($items); $i++) { + $item = $items[$i]; + if (!$item->unpack) { + continue; + } + + $valueType = $getTypeCallback($item->value); + if ($valueType instanceof ConstantArrayType) { + array_splice($items, $i, 1); + foreach ($valueType->getKeyTypes() as $j => $innerKeyType) { + $innerValueType = $valueType->getValueTypes()[$j]; + if ($innerKeyType->isString()->no()) { + $keyExpr = null; + } else { + $keyExpr = new TypeExpr($innerKeyType); + } + array_splice($items, $i++, 0, [new ArrayItem( + new TypeExpr($innerValueType), + $keyExpr, + )]); + } + } else { + array_splice($items, $i, 1, [new ArrayItem( + new TypeExpr($valueType->getIterableValueType()), + new TypeExpr($valueType->getIterableKeyType()), + )]); + } + } + foreach ($items as $item) { + if ($item->unpack) { + throw new ShouldNotHappenException(); + } + if ($item->key !== null) { + $itemKeyType = $getTypeCallback($item->key); + if (!$itemKeyType instanceof ConstantIntegerType) { + $isList = false; + } elseif ($itemKeyType->getValue() !== $nextAutoIndex) { + $isList = false; + $nextAutoIndex = $itemKeyType->getValue() + 1; + } else { + $nextAutoIndex++; + } + } else { + $itemKeyType = new ConstantIntegerType($nextAutoIndex); + $nextAutoIndex++; + } + + $generalizedKeyType = $itemKeyType->generalize(GeneralizePrecision::moreSpecific()); + $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType; + + $itemValueType = $getTypeCallback($item->value); + $generalizedValueType = $itemValueType->generalize(GeneralizePrecision::moreSpecific()); + $valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType; + } + + $keyType = TypeCombinator::union(...array_values($keyTypes)); + $valueType = TypeCombinator::union(...array_values($valueTypes)); + + $arrayType = new ArrayType($keyType, $valueType); + if ($isList) { + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } + + return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType()); + } + +} diff --git a/src/Type/ConstantScalarType.php b/src/Type/ConstantScalarType.php index f44e18333b..b84b381717 100644 --- a/src/Type/ConstantScalarType.php +++ b/src/Type/ConstantScalarType.php @@ -3,7 +3,7 @@ namespace PHPStan\Type; /** @api */ -interface ConstantScalarType extends ConstantType +interface ConstantScalarType extends Type { /** diff --git a/src/Type/ConstantType.php b/src/Type/ConstantType.php deleted file mode 100644 index 0f08d47c81..0000000000 --- a/src/Type/ConstantType.php +++ /dev/null @@ -1,11 +0,0 @@ - ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $arrayBuilder->degradeToGeneralArray(); + $arrayBuilder->degradeToGeneralArray(true); } foreach ($value as $k => $v) { $arrayBuilder->setOffsetValueType(self::getTypeFromValue($k), self::getTypeFromValue($v)); diff --git a/src/Type/DirectTypeAliasResolverProvider.php b/src/Type/DirectTypeAliasResolverProvider.php index b64fe8f46d..f7fa61c09e 100644 --- a/src/Type/DirectTypeAliasResolverProvider.php +++ b/src/Type/DirectTypeAliasResolverProvider.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -class DirectTypeAliasResolverProvider implements TypeAliasResolverProvider +final class DirectTypeAliasResolverProvider implements TypeAliasResolverProvider { public function __construct(private TypeAliasResolver $typeAliasResolver) diff --git a/src/Type/DynamicFunctionReturnTypeExtension.php b/src/Type/DynamicFunctionReturnTypeExtension.php index ac4750ce53..eb7b6222ff 100644 --- a/src/Type/DynamicFunctionReturnTypeExtension.php +++ b/src/Type/DynamicFunctionReturnTypeExtension.php @@ -6,12 +6,28 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -/** @api */ +/** + * This is the interface dynamic return type extensions implement for functions. + * + * To register it in the configuration file use the `phpstan.broker.dynamicFunctionReturnTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.dynamicFunctionReturnTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-return-type-extensions + * + * @api + */ interface DynamicFunctionReturnTypeExtension { 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/DynamicFunctionThrowTypeExtension.php b/src/Type/DynamicFunctionThrowTypeExtension.php index 46f56ce61b..9e16865c3c 100644 --- a/src/Type/DynamicFunctionThrowTypeExtension.php +++ b/src/Type/DynamicFunctionThrowTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -/** @api */ +/** + * This is the interface dynamic throw type extensions implement for functions. + * + * To register it in the configuration file use the `phpstan.dynamicFunctionThrowTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.dynamicFunctionThrowTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions + * + * @api + */ interface DynamicFunctionThrowTypeExtension { diff --git a/src/Type/DynamicMethodReturnTypeExtension.php b/src/Type/DynamicMethodReturnTypeExtension.php index 58635acca7..e8af9137b0 100644 --- a/src/Type/DynamicMethodReturnTypeExtension.php +++ b/src/Type/DynamicMethodReturnTypeExtension.php @@ -6,14 +6,31 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic return type extensions implement for non-static methods. + * + * To register it in the configuration file use the `phpstan.broker.dynamicMethodReturnTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.dynamicMethodReturnTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-return-type-extensions + * + * @api + */ interface DynamicMethodReturnTypeExtension { + /** @return class-string */ 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/DynamicMethodThrowTypeExtension.php b/src/Type/DynamicMethodThrowTypeExtension.php index e6fba9fc7f..228604cb83 100644 --- a/src/Type/DynamicMethodThrowTypeExtension.php +++ b/src/Type/DynamicMethodThrowTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic throw type extensions implement for non-static methods. + * + * To register it in the configuration file use the `phpstan.dynamicMethodThrowTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.dynamicMethodThrowTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions + * + * @api + */ interface DynamicMethodThrowTypeExtension { diff --git a/src/Type/DynamicReturnTypeExtensionRegistry.php b/src/Type/DynamicReturnTypeExtensionRegistry.php index b747ba4a71..e2a30437b3 100644 --- a/src/Type/DynamicReturnTypeExtensionRegistry.php +++ b/src/Type/DynamicReturnTypeExtensionRegistry.php @@ -2,12 +2,11 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; -use PHPStan\Reflection\BrokerAwareExtension; use PHPStan\Reflection\ReflectionProvider; use function array_merge; +use function strtolower; -class DynamicReturnTypeExtensionRegistry +final class DynamicReturnTypeExtensionRegistry { /** @var DynamicMethodReturnTypeExtension[][]|null */ @@ -22,20 +21,12 @@ class DynamicReturnTypeExtensionRegistry * @param DynamicFunctionReturnTypeExtension[] $dynamicFunctionReturnTypeExtensions */ public function __construct( - Broker $broker, private ReflectionProvider $reflectionProvider, private array $dynamicMethodReturnTypeExtensions, private array $dynamicStaticMethodReturnTypeExtensions, private array $dynamicFunctionReturnTypeExtensions, ) { - foreach (array_merge($dynamicMethodReturnTypeExtensions, $dynamicStaticMethodReturnTypeExtensions, $dynamicFunctionReturnTypeExtensions) as $extension) { - if (!($extension instanceof BrokerAwareExtension)) { - continue; - } - - $extension->setBroker($broker); - } } /** @@ -46,7 +37,7 @@ public function getDynamicMethodReturnTypeExtensionsForClass(string $className): if ($this->dynamicMethodReturnTypeExtensionsByClass === null) { $byClass = []; foreach ($this->dynamicMethodReturnTypeExtensions as $extension) { - $byClass[$extension->getClass()][] = $extension; + $byClass[strtolower($extension->getClass())][] = $extension; } $this->dynamicMethodReturnTypeExtensionsByClass = $byClass; @@ -62,7 +53,7 @@ public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $class if ($this->dynamicStaticMethodReturnTypeExtensionsByClass === null) { $byClass = []; foreach ($this->dynamicStaticMethodReturnTypeExtensions as $extension) { - $byClass[$extension->getClass()][] = $extension; + $byClass[strtolower($extension->getClass())][] = $extension; } $this->dynamicStaticMethodReturnTypeExtensionsByClass = $byClass; @@ -83,6 +74,7 @@ private function getDynamicExtensionsForType(array $extensions, string $classNam $extensionsForClass = [[]]; $class = $this->reflectionProvider->getClass($className); foreach (array_merge([$className], $class->getParentClassesNames(), $class->getNativeReflection()->getInterfaceNames()) as $extensionClassName) { + $extensionClassName = strtolower($extensionClassName); if (!isset($extensions[$extensionClassName])) { continue; } diff --git a/src/Type/DynamicStaticMethodReturnTypeExtension.php b/src/Type/DynamicStaticMethodReturnTypeExtension.php index e2de530f9f..e74f69460f 100644 --- a/src/Type/DynamicStaticMethodReturnTypeExtension.php +++ b/src/Type/DynamicStaticMethodReturnTypeExtension.php @@ -6,14 +6,31 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic return type extensions implement for static methods. + * + * To register it in the configuration file use the `phpstan.broker.dynamicStaticMethodReturnTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.dynamicStaticMethodReturnTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-return-type-extensions + * + * @api + */ interface DynamicStaticMethodReturnTypeExtension { + /** @return class-string */ 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/DynamicStaticMethodThrowTypeExtension.php b/src/Type/DynamicStaticMethodThrowTypeExtension.php index b01735a6bd..fa9926dea3 100644 --- a/src/Type/DynamicStaticMethodThrowTypeExtension.php +++ b/src/Type/DynamicStaticMethodThrowTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic throw type extensions implement for static methods. + * + * To register it in the configuration file use the `phpstan.dynamicStaticMethodThrowTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.dynamicStaticMethodThrowTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions + * + * @api + */ interface DynamicStaticMethodThrowTypeExtension { diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index 4181b375d9..1093e24cb4 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -2,16 +2,25 @@ namespace PHPStan\Type\Enum; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Php\EnumPropertyReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\Php\EnumUnresolvedPropertyPrototypeReflection; +use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; +use PHPStan\Type\SubtractableType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -20,6 +29,7 @@ class EnumCaseObjectType extends ObjectType { + /** @api */ public function __construct( string $className, private string $enumCaseName, @@ -47,21 +57,20 @@ public function equals(Type $type): bool return false; } - return $this->getClassName() === $type->getClassName() - && $this->enumCaseName === $type->enumCaseName; + return $this->enumCaseName === $type->enumCaseName && + $this->getClassName() === $type->getClassName(); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { - return $this->isSuperTypeOf($type); + return $this->isSuperTypeOf($type)->toAcceptsResult(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean( - $this->getClassName() === $type->getClassName() - && $this->enumCaseName === $type->enumCaseName, + return IsSuperTypeOfResult::createFromBoolean( + $this->enumCaseName === $type->enumCaseName && $this->getClassName() === $type->getClassName(), ); } @@ -69,12 +78,24 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $type->isSubTypeOf($this); } - return $type->isSuperTypeOf($this)->yes() ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(); + if ( + $type instanceof SubtractableType + && $type->getSubtractedType() !== null + ) { + $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); + if ($isSuperType->yes()) { + return IsSuperTypeOfResult::createNo(); + } + } + + $parent = new parent($this->getClassName(), $this->getSubtractedType(), $this->getClassReflection()); + + return $parent->isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe()); } public function subtract(Type $type): Type { - return $this; + return $this->changeSubtractedType($type); } public function getTypeWithoutSubtractedType(): Type @@ -84,7 +105,11 @@ public function getTypeWithoutSubtractedType(): Type public function changeSubtractedType(?Type $subtractedType): Type { - return $this; + if ($subtractedType === null || ! $this->equals($subtractedType)) { + return $this; + } + + return new NeverType(); } public function getSubtractedType(): ?Type @@ -101,15 +126,17 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { $classReflection = $this->getClassReflection(); if ($classReflection === null) { - return parent::getProperty($propertyName, $scope); + return parent::getUnresolvedPropertyPrototype($propertyName, $scope); } if ($propertyName === 'name') { - return new EnumPropertyReflection($classReflection, new ConstantStringType($this->enumCaseName)); + return new EnumUnresolvedPropertyPrototypeReflection( + new EnumPropertyReflection($propertyName, $classReflection, new ConstantStringType($this->enumCaseName)), + ); } if ($classReflection->isBackedEnum() && $propertyName === 'value') { @@ -120,11 +147,33 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco throw new ShouldNotHappenException(); } - return new EnumPropertyReflection($classReflection, $valueType); + return new EnumUnresolvedPropertyPrototypeReflection( + new EnumPropertyReflection($propertyName, $classReflection, $valueType), + ); } } - return parent::getProperty($propertyName, $scope); + return parent::getUnresolvedPropertyPrototype($propertyName, $scope); + } + + public function getBackingValueType(): ?Type + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return null; + } + + if (!$classReflection->isBackedEnum()) { + return null; + } + + if ($classReflection->hasEnumCase($this->enumCaseName)) { + $enumCase = $classReflection->getEnumCase($this->enumCaseName); + + return $enumCase->getBackingValueType(); + } + + return null; } public function generalize(GeneralizePrecision $precision): Type @@ -132,12 +181,29 @@ 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 + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getEnumCases(): array + { + return [$this]; + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['className'], $properties['enumCaseName'], null); + return new ConstTypeNode( + new ConstFetchNode( + $this->getClassName(), + $this->getEnumCaseName(), + ), + ); } } diff --git a/src/Type/ErrorType.php b/src/Type/ErrorType.php index 345465b306..751271aef3 100644 --- a/src/Type/ErrorType.php +++ b/src/Type/ErrorType.php @@ -41,12 +41,4 @@ public function equals(Type $type): bool return $type instanceof self; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self(); - } - } diff --git a/src/Type/ExponentiateHelper.php b/src/Type/ExponentiateHelper.php new file mode 100644 index 0000000000..fd65dc9e51 --- /dev/null +++ b/src/Type/ExponentiateHelper.php @@ -0,0 +1,131 @@ +getTypes() as $unionType) { + $results[] = self::exponentiate($base, $unionType); + } + return TypeCombinator::union(...$results); + } + + if ($exponent instanceof NeverType) { + return new NeverType(); + } + + $allowedExponentTypes = new UnionType([ + new IntegerType(), + new FloatType(), + new StringType(), + new BooleanType(), + new NullType(), + ]); + if (!$allowedExponentTypes->isSuperTypeOf($exponent)->yes()) { + return new ErrorType(); + } + + if ($base instanceof ConstantScalarType) { + $result = self::exponentiateConstantScalar($base, $exponent); + if ($result !== null) { + return $result; + } + } + + // exponentiation of a float, stays a float + $isFloatBase = $base->isFloat()->yes(); + + $isLooseZero = (new ConstantIntegerType(0))->isSuperTypeOf($exponent->toNumber()); + if ($isLooseZero->yes()) { + if ($isFloatBase) { + return new ConstantFloatType(1); + } + + return new ConstantIntegerType(1); + } + + $isLooseOne = (new ConstantIntegerType(1))->isSuperTypeOf($exponent->toNumber()); + if ($isLooseOne->yes()) { + $possibleResults = new UnionType([ + new FloatType(), + new IntegerType(), + ]); + + if ($possibleResults->isSuperTypeOf($base)->yes()) { + return $base; + } + } + + if ($isFloatBase) { + return new FloatType(); + } + + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + private static function exponentiateConstantScalar(ConstantScalarType $base, Type $exponent): ?Type + { + if ($exponent instanceof IntegerRangeType) { + $min = null; + $max = null; + if ($exponent->getMin() !== null) { + $min = self::pow($base->getValue(), $exponent->getMin()); + if ($min === null) { + return new ErrorType(); + } + } + if ($exponent->getMax() !== null) { + $max = self::pow($base->getValue(), $exponent->getMax()); + if ($max === null) { + return new ErrorType(); + } + } + + if (!is_float($min) && !is_float($max)) { + return IntegerRangeType::fromInterval($min, $max); + } + } + + if ($exponent instanceof ConstantScalarType) { + $result = self::pow($base->getValue(), $exponent->getValue()); + if ($result === null) { + return new ErrorType(); + } + + if (is_int($result)) { + return new ConstantIntegerType($result); + } + return new ConstantFloatType($result); + } + + return null; + } + + private static function pow(mixed $base, mixed $exp): float|int|null + { + if (is_string($base) && !is_numeric($base)) { + return null; + } + if (is_string($exp) && !is_numeric($exp)) { + return null; + } + return pow($base, $exp); + } + +} diff --git a/src/Type/ExpressionTypeResolverExtension.php b/src/Type/ExpressionTypeResolverExtension.php new file mode 100644 index 0000000000..1bc7a710e5 --- /dev/null +++ b/src/Type/ExpressionTypeResolverExtension.php @@ -0,0 +1,28 @@ + $extensions + */ + public function __construct( + private array $extensions, + ) + { + } + + /** + * @return array + */ + public function getExtensions(): array + { + return $this->extensions; + } + +} diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index bf9f08eb9a..d3f6af2137 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -3,15 +3,13 @@ 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\Php\PhpVersion; +use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException; use PHPStan\PhpDoc\PhpDocNodeResolver; use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; @@ -23,7 +21,8 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; -use ReflectionClass; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use function array_key_exists; use function array_keys; use function array_map; @@ -31,20 +30,16 @@ 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 str_contains; use function strtolower; -use function time; -use function trait_exists; -class FileTypeMapper +final class FileTypeMapper { private const SKIP_NODE = 1; @@ -55,7 +50,7 @@ class FileTypeMapper private int $memoryCacheCount = 0; - /** @var (false|callable(): NameScope|NameScope)[][] */ + /** @var (true|callable(): NameScope|NameScope)[][] */ private array $inProcess = []; /** @var array */ @@ -63,17 +58,12 @@ class FileTypeMapper private int $resolvedPhpDocBlockCacheCount = 0; - /** @var array */ - private array $alreadyProcessedDependentFiles = []; - public function __construct( private ReflectionProviderProvider $reflectionProviderProvider, private Parser $phpParser, private PhpDocStringResolver $phpDocStringResolver, private PhpDocNodeResolver $phpDocNodeResolver, - private Cache $cache, private AnonymousClassNameHelper $anonymousClassNameHelper, - private PhpVersion $phpVersion, private FileHelper $fileHelper, ) { @@ -81,15 +71,13 @@ public function __construct( /** @api */ public function getResolvedPhpDoc( - string $fileName, + ?string $fileName, ?string $className, ?string $traitName, ?string $functionName, string $docComment, ): ResolvedPhpDocBlock { - $fileName = $this->fileHelper->normalizePath($fileName); - if ($className === null && $traitName !== null) { throw new ShouldNotHappenException(); } @@ -98,11 +86,40 @@ public function getResolvedPhpDoc( return ResolvedPhpDocBlock::createEmpty(); } + if ($fileName !== null) { + $fileName = $this->fileHelper->normalizePath($fileName); + } + $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName); $phpDocKey = md5(sprintf('%s-%s', $nameScopeKey, $docComment)); if (isset($this->resolvedPhpDocBlockCache[$phpDocKey])) { return $this->resolvedPhpDocBlockCache[$phpDocKey]; } + + if ($fileName === null) { + return $this->createResolvedPhpDocBlock($phpDocKey, new NameScope(null, []), $docComment, null); + } + + try { + $nameScope = $this->getNameScope($fileName, $className, $traitName, $functionName); + } catch (NameScopeAlreadyBeingCreatedException) { + return ResolvedPhpDocBlock::createEmpty(); + } + + return $this->createResolvedPhpDocBlock($phpDocKey, $nameScope, $docComment, $fileName); + } + + /** + * @throws NameScopeAlreadyBeingCreatedException + */ + public function getNameScope( + string $fileName, + ?string $className, + ?string $traitName, + ?string $functionName, + ): NameScope + { + $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName); $nameScopeMap = []; if (!isset($this->inProcess[$fileName])) { @@ -110,30 +127,30 @@ public function getResolvedPhpDoc( } if (isset($nameScopeMap[$nameScopeKey])) { - return $this->createResolvedPhpDocBlock($phpDocKey, $nameScopeMap[$nameScopeKey], $docComment, $fileName); + return $nameScopeMap[$nameScopeKey]; } if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits - return ResolvedPhpDocBlock::createEmpty(); + throw new NameScopeAlreadyBeingCreatedException(); } - if ($this->inProcess[$fileName][$nameScopeKey] === false) { // PHPDoc has cyclic dependency - return ResolvedPhpDocBlock::createEmpty(); + if ($this->inProcess[$fileName][$nameScopeKey] === true) { // PHPDoc has cyclic dependency + throw new NameScopeAlreadyBeingCreatedException(); } if (is_callable($this->inProcess[$fileName][$nameScopeKey])) { $resolveCallback = $this->inProcess[$fileName][$nameScopeKey]; - $this->inProcess[$fileName][$nameScopeKey] = false; + $this->inProcess[$fileName][$nameScopeKey] = true; $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback(); } - return $this->createResolvedPhpDocBlock($phpDocKey, $this->inProcess[$fileName][$nameScopeKey], $docComment, $fileName); + return $this->inProcess[$fileName][$nameScopeKey]; } - private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameScope, string $phpDocString, string $fileName): ResolvedPhpDocBlock + private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameScope, string $phpDocString, ?string $fileName): ResolvedPhpDocBlock { - $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString); - if ($this->resolvedPhpDocBlockCacheCount >= 512) { + $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString); + if ($this->resolvedPhpDocBlockCacheCount >= 2048) { $this->resolvedPhpDocBlockCache = array_slice( $this->resolvedPhpDocBlockCache, 1, @@ -163,33 +180,21 @@ private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameSco new TemplateTypeMap($phpDocTemplateTypes), $templateTags, $this->phpDocNodeResolver, + $this->reflectionProviderProvider->getReflectionProvider(), ); $this->resolvedPhpDocBlockCacheCount++; return $this->resolvedPhpDocBlockCache[$phpDocKey]; } - private function resolvePhpDocStringToDocNode(string $phpDocString): PhpDocNode - { - return $this->phpDocStringResolver->resolve($phpDocString); - } - /** * @return NameScope[] */ private function getNameScopeMap(string $fileName): array { if (!isset($this->memoryCache[$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) { - $map = $this->createResolvedPhpDocMap($fileName); - $this->cache->save($cacheKey, $variableCacheKey, $map); - } - - if ($this->memoryCacheCount >= 512) { + $map = $this->createResolvedPhpDocMap($fileName); + if ($this->memoryCacheCount >= 2048) { $this->memoryCache = array_slice( $this->memoryCache, 1, @@ -211,14 +216,15 @@ private function getNameScopeMap(string $fileName): array */ private function createResolvedPhpDocMap(string $fileName): array { - $nameScopeMap = $this->createNameScopeMap($fileName, null, null, [], $fileName); + $phpDocNodeMap = $this->createPhpDocNodeMap($fileName, null, $fileName, [], $fileName); + $nameScopeMap = $this->createNameScopeMap($fileName, null, null, [], $fileName, $phpDocNodeMap); $resolvedNameScopeMap = []; try { $this->inProcess[$fileName] = $nameScopeMap; foreach ($nameScopeMap as $nameScopeKey => $resolveCallback) { - $this->inProcess[$fileName][$nameScopeKey] = false; + $this->inProcess[$fileName][$nameScopeKey] = true; $this->inProcess[$fileName][$nameScopeKey] = $data = $resolveCallback(); $resolvedNameScopeMap[$nameScopeKey] = $data; } @@ -232,6 +238,200 @@ private function createResolvedPhpDocMap(string $fileName): array /** * @param array $traitMethodAliases + * @return array + */ + private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName): array + { + /** @var array $phpDocNodeMap */ + $phpDocNodeMap = []; + + /** @var string[] $classStack */ + $classStack = []; + if ($lookForTrait !== null && $traitUseClass !== null) { + $classStack[] = $traitUseClass; + } + $namespace = null; + + $traitFound = false; + + /** @var array $functionStack */ + $functionStack = []; + $this->processNodes( + $this->phpParser->parseFile($fileName), + function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$phpDocNodeMap, &$classStack, &$namespace, &$functionStack): ?int { + if ($node instanceof Node\Stmt\ClassLike) { + if ($traitFound && $fileName === $originalClassFileName) { + return self::SKIP_NODE; + } + + if ($lookForTrait !== null && !$traitFound) { + if (!$node instanceof Node\Stmt\Trait_) { + return self::SKIP_NODE; + } + if ((string) $node->namespacedName !== $lookForTrait) { + return self::SKIP_NODE; + } + + $traitFound = true; + $functionStack[] = null; + } else { + if ($node->name === null) { + if (!$node instanceof Node\Stmt\Class_) { + throw new ShouldNotHappenException(); + } + + $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); + } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) { + $className = $node->name->name; + } else { + if ($traitFound) { + return self::SKIP_NODE; + } + $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); + } + $classStack[] = $className; + $functionStack[] = null; + } + } elseif ($node instanceof Node\Stmt\ClassMethod) { + if (array_key_exists($node->name->name, $traitMethodAliases)) { + $functionStack[] = $traitMethodAliases[$node->name->name]; + } else { + $functionStack[] = $node->name->name; + } + } elseif ($node instanceof Node\Stmt\Function_) { + $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString()); + } + } + + $className = $classStack[count($classStack) - 1] ?? null; + $functionName = $functionStack[count($functionStack) - 1] ?? null; + + if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + $docComment = GetLastDocComment::forNode($node); + if ($docComment !== null) { + $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); + $phpDocNodeMap[$nameScopeKey] = $this->phpDocStringResolver->resolve($docComment); + } + + return null; + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $docComment = GetLastDocComment::forNode($node); + if ($docComment !== null) { + $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); + $phpDocNodeMap[$nameScopeKey] = $this->phpDocStringResolver->resolve($docComment); + } + } + + return null; + } + + if ($node instanceof Node\Stmt\Namespace_) { + $namespace = $node->name !== null ? (string) $node->name : null; + } elseif ($node instanceof Node\Stmt\TraitUse) { + $traitMethodAliases = []; + foreach ($node->adaptations as $traitUseAdaptation) { + if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { + continue; + } + + if ($traitUseAdaptation->newName === null) { + continue; + } + + $methodName = $traitUseAdaptation->method->toString(); + $newTraitName = $traitUseAdaptation->newName->toString(); + + if ($traitUseAdaptation->trait === null) { + foreach ($node->traits as $traitName) { + $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName; + } + continue; + } + + $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName; + } + + foreach ($node->traits as $traitName) { + /** @var class-string $traitName */ + $traitName = (string) $traitName; + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + if (!$reflectionProvider->hasClass($traitName)) { + continue; + } + + $traitReflection = $reflectionProvider->getClass($traitName); + if (!$traitReflection->isTrait()) { + continue; + } + if ($traitReflection->getFileName() === null) { + continue; + } + if (!is_file($traitReflection->getFileName())) { + continue; + } + + $className = $classStack[count($classStack) - 1] ?? null; + if ($className === null) { + throw new ShouldNotHappenException(); + } + + $phpDocNodeMap = array_merge($phpDocNodeMap, $this->createPhpDocNodeMap( + $traitReflection->getFileName(), + $traitName, + $className, + $traitMethodAliases[$traitName] ?? [], + $originalClassFileName, + )); + } + } + + return null; + }, + static function (Node $node) use (&$namespace, &$functionStack, &$classStack): void { + if ($node instanceof Node\Stmt\ClassLike) { + if (count($classStack) === 0) { + throw new ShouldNotHappenException(); + } + array_pop($classStack); + + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } elseif ($node instanceof Node\Stmt\Namespace_) { + $namespace = null; + } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } + } + }, + ); + + return $phpDocNodeMap; + } + + /** + * @param array $traitMethodAliases + * @param array $phpDocNodeMap * @return (callable(): NameScope)[] */ private function createNameScopeMap( @@ -240,6 +440,7 @@ private function createNameScopeMap( ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName, + array $phpDocNodeMap, ): array { /** @var (callable(): NameScope)[] $nameScopeMap */ @@ -264,9 +465,10 @@ private function createNameScopeMap( /** @var array $functionStack */ $functionStack = []; $uses = []; + $constUses = []; $this->processNodes( $this->phpParser->parseFile($fileName), - function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack): ?int { + function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack, &$constUses): ?int { if ($node instanceof Node\Stmt\ClassLike) { if ($traitFound && $fileName === $originalClassFileName) { return self::SKIP_NODE; @@ -281,6 +483,13 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } $traitFound = true; + $traitNameScopeKey = $this->getNameScopeKey($originalClassFileName, $classStack[count($classStack) - 1] ?? null, $lookForTrait, null); + if (array_key_exists($traitNameScopeKey, $phpDocNodeMap)) { + $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNodeMap[$traitNameScopeKey]); + } else { + $typeAliasStack[] = []; + } + $functionStack[] = null; } else { if ($node->name === null) { if (!$node instanceof Node\Stmt\Class_) { @@ -288,7 +497,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); - } elseif ((bool) $node->getAttribute('anonymousClass', false)) { + } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) { $className = $node->name->name; } else { if ($traitFound) { @@ -297,7 +506,12 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); } $classStack[] = $className; - $typeAliasStack[] = $this->getTypeAliasesMap($node->getDocComment()); + $classNameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, null); + if (array_key_exists($classNameScopeKey, $phpDocNodeMap)) { + $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNodeMap[$classNameScopeKey]); + } else { + $typeAliasStack[] = []; + } $functionStack[] = null; } } elseif ($node instanceof Node\Stmt\ClassMethod) { @@ -308,19 +522,26 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } } elseif ($node instanceof Node\Stmt\Function_) { $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString()); + } } $className = $classStack[count($classStack) - 1] ?? null; $functionName = $functionStack[count($functionStack) - 1] ?? null; + $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { - $phpDocString = GetLastDocComment::forNode($node); - if ($phpDocString !== '') { - $typeMapStack[] = function () use ($namespace, $uses, $className, $functionName, $phpDocString, $typeMapStack): TemplateTypeMap { - $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString); + // property hook skipped on purpose, it does not support @template + if (array_key_exists($nameScopeKey, $phpDocNodeMap)) { + $phpDocNode = $phpDocNodeMap[$nameScopeKey]; + $typeMapStack[] = function () use ($namespace, $uses, $className, $lookForTrait, $functionName, $phpDocNode, $typeMapStack, $typeAliasStack, $constUses): TemplateTypeMap { $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; $currentTypeMap = $typeMapCb !== null ? $typeMapCb() : null; - $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap); + $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; + $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, $typeAliasesMap, false, $constUses, $lookForTrait); $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); $templateTypeScope = $nameScope->getTemplateTypeScope(); if ($templateTypeScope === null) { @@ -342,20 +563,21 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; - $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); if ( - $node instanceof Node\Stmt - && !$node instanceof Node\Stmt\Namespace_ - && !$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) + ( + $node instanceof Node\PropertyHook + || ( + $node instanceof Node\Stmt + && !$node instanceof Node\Stmt\Namespace_ + && !$node instanceof Node\Stmt\Declare_ + && !$node instanceof Node\Stmt\Use_ + && !$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, @@ -364,12 +586,15 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $functionName, ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty()), $typeAliasesMap, + false, + $constUses, + $lookForTrait, ); } if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { - $phpDocString = GetLastDocComment::forNode($node); - if ($phpDocString !== '') { + // property hook skipped on purpose, it does not support @template + if (array_key_exists($nameScopeKey, $phpDocNodeMap)) { return self::POP_TYPE_MAP_STACK; } @@ -377,19 +602,25 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } if ($node instanceof Node\Stmt\Namespace_) { - $namespace = (string) $node->name; - } 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; + $namespace = $node->name !== null ? (string) $node->name : null; + } elseif ($node instanceof Node\Stmt\Use_) { + if ($node->type === Node\Stmt\Use_::TYPE_NORMAL) { + foreach ($node->uses as $use) { + $uses[strtolower($use->getAlias()->name)] = (string) $use->name; + } + } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT) { + foreach ($node->uses as $use) { + $constUses[strtolower($use->getAlias()->name)] = (string) $use->name; + } } } elseif ($node instanceof Node\Stmt\GroupUse) { $prefix = (string) $node->prefix; foreach ($node->uses as $use) { - if ($node->type !== Node\Stmt\Use_::TYPE_NORMAL && $use->type !== Node\Stmt\Use_::TYPE_NORMAL) { - continue; + if ($node->type === Node\Stmt\Use_::TYPE_NORMAL || $use->type === Node\Stmt\Use_::TYPE_NORMAL) { + $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); + } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT || $use->type === Node\Stmt\Use_::TYPE_CONSTANT) { + $constUses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); } - - $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); } } elseif ($node instanceof Node\Stmt\TraitUse) { $traitMethodAliases = []; @@ -398,15 +629,21 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA continue; } - if ($traitUseAdaptation->trait === null) { + if ($traitUseAdaptation->newName === null) { continue; } - if ($traitUseAdaptation->newName === null) { + $methodName = $traitUseAdaptation->method->toString(); + $newTraitName = $traitUseAdaptation->newName->toString(); + + if ($traitUseAdaptation->trait === null) { + foreach ($node->traits as $traitName) { + $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName; + } continue; } - $traitMethodAliases[$traitUseAdaptation->trait->toString()][$traitUseAdaptation->method->toString()] = $traitUseAdaptation->newName->toString(); + $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName; } $useDocComment = null; @@ -444,6 +681,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $className, $traitMethodAliases[$traitName] ?? [], $originalClassFileName, + $phpDocNodeMap, ); $finalTraitPhpDocMap = []; foreach ($traitPhpDocMap as $nameScopeTraitKey => $callback) { @@ -486,7 +724,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes()); - return $original->withTemplateTypeMap($traitTemplateTypeMap->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap))); + return $original->withTemplateTypeMap($traitTemplateTypeMap->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap, TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createStatic()))); }; } $nameScopeMap = array_merge($nameScopeMap, $finalTraitPhpDocMap); @@ -495,8 +733,8 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA return null; }, - static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack): void { - if ($node instanceof Node\Stmt\ClassLike && $lookForTrait === null) { + static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void { + if ($node instanceof Node\Stmt\ClassLike) { if (count($classStack) === 0) { throw new ShouldNotHappenException(); } @@ -516,12 +754,22 @@ static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, & } elseif ($node instanceof Node\Stmt\Namespace_) { $namespace = null; $uses = []; + $constUses = []; } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { if (count($functionStack) === 0) { throw new ShouldNotHappenException(); } array_pop($functionStack); + } elseif ($node instanceof Node\PropertyHook) { + $propertyName = $node->getAttribute('propertyName'); + if ($propertyName !== null) { + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } } if ($callbackResult !== self::POP_TYPE_MAP_STACK) { return; @@ -544,13 +792,8 @@ static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, & /** * @return array */ - private function getTypeAliasesMap(?Doc $docComment): array + private function getTypeAliasesMap(PhpDocNode $phpDocNode): array { - if ($docComment === null) { - return []; - } - - $phpDocNode = $this->phpDocStringResolver->resolve($docComment->getText()); $nameScope = new NameScope(null, []); $aliasesMap = []; @@ -566,7 +809,7 @@ private function getTypeAliasesMap(?Doc $docComment): array } /** - * @param Node[]|Node|scalar $node + * @param Node[]|Node|scalar|null $node * @param Closure(Node $node): mixed $nodeCallback * @param Closure(Node $node, mixed $callbackResult): void $endNodeCallback */ @@ -590,143 +833,21 @@ private function processNodes($node, Closure $nodeCallback, Closure $endNodeCall } private function getNameScopeKey( - string $file, + ?string $file, ?string $class, ?string $trait, ?string $function, ): string { if ($class === null && $trait === null && $function === null) { - return md5(sprintf('%s', $file)); + return md5(sprintf('%s', $file ?? 'no-file')); } - if ($class !== null && strpos($class, 'class@anonymous') !== false) { + if ($class !== null && str_contains($class, 'class@anonymous')) { throw new ShouldNotHappenException('Wrong anonymous class name, FilTypeMapper should be called with ClassReflection::getName().'); } - return md5(sprintf('%s-%s-%s-%s', $file, $class, $trait, $function)); - } - - /** - * @return array - */ - private function getCachedDependentFilesWithTimestamps(string $fileName): array - { - $cacheKey = sprintf('dependentFilesTimestamps-%s-v3-filter-ast', $fileName); - $fileModifiedTime = filemtime($fileName); - if ($fileModifiedTime === false) { - $fileModifiedTime = time(); - } - $variableCacheKey = sprintf('%d-%s', $fileModifiedTime, $this->phpVersion->getVersionString()); - /** @var array|null $cachedFilesTimestamps */ - $cachedFilesTimestamps = $this->cache->load($cacheKey, $variableCacheKey); - if ($cachedFilesTimestamps !== null) { - $useCached = true; - foreach ($cachedFilesTimestamps as $cachedFile) { - $cachedFilename = $cachedFile['filename']; - $cachedTimestamp = $cachedFile['modifiedTime']; - - if (!is_file($cachedFilename)) { - $useCached = false; - break; - } - - $currentTimestamp = filemtime($cachedFilename); - if ($currentTimestamp === false) { - $useCached = false; - break; - } - - if ($currentTimestamp !== $cachedTimestamp) { - $useCached = false; - break; - } - } - - if ($useCached) { - return $cachedFilesTimestamps; - } - } - - $filesTimestamps = []; - foreach ($this->getDependentFiles($fileName) as $dependentFile) { - $dependentFileModifiedTime = filemtime($dependentFile); - if ($dependentFileModifiedTime === false) { - $dependentFileModifiedTime = time(); - } - - $filesTimestamps[] = [ - 'filename' => $dependentFile, - 'modifiedTime' => $dependentFileModifiedTime, - ]; - } - - $this->cache->save($cacheKey, $variableCacheKey, $filesTimestamps); - - return $filesTimestamps; - } - - /** - * @return string[] - */ - private function getDependentFiles(string $fileName): array - { - $dependentFiles = [$fileName]; - - if (isset($this->alreadyProcessedDependentFiles[$fileName])) { - return $dependentFiles; - } - - $this->alreadyProcessedDependentFiles[$fileName] = true; - - $this->processNodes( - $this->phpParser->parseFile($fileName), - function (Node $node) use (&$dependentFiles) { - if ($node instanceof Node\Stmt\Declare_) { - return null; - } - if ($node instanceof Node\Stmt\Namespace_) { - return null; - } - - if (!$node instanceof Node\Stmt\Class_ && !$node instanceof Node\Stmt\Trait_ && !$node instanceof Node\Stmt\Enum_) { - return null; - } - - foreach ($node->stmts as $stmt) { - if (!$stmt instanceof Node\Stmt\TraitUse) { - continue; - } - - foreach ($stmt->traits as $traitName) { - $traitName = (string) $traitName; - if (!trait_exists($traitName)) { - continue; - } - - $traitReflection = new ReflectionClass($traitName); - if ($traitReflection->getFileName() === false) { - continue; - } - if (!is_file($traitReflection->getFileName())) { - continue; - } - - foreach ($this->getDependentFiles($traitReflection->getFileName()) as $traitFileName) { - $dependentFiles[] = $traitFileName; - } - } - } - - return null; - }, - static function (): void { - }, - ); - - unset($this->alreadyProcessedDependentFiles[$fileName]); - - return $dependentFiles; + return md5(sprintf('%s-%s-%s-%s', $file ?? 'no-file', $class, $trait, $function)); } } diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 091f427e86..e38e5be35a 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -2,10 +2,15 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -21,6 +26,7 @@ class FloatType implements Type { + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -36,41 +42,50 @@ public function __construct() { } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array { - if ($type instanceof self || $type instanceof IntegerType) { - return TrinaryLogic::createYes(); + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof self || $type->isInteger()->yes()) { + return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy(new UnionType([ - $this, - new IntegerType(), - ]), $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -88,6 +103,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toFloat(): Type { return $this; @@ -102,6 +122,7 @@ public function toString(): Type { return new IntersectionType([ new StringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } @@ -111,11 +132,77 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], + [], + TrinaryLogic::createYes(), ); } - public function isArray(): TrinaryLogic + public function toArrayKey(): Type + { + return new IntegerType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this, $this->toString(), $this->toBoolean()); + } + + return $this; + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -135,22 +222,79 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type { - return new self(); + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return ExponentiateHelper::exponentiate($this, $exponent); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('float'); + } + + public function getFiniteTypes(): array + { + return []; } } diff --git a/src/Type/FunctionParameterClosureTypeExtension.php b/src/Type/FunctionParameterClosureTypeExtension.php new file mode 100644 index 0000000000..5a68d5517c --- /dev/null +++ b/src/Type/FunctionParameterClosureTypeExtension.php @@ -0,0 +1,32 @@ +type; } + public function getClassStringObjectType(): Type + { + return $this->getGenericType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->getClassStringObjectType(); + } + public function describe(VerbosityLevel $level): string { return sprintf('%s<%s>', parent::describe($level), $this->type->describe($level)); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof ConstantStringType) { - if (!$type->isClassString()) { - return TrinaryLogic::createNo(); + if (!$type->isClassString()->yes()) { + return AcceptsResult::createNo(); } $objectType = new ObjectType($type->getValue()); @@ -60,15 +77,15 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } elseif ($type instanceof ClassStringType) { $objectType = new ObjectWithoutClassType(); } elseif ($type instanceof StringType) { - return TrinaryLogic::createMaybe(); + return AcceptsResult::createMaybe(); } else { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } return $this->type->accepts($objectType, $strictTypes); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); @@ -77,7 +94,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic if ($type instanceof ConstantStringType) { $genericType = $this->type; if ($genericType instanceof MixedType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($genericType instanceof StaticType) { @@ -96,18 +113,18 @@ public function isSuperTypeOf(Type $type): TrinaryLogic $isSuperType = $genericType->isSuperTypeOf($objectType); } - // Explicitly handle the uncertainty for Maybe. - if ($isSuperType->maybe()) { - return TrinaryLogic::createNo(); + if (!$type->isClassString()->yes()) { + $isSuperType = $isSuperType->and(IsSuperTypeOfResult::createMaybe()); } + return $isSuperType; } elseif ($type instanceof self) { return $this->type->isSuperTypeOf($type->type); } elseif ($type instanceof StringType) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function traverse(callable $cb): Type @@ -120,6 +137,16 @@ public function traverse(callable $cb): Type return new self($newType); } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $newType = $cb($this->type, $right->getClassStringObjectType()); + if ($newType === $this->type) { + return $this; + } + + return new self($newType); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { @@ -130,7 +157,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $typeToInfer = new ObjectType($receivedType->getValue()); } elseif ($receivedType instanceof self) { $typeToInfer = $receivedType->type; - } elseif ($receivedType instanceof ClassStringType) { + } elseif ($receivedType->isClassString()->yes()) { $typeToInfer = $this->type; if ($typeToInfer instanceof TemplateType) { $typeToInfer = $typeToInfer->getBound(); @@ -168,12 +195,31 @@ public function equals(Type $type): bool return true; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode( + new IdentifierTypeNode('class-string'), + [ + $this->type->toPhpDocNode(), + ], + ); + } + + public function tryRemove(Type $typeToRemove): ?Type { - return new self($properties['type']); + if ($typeToRemove instanceof ConstantStringType && $typeToRemove->isClassString()->yes()) { + $generic = $this->getGenericType(); + + $genericObjectClassNames = $generic->getObjectClassNames(); + if (count($genericObjectClassNames) === 1) { + $classReflection = ReflectionProviderStaticAccessor::getInstance()->getClass($genericObjectClassNames[0]); + if ($classReflection->isFinal() && $genericObjectClassNames[0] === $typeToRemove->getValue()) { + return new NeverType(); + } + } + } + + return parent::tryRemove($typeToRemove); } } diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index c7672be92a..a2bdadd7ae 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -2,18 +2,22 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; @@ -31,12 +35,14 @@ class GenericObjectType extends ObjectType /** * @api * @param array $types + * @param array $variances */ public function __construct( string $mainType, private array $types, ?Type $subtractedType = null, private ?ClassReflection $classReflection = null, + private array $variances = [], ) { parent::__construct($mainType, $subtractedType, $classReflection); @@ -47,7 +53,11 @@ public function describe(VerbosityLevel $level): string return sprintf( '%s<%s>', parent::describe($level), - implode(', ', array_map(static fn (Type $type): string => $type->describe($level), $this->types)), + implode(', ', array_map( + static fn (Type $type, ?TemplateTypeVariance $variance = null): string => TypeProjectionHelper::describe($type, $variance, $level), + $this->types, + $this->variances, + )), ); } @@ -70,14 +80,17 @@ public function equals(Type $type): bool if (!$genericType->equals($otherGenericType)) { return false; } + + $variance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + $otherVariance = $type->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if (!$variance->equals($otherVariance)) { + return false; + } } return true; } - /** - * @return string[] - */ public function getReferencedClasses(): array { $classes = parent::getReferencedClasses(); @@ -96,26 +109,32 @@ public function getTypes(): array return $this->types; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + /** @return array */ + public function getVariances(): array + { + return $this->variances; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return $this->isSuperTypeOfInternal($type, true); - } - - public function isSuperTypeOf(Type $type): TrinaryLogic - { - return $this->isSuperTypeOfInternal($type, false); + return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); } - private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } + return $this->isSuperTypeOfInternal($type, false); + } + + private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSuperTypeOfResult + { $nakedSuperTypeOf = parent::isSuperTypeOf($type); if ($nakedSuperTypeOf->no()) { return $nakedSuperTypeOf; @@ -134,11 +153,11 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Trinar return $nakedSuperTypeOf; } - return $nakedSuperTypeOf->and(TrinaryLogic::createMaybe()); + return $nakedSuperTypeOf->and(IsSuperTypeOfResult::createMaybe()); } if (count($this->types) !== count($ancestor->types)) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } $classReflection = $this->getClassReflection(); @@ -162,14 +181,27 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Trinar throw new ShouldNotHappenException(); } - $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]); + $thisVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + $ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if (!$thisVariance->invariant()) { + $results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i]); + } else { + $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]); + } + + $results[] = IsSuperTypeOfResult::createFromBoolean($thisVariance->validPosition($ancestorVariance)); } if (count($results) === 0) { return $nakedSuperTypeOf; } - return $nakedSuperTypeOf->and(...$results); + $result = IsSuperTypeOfResult::createYes(); + foreach ($results as $innerResult) { + $result = $result->and($innerResult); + } + + return $result; } public function getClassReflection(): ?ClassReflection @@ -183,10 +215,12 @@ public function getClassReflection(): ?ClassReflection return null; } - return $this->classReflection = $reflectionProvider->getClass($this->getClassName())->withTypes($this->types); + return $this->classReflection = $reflectionProvider->getClass($this->getClassName()) + ->withTypes($this->types) + ->withVariances($this->variances); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -198,7 +232,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember return $prototype->doNotResolveTemplateTypeMapToBounds(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -253,11 +287,12 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc $references = []; foreach ($this->types as $i => $type) { - $variance = $positionVariance->compose( - isset($typeList[$i]) && $typeList[$i] instanceof TemplateType - ? $typeList[$i]->getVariance() - : TemplateTypeVariance::createInvariant(), - ); + $effectiveVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if ($effectiveVariance->invariant() && isset($typeList[$i]) && $typeList[$i] instanceof TemplateType) { + $effectiveVariance = $typeList[$i]->getVariance(); + } + + $variance = $positionVariance->compose($effectiveVariance); foreach ($type->getReferencedTemplateTypes($variance) as $reference) { $references[] = $reference; } @@ -283,7 +318,42 @@ public function traverse(callable $cb): Type } if ($subtractedType !== $this->getSubtractedType() || $typesChanged) { - return $this->recreate($this->getClassName(), $types, $subtractedType); + return $this->recreate($this->getClassName(), $types, $subtractedType, $this->variances); + } + + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof TypeWithClassName) { + return $this; + } + + $ancestor = $right->getAncestorWithClassName($this->getClassName()); + if (!$ancestor instanceof self) { + return $this; + } + + if (count($this->types) !== count($ancestor->types)) { + return $this; + } + + $typesChanged = false; + $types = []; + foreach ($this->types as $i => $leftType) { + $rightType = $ancestor->types[$i]; + $newType = $cb($leftType, $rightType); + $types[] = $newType; + if ($newType === $leftType) { + continue; + } + + $typesChanged = true; + } + + if ($typesChanged) { + return $this->recreate($this->getClassName(), $types, null); } return $this; @@ -291,30 +361,32 @@ public function traverse(callable $cb): Type /** * @param Type[] $types + * @param TemplateTypeVariance[] $variances */ - protected function recreate(string $className, array $types, ?Type $subtractedType): self + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): self { return new self( $className, $types, $subtractedType, + null, + $variances, ); } public function changeSubtractedType(?Type $subtractedType): Type { - return new self($this->getClassName(), $this->types, $subtractedType); + return new self($this->getClassName(), $this->types, $subtractedType, null, $this->variances); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function toPhpDocNode(): TypeNode { - return new self( - $properties['className'], - $properties['types'], - $properties['subtractedType'] ?? null, + /** @var IdentifierTypeNode $parent */ + $parent = parent::toPhpDocNode(); + return new GenericTypeNode( + $parent, + array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->types), + array_map(static fn (TemplateTypeVariance $variance) => $variance->toPhpDocNodeVariance(), $this->variances), ); } diff --git a/src/Type/Generic/GenericStaticType.php b/src/Type/Generic/GenericStaticType.php new file mode 100644 index 0000000000..61692b704d --- /dev/null +++ b/src/Type/Generic/GenericStaticType.php @@ -0,0 +1,272 @@ + $types + * @param array $variances + */ + public function __construct( + private ClassReflection $classReflection, + private array $types, + private ?Type $subtractedType, + private array $variances, + ) + { + if (count($this->types) === 0) { + throw new ShouldNotHappenException('Cannot create GenericStaticType with zero types.'); + } + parent::__construct($classReflection, $subtractedType); + } + + /** + * @return array + */ + public function getTypes(): array + { + return $this->types; + } + + /** @return array */ + public function getVariances(): array + { + return $this->variances; + } + + public function getStaticObjectType(): ObjectType + { + if ($this->staticObjectType === null) { + if ($this->classReflection->isGeneric()) { + return $this->staticObjectType = new GenericObjectType( + $this->classReflection->getName(), + $this->types, + $this->subtractedType, + $this->classReflection, + $this->variances, + ); + } + + return $this->staticObjectType = parent::getStaticObjectType(); + } + + return $this->staticObjectType; + } + + public function changeBaseClass(ClassReflection $classReflection): StaticType + { + if ($classReflection->getName() === $this->getClassName()) { + return $this; + } + + if (!$classReflection->isGeneric()) { + return new StaticType($classReflection); + } + + $templateTags = $this->getClassReflection()->getTemplateTags(); + $i = 0; + $indexedTypes = []; + $indexedVariances = []; + foreach ($templateTags as $typeName => $tag) { + if (!array_key_exists($i, $this->types)) { + break; + } + if (!array_key_exists($i, $this->variances)) { + break; + } + $indexedTypes[$typeName] = $this->types[$i]; + $indexedVariances[$typeName] = $this->variances[$i]; + $i++; + } + + $newType = new GenericObjectType($classReflection->getName(), $classReflection->typeMapToList($classReflection->getTemplateTypeMap())); + $ancestorType = $newType->getAncestorWithClassName($this->getClassName()); + if ($ancestorType === null) { + return new self( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $this->subtractedType, + $classReflection->varianceMapToList($classReflection->getCallSiteVarianceMap()), + ); + } + + $ancestorClassReflection = $ancestorType->getClassReflection(); + if ($ancestorClassReflection === null) { + return new self( + $classReflection, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + $this->subtractedType, + $classReflection->varianceMapToList($classReflection->getCallSiteVarianceMap()), + ); + } + + $newClassTypes = []; + $newClassVariances = []; + foreach ($ancestorClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } + + if (!array_key_exists($typeName, $indexedTypes)) { + continue; + } + + $newClassTypes[$templateType->getName()] = $indexedTypes[$typeName]; + $newClassVariances[$templateType->getName()] = $indexedVariances[$typeName]; + } + + return new self($classReflection, $classReflection->typeMapToList(new TemplateTypeMap($newClassTypes)), $this->subtractedType, $classReflection->varianceMapToList(new TemplateTypeVarianceMap($newClassVariances))); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($type instanceof self) { + return $this->getStaticObjectType()->isSuperTypeOf($type->getStaticObjectType()); + } + + return parent::isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe()); + } + + public function traverse(callable $cb): Type + { + $subtractedType = $this->getSubtractedType() !== null ? $cb($this->getSubtractedType()) : null; + + $typesChanged = false; + $types = []; + foreach ($this->types as $type) { + $newType = $cb($type); + $types[] = $newType; + if ($newType === $type) { + continue; + } + + $typesChanged = true; + } + + if ($subtractedType !== $this->getSubtractedType() || $typesChanged) { + return new self( + $this->classReflection, + $types, + $subtractedType, + $this->variances, + ); + } + + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof TypeWithClassName) { + return $this; + } + + $ancestor = $right->getAncestorWithClassName($this->getClassName()); + if (!$ancestor instanceof self) { + return $this; + } + + if (count($this->types) !== count($ancestor->types)) { + return $this; + } + + $typesChanged = false; + $types = []; + foreach ($this->types as $i => $leftType) { + $rightType = $ancestor->types[$i]; + $newType = $cb($leftType, $rightType); + $types[] = $newType; + if ($newType === $leftType) { + continue; + } + + $typesChanged = true; + } + + if ($typesChanged) { + return new self( + $this->classReflection, + $types, + null, + $this->variances, + ); + } + + return $this; + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + if ($subtractedType !== null) { + $classReflection = $this->getClassReflection(); + if ($classReflection->getAllowedSubTypes() !== null) { + $objectType = $this->getStaticObjectType()->changeSubtractedType($subtractedType); + if ($objectType instanceof NeverType) { + return $objectType; + } + + if ($objectType instanceof ObjectType && $objectType->getSubtractedType() !== null) { + return new self($classReflection, $this->types, $objectType->getSubtractedType(), $this->variances); + } + + return TypeCombinator::intersect($this, $objectType); + } + } + + return new self( + $this->classReflection, + $this->types, + $subtractedType, + $this->variances, + ); + } + + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { + return $this->getStaticObjectType()->inferTemplateTypes($receivedType); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->getStaticObjectType()->getReferencedTemplateTypes($positionVariance); + } + + public function toPhpDocNode(): TypeNode + { + /** @var IdentifierTypeNode $parent */ + $parent = parent::toPhpDocNode(); + return new GenericTypeNode( + $parent, + array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->types), + array_map(static fn (TemplateTypeVariance $variance) => $variance->toPhpDocNodeVariance(), $this->variances), + ); + } + +} diff --git a/src/Type/Generic/TemplateArrayType.php b/src/Type/Generic/TemplateArrayType.php index 267411d479..e6658632cd 100644 --- a/src/Type/Generic/TemplateArrayType.php +++ b/src/Type/Generic/TemplateArrayType.php @@ -14,12 +14,16 @@ final class TemplateArrayType extends ArrayType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, ArrayType $bound, + ?Type $default, ) { parent::__construct($bound->getKeyType(), $bound->getItemType()); @@ -28,22 +32,7 @@ public function __construct( $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; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateBenevolentUnionType.php b/src/Type/Generic/TemplateBenevolentUnionType.php index 9ce83db602..aea8573131 100644 --- a/src/Type/Generic/TemplateBenevolentUnionType.php +++ b/src/Type/Generic/TemplateBenevolentUnionType.php @@ -12,12 +12,16 @@ final class TemplateBenevolentUnionType extends BenevolentUnionType implements T /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, BenevolentUnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -27,6 +31,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } /** @param Type[] $types */ @@ -38,23 +43,25 @@ public function withTypes(array $types): self $this->variance, $this->name, new BenevolentUnionType($types), + $this->default, ); } - public function traverse(callable $cb): Type + public function filterTypes(callable $filterCb): Type { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof BenevolentUnionType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound, + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), ); } - return $this; + return $result; } } diff --git a/src/Type/Generic/TemplateBooleanType.php b/src/Type/Generic/TemplateBooleanType.php index 54d55b7987..27fc50f21b 100644 --- a/src/Type/Generic/TemplateBooleanType.php +++ b/src/Type/Generic/TemplateBooleanType.php @@ -14,12 +14,16 @@ final class TemplateBooleanType extends BooleanType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, BooleanType $bound, + ?Type $default, ) { parent::__construct(); @@ -28,22 +32,7 @@ public function __construct( $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; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php index f4496e0b79..53ea994935 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -14,36 +14,25 @@ final class TemplateConstantArrayType extends ConstantArrayType implements Templ use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, ConstantArrayType $bound, + ?Type $default, ) { - parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndex(), $bound->getOptionalKeys()); + parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndexes(), $bound->getOptionalKeys(), $bound->isList()); $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; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantIntegerType.php b/src/Type/Generic/TemplateConstantIntegerType.php new file mode 100644 index 0000000000..a4bc35b848 --- /dev/null +++ b/src/Type/Generic/TemplateConstantIntegerType.php @@ -0,0 +1,43 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ConstantIntegerType $bound, + ?Type $default, + ) + { + parent::__construct($bound->getValue()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateConstantStringType.php b/src/Type/Generic/TemplateConstantStringType.php new file mode 100644 index 0000000000..f4d3b8dbbb --- /dev/null +++ b/src/Type/Generic/TemplateConstantStringType.php @@ -0,0 +1,43 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ConstantStringType $bound, + ?Type $default, + ) + { + parent::__construct($bound->getValue()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateFloatType.php b/src/Type/Generic/TemplateFloatType.php index ca69695ae4..b3df6ccd2e 100644 --- a/src/Type/Generic/TemplateFloatType.php +++ b/src/Type/Generic/TemplateFloatType.php @@ -14,12 +14,16 @@ final class TemplateFloatType extends FloatType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, FloatType $bound, + ?Type $default, ) { parent::__construct(); @@ -28,22 +32,7 @@ public function __construct( $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; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php index fc1b1d21af..0c58b3b41e 100644 --- a/src/Type/Generic/TemplateGenericObjectType.php +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -13,40 +13,29 @@ final class TemplateGenericObjectType extends GenericObjectType implements Templ /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, GenericObjectType $bound, + ?Type $default, ) { - parent::__construct($bound->getClassName(), $bound->getTypes()); + parent::__construct($bound->getClassName(), $bound->getTypes(), null, null, $bound->getVariances()); $this->scope = $scope; $this->strategy = $templateTypeStrategy; $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof GenericObjectType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound, - ); - } - - return $this; - } - - protected function recreate(string $className, array $types, ?Type $subtractedType): GenericObjectType + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): GenericObjectType { return new self( $this->scope, @@ -54,6 +43,7 @@ protected function recreate(string $className, array $types, ?Type $subtractedTy $this->variance, $this->name, $this->getBound(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateIntegerType.php b/src/Type/Generic/TemplateIntegerType.php index 2b636d3fe5..b4057fa327 100644 --- a/src/Type/Generic/TemplateIntegerType.php +++ b/src/Type/Generic/TemplateIntegerType.php @@ -14,12 +14,16 @@ final class TemplateIntegerType extends IntegerType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, IntegerType $bound, + ?Type $default, ) { parent::__construct(); @@ -28,22 +32,7 @@ public function __construct( $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 IntegerType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound, - ); - } - - return $this; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateIntersectionType.php b/src/Type/Generic/TemplateIntersectionType.php new file mode 100644 index 0000000000..7576541dbc --- /dev/null +++ b/src/Type/Generic/TemplateIntersectionType.php @@ -0,0 +1,37 @@ + */ + use TemplateTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + IntersectionType $bound, + ?Type $default, + ) + { + parent::__construct($bound->getTypes()); + + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + +} diff --git a/src/Type/Generic/TemplateKeyOfType.php b/src/Type/Generic/TemplateKeyOfType.php new file mode 100644 index 0000000000..d8522eb503 --- /dev/null +++ b/src/Type/Generic/TemplateKeyOfType.php @@ -0,0 +1,57 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + KeyOfType $bound, + ?Type $default, + ) + { + parent::__construct($bound->getType()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + + protected function getResult(): Type + { + $result = $this->getBound()->getResult(); + + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index fd4a74701b..8160633fc3 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -2,7 +2,8 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\StrictMixedType; use PHPStan\Type\Type; @@ -14,12 +15,16 @@ final class TemplateMixedType extends MixedType implements TemplateType /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, MixedType $bound, + ?Type $default, ) { parent::__construct(true); @@ -29,36 +34,21 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } - public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic + public function isSuperTypeOfMixed(MixedType $type): IsSuperTypeOfResult { return $this->isSuperTypeOf($type); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - $isSuperType = $this->isSuperTypeOf($acceptingType); + $isSuperType = $this->isSuperTypeOf($acceptingType)->toAcceptsResult(); if ($isSuperType->no()) { return $isSuperType; } - return TrinaryLogic::createYes(); - } - - public function traverse(callable $cb): Type - { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof MixedType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound, - ); - } - - return $this; + return AcceptsResult::createYes(); } public function toStrictMixedType(): TemplateStrictMixedType @@ -69,6 +59,7 @@ public function toStrictMixedType(): TemplateStrictMixedType $this->variance, $this->name, new StrictMixedType(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateObjectShapeType.php b/src/Type/Generic/TemplateObjectShapeType.php new file mode 100644 index 0000000000..270af37931 --- /dev/null +++ b/src/Type/Generic/TemplateObjectShapeType.php @@ -0,0 +1,43 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ObjectShapeType $bound, + ?Type $default, + ) + { + parent::__construct($bound->getProperties(), $bound->getOptionalProperties()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateObjectType.php b/src/Type/Generic/TemplateObjectType.php index 906bf0fe31..220414ca14 100644 --- a/src/Type/Generic/TemplateObjectType.php +++ b/src/Type/Generic/TemplateObjectType.php @@ -14,12 +14,16 @@ final class TemplateObjectType extends ObjectType implements TemplateType /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, ObjectType $bound, + ?Type $default, ) { parent::__construct($bound->getClassName()); @@ -29,22 +33,7 @@ public function __construct( $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 ObjectType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound, - ); - } - - return $this; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateObjectWithoutClassType.php b/src/Type/Generic/TemplateObjectWithoutClassType.php index 4ed0e8ab7b..7d6aebc6f9 100644 --- a/src/Type/Generic/TemplateObjectWithoutClassType.php +++ b/src/Type/Generic/TemplateObjectWithoutClassType.php @@ -14,12 +14,16 @@ class TemplateObjectWithoutClassType extends ObjectWithoutClassType implements T /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, ObjectWithoutClassType $bound, + ?Type $default, ) { parent::__construct(); @@ -29,22 +33,7 @@ public function __construct( $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 ObjectWithoutClassType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound, - ); - } - - return $this; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateStrictMixedType.php b/src/Type/Generic/TemplateStrictMixedType.php index c1d4e8a4ee..6feefd8696 100644 --- a/src/Type/Generic/TemplateStrictMixedType.php +++ b/src/Type/Generic/TemplateStrictMixedType.php @@ -2,7 +2,8 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\StrictMixedType; use PHPStan\Type\Type; @@ -14,12 +15,16 @@ final class TemplateStrictMixedType extends StrictMixedType implements TemplateT /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, StrictMixedType $bound, + ?Type $default, ) { $this->scope = $scope; @@ -27,32 +32,17 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } - public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic + public function isSuperTypeOfMixed(MixedType $type): IsSuperTypeOfResult { return $this->isSuperTypeOf($type); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - 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; + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } } diff --git a/src/Type/Generic/TemplateStringType.php b/src/Type/Generic/TemplateStringType.php index 20130c8690..1ae72a3384 100644 --- a/src/Type/Generic/TemplateStringType.php +++ b/src/Type/Generic/TemplateStringType.php @@ -14,12 +14,16 @@ final class TemplateStringType extends StringType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, StringType $bound, + ?Type $default, ) { parent::__construct(); @@ -28,22 +32,7 @@ public function __construct( $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 StringType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound, - ); - } - - return $this; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateType.php b/src/Type/Generic/TemplateType.php index fb1e5039bd..2dfa5dafbd 100644 --- a/src/Type/Generic/TemplateType.php +++ b/src/Type/Generic/TemplateType.php @@ -2,26 +2,31 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Type; /** @api */ interface TemplateType extends CompoundType { + /** @return non-empty-string */ public function getName(): string; public function getScope(): TemplateTypeScope; public function getBound(): Type; + public function getDefault(): ?Type; + public function toArgument(): TemplateType; public function isArgument(): bool; - public function isValidVariance(Type $a, Type $b): TrinaryLogic; + public function isValidVariance(Type $a, Type $b): IsSuperTypeOfResult; public function getVariance(): TemplateTypeVariance; + public function getStrategy(): TemplateTypeStrategy; + } diff --git a/src/Type/Generic/TemplateTypeArgumentStrategy.php b/src/Type/Generic/TemplateTypeArgumentStrategy.php index 290dcbd336..0e8f59cf5e 100644 --- a/src/Type/Generic/TemplateTypeArgumentStrategy.php +++ b/src/Type/Generic/TemplateTypeArgumentStrategy.php @@ -2,34 +2,37 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\Type; +use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** * Template type strategy suitable for return type acceptance contexts */ -class TemplateTypeArgumentStrategy implements TemplateTypeStrategy +final class TemplateTypeArgumentStrategy implements TemplateTypeStrategy { - public function accepts(TemplateType $left, Type $right, bool $strictTypes): TrinaryLogic + public function accepts(TemplateType $left, Type $right, bool $strictTypes): AcceptsResult { - if ($right instanceof IntersectionType) { - foreach ($right->getTypes() as $type) { - if ($this->accepts($left, $type, $strictTypes)->yes()) { - return TrinaryLogic::createYes(); - } - } - - return TrinaryLogic::createNo(); - } - if ($right instanceof CompoundType) { $accepts = $right->isAcceptedBy($left, $strictTypes); } else { $accepts = $left->getBound()->accepts($right, $strictTypes) - ->and(TrinaryLogic::createMaybe()); + ->and(AcceptsResult::createMaybe()); + if ($accepts->maybe()) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($left, $right); + + return new AcceptsResult($accepts->result, array_merge($accepts->reasons, [ + sprintf( + 'Type %s is not always the same as %s. It breaks the contract for some argument types, typically subtypes.', + $right->describe($verbosity), + $left->getName(), + ), + ])); + } } return $accepts; @@ -40,12 +43,4 @@ public function isArgument(): bool return true; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self(); - } - } diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index 6bad394abf..8f56cc6cbb 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -7,9 +7,14 @@ use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; +use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; @@ -20,71 +25,94 @@ final class TemplateTypeFactory { - public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance): TemplateType + /** + * @param non-empty-string $name + */ + public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance, ?TemplateTypeStrategy $strategy = null, ?Type $default = null): TemplateType { - $strategy = new TemplateTypeParameterStrategy(); + $strategy ??= new TemplateTypeParameterStrategy(); if ($bound === null) { - return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true)); + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } $boundClass = get_class($bound); if ($bound instanceof ObjectType && ($boundClass === ObjectType::class || $bound instanceof TemplateType)) { - return new TemplateObjectType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof GenericObjectType && ($boundClass === GenericObjectType::class || $bound instanceof TemplateType)) { - return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound); + return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ObjectWithoutClassType && ($boundClass === ObjectWithoutClassType::class || $bound instanceof TemplateType)) { - return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ArrayType && ($boundClass === ArrayType::class || $bound instanceof TemplateType)) { - return new TemplateArrayType($scope, $strategy, $variance, $name, $bound); + return new TemplateArrayType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ConstantArrayType && ($boundClass === ConstantArrayType::class || $bound instanceof TemplateType)) { - return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound); + return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof ObjectShapeType && ($boundClass === ObjectShapeType::class || $bound instanceof TemplateType)) { + return new TemplateObjectShapeType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof StringType && ($boundClass === StringType::class || $bound instanceof TemplateType)) { - return new TemplateStringType($scope, $strategy, $variance, $name, $bound); + return new TemplateStringType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof ConstantStringType && ($boundClass === ConstantStringType::class || $bound instanceof TemplateType)) { + return new TemplateConstantStringType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof IntegerType && ($boundClass === IntegerType::class || $bound instanceof TemplateType)) { - return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound); + return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof ConstantIntegerType && ($boundClass === ConstantIntegerType::class || $bound instanceof TemplateType)) { + return new TemplateConstantIntegerType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof FloatType && ($boundClass === FloatType::class || $bound instanceof TemplateType)) { - return new TemplateFloatType($scope, $strategy, $variance, $name, $bound); + return new TemplateFloatType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof BooleanType && ($boundClass === BooleanType::class || $bound instanceof TemplateType)) { - return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound); + return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof MixedType && ($boundClass === MixedType::class || $bound instanceof TemplateType)) { - return new TemplateMixedType($scope, $strategy, $variance, $name, $bound); + return new TemplateMixedType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof UnionType) { if ($boundClass === UnionType::class || $bound instanceof TemplateUnionType) { - return new TemplateUnionType($scope, $strategy, $variance, $name, $bound); + return new TemplateUnionType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof BenevolentUnionType) { - return new TemplateBenevolentUnionType($scope, $strategy, $variance, $name, $bound); + return new TemplateBenevolentUnionType($scope, $strategy, $variance, $name, $bound, $default); } } - return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true)); + if ($bound instanceof IntersectionType) { + return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof KeyOfType && ($boundClass === KeyOfType::class || $bound instanceof TemplateType)) { + return new TemplateKeyOfType($scope, $strategy, $variance, $name, $bound, $default); + } + + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } public static function fromTemplateTag(TemplateTypeScope $scope, TemplateTag $tag): TemplateType { - return self::create($scope, $tag->getName(), $tag->getBound(), $tag->getVariance()); + return self::create($scope, $tag->getName(), $tag->getBound(), $tag->getVariance(), null, $tag->getDefault()); } } diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index f8e72514cf..29ecd48720 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -2,29 +2,65 @@ namespace PHPStan\Type\Generic; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Type\ErrorType; +use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\VerbosityLevel; -class TemplateTypeHelper +final class TemplateTypeHelper { /** * Replaces template types with standin types */ - public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standins, bool $keepErrorTypes = false): Type + public static function resolveTemplateTypes( + Type $type, + TemplateTypeMap $standins, + TemplateTypeVarianceMap $callSiteVariances, + TemplateTypeVariance $positionVariance, + bool $keepErrorTypes = false, + ): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins, $keepErrorTypes): Type { + $references = $type->getReferencedTemplateTypes($positionVariance); + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins, $references, $callSiteVariances, $keepErrorTypes): Type { if ($type instanceof TemplateType && !$type->isArgument()) { $newType = $standins->getType($type->getName()); + + $variance = TemplateTypeVariance::createInvariant(); + foreach ($references as $reference) { + // this uses identity to distinguish between different occurrences of the same template type + // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details + if ($reference->getType() === $type) { + $variance = $reference->getPositionVariance(); + break; + } + } + if ($newType === null) { return $traverse($type); } if ($newType instanceof ErrorType && !$keepErrorTypes) { + return $traverse($type->getDefault() ?? $type->getBound()); + } + + $callSiteVariance = $callSiteVariances->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { return $traverse($type->getBound()); } + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + return $newType; } @@ -32,6 +68,17 @@ public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standin }); } + public static function resolveToDefaults(Type $type): Type + { + return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof TemplateType) { + return $traverse($type->getDefault() ?? $type->getBound()); + } + + return $traverse($type); + }); + } + public static function resolveToBounds(Type $type): Type { return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { @@ -50,8 +97,35 @@ public static function resolveToBounds(Type $type): Type */ public static function toArgument(Type $type): Type { + $ownedTemplates = []; + /** @var T */ - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$ownedTemplates): Type { + if ($type instanceof ParametersAcceptor) { + $templateTypeMap = $type->getTemplateTypeMap(); + + foreach ($type->getParameters() as $parameter) { + $parameterType = $parameter->getType(); + if (!($parameterType instanceof TemplateType) || !$templateTypeMap->hasType($parameterType->getName())) { + continue; + } + + $ownedTemplates[] = $parameterType; + } + + $returnType = $type->getReturnType(); + + if ($returnType instanceof TemplateType && $templateTypeMap->hasType($returnType->getName())) { + $ownedTemplates[] = $returnType; + } + } + + foreach ($ownedTemplates as $ownedTemplate) { + if ($ownedTemplate === $type) { + return $traverse($type); + } + } + if ($type instanceof TemplateType) { return $traverse($type->toArgument()); } @@ -60,4 +134,18 @@ public static function toArgument(Type $type): Type }); } + public static function generalizeInferredTemplateType(TemplateType $templateType, Type $type): Type + { + if (!$templateType->getVariance()->covariant()) { + $isArrayKey = $templateType->getBound()->describe(VerbosityLevel::precise()) === '(int|string)'; + if ($type->isScalar()->yes() && $isArrayKey) { + $type = $type->generalize(GeneralizePrecision::templateArgument()); + } elseif ($type->isConstantValue()->yes() && (!$templateType->getBound()->isScalar()->yes() || $isArrayKey)) { + $type = $type->generalize(GeneralizePrecision::templateArgument()); + } + } + + return $type; + } + } diff --git a/src/Type/Generic/TemplateTypeMap.php b/src/Type/Generic/TemplateTypeMap.php index f807e3d65e..49f3a08496 100644 --- a/src/Type/Generic/TemplateTypeMap.php +++ b/src/Type/Generic/TemplateTypeMap.php @@ -5,12 +5,15 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use function array_key_exists; use function count; -/** @api */ -class TemplateTypeMap +/** + * @api + */ +final class TemplateTypeMap { private static ?TemplateTypeMap $empty = null; @@ -208,18 +211,10 @@ public function resolveToBounds(): self if ($this->resolvedToBounds !== null) { return $this->resolvedToBounds; } - return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveToBounds($type)); - } - - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['types'], - $properties['lowerBoundTypes'] ?? [], - ); + return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TypeTraverser::map( + $type, + static fn (Type $type, callable $traverse): Type => $type instanceof TemplateType ? $traverse($type->getDefault() ?? $type->getBound()) : $traverse($type), + )); } } diff --git a/src/Type/Generic/TemplateTypeParameterStrategy.php b/src/Type/Generic/TemplateTypeParameterStrategy.php index 5d9a8256d6..3e18bccf2d 100644 --- a/src/Type/Generic/TemplateTypeParameterStrategy.php +++ b/src/Type/Generic/TemplateTypeParameterStrategy.php @@ -2,17 +2,17 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\Type; /** * Template type strategy suitable for parameter type acceptance contexts */ -class TemplateTypeParameterStrategy implements TemplateTypeStrategy +final class TemplateTypeParameterStrategy implements TemplateTypeStrategy { - public function accepts(TemplateType $left, Type $right, bool $strictTypes): TrinaryLogic + public function accepts(TemplateType $left, Type $right, bool $strictTypes): AcceptsResult { if ($right instanceof CompoundType) { return $right->isAcceptedBy($left, $strictTypes); @@ -26,12 +26,4 @@ public function isArgument(): bool return false; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self(); - } - } diff --git a/src/Type/Generic/TemplateTypeReference.php b/src/Type/Generic/TemplateTypeReference.php index 260abd3d93..0be67d5e08 100644 --- a/src/Type/Generic/TemplateTypeReference.php +++ b/src/Type/Generic/TemplateTypeReference.php @@ -2,7 +2,7 @@ namespace PHPStan\Type\Generic; -class TemplateTypeReference +final class TemplateTypeReference { public function __construct(private TemplateType $type, private TemplateTypeVariance $positionVariance) diff --git a/src/Type/Generic/TemplateTypeScope.php b/src/Type/Generic/TemplateTypeScope.php index f9a7625720..f362ecadd4 100644 --- a/src/Type/Generic/TemplateTypeScope.php +++ b/src/Type/Generic/TemplateTypeScope.php @@ -4,9 +4,14 @@ use function sprintf; -class TemplateTypeScope +final class TemplateTypeScope { + public static function createWithAnonymousFunction(): self + { + return new self(null, null); + } + public static function createWithFunction(string $functionName): self { return new self(null, $functionName); @@ -48,6 +53,10 @@ public function equals(self $other): bool /** @api */ public function describe(): string { + if ($this->className === null && $this->functionName === null) { + return 'anonymous function'; + } + if ($this->className === null) { return sprintf('function %s()', $this->functionName); } @@ -59,15 +68,4 @@ public function describe(): string return sprintf('method %s::%s()', $this->className, $this->functionName); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['className'], - $properties['functionName'], - ); - } - } diff --git a/src/Type/Generic/TemplateTypeStrategy.php b/src/Type/Generic/TemplateTypeStrategy.php index d90dc732e1..843710ae7c 100644 --- a/src/Type/Generic/TemplateTypeStrategy.php +++ b/src/Type/Generic/TemplateTypeStrategy.php @@ -2,13 +2,13 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\Type; interface TemplateTypeStrategy { - public function accepts(TemplateType $left, Type $right, bool $strictTypes): TrinaryLogic; + public function accepts(TemplateType $left, Type $right, bool $strictTypes): AcceptsResult; public function isArgument(): bool; diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 2e9d3a9f5a..52c13a9680 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -2,13 +2,18 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; -use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\SubtractableType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -19,6 +24,7 @@ trait TemplateTypeTrait { + /** @var non-empty-string */ private string $name; private TemplateTypeScope $scope; @@ -30,6 +36,9 @@ trait TemplateTypeTrait /** @var TBound */ private Type $bound; + private ?Type $default; + + /** @return non-empty-string */ public function getName(): string { return $this->name; @@ -46,19 +55,26 @@ public function getBound(): Type return $this->bound; } + public function getDefault(): ?Type + { + return $this->default; + } + public function describe(VerbosityLevel $level): string { $basicDescription = function () use ($level): string { - // @phpstan-ignore-next-line + // @phpstan-ignore booleanAnd.alwaysFalse, instanceof.alwaysFalse, booleanAnd.alwaysFalse, instanceof.alwaysFalse, instanceof.alwaysTrue if ($this->bound instanceof MixedType && $this->bound->getSubtractedType() === null && !$this->bound instanceof TemplateMixedType) { $boundDescription = ''; - } else { // @phpstan-ignore-line + } else { $boundDescription = sprintf(' of %s', $this->bound->describe($level)); } + $defaultDescription = $this->default !== null ? sprintf(' = %s', $this->default->describe($level)) : ''; return sprintf( - '%s%s', + '%s%s%s', $this->name, $boundDescription, + $defaultDescription, ); }; @@ -82,74 +98,66 @@ public function toArgument(): TemplateType $this->variance, $this->name, TemplateTypeHelper::toArgument($this->getBound()), + $this->default !== null ? TemplateTypeHelper::toArgument($this->default) : null, ); } - public function isValidVariance(Type $a, Type $b): TrinaryLogic + public function isValidVariance(Type $a, Type $b): IsSuperTypeOfResult { - return $this->variance->isValidVariance($a, $b); + return $this->variance->isValidVariance($this, $a, $b); } public function subtract(Type $typeToRemove): Type { $removedBound = TypeCombinator::remove($this->getBound(), $typeToRemove); - $type = TemplateTypeFactory::create( + return TemplateTypeFactory::create( $this->getScope(), $this->getName(), $removedBound, $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), ); - if ($this->isArgument()) { - return TemplateTypeHelper::toArgument($type); - } - - return $type; } public function getTypeWithoutSubtractedType(): Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line + if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue return $this; } - $type = TemplateTypeFactory::create( + return TemplateTypeFactory::create( $this->getScope(), $this->getName(), $bound->getTypeWithoutSubtractedType(), $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), ); - if ($this->isArgument()) { - return TemplateTypeHelper::toArgument($type); - } - - return $type; } public function changeSubtractedType(?Type $subtractedType): Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line + if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue return $this; } - $type = TemplateTypeFactory::create( + return TemplateTypeFactory::create( $this->getScope(), $this->getName(), $bound->changeSubtractedType($subtractedType), $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), ); - if ($this->isArgument()) { - return TemplateTypeHelper::toArgument($type); - } - - return $type; } public function getSubtractedType(): ?Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line + if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue return null; } @@ -161,12 +169,16 @@ public function equals(Type $type): bool return $type instanceof self && $type->scope->equals($this->scope) && $type->name === $this->name - && $this->bound->equals($type->bound); + && $this->bound->equals($type->bound) + && ( + ($this->default === null && $type->default === null) + || ($this->default !== null && $type->default !== null && $this->default->equals($type->default)) + ); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - /** @var Type $bound */ + /** @var TBound $bound */ $bound = $this->getBound(); if ( !$acceptingType instanceof $bound @@ -186,27 +198,31 @@ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLog } return $acceptingType->getBound()->accepts($this->getBound(), $strictTypes) - ->and(TrinaryLogic::createMaybe()); + ->and(new AcceptsResult(TrinaryLogic::createMaybe(), [])); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { return $this->strategy->accepts($this, $type, $strictTypes); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof TemplateType || $type instanceof IntersectionType) { return $type->isSubTypeOf($this); } + if ($type instanceof NeverType) { + return IsSuperTypeOfResult::createYes(); + } + return $this->getBound()->isSuperTypeOf($type) - ->and(TrinaryLogic::createMaybe()); + ->and(IsSuperTypeOfResult::createMaybe()); } - public function isSubTypeOf(Type $type): TrinaryLogic + public function isSubTypeOf(Type $type): IsSuperTypeOfResult { - /** @var Type $bound */ + /** @var TBound $bound */ $bound = $this->getBound(); if ( !$type instanceof $bound @@ -226,15 +242,21 @@ public function isSubTypeOf(Type $type): TrinaryLogic } return $type->getBound()->isSuperTypeOf($this->getBound()) - ->and(TrinaryLogic::createMaybe()); + ->and(IsSuperTypeOfResult::createMaybe()); } - public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + public function toArrayKey(): Type { - if (!$receivedType instanceof TemplateType && ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType)) { - return $receivedType->inferTemplateTypesOn($this); - } + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { if ( $receivedType instanceof TemplateType && $this->getBound()->isSuperTypeOf($receivedType->getBound())->yes() @@ -245,10 +267,15 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap } $map = $this->getBound()->inferTemplateTypes($receivedType); - $resolvedBound = TemplateTypeHelper::resolveTemplateTypes($this->getBound(), $map); + $resolvedBound = TypeUtils::resolveLateResolvableTypes(TemplateTypeHelper::resolveTemplateTypes( + $this->getBound(), + $map, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createStatic(), + )); if ($resolvedBound->isSuperTypeOf($receivedType)->yes()) { return (new TemplateTypeMap([ - $this->name => $this->shouldGeneralizeInferredType() ? $receivedType->generalize(GeneralizePrecision::templateArgument()) : $receivedType, + $this->name => $receivedType, ]))->union($map); } @@ -265,32 +292,78 @@ public function getVariance(): TemplateTypeVariance return $this->variance; } + public function getStrategy(): TemplateTypeStrategy + { + return $this->strategy; + } + protected function shouldGeneralizeInferredType(): bool { return true; } - public function tryRemove(Type $typeToRemove): ?Type + public function traverse(callable $cb): Type { - if ($this->getBound()->isSuperTypeOf($typeToRemove)->yes()) { - return $this->subtract($typeToRemove); + $bound = $cb($this->getBound()); + $default = $this->getDefault() !== null ? $cb($this->getDefault()) : null; + + if ($this->getBound() === $bound && $this->getDefault() === $default) { + return $this; } - return null; + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $bound, + $this->getVariance(), + $this->getStrategy(), + $default, + ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type { - return new self( - $properties['scope'], - $properties['strategy'], - $properties['variance'], - $properties['name'], - $properties['bound'], + if (!$right instanceof TemplateType) { + return $this; + } + + $bound = $cb($this->getBound(), $right->getBound()); + $default = $this->getDefault() !== null && $right->getDefault() !== null ? $cb($this->getDefault(), $right->getDefault()) : null; + + if ($this->getBound() === $bound && $this->getDefault() === $default) { + return $this; + } + + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $bound, + $this->getVariance(), + $this->getStrategy(), + $default, ); } + public function tryRemove(Type $typeToRemove): ?Type + { + $bound = TypeCombinator::remove($this->getBound(), $typeToRemove); + if ($this->getBound() === $bound) { + return null; + } + + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $bound, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->name); + } + } diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index edd59b7c24..a630895bed 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -2,21 +2,27 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use function sprintf; -/** @api */ -class TemplateTypeVariance +/** + * @api + */ +final class TemplateTypeVariance { private const INVARIANT = 1; private const COVARIANT = 2; private const CONTRAVARIANT = 3; private const STATIC = 4; + private const BIVARIANT = 5; /** @var self[] */ private static array $registry; @@ -51,6 +57,11 @@ public static function createStatic(): self return self::create(self::STATIC); } + public static function createBivariant(): self + { + return self::create(self::BIVARIANT); + } + public function invariant(): bool { return $this->value === self::INVARIANT; @@ -71,6 +82,11 @@ public function static(): bool return $this->value === self::STATIC; } + public function bivariant(): bool + { + return $this->value === self::BIVARIANT; + } + public function compose(self $other): self { if ($this->contravariant()) { @@ -80,50 +96,79 @@ public function compose(self $other): self if ($other->covariant()) { return self::createContravariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } return self::createInvariant(); } if ($this->covariant()) { if ($other->contravariant()) { - return self::createCovariant(); + return self::createContravariant(); } if ($other->covariant()) { return self::createCovariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } + return self::createInvariant(); + } + + if ($this->invariant()) { return self::createInvariant(); } + if ($this->bivariant()) { + return self::createBivariant(); + } + return $other; } - public function isValidVariance(Type $a, Type $b): TrinaryLogic + public function isValidVariance(TemplateType $templateType, Type $a, Type $b): IsSuperTypeOfResult { if ($b instanceof NeverType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($a instanceof MixedType && !$a instanceof TemplateType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($a instanceof BenevolentUnionType) { if (!$a->isSuperTypeOf($b)->no()) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } } if ($b instanceof BenevolentUnionType) { if (!$b->isSuperTypeOf($a)->no()) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } } if ($b instanceof MixedType && !$b instanceof TemplateType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($this->invariant()) { - return TrinaryLogic::createFromBoolean($a->equals($b)); + $result = $a->equals($b); + $reasons = []; + if (!$result) { + if ( + $templateType->getScope()->getClassName() !== null + && $a->isSuperTypeOf($b)->yes() + ) { + $reasons[] = sprintf( + 'Template type %s on class %s is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + $templateType->getName(), + $templateType->getScope()->getClassName(), + ); + } + } + + return new IsSuperTypeOfResult(TrinaryLogic::createFromBoolean($result), $reasons); } if ($this->covariant()) { @@ -134,6 +179,10 @@ public function isValidVariance(Type $a, Type $b): TrinaryLogic return $b->isSuperTypeOf($a); } + if ($this->bivariant()) { + return IsSuperTypeOfResult::createYes(); + } + throw new ShouldNotHappenException(); } @@ -146,6 +195,7 @@ public function validPosition(self $other): bool { return $other->value === $this->value || $other->invariant() + || $this->bivariant() || $this->static(); } @@ -160,17 +210,30 @@ public function describe(): string return 'contravariant'; case self::STATIC: return 'static'; + case self::BIVARIANT: + return 'bivariant'; } throw new ShouldNotHappenException(); } /** - * @param array{value: int} $properties + * @return GenericTypeNode::VARIANCE_* */ - public static function __set_state(array $properties): self + public function toPhpDocNodeVariance(): string { - return new self($properties['value']); + switch ($this->value) { + case self::INVARIANT: + return GenericTypeNode::VARIANCE_INVARIANT; + case self::COVARIANT: + return GenericTypeNode::VARIANCE_COVARIANT; + case self::CONTRAVARIANT: + return GenericTypeNode::VARIANCE_CONTRAVARIANT; + case self::BIVARIANT: + return GenericTypeNode::VARIANCE_BIVARIANT; + } + + throw new ShouldNotHappenException(); } } diff --git a/src/Type/Generic/TemplateTypeVarianceMap.php b/src/Type/Generic/TemplateTypeVarianceMap.php new file mode 100644 index 0000000000..072c7952e5 --- /dev/null +++ b/src/Type/Generic/TemplateTypeVarianceMap.php @@ -0,0 +1,53 @@ + $variances + */ + public function __construct(private array $variances) + { + } + + public static function createEmpty(): self + { + $empty = self::$empty; + + if ($empty !== null) { + return $empty; + } + + $empty = new self([]); + self::$empty = $empty; + + return $empty; + } + + /** @return array */ + public function getVariances(): array + { + return $this->variances; + } + + public function hasVariance(string $name): bool + { + return array_key_exists($name, $this->getVariances()); + } + + public function getVariance(string $name): ?TemplateTypeVariance + { + return $this->getVariances()[$name] ?? null; + } + +} diff --git a/src/Type/Generic/TemplateUnionType.php b/src/Type/Generic/TemplateUnionType.php index 521a3fb536..dc58af565a 100644 --- a/src/Type/Generic/TemplateUnionType.php +++ b/src/Type/Generic/TemplateUnionType.php @@ -12,12 +12,16 @@ final class TemplateUnionType extends UnionType implements TemplateType /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, UnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -27,22 +31,24 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } - public function traverse(callable $cb): Type + public function filterTypes(callable $filterCb): Type { - $newBound = $cb($this->getBound()); - if ($this->getBound() !== $newBound && $newBound instanceof UnionType) { - return new self( - $this->scope, - $this->strategy, - $this->variance, - $this->name, - $newBound, + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), ); } - return $this; + return $result; } } diff --git a/src/Type/Generic/TypeProjectionHelper.php b/src/Type/Generic/TypeProjectionHelper.php new file mode 100644 index 0000000000..c311cde2f8 --- /dev/null +++ b/src/Type/Generic/TypeProjectionHelper.php @@ -0,0 +1,31 @@ +describe($level); + + if ($variance === null || $variance->invariant()) { + return $describedType; + } + + if ($variance->bivariant()) { + return '*'; + } + + return sprintf('%s %s', $variance->describe(), $describedType); + } + +} diff --git a/src/Type/GenericTypeVariableResolver.php b/src/Type/GenericTypeVariableResolver.php deleted file mode 100644 index fc670ee662..0000000000 --- a/src/Type/GenericTypeVariableResolver.php +++ /dev/null @@ -1,47 +0,0 @@ -getClassReflection(); - if ($classReflection === null) { - return null; - } - $ancestorClassReflection = $classReflection->getAncestorWithClassName($genericClassName); - if ($ancestorClassReflection === null) { - return null; - } - - $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 $type; - } - -} diff --git a/src/Type/Helper/GetTemplateTypeType.php b/src/Type/Helper/GetTemplateTypeType.php new file mode 100644 index 0000000000..c45a7be5fa --- /dev/null +++ b/src/Type/Helper/GetTemplateTypeType.php @@ -0,0 +1,106 @@ +type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('template-type<%s, %s, %s>', $this->type->describe($level), $this->ancestorClassName, $this->templateTypeName); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + return $this->type->getTemplateType($this->ancestorClassName, $this->templateTypeName); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type, $this->ancestorClassName, $this->templateTypeName); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type, $this->ancestorClassName, $this->templateTypeName); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode( + new IdentifierTypeNode('template-type'), + [ + $this->type->toPhpDocNode(), + new IdentifierTypeNode($this->ancestorClassName), + new ConstTypeNode(new ConstExprStringNode($this->templateTypeName, ConstExprStringNode::SINGLE_QUOTED)), + ], + ); + } + +} diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index cc1e2035fd..1c956015dd 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -2,13 +2,28 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use function array_filter; +use function array_map; use function assert; use function ceil; +use function count; use function floor; use function get_class; +use function is_float; use function is_int; use function max; use function min; @@ -20,9 +35,8 @@ class IntegerRangeType extends IntegerType implements CompoundType { - public function __construct(private ?int $min, private ?int $max) + private 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); @@ -146,19 +160,16 @@ public function getMin(): ?int return $this->min; } - public function getMax(): ?int { return $this->max; } - public function describe(VerbosityLevel $level): string { return sprintf('int<%s, %s>', $this->min ?? 'min', $this->max ?? 'max'); } - public function shift(int $amount): Type { if ($amount === 0) { @@ -193,22 +204,20 @@ public function shift(int $amount): Type return self::fromInterval($min, $max); } - - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof parent) { - return $this->isSuperTypeOf($type); + return $this->isSuperTypeOf($type)->toAcceptsResult(); } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self || $type instanceof ConstantIntegerType) { if ($type instanceof self) { @@ -220,46 +229,66 @@ public function isSuperTypeOf(Type $type): TrinaryLogic } if (self::isDisjoint($this->min, $this->max, $typeMin, $typeMax)) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } if ( ($this->min === null || $typeMin !== null && $this->min <= $typeMin) && ($this->max === null || $typeMax !== null && $this->max >= $typeMax) ) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof parent) { return $otherType->isSuperTypeOf($this); } - if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + if ($otherType instanceof UnionType) { + return $this->isSubTypeOfUnionWithReason($otherType); + } + + if ($otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + private function isSubTypeOfUnionWithReason(UnionType $otherType): IsSuperTypeOfResult { - return $this->isSubTypeOf($acceptingType); + if ($this->min !== null && $this->max !== null) { + $matchingConstantIntegers = array_filter( + $otherType->getTypes(), + fn (Type $type): bool => $type instanceof ConstantIntegerType && $type->getValue() >= $this->min && $type->getValue() <= $this->max, + ); + + if (count($matchingConstantIntegers) === ($this->max - $this->min + 1)) { + return IsSuperTypeOfResult::createYes(); + } + } + + return IsSuperTypeOfResult::createNo()->or(...array_map(fn (Type $innerType) => $this->isSubTypeOf($innerType), $otherType->getTypes())); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -267,81 +296,120 @@ public function equals(Type $type): bool return $type instanceof self && $this->min === $type->min && $this->max === $type->max; } - public function generalize(GeneralizePrecision $precision): Type { - return new parent(); + return new IntegerType(); } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($this->min === null) { $minIsSmaller = TrinaryLogic::createYes(); } else { - $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThan($otherType); + $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThan($otherType, $phpVersion); } if ($this->max === null) { $maxIsSmaller = TrinaryLogic::createNo(); } else { - $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType); + $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType, $phpVersion); + } + + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $zeroInt->isSmallerThan($otherType, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); } return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($this->min === null) { $minIsSmaller = TrinaryLogic::createYes(); } else { - $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThanOrEqual($otherType); + $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThanOrEqual($otherType, $phpVersion); } if ($this->max === null) { $maxIsSmaller = TrinaryLogic::createNo(); } else { - $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType); + $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType, $phpVersion); + } + + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $zeroInt->isSmallerThanOrEqual($otherType, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); } return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($this->min === null) { $minIsSmaller = TrinaryLogic::createNo(); } else { - $minIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->min))); + $minIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->min)), $phpVersion); } if ($this->max === null) { $maxIsSmaller = TrinaryLogic::createYes(); } else { - $maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max))); + $maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max)), $phpVersion); + } + + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $otherType->isSmallerThan($zeroInt, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); } return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($this->min === null) { $minIsSmaller = TrinaryLogic::createNo(); } else { - $minIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->min))); + $minIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->min)), $phpVersion); } if ($this->max === null) { $maxIsSmaller = TrinaryLogic::createYes(); } else { - $maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max))); + $maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max)), $phpVersion); + } + + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $otherType->isSmallerThanOrEqual($zeroInt, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); } return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(true), @@ -354,7 +422,7 @@ public function getSmallerType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = []; @@ -365,7 +433,7 @@ public function getSmallerOrEqualType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new NullType(), @@ -383,7 +451,7 @@ public function getGreaterType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = []; @@ -413,6 +481,47 @@ public function toBoolean(): BooleanType return new ConstantBooleanType(false); } + public function toAbsoluteNumber(): Type + { + if ($this->min !== null && $this->min >= 0) { + return $this; + } + + if ($this->max === null || $this->max >= 0) { + $inversedMin = $this->min !== null ? $this->min * -1 : null; + + return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null); + } + + return self::fromInterval($this->max * -1, $this->min !== null ? $this->min * -1 : null); + } + + public function toString(): Type + { + $finiteTypes = $this->getFiniteTypes(); + if ($finiteTypes !== []) { + return TypeCombinator::union(...$finiteTypes)->toString(); + } + + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this); + if ($isZero->no()) { + return new IntersectionType([ + new StringType(), + new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), + new AccessoryNumericStringType(), + new AccessoryNonFalsyStringType(), + ]); + } + + return new IntersectionType([ + new StringType(), + new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), + new AccessoryNumericStringType(), + ]); + } + /** * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType * @@ -538,12 +647,111 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function exponentiate(Type $exponent): Type + { + if ($exponent instanceof UnionType) { + $results = []; + foreach ($exponent->getTypes() as $unionType) { + $results[] = $this->exponentiate($unionType); + } + return TypeCombinator::union(...$results); + } + + if ($exponent instanceof IntegerRangeType) { + $min = null; + $max = null; + if ($this->getMin() !== null && $exponent->getMin() !== null) { + $min = $this->getMin() ** $exponent->getMin(); + } + if ($this->getMax() !== null && $exponent->getMax() !== null) { + $max = $this->getMax() ** $exponent->getMax(); + } + + if (($min !== null || $max !== null) && !is_float($min) && !is_float($max)) { + return self::fromInterval($min, $max); + } + } + + if ($exponent instanceof ConstantScalarType) { + $exponentValue = $exponent->getValue(); + if (is_int($exponentValue)) { + $min = null; + $max = null; + if ($this->getMin() !== null) { + $min = $this->getMin() ** $exponentValue; + } + if ($this->getMax() !== null) { + $max = $this->getMax() ** $exponentValue; + } + + if (!is_float($min) && !is_float($max)) { + return self::fromInterval($min, $max); + } + } + } + + return parent::exponentiate($exponent); + } + /** - * @param mixed[] $properties + * @return list */ - public static function __set_state(array $properties): Type + public function getFiniteTypes(): array + { + if ($this->min === null || $this->max === null) { + return []; + } + + $size = $this->max - $this->min; + if ($size > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + $types = []; + for ($i = $this->min; $i <= $this->max; $i++) { + $types[] = new ConstantIntegerType($i); + } + + return $types; + } + + public function toPhpDocNode(): TypeNode + { + if ($this->min === null) { + $min = new IdentifierTypeNode('min'); + } else { + $min = new ConstTypeNode(new ConstExprIntegerNode((string) $this->min)); + } + + if ($this->max === null) { + $max = new IdentifierTypeNode('max'); + } else { + $max = new ConstTypeNode(new ConstExprIntegerNode((string) $this->max)); + } + + return new GenericTypeNode(new IdentifierTypeNode('int'), [$min, $max]); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return new self($properties['min'], $properties['max']); + $zeroInt = new ConstantIntegerType(0); + if ($zeroInt->isSuperTypeOf($this)->no()) { + if ($type->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + if ($type->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + } + + if ( + $this->isSmallerThan($type, $phpVersion)->yes() + || $this->isGreaterThan($type, $phpVersion)->yes() + ) { + return new ConstantBooleanType(false); + } + + return parent::looseCompare($type, $phpVersion); } } diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 3ff1e48778..fcb6fcd893 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -2,9 +2,17 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -19,6 +27,7 @@ class IntegerType implements Type { use JustNullableTypeTrait; + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -38,12 +47,9 @@ public function describe(VerbosityLevel $level): string return 'int'; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function getConstantStrings(): array { - return new self(); + return []; } public function toNumber(): Type @@ -51,6 +57,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return IntegerRangeType::createAllGreaterThanOrEqualTo(0); + } + public function toFloat(): Type { return new FloatType(); @@ -65,6 +76,8 @@ public function toString(): Type { return new IntersectionType([ new StringType(), + new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } @@ -74,10 +87,83 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean()); + } + + return TypeCombinator::union($this, $this->toFloat()); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isArray()->yes()) { + return new ConstantBooleanType(false); + } + + if ( + $phpVersion->nonNumericStringAndIntegerIsFalseOnLooseComparison() + && $type->isString()->yes() + && $type->isNumericString()->no() + ) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + public function tryRemove(Type $typeToRemove): ?Type { if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { @@ -99,4 +185,19 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function getFiniteTypes(): array + { + return []; + } + + public function exponentiate(Type $exponent): Type + { + return ExponentiateHelper::exponentiate($this, $exponent); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('int'); + } + } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index c79e878cca..89c00f26d2 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -2,11 +2,17 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection; @@ -14,23 +20,41 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; +use function array_intersect_key; use function array_map; +use function array_shift; +use function array_unique; +use function array_values; use function count; use function implode; use function in_array; +use function is_int; +use function ksort; +use function md5; use function sprintf; +use function strcasecmp; use function strlen; use function substr; +use function usort; /** @api */ class IntersectionType implements CompoundType @@ -39,14 +63,13 @@ class IntersectionType implements CompoundType use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; - /** @var Type[] */ - private array $types; + private bool $sortedTypes = false; /** * @api * @param Type[] $types */ - public function __construct(array $types) + public function __construct(private array $types) { if (count($types) < 2) { throw new ShouldNotHappenException(sprintf( @@ -55,8 +78,6 @@ public function __construct(array $types) implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)), )); } - - $this->types = UnionTypeHelper::sortTypes($types); } /** @@ -68,69 +89,184 @@ public function getTypes(): array } /** - * @return string[] + * @return Type[] */ + private function getSortedTypes(): array + { + if ($this->sortedTypes) { + return $this->types; + } + + $this->types = UnionTypeHelper::sortTypes($this->types); + $this->sortedTypes = true; + + return $this->types; + } + + public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap + { + $types = TemplateTypeMap::createEmpty(); + + foreach ($this->types as $type) { + $types = $types->intersect($templateType->inferTemplateTypes($type)); + } + + return $types; + } + public function getReferencedClasses(): array { - return UnionTypeHelper::getReferencedClasses($this->types); + $classes = []; + foreach ($this->types as $type) { + foreach ($type->getReferencedClasses() as $className) { + $classes[] = $className; + } + } + + return $classes; + } + + public function getObjectClassNames(): array + { + $objectClassNames = []; + foreach ($this->types as $type) { + $innerObjectClassNames = $type->getObjectClassNames(); + foreach ($innerObjectClassNames as $innerObjectClassName) { + $objectClassNames[] = $innerObjectClassName; + } + } + + return array_values(array_unique($objectClassNames)); + } + + public function getObjectClassReflections(): array + { + $reflections = []; + foreach ($this->types as $type) { + foreach ($type->getObjectClassReflections() as $reflection) { + $reflections[] = $reflection; + } + } + + return $reflections; + } + + public function getArrays(): array + { + $arrays = []; + foreach ($this->types as $type) { + foreach ($type->getArrays() as $array) { + $arrays[] = $array; + } + } + + return $arrays; + } + + public function getConstantArrays(): array + { + $constantArrays = []; + foreach ($this->types as $type) { + foreach ($type->getConstantArrays() as $constantArray) { + $constantArrays[] = $constantArray; + } + } + + return $constantArrays; + } + + public function getConstantStrings(): array + { + $strings = []; + foreach ($this->types as $type) { + foreach ($type->getConstantStrings() as $string) { + $strings[] = $string; + } + } + + return $strings; } - public function accepts(Type $otherType, bool $strictTypes): TrinaryLogic + public function accepts(Type $otherType, bool $strictTypes): AcceptsResult { + $result = AcceptsResult::createYes(); foreach ($this->types as $type) { - if (!$type->accepts($otherType, $strictTypes)->yes()) { - return TrinaryLogic::createNo(); + $result = $result->and($type->accepts($otherType, $strictTypes)); + } + + if (!$result->yes()) { + $isList = $otherType->isList(); + $reasons = $result->reasons; + $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $otherType); + if ($this->isList()->yes() && !$isList->yes()) { + $reasons[] = sprintf( + '%s %s a list.', + $otherType->describe($verbosity), + $isList->no() ? 'is not' : 'might not be', + ); + } + + $isNonEmpty = $otherType->isIterableAtLeastOnce(); + if ($this->isIterableAtLeastOnce()->yes() && !$isNonEmpty->yes()) { + $reasons[] = sprintf( + '%s %s empty.', + $otherType->describe($verbosity), + $isNonEmpty->no() ? 'is' : 'might be', + ); + } + + if (count($reasons) > 0) { + return new AcceptsResult($result->result, $reasons); } } - return TrinaryLogic::createYes(); + return $result; } - public function isSuperTypeOf(Type $otherType): TrinaryLogic + public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof IntersectionType && $this->equals($otherType)) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $innerType->isSuperTypeOf($otherType); + if ($otherType instanceof NeverType) { + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createYes()->and(...$results); + return IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types)); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) { return $otherType->isSuperTypeOf($this); } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $otherType->isSuperTypeOf($innerType); + $result = IsSuperTypeOfResult::maxMin(...array_map(static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType), $this->types)); + if ($this->isOversizedArray()->yes()) { + if (!$result->no()) { + return IsSuperTypeOfResult::createYes(); + } } - return TrinaryLogic::maxMin(...$results); + return $result; } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - if ($acceptingType instanceof self || $acceptingType instanceof UnionType) { - return $acceptingType->isSuperTypeOf($this); - } - - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $acceptingType->accepts($innerType, $strictTypes); + $result = AcceptsResult::maxMin(...array_map(static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), $this->types)); + if ($this->isOversizedArray()->yes()) { + if (!$result->no()) { + return AcceptsResult::createYes(); + } } - return TrinaryLogic::maxMin(...$results); + return $result; } public function equals(Type $type): bool { - if (!$type instanceof self) { + if (!$type instanceof static) { return false; } @@ -138,13 +274,25 @@ public function equals(Type $type): bool return false; } - foreach ($this->types as $i => $innerType) { - if (!$innerType->equals($type->types[$i])) { + $otherTypes = $type->types; + foreach ($this->types as $innerType) { + $match = false; + foreach ($otherTypes as $i => $otherType) { + if (!$innerType->equals($otherType)) { + continue; + } + + $match = true; + unset($otherTypes[$i]); + break; + } + + if (!$match) { return false; } } - return true; + return count($otherTypes) === 0; } public function describe(VerbosityLevel $level): string @@ -152,13 +300,43 @@ public function describe(VerbosityLevel $level): string return $level->handle( function () use ($level): string { $typeNames = []; - foreach ($this->types as $type) { + $isList = $this->isList()->yes(); + $valueType = null; + foreach ($this->getSortedTypes() as $type) { + if ($isList) { + if ($type instanceof ArrayType || $type instanceof ConstantArrayType) { + $valueType = $type->getIterableValueType(); + continue; + } + if ($type instanceof NonEmptyArrayType) { + continue; + } + } if ($type instanceof AccessoryType) { continue; } $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level); } + if ($isList) { + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $innerType = ''; + if ($valueType !== null && !$isMixedValueType) { + $innerType = sprintf('<%s>', $valueType->describe($level)); + } + + $typeNames[] = 'list' . $innerType; + } + + usort($typeNames, static function ($a, $b) { + $cmp = strcasecmp($a, $b); + if ($cmp !== 0) { + return $cmp; + } + + return $a <=> $b; + }); + return implode('&', $typeNames); }, fn (): string => $this->describeItself($level, true), @@ -168,67 +346,155 @@ function () use ($level): string { private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string { + $baseTypes = []; $typesToDescribe = []; $skipTypeNames = []; - foreach ($this->types as $type) { - if ($type instanceof AccessoryNonEmptyStringType || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType) { - $typesToDescribe[] = $type; + + $nonEmptyStr = false; + $nonFalsyStr = false; + $isList = $this->isList()->yes(); + $isArray = $this->isArray()->yes(); + $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes(); + $describedTypes = []; + foreach ($this->getSortedTypes() as $i => $type) { + if ($type instanceof AccessoryNonEmptyStringType + || $type instanceof AccessoryLiteralStringType + || $type instanceof AccessoryNumericStringType + || $type instanceof AccessoryNonFalsyStringType + || $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType + ) { + if ( + ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType) + && !$level->isPrecise() + ) { + continue; + } + if ($type instanceof AccessoryNonFalsyStringType) { + $nonFalsyStr = true; + } + if ($type instanceof AccessoryNonEmptyStringType) { + $nonEmptyStr = true; + } + if ($nonEmptyStr && $nonFalsyStr) { + // prevent redundant 'non-empty-string&non-falsy-string' + foreach ($typesToDescribe as $key => $typeToDescribe) { + if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) { + continue; + } + + unset($typesToDescribe[$key]); + } + } + + $typesToDescribe[$i] = $type; $skipTypeNames[] = 'string'; continue; } - if ($type instanceof NonEmptyArrayType) { - $typesToDescribe[] = $type; - $skipTypeNames[] = 'array'; - continue; + if ($isList || $isArray) { + if ($type instanceof ArrayType) { + $keyType = $type->getKeyType(); + $valueType = $type->getItemType(); + if ($isList) { + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $valueTypeDescription = ''; + if (!$isMixedValueType) { + $valueTypeDescription = sprintf('<%s>', $valueType->describe($level)); + } + + $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription; + } else { + $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed(); + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $typeDescription = ''; + if (!$isMixedKeyType) { + $typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level)); + } elseif (!$isMixedValueType) { + $typeDescription = sprintf('<%s>', $valueType->describe($level)); + } + + $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription; + } + continue; + } elseif ($type instanceof ConstantArrayType) { + $description = $type->describe($level); + $descriptionWithoutKind = substr($description, strlen('array')); + $begin = $isList ? 'list' : 'array'; + if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) { + $begin = 'non-empty-' . $begin; + } + + $describedTypes[$i] = $begin . $descriptionWithoutKind; + continue; + } + if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { + continue; + } } - if ($skipAccessoryTypes) { + if ($type instanceof CallableType && $type->isCommonCallable()) { + $typesToDescribe[$i] = $type; + $skipTypeNames[] = 'object'; + $skipTypeNames[] = 'string'; continue; } if (!$type instanceof AccessoryType) { + $baseTypes[$i] = $type; continue; } - $typesToDescribe[] = $type; - } - - $describedTypes = []; - foreach ($this->types as $type) { - if ($type instanceof AccessoryType) { + if ($skipAccessoryTypes) { continue; } + + $typesToDescribe[$i] = $type; + } + + foreach ($baseTypes as $i => $type) { $typeDescription = $type->describe($level); - if ( - substr($typeDescription, 0, strlen('array<')) === 'array<' - && in_array('array', $skipTypeNames, true) - ) { + + if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) { foreach ($typesToDescribe as $j => $typeToDescribe) { - if (!$typeToDescribe instanceof NonEmptyArrayType) { - continue; + if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) { + $describedTypes[$i] = 'callable-' . $typeDescription; + unset($typesToDescribe[$j]); + continue 2; } - - unset($typesToDescribe[$j]); } - - $describedTypes[] = 'non-empty-array<' . substr($typeDescription, strlen('array<')); - continue; } if (in_array($typeDescription, $skipTypeNames, true)) { continue; } - $describedTypes[] = $type->describe($level); + $describedTypes[$i] = $type->describe($level); } - foreach ($typesToDescribe as $typeToDescribe) { - $describedTypes[] = $typeToDescribe->describe($level); + foreach ($typesToDescribe as $i => $typeToDescribe) { + $describedTypes[$i] = $typeToDescribe->describe($level); } + ksort($describedTypes); + return implode('&', $describedTypes); } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName)); + } + + public function isObject(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isObject()); + } + + public function isEnum(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isEnum()); + } + public function canAccessProperties(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties()); @@ -239,7 +505,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName)); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -277,7 +543,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -315,7 +581,7 @@ public function hasConstant(string $constantName): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName)); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { foreach ($this->types as $type) { if ($type->hasConstant($constantName)->yes()) { @@ -336,21 +602,61 @@ public function isIterableAtLeastOnce(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce()); } + public function getArraySize(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize()); + } + public function getIterableKeyType(): Type { return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } + public function getFirstIterableKeyType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableKeyType()); + } + + public function getLastIterableKeyType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableKeyType()); + } + public function getIterableValueType(): Type { return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType()); } + public function getFirstIterableValueType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableValueType()); + } + + public function getLastIterableValueType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableValueType()); + } + public function isArray(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray()); } + public function isConstantArray(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray()); + } + + public function isOversizedArray(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray()); + } + + public function isList(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isList()); + } + public function isString(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString()); @@ -366,29 +672,166 @@ public function isNonEmptyString(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); } + public function isNonFalsyString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString()); + } + public function isLiteralString(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); } + public function isLowercaseString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString()); + } + + public function isUppercaseString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString()); + } + + public function isClassString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassString()); + } + + public function getClassStringObjectType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringObjectType()); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType()); + } + + public function isVoid(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isVoid()); + } + + public function isScalar(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isScalar()); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return $this->intersectResults( + static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic() + )->toBooleanType(); + } + public function isOffsetAccessible(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal()); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { + if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) { + $arrayKeyOffsetType = $offsetType->toArrayKey(); + if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + return TrinaryLogic::createYes(); + } + + foreach ($this->types as $type) { + if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) { + continue; + } + + foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) { + if (!is_int($constantScalarValue)) { + continue; + } + if (IntegerRangeType::fromInterval(0, $constantScalarValue)->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + return TrinaryLogic::createYes(); + } + } + } + } + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); } public function getOffsetValueType(Type $offsetType): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType)); + $result = $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType)); + if ($this->isOversizedArray()->yes()) { + return TypeUtils::toBenevolentUnion($result); + } + + return $result; } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + if ($this->isOversizedArray()->yes()) { + return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type { + // avoid new HasOffsetValueType being intersected with oversized array + if (!$type instanceof ArrayType) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + if (!$offsetType instanceof ConstantStringType && !$offsetType instanceof ConstantIntegerType) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + if (!$offsetType->isSuperTypeOf($type->getKeyType())->yes()) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + return TypeCombinator::intersect( + new ArrayType( + TypeCombinator::union($type->getKeyType(), $offsetType), + TypeCombinator::union($type->getItemType(), $valueType), + ), + new NonEmptyArrayType(), + ); + }); + } + + $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + + if ( + $offsetType !== null + && $this->isList()->yes() + && !$result->isList()->yes() + ) { + if ($this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + } else { + foreach ($this->types as $type) { + if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) { + continue; + } + + foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) { + if (!is_int($constantScalarValue)) { + continue; + } + if (IntegerRangeType::fromInterval(0, $constantScalarValue + 1)->isSuperTypeOf($offsetType)->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + break 2; + } + } + } + } + } + + return $result; + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); } public function unsetOffset(Type $offsetType): Type @@ -396,14 +839,96 @@ public function unsetOffset(Type $offsetType): Type return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); } + public function getKeysArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray()); + } + + public function getValuesArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray()); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys)); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType)); + } + + public function flipArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray()); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType)); + } + + public function popArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->popArray()); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); + } + + public function searchArray(Type $needleType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType)); + } + + public function shiftArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->shiftArray()); + } + + public function shuffleArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->shuffleArray()); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + $result = $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); + + if ( + $this->isList()->yes() + && $this->isIterableAtLeastOnce()->yes() + && (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes() + ) { + $result = TypeCombinator::intersect($result, new NonEmptyArrayType()); + } + + return $result; + } + + public function getEnumCases(): array + { + $compare = []; + foreach ($this->types as $type) { + $oneType = []; + foreach ($type->getEnumCases() as $enumCase) { + $oneType[$enumCase->getClassName() . '::' . $enumCase->getEnumCaseName()] = $enumCase; + } + $compare[] = $oneType; + } + + return array_values(array_intersect_key(...$compare)); + } + public function isCallable(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->isCallable()->no()) { @@ -418,44 +943,108 @@ public function isCloneable(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable()); } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion)); + } + + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion)); + } + + public function isNull(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull()); + } + + public function isConstantValue(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue()); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue()); + } + + public function getConstantScalarTypes(): array + { + $scalarTypes = []; + foreach ($this->types as $type) { + foreach ($type->getConstantScalarTypes() as $scalarType) { + $scalarTypes[] = $scalarType; + } + } + + return $scalarTypes; + } + + public function getConstantScalarValues(): array + { + $values = []; + foreach ($this->types as $type) { + foreach ($type->getConstantScalarValues() as $value) { + $values[] = $value; + } + } + + return $values; + } + + public function isTrue(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue()); + } + + public function isFalse(): TrinaryLogic { - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType)); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse()); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isBoolean(): TrinaryLogic { - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean()); } - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isFloat(): TrinaryLogic { - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type)); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat()); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isInteger(): TrinaryLogic { - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type)); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger()); } - public function getSmallerType(): Type + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType()); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion)); } - public function getSmallerOrEqualType(): Type + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { - return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType()); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion)); } - public function getGreaterType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion)); } - public function getGreaterOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion)); + } + + public function getGreaterType(PhpVersion $phpVersion): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion)); + } + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion)); } public function toBoolean(): BooleanType @@ -476,6 +1065,13 @@ public function toNumber(): Type return $type; } + public function toAbsoluteNumber(): Type + { + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); + + return $type; + } + public function toString(): Type { $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString()); @@ -504,23 +1100,30 @@ public function toArray(): Type return $type; } - public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + public function toArrayKey(): Type { - $types = TemplateTypeMap::createEmpty(); + if ($this->isNumericString()->yes()) { + return new IntegerType(); + } - foreach ($this->types as $type) { - $types = $types->intersect($type->inferTemplateTypes($receivedType)); + if ($this->isString()->yes()) { + return $this; } - return $types; + return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey()); } - public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes)); + } + + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { $types = TemplateTypeMap::createEmpty(); foreach ($this->types as $type) { - $types = $types->intersect($templateType->inferTemplateTypes($type)); + $types = $types->intersect($type->inferTemplateTypes($receivedType)); } return $types; @@ -559,17 +1162,63 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $types = []; + $changed = false; + + if (!$right instanceof self) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeCombinator::intersect(...$types); + } + + return $this; + } + public function tryRemove(Type $typeToRemove): ?Type { return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function exponentiate(Type $exponent): Type { - return new self($properties['types']); + return $this->intersectTypes(static fn (Type $type): Type => $type->exponentiate($exponent)); + } + + public function getFiniteTypes(): array + { + $compare = []; + foreach ($this->types as $type) { + $oneType = []; + foreach ($type->getFiniteTypes() as $finiteType) { + $oneType[md5($finiteType->describe(VerbosityLevel::typeOnly()))] = $finiteType; + } + $compare[] = $oneType; + } + + $result = array_values(array_intersect_key(...$compare)); + + if (count($result) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + return $result; } /** @@ -577,8 +1226,7 @@ public static function __set_state(array $properties): Type */ private function intersectResults(callable $getResult): TrinaryLogic { - $operands = array_map($getResult, $this->types); - return TrinaryLogic::maxMin(...$operands); + return TrinaryLogic::lazyMaxMin($this->types, $getResult); } /** @@ -590,4 +1238,176 @@ private function intersectTypes(callable $getType): Type return TypeCombinator::intersect(...$operands); } + public function toPhpDocNode(): TypeNode + { + $baseTypes = []; + $typesToDescribe = []; + $skipTypeNames = []; + + $nonEmptyStr = false; + $nonFalsyStr = false; + $isList = $this->isList()->yes(); + $isArray = $this->isArray()->yes(); + $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes(); + $describedTypes = []; + + foreach ($this->getSortedTypes() as $i => $type) { + if ($type instanceof AccessoryNonEmptyStringType + || $type instanceof AccessoryLiteralStringType + || $type instanceof AccessoryNumericStringType + || $type instanceof AccessoryNonFalsyStringType + || $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType + ) { + if ($type instanceof AccessoryNonFalsyStringType) { + $nonFalsyStr = true; + } + if ($type instanceof AccessoryNonEmptyStringType) { + $nonEmptyStr = true; + } + if ($nonEmptyStr && $nonFalsyStr) { + // prevent redundant 'non-empty-string&non-falsy-string' + foreach ($typesToDescribe as $key => $typeToDescribe) { + if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) { + continue; + } + + unset($typesToDescribe[$key]); + } + } + + $typesToDescribe[$i] = $type; + $skipTypeNames[] = 'string'; + continue; + } + + if ($isList || $isArray) { + if ($type instanceof ArrayType) { + $keyType = $type->getKeyType(); + $valueType = $type->getItemType(); + if ($isList) { + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-list' : 'list'); + if (!$isMixedValueType) { + $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [ + $valueType->toPhpDocNode(), + ]); + } else { + $describedTypes[$i] = $identifierTypeNode; + } + } else { + $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed(); + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-array' : 'array'); + if (!$isMixedKeyType) { + $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [ + $keyType->toPhpDocNode(), + $valueType->toPhpDocNode(), + ]); + } elseif (!$isMixedValueType) { + $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [ + $valueType->toPhpDocNode(), + ]); + } else { + $describedTypes[$i] = $identifierTypeNode; + } + } + continue; + } elseif ($type instanceof ConstantArrayType) { + $constantArrayTypeNode = $type->toPhpDocNode(); + if ($constantArrayTypeNode instanceof ArrayShapeNode) { + $newKind = $constantArrayTypeNode->kind; + if ($isList) { + if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) { + $newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST; + } else { + $newKind = ArrayShapeNode::KIND_LIST; + } + } elseif ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) { + $newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY; + } + + if ($newKind !== $constantArrayTypeNode->kind) { + if ($constantArrayTypeNode->sealed) { + $constantArrayTypeNode = ArrayShapeNode::createSealed($constantArrayTypeNode->items, $newKind); + } else { + $constantArrayTypeNode = ArrayShapeNode::createUnsealed($constantArrayTypeNode->items, $constantArrayTypeNode->unsealedType, $newKind); + } + } + + $describedTypes[$i] = $constantArrayTypeNode; + continue; + } + } + if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { + continue; + } + } + + if (!$type instanceof AccessoryType) { + $baseTypes[$i] = $type; + continue; + } + + $accessoryPhpDocNode = $type->toPhpDocNode(); + if ($accessoryPhpDocNode instanceof IdentifierTypeNode && $accessoryPhpDocNode->name === '') { + continue; + } + + $typesToDescribe[$i] = $type; + } + + foreach ($baseTypes as $i => $type) { + $typeNode = $type->toPhpDocNode(); + if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') { + $nonEmpty = false; + $typeName = 'array'; + foreach ($typesToDescribe as $j => $typeToDescribe) { + if ($typeToDescribe instanceof AccessoryArrayListType) { + $typeName = 'list'; + if (count($typeNode->genericTypes) > 1) { + array_shift($typeNode->genericTypes); + } + } elseif ($typeToDescribe instanceof NonEmptyArrayType) { + $nonEmpty = true; + } else { + continue; + } + + unset($typesToDescribe[$j]); + } + + if ($nonEmpty) { + $typeName = 'non-empty-' . $typeName; + } + + $describedTypes[$i] = new GenericTypeNode( + new IdentifierTypeNode($typeName), + $typeNode->genericTypes, + ); + continue; + } + + if ($typeNode instanceof IdentifierTypeNode && in_array($typeNode->name, $skipTypeNames, true)) { + continue; + } + + $describedTypes[$i] = $typeNode; + } + + foreach ($typesToDescribe as $i => $typeToDescribe) { + $describedTypes[$i] = $typeToDescribe->toPhpDocNode(); + } + + ksort($describedTypes); + + $describedTypes = array_values($describedTypes); + + if (count($describedTypes) === 1) { + return $describedTypes[0]; + } + + return new IntersectionTypeNode($describedTypes); + } + } diff --git a/src/Type/IsSuperTypeOfResult.php b/src/Type/IsSuperTypeOfResult.php new file mode 100644 index 0000000000..c1decae155 --- /dev/null +++ b/src/Type/IsSuperTypeOfResult.php @@ -0,0 +1,164 @@ + $reasons + */ + public function __construct( + public readonly TrinaryLogic $result, + public readonly array $reasons, + ) + { + } + + public function yes(): bool + { + return $this->result->yes(); + } + + public function maybe(): bool + { + return $this->result->maybe(); + } + + public function no(): bool + { + return $this->result->no(); + } + + public static function createYes(): self + { + return new self(TrinaryLogic::createYes(), []); + } + + /** + * @param list $reasons + */ + public static function createNo(array $reasons = []): self + { + return new self(TrinaryLogic::createNo(), $reasons); + } + + public static function createMaybe(): self + { + return new self(TrinaryLogic::createMaybe(), []); + } + + public static function createFromBoolean(bool $value): self + { + return new self(TrinaryLogic::createFromBoolean($value), []); + } + + public function toAcceptsResult(): AcceptsResult + { + return new AcceptsResult($this->result, $this->reasons); + } + + public function and(self ...$others): self + { + $results = []; + $reasons = []; + foreach ($others as $other) { + $results[] = $other->result; + $reasons[] = $other->reasons; + } + + return new self( + $this->result->and(...$results), + array_values(array_unique(array_merge($this->reasons, ...$reasons))), + ); + } + + public function or(self ...$others): self + { + $results = []; + $reasons = []; + foreach ($others as $other) { + $results[] = $other->result; + $reasons[] = $other->reasons; + } + + return new self( + $this->result->or(...$results), + array_values(array_unique(array_merge($this->reasons, ...$reasons))), + ); + } + + /** + * @param callable(string): string $cb + */ + public function decorateReasons(callable $cb): self + { + $reasons = []; + foreach ($this->reasons as $reason) { + $reasons[] = $cb($reason); + } + + return new self($this->result, $reasons); + } + + public static function extremeIdentity(self ...$operands): self + { + if ($operands === []) { + throw new ShouldNotHappenException(); + } + + $result = TrinaryLogic::extremeIdentity(...array_map(static fn (self $result) => $result->result, $operands)); + + return new self($result, self::mergeReasons($operands)); + } + + public static function maxMin(self ...$operands): self + { + if ($operands === []) { + throw new ShouldNotHappenException(); + } + + $result = TrinaryLogic::maxMin(...array_map(static fn (self $result) => $result->result, $operands)); + + return new self($result, self::mergeReasons($operands)); + } + + public function negate(): self + { + return new self($this->result->negate(), $this->reasons); + } + + public function describe(): string + { + return $this->result->describe(); + } + + /** + * @param array $operands + * + * @return list + */ + private static function mergeReasons(array $operands): array + { + $reasons = []; + foreach ($operands as $operand) { + foreach ($operand->reasons as $reason) { + $reasons[] = $reason; + } + } + + return array_values(array_unique($reasons)); + } + +} diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 3b696ed514..2e6d26a381 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -2,13 +2,16 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateMixedType; -use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\MaybeArrayTypeTrait; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; @@ -23,6 +26,7 @@ class IterableType implements CompoundType { + use MaybeArrayTypeTrait; use MaybeCallableTypeTrait; use MaybeObjectTypeTrait; use MaybeOffsetAccessibleTypeTrait; @@ -48,9 +52,6 @@ public function getItemType(): Type return $this->itemType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return array_merge( @@ -59,10 +60,25 @@ public function getReferencedClasses(): array ); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array { - if ($type instanceof ConstantArrayType && $type->isEmpty()) { - return TrinaryLogic::createYes(); + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + return AcceptsResult::createYes(); } if ($type->isIterable()->yes()) { return $this->getIterableValueType()->accepts($type->getIterableValueType(), $strictTypes) @@ -73,24 +89,28 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - return $type->isIterable() + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return (new IsSuperTypeOfResult($type->isIterable(), [])) ->and($this->getIterableValueType()->isSuperTypeOf($type->getIterableValueType())) ->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType())); } - public function isSuperTypeOfMixed(Type $type): TrinaryLogic + public function isSuperTypeOfMixed(Type $type): IsSuperTypeOfResult { - return $type->isIterable() + return (new IsSuperTypeOfResult($type->isIterable(), [])) ->and($this->isNestedTypeSuperTypeOf($this->getIterableValueType(), $type->getIterableValueType())) ->and($this->isNestedTypeSuperTypeOf($this->getIterableKeyType(), $type->getIterableKeyType())); } - private function isNestedTypeSuperTypeOf(Type $a, Type $b): TrinaryLogic + private function isNestedTypeSuperTypeOf(Type $a, Type $b): IsSuperTypeOfResult { if (!$a instanceof MixedType || !$b instanceof MixedType) { return $a->isSuperTypeOf($b); @@ -102,16 +122,16 @@ private function isNestedTypeSuperTypeOf(Type $a, Type $b): TrinaryLogic if ($a->isExplicitMixed()) { if ($b->isExplicitMixed()) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) { return $otherType->isSuperTypeOf(new UnionType([ @@ -124,25 +144,25 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic } if ($otherType instanceof self) { - $limit = TrinaryLogic::createYes(); + $limit = IsSuperTypeOfResult::createYes(); } else { - $limit = TrinaryLogic::createMaybe(); + $limit = IsSuperTypeOfResult::createMaybe(); } - if ($otherType instanceof ConstantArrayType && $otherType->isEmpty()) { - return TrinaryLogic::createMaybe(); + if ($otherType->isConstantArray()->yes() && $otherType->isIterableAtLeastOnce()->no()) { + return IsSuperTypeOfResult::createMaybe(); } return $limit->and( - $otherType->isIterable(), + new IsSuperTypeOfResult($otherType->isIterable(), []), $otherType->getIterableValueType()->isSuperTypeOf($this->itemType), $otherType->getIterableKeyType()->isSuperTypeOf($this->keyType), ); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function equals(Type $type): bool @@ -157,9 +177,8 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - $isMixedKeyType = $this->keyType instanceof MixedType && !$this->keyType instanceof TemplateType; - $isMixedItemType = $this->itemType instanceof MixedType && !$this->itemType instanceof TemplateType; - + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; if ($isMixedKeyType) { if ($isMixedItemType) { return 'iterable'; @@ -185,6 +204,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -205,6 +229,37 @@ public function toArray(): Type return new ArrayType($this->keyType, $this->getItemType()); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return TypeCombinator::union( + $this, + new ArrayType( + TypeCombinator::intersect( + $this->keyType->toArrayKey(), + new UnionType([ + new IntegerType(), + new StringType(), + ]), + ), + $this->itemType, + ), + new GenericObjectType(Traversable::class, [ + $this->keyType, + $this->itemType, + ]), + ); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -215,19 +270,89 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + public function getIterableKeyType(): Type { return $this->keyType; } + public function getFirstIterableKeyType(): Type + { + return $this->keyType; + } + + public function getLastIterableKeyType(): Type + { + return $this->keyType; + } + public function getIterableValueType(): Type { return $this->getItemType(); } - public function isArray(): TrinaryLogic + public function getFirstIterableValueType(): Type { - return TrinaryLogic::createMaybe(); + return $this->getItemType(); + } + + public function getLastIterableValueType(): Type + { + return $this->getItemType(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); } public function isString(): TrinaryLogic @@ -245,11 +370,61 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getEnumCases(): array + { + return []; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { @@ -268,9 +443,11 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); + return array_merge( - $this->getIterableKeyType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()), - $this->getIterableValueType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()), + $this->getIterableKeyType()->getReferencedTemplateTypes($variance), + $this->getIterableValueType()->getReferencedTemplateTypes($variance), ); } @@ -286,6 +463,18 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $keyType = $cb($this->keyType, $right->getIterableKeyType()); + $itemType = $cb($this->itemType, $right->getIterableValueType()); + + if ($keyType !== $this->keyType || $itemType !== $this->itemType) { + return new self($keyType, $itemType); + } + + return $this; + } + public function tryRemove(Type $typeToRemove): ?Type { $arrayType = new ArrayType(new MixedType(), new MixedType()); @@ -304,12 +493,41 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['keyType'], $properties['itemType']); + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; + + if ($isMixedKeyType) { + if ($isMixedItemType) { + return new IdentifierTypeNode('iterable'); + } + + return new GenericTypeNode( + new IdentifierTypeNode('iterable'), + [ + $this->itemType->toPhpDocNode(), + ], + ); + } + + return new GenericTypeNode( + new IdentifierTypeNode('iterable'), + [ + $this->keyType->toPhpDocNode(), + $this->itemType->toPhpDocNode(), + ], + ); } } diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index e1c10120b1..5435c540ff 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -8,39 +8,45 @@ trait JustNullableTypeTrait { - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof static) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -53,7 +59,57 @@ public function traverse(callable $cb): Type return $this; } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -73,9 +129,44 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Type/KeyOfType.php b/src/Type/KeyOfType.php new file mode 100644 index 0000000000..10a4cb2ea5 --- /dev/null +++ b/src/Type/KeyOfType.php @@ -0,0 +1,94 @@ +type; + } + + public function getReferencedClasses(): array + { + return $this->type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('key-of<%s>', $this->type->describe($level)); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + return $this->type->getIterableKeyType(); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('key-of'), [$this->type->toPhpDocNode()]); + } + +} diff --git a/src/Type/LateResolvableType.php b/src/Type/LateResolvableType.php new file mode 100644 index 0000000000..5c95444ce9 --- /dev/null +++ b/src/Type/LateResolvableType.php @@ -0,0 +1,13 @@ +castsNumbersToStringsOnLooseComparison()) { + $isNumber = new UnionType([ + new IntegerType(), + new FloatType(), + ]); + + if ($leftType->isString()->yes() && $leftType->isNumericString()->no() && $isNumber->isSuperTypeOf($rightType)->yes()) { + $stringValue = (string) $rightType->getValue(); + return new ConstantBooleanType($stringValue === $leftType->getValue()); + } + if ($rightType->isString()->yes() && $rightType->isNumericString()->no() && $isNumber->isSuperTypeOf($leftType)->yes()) { + $stringValue = (string) $leftType->getValue(); + return new ConstantBooleanType($stringValue === $rightType->getValue()); + } + } else { + if ($leftType->isString()->yes() && $leftType->isNumericString()->no() && $rightType->isFloat()->yes()) { + $numericPart = (float) $leftType->getValue(); + return new ConstantBooleanType($numericPart === $rightType->getValue()); + } + if ($rightType->isString()->yes() && $rightType->isNumericString()->no() && $leftType->isFloat()->yes()) { + $numericPart = (float) $rightType->getValue(); + return new ConstantBooleanType($numericPart === $leftType->getValue()); + } + if ($leftType->isString()->yes() && $leftType->isNumericString()->no() && $rightType->isInteger()->yes()) { + $numericPart = (int) $leftType->getValue(); + return new ConstantBooleanType($numericPart === $rightType->getValue()); + } + if ($rightType->isString()->yes() && $rightType->isNumericString()->no() && $leftType->isInteger()->yes()) { + $numericPart = (int) $rightType->getValue(); + return new ConstantBooleanType($numericPart === $leftType->getValue()); + } + } + + // @phpstan-ignore equal.notAllowed + return new ConstantBooleanType($leftType->getValue() == $rightType->getValue()); // phpcs:ignore + } + +} diff --git a/src/Type/MethodParameterClosureTypeExtension.php b/src/Type/MethodParameterClosureTypeExtension.php new file mode 100644 index 0000000000..6272a26ef8 --- /dev/null +++ b/src/Type/MethodParameterClosureTypeExtension.php @@ -0,0 +1,32 @@ +subtractedType = $subtractedType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array { - return TrinaryLogic::createYes(); + return []; } - public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic + public function getObjectClassReflections(): array + { + return []; + } + + public function getArrays(): array + { + return []; + } + + public function getConstantArrays(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + return AcceptsResult::createYes(); + } + + public function isSuperTypeOfMixed(MixedType $type): IsSuperTypeOfResult { if ($this->subtractedType === null) { if ($this->isExplicitMixed) { if ($type->isExplicitMixed) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type->subtractedType === null) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } $isSuperType = $type->subtractedType->isSuperTypeOf($this->subtractedType); if ($isSuperType->yes()) { if ($this->isExplicitMixed) { if ($type->isExplicitMixed) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($this->subtractedType === null || $type instanceof NeverType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof self) { if ($type->subtractedType === null) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } $isSuperType = $type->subtractedType->isSuperTypeOf($this->subtractedType); if ($isSuperType->yes()) { - return TrinaryLogic::createYes(); + return $isSuperType; } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); + } + + $result = $this->subtractedType->isSuperTypeOf($type)->negate(); + if ($result->no()) { + return IsSuperTypeOfResult::createNo([ + sprintf( + 'Type %s has already been eliminated from %s.', + $this->subtractedType->describe(VerbosityLevel::precise()), + $this->describe(VerbosityLevel::typeOnly()), + ), + ]); } - return $this->subtractedType->isSuperTypeOf($type)->negate(); + return $result; } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type @@ -118,26 +166,143 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new self($this->isExplicitMixed); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new self($this->isExplicitMixed); + } + public function unsetOffset(Type $offsetType): Type { + if ($this->subtractedType !== null) { + return new self($this->isExplicitMixed, TypeCombinator::remove($this->subtractedType, new ConstantArrayType([], []))); + } return $this; } + public function getKeysArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new StringType()])), new AccessoryArrayListType()); + } + + public function getValuesArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)), new AccessoryArrayListType()); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)), new AccessoryArrayListType()); + } + + public function fillKeysArray(Type $valueType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType($this->getIterableValueType(), $valueType); + } + + public function flipArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function popArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function searchArray(Type $needleType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::union(new IntegerType(), new StringType(), new ConstantBooleanType(false)); + } + + public function shiftArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function shuffleArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)), new AccessoryArrayListType()); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + public function isCallable(): TrinaryLogic { - if ( - $this->subtractedType !== null - && $this->subtractedType->isCallable()->yes() - ) { - return TrinaryLogic::createNo(); + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new CallableType())->yes()) { + return TrinaryLogic::createNo(); + } } return TrinaryLogic::createMaybe(); } - /** - * @return ParametersAcceptor[] - */ + public function getEnumCases(): array + { + return []; + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [new TrivialParametersAcceptor()]; @@ -164,29 +329,54 @@ public function equals(Type $type): bool return $this->subtractedType->equals($type->subtractedType); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof self && !$otherType instanceof TemplateMixedType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($this->subtractedType !== null) { $isSuperType = $this->subtractedType->isSuperTypeOf($otherType); if ($isSuperType->yes()) { - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - $isSuperType = $this->isSuperTypeOf($acceptingType); + $isSuperType = $this->isSuperTypeOf($acceptingType)->toAcceptsResult(); if ($isSuperType->no()) { return $isSuperType; } - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); + } + + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new self(); + } + + public function isObject(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ObjectWithoutClassType())->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); + } + + public function isEnum(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ObjectWithoutClassType())->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function canAccessProperties(): TrinaryLogic @@ -199,14 +389,14 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createYes(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -225,7 +415,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createYes(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -251,9 +441,9 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createYes(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { - return new DummyConstantReflection($constantName); + return new DummyClassConstantReflection($constantName); } public function isCloneable(): TrinaryLogic @@ -269,7 +459,9 @@ public function describe(VerbosityLevel $level): string function () use ($level): string { $description = 'mixed'; if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe($level)) + : sprintf('~%s', $this->subtractedType->describe($level)); } return $description; @@ -277,7 +469,9 @@ function () use ($level): string { function () use ($level): string { $description = 'mixed'; if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe($level)) + : sprintf('~%s', $this->subtractedType->describe($level)); } if ($this->isExplicitMixed) { @@ -293,8 +487,10 @@ function () use ($level): string { public function toBoolean(): BooleanType { - if ($this->subtractedType !== null && StaticTypeFactory::falsey()->equals($this->subtractedType)) { - return new ConstantBooleanType(true); + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(StaticTypeFactory::falsey())->yes()) { + return new ConstantBooleanType(true); + } } return new BooleanType(); @@ -302,14 +498,37 @@ public function toBoolean(): BooleanType public function toNumber(): Type { - return new UnionType([ + return TypeCombinator::union( $this->toInteger(), $this->toFloat(), - ]); + ); + } + + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); } public function toInteger(): Type { + $castsToZero = new UnionType([ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantArrayType([], []), + new StringType(), + new FloatType(), // every 0.x float casts to int(0) + ]); + if ( + $this->subtractedType !== null + && $this->subtractedType->isSuperTypeOf($castsToZero)->yes() + ) { + return new UnionType([ + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), + ]); + } + return new IntegerType(); } @@ -320,6 +539,32 @@ public function toFloat(): Type public function toString(): Type { + if ($this->subtractedType !== null) { + $castsToEmptyString = new UnionType([ + new NullType(), + new ConstantBooleanType(false), + new ConstantStringType(''), + ]); + if ($this->subtractedType->isSuperTypeOf($castsToEmptyString)->yes()) { + $accessories = [ + new StringType(), + new AccessoryNonEmptyStringType(), + ]; + + $castsToZeroString = new UnionType([ + new ConstantFloatType(0.0), + new ConstantStringType('0'), + new ConstantIntegerType(0), + ]); + if ($this->subtractedType->isSuperTypeOf($castsToZeroString)->yes()) { + $accessories[] = new AccessoryNonFalsyStringType(); + } + return new IntersectionType( + $accessories, + ); + } + } + return new StringType(); } @@ -330,14 +575,39 @@ public function toArray(): Type return new ArrayType($mixed, $mixed); } + public function toArrayKey(): Type + { + return new BenevolentUnionType([new IntegerType(), new StringType()]); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function isIterable(): TrinaryLogic { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new IterableType(new MixedType(), new MixedType()))->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isIterableAtLeastOnce(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return $this->isIterable(); + } + + public function getArraySize(): Type + { + if ($this->isIterable()->no()) { + return new ErrorType(); + } + + return IntegerRangeType::fromInterval(0, null); } public function getIterableKeyType(): Type @@ -345,18 +615,63 @@ public function getIterableKeyType(): Type return new self($this->isExplicitMixed); } + public function getFirstIterableKeyType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getLastIterableKeyType(): Type + { + return new self($this->isExplicitMixed); + } + public function getIterableValueType(): Type { return new self($this->isExplicitMixed); } + public function getFirstIterableValueType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getLastIterableValueType(): Type + { + return new self($this->isExplicitMixed); + } + public function isOffsetAccessible(): TrinaryLogic { + if ($this->subtractedType !== null) { + $offsetAccessibles = new UnionType([ + new StringType(), + new ArrayType(new MixedType(), new MixedType()), + new ObjectType(ArrayAccess::class), + ]); + + if ($this->subtractedType->isSuperTypeOf($offsetAccessibles)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ObjectWithoutClassType())->yes()) { + return TrinaryLogic::createYes(); + } + } return TrinaryLogic::createMaybe(); } public function hasOffsetValueType(Type $offsetType): TrinaryLogic { + if ($this->isOffsetAccessible()->no()) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createMaybe(); } @@ -402,31 +717,314 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + public function isArray(): TrinaryLogic { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ArrayType(new MixedType(), new MixedType()))->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isConstantArray(): TrinaryLogic + { + return $this->isArray(); + } + + public function isOversizedArray(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $oversizedArray = TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + new OversizedArrayType(), + ); + + if ($this->subtractedType->isSuperTypeOf($oversizedArray)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isList(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $list = TypeCombinator::intersect( + new ArrayType(new IntegerType(), new MixedType()), + new AccessoryArrayListType(), + ); + + if ($this->subtractedType->isSuperTypeOf($list)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isNull(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new NullType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ConstantBooleanType(true))->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isFalse(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ConstantBooleanType(false))->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isBoolean(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new BooleanType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isFloat(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new FloatType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isInteger(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new IntegerType())->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isString(): TrinaryLogic { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new StringType())->yes()) { + return TrinaryLogic::createNo(); + } + } return TrinaryLogic::createMaybe(); } public function isNumericString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $numericString = TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($numericString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isNonEmptyString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $nonEmptyString = TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($nonEmptyString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $nonFalsyString = TypeCombinator::intersect( + new StringType(), + new AccessoryNonFalsyStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($nonFalsyString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isLiteralString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $literalString = TypeCombinator::intersect( + new StringType(), + new AccessoryLiteralStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($literalString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $lowercaseString = TypeCombinator::intersect( + new StringType(), + new AccessoryLowercaseStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($lowercaseString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $uppercaseString = TypeCombinator::intersect( + new StringType(), + new AccessoryUppercaseStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($uppercaseString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new StringType())->yes()) { + return TrinaryLogic::createNo(); + } + if ($this->subtractedType->isSuperTypeOf(new ClassStringType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + if (!$this->isClassString()->no()) { + return new ObjectWithoutClassType(); + } + + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + $objectOrClass = new UnionType([ + new ObjectWithoutClassType(), + new ClassStringType(), + ]); + if (!$this->isSuperTypeOf($objectOrClass)->no()) { + return new ObjectWithoutClassType(); + } + + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new VoidType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isScalar(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType()]))->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function tryRemove(Type $typeToRemove): ?Type { if ($this->isSuperTypeOf($typeToRemove)->yes()) { @@ -436,15 +1034,22 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function exponentiate(Type $exponent): Type { - return new self( - $properties['isExplicitMixed'], - $properties['subtractedType'] ?? null, - ); + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('mixed'); } } diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index ec0c5a66dd..518ffa8f4a 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -2,12 +2,13 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\ClassConstantReflection; 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\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -38,26 +39,48 @@ public function isExplicit(): bool return $this->isExplicit; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getArrays(): array { - return TrinaryLogic::createYes(); + return []; + } + + public function getConstantArrays(): array + { + return []; } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + return AcceptsResult::createYes(); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -65,14 +88,14 @@ public function equals(Type $type): bool return $type instanceof self; } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - return $this->isSubTypeOf($acceptingType); + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } public function describe(VerbosityLevel $level): string @@ -80,6 +103,21 @@ public function describe(VerbosityLevel $level): string return '*NEVER*'; } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new NeverType(); + } + + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -90,7 +128,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { throw new ShouldNotHappenException(); } @@ -110,7 +148,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { throw new ShouldNotHappenException(); } @@ -130,7 +168,7 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { throw new ShouldNotHappenException(); } @@ -145,21 +183,71 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getArraySize(): Type + { + return new NeverType(); + } + public function getIterableKeyType(): Type { return new NeverType(); } + public function getFirstIterableKeyType(): Type + { + return new NeverType(); + } + + public function getLastIterableKeyType(): Type + { + return new NeverType(); + } + public function getIterableValueType(): Type { return new NeverType(); } + public function getFirstIterableValueType(): Type + { + return new NeverType(); + } + + public function getLastIterableValueType(): Type + { + return new NeverType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isList(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createYes(); @@ -172,7 +260,12 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return new NeverType(); + return new ErrorType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); } public function unsetOffset(Type $offsetType): Type @@ -180,17 +273,74 @@ public function unsetOffset(Type $offsetType): Type return new NeverType(); } + public function getKeysArray(): Type + { + return new NeverType(); + } + + public function getValuesArray(): Type + { + return new NeverType(); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new NeverType(); + } + + public function fillKeysArray(Type $valueType): Type + { + return new NeverType(); + } + + public function flipArray(): Type + { + return new NeverType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return new NeverType(); + } + + public function popArray(): Type + { + return new NeverType(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return new NeverType(); + } + + public function searchArray(Type $needleType): Type + { + return new NeverType(); + } + + public function shiftArray(): Type + { + return new NeverType(); + } + + public function shuffleArray(): Type + { + return new NeverType(); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return new NeverType(); + } + public function isCallable(): TrinaryLogic { - return TrinaryLogic::createYes(); + return TrinaryLogic::createNo(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - return [new TrivialParametersAcceptor()]; + throw new ShouldNotHappenException(); } public function isCloneable(): TrinaryLogic @@ -203,6 +353,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toString(): Type { return $this; @@ -223,12 +378,72 @@ public function toArray(): Type return $this; } + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -248,17 +463,74 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getEnumCases(): array + { + return []; + } + + public function exponentiate(Type $exponent): Type + { + return $this; + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['isExplicit']); + return new IdentifierTypeNode('never'); } } diff --git a/src/Type/NewObjectType.php b/src/Type/NewObjectType.php new file mode 100644 index 0000000000..93d14c6936 --- /dev/null +++ b/src/Type/NewObjectType.php @@ -0,0 +1,94 @@ +type; + } + + public function getReferencedClasses(): array + { + return $this->type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('new<%s>', $this->type->describe($level)); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + return $this->type->getObjectTypeOrClassStringObjectType(); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('new'), [$this->type->toPhpDocNode()]); + } + +} diff --git a/src/Type/NonAcceptingNeverType.php b/src/Type/NonAcceptingNeverType.php new file mode 100644 index 0000000000..dd14d3f9d2 --- /dev/null +++ b/src/Type/NonAcceptingNeverType.php @@ -0,0 +1,41 @@ +isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -83,27 +100,27 @@ public function equals(Type $type): bool return $type instanceof self; } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($otherType instanceof ConstantScalarType) { return TrinaryLogic::createFromBoolean(null < $otherType->getValue()); } if ($otherType instanceof CompoundType) { - return $otherType->isGreaterThan($this); + return $otherType->isGreaterThan($this, $phpVersion); } return TrinaryLogic::createMaybe(); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($otherType instanceof ConstantScalarType) { return TrinaryLogic::createFromBoolean(null <= $otherType->getValue()); } if ($otherType instanceof CompoundType) { - return $otherType->isGreaterThanOrEqual($this); + return $otherType->isGreaterThanOrEqual($this, $phpVersion); } return TrinaryLogic::createMaybe(); @@ -119,6 +136,11 @@ public function toNumber(): Type return new ConstantIntegerType(0); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return new ConstantStringType(''); @@ -139,11 +161,26 @@ public function toArray(): Type return new ConstantArrayType([], []); } + public function toArrayKey(): Type + { + return new ConstantStringType(''); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createNo(); @@ -160,6 +197,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $array->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return $this; @@ -170,7 +212,57 @@ public function traverse(callable $cb): Type return $this; } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getConstantScalarTypes(): array + { + return [$this]; + } + + public function getConstantScalarValues(): array + { + return [$this->getValue()]; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -190,17 +282,75 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function getSmallerType(): Type + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type instanceof ConstantScalarType) { + return LooseComparisonHelper::compareConstantScalars($this, $type, $phpVersion); + } + + if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + // @phpstan-ignore equal.alwaysTrue, equal.notAllowed + return new ConstantBooleanType($this->getValue() == []); // phpcs:ignore + } + + if ($type instanceof CompoundType) { + return $type->looseCompare($this, $phpVersion); + } + + return new BooleanType(); + } + + public function getSmallerType(PhpVersion $phpVersion): Type { return new NeverType(); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { // All falsey types except '0' return new UnionType([ @@ -213,7 +363,7 @@ public function getSmallerOrEqualType(): Type ]); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { // All truthy types, but also '0' return new MixedType(false, new UnionType([ @@ -226,17 +376,29 @@ public function getGreaterType(): Type ])); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { return new MixedType(); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function getFiniteTypes(): array + { + return [$this]; + } + + public function exponentiate(Type $exponent): Type + { + return new UnionType( + [ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], + ); + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('null'); } } diff --git a/src/Type/ObjectShapePropertyReflection.php b/src/Type/ObjectShapePropertyReflection.php new file mode 100644 index 0000000000..594c3278ca --- /dev/null +++ b/src/Type/ObjectShapePropertyReflection.php @@ -0,0 +1,152 @@ +name; + } + + public function getDeclaringClass(): ClassReflection + { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + + return $reflectionProvider->getClass(stdClass::class); + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function hasPhpDocType(): bool + { + return true; + } + + public function getPhpDocType(): Type + { + return $this->type; + } + + public function hasNativeType(): bool + { + return false; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + + public function getReadableType(): Type + { + return $this->type; + } + + public function getWritableType(): Type + { + return new NeverType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return false; + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return false; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isVirtual(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function hasHook(string $hookType): bool + { + return false; + } + + public function getHook(string $hookType): ExtendedMethodReflection + { + throw new ShouldNotHappenException(); + } + + public function isProtectedSet(): bool + { + return false; + } + + public function isPrivateSet(): bool + { + return false; + } + + public function getAttributes(): array + { + return []; + } + +} diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php new file mode 100644 index 0000000000..1e35513f0b --- /dev/null +++ b/src/Type/ObjectShapeType.php @@ -0,0 +1,525 @@ + $properties + * @param list $optionalProperties + */ + public function __construct(private array $properties, private array $optionalProperties) + { + } + + /** + * @return array + */ + public function getProperties(): array + { + return $this->properties; + } + + /** + * @return list + */ + public function getOptionalProperties(): array + { + return $this->optionalProperties; + } + + public function getReferencedClasses(): array + { + $classes = []; + foreach ($this->properties as $propertyType) { + foreach ($propertyType->getReferencedClasses() as $referencedClass) { + $classes[] = $referencedClass; + } + } + + return $classes; + } + + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function hasProperty(string $propertyName): TrinaryLogic + { + if (!array_key_exists($propertyName, $this->properties)) { + return TrinaryLogic::createNo(); + } + + if (in_array($propertyName, $this->optionalProperties, true)) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createYes(); + } + + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + if (!array_key_exists($propertyName, $this->properties)) { + throw new ShouldNotHappenException(); + } + + $property = new ObjectShapePropertyReflection($propertyName, $this->properties[$propertyName]); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($type->getObjectClassReflections() as $classReflection) { + if (!UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( + $reflectionProvider, + $classReflection, + )) { + continue; + } + + return AcceptsResult::createMaybe(); + } + + $result = AcceptsResult::createYes(); + $scope = new OutOfClassScope(); + foreach ($this->properties as $propertyName => $propertyType) { + $typeHasProperty = $type->hasProperty($propertyName); + $hasProperty = new AcceptsResult( + $typeHasProperty, + $typeHasProperty->yes() ? [] : [ + sprintf( + '%s %s have property $%s.', + $type->describe(VerbosityLevel::typeOnly()), + $typeHasProperty->no() ? 'does not' : 'might not', + $propertyName, + ), + ], + ); + if ($hasProperty->no()) { + if (in_array($propertyName, $this->optionalProperties, true)) { + continue; + } + return $hasProperty; + } + if ($hasProperty->maybe() && in_array($propertyName, $this->optionalProperties, true)) { + $hasProperty = AcceptsResult::createYes(); + } + + $result = $result->and($hasProperty); + + try { + $otherProperty = $type->getProperty($propertyName, $scope); + } catch (MissingPropertyFromReflectionException) { + return new AcceptsResult( + $result->result, + [ + sprintf( + '%s %s not have property $%s.', + $type->describe(VerbosityLevel::typeOnly()), + $result->no() ? 'does' : 'might', + $propertyName, + ), + ], + ); + } + if (!$otherProperty->isPublic()) { + return new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf('Property %s::$%s is not public.', $otherProperty->getDeclaringClass()->getDisplayName(), $propertyName), + ]); + } + + if ($otherProperty->isStatic()) { + return new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf('Property %s::$%s is static.', $otherProperty->getDeclaringClass()->getDisplayName(), $propertyName), + ]); + } + + if (!$otherProperty->isReadable()) { + return new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf('Property %s::$%s is not readable.', $otherProperty->getDeclaringClass()->getDisplayName(), $propertyName), + ]); + } + + $otherPropertyType = $otherProperty->getReadableType(); + $verbosity = VerbosityLevel::getRecommendedLevelByType($propertyType, $otherPropertyType); + $acceptsValue = $propertyType->accepts($otherPropertyType, $strictTypes)->decorateReasons( + static fn (string $reason) => sprintf( + 'Property ($%s) type %s does not accept type %s: %s', + $propertyName, + $propertyType->describe($verbosity), + $otherPropertyType->describe($verbosity), + $reason, + ), + ); + if (!$acceptsValue->yes() && count($acceptsValue->reasons) === 0) { + $acceptsValue = new AcceptsResult($acceptsValue->result, [ + sprintf( + 'Property ($%s) type %s does not accept type %s.', + $propertyName, + $propertyType->describe($verbosity), + $otherPropertyType->describe($verbosity), + ), + ]); + } + if ($acceptsValue->no()) { + return $acceptsValue; + } + $result = $result->and($acceptsValue); + } + + return $result->and(new AcceptsResult($type->isObject(), [])); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($type instanceof ObjectWithoutClassType) { + return IsSuperTypeOfResult::createMaybe(); + } + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($type->getObjectClassReflections() as $classReflection) { + if (!UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( + $reflectionProvider, + $classReflection, + )) { + continue; + } + + return IsSuperTypeOfResult::createMaybe(); + } + + $result = IsSuperTypeOfResult::createYes(); + $scope = new OutOfClassScope(); + foreach ($this->properties as $propertyName => $propertyType) { + $hasProperty = new IsSuperTypeOfResult($type->hasProperty($propertyName), []); + if ($hasProperty->no()) { + if (in_array($propertyName, $this->optionalProperties, true)) { + continue; + } + return $hasProperty; + } + if ($hasProperty->maybe() && in_array($propertyName, $this->optionalProperties, true)) { + $hasProperty = IsSuperTypeOfResult::createYes(); + } + + $result = $result->and($hasProperty); + + try { + $otherProperty = $type->getProperty($propertyName, $scope); + } catch (MissingPropertyFromReflectionException) { + return $result; + } + + if (!$otherProperty->isPublic()) { + return IsSuperTypeOfResult::createNo(); + } + + if ($otherProperty->isStatic()) { + return IsSuperTypeOfResult::createNo(); + } + + if (!$otherProperty->isReadable()) { + return IsSuperTypeOfResult::createNo(); + } + + $otherPropertyType = $otherProperty->getReadableType(); + $isSuperType = $propertyType->isSuperTypeOf($otherPropertyType); + if ($isSuperType->no()) { + return $isSuperType; + } + $result = $result->and($isSuperType); + } + + return $result->and(new IsSuperTypeOfResult($type->isObject(), [])); + } + + public function equals(Type $type): bool + { + if (!$type instanceof self) { + return false; + } + + if (count($this->properties) !== count($type->properties)) { + return false; + } + + foreach ($this->properties as $name => $propertyType) { + if (!array_key_exists($name, $type->properties)) { + return false; + } + + if (!$propertyType->equals($type->properties[$name])) { + return false; + } + } + + if (count($this->optionalProperties) !== count($type->optionalProperties)) { + return false; + } + + foreach ($this->optionalProperties as $name) { + if (in_array($name, $type->optionalProperties, true)) { + continue; + } + + return false; + } + + return true; + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof HasPropertyType) { + $properties = $this->properties; + unset($properties[$typeToRemove->getPropertyName()]); + $optionalProperties = array_values(array_filter($this->optionalProperties, static fn (string $propertyName) => $propertyName !== $typeToRemove->getPropertyName())); + + return new self($properties, $optionalProperties); + } + + return null; + } + + public function makePropertyRequired(string $propertyName): self + { + if (array_key_exists($propertyName, $this->properties)) { + $optionalProperties = array_values(array_filter($this->optionalProperties, static fn (string $currentPropertyName) => $currentPropertyName !== $propertyName)); + + return new self($this->properties, $optionalProperties); + } + + return $this; + } + + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { + if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { + return $receivedType->inferTemplateTypesOn($this); + } + + if ($receivedType instanceof self) { + $typeMap = TemplateTypeMap::createEmpty(); + $scope = new OutOfClassScope(); + foreach ($this->properties as $name => $propertyType) { + if ($receivedType->hasProperty($name)->no()) { + continue; + } + + try { + $receivedProperty = $receivedType->getProperty($name, $scope); + } catch (MissingPropertyFromReflectionException) { + continue; + } + if (!$receivedProperty->isPublic()) { + continue; + } + if ($receivedProperty->isStatic()) { + continue; + } + $receivedPropertyType = $receivedProperty->getReadableType(); + $typeMap = $typeMap->union($propertyType->inferTemplateTypes($receivedPropertyType)); + } + + return $typeMap; + } + + return TemplateTypeMap::createEmpty(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); + $references = []; + foreach ($this->properties as $propertyType) { + foreach ($propertyType->getReferencedTemplateTypes($variance) as $reference) { + $references[] = $reference; + } + } + + return $references; + } + + public function describe(VerbosityLevel $level): string + { + $callback = function () use ($level): string { + $items = []; + foreach ($this->properties as $name => $propertyType) { + $optional = in_array($name, $this->optionalProperties, true); + $items[] = sprintf('%s%s: %s', $name, $optional ? '?' : '', $propertyType->describe($level)); + } + return sprintf('object{%s}', implode(', ', $items)); + }; + return $level->handle( + $callback, + $callback, + ); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getEnumCases(): array + { + return []; + } + + public function traverse(callable $cb): Type + { + $properties = []; + $stillOriginal = true; + + foreach ($this->properties as $name => $propertyType) { + $transformed = $cb($propertyType); + if ($transformed !== $propertyType) { + $stillOriginal = false; + } + + $properties[$name] = $transformed; + } + + if ($stillOriginal) { + return $this; + } + + return new self($properties, $this->optionalProperties); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right->isObject()->yes()) { + return $this; + } + + $properties = []; + $stillOriginal = true; + + $scope = new OutOfClassScope(); + foreach ($this->properties as $name => $propertyType) { + if (!$right->hasProperty($name)->yes()) { + return $this; + } + $transformed = $cb($propertyType, $right->getProperty($name, $scope)->getReadableType()); + if ($transformed !== $propertyType) { + $stillOriginal = false; + } + + $properties[$name] = $transformed; + } + + if ($stillOriginal) { + return $this; + } + + return new self($properties, $this->optionalProperties); + } + + public function exponentiate(Type $exponent): Type + { + if (!$exponent instanceof NeverType && !$this->isSuperTypeOf($exponent)->no()) { + return TypeCombinator::union($this, $exponent); + } + + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + $items = []; + foreach ($this->properties as $name => $type) { + if (ConstantArrayType::isValidIdentifier($name)) { + $keyNode = new IdentifierTypeNode($name); + } else { + $keyPhpDocNode = (new ConstantStringType($name))->toPhpDocNode(); + if (!$keyPhpDocNode instanceof ConstTypeNode) { + continue; + } + + /** @var ConstExprStringNode $keyNode */ + $keyNode = $keyPhpDocNode->constExpr; + } + $items[] = new ObjectShapeItemNode( + $keyNode, + in_array($name, $this->optionalProperties, true), + $type->toPhpDocNode(), + ); + } + + return new ObjectShapeNode($items); + } + +} diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index eb0f926598..cc834e2950 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -3,44 +3,56 @@ namespace PHPStan\Type; use ArrayAccess; +use ArrayObject; use Closure; -use DateTime; -use DateTimeImmutable; +use Countable; use DateTimeInterface; use Iterator; use IteratorAggregate; use PHPStan\Analyser\OutOfClassScope; -use PHPStan\Broker\Broker; use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\FunctionCallableVariant; +use PHPStan\Reflection\ClassConstantReflection; 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\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; 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\UnionTypeUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; 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\Generic\TemplateTypeHelper; +use PHPStan\Type\Traits\MaybeIterableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use Stringable; +use Throwable; use Traversable; -use function array_keys; +use function array_key_exists; use function array_map; use function array_values; use function count; +use function implode; use function in_array; use function sprintf; use function strtolower; @@ -49,15 +61,29 @@ class ObjectType implements TypeWithClassName, SubtractableType { + use MaybeIterableTypeTrait; + use NonArrayTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; use NonGeneralizableTypeTrait; - private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList', 'Threaded']; + private const EXTRA_OFFSET_CLASSES = [ + 'DOMNamedNodeMap', // Only read and existence + 'Dom\NamedNodeMap', // Only read and existence + 'DOMNodeList', // Only read and existence + 'Dom\NodeList', // Only read and existence + 'Dom\HTMLCollection', // Only read and existence + 'Dom\DtdNamedNodeMap', // Only read and existence + 'PDORow', // Only read and existence + 'ResourceBundle', // Only read + 'FFI\CData', // Very funky and weird + 'SimpleXMLElement', + 'Threaded', + ]; private ?Type $subtractedType; - /** @var array> */ + /** @var array> */ private static array $superTypes = []; private ?self $cachedParent = null; @@ -71,12 +97,17 @@ class ObjectType implements TypeWithClassName, SubtractableType /** @var array>> */ private static array $properties = []; - /** @var array> */ + /** @var array> */ private static array $ancestors = []; - /** @var array */ + /** @var array */ private array $currentAncestors = []; + private ?string $cachedDescription = null; + + /** @var array> */ + private static array $enumCases = []; + /** @api */ public function __construct( private string $className, @@ -97,6 +128,7 @@ public static function resetCaches(): void self::$methods = []; self::$properties = []; self::$ancestors = []; + self::$enumCases = []; } private static function createFromReflection(ClassReflection $reflection): self @@ -108,6 +140,9 @@ private static function createFromReflection(ClassReflection $reflection): self return new GenericObjectType( $reflection->getName(), $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()), + null, + null, + $reflection->varianceMapToList($reflection->getCallSiteVarianceMap()), ); } @@ -127,14 +162,18 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createYes(); } - if ($classReflection->isFinal()) { - return TrinaryLogic::createNo(); + if ($classReflection->allowsDynamicProperties()) { + return TrinaryLogic::createMaybe(); } - return TrinaryLogic::createMaybe(); + if (!$classReflection->isFinal()) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createNo(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -157,7 +196,27 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember throw new ClassNotFoundException($this->className); } - if (!$nakedClassReflection->hasProperty($propertyName)) { + if ($nakedClassReflection->isEnum()) { + if ( + $propertyName === 'name' + || ($propertyName === 'value' && $nakedClassReflection->isBackedEnum()) + ) { + $properties = []; + foreach ($this->getEnumCases() as $enumCase) { + $properties[] = $enumCase->getUnresolvedPropertyPrototype($propertyName, $scope); + } + + if (count($properties) > 0) { + if (count($properties) === 1) { + return $properties[0]; + } + + return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $properties); + } + } + } + + if (!$nakedClassReflection->hasNativeProperty($propertyName)) { $nakedClassReflection = $this->getClassReflection(); } @@ -169,7 +228,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); $resolvedClassReflection = null; - if ($ancestor !== null) { + if ($ancestor !== null && $ancestor->hasProperty($propertyName)->yes()) { $resolvedClassReflection = $ancestor->getClassReflection(); if ($ancestor !== $this) { $property = $ancestor->getUnresolvedPropertyPrototype($propertyName, $scope)->getNakedProperty(); @@ -205,15 +264,30 @@ public function getPropertyWithoutTransformingStatic(string $propertyName, Class return $classReflection->getProperty($propertyName, $scope); } - /** - * @return string[] - */ public function getReferencedClasses(): array { return [$this->className]; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + if ($this->className === '') { + return []; + } + return [$this->className]; + } + + public function getObjectClassReflections(): array + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return []; + } + + return [$classReflection]; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof StaticType) { return $this->checkSubclassAcceptability($type->getClassName()); @@ -224,22 +298,32 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof ClosureType) { - return $this->isInstanceOf(Closure::class); + return new AcceptsResult($this->isInstanceOf(Closure::class), []); } if ($type instanceof ObjectWithoutClassType) { - return TrinaryLogic::createMaybe(); + return AcceptsResult::createMaybe(); } - if (!$type instanceof TypeWithClassName) { - return TrinaryLogic::createNo(); + $thatClassNames = $type->getObjectClassNames(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); + } + + if ($thatClassNames === []) { + return AcceptsResult::createNo(); } - return $this->checkSubclassAcceptability($type->getClassName()); + return $this->checkSubclassAcceptability($thatClassNames[0]); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { + $thatClassNames = $type->getObjectClassNames(); + if (!$type instanceof CompoundType && $thatClassNames === [] && !$type instanceof ObjectWithoutClassType) { + return IsSuperTypeOfResult::createNo(); + } + $thisDescription = $this->describeCache(); if ($type instanceof self) { @@ -256,24 +340,28 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return self::$superTypes[$thisDescription][$description] = $type->isSubTypeOf($this); } + if ($type instanceof ClosureType) { + return self::$superTypes[$thisDescription][$description] = new IsSuperTypeOfResult($this->isInstanceOf(Closure::class), []); + } + if ($type instanceof ObjectWithoutClassType) { if ($type->getSubtractedType() !== null) { $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); if ($isSuperType->yes()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } } - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); - } - - if (!$type instanceof TypeWithClassName) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } + $transformResult = static fn (IsSuperTypeOfResult $result) => $result; if ($this->subtractedType !== null) { $isSuperType = $this->subtractedType->isSuperTypeOf($type); if ($isSuperType->yes()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); + } + if ($isSuperType->maybe()) { + $transformResult = static fn (IsSuperTypeOfResult $result) => $result->and(IsSuperTypeOfResult::createMaybe()); } } @@ -283,47 +371,69 @@ public function isSuperTypeOf(Type $type): TrinaryLogic ) { $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); if ($isSuperType->yes()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } } $thisClassName = $this->className; - $thatClassName = $type->getClassName(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); + } - if ($thatClassName === $thisClassName) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); + $thisClassReflection = $this->getClassReflection(); + $thatClassReflections = $type->getObjectClassReflections(); + if (count($thatClassReflections) === 1) { + $thatClassReflection = $thatClassReflections[0]; + } else { + $thatClassReflection = null; } - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if ($thisClassReflection === null || $thatClassReflection === null) { + if ($thatClassNames[0] === $thisClassName) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); + } + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); + } + + if ($thatClassNames[0] === $thisClassName) { + if ($thisClassReflection->getNativeReflection()->isFinal()) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); + } + + if ($thisClassReflection->hasFinalByKeywordOverride()) { + if (!$thatClassReflection->hasFinalByKeywordOverride()) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createMaybe()); + } + } - if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClassName)) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - $thisClassReflection = $this->getClassReflection(); - $thatClassReflection = $reflectionProvider->getClass($thatClassName); + if ($thisClassReflection->isTrait() || $thatClassReflection->isTrait()) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); + } if ($thisClassReflection->getName() === $thatClassReflection->getName()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - if ($thatClassReflection->isSubclassOf($thisClassName)) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); + if ($thatClassReflection->isSubclassOfClass($thisClassReflection)) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - if ($thisClassReflection->isSubclassOf($thatClassName)) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + if ($thisClassReflection->isSubclassOfClass($thatClassReflection)) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - if ($thisClassReflection->isInterface() && !$thatClassReflection->getNativeReflection()->isFinal()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + if ($thisClassReflection->isInterface() && !$thatClassReflection->isFinalByKeyword()) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - if ($thatClassReflection->isInterface() && !$thisClassReflection->getNativeReflection()->isFinal()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + if ($thatClassReflection->isInterface() && !$thisClassReflection->isFinalByKeyword()) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -341,11 +451,7 @@ public function equals(Type $type): bool } if ($this->subtractedType === null) { - if ($type->subtractedType === null) { - return true; - } - - return false; + return $type->subtractedType === null; } if ($type->subtractedType === null) { @@ -355,16 +461,16 @@ public function equals(Type $type): bool return $this->subtractedType->equals($type->subtractedType); } - private function checkSubclassAcceptability(string $thatClass): TrinaryLogic + private function checkSubclassAcceptability(string $thatClass): AcceptsResult { if ($this->className === $thatClass) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClass)) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } $thisReflection = $this->getClassReflection(); @@ -372,17 +478,17 @@ private function checkSubclassAcceptability(string $thatClass): TrinaryLogic if ($thisReflection->getName() === $thatReflection->getName()) { // class alias - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($thisReflection->isInterface() && $thatReflection->isInterface()) { - return TrinaryLogic::createFromBoolean( - $thatReflection->implementsInterface($this->className), + return AcceptsResult::createFromBoolean( + $thatReflection->implementsInterface($thisReflection->getName()), ); } - return TrinaryLogic::createFromBoolean( - $thatReflection->isSubclassOf($this->className), + return AcceptsResult::createFromBoolean( + $thatReflection->isSubclassOfClass($thisReflection), ); } @@ -400,7 +506,9 @@ public function describe(VerbosityLevel $level): string $preciseWithSubtracted = function () use ($level): string { $description = $this->className; if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe($level)) + : sprintf('~%s', $this->subtractedType->describe($level)); } return $description; @@ -431,13 +539,29 @@ protected function describeAdditionalCacheKey(): string private function describeCache(): string { + if ($this->cachedDescription !== null) { + return $this->cachedDescription; + } + if (static::class !== self::class) { - return $this->describe(VerbosityLevel::cache()); + return $this->cachedDescription = $this->describe(VerbosityLevel::cache()); } $description = $this->className; + + if ($this instanceof GenericObjectType) { + $description .= '<'; + $typeDescriptions = []; + foreach ($this->getTypes() as $type) { + $typeDescriptions[] = $type->describe(VerbosityLevel::cache()); + } + $description .= '<' . implode(', ', $typeDescriptions) . '>'; + } + if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache())); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe(VerbosityLevel::cache())) + : sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache())); } $reflection = $this->classReflection; @@ -445,9 +569,13 @@ private function describeCache(): string $description .= '-'; $description .= (string) $reflection->getNativeReflection()->getStartLine(); $description .= '-'; + + if ($reflection->hasFinalByKeywordOverride()) { + $description .= 'f=' . ($reflection->isFinalByKeyword() ? 't' : 'f'); + } } - return $description; + return $this->cachedDescription = $description; } public function toNumber(): Type @@ -462,6 +590,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { if ($this->isInstanceOf('SimpleXMLElement')->yes()) { @@ -485,13 +618,21 @@ public function toFloat(): Type public function toString(): Type { + if ($this->isInstanceOf('BcMath\Number')->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryNonEmptyStringType(), + ]); + } + $classReflection = $this->getClassReflection(); if ($classReflection === null) { return new ErrorType(); } if ($classReflection->hasNativeMethod('__toString')) { - return ParametersAcceptorSelector::selectSingle($this->getMethod('__toString', new OutOfClassScope())->getVariants())->getReturnType(); + return $this->getMethod('__toString', new OutOfClassScope())->getOnlyVariant()->getReturnType(); } return new ErrorType(); @@ -508,9 +649,9 @@ public function toArray(): Type if ( !$classReflection->getNativeReflection()->isUserDefined() + || $classReflection->is(ArrayObject::class) || UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( $reflectionProvider, - Broker::getInstance()->getUniversalObjectCratesClasses(), $classReflection, ) ) { @@ -519,6 +660,8 @@ public function toArray(): Type $arrayKeys = []; $arrayValues = []; + $isFinal = $classReflection->isFinal(); + do { foreach ($classReflection->getNativeReflection()->getProperties() as $nativeProperty) { if ($nativeProperty->isStatic()) { @@ -549,18 +692,78 @@ public function toArray(): Type $classReflection = $classReflection->getParentClass(); } while ($classReflection !== null); + if (!$isFinal && count($arrayKeys) === 0) { + return new ArrayType(new MixedType(), new MixedType()); + } + return new ConstantArrayType($arrayKeys, $arrayValues); } + public function toArrayKey(): Type + { + return $this->toString(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + $classReflection = $this->getClassReflection(); + if ( + $classReflection === null + || !$classReflection->hasNativeMethod('__toString') + ) { + return $this; + } + + return TypeCombinator::union($this, $this->toString()); + } + + return $this; + } + public function toBoolean(): BooleanType { - if ($this->isInstanceOf('SimpleXMLElement')->yes()) { + if ( + $this->isInstanceOf('SimpleXMLElement')->yes() + || $this->isInstanceOf('BcMath\Number')->yes() + ) { return new BooleanType(); } return new ConstantBooleanType(true); } + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isEnum(): TrinaryLogic + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return TrinaryLogic::createMaybe(); + } + + if ( + $classReflection->isEnum() + || $classReflection->is('UnitEnum') + ) { + return TrinaryLogic::createYes(); + } + + if ( + $classReflection->isInterface() + && !$classReflection->is(Stringable::class) // enums cannot have __toString + && !$classReflection->is(Throwable::class) // enums cannot extend Exception/Error + && !$classReflection->is(DateTimeInterface::class) // userland classes cannot extend DateTimeInterface + ) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createNo(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -593,7 +796,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -615,7 +818,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce throw new ClassNotFoundException($this->className); } - if (!$nakedClassReflection->hasMethod($methodName)) { + if (!$nakedClassReflection->hasNativeMethod($methodName)) { $nakedClassReflection = $this->getClassReflection(); } @@ -662,7 +865,7 @@ public function hasConstant(string $constantName): TrinaryLogic ); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { $class = $this->getClassReflection(); if ($class === null) { @@ -672,6 +875,46 @@ public function getConstant(string $constantName): ConstantReflection return $class->getConstant($constantName); } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return new ErrorType(); + } + + $ancestorClassReflection = $classReflection->getAncestorWithClassName($ancestorClassName); + if ($ancestorClassReflection === null) { + return new ErrorType(); + } + + $activeTemplateTypeMap = $ancestorClassReflection->getPossiblyIncompleteActiveTemplateTypeMap(); + $type = $activeTemplateTypeMap->getType($templateTypeName); + if ($type === null) { + return new ErrorType(); + } + if ($type instanceof ErrorType) { + $templateTypeMap = $ancestorClassReflection->getTemplateTypeMap(); + $templateType = $templateTypeMap->getType($templateTypeName); + if ($templateType === null) { + return $type; + } + + $bound = TemplateTypeHelper::resolveToBounds($templateType); + if ($bound instanceof MixedType && $bound->isExplicitMixed()) { + return new MixedType(false); + } + + return TemplateTypeHelper::resolveToDefaults($templateType); + } + + return $type; + } + + public function getConstantStrings(): array + { + return []; + } + public function isIterable(): TrinaryLogic { return $this->isInstanceOf(Traversable::class); @@ -683,43 +926,43 @@ public function isIterableAtLeastOnce(): TrinaryLogic ->and(TrinaryLogic::createMaybe()); } + public function getArraySize(): Type + { + if ($this->isInstanceOf(Countable::class)->no()) { + return new ErrorType(); + } + + return IntegerRangeType::fromInterval(0, null); + } + public function getIterableKeyType(): Type { $isTraversable = false; - if ($this->isInstanceOf(Traversable::class)->yes()) { + if ($this->isInstanceOf(IteratorAggregate::class)->yes()) { + $keyType = RecursionGuard::run($this, fn (): Type => $this->getMethod('getIterator', new OutOfClassScope())->getOnlyVariant()->getReturnType()->getIterableKeyType()); $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(); - } + if (!$keyType instanceof MixedType || $keyType->isExplicitMixed()) { + return $keyType; + } + } - return $traverse($type); - }); + $extraOffsetAccessible = $this->isExtraOffsetAccessibleClass()->yes(); + if (!$extraOffsetAccessible && $this->isInstanceOf(Traversable::class)->yes()) { + $isTraversable = true; + $tKey = $this->getTemplateType(Traversable::class, 'TKey'); + if (!$tKey instanceof ErrorType) { + if (!$tKey instanceof MixedType || $tKey->isExplicitMixed()) { + return $tKey; } } } + if ($this->isInstanceOf(Iterator::class)->yes()) { - return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle( - $this->getMethod('key', new OutOfClassScope())->getVariants(), - )->getReturnType()); + return RecursionGuard::run($this, fn (): Type => $this->getMethod('key', new OutOfClassScope())->getOnlyVariant()->getReturnType()); } - 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 ($extraOffsetAccessible) { + return new MixedType(true); } if ($isTraversable) { @@ -729,44 +972,44 @@ public function getIterableKeyType(): Type return new ErrorType(); } + public function getFirstIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getLastIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + public function getIterableValueType(): Type { $isTraversable = false; - if ($this->isInstanceOf(Traversable::class)->yes()) { + if ($this->isInstanceOf(IteratorAggregate::class)->yes()) { + $valueType = RecursionGuard::run($this, fn (): Type => $this->getMethod('getIterator', new OutOfClassScope())->getOnlyVariant()->getReturnType()->getIterableValueType()); $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(); - } + if (!$valueType instanceof MixedType || $valueType->isExplicitMixed()) { + return $valueType; + } + } - return $traverse($type); - }); + $extraOffsetAccessible = $this->isExtraOffsetAccessibleClass()->yes(); + if (!$extraOffsetAccessible && $this->isInstanceOf(Traversable::class)->yes()) { + $isTraversable = true; + $tValue = $this->getTemplateType(Traversable::class, 'TValue'); + if (!$tValue instanceof ErrorType) { + if (!$tValue instanceof MixedType || $tValue->isExplicitMixed()) { + return $tValue; } } } if ($this->isInstanceOf(Iterator::class)->yes()) { - return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle( - $this->getMethod('current', new OutOfClassScope())->getVariants(), - )->getReturnType()); + return RecursionGuard::run($this, fn (): Type => $this->getMethod('current', new OutOfClassScope())->getOnlyVariant()->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 ($extraOffsetAccessible) { + return new MixedType(true); } if ($isTraversable) { @@ -776,7 +1019,62 @@ public function getIterableValueType(): Type return new ErrorType(); } - public function isArray(): TrinaryLogic + public function getFirstIterableValueType(): Type + { + return $this->getIterableValueType(); + } + + public function getLastIterableValueType(): Type + { + return $this->getIterableValueType(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -796,11 +1094,62 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + return $type->isFalse()->yes() + ? new ConstantBooleanType(false) + : new BooleanType(); + } + private function isExtraOffsetAccessibleClass(): TrinaryLogic { $classReflection = $this->getClassReflection(); @@ -809,10 +1158,7 @@ private function isExtraOffsetAccessibleClass(): TrinaryLogic } foreach (self::EXTRA_OFFSET_CLASSES as $extraOffsetClass) { - if ($classReflection->getName() === $extraOffsetClass) { - return TrinaryLogic::createYes(); - } - if ($classReflection->isSubclassOf($extraOffsetClass)) { + if ($classReflection->is($extraOffsetClass)) { return TrinaryLogic::createYes(); } } @@ -835,11 +1181,16 @@ public function isOffsetAccessible(): TrinaryLogic ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->isOffsetAccessible(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($this->isInstanceOf(ArrayAccess::class)->yes()) { $acceptedOffsetType = RecursionGuard::run($this, function (): Type { - $parameters = ParametersAcceptorSelector::selectSingle($this->getMethod('offsetSet', new OutOfClassScope())->getVariants())->getParameters(); + $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters(); if (count($parameters) < 2) { throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', @@ -871,7 +1222,7 @@ public function getOffsetValueType(Type $offsetType): Type } if ($this->isInstanceOf(ArrayAccess::class)->yes()) { - return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle($this->getMethod('offsetGet', new OutOfClassScope())->getVariants())->getReturnType()); + return RecursionGuard::run($this, fn (): Type => $this->getMethod('offsetGet', new OutOfClassScope())->getOnlyVariant()->getReturnType()); } return new ErrorType(); @@ -886,7 +1237,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni 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(); + $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters(); if (count($parameters) < 2) { throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', @@ -917,6 +1268,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + if ($this->isOffsetAccessible()->no()) { + return new ErrorType(); + } + + return $this; + } + public function unsetOffset(Type $offsetType): Type { if ($this->isOffsetAccessible()->no()) { @@ -926,12 +1286,57 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function getEnumCases(): array + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return []; + } + + if (!$classReflection->isEnum()) { + return []; + } + + $cacheKey = $this->describeCache(); + if (array_key_exists($cacheKey, self::$enumCases)) { + return self::$enumCases[$cacheKey]; + } + + $className = $classReflection->getName(); + + if ($this->subtractedType !== null) { + $subtractedEnumCaseNames = []; + + foreach ($this->subtractedType->getEnumCases() as $subtractedCase) { + $subtractedEnumCaseNames[$subtractedCase->getEnumCaseName()] = true; + } + + $cases = []; + foreach ($classReflection->getEnumCases() as $enumCase) { + if (array_key_exists($enumCase->getName(), $subtractedEnumCaseNames)) { + continue; + } + $cases[] = new EnumCaseObjectType($className, $enumCase->getName(), $classReflection); + } + } else { + $cases = []; + foreach ($classReflection->getEnumCases() as $enumCase) { + $cases[] = new EnumCaseObjectType($className, $enumCase->getName(), $classReflection); + } + } + + return self::$enumCases[$cacheKey] = $cases; + } + public function isCallable(): TrinaryLogic { - $parametersAcceptors = $this->findCallableParametersAcceptors(); + $parametersAcceptors = RecursionGuard::run($this, fn () => $this->findCallableParametersAcceptors()); if ($parametersAcceptors === null) { return TrinaryLogic::createNo(); } + if ($parametersAcceptors instanceof ErrorType) { + return TrinaryLogic::createNo(); + } if ( count($parametersAcceptors) === 1 @@ -943,13 +1348,10 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->className === Closure::class) { - return [new TrivialParametersAcceptor()]; + return [new TrivialParametersAcceptor('Closure')]; } $parametersAcceptors = $this->findCallableParametersAcceptors(); if ($parametersAcceptors === null) { @@ -960,7 +1362,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) } /** - * @return ParametersAcceptor[]|null + * @return CallableParametersAcceptor[]|null */ private function findCallableParametersAcceptors(): ?array { @@ -970,10 +1372,14 @@ private function findCallableParametersAcceptors(): ?array } if ($classReflection->hasNativeMethod('__invoke')) { - return $this->getMethod('__invoke', new OutOfClassScope())->getVariants(); + $method = $this->getMethod('__invoke', new OutOfClassScope()); + return FunctionCallableVariant::createFromVariants( + $method, + $method->getVariants(), + ); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } @@ -985,17 +1391,6 @@ public function isCloneable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['className'], - $properties['subtractedType'] ?? null, - ); - } - public function isInstanceOf(string $className): TrinaryLogic { $classReflection = $this->getClassReflection(); @@ -1003,10 +1398,18 @@ public function isInstanceOf(string $className): TrinaryLogic return TrinaryLogic::createMaybe(); } - if ($classReflection->isSubclassOf($className) || $classReflection->getName() === $className) { + if ($classReflection->is($className)) { return TrinaryLogic::createYes(); } + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if ($reflectionProvider->hasClass($className)) { + $thatClassReflection = $reflectionProvider->getClass($className); + if ($thatClassReflection->isFinal()) { + return TrinaryLogic::createNo(); + } + } + if ($classReflection->isInterface()) { return TrinaryLogic::createMaybe(); } @@ -1030,35 +1433,53 @@ 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); - } + if ($subtractedType !== null) { + $classReflection = $this->getClassReflection(); + $allowedSubTypes = $classReflection !== null ? $classReflection->getAllowedSubTypes() : null; + if ($allowedSubTypes !== null) { + $preciseVerbosity = VerbosityLevel::precise(); + + $originalAllowedSubTypes = $allowedSubTypes; + $subtractedSubTypes = []; + + $subtractedTypes = TypeUtils::flattenTypes($subtractedType); + foreach ($subtractedTypes as $subType) { + foreach ($allowedSubTypes as $key => $allowedSubType) { + if ($subType->equals($allowedSubType)) { + $description = $allowedSubType->describe($preciseVerbosity); + $subtractedSubTypes[$description] = $subType; + unset($allowedSubTypes[$key]); + continue 2; + } + } - 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); + if (count($allowedSubTypes) === 1) { + return array_values($allowedSubTypes)[0]; } - unset($cases[$subType->getEnumCaseName()]); - } + $subtractedSubTypes = array_values($subtractedSubTypes); + $subtractedSubTypesCount = count($subtractedSubTypes); + if ($subtractedSubTypesCount === count($originalAllowedSubTypes)) { + return new NeverType(); + } - $cases = array_values($cases); - if (count($cases) === 0) { - return new NeverType(); - } + if ($subtractedSubTypesCount === 0) { + return new self($this->className); + } + + if ($subtractedSubTypesCount === 1) { + return new self($this->className, $subtractedSubTypes[0]); + } - if (count($cases) === 1) { - return $cases[0]; + return new self($this->className, new UnionType($subtractedSubTypes)); } + } - return new UnionType(array_values($cases)); + if ($this->subtractedType === null && $subtractedType === null) { + return $this; } return new self($this->className, $subtractedType); @@ -1083,6 +1504,15 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->subtractedType === null) { + return $this; + } + + return new self($this->className); + } + public function getNakedClassReflection(): ?ClassReflection { if ($this->classReflection !== null) { @@ -1116,31 +1546,38 @@ public function getClassReflection(): ?ClassReflection return $classReflection; } - /** - * @return self|null - */ - public function getAncestorWithClassName(string $className): ?TypeWithClassName + public function getAncestorWithClassName(string $className): ?self { - if (isset($this->currentAncestors[$className])) { - return $this->currentAncestors[$className]; + if ($this->className === $className) { + return $this; } - $thisReflection = $this->getClassReflection(); - if ($thisReflection === null) { - return null; + if ($this->classReflection !== null && $className === $this->classReflection->getName()) { + return $this; + } + + if (array_key_exists($className, $this->currentAncestors)) { + return $this->currentAncestors[$className]; } - $description = $this->describeCache() . '-' . $thisReflection->getCacheKey(); - if (isset(self::$ancestors[$description][$className])) { + $description = $this->describeCache(); + if ( + array_key_exists($description, self::$ancestors) + && array_key_exists($className, self::$ancestors[$description]) + ) { return self::$ancestors[$description][$className]; } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); if (!$reflectionProvider->hasClass($className)) { - return null; + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null; } $theirReflection = $reflectionProvider->getClass($className); + $thisReflection = $this->getClassReflection(); + if ($thisReflection === null) { + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null; + } if ($theirReflection->getName() === $thisReflection->getName()) { return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = $this; } @@ -1160,7 +1597,7 @@ public function getAncestorWithClassName(string $className): ?TypeWithClassName } } - return null; + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null; } private function getParent(): ?ObjectType @@ -1197,13 +1634,21 @@ private function getInterfaces(): array 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) { + foreach (UnionType::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if ($this->getClassName() !== $baseClass) { + continue; + } - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTime::class) { - return new ObjectType(DateTimeImmutable::class); + foreach ($classes as $index => $class) { + if ($typeToRemove->getClassName() === $class) { + unset($classes[$index]); + + return TypeCombinator::union( + ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes), + ); + } + } } } @@ -1214,4 +1659,23 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function getFiniteTypes(): array + { + return $this->getEnumCases(); + } + + public function exponentiate(Type $exponent): Type + { + $object = new ObjectWithoutClassType(); + if (!$exponent instanceof NeverType && !$object->isSuperTypeOf($this)->no() && !$object->isSuperTypeOf($exponent)->no()) { + return TypeCombinator::union($this, $exponent); + } + return new ErrorType(); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->getClassName()); + } + } diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index b602f4651b..3abe064e3f 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -2,6 +2,8 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -32,26 +34,33 @@ public function __construct( $this->subtractedType = $subtractedType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createFromBoolean( - $type instanceof self || $type instanceof TypeWithClassName, + return AcceptsResult::createFromBoolean( + $type instanceof self || $type instanceof ObjectShapeType || $type->getObjectClassNames() !== [], ); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); @@ -59,27 +68,31 @@ public function isSuperTypeOf(Type $type): TrinaryLogic if ($type instanceof self) { if ($this->subtractedType === null) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type->subtractedType !== null) { $isSuperType = $type->subtractedType->isSuperTypeOf($this->subtractedType); if ($isSuperType->yes()) { - return TrinaryLogic::createYes(); + return $isSuperType; } } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } - if ($type instanceof TypeWithClassName) { - if ($this->subtractedType === null) { - return TrinaryLogic::createYes(); - } + if ($type instanceof ObjectShapeType) { + return IsSuperTypeOfResult::createYes(); + } - return $this->subtractedType->isSuperTypeOf($type)->negate(); + if ($type->getObjectClassNames() === []) { + return IsSuperTypeOfResult::createNo(); } - return TrinaryLogic::createNo(); + if ($this->subtractedType === null) { + return IsSuperTypeOfResult::createYes(); + } + + return $this->subtractedType->isSuperTypeOf($type)->negate(); } public function equals(Type $type): bool @@ -111,7 +124,9 @@ public function describe(VerbosityLevel $level): string function () use ($level): string { $description = 'object'; if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe($level)) + : sprintf('~%s', $this->subtractedType->describe($level)); } return $description; @@ -119,6 +134,16 @@ function () use ($level): string { ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getEnumCases(): array + { + return []; + } + public function subtract(Type $type): Type { if ($type instanceof self) { @@ -146,7 +171,6 @@ public function getSubtractedType(): ?Type return $this->subtractedType; } - public function traverse(callable $cb): Type { $subtractedType = $this->subtractedType !== null ? $cb($this->subtractedType) : null; @@ -158,6 +182,15 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->subtractedType === null) { + return $this; + } + + return new self(); + } + public function tryRemove(Type $typeToRemove): ?Type { if ($this->isSuperTypeOf($typeToRemove)->yes()) { @@ -167,12 +200,26 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function exponentiate(Type $exponent): Type + { + if (!$exponent instanceof NeverType && !$this->isSuperTypeOf($exponent)->no()) { + return TypeCombinator::union($this, $exponent); + } + + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['subtractedType'] ?? null); + return new IdentifierTypeNode('object'); } } diff --git a/src/Type/OffsetAccessType.php b/src/Type/OffsetAccessType.php new file mode 100644 index 0000000000..5e4ef1aec3 --- /dev/null +++ b/src/Type/OffsetAccessType.php @@ -0,0 +1,117 @@ +type->getReferencedClasses(), + $this->offset->getReferencedClasses(), + ); + } + + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return array_merge( + $this->type->getReferencedTemplateTypes($positionVariance), + $this->offset->getReferencedTemplateTypes($positionVariance), + ); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type) + && $this->offset->equals($type->offset); + } + + public function describe(VerbosityLevel $level): string + { + $printer = new Printer(); + + return $printer->print($this->toPhpDocNode()); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type) + && !TypeUtils::containsTemplateType($this->offset); + } + + protected function getResult(): Type + { + return $this->type->getOffsetValueType($this->offset); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + $offset = $cb($this->offset); + + if ($this->type === $type && $this->offset === $offset) { + return $this; + } + + return new self($type, $offset); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + $offset = $cb($this->offset, $right->offset); + + if ($this->type === $type && $this->offset === $offset) { + return $this; + } + + return new self($type, $offset); + } + + public function toPhpDocNode(): TypeNode + { + return new OffsetAccessTypeNode( + $this->type->toPhpDocNode(), + $this->offset->toPhpDocNode(), + ); + } + +} diff --git a/src/Type/OperatorTypeSpecifyingExtension.php b/src/Type/OperatorTypeSpecifyingExtension.php index dc26ce3633..a9d2fbe129 100644 --- a/src/Type/OperatorTypeSpecifyingExtension.php +++ b/src/Type/OperatorTypeSpecifyingExtension.php @@ -2,7 +2,25 @@ namespace PHPStan\Type; -/** @api */ +/** + * This is the extension interface to implement if you want to describe + * how arithmetic operators like +, -, *, ^, / should infer types + * for PHP extensions that overload the behaviour, like GMP. + * + * To register it in the configuration file use the `phpstan.broker.operatorTypeSpecifyingExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.operatorTypeSpecifyingExtension + * ``` + * + * Learn more: https://github.com/phpstan/phpstan/pull/2114 + * + * @api + */ interface OperatorTypeSpecifyingExtension { diff --git a/src/Type/OperatorTypeSpecifyingExtensionRegistry.php b/src/Type/OperatorTypeSpecifyingExtensionRegistry.php index 9061500cf2..7b84648f28 100644 --- a/src/Type/OperatorTypeSpecifyingExtensionRegistry.php +++ b/src/Type/OperatorTypeSpecifyingExtensionRegistry.php @@ -2,29 +2,19 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; -use PHPStan\Reflection\BrokerAwareExtension; use function array_filter; use function array_values; -class OperatorTypeSpecifyingExtensionRegistry +final class OperatorTypeSpecifyingExtensionRegistry { /** * @param OperatorTypeSpecifyingExtension[] $extensions */ public function __construct( - Broker $broker, private array $extensions, ) { - foreach ($extensions as $extension) { - if (!$extension instanceof BrokerAwareExtension) { - continue; - } - - $extension->setBroker($broker); - } } /** diff --git a/src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php b/src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..0b847b8ca6 --- /dev/null +++ b/src/Type/PHPStan/ClassNameUsageLocationCreateIdentifierDynamicReturnTypeExtension.php @@ -0,0 +1,66 @@ +getName() === 'createIdentifier'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + if (!isset($args[0])) { + return null; + } + + $secondPartType = $scope->getType($args[0]->value); + $secondPartValues = $secondPartType->getConstantStrings(); + if (count($secondPartValues) === 0) { + return null; + } + + $reflection = new ReflectionClass(ClassNameUsageLocation::class); + $identifiers = []; + $locationValueType = $scope->getType(new PropertyFetch($methodCall->var, 'value')); + foreach ($reflection->getConstants() as $constant) { + if (!$locationValueType->isSuperTypeOf($scope->getTypeFromValue($constant))->yes()) { + continue; + } + $location = ClassNameUsageLocation::from($constant); + foreach ($secondPartValues as $secondPart) { + $identifiers[] = $location->createIdentifier($secondPart->getValue()); + } + } + + sort($identifiers); + + $types = []; + foreach ($identifiers as $identifier) { + $types[] = $scope->getTypeFromValue($identifier); + } + + return TypeCombinator::union(...$types); + } + +} diff --git a/src/Type/ParserNodeTypeToPHPStanType.php b/src/Type/ParserNodeTypeToPHPStanType.php index c55b6ba2fe..e616fffc0e 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -13,7 +13,7 @@ use function in_array; use function strtolower; -class ParserNodeTypeToPHPStanType +final class ParserNodeTypeToPHPStanType { /** @@ -53,7 +53,7 @@ public static function resolve($type, ?ClassReflection $classReflection): Type $types = []; foreach ($type->types as $intersectionTypeType) { $innerType = self::resolve($intersectionTypeType, $classReflection); - if (!$innerType instanceof ObjectType) { + if (!$innerType->isObject()->yes()) { return new NeverType(); } @@ -84,6 +84,8 @@ public static function resolve($type, ?ClassReflection $classReflection): Type return new VoidType(); } elseif ($type === 'object') { return new ObjectWithoutClassType(); + } elseif ($type === 'true') { + return new ConstantBooleanType(true); } elseif ($type === 'false') { return new ConstantBooleanType(false); } elseif ($type === 'null') { @@ -91,7 +93,7 @@ public static function resolve($type, ?ClassReflection $classReflection): Type } elseif ($type === 'mixed') { return new MixedType(true); } elseif ($type === 'never') { - return new NeverType(true); + return new NonAcceptingNeverType(); } return new MixedType(); diff --git a/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..5ce81dbe84 --- /dev/null +++ b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'abs'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + + if (!isset($args[0])) { + return null; + } + + $inputType = $scope->getType($args[0]->value); + + $outputType = $inputType->toAbsoluteNumber(); + + if ($outputType instanceof ErrorType) { + return null; + } + + return $outputType; + } + +} diff --git a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php index 2149ad66d2..6e3c75b9a1 100644 --- a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php @@ -5,19 +5,19 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function array_key_exists; -class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var int[] */ - private array $functionNames = [ + private const FUNCTION_NAMES = [ 'array_unique' => 0, - 'array_change_key_case' => 0, 'array_diff_assoc' => 0, 'array_diff_key' => 0, 'array_diff_uassoc' => 0, @@ -27,7 +27,6 @@ class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnT 'array_udiff_uassoc' => 0, 'array_udiff' => 0, 'array_intersect_assoc' => 0, - 'array_intersect_key' => 0, 'array_intersect_uassoc' => 0, 'array_intersect_ukey' => 0, 'array_intersect' => 0, @@ -38,15 +37,15 @@ class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnT public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return isset($this->functionNames[$functionReflection->getName()]); + return array_key_exists($functionReflection->getName(), self::FUNCTION_NAMES); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $argumentPosition = $this->functionNames[$functionReflection->getName()]; + $argumentPosition = self::FUNCTION_NAMES[$functionReflection->getName()]; if (!isset($functionCall->getArgs()[$argumentPosition])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argument = $functionCall->getArgs()[$argumentPosition]; @@ -58,10 +57,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argumentValueType = $argumentValueType->getIterableValueType()->generalize(GeneralizePrecision::moreSpecific()); } - return new ArrayType( + $array = new ArrayType( $argumentKeyType, $argumentValueType, ); + if ($functionReflection->getName() === 'array_unique' && $argumentType->isIterableAtLeastOnce()->yes()) { + $array = TypeCombinator::intersect($array, new NonEmptyArrayType()); + } + + return $array; } } diff --git a/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..5367744502 --- /dev/null +++ b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php @@ -0,0 +1,159 @@ +getName() === 'array_change_key_case'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (!isset($functionCall->getArgs()[0])) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (!isset($functionCall->getArgs()[1])) { + $case = CASE_LOWER; + } else { + $caseType = $scope->getType($functionCall->getArgs()[1]->value); + $scalarValues = $caseType->getConstantScalarValues(); + if (count($scalarValues) === 1) { + $case = (int) $scalarValues[0]; + } else { + $case = null; + } + } + + $constantArrays = $arrayType->getConstantArrays(); + if (count($constantArrays) > 0) { + $arrayTypes = []; + foreach ($constantArrays as $constantArray) { + $newConstantArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $valueTypes = $constantArray->getValueTypes(); + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $valueType = $valueTypes[$i]; + + $constantStrings = $keyType->getConstantStrings(); + if (count($constantStrings) > 0) { + $keyType = TypeCombinator::union( + ...array_map( + fn (ConstantStringType $type): Type => $this->mapConstantString($type, $case), + $constantStrings, + ), + ); + } + + $newConstantArrayBuilder->setOffsetValueType( + $keyType, + $valueType, + $constantArray->isOptionalKey($i), + ); + } + $newConstantArrayType = $newConstantArrayBuilder->getArray(); + if ($constantArray->isList()->yes()) { + $newConstantArrayType = TypeCombinator::intersect($newConstantArrayType, new AccessoryArrayListType()); + } + $arrayTypes[] = $newConstantArrayType; + } + + $newArrayType = TypeCombinator::union(...$arrayTypes); + } else { + $keysType = $arrayType->getIterableKeyType(); + + $keysType = TypeTraverser::map($keysType, function (Type $type, callable $traverse) use ($case): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + $constantStrings = $type->getConstantStrings(); + if (count($constantStrings) > 0) { + return TypeCombinator::union( + ...array_map( + fn (ConstantStringType $type): Type => $this->mapConstantString($type, $case), + $constantStrings, + ), + ); + } + + if ($type->isString()->yes()) { + $types = [new StringType()]; + if ($type->isNonFalsyString()->yes()) { + $types[] = new AccessoryNonFalsyStringType(); + } elseif ($type->isNonEmptyString()->yes()) { + $types[] = new AccessoryNonEmptyStringType(); + } + if ($type->isNumericString()->yes()) { + $types[] = new AccessoryNumericStringType(); + } + if ($case === CASE_LOWER) { + $types[] = new AccessoryLowercaseStringType(); + } elseif ($case === CASE_UPPER) { + $types[] = new AccessoryUppercaseStringType(); + } + + return TypeCombinator::intersect(...$types); + } + + return $type; + }); + + $newArrayType = TypeCombinator::intersect(new ArrayType( + $keysType, + $arrayType->getIterableValueType(), + ), ...TypeUtils::getAccessoryTypes($arrayType)); + } + + if ($arrayType->isIterableAtLeastOnce()->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + return $newArrayType; + } + + private function mapConstantString(ConstantStringType $type, ?int $case): Type + { + if ($case === CASE_LOWER) { + return new ConstantStringType(strtolower($type->getValue())); + } elseif ($case === CASE_UPPER) { + return new ConstantStringType(strtoupper($type->getValue())); + } + + return TypeCombinator::union( + new ConstantStringType(strtolower($type->getValue())), + new ConstantStringType(strtoupper($type->getValue())), + ); + } + +} diff --git a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..84ca5046de --- /dev/null +++ b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php @@ -0,0 +1,51 @@ +getName() === 'array_chunk'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + } + + $lengthType = $scope->getType($functionCall->getArgs()[1]->value); + $negativeOrZero = IntegerRangeType::fromInterval(null, 0); + if ($negativeOrZero->isSuperTypeOf($lengthType)->yes()) { + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new NullType(); + } + + $preserveKeysType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : new ConstantBooleanType(false); + + return $arrayType->chunkArray($lengthType, $preserveKeysType->isTrue()); + } + +} diff --git a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php index f044cc5f8f..51d8999c2f 100644 --- a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -6,9 +6,9 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -17,13 +17,11 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; -class ArrayColumnFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayColumnFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) @@ -35,18 +33,18 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_column'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $numArgs = count($functionCall->getArgs()); if ($numArgs < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $arrayType = $scope->getType($functionCall->getArgs()[0]->value); $columnType = $scope->getType($functionCall->getArgs()[1]->value); $indexType = $numArgs >= 3 ? $scope->getType($functionCall->getArgs()[2]->value) : null; - $constantArrayTypes = TypeUtils::getConstantArrays($arrayType); + $constantArrayTypes = $arrayType->getConstantArrays(); if (count($constantArrayTypes) === 1) { $type = $this->handleConstantArray($constantArrayTypes[0], $columnType, $indexType, $scope); if ($type !== null) { @@ -100,6 +98,9 @@ private function handleAnyArray(Type $arrayType, Type $columnType, ?Type $indexT if ($iterableAtLeastOnce->yes()) { $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); } + if ($indexType === null) { + $returnType = TypeCombinator::intersect($returnType, new AccessoryArrayListType()); + } return $returnType; } @@ -108,11 +109,14 @@ private function handleConstantArray(ConstantArrayType $arrayType, Type $columnT { $builder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($arrayType->getValueTypes() as $iterableValueType) { + foreach ($arrayType->getValueTypes() as $i => $iterableValueType) { $valueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); if ($valueType === null) { return null; } + if ($valueType instanceof NeverType) { + continue; + } if ($indexType !== null) { $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); @@ -133,7 +137,7 @@ private function handleConstantArray(ConstantArrayType $arrayType, Type $columnT if ($keyType !== null) { $keyType = $this->castToArrayKeyType($keyType); } - $builder->setOffsetValueType($keyType, $valueType); + $builder->setOffsetValueType($keyType, $valueType, $arrayType->isOptionalKey($i)); } return $builder->getArray(); @@ -141,7 +145,7 @@ private function handleConstantArray(ConstantArrayType $arrayType, Type $columnT private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $scope, bool $allowMaybe): ?Type { - $offsetIsNull = (new NullType())->isSuperTypeOf($offsetOrProperty); + $offsetIsNull = $offsetOrProperty->isNull(); if ($offsetIsNull->yes()) { return $type; } @@ -153,7 +157,7 @@ private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $ } if (!$type->canAccessProperties()->no()) { - $propertyTypes = TypeUtils::getConstantStrings($offsetOrProperty); + $propertyTypes = $offsetOrProperty->getConstantStrings(); if ($propertyTypes === []) { return new MixedType(); } @@ -195,10 +199,10 @@ private function castToArrayKeyType(Type $type): Type return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); } if ($isArray->no()) { - return ArrayType::castToArrayKeyType($type); + return $type->toArrayKey(); } $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); - $keyType = ArrayType::castToArrayKeyType($withoutArrayType); + $keyType = $withoutArrayType->toArrayKey(); if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return $keyType; } diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index 7b50fe74a2..f809967ad8 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -7,21 +7,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use function count; -class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) @@ -33,10 +35,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_combine'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $firstArg = $functionCall->getArgs()[0]->value; @@ -53,21 +55,43 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $valueTypes = $valuesParamType->getValueTypes(); if (count($keyTypes) !== count($valueTypes)) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } return new ConstantBooleanType(false); } $keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes); if ($keyTypes !== null) { - return new ConstantArrayType( - $keyTypes, - $valueTypes, - ); + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($keyTypes as $i => $keyType) { + $valueType = $valueTypes[$i]; + $builder->setOffsetValueType($keyType, $valueType); + } + + return $builder->getArray(); + } + } + + if ($keysParamType->isArray()->yes()) { + $itemType = $keysParamType->getIterableValueType(); + + if ($itemType->isInteger()->no()) { + if ($itemType->toString() instanceof ErrorType) { + return new NeverType(); + } + + $keyType = $itemType->toString(); + } else { + $keyType = $itemType; } + } else { + $keyType = new MixedType(); } $arrayType = new ArrayType( - $keysParamType instanceof ArrayType ? $keysParamType->getItemType() : new MixedType(), - $valuesParamType instanceof ArrayType ? $valuesParamType->getItemType() : new MixedType(), + $keyType, + $valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(), ); if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) { @@ -95,6 +119,10 @@ private function sanitizeConstantArrayKeyTypes(array $types): ?array $sanitizedTypes = []; foreach ($types as $type) { + if ($type->isInteger()->no() && ! $type->toString() instanceof ErrorType) { + $type = $type->toString(); + } + if ( !$type instanceof ConstantIntegerType && !$type instanceof ConstantStringType diff --git a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php index 0830d0e3c6..9871a469c9 100644 --- a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php @@ -5,13 +5,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayCurrentDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayCurrentDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'current'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index 9456a3e92e..fbadbac593 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php @@ -6,7 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -15,13 +15,12 @@ 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 DynamicFunctionReturnTypeExtension +final class ArrayFillFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const MAX_SIZE_USE_CONSTANT_ARRAY = 100; @@ -35,33 +34,26 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_fill'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 3) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $startIndexType = $scope->getType($functionCall->getArgs()[0]->value); $numberType = $scope->getType($functionCall->getArgs()[1]->value); - $valueType = $scope->getType($functionCall->getArgs()[2]->value); - - if ($numberType instanceof IntegerRangeType) { - if ($numberType->getMin() < 0) { - return TypeCombinator::union( - new ArrayType(new IntegerType(), $valueType), - new ConstantBooleanType(false), - ); - } - } + $isValidNumberType = IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($numberType); // check against negative-int, which is not allowed - if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($numberType)->yes()) { + if ($isValidNumberType->no()) { if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return new NeverType(); } return new ConstantBooleanType(false); } + $startIndexType = $scope->getType($functionCall->getArgs()[0]->value); + $valueType = $scope->getType($functionCall->getArgs()[2]->value); + if ( $startIndexType instanceof ConstantIntegerType && $numberType instanceof ConstantIntegerType @@ -84,14 +76,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $arrayBuilder->getArray(); } + $resultType = new ArrayType(new IntegerType(), $valueType); + if ((new ConstantIntegerType(0))->isSuperTypeOf($startIndexType)->yes()) { + $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType()); + } if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($numberType)->yes()) { - return new IntersectionType([ - new ArrayType(new IntegerType(), $valueType), - new NonEmptyArrayType(), - ]); + $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + + if (!$isValidNumberType->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $resultType = TypeCombinator::union($resultType, new ConstantBooleanType(false)); } - return new ArrayType(new IntegerType(), $valueType); + return $resultType; } } diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index e3792521de..b8c7fdfe97 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php @@ -4,47 +4,38 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; -class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_fill_keys'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $valueType = $scope->getType($functionCall->getArgs()[1]->value); $keysType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getConstantArrays($keysType); - if (count($constantArrays) === 0) { - return new ArrayType($keysType->getIterableValueType(), $valueType); - } - - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getValueTypes() as $keyType) { - $arrayBuilder->setOffsetValueType($keyType, $valueType); - } - $arrayTypes[] = $arrayBuilder->getArray(); + if ($keysType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return TypeCombinator::union(...$arrayTypes); + return $keysType->fillKeysArray($scope->getType($functionCall->getArgs()[1]->value)); } } diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..62dc52abf0 --- /dev/null +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php @@ -0,0 +1,32 @@ +getName() === 'array_filter'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $arrayArg = $functionCall->getArgs()[0]->value ?? null; + $callbackArg = $functionCall->getArgs()[1]->value ?? null; + $flagArg = $functionCall->getArgs()[2]->value ?? null; + + return $this->arrayFilterFunctionReturnTypeHelper->getType($scope, $arrayArg, $callbackArg, $flagArg); + } + +} diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php new file mode 100644 index 0000000000..30bbe16741 --- /dev/null +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php @@ -0,0 +1,340 @@ +getType($arrayArg); + $arrayArgType = TypeUtils::toBenevolentUnion($arrayArgType); + $keyType = $arrayArgType->getIterableKeyType(); + $itemType = $arrayArgType->getIterableValueType(); + + if ($itemType instanceof NeverType || $keyType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + if ($arrayArgType instanceof MixedType) { + return new BenevolentUnionType([ + new ArrayType(new MixedType(), new MixedType()), + new NullType(), + ]); + } + + if ($callbackArg === null || $scope->getType($callbackArg)->isNull()->yes()) { + return TypeCombinator::union( + ...array_map([$this, 'removeFalsey'], $arrayArgType->getArrays()), + ); + } + + $mode = $this->determineMode($flagArg, $scope); + if ($mode === null) { + return new ArrayType($keyType, $itemType); + } + + if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { + $statement = $callbackArg->stmts[0]; + if ($statement instanceof Return_ && $statement->expr !== null) { + if ($mode === self::USE_ITEM) { + $keyVar = null; + $itemVar = $callbackArg->params[0]->var; + } elseif ($mode === self::USE_KEY) { + $keyVar = $callbackArg->params[0]->var; + $itemVar = null; + } elseif ($mode === self::USE_BOTH) { + $keyVar = $callbackArg->params[1]->var ?? null; + $itemVar = $callbackArg->params[0]->var; + } + return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $statement->expr); + } + } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { + if ($mode === self::USE_ITEM) { + $keyVar = null; + $itemVar = $callbackArg->params[0]->var; + } elseif ($mode === self::USE_KEY) { + $keyVar = $callbackArg->params[0]->var; + $itemVar = null; + } elseif ($mode === self::USE_BOTH) { + $keyVar = $callbackArg->params[1]->var ?? null; + $itemVar = $callbackArg->params[0]->var; + } + return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $callbackArg->expr); + } elseif ( + ($callbackArg instanceof FuncCall || $callbackArg instanceof MethodCall || $callbackArg instanceof StaticCall) + && $callbackArg->isFirstClassCallable() + ) { + [$args, $itemVar, $keyVar] = $this->createDummyArgs($mode); + $expr = clone $callbackArg; + $expr->args = $args; + return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); + } else { + $constantStrings = $scope->getType($callbackArg)->getConstantStrings(); + if (count($constantStrings) > 0) { + $results = []; + [$args, $itemVar, $keyVar] = $this->createDummyArgs($mode); + + foreach ($constantStrings as $constantString) { + $funcName = self::createFunctionName($constantString->getValue()); + if ($funcName === null) { + $results[] = new ErrorType(); + continue; + } + + $expr = new FuncCall($funcName, $args); + $results[] = $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); + } + return TypeCombinator::union(...$results); + } + } + + return new ArrayType($keyType, $itemType); + } + + private function removeFalsey(Type $type): Type + { + $falseyTypes = StaticTypeFactory::falsey(); + + if (count($type->getConstantArrays()) > 0) { + $result = []; + foreach ($type->getConstantArrays() as $constantArray) { + $keys = $constantArray->getKeyTypes(); + $values = $constantArray->getValueTypes(); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($values as $offset => $value) { + $isFalsey = $falseyTypes->isSuperTypeOf($value); + + if ($isFalsey->maybe()) { + $builder->setOffsetValueType($keys[$offset], TypeCombinator::remove($value, $falseyTypes), true); + } elseif ($isFalsey->no()) { + $builder->setOffsetValueType($keys[$offset], $value, $constantArray->isOptionalKey($offset)); + } + } + + $result[] = $builder->getArray(); + } + + return TypeCombinator::union(...$result); + } + + $keyType = $type->getIterableKeyType(); + $valueType = $type->getIterableValueType(); + + $valueType = TypeCombinator::remove($valueType, $falseyTypes); + + if ($valueType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + return new ArrayType($keyType, $valueType); + } + + private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, Type $arrayType, Error|Variable|null $keyVar, Expr $expr): Type + { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $constantArrays = $arrayType->getConstantArrays(); + if (count($constantArrays) > 0) { + $results = []; + foreach ($constantArrays as $constantArray) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + $optionalKeys = $constantArray->getOptionalKeys(); + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $itemType = $constantArray->getValueTypes()[$i]; + [$newKeyType, $newItemType, $optional] = $this->processKeyAndItemType($scope, $keyType, $itemType, $itemVar, $keyVar, $expr); + $optional = $optional || in_array($i, $optionalKeys, true); + if ($newKeyType instanceof NeverType || $newItemType instanceof NeverType) { + continue; + } + if ($itemType->equals($newItemType) && $keyType->equals($newKeyType)) { + $builder->setOffsetValueType($keyType, $itemType, $optional); + continue; + } + + $builder->setOffsetValueType($newKeyType, $newItemType, true); + } + + $results[] = $builder->getArray(); + } + + return TypeCombinator::union(...$results); + } + + [$newKeyType, $newItemType] = $this->processKeyAndItemType($scope, $arrayType->getIterableKeyType(), $arrayType->getIterableValueType(), $itemVar, $keyVar, $expr); + + if ($newItemType instanceof NeverType || $newKeyType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + return new ArrayType($newKeyType, $newItemType); + } + + /** + * @return array{Type, Type, bool} + */ + private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type $itemType, Error|Variable|null $itemVar, Error|Variable|null $keyVar, Expr $expr): array + { + $itemVarName = null; + if ($itemVar !== null) { + if (!$itemVar instanceof Variable || !is_string($itemVar->name)) { + throw new ShouldNotHappenException(); + } + $itemVarName = $itemVar->name; + $scope = $scope->assignVariable($itemVarName, $itemType, new MixedType(), TrinaryLogic::createYes()); + } + + $keyVarName = null; + if ($keyVar !== null) { + if (!$keyVar instanceof Variable || !is_string($keyVar->name)) { + throw new ShouldNotHappenException(); + } + $keyVarName = $keyVar->name; + $scope = $scope->assignVariable($keyVarName, $keyType, new MixedType(), TrinaryLogic::createYes()); + } + + $booleanResult = $scope->getType($expr)->toBoolean(); + if ($booleanResult->isFalse()->yes()) { + return [new NeverType(), new NeverType(), false]; + } + + $scope = $scope->filterByTruthyValue($expr); + + return [ + $keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType, + $itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType, + !$booleanResult->isTrue()->yes(), + ]; + } + + private static function createFunctionName(string $funcName): ?Name + { + if ($funcName === '') { + return null; + } + + if ($funcName[0] === '\\') { + $funcName = substr($funcName, 1); + + if ($funcName === '') { + return null; + } + + return new Name\FullyQualified($funcName); + } + + return new Name($funcName); + } + + /** + * @param self::USE_* $mode + * @return array{list, ?Variable, ?Variable} + */ + private function createDummyArgs(int $mode): array + { + if ($mode === self::USE_ITEM) { + $itemVar = new Variable('item'); + $keyVar = null; + $args = [new Arg($itemVar)]; + } elseif ($mode === self::USE_KEY) { + $itemVar = null; + $keyVar = new Variable('key'); + $args = [new Arg($keyVar)]; + } elseif ($mode === self::USE_BOTH) { + $itemVar = new Variable('item'); + $keyVar = new Variable('key'); + $args = [new Arg($itemVar), new Arg($keyVar)]; + } + return [$args, $itemVar, $keyVar]; + } + + /** + * @param non-empty-string $constantName + */ + private function getConstant(string $constantName): int + { + $constant = $this->reflectionProvider->getConstant(new Name($constantName), null); + $valueType = $constant->getValueType(); + if (!$valueType instanceof ConstantIntegerType) { + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); + } + + return $valueType->getValue(); + } + + /** + * @return self::USE_*|null + */ + private function determineMode(?Expr $flagArg, Scope $scope): ?int + { + if ($flagArg === null) { + return self::USE_ITEM; + } + + $flagValues = $scope->getType($flagArg)->getConstantScalarValues(); + if (count($flagValues) !== 1) { + return null; + } + + if ($flagValues[0] === $this->getConstant('ARRAY_FILTER_USE_KEY')) { + return self::USE_KEY; + } elseif ($flagValues[0] === $this->getConstant('ARRAY_FILTER_USE_BOTH')) { + return self::USE_BOTH; + } + + return null; + } + +} diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php deleted file mode 100644 index bf0eb42757..0000000000 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ /dev/null @@ -1,195 +0,0 @@ -getName() === 'array_filter'; - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - $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 ($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 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); - } - } 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); - } - } - - if ($itemType instanceof NeverType || $keyType instanceof NeverType) { - return new ConstantArrayType([], []); - } - - return new ArrayType($keyType, $itemType); - } - - public function removeFalsey(Type $type): Type - { - $falseyTypes = StaticTypeFactory::falsey(); - - if ($type instanceof ConstantArrayType) { - $keys = $type->getKeyTypes(); - $values = $type->getValueTypes(); - - $builder = ConstantArrayTypeBuilder::createEmpty(); - - foreach ($values as $offset => $value) { - $isFalsey = $falseyTypes->isSuperTypeOf($value); - - if ($isFalsey->maybe()) { - $builder->setOffsetValueType($keys[$offset], TypeCombinator::remove($value, $falseyTypes), true); - } elseif ($isFalsey->no()) { - $builder->setOffsetValueType($keys[$offset], $value); - } - } - - return $builder->getArray(); - } - - $keyType = $type->getIterableKeyType(); - $valueType = $type->getIterableValueType(); - - $valueType = TypeCombinator::remove($valueType, $falseyTypes); - - if ($valueType instanceof NeverType) { - return new ConstantArrayType([], []); - } - - 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/ArrayFindFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..220d8fa0ef --- /dev/null +++ b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php @@ -0,0 +1,46 @@ +getName() === 'array_find'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($arrayType->getArrays()) < 1) { + return null; + } + + $arrayArg = $functionCall->getArgs()[0]->value ?? null; + $callbackArg = $functionCall->getArgs()[1]->value ?? null; + + $resultTypes = $this->arrayFilterFunctionReturnTypeHelper->getType($scope, $arrayArg, $callbackArg, null); + $resultType = TypeCombinator::union(...array_map(static fn ($type) => $type->getIterableValueType(), $resultTypes->getArrays())); + + return $resultTypes->isIterableAtLeastOnce()->yes() ? $resultType : TypeCombinator::addNull($resultType); + } + +} diff --git a/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..97c514f427 --- /dev/null +++ b/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php @@ -0,0 +1,36 @@ +getName() === 'array_find_key'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($arrayType->getArrays()) < 1) { + return null; + } + + return TypeCombinator::union($arrayType->getIterableKeyType(), new NullType()); + } + +} diff --git a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php index 71f1140218..b5b0eb1df8 100644 --- a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php @@ -4,51 +4,44 @@ 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\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; -class ArrayFlipFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayFlipFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_flip'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) !== 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $array = $functionCall->getArgs()[0]->value; - $argType = $scope->getType($array); - - if ($argType->isArray()->yes()) { - $keyType = $argType->getIterableKeyType(); - $itemType = $argType->getIterableValueType(); - - $itemType = ArrayType::castToArrayKeyType($itemType); - - $flippedArrayType = new ArrayType( - $itemType, - $keyType, - ); - - if ($argType->isIterableAtLeastOnce()->yes()) { - $flippedArrayType = TypeCombinator::intersect($flippedArrayType, new NonEmptyArrayType()); - } - - return $flippedArrayType; + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $flipped = $arrayType->flipArray(); + if ($arrayType->isIterableAtLeastOnce()->yes()) { + return TypeCombinator::intersect($flipped, new NonEmptyArrayType()); + } + return $flipped; } } diff --git a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..e7436d8275 --- /dev/null +++ b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php @@ -0,0 +1,62 @@ +getName() === 'array_intersect_key'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + return null; + } + + $argTypes = []; + foreach ($args as $arg) { + $argType = $scope->getType($arg->value); + if ($arg->unpack) { + $argTypes[] = $argType->getIterableValueType(); + continue; + } + + $argTypes[] = $argType; + } + + $firstArrayType = $argTypes[0]; + $otherArraysType = TypeCombinator::union(...array_slice($argTypes, 1)); + $onlyOneArrayGiven = count($argTypes) === 1; + + if ($firstArrayType->isArray()->no() || (!$onlyOneArrayGiven && $otherArraysType->isArray()->no())) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + } + + if ($onlyOneArrayGiven) { + return $firstArrayType; + } + + return $firstArrayType->intersectKeyArray($otherArraysType); + } + +} diff --git a/src/Type/Php/ArrayIsListFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayIsListFunctionTypeSpecifyingExtension.php deleted file mode 100644 index da9de1508c..0000000000 --- a/src/Type/Php/ArrayIsListFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,66 +0,0 @@ -getName()) === 'array_is_list' - && !$context->null(); - } - - public function specifyTypes( - FunctionReflection $functionReflection, - FuncCall $node, - Scope $scope, - TypeSpecifierContext $context, - ): SpecifiedTypes - { - $arrayArg = $node->getArgs()[0]->value ?? null; - if ($arrayArg === null) { - return new SpecifiedTypes(); - } - - $valueType = $scope->getType($arrayArg); - if ($valueType instanceof ConstantArrayType) { - return $this->typeSpecifier->create($arrayArg, $valueType->getValuesArray(), $context, false, $scope); - } - - return $this->typeSpecifier->create( - $arrayArg, - TypeCombinator::intersect(new ArrayType(new IntegerType(), $valueType->getIterableValueType()), ...TypeUtils::getAccessoryTypes($valueType)), - $context, - false, - $scope, - ); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php index 2a698e153f..fe418f4daa 100644 --- a/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php @@ -5,13 +5,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayKeyDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayKeyDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'key'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index 39213613fd..7d2eb3b2a4 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -2,7 +2,11 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\ArrayDimFetch; +use PhpParser\Node\Expr\BinaryOp\Identical; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -10,13 +14,17 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; use PHPStan\Type\TypeCombinator; use function count; +use function in_array; -class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -32,7 +40,7 @@ public function isFunctionSupported( TypeSpecifierContext $context, ): bool { - return $functionReflection->getName() === 'array_key_exists' + return in_array($functionReflection->getName(), ['array_key_exists', 'key_exists'], true) && !$context->null(); } @@ -46,9 +54,56 @@ public function specifyTypes( if (count($node->getArgs()) < 2) { return new SpecifiedTypes(); } - $keyType = $scope->getType($node->getArgs()[0]->value); + $key = $node->getArgs()[0]->value; + $array = $node->getArgs()[1]->value; + $keyType = $scope->getType($key); + $arrayType = $scope->getType($array); - if ($context->truthy()) { + if ( + !$keyType instanceof ConstantIntegerType + && !$keyType instanceof ConstantStringType + ) { + if ($context->true()) { + if ($arrayType->isIterableAtLeastOnce()->no()) { + return $this->typeSpecifier->create( + $array, + new NonEmptyArrayType(), + $context, + $scope, + ); + } + + $arrayKeyType = $arrayType->getIterableKeyType(); + if ($keyType->isString()->yes()) { + $arrayKeyType = $arrayKeyType->toString(); + } elseif ($keyType->isString()->maybe()) { + $arrayKeyType = TypeCombinator::union($arrayKeyType, $arrayKeyType->toString()); + } + + $specifiedTypes = $this->typeSpecifier->create( + $key, + $arrayKeyType, + $context, + $scope, + ); + + $arrayDimFetch = new ArrayDimFetch( + $array, + $key, + ); + + return $specifiedTypes->unionWith($this->typeSpecifier->create( + $arrayDimFetch, + $arrayType->getIterableValueType(), + $context, + $scope, + ))->setRootExpr(new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT')))); + } + + return new SpecifiedTypes(); + } + + if ($context->true()) { $type = TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), new HasOffsetType($keyType), @@ -58,10 +113,9 @@ public function specifyTypes( } return $this->typeSpecifier->create( - $node->getArgs()[1]->value, + $array, $type, $context, - false, $scope, ); } diff --git a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php index 1fa52f314f..bd9fdd6f0a 100644 --- a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php @@ -5,15 +5,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use function count; -class ArrayKeyFirstDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayKeyFirstDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_key_first'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -33,23 +30,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $keyTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $keyTypes[] = new NullType(); - continue; - } - - $keyTypes[] = $arrayKeyTypes[0]; - } - - return TypeCombinator::union(...$keyTypes); - } - - $keyType = $argType->getIterableKeyType(); + $keyType = $argType->getFirstIterableKeyType(); if ($iterableAtLeastOnce->yes()) { return $keyType; } diff --git a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php index 29f913b62d..a13714293c 100644 --- a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php @@ -5,15 +5,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use function count; -class ArrayKeyLastDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayKeyLastDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_key_last'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -33,23 +30,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $keyTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $keyTypes[] = new NullType(); - continue; - } - - $keyTypes[] = $arrayKeyTypes[count($arrayKeyTypes) - 1]; - } - - return TypeCombinator::union(...$keyTypes); - } - - $keyType = $argType->getIterableKeyType(); + $keyType = $argType->getLastIterableKeyType(); if ($iterableAtLeastOnce->yes()) { return $keyType; } diff --git a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php index e0f0d451c8..74a2903ea5 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -4,43 +4,39 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; 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\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; +use function count; +use function strtolower; -class ArrayKeysFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayKeysFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'array_keys'; + return strtolower($functionReflection->getName()) === 'array_keys'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - if ($arrayArg !== null) { - $valueType = $scope->getType($arrayArg); - if ($valueType->isArray()->yes()) { - if ($valueType instanceof ConstantArrayType) { - return $valueType->getKeysArray(); - } - $keyType = $valueType->getIterableKeyType(); - return TypeCombinator::intersect(new ArrayType(new IntegerType(), $keyType), ...TypeUtils::getAccessoryTypes($valueType)); - } + if (count($functionCall->getArgs()) !== 1) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return new ArrayType( - new IntegerType(), - new UnionType([new StringType(), new IntegerType()]), - ); + return $arrayType->getKeysArray(); } } diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 4284d4ee0e..145756b971 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -2,10 +2,12 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -13,15 +15,15 @@ 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_map; +use function array_reduce; use function array_slice; use function count; -class ArrayMapFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayMapFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -29,27 +31,83 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_map'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $numArgs = count($functionCall->getArgs()); + if ($numArgs < 2) { + return null; } $singleArrayArgument = !isset($functionCall->getArgs()[2]); $callableType = $scope->getType($functionCall->getArgs()[0]->value); - $callableIsNull = (new NullType())->isSuperTypeOf($callableType)->yes(); + $callableIsNull = $callableType->isNull()->yes(); + + $callableParametersAcceptors = null; if ($callableType->isCallable()->yes()) { - $valueType = new NeverType(); - foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) { - $valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType()); - } + $callableParametersAcceptors = $callableType->getCallableParametersAcceptors($scope); + $valueType = ParametersAcceptorSelector::selectFromTypes( + array_map( + static fn (Node\Arg $arg) => $scope->getType($arg->value)->getIterableValueType(), + array_slice($functionCall->getArgs(), 1), + ), + $callableParametersAcceptors, + false, + )->getReturnType(); } elseif ($callableIsNull) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $argTypes = []; + $areAllSameSize = true; + $expectedSize = null; foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { + $argTypes[$index] = $argType = $scope->getType($arg->value); + if (!$areAllSameSize || $numArgs === 2) { + continue; + } + + $arraySizes = $argType->getArraySize()->getConstantScalarValues(); + if ($arraySizes === []) { + $areAllSameSize = false; + continue; + } + + foreach ($arraySizes as $size) { + $expectedSize ??= $size; + if ($expectedSize === $size) { + continue; + } + + $areAllSameSize = false; + continue 2; + } + } + + if (!$areAllSameSize) { + $firstArr = $functionCall->getArgs()[1]->value; + $identities = []; + foreach (array_slice($functionCall->getArgs(), 2) as $arg) { + $identities[] = new Node\Expr\BinaryOp\Identical($firstArr, $arg->value); + } + + $and = array_reduce( + $identities, + static fn (Node\Expr $a, Node\Expr $b) => new Node\Expr\BinaryOp\BooleanAnd($a, $b), + new Node\Expr\ConstFetch(new Node\Name('true')), + ); + $areAllSameSize = $scope->getType($and)->isTrue()->yes(); + } + + $addNull = !$areAllSameSize; + + foreach ($argTypes as $index => $argType) { + $offsetValueType = $argType->getIterableValueType(); + if ($addNull) { + $offsetValueType = TypeCombinator::addNull($offsetValueType); + } + $arrayBuilder->setOffsetValueType( new ConstantIntegerType($index), - $scope->getType($arg->value)->getIterableValueType(), + $offsetValueType, ); } $valueType = $arrayBuilder->getArray(); @@ -63,21 +121,41 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($callableIsNull) { return $arrayType; } - $constantArrays = TypeUtils::getConstantArrays($arrayType); + $constantArrays = $arrayType->getConstantArrays(); if (count($constantArrays) > 0) { $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getKeyTypes() as $keyType) { - $returnedArrayBuilder->setOffsetValueType( - $keyType, - $valueType, - ); + $totalCount = TypeCombinator::countConstantArrayValueTypes($constantArrays) * TypeCombinator::countConstantArrayValueTypes([$valueType]); + if ($totalCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + foreach ($constantArrays as $constantArray) { + $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $valueTypes = $constantArray->getValueTypes(); + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $returnedArrayBuilder->setOffsetValueType( + $keyType, + $callableParametersAcceptors !== null + ? ParametersAcceptorSelector::selectFromTypes( + [$valueTypes[$i]], + $callableParametersAcceptors, + false, + )->getReturnType() + : $valueType, + $constantArray->isOptionalKey($i), + ); + } + $returnedArray = $returnedArrayBuilder->getArray(); + if ($constantArray->isList()->yes()) { + $returnedArray = TypeCombinator::intersect($returnedArray, new AccessoryArrayListType()); + } + $arrayTypes[] = $returnedArray; } - $arrayTypes[] = $returnedArrayBuilder->getArray(); - } - $mappedArrayType = TypeCombinator::union(...$arrayTypes); + $mappedArrayType = TypeCombinator::union(...$arrayTypes); + } else { + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + $arrayType->getIterableKeyType(), + $valueType, + ), ...TypeUtils::getAccessoryTypes($arrayType)); + } } elseif ($arrayType->isArray()->yes()) { $mappedArrayType = TypeCombinator::intersect(new ArrayType( $arrayType->getIterableKeyType(), @@ -93,7 +171,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $mappedArrayType = TypeCombinator::intersect(new ArrayType( new IntegerType(), $valueType, - ), ...TypeUtils::getAccessoryTypes($arrayType)); + ), new AccessoryArrayListType(), ...TypeUtils::getAccessoryTypes($arrayType)); } if ($arrayType->isIterableAtLeastOnce()->yes()) { diff --git a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php index 4fcc3338bf..712bd4edc3 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -5,18 +5,24 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; +use function array_keys; +use function count; +use function in_array; -class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,30 +30,84 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_merge'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + + if (!isset($args[0])) { + return null; } - $keyTypes = []; - $valueTypes = []; - $nonEmpty = false; - foreach ($functionCall->getArgs() as $arg) { + $argTypes = []; + $optionalArgTypes = []; + foreach ($args as $arg) { $argType = $scope->getType($arg->value); + if ($arg->unpack) { - $argType = $argType->getIterableValueType(); - if ($argType instanceof UnionType) { - foreach ($argType->getTypes() as $innerType) { - $argType = $innerType; + if ($argType->isConstantArray()->yes()) { + foreach ($argType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getValueTypes() as $valueType) { + $argTypes[] = $valueType; + } } + } else { + $argTypes[] = $argType->getIterableValueType(); } + + if (!$argType->isIterableAtLeastOnce()->yes()) { + // unpacked params can be empty, making them optional + $optionalArgTypesOffset = count($argTypes) - 1; + foreach (array_keys($argTypes) as $key) { + $optionalArgTypes[] = $optionalArgTypesOffset + $key; + } + } + } else { + $argTypes[] = $argType; } + } + + $allConstant = TrinaryLogic::createYes()->lazyAnd( + $argTypes, + static fn (Type $argType) => $argType->isConstantArray(), + ); - $keyTypes[] = $argType->getIterableKeyType()->generalize(GeneralizePrecision::moreSpecific()); + if ($allConstant->yes()) { + $newArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($argTypes as $argType) { + /** @var array $keyTypes */ + $keyTypes = []; + foreach ($argType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getKeyTypes() as $keyType) { + $keyTypes[$keyType->getValue()] = $keyType; + } + } + + foreach ($keyTypes as $keyType) { + $newArrayBuilder->setOffsetValueType( + $keyType instanceof ConstantIntegerType ? null : $keyType, + $argType->getOffsetValueType($keyType), + !$argType->hasOffsetValueType($keyType)->yes(), + ); + } + } + + return $newArrayBuilder->getArray(); + } + + $keyTypes = []; + $valueTypes = []; + $nonEmpty = false; + $isList = true; + foreach ($argTypes as $key => $argType) { + $keyType = $argType->getIterableKeyType(); + $keyTypes[] = $keyType; $valueTypes[] = $argType->getIterableValueType(); - if (!$argType->isIterableAtLeastOnce()->yes()) { + if (!(new IntegerType())->isSuperTypeOf($keyType)->yes()) { + $isList = false; + } + + if (in_array($key, $optionalArgTypes, true) || !$argType->isIterableAtLeastOnce()->yes()) { continue; } @@ -55,7 +115,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $keyType = TypeCombinator::union(...$keyTypes); - if ($keyType instanceof NeverType && !$keyType->isExplicit()) { + if ($keyType instanceof NeverType) { return new ConstantArrayType([], []); } @@ -67,6 +127,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($nonEmpty) { $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); } + if ($isList) { + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } return $arrayType; } diff --git a/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php index f5c0fed29c..0f33b7f532 100644 --- a/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php @@ -5,14 +5,13 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function in_array; -class ArrayNextDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayNextDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -20,10 +19,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return in_array($functionReflection->getName(), ['next', 'prev'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php index 798ded9529..4e49cd465f 100644 --- a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php @@ -5,16 +5,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use function in_array; -class ArrayPointerFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayPointerFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var string[] */ @@ -32,10 +30,10 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -44,27 +42,9 @@ public function getTypeFromFunctionCall( return new ConstantBooleanType(false); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $keyTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $keyTypes[] = new ConstantBooleanType(false); - continue; - } - - $valueOffset = $functionReflection->getName() === 'reset' - ? $arrayKeyTypes[0] - : $arrayKeyTypes[count($arrayKeyTypes) - 1]; - - $keyTypes[] = $constantArray->getOffsetValueType($valueOffset); - } - - return TypeCombinator::union(...$keyTypes); - } - - $itemType = $argType->getIterableValueType(); + $itemType = $functionReflection->getName() === 'reset' + ? $argType->getFirstIterableValueType() + : $argType->getLastIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php index 75c42f8b7b..540dda82bb 100644 --- a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php @@ -5,15 +5,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use function count; -class ArrayPopFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayPopFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_pop'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -33,23 +30,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $valueTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $valueTypes[] = new NullType(); - continue; - } - - $valueTypes[] = $constantArray->getOffsetValueType($arrayKeyTypes[count($arrayKeyTypes) - 1]); - } - - return TypeCombinator::union(...$valueTypes); - } - - $itemType = $argType->getIterableValueType(); + $itemType = $argType->getLastIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php index 86edddb617..1d390b59d0 100644 --- a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php @@ -5,10 +5,10 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -16,7 +16,7 @@ use PHPStan\Type\UnionType; use function count; -class ArrayRandFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayRandFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,16 +24,16 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_rand'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $argsCount = count($functionCall->getArgs()); if ($argsCount < 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); - $isInteger = (new IntegerType())->isSuperTypeOf($firstArgType->getIterableKeyType()); - $isString = (new StringType())->isSuperTypeOf($firstArgType->getIterableKeyType()); + $isInteger = $firstArgType->getIterableKeyType()->isInteger(); + $isString = $firstArgType->getIterableKeyType()->isString(); if ($isInteger->yes()) { $valueType = new IntegerType(); @@ -49,14 +49,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $secondArgType = $scope->getType($functionCall->getArgs()[1]->value); - if ($secondArgType instanceof ConstantIntegerType) { - if ($secondArgType->getValue() === 1) { - return $valueType; - } + $one = new ConstantIntegerType(1); + if ($one->isSuperTypeOf($secondArgType)->yes()) { + return $valueType; + } - if ($secondArgType->getValue() >= 2) { - return new ArrayType(new IntegerType(), $valueType); - } + $bigger2 = IntegerRangeType::fromInterval(2, null); + if ($bigger2->isSuperTypeOf($secondArgType)->yes()) { + return new ArrayType(new IntegerType(), $valueType); } return TypeCombinator::union($valueType, new ArrayType(new IntegerType(), $valueType)); diff --git a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php index 775ac8de55..970e2cb39f 100644 --- a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php @@ -6,14 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\TrinaryLogic; 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 DynamicFunctionReturnTypeExtension +final class ArrayReduceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,15 +21,15 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_reduce'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[1])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $callbackType = $scope->getType($functionCall->getArgs()[1]->value); if ($callbackType->isCallable()->no()) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $callbackReturnType = ParametersAcceptorSelector::selectFromArgs( @@ -45,20 +45,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $arraysType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getConstantArrays($arraysType); + $constantArrays = $arraysType->getConstantArrays(); if (count($constantArrays) > 0) { - $onlyEmpty = true; - $onlyNonEmpty = true; + $onlyEmpty = TrinaryLogic::createYes(); + $onlyNonEmpty = TrinaryLogic::createYes(); foreach ($constantArrays as $constantArray) { - $isEmpty = count($constantArray->getValueTypes()) === 0; - $onlyEmpty = $onlyEmpty && $isEmpty; - $onlyNonEmpty = $onlyNonEmpty && !$isEmpty; + $iterableAtLeastOnce = $constantArray->isIterableAtLeastOnce(); + $onlyEmpty = $onlyEmpty->and($iterableAtLeastOnce->negate()); + $onlyNonEmpty = $onlyNonEmpty->and($iterableAtLeastOnce); } - if ($onlyEmpty) { + if ($onlyEmpty->yes()) { return $initialType; } - if ($onlyNonEmpty) { + if ($onlyNonEmpty->yes()) { return $callbackReturnType; } } diff --git a/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..e68f0338f7 --- /dev/null +++ b/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php @@ -0,0 +1,76 @@ +getName()) === 'array_replace'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $arrayTypes = $this->collectArrayTypes($functionCall, $scope); + + if (count($arrayTypes) === 0) { + return null; + } + + return $this->getResultType(...$arrayTypes); + } + + private function getResultType(Type ...$arrayTypes): Type + { + $keyTypes = []; + $valueTypes = []; + $nonEmptyArray = false; + foreach ($arrayTypes as $arrayType) { + if (!$nonEmptyArray && $arrayType->isIterableAtLeastOnce()->yes()) { + $nonEmptyArray = true; + } + + $keyTypes[] = $arrayType->getIterableKeyType(); + $valueTypes[] = $arrayType->getIterableValueType(); + } + + $keyType = TypeCombinator::union(...$keyTypes); + $valueType = TypeCombinator::union(...$valueTypes); + + $arrayType = new ArrayType($keyType, $valueType); + return $nonEmptyArray ? TypeCombinator::intersect($arrayType, new NonEmptyArrayType()) : $arrayType; + } + + /** + * @return Type[] + */ + private function collectArrayTypes(FuncCall $functionCall, Scope $scope): array + { + $args = $functionCall->getArgs(); + + $arrayTypes = []; + foreach ($args as $arg) { + $argType = $scope->getType($arg->value); + if (!$argType->isArray()->yes()) { + continue; + } + + $arrayTypes[] = $arg->unpack ? $argType->getIterableValueType() : $argType; + } + + return $arrayTypes; + } + +} diff --git a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php index 37421aa683..825f7d6c1a 100644 --- a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php @@ -4,26 +4,40 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -class ArrayReverseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayReverseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_reverse'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - return $scope->getType($functionCall->getArgs()[0]->value); + $type = $scope->getType($functionCall->getArgs()[0]->value); + if ($type->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + } + + $preserveKeysType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : new ConstantBooleanType(false); + + return $type->reverseArray($preserveKeysType->isTrue()); } } diff --git a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php index 997178b24d..762e577211 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php @@ -4,41 +4,38 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\NullType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; use function count; final class ArraySearchFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_search'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $argsCount = count($functionCall->getArgs()); if ($argsCount < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $haystackArgType = $scope->getType($functionCall->getArgs()[1]->value); - $haystackIsArray = (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($haystackArgType); - if ($haystackIsArray->no()) { - return new NullType(); + if ($haystackArgType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } if ($argsCount < 3) { @@ -46,9 +43,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $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) { + if (!$strictArgType->isTrue()->yes()) { return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); } @@ -57,107 +52,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ConstantBooleanType(false); } - $typesFromConstantArrays = []; - if ($haystackIsArray->maybe()) { - $typesFromConstantArrays[] = new NullType(); - } - - $haystackArrays = $this->pickArrays($haystackArgType); - if (count($haystackArrays) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - - $arrays = []; - $typesFromConstantArraysCount = 0; - foreach ($haystackArrays as $haystackArray) { - if (!$haystackArray instanceof ConstantArrayType) { - $arrays[] = $haystackArray; - continue; - } - - $typesFromConstantArrays[] = $this->resolveTypeFromConstantHaystackAndNeedle($needleArgType, $haystackArray); - $typesFromConstantArraysCount++; - } - - if ( - $typesFromConstantArraysCount > 0 - && count($haystackArrays) === $typesFromConstantArraysCount - ) { - return TypeCombinator::union(...$typesFromConstantArrays); - } - - $iterableKeyType = TypeCombinator::union(...$arrays)->getIterableKeyType(); - - return TypeCombinator::union( - $iterableKeyType, - new ConstantBooleanType(false), - ...$typesFromConstantArrays, - ); - } - - private function resolveTypeFromConstantHaystackAndNeedle(Type $needle, ConstantArrayType $haystack): Type - { - $matchesByType = []; - - foreach ($haystack->getValueTypes() as $index => $valueType) { - $isNeedleSuperType = $valueType->isSuperTypeOf($needle); - if ($isNeedleSuperType->no()) { - $matchesByType[] = new ConstantBooleanType(false); - continue; - } - - if ($needle instanceof ConstantScalarType && $valueType instanceof ConstantScalarType - && $needle->getValue() === $valueType->getValue() - ) { - return $haystack->getKeyTypes()[$index]; - } - - $matchesByType[] = $haystack->getKeyTypes()[$index]; - if (!$isNeedleSuperType->maybe()) { - continue; - } - - $matchesByType[] = new ConstantBooleanType(false); - } - - if (count($matchesByType) > 0) { - if ( - $haystack->getIterableValueType()->accepts($needle, true)->yes() - && $needle->isSuperTypeOf(new ObjectWithoutClassType())->no() - ) { - return TypeCombinator::union(...$matchesByType); - } - - return TypeCombinator::union(new ConstantBooleanType(false), ...$matchesByType); - } - - return new ConstantBooleanType(false); - } - - /** - * @return Type[] - */ - private function pickArrays(Type $type): array - { - if ($type instanceof ArrayType) { - return [$type]; - } - - if ($type instanceof UnionType || $type instanceof IntersectionType) { - $arrayTypes = []; - - foreach ($type->getTypes() as $innerType) { - if (!($innerType instanceof ArrayType)) { - continue; - } - - $arrayTypes[] = $innerType; - } - - return $arrayTypes; - } - - return []; + return $haystackArgType->searchArray($needleArgType); } } diff --git a/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..68f2992096 --- /dev/null +++ b/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php @@ -0,0 +1,56 @@ +getName()) === 'array_search' + && $context->true(); + } + + public function specifyTypes( + FunctionReflection $functionReflection, + FuncCall $node, + Scope $scope, + TypeSpecifierContext $context, + ): SpecifiedTypes + { + $arrayArg = $node->getArgs()[1]->value ?? null; + if ($arrayArg === null) { + return new SpecifiedTypes(); + } + + return $this->typeSpecifier->create( + $arrayArg, + new NonEmptyArrayType(), + $context, + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php index b38a2889c5..cb6c35bbdd 100644 --- a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php @@ -5,15 +5,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use function count; -class ArrayShiftFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayShiftFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_shift'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -33,23 +30,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getConstantArrays($argType); - if (count($constantArrays) > 0) { - $valueTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayKeyTypes = $constantArray->getKeyTypes(); - if (count($arrayKeyTypes) === 0) { - $valueTypes[] = new NullType(); - continue; - } - - $valueTypes[] = $constantArray->getOffsetValueType($arrayKeyTypes[0]); - } - - return TypeCombinator::union(...$valueTypes); - } - - $itemType = $argType->getIterableValueType(); + $itemType = $argType->getFirstIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 14b51256c7..75f6a14b63 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -4,86 +4,44 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -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\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_map; use function count; -class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_slice'; } - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - FuncCall $functionCall, - Scope $scope, - ): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - - if ($arrayArg === null) { - return new ArrayType( - new IntegerType(), - new MixedType(), - ); - } - - $valueType = $scope->getType($arrayArg); - - if (isset($functionCall->getArgs()[1])) { - $offset = $scope->getType($functionCall->getArgs()[1]->value); - if (!$offset instanceof ConstantIntegerType) { - $offset = new ConstantIntegerType(0); - } - } else { - $offset = new ConstantIntegerType(0); - } - - if (isset($functionCall->getArgs()[2])) { - $limit = $scope->getType($functionCall->getArgs()[2]->value); - if (!$limit instanceof ConstantIntegerType) { - $limit = new NullType(); - } - } else { - $limit = new NullType(); - } - - $constantArrays = TypeUtils::getConstantArrays($valueType); - if (count($constantArrays) === 0) { - $arrays = TypeUtils::getArrays($valueType); - if (count($arrays) !== 0) { - return TypeCombinator::union(...$arrays); - } - return new ArrayType( - new MixedType(), - new MixedType(), - ); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; } - if (isset($functionCall->getArgs()[3])) { - $preserveKeys = $scope->getType($functionCall->getArgs()[3]->value); - $preserveKeys = (new ConstantBooleanType(true))->isSuperTypeOf($preserveKeys)->yes(); - } else { - $preserveKeys = false; + $arrayType = $scope->getType($args[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - $arrayTypes = array_map(static fn (ConstantArrayType $constantArray): ConstantArrayType => $constantArray->slice($offset->getValue(), $limit->getValue(), $preserveKeys), $constantArrays); + $offsetType = $scope->getType($args[1]->value); + $lengthType = isset($args[2]) ? $scope->getType($args[2]->value) : new NullType(); + $preserveKeysType = isset($args[3]) ? $scope->getType($args[3]->value) : new ConstantBooleanType(false); - return TypeCombinator::union(...$arrayTypes); + return $arrayType->sliceArray($offsetType, $lengthType, $preserveKeysType->isTrue()); } } diff --git a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php index 27e763c0d9..85def351d2 100644 --- a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php @@ -4,15 +4,22 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ArrayType; +use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; +use function count; -class ArraySpliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArraySpliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_splice'; @@ -22,15 +29,22 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { - if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; + } + + $arrayType = $scope->getType($args[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - $arrayArg = $scope->getType($functionCall->getArgs()[0]->value); + $offsetType = $scope->getType($args[1]->value); + $lengthType = isset($args[2]) ? $scope->getType($args[2]->value) : new NullType(); - return new ArrayType($arrayArg->getIterableKeyType(), $arrayArg->getIterableValueType()); + return $arrayType->sliceArray($offsetType, $lengthType, TrinaryLogic::createNo()); } } diff --git a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php index 03a5d15910..5185fbccc1 100644 --- a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php @@ -2,17 +2,19 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\BinaryOp\Mul; +use PhpParser\Node\Expr\BinaryOp\Plus; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Scalar\Int_; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; +use function count; final class ArraySumFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -22,34 +24,42 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_sum'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $arrayType = $scope->getType($functionCall->getArgs()[0]->value); - $itemType = $arrayType->getIterableValueType(); + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $resultTypes = []; - if ($arrayType->isIterableAtLeastOnce()->no()) { - return new ConstantIntegerType(0); - } + if (count($argType->getConstantArrays()) > 0) { + foreach ($argType->getConstantArrays() as $constantArray) { + $node = new Int_(0); - $intUnionFloat = new UnionType([new IntegerType(), new FloatType()]); + foreach ($constantArray->getValueTypes() as $i => $type) { + if ($constantArray->isOptionalKey($i)) { + $node = new Plus($node, new TypeExpr(TypeCombinator::union($type, new ConstantIntegerType(0)))); + } else { + $node = new Plus($node, new TypeExpr($type)); + } + } - if ($arrayType->isIterableAtLeastOnce()->yes()) { - if ($intUnionFloat->isSuperTypeOf($itemType)->yes()) { - return $itemType; + $resultTypes[] = $scope->getType($node); } + } else { + $itemType = $argType->getIterableValueType(); + + $mulNode = new Mul(new TypeExpr($itemType), new TypeExpr(IntegerRangeType::fromInterval(0, null))); - return $intUnionFloat; + $resultTypes[] = $scope->getType(new Plus(new TypeExpr($itemType), $mulNode)); } - if ($intUnionFloat->isSuperTypeOf($itemType)->yes()) { - return TypeCombinator::union(new ConstantIntegerType(0), $itemType); + if (!$argType->isIterableAtLeastOnce()->yes()) { + $resultTypes[] = new ConstantIntegerType(0); } - return TypeCombinator::union(new ConstantIntegerType(0), $intUnionFloat); + return TypeCombinator::union(...$resultTypes)->toNumber(); } } diff --git a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php index 8bd638a5b7..2378a291b3 100644 --- a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php @@ -4,41 +4,39 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; 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\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use function count; +use function strtolower; -class ArrayValuesFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ArrayValuesFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'array_values'; + return strtolower($functionReflection->getName()) === 'array_values'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - if ($arrayArg !== null) { - $valueType = $scope->getType($arrayArg); - if ($valueType->isArray()->yes()) { - if ($valueType instanceof ConstantArrayType) { - return $valueType->getValuesArray(); - } - return TypeCombinator::intersect(new ArrayType(new IntegerType(), $valueType->getIterableValueType()), ...TypeUtils::getAccessoryTypes($valueType)); - } + if (count($functionCall->getArgs()) !== 1) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return new ArrayType( - new IntegerType(), - new MixedType(), - ); + return $arrayType->getValuesArray(); } } diff --git a/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php b/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php index 413a163535..d1458a27dd 100644 --- a/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php @@ -11,7 +11,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\FunctionTypeSpecifyingExtension; -class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; diff --git a/src/Type/Php/AssertThrowTypeExtension.php b/src/Type/Php/AssertThrowTypeExtension.php new file mode 100644 index 0000000000..2cf4226047 --- /dev/null +++ b/src/Type/Php/AssertThrowTypeExtension.php @@ -0,0 +1,36 @@ +getName() === 'assert'; + } + + public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type + { + if (count($funcCall->getArgs()) < 2) { + return $functionReflection->getThrowType(); + } + + $customThrow = $scope->getType($funcCall->getArgs()[1]->value); + if ((new ObjectType(Throwable::class))->isSuperTypeOf($customThrow)->yes()) { + return $customThrow; + } + + return $functionReflection->getThrowType(); + } + +} diff --git a/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php b/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..c80bb0950b --- /dev/null +++ b/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php @@ -0,0 +1,100 @@ +getName(), ['from', 'tryFrom'], true); + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (!$methodReflection->getDeclaringClass()->isBackedEnum()) { + return null; + } + + $arguments = $methodCall->getArgs(); + if (count($arguments) < 1) { + return null; + } + + $valueType = $scope->getType($arguments[0]->value); + + $enumCases = $methodReflection->getDeclaringClass()->getEnumCases(); + if (count($enumCases) === 0) { + if ($methodReflection->getName() === 'tryFrom') { + return new NullType(); + } + + return null; + } + + if (count($valueType->getConstantScalarValues()) === 0) { + return null; + } + + $resultEnumCases = []; + $addNull = false; + foreach ($valueType->getConstantScalarValues() as $value) { + $hasMatching = false; + foreach ($enumCases as $enumCase) { + if ($enumCase->getBackingValueType() === null) { + continue; + } + + $enumCaseValues = $enumCase->getBackingValueType()->getConstantScalarValues(); + if (count($enumCaseValues) !== 1) { + continue; + } + + if ($value === $enumCaseValues[0]) { + $resultEnumCases[] = new EnumCaseObjectType($enumCase->getDeclaringEnum()->getName(), $enumCase->getName(), $enumCase->getDeclaringEnum()); + $hasMatching = true; + break; + } + } + + if ($hasMatching) { + continue; + } + + $addNull = true; + } + + if (count($resultEnumCases) === 0) { + if ($methodReflection->getName() === 'tryFrom') { + return new NullType(); + } + + return null; + } + + $result = TypeCombinator::union(...$resultEnumCases); + if ($addNull && $methodReflection->getName() === 'tryFrom') { + return TypeCombinator::addNull($result); + } + + return $result; + } + +} diff --git a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php index 158f1da5b1..6e38a43a25 100644 --- a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php @@ -13,7 +13,7 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class Base64DecodeDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class Base64DecodeDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -37,8 +37,8 @@ public function getTypeFromFunctionCall( return new BenevolentUnionType([new StringType(), new ConstantBooleanType(false)]); } - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return new UnionType([new StringType(), new ConstantBooleanType(false)]); diff --git a/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php b/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php new file mode 100644 index 0000000000..3e0d793052 --- /dev/null +++ b/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php @@ -0,0 +1,53 @@ +phpVersion->supportsBcMathNumberOperatorOverloading() || $leftSide instanceof NeverType || $rightSide instanceof NeverType) { + return false; + } + + $bcMathNumberType = new ObjectType('BcMath\Number'); + + return in_array($operatorSigil, ['-', '+', '*', '/', '**', '%'], true) + && ( + $bcMathNumberType->isSuperTypeOf($leftSide)->yes() + || $bcMathNumberType->isSuperTypeOf($rightSide)->yes() + ); + } + + public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type + { + $bcMathNumberType = new ObjectType('BcMath\Number'); + $otherSide = $bcMathNumberType->isSuperTypeOf($leftSide)->yes() + ? $rightSide + : $leftSide; + + if ( + $otherSide->isInteger()->yes() + || $otherSide->isNumericString()->yes() + || $bcMathNumberType->isSuperTypeOf($otherSide)->yes() + ) { + return $bcMathNumberType; + } + + return new ErrorType(); + } + +} diff --git a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php index 0d95c792fd..2651cb6b90 100644 --- a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php +++ b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php @@ -5,13 +5,14 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\UnaryMinus; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; 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\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -20,9 +21,13 @@ use function in_array; use function is_numeric; -class BcMathStringOrNullReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class BcMathStringOrNullReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['bcdiv', 'bcmod', 'bcpowmod', 'bcsqrt'], true); @@ -40,16 +45,28 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); - $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); - if (isset($functionCall->getArgs()[1]) === false) { - return $stringAndNumericStringType; + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + + return new NullType(); + } + + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + $defaultReturnType = $stringAndNumericStringType; + } else { + $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); } $secondArgument = $scope->getType($functionCall->getArgs()[1]->value); - $secondArgumentIsNumeric = ($secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue())) || $secondArgument instanceof IntegerType; + $secondArgumentIsNumeric = ($secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue())) || $secondArgument->isInteger()->yes(); if ($secondArgument instanceof ConstantScalarType && ($this->isZero($secondArgument->getValue()) || !$secondArgumentIsNumeric)) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new NullType(); } @@ -62,12 +79,30 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $thirdArgument = $scope->getType($functionCall->getArgs()[2]->value); - $thirdArgumentIsNumeric = ($thirdArgument instanceof ConstantScalarType && is_numeric($thirdArgument->getValue())) || $thirdArgument instanceof IntegerType; + $thirdArgumentIsNumeric = false; + $thirdArgumentIsNegative = false; + if ($thirdArgument instanceof ConstantScalarType && is_numeric($thirdArgument->getValue())) { + $thirdArgumentIsNumeric = true; + $thirdArgumentIsNegative = ($thirdArgument->getValue() < 0); + } elseif ($thirdArgument->isInteger()->yes()) { + $thirdArgumentIsNumeric = true; + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($thirdArgument)->yes()) { + $thirdArgumentIsNegative = true; + } + } if ($thirdArgument instanceof ConstantScalarType && !is_numeric($thirdArgument->getValue())) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new NullType(); } + if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && $thirdArgumentIsNegative) { + return new NeverType(); + } + if (($secondArgument instanceof ConstantScalarType || $secondArgumentIsNumeric) && $thirdArgumentIsNumeric) { return $stringAndNumericStringType; } @@ -84,9 +119,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type { $stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); - $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + $defaultReturnType = $stringAndNumericStringType; + } else { + $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); + } if (isset($functionCall->getArgs()[0]) === false) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return $defaultReturnType; } @@ -95,8 +138,11 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type $firstArgumentIsPositive = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() >= 0; $firstArgumentIsNegative = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() < 0; - if ($firstArgument instanceof UnaryMinus || - ($firstArgumentIsNegative)) { + if ($firstArgument instanceof UnaryMinus || $firstArgumentIsNegative) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new NullType(); } @@ -111,11 +157,22 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type $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()); + $secondArgumentIsNegative = $secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue()) && $secondArgument->getValue() < 0; if ($secondArgumentIsNonNumeric) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new NullType(); } + if ($secondArgument instanceof UnaryMinus || $secondArgumentIsNegative) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + } + if ($firstArgumentIsPositive && $secondArgumentIsValid) { return $stringAndNumericStringType; } @@ -130,13 +187,29 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type */ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && isset($functionCall->getArgs()[0]) === false) { + return new NeverType(); + } + $stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); if (isset($functionCall->getArgs()[1]) === false) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new UnionType([$stringAndNumericStringType, new ConstantBooleanType(false)]); } $exponent = $scope->getType($functionCall->getArgs()[1]->value); + + // Expontent is non numeric + if ($this->phpVersion->throwsTypeErrorForInternalFunctions() + && $exponent instanceof ConstantScalarType && !is_numeric($exponent->getValue()) + ) { + return new NeverType(); + } + $exponentIsNegative = IntegerRangeType::fromInterval(null, 0)->isSuperTypeOf($exponent)->yes(); if ($exponent instanceof ConstantScalarType) { @@ -144,6 +217,10 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type } if ($exponentIsNegative) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new ConstantBooleanType(false); } @@ -153,12 +230,24 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type $modulusIsNonNumeric = $modulus instanceof ConstantScalarType && !is_numeric($modulus->getValue()); if ($modulusIsZero || $modulusIsNonNumeric) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + return new ConstantBooleanType(false); } if ($modulus instanceof ConstantScalarType) { return $stringAndNumericStringType; } + } else { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + } + + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return $stringAndNumericStringType; } return new UnionType([$stringAndNumericStringType, new ConstantBooleanType(false)]); diff --git a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php index 7d9436c55f..e10e74a53e 100644 --- a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php @@ -16,10 +16,12 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\ObjectType; use function in_array; use function ltrim; -class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -34,13 +36,13 @@ public function isFunctionSupported( 'class_exists', 'interface_exists', 'trait_exists', - ], true) && isset($node->getArgs()[0]) && $context->truthy(); + 'enum_exists', + ], true) && isset($node->getArgs()[0]) && $context->true(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { $argType = $scope->getType($node->getArgs()[0]->value); - $classStringType = new ClassStringType(); if ($argType instanceof ConstantStringType) { return $this->typeSpecifier->create( new FuncCall(new FullyQualified('class_exists'), [ @@ -48,16 +50,19 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n ]), new ConstantBooleanType(true), $context, - false, $scope, ); } + $narrowedType = new ClassStringType(); + if ($functionReflection->getName() === 'enum_exists') { + $narrowedType = new GenericClassStringType(new ObjectType('UnitEnum')); + } + return $this->typeSpecifier->create( $node->getArgs()[0]->value, - $classStringType, + $narrowedType, $context, - false, $scope, ); } diff --git a/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..df1b7022ab --- /dev/null +++ b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php @@ -0,0 +1,67 @@ +getName(), + ['class_implements', 'class_uses', 'class_parents'], + true, + ); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $firstArgType = $scope->getType($args[0]->value); + $autoload = TrinaryLogic::createYes(); + if (isset($args[1])) { + $autoload = $scope->getType($args[1]->value)->isTrue(); + } + + $isObject = $firstArgType->isObject(); + $variant = ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants()); + if ($isObject->yes()) { + return TypeCombinator::remove($variant->getReturnType(), new ConstantBooleanType(false)); + } + $isClassStringOrObject = (new UnionType([new ObjectWithoutClassType(), new ClassStringType()]))->isSuperTypeOf($firstArgType); + if ($isClassStringOrObject->yes()) { + if ($autoload->yes()) { + return TypeUtils::toBenevolentUnion($variant->getReturnType()); + } + + return $variant->getReturnType(); + } + + if ($firstArgType->isClassString()->no()) { + return new ConstantBooleanType(false); + } + + return null; + } + +} diff --git a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php index 312a3cd42c..719fd172d3 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -6,12 +6,11 @@ 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 DynamicStaticMethodReturnTypeExtension +final class ClosureBindDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string @@ -24,11 +23,11 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo return $methodReflection->getName() === 'bind'; } - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { $closureType = $scope->getType($methodCall->getArgs()[0]->value); if (!($closureType instanceof ClosureType)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } return $closureType; diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index d2d74ff813..9e495b3163 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -6,12 +6,11 @@ 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 DynamicMethodReturnTypeExtension +final class ClosureBindToDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string @@ -24,11 +23,11 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return $methodReflection->getName() === 'bindTo'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { $closureType = $scope->getType($methodCall->var); if (!($closureType instanceof ClosureType)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } return $closureType; diff --git a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php index f5f8de9f1b..d579157160 100644 --- a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php @@ -5,15 +5,16 @@ use Closure; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ClosureFromCallableDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +final class ClosureFromCallableDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string @@ -26,14 +27,10 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo return $methodReflection->getName() === 'fromCallable'; } - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { if (!isset($methodCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants(), - )->getReturnType(); + return null; } $callableType = $scope->getType($methodCall->getArgs()[0]->value); @@ -50,6 +47,13 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, $variant->isVariadic(), $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), + $variant instanceof ExtendedParametersAcceptor ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + [], + $variant->getThrowPoints(), + $variant->getImpurePoints(), + $variant->getInvalidateExpressions(), + $variant->getUsedVariables(), + $variant->acceptsNamedArguments(), ); } diff --git a/src/Type/Php/CompactFunctionReturnTypeExtension.php b/src/Type/Php/CompactFunctionReturnTypeExtension.php index e17ddfb6a5..435d4b067d 100644 --- a/src/Type/Php/CompactFunctionReturnTypeExtension.php +++ b/src/Type/Php/CompactFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantStringType; @@ -14,7 +13,7 @@ use function array_merge; use function count; -class CompactFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class CompactFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private bool $checkMaybeUndefinedVariables) @@ -30,15 +29,14 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if (count($functionCall->getArgs()) === 0) { - return $defaultReturnType; + return null; } if ($scope->canAnyVariableExist() && !$this->checkMaybeUndefinedVariables) { - return $defaultReturnType; + return null; } $array = ConstantArrayTypeBuilder::createEmpty(); @@ -46,7 +44,7 @@ public function getTypeFromFunctionCall( $type = $scope->getType($arg->value); $constantStrings = $this->findConstantStrings($type); if ($constantStrings === null) { - return $defaultReturnType; + return null; } foreach ($constantStrings as $constantString) { $has = $scope->hasVariableType($constantString->getValue()); diff --git a/src/Type/Php/ConstantFunctionReturnTypeExtension.php b/src/Type/Php/ConstantFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..83cfb69ef5 --- /dev/null +++ b/src/Type/Php/ConstantFunctionReturnTypeExtension.php @@ -0,0 +1,55 @@ +getName() === 'constant'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $nameType = $scope->getType($functionCall->getArgs()[0]->value); + + $results = []; + foreach ($nameType->getConstantStrings() as $constantName) { + $expr = $this->constantHelper->createExprFromConstantName($constantName->getValue()); + if ($expr === null) { + return new ErrorType(); + } + + $results[] = $scope->getType($expr); + } + + if (count($results) > 0) { + return TypeCombinator::union(...$results); + } + + return null; + } + +} diff --git a/src/Type/Php/ConstantHelper.php b/src/Type/Php/ConstantHelper.php new file mode 100644 index 0000000000..a3e8c5224f --- /dev/null +++ b/src/Type/Php/ConstantHelper.php @@ -0,0 +1,42 @@ += 2) { + $fqcn = ltrim($classConstParts[0], '\\'); + if ($fqcn === '' || $classConstParts[1] === '') { + return null; + } + + $classConstName = new FullyQualified($fqcn); + if ($classConstName->isSpecialClassName()) { + $classConstName = new Name($classConstName->toString()); + } + + return new ClassConstFetch($classConstName, new Identifier($classConstParts[1])); + } + + return new ConstFetch(new FullyQualified($constantName)); + } + +} diff --git a/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..e798af6bbd --- /dev/null +++ b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,62 @@ +getName() === 'count_chars'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + + if (count($args) < 1) { + return null; + } + + $modeType = count($args) === 2 ? $scope->getType($args[1]->value) : new ConstantIntegerType(0); + + if (IntegerRangeType::fromInterval(0, 2)->isSuperTypeOf($modeType)->yes()) { + $arrayType = new ArrayType(new IntegerType(), new IntegerType()); + + return $this->phpVersion->throwsValueErrorForInternalFunctions() + ? $arrayType + : TypeUtils::toBenevolentUnion(new UnionType([$arrayType, new ConstantBooleanType(false)])); + } + + $stringType = new StringType(); + + return $this->phpVersion->throwsValueErrorForInternalFunctions() + ? $stringType + : TypeUtils::toBenevolentUnion(new UnionType([$stringType, new ConstantBooleanType(false)])); + } + +} diff --git a/src/Type/Php/CountFunctionReturnTypeExtension.php b/src/Type/Php/CountFunctionReturnTypeExtension.php index c9d13e34cd..9368cb4279 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -5,18 +5,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use function in_array; use const COUNT_RECURSIVE; -class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -28,34 +24,20 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) < 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } if (count($functionCall->getArgs()) > 1) { $mode = $scope->getType($functionCall->getArgs()[1]->value); if ($mode->isSuperTypeOf(new ConstantIntegerType(COUNT_RECURSIVE))->yes()) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } } - $argType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getConstantArrays($scope->getType($functionCall->getArgs()[0]->value)); - if (count($constantArrays) === 0) { - if ($argType->isIterableAtLeastOnce()->yes()) { - return IntegerRangeType::fromInterval(1, null); - } - - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - $countTypes = []; - foreach ($constantArrays as $array) { - $countTypes[] = $array->count(); - } - - return TypeCombinator::union(...$countTypes); + return $scope->getType($functionCall->getArgs()[0]->value)->getArraySize(); } } diff --git a/src/Type/Php/CountFunctionTypeSpecifyingExtension.php b/src/Type/Php/CountFunctionTypeSpecifyingExtension.php index 8fd8f91f84..b109b13f91 100644 --- a/src/Type/Php/CountFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CountFunctionTypeSpecifyingExtension.php @@ -10,13 +10,11 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\MixedType; use function count; use function in_array; -class CountFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class CountFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -39,11 +37,11 @@ public function specifyTypes( TypeSpecifierContext $context, ): SpecifiedTypes { - if (!(new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($node->getArgs()[0]->value))->yes()) { + if (!$scope->getType($node->getArgs()[0]->value)->isArray()->yes()) { return new SpecifiedTypes([], []); } - return $this->typeSpecifier->create($node->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..837c3fb890 --- /dev/null +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -0,0 +1,86 @@ +getName()) === 'ctype_digit' + && !$context->null(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + if (!isset($node->getArgs()[0])) { + return new SpecifiedTypes(); + } + if ($context->null()) { + throw new ShouldNotHappenException(); + } + + $exprArg = $node->getArgs()[0]->value; + if ($context->true() && $scope->getType($exprArg)->isNumericString()->yes()) { + return new SpecifiedTypes(); + } + + $types = [ + IntegerRangeType::fromInterval(48, 57), // ASCII-codes for 0-9 + IntegerRangeType::createAllGreaterThanOrEqualTo(256), // Starting from 256 ints are interpreted as strings + ]; + + if ($context->true()) { + $types[] = new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + + $unionType = TypeCombinator::union(...$types); + $specifiedTypes = $this->typeSpecifier->create($exprArg, $unionType, $context, $scope); + + if ($exprArg instanceof Cast\String_) { + $castedType = new UnionType([ + IntegerRangeType::fromInterval(0, null), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + new ConstantBooleanType(true), + ]); + $specifiedTypes = $specifiedTypes->unionWith( + $this->typeSpecifier->create($exprArg->expr, $castedType, $context, $scope), + ); + } + + return $specifiedTypes; + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php b/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..b5de9de5f1 --- /dev/null +++ b/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,200 @@ +getName() === 'curl_getinfo'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + if (count($functionCall->getArgs()) <= 1) { + return $this->createAllComponentsReturnType(); + } + + $componentType = $scope->getType($functionCall->getArgs()[1]->value); + if (!$componentType->isNull()->no()) { + return $this->createAllComponentsReturnType(); + } + + $componentType = $componentType->toInteger(); + if (!$componentType instanceof ConstantIntegerType) { + return $this->createAllComponentsReturnType(); + } + + $stringType = new StringType(); + $integerType = new IntegerType(); + $floatType = new FloatType(); + $falseType = new ConstantBooleanType(false); + $stringFalseType = TypeCombinator::union($stringType, $falseType); + $integerStringArrayType = new ArrayType($integerType, $stringType); + $nestedStringStringArrayType = new ArrayType($integerType, new ArrayType($stringType, $stringType)); + + $componentTypesPairedConstants = [ + 'CURLINFO_EFFECTIVE_URL' => $stringType, + 'CURLINFO_FILETIME' => $integerType, + 'CURLINFO_TOTAL_TIME' => $floatType, + 'CURLINFO_NAMELOOKUP_TIME' => $floatType, + 'CURLINFO_CONNECT_TIME' => $floatType, + 'CURLINFO_PRETRANSFER_TIME' => $floatType, + 'CURLINFO_STARTTRANSFER_TIME' => $floatType, + 'CURLINFO_REDIRECT_COUNT' => $integerType, + 'CURLINFO_REDIRECT_TIME' => $floatType, + 'CURLINFO_REDIRECT_URL' => $stringType, + 'CURLINFO_PRIMARY_IP' => $stringType, + 'CURLINFO_PRIMARY_PORT' => $integerType, + 'CURLINFO_LOCAL_IP' => $stringType, + 'CURLINFO_LOCAL_PORT' => $integerType, + 'CURLINFO_SIZE_UPLOAD' => $integerType, + 'CURLINFO_SIZE_DOWNLOAD' => $integerType, + 'CURLINFO_SPEED_DOWNLOAD' => $integerType, + 'CURLINFO_SPEED_UPLOAD' => $integerType, + 'CURLINFO_HEADER_SIZE' => $integerType, + 'CURLINFO_HEADER_OUT' => $stringFalseType, + 'CURLINFO_REQUEST_SIZE' => $integerType, + 'CURLINFO_SSL_VERIFYRESULT' => $integerType, + 'CURLINFO_CONTENT_LENGTH_DOWNLOAD' => $floatType, + 'CURLINFO_CONTENT_LENGTH_UPLOAD' => $floatType, + 'CURLINFO_CONTENT_TYPE' => $stringFalseType, + 'CURLINFO_PRIVATE' => $stringFalseType, + 'CURLINFO_RESPONSE_CODE' => $integerType, + 'CURLINFO_HTTP_CONNECTCODE' => $integerType, + 'CURLINFO_HTTPAUTH_AVAIL' => $integerType, + 'CURLINFO_PROXYAUTH_AVAIL' => $integerType, + 'CURLINFO_OS_ERRNO' => $integerType, + 'CURLINFO_NUM_CONNECTS' => $integerType, + 'CURLINFO_SSL_ENGINES' => $integerStringArrayType, + 'CURLINFO_COOKIELIST' => $integerStringArrayType, + 'CURLINFO_FTP_ENTRY_PATH' => $stringFalseType, + 'CURLINFO_APPCONNECT_TIME' => $floatType, + 'CURLINFO_CERTINFO' => $nestedStringStringArrayType, + 'CURLINFO_CONDITION_UNMET' => $integerType, + 'CURLINFO_RTSP_CLIENT_CSEQ' => $integerType, + 'CURLINFO_RTSP_CSEQ_RECV' => $integerType, + 'CURLINFO_RTSP_SERVER_CSEQ' => $integerType, + 'CURLINFO_RTSP_SESSION_ID' => $integerType, + 'CURLINFO_HTTP_VERSION' => $integerType, + 'CURLINFO_PROTOCOL' => $stringType, + 'CURLINFO_PROXY_SSL_VERIFYRESULT' => $integerType, + 'CURLINFO_SCHEME' => $stringType, + 'CURLINFO_CONTENT_LENGTH_DOWNLOAD_T' => $integerType, + 'CURLINFO_CONTENT_LENGTH_UPLOAD_T' => $integerType, + 'CURLINFO_SIZE_DOWNLOAD_T' => $integerType, + 'CURLINFO_SIZE_UPLOAD_T' => $integerType, + 'CURLINFO_SPEED_DOWNLOAD_T' => $integerType, + 'CURLINFO_SPEED_UPLOAD_T' => $integerType, + 'CURLINFO_APPCONNECT_TIME_T' => $integerType, + 'CURLINFO_CONNECT_TIME_T' => $integerType, + 'CURLINFO_FILETIME_T' => $integerType, + 'CURLINFO_NAMELOOKUP_TIME_T' => $integerType, + 'CURLINFO_PRETRANSFER_TIME_T' => $integerType, + 'CURLINFO_REDIRECT_TIME_T' => $integerType, + 'CURLINFO_STARTTRANSFER_TIME_T' => $integerType, + 'CURLINFO_TOTAL_TIME_T' => $integerType, + ]; + + foreach ($componentTypesPairedConstants as $constantName => $type) { + $constantNameNode = new Name($constantName); + if ($this->reflectionProvider->hasConstant($constantNameNode, $scope) === false) { + continue; + } + + $valueType = $this->reflectionProvider->getConstant($constantNameNode, $scope)->getValueType(); + if ($componentType->isSuperTypeOf($valueType)->yes()) { + return $type; + } + } + + return $falseType; + } + + private function createAllComponentsReturnType(): Type + { + $returnTypes = [ + new ConstantBooleanType(false), + ]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $stringType = new StringType(); + $integerType = new IntegerType(); + $floatType = new FloatType(); + $stringOrNullType = TypeCombinator::union($stringType, new NullType()); + $nestedStringStringArrayType = new ArrayType($integerType, new ArrayType($stringType, $stringType)); + + $componentTypesPairedStrings = [ + 'url' => $stringType, + 'content_type' => $stringOrNullType, + 'http_code' => $integerType, + 'header_size' => $integerType, + 'request_size' => $integerType, + 'filetime' => $integerType, + 'ssl_verify_result' => $integerType, + 'redirect_count' => $integerType, + 'total_time' => $floatType, + 'namelookup_time' => $floatType, + 'connect_time' => $floatType, + 'pretransfer_time' => $floatType, + 'size_upload' => $floatType, + 'size_download' => $floatType, + 'speed_download' => $floatType, + 'speed_upload' => $floatType, + 'download_content_length' => $floatType, + 'upload_content_length' => $floatType, + 'starttransfer_time' => $floatType, + 'redirect_time' => $floatType, + 'redirect_url' => $stringType, + 'primary_ip' => $stringType, + 'certinfo' => $nestedStringStringArrayType, + 'primary_port' => $integerType, + 'local_ip' => $stringType, + 'local_port' => $integerType, + 'http_version' => $integerType, + 'protocol' => $integerType, + 'ssl_verifyresult' => $integerType, + 'scheme' => $stringType, + ]; + foreach ($componentTypesPairedStrings as $componentName => $componentValueType) { + $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType); + } + + $returnTypes[] = $builder->getArray(); + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$returnTypes)); + } + +} diff --git a/src/Type/Php/CurlInitReturnTypeExtension.php b/src/Type/Php/CurlInitReturnTypeExtension.php deleted file mode 100644 index e9564278fe..0000000000 --- a/src/Type/Php/CurlInitReturnTypeExtension.php +++ /dev/null @@ -1,38 +0,0 @@ -getName() === 'curl_init'; - } - - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - Node\Expr\FuncCall $functionCall, - Scope $scope, - ): Type - { - $argsCount = count($functionCall->getArgs()); - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if ($argsCount === 0) { - return TypeCombinator::remove($returnType, new ConstantBooleanType(false)); - } - - return $returnType; - } - -} diff --git a/src/Type/Php/DateFormatFunctionReturnTypeExtension.php b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php index b926ed181d..1a404526f3 100644 --- a/src/Type/Php/DateFormatFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php @@ -3,7 +3,6 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -11,9 +10,13 @@ use PHPStan\Type\Type; use function count; -class DateFormatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class DateFormatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'date_format'; @@ -29,10 +32,9 @@ public function getTypeFromFunctionCall( return new StringType(); } - return $scope->getType( - new FuncCall(new FullyQualified('date'), [ - $functionCall->getArgs()[1], - ]), + return $this->dateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($functionCall->getArgs()[1]->value), + true, ); } diff --git a/src/Type/Php/DateFormatMethodReturnTypeExtension.php b/src/Type/Php/DateFormatMethodReturnTypeExtension.php index f6b71786bc..34854d28cc 100644 --- a/src/Type/Php/DateFormatMethodReturnTypeExtension.php +++ b/src/Type/Php/DateFormatMethodReturnTypeExtension.php @@ -3,9 +3,7 @@ namespace PHPStan\Type\Php; use DateTimeInterface; -use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -13,9 +11,13 @@ use PHPStan\Type\Type; use function count; -class DateFormatMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class DateFormatMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function getClass(): string { return DateTimeInterface::class; @@ -32,10 +34,9 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return new StringType(); } - return $scope->getType( - new FuncCall(new FullyQualified('date'), [ - $methodCall->getArgs()[0], - ]), + return $this->dateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($methodCall->getArgs()[0]->value), + true, ); } diff --git a/src/Type/Php/DateFunctionReturnTypeExtension.php b/src/Type/Php/DateFunctionReturnTypeExtension.php index ce9b739295..32113eb312 100644 --- a/src/Type/Php/DateFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFunctionReturnTypeExtension.php @@ -5,24 +5,17 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\Accessory\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 +final class DateFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'date'; @@ -32,94 +25,16 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) === 0) { - return new StringType(); - } - $argType = $scope->getType($functionCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($argType); - - if (count($constantStrings) === 0) { - return new StringType(); - } - - if (count($constantStrings) === 1) { - $constantString = $constantStrings[0]->getValue(); - - // see see https://www.php.net/manual/en/datetime.format.php - switch ($constantString) { - case 'd': - return $this->buildNumericRangeType(1, 31, true); - case 'j': - return $this->buildNumericRangeType(1, 31, false); - case 'N': - return $this->buildNumericRangeType(1, 7, false); - case 'w': - return $this->buildNumericRangeType(0, 6, false); - case 'm': - return $this->buildNumericRangeType(1, 12, true); - case 'n': - return $this->buildNumericRangeType(1, 12, false); - case 't': - return $this->buildNumericRangeType(28, 31, false); - case 'L': - return $this->buildNumericRangeType(0, 1, false); - case 'g': - return $this->buildNumericRangeType(1, 12, false); - case 'G': - return $this->buildNumericRangeType(0, 23, false); - case 'h': - return $this->buildNumericRangeType(1, 12, true); - case 'H': - return $this->buildNumericRangeType(0, 23, true); - case 'I': - return $this->buildNumericRangeType(0, 1, false); - } - } - - $types = []; - foreach ($constantStrings as $constantString) { - $types[] = 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 null; } - return new UnionType($types); + return $this->dateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($functionCall->getArgs()[0]->value), + false, + ); } } diff --git a/src/Type/Php/DateFunctionReturnTypeHelper.php b/src/Type/Php/DateFunctionReturnTypeHelper.php new file mode 100644 index 0000000000..7723ee2151 --- /dev/null +++ b/src/Type/Php/DateFunctionReturnTypeHelper.php @@ -0,0 +1,118 @@ +getConstantStrings() as $formatString) { + $types[] = $this->buildReturnTypeFromFormat($formatString->getValue(), $useMicrosec); + } + + if (count($types) === 0) { + $types[] = $formatType->isNonEmptyString()->yes() + ? new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]) + : new StringType(); + } + + $type = TypeCombinator::union(...$types); + + if ($type->isNumericString()->no() && $formatType->isNonEmptyString()->yes()) { + $type = TypeCombinator::union($type, new IntersectionType([ + new StringType(), new AccessoryNonEmptyStringType(), + ])); + } + + return $type; + } + + public function buildReturnTypeFromFormat(string $formatString, bool $useMicrosec): Type + { + // see see https://www.php.net/manual/en/datetime.format.php + switch ($formatString) { + case 'd': + return $this->buildNumericRangeType(1, 31, true); + case 'j': + return $this->buildNumericRangeType(1, 31, false); + case 'N': + return $this->buildNumericRangeType(1, 7, false); + case 'w': + return $this->buildNumericRangeType(0, 6, false); + case 'm': + return $this->buildNumericRangeType(1, 12, true); + case 'n': + return $this->buildNumericRangeType(1, 12, false); + case 't': + return $this->buildNumericRangeType(28, 31, false); + case 'L': + return $this->buildNumericRangeType(0, 1, false); + case 'g': + return $this->buildNumericRangeType(1, 12, false); + case 'G': + return $this->buildNumericRangeType(0, 23, false); + case 'h': + return $this->buildNumericRangeType(1, 12, true); + case 'H': + return $this->buildNumericRangeType(0, 23, true); + case 'I': + return $this->buildNumericRangeType(0, 1, false); + case 'u': + return $useMicrosec + ? new IntersectionType([new StringType(), new AccessoryNonFalsyStringType(), new AccessoryNumericStringType()]) + : new ConstantStringType('000000'); + case 'v': + return $useMicrosec + ? new IntersectionType([new StringType(), new AccessoryNonFalsyStringType(), new AccessoryNumericStringType()]) + : new ConstantStringType('000'); + } + + $date = date($formatString); + + // If parameter string is not included, returned as ConstantStringType + if ($date === $formatString) { + return new ConstantStringType($date); + } + + if (is_numeric($date)) { + return new IntersectionType([new StringType(), new AccessoryNumericStringType()]); + } + + return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + } + + private function buildNumericRangeType(int $min, int $max, bool $zeroPad): Type + { + $types = []; + + for ($i = $min; $i <= $max; $i++) { + $string = (string) $i; + + if ($zeroPad) { + $string = str_pad($string, 2, '0', STR_PAD_LEFT); + } + + $types[] = new ConstantStringType($string); + } + + return new UnionType($types); + } + +} diff --git a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index e7fda0fa2c..04c356151d 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -5,17 +5,22 @@ use DateInterval; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; -class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +final class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === DateInterval::class; @@ -28,23 +33,32 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); foreach ($constantStrings as $constantString) { try { new DateInterval($constantString->getValue()); } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $methodReflection->getThrowType(); + return $this->exceptionType(); } return null; } + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateMalformedIntervalStringException'); + } + + return new ObjectType('Exception'); + } + } diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..563ade3589 --- /dev/null +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -0,0 +1,68 @@ +getName() === 'createFromDateString'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + $arguments = $methodCall->getArgs(); + + if (!isset($arguments[0])) { + return null; + } + + $strings = $scope->getType($arguments[0]->value)->getConstantStrings(); + + $possibleReturnTypes = []; + foreach ($strings as $string) { + try { + $result = @DateInterval::createFromDateString($string->getValue()); + } catch (Throwable) { + $possibleReturnTypes[] = false; + continue; + } + $possibleReturnTypes[] = $result instanceof DateInterval ? DateInterval::class : false; + } + + // the error case, when wrong types are passed + if (count($possibleReturnTypes) === 0) { + return null; + } + + if (in_array(false, $possibleReturnTypes, true) && in_array(DateInterval::class, $possibleReturnTypes, true)) { + return null; + } + + if (in_array(false, $possibleReturnTypes, true)) { + return new ConstantBooleanType(false); + } + + return new ObjectType(DateInterval::class); + } + +} diff --git a/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php index 8b52604f48..e20c503c05 100644 --- a/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php +++ b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php @@ -14,11 +14,10 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use function strtolower; -class DatePeriodConstructorReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +final class DatePeriodConstructorReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string @@ -47,7 +46,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, } $firstArgType = $scope->getType($methodCall->getArgs()[0]->value); - if ((new StringType())->isSuperTypeOf($firstArgType)->yes()) { + if ($firstArgType->isString()->yes()) { $firstArgType = new ObjectType(DateTime::class); } $thirdArgType = null; @@ -71,7 +70,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, ]); } - if ((new IntegerType())->isSuperTypeOf($thirdArgType)->yes()) { + if ($thirdArgType->isInteger()->yes()) { return new GenericObjectType(DatePeriod::class, [ $firstArgType, new NullType(), diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index c84512c05a..6bde75bc6d 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -6,18 +6,23 @@ use DateTimeImmutable; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use function in_array; -class DateTimeConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +final class DateTimeConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && in_array($methodReflection->getDeclaringClass()->getName(), [DateTime::class, DateTimeImmutable::class], true); @@ -30,23 +35,32 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); foreach ($constantStrings as $constantString) { try { new DateTime($constantString->getValue()); } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $methodReflection->getThrowType(); + return $this->exceptionType(); } return null; } + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateMalformedStringException'); + } + + return new ObjectType('Exception'); + } + } diff --git a/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php b/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..9e3e5c892d --- /dev/null +++ b/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php @@ -0,0 +1,49 @@ +getName(), ['date_create', 'date_create_immutable'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $datetimes = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); + + if (count($datetimes) === 0) { + return null; + } + + $types = []; + $className = $functionReflection->getName() === 'date_create' ? DateTime::class : DateTimeImmutable::class; + foreach ($datetimes as $constantString) { + $isValid = date_create($constantString->getValue()) !== false; + $types[] = $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + } + + return TypeCombinator::union(...$types); + } + +} diff --git a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php index 22945f9966..ef42505ede 100644 --- a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php @@ -7,16 +7,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; use function in_array; -class DateTimeDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class DateTimeDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,25 +23,29 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return in_array($functionReflection->getName(), ['date_create_from_format', 'date_create_immutable_from_format'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->getArgs()) < 2) { - return $defaultReturnType; + return null; } - $format = $scope->getType($functionCall->getArgs()[0]->value); - $datetime = $scope->getType($functionCall->getArgs()[1]->value); + $formats = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); + $datetimes = $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(); - if (!$format instanceof ConstantStringType || !$datetime instanceof ConstantStringType) { - return $defaultReturnType; + if (count($formats) === 0 || count($datetimes) === 0) { + return null; } - $isValid = (DateTime::createFromFormat($format->getValue(), $datetime->getValue()) !== false); - + $types = []; $className = $functionReflection->getName() === 'date_create_from_format' ? DateTime::class : DateTimeImmutable::class; - return $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + foreach ($formats as $formatConstantString) { + foreach ($datetimes as $datetimeConstantString) { + $isValid = (DateTime::createFromFormat($formatConstantString->getValue(), $datetimeConstantString->getValue()) !== false); + $types[] = $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + } + } + + return TypeCombinator::union(...$types); } } diff --git a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php new file mode 100644 index 0000000000..d22637d4e5 --- /dev/null +++ b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php @@ -0,0 +1,71 @@ +getName() === 'modify' && in_array($methodReflection->getDeclaringClass()->getName(), [DateTime::class, DateTimeImmutable::class], true); + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) === 0) { + return null; + } + + if (!$this->phpVersion->hasDateTimeExceptions()) { + return null; + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + + foreach ($constantStrings as $constantString) { + try { + $dateTime = new DateTime(); + $dateTime->modify($constantString->getValue()); + } catch (\Exception $e) { // phpcs:ignore + return $this->exceptionType(); + } + + $valueType = TypeCombinator::remove($valueType, $constantString); + } + + if (!$valueType instanceof NeverType) { + return $this->exceptionType(); + } + + return null; + } + + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateMalformedStringException'); + } + + return new ObjectType('Exception'); + } + +} diff --git a/src/Type/Php/DateTimeModifyReturnTypeExtension.php b/src/Type/Php/DateTimeModifyReturnTypeExtension.php new file mode 100644 index 0000000000..0ed2933856 --- /dev/null +++ b/src/Type/Php/DateTimeModifyReturnTypeExtension.php @@ -0,0 +1,90 @@ + $dateTimeClass */ + public function __construct( + private PhpVersion $phpVersion, + private string $dateTimeClass, + ) + { + } + + public function getClass(): string + { + return $this->dateTimeClass; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'modify'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) < 1) { + return null; + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + + $hasFalse = false; + $hasDateTime = false; + + foreach ($constantStrings as $constantString) { + try { + $result = @(new DateTime())->modify($constantString->getValue()); + } catch (Throwable) { + $valueType = TypeCombinator::remove($valueType, $constantString); + continue; + } + + if ($result === false) { + $hasFalse = true; + } else { + $hasDateTime = true; + } + + $valueType = TypeCombinator::remove($valueType, $constantString); + } + + if (!$valueType instanceof NeverType) { + return null; + } + + if ($hasFalse) { + if (!$hasDateTime) { + return new ConstantBooleanType(false); + } + + return null; + } elseif ($hasDateTime) { + return $scope->getType($methodCall->var); + } + + if ($this->phpVersion->hasDateTimeExceptions()) { + return new NeverType(); + } + + return null; + } + +} diff --git a/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php b/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php new file mode 100644 index 0000000000..ce6d31b581 --- /dev/null +++ b/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'sub' + && in_array($methodReflection->getDeclaringClass()->getName(), [DateTime::class, DateTimeImmutable::class], true); + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) === 0) { + return null; + } + + if (!$this->phpVersion->hasDateTimeExceptions()) { + return null; + } + + return new ObjectType('DateInvalidOperationException'); + } + +} diff --git a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php new file mode 100644 index 0000000000..e1b35ac7ee --- /dev/null +++ b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php @@ -0,0 +1,64 @@ +getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === DateTimeZone::class; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) === 0) { + return null; + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + + foreach ($constantStrings as $constantString) { + try { + new DateTimeZone($constantString->getValue()); + } catch (\Exception $e) { // phpcs:ignore + return $this->exceptionType(); + } + + $valueType = TypeCombinator::remove($valueType, $constantString); + } + + if (!$valueType instanceof NeverType) { + return $this->exceptionType(); + } + + return null; + } + + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateInvalidTimeZoneException'); + } + + return new ObjectType('Exception'); + } + +} diff --git a/src/Type/Php/DefineConstantTypeSpecifyingExtension.php b/src/Type/Php/DefineConstantTypeSpecifyingExtension.php index fa6f5bba57..9d4ac3d682 100644 --- a/src/Type/Php/DefineConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefineConstantTypeSpecifyingExtension.php @@ -14,7 +14,7 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use function count; -class DefineConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class DefineConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -56,9 +56,8 @@ public function specifyTypes( ), $scope->getType($node->getArgs()[1]->value), TypeSpecifierContext::createTruthy(), - false, $scope, - ); + )->setAlwaysOverwriteTypes(); } } diff --git a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index 0e90075013..01c310459b 100644 --- a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php @@ -2,7 +2,6 @@ namespace PHPStan\Type\Php; -use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -15,11 +14,15 @@ use PHPStan\Type\MixedType; use function count; -class DefinedConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class DefinedConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; + public function __construct(private ConstantHelper $constantHelper) + { + } + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { $this->typeSpecifier = $typeSpecifier; @@ -33,7 +36,7 @@ public function isFunctionSupported( { return $functionReflection->getName() === 'defined' && count($node->getArgs()) >= 1 - && !$context->null(); + && $context->true(); } public function specifyTypes( @@ -51,13 +54,15 @@ public function specifyTypes( return new SpecifiedTypes([], []); } + $expr = $this->constantHelper->createExprFromConstantName($constantName->getValue()); + if ($expr === null) { + return new SpecifiedTypes([], []); + } + return $this->typeSpecifier->create( - new Node\Expr\ConstFetch( - new Node\Name\FullyQualified($constantName->getValue()), - ), + $expr, new MixedType(), $context, - false, $scope, ); } diff --git a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php index 0a96b8d73f..5de459fb73 100644 --- a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php @@ -12,7 +12,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class DioStatDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class DioStatDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php b/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php new file mode 100644 index 0000000000..3075827a96 --- /dev/null +++ b/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php @@ -0,0 +1,31 @@ +getDeclaringClass()->getName() === 'Ds\Map' + && ($methodReflection->getName() === 'get' || $methodReflection->getName() === 'remove'); + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->args) < 2) { + return $methodReflection->getThrowType(); + } + + return new VoidType(); + } + +} diff --git a/src/Type/Php/DsMapDynamicReturnTypeExtension.php b/src/Type/Php/DsMapDynamicReturnTypeExtension.php index 140e41a3f5..50ecba6226 100644 --- a/src/Type/Php/DsMapDynamicReturnTypeExtension.php +++ b/src/Type/Php/DsMapDynamicReturnTypeExtension.php @@ -5,16 +5,11 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\Generic\TemplateType; -use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; -use function array_filter; -use function array_values; +use PHPStan\Type\TypeWithClassName; use function count; +use function in_array; final class DsMapDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -26,54 +21,41 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'get' || $methodReflection->getName() === 'remove'; + return in_array($methodReflection->getName(), ['get', 'remove'], true); } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - $returnType = ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants(), - )->getReturnType(); - - if (count($methodCall->getArgs()) > 1) { - return $returnType; + $argsCount = count($methodCall->getArgs()); + if ($argsCount > 1) { + return null; } - if ($returnType instanceof UnionType) { - $types = array_values( - array_filter( - $returnType->getTypes(), - static function (Type $type): bool { - if ( - $type instanceof TemplateType - && $type->getName() === 'TDefault' - && ( - $type->getScope()->equals(TemplateTypeScope::createWithMethod('Ds\Map', 'get')) - || $type->getScope()->equals(TemplateTypeScope::createWithMethod('Ds\Map', 'remove')) - ) - ) { - return false; - } + if ($argsCount === 0) { + return null; + } - return true; - }, - ), - ); + $mapType = $scope->getType($methodCall->var); + if (!$mapType instanceof TypeWithClassName) { + return null; + } - if (count($types) === 1) { - return $types[0]; - } + $mapAncestor = $mapType->getAncestorWithClassName('Ds\Map'); + if ($mapAncestor === null) { + return null; + } - if (count($types) === 0) { - return $returnType; - } + $mapAncestorClass = $mapAncestor->getClassReflection(); + if ($mapAncestorClass === null) { + return null; + } - return TypeCombinator::union(...$types); + $valueType = $mapAncestorClass->getActiveTemplateTypeMap()->getType('TValue'); + if ($valueType === null) { + return null; } - return $returnType; + return $valueType; } } diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index 8440f8f2dd..d43c328df5 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -6,7 +6,9 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -14,6 +16,7 @@ use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\StringType; @@ -22,7 +25,7 @@ use PHPStan\Type\TypeUtils; use function count; -class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) @@ -38,34 +41,51 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { - if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; } - $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); - $isSuperset = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); - if ($isSuperset->yes()) { - if ($this->phpVersion->getVersionId() >= 80000) { + $delimiterType = $scope->getType($args[0]->value); + $isEmptyString = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); + if ($isEmptyString->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } return new ConstantBooleanType(false); - } elseif ($isSuperset->no()) { - $arrayType = new ArrayType(new IntegerType(), new StringType()); - if ( - !isset($functionCall->getArgs()[2]) - || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($functionCall->getArgs()[2]->value))->yes() - ) { - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); - } + } + + $stringType = $scope->getType($args[1]->value); + $accessory = []; + if ($stringType->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + $returnValueType = new IntersectionType($accessory); + } else { + $returnValueType = new StringType(); + } + + $returnType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $returnValueType), new AccessoryArrayListType()); + if ( + !isset($args[2]) + || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($args[2]->value))->yes() + ) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } - return $arrayType; + if (!$this->phpVersion->throwsValueErrorForInternalFunctions() && $isEmptyString->maybe()) { + $returnType = TypeCombinator::union($returnType, new ConstantBooleanType(false)); } - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($delimiterType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($returnType); + $returnType = TypeUtils::toBenevolentUnion($returnType); } return $returnType; diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php new file mode 100644 index 0000000000..cebc157759 --- /dev/null +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -0,0 +1,450 @@ +|null */ + private ?array $filterTypeMap = null; + + /** @var array>|null */ + private ?array $filterTypeOptions = null; + + private ?Type $supportedFilterInputTypes = null; + + public function __construct(private ReflectionProvider $reflectionProvider, private PhpVersion $phpVersion) + { + $this->flagsString = new ConstantStringType('flags'); + } + + public function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): Type + { + $inexistentOffsetType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType) + ? new ConstantBooleanType(false) + : new NullType(); + + $hasOffsetValueType = $inputType->hasOffsetValueType($offsetType); + if ($hasOffsetValueType->no()) { + return $inexistentOffsetType; + } + + $filteredType = $this->getType($inputType->getOffsetValueType($offsetType), $filterType, $flagsType); + + return $hasOffsetValueType->maybe() + ? TypeCombinator::union($filteredType, $inexistentOffsetType) + : $filteredType; + } + + public function getInputType(Type $typeType, Type $varNameType, ?Type $filterType, ?Type $flagsType): Type + { + $this->supportedFilterInputTypes ??= TypeCombinator::union( + $this->reflectionProvider->getConstant(new Node\Name('INPUT_GET'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_POST'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_COOKIE'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_SERVER'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_ENV'), null)->getValueType(), + ); + + if (!$typeType->isInteger()->yes() || $this->supportedFilterInputTypes->isSuperTypeOf($typeType)->no()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + + // Using a null as input mimics pre PHP 8 behaviour where filter_input + // would return the same as if the offset does not exist + $inputType = new NullType(); + } else { + // Pragmatical solution since global expressions are not passed through the scope for performance reasons + // See https://github.com/phpstan/phpstan-src/pull/2012 for details + $inputType = new ArrayType(new StringType(), new MixedType()); + } + + return $this->getOffsetValueType($inputType, $varNameType, $filterType, $flagsType); + } + + public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): Type + { + $mixedType = new MixedType(); + + if ($filterType === null) { + $filterValue = $this->getConstant('FILTER_DEFAULT'); + } else { + if (!$filterType instanceof ConstantIntegerType) { + return $mixedType; + } + $filterValue = $filterType->getValue(); + } + + if ($flagsType === null) { + $flagsType = new ConstantIntegerType(0); + } + + $hasOptions = $this->hasOptions($flagsType); + $options = $hasOptions->yes() ? $this->getOptions($flagsType, $filterValue) : []; + + $defaultType = $options['default'] ?? ($this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType) + ? new NullType() + : new ConstantBooleanType(false)); + + $inputIsArray = $inputType->isArray(); + $hasRequireArrayFlag = $this->hasFlag($this->getConstant('FILTER_REQUIRE_ARRAY'), $flagsType); + if ($inputIsArray->no() && $hasRequireArrayFlag) { + return $defaultType; + } + + $hasForceArrayFlag = $this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsType); + if ($inputIsArray->yes() && ($hasRequireArrayFlag || $hasForceArrayFlag)) { + $inputArrayKeyType = $inputType->getIterableKeyType(); + $inputType = $inputType->getIterableValueType(); + } + + if ($inputType->isScalar()->no() && $inputType->isNull()->no()) { + return $defaultType; + } + + $exactType = $this->determineExactType($inputType, $filterValue, $defaultType, $flagsType); + $type = $exactType ?? $this->getFilterTypeMap()[$filterValue] ?? $mixedType; + $type = $this->applyRangeOptions($type, $options, $defaultType); + + if ($inputType->isNonEmptyString()->yes() + && $type->isString()->yes() + && !$this->canStringBeSanitized($filterValue, $flagsType)) { + $accessory = new AccessoryNonEmptyStringType(); + if ($inputType->isNonFalsyString()->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } + $type = TypeCombinator::intersect($type, $accessory); + } + + if ($hasRequireArrayFlag) { + $type = new ArrayType($inputArrayKeyType ?? $mixedType, $type); + } + + if ($exactType === null || $hasOptions->maybe() || (!$inputType->equals($type) && $inputType->isSuperTypeOf($type)->yes())) { + if ($defaultType->isSuperTypeOf($type)->no()) { + $type = TypeCombinator::union($type, $defaultType); + } + } + + if (!$hasRequireArrayFlag && $hasForceArrayFlag) { + return new ArrayType($inputArrayKeyType ?? $mixedType, $type); + } + + return $type; + } + + /** + * @return array + */ + private function getFilterTypeMap(): array + { + if ($this->filterTypeMap !== null) { + return $this->filterTypeMap; + } + + $booleanType = new BooleanType(); + $floatType = new FloatType(); + $intType = new IntegerType(); + $stringType = new StringType(); + $nonFalsyStringType = TypeCombinator::intersect($stringType, new AccessoryNonFalsyStringType()); + + $this->filterTypeMap = [ + $this->getConstant('FILTER_UNSAFE_RAW') => $stringType, + $this->getConstant('FILTER_SANITIZE_EMAIL') => $stringType, + $this->getConstant('FILTER_SANITIZE_ENCODED') => $stringType, + $this->getConstant('FILTER_SANITIZE_NUMBER_FLOAT') => $stringType, + $this->getConstant('FILTER_SANITIZE_NUMBER_INT') => $stringType, + $this->getConstant('FILTER_SANITIZE_SPECIAL_CHARS') => $stringType, + $this->getConstant('FILTER_SANITIZE_STRING') => $stringType, + $this->getConstant('FILTER_SANITIZE_URL') => $stringType, + $this->getConstant('FILTER_VALIDATE_BOOLEAN') => $booleanType, + $this->getConstant('FILTER_VALIDATE_DOMAIN') => $stringType, + $this->getConstant('FILTER_VALIDATE_EMAIL') => $nonFalsyStringType, + $this->getConstant('FILTER_VALIDATE_FLOAT') => $floatType, + $this->getConstant('FILTER_VALIDATE_INT') => $intType, + $this->getConstant('FILTER_VALIDATE_IP') => $nonFalsyStringType, + $this->getConstant('FILTER_VALIDATE_MAC') => $nonFalsyStringType, + $this->getConstant('FILTER_VALIDATE_REGEXP') => $stringType, + $this->getConstant('FILTER_VALIDATE_URL') => $nonFalsyStringType, + ]; + + if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_MAGIC_QUOTES'), null)) { + $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES')] = $stringType; + } + + if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_ADD_SLASHES'), null)) { + $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_ADD_SLASHES')] = $stringType; + } + + return $this->filterTypeMap; + } + + /** + * @return array> + */ + private function getFilterTypeOptions(): array + { + if ($this->filterTypeOptions !== null) { + return $this->filterTypeOptions; + } + + $this->filterTypeOptions = [ + $this->getConstant('FILTER_VALIDATE_INT') => ['min_range', 'max_range'], + // PHPStan does not yet support FloatRangeType + // $this->getConstant('FILTER_VALIDATE_FLOAT') => ['min_range', 'max_range'], + ]; + + return $this->filterTypeOptions; + } + + /** + * @param non-empty-string $constantName + */ + private function getConstant(string $constantName): int + { + $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); + $valueType = $constant->getValueType(); + if (!$valueType instanceof ConstantIntegerType) { + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); + } + + return $valueType->getValue(); + } + + private function determineExactType(Type $in, int $filterValue, Type $defaultType, ?Type $flagsType): ?Type + { + if ($filterValue === $this->getConstant('FILTER_VALIDATE_BOOLEAN')) { + if ($in->isBoolean()->yes()) { + return $in; + } + + if ($in->isNull()->yes()) { + return $defaultType; + } + } + + if ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT')) { + if ($in->isFloat()->yes()) { + return $in; + } + + if ($in->isInteger()->yes()) { + return $in->toFloat(); + } + + if ($in->isTrue()->yes()) { + return new ConstantFloatType(1); + } + + if ($in->isFalse()->yes() || $in->isNull()->yes()) { + return $defaultType; + } + } + + if ($filterValue === $this->getConstant('FILTER_VALIDATE_INT')) { + if ($in->isInteger()->yes()) { + return $in; + } + + if ($in->isTrue()->yes()) { + return new ConstantIntegerType(1); + } + + if ($in->isFalse()->yes() || $in->isNull()->yes()) { + return $defaultType; + } + + if ($in instanceof ConstantFloatType) { + return $in->getValue() - (int) $in->getValue() === 0.0 + ? $in->toInteger() + : $defaultType; + } + + if ($in instanceof ConstantStringType) { + $value = $in->getValue(); + $allowOctal = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_OCTAL'), $flagsType); + $allowHex = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_HEX'), $flagsType); + + if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) { + $octalValue = octdec($value); + return is_int($octalValue) ? new ConstantIntegerType($octalValue) : $defaultType; + } + + if ($allowHex && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) { + $hexValue = hexdec($value); + return is_int($hexValue) ? new ConstantIntegerType($hexValue) : $defaultType; + } + + return preg_match('/\A[+-]?(?:0|[1-9][0-9]*)\z/', $value) === 1 ? $in->toInteger() : $defaultType; + } + } + + if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { + if (!$this->canStringBeSanitized($filterValue, $flagsType) && $in->isString()->yes()) { + return $in; + } + + if ($in->isBoolean()->yes() || $in->isFloat()->yes() || $in->isInteger()->yes() || $in->isNull()->yes()) { + return $in->toString(); + } + } + + return null; + } + + /** @param array $typeOptions */ + private function applyRangeOptions(Type $type, array $typeOptions, Type $defaultType): Type + { + if (!$type->isInteger()->yes()) { + return $type; + } + + $range = []; + if (isset($typeOptions['min_range'])) { + if ($typeOptions['min_range'] instanceof ConstantScalarType) { + $range['min'] = (int) $typeOptions['min_range']->getValue(); + } elseif ($typeOptions['min_range'] instanceof IntegerRangeType) { + $range['min'] = $typeOptions['min_range']->getMin(); + } else { + $range['min'] = null; + } + } + if (isset($typeOptions['max_range'])) { + if ($typeOptions['max_range'] instanceof ConstantScalarType) { + $range['max'] = (int) $typeOptions['max_range']->getValue(); + } elseif ($typeOptions['max_range'] instanceof IntegerRangeType) { + $range['max'] = $typeOptions['max_range']->getMax(); + } else { + $range['max'] = null; + } + } + + if (array_key_exists('min', $range) || array_key_exists('max', $range)) { + $min = $range['min'] ?? null; + $max = $range['max'] ?? null; + $rangeType = IntegerRangeType::fromInterval($min, $max); + $rangeTypeIsSuperType = $rangeType->isSuperTypeOf($type); + + if ($rangeTypeIsSuperType->no()) { + // e.g. if 9 is filtered with a range of int<17, 19> + return $defaultType; + } + + if ($rangeTypeIsSuperType->yes() && !$rangeType->equals($type)) { + // e.g. if 18 or int<18, 19> are filtered with a range of int<17, 19> + return $type; + } + + // Open ranges on either side means that the input is potentially not part of the range + return $min === null || $max === null ? TypeCombinator::union($rangeType, $defaultType) : $rangeType; + } + + return $type; + } + + private function hasOptions(Type $flagsType): TrinaryLogic + { + return $flagsType->isArray() + ->and($flagsType->hasOffsetValueType(new ConstantStringType('options'))); + } + + /** @return array */ + private function getOptions(Type $flagsType, int $filterValue): array + { + $options = []; + + $optionsType = $flagsType->getOffsetValueType(new ConstantStringType('options')); + if (!$optionsType->isConstantArray()->yes()) { + return $options; + } + + $optionNames = array_merge(['default'], $this->getFilterTypeOptions()[$filterValue] ?? []); + foreach ($optionNames as $optionName) { + $optionalNameType = new ConstantStringType($optionName); + if (!$optionsType->hasOffsetValueType($optionalNameType)->yes()) { + $options[$optionName] = null; + continue; + } + + $options[$optionName] = $optionsType->getOffsetValueType($optionalNameType); + } + + return $options; + } + + private function hasFlag(int $flag, ?Type $flagsType): bool + { + if ($flagsType === null) { + return false; + } + + $type = $this->getFlagsValue($flagsType); + + return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; + } + + private function getFlagsValue(Type $exprType): Type + { + if (!$exprType->isConstantArray()->yes()) { + return $exprType; + } + + return $exprType->getOffsetValueType($this->flagsString); + } + + private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool + { + // If it is a validation filter, the string will not be changed + if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) { + return false; + } + + // FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW, + // FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK + if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { + return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsType) + || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsType) + || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsType); + } + + return true; + } + +} diff --git a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..1a04c7d5b9 --- /dev/null +++ b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php @@ -0,0 +1,38 @@ +getName() === 'filter_input'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + return $this->filterFunctionReturnTypeHelper->getInputType( + $scope->getType($functionCall->getArgs()[0]->value), + $scope->getType($functionCall->getArgs()[1]->value), + isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null, + isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null, + ); + } + +} diff --git a/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..9f9a51cad3 --- /dev/null +++ b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php @@ -0,0 +1,198 @@ +getName()), ['filter_var_array', 'filter_input_array'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $functionName = strtolower($functionReflection->getName()); + $inputArgType = $scope->getType($functionCall->getArgs()[0]->value); + $inputConstantArrayType = null; + if ($functionName === 'filter_var_array') { + if ($inputArgType->isArray()->no()) { + return new NeverType(); + } + + $inputConstantArrayType = $inputArgType->getConstantArrays()[0] ?? null; + } elseif ($functionName === 'filter_input_array') { + $supportedTypes = TypeCombinator::union( + $this->reflectionProvider->getConstant(new Node\Name('INPUT_GET'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_POST'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_COOKIE'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_SERVER'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_ENV'), null)->getValueType(), + ); + + if (!$inputArgType->isInteger()->yes() || $supportedTypes->isSuperTypeOf($inputArgType)->no()) { + return null; + } + + // Pragmatical solution since global expressions are not passed through the scope for performance reasons + // See https://github.com/phpstan/phpstan-src/pull/2012 for details + $inputArgType = new ArrayType(new StringType(), new MixedType()); + } + + $filterArgType = $scope->getType($functionCall->getArgs()[1]->value); + $filterConstantArrayType = $filterArgType->getConstantArrays()[0] ?? null; + $addEmptyType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; + $addEmpty = $addEmptyType === null || $addEmptyType->isTrue()->yes(); + + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + + if ($filterArgType instanceof ConstantIntegerType) { + if ($inputConstantArrayType === null) { + $isList = $inputArgType->isList()->yes(); + $valueType = $this->filterFunctionReturnTypeHelper->getType( + $inputArgType->getIterableValueType(), + $filterArgType, + null, + ); + $arrayType = new ArrayType($inputArgType->getIterableKeyType(), $valueType); + + return $isList ? TypeCombinator::intersect($arrayType, new AccessoryArrayListType()) : $arrayType; + } + + // Override $add_empty option + $addEmpty = false; + + $keysType = $inputConstantArrayType; + $inputKeysList = array_map(static fn ($type) => $type->getValue(), $inputConstantArrayType->getKeyTypes()); + $filterTypesMap = array_fill_keys($inputKeysList, $filterArgType); + $inputTypesMap = array_combine($inputKeysList, $inputConstantArrayType->getValueTypes()); + $optionalKeys = []; + foreach ($inputConstantArrayType->getOptionalKeys() as $index) { + if (!isset($inputKeysList[$index])) { + continue; + } + + $optionalKeys[] = $inputKeysList[$index]; + } + } elseif ($filterConstantArrayType === null) { + if ($inputConstantArrayType === null) { + $isList = $inputArgType->isList()->yes(); + $valueType = $this->filterFunctionReturnTypeHelper->getType($inputArgType, $filterArgType, null); + + $arrayType = new ArrayType( + $inputArgType->getIterableKeyType(), + $addEmpty ? TypeCombinator::addNull($valueType) : $valueType, + ); + + return $isList ? TypeCombinator::intersect($arrayType, new AccessoryArrayListType()) : $arrayType; + } + + return null; + } else { + $keysType = $filterConstantArrayType; + $filterKeyTypes = $filterConstantArrayType->getKeyTypes(); + $filterKeysList = array_map(static fn ($type) => $type->getValue(), $filterKeyTypes); + $filterTypesMap = array_combine($filterKeysList, $keysType->getValueTypes()); + + if ($inputConstantArrayType !== null) { + $inputKeysList = array_map(static fn ($type) => $type->getValue(), $inputConstantArrayType->getKeyTypes()); + $inputTypesMap = array_combine($inputKeysList, $inputConstantArrayType->getValueTypes()); + + $optionalKeys = []; + foreach ($inputConstantArrayType->getOptionalKeys() as $index) { + if (!isset($inputKeysList[$index])) { + continue; + } + + $optionalKeys[] = $inputKeysList[$index]; + } + } else { + $optionalKeys = $filterKeysList; + $inputTypesMap = array_fill_keys($optionalKeys, $inputArgType->getIterableValueType()); + } + } + + foreach ($keysType->getKeyTypes() as $keyType) { + $optional = false; + $key = $keyType->getValue(); + $inputType = $inputTypesMap[$key] ?? null; + if ($inputType === null) { + if ($addEmpty) { + $valueTypesBuilder->setOffsetValueType($keyType, new NullType()); + } + + continue; + } + + [$filterType, $flagsType] = $this->fetchFilter($filterTypesMap[$key] ?? new MixedType()); + $valueType = $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); + + if (in_array($key, $optionalKeys, true)) { + if ($addEmpty) { + $valueType = TypeCombinator::addNull($valueType); + } else { + $optional = true; + } + } + + $valueTypesBuilder->setOffsetValueType($keyType, $valueType, $optional); + } + + return $valueTypesBuilder->getArray(); + } + + /** @return array{?Type, ?Type} */ + public function fetchFilter(Type $type): array + { + if (!$type->isArray()->yes()) { + return [$type, null]; + } + + $filterKey = new ConstantStringType('filter'); + if (!$type->hasOffsetValueType($filterKey)->yes()) { + return [$type, null]; + } + + $filterOffsetType = $type->getOffsetValueType($filterKey); + $filterType = null; + + if (count($filterOffsetType->getConstantScalarTypes()) > 0) { + $filterType = TypeCombinator::union(...$filterOffsetType->getConstantScalarTypes()); + } + + return [$filterType, $type]; + } + +} diff --git a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php index 6340124fef..26c2773555 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -2,104 +2,19 @@ namespace PHPStan\Type\Php; -use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\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\DynamicFunctionReturnTypeExtension; -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 count; use function strtolower; -class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** - * All validation filters match 0x100. - */ - private const VALIDATION_FILTER_BITMASK = 0x100; - - private ConstantStringType $flagsString; - - /** @var array|null */ - private ?array $filterTypeMap = null; - - public function __construct(private ReflectionProvider $reflectionProvider) - { - $this->flagsString = new ConstantStringType('flags'); - } - - /** - * @return array - */ - private function getFilterTypeMap(): array + public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper) { - if ($this->filterTypeMap !== null) { - return $this->filterTypeMap; - } - - $booleanType = new BooleanType(); - $floatType = new FloatType(); - $intType = new IntegerType(); - $stringType = new StringType(); - - $this->filterTypeMap = [ - $this->getConstant('FILTER_UNSAFE_RAW') => $stringType, - $this->getConstant('FILTER_SANITIZE_EMAIL') => $stringType, - $this->getConstant('FILTER_SANITIZE_ENCODED') => $stringType, - $this->getConstant('FILTER_SANITIZE_NUMBER_FLOAT') => $stringType, - $this->getConstant('FILTER_SANITIZE_NUMBER_INT') => $stringType, - $this->getConstant('FILTER_SANITIZE_SPECIAL_CHARS') => $stringType, - $this->getConstant('FILTER_SANITIZE_STRING') => $stringType, - $this->getConstant('FILTER_SANITIZE_URL') => $stringType, - $this->getConstant('FILTER_VALIDATE_BOOLEAN') => $booleanType, - $this->getConstant('FILTER_VALIDATE_DOMAIN') => $stringType, - $this->getConstant('FILTER_VALIDATE_EMAIL') => $stringType, - $this->getConstant('FILTER_VALIDATE_FLOAT') => $floatType, - $this->getConstant('FILTER_VALIDATE_INT') => $intType, - $this->getConstant('FILTER_VALIDATE_IP') => $stringType, - $this->getConstant('FILTER_VALIDATE_MAC') => $stringType, - $this->getConstant('FILTER_VALIDATE_REGEXP') => $stringType, - $this->getConstant('FILTER_VALIDATE_URL') => $stringType, - ]; - - if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_MAGIC_QUOTES'), null)) { - $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES')] = $stringType; - } - - if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_ADD_SLASHES'), null)) { - $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_ADD_SLASHES')] = $stringType; - } - - return $this->filterTypeMap; - } - - private function getConstant(string $constantName): int - { - $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); - $valueType = $constant->getValueType(); - if (!$valueType instanceof ConstantIntegerType) { - throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); - } - - return $valueType->getValue(); } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -107,144 +22,17 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return strtolower($functionReflection->getName()) === 'filter_var'; } - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - FuncCall $functionCall, - Scope $scope, - ): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $mixedType = new MixedType(); - - $filterArg = $functionCall->getArgs()[1] ?? null; - if ($filterArg === null) { - $filterValue = $this->getConstant('FILTER_DEFAULT'); - } else { - $filterType = $scope->getType($filterArg->value); - if (!$filterType instanceof ConstantIntegerType) { - return $mixedType; - } - $filterValue = $filterType->getValue(); - } - - $flagsArg = $functionCall->getArgs()[2] ?? null; - $inputType = $scope->getType($functionCall->getArgs()[0]->value); - $exactType = $this->determineExactType($inputType, $filterValue); - if ($exactType !== null) { - $type = $exactType; - } else { - $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]); - } - } - - if ($this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsArg, $scope)) { - return new ArrayType(new MixedType(), $type); - } - - return $type; - } - - - private function determineExactType(Type $in, int $filterValue): ?Type - { - if (($filterValue === $this->getConstant('FILTER_VALIDATE_BOOLEAN') && $in instanceof BooleanType) - || ($filterValue === $this->getConstant('FILTER_VALIDATE_INT') && $in instanceof IntegerType) - || ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT') && $in instanceof FloatType)) { - return $in; - } - - if ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT') && $in instanceof IntegerType) { - return $in->toFloat(); - } - - return null; - } - - private function getOtherType(?Node\Arg $flagsArg, Scope $scope): Type - { - $falseType = new ConstantBooleanType(false); - if ($flagsArg === null) { - return $falseType; - } - - $defaultType = $this->getDefault($flagsArg, $scope); - if ($defaultType !== null) { - return $defaultType; - } - - if ($this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsArg, $scope)) { - return new NullType(); - } - - return $falseType; - } - - private function getDefault(Node\Arg $expression, Scope $scope): ?Type - { - $exprType = $scope->getType($expression->value); - if (!$exprType instanceof ConstantArrayType) { + if (count($functionCall->getArgs()) < 1) { return null; } - $optionsType = $exprType->getOffsetValueType(new ConstantStringType('options')); - if (!$optionsType instanceof ConstantArrayType) { - return null; - } - - $defaultType = $optionsType->getOffsetValueType(new ConstantStringType('default')); - if (!$defaultType instanceof ErrorType) { - return $defaultType; - } - - return null; - } - - - private function hasFlag(int $flag, ?Node\Arg $expression, Scope $scope): bool - { - if ($expression === null) { - return false; - } - - $type = $this->getFlagsValue($scope->getType($expression->value)); - - return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; - } - - private function getFlagsValue(Type $exprType): Type - { - if (!$exprType instanceof ConstantArrayType) { - return $exprType; - } - - return $exprType->getOffsetValueType($this->flagsString); - } - - private function canStringBeSanitized(int $filterValue, ?Node\Arg $flagsArg, Scope $scope): bool - { - // If it is a validation filter, the string will not be changed - if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) { - return false; - } - - // FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW, - // FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK - if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsArg, $scope) - || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsArg, $scope) - || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsArg, $scope); - } + $inputType = $scope->getType($functionCall->getArgs()[0]->value); + $filterType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null; + $flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; - return true; + return $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); } } diff --git a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php index 12df64cf17..5d320104a7 100644 --- a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php @@ -18,7 +18,7 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use function ltrim; -class FunctionExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class FunctionExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -29,7 +29,7 @@ public function isFunctionSupported( TypeSpecifierContext $context, ): bool { - return $functionReflection->getName() === 'function_exists' && isset($node->getArgs()[0]) && $context->truthy(); + return $functionReflection->getName() === 'function_exists' && isset($node->getArgs()[0]) && $context->true(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes @@ -42,7 +42,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n ]), new ConstantBooleanType(true), $context, - false, $scope, ); } @@ -51,7 +50,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $node->getArgs()[0]->value, new CallableType(), $context, - false, $scope, ); } diff --git a/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php b/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php index 77381c798c..612c66d786 100644 --- a/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php @@ -2,15 +2,16 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; -class GetCalledClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class GetCalledClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -20,9 +21,8 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $classContext = $scope->getClassReflection(); - if ($classContext !== null) { - return new ConstantStringType($classContext->getName(), true); + if ($scope->isInClass()) { + return $scope->getType(new ClassConstFetch(new Name('static'), 'class')); } return new ConstantBooleanType(false); } diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index e605618e66..bd1f73b5c2 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -9,19 +9,21 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function count; -class GetClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class GetClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -32,7 +34,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { $args = $functionCall->getArgs(); + if (count($args) === 0) { + if ($scope->isInTrait()) { + return new ClassStringType(); + } + if ($scope->isInClass()) { return new ConstantStringType($scope->getClassReflection()->getName(), true); } @@ -42,6 +49,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argType = $scope->getType($args[0]->value); + if ($scope->isInTrait() && TypeUtils::findThisType($argType) !== null) { + return new ClassStringType(); + } + return TypeTraverser::map( $argType, static function (Type $type, callable $traverse): Type { @@ -49,7 +60,12 @@ static function (Type $type, callable $traverse): Type { return $traverse($type); } - if ($type instanceof TemplateType && !$type instanceof TypeWithClassName) { + if ($type instanceof EnumCaseObjectType) { + return new GenericClassStringType(new ObjectType($type->getClassName())); + } + + $objectClassNames = $type->getObjectClassNames(); + if ($type instanceof TemplateType && $objectClassNames === []) { if ($type instanceof ObjectWithoutClassType) { return new GenericClassStringType($type); } @@ -65,7 +81,7 @@ static function (Type $type, callable $traverse): Type { ]); } elseif ($type instanceof StaticType) { return new GenericClassStringType($type->getStaticObjectType()); - } elseif ($type instanceof TypeWithClassName) { + } elseif ($objectClassNames !== []) { return new GenericClassStringType($type); } elseif ($type instanceof ObjectWithoutClassType) { return new ClassStringType(); diff --git a/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..6860694293 --- /dev/null +++ b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php @@ -0,0 +1,102 @@ +getName() === 'get_debug_type'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + if ($argType instanceof UnionType) { + return TypeCombinator::union(...array_map(static fn (Type $type) => self::resolveOneType($type), $argType->getTypes())); + } + return self::resolveOneType($argType); + } + + /** + * @see https://www.php.net/manual/en/function.get-debug-type.php#refsect1-function.get-debug-type-returnvalues + * @see https://github.com/php/php-src/commit/ef0e4478c51540510b67f7781ad240f5e0592ee4 + */ + private static function resolveOneType(Type $type): Type + { + if ($type->isNull()->yes()) { + return new ConstantStringType('null'); + } + if ($type->isBoolean()->yes()) { + return new ConstantStringType('bool'); + } + if ($type->isInteger()->yes()) { + return new ConstantStringType('int'); + } + if ($type->isFloat()->yes()) { + return new ConstantStringType('float'); + } + if ($type->isString()->yes()) { + return new ConstantStringType('string'); + } + if ($type->isArray()->yes()) { + return new ConstantStringType('array'); + } + + // "resources" type+state is skipped since we cannot infer the state + + if ($type->isObject()->yes()) { + $reflections = $type->getObjectClassReflections(); + $types = []; + foreach ($reflections as $reflection) { + // if the class is not final, the actual returned string might be of a child class + if ($reflection->isFinal() && !$reflection->isAnonymous()) { + $types[] = new ConstantStringType($reflection->getName()); + } + + if ($reflection->isAnonymous()) { // phpcs:ignore + $parentClass = $reflection->getParentClass(); + $implementedInterfaces = $reflection->getImmediateInterfaces(); + if ($parentClass !== null) { + $types[] = new ConstantStringType($parentClass->getName() . '@anonymous'); + } elseif ($implementedInterfaces !== []) { + $firstInterface = $implementedInterfaces[array_key_first($implementedInterfaces)]; + $types[] = new ConstantStringType($firstInterface->getName() . '@anonymous'); + } else { + $types[] = new ConstantStringType('class@anonymous'); + } + } + } + + switch (count($types)) { + case 0: + return new StringType(); + case 1: + return $types[0]; + default: + return TypeCombinator::union(...$types); + } + } + + return new StringType(); + } + +} diff --git a/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php b/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..35999b424b --- /dev/null +++ b/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php @@ -0,0 +1,46 @@ +getName() === 'get_defined_vars'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + if ($scope->canAnyVariableExist()) { + return new ArrayType( + new StringType(), + new MixedType(), + ); + } + + $typeBuilder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($scope->getDefinedVariables() as $variable) { + $typeBuilder->setOffsetValueType(new ConstantStringType($variable), $scope->getVariableType($variable), false); + } + + foreach ($scope->getMaybeDefinedVariables() as $variable) { + $typeBuilder->setOffsetValueType(new ConstantStringType($variable), $scope->getVariableType($variable), true); + } + + return $typeBuilder->getArray(); + } + +} diff --git a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php index 2ba8086d47..b03a68655d 100644 --- a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -19,7 +18,7 @@ use function array_map; use function count; -class GetParentClassDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class GetParentClassDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private ReflectionProvider $reflectionProvider) @@ -37,14 +36,11 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants(), - )->getReturnType(); if (count($functionCall->getArgs()) === 0) { if ($scope->isInTrait()) { - return $defaultReturnType; + return null; } if ($scope->isInClass()) { return $this->findParentClassType( @@ -57,20 +53,20 @@ public function getTypeFromFunctionCall( $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($scope->isInTrait() && TypeUtils::findThisType($argType) !== null) { - return $defaultReturnType; + return null; } - $constantStrings = TypeUtils::getConstantStrings($argType); + $constantStrings = $argType->getConstantStrings(); if (count($constantStrings) > 0) { return TypeCombinator::union(...array_map(fn (ConstantStringType $stringType): Type => $this->findParentClassNameType($stringType->getValue()), $constantStrings)); } - $classNames = TypeUtils::getDirectClassNames($argType); + $classNames = $argType->getObjectClassNames(); if (count($classNames) > 0) { return TypeCombinator::union(...array_map(fn (string $classNames): Type => $this->findParentClassNameType($classNames), $classNames)); } - return $defaultReturnType; + return null; } private function findParentClassNameType(string $className): Type @@ -82,7 +78,15 @@ private function findParentClassNameType(string $className): Type ]); } - return $this->findParentClassType($this->reflectionProvider->getClass($className)); + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->isInterface()) { + return new UnionType([ + new ClassStringType(), + new ConstantBooleanType(false), + ]); + } + + return $this->findParentClassType($classReflection); } private function findParentClassType( diff --git a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php index 92ff653718..8b6a6133cf 100644 --- a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; @@ -16,7 +15,7 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class GettimeofdayDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class GettimeofdayDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -44,8 +43,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return $floatType; diff --git a/src/Type/Php/GettypeFunctionReturnTypeExtension.php b/src/Type/Php/GettypeFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..875529ae47 --- /dev/null +++ b/src/Type/Php/GettypeFunctionReturnTypeExtension.php @@ -0,0 +1,90 @@ +getName() === 'gettype'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $valueType = $scope->getType($functionCall->getArgs()[0]->value); + + return TypeTraverser::map($valueType, static function (Type $valueType, callable $traverse): Type { + if ($valueType instanceof UnionType || $valueType instanceof IntersectionType) { + return $traverse($valueType); + } + + if ($valueType->isString()->yes()) { + return new ConstantStringType('string'); + } + if ($valueType->isArray()->yes()) { + return new ConstantStringType('array'); + } + + if ($valueType->isBoolean()->yes()) { + return new ConstantStringType('boolean'); + } + + $resource = new ResourceType(); + if ($resource->isSuperTypeOf($valueType)->yes()) { + return new UnionType([ + new ConstantStringType('resource'), + new ConstantStringType('resource (closed)'), + ]); + } + + if ($valueType->isInteger()->yes()) { + return new ConstantStringType('integer'); + } + + if ($valueType->isFloat()->yes()) { + // for historical reasons "double" is returned in case of a float, and not simply "float" + return new ConstantStringType('double'); + } + + if ($valueType->isNull()->yes()) { + return new ConstantStringType('NULL'); + } + + if ($valueType->isObject()->yes()) { + return new ConstantStringType('object'); + } + + return TypeCombinator::union( + new ConstantStringType('string'), + new ConstantStringType('array'), + new ConstantStringType('boolean'), + new ConstantStringType('resource'), + new ConstantStringType('resource (closed)'), + new ConstantStringType('integer'), + new ConstantStringType('double'), + new ConstantStringType('NULL'), + new ConstantStringType('object'), + new ConstantStringType('unknown type'), + ); + }); + } + +} diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index a1c6d2e06e..85ba080957 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -6,21 +6,22 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; 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 count; use function hash_algos; use function in_array; +use function is_bool; use function strtolower; final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -30,26 +31,32 @@ final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTyp 'hash' => [ 'cryptographic' => false, 'possiblyFalse' => false, + 'binary' => 2, ], 'hash_file' => [ 'cryptographic' => false, 'possiblyFalse' => true, + 'binary' => 2, ], 'hash_hkdf' => [ 'cryptographic' => true, 'possiblyFalse' => false, + 'binary' => true, ], 'hash_hmac' => [ 'cryptographic' => true, 'possiblyFalse' => false, + 'binary' => 3, ], 'hash_hmac_file' => [ 'cryptographic' => true, 'possiblyFalse' => true, + 'binary' => 3, ], 'hash_pbkdf2' => [ 'cryptographic' => true, 'possiblyFalse' => false, + 'binary' => 5, ], ]; @@ -86,37 +93,46 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return isset(self::SUPPORTED_FUNCTIONS[$name]); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!isset($functionCall->getArgs()[0])) { - return $defaultReturnType; + return null; } - $algorithmType = $scope->getType($functionCall->getArgs()[0]->value); - if ($algorithmType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($defaultReturnType); + $functionData = self::SUPPORTED_FUNCTIONS[strtolower($functionReflection->getName())]; + if (is_bool($functionData['binary'])) { + $binaryType = new ConstantBooleanType($functionData['binary']); + } elseif (isset($functionCall->getArgs()[$functionData['binary']])) { + $binaryType = $scope->getType($functionCall->getArgs()[$functionData['binary']]->value); + } else { + $binaryType = new ConstantBooleanType(false); + } + + $stringTypes = [ + new StringType(), + new AccessoryNonFalsyStringType(), + ]; + if ($binaryType->isFalse()->yes()) { + $stringTypes[] = new AccessoryLowercaseStringType(); } + $stringReturnType = new IntersectionType($stringTypes); - $constantAlgorithmTypes = TypeUtils::getConstantStrings($algorithmType); + $algorithmType = $scope->getType($functionCall->getArgs()[0]->value); + $constantAlgorithmTypes = $algorithmType->getConstantStrings(); + if (count($constantAlgorithmTypes) === 0) { + if ($functionData['possiblyFalse'] || !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + return TypeUtils::toBenevolentUnion(TypeCombinator::union($stringReturnType, new ConstantBooleanType(false))); + } - if ($constantAlgorithmTypes === []) { - return TypeUtils::toBenevolentUnion($defaultReturnType); + return $stringReturnType; } $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) { + function (ConstantStringType $type) use ($functionData, $stringReturnType, $invalidAlgorithmType) { $algorithm = strtolower($type->getValue()); if (!in_array($algorithm, $this->hashAlgorithms, true)) { return $invalidAlgorithmType; @@ -124,7 +140,7 @@ function (ConstantStringType $type) use ($functionData, $nonEmptyString, $invali if ($functionData['cryptographic'] && in_array($algorithm, self::NON_CRYPTOGRAPHIC_ALGORITHMS, true)) { return $invalidAlgorithmType; } - return $nonEmptyString; + return $stringReturnType; }, $constantAlgorithmTypes, ); diff --git a/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php b/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..12236c20cf --- /dev/null +++ b/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php @@ -0,0 +1,51 @@ +getName() === 'highlight_string'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $args = $functionCall->getArgs(); + if (count($args) < 2) { + if ($this->phpVersion->highlightStringDoesNotReturnFalse()) { + return new ConstantBooleanType(true); + } + + return new BooleanType(); + } + + $returnType = $scope->getType($args[1]->value); + if ($returnType->isTrue()->yes()) { + return new StringType(); + } + + if ($this->phpVersion->highlightStringDoesNotReturnFalse()) { + return new ConstantBooleanType(true); + } + + return new BooleanType(); + } + +} diff --git a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php index 05418cf926..fdbf783378 100644 --- a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php @@ -5,8 +5,8 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; @@ -16,7 +16,7 @@ use PHPStan\Type\TypeUtils; use function count; -class HrtimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class HrtimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -26,7 +26,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $arrayType = new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new IntegerType(), new IntegerType()], 2); + $arrayType = new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new IntegerType(), new IntegerType()], [2], [], TrinaryLogic::createYes()); $numberType = TypeUtils::toBenevolentUnion(TypeCombinator::union(new IntegerType(), new FloatType())); if (count($functionCall->getArgs()) < 1) { @@ -34,8 +34,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return $numberType; diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index 478eb9613b..15d1d86706 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -4,12 +4,16 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Internal\CombinationsHelper; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; @@ -19,7 +23,7 @@ use function implode; use function in_array; -class ImplodeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ImplodeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -56,23 +60,46 @@ public function getTypeFromFunctionCall( private function implode(Type $arrayType, Type $separatorType): Type { - if ($arrayType instanceof ConstantArrayType && $separatorType instanceof ConstantStringType) { - $constantType = $this->inferConstantType($arrayType, $separatorType); - if ($constantType !== null) { - return $constantType; + if (count($arrayType->getConstantArrays()) > 0 && count($separatorType->getConstantStrings()) > 0) { + $result = []; + foreach ($separatorType->getConstantStrings() as $separator) { + foreach ($arrayType->getConstantArrays() as $constantArray) { + $constantType = $this->inferConstantType($constantArray, $separator); + if ($constantType !== null) { + $result[] = $constantType; + continue; + } + + $result = []; + break 2; + } + } + + if (count($result) > 0) { + return TypeCombinator::union(...$result); } } $accessoryTypes = []; + $valueTypeAsString = $arrayType->getIterableValueType()->toString(); if ($arrayType->isIterableAtLeastOnce()->yes()) { - if ($arrayType->getIterableValueType()->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { + if ($valueTypeAsString->isNonFalsyString()->yes() || $separatorType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($valueTypeAsString->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { $accessoryTypes[] = new AccessoryNonEmptyStringType(); } } + // implode is one of the four functions that can produce literal strings as blessed by the original RFC: wiki.php.net/rfc/is_literal if ($arrayType->getIterableValueType()->isLiteralString()->yes() && $separatorType->isLiteralString()->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); } + if ($valueTypeAsString->isLowercaseString()->yes() && $separatorType->isLowercaseString()->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + if ($valueTypeAsString->isUppercaseString()->yes() && $separatorType->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); @@ -89,14 +116,28 @@ private function inferConstantType(ConstantArrayType $arrayType, ConstantStringT $valueTypes = $array->getValueTypes(); $arrayValues = []; + $combinationsCount = 1; foreach ($valueTypes as $valueType) { - if (!$valueType instanceof ConstantScalarType) { + $constScalars = $valueType->getConstantScalarValues(); + if (count($constScalars) === 0) { return null; } - $arrayValues[] = $valueType->getValue(); + $arrayValues[] = $constScalars; + $combinationsCount *= count($constScalars); } - $strings[] = new ConstantStringType(implode($separatorType->getValue(), $arrayValues)); + if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; + } + + $combinations = CombinationsHelper::combinations($arrayValues); + foreach ($combinations as $combination) { + $strings[] = new ConstantStringType(implode($separatorType->getValue(), $combination)); + } + } + + if (count($strings) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; } return TypeCombinator::union(...$strings); diff --git a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index 98576400ef..d83e9f78ad 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -2,20 +2,26 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\BinaryOp\Equal; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\ArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\MixedType; +use PHPStan\Type\TypeCombinator; use function count; use function strtolower; -class InArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class InArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -33,30 +39,133 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (count($node->getArgs()) < 3) { + $argsCount = count($node->getArgs()); + if ($argsCount < 2) { return new SpecifiedTypes(); } - $strictNodeType = $scope->getType($node->getArgs()[2]->value); - if (!(new ConstantBooleanType(true))->isSuperTypeOf($strictNodeType)->yes()) { - return new SpecifiedTypes([], []); + + $isStrictComparison = false; + if ($argsCount >= 3) { + $strictNodeType = $scope->getType($node->getArgs()[2]->value); + $isStrictComparison = $strictNodeType->isTrue()->yes(); + } + + $needleExpr = $node->getArgs()[0]->value; + $arrayExpr = $node->getArgs()[1]->value; + + $needleType = $scope->getType($needleExpr); + $arrayType = $scope->getType($arrayExpr); + $arrayValueType = $arrayType->getIterableValueType(); + + $isStrictComparison = $isStrictComparison + || $needleType->isEnum()->yes() + || $arrayValueType->isEnum()->yes() + || ($needleType->isString()->yes() && $arrayValueType->isString()->yes()) + || ($needleType->isInteger()->yes() && $arrayValueType->isInteger()->yes()) + || ($needleType->isFloat()->yes() && $arrayValueType->isFloat()->yes()) + || ($needleType->isBoolean()->yes() && $arrayValueType->isBoolean()->yes()); + + if ($arrayExpr instanceof Array_) { + $types = null; + foreach ($arrayExpr->items as $item) { + if ($item->unpack) { + $types = null; + break; + } + + if ($isStrictComparison) { + $itemTypes = $this->typeSpecifier->resolveIdentical(new Identical($needleExpr, $item->value), $scope, $context); + } else { + $itemTypes = $this->typeSpecifier->resolveEqual(new Equal($needleExpr, $item->value), $scope, $context); + } + + if ($types === null) { + $types = $itemTypes; + continue; + } + + $types = $context->true() ? $types->normalize($scope)->intersectWith($itemTypes->normalize($scope)) : $types->unionWith($itemTypes); + } + + if ($types !== null) { + return $types; + } } - $arrayValueType = $scope->getType($node->getArgs()[1]->value)->getIterableValueType(); + if (!$isStrictComparison) { + if ( + $context->true() + && $arrayType->isArray()->yes() + && $arrayType->getIterableValueType()->isSuperTypeOf($needleType)->yes() + ) { + return $this->typeSpecifier->create( + $node->getArgs()[1]->value, + TypeCombinator::intersect($arrayType, new NonEmptyArrayType()), + $context, + $scope, + ); + } + return new SpecifiedTypes(); + } + + $specifiedTypes = new SpecifiedTypes(); if ( - $context->truthy() - || count(TypeUtils::getConstantScalars($arrayValueType)) > 0 + $context->true() + || ( + $context->false() + && count($arrayValueType->getFiniteTypes()) > 0 + && count($needleType->getFiniteTypes()) > 0 + && $arrayType->isIterableAtLeastOnce()->yes() + ) ) { - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, + $specifiedTypes = $this->typeSpecifier->create( + $needleExpr, $arrayValueType, $context, - false, $scope, ); + if ($needleExpr instanceof AlwaysRememberedExpr) { + $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( + $needleExpr->getExpr(), + $arrayValueType, + $context, + $scope, + )); + } + } + + if ( + $context->true() + || ( + $context->false() + && count($needleType->getFiniteTypes()) === 1 + ) + ) { + if ($context->true()) { + $arrayValueType = TypeCombinator::union($arrayValueType, $needleType); + } else { + $arrayValueType = TypeCombinator::remove($arrayValueType, $needleType); + } + + $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( + $node->getArgs()[1]->value, + new ArrayType(new MixedType(), $arrayValueType), + TypeSpecifierContext::createTrue(), + $scope, + )); + } + + if ($context->true() && $arrayType->isArray()->yes()) { + $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( + $node->getArgs()[1]->value, + TypeCombinator::intersect($arrayType, new NonEmptyArrayType()), + $context, + $scope, + )); } - return new SpecifiedTypes([], []); + return $specifiedTypes; } } diff --git a/src/Type/Php/IniGetReturnTypeExtension.php b/src/Type/Php/IniGetReturnTypeExtension.php new file mode 100644 index 0000000000..15dc36a481 --- /dev/null +++ b/src/Type/Php/IniGetReturnTypeExtension.php @@ -0,0 +1,64 @@ +getName() === 'ini_get'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $numericString = TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ); + $types = [ + 'date.timezone' => new StringType(), + 'memory_limit' => new StringType(), + 'max_execution_time' => $numericString, + 'max_input_time' => $numericString, + 'default_socket_timeout' => $numericString, + 'precision' => $numericString, + ]; + + $argType = $scope->getType($args[0]->value); + $results = []; + foreach ($argType->getConstantStrings() as $constantString) { + if (!array_key_exists($constantString->getValue(), $types)) { + return null; + } + $results[] = $types[$constantString->getValue()]; + } + + if (count($results) > 0) { + return TypeCombinator::union(...$results); + } + + return null; + } + +} diff --git a/src/Type/Php/IntdivThrowTypeExtension.php b/src/Type/Php/IntdivThrowTypeExtension.php index 8b97f9265a..46920122a6 100644 --- a/src/Type/Php/IntdivThrowTypeExtension.php +++ b/src/Type/Php/IntdivThrowTypeExtension.php @@ -7,16 +7,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionThrowTypeExtension; -use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use const PHP_INT_MIN; -class IntdivThrowTypeExtension implements DynamicFunctionThrowTypeExtension +final class IntdivThrowTypeExtension implements DynamicFunctionThrowTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -30,39 +28,19 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect return $functionReflection->getThrowType(); } - $containsMin = false; - $valueType = $scope->getType($funcCall->getArgs()[0]->value); - foreach (TypeUtils::getConstantScalars($valueType) as $constantScalarType) { - if ($constantScalarType->getValue() === PHP_INT_MIN) { - $containsMin = true; - } - - $valueType = TypeCombinator::remove($valueType, $constantScalarType); - } - - if (!$valueType instanceof NeverType) { - $containsMin = true; - } + $valueType = $scope->getType($funcCall->getArgs()[0]->value)->toInteger(); + $containsMin = $valueType->isSuperTypeOf(new ConstantIntegerType(PHP_INT_MIN)); - $divisionByZero = false; - $divisorType = $scope->getType($funcCall->getArgs()[1]->value); - foreach (TypeUtils::getConstantScalars($divisorType) as $constantScalarType) { - if ($containsMin && $constantScalarType->getValue() === -1) { + $divisorType = $scope->getType($funcCall->getArgs()[1]->value)->toInteger(); + if (!$containsMin->no()) { + $divisionByMinusOne = $divisorType->isSuperTypeOf(new ConstantIntegerType(-1)); + if (!$divisionByMinusOne->no()) { return new ObjectType(ArithmeticError::class); } - - if ($constantScalarType->getValue() === 0) { - $divisionByZero = true; - } - - $divisorType = TypeCombinator::remove($divisorType, $constantScalarType); - } - - if (!$divisorType instanceof NeverType) { - return new ObjectType($containsMin ? ArithmeticError::class : DivisionByZeroError::class); } - if ($divisionByZero) { + $divisionByZero = $divisorType->isSuperTypeOf(new ConstantIntegerType(0)); + if (!$divisionByZero->no()) { return new ObjectType(DivisionByZeroError::class); } diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php index 2e03074b37..bc4591b9a8 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -15,7 +15,7 @@ use function count; use function strtolower; -class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -37,20 +37,27 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if (count($node->getArgs()) < 2) { return new SpecifiedTypes(); } - $objectOrClassType = $scope->getType($node->getArgs()[0]->value); $classType = $scope->getType($node->getArgs()[1]->value); + + if (!$classType instanceof ConstantStringType && !$context->true()) { + return new SpecifiedTypes([], []); + } + + $objectOrClassType = $scope->getType($node->getArgs()[0]->value); $allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(false); $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); - if (!$classType instanceof ConstantStringType && !$context->truthy()) { + $resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true); + + // prevent false-positives in IsAFunctionTypeSpecifyingHelper + if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { return new SpecifiedTypes([], []); } return $this->typeSpecifier->create( $node->getArgs()[0]->value, - $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true), + $resultType, $context, - false, $scope, ); } diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php index c608156f84..d1f4a1bd82 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -12,8 +12,9 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; +use function array_unique; +use function array_values; final class IsAFunctionTypeSpecifyingHelper { @@ -25,16 +26,22 @@ public function determineType( bool $allowSameClass, ): Type { - $objectOrClassTypeClassName = $this->determineClassNameFromObjectOrClassType($objectOrClassType, $allowString); + $objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames(); + if ($allowString) { + foreach ($objectOrClassType->getConstantStrings() as $constantString) { + $objectOrClassTypeClassNames[] = $constantString->getValue(); + } + $objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames)); + } return TypeTraverser::map( $classType, - static function (Type $type, callable $traverse) use ($objectOrClassTypeClassName, $allowString, $allowSameClass): Type { + static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNames, $allowString, $allowSameClass): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } if ($type instanceof ConstantStringType) { - if (!$allowSameClass && $type->getValue() === $objectOrClassTypeClassName) { + if (!$allowSameClass && $objectOrClassTypeClassNames === [$type->getValue()]) { return new NeverType(); } if ($allowString) { @@ -68,17 +75,4 @@ static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNam ); } - private function determineClassNameFromObjectOrClassType(Type $type, bool $allowString): ?string - { - if ($type instanceof TypeWithClassName) { - return $type->getClassName(); - } - - if ($allowString && $type instanceof ConstantStringType) { - return $type->getValue(); - } - - return null; - } - } diff --git a/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php index 626e5de0eb..e31033185e 100644 --- a/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php @@ -15,7 +15,7 @@ use PHPStan\Type\MixedType; use function strtolower; -class IsArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class IsArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -35,7 +35,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n throw new ShouldNotHappenException(); } - return $this->typeSpecifier->create($node->getArgs()[0]->value, new ArrayType(new MixedType(), new MixedType()), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new ArrayType(new MixedType(true), new MixedType(true)), $context, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 44a9b8b088..0000000000 --- a/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,45 +0,0 @@ -getName()) === 'is_bool' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new BooleanType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php index fc832c4737..42c5fe8505 100644 --- a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php @@ -14,12 +14,11 @@ 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 +final class IsCallableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -49,13 +48,9 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ( $value instanceof Array_ && count($value->items) === 2 - && $valueType instanceof ConstantArrayType + && $valueType->isConstantArray()->yes() && !$valueType->isCallable()->no() ) { - if ($value->items[0] === null || $value->items[1] === null) { - throw new ShouldNotHappenException(); - } - $functionCall = new FuncCall(new Name('method_exists'), [ new Arg($value->items[0]->value), new Arg($value->items[1]->value), @@ -63,7 +58,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return $this->methodExistsExtension->specifyTypes($functionReflection, $functionCall, $scope, $context); } - return $this->typeSpecifier->create($value, new CallableType(), $context, false, $scope); + return $this->typeSpecifier->create($value, new CallableType(), $context, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php deleted file mode 100644 index c05579be5a..0000000000 --- a/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,59 +0,0 @@ -getName()) === 'is_countable' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - new UnionType([ - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(Countable::class), - ]), - $context, - false, - $scope, - ); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php deleted file mode 100644 index de8c3f6cbd..0000000000 --- a/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,50 +0,0 @@ -getName()), [ - 'is_float', - 'is_double', - 'is_real', - ], true) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new FloatType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php deleted file mode 100644 index a5504cf663..0000000000 --- a/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,50 +0,0 @@ -getName()), [ - 'is_int', - 'is_integer', - 'is_long', - ], true) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new IntegerType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php index 470d196894..a8404ef99f 100644 --- a/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php @@ -15,7 +15,7 @@ use PHPStan\Type\MixedType; use function strtolower; -class IsIterableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class IsIterableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -36,7 +36,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes(); } - return $this->typeSpecifier->create($node->getArgs()[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, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 403ee6c337..0000000000 --- a/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,46 +0,0 @@ -getName()) === 'is_null' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new NullType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 3b16c1b51b..0000000000 --- a/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,61 +0,0 @@ -getName() === 'is_numeric' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - $numericTypes = [ - new IntegerType(), - new FloatType(), - ]; - - if ($context->truthy()) { - $numericTypes[] = new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new UnionType($numericTypes), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 33bd282b65..0000000000 --- a/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,46 +0,0 @@ -getName()) === 'is_object' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectWithoutClassType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 392fb1159b..0000000000 --- a/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,46 +0,0 @@ -getName()) === 'is_resource' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new ResourceType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 33ce803d01..0000000000 --- a/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,54 +0,0 @@ -getName() === 'is_scalar' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new UnionType([ - new StringType(), - new IntegerType(), - new FloatType(), - new BooleanType(), - ]), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 7e307545b6..0000000000 --- a/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,46 +0,0 @@ -getName()) === 'is_string' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new StringType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index 2740401e4f..c62a11b150 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -10,12 +10,12 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use PHPStan\Type\Generic\GenericClassStringType; use function count; use function strtolower; -class IsSubclassOfFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class IsSubclassOfFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -34,23 +34,31 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (count($node->getArgs()) < 2) { + if (!$context->true() || 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(true); $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); - if (!$classType instanceof ConstantStringType && !$context->truthy()) { + // prevent false-positives in IsAFunctionTypeSpecifyingHelper + if ($objectOrClassType instanceof GenericClassStringType && $classType instanceof GenericClassStringType) { + return new SpecifiedTypes([], []); + } + + $resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false); + + // prevent false-positives in IsAFunctionTypeSpecifyingHelper + if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { return new SpecifiedTypes([], []); } return $this->typeSpecifier->create( $node->getArgs()[0]->value, - $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false), + $resultType, $context, - false, $scope, ); } diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php index fe8be93631..618fdb1fdc 100644 --- a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -5,12 +5,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function strtolower; final class IteratorToArrayFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -21,25 +23,33 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return strtolower($functionReflection->getName()) === 'iterator_to_array'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $arguments = $functionCall->getArgs(); if ($arguments === []) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $traversableType = $scope->getType($arguments[0]->value); - $arrayKeyType = $traversableType->getIterableKeyType(); if (isset($arguments[1])) { $preserveKeysType = $scope->getType($arguments[1]->value); - if ($preserveKeysType instanceof ConstantBooleanType && !$preserveKeysType->getValue()) { - $arrayKeyType = new IntegerType(); + if ($preserveKeysType->isFalse()->yes()) { + return TypeCombinator::intersect(new ArrayType( + new IntegerType(), + $traversableType->getIterableValueType(), + ), new AccessoryArrayListType()); } } + $arrayKeyType = $traversableType->getIterableKeyType()->toArrayKey(); + + if ($arrayKeyType instanceof ErrorType) { + return new NeverType(true); + } + return new ArrayType( $arrayKeyType, $traversableType->getIterableValueType(), diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index 15de31eda3..65d82ccd7b 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -2,23 +2,25 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\BitwiseOr; -use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use function in_array; +use function is_bool; +use function json_decode; -class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var array */ @@ -27,7 +29,10 @@ class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionRetur 'json_decode' => 3, ]; - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private BitwiseFlagHelper $bitwiseFlagAnalyser, + ) { } @@ -35,14 +40,11 @@ public function isFunctionSupported( FunctionReflection $functionReflection, ): bool { - return $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array( - $functionReflection->getName(), - [ - 'json_encode', - 'json_decode', - ], - true, - ); + if ($functionReflection->getName() === 'json_decode') { + return true; + } + + return $functionReflection->getName() === 'json_encode' && $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null); } public function getTypeFromFunctionCall( @@ -52,51 +54,78 @@ public function getTypeFromFunctionCall( ): Type { $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + if ($functionReflection->getName() === 'json_decode') { + $defaultReturnType = $this->narrowTypeForJsonDecode($functionCall, $scope, $defaultReturnType); + } + if (!isset($functionCall->getArgs()[$argumentPosition])) { return $defaultReturnType; } $optionsExpr = $functionCall->getArgs()[$argumentPosition]->value; - $constrictedReturnType = TypeCombinator::remove($defaultReturnType, new ConstantBooleanType(false)); - if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) { - return $constrictedReturnType; + if ($functionReflection->getName() === 'json_encode' && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($optionsExpr, $scope, 'JSON_THROW_ON_ERROR')->yes()) { + return TypeCombinator::remove($defaultReturnType, new ConstantBooleanType(false)); } - $valueType = $scope->getType($optionsExpr); - if (!$valueType instanceof ConstantIntegerType) { - return $defaultReturnType; + return $defaultReturnType; + } + + private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type $fallbackType): Type + { + $args = $funcCall->getArgs(); + $isForceArray = $this->isForceArray($funcCall, $scope); + if (!isset($args[0])) { + return $fallbackType; } - $value = $valueType->getValue(); - $throwOnErrorType = $this->reflectionProvider->getConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null)->getValueType(); - if (!$throwOnErrorType instanceof ConstantIntegerType) { - return $defaultReturnType; + $firstValueType = $scope->getType($args[0]->value); + if ($firstValueType instanceof ConstantStringType) { + return $this->resolveConstantStringType($firstValueType, $isForceArray); } - $throwOnErrorValue = $throwOnErrorType->getValue(); - if (($value & $throwOnErrorValue) !== $throwOnErrorValue) { - return $defaultReturnType; + if ($isForceArray) { + return TypeCombinator::remove($fallbackType, new ObjectWithoutClassType()); } - return $constrictedReturnType; + return $fallbackType; } - private function isBitwiseOrWithJsonThrowOnError(Expr $expr, Scope $scope): bool + /** + * Is "json_decode(..., true)"? + */ + private function isForceArray(FuncCall $funcCall, Scope $scope): bool { - if ($expr instanceof ConstFetch) { - $constant = $this->reflectionProvider->resolveConstantName($expr->name, $scope); - if ($constant === 'JSON_THROW_ON_ERROR') { - return true; - } + $args = $funcCall->getArgs(); + if (!isset($args[1])) { + return false; + } + + $secondArgType = $scope->getType($args[1]->value); + $secondArgValue = $secondArgType instanceof ConstantScalarType ? $secondArgType->getValue() : null; + + if (is_bool($secondArgValue)) { + return $secondArgValue; } - if (!$expr instanceof BitwiseOr) { + if ($secondArgValue !== null || !isset($args[3])) { return false; } - return $this->isBitwiseOrWithJsonThrowOnError($expr->left, $scope) || - $this->isBitwiseOrWithJsonThrowOnError($expr->right, $scope); + // depends on used constants, @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array + return $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($args[3]->value, $scope, 'JSON_OBJECT_AS_ARRAY')->yes(); + } + + private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type + { + $decodedValue = json_decode($constantStringType->getValue(), $isForceArray); + + return ConstantTypeHelper::getTypeFromValue($decodedValue); } } diff --git a/src/Type/Php/JsonThrowTypeExtension.php b/src/Type/Php/JsonThrowTypeExtension.php index 21c4cc6920..68e7855bb7 100644 --- a/src/Type/Php/JsonThrowTypeExtension.php +++ b/src/Type/Php/JsonThrowTypeExtension.php @@ -2,30 +2,29 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\BitwiseOr; -use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use function in_array; -class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension +final class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - /** @var array */ - private array $argumentPositions = [ + private const ARGUMENTS_POSITIONS = [ 'json_encode' => 1, 'json_decode' => 3, ]; - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private BitwiseFlagHelper $bitwiseFlagAnalyser, + ) { } @@ -33,14 +32,14 @@ public function isFunctionSupported( FunctionReflection $functionReflection, ): bool { - return $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array( + return in_array( $functionReflection->getName(), [ 'json_encode', 'json_decode', ], true, - ); + ) && $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null); } public function getThrowTypeFromFunctionCall( @@ -49,50 +48,17 @@ public function getThrowTypeFromFunctionCall( Scope $scope, ): ?Type { - $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; + $argumentPosition = self::ARGUMENTS_POSITIONS[$functionReflection->getName()]; if (!isset($functionCall->getArgs()[$argumentPosition])) { return null; } $optionsExpr = $functionCall->getArgs()[$argumentPosition]->value; - if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) { + if (!$this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($optionsExpr, $scope, 'JSON_THROW_ON_ERROR')->no()) { return new ObjectType('JsonException'); } - $valueType = $scope->getType($optionsExpr); - if (!$valueType instanceof ConstantIntegerType) { - return null; - } - - $value = $valueType->getValue(); - $throwOnErrorType = $this->reflectionProvider->getConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null)->getValueType(); - if (!$throwOnErrorType instanceof ConstantIntegerType) { - return null; - } - - $throwOnErrorValue = $throwOnErrorType->getValue(); - if (($value & $throwOnErrorValue) !== $throwOnErrorValue) { - return null; - } - - return new ObjectType('JsonException'); - } - - private function isBitwiseOrWithJsonThrowOnError(Expr $expr, Scope $scope): bool - { - if ($expr instanceof ConstFetch) { - $constant = $this->reflectionProvider->resolveConstantName($expr->name, $scope); - if ($constant === 'JSON_THROW_ON_ERROR') { - return true; - } - } - - if (!$expr instanceof BitwiseOr) { - return false; - } - - return $this->isBitwiseOrWithJsonThrowOnError($expr->left, $scope) || - $this->isBitwiseOrWithJsonThrowOnError($expr->right, $scope); + return null; } } diff --git a/src/Type/Php/LtrimFunctionReturnTypeExtension.php b/src/Type/Php/LtrimFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..284be58a82 --- /dev/null +++ b/src/Type/Php/LtrimFunctionReturnTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'ltrim'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) !== 2) { + return null; + } + + $string = $scope->getType($functionCall->getArgs()[0]->value); + $trimChars = $scope->getType($functionCall->getArgs()[1]->value); + + if ($trimChars instanceof ConstantStringType && $trimChars->getValue() === '\\' && $string->isClassString()->yes()) { + if ($string instanceof ConstantStringType) { + return new ConstantStringType(ltrim($string->getValue(), $trimChars->getValue()), true); + } + + return new ClassStringType(); + } + + return null; + } + +} diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index 7b84444465..dd46ed4c2a 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -5,15 +5,13 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; -class MbConvertEncodingFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class MbConvertEncodingFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -25,16 +23,15 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if (!isset($functionCall->getArgs()[0])) { - return $defaultReturnType; + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isString = (new StringType())->isSuperTypeOf($argType); - $isArray = (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($argType); + $isString = $argType->isString(); + $isArray = $argType->isArray(); $compare = $isString->compareTo($isArray); if ($compare === $isString) { return new StringType(); @@ -42,7 +39,7 @@ public function getTypeFromFunctionCall( return new ArrayType(new IntegerType(), new StringType()); } - return $defaultReturnType; + return null; } } diff --git a/src/Type/Php/MbFunctionsReturnTypeExtension.php b/src/Type/Php/MbFunctionsReturnTypeExtension.php index 7de6a457f2..dc27fe349b 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtension.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtension.php @@ -7,7 +7,6 @@ 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; @@ -16,24 +15,16 @@ 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 DynamicFunctionReturnTypeExtension +final class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var string[] */ - private array $supportedEncodings; + use MbFunctionsReturnTypeExtensionTrait; /** @var int[] */ private array $encodingPositionMap = [ @@ -41,24 +32,12 @@ class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtensi 'mb_regex_encoding' => 1, 'mb_internal_encoding' => 1, 'mb_encoding_aliases' => 1, - 'mb_strlen' => 2, 'mb_chr' => 2, 'mb_ord' => 2, ]; 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 ShouldNotHappenException(); - } - $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); - } - } - $this->supportedEncodings = array_map('strtoupper', $supportedEncodings); } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -66,21 +45,20 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return array_key_exists($functionReflection->getName(), $this->encodingPositionMap); } - private function isSupportedEncoding(string $encoding): bool - { - return in_array(strtoupper($encoding), $this->supportedEncodings, true); - } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $returnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); $positionEncodingParam = $this->encodingPositionMap[$functionReflection->getName()]; if (count($functionCall->getArgs()) < $positionEncodingParam) { return TypeCombinator::remove($returnType, new BooleanType()); } - $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[$positionEncodingParam - 1]->value)); + $strings = $scope->getType($functionCall->getArgs()[$positionEncodingParam - 1]->value)->getConstantStrings(); $results = array_unique(array_map(fn (ConstantStringType $encoding): bool => $this->isSupportedEncoding($encoding->getValue()), $strings)); if ($returnType->equals(new UnionType([new StringType(), new BooleanType()]))) { diff --git a/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php b/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php new file mode 100644 index 0000000000..64036c984b --- /dev/null +++ b/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php @@ -0,0 +1,57 @@ +getSupportedEncodings(), true); + } + + /** @return string[] */ + private function getSupportedEncodings(): array + { + if (!is_null($this->supportedEncodings)) { + return $this->supportedEncodings; + } + + $supportedEncodings = []; + if (function_exists('mb_list_encodings')) { + foreach (mb_list_encodings() as $encoding) { + $aliases = @mb_encoding_aliases($encoding); + if ($aliases === false) { + throw new ShouldNotHappenException(); + } + $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); + } + } + $this->supportedEncodings = array_map('strtoupper', $supportedEncodings); + + // PHP 7.3 and 7.4 claims 'pass' and its alias 'none' to be supported, but actually 'pass' was removed in 7.3 + if (!$this->phpVersion->supportsPassNoneEncodings()) { + $this->supportedEncodings = array_filter( + $this->supportedEncodings, + static fn (string $enc) => !in_array($enc, ['PASS', 'NONE'], true), + ); + } + + return $this->supportedEncodings; + } + +} diff --git a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..8f6fd25819 --- /dev/null +++ b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php @@ -0,0 +1,151 @@ +getName() === 'mb_strlen'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + return null; + } + + $encodings = []; + + if (count($functionCall->getArgs()) === 1) { + // there is a chance to get an unsupported encoding 'pass' or 'none' here on PHP 7.3-7.4 + $encodings = [mb_internal_encoding()]; + } elseif (count($functionCall->getArgs()) === 2) { // custom encoding is specified + $encodings = array_map( + static fn (ConstantStringType $t) => $t->getValue(), + $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(), + ); + } + + if (count($encodings) > 0) { + for ($i = 0; $i < count($encodings); $i++) { + if ($this->isSupportedEncoding($encodings[$i])) { + continue; + } + $encodings[$i] = self::UNSUPPORTED_ENCODING; + } + + $encodings = array_unique($encodings); + + if (in_array(self::UNSUPPORTED_ENCODING, $encodings, true) && count($encodings) === 1) { + if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) { + return new NeverType(); + } + return new ConstantBooleanType(false); + } + } else { // if there aren't encoding constants, use all available encodings + $encodings = array_merge($this->getSupportedEncodings(), [self::UNSUPPORTED_ENCODING]); + } + + $argType = $scope->getType($args[0]->value); + $constantScalars = $argType->getConstantScalarValues(); + + $lengths = []; + foreach ($constantScalars as $constantScalar) { + $stringScalar = (string) $constantScalar; + + foreach ($encodings as $encoding) { + if (!$this->isSupportedEncoding($encoding)) { + continue; + } + + $length = @mb_strlen($stringScalar, $encoding); + if ($length === false) { + throw new ShouldNotHappenException(sprintf('Got false on a supported encoding %s and value %s', $encoding, var_export($stringScalar, true))); + } + $lengths[] = $length; + } + } + + $isNonEmpty = $argType->isNonEmptyString(); + $numeric = TypeCombinator::union(new IntegerType(), new FloatType()); + if (count($lengths) > 0) { + $lengths = array_unique($lengths); + sort($lengths); + if ($lengths === range(min($lengths), max($lengths))) { + $range = IntegerRangeType::fromInterval(min($lengths), max($lengths)); + } else { + $range = TypeCombinator::union(...array_map(static fn ($l) => new ConstantIntegerType($l), $lengths)); + } + } elseif ($argType->isBoolean()->yes()) { + $range = IntegerRangeType::fromInterval(0, 1); + } elseif ( + $isNonEmpty->yes() + || $numeric->isSuperTypeOf($argType)->yes() + || TypeCombinator::remove($argType, $numeric)->isNonEmptyString()->yes() + ) { + $range = IntegerRangeType::fromInterval(1, null); + } elseif ($argType->isString()->yes() && $isNonEmpty->no()) { + $range = new ConstantIntegerType(0); + } else { + $range = TypeCombinator::remove( + ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(), + new ConstantBooleanType(false), + ); + } + + if (!$this->phpVersion->throwsOnInvalidMbStringEncoding() && in_array(self::UNSUPPORTED_ENCODING, $encodings, true)) { + return TypeCombinator::union($range, new ConstantBooleanType(false)); + } + return $range; + } + +} diff --git a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php index a379b602a9..a782db6686 100644 --- a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php +++ b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php @@ -12,16 +12,13 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function in_array; use function strtolower; -class MbSubstituteCharacterDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class MbSubstituteCharacterDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) @@ -57,9 +54,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isString = (new StringType())->isSuperTypeOf($argType); - $isNull = (new NullType())->isSuperTypeOf($argType); - $isInteger = (new IntegerType())->isSuperTypeOf($argType); + $isString = $argType->isString(); + $isNull = $argType->isNull(); + $isInteger = $argType->isInteger(); if ($isString->no() && $isNull->no() && $isInteger->no()) { if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { @@ -106,7 +103,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($argType instanceof ConstantStringType) { $value = strtolower($argType->getValue()); - if ($value === 'none' || $value === 'long' || $value === 'entity') { + if (in_array($value, ['none', 'long', 'entity'], true)) { return new ConstantBooleanType(true); } diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index d11b1af70b..751a69a41a 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -11,16 +12,15 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\ClassStringType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; -use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StringType; use PHPStan\Type\UnionType; use function count; -class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -37,7 +37,7 @@ public function isFunctionSupported( ): bool { return $functionReflection->getName() === 'method_exists' - && $context->truthy() + && $context->true() && count($node->getArgs()) >= 2; } @@ -48,15 +48,30 @@ public function specifyTypes( TypeSpecifierContext $context, ): SpecifiedTypes { + $methodNameType = $scope->getType($node->getArgs()[1]->value); + if (!$methodNameType instanceof ConstantStringType) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()), + new ConstantBooleanType(true), + $context, + $scope, + ); + } + $objectType = $scope->getType($node->getArgs()[0]->value); - if (!$objectType instanceof ObjectType) { - if ((new StringType())->isSuperTypeOf($objectType)->yes()) { - return new SpecifiedTypes([], []); + if ($objectType->isString()->yes()) { + if ($objectType->isClassString()->yes()) { + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + new IntersectionType([ + $objectType, + new HasMethodType($methodNameType->getValue()), + ]), + $context, + $scope, + ); } - } - $methodNameType = $scope->getType($node->getArgs()[1]->value); - if (!$methodNameType instanceof ConstantStringType) { return new SpecifiedTypes([], []); } @@ -70,7 +85,6 @@ public function specifyTypes( new ClassStringType(), ]), $context, - false, $scope, ); } diff --git a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php index de55a206d8..f0cc1561fb 100644 --- a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BenevolentUnionType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\MixedType; @@ -15,7 +14,7 @@ use PHPStan\Type\UnionType; use function count; -class MicrotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class MicrotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -30,8 +29,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return new FloatType(); diff --git a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php index 4c346d3459..9faa3563c7 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -6,62 +6,46 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Ternary; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\ConstantType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use function count; +use function in_array; -class MinMaxFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class MinMaxFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var string[] */ - private array $functionNames = [ - 'min' => '', - 'max' => '', - ]; + public function __construct( + private PhpVersion $phpVersion, + ) + { + } public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return isset($this->functionNames[$functionReflection->getName()]); + return in_array($functionReflection->getName(), ['min', 'max'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } if (count($functionCall->getArgs()) === 1) { $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($argType->isArray()->yes()) { - $isIterable = $argType->isIterableAtLeastOnce(); - if ($isIterable->no()) { - return new ConstantBooleanType(false); - } - $iterableValueType = $argType->getIterableValueType(); - $argumentTypes = []; - if (!$isIterable->yes()) { - $argumentTypes[] = new ConstantBooleanType(false); - } - if ($iterableValueType instanceof UnionType) { - foreach ($iterableValueType->getTypes() as $innerType) { - $argumentTypes[] = $innerType; - } - } else { - $argumentTypes[] = $iterableValueType; - } - - return $this->processType( + return $this->processArrayType( $functionReflection->getName(), - $argumentTypes, + $argType, ); } @@ -77,15 +61,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argType1 = $scope->getType($args[1]->value); if ($argType0->isArray()->no() && $argType1->isArray()->no()) { + $comparisonExpr = new Smaller( + new AlwaysRememberedExpr($args[0]->value, $argType0, $scope->getNativeType($args[0]->value)), + new AlwaysRememberedExpr($args[1]->value, $argType1, $scope->getNativeType($args[1]->value)), + ); + if ($functionName === 'min') { return $scope->getType(new Ternary( - new Smaller($args[0]->value, $args[1]->value), + $comparisonExpr, $args[0]->value, $args[1]->value, )); } elseif ($functionName === 'max') { return $scope->getType(new Ternary( - new Smaller($args[0]->value, $args[1]->value), + $comparisonExpr, $args[1]->value, $args[0]->value, )); @@ -117,6 +106,47 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } + private function processArrayType(string $functionName, Type $argType): Type + { + $constArrayTypes = $argType->getConstantArrays(); + if (count($constArrayTypes) > 0) { + $resultTypes = []; + foreach ($constArrayTypes as $constArrayType) { + $isIterable = $constArrayType->isIterableAtLeastOnce(); + if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $resultTypes[] = new ConstantBooleanType(false); + continue; + } + $argumentTypes = []; + if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $argumentTypes[] = new ConstantBooleanType(false); + } + + foreach ($constArrayType->getValueTypes() as $innerType) { + $argumentTypes[] = $innerType; + } + + $resultTypes[] = $this->processType($functionName, $argumentTypes); + } + + return TypeCombinator::union(...$resultTypes); + } + + $isIterable = $argType->isIterableAtLeastOnce(); + if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new ConstantBooleanType(false); + } + $iterableValueType = $argType->getIterableValueType(); + $argumentTypes = []; + if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $argumentTypes[] = new ConstantBooleanType(false); + } + + $argumentTypes[] = $iterableValueType; + + return $this->processType($functionName, $argumentTypes); + } + /** * @param Type[] $types */ @@ -127,16 +157,16 @@ private function processType( { $resultType = null; foreach ($types as $type) { - if (!$type instanceof ConstantType) { - return TypeCombinator::union(...$types); - } - if ($resultType === null) { $resultType = $type; continue; } $compareResult = $this->compareTypes($resultType, $type); + if ($compareResult === null) { + return TypeCombinator::union(...$types); + } + if ($functionName === 'min') { if ($compareResult === $type) { $resultType = $type; @@ -161,15 +191,15 @@ private function compareTypes( ): ?Type { if ( - $firstType instanceof ConstantArrayType - && $secondType instanceof ConstantScalarType + $firstType->isArray()->yes() + && $secondType->isConstantScalarValue()->yes() ) { return $secondType; } if ( - $firstType instanceof ConstantScalarType - && $secondType instanceof ConstantArrayType + $firstType->isConstantScalarValue()->yes() + && $secondType->isArray()->yes() ) { return $firstType; } @@ -178,9 +208,9 @@ private function compareTypes( $firstType instanceof ConstantArrayType && $secondType instanceof ConstantArrayType ) { - if ($secondType->count() < $firstType->count()) { + if ($secondType->getArraySize() < $firstType->getArraySize()) { return $secondType; - } elseif ($firstType->count() < $secondType->count()) { + } elseif ($firstType->getArraySize() < $secondType->getArraySize()) { return $firstType; } diff --git a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php index 94055e1499..893627aae2 100644 --- a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php +++ b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php @@ -2,19 +2,22 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use function count; use function in_array; +use const ENT_SUBSTITUTE; -class NonEmptyStringFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class NonEmptyStringFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,13 +27,6 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 'addcslashes', 'escapeshellarg', 'escapeshellcmd', - 'strtoupper', - 'strtolower', - 'mb_strtoupper', - 'mb_strtolower', - 'lcfirst', - 'ucfirst', - 'ucwords', 'htmlspecialchars', 'htmlentities', 'urlencode', @@ -38,7 +34,6 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 'preg_quote', 'rawurlencode', 'rawurldecode', - 'vsprintf', ], true); } @@ -46,14 +41,29 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; + } + + if (in_array($functionReflection->getName(), [ + 'htmlspecialchars', + 'htmlentities', + ], true)) { + if (!$this->isSubstituteFlagSet($args, $scope)) { + return new StringType(); + } } $argType = $scope->getType($args[0]->value); + if ($argType->isNonFalsyString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } if ($argType->isNonEmptyString()->yes()) { return new IntersectionType([ new StringType(), @@ -64,4 +74,22 @@ public function getTypeFromFunctionCall( return new StringType(); } + /** + * @param Arg[] $args + */ + private function isSubstituteFlagSet( + array $args, + Scope $scope, + ): bool + { + if (!isset($args[1])) { + return true; + } + $flagsType = $scope->getType($args[1]->value); + if (!$flagsType instanceof ConstantIntegerType) { + return false; + } + return (bool) ($flagsType->getValue() & ENT_SUBSTITUTE); + } + } diff --git a/src/Type/Php/OpenSslEncryptParameterOutTypeExtension.php b/src/Type/Php/OpenSslEncryptParameterOutTypeExtension.php new file mode 100644 index 0000000000..5d24f86f52 --- /dev/null +++ b/src/Type/Php/OpenSslEncryptParameterOutTypeExtension.php @@ -0,0 +1,70 @@ +getName() === 'openssl_encrypt' && $parameter->getName() === 'tag'; + } + + public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $funcCall->getArgs(); + $cipherArg = $args[1] ?? null; + + if ($cipherArg === null) { + return null; + } + + $tagTypes = []; + + foreach ($scope->getType($cipherArg->value)->getConstantStrings() as $cipherType) { + $cipher = strtolower($cipherType->getValue()); + $mode = substr($cipher, -3); + + if (!in_array($cipher, openssl_get_cipher_methods(), true)) { + $tagTypes[] = new NullType(); + continue; + } + + if (in_array($mode, ['gcm', 'ccm'], true)) { + $tagTypes[] = TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ); + + continue; + } + + $tagTypes[] = new NullType(); + } + + if ($tagTypes === []) { + return TypeCombinator::addNull(TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + )); + } + + return TypeCombinator::union(...$tagTypes); + } + +} diff --git a/src/Type/Php/ParseStrParameterOutTypeExtension.php b/src/Type/Php/ParseStrParameterOutTypeExtension.php new file mode 100644 index 0000000000..caabc319bf --- /dev/null +++ b/src/Type/Php/ParseStrParameterOutTypeExtension.php @@ -0,0 +1,60 @@ +getName()), ['parse_str', 'mb_parse_str'], true) + && $parameter->getName() === 'result'; + } + + public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $funcCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $stringType = $scope->getType($args[0]->value); + $accessory = []; + if ($stringType->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + $valueType = new IntersectionType($accessory); + } else { + $valueType = new StringType(); + } + + return new ArrayType( + new UnionType([new StringType(), new IntegerType()]), + new UnionType([new ArrayType(new MixedType(), new MixedType(true)), $valueType]), + ); + } + +} diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index 307ddb4549..19d6c20b26 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -5,15 +5,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerType; +use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -39,19 +39,25 @@ final class ParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctio /** @var array|null */ private ?array $componentTypesPairedStrings = null; + /** @var array|null */ + private ?array $componentTypesPairedConstantsForLowercaseString = null; + + /** @var array|null */ + private ?array $componentTypesPairedStringsForLowercaseString = null; + private ?Type $allComponentsTogetherType = null; + private ?Type $allComponentsTogetherTypeForLowercaseString = null; + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'parse_url'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 1) { - return ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants(), - )->getReturnType(); + return null; } $this->cacheReturnTypes(); @@ -60,45 +66,94 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (count($functionCall->getArgs()) > 1) { $componentType = $scope->getType($functionCall->getArgs()[1]->value); - if (!$componentType instanceof ConstantType) { - return $this->createAllComponentsReturnType(); + if (!$componentType->isConstantValue()->yes()) { + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } $componentType = $componentType->toInteger(); - if (!$componentType instanceof ConstantIntegerType) { - throw new ShouldNotHappenException(); + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } } else { $componentType = new ConstantIntegerType(-1); } - if ($urlType instanceof ConstantStringType) { - try { - $result = @parse_url(http://github.com/$urlType-%3EgetValue(), $componentType->getValue()); - } catch (ValueError) { - return new ConstantBooleanType(false); + if (count($urlType->getConstantStrings()) > 0) { + $types = []; + foreach ($urlType->getConstantStrings() as $constantString) { + try { + $result = @parse_url(http://github.com/$constantString-%3EgetValue(), $componentType->getValue()); + } catch (ValueError) { + $types[] = new ConstantBooleanType(false); + continue; + } + + $types[] = $scope->getTypeFromValue($result); } - return $scope->getTypeFromValue($result); + return TypeCombinator::union(...$types); } if ($componentType->getValue() === -1) { - return $this->createAllComponentsReturnType(); + return TypeCombinator::union( + $this->createComponentsArray($urlType->isLowercaseString()->yes()), + new ConstantBooleanType(false), + ); + } + + if ($urlType->isLowercaseString()->yes()) { + return $this->componentTypesPairedConstantsForLowercaseString[$componentType->getValue()] ?? new ConstantBooleanType(false); } return $this->componentTypesPairedConstants[$componentType->getValue()] ?? new ConstantBooleanType(false); } - private function createAllComponentsReturnType(): Type + private function createAllComponentsReturnType(bool $urlIsLowercase): Type { + if ($urlIsLowercase) { + if ($this->allComponentsTogetherTypeForLowercaseString === null) { + $returnTypes = [ + new ConstantBooleanType(false), + new NullType(), + IntegerRangeType::fromInterval(0, 65535), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + $this->createComponentsArray(true), + ]; + + $this->allComponentsTogetherTypeForLowercaseString = TypeCombinator::union(...$returnTypes); + } + + return $this->allComponentsTogetherTypeForLowercaseString; + } + if ($this->allComponentsTogetherType === null) { $returnTypes = [ new ConstantBooleanType(false), + new NullType(), + IntegerRangeType::fromInterval(0, 65535), + new StringType(), + $this->createComponentsArray(false), ]; - $builder = ConstantArrayTypeBuilder::createEmpty(); + $this->allComponentsTogetherType = TypeCombinator::union(...$returnTypes); + } + return $this->allComponentsTogetherType; + } + + private function createComponentsArray(bool $urlIsLowercase): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + if ($urlIsLowercase) { + if ($this->componentTypesPairedStringsForLowercaseString === null) { + throw new ShouldNotHappenException(); + } + + foreach ($this->componentTypesPairedStringsForLowercaseString as $componentName => $componentValueType) { + $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); + } + } else { if ($this->componentTypesPairedStrings === null) { throw new ShouldNotHappenException(); } @@ -106,13 +161,9 @@ private function createAllComponentsReturnType(): Type foreach ($this->componentTypesPairedStrings as $componentName => $componentValueType) { $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); } - - $returnTypes[] = $builder->getArray(); - - $this->allComponentsTogetherType = TypeCombinator::union(...$returnTypes); } - return $this->allComponentsTogetherType; + return $builder->getArray(); } private function cacheReturnTypes(): void @@ -122,34 +173,56 @@ private function cacheReturnTypes(): void } $string = new StringType(); - $integer = new IntegerType(); + $lowercaseString = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + $port = IntegerRangeType::fromInterval(0, 65535); $false = new ConstantBooleanType(false); $null = new NullType(); $stringOrFalseOrNull = TypeCombinator::union($string, $false, $null); - $integerOrFalseOrNull = TypeCombinator::union($integer, $false, $null); + $lowercaseStringOrFalseOrNull = TypeCombinator::union($lowercaseString, $false, $null); + $portOrFalseOrNull = TypeCombinator::union($port, $false, $null); $this->componentTypesPairedConstants = [ PHP_URL_SCHEME => $stringOrFalseOrNull, PHP_URL_HOST => $stringOrFalseOrNull, - PHP_URL_PORT => $integerOrFalseOrNull, + PHP_URL_PORT => $portOrFalseOrNull, PHP_URL_USER => $stringOrFalseOrNull, PHP_URL_PASS => $stringOrFalseOrNull, PHP_URL_PATH => $stringOrFalseOrNull, PHP_URL_QUERY => $stringOrFalseOrNull, PHP_URL_FRAGMENT => $stringOrFalseOrNull, ]; + $this->componentTypesPairedConstantsForLowercaseString = [ + PHP_URL_SCHEME => $lowercaseStringOrFalseOrNull, + PHP_URL_HOST => $lowercaseStringOrFalseOrNull, + PHP_URL_PORT => $portOrFalseOrNull, + PHP_URL_USER => $lowercaseStringOrFalseOrNull, + PHP_URL_PASS => $lowercaseStringOrFalseOrNull, + PHP_URL_PATH => $lowercaseStringOrFalseOrNull, + PHP_URL_QUERY => $lowercaseStringOrFalseOrNull, + PHP_URL_FRAGMENT => $lowercaseStringOrFalseOrNull, + ]; $this->componentTypesPairedStrings = [ 'scheme' => $string, 'host' => $string, - 'port' => $integer, + 'port' => $port, 'user' => $string, 'pass' => $string, 'path' => $string, 'query' => $string, 'fragment' => $string, ]; + $this->componentTypesPairedStringsForLowercaseString = [ + 'scheme' => $lowercaseString, + 'host' => $lowercaseString, + 'port' => $port, + 'user' => $lowercaseString, + 'pass' => $lowercaseString, + 'path' => $lowercaseString, + 'query' => $lowercaseString, + 'fragment' => $lowercaseString, + ]; } } diff --git a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php index ecbf504a31..60fac48358 100644 --- a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php @@ -5,18 +5,25 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; +use function sprintf; -class PathinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class PathinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'pathinfo'; @@ -26,26 +33,66 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, Node\Expr\FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $argsCount = count($functionCall->getArgs()); if ($argsCount === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } elseif ($argsCount === 1) { - $stringType = new StringType(); - - $builder = ConstantArrayTypeBuilder::createFromConstantArray( - new ConstantArrayType( - [new ConstantStringType('dirname'), new ConstantStringType('basename'), new ConstantStringType('filename')], - [$stringType, $stringType, $stringType], - ), - ); - $builder->setOffsetValueType(new ConstantStringType('extension'), $stringType, true); - - return $builder->getArray(); + return null; + } + + $pathType = $scope->getType($functionCall->getArgs()[0]->value); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('dirname'), new StringType(), !$pathType->isNonEmptyString()->yes()); + $builder->setOffsetValueType(new ConstantStringType('basename'), new StringType()); + $builder->setOffsetValueType(new ConstantStringType('extension'), new StringType(), true); + $builder->setOffsetValueType(new ConstantStringType('filename'), new StringType()); + $arrayType = $builder->getArray(); + + if ($argsCount === 1) { + return $arrayType; + } + + $flagsType = $scope->getType($functionCall->getArgs()[1]->value); + + $scalarValues = $flagsType->getConstantScalarValues(); + if ($scalarValues !== []) { + $pathInfoAll = $this->getConstant('PATHINFO_ALL'); + if ($pathInfoAll === null) { + return null; + } + + $result = []; + foreach ($scalarValues as $scalarValue) { + if ($scalarValue === $pathInfoAll) { + $result[] = $arrayType; + } else { + $result[] = new StringType(); + } + } + + return TypeCombinator::union(...$result); + } + + return TypeCombinator::union($arrayType, new StringType()); + } + + /** + * @param non-empty-string $constantName + */ + private function getConstant(string $constantName): ?int + { + if (!$this->reflectionProvider->hasConstant(new Node\Name($constantName), null)) { + return null; + } + + $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); + $valueType = $constant->getValueType(); + if (!$valueType instanceof ConstantIntegerType) { + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); } - return new StringType(); + return $valueType->getValue(); } } diff --git a/src/Type/Php/PowFunctionReturnTypeExtension.php b/src/Type/Php/PowFunctionReturnTypeExtension.php index bbae101311..6be372d38f 100644 --- a/src/Type/Php/PowFunctionReturnTypeExtension.php +++ b/src/Type/Php/PowFunctionReturnTypeExtension.php @@ -2,20 +2,15 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\BinaryOp\Pow; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; -class PowFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class PowFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,31 +18,13 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'pow'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - ]); if (count($functionCall->getArgs()) < 2) { - return $defaultReturnType; + return null; } - $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); - $secondArgType = $scope->getType($functionCall->getArgs()[1]->value); - if ($firstArgType instanceof MixedType || $secondArgType instanceof MixedType) { - return $defaultReturnType; - } - - $object = new ObjectWithoutClassType(); - if ( - !$object->isSuperTypeOf($firstArgType)->no() - || !$object->isSuperTypeOf($secondArgType)->no() - ) { - return TypeCombinator::union($firstArgType, $secondArgType); - } - - return $defaultReturnType; + return $scope->getType(new Pow($functionCall->getArgs()[0]->value, $functionCall->getArgs()[1]->value)); } } diff --git a/src/Type/Php/PregFilterFunctionReturnTypeExtension.php b/src/Type/Php/PregFilterFunctionReturnTypeExtension.php index 3e158d1bcc..c2c23e4155 100644 --- a/src/Type/Php/PregFilterFunctionReturnTypeExtension.php +++ b/src/Type/Php/PregFilterFunctionReturnTypeExtension.php @@ -15,7 +15,7 @@ use PHPStan\Type\UnionType; use function count; -class PregFilterFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class PregFilterFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -25,7 +25,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $defaultReturn = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $defaultReturn = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); $argsCount = count($functionCall->getArgs()); if ($argsCount < 3) { diff --git a/src/Type/Php/PregMatchParameterOutTypeExtension.php b/src/Type/Php/PregMatchParameterOutTypeExtension.php new file mode 100644 index 0000000000..9066351d9f --- /dev/null +++ b/src/Type/Php/PregMatchParameterOutTypeExtension.php @@ -0,0 +1,55 @@ +getName()), ['preg_match', 'preg_match_all'], true) + // the parameter is named different, depending on PHP version. + && in_array($parameter->getName(), ['subpatterns', 'matches'], true); + } + + public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $funcCall->getArgs(); + $patternArg = $args[0] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + if ( + $patternArg === null || $matchesArg === null + ) { + return null; + } + + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + if ($functionReflection->getName() === 'preg_match') { + return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); + } + return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); + } + +} diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php new file mode 100644 index 0000000000..09606087f1 --- /dev/null +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -0,0 +1,84 @@ +typeSpecifier = $typeSpecifier; + } + + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool + { + return in_array(strtolower($functionReflection->getName()), ['preg_match', 'preg_match_all'], true) && !$context->null(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + $patternArg = $args[0] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + if ( + $patternArg === null || $matchesArg === null + ) { + return new SpecifiedTypes(); + } + + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + if ($functionReflection->getName() === 'preg_match') { + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); + } else { + $matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); + } + if ($matchedType === null) { + return new SpecifiedTypes(); + } + + $overwrite = false; + if ($context->false()) { + $overwrite = true; + $context = $context->negate(); + } + + $types = $this->typeSpecifier->create( + $matchesArg->value, + $matchedType, + $context, + $scope, + )->setRootExpr($node); + if ($overwrite) { + $types = $types->setAlwaysOverwriteTypes(); + } + + return $types; + } + +} diff --git a/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php b/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php new file mode 100644 index 0000000000..a7ea4dc133 --- /dev/null +++ b/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php @@ -0,0 +1,60 @@ +getName() === 'preg_replace_callback' && $parameter->getName() === 'callback'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + $patternArg = $args[0] ?? null; + $flagsArg = $args[5] ?? null; + + if ( + $patternArg === null + ) { + return null; + } + + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + $matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + if ($matchesType === null) { + return null; + } + + return new ClosureType( + [ + new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()), + ], + new StringType(), + ); + } + +} diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 53f9ba6e66..d51b5314b0 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -2,17 +2,13 @@ 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\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; +use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -22,71 +18,35 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use function sprintf; use function strtolower; -class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private BitwiseFlagHelper $bitwiseFlagAnalyser, + ) { } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return strtolower($functionReflection->getName()) === 'preg_split'; } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $flagsArg = $functionCall->getArgs()[3] ?? null; - if ($this->hasFlag($this->getConstant('PREG_SPLIT_OFFSET_CAPTURE'), $flagsArg, $scope)) { + if ($flagsArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagsArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE')->yes()) { $type = new ArrayType( new IntegerType(), - new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)]), + new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes()), ); - return TypeCombinator::union($type, new ConstantBooleanType(false)); - } - - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - - - private function hasFlag(int $flag, ?Arg $arg, Scope $scope): bool - { - if ($arg === null) { - return false; - } - - 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; - } - - - private function getConstant(string $constantName): int - { - $constant = $this->reflectionProvider->getConstant(new Name($constantName), null); - $valueType = $constant->getValueType(); - if (!$valueType instanceof ConstantIntegerType) { - throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); + return TypeCombinator::union(TypeCombinator::intersect($type, new AccessoryArrayListType()), new ConstantBooleanType(false)); } - return $valueType->getValue(); + return null; } } diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index dbee387fc5..8b4d51142a 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Identifier; +use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -13,13 +14,14 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\Accessory\HasPropertyType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectWithoutClassType; use function count; -class PropertyExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class PropertyExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -40,7 +42,7 @@ public function isFunctionSupported( ): bool { return $functionReflection->getName() === 'property_exists' - && $context->truthy() + && $context->true() && count($node->getArgs()) >= 2; } @@ -53,13 +55,22 @@ public function specifyTypes( { $propertyNameType = $scope->getType($node->getArgs()[1]->value); if (!$propertyNameType instanceof ConstantStringType) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), + new ConstantBooleanType(true), + $context, + $scope, + ); + } + + if ($propertyNameType->getValue() === '') { return new SpecifiedTypes([], []); } $objectType = $scope->getType($node->getArgs()[0]->value); if ($objectType instanceof ConstantStringType) { return new SpecifiedTypes([], []); - } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($objectType)->yes()) { + } elseif ($objectType->isObject()->yes()) { $propertyNode = new PropertyFetch( $node->getArgs()[0]->value, new Identifier($propertyNameType->getValue()), @@ -82,7 +93,6 @@ public function specifyTypes( new HasPropertyType($propertyNameType->getValue()), ]), $context, - false, $scope, ); } diff --git a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php index 5b71dbf433..3e0873ee11 100644 --- a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php +++ b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -18,22 +17,22 @@ use function max; use function min; -class RandomIntFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class RandomIntFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return in_array($functionReflection->getName(), ['random_int', 'rand'], true); + return in_array($functionReflection->getName(), ['random_int', 'rand', 'mt_rand'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if ($functionReflection->getName() === 'rand' && count($functionCall->getArgs()) === 0) { + if (in_array($functionReflection->getName(), ['rand', 'mt_rand'], true) && count($functionCall->getArgs()) === 0) { return IntegerRangeType::fromInterval(0, null); } if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + return null; } $minType = $scope->getType($functionCall->getArgs()[0]->value)->toInteger(); diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 1f52b71b3a..175370be90 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -18,16 +18,16 @@ use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use ValueError; use function count; +use function is_array; use function range; -class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const RANGE_LENGTH_THRESHOLD = 50; @@ -37,10 +37,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'range'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $startType = $scope->getType($functionCall->getArgs()[0]->value); @@ -49,51 +49,79 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $constantReturnTypes = []; - $startConstants = TypeUtils::getConstantScalars($startType); + $startConstants = $startType->getConstantScalarTypes(); foreach ($startConstants as $startConstant) { if (!$startConstant instanceof ConstantIntegerType && !$startConstant instanceof ConstantFloatType && !$startConstant instanceof ConstantStringType) { continue; } - $endConstants = TypeUtils::getConstantScalars($endType); + $endConstants = $endType->getConstantScalarTypes(); foreach ($endConstants as $endConstant) { if (!$endConstant instanceof ConstantIntegerType && !$endConstant instanceof ConstantFloatType && !$endConstant instanceof ConstantStringType) { continue; } - $stepConstants = TypeUtils::getConstantScalars($stepType); + $stepConstants = $stepType->getConstantScalarTypes(); foreach ($stepConstants as $stepConstant) { if (!$stepConstant instanceof ConstantIntegerType && !$stepConstant instanceof ConstantFloatType) { continue; } - $rangeValues = range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); + try { + $rangeValues = @range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); + } catch (ValueError) { + continue; + } + + // @phpstan-ignore function.alreadyNarrowedType + if (!is_array($rangeValues)) { + continue; + } + if (count($rangeValues) > self::RANGE_LENGTH_THRESHOLD) { - if ($startConstant instanceof ConstantIntegerType && $endConstant instanceof ConstantIntegerType) { + if ( + $startConstant instanceof ConstantIntegerType + && $endConstant instanceof ConstantIntegerType + && $stepConstant instanceof ConstantIntegerType + ) { if ($startConstant->getValue() > $endConstant->getValue()) { $tmp = $startConstant; $startConstant = $endConstant; $endConstant = $tmp; } - return new IntersectionType([ + return TypeCombinator::intersect( new ArrayType( new IntegerType(), IntegerRangeType::fromInterval($startConstant->getValue(), $endConstant->getValue()), ), new NonEmptyArrayType(), - ]); + new AccessoryArrayListType(), + ); } - return new IntersectionType([ + if ($stepType->isFloat()->yes()) { + return TypeCombinator::intersect( + new ArrayType( + new IntegerType(), + new FloatType(), + ), + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ); + } + + return TypeCombinator::intersect( new ArrayType( new IntegerType(), TypeCombinator::union( $startConstant->generalize(GeneralizePrecision::moreSpecific()), $endConstant->generalize(GeneralizePrecision::moreSpecific()), + $stepType->generalize(GeneralizePrecision::moreSpecific()), ), ), new NonEmptyArrayType(), - ]); + new AccessoryArrayListType(), + ); } $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($rangeValues as $value) { @@ -110,31 +138,35 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = TypeCombinator::union($startType, $endType); - $isInteger = (new IntegerType())->isSuperTypeOf($argType)->yes(); - $isStepInteger = (new IntegerType())->isSuperTypeOf($stepType)->yes(); + $isInteger = $argType->isInteger()->yes(); + $isStepInteger = $stepType->isInteger()->yes(); if ($isInteger && $isStepInteger) { - return new ArrayType(new IntegerType(), new IntegerType()); + if ($argType instanceof IntegerRangeType) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $argType), new AccessoryArrayListType()); + } + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new IntegerType()), new AccessoryArrayListType()); } - $isFloat = (new FloatType())->isSuperTypeOf($argType)->yes(); - if ($isFloat) { - return new ArrayType(new IntegerType(), new FloatType()); + if ($argType->isFloat()->yes()) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new FloatType()), new AccessoryArrayListType()); } $numberType = new UnionType([new IntegerType(), new FloatType()]); $isNumber = $numberType->isSuperTypeOf($argType)->yes(); $isNumericString = $argType->isNumericString()->yes(); if ($isNumber || $isNumericString) { - return new ArrayType(new IntegerType(), $numberType); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $numberType), new AccessoryArrayListType()); } - $isString = (new StringType())->isSuperTypeOf($argType)->yes(); - if ($isString) { - return new ArrayType(new IntegerType(), new StringType()); + if ($argType->isString()->yes()) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new StringType()), new AccessoryArrayListType()); } - return new ArrayType(new IntegerType(), new BenevolentUnionType([new IntegerType(), new FloatType(), new StringType()])); + return TypeCombinator::intersect(new ArrayType( + new IntegerType(), + new BenevolentUnionType([new IntegerType(), new FloatType(), new StringType()]), + ), new AccessoryArrayListType()); } } diff --git a/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php index 3efa672a99..487857213d 100644 --- a/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php @@ -5,24 +5,17 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ClassStringType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; -use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\UnionType; use ReflectionClass; use function count; -class ReflectionClassConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +final class ReflectionClassConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - public function __construct(private ReflectionProvider $reflectionProvider) - { - } - public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === ReflectionClass::class; @@ -35,22 +28,15 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - foreach (TypeUtils::flattenTypes($valueType) as $type) { - if ($type instanceof ClassStringType || $type instanceof ObjectWithoutClassType || $type instanceof ObjectType) { - continue; - } - - if ( - $type instanceof ConstantStringType - && $this->reflectionProvider->hasClass($type->getValue()) - ) { - continue; - } - - return $methodReflection->getThrowType(); + $classOrString = new UnionType([ + new ClassStringType(), + new ObjectWithoutClassType(), + ]); + if ($classOrString->isSuperTypeOf($valueType)->yes()) { + return null; } - return null; + return $methodReflection->getThrowType(); } } diff --git a/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php index 842821b1e2..f472eab562 100644 --- a/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php +++ b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php @@ -9,13 +9,13 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\MethodReflection; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\MethodTypeSpecifyingExtension; -use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\TypeCombinator; use ReflectionClass; -class ReflectionClassIsSubclassOfTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class ReflectionClassIsSubclassOfTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -34,25 +34,38 @@ public function isMethodSupported(MethodReflection $methodReflection, MethodCall { return $methodReflection->getName() === 'isSubclassOf' && isset($node->getArgs()[0]) - && $context->true(); + && !$context->null(); } public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $calledOnType = $scope->getType($node->var); + $reflectionType = $calledOnType->getTemplateType(ReflectionClass::class, 'T'); + if (!(new ObjectWithoutClassType())->isSuperTypeOf($reflectionType)->yes()) { + return new SpecifiedTypes(); + } + $valueType = $scope->getType($node->getArgs()[0]->value); - if (!$valueType instanceof ConstantStringType) { - return new SpecifiedTypes([], []); + $objectType = $valueType->getClassStringObjectType(); + + $intersected = TypeCombinator::intersect($reflectionType, $objectType); + $narrowingType = new GenericObjectType(ReflectionClass::class, [$intersected]); + + if ($reflectionType->isSuperTypeOf($objectType)->no()) { + return $this->typeSpecifier->create( + $node->var, + $narrowingType, + $context, + $scope, + ); } return $this->typeSpecifier->create( $node->var, - new GenericObjectType(ReflectionClass::class, [ - new ObjectType($valueType->getValue()), - ]), + $narrowingType, $context, - false, $scope, - ); + )->setAlwaysOverwriteTypes(); } } diff --git a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php index 7dd3ba3e22..83df24904f 100644 --- a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php @@ -11,11 +11,10 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use ReflectionFunction; use function count; -class ReflectionFunctionConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +final class ReflectionFunctionConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function __construct(private ReflectionProvider $reflectionProvider) @@ -34,7 +33,11 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - foreach (TypeUtils::getConstantStrings($valueType) as $constantString) { + foreach ($valueType->getConstantStrings() as $constantString) { + if ($constantString->getValue() === '') { + return null; + } + if (!$this->reflectionProvider->hasFunction(new Name($constantString->getValue()), $scope)) { return $methodReflection->getThrowType(); } diff --git a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php index 6b4a470920..493ec0e9c3 100644 --- a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php +++ b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php @@ -5,19 +5,17 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; -use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; +use PHPStan\Type\IntegerType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use ReflectionAttribute; use function count; -class ReflectionGetAttributesMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class ReflectionGetAttributesMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { /** @@ -34,34 +32,19 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'getAttributes'; + return $methodReflection->getDeclaringClass()->getName() === $this->className + && $methodReflection->getName() === 'getAttributes'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { if (count($methodCall->getArgs()) === 0) { - return $this->getDefaultReturnType($scope, $methodCall, $methodReflection); + return null; } $argType = $scope->getType($methodCall->getArgs()[0]->value); + $classType = $argType->getClassStringObjectType(); - 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(); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new GenericObjectType(ReflectionAttribute::class, [$classType])), new AccessoryArrayListType()); } } diff --git a/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php index 18890c4236..c0b0df2cf5 100644 --- a/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php @@ -16,7 +16,7 @@ use ReflectionMethod; use function count; -class ReflectionMethodConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +final class ReflectionMethodConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function __construct(private ReflectionProvider $reflectionProvider) @@ -38,7 +38,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $propertyType = $scope->getType($methodCall->getArgs()[1]->value); foreach (TypeUtils::flattenTypes($valueType) as $type) { if ($type instanceof GenericClassStringType) { - $classes = $type->getReferencedClasses(); + $classes = $type->getGenericType()->getObjectClassNames(); } elseif ( $type instanceof ConstantStringType && $this->reflectionProvider->hasClass($type->getValue()) @@ -50,7 +50,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect foreach ($classes as $class) { $classReflection = $this->reflectionProvider->getClass($class); - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { if (!$classReflection->hasMethod($constantPropertyString->getValue())) { return $methodReflection->getThrowType(); } @@ -65,7 +65,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } // Look for non constantStrings value. - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { $propertyType = TypeCombinator::remove($propertyType, $constantPropertyString); } diff --git a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php index eed7c7d115..b988a7883a 100644 --- a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php @@ -10,11 +10,10 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use ReflectionProperty; use function count; -class ReflectionPropertyConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +final class ReflectionPropertyConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function __construct(private ReflectionProvider $reflectionProvider) @@ -34,13 +33,13 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $valueType = $scope->getType($methodCall->getArgs()[0]->value); $propertyType = $scope->getType($methodCall->getArgs()[1]->value); - foreach (TypeUtils::getConstantStrings($valueType) as $constantString) { + foreach ($valueType->getConstantStrings() as $constantString) { if (!$this->reflectionProvider->hasClass($constantString->getValue())) { return $methodReflection->getThrowType(); } $classReflection = $this->reflectionProvider->getClass($constantString->getValue()); - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { if (!$classReflection->hasProperty($constantPropertyString->getValue())) { return $methodReflection->getThrowType(); } @@ -54,7 +53,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } // Look for non constantStrings value. - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { $propertyType = TypeCombinator::remove($propertyType, $constantPropertyString); } diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php new file mode 100644 index 0000000000..a23e3445d8 --- /dev/null +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -0,0 +1,498 @@ +matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, true); + } + + public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type + { + return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false); + } + + private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll): ?Type + { + if ($wasMatched->no()) { + return ConstantArrayTypeBuilder::createEmpty()->getArray(); + } + + $constantStrings = $patternType->getConstantStrings(); + if (count($constantStrings) === 0) { + return null; + } + + $flags = null; + if ($flagsType !== null) { + if (!$flagsType instanceof ConstantIntegerType) { + return null; + } + + /** @var int-mask $flags */ + $flags = $flagsType->getValue() & (PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER | PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | self::PREG_UNMATCHED_AS_NULL_ON_72_73); + + // some other unsupported/unexpected flag was passed in + if ($flags !== $flagsType->getValue()) { + return null; + } + } + + $matchedTypes = []; + foreach ($constantStrings as $constantString) { + $matched = $this->matchRegex($constantString->getValue(), $flags, $wasMatched, $matchesAll); + if ($matched === null) { + return null; + } + + $matchedTypes[] = $matched; + } + + if (count($matchedTypes) === 1) { + return $matchedTypes[0]; + } + + return TypeCombinator::union(...$matchedTypes); + } + + /** + * @param int-mask|null $flags + */ + private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll): ?Type + { + $astWalkResult = $this->regexGroupParser->parseGroups($regex); + if ($astWalkResult === null) { + // regex could not be parsed by Hoa/Regex + return null; + } + $groupList = $astWalkResult->getCapturingGroups(); + $markVerbs = $astWalkResult->getMarkVerbs(); + $subjectBaseType = new StringType(); + if ($wasMatched->yes()) { + $subjectBaseType = $astWalkResult->getSubjectBaseType(); + } + + $regexGroupList = new RegexGroupList($groupList); + $trailingOptionals = $regexGroupList->countTrailingOptionals(); + $onlyOptionalTopLevelGroup = $regexGroupList->getOnlyOptionalTopLevelGroup(); + $onlyTopLevelAlternation = $regexGroupList->getOnlyTopLevelAlternation(); + $flags ??= 0; + + if ( + !$matchesAll + && $wasMatched->yes() + && $onlyOptionalTopLevelGroup !== null + ) { + // if only one top level capturing optional group exists + // we build a more precise tagged union of a empty-match and a match with the group + $regexGroupList = $regexGroupList->forceGroupNonOptional($onlyOptionalTopLevelGroup); + + $combiType = $this->buildArrayType( + $subjectBaseType, + $regexGroupList, + $wasMatched, + $trailingOptionals, + $flags, + $markVerbs, + $matchesAll, + ); + + if (!$this->containsUnmatchedAsNull($flags, $matchesAll)) { + // positive match has a subject but not any capturing group + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)); + + $combiType = TypeCombinator::union( + $builder->getArray(), + $combiType, + ); + } + + return $combiType; + } elseif ( + !$matchesAll + && $onlyOptionalTopLevelGroup === null + && $onlyTopLevelAlternation !== null + && !$wasMatched->no() + ) { + // if only a single top level alternation exist built a more precise tagged union + + $combiTypes = []; + $isOptionalAlternation = false; + foreach ($onlyTopLevelAlternation->getGroupCombinations() as $groupCombo) { + $comboList = new RegexGroupList($groupList); + + $beforeCurrentCombo = true; + foreach ($comboList as $group) { + if (in_array($group->getId(), $groupCombo, true)) { + $isOptionalAlternation = $group->inOptionalAlternation(); + $comboList = $comboList->forceGroupNonOptional($group); + $beforeCurrentCombo = false; + } elseif ($beforeCurrentCombo && !$group->resetsGroupCounter()) { + $comboList = $comboList->forceGroupTypeAndNonOptional( + $group, + $this->containsUnmatchedAsNull($flags, $matchesAll) ? new NullType() : new ConstantStringType(''), + ); + } elseif ( + $group->getAlternationId() === $onlyTopLevelAlternation->getId() + && !$this->containsUnmatchedAsNull($flags, $matchesAll) + ) { + $comboList = $comboList->removeGroup($group); + } + } + + $combiType = $this->buildArrayType( + $subjectBaseType, + $comboList, + $wasMatched, + $trailingOptionals, + $flags, + $markVerbs, + $matchesAll, + ); + + $combiTypes[] = $combiType; + } + + if ( + !$this->containsUnmatchedAsNull($flags, $matchesAll) + && ( + $onlyTopLevelAlternation->getAlternationsCount() !== count($onlyTopLevelAlternation->getGroupCombinations()) + || $isOptionalAlternation + ) + ) { + // positive match has a subject but not any capturing group + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)); + + $combiTypes[] = $builder->getArray(); + } + + return TypeCombinator::union(...$combiTypes); + } + + // the general case, which should work in all cases but does not yield the most + // precise result possible in some cases + return $this->buildArrayType( + $subjectBaseType, + $regexGroupList, + $wasMatched, + $trailingOptionals, + $flags, + $markVerbs, + $matchesAll, + ); + } + + /** + * @param list $markVerbs + */ + private function buildArrayType( + Type $subjectBaseType, + RegexGroupList $captureGroups, + TrinaryLogic $wasMatched, + int $trailingOptionals, + int $flags, + array $markVerbs, + bool $matchesAll, + ): Type + { + $forceList = count($markVerbs) === 0; + $builder = ConstantArrayTypeBuilder::createEmpty(); + + // first item in matches contains the overall match. + $builder->setOffsetValueType( + $this->getKeyType(0), + $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll), + $this->isSubjectOptional($wasMatched, $matchesAll), + ); + + $countGroups = count($captureGroups); + $i = 0; + foreach ($captureGroups as $captureGroup) { + $isTrailingOptional = $i >= $countGroups - $trailingOptionals; + $isLastGroup = $i === $countGroups - 1; + $groupValueType = $this->createGroupValueType($captureGroup, $wasMatched, $flags, $isTrailingOptional, $isLastGroup, $matchesAll); + $optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll); + + if ($captureGroup->isNamed()) { + $forceList = false; + + $builder->setOffsetValueType( + $this->getKeyType($captureGroup->getName()), + $groupValueType, + $optional, + ); + } + + $builder->setOffsetValueType( + $this->getKeyType($i + 1), + $groupValueType, + $optional, + ); + + $i++; + } + + if (count($markVerbs) > 0) { + $markTypes = []; + foreach ($markVerbs as $mark) { + $markTypes[] = new ConstantStringType($mark); + } + $builder->setOffsetValueType( + $this->getKeyType('MARK'), + TypeCombinator::union(...$markTypes), + true, + ); + } + + if ($matchesAll && $this->containsSetOrder($flags)) { + $arrayType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $builder->getArray()), new AccessoryArrayListType()); + if (!$wasMatched->yes()) { + $arrayType = TypeCombinator::union( + ConstantArrayTypeBuilder::createEmpty()->getArray(), + $arrayType, + ); + } + return $arrayType; + } + + if ($forceList) { + return TypeCombinator::intersect($builder->getArray(), new AccessoryArrayListType()); + } + + return $builder->getArray(); + } + + private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll): bool + { + if ($matchesAll) { + return false; + } + + return !$wasMatched->yes(); + } + + /** + * @param Type $baseType A string type (or string variant) representing the subject of the match + */ + private function createSubjectValueType(Type $baseType, int $flags, bool $matchesAll): Type + { + $subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll)); + + if ($matchesAll) { + $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll)); + + if ($this->containsPatternOrder($flags)) { + $subjectValueType = TypeCombinator::intersect( + new ArrayType(new IntegerType(), $subjectValueType), + new AccessoryArrayListType(), + ); + } + } + + return $subjectValueType; + } + + private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $matchesAll): bool + { + if ($matchesAll) { + if ($isTrailingOptional && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $this->containsSetOrder($flags)) { + return true; + } + + return false; + } + + if (!$wasMatched->yes()) { + $optional = true; + } else { + if (!$isTrailingOptional) { + $optional = false; + } elseif ($this->containsUnmatchedAsNull($flags, $matchesAll)) { + $optional = false; + } else { + $optional = $captureGroup->isOptional(); + } + } + + return $optional; + } + + private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll): Type + { + if ($matchesAll) { + if ( + ( + !$this->containsSetOrder($flags) + && !$this->containsUnmatchedAsNull($flags, $matchesAll) + && $captureGroup->isOptional() + ) + || + ( + $this->containsSetOrder($flags) + && !$this->containsUnmatchedAsNull($flags, $matchesAll) + && $captureGroup->isOptional() + && !$isTrailingOptional + ) + ) { + $groupValueType = $this->getValueType( + TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), + $flags, + $matchesAll, + ); + $groupValueType = TypeCombinator::removeNull($groupValueType); + } else { + $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); + } + + if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { + $groupValueType = TypeCombinator::removeNull($groupValueType); + } + + if ($this->containsPatternOrder($flags)) { + $groupValueType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $groupValueType), new AccessoryArrayListType()); + } + + return $groupValueType; + } + + if (!$isLastGroup && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) { + $groupValueType = $this->getValueType( + TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), + $flags, + $matchesAll, + ); + } else { + $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); + } + + if ($wasMatched->yes()) { + if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { + $groupValueType = TypeCombinator::removeNull($groupValueType); + } + } + + return $groupValueType; + } + + private function containsOffsetCapture(int $flags): bool + { + return ($flags & PREG_OFFSET_CAPTURE) !== 0; + } + + private function containsPatternOrder(int $flags): bool + { + // If no order flag is given, PREG_PATTERN_ORDER is assumed. + return !$this->containsSetOrder($flags); + } + + private function containsSetOrder(int $flags): bool + { + return ($flags & PREG_SET_ORDER) !== 0; + } + + private function containsUnmatchedAsNull(int $flags, bool $matchesAll): bool + { + if ($matchesAll) { + // preg_match_all() with PREG_UNMATCHED_AS_NULL works consistently across php-versions + // https://3v4l.org/tKmPn + return ($flags & PREG_UNMATCHED_AS_NULL) !== 0; + } + + return ($flags & PREG_UNMATCHED_AS_NULL) !== 0 && (($flags & self::PREG_UNMATCHED_AS_NULL_ON_72_73) !== 0 || $this->phpVersion->supportsPregUnmatchedAsNull()); + } + + private function getKeyType(int|string $key): Type + { + if (is_string($key)) { + return new ConstantStringType($key); + } + + return new ConstantIntegerType($key); + } + + private function getValueType(Type $baseType, int $flags, bool $matchesAll): Type + { + $valueType = $baseType; + + // unmatched groups return -1 as offset + $offsetType = IntegerRangeType::fromInterval(-1, null); + if ($this->containsUnmatchedAsNull($flags, $matchesAll)) { + $valueType = TypeCombinator::addNull($valueType); + } + + if ($this->containsOffsetCapture($flags)) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $builder->setOffsetValueType( + new ConstantIntegerType(0), + $valueType, + ); + $builder->setOffsetValueType( + new ConstantIntegerType(1), + $offsetType, + ); + + return $builder->getArray(); + } + + return $valueType; + } + + private function getPatternType(Expr $patternExpr, Scope $scope): Type + { + if ($patternExpr instanceof Expr\BinaryOp\Concat) { + return $this->regexExpressionHelper->resolvePatternConcat($patternExpr, $scope); + } + + return $scope->getType($patternExpr); + } + +} diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 87bb1aaafb..01bb1dd29e 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -6,8 +6,11 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\ArrayType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; @@ -17,31 +20,32 @@ use PHPStan\Type\TypeUtils; use function array_key_exists; use function count; +use function in_array; -class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var array */ - private array $functionsSubjectPosition = [ + private const FUNCTIONS_SUBJECT_POSITION = [ 'preg_replace' => 2, 'preg_replace_callback' => 2, 'preg_replace_callback_array' => 1, 'str_replace' => 2, 'str_ireplace' => 2, 'substr_replace' => 0, + 'strtr' => 0, ]; - /** @var array */ - private array $functionsReplacePosition = [ + private const FUNCTIONS_REPLACE_POSITION = [ 'preg_replace' => 1, 'str_replace' => 1, 'str_ireplace' => 1, 'substr_replace' => 1, + 'strtr' => 2, ]; public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return array_key_exists($functionReflection->getName(), $this->functionsSubjectPosition); + return array_key_exists($functionReflection->getName(), self::FUNCTIONS_SUBJECT_POSITION); } public function getTypeFromFunctionCall( @@ -52,9 +56,7 @@ public function getTypeFromFunctionCall( { $type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope); - $possibleTypes = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - - if (TypeCombinator::containsNull($possibleTypes)) { + if ($this->canReturnNull($functionReflection, $functionCall, $scope)) { $type = TypeCombinator::addNull($type); } @@ -67,40 +69,86 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( Scope $scope, ): Type { - $argumentPosition = $this->functionsSubjectPosition[$functionReflection->getName()]; - if (count($functionCall->getArgs()) <= $argumentPosition) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $subjectArgumentType = $this->getSubjectType($functionReflection, $functionCall, $scope); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + if ($subjectArgumentType === null) { + return $defaultReturnType; } - $subjectArgumentType = $scope->getType($functionCall->getArgs()[$argumentPosition]->value); - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($subjectArgumentType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - if ($subjectArgumentType->isNonEmptyString()->yes() && array_key_exists($functionReflection->getName(), $this->functionsReplacePosition)) { - $replaceArgumentPosition = $this->functionsReplacePosition[$functionReflection->getName()]; + if (array_key_exists($functionReflection->getName(), self::FUNCTIONS_REPLACE_POSITION)) { + $replaceArgumentPosition = self::FUNCTIONS_REPLACE_POSITION[$functionReflection->getName()]; if (count($functionCall->getArgs()) > $replaceArgumentPosition) { $replaceArgumentType = $scope->getType($functionCall->getArgs()[$replaceArgumentPosition]->value); + if ($replaceArgumentType->isArray()->yes()) { + $replaceArgumentType = $replaceArgumentType->getIterableValueType(); + } + + $accessories = []; + if ($subjectArgumentType->isNonFalsyString()->yes() && $replaceArgumentType->isNonFalsyString()->yes()) { + $accessories[] = new AccessoryNonFalsyStringType(); + } elseif ($subjectArgumentType->isNonEmptyString()->yes() && $replaceArgumentType->isNonEmptyString()->yes()) { + $accessories[] = new AccessoryNonEmptyStringType(); + } + + if ($subjectArgumentType->isLowercaseString()->yes() && $replaceArgumentType->isLowercaseString()->yes()) { + $accessories[] = new AccessoryLowercaseStringType(); + } + + if ($subjectArgumentType->isUppercaseString()->yes() && $replaceArgumentType->isUppercaseString()->yes()) { + $accessories[] = new AccessoryUppercaseStringType(); + } - if ($replaceArgumentType->isNonEmptyString()->yes()) { - return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + if (count($accessories) > 0) { + $accessories[] = new StringType(); + return new IntersectionType($accessories); } } } - $stringType = new StringType(); - $arrayType = new ArrayType(new MixedType(), new MixedType()); - - $isStringSuperType = $stringType->isSuperTypeOf($subjectArgumentType); - $isArraySuperType = $arrayType->isSuperTypeOf($subjectArgumentType); + $isStringSuperType = $subjectArgumentType->isString(); + $isArraySuperType = $subjectArgumentType->isArray(); $compareSuperTypes = $isStringSuperType->compareTo($isArraySuperType); if ($compareSuperTypes === $isStringSuperType) { - return $stringType; + return new StringType(); } elseif ($compareSuperTypes === $isArraySuperType) { - if ($subjectArgumentType instanceof ArrayType) { - return $subjectArgumentType->generalizeValues(); + $subjectArrays = $subjectArgumentType->getArrays(); + if (count($subjectArrays) > 0) { + $result = []; + foreach ($subjectArrays as $arrayType) { + $constantArrays = $arrayType->getConstantArrays(); + + if ( + $constantArrays !== [] + && in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true) + ) { + foreach ($constantArrays as $constantArray) { + $generalizedArray = $constantArray->generalizeValues(); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + // turn all keys optional + foreach ($constantArray->getKeyTypes() as $keyType) { + $builder->setOffsetValueType($keyType, $generalizedArray->getOffsetValueType($keyType), true); + } + $result[] = $builder->getArray(); + } + + continue; + } + + $result[] = $arrayType->generalizeValues(); + } + + return TypeCombinator::union(...$result); } return $subjectArgumentType; } @@ -108,4 +156,49 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( return $defaultReturnType; } + private function getSubjectType( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $argumentPosition = self::FUNCTIONS_SUBJECT_POSITION[$functionReflection->getName()]; + if (count($functionCall->getArgs()) <= $argumentPosition) { + return null; + } + return $scope->getType($functionCall->getArgs()[$argumentPosition]->value); + } + + private function canReturnNull( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): bool + { + if ( + in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true) + && count($functionCall->getArgs()) > 0 + ) { + $subjectArgumentType = $this->getSubjectType($functionReflection, $functionCall, $scope); + + if ( + $subjectArgumentType !== null + && $subjectArgumentType->isArray()->yes() + ) { + return false; + } + } + + $possibleTypes = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + // resolve conditional return types + $possibleTypes = TypeUtils::resolveLateResolvableTypes($possibleTypes); + + return TypeCombinator::containsNull($possibleTypes); + } + } diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 839c5c1587..8d064c1ef3 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -6,20 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; use function in_array; -class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function __construct(private PhpVersion $phpVersion) @@ -39,21 +42,18 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo ); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { + // PHP 7 can return either a float or false. + // PHP 8 can either return a float or fatal. + $defaultReturnType = null; + if ($this->phpVersion->hasStricterRoundFunctions()) { // PHP 8 fatals with a missing parameter. $noArgsReturnType = new NeverType(true); - // PHP 8 can either return a float or fatal. - $defaultReturnType = new FloatType(); } else { // PHP 7 returns null with a missing parameter. $noArgsReturnType = new NullType(); - // PHP 7 can return either a float or false. - $defaultReturnType = new BenevolentUnionType([ - new FloatType(), - new ConstantBooleanType(false), - ]); } if (count($functionCall->getArgs()) < 1) { @@ -71,6 +71,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, new IntegerType(), new FloatType(), ); + + if (!$scope->isDeclareStrictTypes()) { + $allowed = TypeCombinator::union( + $allowed, + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + new NullType(), + new BooleanType(), + ); + } + if ($allowed->isSuperTypeOf($firstArgType)->no()) { // PHP 8 fatals if the parameter is not an integer or float. return new NeverType(true); diff --git a/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php b/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..a9134026ea --- /dev/null +++ b/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php @@ -0,0 +1,90 @@ +getName()) === 'settype' + && count($node->getArgs()) > 1 + && $context->null(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $value = $node->getArgs()[0]->value; + $valueType = $scope->getType($value); + $castType = $scope->getType($node->getArgs()[1]->value); + + $constantStrings = $castType->getConstantStrings(); + if (count($constantStrings) < 1) { + return new SpecifiedTypes(); + } + + $types = []; + + foreach ($constantStrings as $constantString) { + switch ($constantString->getValue()) { + case 'bool': + case 'boolean': + $types[] = $valueType->toBoolean(); + break; + case 'int': + case 'integer': + $types[] = $valueType->toInteger(); + break; + case 'float': + case 'double': + $types[] = $valueType->toFloat(); + break; + case 'string': + $types[] = $valueType->toString(); + break; + case 'array': + $types[] = $valueType->toArray(); + break; + case 'object': + $types[] = new ObjectType(stdClass::class); + break; + case 'null': + $types[] = new NullType(); + break; + default: + $types[] = new ErrorType(); + } + } + + return $this->typeSpecifier->create( + $value, + TypeCombinator::union(...$types), + TypeSpecifierContext::createTruthy(), + $scope, + )->setAlwaysOverwriteTypes(); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php index e6c390cd22..a0f33f1841 100644 --- a/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php @@ -14,7 +14,7 @@ use SimpleXMLElement; use function count; -class SimpleXMLElementAsXMLMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class SimpleXMLElementAsXMLMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php b/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php index 503e998fd3..c670914835 100644 --- a/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php +++ b/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php @@ -10,18 +10,17 @@ use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; -class SimpleXMLElementClassPropertyReflectionExtension implements PropertiesClassReflectionExtension +final class SimpleXMLElementClassPropertyReflectionExtension implements PropertiesClassReflectionExtension { public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - return $classReflection->getName() === 'SimpleXMLElement' || $classReflection->isSubclassOf('SimpleXMLElement'); + return $classReflection->is('SimpleXMLElement'); } - public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { - return new SimpleXMLElementProperty($classReflection, new BenevolentUnionType([new ObjectType($classReflection->getName()), new NullType()])); + return new SimpleXMLElementProperty($propertyName, $classReflection, new BenevolentUnionType([new ObjectType($classReflection->getName()), new NullType()])); } } diff --git a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php index e7b8247870..28995db8ea 100644 --- a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php @@ -9,16 +9,19 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use SimpleXMLElement; use function count; +use function extension_loaded; +use function libxml_use_internal_errors; -class SimpleXMLElementConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +final class SimpleXMLElementConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function isStaticMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === SimpleXMLElement::class; + return extension_loaded('simplexml') + && $methodReflection->getName() === '__construct' + && $methodReflection->getDeclaringClass()->getName() === SimpleXMLElement::class; } public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type @@ -28,16 +31,22 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); - foreach ($constantStrings as $constantString) { - try { - new SimpleXMLElement($constantString->getValue()); - } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); - } + $internalErrorsOld = libxml_use_internal_errors(true); + + try { + foreach ($constantStrings as $constantString) { + try { + new SimpleXMLElement($constantString->getValue()); + } catch (\Exception $e) { // phpcs:ignore + return $methodReflection->getThrowType(); + } - $valueType = TypeCombinator::remove($valueType, $constantString); + $valueType = TypeCombinator::remove($valueType, $constantString); + } + } finally { + libxml_use_internal_errors($internalErrorsOld); } if (!$valueType instanceof NeverType) { diff --git a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php index 175cd703ed..7255d64887 100644 --- a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php @@ -5,17 +5,16 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use SimpleXMLElement; +use function extension_loaded; -class SimpleXMLElementXpathMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class SimpleXMLElementXpathMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string @@ -25,31 +24,31 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'xpath'; + return extension_loaded('simplexml') && $methodReflection->getName() === 'xpath'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { if (!isset($methodCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($methodCall->getArgs()[0]->value); $xmlElement = new SimpleXMLElement(''); - foreach (TypeUtils::getConstantStrings($argType) as $constantString) { + foreach ($argType->getConstantStrings() as $constantString) { $result = @$xmlElement->xpath($constantString->getValue()); if ($result === false) { // We can't be sure since it's maybe a namespaced xpath - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } $argType = TypeCombinator::remove($argType, $constantString); } if (!$argType instanceof NeverType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } return new ArrayType(new MixedType(), $scope->getType($methodCall->var)); diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index b3736e663a..e379c4cc3c 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -2,73 +2,372 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Internal\CombinationsHelper; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use Throwable; +use function array_fill; +use function array_key_exists; use function array_shift; +use function array_values; use function count; +use function in_array; +use function intval; +use function is_array; use function is_string; +use function preg_match; use function sprintf; +use function substr; +use function vsprintf; -class SprintfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class SprintfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'sprintf'; + return in_array($functionReflection->getName(), ['sprintf', 'vsprintf'], true); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; + } + + $constantType = $this->getConstantType($args, $functionReflection, $scope); + if ($constantType !== null) { + return $constantType; } $formatType = $scope->getType($args[0]->value); - if ($formatType->isNonEmptyString()->yes()) { - $returnType = new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + $formatStrings = $formatType->getConstantStrings(); + + $isLowercase = $formatType->isLowercaseString()->yes() && $this->allValuesSatisfies( + $functionReflection, + $scope, + $args, + static fn (Type $type): bool => $type->toString()->isLowercaseString()->yes() + ); + + $singlePlaceholderEarlyReturn = []; + $allPatternsNonEmpty = count($formatStrings) !== 0; + $allPatternsNonFalsy = count($formatStrings) !== 0; + foreach ($formatStrings as $constantString) { + $constantParts = $this->getFormatConstantParts( + $constantString->getValue(), + $functionReflection, + $functionCall, + $scope, + ); + if ($constantParts !== null) { + if ($constantParts->isNonFalsyString()->yes()) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf + // keep all bool flags as is + } elseif ($constantParts->isNonEmptyString()->yes()) { + $allPatternsNonFalsy = false; + } else { + $allPatternsNonEmpty = false; + $allPatternsNonFalsy = false; + } + } else { + $allPatternsNonEmpty = false; + $allPatternsNonFalsy = false; + } + + if ( + is_array($singlePlaceholderEarlyReturn) + // The printf format is %[argnum$][flags][width][.precision]specifier. + && preg_match('/^%(?P[0-9]*\$)?(?P[0-9]*)\.?[0-9]*(?P[sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1 + ) { + if ($matches['argnum'] !== '') { + // invalid positional argument + if ($matches['argnum'] === '0$') { + return null; + } + $checkArg = intval(substr($matches['argnum'], 0, -1)); + } else { + $checkArg = 1; + } + + $checkArgType = $this->getValueType($functionReflection, $scope, $args, $checkArg); + if ($checkArgType === null) { + return null; + } + + // if the format string is just a placeholder and specified an argument + // of stringy type, then the return value will be of the same type + if ( + $matches['specifier'] === 's' + && ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes()) + ) { + if ($checkArgType instanceof IntegerRangeType) { + $constArgTypes = $checkArgType->getFiniteTypes(); + } else { + $constArgTypes = $checkArgType->getConstantScalarTypes(); + } + if ($constArgTypes !== []) { + $printfArgs = array_fill(0, count($args) - 1, ''); + foreach ($constArgTypes as $constArgType) { + $printfArgs[$checkArg - 1] = $constArgType->getValue(); + try { + $singlePlaceholderEarlyReturn[] = new ConstantStringType(@sprintf($constantString->getValue(), ...$printfArgs)); + } catch (Throwable) { + continue 2; + } + } + + continue; + } + + $singlePlaceholderEarlyReturn[] = $checkArgType->toString(); + } elseif ($matches['specifier'] !== 's') { + $singlePlaceholderEarlyReturn[] = $this->getStringReturnType( + new AccessoryNumericStringType(), + $isLowercase, + ); + } + + continue; + } + + $singlePlaceholderEarlyReturn = null; + } + + if (is_array($singlePlaceholderEarlyReturn) && count($singlePlaceholderEarlyReturn) > 0) { + return TypeCombinator::union(...$singlePlaceholderEarlyReturn); + } + + if ($allPatternsNonFalsy) { + return $this->getStringReturnType(new AccessoryNonFalsyStringType(), $isLowercase); + } + + $isNonEmpty = $allPatternsNonEmpty; + if (!$isNonEmpty && $formatType->isNonEmptyString()->yes()) { + $isNonEmpty = $this->allValuesSatisfies( + $functionReflection, + $scope, + $args, + static fn (Type $type): bool => $type->toString()->isNonEmptyString()->yes() + ); + } + + if ($isNonEmpty) { + return $this->getStringReturnType(new AccessoryNonEmptyStringType(), $isLowercase); + } + + return $this->getStringReturnType(null, $isLowercase); + } + + /** + * @param array $args + * @param callable(Type): bool $cb + */ + private function allValuesSatisfies(FunctionReflection $functionReflection, Scope $scope, array $args, callable $cb): bool + { + if ($functionReflection->getName() === 'sprintf' && count($args) >= 2) { + foreach ($args as $key => $arg) { + if ($key === 0) { + continue; + } + + if (!$cb($scope->getType($arg->value))) { + return false; + } + } + + return true; + } + + if ($functionReflection->getName() === 'vsprintf' && count($args) >= 2) { + return $cb($scope->getType($args[1]->value)->getIterableValueType()); + } + + return false; + } + + /** + * @param Arg[] $args + */ + private function getValueType(FunctionReflection $functionReflection, Scope $scope, array $args, int $argNumber): ?Type + { + if ($functionReflection->getName() === 'sprintf') { + // constant string specifies a numbered argument that does not exist + if (!array_key_exists($argNumber, $args)) { + return null; + } + + return $scope->getType($args[$argNumber]->value); + } + + if ($functionReflection->getName() === 'vsprintf') { + if (!array_key_exists(1, $args)) { + return null; + } + + $valuesType = $scope->getType($args[1]->value); + $resultTypes = []; + + $valuesConstantArrays = $valuesType->getConstantArrays(); + foreach ($valuesConstantArrays as $valuesConstantArray) { + // vsprintf does not care about the keys of the array, only the order + $types = array_values($valuesConstantArray->getValueTypes()); + if (!array_key_exists($argNumber - 1, $types)) { + return null; + } + + $resultTypes[] = $types[$argNumber - 1]; + } + if (count($resultTypes) === 0) { + return $valuesType->getIterableValueType(); + } + + return TypeCombinator::union(...$resultTypes); + } + + return null; + } + + /** + * Detect constant strings in the format which neither depend on placeholders nor on given value arguments. + */ + private function getFormatConstantParts( + string $format, + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?ConstantStringType + { + $args = $functionCall->getArgs(); + if ($functionReflection->getName() === 'sprintf') { + $valuesCount = count($args) - 1; + } elseif ( + $functionReflection->getName() === 'vsprintf' + && count($args) >= 2 + ) { + $arraySize = $scope->getType($args[1]->value)->getArraySize(); + if (!($arraySize instanceof ConstantIntegerType)) { + return null; + } + + $valuesCount = $arraySize->getValue(); } else { - $returnType = new StringType(); + return null; } + if ($valuesCount <= 0) { + return null; + } + $dummyValues = array_fill(0, $valuesCount, ''); + + try { + $formatted = @vsprintf($format, $dummyValues); + if ($formatted === false) { // @phpstan-ignore identical.alwaysFalse (PHP7.2 compat) + return null; + } + return new ConstantStringType($formatted); + } catch (Throwable) { + return null; + } + } + + /** + * @param Arg[] $args + */ + private function getConstantType(array $args, FunctionReflection $functionReflection, Scope $scope): ?Type + { $values = []; + $combinationsCount = 1; foreach ($args as $arg) { + if ($arg->unpack) { + return null; + } + $argType = $scope->getType($arg->value); - if (!$argType instanceof ConstantScalarType) { - return $returnType; + $constantScalarValues = $argType->getConstantScalarValues(); + + if (count($constantScalarValues) === 0) { + if ($argType instanceof IntegerRangeType) { + foreach ($argType->getFiniteTypes() as $finiteType) { + $constantScalarValues[] = $finiteType->getValue(); + } + } + } + + if (count($constantScalarValues) === 0) { + return null; } - $values[] = $argType->getValue(); + $values[] = $constantScalarValues; + $combinationsCount *= count($constantScalarValues); } - $format = array_shift($values); - if (!is_string($format)) { - return $returnType; + if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; } - try { - $value = @sprintf($format, ...$values); - } catch (Throwable) { - return $returnType; + $combinations = CombinationsHelper::combinations($values); + $returnTypes = []; + foreach ($combinations as $combination) { + $format = array_shift($combination); + if (!is_string($format)) { + return null; + } + + try { + if ($functionReflection->getName() === 'sprintf') { + $returnTypes[] = $scope->getTypeFromValue(@sprintf($format, ...$combination)); + } else { + $returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination)); + } + } catch (Throwable) { + return null; + } } - return $scope->getTypeFromValue($value); + if (count($returnTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; + } + + return TypeCombinator::union(...$returnTypes); + } + + private function getStringReturnType(?AccessoryType $accessoryType, bool $isLowercase): Type + { + $accessoryTypes = []; + if ($accessoryType !== null) { + $accessoryTypes[] = $accessoryType; + } + if ($isLowercase) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + + if (count($accessoryTypes) === 0) { + return new StringType(); + } + + $accessoryTypes[] = new StringType(); + + return new IntersectionType($accessoryTypes); } } diff --git a/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..18a1275ca6 --- /dev/null +++ b/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,89 @@ +getName(), ['sscanf', 'fscanf'], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) !== 2) { + return null; + } + + $formatType = $scope->getType($args[1]->value); + + if (!$formatType instanceof ConstantStringType) { + return null; + } + + if (preg_match_all('/%(\d*)(\[[^\]]+\]|[cdeEfosux]{1})/', $formatType->getValue(), $matches) > 0) { + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + + for ($i = 0; $i < count($matches[0]); $i++) { + $length = $matches[1][$i]; + $specifier = $matches[2][$i]; + + $type = new StringType(); + if ($length !== '') { + if (((int) $length) > 1) { + $type = new IntersectionType([ + $type, + new AccessoryNonFalsyStringType(), + ]); + } else { + $type = new IntersectionType([ + $type, + new AccessoryNonEmptyStringType(), + ]); + } + } + + if (in_array($specifier, ['d', 'o', 'u', 'x'], true)) { + $type = new IntegerType(); + } + + if (in_array($specifier, ['e', 'E', 'f'], true)) { + $type = new FloatType(); + } + + $type = TypeCombinator::addNull($type); + $arrayBuilder->setOffsetValueType(new ConstantIntegerType($i), $type); + } + + return TypeCombinator::addNull($arrayBuilder->getArray()); + } + + return null; + } + +} diff --git a/src/Type/Php/StatDynamicReturnTypeExtension.php b/src/Type/Php/StatDynamicReturnTypeExtension.php index 7b8c08f194..4a0141b106 100644 --- a/src/Type/Php/StatDynamicReturnTypeExtension.php +++ b/src/Type/Php/StatDynamicReturnTypeExtension.php @@ -18,7 +18,7 @@ use SplFileObject; use function in_array; -class StatDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, DynamicMethodReturnTypeExtension +final class StatDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, DynamicMethodReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php new file mode 100644 index 0000000000..c6226f5a5f --- /dev/null +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -0,0 +1,170 @@ + minimum arity] + */ + private const FUNCTIONS = [ + 'strtoupper' => 1, + 'strtolower' => 1, + 'mb_strtoupper' => 1, + 'mb_strtolower' => 1, + 'lcfirst' => 1, + 'ucfirst' => 1, + 'mb_lcfirst' => 1, + 'mb_ucfirst' => 1, + 'ucwords' => 1, + 'mb_convert_case' => 2, + 'mb_convert_kana' => 1, + ]; + + public function isFunctionSupported(FunctionReflection $functionReflection): bool + { + return isset(self::FUNCTIONS[$functionReflection->getName()]); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $fnName = $functionReflection->getName(); + $args = $functionCall->getArgs(); + + if (count($args) < self::FUNCTIONS[$fnName]) { + return null; + } + + $argType = $scope->getType($args[0]->value); + if (!is_callable($fnName)) { + return null; + } + + $modes = []; + $keepLowercase = false; + $forceLowercase = false; + $keepUppercase = false; + $forceUppercase = false; + + if ($fnName === 'mb_convert_case') { + $modeType = $scope->getType($args[1]->value); + $modes = array_map(static fn ($mode) => $mode->getValue(), TypeUtils::getConstantIntegers($modeType)); + if (count($modes) > 0) { + $forceLowercase = count(array_diff($modes, [ + MB_CASE_LOWER, + 5, // MB_CASE_LOWER_SIMPLE + ])) === 0; + $keepLowercase = count(array_diff($modes, [ + MB_CASE_LOWER, + 5, // MB_CASE_LOWER_SIMPLE + 3, // MB_CASE_FOLD, + 7, // MB_CASE_FOLD_SIMPLE + ])) === 0; + $forceUppercase = count(array_diff($modes, [ + MB_CASE_UPPER, + 4, // MB_CASE_UPPER_SIMPLE + ])) === 0; + $keepUppercase = count(array_diff($modes, [ + MB_CASE_UPPER, + 4, // MB_CASE_UPPER_SIMPLE + 3, // MB_CASE_FOLD, + 7, // MB_CASE_FOLD_SIMPLE + ])) === 0; + } + } elseif (in_array($fnName, ['ucwords', 'mb_convert_kana'], true)) { + if (count($args) >= 2) { + $modeType = $scope->getType($args[1]->value); + $modes = array_map(static fn ($mode) => $mode->getValue(), $modeType->getConstantStrings()); + } else { + $modes = $fnName === 'mb_convert_kana' ? ['KV'] : [" \t\r\n\f\v"]; + } + } elseif (in_array($fnName, ['strtolower', 'mb_strtolower'], true)) { + $forceLowercase = true; + } elseif (in_array($fnName, ['lcfirst', 'mb_lcfirst'], true)) { + $keepLowercase = true; + } elseif (in_array($fnName, ['strtoupper', 'mb_strtoupper'], true)) { + $forceUppercase = true; + } elseif (in_array($fnName, ['ucfirst', 'mb_ucfirst'], true)) { + $keepUppercase = true; + } + + $constantStrings = array_map(static fn ($type) => $type->getValue(), $argType->getConstantStrings()); + if (count($constantStrings) > 0 && mb_check_encoding($constantStrings, 'UTF-8')) { + $strings = []; + + $parameters = []; + if (in_array($fnName, ['ucwords', 'mb_convert_case', 'mb_convert_kana'], true)) { + foreach ($modes as $mode) { + foreach ($constantStrings as $constantString) { + $parameters[] = [$constantString, $mode]; + } + } + } else { + $parameters = array_map(static fn ($s) => [$s], $constantStrings); + } + + foreach ($parameters as $parameter) { + $strings[] = $fnName(...$parameter); + } + + if (count($strings) !== 0 && mb_check_encoding($strings, 'UTF-8')) { + return TypeCombinator::union(...array_map(static fn ($s) => new ConstantStringType($s), $strings)); + } + } + + $accessoryTypes = []; + $argStringType = $argType->toString(); + if ($forceLowercase || ($keepLowercase && $argStringType->isLowercaseString()->yes())) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + if ($forceUppercase || ($keepUppercase && $argStringType->isUppercaseString()->yes())) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } + + if ($argStringType->isNumericString()->yes()) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } elseif ($argStringType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($argStringType->isNonEmptyString()->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + + return new IntersectionType($accessoryTypes); + } + + return new StringType(); + } + +} diff --git a/src/Type/Php/StrContainingTypeSpecifyingExtension.php b/src/Type/Php/StrContainingTypeSpecifyingExtension.php new file mode 100644 index 0000000000..655e77ddd3 --- /dev/null +++ b/src/Type/Php/StrContainingTypeSpecifyingExtension.php @@ -0,0 +1,109 @@ + [1, 0], + 'str_contains' => [0, 1], + 'str_starts_with' => [0, 1], + 'str_ends_with' => [0, 1], + 'strpos' => [0, 1], + 'strrpos' => [0, 1], + 'stripos' => [0, 1], + 'strripos' => [0, 1], + 'strstr' => [0, 1], + 'mb_strpos' => [0, 1], + 'mb_strrpos' => [0, 1], + 'mb_stripos' => [0, 1], + 'mb_strripos' => [0, 1], + 'mb_strstr' => [0, 1], + ]; + + private TypeSpecifier $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool + { + return array_key_exists(strtolower($functionReflection->getName()), self::STR_CONTAINING_FUNCTIONS) + && $context->true(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + + if (count($args) >= 2) { + [$hackstackArg, $needleArg] = self::STR_CONTAINING_FUNCTIONS[strtolower($functionReflection->getName())]; + + $haystackType = $scope->getType($args[$hackstackArg]->value); + $needleType = $scope->getType($args[$needleArg]->value)->toString(); + + if ($needleType->isNonEmptyString()->yes() && $haystackType->isString()->yes()) { + $accessories = [ + new StringType(), + ]; + + if ($needleType->isNonFalsyString()->yes()) { + $accessories[] = new AccessoryNonFalsyStringType(); + } else { + $accessories[] = new AccessoryNonEmptyStringType(); + } + + if ($haystackType->isLiteralString()->yes()) { + $accessories[] = new AccessoryLiteralStringType(); + } + if ($haystackType->isNumericString()->yes()) { + $accessories[] = new AccessoryNumericStringType(); + } + + return $this->typeSpecifier->create( + $args[$hackstackArg]->value, + new IntersectionType($accessories), + $context, + $scope, + )->setRootExpr(new BooleanAnd( + new NotIdentical( + $args[$needleArg]->value, + new String_(''), + ), + new FuncCall(new Name('FAUX_FUNCTION'), [ + new Arg($args[$needleArg]->value), + ]), + )); + } + } + + return new SpecifiedTypes(); + } + +} diff --git a/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php b/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..f44a76b71c --- /dev/null +++ b/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php @@ -0,0 +1,143 @@ +getName(), ['str_increment', 'str_decrement'], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $fnName = $functionReflection->getName(); + $args = $functionCall->getArgs(); + + if (count($args) !== 1) { + return null; + } + + $argType = $scope->getType($args[0]->value); + if (count($argType->getConstantScalarValues()) === 0) { + return null; + } + + $types = []; + foreach ($argType->getConstantScalarValues() as $value) { + if (!(is_string($value) || is_int($value) || is_float($value))) { + continue; + } + $string = (string) $value; + + if (preg_match('/\A(?:0|[1-9A-Za-z][0-9A-Za-z]*)+\z/', $string) < 1) { + continue; + } + + $result = null; + if ($fnName === 'str_increment') { + $result = $this->increment($string); + } elseif ($fnName === 'str_decrement') { + $result = $this->decrement($string); + } + + if ($result === null) { + continue; + } + + $types[] = new ConstantStringType($result); + } + + return count($types) === 0 + ? new ErrorType() + : TypeCombinator::union(...$types); + } + + private function increment(string $s): string + { + if (is_numeric($s)) { + $offset = stripos($s, 'e'); + if ($offset !== false) { + // Using increment operator would cast the string to float + // Therefore we manually increment it to convert it to an "f"/"F" that doesn't get affected + $c = $s[$offset]; + $c++; + $s[$offset] = $c; + $s++; + $s[$offset] = [ + 'f' => 'e', + 'F' => 'E', + 'g' => 'f', + 'G' => 'F', + ][$s[$offset]]; + + return $s; + } + } + + return (string) ++$s; + } + + private function decrement(string $s): ?string + { + if (in_array($s, ['a', 'A', '0'], true)) { + return null; + } + + $decremented = str_split($s, 1); + $position = count($decremented) - 1; + $carry = false; + $map = [ + '0' => '9', + 'A' => 'Z', + 'a' => 'z', + ]; + do { + $c = $decremented[$position]; + if (!in_array($c, ['a', 'A', '0'], true)) { + $carry = false; + $decremented[$position] = chr(ord($c) - 1); + } else { + $carry = true; + $decremented[$position] = $map[$c]; + } + } while ($carry && $position-- > 0); + + if ($carry || count($decremented) > 1 && $decremented[0] === '0') { + if (count($decremented) === 1) { + return null; + } + + unset($decremented[0]); + } + + return implode($decremented); + } + +} diff --git a/src/Type/Php/StrPadFunctionReturnTypeExtension.php b/src/Type/Php/StrPadFunctionReturnTypeExtension.php index 3c7f831331..d556fff1be 100644 --- a/src/Type/Php/StrPadFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrPadFunctionReturnTypeExtension.php @@ -6,7 +6,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; @@ -14,7 +17,7 @@ use PHPStan\Type\Type; use function count; -class StrPadFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class StrPadFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -37,19 +40,26 @@ public function getTypeFromFunctionCall( $lengthType = $scope->getType($args[1]->value); $accessoryTypes = []; - if ($inputType->isNonEmptyString()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()) { + if ($inputType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($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($args) < 3) { + $padStringType = null; + } else { + $padStringType = $scope->getType($args[2]->value); + } + + if ($inputType->isLiteralString()->yes() && ($padStringType === null || $padStringType->isLiteralString()->yes())) { + $accessoryTypes[] = new AccessoryLiteralStringType(); + } + if ($inputType->isLowercaseString()->yes() && ($padStringType === null || $padStringType->isLowercaseString()->yes())) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + if ($inputType->isUppercaseString()->yes() && ($padStringType === null || $padStringType->isUppercaseString()->yes())) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); } if (count($accessoryTypes) > 0) { diff --git a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php index cc8a09a3cd..98afacb7d7 100644 --- a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -2,11 +2,16 @@ namespace PHPStan\Type\Php; +use Nette\Utils\Strings; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -17,8 +22,9 @@ use PHPStan\Type\Type; use function count; use function str_repeat; +use function strlen; -class StrRepeatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class StrRepeatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -37,37 +43,70 @@ public function getTypeFromFunctionCall( 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) { + if (IntegerRangeType::fromInterval(null, 0)->isSuperTypeOf($multiplierType)->yes()) { return new NeverType(); } - if ($inputType instanceof ConstantStringType && $multiplierType instanceof ConstantIntegerType) { + $inputType = $scope->getType($args[0]->value); + if ( + $inputType instanceof ConstantStringType + && $multiplierType instanceof ConstantIntegerType + // don't generate type too big to avoid hitting memory limit + && strlen($inputType->getValue()) * $multiplierType->getValue() < 100 + ) { 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->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } else { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } } } if ($inputType->isLiteralString()->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); + + if ( + $inputType->isNumericString()->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($multiplierType)->yes() + ) { + $onlyNumbers = true; + foreach ($inputType->getConstantStrings() as $constantString) { + if (Strings::match($constantString->getValue(), '#^[0-9]+$#') === null) { + $onlyNumbers = false; + break; + } + } + + if ($onlyNumbers) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } + } + } + + if ($inputType->isLowercaseString()->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + + if ($inputType->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); } 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 dce61f03fd..9c28bc3c69 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -4,9 +4,11 @@ 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\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -18,39 +20,22 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use function array_is_list; 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 { - /** @var string[] */ - private array $supportedEncodings; + use MbFunctionsReturnTypeExtensionTrait; - 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 ShouldNotHappenException(); - } - $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); - } - } - $this->supportedEncodings = array_map('strtoupper', $supportedEncodings); } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -58,12 +43,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return in_array($functionReflection->getName(), ['str_split', 'mb_str_split'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->getArgs()) < 1) { - return $defaultReturnType; + return null; } if (count($functionCall->getArgs()) >= 2) { @@ -78,13 +61,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $splitLength = 1; } + $encoding = null; if ($functionReflection->getName() === 'mb_str_split') { if (count($functionCall->getArgs()) >= 3) { - $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[2]->value)); + $strings = $scope->getType($functionCall->getArgs()[2]->value)->getConstantStrings(); $values = array_unique(array_map(static fn (ConstantStringType $encoding): string => $encoding->getValue(), $strings)); if (count($values) !== 1) { - return $defaultReturnType; + return null; } $encoding = $values[0]; @@ -97,31 +81,33 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (!isset($splitLength)) { - return $defaultReturnType; + return null; } $stringType = $scope->getType($functionCall->getArgs()[0]->value); - if (!$stringType instanceof ConstantStringType) { - 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 ($items === false) { - throw new ShouldNotHappenException(); + $constantStrings = $stringType->getConstantStrings(); + if (count($constantStrings) > 0) { + $results = []; + foreach ($constantStrings as $constantString) { + $items = $encoding === null + ? str_split($constantString->getValue(), $splitLength) + : @mb_str_split($constantString->getValue(), $splitLength, $encoding); + if ($items === false) { + throw new ShouldNotHappenException(); + } + + $results[] = self::createConstantArrayFrom($items, $scope); + } + + return TypeCombinator::union(...$results); } - return self::createConstantArrayFrom($items, $scope); - } + $returnType = TypeCombinator::intersect(new ArrayType(new IntegerType(), new StringType()), new AccessoryArrayListType()); - private function isSupportedEncoding(string $encoding): bool - { - return in_array(strtoupper($encoding), $this->supportedEncodings, true); + return $encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray() + ? TypeCombinator::intersect($returnType, new NonEmptyArrayType()) + : $returnType; } /** @@ -147,7 +133,7 @@ private static function createConstantArrayFrom(array $constantArray, Scope $sco $i++; } - return new ConstantArrayType($keyTypes, $valueTypes, $isList ? $i : 0); + return new ConstantArrayType($keyTypes, $valueTypes, $isList ? [$i] : [0], [], TrinaryLogic::createFromBoolean(array_is_list($constantArray))); } } diff --git a/src/Type/Php/StrTokFunctionReturnTypeExtension.php b/src/Type/Php/StrTokFunctionReturnTypeExtension.php index 0d82a17a37..85efaa5ad2 100644 --- a/src/Type/Php/StrTokFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrTokFunctionReturnTypeExtension.php @@ -5,15 +5,16 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use function count; -class StrTokFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class StrTokFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,11 +22,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'strtok'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $args = $functionCall->getArgs(); if (count($args) !== 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType(); + return null; } $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); @@ -35,10 +36,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($isEmptyString->no()) { - return new StringType(); + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); } - return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php index 96a532ceef..33ed245739 100644 --- a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php @@ -16,7 +16,7 @@ use PHPStan\Type\UnionType; use function count; -class StrWordCountFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class StrWordCountFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/StrlenFunctionReturnTypeExtension.php b/src/Type/Php/StrlenFunctionReturnTypeExtension.php index e06ccc1e52..bc91f8595b 100644 --- a/src/Type/Php/StrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrlenFunctionReturnTypeExtension.php @@ -5,18 +5,23 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\FloatType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\TypeCombinator; +use function array_map; +use function array_unique; use function count; +use function max; +use function min; +use function range; +use function sort; use function strlen; -class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -28,57 +33,47 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($args[0]->value); + $constantScalars = $argType->getConstantScalarValues(); - $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); + $lengths = []; + foreach ($constantScalars as $constantScalar) { + $stringScalar = (string) $constantScalar; + $length = strlen($stringScalar); + $lengths[] = $length; } $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); + $numeric = TypeCombinator::union(new IntegerType(), new FloatType()); + $range = null; + if (count($lengths) > 0) { + $lengths = array_unique($lengths); + sort($lengths); + if ($lengths === range(min($lengths), max($lengths))) { + $range = IntegerRangeType::fromInterval(min($lengths), max($lengths)); + } else { + $range = TypeCombinator::union(...array_map(static fn ($l) => new ConstantIntegerType($l), $lengths)); + } + } elseif ($argType->isBoolean()->yes()) { + $range = IntegerRangeType::fromInterval(0, 1); + } elseif ( + $isNonEmpty->yes() + || $numeric->isSuperTypeOf($argType)->yes() + || TypeCombinator::remove($argType, $numeric)->isNonEmptyString()->yes() + ) { + $range = IntegerRangeType::fromInterval(1, null); + } elseif ($argType->isString()->yes() && $isNonEmpty->no()) { + $range = new ConstantIntegerType(0); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return $range; } } diff --git a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php index 304b5401d3..084ee527af 100644 --- a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php @@ -21,7 +21,7 @@ use function min; use function strtotime; -class StrtotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class StrtotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -31,7 +31,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); if (count($functionCall->getArgs()) === 0) { return $defaultReturnType; } @@ -39,7 +43,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($argType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $results = array_unique(array_map(static fn (ConstantStringType $string): int|bool => strtotime($string->getValue()), TypeUtils::getConstantStrings($argType))); + $results = array_unique(array_map(static fn (ConstantStringType $string): int|bool => strtotime($string->getValue()), $argType->getConstantStrings())); $resultTypes = array_unique(array_map(static fn (int|bool $value): string => gettype($value), $results)); if (count($resultTypes) !== 1 || count($results) === 0) { diff --git a/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php index cef472cf06..8ed6c637fc 100644 --- a/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php @@ -7,12 +7,15 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; +use PHPStan\Type\FloatType; +use PHPStan\Type\IntegerType; use PHPStan\Type\NullType; use PHPStan\Type\Type; use function count; use function in_array; -class StrvalFamilyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class StrvalFamilyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const FUNCTIONS = [ @@ -44,12 +47,14 @@ public function getTypeFromFunctionCall( case 'strval': return $argType->toString(); case 'intval': - return $argType->toInteger(); + $type = $argType->toInteger(); + return $type instanceof ErrorType ? new IntegerType() : $type; case 'boolval': return $argType->toBoolean(); case 'floatval': case 'doubleval': - return $argType->toFloat(); + $type = $argType->toFloat(); + return $type instanceof ErrorType ? new FloatType() : $type; default: throw new ShouldNotHappenException(); } diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 3aa09682cc..df1d0cf060 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -4,58 +4,127 @@ 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\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; +use function in_array; +use function is_bool; +use function mb_substr; +use function substr; -class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'substr'; + return in_array($functionReflection->getName(), ['substr', 'mb_substr'], true); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $args = $functionCall->getArgs(); - if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (count($args) < 2) { + return null; + } + + $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(); + $length = null; + $positiveLength = false; + $maybeOneLength = false; + + if (count($args) === 3) { + $length = $scope->getType($args[2]->value); + $positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes(); + $maybeOneLength = !(new ConstantIntegerType(1))->isSuperTypeOf($length)->no(); } - if (count($args) >= 2) { - $string = $scope->getType($args[0]->value); - $offset = $scope->getType($args[1]->value); + $constantStrings = $string->getConstantStrings(); + if ( + count($constantStrings) > 0 + && $offset instanceof ConstantIntegerType + && ($length === null || $length instanceof ConstantIntegerType) + ) { + $results = []; + foreach ($constantStrings as $constantString) { + if ($length !== null) { + if ($functionReflection->getName() === 'mb_substr') { + $substr = mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue()); + } else { + $substr = substr($constantString->getValue(), $offset->getValue(), $length->getValue()); + } + } else { + if ($functionReflection->getName() === 'mb_substr') { + $substr = mb_substr($constantString->getValue(), $offset->getValue()); + } else { + $substr = substr($constantString->getValue(), $offset->getValue()); + } + } - $negativeOffset = IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($offset)->yes(); - $zeroOffset = (new ConstantIntegerType(0))->isSuperTypeOf($offset)->yes(); - $positiveLength = false; + if (is_bool($substr)) { + $results[] = new ConstantBooleanType($substr); + } else { + $results[] = new ConstantStringType($substr); + } + } + + return TypeCombinator::union(...$results); + } - if (count($args) === 3) { - $length = $scope->getType($args[2]->value); - $positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes(); + $accessoryTypes = []; + $isNotEmpty = false; + if ($string->isLowercaseString()->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + if ($string->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } + if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { + $isNotEmpty = true; + if ($string->isNonFalsyString()->yes() && !$maybeOneLength) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } else { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); } + } + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); - if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + if (!$isNotEmpty && $this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + return TypeCombinator::union( + new ConstantBooleanType(false), + new IntersectionType($accessoryTypes), + ); } + + return new IntersectionType($accessoryTypes); } - return new StringType(); + return null; } } diff --git a/src/Type/Php/ThrowableReturnTypeExtension.php b/src/Type/Php/ThrowableReturnTypeExtension.php index fa3a1a8d3b..9437b82485 100644 --- a/src/Type/Php/ThrowableReturnTypeExtension.php +++ b/src/Type/Php/ThrowableReturnTypeExtension.php @@ -13,7 +13,6 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use Throwable; use function count; use function in_array; @@ -37,7 +36,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $type = $scope->getType($methodCall->var); $types = []; $pdoException = new ObjectType('PDOException'); - foreach (TypeUtils::getDirectClassNames($type) as $class) { + foreach ($type->getObjectClassNames() as $class) { $classType = new ObjectType($class); if ($classType->getClassReflection() !== null) { $classReflection = $classType->getClassReflection(); diff --git a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php index 1f7566f28e..c05c6f6cef 100644 --- a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php @@ -4,37 +4,65 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use function count; +use function in_array; +use const E_USER_DEPRECATED; use const E_USER_ERROR; +use const E_USER_NOTICE; +use const E_USER_WARNING; -class TriggerErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class TriggerErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'trigger_error'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + + if (count($args) === 0) { + return null; + } + + if (count($args) === 1) { + return new ConstantBooleanType(true); } - $errorType = $scope->getType($functionCall->getArgs()[1]->value); - if ($errorType instanceof ConstantScalarType) { - if ($errorType->getValue() === E_USER_ERROR) { + $errorType = $scope->getType($args[1]->value); + + if ($errorType instanceof ConstantIntegerType) { + $errorLevel = $errorType->getValue(); + + if ($errorLevel === E_USER_ERROR) { return new NeverType(true); } + + if (!in_array($errorLevel, [E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED], true)) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(true); + } + + return new ConstantBooleanType(false); + } + + return new ConstantBooleanType(true); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..df06d98b43 --- /dev/null +++ b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,52 @@ +getName(), ['trim', 'rtrim', 'ltrim'], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $stringType = $scope->getType($args[0]->value); + $accessory = []; + if ($stringType->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + return new IntersectionType($accessory); + } + + return new StringType(); + } + +} diff --git a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php index 08eaf84e5b..a6ab212328 100644 --- a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Type\Constant\ConstantBooleanType; @@ -16,7 +15,7 @@ use function count; use function in_array; -class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, TypeSpecifierAwareExtension +final class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -39,23 +38,9 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo { return in_array($functionReflection->getName(), [ 'array_key_exists', + 'key_exists', 'in_array', - 'is_numeric', - 'is_int', - 'is_array', - 'is_bool', - 'is_callable', - 'is_float', - 'is_double', - 'is_real', - 'is_iterable', - 'is_null', - 'is_object', - 'is_resource', - 'is_scalar', - 'is_string', 'is_subclass_of', - 'is_countable', ], true); } @@ -63,10 +48,10 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $isAlways = $this->getHelper()->findSpecifiedType( @@ -74,7 +59,7 @@ public function getTypeFromFunctionCall( $functionCall, ); if ($isAlways === null) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } return new ConstantBooleanType($isAlways); diff --git a/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php deleted file mode 100644 index 7a6aa95976..0000000000 --- a/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php +++ /dev/null @@ -1,64 +0,0 @@ -getName(), - [ - 'var_export', - 'highlight_file', - 'highlight_string', - 'print_r', - ], - true, - ); - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, Node\Expr\FuncCall $functionCall, Scope $scope): Type - { - if ($functionReflection->getName() === 'var_export') { - $fallbackReturnType = new NullType(); - } elseif ($functionReflection->getName() === 'print_r') { - $fallbackReturnType = new ConstantBooleanType(true); - } else { - $fallbackReturnType = new BooleanType(); - } - - if (count($functionCall->getArgs()) < 1) { - return TypeCombinator::union( - new StringType(), - $fallbackReturnType, - ); - } - - if (count($functionCall->getArgs()) < 2) { - return $fallbackReturnType; - } - - $returnArgumentType = $scope->getType($functionCall->getArgs()[1]->value); - if ((new ConstantBooleanType(true))->isSuperTypeOf($returnArgumentType)->yes()) { - return new StringType(); - } - - return $fallbackReturnType; - } - -} diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index f695b20899..7ca5d8bac9 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -2,24 +2,37 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\ComposerPhpVersionFactory; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; 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 is_array; use function version_compare; -class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + /** + * @param int|array{min: int, max: int}|null $configPhpVersion + */ + public function __construct( + private int|array|null $configPhpVersion, + private ComposerPhpVersionFactory $composerPhpVersionFactory, + ) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'version_compare'; @@ -29,21 +42,22 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { - if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; } - $version1Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[0]->value)); - $version2Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[1]->value)); + $version1Strings = $this->getVersionStrings($args[0]->value, $scope); + $version2Strings = $this->getVersionStrings($args[1]->value, $scope); $counts = [ count($version1Strings), count($version2Strings), ]; - if (isset($functionCall->getArgs()[2])) { - $operatorStrings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[2]->value)); + if (isset($args[2])) { + $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); $counts[] = count($operatorStrings); $returnType = new BooleanType(); } else { @@ -79,4 +93,32 @@ public function getTypeFromFunctionCall( return TypeCombinator::union(...$types); } + /** + * @return ConstantStringType[] + */ + private function getVersionStrings(Expr $expr, Scope $scope): array + { + if ( + $expr instanceof Expr\ConstFetch + && $expr->name->toString() === 'PHP_VERSION' + ) { + if (is_array($this->configPhpVersion)) { + $minVersion = new PhpVersion($this->configPhpVersion['min']); + $maxVersion = new PhpVersion($this->configPhpVersion['max']); + } else { + $minVersion = $this->composerPhpVersionFactory->getMinVersion(); + $maxVersion = $this->composerPhpVersionFactory->getMaxVersion(); + } + + if ($minVersion !== null && $maxVersion !== null) { + return [ + new ConstantStringType($minVersion->getVersionString()), + new ConstantStringType($maxVersion->getVersionString()), + ]; + } + } + + return $scope->getType($expr)->getConstantStrings(); + } + } diff --git a/src/Type/Php/XMLReaderOpenReturnTypeExtension.php b/src/Type/Php/XMLReaderOpenReturnTypeExtension.php index 62e820b703..26551ff830 100644 --- a/src/Type/Php/XMLReaderOpenReturnTypeExtension.php +++ b/src/Type/Php/XMLReaderOpenReturnTypeExtension.php @@ -14,7 +14,7 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class XMLReaderOpenReturnTypeExtension implements DynamicMethodReturnTypeExtension, DynamicStaticMethodReturnTypeExtension +final class XMLReaderOpenReturnTypeExtension implements DynamicMethodReturnTypeExtension, DynamicStaticMethodReturnTypeExtension { private const XML_READER_CLASS = 'XMLReader'; diff --git a/src/Type/RecursionGuard.php b/src/Type/RecursionGuard.php index 2149fb1015..aeecba0db7 100644 --- a/src/Type/RecursionGuard.php +++ b/src/Type/RecursionGuard.php @@ -2,17 +2,18 @@ namespace PHPStan\Type; -class RecursionGuard +final class RecursionGuard { /** @var true[] */ private static array $context = []; /** - * @param callable(): Type $callback - * + * @template T + * @param callable(): T $callback + * @return T|ErrorType */ - public static function run(Type $type, callable $callback): Type + public static function run(Type $type, callable $callback) { $key = $type->describe(VerbosityLevel::value()); if (isset(self::$context[$key])) { diff --git a/src/Type/Regex/RegexAlternation.php b/src/Type/Regex/RegexAlternation.php new file mode 100644 index 0000000000..9a00411c10 --- /dev/null +++ b/src/Type/Regex/RegexAlternation.php @@ -0,0 +1,47 @@ +> */ + private array $groupCombinations = []; + + public function __construct( + private readonly int $alternationId, + private readonly int $alternationsCount, + ) + { + } + + public function getId(): int + { + return $this->alternationId; + } + + public function pushGroup(int $combinationIndex, RegexCapturingGroup $group): void + { + if (!array_key_exists($combinationIndex, $this->groupCombinations)) { + $this->groupCombinations[$combinationIndex] = []; + } + + $this->groupCombinations[$combinationIndex][] = $group->getId(); + } + + public function getAlternationsCount(): int + { + return $this->alternationsCount; + } + + /** + * @return array> + */ + public function getGroupCombinations(): array + { + return $this->groupCombinations; + } + +} diff --git a/src/Type/Regex/RegexAstWalkResult.php b/src/Type/Regex/RegexAstWalkResult.php new file mode 100644 index 0000000000..ff234b6092 --- /dev/null +++ b/src/Type/Regex/RegexAstWalkResult.php @@ -0,0 +1,130 @@ + $capturingGroups + * @param list $markVerbs + */ + public function __construct( + private int $alternationId, + private int $captureGroupId, + private array $capturingGroups, + private array $markVerbs, + private Type $subjectBaseType, + ) + { + } + + public static function createEmpty(): self + { + return new self( + -1, + // use different start-index for groups to make it easier to distinguish groupids from other ids + 100, + [], + [], + new StringType(), + ); + } + + public function nextAlternationId(): self + { + return new self( + $this->alternationId + 1, + $this->captureGroupId, + $this->capturingGroups, + $this->markVerbs, + $this->subjectBaseType, + ); + } + + public function nextCaptureGroupId(): self + { + return new self( + $this->alternationId, + $this->captureGroupId + 1, + $this->capturingGroups, + $this->markVerbs, + $this->subjectBaseType, + ); + } + + public function addCapturingGroup(RegexCapturingGroup $group): self + { + $capturingGroups = $this->capturingGroups; + $capturingGroups[$group->getId()] = $group; + + return new self( + $this->alternationId, + $this->captureGroupId, + $capturingGroups, + $this->markVerbs, + $this->subjectBaseType, + ); + } + + public function markVerb(string $markVerb): self + { + $verbs = $this->markVerbs; + $verbs[] = $markVerb; + + return new self( + $this->alternationId, + $this->captureGroupId, + $this->capturingGroups, + $verbs, + $this->subjectBaseType, + ); + } + + public function withSubjectBaseType(Type $subjectBaseType): self + { + return new self( + $this->alternationId, + $this->captureGroupId, + $this->capturingGroups, + $this->markVerbs, + $subjectBaseType, + ); + } + + public function getAlternationId(): int + { + return $this->alternationId; + } + + public function getCaptureGroupId(): int + { + return $this->captureGroupId; + } + + /** + * @return array + */ + public function getCapturingGroups(): array + { + return $this->capturingGroups; + } + + /** + * @return list + */ + public function getMarkVerbs(): array + { + return $this->markVerbs; + } + + public function getSubjectBaseType(): Type + { + return $this->subjectBaseType; + } + +} diff --git a/src/Type/Regex/RegexCapturingGroup.php b/src/Type/Regex/RegexCapturingGroup.php new file mode 100644 index 0000000000..3cc16fa182 --- /dev/null +++ b/src/Type/Regex/RegexCapturingGroup.php @@ -0,0 +1,160 @@ +id; + } + + public function forceNonOptional(): self + { + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $this->parent, + $this->type, + true, + $this->forceType, + ); + } + + public function forceType(Type $type): self + { + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $this->parent, + $type, + $this->forceNonOptional, + $this->forceType, + ); + } + + public function withParent(RegexCapturingGroup|RegexNonCapturingGroup $parent): self + { + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $parent, + $this->type, + $this->forceNonOptional, + $this->forceType, + ); + } + + public function resetsGroupCounter(): bool + { + return $this->parent instanceof RegexNonCapturingGroup && $this->parent->resetsGroupCounter(); + } + + /** + * @phpstan-assert-if-true !null $this->getAlternationId() + * @phpstan-assert-if-true !null $this->getAlternation() + */ + public function inAlternation(): bool + { + return $this->alternation !== null; + } + + public function getAlternation(): ?RegexAlternation + { + return $this->alternation; + } + + public function getAlternationId(): ?int + { + if ($this->alternation === null) { + return null; + } + + return $this->alternation->getId(); + } + + public function isOptional(): bool + { + if ($this->forceNonOptional) { + return false; + } + + return $this->inAlternation() + || $this->inOptionalQuantification + || $this->parent !== null && $this->parent->isOptional(); + } + + public function inOptionalQuantification(): bool + { + return $this->inOptionalQuantification; + } + + public function inOptionalAlternation(): bool + { + if (!$this->inAlternation()) { + return false; + } + + $parent = $this->parent; + while ($parent !== null && $parent->getAlternationId() === $this->getAlternationId()) { + if (!$parent instanceof RegexNonCapturingGroup) { + return false; + } + $parent = $parent->getParent(); + } + return $parent !== null && $parent->isOptional(); + } + + public function isTopLevel(): bool + { + return $this->parent === null + || $this->parent instanceof RegexNonCapturingGroup && $this->parent->isTopLevel(); + } + + /** @phpstan-assert-if-true !null $this->getName() */ + public function isNamed(): bool + { + return $this->name !== null; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getType(): Type + { + if ($this->forceType !== null) { + return $this->forceType; + } + return $this->type; + } + + public function getParent(): RegexCapturingGroup|RegexNonCapturingGroup|null + { + return $this->parent; + } + +} diff --git a/src/Type/Regex/RegexExpressionHelper.php b/src/Type/Regex/RegexExpressionHelper.php new file mode 100644 index 0000000000..0df0dc011c --- /dev/null +++ b/src/Type/Regex/RegexExpressionHelper.php @@ -0,0 +1,162 @@ +name instanceof Name + && $expr->name->toLowerString() === 'preg_quote' + ) { + return new ConstantStringType('(?:.*)'); + } + + if ($expr instanceof Concat) { + $left = $this->resolve($expr->left); + $right = $this->resolve($expr->right); + + $strings = []; + foreach ($left->toString()->getConstantStrings() as $leftString) { + foreach ($right->toString()->getConstantStrings() as $rightString) { + $strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue()); + } + } + + return TypeCombinator::union(...$strings); + } + + return $this->scope->getType($expr); + } + + }; + + return $this->initializerExprTypeResolver->getConcatType($concat->left, $concat->right, static fn (Expr $expr): Type => $resolver->resolve($expr)); + } + + public function getPatternModifiers(string $pattern): ?string + { + $endDelimiterPos = $this->getEndDelimiterPos($pattern); + + if ($endDelimiterPos === false) { + return null; + } + + return substr($pattern, $endDelimiterPos + 1); + } + + public function removeDelimitersAndModifiers(string $pattern): string + { + $pattern = ltrim($pattern); + + $endDelimiterPos = $this->getEndDelimiterPos($pattern); + + if ($endDelimiterPos === false) { + return $pattern; + } + + return substr($pattern, 1, $endDelimiterPos - 1); + } + + private function getEndDelimiterPos(string $pattern): false|int + { + $startDelimiter = $this->getPatternDelimiter($pattern); + if ($startDelimiter === null) { + return false; + } + + // delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php + $bracketStyleDelimiters = [ + '{' => '}', + '(' => ')', + '[' => ']', + '<' => '>', + ]; + if (array_key_exists($startDelimiter, $bracketStyleDelimiters)) { + $endDelimiterPos = strrpos($pattern, $bracketStyleDelimiters[$startDelimiter]); + } else { + // same start and end delimiter + $endDelimiterPos = strrpos($pattern, $startDelimiter); + } + + return $endDelimiterPos; + } + + /** + * Get delimiters from non-constant patterns, if possible. + * + * @return string[] + */ + public function getPatternDelimiters(Concat $concat, Scope $scope): array + { + if ($concat->left instanceof Concat) { + return $this->getPatternDelimiters($concat->left, $scope); + } + + $left = $scope->getType($concat->left); + + $delimiters = []; + foreach ($left->getConstantStrings() as $leftString) { + $delimiter = $this->getPatternDelimiter($leftString->getValue()); + if ($delimiter === null) { + continue; + } + + $delimiters[] = $delimiter; + } + return $delimiters; + } + + private function getPatternDelimiter(string $regex): ?string + { + $regex = ltrim($regex); + + if ($regex === '') { + return null; + } + + return substr($regex, 0, 1); + } + +} diff --git a/src/Type/Regex/RegexGroupList.php b/src/Type/Regex/RegexGroupList.php new file mode 100644 index 0000000000..d5f624f5df --- /dev/null +++ b/src/Type/Regex/RegexGroupList.php @@ -0,0 +1,166 @@ + + */ +final class RegexGroupList implements Countable, IteratorAggregate +{ + + /** + * @param array $groups + */ + public function __construct( + private readonly array $groups, + ) + { + } + + public function countTrailingOptionals(): int + { + $trailingOptionals = 0; + foreach (array_reverse($this->groups) as $captureGroup) { + if (!$captureGroup->isOptional()) { + break; + } + $trailingOptionals++; + } + return $trailingOptionals; + } + + public function forceGroupNonOptional(RegexCapturingGroup $group): self + { + return $this->cloneAndReParentList($group); + } + + public function forceGroupTypeAndNonOptional(RegexCapturingGroup $group, Type $type): self + { + return $this->cloneAndReParentList($group, $type); + } + + private function cloneAndReParentList(RegexCapturingGroup $target, ?Type $type = null): self + { + $groups = []; + $forcedGroup = null; + foreach ($this->groups as $i => $group) { + if ($group->getId() === $target->getId()) { + $forcedGroup = $group->forceNonOptional(); + if ($type !== null) { + $forcedGroup = $forcedGroup->forceType($type); + } + $groups[$i] = $forcedGroup; + + continue; + } + + $groups[$i] = $group; + } + + if ($forcedGroup === null) { + throw new ShouldNotHappenException(); + } + + foreach ($groups as $i => $group) { + $parent = $group->getParent(); + + while ($parent !== null) { + if ($parent instanceof RegexNonCapturingGroup) { + $parent = $parent->getParent(); + continue; + } + + if ($parent->getId() === $target->getId()) { + $groups[$i] = $groups[$i]->withParent($forcedGroup); + } + $parent = $parent->getParent(); + } + } + + return new self($groups); + } + + public function removeGroup(RegexCapturingGroup $remove): self + { + $groups = []; + foreach ($this->groups as $i => $group) { + if ($group->getId() === $remove->getId()) { + continue; + } + + $groups[$i] = $group; + } + + return new self($groups); + } + + public function getOnlyOptionalTopLevelGroup(): ?RegexCapturingGroup + { + $group = null; + foreach ($this->groups as $captureGroup) { + if (!$captureGroup->isTopLevel()) { + continue; + } + + if (!$captureGroup->isOptional()) { + return null; + } + + if ($group !== null) { + return null; + } + + $group = $captureGroup; + } + + return $group; + } + + public function getOnlyTopLevelAlternation(): ?RegexAlternation + { + $alternation = null; + foreach ($this->groups as $captureGroup) { + if (!$captureGroup->isTopLevel()) { + continue; + } + + if (!$captureGroup->inAlternation()) { + return null; + } + + if ($captureGroup->inOptionalQuantification()) { + return null; + } + + if ($alternation === null) { + $alternation = $captureGroup->getAlternation(); + } elseif ($alternation->getId() !== $captureGroup->getAlternation()->getId()) { + return null; + } + } + + return $alternation; + } + + public function count(): int + { + return count($this->groups); + } + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->groups); + } + +} diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php new file mode 100644 index 0000000000..2f90e08c22 --- /dev/null +++ b/src/Type/Regex/RegexGroupParser.php @@ -0,0 +1,721 @@ +regexExpressionHelper->getPatternModifiers($regex) ?? ''; + foreach (self::NOT_SUPPORTED_MODIFIERS as $notSupportedModifier) { + if (str_contains($modifiers, $notSupportedModifier)) { + return null; + } + } + + if (str_contains($modifiers, 'x')) { + // in freespacing mode the # character starts a comment and runs until the end of the line + $regex = preg_replace('/(?regexExpressionHelper->removeDelimitersAndModifiers($regex); + try { + $ast = self::$parser->parse($rawRegex); + } catch (Exception) { + return null; + } + + $this->updateAlternationAstRemoveVerticalBarsAndAddEmptyToken($ast); + $this->updateCapturingAstAddEmptyToken($ast); + + $captureOnlyNamed = false; + if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { + $captureOnlyNamed = str_contains($modifiers, 'n'); + } + + $astWalkResult = $this->walkRegexAst( + $ast, + null, + 0, + false, + null, + $captureOnlyNamed, + false, + $modifiers, + RegexAstWalkResult::createEmpty(), + ); + + $subjectAsGroupResult = $this->walkGroupAst( + $ast, + false, + false, + $modifiers, + RegexGroupWalkResult::createEmpty(), + ); + + if (!$subjectAsGroupResult->mightContainEmptyStringLiteral() && !$this->containsEscapeK($ast)) { + // we could handle numeric-string, in case we know the regex is delimited by ^ and $ + if ($subjectAsGroupResult->isNonFalsy()->yes()) { + $astWalkResult = $astWalkResult->withSubjectBaseType( + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), + ); + } elseif ($subjectAsGroupResult->isNonEmpty()->yes()) { + $astWalkResult = $astWalkResult->withSubjectBaseType( + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + ); + } + } + + return $astWalkResult; + } + + private function createEmptyTokenTreeNode(TreeNode $parentAst): TreeNode + { + return new TreeNode('token', ['token' => 'literal', 'value' => '', 'namespace' => 'default'], [], $parentAst); + } + + private function updateAlternationAstRemoveVerticalBarsAndAddEmptyToken(TreeNode $ast): void + { + $children = $ast->getChildren(); + + foreach ($children as $i => $child) { + $this->updateAlternationAstRemoveVerticalBarsAndAddEmptyToken($child); + + if ($ast->getId() !== '#alternation' || $child->getValueToken() !== 'alternation') { + continue; + } + + unset($children[$i]); + + if ($i !== 0 + && isset($children[$i + 1]) + && $children[$i + 1]->getValueToken() !== 'alternation') { + continue; + } + + $children[$i] = $this->createEmptyTokenTreeNode($ast); + } + + $ast->setChildren(array_values($children)); + } + + private function updateCapturingAstAddEmptyToken(TreeNode $ast): void + { + foreach ($ast->getChildren() as $child) { + $this->updateCapturingAstAddEmptyToken($child); + } + + if ($ast->getId() !== '#capturing' || $ast->getChildren() !== []) { + return; + } + + $emptyAlternationAst = new TreeNode('#alternation', null, [], $ast); + $emptyAlternationAst->setChildren([$this->createEmptyTokenTreeNode($emptyAlternationAst)]); + $ast->setChildren([$emptyAlternationAst]); + } + + private function containsEscapeK(TreeNode $ast): bool + { + if ($ast->getId() === 'token' && $ast->getValueToken() === 'match_point_reset') { + return true; + } + + foreach ($ast->getChildren() as $child) { + if ($this->containsEscapeK($child)) { + return true; + } + } + + return false; + } + + private function walkRegexAst( + TreeNode $ast, + ?RegexAlternation $alternation, + int $combinationIndex, + bool $inOptionalQuantification, + RegexCapturingGroup|RegexNonCapturingGroup|null $parentGroup, + bool $captureOnlyNamed, + bool $repeatedMoreThanOnce, + string $patternModifiers, + RegexAstWalkResult $astWalkResult, + ): RegexAstWalkResult + { + $group = null; + if ($ast->getId() === '#capturing') { + $astWalkResult = $astWalkResult->nextCaptureGroupId(); + + $group = new RegexCapturingGroup( + $astWalkResult->getCaptureGroupId(), + null, + $alternation, + $inOptionalQuantification, + $parentGroup, + $this->createGroupType( + $ast, + $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup), + $patternModifiers, + ), + ); + $parentGroup = $group; + } elseif ($ast->getId() === '#namedcapturing') { + $astWalkResult = $astWalkResult->nextCaptureGroupId(); + + $name = $ast->getChild(0)->getValueValue(); + $group = new RegexCapturingGroup( + $astWalkResult->getCaptureGroupId(), + $name, + $alternation, + $inOptionalQuantification, + $parentGroup, + $this->createGroupType( + $ast, + $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup), + $patternModifiers, + ), + ); + $parentGroup = $group; + } elseif ($ast->getId() === '#noncapturing') { + $group = new RegexNonCapturingGroup( + $alternation, + $inOptionalQuantification, + $parentGroup, + false, + ); + $parentGroup = $group; + } elseif ($ast->getId() === '#noncapturingreset') { + $group = new RegexNonCapturingGroup( + $alternation, + $inOptionalQuantification, + $parentGroup, + true, + ); + $parentGroup = $group; + } + + $inOptionalQuantification = false; + if ($ast->getId() === '#quantification') { + [$min, $max] = $this->getQuantificationRange($ast); + + if ($min === 0) { + $inOptionalQuantification = true; + } + + if ($max === null || $max > 1) { + $repeatedMoreThanOnce = true; + } + } + + if ($ast->getId() === '#alternation') { + $astWalkResult = $astWalkResult->nextAlternationId(); + $alternation = new RegexAlternation($astWalkResult->getAlternationId(), count($ast->getChildren())); + } + + if ($ast->getId() === '#mark') { + return $astWalkResult->markVerb($ast->getChild(0)->getValueValue()); + } + + if ( + $group instanceof RegexCapturingGroup && + (!$captureOnlyNamed || $group->isNamed()) + ) { + $astWalkResult = $astWalkResult->addCapturingGroup($group); + + if ($alternation !== null) { + $alternation->pushGroup($combinationIndex, $group); + } + } + + foreach ($ast->getChildren() as $child) { + $astWalkResult = $this->walkRegexAst( + $child, + $alternation, + $combinationIndex, + $inOptionalQuantification, + $parentGroup, + $captureOnlyNamed, + $repeatedMoreThanOnce, + $patternModifiers, + $astWalkResult, + ); + + if ($ast->getId() !== '#alternation') { + continue; + } + + $combinationIndex++; + } + + return $astWalkResult; + } + + private function allowConstantTypes( + string $patternModifiers, + bool $repeatedMoreThanOnce, + RegexCapturingGroup|RegexNonCapturingGroup|null $parentGroup, + ): bool + { + if (str_contains($patternModifiers, 'i')) { + // if caseless, we don't use constant types + // because it likely yields too many combinations + return false; + } + + if ($repeatedMoreThanOnce) { + return false; + } + + if ($parentGroup !== null && $parentGroup->resetsGroupCounter()) { + return false; + } + + return true; + } + + /** @return array{?int, ?int} */ + private function getQuantificationRange(TreeNode $node): array + { + if ($node->getId() !== '#quantification') { + throw new ShouldNotHappenException(); + } + + $min = null; + $max = null; + + $lastChild = $node->getChild($node->getChildrenNumber() - 1); + $value = $lastChild->getValue(); + + // normalize away possessive and lazy quantifier-modifiers + $token = str_replace(['_possessive', '_lazy'], '', $value['token']); + $value = rtrim($value['value'], '+?'); + + if ($token === 'n_to_m') { + if (sscanf($value, '{%d,%d}', $n, $m) !== 2 || !is_int($n) || !is_int($m)) { + throw new ShouldNotHappenException(); + } + + $min = $n; + $max = $m; + } elseif ($token === 'n_or_more') { + if (sscanf($value, '{%d,}', $n) !== 1 || !is_int($n)) { + throw new ShouldNotHappenException(); + } + + $min = $n; + } elseif ($token === 'exactly_n') { + if (sscanf($value, '{%d}', $n) !== 1 || !is_int($n)) { + throw new ShouldNotHappenException(); + } + + $min = $n; + $max = $n; + } elseif ($token === 'zero_or_one') { + $min = 0; + $max = 1; + } elseif ($token === 'zero_or_more') { + $min = 0; + } elseif ($token === 'one_or_more') { + $min = 1; + } + + return [$min, $max]; + } + + private function createGroupType(TreeNode $group, bool $maybeConstant, string $patternModifiers): Type + { + $rootAlternation = $this->getRootAlternation($group); + if ($rootAlternation !== null) { + $types = []; + foreach ($rootAlternation->getChildren() as $alternative) { + $types[] = $this->createGroupType($alternative, $maybeConstant, $patternModifiers); + } + + return TypeCombinator::union(...$types); + } + + $walkResult = $this->walkGroupAst( + $group, + false, + false, + $patternModifiers, + RegexGroupWalkResult::createEmpty(), + ); + + if ($maybeConstant && $walkResult->getOnlyLiterals() !== null && $walkResult->getOnlyLiterals() !== []) { + $result = []; + foreach ($walkResult->getOnlyLiterals() as $literal) { + $result[] = new ConstantStringType($literal); + + } + return TypeCombinator::union(...$result); + } + + if ($walkResult->isNumeric()->yes()) { + if ($walkResult->isNonFalsy()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryNonFalsyStringType(), + ]); + } + + $result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); + if (!$walkResult->isNonEmpty()->yes()) { + return TypeCombinator::union(new ConstantStringType(''), $result); + } + return $result; + } elseif ($walkResult->isNonFalsy()->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + } elseif ($walkResult->isNonEmpty()->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + + return new StringType(); + } + + private function getRootAlternation(TreeNode $group): ?TreeNode + { + if ( + $group->getId() === '#capturing' + && count($group->getChildren()) === 1 + && $group->getChild(0)->getId() === '#alternation' + ) { + return $group->getChild(0); + } + + // 1st token within a named capturing group is a token holding the group-name + if ( + $group->getId() === '#namedcapturing' + && count($group->getChildren()) === 2 + && $group->getChild(1)->getId() === '#alternation' + ) { + return $group->getChild(1); + } + + return null; + } + + private function walkGroupAst( + TreeNode $ast, + bool $inAlternation, + bool $inClass, + string $patternModifiers, + RegexGroupWalkResult $walkResult, + ): RegexGroupWalkResult + { + $children = $ast->getChildren(); + + if ( + $ast->getId() === '#concatenation' + && count($children) > 0 + && !$walkResult->isInOptionalQuantification() + ) { + $meaningfulTokens = 0; + foreach ($children as $child) { + $nonFalsy = false; + if ($this->isMaybeEmptyNode($child, $patternModifiers, $nonFalsy)) { + continue; + } + + $meaningfulTokens++; + + if (!$nonFalsy || $inAlternation) { + continue; + } + + // a single token non-falsy on its own + $walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes()); + break; + } + + if ($meaningfulTokens > 0) { + $walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes()); + + // two non-empty tokens concatenated results in a non-falsy string + if ($meaningfulTokens > 1 && !$inAlternation) { + $walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes()); + } + } + } elseif ($ast->getId() === '#quantification') { + [$min] = $this->getQuantificationRange($ast); + + if ($min === 0) { + $walkResult = $walkResult->inOptionalQuantification(true); + } + + if (!$walkResult->isInOptionalQuantification()) { + if ($min >= 1) { + $walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes()); + } + if ($min >= 2 && !$inAlternation) { + $walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes()); + } + } + + $walkResult = $walkResult->onlyLiterals(null); + } elseif ($ast->getId() === '#class' && $walkResult->getOnlyLiterals() !== null) { + $inClass = true; + + $newLiterals = []; + foreach ($children as $child) { + $oldLiterals = $walkResult->getOnlyLiterals(); + + $this->getLiteralValue($child, $oldLiterals, true, $patternModifiers, true); + foreach ($oldLiterals ?? [] as $oldLiteral) { + $newLiterals[] = $oldLiteral; + } + } + $walkResult = $walkResult->onlyLiterals($newLiterals); + } elseif ($ast->getId() === 'token') { + $onlyLiterals = $walkResult->getOnlyLiterals(); + $literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass, $patternModifiers, false); + $walkResult = $walkResult->onlyLiterals($onlyLiterals); + + if ($literalValue !== null) { + if (Strings::match($literalValue, '/^\d+$/') === null) { + $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); + } elseif ($walkResult->isNumeric()->maybe()) { + $walkResult = $walkResult->numeric(TrinaryLogic::createYes()); + } + + if (!$walkResult->isInOptionalQuantification() && $literalValue !== '') { + $walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes()); + } + } + } elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing', '#alternation'], true)) { + $walkResult = $walkResult->onlyLiterals(null); + } + + if ($ast->getId() === '#alternation') { + $newLiterals = []; + foreach ($children as $child) { + $walkResult = $this->walkGroupAst( + $child, + true, + $inClass, + $patternModifiers, + $walkResult->onlyLiterals([]), + ); + + if ($newLiterals === null) { + continue; + } + + if (count($walkResult->getOnlyLiterals() ?? []) > 0) { + foreach ($walkResult->getOnlyLiterals() as $alternationLiterals) { + $newLiterals[] = $alternationLiterals; + } + } else { + $newLiterals = null; + } + } + + return $walkResult->onlyLiterals($newLiterals); + } + + // [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically + // doable but really silly compared to just \d so we can safely assume the string is not numeric + // for negative classes + if ($ast->getId() === '#negativeclass') { + $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); + } + + foreach ($children as $child) { + $walkResult = $this->walkGroupAst( + $child, + $inAlternation, + $inClass, + $patternModifiers, + $walkResult, + ); + } + + return $walkResult; + } + + private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy): bool + { + if ($node->getId() === '#quantification') { + [$min] = $this->getQuantificationRange($node); + + if ($min > 0) { + return false; + } + + if ($min === 0) { + return true; + } + } + + $literal = $this->getLiteralValue($node, $onlyLiterals, false, $patternModifiers, false); + if ($literal !== null) { + if ($literal !== '' && $literal !== '0') { + $isNonFalsy = true; + } + return $literal === ''; + } + + foreach ($node->getChildren() as $child) { + if (!$this->isMaybeEmptyNode($child, $patternModifiers, $isNonFalsy)) { + return false; + } + } + + return true; + } + + /** + * @param array|null $onlyLiterals + */ + private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers, bool $inCharacterClass): ?string + { + if ($node->getId() !== 'token') { + return null; + } + + // token is the token name from grammar without the namespace so literal and class:literal are both called literal here + $token = $node->getValueToken(); + $value = $node->getValueValue(); + + if ( + in_array($token, [ + 'literal', + // literal "-" in front/back of a character class like '[-a-z]' or '[abc-]', not forming a range + 'range', + // literal "[" or "]" inside character classes '[[]' or '[]]' + 'class_', '_class', + ], true) + ) { + if (str_contains($patternModifiers, 'x') && trim($value) === '') { + return null; + } + + $isEscaped = false; + if (strlen($value) > 1 && $value[0] === '\\') { + $value = substr($value, 1) ?: ''; + $isEscaped = true; + } + + if ( + $appendLiterals + && $onlyLiterals !== null + ) { + if ( + in_array($value, ['.'], true) + && !($isEscaped || $inCharacterClass) + ) { + $onlyLiterals = null; + } else { + if ($onlyLiterals === []) { + $onlyLiterals = [$value]; + } else { + foreach ($onlyLiterals as &$literal) { + $literal .= $value; + } + } + } + } + + return $value; + } + + if (!in_array($token, ['capturing_name'], true)) { + $onlyLiterals = null; + } + + // character escape sequences, just return a fixed string + if (in_array($token, ['character', 'dynamic_character', 'character_type'], true)) { + if ($token === 'character_type' && $value === '\d') { + return '0'; + } + + return $value; + } + + // [:digit:] and the like, more support coming later + if ($token === 'posix_class') { + if ($value === '[:digit:]') { + return '0'; + } + if (in_array($value, ['[:alpha:]', '[:alnum:]', '[:upper:]', '[:lower:]', '[:word:]', '[:ascii:]', '[:print:]', '[:xdigit:]', '[:graph:]'], true)) { + return 'a'; + } + if ($value === '[:blank:]') { + return " \t"; + } + if ($value === '[:cntrl:]') { + return "\x00\x1F"; + } + if ($value === '[:space:]') { + return " \t\r\n\v\f"; + } + if ($value === '[:punct:]') { + return '!"#$%&\'()*+,\-./:;<=>?@[\]^_`{|}~'; + } + } + + if ($token === 'anchor' || $token === 'match_point_reset') { + return ''; + } + + return null; + } + +} diff --git a/src/Type/Regex/RegexGroupWalkResult.php b/src/Type/Regex/RegexGroupWalkResult.php new file mode 100644 index 0000000000..9169af89ba --- /dev/null +++ b/src/Type/Regex/RegexGroupWalkResult.php @@ -0,0 +1,135 @@ +|null $onlyLiterals + */ + public function __construct( + private bool $inOptionalQuantification, + private ?array $onlyLiterals, + private TrinaryLogic $isNonEmpty, + private TrinaryLogic $isNonFalsy, + private TrinaryLogic $isNumeric, + ) + { + } + + public static function createEmpty(): self + { + return new self( + false, + [], + TrinaryLogic::createMaybe(), + TrinaryLogic::createMaybe(), + TrinaryLogic::createMaybe(), + ); + } + + public function inOptionalQuantification(bool $inOptionalQuantification): self + { + return new self( + $inOptionalQuantification, + $this->onlyLiterals, + $this->isNonEmpty, + $this->isNonFalsy, + $this->isNumeric, + ); + } + + /** + * @param array|null $onlyLiterals + */ + public function onlyLiterals(?array $onlyLiterals): self + { + return new self( + $this->inOptionalQuantification, + $onlyLiterals, + $this->isNonEmpty, + $this->isNonFalsy, + $this->isNumeric, + ); + } + + public function nonEmpty(TrinaryLogic $nonEmpty): self + { + return new self( + $this->inOptionalQuantification, + $this->onlyLiterals, + $nonEmpty, + $this->isNonFalsy, + $this->isNumeric, + ); + } + + public function nonFalsy(TrinaryLogic $nonFalsy): self + { + return new self( + $this->inOptionalQuantification, + $this->onlyLiterals, + $this->isNonEmpty, + $nonFalsy, + $this->isNumeric, + ); + } + + public function numeric(TrinaryLogic $numeric): self + { + return new self( + $this->inOptionalQuantification, + $this->onlyLiterals, + $this->isNonEmpty, + $this->isNonFalsy, + $numeric, + ); + } + + public function isInOptionalQuantification(): bool + { + return $this->inOptionalQuantification; + } + + /** + * @return array|null + */ + public function getOnlyLiterals(): ?array + { + return $this->onlyLiterals; + } + + public function mightContainEmptyStringLiteral(): bool + { + if ($this->onlyLiterals === null) { + return false; + } + foreach ($this->onlyLiterals as $onlyLiteral) { + if ($onlyLiteral === '') { + return true; + } + } + + return false; + } + + public function isNonEmpty(): TrinaryLogic + { + return $this->isNonEmpty; + } + + public function isNonFalsy(): TrinaryLogic + { + return $this->isNonFalsy; + } + + public function isNumeric(): TrinaryLogic + { + return $this->isNumeric; + } + +} diff --git a/src/Type/Regex/RegexNonCapturingGroup.php b/src/Type/Regex/RegexNonCapturingGroup.php new file mode 100644 index 0000000000..79b4d8bc08 --- /dev/null +++ b/src/Type/Regex/RegexNonCapturingGroup.php @@ -0,0 +1,55 @@ +getAlternationId() */ + public function inAlternation(): bool + { + return $this->alternation !== null; + } + + public function getAlternationId(): ?int + { + if ($this->alternation === null) { + return null; + } + + return $this->alternation->getId(); + } + + public function isOptional(): bool + { + return $this->inAlternation() + || $this->inOptionalQuantification + || ($this->parent !== null && $this->parent->isOptional()); + } + + public function isTopLevel(): bool + { + return $this->parent === null + || $this->parent instanceof RegexNonCapturingGroup && $this->parent->isTopLevel(); + } + + public function getParent(): RegexCapturingGroup|RegexNonCapturingGroup|null + { + return $this->parent; + } + + public function resetsGroupCounter(): bool + { + return $this->resetGroupCounter; + } + +} diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index 1f1028b11a..4ed9c79c1c 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -2,8 +2,13 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -19,6 +24,7 @@ class ResourceType implements Type { use JustNullableTypeTrait; + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -39,11 +45,21 @@ public function describe(VerbosityLevel $level): string return 'resource'; } + public function getConstantStrings(): array + { + return []; + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new StringType(); @@ -64,16 +80,50 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], + [], + TrinaryLogic::createYes(), ); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('resource'); } } diff --git a/src/Type/SimultaneousTypeTraverser.php b/src/Type/SimultaneousTypeTraverser.php new file mode 100644 index 0000000000..046727de88 --- /dev/null +++ b/src/Type/SimultaneousTypeTraverser.php @@ -0,0 +1,39 @@ +mapInternal($left, $right); + } + + /** @param callable(Type $left, Type $right, callable(Type, Type): Type $traverse): Type $cb */ + private function __construct(callable $cb) + { + $this->cb = $cb; + } + + /** @internal */ + public function mapInternal(Type $left, Type $right): Type + { + return ($this->cb)($left, $right, [$this, 'traverseInternal']); + } + + /** @internal */ + public function traverseInternal(Type $left, Type $right): Type + { + return $left->traverseSimultaneously($right, [$this, 'mapInternal']); + } + +} diff --git a/src/Type/StaticMethodParameterClosureTypeExtension.php b/src/Type/StaticMethodParameterClosureTypeExtension.php new file mode 100644 index 0000000000..94727efb21 --- /dev/null +++ b/src/Type/StaticMethodParameterClosureTypeExtension.php @@ -0,0 +1,32 @@ +baseClass; } - public function getClassReflection(): ?ClassReflection + public function getClassReflection(): ClassReflection { return $this->classReflection; } @@ -86,10 +83,13 @@ public function getStaticObjectType(): ObjectType if ($this->staticObjectType === null) { if ($this->classReflection->isGeneric()) { $typeMap = $this->classReflection->getActiveTemplateTypeMap()->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::toArgument($type)); + $varianceMap = $this->classReflection->getCallSiteVarianceMap(); return $this->staticObjectType = new GenericObjectType( $this->classReflection->getName(), $this->classReflection->typeMapToList($typeMap), $this->subtractedType, + null, + $this->classReflection->varianceMapToList($varianceMap), ); } @@ -99,52 +99,76 @@ public function getStaticObjectType(): ObjectType return $this->staticObjectType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return $this->getStaticObjectType()->getReferencedClasses(); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return $this->getStaticObjectType()->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->getStaticObjectType()->getObjectClassReflections(); + } + + public function getArrays(): array + { + return $this->getStaticObjectType()->getArrays(); + } + + public function getConstantArrays(): array + { + return $this->getStaticObjectType()->getConstantArrays(); + } + + public function getConstantStrings(): array + { + return $this->getStaticObjectType()->getConstantStrings(); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } if (!$type instanceof static) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } return $this->getStaticObjectType()->accepts($type->getStaticObjectType(), $strictTypes); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { return $this->getStaticObjectType()->isSuperTypeOf($type); } if ($type instanceof ObjectWithoutClassType) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof ObjectType) { $result = $this->getStaticObjectType()->isSuperTypeOf($type); - $classReflection = $type->getClassReflection(); - if ($result->yes() && $classReflection !== null && $classReflection->isFinal()) { - return $result; + if ($result->yes()) { + $classReflection = $type->getClassReflection(); + if ($classReflection !== null && $classReflection->isFinal()) { + return $result; + } } - return TrinaryLogic::createMaybe()->and($result); + return $result->and(IsSuperTypeOfResult::createMaybe()); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -153,8 +177,6 @@ public function equals(Type $type): bool return false; } - /** @var StaticType $type */ - $type = $type; return $this->getStaticObjectType()->equals($type->getStaticObjectType()); } @@ -163,6 +185,21 @@ public function describe(VerbosityLevel $level): string return sprintf('static(%s)', $this->getStaticObjectType()->describe($level)); } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->getStaticObjectType()->getTemplateType($ancestorClassName, $templateTypeName); + } + + public function isObject(): TrinaryLogic + { + return $this->getStaticObjectType()->isObject(); + } + + public function isEnum(): TrinaryLogic + { + return $this->getStaticObjectType()->isEnum(); + } + public function canAccessProperties(): TrinaryLogic { return $this->getStaticObjectType()->canAccessProperties(); @@ -173,7 +210,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return $this->getStaticObjectType()->hasProperty($propertyName); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -210,7 +247,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->getStaticObjectType()->hasMethod($methodName); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -248,11 +285,11 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop $isFinal = $classReflection->isFinal(); } $type = $type->changeBaseClass($classReflection); - if (!$isFinal) { - return $type; + if (!$isFinal || $type instanceof ThisType) { + return $traverse($type); } - return $type->getStaticObjectType(); + return $traverse($type->getStaticObjectType()); } return $traverse($type); @@ -269,7 +306,7 @@ public function hasConstant(string $constantName): TrinaryLogic return $this->getStaticObjectType()->hasConstant($constantName); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { return $this->getStaticObjectType()->getConstant($constantName); } @@ -289,21 +326,51 @@ public function isIterableAtLeastOnce(): TrinaryLogic return $this->getStaticObjectType()->isIterableAtLeastOnce(); } + public function getArraySize(): Type + { + return $this->getStaticObjectType()->getArraySize(); + } + public function getIterableKeyType(): Type { return $this->getStaticObjectType()->getIterableKeyType(); } + public function getFirstIterableKeyType(): Type + { + return $this->getStaticObjectType()->getFirstIterableKeyType(); + } + + public function getLastIterableKeyType(): Type + { + return $this->getStaticObjectType()->getLastIterableKeyType(); + } + public function getIterableValueType(): Type { return $this->getStaticObjectType()->getIterableValueType(); } + public function getFirstIterableValueType(): Type + { + return $this->getStaticObjectType()->getFirstIterableValueType(); + } + + public function getLastIterableValueType(): Type + { + return $this->getStaticObjectType()->getLastIterableValueType(); + } + public function isOffsetAccessible(): TrinaryLogic { return $this->getStaticObjectType()->isOffsetAccessible(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->getStaticObjectType()->isOffsetAccessLegal(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->getStaticObjectType()->hasOffsetValueType($offsetType); @@ -319,21 +386,156 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->getStaticObjectType()->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->getStaticObjectType()->setExistingOffsetValueType($offsetType, $valueType); + } + public function unsetOffset(Type $offsetType): Type { return $this->getStaticObjectType()->unsetOffset($offsetType); } + public function getKeysArray(): Type + { + return $this->getStaticObjectType()->getKeysArray(); + } + + public function getValuesArray(): Type + { + return $this->getStaticObjectType()->getValuesArray(); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->getStaticObjectType()->chunkArray($lengthType, $preserveKeys); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->getStaticObjectType()->fillKeysArray($valueType); + } + + public function flipArray(): Type + { + return $this->getStaticObjectType()->flipArray(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->getStaticObjectType()->intersectKeyArray($otherArraysType); + } + + public function popArray(): Type + { + return $this->getStaticObjectType()->popArray(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->getStaticObjectType()->reverseArray($preserveKeys); + } + + public function searchArray(Type $needleType): Type + { + return $this->getStaticObjectType()->searchArray($needleType); + } + + public function shiftArray(): Type + { + return $this->getStaticObjectType()->shiftArray(); + } + + public function shuffleArray(): Type + { + return $this->getStaticObjectType()->shuffleArray(); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->getStaticObjectType()->sliceArray($offsetType, $lengthType, $preserveKeys); + } + public function isCallable(): TrinaryLogic { return $this->getStaticObjectType()->isCallable(); } + public function getEnumCases(): array + { + return $this->getStaticObjectType()->getEnumCases(); + } + public function isArray(): TrinaryLogic { return $this->getStaticObjectType()->isArray(); } + public function isConstantArray(): TrinaryLogic + { + return $this->getStaticObjectType()->isConstantArray(); + } + + public function isOversizedArray(): TrinaryLogic + { + return $this->getStaticObjectType()->isOversizedArray(); + } + + public function isList(): TrinaryLogic + { + return $this->getStaticObjectType()->isList(); + } + + public function isNull(): TrinaryLogic + { + return $this->getStaticObjectType()->isNull(); + } + + public function isConstantValue(): TrinaryLogic + { + return $this->getStaticObjectType()->isConstantValue(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return $this->getStaticObjectType()->isConstantScalarValue(); + } + + public function getConstantScalarTypes(): array + { + return $this->getStaticObjectType()->getConstantScalarTypes(); + } + + public function getConstantScalarValues(): array + { + return $this->getStaticObjectType()->getConstantScalarValues(); + } + + public function isTrue(): TrinaryLogic + { + return $this->getStaticObjectType()->isTrue(); + } + + public function isFalse(): TrinaryLogic + { + return $this->getStaticObjectType()->isFalse(); + } + + public function isBoolean(): TrinaryLogic + { + return $this->getStaticObjectType()->isBoolean(); + } + + public function isFloat(): TrinaryLogic + { + return $this->getStaticObjectType()->isFloat(); + } + + public function isInteger(): TrinaryLogic + { + return $this->getStaticObjectType()->isInteger(); + } + public function isString(): TrinaryLogic { return $this->getStaticObjectType()->isString(); @@ -349,14 +551,56 @@ public function isNonEmptyString(): TrinaryLogic return $this->getStaticObjectType()->isNonEmptyString(); } + public function isNonFalsyString(): TrinaryLogic + { + return $this->getStaticObjectType()->isNonFalsyString(); + } + public function isLiteralString(): TrinaryLogic { return $this->getStaticObjectType()->isLiteralString(); } - /** - * @return ParametersAcceptor[] - */ + public function isLowercaseString(): TrinaryLogic + { + return $this->getStaticObjectType()->isLowercaseString(); + } + + public function isUppercaseString(): TrinaryLogic + { + return $this->getStaticObjectType()->isUppercaseString(); + } + + public function isClassString(): TrinaryLogic + { + return $this->getStaticObjectType()->isClassString(); + } + + public function getClassStringObjectType(): Type + { + return $this->getStaticObjectType()->getClassStringObjectType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return $this->getStaticObjectType()->isVoid(); + } + + public function isScalar(): TrinaryLogic + { + return $this->getStaticObjectType()->isScalar(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return $this->getStaticObjectType()->getCallableParametersAcceptors($scope); @@ -372,6 +616,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return $this->getStaticObjectType()->toString(); @@ -392,6 +641,16 @@ public function toArray(): Type return $this->getStaticObjectType()->toArray(); } + public function toArrayKey(): Type + { + return $this->getStaticObjectType()->toArrayKey(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this->getStaticObjectType()->toCoercedArgumentType($strictTypes); + } + public function toBoolean(): BooleanType { return $this->getStaticObjectType()->toBoolean(); @@ -399,9 +658,27 @@ public function toBoolean(): BooleanType public function traverse(callable $cb): Type { + $subtractedType = $this->subtractedType !== null ? $cb($this->subtractedType) : null; + + if ($subtractedType !== $this->subtractedType) { + return new self( + $this->classReflection, + $subtractedType, + ); + } + return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->subtractedType === null) { + return $this; + } + + return new self($this->classReflection); + } + public function subtract(Type $type): Type { if ($this->subtractedType !== null) { @@ -418,35 +695,20 @@ 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 $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 ($subtractedType !== null) { + $classReflection = $this->getClassReflection(); + if ($classReflection->getAllowedSubTypes() !== null) { + $objectType = $this->getStaticObjectType()->changeSubtractedType($subtractedType); + if ($objectType instanceof NeverType) { + return $objectType; } - if ($subType->getClassName() !== $this->getClassName()) { - return new self($this->classReflection, $subtractedType); + if ($objectType instanceof ObjectType && $objectType->getSubtractedType() !== null) { + return new self($classReflection, $objectType->getSubtractedType()); } - unset($cases[$subType->getEnumCaseName()]); + return TypeCombinator::intersect($this, $objectType); } - - $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); @@ -459,24 +721,26 @@ public function getSubtractedType(): ?Type public function tryRemove(Type $typeToRemove): ?Type { - if ($this->isSuperTypeOf($typeToRemove)->yes()) { + if ($this->getStaticObjectType()->isSuperTypeOf($typeToRemove)->yes()) { return $this->subtract($typeToRemove); } return null; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function exponentiate(Type $exponent): Type { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($reflectionProvider->hasClass($properties['baseClass'])) { - return new self($reflectionProvider->getClass($properties['baseClass']), $properties['subtractedType'] ?? null); - } + return $this->getStaticObjectType()->exponentiate($exponent); + } - return new ErrorType(); + public function getFiniteTypes(): array + { + return $this->getStaticObjectType()->getFiniteTypes(); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('static'); } } diff --git a/src/Type/StaticTypeFactory.php b/src/Type/StaticTypeFactory.php index ba99a36130..382e4208ad 100644 --- a/src/Type/StaticTypeFactory.php +++ b/src/Type/StaticTypeFactory.php @@ -8,7 +8,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -class StaticTypeFactory +final class StaticTypeFactory { public static function falsey(): Type diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 1250c77a5e..0ff25dc124 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -2,10 +2,13 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -13,7 +16,9 @@ use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; @@ -21,6 +26,8 @@ class StrictMixedType implements CompoundType { use UndecidedComparisonCompoundTypeTrait; + use NonArrayTypeTrait; + use NonIterableTypeTrait; use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; @@ -29,38 +36,53 @@ public function getReferencedClasses(): array return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array { - return TrinaryLogic::createYes(); + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + return AcceptsResult::createYes(); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { if ($acceptingType instanceof self) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($acceptingType instanceof MixedType && !$acceptingType instanceof TemplateMixedType) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } - return TrinaryLogic::createMaybe(); + return AcceptsResult::createMaybe(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($otherType instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($otherType instanceof MixedType && !$otherType instanceof TemplateMixedType) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } public function equals(Type $type): bool @@ -70,7 +92,27 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - return 'mixed'; + return $level->handle( + static fn () => 'mixed', + static fn () => 'mixed', + static fn () => 'mixed', + static fn () => 'strict-mixed', + ); + } + + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new ErrorType(); + } + + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createNo(); } public function canAccessProperties(): TrinaryLogic @@ -83,7 +125,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { throw new ShouldNotHappenException(); } @@ -103,7 +145,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { throw new ShouldNotHappenException(); } @@ -123,7 +165,7 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { throw new ShouldNotHappenException(); } @@ -148,7 +190,52 @@ public function getIterableValueType(): Type return $this; } - public function isArray(): TrinaryLogic + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -168,16 +255,66 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createNo(); @@ -193,6 +330,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); @@ -223,6 +365,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); @@ -243,6 +390,16 @@ public function toArray(): Type return new ErrorType(); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { return TemplateTypeMap::createEmpty(); @@ -253,17 +410,34 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc return []; } + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { return $this; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('mixed'); } } diff --git a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php index fbfd0b9b99..feb18e97d6 100644 --- a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php +++ b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php @@ -3,26 +3,54 @@ namespace PHPStan\Type; use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\TrinaryLogic; class StringAlwaysAcceptingObjectWithToStringType extends StringType { - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - if ($type instanceof TypeWithClassName) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($type->getClassName())) { - return TrinaryLogic::createNo(); + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + $thatClassNames = $type->getObjectClassNames(); + if ($thatClassNames === []) { + return parent::isSuperTypeOf($type); + } + + $result = IsSuperTypeOfResult::createNo(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($thatClassNames as $thatClassName) { + if (!$reflectionProvider->hasClass($thatClassName)) { + return IsSuperTypeOfResult::createNo(); + } + + $typeClass = $reflectionProvider->getClass($thatClassName); + $result = $result->or(IsSuperTypeOfResult::createFromBoolean($typeClass->hasNativeMethod('__toString'))); + } + + return $result; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + $thatClassNames = $type->getObjectClassNames(); + if ($thatClassNames === []) { + return parent::accepts($type, $strictTypes); + } + + $result = AcceptsResult::createNo(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($thatClassNames as $thatClassName) { + if (!$reflectionProvider->hasClass($thatClassName)) { + return AcceptsResult::createNo(); } - $typeClass = $reflectionProvider->getClass($type->getClassName()); - return TrinaryLogic::createFromBoolean( - $typeClass->hasNativeMethod('__toString'), - ); + $typeClass = $reflectionProvider->getClass($thatClassName); + $result = $result->or(AcceptsResult::createFromBoolean($typeClass->hasNativeMethod('__toString'))); } - return parent::accepts($type, $strictTypes); + return $result; } } diff --git a/src/Type/StringType.php b/src/Type/StringType.php index f151910b3d..0f1778aa21 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -2,19 +2,26 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function count; /** @api */ class StringType implements Type @@ -22,6 +29,7 @@ class StringType implements Type use JustNullableTypeTrait; use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; @@ -39,14 +47,24 @@ public function describe(VerbosityLevel $level): string return 'string'; } + public function getConstantStrings(): array + { + return []; + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -55,7 +73,10 @@ public function getOffsetValueType(Type $offsetType): Type return new ErrorType(); } - return new StringType(); + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type @@ -69,41 +90,54 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } - if ((new IntegerType())->isSuperTypeOf($offsetType)->yes() || $offsetType instanceof MixedType) { - return new StringType(); + if ($offsetType->isInteger()->yes() || $offsetType instanceof MixedType) { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); } return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - if ($type instanceof TypeWithClassName && !$strictTypes) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($type->getClassName())) { - return TrinaryLogic::createNo(); - } + $thatClassNames = $type->getObjectClassNames(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); + } - $typeClass = $reflectionProvider->getClass($type->getClassName()); - return TrinaryLogic::createFromBoolean( - $typeClass->hasNativeMethod('__toString'), - ); + if ($thatClassNames === [] || $strictTypes) { + return AcceptsResult::createNo(); } - return TrinaryLogic::createNo(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($thatClassNames[0])) { + return AcceptsResult::createNo(); + } + + $typeClass = $reflectionProvider->getClass($thatClassNames[0]); + return AcceptsResult::createFromBoolean( + $typeClass->hasNativeMethod('__toString'), + ); } public function toNumber(): Type @@ -111,6 +145,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); @@ -131,10 +170,59 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + if ($this->isNumericString()->no()) { + return TypeCombinator::union($this, $this->toBoolean()); + } + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -150,16 +238,69 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isArray()->yes()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + if ($this->isClassString()->yes()) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createNo(); + } + 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(''); } @@ -167,12 +308,19 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function getFiniteTypes(): array + { + return []; + } + + public function exponentiate(Type $exponent): Type + { + return ExponentiateHelper::exponentiate($this, $exponent); + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('string'); } } diff --git a/src/Type/ThisType.php b/src/Type/ThisType.php index 3eb2f6d83c..39d4949ca8 100644 --- a/src/Type/ThisType.php +++ b/src/Type/ThisType.php @@ -2,8 +2,9 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; use function sprintf; /** @api */ @@ -31,17 +32,57 @@ public function describe(VerbosityLevel $level): string return sprintf('$this(%s)', $this->getStaticObjectType()->describe($level)); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof self) { + return $this->getStaticObjectType()->isSuperTypeOf($type); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + $parent = new parent($this->getClassReflection(), $this->getSubtractedType()); + + return $parent->isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe()); + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + $type = parent::changeSubtractedType($subtractedType); + if ($type instanceof parent) { + return new self($type->getClassReflection(), $subtractedType); + } + + return $type; + } + + public function traverse(callable $cb): Type + { + $subtractedType = $this->getSubtractedType() !== null ? $cb($this->getSubtractedType()) : null; + + if ($subtractedType !== $this->getSubtractedType()) { + return new self( + $this->getClassReflection(), + $subtractedType, + ); + } + + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($reflectionProvider->hasClass($properties['baseClass'])) { - return new self($reflectionProvider->getClass($properties['baseClass']), $properties['subtractedType'] ?? null); + if ($this->getSubtractedType() === null) { + return $this; } - return new ErrorType(); + return new self($this->getClassReflection()); + } + + public function toPhpDocNode(): TypeNode + { + return new ThisTypeNode(); } } diff --git a/src/Type/Traits/ArrayTypeTrait.php b/src/Type/Traits/ArrayTypeTrait.php new file mode 100644 index 0000000000..813b0136d9 --- /dev/null +++ b/src/Type/Traits/ArrayTypeTrait.php @@ -0,0 +1,211 @@ +yes() + ? $this + : TypeCombinator::intersect(new ArrayType(new IntegerType(), $this->getIterableValueType()), new AccessoryArrayListType()); + $chunkType = TypeCombinator::intersect($chunkType, new NonEmptyArrayType()); + + $arrayType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $chunkType), new AccessoryArrayListType()); + + return $this->isIterableAtLeastOnce()->yes() + ? TypeCombinator::intersect($arrayType, new NonEmptyArrayType()) + : $arrayType; + } + +} diff --git a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php index 44cac2d98e..c6efe96950 100644 --- a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php +++ b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php @@ -2,7 +2,9 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; @@ -12,7 +14,7 @@ trait ConstantNumericComparisonTypeTrait { - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new ConstantBooleanType(true), @@ -22,15 +24,17 @@ public function getSmallerType(): Type if (!(bool) $this->value) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); + $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ IntegerRangeType::createAllGreaterThan($this->value), + // subtract range when we support float-ranges ]; if (!(bool) $this->value) { @@ -40,11 +44,12 @@ public function getSmallerOrEqualType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { $subtractedTypes = [ new NullType(), new ConstantBooleanType(false), + new ConstantFloatType(0.0), // subtract range when we support float-ranges IntegerRangeType::createAllSmallerThanOrEqualTo($this->value), ]; @@ -55,7 +60,7 @@ public function getGreaterType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { $subtractedTypes = [ IntegerRangeType::createAllSmallerThan($this->value), @@ -64,6 +69,7 @@ public function getGreaterOrEqualType(): Type if ((bool) $this->value) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); + $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); diff --git a/src/Type/Traits/ConstantScalarTypeTrait.php b/src/Type/Traits/ConstantScalarTypeTrait.php index 44e8674c39..f458512622 100644 --- a/src/Type/Traits/ConstantScalarTypeTrait.php +++ b/src/Type/Traits/ConstantScalarTypeTrait.php @@ -2,43 +2,71 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\LooseComparisonHelper; use PHPStan\Type\Type; trait ConstantScalarTypeTrait { - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean($this->value === $type->value); + return AcceptsResult::createFromBoolean($this->equals($type)); } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return parent::accepts($type, $strictTypes)->and(AcceptsResult::createMaybe()); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return $this->value === $type->value ? TrinaryLogic::createYes() : TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createFromBoolean($this->equals($type)); } if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if (!$this instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + if ($type instanceof ConstantScalarType) { + return LooseComparisonHelper::compareConstantScalars($this, $type, $phpVersion); + } + + if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + // @phpstan-ignore equal.notAllowed, equal.invalid, equal.alwaysFalse + return new ConstantBooleanType($this->getValue() == []); // phpcs:ignore + } + + if ($type instanceof CompoundType) { + return $type->looseCompare($this, $phpVersion); + } + + return parent::looseCompare($type, $phpVersion); } public function equals(Type $type): bool @@ -46,35 +74,55 @@ public function equals(Type $type): bool return $type instanceof self && $this->value === $type->value; } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($otherType instanceof ConstantScalarType) { return TrinaryLogic::createFromBoolean($this->value < $otherType->getValue()); } if ($otherType instanceof CompoundType) { - return $otherType->isGreaterThan($this); + return $otherType->isGreaterThan($this, $phpVersion); } return TrinaryLogic::createMaybe(); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { if ($otherType instanceof ConstantScalarType) { return TrinaryLogic::createFromBoolean($this->value <= $otherType->getValue()); } if ($otherType instanceof CompoundType) { - return $otherType->isGreaterThanOrEqual($this); + return $otherType->isGreaterThanOrEqual($this, $phpVersion); } return TrinaryLogic::createMaybe(); } - public function generalize(GeneralizePrecision $precision): Type + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getConstantScalarTypes(): array + { + return [$this]; + } + + public function getConstantScalarValues(): array + { + return [$this->getValue()]; + } + + public function getFiniteTypes(): array { - return new parent(); + return [$this]; } } diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php new file mode 100644 index 0000000000..5eb703077f --- /dev/null +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -0,0 +1,596 @@ +resolve()->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->resolve()->getObjectClassReflections(); + } + + public function getArrays(): array + { + return $this->resolve()->getArrays(); + } + + public function getConstantArrays(): array + { + return $this->resolve()->getConstantArrays(); + } + + public function getConstantStrings(): array + { + return $this->resolve()->getConstantStrings(); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + return $this->resolve()->accepts($type, $strictTypes); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + return $this->isSuperTypeOfDefault($type); + } + + private function isSuperTypeOfDefault(Type $type): IsSuperTypeOfResult + { + if ($type instanceof NeverType) { + return IsSuperTypeOfResult::createYes(); + } + + if ($type instanceof LateResolvableType) { + $type = $type->resolve(); + } + + $isSuperType = $this->resolve()->isSuperTypeOf($type); + + if (!$this->isResolvable()) { + $isSuperType = $isSuperType->and(IsSuperTypeOfResult::createMaybe()); + } + + return $isSuperType; + } + + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->resolve()->getTemplateType($ancestorClassName, $templateTypeName); + } + + public function isObject(): TrinaryLogic + { + return $this->resolve()->isObject(); + } + + public function isEnum(): TrinaryLogic + { + return $this->resolve()->isEnum(); + } + + public function canAccessProperties(): TrinaryLogic + { + return $this->resolve()->canAccessProperties(); + } + + public function hasProperty(string $propertyName): TrinaryLogic + { + return $this->resolve()->hasProperty($propertyName); + } + + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->resolve()->getProperty($propertyName, $scope); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->resolve()->getUnresolvedPropertyPrototype($propertyName, $scope); + } + + public function canCallMethods(): TrinaryLogic + { + return $this->resolve()->canCallMethods(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + return $this->resolve()->hasMethod($methodName); + } + + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection + { + return $this->resolve()->getMethod($methodName, $scope); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection + { + return $this->resolve()->getUnresolvedMethodPrototype($methodName, $scope); + } + + public function canAccessConstants(): TrinaryLogic + { + return $this->resolve()->canAccessConstants(); + } + + public function hasConstant(string $constantName): TrinaryLogic + { + return $this->resolve()->hasConstant($constantName); + } + + public function getConstant(string $constantName): ClassConstantReflection + { + return $this->resolve()->getConstant($constantName); + } + + public function isIterable(): TrinaryLogic + { + return $this->resolve()->isIterable(); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + return $this->resolve()->isIterableAtLeastOnce(); + } + + public function getArraySize(): Type + { + return $this->resolve()->getArraySize(); + } + + public function getIterableKeyType(): Type + { + return $this->resolve()->getIterableKeyType(); + } + + public function getFirstIterableKeyType(): Type + { + return $this->resolve()->getFirstIterableKeyType(); + } + + public function getLastIterableKeyType(): Type + { + return $this->resolve()->getLastIterableKeyType(); + } + + public function getIterableValueType(): Type + { + return $this->resolve()->getIterableValueType(); + } + + public function getFirstIterableValueType(): Type + { + return $this->resolve()->getFirstIterableValueType(); + } + + public function getLastIterableValueType(): Type + { + return $this->resolve()->getLastIterableValueType(); + } + + public function isArray(): TrinaryLogic + { + return $this->resolve()->isArray(); + } + + public function isConstantArray(): TrinaryLogic + { + return $this->resolve()->isConstantArray(); + } + + public function isOversizedArray(): TrinaryLogic + { + return $this->resolve()->isOversizedArray(); + } + + public function isList(): TrinaryLogic + { + return $this->resolve()->isList(); + } + + public function isOffsetAccessible(): TrinaryLogic + { + return $this->resolve()->isOffsetAccessible(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->resolve()->isOffsetAccessLegal(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $this->resolve()->hasOffsetValueType($offsetType); + } + + public function getOffsetValueType(Type $offsetType): Type + { + return $this->resolve()->getOffsetValueType($offsetType); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + return $this->resolve()->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->resolve()->setExistingOffsetValueType($offsetType, $valueType); + } + + public function unsetOffset(Type $offsetType): Type + { + return $this->resolve()->unsetOffset($offsetType); + } + + public function getKeysArray(): Type + { + return $this->resolve()->getKeysArray(); + } + + public function getValuesArray(): Type + { + return $this->resolve()->getValuesArray(); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->resolve()->chunkArray($lengthType, $preserveKeys); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->resolve()->fillKeysArray($valueType); + } + + public function flipArray(): Type + { + return $this->resolve()->flipArray(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->resolve()->intersectKeyArray($otherArraysType); + } + + public function popArray(): Type + { + return $this->resolve()->popArray(); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->resolve()->reverseArray($preserveKeys); + } + + public function searchArray(Type $needleType): Type + { + return $this->resolve()->searchArray($needleType); + } + + public function shiftArray(): Type + { + return $this->resolve()->shiftArray(); + } + + public function shuffleArray(): Type + { + return $this->resolve()->shuffleArray(); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->resolve()->sliceArray($offsetType, $lengthType, $preserveKeys); + } + + public function isCallable(): TrinaryLogic + { + return $this->resolve()->isCallable(); + } + + public function getEnumCases(): array + { + return $this->resolve()->getEnumCases(); + } + + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + { + return $this->resolve()->getCallableParametersAcceptors($scope); + } + + public function isCloneable(): TrinaryLogic + { + return $this->resolve()->isCloneable(); + } + + public function toBoolean(): BooleanType + { + return $this->resolve()->toBoolean(); + } + + public function toNumber(): Type + { + return $this->resolve()->toNumber(); + } + + public function toAbsoluteNumber(): Type + { + return $this->resolve()->toAbsoluteNumber(); + } + + public function toInteger(): Type + { + return $this->resolve()->toInteger(); + } + + public function toFloat(): Type + { + return $this->resolve()->toFloat(); + } + + public function toString(): Type + { + return $this->resolve()->toString(); + } + + public function toArray(): Type + { + return $this->resolve()->toArray(); + } + + public function toArrayKey(): Type + { + return $this->resolve()->toArrayKey(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this->resolve()->toCoercedArgumentType($strictTypes); + } + + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->resolve()->isSmallerThan($otherType, $phpVersion); + } + + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->resolve()->isSmallerThanOrEqual($otherType, $phpVersion); + } + + public function isNull(): TrinaryLogic + { + return $this->resolve()->isNull(); + } + + public function isConstantValue(): TrinaryLogic + { + return $this->resolve()->isConstantValue(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return $this->resolve()->isConstantScalarValue(); + } + + public function getConstantScalarTypes(): array + { + return $this->resolve()->getConstantScalarTypes(); + } + + public function getConstantScalarValues(): array + { + return $this->resolve()->getConstantScalarValues(); + } + + public function isTrue(): TrinaryLogic + { + return $this->resolve()->isTrue(); + } + + public function isFalse(): TrinaryLogic + { + return $this->resolve()->isFalse(); + } + + public function isBoolean(): TrinaryLogic + { + return $this->resolve()->isBoolean(); + } + + public function isFloat(): TrinaryLogic + { + return $this->resolve()->isFloat(); + } + + public function isInteger(): TrinaryLogic + { + return $this->resolve()->isInteger(); + } + + public function isString(): TrinaryLogic + { + return $this->resolve()->isString(); + } + + public function isNumericString(): TrinaryLogic + { + return $this->resolve()->isNumericString(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return $this->resolve()->isNonEmptyString(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return $this->resolve()->isNonFalsyString(); + } + + public function isLiteralString(): TrinaryLogic + { + return $this->resolve()->isLiteralString(); + } + + public function isLowercaseString(): TrinaryLogic + { + return $this->resolve()->isLowercaseString(); + } + + public function isUppercaseString(): TrinaryLogic + { + return $this->resolve()->isUppercaseString(); + } + + public function isClassString(): TrinaryLogic + { + return $this->resolve()->isClassString(); + } + + public function getClassStringObjectType(): Type + { + return $this->resolve()->getClassStringObjectType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->resolve()->getObjectTypeOrClassStringObjectType(); + } + + public function isVoid(): TrinaryLogic + { + return $this->resolve()->isVoid(); + } + + public function isScalar(): TrinaryLogic + { + return $this->resolve()->isScalar(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getSmallerType(PhpVersion $phpVersion): Type + { + return $this->resolve()->getSmallerType($phpVersion); + } + + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type + { + return $this->resolve()->getSmallerOrEqualType($phpVersion); + } + + public function getGreaterType(PhpVersion $phpVersion): Type + { + return $this->resolve()->getGreaterType($phpVersion); + } + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type + { + return $this->resolve()->getGreaterOrEqualType($phpVersion); + } + + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { + return $this->resolve()->inferTemplateTypes($receivedType); + } + + public function tryRemove(Type $typeToRemove): ?Type + { + return $this->resolve()->tryRemove($typeToRemove); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + $result = $this->resolve(); + + if ($result instanceof CompoundType) { + return $result->isSubTypeOf($otherType); + } + + return $otherType->isSuperTypeOf($result); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + $result = $this->resolve(); + + if ($result instanceof CompoundType) { + return $result->isAcceptedBy($acceptingType, $strictTypes); + } + + return $acceptingType->accepts($result, $strictTypes); + } + + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + $result = $this->resolve(); + + if ($result instanceof CompoundType) { + return $result->isGreaterThan($otherType, $phpVersion); + } + + return $otherType->isSmallerThan($result, $phpVersion); + } + + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + $result = $this->resolve(); + + if ($result instanceof CompoundType) { + return $result->isGreaterThanOrEqual($otherType, $phpVersion); + } + + return $otherType->isSmallerThanOrEqual($result, $phpVersion); + } + + public function exponentiate(Type $exponent): Type + { + return $this->resolve()->exponentiate($exponent); + } + + public function getFiniteTypes(): array + { + return $this->resolve()->getFiniteTypes(); + } + + public function resolve(): Type + { + if ($this->result === null) { + return $this->result = $this->getResult(); + } + + return $this->result; + } + + abstract protected function getResult(): Type; + +} diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php new file mode 100644 index 0000000000..afafc91708 --- /dev/null +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -0,0 +1,102 @@ +isIterable()->no()) { + return new ErrorType(); + } + + if ($this->isIterableAtLeastOnce()->yes()) { + return IntegerRangeType::fromInterval(1, null); + } + + return IntegerRangeType::fromInterval(0, null); + } + public function getIterableKeyType(): Type { return new MixedType(); } + public function getFirstIterableKeyType(): Type + { + return new MixedType(); + } + + public function getLastIterableKeyType(): Type + { + return new MixedType(); + } + public function getIterableValueType(): Type { return new MixedType(); } + public function getFirstIterableValueType(): Type + { + return new MixedType(); + } + + public function getLastIterableValueType(): Type + { + return new MixedType(); + } + } diff --git a/src/Type/Traits/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index 607e9360ee..4625da358b 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -2,23 +2,39 @@ namespace PHPStan\Type\Traits; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\Dummy\DummyConstantReflection; +use PHPStan\Reflection\Dummy\DummyClassConstantReflection; use PHPStan\Reflection\Dummy\DummyMethodReflection; use PHPStan\Reflection\Dummy\DummyPropertyReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; trait MaybeObjectTypeTrait { + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new MixedType(); + } + + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -29,14 +45,14 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -55,7 +71,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -81,9 +97,9 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { - return new DummyConstantReflection($constantName); + return new DummyClassConstantReflection($constantName); } public function isCloneable(): TrinaryLogic diff --git a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php index 55889eb574..6e83395e95 100644 --- a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php @@ -29,6 +29,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return $this; diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php new file mode 100644 index 0000000000..1d1b948242 --- /dev/null +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -0,0 +1,102 @@ +getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -64,7 +83,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -90,9 +109,14 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection + { + return new DummyClassConstantReflection($constantName); + } + + public function getConstantStrings(): array { - return new DummyConstantReflection($constantName); + return []; } public function isCloneable(): TrinaryLogic @@ -100,7 +124,52 @@ public function isCloneable(): TrinaryLogic return TrinaryLogic::createYes(); } - public function isArray(): TrinaryLogic + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -120,19 +189,69 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { - return new StringType(); + return new ErrorType(); } public function toInteger(): Type @@ -150,4 +269,18 @@ public function toArray(): Type return new ArrayType(new MixedType(), new MixedType()); } + public function toArrayKey(): Type + { + return new StringType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toString()); + } + + return $this; + } + } diff --git a/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php b/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php index e40b72fad4..6adf571d54 100644 --- a/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php +++ b/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; @@ -10,12 +11,12 @@ trait UndecidedComparisonCompoundTypeTrait use UndecidedComparisonTypeTrait; - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { return TrinaryLogic::createMaybe(); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { return TrinaryLogic::createMaybe(); } diff --git a/src/Type/Traits/UndecidedComparisonTypeTrait.php b/src/Type/Traits/UndecidedComparisonTypeTrait.php index e5c6d2c891..6761274cf1 100644 --- a/src/Type/Traits/UndecidedComparisonTypeTrait.php +++ b/src/Type/Traits/UndecidedComparisonTypeTrait.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -9,32 +10,32 @@ trait UndecidedComparisonTypeTrait { - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { return TrinaryLogic::createMaybe(); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { return TrinaryLogic::createMaybe(); } - public function getSmallerType(): Type + public function getSmallerType(PhpVersion $phpVersion): Type { return new MixedType(); } - public function getSmallerOrEqualType(): Type + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type { return new MixedType(); } - public function getGreaterType(): Type + public function getGreaterType(PhpVersion $phpVersion): Type { return new MixedType(); } - public function getGreaterOrEqualType(): Type + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { return new MixedType(); } diff --git a/src/Type/Type.php b/src/Type/Type.php index 44d71ef154..15886a053c 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -2,30 +2,71 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeReference; use PHPStan\Type\Generic\TemplateTypeVariance; -/** @api */ +/** + * @api + * @see https://phpstan.org/developing-extensions/type-system + */ interface Type { /** - * @return string[] + * @return list */ public function getReferencedClasses(): array; - public function accepts(Type $type, bool $strictTypes): TrinaryLogic; + /** @return list */ + public function getObjectClassNames(): array; - public function isSuperTypeOf(Type $type): TrinaryLogic; + /** + * @return list + */ + public function getObjectClassReflections(): array; + + /** + * Returns object type Foo for class-string and 'Foo' (if Foo is a valid class). + */ + public function getClassStringObjectType(): Type; + + /** + * Returns object type Foo for class-string, 'Foo' (if Foo is a valid class), + * and object type Foo. + */ + public function getObjectTypeOrClassStringObjectType(): Type; + + public function isObject(): TrinaryLogic; + + public function isEnum(): TrinaryLogic; + + /** @return list */ + public function getArrays(): array; + + /** @return list */ + public function getConstantArrays(): array; + + /** @return list */ + public function getConstantStrings(): array; + + public function accepts(Type $type, bool $strictTypes): AcceptsResult; + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult; public function equals(Type $type): bool; @@ -35,7 +76,7 @@ public function canAccessProperties(): TrinaryLogic; public function hasProperty(string $propertyName): TrinaryLogic; - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection; + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection; public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection; @@ -43,7 +84,7 @@ public function canCallMethods(): TrinaryLogic; public function hasMethod(string $methodName): TrinaryLogic; - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection; + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection; public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection; @@ -51,32 +92,99 @@ public function canAccessConstants(): TrinaryLogic; public function hasConstant(string $constantName): TrinaryLogic; - public function getConstant(string $constantName): ConstantReflection; + public function getConstant(string $constantName): ClassConstantReflection; public function isIterable(): TrinaryLogic; public function isIterableAtLeastOnce(): TrinaryLogic; + public function getArraySize(): Type; + public function getIterableKeyType(): Type; + public function getFirstIterableKeyType(): Type; + + public function getLastIterableKeyType(): Type; + public function getIterableValueType(): Type; + public function getFirstIterableValueType(): Type; + + public function getLastIterableValueType(): Type; + public function isArray(): TrinaryLogic; + public function isConstantArray(): TrinaryLogic; + + public function isOversizedArray(): TrinaryLogic; + + public function isList(): TrinaryLogic; + public function isOffsetAccessible(): TrinaryLogic; + public function isOffsetAccessLegal(): TrinaryLogic; + public function hasOffsetValueType(Type $offsetType): TrinaryLogic; public function getOffsetValueType(Type $offsetType): Type; public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type; + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type; + public function unsetOffset(Type $offsetType): Type; + public function getKeysArray(): Type; + + public function getValuesArray(): Type; + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type; + + public function fillKeysArray(Type $valueType): Type; + + public function flipArray(): Type; + + public function intersectKeyArray(Type $otherArraysType): Type; + + public function popArray(): Type; + + public function reverseArray(TrinaryLogic $preserveKeys): Type; + + public function searchArray(Type $needleType): Type; + + public function shiftArray(): Type; + + public function shuffleArray(): Type; + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type; + + /** + * @return list + */ + public function getEnumCases(): array; + + /** + * Returns a list of finite values. + * + * Examples: + * + * - for bool: [true, false] + * - for int<0, 3>: [0, 1, 2, 3] + * - for enums: list of enum cases + * - for scalars: the scalar itself + * + * For infinite types it returns an empty array. + * + * @return list + */ + public function getFiniteTypes(): array; + + public function exponentiate(Type $exponent): Type; + public function isCallable(): TrinaryLogic; /** - * @return ParametersAcceptor[] + * @return CallableParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array; @@ -94,9 +202,54 @@ public function toString(): Type; public function toArray(): Type; - public function isSmallerThan(Type $otherType): TrinaryLogic; + public function toArrayKey(): Type; - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic; + /** + * Tells how a type might change when passed to an argument + * or assigned to a typed property. + * + * Example: int is accepted by int|float with strict_types = 1 + * Stringable is accepted by string|Stringable even without strict_types. + * + * Note: Logic with $strictTypes=false is mostly not implemented in Type subclasses. + */ + public function toCoercedArgumentType(bool $strictTypes): self; + + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; + + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; + + /** + * Is Type of a known constant value? Includes literal strings, integers, floats, true, false, null, and array shapes. + */ + public function isConstantValue(): TrinaryLogic; + + /** + * Is Type of a known constant scalar value? Includes literal strings, integers, floats, true, false, and null. + */ + public function isConstantScalarValue(): TrinaryLogic; + + /** + * @return list + */ + public function getConstantScalarTypes(): array; + + /** + * @return list + */ + public function getConstantScalarValues(): array; + + public function isNull(): TrinaryLogic; + + public function isTrue(): TrinaryLogic; + + public function isFalse(): TrinaryLogic; + + public function isBoolean(): TrinaryLogic; + + public function isFloat(): TrinaryLogic; + + public function isInteger(): TrinaryLogic; public function isString(): TrinaryLogic; @@ -104,15 +257,47 @@ public function isNumericString(): TrinaryLogic; public function isNonEmptyString(): TrinaryLogic; + public function isNonFalsyString(): TrinaryLogic; + public function isLiteralString(): TrinaryLogic; - public function getSmallerType(): Type; + public function isLowercaseString(): TrinaryLogic; + + public function isUppercaseString(): TrinaryLogic; + + public function isClassString(): TrinaryLogic; + + public function isVoid(): TrinaryLogic; + + public function isScalar(): TrinaryLogic; + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType; - public function getSmallerOrEqualType(): Type; + public function getSmallerType(PhpVersion $phpVersion): Type; - public function getGreaterType(): Type; + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type; - public function getGreaterOrEqualType(): Type; + public function getGreaterType(PhpVersion $phpVersion): Type; + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type; + + /** + * Returns actual template type for a given object. + * + * Example: + * + * @-template T + * class Foo {} + * + * // $fooType is Foo + * $t = $fooType->getTemplateType(Foo::class, 'T'); + * $t->isInteger(); // yes + * + * Returns ErrorType in case of a missing type. + * + * @param class-string $ancestorClassName + */ + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type; /** * Infers template types @@ -136,10 +321,12 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap; * which the receiver type was * found. * - * @return TemplateTypeReference[] + * @return list */ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array; + public function toAbsoluteNumber(): Type; + /** * Traverses inner types * @@ -150,6 +337,15 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc */ public function traverse(callable $cb): Type; + /** + * Traverses inner types while keeping the same context in another type. + * + * @param callable(Type $left, Type $right): Type $cb + */ + public function traverseSimultaneously(Type $right, callable $cb): Type; + + public function toPhpDocNode(): TypeNode; + /** * Return the difference with another type, or null if it cannot be represented. * @@ -159,9 +355,4 @@ public function tryRemove(Type $typeToRemove): ?Type; public function generalize(GeneralizePrecision $precision): Type; - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self; - } diff --git a/src/Type/TypeAlias.php b/src/Type/TypeAlias.php index 10d6c09922..7dc7803af0 100644 --- a/src/Type/TypeAlias.php +++ b/src/Type/TypeAlias.php @@ -7,7 +7,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -class TypeAlias +final class TypeAlias { private ?Type $resolvedType = null; diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index b9259a7380..e6cf82bf5f 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -2,22 +2,32 @@ namespace PHPStan\Type; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; +use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\TemplateArrayType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; -use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateUnionType; -use function array_intersect_key; use function array_key_exists; +use function array_key_first; +use function array_map; use function array_merge; use function array_slice; use function array_splice; @@ -26,16 +36,23 @@ use function get_class; use function is_int; use function md5; +use function sprintf; use function usort; +use const PHP_INT_MAX; +use const PHP_INT_MIN; -/** @api */ -class TypeCombinator +/** + * @api + */ +final class TypeCombinator { public static function addNull(Type $type): Type { - if ((new NullType())->isSuperTypeOf($type)->no()) { - return self::union($type, new NullType()); + $nullType = new NullType(); + + if ($nullType->isSuperTypeOf($type)->no()) { + return self::union($type, $nullType); } return $type; @@ -65,7 +82,41 @@ public static function remove(Type $fromType, Type $typeToRemove): Type } } - return $fromType->tryRemove($typeToRemove) ?? $fromType; + $removed = $fromType->tryRemove($typeToRemove); + if ($removed !== null) { + return $removed; + } + + $fromFiniteTypes = $fromType->getFiniteTypes(); + if (count($fromFiniteTypes) > 0) { + $finiteTypesToRemove = $typeToRemove->getFiniteTypes(); + if (count($finiteTypesToRemove) === 1) { + $result = []; + foreach ($fromFiniteTypes as $finiteType) { + if ($finiteType->equals($finiteTypesToRemove[0])) { + continue; + } + + $result[] = $finiteType; + } + + if (count($result) === count($fromFiniteTypes)) { + return $fromType; + } + + if (count($result) === 0) { + return new NeverType(); + } + + if (count($result) === 1) { + return $result[0]; + } + + return new UnionType($result); + } + } + + return $fromType; } public static function removeNull(Type $type): Type @@ -95,6 +146,9 @@ public static function containsNull(Type $type): bool public static function union(Type ...$types): Type { $typesCount = count($types); + if ($typesCount === 0) { + return new NeverType(); + } $benevolentTypes = []; $benevolentUnionObject = null; @@ -126,15 +180,16 @@ public static function union(Type ...$types): Type $typesCount += count($typesInner) - 1; } + if ($typesCount === 1) { + return $types[0]; + } + $arrayTypes = []; - $arrayAccessoryTypes = []; $scalarTypes = []; $hasGenericScalarTypes = []; + $enumCaseTypes = []; + $integerRangeTypes = []; for ($i = 0; $i < $typesCount; $i++) { - if ($types[$i] instanceof NeverType) { - unset($types[$i]); - continue; - } if ($types[$i] instanceof ConstantScalarType) { $type = $types[$i]; $scalarTypes[get_class($type)][md5($type->describe(VerbosityLevel::cache()))] = $type; @@ -153,33 +208,26 @@ public static function union(Type ...$types): Type if ($types[$i] instanceof StringType && !$types[$i] instanceof ClassStringType) { $hasGenericScalarTypes[ConstantStringType::class] = true; } - if ($types[$i] instanceof IntersectionType) { - $intermediateArrayType = null; - $intermediateAccessoryTypes = []; - foreach ($types[$i]->getTypes() as $innerType) { - if ($innerType instanceof ArrayType) { - $intermediateArrayType = $innerType; - continue; - } - if ($innerType instanceof AccessoryType || $innerType instanceof CallableType) { - $intermediateAccessoryTypes[$innerType->describe(VerbosityLevel::cache())] = $innerType; - continue; - } - } + $enumCases = $types[$i]->getEnumCases(); + if (count($enumCases) === 1) { + $enumCaseTypes[$types[$i]->describe(VerbosityLevel::cache())] = $types[$i]; - if ($intermediateArrayType !== null) { - $arrayTypes[] = $intermediateArrayType; - $arrayAccessoryTypes[] = $intermediateAccessoryTypes; - unset($types[$i]); - continue; - } + unset($types[$i]); + continue; } - if (!$types[$i] instanceof ArrayType) { + + if ($types[$i] instanceof IntegerRangeType) { + $integerRangeTypes[] = $types[$i]; + unset($types[$i]); + + continue; + } + + if (!$types[$i]->isArray()->yes()) { continue; } $arrayTypes[] = $types[$i]; - $arrayAccessoryTypes[] = []; unset($types[$i]); } @@ -187,39 +235,16 @@ public static function union(Type ...$types): Type $scalarTypes[$classType] = array_values($scalarTypeItems); } - /** @var ArrayType[] $arrayTypes */ - $arrayTypes = $arrayTypes; - - $arrayAccessoryTypesToProcess = []; - if (count($arrayAccessoryTypes) > 1) { - $arrayAccessoryTypesToProcess = array_values(array_intersect_key(...$arrayAccessoryTypes)); - } elseif (count($arrayAccessoryTypes) > 0) { - $arrayAccessoryTypesToProcess = array_values($arrayAccessoryTypes[0]); - } - - $types = array_values( - array_merge( - $types, - self::processArrayTypes($arrayTypes, $arrayAccessoryTypesToProcess), - ), + $enumCaseTypes = array_values($enumCaseTypes); + usort( + $integerRangeTypes, + static fn (IntegerRangeType $a, IntegerRangeType $b): int => ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN) + ?: ($a->getMax() ?? PHP_INT_MAX) <=> ($b->getMax() ?? PHP_INT_MAX) ); + $types = array_merge($types, $integerRangeTypes); + $types = array_values($types); $typesCount = count($types); - // simplify string[] | int[] to (string|int)[] - 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()), - ); - array_splice($types, $j, 1); - $typesCount--; - continue 2; - } - } - } - foreach ($scalarTypes as $classType => $scalarTypeItems) { if (isset($hasGenericScalarTypes[$classType])) { unset($scalarTypes[$classType]); @@ -265,9 +290,14 @@ public static function union(Type ...$types): Type $newTypes[$type->describe(VerbosityLevel::cache())] = $type; } $types = array_values($newTypes); - $typesCount = count($types); } + $types = array_merge( + $types, + self::processArrayTypes($arrayTypes), + ); + $typesCount = count($types); + // transform A | A to A // transform A | never to A for ($i = 0; $i < $typesCount; $i++) { @@ -293,6 +323,35 @@ public static function union(Type ...$types): Type } } + $enumCasesCount = count($enumCaseTypes); + for ($i = 0; $i < $typesCount; $i++) { + for ($j = 0; $j < $enumCasesCount; $j++) { + $compareResult = self::compareTypesInUnion($types[$i], $enumCaseTypes[$j]); + if ($compareResult === null) { + continue; + } + + [$a, $b] = $compareResult; + if ($a !== null) { + $types[$i] = $a; + array_splice($enumCaseTypes, $j--, 1); + $enumCasesCount--; + continue 1; + } + if ($b !== null) { + $enumCaseTypes[$j] = $b; + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + } + } + + foreach ($enumCaseTypes as $enumCaseType) { + $types[] = $enumCaseType; + $typesCount++; + } + foreach ($scalarTypes as $scalarTypeItems) { foreach ($scalarTypeItems as $scalarType) { $types[] = $scalarType; @@ -322,11 +381,11 @@ public static function union(Type ...$types): Type return $benevolentUnionObject->withTypes($types); } - return new BenevolentUnionType($types); + return new BenevolentUnionType($types, true); } } - return new UnionType($types); + return new UnionType($types, true); } /** @@ -351,6 +410,25 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array if ($a instanceof IntegerRangeType && $b instanceof IntegerRangeType) { return null; } + if ($a instanceof HasOffsetValueType && $b instanceof HasOffsetValueType) { + if ($a->getOffsetType()->equals($b->getOffsetType())) { + return [new HasOffsetValueType($a->getOffsetType(), self::union($a->getValueType(), $b->getValueType())), null]; + } + } + if ($a->isConstantArray()->yes() && $b->isConstantArray()->yes()) { + return null; + } + + // simplify string[] | int[] to (string|int)[] + if ($a instanceof IterableType && $b instanceof IterableType) { + return [ + new IterableType( + self::union($a->getIterableKeyType(), $b->getIterableKeyType()), + self::union($a->getIterableValueType(), $b->getIterableValueType()), + ), + null, + ]; + } if ($a instanceof SubtractableType) { $typeWithoutSubtractedTypeA = $a->getTypeWithoutSubtractedType(); @@ -378,39 +456,81 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array } } - if ( - !$b instanceof ConstantArrayType - && $b->isSuperTypeOf($a)->yes() - ) { + if ($b->isSuperTypeOf($a)->yes()) { return [null, $b]; } - if ( - !$a instanceof ConstantArrayType - && $a->isSuperTypeOf($b)->yes() - ) { + if ($a->isSuperTypeOf($b)->yes()) { return [$a, null]; } if ( $a instanceof ConstantStringType && $a->getValue() === '' - && $b->describe(VerbosityLevel::value()) === 'non-empty-string' + && ($b->describe(VerbosityLevel::value()) === 'non-empty-string' + || $b->describe(VerbosityLevel::value()) === 'non-falsy-string') ) { - return [null, new StringType()]; + return [null, self::intersect( + new StringType(), + ...self::getAccessoryCaseStringTypes($b), + )]; } if ( $b instanceof ConstantStringType && $b->getValue() === '' - && $a->describe(VerbosityLevel::value()) === 'non-empty-string' + && ($a->describe(VerbosityLevel::value()) === 'non-empty-string' + || $a->describe(VerbosityLevel::value()) === 'non-falsy-string') + ) { + return [self::intersect( + new StringType(), + ...self::getAccessoryCaseStringTypes($a), + ), null]; + } + + if ( + $a instanceof ConstantStringType + && $a->getValue() === '0' + && $b->describe(VerbosityLevel::value()) === 'non-falsy-string' + ) { + return [null, self::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ...self::getAccessoryCaseStringTypes($b), + )]; + } + + if ( + $b instanceof ConstantStringType + && $b->getValue() === '0' + && $a->describe(VerbosityLevel::value()) === 'non-falsy-string' ) { - return [new StringType(), null]; + return [self::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ...self::getAccessoryCaseStringTypes($a), + ), null]; } return null; } + /** + * @return array + */ + private static function getAccessoryCaseStringTypes(Type $type): array + { + $accessory = []; + if ($type->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($type->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + + return $accessory; + } + private static function unionWithSubtractedType( Type $type, ?Type $subtractedType, @@ -421,14 +541,9 @@ private static function unionWithSubtractedType( } if ($type instanceof SubtractableType) { - if ($type->getSubtractedType() === null) { - return $type; - } - - $subtractedType = self::union( - $type->getSubtractedType(), - $subtractedType, - ); + $subtractedType = $type->getSubtractedType() === null + ? $subtractedType + : self::union($type->getSubtractedType(), $subtractedType); if ($subtractedType instanceof NeverType) { $subtractedType = null; } @@ -452,17 +567,60 @@ private static function intersectWithSubtractedType( return $a; } - if ($b instanceof SubtractableType) { - $subtractedType = $b->getSubtractedType(); - if ($subtractedType === null) { + if ($b instanceof IntersectionType) { + $subtractableTypes = []; + foreach ($b->getTypes() as $innerType) { + if (!$innerType instanceof SubtractableType) { + continue; + } + + $subtractableTypes[] = $innerType; + } + + if (count($subtractableTypes) === 0) { return $a->getTypeWithoutSubtractedType(); } - } else { - $subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType()); - if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) { + + $subtractedTypes = []; + foreach ($subtractableTypes as $subtractableType) { + if ($subtractableType->getSubtractedType() === null) { + continue; + } + + $subtractedTypes[] = $subtractableType->getSubtractedType(); + } + + if (count($subtractedTypes) === 0) { return $a->getTypeWithoutSubtractedType(); + + } + + $subtractedType = self::union(...$subtractedTypes); + } else { + $isBAlreadySubtracted = $a->getSubtractedType()->isSuperTypeOf($b); + + if ($isBAlreadySubtracted->no()) { + return $a; + } elseif ($isBAlreadySubtracted->yes()) { + $subtractedType = self::remove($a->getSubtractedType(), $b); + + if ($subtractedType instanceof NeverType) { + $subtractedType = null; + } + + return $a->changeSubtractedType($subtractedType); + } elseif ($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 = new MixedType(false, $b); } $subtractedType = self::intersect( @@ -477,36 +635,100 @@ private static function intersectWithSubtractedType( } /** - * @param ArrayType[] $arrayTypes - * @param Type[] $accessoryTypes + * @param Type[] $arrayTypes * @return Type[] */ - private static function processArrayTypes(array $arrayTypes, array $accessoryTypes): array + private static function processArrayAccessoryTypes(array $arrayTypes): array { - foreach ($arrayTypes as $arrayType) { - if (!$arrayType instanceof ConstantArrayType) { - continue; + $isIterableAtLeastOnce = []; + $accessoryTypes = []; + foreach ($arrayTypes as $i => $arrayType) { + $isIterableAtLeastOnce[] = $arrayType->isIterableAtLeastOnce(); + + if ($arrayType instanceof IntersectionType) { + foreach ($arrayType->getTypes() as $innerType) { + if ($innerType instanceof TemplateType) { + break; + } + if (!($innerType instanceof AccessoryType) && !($innerType instanceof CallableType)) { + continue; + } + if ($innerType instanceof HasOffsetType) { + $offset = $innerType->getOffsetType(); + if ($offset instanceof ConstantStringType || $offset instanceof ConstantIntegerType) { + $innerType = new HasOffsetValueType($offset, $arrayType->getIterableValueType()); + } + } + if ($innerType instanceof HasOffsetValueType) { + $accessoryTypes[sprintf('hasOffsetValue(%s)', $innerType->getOffsetType()->describe(VerbosityLevel::cache()))][$i] = $innerType; + continue; + } + + $accessoryTypes[$innerType->describe(VerbosityLevel::cache())][$i] = $innerType; + } } - if (count($arrayType->getKeyTypes()) > 0) { + + if (!$arrayType->isConstantArray()->yes()) { continue; } + $constantArrays = $arrayType->getConstantArrays(); - foreach ($accessoryTypes as $i => $accessoryType) { - if (!$accessoryType instanceof NonEmptyArrayType) { + foreach ($constantArrays as $constantArray) { + if ($constantArray->isList()->yes()) { + $list = new AccessoryArrayListType(); + $accessoryTypes[$list->describe(VerbosityLevel::cache())][$i] = $list; + } + + if (!$constantArray->isIterableAtLeastOnce()->yes()) { continue; } - unset($accessoryTypes[$i]); - break 2; + $nonEmpty = new NonEmptyArrayType(); + $accessoryTypes[$nonEmpty->describe(VerbosityLevel::cache())][$i] = $nonEmpty; } } + $commonAccessoryTypes = []; + $arrayTypeCount = count($arrayTypes); + foreach ($accessoryTypes as $accessoryType) { + if (count($accessoryType) !== $arrayTypeCount) { + $firstKey = array_key_first($accessoryType); + if ($accessoryType[$firstKey] instanceof OversizedArrayType) { + $commonAccessoryTypes[] = $accessoryType[$firstKey]; + } + continue; + } + + if ($accessoryType[0] instanceof HasOffsetValueType) { + $commonAccessoryTypes[] = self::union(...$accessoryType); + continue; + } + + $commonAccessoryTypes[] = $accessoryType[0]; + } + + if (TrinaryLogic::createYes()->and(...$isIterableAtLeastOnce)->yes()) { + $commonAccessoryTypes[] = new NonEmptyArrayType(); + } + + return $commonAccessoryTypes; + } + + /** + * @param list $arrayTypes + * @return Type[] + */ + private static function processArrayTypes(array $arrayTypes): array + { if ($arrayTypes === []) { return []; } + + $accessoryTypes = self::processArrayAccessoryTypes($arrayTypes); + if (count($arrayTypes) === 1) { return [ - self::intersect($arrayTypes[0], ...$accessoryTypes), + self::intersect(...$arrayTypes, ...$accessoryTypes), ]; } @@ -514,127 +736,316 @@ private static function processArrayTypes(array $arrayTypes, array $accessoryTyp $valueTypesForGeneralArray = []; $generalArrayOccurred = false; $constantKeyTypesNumbered = []; + $filledArrays = 0; + $overflowed = false; /** @var int|float $nextConstantKeyTypeIndex */ $nextConstantKeyTypeIndex = 1; + $constantArraysMap = array_map( + static fn (Type $t) => $t->getConstantArrays(), + $arrayTypes, + ); - foreach ($arrayTypes as $arrayType) { - if (!$arrayType instanceof ConstantArrayType || $generalArrayOccurred) { - $keyTypesForGeneralArray[] = $arrayType->getKeyType(); - $valueTypesForGeneralArray[] = $arrayType->getItemType(); - $generalArrayOccurred = true; + foreach ($arrayTypes as $arrayIdx => $arrayType) { + $constantArrays = $constantArraysMap[$arrayIdx]; + $isConstantArray = $constantArrays !== []; + if (!$isConstantArray || !$arrayType->isIterableAtLeastOnce()->no()) { + $filledArrays++; + } + + if ($generalArrayOccurred || !$isConstantArray) { + foreach ($arrayType->getArrays() as $type) { + $keyTypesForGeneralArray[] = $type->getIterableKeyType(); + $valueTypesForGeneralArray[] = $type->getItemType(); + $generalArrayOccurred = true; + } continue; } - foreach ($arrayType->getKeyTypes() as $i => $keyType) { - $keyTypesForGeneralArray[] = $keyType; - $valueTypesForGeneralArray[] = $arrayType->getValueTypes()[$i]; + $constantArrays = $arrayType->getConstantArrays(); + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $keyTypesForGeneralArray[] = $keyType; + $valueTypesForGeneralArray[] = $constantArray->getValueTypes()[$i]; - $keyTypeValue = $keyType->getValue(); - if (array_key_exists($keyTypeValue, $constantKeyTypesNumbered)) { - continue; - } + $keyTypeValue = $keyType->getValue(); + if (array_key_exists($keyTypeValue, $constantKeyTypesNumbered)) { + continue; + } - $constantKeyTypesNumbered[$keyTypeValue] = $nextConstantKeyTypeIndex; - $nextConstantKeyTypeIndex *= 2; - if (!is_int($nextConstantKeyTypeIndex)) { - $generalArrayOccurred = true; - continue; + $constantKeyTypesNumbered[$keyTypeValue] = $nextConstantKeyTypeIndex; + $nextConstantKeyTypeIndex *= 2; + if (!is_int($nextConstantKeyTypeIndex)) { + $generalArrayOccurred = true; + $overflowed = true; + continue 2; + } } } } - if ($generalArrayOccurred) { + if ($generalArrayOccurred && (!$overflowed || $filledArrays > 1)) { + $reducedArrayTypes = self::reduceArrays($arrayTypes, false); + if (count($reducedArrayTypes) === 1) { + return [self::intersect($reducedArrayTypes[0], ...$accessoryTypes)]; + } + $scopes = []; + $useTemplateArray = true; + foreach ($arrayTypes as $arrayType) { + if (!$arrayType instanceof TemplateArrayType) { + $useTemplateArray = false; + break; + } + + $scopes[$arrayType->getScope()->describe()] = $arrayType; + } + + $arrayType = new ArrayType( + self::union(...$keyTypesForGeneralArray), + self::union(...self::optimizeConstantArrays($valueTypesForGeneralArray)), + ); + + if ($useTemplateArray && count($scopes) === 1) { + $templateArray = array_values($scopes)[0]; + $arrayType = new TemplateArrayType( + $templateArray->getScope(), + $templateArray->getStrategy(), + $templateArray->getVariance(), + $templateArray->getName(), + $arrayType, + $templateArray->getDefault(), + ); + } + return [ - self::intersect(new ArrayType( - self::union(...$keyTypesForGeneralArray), - self::union(...$valueTypesForGeneralArray), - ), ...$accessoryTypes), + self::intersect($arrayType, ...$accessoryTypes), ]; } - /** @var ConstantArrayType[] $arrayTypes */ - $arrayTypes = $arrayTypes; + $reducedArrayTypes = self::reduceArrays($arrayTypes, true); - /** @var int[] $constantKeyTypesNumbered */ - $constantKeyTypesNumbered = $constantKeyTypesNumbered; + return array_map( + static fn (Type $arrayType) => self::intersect($arrayType, ...$accessoryTypes), + self::optimizeConstantArrays($reducedArrayTypes), + ); + } - $constantArraysBuckets = []; - foreach ($arrayTypes as $arrayTypeAgain) { - $arrayIndex = 0; - foreach ($arrayTypeAgain->getKeyTypes() as $keyType) { - $arrayIndex += $constantKeyTypesNumbered[$keyType->getValue()]; - } + /** + * @param Type[] $types + * @return Type[] + */ + private static function optimizeConstantArrays(array $types): array + { + $constantArrayValuesCount = self::countConstantArrayValueTypes($types); + + if ($constantArrayValuesCount <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + return $types; + } - if (!array_key_exists($arrayIndex, $constantArraysBuckets)) { - $bucket = []; - foreach ($arrayTypeAgain->getKeyTypes() as $i => $keyType) { - $bucket[$keyType->getValue()] = [ - 'keyType' => $keyType, - 'valueType' => $arrayTypeAgain->getValueTypes()[$i], - 'optional' => $arrayTypeAgain->isOptionalKey($i), - ]; + $results = []; + $eachIsOversized = true; + foreach ($types as $type) { + $isOversized = false; + $result = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$isOversized): Type { + if (!$type instanceof ConstantArrayType) { + return $traverse($type); } - $constantArraysBuckets[$arrayIndex] = $bucket; - continue; - } - $bucket = $constantArraysBuckets[$arrayIndex]; - foreach ($arrayTypeAgain->getKeyTypes() as $i => $keyType) { - $bucket[$keyType->getValue()]['valueType'] = self::union( - $bucket[$keyType->getValue()]['valueType'], - $arrayTypeAgain->getValueTypes()[$i], - ); - $bucket[$keyType->getValue()]['optional'] = $bucket[$keyType->getValue()]['optional'] || $arrayTypeAgain->isOptionalKey($i); + if ($type->isIterableAtLeastOnce()->no()) { + return $type; + } + + $isOversized = true; + + $isList = true; + $valueTypes = []; + $keyTypes = []; + $nextAutoIndex = 0; + foreach ($type->getKeyTypes() as $i => $innerKeyType) { + if (!$innerKeyType instanceof ConstantIntegerType) { + $isList = false; + } elseif ($innerKeyType->getValue() !== $nextAutoIndex) { + $isList = false; + $nextAutoIndex = $innerKeyType->getValue() + 1; + } else { + $nextAutoIndex++; + } + + $generalizedKeyType = $innerKeyType->generalize(GeneralizePrecision::moreSpecific()); + $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType; + + $innerValueType = $type->getValueTypes()[$i]; + $generalizedValueType = TypeTraverser::map($innerValueType, static function (Type $type) use ($traverse): Type { + if ($type instanceof ArrayType || $type instanceof ConstantArrayType) { + return TypeCombinator::intersect($type, new OversizedArrayType()); + } + + if ($type instanceof ConstantScalarType) { + return $type->generalize(GeneralizePrecision::moreSpecific()); + } + + return $traverse($type); + }); + $valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType; + } + + $keyType = TypeCombinator::union(...array_values($keyTypes)); + $valueType = TypeCombinator::union(...array_values($valueTypes)); + + $arrayType = new ArrayType($keyType, $valueType); + if ($isList) { + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } + + return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType()); + }); + + if (!$isOversized) { + $eachIsOversized = false; } - $constantArraysBuckets[$arrayIndex] = $bucket; + $results[] = $result; } - $resultArrays = []; - foreach ($constantArraysBuckets as $bucket) { - $builder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($bucket as $data) { - $builder->setOffsetValueType($data['keyType'], $data['valueType'], $data['optional']); + if ($eachIsOversized) { + $eachIsList = true; + $keyTypes = []; + $valueTypes = []; + foreach ($results as $result) { + $keyTypes[] = $result->getIterableKeyType(); + $valueTypes[] = $result->getLastIterableValueType(); + if ($result->isList()->yes()) { + continue; + } + $eachIsList = false; } - $resultArrays[] = self::intersect($builder->getArray(), ...$accessoryTypes); + $keyType = self::union(...$keyTypes); + $valueType = self::union(...$valueTypes); + + if ($valueType instanceof UnionType && count($valueType->getTypes()) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $valueType = $valueType->generalize(GeneralizePrecision::lessSpecific()); + } + + $arrayType = new ArrayType($keyType, $valueType); + if ($eachIsList) { + $arrayType = self::intersect($arrayType, new AccessoryArrayListType()); + } + + return [self::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType())]; } - return self::reduceArrays($resultArrays); + return $results; } /** - * @param Type[] $constantArrays - * @return Type[] + * @param Type[] $types + */ + public static function countConstantArrayValueTypes(array $types): int + { + $constantArrayValuesCount = 0; + foreach ($types as $type) { + TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$constantArrayValuesCount): Type { + if ($type instanceof ConstantArrayType) { + $constantArrayValuesCount += count($type->getValueTypes()); + } + + return $traverse($type); + }); + } + return $constantArrayValuesCount; + } + + /** + * @param list $constantArrays + * @return list */ - private static function reduceArrays(array $constantArrays): array + private static function reduceArrays(array $constantArrays, bool $preserveTaggedUnions): array { $newArrays = []; $arraysToProcess = []; + $emptyArray = null; foreach ($constantArrays as $constantArray) { - if (!$constantArray instanceof ConstantArrayType) { + if (!$constantArray->isConstantArray()->yes()) { + // This is an optimization for current use-case of $preserveTaggedUnions=false, where we need + // one constant array as a result, or we generalize the $constantArrays. + if (!$preserveTaggedUnions) { + return $constantArrays; + } $newArrays[] = $constantArray; continue; } - $arraysToProcess[] = $constantArray; + if ($constantArray->isIterableAtLeastOnce()->no()) { + $emptyArray = $constantArray; + continue; + } + + $arraysToProcess = array_merge($arraysToProcess, $constantArray->getConstantArrays()); + } + + if ($emptyArray !== null) { + $newArrays[] = $emptyArray; } - for ($i = 0, $arraysToProcessCount = count($arraysToProcess); $i < $arraysToProcessCount; $i++) { - for ($j = $i + 1; $j < $arraysToProcessCount; $j++) { - if ($arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])) { + $arraysToProcessPerKey = []; + foreach ($arraysToProcess as $i => $arrayToProcess) { + foreach ($arrayToProcess->getKeyTypes() as $keyType) { + $arraysToProcessPerKey[$keyType->getValue()][] = $i; + } + } + + $eligibleCombinations = []; + + foreach ($arraysToProcessPerKey as $arrays) { + for ($i = 0, $arraysCount = count($arrays); $i < $arraysCount - 1; $i++) { + for ($j = $i + 1; $j < $arraysCount; $j++) { + $eligibleCombinations[$arrays[$i]][$arrays[$j]] ??= 0; + $eligibleCombinations[$arrays[$i]][$arrays[$j]]++; + } + } + } + + foreach ($eligibleCombinations as $i => $other) { + if (!array_key_exists($i, $arraysToProcess)) { + continue; + } + + foreach ($other as $j => $overlappingKeysCount) { + if (!array_key_exists($j, $arraysToProcess)) { + continue; + } + + if ( + $preserveTaggedUnions + && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) + && $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i]) + ) { $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); - array_splice($arraysToProcess, $i--, 1); - $arraysToProcessCount--; + unset($arraysToProcess[$i]); continue 2; + } - } elseif ($arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j])) { + if ( + $preserveTaggedUnions + && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) + && $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j]) + ) { $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]); - array_splice($arraysToProcess, $j--, 1); - $arraysToProcessCount--; + unset($arraysToProcess[$j]); continue 1; } + + if ( + !$preserveTaggedUnions + // both arrays have same keys + && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) + && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) + ) { + $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); + unset($arraysToProcess[$i]); + continue 2; + } } } @@ -645,6 +1056,14 @@ public static function intersect(Type ...$types): Type { $types = array_values($types); + $typesCount = count($types); + if ($typesCount === 0) { + return new NeverType(); + } + if ($typesCount === 1) { + return $types[0]; + } + $sortTypes = static function (Type $a, Type $b): int { if (!$a instanceof UnionType || !$b instanceof UnionType) { return 0; @@ -676,15 +1095,21 @@ public static function intersect(Type ...$types): Type $topLevelUnionSubTypes = []; $innerTypes = $type->getTypes(); usort($innerTypes, $sortTypes); + $slice1 = array_slice($types, 0, $i); + $slice2 = array_slice($types, $i + 1); foreach ($innerTypes as $innerUnionSubType) { $topLevelUnionSubTypes[] = self::intersect( $innerUnionSubType, - ...array_slice($types, 0, $i), - ...array_slice($types, $i + 1), + ...$slice1, + ...$slice2, ); } $union = self::union(...$topLevelUnionSubTypes); + if ($union instanceof NeverType) { + return $union; + } + if ($type instanceof BenevolentUnionType) { $union = TypeUtils::toBenevolentUnion($union); } @@ -695,10 +1120,9 @@ public static function intersect(Type ...$types): Type $type->getName(), $union, $type->getVariance(), + $type->getStrategy(), + $type->getDefault(), ); - if ($type->isArgument()) { - return TemplateTypeHelper::toArgument($union); - } } return $union; @@ -708,6 +1132,7 @@ public static function intersect(Type ...$types): Type // transform A & (B & C) to A & B & C for ($i = 0; $i < $typesCount; $i++) { $type = $types[$i]; + if (!($type instanceof IntersectionType)) { continue; } @@ -716,8 +1141,25 @@ public static function intersect(Type ...$types): Type $typesCount = count($types); } - // move subtractables with subtracts before those without to avoid loosing them in the union logic + $hasOffsetValueTypeCount = 0; + $newTypes = []; + foreach ($types as $type) { + if (!$type instanceof HasOffsetValueType) { + $newTypes[] = $type; + continue; + } + + $hasOffsetValueTypeCount++; + } + + if ($hasOffsetValueTypeCount > 32) { + $newTypes[] = new OversizedArrayType(); + $types = $newTypes; + $typesCount = count($types); + } + usort($types, static function (Type $a, Type $b): int { + // move subtractables with subtracts before those without to avoid loosing them in the union logic if ($a instanceof SubtractableType && $a->getSubtractedType() !== null) { return -1; } @@ -725,6 +1167,13 @@ public static function intersect(Type ...$types): Type return 1; } + if ($a instanceof ConstantArrayType && !$b instanceof ConstantArrayType) { + return -1; + } + if ($b instanceof ConstantArrayType && !$a instanceof ConstantArrayType) { + return 1; + } + return 0; }); @@ -814,10 +1263,119 @@ public static function intersect(Type ...$types): Type } if ( - ($types[$i] instanceof ArrayType || $types[$i] instanceof IterableType) && - ($types[$j] instanceof ArrayType || $types[$j] instanceof IterableType) + $types[$i] instanceof ConstantArrayType + && count($types[$i]->getKeyTypes()) === 1 + && $types[$i]->isOptionalKey(0) + && $types[$j] instanceof NonEmptyArrayType ) { - $keyType = self::intersect($types[$i]->getKeyType(), $types[$j]->getKeyType()); + $types[$i] = $types[$i]->makeOffsetRequired($types[$i]->getKeyTypes()[0]); + array_splice($types, $j--, 1); + $typesCount--; + continue; + } + + if ( + $types[$j] instanceof ConstantArrayType + && count($types[$j]->getKeyTypes()) === 1 + && $types[$j]->isOptionalKey(0) + && $types[$i] instanceof NonEmptyArrayType + ) { + $types[$j] = $types[$j]->makeOffsetRequired($types[$j]->getKeyTypes()[0]); + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + + if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof HasOffsetValueType) { + $offsetType = $types[$j]->getOffsetType(); + $valueType = $types[$j]->getValueType(); + $newValueType = self::intersect($types[$i]->getOffsetValueType($offsetType), $valueType); + if ($newValueType instanceof NeverType) { + return $newValueType; + } + $types[$i] = $types[$i]->setOffsetValueType($offsetType, $newValueType); + array_splice($types, $j--, 1); + $typesCount--; + continue; + } + + if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof HasOffsetValueType) { + $offsetType = $types[$i]->getOffsetType(); + $valueType = $types[$i]->getValueType(); + $newValueType = self::intersect($types[$j]->getOffsetValueType($offsetType), $valueType); + if ($newValueType instanceof NeverType) { + return $newValueType; + } + + $types[$j] = $types[$j]->setOffsetValueType($offsetType, $newValueType); + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + + if ($types[$i] instanceof OversizedArrayType && $types[$j] instanceof HasOffsetValueType) { + array_splice($types, $j--, 1); + $typesCount--; + continue; + } + + if ($types[$j] instanceof OversizedArrayType && $types[$i] instanceof HasOffsetValueType) { + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + + if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof HasPropertyType) { + $types[$i] = $types[$i]->makePropertyRequired($types[$j]->getPropertyName()); + array_splice($types, $j--, 1); + $typesCount--; + continue; + } + + if ($types[$j] instanceof ObjectShapeType && $types[$i] instanceof HasPropertyType) { + $types[$j] = $types[$j]->makePropertyRequired($types[$i]->getPropertyName()); + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + + if ($types[$i] instanceof ConstantArrayType && ($types[$j] instanceof ArrayType || $types[$j] instanceof ConstantArrayType)) { + $newArray = ConstantArrayTypeBuilder::createEmpty(); + $valueTypes = $types[$i]->getValueTypes(); + foreach ($types[$i]->getKeyTypes() as $k => $keyType) { + $newArray->setOffsetValueType( + self::intersect($keyType, $types[$j]->getIterableKeyType()), + self::intersect($valueTypes[$k], $types[$j]->getIterableValueType()), + $types[$i]->isOptionalKey($k) && !$types[$j]->hasOffsetValueType($keyType)->yes(), + ); + } + $types[$i] = $newArray->getArray(); + array_splice($types, $j--, 1); + $typesCount--; + continue 2; + } + + if ($types[$j] instanceof ConstantArrayType && ($types[$i] instanceof ArrayType || $types[$i] instanceof ConstantArrayType)) { + $newArray = ConstantArrayTypeBuilder::createEmpty(); + $valueTypes = $types[$j]->getValueTypes(); + foreach ($types[$j]->getKeyTypes() as $k => $keyType) { + $newArray->setOffsetValueType( + self::intersect($keyType, $types[$i]->getIterableKeyType()), + self::intersect($valueTypes[$k], $types[$i]->getIterableValueType()), + $types[$j]->isOptionalKey($k) && !$types[$i]->hasOffsetValueType($keyType)->yes(), + ); + } + $types[$j] = $newArray->getArray(); + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + + if ( + ($types[$i] instanceof ArrayType || $types[$i] instanceof ConstantArrayType || $types[$i] instanceof IterableType) && + ($types[$j] instanceof ArrayType || $types[$j] instanceof ConstantArrayType || $types[$j] instanceof IterableType) + ) { + $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getKeyType()); $itemType = self::intersect($types[$i]->getItemType(), $types[$j]->getItemType()); if ($types[$i] instanceof IterableType && $types[$j] instanceof IterableType) { $types[$j] = new IterableType($keyType, $itemType); @@ -829,6 +1387,28 @@ public static function intersect(Type ...$types): Type continue 2; } + if ($types[$i] instanceof GenericClassStringType && $types[$j] instanceof GenericClassStringType) { + $genericType = self::intersect($types[$i]->getGenericType(), $types[$j]->getGenericType()); + $types[$i] = new GenericClassStringType($genericType); + array_splice($types, $j--, 1); + $typesCount--; + continue; + } + + if ( + $types[$i] instanceof ArrayType + && get_class($types[$i]) === ArrayType::class + && $types[$j] instanceof AccessoryArrayListType + && !$types[$j]->getIterableKeyType()->isSuperTypeOf($types[$i]->getIterableKeyType())->yes() + ) { + $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getIterableKeyType()); + if ($keyType instanceof NeverType) { + return $keyType; + } + $types[$i] = new ArrayType($keyType, $types[$i]->getItemType()); + continue; + } + continue; } @@ -851,4 +1431,14 @@ public static function intersect(Type ...$types): Type return new IntersectionType($types); } + public static function removeFalsey(Type $type): Type + { + return self::remove($type, StaticTypeFactory::falsey()); + } + + public static function removeTruthy(Type $type): Type + { + return self::remove($type, StaticTypeFactory::truthy()); + } + } diff --git a/src/Type/TypeResult.php b/src/Type/TypeResult.php new file mode 100644 index 0000000000..5d10bced1f --- /dev/null +++ b/src/Type/TypeResult.php @@ -0,0 +1,29 @@ + */ + public readonly array $reasons; + + /** + * @param T $type + * @param list $reasons + */ + public function __construct( + Type $type, + array $reasons, + ) + { + $this->type = $type; + $this->reasons = $reasons; + } + +} diff --git a/src/Type/TypeTraverser.php b/src/Type/TypeTraverser.php index 1507e8ce58..a95cf246c1 100644 --- a/src/Type/TypeTraverser.php +++ b/src/Type/TypeTraverser.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -class TypeTraverser +final class TypeTraverser { /** @var callable(Type $type, callable(Type): Type $traverse): Type */ diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 91dcc50bb0..7213a8140f 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -5,150 +5,28 @@ use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Generic\TemplateBenevolentUnionType; +use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateUnionType; use function array_merge; -/** @api */ -class TypeUtils +/** + * @api + */ +final class TypeUtils { /** - * @return ArrayType[] + * @return list */ - public static function getArrays(Type $type): array + public static function getConstantIntegers(Type $type): array { - if ($type instanceof ConstantArrayType) { - return $type->getAllArrays(); - } - - if ($type instanceof ArrayType) { - return [$type]; - } - - if ($type instanceof UnionType) { - $matchingTypes = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof ArrayType) { - return []; - } - foreach (self::getArrays($innerType) as $innerInnerType) { - $matchingTypes[] = $innerInnerType; - } - } - - return $matchingTypes; - } - - if ($type instanceof IntersectionType) { - $matchingTypes = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof ArrayType) { - continue; - } - foreach (self::getArrays($innerType) as $innerInnerType) { - $matchingTypes[] = $innerInnerType; - } - } - - return $matchingTypes; - } - - return []; + return self::map(ConstantIntegerType::class, $type, false); } /** - * @return ConstantArrayType[] - */ - public static function getConstantArrays(Type $type): array - { - if ($type instanceof ConstantArrayType) { - return $type->getAllArrays(); - } - - if ($type instanceof UnionType) { - $matchingTypes = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof ConstantArrayType) { - return []; - } - foreach (self::getConstantArrays($innerType) as $innerInnerType) { - $matchingTypes[] = $innerInnerType; - } - } - - return $matchingTypes; - } - - return []; - } - - /** - * @return ConstantStringType[] - */ - public static function getConstantStrings(Type $type): array - { - return self::map(ConstantStringType::class, $type, false); - } - - /** - * @return ConstantType[] - */ - public static function getConstantTypes(Type $type): array - { - return self::map(ConstantType::class, $type, false); - } - - /** - * @return ConstantType[] - */ - public static function getAnyConstantTypes(Type $type): array - { - return self::map(ConstantType::class, $type, false, false); - } - - /** - * @return ArrayType[] - */ - public static function getAnyArrays(Type $type): array - { - return self::map(ArrayType::class, $type, true, false); - } - - /** - * @deprecated Use PHPStan\Type\Type::generalize() instead. - */ - public static function generalizeType(Type $type, GeneralizePrecision $precision): Type - { - return $type->generalize($precision); - } - - /** - * @return string[] - */ - public static function getDirectClassNames(Type $type): array - { - if ($type instanceof TypeWithClassName) { - return [$type->getClassName()]; - } - - if ($type instanceof UnionType || $type instanceof IntersectionType) { - $classNames = []; - foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof TypeWithClassName) { - continue; - } - - $classNames[] = $innerType->getClassName(); - } - - return $classNames; - } - - return []; - } - - /** - * @return IntegerRangeType[] + * @return list */ public static function getIntegerRanges(Type $type): array { @@ -156,24 +34,7 @@ public static function getIntegerRanges(Type $type): array } /** - * @return ConstantScalarType[] - */ - public static function getConstantScalars(Type $type): array - { - return self::map(ConstantScalarType::class, $type, false); - } - - /** - * @internal - * @return ConstantArrayType[] - */ - public static function getOldConstantArrays(Type $type): array - { - return self::map(ConstantArrayType::class, $type, false); - } - - /** - * @return mixed[] + * @return list */ private static function map( string $typeClass, @@ -189,7 +50,9 @@ private static function map( if ($type instanceof UnionType) { $matchingTypes = []; foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof $typeClass) { + $matchingInner = self::map($typeClass, $innerType, $inspectIntersections, $stopOnUnmatched); + + if ($matchingInner === []) { if ($stopOnUnmatched) { return []; } @@ -197,7 +60,9 @@ private static function map( continue; } - $matchingTypes[] = $innerType; + foreach ($matchingInner as $innerMapped) { + $matchingTypes[] = $innerMapped; + } } return $matchingTypes; @@ -236,6 +101,29 @@ public static function toBenevolentUnion(Type $type): Type return $type; } + /** + * @return ($type is UnionType ? UnionType : Type) + */ + public static function toStrictUnion(Type $type): Type + { + if ($type instanceof TemplateBenevolentUnionType) { + return new TemplateUnionType( + $type->getScope(), + $type->getStrategy(), + $type->getVariance(), + $type->getName(), + static::toStrictUnion($type->getBound()), + $type->getDefault(), + ); + } + + if ($type instanceof BenevolentUnionType) { + return new UnionType($type->getTypes()); + } + + return $type; + } + /** * @return Type[] */ @@ -311,21 +199,40 @@ public static function getAccessoryTypes(Type $type): array return self::map(AccessoryType::class, $type, true, false); } - public static function containsCallable(Type $type): bool + public static function containsTemplateType(Type $type): bool { - if ($type->isCallable()->yes()) { - return true; - } + $containsTemplateType = false; + TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$containsTemplateType): Type { + if ($type instanceof TemplateType) { + $containsTemplateType = true; + } - if ($type instanceof UnionType) { - foreach ($type->getTypes() as $innerType) { - if ($innerType->isCallable()->yes()) { - return true; - } + return $containsTemplateType ? $type : $traverse($type); + }); + + return $containsTemplateType; + } + + public static function resolveLateResolvableTypes(Type $type, bool $resolveUnresolvableTypes = true): Type + { + /** @var int $ignoreResolveUnresolvableTypesLevel */ + $ignoreResolveUnresolvableTypesLevel = 0; + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($resolveUnresolvableTypes, &$ignoreResolveUnresolvableTypesLevel): Type { + while ($type instanceof LateResolvableType && (($resolveUnresolvableTypes && $ignoreResolveUnresolvableTypesLevel === 0) || $type->isResolvable())) { + $type = $type->resolve(); + } + + if ($type instanceof CallableType || $type instanceof ClosureType) { + $ignoreResolveUnresolvableTypesLevel++; + $result = $traverse($type); + $ignoreResolveUnresolvableTypesLevel--; + + return $result; } - } - return false; + return $traverse($type); + }); } } diff --git a/src/Type/TypehintHelper.php b/src/Type/TypehintHelper.php index db139aab8b..68ab00e39c 100644 --- a/src/Type/TypehintHelper.php +++ b/src/Type/TypehintHelper.php @@ -2,86 +2,34 @@ namespace PHPStan\Type; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name\FullyQualified; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionIntersectionType; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionUnionType; +use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantArrayType; 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 +final class TypehintHelper { - private static function getTypeObjectFromTypehint(string $typeString, ?string $selfClass): Type - { - switch (strtolower($typeString)) { - case 'int': - return new IntegerType(); - case 'bool': - return new BooleanType(); - case 'false': - return new ConstantBooleanType(false); - case 'string': - return new StringType(); - case 'float': - return new FloatType(); - case 'array': - return new ArrayType(new MixedType(), new MixedType()); - case 'iterable': - return new IterableType(new MixedType(), new MixedType()); - case 'callable': - return new CallableType(); - case 'void': - return new VoidType(); - case 'object': - return new ObjectWithoutClassType(); - case 'mixed': - return new MixedType(true); - case 'self': - return $selfClass !== null ? new ObjectType($selfClass) : new ErrorType(); - case 'parent': - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($selfClass !== null && $reflectionProvider->hasClass($selfClass)) { - $classReflection = $reflectionProvider->getClass($selfClass); - if ($classReflection->getParentClass() !== null) { - return new ObjectType($classReflection->getParentClass()->getName()); - } - } - return new NonexistentParentClassType(); - case 'static': - $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, ?Type $phpDocType = null, - ?string $selfClass = null, + ClassReflection|null $selfClass = null, bool $isVariadic = false, ): Type { if ($reflectionType === null) { - if ($isVariadic && $phpDocType instanceof ArrayType) { + if ($isVariadic && ($phpDocType instanceof ArrayType || $phpDocType instanceof ConstantArrayType)) { $phpDocType = $phpDocType->getItemType(); } return $phpDocType ?? new MixedType(); @@ -97,7 +45,7 @@ public static function decideTypeFromReflection( $types = []; foreach ($reflectionType->getTypes() as $innerReflectionType) { $innerType = self::decideTypeFromReflection($innerReflectionType, null, $selfClass, false); - if (!$innerType instanceof ObjectType) { + if (!$innerType->isObject()->yes()) { return new NeverType(); } @@ -111,28 +59,15 @@ public static function decideTypeFromReflection( throw new ShouldNotHappenException(sprintf('Unexpected type: %s', get_class($reflectionType))); } - $reflectionTypeString = $reflectionType->getName(); - if (str_ends_with(strtolower($reflectionTypeString), '\\object')) { - $reflectionTypeString = 'object'; - } - if (str_ends_with(strtolower($reflectionTypeString), '\\mixed')) { - $reflectionTypeString = 'mixed'; - } - if (str_ends_with(strtolower($reflectionTypeString), '\\false')) { - $reflectionTypeString = 'false'; - } - if (str_ends_with(strtolower($reflectionTypeString), '\\null')) { - $reflectionTypeString = 'null'; - } - if (str_ends_with(strtolower($reflectionTypeString), '\\never')) { - $reflectionTypeString = 'never'; + if ($reflectionType->isIdentifier()) { + $typeNode = new Identifier($reflectionType->getName()); + } else { + $typeNode = new FullyQualified($reflectionType->getName()); } - $type = self::getTypeObjectFromTypehint($reflectionTypeString, $selfClass); + $type = ParserNodeTypeToPHPStanType::resolve($typeNode, $selfClass); if ($reflectionType->allowsNull()) { $type = TypeCombinator::addNull($type); - } elseif ($phpDocType !== null) { - $phpDocType = TypeCombinator::removeNull($phpDocType); } return self::decideType($type, $phpDocType); @@ -140,20 +75,24 @@ public static function decideTypeFromReflection( public static function decideType( Type $type, - ?Type $phpDocType = null, + ?Type $phpDocType, ): Type { + if ($phpDocType !== null && $type->isNull()->no()) { + $phpDocType = TypeCombinator::removeNull($phpDocType); + } + if ($type instanceof BenevolentUnionType) { + return $type; + } + if ($phpDocType !== null && !$phpDocType instanceof ErrorType) { if ($phpDocType instanceof NeverType && $phpDocType->isExplicit()) { return $phpDocType; } - if ($type instanceof VoidType) { - return new VoidType(); - } if ( $type instanceof MixedType && !$type->isExplicitMixed() - && $phpDocType instanceof VoidType + && $phpDocType->isVoid()->yes() ) { return $phpDocType; } @@ -162,9 +101,9 @@ public static function decideType( if ($phpDocType instanceof UnionType) { $innerTypes = []; foreach ($phpDocType->getTypes() as $innerType) { - if ($innerType instanceof ArrayType) { + if ($innerType instanceof ArrayType || $innerType instanceof ConstantArrayType) { $innerTypes[] = new IterableType( - $innerType->getKeyType(), + $innerType->getIterableKeyType(), $innerType->getItemType(), ); } else { @@ -172,7 +111,7 @@ public static function decideType( } } $phpDocType = new UnionType($innerTypes); - } elseif ($phpDocType instanceof ArrayType) { + } elseif ($phpDocType instanceof ArrayType || $phpDocType instanceof ConstantArrayType) { $phpDocType = new IterableType( $phpDocType->getKeyType(), $phpDocType->getItemType(), @@ -181,8 +120,11 @@ public static function decideType( } if ( - (!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) - && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() + ($type->isCallable()->yes() && $phpDocType->isCallable()->yes()) + || ( + (!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) + && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() + ) ) { $resultType = $phpDocType; } else { diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 95c12bb756..08d678152a 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -5,29 +5,42 @@ use DateTime; use DateTimeImmutable; use DateTimeInterface; +use Error; +use Exception; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; 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\TemplateMixedType; 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 Throwable; +use function array_diff_assoc; +use function array_fill_keys; use function array_map; +use function array_merge; +use function array_slice; +use function array_unique; +use function array_values; use function count; use function implode; +use function md5; use function sprintf; -use function strpos; +use function str_contains; /** @api */ class UnionType implements CompoundType @@ -35,14 +48,21 @@ class UnionType implements CompoundType use NonGeneralizableTypeTrait; - /** @var Type[] */ - private array $types; + public const EQUAL_UNION_CLASSES = [ + DateTimeInterface::class => [DateTimeImmutable::class, DateTime::class], + Throwable::class => [Error::class, Exception::class], // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException + ]; + + private bool $sortedTypes = false; + + /** @var array */ + private array $cachedDescriptions = []; /** * @api * @param Type[] $types */ - public function __construct(array $types) + public function __construct(private array $types, private bool $normalized = false) { $throwException = static function () use ($types): void { throw new ShouldNotHappenException(sprintf( @@ -64,7 +84,6 @@ public function __construct(array $types) $throwException(); } - $this->types = UnionTypeHelper::sortTypes($types); } /** @@ -76,80 +95,180 @@ public function getTypes(): array } /** - * @return string[] + * @param callable(Type $type): bool $filterCb */ + public function filterTypes(callable $filterCb): Type + { + $newTypes = []; + $changed = false; + foreach ($this->getTypes() as $innerType) { + if (!$filterCb($innerType)) { + $changed = true; + continue; + } + + $newTypes[] = $innerType; + } + + if (!$changed) { + return $this; + } + + return TypeCombinator::union(...$newTypes); + } + + public function isNormalized(): bool + { + return $this->normalized; + } + + /** + * @return Type[] + */ + protected function getSortedTypes(): array + { + if ($this->sortedTypes) { + return $this->types; + } + + $this->types = UnionTypeHelper::sortTypes($this->types); + $this->sortedTypes = true; + + return $this->types; + } + public function getReferencedClasses(): array { - return UnionTypeHelper::getReferencedClasses($this->getTypes()); + $classes = []; + foreach ($this->types as $type) { + foreach ($type->getReferencedClasses() as $className) { + $classes[] = $className; + } + } + + return $classes; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array { - if ( - $type->equals(new ObjectType(DateTimeInterface::class)) - && $this->accepts( - new UnionType([new ObjectType(DateTime::class), new ObjectType(DateTimeImmutable::class)]), - $strictTypes, - )->yes() - ) { - return TrinaryLogic::createYes(); + return array_values(array_unique($this->pickFromTypes( + static fn (Type $type) => $type->getObjectClassNames(), + static fn (Type $type) => $type->isObject()->yes(), + ))); + } + + public function getObjectClassReflections(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getObjectClassReflections(), + static fn (Type $type) => $type->isObject()->yes(), + ); + } + + public function getArrays(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getArrays(), + static fn (Type $type) => $type->isArray()->yes(), + ); + } + + public function getConstantArrays(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getConstantArrays(), + static fn (Type $type) => $type->isArray()->yes(), + ); + } + + public function getConstantStrings(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getConstantStrings(), + static fn (Type $type) => $type->isString()->yes(), + ); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if (!$type->equals(new ObjectType($baseClass))) { + continue; + } + + $union = TypeCombinator::union( + ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes), + ); + if ($this->accepts($union, $strictTypes)->yes()) { + return AcceptsResult::createYes(); + } + break; } - if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType) { - return $type->isAcceptedBy($this, $strictTypes); + $result = AcceptsResult::createNo(); + foreach ($this->getSortedTypes() as $i => $innerType) { + $result = $result->or($innerType->accepts($type, $strictTypes)->decorateReasons(static fn (string $reason) => sprintf('Type #%d from the union: %s', $i + 1, $reason))); + } + if ($result->yes()) { + return $result; } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $innerType->accepts($type, $strictTypes); + if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) { + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof TemplateUnionType) { - $results[] = $type->isAcceptedBy($this, $strictTypes); + return $result->or($type->isAcceptedBy($this, $strictTypes)); + } + + if ($type->isEnum()->yes() && !$this->isEnum()->no()) { + $enumCasesUnion = TypeCombinator::union(...$type->getEnumCases()); + if (!$type->equals($enumCasesUnion)) { + return $this->accepts($enumCasesUnion, $strictTypes); + } } - return TrinaryLogic::createNo()->or(...$results); + return $result; } - public function isSuperTypeOf(Type $otherType): TrinaryLogic + public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult { if ( ($otherType instanceof self && !$otherType instanceof TemplateUnionType) || $otherType instanceof IterableType + || $otherType instanceof NeverType + || $otherType instanceof ConditionalType + || $otherType instanceof ConditionalTypeForParameter + || $otherType instanceof IntegerRangeType ) { return $otherType->isSubTypeOf($this); } $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $innerType->isSuperTypeOf($otherType); + foreach ($this->types as $innerType) { + $result = $innerType->isSuperTypeOf($otherType); + if ($result->yes()) { + return $result; + } + $results[] = $result; } + $result = IsSuperTypeOfResult::createNo()->or(...$results); if ($otherType instanceof TemplateUnionType) { - $results[] = $otherType->isSubTypeOf($this); + return $result->or($otherType->isSubTypeOf($this)); } - return TrinaryLogic::createNo()->or(...$results); + return $result; } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $otherType->isSuperTypeOf($innerType); - } - - return TrinaryLogic::extremeIdentity(...$results); + return IsSuperTypeOfResult::extremeIdentity(...array_map(static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType), $this->types)); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $acceptingType->accepts($innerType, $strictTypes); - } - - return TrinaryLogic::extremeIdentity(...$results); + return AcceptsResult::extremeIdentity(...array_map(static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), $this->types)); } public function equals(Type $type): bool @@ -162,25 +281,52 @@ public function equals(Type $type): bool return false; } - foreach ($this->types as $i => $innerType) { - if (!$innerType->equals($type->types[$i])) { + $otherTypes = $type->types; + foreach ($this->types as $innerType) { + $match = false; + foreach ($otherTypes as $i => $otherType) { + if (!$innerType->equals($otherType)) { + continue; + } + + $match = true; + unset($otherTypes[$i]); + break; + } + + if (!$match) { return false; } } - return true; + return count($otherTypes) === 0; } public function describe(VerbosityLevel $level): string { + if (isset($this->cachedDescriptions[$level->getLevelValue()])) { + return $this->cachedDescriptions[$level->getLevelValue()]; + } $joinTypes = static function (array $types) use ($level): string { $typeNames = []; - foreach ($types as $type) { + foreach ($types as $i => $type) { if ($type instanceof ClosureType || $type instanceof CallableType || $type instanceof TemplateUnionType) { $typeNames[] = sprintf('(%s)', $type->describe($level)); + } elseif ($type instanceof TemplateType) { + $isLast = $i >= count($types) - 1; + $bound = $type->getBound(); + if ( + !$isLast + && ($level->isTypeOnly() || $level->isValue()) + && !($bound instanceof MixedType && $bound->getSubtractedType() === null && !$bound instanceof TemplateMixedType) + ) { + $typeNames[] = sprintf('(%s)', $type->describe($level)); + } else { + $typeNames[] = $type->describe($level); + } } elseif ($type instanceof IntersectionType) { $intersectionDescription = $type->describe($level); - if (strpos($intersectionDescription, '&') !== false) { + if (str_contains($intersectionDescription, '&')) { $typeNames[] = sprintf('(%s)', $type->describe($level)); } else { $typeNames[] = $intersectionDescription; @@ -190,29 +336,49 @@ public function describe(VerbosityLevel $level): string } } + if ($level->isPrecise()) { + $duplicates = array_diff_assoc($typeNames, array_unique($typeNames)); + if (count($duplicates) > 0) { + $indexByDuplicate = array_fill_keys($duplicates, 0); + foreach ($typeNames as $key => $typeName) { + if (!isset($indexByDuplicate[$typeName])) { + continue; + } + + $typeNames[$key] = $typeName . '#' . ++$indexByDuplicate[$typeName]; + } + } + } else { + $typeNames = array_unique($typeNames); + } + + if (count($typeNames) > 1024) { + return implode('|', array_slice($typeNames, 0, 1024)) . "|\u{2026}"; + } + return implode('|', $typeNames); }; - return $level->handle( + return $this->cachedDescriptions[$level->getLevelValue()] = $level->handle( function () use ($joinTypes): string { $types = TypeCombinator::union(...array_map(static function (Type $type): Type { if ( - $type instanceof ConstantType - && !$type instanceof ConstantBooleanType + $type->isConstantValue()->yes() + && $type->isTrue()->or($type->isFalse())->no() ) { return $type->generalize(GeneralizePrecision::lessSpecific()); } return $type; - }, $this->types)); + }, $this->getSortedTypes())); if ($types instanceof UnionType) { - return $joinTypes($types->getTypes()); + return $joinTypes($types->getSortedTypes()); } return $joinTypes([$types]); }, - fn (): string => $joinTypes($this->types), + fn (): string => $joinTypes($this->getSortedTypes()), ); } @@ -225,21 +391,20 @@ private function hasInternal( callable $hasCallback, ): TrinaryLogic { - $results = []; - foreach ($this->types as $type) { + return TrinaryLogic::lazyExtremeIdentity($this->types, static function (Type $type) use ($canCallback, $hasCallback): TrinaryLogic { if ($canCallback($type)->no()) { - $results[] = TrinaryLogic::createNo(); - continue; + return TrinaryLogic::createNo(); } - $results[] = $hasCallback($type); - } - return TrinaryLogic::extremeIdentity(...$results); + return $hasCallback($type); + }); } /** + * @template TObject of object * @param callable(Type $type): TrinaryLogic $hasCallback - * @param callable(Type $type): object $getCallback + * @param callable(Type $type): TObject $getCallback + * @return TObject */ private function getInternal( callable $hasCallback, @@ -249,7 +414,7 @@ private function getInternal( /** @var TrinaryLogic|null $result */ $result = null; - /** @var object|null $object */ + /** @var TObject|null $object */ $object = null; foreach ($this->types as $type) { $has = $hasCallback($type); @@ -272,6 +437,21 @@ private function getInternal( return $object; } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName)); + } + + public function isObject(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isObject()); + } + + public function isEnum(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isEnum()); + } + public function canAccessProperties(): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties()); @@ -282,7 +462,7 @@ public function hasProperty(string $propertyName): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName)); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } @@ -320,7 +500,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -361,11 +541,11 @@ public function hasConstant(string $constantName): TrinaryLogic ); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { return $this->getInternal( static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName), - static fn (Type $type): ConstantReflection => $type->getConstant($constantName), + static fn (Type $type): ClassConstantReflection => $type->getConstant($constantName), ); } @@ -379,39 +559,126 @@ public function isIterableAtLeastOnce(): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce()); } + public function getArraySize(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getArraySize()); + } + public function getIterableKeyType(): Type { return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } + public function getFirstIterableKeyType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getFirstIterableKeyType()); + } + + public function getLastIterableKeyType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getLastIterableKeyType()); + } + public function getIterableValueType(): Type { return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType()); } + public function getFirstIterableValueType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getFirstIterableValueType()); + } + + public function getLastIterableValueType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getLastIterableValueType()); + } + public function isArray(): TrinaryLogic { - return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isArray()); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isArray()); + } + + public function isConstantArray(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray()); + } + + public function isOversizedArray(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray()); + } + + public function isList(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isList()); } public function isString(): TrinaryLogic { - return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isString()); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isString()); } public function isNumericString(): TrinaryLogic { - return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); } public function isNonEmptyString(): TrinaryLogic { - return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); + } + + public function isNonFalsyString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString()); } public function isLiteralString(): TrinaryLogic { - return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); + } + + public function isLowercaseString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString()); + } + + public function isUppercaseString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString()); + } + + public function isClassString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassString()); + } + + public function getClassStringObjectType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringObjectType()); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType()); + } + + public function isVoid(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isVoid()); + } + + public function isScalar(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isScalar()); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return $this->unionResults( + static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic() + )->toBooleanType(); } public function isOffsetAccessible(): TrinaryLogic @@ -419,6 +686,11 @@ public function isOffsetAccessible(): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal()); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); @@ -448,30 +720,106 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); + } + public function unsetOffset(Type $offsetType): Type { return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); } + public function getKeysArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArray()); + } + + public function getValuesArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getValuesArray()); + } + + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys)); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType)); + } + + public function flipArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->flipArray()); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType)); + } + + public function popArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->popArray()); + } + + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); + } + + public function searchArray(Type $needleType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType)); + } + + public function shiftArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->shiftArray()); + } + + public function shuffleArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->shuffleArray()); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); + } + + public function getEnumCases(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getEnumCases(), + static fn (Type $type) => $type->isObject()->yes(), + ); + } + public function isCallable(): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { + $acceptors = []; + foreach ($this->types as $type) { if ($type->isCallable()->no()) { continue; } - return $type->getCallableParametersAcceptors($scope); + $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope)); } - throw new ShouldNotHappenException(); + if (count($acceptors) === 0) { + throw new ShouldNotHappenException(); + } + + return $acceptors; } public function isCloneable(): TrinaryLogic @@ -479,44 +827,94 @@ public function isCloneable(): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable()); } - public function isSmallerThan(Type $otherType): TrinaryLogic + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion)); + } + + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion)); + } + + public function isNull(): TrinaryLogic { - return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNull()); } - public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic + public function isConstantValue(): TrinaryLogic { - return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue()); } - public function getSmallerType(): Type + public function isConstantScalarValue(): TrinaryLogic { - return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType()); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue()); } - public function getSmallerOrEqualType(): Type + public function getConstantScalarTypes(): array { - return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType()); + return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes()); } - public function getGreaterType(): Type + public function getConstantScalarValues(): array { - return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType()); + return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues()); } - public function getGreaterOrEqualType(): Type + public function isTrue(): TrinaryLogic { - return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType()); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isTrue()); } - public function isGreaterThan(Type $otherType): TrinaryLogic + public function isFalse(): TrinaryLogic { - return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFalse()); } - public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic + public function isBoolean(): TrinaryLogic { - return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type)); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isBoolean()); + } + + public function isFloat(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFloat()); + } + + public function isInteger(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger()); + } + + public function getSmallerType(PhpVersion $phpVersion): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion)); + } + + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion)); + } + + public function getGreaterType(PhpVersion $phpVersion): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion)); + } + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion)); + } + + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion)); + } + + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion)); } public function toBoolean(): BooleanType @@ -534,6 +932,13 @@ public function toNumber(): Type return $type; } + public function toAbsoluteNumber(): Type + { + $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); + + return $type; + } + public function toString(): Type { $type = $this->unionTypes(static fn (Type $type): Type => $type->toString()); @@ -562,6 +967,16 @@ public function toArray(): Type return $type; } + public function toArrayKey(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->toArrayKey()); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes)); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { $types = TemplateTypeMap::createEmpty(); @@ -586,10 +1001,8 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $myTypes = $this->types; } - $myTemplateTypes = []; foreach ($myTypes as $type) { if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) { - $myTemplateTypes[] = $type; continue; } $types = $types->union($type->inferTemplateTypes($receivedType)); @@ -650,17 +1063,58 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $types = []; + $changed = false; + + if (!$right instanceof self) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeCombinator::union(...$types); + } + + return $this; + } + public function tryRemove(Type $typeToRemove): ?Type { return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function exponentiate(Type $exponent): Type { - return new self($properties['types']); + return $this->unionTypes(static fn (Type $type): Type => $type->exponentiate($exponent)); + } + + public function getFiniteTypes(): array + { + $types = $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getFiniteTypes()); + $uniquedTypes = []; + foreach ($types as $type) { + $uniquedTypes[md5($type->describe(VerbosityLevel::cache()))] = $type; + } + + if (count($uniquedTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + return array_values($uniquedTypes); } /** @@ -668,7 +1122,7 @@ public static function __set_state(array $properties): Type */ protected function unionResults(callable $getResult): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map($getResult, $this->types)); + return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult); } /** @@ -676,7 +1130,7 @@ protected function unionResults(callable $getResult): TrinaryLogic */ private function notBenevolentUnionResults(callable $getResult): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map($getResult, $this->types)); + return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult); } /** @@ -687,4 +1141,57 @@ protected function unionTypes(callable $getType): Type return TypeCombinator::union(...array_map($getType, $this->types)); } + /** + * @template T + * @param callable(Type $type): list $getValues + * @param callable(Type $type): bool $criteria + * @return list + */ + protected function pickFromTypes( + callable $getValues, + callable $criteria, + ): array + { + $values = []; + foreach ($this->types as $type) { + $innerValues = $getValues($type); + if ($innerValues === []) { + return []; + } + + foreach ($innerValues as $innerType) { + $values[] = $innerType; + } + } + + return $values; + } + + public function toPhpDocNode(): TypeNode + { + return new UnionTypeNode(array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->getSortedTypes())); + } + + /** + * @template T + * @param callable(Type $type): list $getValues + * @return list + */ + private function notBenevolentPickFromTypes(callable $getValues): array + { + $values = []; + foreach ($this->types as $type) { + $innerValues = $getValues($type); + if ($innerValues === []) { + return []; + } + + foreach ($innerValues as $innerType) { + $values[] = $innerType; + } + } + + return $values; + } + } diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index 68f9d1c534..f91ea5cb45 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -3,34 +3,18 @@ namespace PHPStan\Type; use PHPStan\Type\Accessory\AccessoryType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use function array_merge; use function count; use function strcasecmp; use function usort; use const PHP_INT_MIN; -class UnionTypeHelper +final class UnionTypeHelper { - /** - * @param Type[] $types - * @return string[] - */ - public static function getReferencedClasses(array $types): array - { - $subTypeClasses = []; - foreach ($types as $type) { - $subTypeClasses[] = $type->getReferencedClasses(); - } - - return array_merge(...$subTypeClasses); - } - /** * @param Type[] $types * @return Type[] @@ -50,7 +34,7 @@ public static function sortTypes(array $types): array if ($a instanceof AccessoryType) { if ($b instanceof AccessoryType) { - return strcasecmp($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); + return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); } return 1; @@ -108,26 +92,47 @@ public static function sortTypes(array $types): array } if ($a instanceof ConstantStringType && $b instanceof ConstantStringType) { - return strcasecmp($a->getValue(), $b->getValue()); + return self::compareStrings($a->getValue(), $b->getValue()); } - if ($a instanceof ConstantArrayType && $b instanceof ConstantArrayType) { - if ($a->isEmpty()) { - if ($b->isEmpty()) { + if ($a->isConstantArray()->yes() && $b->isConstantArray()->yes()) { + if ($a->isIterableAtLeastOnce()->no()) { + if ($b->isIterableAtLeastOnce()->no()) { return 0; } return -1; - } elseif ($b->isEmpty()) { + } elseif ($b->isIterableAtLeastOnce()->no()) { return 1; } - return strcasecmp($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); + return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); } - return strcasecmp($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); + if ( + ($a instanceof CallableType || $a instanceof ClosureType) + && ($b instanceof CallableType || $b instanceof ClosureType) + ) { + return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); + } + + if ($a->isString()->yes() && $b->isString()->yes()) { + return self::compareStrings($a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())); + } + + return self::compareStrings($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); }); return $types; } + private static function compareStrings(string $a, string $b): int + { + $cmp = strcasecmp($a, $b); + if ($cmp !== 0) { + return $cmp; + } + + return $a <=> $b; + } + } diff --git a/src/Type/UsefulTypeAliasResolver.php b/src/Type/UsefulTypeAliasResolver.php index 6cfb76d373..f4ad8abbb8 100644 --- a/src/Type/UsefulTypeAliasResolver.php +++ b/src/Type/UsefulTypeAliasResolver.php @@ -10,7 +10,7 @@ use function array_key_exists; use function sprintf; -class UsefulTypeAliasResolver implements TypeAliasResolver +final class UsefulTypeAliasResolver implements TypeAliasResolver { /** @var array */ @@ -69,7 +69,7 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): return null; } - $className = $nameScope->getClassName(); + $className = $nameScope->getClassNameForTypeAlias(); if ($className === null) { return null; } diff --git a/src/Type/ValueOfType.php b/src/Type/ValueOfType.php new file mode 100644 index 0000000000..d02b7d11f7 --- /dev/null +++ b/src/Type/ValueOfType.php @@ -0,0 +1,103 @@ +type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('value-of<%s>', $this->type->describe($level)); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + if ($this->type->isEnum()->yes()) { + $valueTypes = []; + foreach ($this->type->getEnumCases() as $enumCase) { + $valueType = $enumCase->getBackingValueType(); + if ($valueType === null) { + continue; + } + + $valueTypes[] = $valueType; + } + + return TypeCombinator::union(...$valueTypes); + } + + return $this->type->getIterableValueType(); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('value-of'), [$this->type->toPhpDocNode()]); + } + +} diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index fe3bfc0610..12b5ecbd15 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -2,15 +2,19 @@ namespace PHPStan\Type; -use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateType; -class VerbosityLevel +final class VerbosityLevel { private const TYPE_ONLY = 1; @@ -21,16 +25,28 @@ class VerbosityLevel /** @var self[] */ private static array $registry; + /** + * @param self::* $value + */ private function __construct(private int $value) { } + /** + * @param self::* $value + */ private static function create(int $value): self { self::$registry[$value] ??= new self($value); return self::$registry[$value]; } + /** @return self::* */ + public function getLevelValue(): int + { + return $this->value; + } + /** @api */ public static function typeOnly(): self { @@ -60,28 +76,56 @@ public function isTypeOnly(): bool return $this->value === self::TYPE_ONLY; } + public function isValue(): bool + { + return $this->value === self::VALUE; + } + + public function isPrecise(): bool + { + return $this->value === self::PRECISE; + } + /** @api */ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self { - $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose): Type { + $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose, &$veryVerbose): Type { if ($type->isCallable()->yes()) { $moreVerbose = true; - return $type; + + // Keep checking if we need to be very verbose. + return $traverse($type); } - if ($type instanceof ConstantType && !$type instanceof NullType) { + if ($type->isConstantValue()->yes() && $type->isNull()->no()) { $moreVerbose = true; + + // For ConstantArrayType we need to keep checking if we need to be very verbose. + if (!$type->isArray()->no()) { + return $traverse($type); + } + return $type; } if ( // synced with IntersectionType::describe() $type instanceof AccessoryNonEmptyStringType + || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType || $type instanceof NonEmptyArrayType + || $type instanceof AccessoryArrayListType ) { $moreVerbose = true; return $type; } + if ( + $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType + ) { + $moreVerbose = true; + $veryVerbose = true; + return $type; + } if ($type instanceof IntegerRangeType) { $moreVerbose = true; return $type; @@ -91,19 +135,25 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc /** @var bool $moreVerbose */ $moreVerbose = false; + /** @var bool $veryVerbose */ + $veryVerbose = false; TypeTraverser::map($acceptingType, $moreVerboseCallback); + if ($veryVerbose) { + return self::precise(); + } + if ($moreVerbose) { - return self::value(); + $verbosity = self::value(); } if ($acceptedType === null) { - return self::typeOnly(); + return $verbosity ?? self::typeOnly(); } $containsInvariantTemplateType = false; TypeTraverser::map($acceptingType, static function (Type $type, callable $traverse) use (&$containsInvariantTemplateType): Type { - if ($type instanceof GenericObjectType) { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { $reflection = $type->getClassReflection(); if ($reflection !== null) { $templateTypeMap = $reflection->getTemplateTypeMap(); @@ -126,14 +176,20 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc }); if (!$containsInvariantTemplateType) { - return self::typeOnly(); + return $verbosity ?? self::typeOnly(); } /** @var bool $moreVerbose */ $moreVerbose = false; + /** @var bool $veryVerbose */ + $veryVerbose = false; TypeTraverser::map($acceptedType, $moreVerboseCallback); - return $moreVerbose ? self::value() : self::typeOnly(); + if ($veryVerbose) { + return self::precise(); + } + + return $moreVerbose ? self::value() : $verbosity ?? self::typeOnly(); } /** @@ -165,19 +221,15 @@ public function handle( return $valueCallback(); } - if ($this->value === self::CACHE) { - if ($cacheCallback !== null) { - return $cacheCallback(); - } - - if ($preciseCallback !== null) { - return $preciseCallback(); - } + if ($cacheCallback !== null) { + return $cacheCallback(); + } - return $valueCallback(); + if ($preciseCallback !== null) { + return $preciseCallback(); } - throw new ShouldNotHappenException(); + return $valueCallback(); } } diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index db99d85e7e..83c5a05726 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -2,8 +2,12 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\Traits\FalseyBooleanTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -17,6 +21,7 @@ class VoidType implements Type { + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -32,34 +37,41 @@ public function __construct() { } - /** - * @return string[] - */ public function getReferencedClasses(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createFromBoolean($type instanceof self); + return new AcceptsResult($type->isVoid()->or($type->isNull()), []); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return IsSuperTypeOfResult::createYes(); } if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -77,6 +89,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -97,7 +114,67 @@ public function toArray(): Type return new ErrorType(); } - public function isArray(): TrinaryLogic + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return new NullType(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -117,22 +194,79 @@ public function isNonEmptyString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new self(); + return new IdentifierTypeNode('void'); } } diff --git a/src/autoloadFunctions.php b/src/autoloadFunctions.php new file mode 100644 index 0000000000..3239fe6aff --- /dev/null +++ b/src/autoloadFunctions.php @@ -0,0 +1,11 @@ + + */ +function autoloadFunctions(): array // phpcs:ignore Squiz.Functions.GlobalFunction.Found +{ + return $GLOBALS['__phpstanAutoloadFunctions'] ?? []; +} diff --git a/src/debugScope.php b/src/debugScope.php new file mode 100644 index 0000000000..6f331c97ba --- /dev/null +++ b/src/debugScope.php @@ -0,0 +1,14 @@ + + * @return list',args?:mixed[],object?:object}> * @throws void */ public function getTrace(); @@ -78,7 +78,7 @@ class Exception implements Throwable final public function getLine(): int {} /** - * @return list + * @return list',args?:mixed[],object?:object}> * @throws void */ final public function getTrace(): array {} @@ -125,7 +125,7 @@ class Error implements Throwable final public function getLine(): int {} /** - * @return list + * @return list',args?:mixed[],object?:object}> * @throws void */ final public function getTrace(): array {} diff --git a/stubs/ImagickPixel.stub b/stubs/ImagickPixel.stub new file mode 100644 index 0000000000..49ac0c9161 --- /dev/null +++ b/stubs/ImagickPixel.stub @@ -0,0 +1,9 @@ +, g: int<0, 255>, b: int<0, 255>, a: int<0, 1>} : ($normalized is 1 ? array{r: float, g: float, b: float, a: float} : ($normalized is 2 ? array{r: int<0, 255>, g: int<0, 255>, b: int<0, 255>, a: int<0, 255>} : array{}))) + */ + public function getColor(int $normalized = 0): array; +} diff --git a/stubs/PDOStatement.stub b/stubs/PDOStatement.stub index 79637d8370..46d0be7c5d 100644 --- a/stubs/PDOStatement.stub +++ b/stubs/PDOStatement.stub @@ -7,5 +7,16 @@ */ class PDOStatement implements Traversable, IteratorAggregate { + /** + * @template T of object + * @param class-string $class + * @param array $ctorArgs + * @return false|T + */ + public function fetchObject($class = \stdClass::class, array $ctorArgs = array()) {} + /** + * @return array{name: string, table?: string, native_type?: string, len: int, flags: array, precision: int<0, max>, pdo_type: PDO::PARAM_* }|false + */ + public function getColumnMeta(int $column) {} } diff --git a/stubs/ReflectionClass.stub b/stubs/ReflectionClass.stub index e5d2a0908a..06f8e08a2e 100644 --- a/stubs/ReflectionClass.stub +++ b/stubs/ReflectionClass.stub @@ -2,12 +2,12 @@ /** * @template-covariant T of object - * @property-read class-string $name */ class ReflectionClass { /** + * @readonly * @var class-string */ public $name; @@ -31,7 +31,7 @@ class ReflectionClass public function newInstance(...$args) {} /** - * @param array $args + * @param array $args * * @return T */ @@ -43,7 +43,7 @@ class ReflectionClass public function newInstanceWithoutConstructor(); /** - * @return array> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/ReflectionClassConstant.stub b/stubs/ReflectionClassConstant.stub index 669ccbef89..4396980e06 100644 --- a/stubs/ReflectionClassConstant.stub +++ b/stubs/ReflectionClassConstant.stub @@ -3,7 +3,7 @@ class ReflectionClassConstant { /** - * @return array> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/ReflectionClassWithLazyObjects.stub b/stubs/ReflectionClassWithLazyObjects.stub new file mode 100644 index 0000000000..1a53ae8750 --- /dev/null +++ b/stubs/ReflectionClassWithLazyObjects.stub @@ -0,0 +1,113 @@ + + */ + public $name; + + /** + * @param T|class-string $argument + * @throws ReflectionException + */ + public function __construct($argument) {} + + /** + * @return class-string + */ + public function getName() : string; + + /** + * @param mixed ...$args + * + * @return T + */ + public function newInstance(...$args) {} + + /** + * @param array $args + * + * @return T + */ + public function newInstanceArgs(array $args) {} + + /** + * @return T + */ + public function newInstanceWithoutConstructor(); + + /** + * @return list> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } + + /** + * @param callable(T): void $initializer + * @return T + */ + public function newLazyGhost(callable $initializer, int $options = 0): object + { + } + + /** + * @param callable(T): T $factory + * @return T + */ + public function newLazyProxy(callable $factory, int $options = 0): object + { + } + + /** + * @param T $object + * @param callable(T): void $initializer + */ + public function resetAsLazyGhost(object $object, callable $initializer, int $options = 0): void + { + } + + /** + * @param T $object + * @param callable(T): T $factory + */ + public function resetAsLazyProxy(object $object, callable $factory, int $options = 0): void + { + } + + /** + * @param T $object + * @return T + */ + public function initializeLazyObject(object $object): object + { + } + + /** + * @param T $object + * @return T + */ + public function markLazyObjectAsInitialized(object $object): object + { + } + + /** + * @param T $object + */ + public function getLazyInitializer(object $object): ?callable + { + } + + /** + * @param T $object + */ + public function isUninitializedLazyObject(object $object): bool + { + } +} diff --git a/stubs/ReflectionEnum.stub b/stubs/ReflectionEnum.stub new file mode 100644 index 0000000000..6a13532d9a --- /dev/null +++ b/stubs/ReflectionEnum.stub @@ -0,0 +1,27 @@ + + */ +class ReflectionEnum extends ReflectionClass +{ + + /** + * @return (T is BackedEnum ? ReflectionEnumBackedCase[] : ReflectionEnumUnitCase[]) + */ + public function getCases(): array {} + + /** + * @return (T is BackedEnum ? ReflectionEnumBackedCase : ReflectionEnumUnitCase) + * @throws ReflectionException + */ + public function getCase(string $name): ReflectionEnumUnitCase {} + + /** + * @phpstan-assert-if-true self $this + * @phpstan-assert-if-true !null $this->getBackingType() + */ + public function isBacked(): bool {} + +} diff --git a/stubs/ReflectionEnumWithLazyObjects.stub b/stubs/ReflectionEnumWithLazyObjects.stub new file mode 100644 index 0000000000..3955a5013f --- /dev/null +++ b/stubs/ReflectionEnumWithLazyObjects.stub @@ -0,0 +1,27 @@ + + */ +class ReflectionEnum extends ReflectionClass +{ + + /** + * @return (T is BackedEnum ? ReflectionEnumBackedCase[] : ReflectionEnumUnitCase[]) + */ + public function getCases(): array {} + + /** + * @return (T is BackedEnum ? ReflectionEnumBackedCase : ReflectionEnumUnitCase) + * @throws ReflectionException + */ + public function getCase(string $name): ReflectionEnumUnitCase {} + + /** + * @phpstan-assert-if-true self $this + * @phpstan-assert-if-true !null $this->getBackingType() + */ + public function isBacked(): bool {} + +} diff --git a/stubs/ReflectionFunctionAbstract.stub b/stubs/ReflectionFunctionAbstract.stub index 36e48df100..0154996741 100644 --- a/stubs/ReflectionFunctionAbstract.stub +++ b/stubs/ReflectionFunctionAbstract.stub @@ -9,7 +9,7 @@ abstract class ReflectionFunctionAbstract public function getFileName () {} /** - * @return array> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/ReflectionMethod.stub b/stubs/ReflectionMethod.stub new file mode 100644 index 0000000000..cb060a6c97 --- /dev/null +++ b/stubs/ReflectionMethod.stub @@ -0,0 +1,16 @@ +> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/ReflectionProperty.stub b/stubs/ReflectionProperty.stub index 312688067d..002daeeeee 100644 --- a/stubs/ReflectionProperty.stub +++ b/stubs/ReflectionProperty.stub @@ -3,7 +3,7 @@ class ReflectionProperty { /** - * @return array> + * @return list> */ public function getAttributes(?string $name = null, int $flags = 0) { diff --git a/stubs/SplObjectStorage.stub b/stubs/SplObjectStorage.stub index 72a75982dd..146785e522 100644 --- a/stubs/SplObjectStorage.stub +++ b/stubs/SplObjectStorage.stub @@ -5,9 +5,10 @@ * @template TData * * @template-implements Iterator + * @template-implements SeekableIterator * @template-implements ArrayAccess */ -class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess +class SplObjectStorage implements Countable, Iterator, SeekableIterator, Serializable, ArrayAccess { /** @@ -31,11 +32,6 @@ class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess */ public function detach(object $object): void { } - /** - * @param TObject $object - */ - public function detach(object $object): void { } - /** * @param TObject $object */ @@ -47,12 +43,12 @@ class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess public function getInfo() { } /** - * @param \SplObjectStorage $storage + * @param \SplObjectStorage<*, *> $storage */ public function removeAll(SplObjectStorage $storage): void { } /** - * @param \SplObjectStorage $storage + * @param \SplObjectStorage<*, *> $storage */ public function removeAllExcept(SplObjectStorage $storage): void { } diff --git a/stubs/WeakReference.stub b/stubs/WeakReference.stub index 5f23dbca9c..060dfe1a8d 100644 --- a/stubs/WeakReference.stub +++ b/stubs/WeakReference.stub @@ -26,4 +26,9 @@ final class WeakReference */ final class WeakMap implements \ArrayAccess, \Countable, \IteratorAggregate { + /** + * @param TKey $offset + * @return TValue + */ + public function offsetGet($offset) {} } diff --git a/stubs/arrayFunctions.stub b/stubs/arrayFunctions.stub index 929aaa589b..3297e47316 100644 --- a/stubs/arrayFunctions.stub +++ b/stubs/arrayFunctions.stub @@ -17,51 +17,204 @@ function array_reduce( ) {} /** - * @template TKey of array-key - * @template TValue of mixed - * @template TUser of mixed + * @template T of mixed * - * @param array $one - * @param callable(TValue, TKey, TUser=): mixed $two - * @param TUser $three + * @param array $array + * @return ($array is non-empty-array ? non-empty-list : list) + */ +function array_values(array $array): array {} + +/** + * @template TKey as (int|string) + * @template T + * @template TArray as array * - * @return true + * @param TArray $array + * @param callable(T,T):int $callback */ -function array_walk( - array &$one, - callable $two, - $three = null -): bool {} +function uasort(array &$array, callable $callback): bool +{} /** - * @template T of mixed + * @template T + * @template TArray as array * - * @param array $one - * @param callable(T, T): int $two + * @param TArray $array + * @param callable(T,T):int $callback */ -function uasort( - array &$one, - callable $two -): bool {} +function usort(array &$array, callable $callback): bool +{} /** - * @template T of mixed + * @template TKey as (int|string) + * @template T + * @template TArray as array + * + * @param TArray $array + * @param callable(TKey,TKey):int $callback + */ +function uksort(array &$array, callable $callback): bool +{ +} + +/** + * @template TV of mixed + * @template TK of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array + */ +function array_udiff( + array $one, + array $two, + callable $three +): array {} + +/** + * @param array $value + * @return ($value is list ? true : false) + */ +function array_is_list(array $value): bool {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_diff_uassoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_diff_ukey( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_intersect_uassoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * + * @return array + */ +function array_intersect_ukey( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * + * @return array + */ +function array_udiff_assoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @param callable(TK, TK): int $four + * @return array + */ +function array_udiff_uassoc( + array $one, + array $two, + callable $three, + callable $four +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array + */ +function array_uintersect_assoc( + array $one, + array $two, + callable $three, +): array {} + +/** + * @template TK of array-key + * @template TV of mixed * - * @param array $one - * @param callable(T, T): int $two + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @param callable(TK, TK): int $four + * @return array */ -function usort( - array &$one, - callable $two -): bool {} +function array_uintersect_uassoc( + array $one, + array $two, + callable $three, + callable $four +): array {} /** - * @template T of array-key + * @template TK of array-key + * @template TV of mixed * - * @param array $one - * @param callable(T, T): int $two + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array */ -function uksort( - array &$one, - callable $two -): bool {} +function array_uintersect( + array $one, + array $two, + callable $three, +): array {} diff --git a/stubs/core.stub b/stubs/core.stub new file mode 100644 index 0000000000..de4c904423 --- /dev/null +++ b/stubs/core.stub @@ -0,0 +1,329 @@ + $result + * @param-out array|string> $result + */ +function parse_str(string $string, array &$result): void {} + +/** + * @param array $result + * @param-out array|string> $result + */ +function mb_parse_str(string $string, array &$result): bool {} + +/** @param-out float $percent */ +function similar_text(string $string1, string $string2, ?float &$percent = null) : int {} + +/** + * @param mixed $output + * @param mixed $result_code + * + * @param-out list $output + * @param-out int $result_code + * + * @return string|false + */ +function exec(string $command, &$output, &$result_code) {} + +/** + * @param mixed $result_code + * @param-out int $result_code + * + * @return string|false + */ +function system(string $command, &$result_code) {} + +/** + * @param mixed $result_code + * @param-out int $result_code + */ +function passthru(string $command, &$result_code): ?bool {} + + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + */ +function shuffle(array &$array): bool +{ +} + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + */ +function sort(array &$array, int $flags = SORT_REGULAR): bool +{ +} + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + */ +function rsort(array &$array, int $flags = SORT_REGULAR): bool +{ +} + +/** + * @param string $string + * @param-out null $string + */ +function sodium_memzero(string &$string): void +{ +} + +/** + * @param resource $stream + * @param mixed $vars + * @param-out string|int|float|null $vars + * + * @return list|int|false + */ +function fscanf($stream, string $format, &...$vars) {} + +/** + * @param mixed $war + * @param mixed $vars + * @param-out string|int|float|null $war + * @param-out string|int|float|null $vars + * + * @return int|array|null + */ +function sscanf(string $string, string $format, &$war, &...$vars) {} + +/** + * @template TFlags as int + * + * @param string $pattern + * @param string $subject + * @param mixed $matches + * @param TFlags $flags + * @param-out ( + * TFlags is 1 + * ? array> + * : (TFlags is 2 + * ? list> + * : (TFlags is 256|257 + * ? array> + * : (TFlags is 258 + * ? list> + * : (TFlags is 512|513 + * ? array> + * : (TFlags is 514 + * ? list> + * : (TFlags is 770 + * ? list> + * : (TFlags is 0 ? array> : array) + * ) + * ) + * ) + * ) + * ) + * ) + * ) $matches + * @return int|false + */ +function preg_match_all($pattern, $subject, &$matches = [], int $flags = 1, int $offset = 0) {} + +/** + * @template TFlags as int-mask<0, 256, 512> + * + * @param string $pattern + * @param string $subject + * @param mixed $matches + * @param TFlags $flags + * @param-out ( + * TFlags is 256 + * ? array + * : (TFlags is 512 + * ? array + * : (TFlags is 768 + * ? array + * : array + * ) + * ) + * ) $matches + * @return 1|0|false + */ +function preg_match($pattern, $subject, &$matches = [], int $flags = 0, int $offset = 0) {} + +/** + * @param string|string[] $pattern + * @param callable(string[]):string $callback + * @param string|array $subject + * @param int $count + * @param-out 0|positive-int $count + * @return ($subject is array ? list|null : string|null) + */ +function preg_replace_callback($pattern, $callback, $subject, int $limit = -1, &$count = null, int $flags = 0) {} + +/** + * @param string|string[] $pattern + * @param string|array $replacement + * @param string|array $subject + * @param int $count + * @param-out 0|positive-int $count + * @return ($subject is array ? list|null : string|null) + */ +function preg_replace($pattern, $replacement, $subject, int $limit = -1, &$count = null) {} + +/** + * @param string|string[] $pattern + * @param string|array $replacement + * @param string|array $subject + * @param int $count + * @param-out 0|positive-int $count + * @return ($subject is array ? list : string|null) + */ +function preg_filter($pattern, $replacement, $subject, int $limit = -1, &$count = null) {} + +/** + * @param array|string $search + * @param array|string $replace + * @param array|string $subject + * @param-out int $count + * @return list|string + */ +function str_replace($search, $replace, $subject, ?int &$count = null) {} + +/** + * @param array|string $search + * @param array|string $replace + * @param array|string $subject + * @param-out int $count + * @return list|string + */ +function str_ireplace($search, $replace, $subject, ?int &$count = null) {} + +/** + * @template TRead of null|array + * @template TWrite of null|array + * @template TExcept of null|array + * @param TRead $read + * @param TWrite $write + * @param TExcept $except + * @return false|0|positive-int + * @param-out (TRead is null ? null : array) $read + * @param-out (TWrite is null ? null : array) $write + * @param-out (TExcept is null ? null : array) $except + */ +function stream_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, ?int $microseconds = null) {} + +/** + * @param resource $stream + * @param-out 0|1 $would_block + */ +function flock($stream, int $operation, mixed &$would_block = null): bool {} + +/** + * @param-out int $error_code + * @param-out string $error_message + * @return resource|false + */ +function fsockopen(string $hostname, int $port = -1, ?int &$error_code = null, ?string &$error_message = null, ?float $timeout = null) {} + +/** + * @param-out string $filename + * @param-out int $line + */ +function headers_sent(?string &$filename = null, ?int &$line = null): bool {} + +/** + * @param-out callable-string $callable_name + * @return ($value is callable ? true : false) + */ +function is_callable(mixed $value, bool $syntax_only = false, ?string &$callable_name = null): bool {} + +/** + * @param float|int $num + * @return ($num is float ? float : $num is int ? non-negative-int : float|non-negative-int) + */ +function abs($num) {} + +/** + * @return ($categorize is true ? array> : array) + */ +function get_defined_constants(bool $categorize = false): array {} + +/** + * @param array $long_options + * @param mixed $rest_index + * @param-out positive-int $rest_index + * @return __benevolent|array|array>|false> + */ +function getopt(string $short_options, array $long_options = [], &$rest_index = null) {} + diff --git a/stubs/date.stub b/stubs/date.stub index c9dc5c1882..e58c7f0682 100644 --- a/stubs/date.stub +++ b/stubs/date.stub @@ -1,19 +1,9 @@ * @implements \Traversable */ diff --git a/stubs/dom.stub b/stubs/dom.stub index 074232a7d3..df03926915 100644 --- a/stubs/dom.stub +++ b/stubs/dom.stub @@ -30,6 +30,17 @@ class DOMDocument class DOMNode { + /** + * @var DOMNamedNodeMap|null + */ + public $attributes; + + /** + * @phpstan-assert-if-true !null $this->attributes + * @return bool + */ + public function hasAttributes() {} + } class DOMElement extends DOMNode @@ -56,8 +67,9 @@ class DOMElement extends DOMNode /** * @template-covariant TNode as DOMNode * @implements Traversable + * @implements IteratorAggregate */ -class DOMNodeList implements Traversable +class DOMNodeList implements Traversable, IteratorAggregate, Countable { /** @@ -68,15 +80,20 @@ class DOMNodeList implements Traversable } -class DOMAttr +class DOMXPath { - /** @var DOMDocument */ - public $ownerDocument; + /** + * @param string $expression + * @param DOMNode|null $contextNode + * @param boolean $registerNodeNS + * @return DOMNodeList|false + */ + public function query($expression, $contextNode, $registerNodeNS) {} } -class DOMCharacterData +class DOMAttr { /** @var DOMDocument */ diff --git a/stubs/ext-ds.stub b/stubs/ext-ds.stub index 1075d9a31d..e05628d968 100644 --- a/stubs/ext-ds.stub +++ b/stubs/ext-ds.stub @@ -7,18 +7,18 @@ use Countable; use JsonSerializable; use OutOfBoundsException; use OutOfRangeException; -use Traversable; +use IteratorAggregate; use UnderflowException; /** * @template-covariant TKey * @template-covariant TValue - * @extends Traversable + * @extends IteratorAggregate */ -interface Collection extends Traversable, Countable, JsonSerializable +interface Collection extends IteratorAggregate, Countable, JsonSerializable { /** - * @return Collection + * @return static */ public function copy(); @@ -61,7 +61,7 @@ final class Deque implements Sequence * @param (callable(TValue): bool)|null $callback * @return Deque */ - public function filter(callable $callback = null): Deque + public function filter(?callable $callback = null): Deque { } @@ -190,7 +190,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TKey, TValue): bool)|null $callback * @return Map */ - public function filter(callable $callback = null): Map + public function filter(?callable $callback = null): Map { } @@ -284,7 +284,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TValue, TValue): int)|null $comparator * @return void */ - public function sort(callable $comparator = null) + public function sort(?callable $comparator = null) { } @@ -292,7 +292,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TValue, TValue): int)|null $comparator * @return Map */ - public function sorted(callable $comparator = null): Map + public function sorted(?callable $comparator = null): Map { } @@ -300,7 +300,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TKey, TKey): int)|null $comparator * @return void */ - public function ksort(callable $comparator = null) + public function ksort(?callable $comparator = null) { } @@ -308,7 +308,7 @@ final class Map implements Collection, ArrayAccess * @param (callable(TKey, TKey): int)|null $comparator * @return Map */ - public function ksorted(callable $comparator = null): Map + public function ksorted(?callable $comparator = null): Map { } @@ -348,8 +348,8 @@ final class Map implements Collection, ArrayAccess } /** - * @template-covariant TKey - * @template-covariant TValue + * @template TKey + * @template TValue */ final class Pair implements JsonSerializable { @@ -392,11 +392,6 @@ interface Sequence extends Collection, ArrayAccess */ public function apply(callable $callback); - /** - * @return Sequence - */ - public function copy(); - /** * @param TValue ...$values */ @@ -406,7 +401,7 @@ interface Sequence extends Collection, ArrayAccess * @param (callable(TValue): bool)|null $callback * @return Sequence */ - public function filter(callable $callback = null); + public function filter(?callable $callback = null); /** * @param TValue $value @@ -437,7 +432,7 @@ interface Sequence extends Collection, ArrayAccess * @param string $glue * @return string */ - public function join(string $glue = null): string; + public function join(?string $glue = null): string; /** * @return TValue @@ -462,6 +457,7 @@ interface Sequence extends Collection, ArrayAccess /** * @return TValue * @throws \UnderflowException + * @phpstan-impure */ public function pop(); @@ -500,6 +496,7 @@ interface Sequence extends Collection, ArrayAccess /** * @return TValue * @throws \UnderflowException + * @phpstan-impure */ public function shift(); @@ -512,13 +509,13 @@ interface Sequence extends Collection, ArrayAccess * @param (callable(TValue, TValue): int)|null $comparator * @return void */ - public function sort(callable $comparator = null); + public function sort(?callable $comparator = null); /** * @param (callable(TValue, TValue): int)|null $comparator * @return Sequence */ - public function sorted(callable $comparator = null); + public function sorted(?callable $comparator = null); /** * @param TValue ...$values @@ -566,7 +563,7 @@ final class Vector implements Sequence * @param (callable(TValue, TValue): int)|null $comparator * @return Vector */ - public function sorted(callable $comparator = null): Vector + public function sorted(?callable $comparator = null): Vector { } @@ -574,7 +571,7 @@ final class Vector implements Sequence * @param (callable(TValue): bool)|null $callback * @return Vector */ - public function filter(callable $callback = null): Vector + public function filter(?callable $callback = null): Vector { } @@ -645,7 +642,7 @@ final class Set implements Collection, ArrayAccess * @param (callable(TValue): bool)|null $callback * @return Set */ - public function filter(callable $callback = null): Set + public function filter(?callable $callback = null): Set { } @@ -734,7 +731,7 @@ final class Set implements Collection, ArrayAccess /** * @param (callable(TValue, TValue): int)|null $comparator */ - public function sort(callable $comparator = null): void + public function sort(?callable $comparator = null): void { } @@ -742,7 +739,7 @@ final class Set implements Collection, ArrayAccess * @param (callable(TValue, TValue): int)|null $comparator * @return Set */ - public function sorted(callable $comparator = null): Set + public function sorted(?callable $comparator = null): Set { } @@ -804,6 +801,7 @@ final class Stack implements Collection, ArrayAccess /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { @@ -857,6 +855,7 @@ final class Queue implements Collection, ArrayAccess /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { @@ -901,6 +900,7 @@ final class PriorityQueue implements Collection /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { diff --git a/stubs/ibm_db2.stub b/stubs/ibm_db2.stub new file mode 100644 index 0000000000..544473f615 --- /dev/null +++ b/stubs/ibm_db2.stub @@ -0,0 +1,9 @@ + + * + * @template-extends IteratorIterator + */ +class AppendIterator extends IteratorIterator { + + /** + * @param TIterator $iterator + * @return void + */ + public function append(Iterator $iterator) {} + + /** + * @return ArrayIterator + */ + public function getArrayIterator() {} + +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Iterator + * + * @template-extends IteratorIterator + */ +class NoRewindIterator extends IteratorIterator { + /** + * @param TIterator $iterator + */ + public function __construct(Iterator $iterator) {} + + /** + * @return TValue + */ + public function current() {} + + /** + * @return TKey + */ + public function key() {} +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Iterator + * + * @template-implements OuterIterator + * @template-extends IteratorIterator + */ +class LimitIterator extends IteratorIterator implements OuterIterator { + /** + * @param TIterator $iterator + */ + public function __construct(Iterator $iterator, int $offset = 0, int $count = -1) {} + + /** + * @return TValue + */ + public function current() {} + + /** + * @return TKey + */ + public function key() {} +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Iterator + * + * @template-extends IteratorIterator + */ +class InfiniteIterator extends IteratorIterator { + /** + * @param TIterator $iterator + */ + public function __construct(Iterator $iterator) {} + + /** + * @return TValue + */ + public function current() {} + + /** + * @return TKey + */ + public function key() {} +} + +/** + * @template TKey + * @template TValue + * @template TIterator as Iterator + * + * @template-implements OuterIterator + * @template-implements ArrayAccess + * + * @template-extends IteratorIterator + */ +class CachingIterator extends IteratorIterator implements OuterIterator, ArrayAccess, Countable { + const CALL_TOSTRING = 1 ; + const CATCH_GET_CHILD = 16 ; + const TOSTRING_USE_KEY = 2 ; + const TOSTRING_USE_CURRENT = 4 ; + const TOSTRING_USE_INNER = 8 ; + const FULL_CACHE = 256 ; + + /** + * @param TIterator $iterator + * @param int-mask-of $flags + */ + public function __construct(Iterator $iterator, int $flags = self::CALL_TOSTRING) {} + + /** + * @return TValue + */ + public function current() {} + + /** + * @return TKey + */ + public function key() {} + + /** + * @return array + */ + public function getCache() {} +} + +/** + * @template TKey + * @template TValue + * @template TIterator of Traversable + * + * @template-extends FilterIterator + */ +class RegexIterator extends FilterIterator { + const MATCH = 0 ; + const GET_MATCH = 1 ; + const ALL_MATCHES = 2 ; + const SPLIT = 3 ; + const REPLACE = 4 ; + const USE_KEY = 1 ; + + /** + * @param Iterator $iterator + * @param self::MATCH|self::GET_MATCH|self::ALL_MATCHES|self::SPLIT|self::REPLACE $mode + */ + public function __construct(Iterator $iterator, string $regex, int $mode = self::MATCH, int $flags = 0, int $preg_flags = 0) {} + + /** + * @return TValue + */ + public function current() {} + + /** + * @return TKey + */ + public function key() {} +} + +/** + * @template-implements Iterator + */ +class EmptyIterator implements Iterator { + /** + * @return never + */ + public function current() {} + + /** + * @return never + */ + public function key() {} + + /** + * @return false + */ + public function valid() {} +} diff --git a/stubs/json_validate.stub b/stubs/json_validate.stub new file mode 100644 index 0000000000..31ed4a777b --- /dev/null +++ b/stubs/json_validate.stub @@ -0,0 +1,10 @@ + $flags + * @phpstan-assert-if-true =non-empty-string $json + */ +function json_validate(string $json, int $depth = 512, int $flags = 0): bool +{ +} diff --git a/stubs/mysqli.stub b/stubs/mysqli.stub index 350a645ba3..cc88f4f6e1 100644 --- a/stubs/mysqli.stub +++ b/stubs/mysqli.stub @@ -1,10 +1,43 @@ |numeric-string + */ + public $affected_rows; +} +class mysqli_result +{ /** - * @var int|string + * @var int<0,max>|numeric-string + */ + public $num_rows; + + /** + * @template T of object + * @param class-string $class + * @param array $constructor_args + * @return T|null|false + */ + function fetch_object(string $class = 'stdClass', array $constructor_args = []) {} +} + + +/** + * @template T of object + * + * @param class-string $class + * @param array $constructor_args + * @return T|null|false + */ +function mysqli_fetch_object(mysqli_result $result, string $class = 'stdClass', array $constructor_args = []) {} + +class mysqli_stmt +{ + /** + * @var int<-1,max>|numeric-string */ public $affected_rows; @@ -34,7 +67,7 @@ class mysqli_stmt public $insert_id; /** - * @var 0|positive-int + * @var int<0,max>|numeric-string */ public $num_rows; diff --git a/stubs/runtime/Attribute.php b/stubs/runtime/Attribute.php index e41f42cfc4..18d7de3da4 100644 --- a/stubs/runtime/Attribute.php +++ b/stubs/runtime/Attribute.php @@ -67,3 +67,17 @@ final class ReturnTypeWillChange { } } + +if (\PHP_VERSION_ID < 80200 && !class_exists('AllowDynamicProperties', false)) { + #[Attribute(Attribute::TARGET_CLASS)] + final class AllowDynamicProperties + { + } +} + +if (\PHP_VERSION_ID < 80200 && !class_exists('SensitiveParameter', false)) { + #[Attribute(Attribute::TARGET_PARAMETER)] + final class SensitiveParameter + { + } +} diff --git a/stubs/runtime/Enum/UnitEnum.php b/stubs/runtime/Enum/UnitEnum.php index a8d1a8d468..0a9e6282e6 100644 --- a/stubs/runtime/Enum/UnitEnum.php +++ b/stubs/runtime/Enum/UnitEnum.php @@ -8,7 +8,7 @@ interface UnitEnum { /** - * @return static[] + * @return list */ public static function cases(): array; } diff --git a/stubs/socket_select.stub b/stubs/socket_select.stub new file mode 100644 index 0000000000..25a6cb1f1a --- /dev/null +++ b/stubs/socket_select.stub @@ -0,0 +1,12 @@ +|null &$read + * @param array|null &$write + * @param array|null &$except + * @param-out ($read is not null ? array : null) $read + * @param-out ($write is not null ? array : null) $write + * @param-out ($except is not null ? array : null) $except + * @return int|false + */ +function socket_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, int $microseconds = 0) {} diff --git a/stubs/socket_select_php8.stub b/stubs/socket_select_php8.stub new file mode 100644 index 0000000000..f2a1704b42 --- /dev/null +++ b/stubs/socket_select_php8.stub @@ -0,0 +1,11 @@ +|null &$read + * @param array|null &$write + * @param array|null &$except + * @param-out ($read is not null ? array : null) $read + * @param-out ($write is not null ? array : null) $write + * @param-out ($except is not null ? array : null) $except + */ +function socket_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, int $microseconds = 0): int|false {} diff --git a/stubs/spl.stub b/stubs/spl.stub index 43a2a06d97..daf46ae1a7 100644 --- a/stubs/spl.stub +++ b/stubs/spl.stub @@ -46,6 +46,12 @@ class SplDoublyLinkedList implements \Iterator, \ArrayAccess { * @return TValue */ public function bottom () {} + + /** + * @param int $offset + * @return TValue + */ + public function offsetGet ($offset) {} } /** diff --git a/stubs/typeCheckingFunctions.stub b/stubs/typeCheckingFunctions.stub new file mode 100644 index 0000000000..4f06338125 --- /dev/null +++ b/stubs/typeCheckingFunctions.stub @@ -0,0 +1,130 @@ +|\Countable ? true : false) + */ +function is_countable(mixed $value): bool +{ + +} + +/** + * @return ($value is object ? true : false) + */ +function is_object(mixed $value): bool +{ + +} + +/** + * @return ($value is scalar ? true : false) + */ +function is_scalar(mixed $value): bool +{ + +} + +/** + * @return ($value is int ? true : false) + */ +function is_int(mixed $value): bool +{ + +} + +/** + * @return ($value is int ? true : false) + */ +function is_integer(mixed $value): bool +{ + +} + +/** + * @return ($value is int ? true : false) + */ +function is_long(mixed $value): bool +{ + +} + +/** + * @phpstan-assert-if-true =resource $value + * @return bool + */ +function is_resource(mixed $value): bool +{ + +} + +/** + * @return ($value is array ? true : false) + */ +function is_array(mixed $value): bool +{ + +} + +/** + * @return ($value is iterable ? true : false) + */ +function is_iterable(mixed $value): bool +{ + +} diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index af416b7a44..325f09f20b 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -5,16 +5,15 @@ use Bug4288\MyClass; use Bug4713\Service; use ExtendingKnownClassWithCheck\Foo; -use PHPStan\File\FileHelper; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use function array_reverse; use function extension_loaded; -use function method_exists; use function restore_error_handler; +use function sprintf; use const PHP_VERSION_ID; class AnalyserIntegrationTest extends PHPStanTestCase @@ -62,7 +61,7 @@ public function testAnonymousClassWithInheritedConstructor(): void public function testNestedFunctionCallsDoNotCauseExcessiveFunctionNesting(): void { if (extension_loaded('xdebug')) { - $this->markTestSkipped('This test takes too long with XDebug enabled.'); + $this->markTestSkipped('This test takes too long with Xdebug enabled.'); } $errors = $this->runAnalyse(__DIR__ . '/data/nested-functions.php'); $this->assertNoErrors($errors); @@ -73,13 +72,8 @@ public function testExtendingUnknownClass(): void $errors = $this->runAnalyse(__DIR__ . '/data/extending-unknown-class.php'); $this->assertCount(1, $errors); - if (self::$useStaticReflectionProvider) { - $this->assertSame(5, $errors[0]->getLine()); - $this->assertSame('Class ExtendingUnknownClass\Foo extends unknown class ExtendingUnknownClass\Bar.', $errors[0]->getMessage()); - } else { - $this->assertNull($errors[0]->getLine()); - $this->assertSame('Class ExtendingUnknownClass\Bar not found.', $errors[0]->getMessage()); - } + $this->assertSame(5, $errors[0]->getLine()); + $this->assertSame('Class ExtendingUnknownClass\Foo extends unknown class ExtendingUnknownClass\Bar.', $errors[0]->getMessage()); } public function testExtendingKnownClassWithCheck(): void @@ -155,13 +149,16 @@ public function testAnonymousClassWithWrongFilename(): void public function testExtendsPdoStatementCrash(): void { - if (PHP_VERSION_ID >= 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped(); - } $errors = $this->runAnalyse(__DIR__ . '/data/extends-pdo-statement.php'); $this->assertNoErrors($errors); } + public function testBug12803(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12803.php'); + $this->assertNoErrors($errors); + } + public function testArrayDestructuringArrayDimFetch(): void { $errors = $this->runAnalyse(__DIR__ . '/data/array-destructuring-array-dim-fetch.php'); @@ -186,28 +183,11 @@ public function testClassExistsAutoloadingError(): void public function testCollectWarnings(): void { - if (PHP_VERSION_ID >= 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Fatal error in PHP 8.0'); - } restore_error_handler(); $errors = $this->runAnalyse(__DIR__ . '/data/declaration-warning.php'); - if (self::$useStaticReflectionProvider) { - $this->assertCount(1, $errors); - $this->assertSame('Parameter #1 $i of method DeclarationWarning\Bar::doFoo() is not optional.', $errors[0]->getMessage()); - $this->assertSame(22, $errors[0]->getLine()); - return; - } - $this->assertCount(2, $errors); - $messages = [ - 'Declaration of DeclarationWarning\Bar::doFoo(int $i): void should be compatible with DeclarationWarning\Foo::doFoo(): void', - 'Parameter #1 $i of method DeclarationWarning\Bar::doFoo() is not optional.', - ]; - if (PHP_VERSION_ID < 70400) { - $messages = array_reverse($messages); - } - foreach ($messages as $i => $message) { - $this->assertSame($message, $errors[$i]->getMessage()); - } + $this->assertCount(1, $errors); + $this->assertSame('Parameter #1 $i of method DeclarationWarning\Bar::doFoo() is not optional.', $errors[0]->getMessage()); + $this->assertSame(22, $errors[0]->getLine()); } public function testPropertyAssignIntersectionStaticTypeBug(): void @@ -248,10 +228,18 @@ public function testTwoSameClassesInSingleFile(): void $this->assertSame(36, $error->getLine()); } + public function testBug6936(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6936.php'); + $this->assertNoErrors($errors); + } + public function testBug3405(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3405.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Magic constant __TRAIT__ is always empty outside a trait.', $errors[0]->getMessage()); + $this->assertSame(16, $errors[0]->getLine()); } public function testBug3415(): void @@ -280,10 +268,7 @@ public function testBug3686(): void public function testBug3379(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-3379.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-3379.php'); $this->assertCount(1, $errors); $this->assertSame('Constant SOME_UNKNOWN_CONST not found.', $errors[0]->getMessage()); } @@ -332,6 +317,22 @@ public function testBug3309(): void $this->assertNoErrors($errors); } + public function testBug11649(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11649.php'); + $this->assertNoErrors($errors); + } + + public function testBug6872(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6872.php'); + $this->assertNoErrors($errors); + } + public function testBug3769(): void { require_once __DIR__ . '/../Rules/Generics/data/bug-3769.php'; @@ -339,6 +340,13 @@ public function testBug3769(): void $this->assertNoErrors($errors); } + public function testBug6301(): void + { + require_once __DIR__ . '/../Rules/Generics/data/bug-6301.php'; + $errors = $this->runAnalyse(__DIR__ . '/../Rules/Generics/data/bug-6301.php'); + $this->assertNoErrors($errors); + } + public function testBug3922(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3922-integration.php'); @@ -351,6 +359,13 @@ public function testBug1843(): void $this->assertNoErrors($errors); } + public function testBug9711(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9711.php'); + $this->assertCount(1, $errors); + $this->assertSame('Function in_array invoked with 1 parameter, 2-3 required.', $errors[0]->getMessage()); + } + public function testBug4713(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4713.php'); @@ -359,7 +374,7 @@ public function testBug4713(): void $reflectionProvider = $this->createReflectionProvider(); $class = $reflectionProvider->getClass(Service::class); - $parameter = ParametersAcceptorSelector::selectSingle($class->getNativeMethod('createInstance')->getVariants())->getParameters()[0]; + $parameter = $class->getNativeMethod('createInstance')->getOnlyVariant()->getParameters()[0]; $defaultValue = $parameter->getDefaultValue(); $this->assertInstanceOf(ConstantStringType::class, $defaultValue); $this->assertSame(Service::class, $defaultValue->getValue()); @@ -372,17 +387,19 @@ public function testBug4288(): void $reflectionProvider = $this->createReflectionProvider(); $class = $reflectionProvider->getClass(MyClass::class); - $parameter = ParametersAcceptorSelector::selectSingle($class->getNativeMethod('paginate')->getVariants())->getParameters()[0]; + $parameter = $class->getNativeMethod('paginate')->getOnlyVariant()->getParameters()[0]; $defaultValue = $parameter->getDefaultValue(); $this->assertInstanceOf(ConstantIntegerType::class, $defaultValue); $this->assertSame(10, $defaultValue->getValue()); $nativeProperty = $class->getNativeReflection()->getProperty('test'); - if (!method_exists($nativeProperty, 'getDefaultValue')) { - return; - } - - $this->assertSame(10, $nativeProperty->getDefaultValue()); + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + $defaultValueType = $initializerExprTypeResolver->getType( + $nativeProperty->getDefaultValueExpression(), + InitializerExprContext::fromClassReflection($class->getNativeProperty('test')->getDeclaringClass()), + ); + $this->assertInstanceOf(ConstantIntegerType::class, $defaultValueType); + $this->assertSame(10, $defaultValueType->getValue()); } public function testBug4702(): void @@ -394,20 +411,17 @@ public function testBug4702(): void public function testFunctionThatExistsOn72AndLater(): void { $errors = $this->runAnalyse(__DIR__ . '/data/ldap-exop-passwd.php'); - if (PHP_VERSION_ID >= 70200) { + if (PHP_VERSION_ID < 80100) { $this->assertNoErrors($errors); return; } $this->assertCount(1, $errors); - $this->assertSame('Function ldap_exop_passwd not found.', $errors[0]->getMessage()); + $this->assertSame('Parameter #1 $ldap of function ldap_exop_passwd expects LDAP\Connection, resource given.', $errors[0]->getMessage()); } public function testBug4715(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $errors = $this->runAnalyse(__DIR__ . '/data/bug-4715.php'); $this->assertNoErrors($errors); } @@ -434,9 +448,19 @@ public function testBug5231Two(): void $this->assertNotEmpty($errors); } + public function testBug12512(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12512.php'); + $this->assertNoErrors($errors); + } + public function testBug5529(): void { - $errors = $this->runAnalyse(__DIR__ . '/data/bug-5529.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-5529.php'); $this->assertNoErrors($errors); } @@ -506,6 +530,15 @@ public function testBug6466(): void $this->assertNoErrors($errors); } + public function testBug6494(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6494.php'); + $this->assertNoErrors($errors); + } + public function testBug6253(): void { $errors = $this->runAnalyse( @@ -523,9 +556,9 @@ 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('Dumped type: \'Bug6442\\\B\'', $errors[0]->getMessage()); $this->assertSame(9, $errors[0]->getLine()); - $this->assertSame('Dumped type: \'Bug6442\\\B\'', $errors[1]->getMessage()); + $this->assertSame('Dumped type: \'Bug6442\\\A\'', $errors[1]->getMessage()); $this->assertSame(9, $errors[1]->getLine()); } @@ -538,7 +571,9 @@ public function testBug6375(): void public function testBug6501(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-6501.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('PHPDoc tag @var with type R of Exception|stdClass is not subtype of native type stdClass.', $errors[0]->getMessage()); + $this->assertSame(24, $errors[0]->getLine()); } public function testBug6114(): void @@ -569,21 +604,1013 @@ public function testBug6740(): void $this->assertNoErrors($errors); } - /** - * @param string[]|null $allAnalysedFiles - * @return Error[] - */ - private function runAnalyse(string $file, ?array $allAnalysedFiles = null): array + public function testBug6866(): void { - $file = $this->getFileHelper()->normalizePath($file); - /** @var Analyser $analyser */ + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6866.php'); + $this->assertNoErrors($errors); + } + + public function testBug6649(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6649.php'); + $this->assertNoErrors($errors); + } + + public function testBug12778(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12778.php'); + $this->assertNoErrors($errors); + } + + public function testBug6842(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6842.php'); + $this->assertCount(2, $errors); + $this->assertSame('Generator expects value type T of DateTimeInterface, DateTime|DateTimeImmutable|T of DateTimeInterface given.', $errors[0]->getMessage()); + $this->assertSame(28, $errors[0]->getLine()); + + $this->assertSame('Generator expects value type T of DateTimeInterface, DateTime|DateTimeImmutable|T of DateTimeInterface given.', $errors[1]->getMessage()); + $this->assertSame(54, $errors[1]->getLine()); + } + + public function testBug6896(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6896.php'); + $this->assertCount(4, $errors); + $this->assertSame('Generic type IteratorIterator<(int|string), mixed> in PHPDoc tag @return does not specify all template types of class IteratorIterator: TKey, TValue, TIterator', $errors[0]->getMessage()); + $this->assertSame(38, $errors[0]->getLine()); + $this->assertSame('Generic type LimitIterator<(int|string), mixed> in PHPDoc tag @return does not specify all template types of class LimitIterator: TKey, TValue, TIterator', $errors[1]->getMessage()); + $this->assertSame(38, $errors[1]->getLine()); + $this->assertSame('Method Bug6896\RandHelper::getPseudoRandomWithUrl() return type with generic class Bug6896\XIterator does not specify its types: TKey, TValue', $errors[2]->getMessage()); + $this->assertSame(38, $errors[2]->getLine()); + $this->assertSame('Method Bug6896\RandHelper::getPseudoRandomWithUrl() should return array|Bug6896\XIterator|IteratorIterator|LimitIterator but returns TRandList of array|Traversable.', $errors[3]->getMessage()); + $this->assertSame(42, $errors[3]->getLine()); + } + + public function testBug6940(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6940.php'); + $this->assertCount(1, $errors); + $this->assertSame('Loose comparison using == between array{} and array{} will always evaluate to true.', $errors[0]->getMessage()); + $this->assertSame(12, $errors[0]->getLine()); + } + + public function testBug1447(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-1447.php'); + $this->assertNoErrors($errors); + } + + public function testBug5081(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5081.php'); + $this->assertNoErrors($errors); + } + + public function testBug1388(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-1388.php'); + $this->assertNoErrors($errors); + } + + public function testBug4308(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4308.php'); + $this->assertNoErrors($errors); + } + + public function testBug4732(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4732.php'); + $this->assertNoErrors($errors); + } + + public function testBug6160(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6160.php'); + $this->assertCount(2, $errors); + $this->assertSame('Parameter #1 $flags of static method Bug6160\HelloWorld::split() expects 0|1|2, 94561 given.', $errors[0]->getMessage()); + $this->assertSame(19, $errors[0]->getLine()); + $this->assertSame('Parameter #1 $flags of static method Bug6160\HelloWorld::split() expects 0|1|2, \'sdf\' given.', $errors[1]->getMessage()); + $this->assertSame(23, $errors[1]->getLine()); + } + + public function testBug6979(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6979.php'); + $this->assertNoErrors($errors); + } + + public function testBug7030(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7030.php'); + $this->assertCount(1, $errors); + $this->assertSame('PHPDoc tag @method has invalid value (array getItemsForID($id, $quantity, $shippingPostCode = null, $wholesalerList = null, $shippingLatitude = + null, $shippingLongitude = null, $shippingNeutralShipping = null)): Unexpected token "\n * ", expected type at offset 193 on line 6', $errors[0]->getMessage()); + } + + public function testBug7012(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7012.php'); + $this->assertNoErrors($errors); + } + + public function testBug6192(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6192.php'); + $this->assertNoErrors($errors); + } + + public function testBug7068(): void + { + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-7068.php'); + $this->assertNoErrors($errors); + } + + public function testDiscussion6993(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-6993.php'); + $this->assertCount(1, $errors); + $this->assertSame('Parameter #1 $specificable of method Bug6993\AndSpecificationValidator::isSatisfiedBy() expects Bug6993\Foo, Bug6993\Bar given.', $errors[0]->getMessage()); + } + + public function testBug7077(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7077.php'); + $this->assertNoErrors($errors); + } + + public function testBug7078(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-7078.php'); + $this->assertNoErrors($errors); + } + + public function testBug7116(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7116.php'); + $this->assertNoErrors($errors); + } + + public function testBug3853(): void + { + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-3853.php'); + $this->assertNoErrors($errors); + } + + public function testBug7135(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7135.php'); + $this->assertCount(1, $errors); + $this->assertSame('Cannot create callable from the new operator.', $errors[0]->getMessage()); + } + + public function testDiscussion7124(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/discussion-7124.php'); + $this->assertCount(4, $errors); + $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool, 0|1|2=): bool, Closure(int, bool): bool given.', $errors[0]->getMessage()); + $this->assertSame(38, $errors[0]->getLine()); + $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool, 0|1|2=): bool, Closure(int): bool given.', $errors[1]->getMessage()); + $this->assertSame(45, $errors[1]->getLine()); + $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(0|1|2): bool, Closure(bool): bool given.', $errors[2]->getMessage()); + $this->assertSame(52, $errors[2]->getLine()); + $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool): bool, Closure(int): bool given.', $errors[3]->getMessage()); + $this->assertSame(59, $errors[3]->getLine()); + } + + public function testBug7214(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7214.php'); + $this->assertCount(1, $errors); + $this->assertSame('Method Bug7214\HelloWorld::getFoo() has no return type specified.', $errors[0]->getMessage()); + $this->assertSame(6, $errors[0]->getLine()); + } + + public function testBug12327(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12327.php'); + $this->assertCount(1, $errors); + + $this->assertSame('Class Bug12327\DoesNotMatter uses unknown trait Bug12327\ThisTriggersTheIssue.', $errors[0]->getMessage()); + $this->assertSame(15, $errors[0]->getLine()); + } + + public function testBug7215(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7215.php'); + $this->assertNoErrors($errors); + } + + public function testBug7094(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7094.php'); + $this->assertCount(6, $errors); + + $this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() contains unresolvable type.', $errors[0]->getMessage()); + $this->assertSame(74, $errors[0]->getLine()); + $this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects string, int given.', $errors[1]->getMessage()); + $this->assertSame(75, $errors[1]->getLine()); + $this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects 5|6|7, 3 given.', $errors[2]->getMessage()); + $this->assertSame(76, $errors[2]->getLine()); + $this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects string, int given.', $errors[3]->getMessage()); + $this->assertSame(78, $errors[3]->getLine()); + $this->assertSame('Return type of call to method Bug7094\Foo::getAttribute() contains unresolvable type.', $errors[4]->getMessage()); + $this->assertSame(79, $errors[4]->getLine()); + + $this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, non-empty-array given.', $errors[5]->getMessage()); + $this->assertSame(29, $errors[5]->getLine()); + } + + public function testOffsetAccess(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/nsrt/offset-access.php'); + $this->assertCount(1, $errors); + $this->assertSame('PHPDoc tag @return contains unresolvable type.', $errors[0]->getMessage()); + $this->assertSame(42, $errors[0]->getLine()); + } + + public function testUnresolvableParameter(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/unresolvable-parameter.php'); + $this->assertCount(3, $errors); + $this->assertSame('Parameter #2 $array of function array_map expects array, list|false given.', $errors[0]->getMessage()); + $this->assertSame(18, $errors[0]->getLine()); + $this->assertSame('Method UnresolvableParameter\Collection::pipeInto() has parameter $class with no type specified.', $errors[1]->getMessage()); + $this->assertSame(30, $errors[1]->getLine()); + $this->assertSame('PHPDoc tag @param for parameter $class contains unresolvable type.', $errors[2]->getMessage()); + $this->assertSame(30, $errors[2]->getLine()); + } + + public function testBug7248(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7248.php'); + $this->assertNoErrors($errors); + } + + public function testBug7351(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7351.php'); + $this->assertNoErrors($errors); + } + + public function testBug7381(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7381.php'); + $this->assertNoErrors($errors); + } + + public function testBug7153(): void + { + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-7153.php'); + $this->assertNoErrors($errors); + } + + public function testBug7275(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7275.php'); + $this->assertNoErrors($errors); + } + + public function testBug7500(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7500.php'); + $this->assertNoErrors($errors); + } + + public function testBug12767(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12767.php'); + $this->assertCount(3, $errors); + + $this->assertSame('Expected type int, actual: *ERROR*', $errors[0]->getMessage()); + $this->assertSame('Undefined variable: $field1', $errors[1]->getMessage()); + $this->assertSame('Undefined variable: $field2', $errors[2]->getMessage()); + } + + public function testBug7554(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7554.php'); + $this->assertCount(2, $errors); + + $this->assertSame(sprintf('Parameter #1 $%s of function count expects array|Countable, list|string>>|false given.', PHP_VERSION_ID < 80000 ? 'var' : 'value'), $errors[0]->getMessage()); + $this->assertSame(26, $errors[0]->getLine()); + + $this->assertSame('Cannot access offset int<1, max> on list}>|false.', $errors[1]->getMessage()); + $this->assertSame(27, $errors[1]->getLine()); + } + + public function testBug7637(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7637.php'); + $this->assertCount(3, $errors); + + $this->assertSame('Method Bug7637\HelloWorld::getProperty() has invalid return type Bug7637\rex_backend_login.', $errors[0]->getMessage()); + $this->assertSame(54, $errors[0]->getLine()); + + $this->assertSame('Method Bug7637\HelloWorld::getProperty() has invalid return type Bug7637\rex_timer.', $errors[1]->getMessage()); + $this->assertSame(54, $errors[1]->getLine()); + + $this->assertSame('Call to function is_string() with string will always evaluate to true.', $errors[2]->getMessage()); + $this->assertSame(57, $errors[2]->getLine()); + } + + public function testBug12671(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12671.php'); + $this->assertNoErrors($errors); + } + + public function testBug7737(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7737.php'); + $this->assertNoErrors($errors); + } + + public function testBug7762(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7762.php'); + $this->assertCount(2, $errors); + $this->assertSame('Function json_decode invoked with 0 parameters, 1-4 required.', $errors[0]->getMessage()); + $this->assertSame('Function json_encode invoked with 0 parameters, 1-3 required.', $errors[1]->getMessage()); + } + + public function testPrestashopInfiniteRunXmlLoaderBug(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/prestashop-xml-loader.php'); + $this->assertCount(4, $errors); + $this->assertSame('Property PrestaShopBundleInfiniteRunBug\XmlLoader::$data_path has no type specified.', $errors[0]->getMessage()); + $this->assertSame('Method PrestaShopBundleInfiniteRunBug\XmlLoader::getEntityInfo() has no return type specified.', $errors[1]->getMessage()); + $this->assertSame('Method PrestaShopBundleInfiniteRunBug\XmlLoader::getEntityInfo() has parameter $entity with no type specified.', $errors[2]->getMessage()); + $this->assertSame('Method PrestaShopBundleInfiniteRunBug\XmlLoader::getEntityInfo() has parameter $exists with no type specified.', $errors[3]->getMessage()); + } + + public function testBug7320(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7320.php'); + $this->assertCount(1, $errors); + $this->assertSame('Parameter #1 $c of function Bug7320\foo expects callable(int=): void, Closure(int): void given.', $errors[0]->getMessage()); + $this->assertSame(13, $errors[0]->getLine()); + } + + public function testBug7581(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7581.php'); + $this->assertNoErrors($errors); + } + + public function testBug7903(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7903.php'); + $this->assertNoErrors($errors); + } + + public function testBug7901(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7901.php'); + $this->assertNoErrors($errors); + } + + public function testBug7918(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7918.php'); + $this->assertNoErrors($errors); + } + + public function testBug7140(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7140.php'); + $this->assertNoErrors($errors); + } + + public function testArrayUnion(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/array-union.php'); + $this->assertNoErrors($errors); + } + + public function testBug6948(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6948.php'); + $this->assertNoErrors($errors); + } + + public function testBug7963(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7963.php'); + $this->assertNoErrors($errors); + } + + public function testBug7963Two(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7963-two.php'); + $this->assertNoErrors($errors); + } + + public function testBug8078(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8078.php'); + $this->assertNoErrors($errors); + } + + public function testBug8072(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8072.php'); + $this->assertNoErrors($errors); + } + + public function testBug7787(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7787.php'); + $this->assertCount(1, $errors); + $this->assertSame('Reflection error: Circular reference to class "Bug7787\TestClass"', $errors[0]->getMessage()); + } + + public function testBug3865(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-3865.php'); + $this->assertCount(1, $errors); + $this->assertSame('The @extends tag of class Bug3865\RecursiveClass describes Bug3865\RecursiveClass but the class extends Bug3865\EntityRepository.', $errors[0]->getMessage()); + $this->assertSame(14, $errors[0]->getLine()); + } + + public function testBug5312(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5312.php'); + $this->assertCount(3, $errors); + $this->assertSame('Parameter $object of method Bug5312\Updatable::update() has invalid type Bug5312\T.', $errors[0]->getMessage()); + $this->assertSame(13, $errors[0]->getLine()); + $this->assertSame('Type Bug5312\T in generic type Bug5312\Updatable in PHPDoc tag @param for parameter $object is not subtype of template type T of Bug5312\Updatable of interface Bug5312\Updatable.', $errors[1]->getMessage()); + $this->assertSame(13, $errors[1]->getLine()); + $this->assertSame('Type Bug5312\T in generic type Bug5312\Updatable in PHPDoc tag @param for parameter $object is not subtype of template type T of Bug5312\Updatable of interface Bug5312\Updatable.', $errors[2]->getMessage()); + $this->assertSame(13, $errors[2]->getLine()); + } + + public function testBug5390(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5390.php'); + $this->assertCount(3, $errors); + $this->assertSame('Property Bug5390\A::$b is never written, only read.', $errors[0]->getMessage()); + $this->assertSame(9, $errors[0]->getLine()); + $this->assertSame('Method Bug5390\A::infiniteRecursion() has no return type specified.', $errors[1]->getMessage()); + $this->assertSame(11, $errors[1]->getLine()); + $this->assertSame('Call to an undefined method Bug5390\B::someMethod().', $errors[2]->getMessage()); + $this->assertSame(12, $errors[2]->getLine()); + } + + public function testBug7110(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7110.php'); + $this->assertCount(1, $errors); + $this->assertSame('Parameter #1 $s of function Bug7110\takesInt expects int, string given.', $errors[0]->getMessage()); + $this->assertSame(34, $errors[0]->getLine()); + } + + public function testBug8376(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8376.php'); + $this->assertNoErrors($errors); + } + + public function testAssertDocblock(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/nsrt/assert-docblock.php'); + $this->assertCount(4, $errors); + $this->assertSame('Call to method AssertDocblock\A::testInt() with string will always evaluate to false.', $errors[0]->getMessage()); + $this->assertSame(218, $errors[0]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testNotInt() with string will always evaluate to true.', $errors[1]->getMessage()); + $this->assertSame(224, $errors[1]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testInt() with int will always evaluate to true.', $errors[2]->getMessage()); + $this->assertSame(232, $errors[2]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testNotInt() with int will always evaluate to false.', $errors[3]->getMessage()); + $this->assertSame(238, $errors[3]->getLine()); + } + + public function testBug8147(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8147.php'); + $this->assertNoErrors($errors); + } + + public function testBug12934(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12934.php'); + $this->assertNoErrors($errors); + } + + public function testConditionalExpressionInfiniteLoop(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/conditional-expression-infinite-loop.php'); + $this->assertNoErrors($errors); + } + + public function testPr2030(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/pr-2030.php'); + $this->assertNoErrors($errors); + } + + public function testBug6265(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6265.php'); + $this->assertNotEmpty($errors); + } + + public function testBug8503(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8503.php'); + $this->assertNoErrors($errors); + } + + public function testBug8537(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8537.php'); + $this->assertNoErrors($errors); + } + + public function testBug8146(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8146b.php'); + $this->assertNoErrors($errors); + } + + public function testBug8215(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8215.php'); + $this->assertNoErrors($errors); + } + + public function testBug8146a(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8146a.php'); + $this->assertNoErrors($errors); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + + public function testBug8004(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8004.php'); + $this->assertCount(2, $errors); + $this->assertSame('Strict comparison using !== between null and DateTimeInterface|string will always evaluate to true.', $errors[0]->getMessage()); + $this->assertSame(49, $errors[0]->getLine()); + + $this->assertSame('Strict comparison using !== between null and DateTimeInterface|string will always evaluate to true.', $errors[1]->getMessage()); + $this->assertSame(59, $errors[1]->getLine()); + } + + public function testSkipCheckNoGenericClasses(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/skip-check-no-generic-classes.php'); + $this->assertCount(1, $errors); + $this->assertSame('Method SkipCheckNoGenericClasses\Foo::doFoo() has parameter $i with generic class LimitIterator but does not specify its types: TKey, TValue, TIterator', $errors[0]->getMessage()); + } + + public function testBug8983(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8983.php'); + $this->assertNoErrors($errors); + } + + public function testBug9008(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9008.php'); + $this->assertNoErrors($errors); + } + + public function testBug5091(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5091.php'); + $this->assertNoErrors($errors); + } + + public function testBug9459(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9459.php'); + $this->assertCount(1, $errors); + $this->assertSame('PHPDoc tag @var with type callable(): array is not subtype of native type Closure(): array{}.', $errors[0]->getMessage()); + } + + public function testBug9573(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9573.php'); + $this->assertNoErrors($errors); + } + + public function testBug9039(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9039.php'); + $this->assertCount(1, $errors); + $this->assertSame('Constant Bug9039\Test::RULES is unused.', $errors[0]->getMessage()); + } + + public function testDiscussion9053(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/discussion-9053.php'); + $this->assertNoErrors($errors); + } + + public function testProcessCalledMethodInfiniteLoop(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/process-called-method-infinite-loop.php'); + $this->assertNoErrors($errors); + } + + public function testBug9428(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9428.php'); + $this->assertNoErrors($errors); + } + + public function testBug9690(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9690.php'); + $this->assertNoErrors($errors); + } + + public function testIgnoreIdentifiers(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/ignore-identifiers.php'); + $this->assertCount(5, $errors); + + $this->assertSame('No error with identifier wrong.id is reported on line 12.', $errors[0]->getMessage()); + $this->assertSame(12, $errors[0]->getLine()); + + $this->assertSame('Undefined variable: $foo', $errors[1]->getMessage()); + $this->assertSame(12, $errors[1]->getLine()); + + $this->assertSame('Undefined variable: $bar', $errors[2]->getMessage()); + $this->assertSame(14, $errors[2]->getLine()); + + $this->assertSame('Undefined variable: $foo', $errors[3]->getMessage()); + $this->assertSame(14, $errors[3]->getLine()); + + $this->assertSame('Undefined variable: $bar', $errors[4]->getMessage()); + $this->assertSame(16, $errors[4]->getLine()); + } + + public function testBug9994(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9994.php'); + $this->assertCount(2, $errors); + $this->assertSame('Negated boolean expression is always false.', $errors[0]->getMessage()); + $this->assertSame('Parameter #2 $callback of function array_filter expects (callable(1|2|3|null): bool)|null, false given.', $errors[1]->getMessage()); + } + + public function testBug10049(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10049-recursive.php'); + $this->assertNoErrors($errors); + } + + public function testBug10086(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10086.php'); + $this->assertNoErrors($errors); + } + + public function testBug10147(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10147.php'); + $this->assertNoErrors($errors); + } + + public function testBug10302(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10302.php'); + $this->assertNoErrors($errors); + } + + public function testBug10358(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10358.php'); + $this->assertCount(1, $errors); + $this->assertSame('Cannot use Ns\Foo2 as Foo because the name is already in use', $errors[0]->getMessage()); + $this->assertSame(6, $errors[0]->getLine()); + } + + public function testBug10509(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10509.php'); + $this->assertCount(2, $errors); + $this->assertSame('Method Bug10509\Foo::doFoo() has no return type specified.', $errors[0]->getMessage()); + $this->assertSame('PHPDoc tag @return contains unresolvable type.', $errors[1]->getMessage()); + } + + public function testBug10538(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10538.php'); + $this->assertNoErrors($errors); + } + + public function testBug10847(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10847.php'); + $this->assertNoErrors($errors); + } + + public function testBug10772(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10772.php'); + $this->assertNoErrors($errors); + } + + public function testBug10985(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10985.php'); + $this->assertNoErrors($errors); + } + + public function testBug10979(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10979.php'); + $this->assertNoErrors($errors); + } + + public function testBug11026(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11026.php'); + $this->assertNoErrors($errors); + } + + public function testBug10867(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10867.php'); + $this->assertNoErrors($errors); + } + + public function testBug11263(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11263.php'); + $this->assertNoErrors($errors); + } + + public function testBug11147(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11147.php'); + $this->assertCount(1, $errors); + $this->assertSame('Method Bug11147\RedisAdapter::createConnection() has invalid return type Bug11147\NonExistentClass.', $errors[0]->getMessage()); + } + + public function testBug11283(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11283.php'); + $this->assertNoErrors($errors); + } + + public function testBug11292(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11292.php'); + $this->assertNoErrors($errors); + } + + public function testBug11297(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11297.php'); + $this->assertNoErrors($errors); + } + + public function testBug5597(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5597.php'); + $this->assertNoErrors($errors); + } + + public function testBug11511(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11511.php'); + $this->assertCount(1, $errors); + $this->assertSame('Access to an undefined property object::$bar.', $errors[0]->getMessage()); + } + + public function testBug11640(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11640.php'); + $this->assertNoErrors($errors); + } + + public function testBug11709(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11709.php'); + $this->assertNoErrors($errors); + } + + public function testBug11913(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11913.php'); + $this->assertNoErrors($errors); + } + + public function testBug12549(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12549.php'); + $this->assertNoErrors($errors); + } + + public function testBug12627(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12627.php'); + $this->assertNoErrors($errors); + } + + public function testBug12159(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12159.php'); + $this->assertNoErrors($errors); + } + + public function testBug12787(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12787.php'); + $this->assertNoErrors($errors); + } + + public function testBug12800(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12800.php'); + $this->assertNoErrors($errors); + } + + public function testBug12949(): void + { + // Fetching class constants with a dynamic name is supported only on PHP 8.3 and later + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12949.php'); + $this->assertCount(3, $errors); + $this->assertSame('Call to an undefined method object::0().', $errors[0]->getMessage()); + $this->assertSame('Call to an undefined static method object::0().', $errors[1]->getMessage()); + $this->assertSame('Access to undefined constant object::0.', $errors[2]->getMessage()); + } + + /** + * @param string[]|null $allAnalysedFiles + * @return Error[] + */ + private function runAnalyse(string $file, ?array $allAnalysedFiles = null): array + { + $file = $this->getFileHelper()->normalizePath($file); + $analyser = self::getContainer()->getByType(Analyser::class); - /** @var FileHelper $fileHelper */ - $fileHelper = self::getContainer()->getByType(FileHelper::class); - /** @var Error[] $errors */ - $errors = $analyser->analyse([$file], null, null, true, $allAnalysedFiles)->getErrors(); + $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); + $errors = $finalizer->finalize( + $analyser->analyse([$file], null, null, true, $allAnalysedFiles), + false, + true, + )->getErrors(); foreach ($errors as $error) { - $this->assertSame($fileHelper->normalizePath($file), $error->getFilePath()); + $this->assertSame($file, $error->getFilePath()); } return $errors; diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 55be9d0d26..1b41db9157 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -2,23 +2,35 @@ namespace PHPStan\Analyser; +use Nette\DI\Container; use PhpParser\Lexer; use PhpParser\NodeVisitor\NameResolver; -use PhpParser\NodeVisitor\NodeConnectingVisitor; use PhpParser\Parser\Php7; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\Ignore\IgnoreLexer; +use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\ExportedNodeResolver; +use PHPStan\DependencyInjection\Nette\NetteContainer; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; -use PHPStan\NodeVisitor\StatementOrderVisitor; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\Printer; use PHPStan\Parser\RichParser; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\Reflection\AttributeReflectionFactory; +use PHPStan\Reflection\Deprecation\DeprecationProvider; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\Rules\AlwaysFailRule; -use PHPStan\Rules\Registry; +use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; +use stdClass; use function array_map; use function array_merge; use function assert; @@ -59,6 +71,50 @@ public function testFileWithAnIgnoredError(): void $this->assertEmpty($result); } + public function testFileWithAnIgnoredErrorMessage(): void + { + $result = $this->runAnalyser([['message' => '#Fail\.#']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertEmpty($result); + } + + public function testFileWithAnIgnoredErrorMessageAndWrongIdentifier(): void + { + $result = $this->runAnalyser([['message' => '#Fail\.#', 'identifier' => 'wrong.identifier']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertCount(2, $result); + assert($result[0] instanceof Error); + $this->assertSame('Fail.', $result[0]->getMessage()); + assert(is_string($result[1])); + $this->assertSame('Ignored error pattern #Fail\.# (wrong.identifier) was not matched in reported errors.', $result[1]); + } + + public function testFileWithAnIgnoredWrongIdentifier(): void + { + $result = $this->runAnalyser([['identifier' => 'wrong.identifier']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertCount(2, $result); + assert($result[0] instanceof Error); + $this->assertSame('Fail.', $result[0]->getMessage()); + assert(is_string($result[1])); + $this->assertSame('Ignored error pattern wrong.identifier was not matched in reported errors.', $result[1]); + } + + public function testFileWithAnIgnoredErrorMessageAndCorrectIdentifier(): void + { + $result = $this->runAnalyser([['message' => '#Fail\.#', 'identifier' => 'tests.alwaysFail']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertEmpty($result); + } + + public function testFileWithAnIgnoredErrorIdentifier(): void + { + $result = $this->runAnalyser([['identifier' => 'tests.alwaysFail']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertEmpty($result); + } + + public function testFileWithAnIgnoredErrorMessages(): void + { + $result = $this->runAnalyser([['messages' => ['#Fail\.#']]], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertEquals([], $result); + } + public function testIgnoringBrokenConfigurationDoesNotWork(): void { $this->markTestIncomplete(); @@ -81,15 +137,69 @@ public function testIgnoreErrorByPath(): void $this->assertNoErrors($result); } - public function testIgnoreErrorByPathAndCount(): void + public function testIgnoreErrorMultiByPath(): void { $ignoreErrors = [ [ - 'message' => '#Fail\.#', - 'count' => 3, - 'path' => __DIR__ . '/data/two-fails.php', + 'messages' => [ + '#First fail#', + '#Second fail#', + ], + 'path' => __DIR__ . '/data/two-different-fails.php', + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/two-different-fails.php', false); + $this->assertNoErrors($result); + } + + public function dataIgnoreErrorByPathAndCount(): iterable + { + yield [ + [ + [ + 'message' => '#Fail\.#', + 'count' => 3, + 'path' => __DIR__ . '/data/two-fails.php', + ], + ], + ]; + + yield [ + [ + [ + 'message' => '#Fail\.#', + 'count' => 2, + 'path' => __DIR__ . '/data/two-fails.php', + ], + [ + 'message' => '#Fail\.#', + 'count' => 1, + 'path' => __DIR__ . '/data/two-fails.php', + ], + ], + ]; + + yield [ + [ + [ + 'message' => '#Fail\.#', + 'count' => 2, + 'path' => __DIR__ . '/data/two-fails.php', + ], + [ + 'message' => '#Fail\.#', + 'path' => __DIR__ . '/data/two-fails.php', + ], ], ]; + } + + /** + * @dataProvider dataIgnoreErrorByPathAndCount + * @param mixed[] $ignoreErrors + */ + public function testIgnoreErrorByPathAndCount(array $ignoreErrors): void + { $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/two-fails.php', false); $this->assertNoErrors($result); } @@ -102,6 +212,32 @@ public function dataTrueAndFalse(): array ]; } + /** + * @dataProvider dataTrueAndFalse + */ + public function testIgnoreErrorByPathAndIdentifierCountsCorrectly(bool $onlyFiles): void + { + $ignoreErrors = [ + [ + 'identifier' => 'tests.alwaysFail', + 'count' => 3, + 'path' => __DIR__ . '/data/two-fails.php', + ], + [ + 'identifier' => 'tests.alwaysFail', + 'count' => 2, + 'path' => __DIR__ . '/data/two-different-fails.php', + ], + ]; + + $filesToAnalyze = [ + __DIR__ . '/data/two-fails.php', + __DIR__ . '/data/two-different-fails.php', + ]; + $result = $this->runAnalyser($ignoreErrors, true, $filesToAnalyze, $onlyFiles); + $this->assertNoErrors($result); + } + /** * @dataProvider dataTrueAndFalse */ @@ -198,6 +334,21 @@ public function testIgnoreErrorByPaths(): void $this->assertNoErrors($result); } + public function testIgnoreErrorMultiByPaths(): void + { + $ignoreErrors = [ + [ + 'messages' => [ + '#First fail#', + '#Second fail#', + ], + 'paths' => [__DIR__ . '/data/two-different-fails.php'], + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/two-different-fails.php', false); + $this->assertNoErrors($result); + } + public function testIgnoreErrorByPathsMultipleUnmatched(): void { $ignoreErrors = [ @@ -228,6 +379,22 @@ public function testIgnoreErrorByPathsUnmatched(): void $this->assertStringContainsString('was not matched in reported errors', $result[0]); } + public function testIgnoreErrorByPathsUnmatchedExplicitReportUnmatched(): void + { + $ignoreErrors = [ + [ + 'message' => '#Fail\.#', + 'paths' => [__DIR__ . '/data/bootstrap-error.php', __DIR__ . '/data/another-path.php'], + 'reportUnmatched' => true, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, false, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertCount(1, $result); + $this->assertIsString($result[0]); + $this->assertStringContainsString('Ignored error pattern #Fail\.# in path ', $result[0]); + $this->assertStringContainsString('was not matched in reported errors', $result[0]); + } + public function testIgnoreErrorNotFoundInPath(): void { $ignoreErrors = [ @@ -241,6 +408,20 @@ public function testIgnoreErrorNotFoundInPath(): void $this->assertSame('Ignored error pattern #Fail\.# in path ' . __DIR__ . '/data/not-existent-path.php was not matched in reported errors.', $result[0]); } + public function testIgnoreErrorNotFoundInPathExplicitReportUnmatched(): void + { + $ignoreErrors = [ + [ + 'message' => '#Fail\.#', + 'path' => __DIR__ . '/data/not-existent-path.php', + 'reportUnmatched' => true, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, false, __DIR__ . '/data/empty/empty.php', false); + $this->assertCount(1, $result); + $this->assertSame('Ignored error pattern #Fail\.# in path ' . __DIR__ . '/data/not-existent-path.php was not matched in reported errors.', $result[0]); + } + public function dataIgnoreErrorInTraitUsingClassFilePath(): array { return [ @@ -287,19 +468,7 @@ public function testIgnoredErrorMissingMessage(): void $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/empty/empty.php', false); $this->assertCount(1, $result); - $this->assertSame('Ignored error {"path":"' . $expectedPath . '/data/empty/empty.php"} is missing a message.', $result[0]); - } - - public function testIgnoredErrorMissingPath(): void - { - $ignoreErrors = [ - [ - 'message' => '#Fail\.#', - ], - ]; - $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/empty/empty.php', false); - $this->assertCount(1, $result); - $this->assertSame('Ignored error {"message":"#Fail\\\\.#"} is missing a path.', $result[0]); + $this->assertSame('Ignored error {"path":"' . $expectedPath . '/data/empty/empty.php"} is missing a message or an identifier.', $result[0]); } public function testReportMultipleParserErrorsAtOnce(): void @@ -362,30 +531,32 @@ public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasN $this->assertNoErrors($result); } - /** - * @dataProvider dataTrueAndFalse - */ - public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void + public function testIgnoreNextLine(): void { - $result = $this->runAnalyser([], $reportUnmatchedIgnoredErrors, [ + $result = $this->runAnalyser([], false, [ __DIR__ . '/data/ignore-next-line.php', ], true); - $this->assertCount($reportUnmatchedIgnoredErrors ? 4 : 3, $result); - foreach ([10, 30, 34] as $i => $line) { + $this->assertCount(5, $result); + foreach ([10, 20, 24, 31, 50] as $i => $line) { $this->assertArrayHasKey($i, $result); $this->assertInstanceOf(Error::class, $result[$i]); $this->assertSame('Fail.', $result[$i]->getMessage()); $this->assertSame($line, $result[$i]->getLine()); } + } - if (!$reportUnmatchedIgnoredErrors) { - return; + public function testIgnoreNextLineUnmatched(): void + { + $result = $this->runAnalyser([], true, [ + __DIR__ . '/data/ignore-next-line-unmatched.php', + ], true); + $this->assertCount(2, $result); + foreach ([11, 15] as $i => $line) { + $this->assertArrayHasKey($i, $result); + $this->assertInstanceOf(Error::class, $result[$i]); + $this->assertStringContainsString('No error to ignore is reported on line', $result[$i]->getMessage()); + $this->assertSame($line, $result[$i]->getLine()); } - - $this->assertArrayHasKey(3, $result); - $this->assertInstanceOf(Error::class, $result[3]); - $this->assertSame('No error to ignore is reported on line 38.', $result[3]->getMessage()); - $this->assertSame(38, $result[3]->getLine()); } /** @@ -414,6 +585,56 @@ public function testIgnoreLine(bool $reportUnmatchedIgnoredErrors): void $this->assertSame(26, $result[3]->getLine()); } + public function testIgnoreErrorExplicitReportUnmatchedDisable(): void + { + $ignoreErrors = [ + [ + 'message' => '#Fail#', + 'reportUnmatched' => false, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap.php', false); + $this->assertNoErrors($result); + } + + public function testIgnoreErrorExplicitReportUnmatchedDisableMulti(): void + { + $ignoreErrors = [ + [ + 'message' => ['#Fail#'], + 'reportUnmatched' => false, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap.php', false); + $this->assertNoErrors($result); + } + + public function testIgnoreErrorExplicitReportUnmatchedEnable(): void + { + $ignoreErrors = [ + [ + 'message' => '#Fail#', + 'reportUnmatched' => true, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, false, __DIR__ . '/data/bootstrap.php', false); + $this->assertCount(1, $result); + $this->assertSame('Ignored error pattern #Fail# was not matched in reported errors.', $result[0]); + } + + public function testIgnoreErrorExplicitReportUnmatchedEnableMulti(): void + { + $ignoreErrors = [ + [ + 'messages' => ['#Fail#'], + 'reportUnmatched' => true, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, false, __DIR__ . '/data/bootstrap.php', false); + $this->assertCount(1, $result); + $this->assertSame('Ignored error pattern #Fail# was not matched in reported errors.', $result[0]); + } + /** * @param mixed[] $ignoreErrors * @param string|string[] $filePaths @@ -426,7 +647,7 @@ private function runAnalyser( bool $onlyFiles, ): array { - $analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors); + $analyser = $this->createAnalyser(); if (is_string($filePaths)) { $filePaths = [$filePaths]; @@ -446,25 +667,40 @@ private function runAnalyser( $analyserResult = $analyser->analyse($normalizedFilePaths); - $errors = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $normalizedFilePaths, $analyserResult->hasReachedInternalErrorsCountLimit()); + $finalizer = new AnalyserResultFinalizer( + new DirectRuleRegistry([]), + new IgnoreErrorExtensionProvider(new NetteContainer(new Container([]))), + new RuleErrorTransformer(), + $this->createScopeFactory( + $this->createReflectionProvider(), + self::getContainer()->getService('typeSpecifier'), + ), + new LocalIgnoresProcessor(), + $reportUnmatchedIgnoredErrors, + ); + $analyserResult = $finalizer->finalize($analyserResult, $onlyFiles, false)->getAnalyserResult(); + + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $normalizedFilePaths, $analyserResult->hasReachedInternalErrorsCountLimit()); + $errors = $ignoredErrorHelperProcessedResult->getNotIgnoredErrors(); + $errors = array_merge($errors, $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages()); if ($analyserResult->hasReachedInternalErrorsCountLimit()) { $errors[] = sprintf('Reached internal errors count limit of %d, exiting...', 50); } return array_merge( $errors, - $analyserResult->getInternalErrors(), + array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $analyserResult->getInternalErrors()), ); } - private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser + private function createAnalyser(): Analyser { - $registry = new Registry([ + $ruleRegistry = new DirectRuleRegistry([ new AlwaysFailRule(), ]); + $collectorRegistry = new CollectorRegistry([]); $reflectionProvider = $this->createReflectionProvider(); - $printer = new Standard(); $fileHelper = $this->getFileHelper(); $typeSpecifier = self::getContainer()->getService('typeSpecifier'); @@ -473,40 +709,54 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser $nodeScopeResolver = new NodeScopeResolver( $reflectionProvider, + self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), - $this->getClassReflectionExtensionRegistryProvider(), + self::getClassReflectionExtensionRegistryProvider(), + self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class), $this->getParser(), $fileTypeMapper, self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(SignatureMapProvider::class), + self::getContainer()->getByType(DeprecationProvider::class), + self::getContainer()->getByType(AttributeReflectionFactory::class), $phpDocInheritanceResolver, $fileHelper, $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), + self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), + self::createScopeFactory($reflectionProvider, $typeSpecifier), false, true, + true, [], [], + [stdClass::class], + true, + $this->shouldTreatPhpDocTypesAsCertain(), true, ); - $lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]); + $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, new RichParser( new Php7($lexer), - $lexer, new NameResolver(), - new NodeConnectingVisitor(), - new StatementOrderVisitor(), + self::getContainer(), + new IgnoreLexer(), ), - new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, $printer)), - $reportUnmatchedIgnoredErrors, + new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), + new IgnoreErrorExtensionProvider(new NetteContainer(new Container([]))), + new RuleErrorTransformer(), + new LocalIgnoresProcessor(), ); return new Analyser( $fileAnalyser, - $registry, + $ruleRegistry, + $collectorRegistry, $nodeScopeResolver, 50, ); diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index 611d11121d..023d00e9f4 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -5,7 +5,11 @@ use PHPStan\File\FileHelper; use PHPStan\Testing\PHPStanTestCase; use function array_map; +use function array_merge; +use function array_unique; use function sprintf; +use function usort; +use const PHP_VERSION_ID; class AnalyserTraitsIntegrationTest extends PHPStanTestCase { @@ -165,6 +169,36 @@ public function testMissingReturnInAbstractTraitMethod(): void $this->assertNoErrors($errors); } + public function testUnititializedReadonlyPropertyAccessedInTrait(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped(); + } + + $errors = $this->runAnalyse([ + __DIR__ . '/traits/uninitializedProperty/FooClass.php', + __DIR__ . '/traits/uninitializedProperty/FooTrait.php', + ]); + $this->assertCount(3, $errors); + usort($errors, static fn (Error $a, Error $b) => $a->getLine() <=> $b->getLine()); + $expectedFile = sprintf('%s (in context of class TraitsUnititializedProperty\FooClass)', $this->fileHelper->normalizePath(__DIR__ . '/traits/uninitializedProperty/FooTrait.php')); + + $error = $errors[0]; + $this->assertSame('Access to an uninitialized readonly property TraitsUnititializedProperty\FooClass::$x.', $error->getMessage()); + $this->assertSame(15, $error->getLine()); + $this->assertSame($expectedFile, $error->getFile()); + + $error = $errors[1]; + $this->assertSame('Access to an uninitialized @readonly property TraitsUnititializedProperty\FooClass::$y.', $error->getMessage()); + $this->assertSame(16, $error->getLine()); + $this->assertSame($expectedFile, $error->getFile()); + + $error = $errors[2]; + $this->assertSame('Access to an uninitialized property TraitsUnititializedProperty\FooClass::$z.', $error->getMessage()); + $this->assertSame(17, $error->getLine()); + $this->assertSame($expectedFile, $error->getFile()); + } + /** * @param string[] $files * @return Error[] @@ -174,9 +208,21 @@ private function runAnalyse(array $files): array $files = array_map(fn (string $file): string => $this->getFileHelper()->normalizePath($file), $files); /** @var Analyser $analyser */ $analyser = self::getContainer()->getByType(Analyser::class); - /** @var Error[] $errors */ - $errors = $analyser->analyse($files)->getErrors(); - return $errors; + + return $analyser->analyse($files)->getErrors(); + } + + public static function getAdditionalConfigFiles(): array + { + return array_unique( + array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/traits-integration.neon', + ], + ), + ); } } diff --git a/tests/PHPStan/Analyser/AnonymousClassNameRule.php b/tests/PHPStan/Analyser/AnonymousClassNameRule.php index 0bba0df265..2c4f09e0eb 100644 --- a/tests/PHPStan/Analyser/AnonymousClassNameRule.php +++ b/tests/PHPStan/Analyser/AnonymousClassNameRule.php @@ -7,7 +7,11 @@ use PHPStan\Broker\ClassNotFoundException; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +/** + * @implements Rule + */ class AnonymousClassNameRule implements Rule { @@ -20,10 +24,6 @@ public function getNodeType(): string return Class_::class; } - /** - * @param Class_ $node - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { $className = isset($node->namespacedName) @@ -32,10 +32,18 @@ public function processNode(Node $node, Scope $scope): array try { $this->reflectionProvider->getClass($className); } catch (ClassNotFoundException) { - return ['not found']; + return [ + RuleErrorBuilder::message('not found') + ->identifier('tests.anonymousClassName') + ->build(), + ]; } - return ['found']; + return [ + RuleErrorBuilder::message('found') + ->identifier('tests.anonymousClassName') + ->build(), + ]; } } diff --git a/tests/PHPStan/Analyser/ArgumentsNormalizerLegacyTest.php b/tests/PHPStan/Analyser/ArgumentsNormalizerLegacyTest.php new file mode 100644 index 0000000000..b7f4e23bf0 --- /dev/null +++ b/tests/PHPStan/Analyser/ArgumentsNormalizerLegacyTest.php @@ -0,0 +1,188 @@ +markTestSkipped('Test requires PHP 8.0.'); + } + + $funcName = new Name('json_encode'); + $reflectionProvider = self::getContainer()->getByType(NativeFunctionReflectionProvider::class); + $functionReflection = $reflectionProvider->findFunctionReflection('json_encode'); + if ($functionReflection === null) { + throw new ShouldNotHappenException(); + } + $parameterAcceptor = $functionReflection->getOnlyVariant(); + + $args = [ + new Arg( + new LNumber(0), + false, + false, + [], + new Identifier('flags'), + ), + new Arg( + new String_('my json value'), + false, + false, + [], + new Identifier('value'), + ), + ]; + $funcCall = new FuncCall($funcName, $args); + + $funcCall = ArgumentsNormalizer::reorderFuncArguments($parameterAcceptor, $funcCall); + $this->assertNotNull($funcCall); + $reorderedArgs = $funcCall->getArgs(); + $this->assertCount(2, $reorderedArgs); + + $this->assertArrayHasKey(0, $reorderedArgs); + $this->assertNull($reorderedArgs[0]->name, 'named-arg turned into regular numeric arg'); + $this->assertInstanceOf(String_::class, $reorderedArgs[0]->value, 'value-arg at the right position'); + + $this->assertArrayHasKey(1, $reorderedArgs); + $this->assertNull($reorderedArgs[1]->name, 'named-arg turned into regular numeric arg'); + $this->assertInstanceOf(LNumber::class, $reorderedArgs[1]->value, 'flags-arg at the right position'); + $this->assertSame(0, $reorderedArgs[1]->value->value); + } + + /** + * function call, all args named, not in order + */ + public function testArgumentReorderAllNamedWithSkipped(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $funcName = new Name('json_encode'); + $reflectionProvider = self::getContainer()->getByType(NativeFunctionReflectionProvider::class); + $functionReflection = $reflectionProvider->findFunctionReflection('json_encode'); + if ($functionReflection === null) { + throw new ShouldNotHappenException(); + } + $parameterAcceptor = $functionReflection->getOnlyVariant(); + + $args = [ + new Arg( + new LNumber(128), + false, + false, + [], + new Identifier('depth'), + ), + new Arg( + new String_('my json value'), + false, + false, + [], + new Identifier('value'), + ), + ]; + $funcCall = new FuncCall($funcName, $args); + + $funcCall = ArgumentsNormalizer::reorderFuncArguments($parameterAcceptor, $funcCall); + $this->assertNotNull($funcCall); + $reorderedArgs = $funcCall->getArgs(); + $this->assertCount(3, $reorderedArgs); + + $this->assertArrayHasKey(0, $reorderedArgs); + $this->assertNull($reorderedArgs[0]->name, 'named-arg turned into regular numeric arg'); + $this->assertInstanceOf(String_::class, $reorderedArgs[0]->value, 'value-arg at the right position'); + + $this->assertArrayHasKey(1, $reorderedArgs); + $this->assertNull($reorderedArgs[1]->name, 'named-arg turned into regular numeric arg'); + $this->assertInstanceOf(TypeExpr::class, $reorderedArgs[1]->value, 'flags-arg at the right position'); + $this->assertInstanceOf(ConstantIntegerType::class, $reorderedArgs[1]->value->getExprType()); + $this->assertSame(0, $reorderedArgs[1]->value->getExprType()->getValue(), 'flags-arg with default value'); + + $this->assertArrayHasKey(2, $reorderedArgs); + $this->assertNull($reorderedArgs[2]->name, 'named-arg turned into regular numeric arg'); + $this->assertInstanceOf(LNumber::class, $reorderedArgs[2]->value, 'depth-arg at the right position'); + $this->assertSame(128, $reorderedArgs[2]->value->value); + } + + public function testMissingRequiredParameter(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $funcName = new Name('json_encode'); + $reflectionProvider = self::getContainer()->getByType(NativeFunctionReflectionProvider::class); + $functionReflection = $reflectionProvider->findFunctionReflection('json_encode'); + if ($functionReflection === null) { + throw new ShouldNotHappenException(); + } + $parameterAcceptor = $functionReflection->getOnlyVariant(); + + $args = [ + new Arg( + new LNumber(128), + false, + false, + [], + new Identifier('depth'), + ), + ]; + $funcCall = new FuncCall($funcName, $args); + + $this->assertNull(ArgumentsNormalizer::reorderFuncArguments($parameterAcceptor, $funcCall)); + } + + public function testLeaveRegularCallAsIs(): void + { + $funcName = new Name('json_encode'); + $reflectionProvider = self::getContainer()->getByType(NativeFunctionReflectionProvider::class); + $functionReflection = $reflectionProvider->findFunctionReflection('json_encode'); + if ($functionReflection === null) { + throw new ShouldNotHappenException(); + } + $parameterAcceptor = $functionReflection->getOnlyVariant(); + + $args = [ + new Arg( + new String_('my json value'), + ), + new Arg( + new LNumber(0), + ), + ]; + $funcCall = new FuncCall($funcName, $args); + + $funcCall = ArgumentsNormalizer::reorderFuncArguments($parameterAcceptor, $funcCall); + $this->assertNotNull($funcCall); + $reorderedArgs = $funcCall->getArgs(); + $this->assertCount(2, $reorderedArgs); + + $this->assertArrayHasKey(0, $reorderedArgs); + $this->assertInstanceOf(String_::class, $reorderedArgs[0]->value, 'value-arg at unchanged position'); + + $this->assertArrayHasKey(1, $reorderedArgs); + $this->assertInstanceOf(LNumber::class, $reorderedArgs[1]->value, 'flags-arg at unchanged position'); + $this->assertSame(0, $reorderedArgs[1]->value->value); + } + +} diff --git a/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php b/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php new file mode 100644 index 0000000000..6ee268c32a --- /dev/null +++ b/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php @@ -0,0 +1,367 @@ + $parameterSettings + * @param array $argumentSettings + * @param array $expectedArgumentTypes + */ + public function testReorderValid( + array $parameterSettings, + array $argumentSettings, + array $expectedArgumentTypes, + ): void + { + $parameters = []; + foreach ($parameterSettings as [$name, $optional, $variadic, $defaultValue]) { + $parameters[] = new DummyParameter( + $name, + new MixedType(), + $optional, + null, + $variadic, + $defaultValue, + ); + } + + $arguments = []; + foreach ($argumentSettings as [$type, $name]) { + $arguments[] = new Arg(new TypeExpr($type), false, false, [], $name === null ? null : new Identifier($name)); + } + + $normalized = ArgumentsNormalizer::reorderFuncArguments( + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + $parameters, + false, + new MixedType(), + ), + new FuncCall(new Name('foo'), $arguments), + ); + $this->assertNotNull($normalized); + + $actualArguments = $normalized->getArgs(); + $this->assertCount(count($expectedArgumentTypes), $actualArguments); + foreach ($actualArguments as $i => $actualArgument) { + $this->assertNull($actualArgument->name); + $value = $actualArgument->value; + $this->assertInstanceOf(TypeExpr::class, $value); + $this->assertSame( + $expectedArgumentTypes[$i]->describe(VerbosityLevel::precise()), + $value->getExprType()->describe(VerbosityLevel::precise()), + ); + } + } + + public function dataReorderInvalid(): iterable + { + yield [ + [ + ['one', false, false, null], + ['two', false, false, null], + ['three', false, false, null], + ], + [ + [new StringType(), 'two'], + ], + ]; + + yield [ + [ + ['one', false, false, null], + ['two', false, false, null], + ['three', false, false, null], + ], + [ + [new IntegerType(), null], + [new StringType(), 'three'], + ], + ]; + } + + /** + * @dataProvider dataReorderInvalid + * @param array $parameterSettings + * @param array $argumentSettings + */ + public function testReorderInvalid( + array $parameterSettings, + array $argumentSettings, + ): void + { + $parameters = []; + foreach ($parameterSettings as [$name, $optional, $variadic, $defaultValue]) { + $parameters[] = new DummyParameter( + $name, + new MixedType(), + $optional, + null, + $variadic, + $defaultValue, + ); + } + + $arguments = []; + foreach ($argumentSettings as [$type, $name]) { + $arguments[] = new Arg(new TypeExpr($type), false, false, [], $name === null ? null : new Identifier($name)); + } + + $normalized = ArgumentsNormalizer::reorderFuncArguments( + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + $parameters, + false, + new MixedType(), + ), + new FuncCall(new Name('foo'), $arguments), + ); + $this->assertNull($normalized); + } + +} diff --git a/tests/PHPStan/Analyser/AssertStubTest.php b/tests/PHPStan/Analyser/AssertStubTest.php new file mode 100644 index 0000000000..2ea24ac4ce --- /dev/null +++ b/tests/PHPStan/Analyser/AssertStubTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/assert-stub.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/assert-stub.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Bug10922Test.php b/tests/PHPStan/Analyser/Bug10922Test.php new file mode 100644 index 0000000000..2dccf946c8 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug10922Test.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/bug-10922.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/bug-10922.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Bug10980Test.php b/tests/PHPStan/Analyser/Bug10980Test.php new file mode 100644 index 0000000000..6c3821447f --- /dev/null +++ b/tests/PHPStan/Analyser/Bug10980Test.php @@ -0,0 +1,28 @@ +assertFileAsserts($assertType, $file, ...$args); + } + +} diff --git a/tests/PHPStan/Analyser/Bug11009Test.php b/tests/PHPStan/Analyser/Bug11009Test.php new file mode 100644 index 0000000000..480e170756 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug11009Test.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/bug-11009.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/bug-11009.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php new file mode 100644 index 0000000000..278e2d805d --- /dev/null +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -0,0 +1,41 @@ + + */ +class Bug9307CallMethodsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false, true); + return new CallMethodsRule( + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-9307.php'], []); + } + +} diff --git a/tests/PHPStan/Analyser/Bug9307Test.php b/tests/PHPStan/Analyser/Bug9307Test.php new file mode 100644 index 0000000000..6502ac2701 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug9307Test.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/bug-9307.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/bug-9307.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php b/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php new file mode 100644 index 0000000000..84be755a38 --- /dev/null +++ b/tests/PHPStan/Analyser/ConditionalReturnTypeFromMethodStubTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/conditional-return-type-stub.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/conditional-return-type-stub.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/DoNotPolluteScopeWithBlockTest.php b/tests/PHPStan/Analyser/DoNotPolluteScopeWithBlockTest.php new file mode 100644 index 0000000000..cd4ceb5d85 --- /dev/null +++ b/tests/PHPStan/Analyser/DoNotPolluteScopeWithBlockTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/do-not-pollute-scope-with-block.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/do-not-pollute-scope-with-block.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php b/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php new file mode 100644 index 0000000000..61c93088f9 --- /dev/null +++ b/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/do-not-remember-possibly-impure-function-values.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__ . '/do-not-remember-possibly-impure-function-values.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php index 39b17b66f7..0d5f8a8dc8 100644 --- a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php +++ b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php @@ -3,13 +3,19 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use const PHP_VERSION_ID; class DynamicMethodThrowTypeExtensionTest extends TypeInferenceTestCase { public function dataFileAsserts(): iterable { + if (PHP_VERSION_ID < 80000) { + return []; + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension-named-args-fixture.php'); } /** diff --git a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php index 85eebb189f..7e5fad5dda 100644 --- a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php +++ b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use const PHP_VERSION_ID; class DynamicReturnTypeExtensionTypeInferenceTest extends TypeInferenceTestCase { @@ -10,7 +11,14 @@ class DynamicReturnTypeExtensionTypeInferenceTest extends TypeInferenceTestCase public function dataAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types-named-args.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-compound-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7344.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7391b.php'); } /** diff --git a/tests/PHPStan/Analyser/ErrorTest.php b/tests/PHPStan/Analyser/ErrorTest.php index 5e7d1e8ad0..7bd49a242a 100644 --- a/tests/PHPStan/Analyser/ErrorTest.php +++ b/tests/PHPStan/Analyser/ErrorTest.php @@ -15,4 +15,45 @@ public function testError(): void $this->assertSame(10, $error->getLine()); } + public function dataValidIdentifier(): iterable + { + yield ['a']; + yield ['aa']; + yield ['phpstan']; + yield ['phpstan.internal']; + yield ['phpstan.alwaysFail']; + yield ['Phpstan.alwaysFail']; + yield ['phpstan.internal.foo']; + yield ['foo2.test']; + yield ['phpstan123']; + yield ['3m.blah']; + } + + /** + * @dataProvider dataValidIdentifier + */ + public function testValidIdentifier(string $identifier): void + { + $this->assertTrue(Error::validateIdentifier($identifier)); + } + + public function dataInvalidIdentifier(): iterable + { + yield ['']; + yield [' ']; + yield ['phpstan ']; + yield [' phpstan']; + yield ['.phpstan']; + yield ['phpstan.']; + yield ['.']; + } + + /** + * @dataProvider dataInvalidIdentifier + */ + public function testInvalidIdentifier(string $identifier): void + { + $this->assertFalse(Error::validateIdentifier($identifier)); + } + } diff --git a/tests/PHPStan/Analyser/EvaluationOrderRule.php b/tests/PHPStan/Analyser/EvaluationOrderRule.php index 14fe278783..6cec461953 100644 --- a/tests/PHPStan/Analyser/EvaluationOrderRule.php +++ b/tests/PHPStan/Analyser/EvaluationOrderRule.php @@ -4,7 +4,11 @@ use PhpParser\Node; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +/** + * @implements Rule + */ class EvaluationOrderRule implements Rule { @@ -13,20 +17,25 @@ public function getNodeType(): string return Node::class; } - /** - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { if ( $node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name ) { - return [$node->name->toString()]; + return [ + RuleErrorBuilder::message($node->name->toString()) + ->identifier('tests.evaluationOrder') + ->build(), + ]; } if ($node instanceof Node\Scalar\String_) { - return [$node->value]; + return [ + RuleErrorBuilder::message($node->value) + ->identifier('tests.evaluationOrder') + ->build(), + ]; } return []; diff --git a/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php new file mode 100644 index 0000000000..b8dda807d4 --- /dev/null +++ b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php @@ -0,0 +1,35 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/expression-type-resolver-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php b/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php new file mode 100644 index 0000000000..168b6799b1 --- /dev/null +++ b/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php @@ -0,0 +1,96 @@ + $expectedTokens + */ + public function testTokenize(string $input, array $expectedTokens): void + { + $lexer = new IgnoreLexer(); + $tokens = $lexer->tokenize($input); + $lastToken = array_pop($tokens); + + $this->assertSame(['', IgnoreLexer::TOKEN_END, substr_count($input, PHP_EOL) + 1], $lastToken); + $this->assertSame($expectedTokens, $tokens); + } + +} diff --git a/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php b/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php new file mode 100644 index 0000000000..f810ed04a2 --- /dev/null +++ b/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/immediately-called-function-without-implicit-throw.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/data/immediately-called-function-without-implicit-throw.neon'], + ); + } + +} diff --git a/tests/PHPStan/Analyser/InstanceMethodsParameterScopeFunctionRule.php b/tests/PHPStan/Analyser/InstanceMethodsParameterScopeFunctionRule.php new file mode 100644 index 0000000000..f3fce8fd9d --- /dev/null +++ b/tests/PHPStan/Analyser/InstanceMethodsParameterScopeFunctionRule.php @@ -0,0 +1,34 @@ + + */ +class InstanceMethodsParameterScopeFunctionRule implements Rule +{ + + public function getNodeType(): string + { + return FullyQualified::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($scope->getFunction() !== null) { + throw new ShouldNotHappenException('All names in the tests should not have a function scope.'); + } + + return [ + RuleErrorBuilder::message(sprintf('Name %s found in function scope null', $node->toString()))->identifier('test.instanceOfMethodsParameterRule')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Analyser/InstanceMethodsParameterScopeFunctionTest.php b/tests/PHPStan/Analyser/InstanceMethodsParameterScopeFunctionTest.php new file mode 100644 index 0000000000..7cbae1c8be --- /dev/null +++ b/tests/PHPStan/Analyser/InstanceMethodsParameterScopeFunctionTest.php @@ -0,0 +1,43 @@ + + */ +class InstanceMethodsParameterScopeFunctionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InstanceMethodsParameterScopeFunctionRule(); + } + + protected function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/instance-methods-parameter-scope.php'], [ + [ + 'Name DateTime found in function scope null', + 12, + ], + [ + 'Name Baz\Waldo found in function scope null', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index ac031bd37e..167cad5f8e 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -5,7 +5,7 @@ use Generator; use PhpParser\Node; use PhpParser\Node\Expr\Exit_; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\Printer; use PHPStan\Node\VirtualNode; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\TypeInferenceTestCase; @@ -36,7 +36,7 @@ class LegacyNodeScopeResolverTest extends TypeInferenceTestCase public function testClassMethodScope(): void { - $this->processFile(__DIR__ . '/data/class.php', function (Node $node, Scope $scope): void { + self::processFile(__DIR__ . '/data/class.php', function (Node $node, Scope $scope): void { if (!($node instanceof Exit_)) { return; } @@ -65,7 +65,6 @@ public function testClassMethodScope(): void private function getFileScope(string $filename): Scope { - /** @var Scope $testScope */ $testScope = null; $this->processFile($filename, static function (Node $node, Scope $scope) use (&$testScope): void { if (!($node instanceof Exit_)) { @@ -75,6 +74,7 @@ private function getFileScope(string $filename): Scope $testScope = $scope; }); + /** @var Scope */ return $testScope; } @@ -302,7 +302,7 @@ public function dataAssignInIf(): array $testScope, 'matches', TrinaryLogic::createYes(), - 'mixed', + 'array{0?: string}', ], [ $testScope, @@ -343,7 +343,7 @@ public function dataAssignInIf(): array $testScope, 'matches2', TrinaryLogic::createMaybe(), - 'mixed', + 'array{0?: string}', ], [ $testScope, @@ -355,13 +355,13 @@ public function dataAssignInIf(): array $testScope, 'matches3', TrinaryLogic::createYes(), - 'mixed', + 'array{}|array{string}', ], [ $testScope, 'matches4', TrinaryLogic::createMaybe(), - 'mixed', + 'array{}|array{string}', ], [ $testScope, @@ -415,7 +415,7 @@ public function dataAssignInIf(): array $testScope, 'ternaryMatches', TrinaryLogic::createYes(), - 'mixed', + 'array{string}', ], [ $testScope, @@ -906,7 +906,7 @@ private function assertVariables( $this->assertTrue( $expectedCertainty->equals($certainty), sprintf( - 'Certainty of variable $%s is %s, expected %s', + 'Certainty of %s is %s, expected %s', $variableName, $certainty->describe(), $expectedCertainty->describe(), @@ -1073,7 +1073,7 @@ public function dataArrayDestructuring(): array '$secondStringArray', ], [ - 'string', + 'non-empty-string', '$thirdStringArray', ], [ @@ -1089,7 +1089,7 @@ public function dataArrayDestructuring(): array '$secondStringArrayList', ], [ - 'string', + 'non-empty-string', '$thirdStringArrayList', ], [ @@ -1097,43 +1097,43 @@ public function dataArrayDestructuring(): array '$fourthStringArrayList', ], [ - 'string', + 'non-empty-string', '$firstStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$secondStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$thirdStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$fourthStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$firstStringArrayForeachList', ], [ - 'string', + 'non-empty-string', '$secondStringArrayForeachList', ], [ - 'string', + 'non-empty-string', '$thirdStringArrayForeachList', ], [ - 'string', + 'non-empty-string', '$fourthStringArrayForeachList', ], [ - 'string', + 'lowercase-string&uppercase-string', '$dateArray[\'Y\']', ], [ - 'string', + 'lowercase-string&uppercase-string', '$dateArray[\'m\']', ], [ @@ -1141,7 +1141,7 @@ public function dataArrayDestructuring(): array '$dateArray[\'d\']', ], [ - 'string', + 'lowercase-string&uppercase-string', '$intArrayForRewritingFirstElement[0]', ], [ @@ -1149,7 +1149,7 @@ public function dataArrayDestructuring(): array '$intArrayForRewritingFirstElement[1]', ], [ - 'stdClass', + 'ArrayAccess&stdClass', '$obj', ], [ @@ -1282,7 +1282,7 @@ public function dataParameterTypes(): array '$callable', ], [ - PHP_VERSION_ID < 80000 ? 'array' : 'array', + PHP_VERSION_ID < 80000 ? 'list' : 'array', '$variadicStrings', ], [ @@ -1412,11 +1412,11 @@ public function dataVarAnnotations(): array '$callable', ], [ - 'callable(int, ...string): void', + 'callable(int, string ...): void', '$callableWithTypes', ], [ - 'Closure(int, ...string): void', + 'Closure(int, string ...): void', '$closureWithTypes', ], [ @@ -1605,36 +1605,6 @@ public function testCasts( ); } - public function dataUnsetCast(): array - { - return [ - [ - 'null', - '$castedNull', - ], - ]; - } - - /** - * @dataProvider dataUnsetCast - */ - public function testUnsetCast( - string $desciptiion, - 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.', - ); - } - $this->assertTypes( - __DIR__ . '/data/cast-unset.php', - $desciptiion, - $expression, - ); - } - public function dataDeductedTypes(): array { return [ @@ -1793,7 +1763,7 @@ public function dataProperties(): array '$this->arrayPropertyOne', ], [ - 'array', + 'array', '$this->arrayPropertyOther', ], [ @@ -1849,7 +1819,7 @@ public function dataProperties(): array '$this->resource', ], [ - 'array', + 'mixed', '$this->yetAnotherAnotherMixedParameter', ], [ @@ -2286,15 +2256,15 @@ public function dataBinaryOperations(): array 'false ? 1 : 2', ], [ - '12|non-empty-string', + '12|non-falsy-string', '$string ?: 12', ], [ - '12|non-empty-string', + '12|non-falsy-string', '$stringOrNull ?: 12', ], [ - '12|non-empty-string', + '12|non-falsy-string', '@$stringOrNull ?: 12', ], [ @@ -2338,11 +2308,11 @@ public function dataBinaryOperations(): array '$line', ], [ - (new ConstantStringType(__DIR__ . '/data'))->describe(VerbosityLevel::precise()), + 'literal-string&non-falsy-string', '$dir', ], [ - (new ConstantStringType(__DIR__ . '/data/binary.php'))->describe(VerbosityLevel::precise()), + 'literal-string&non-falsy-string', '$file', ], [ @@ -2393,14 +2363,42 @@ public function dataBinaryOperations(): array 'array', 'max($arrayOfUnknownIntegers, $arrayOfUnknownIntegers)', ], - /*[ - 'array(1, 1, 1, 1)', + [ + 'array{1, 1, 1, 1}', 'max(array(2, 2, 2), 5, array(1, 1, 1, 1))', ], + [ + 'array{int, int, int}', + 'max($arrayOfIntegers, 5)', + ], [ 'array', + 'max($arrayOfUnknownIntegers, 5)', + ], + [ + 'array|int', // could be array 'max($arrayOfUnknownIntegers, $integer, $arrayOfUnknownIntegers)', - ],*/ + ], + [ + 'array', + 'max($arrayOfUnknownIntegers, $conditionalInt)', + ], + [ + '5', + 'min($arrayOfIntegers, 5)', + ], + [ + '5', + 'min($arrayOfUnknownIntegers, 5)', + ], + [ + '1|2', + 'min($arrayOfUnknownIntegers, $conditionalInt)', + ], + [ + '5', + 'min(array(2, 2, 2), 5, array(1, 1, 1, 1))', + ], [ '1.1', 'min(...[1.1, 2.2, 3.3])', @@ -2466,11 +2464,11 @@ public function dataBinaryOperations(): array 'min(1, 2.2, 3.3)', ], [ - 'non-empty-string', + 'non-falsy-string', '"Hello $world"', ], [ - 'non-empty-string', + 'non-falsy-string', '$string .= "str"', ], [ @@ -2622,11 +2620,11 @@ public function dataBinaryOperations(): array '$arrayOfIntegers += $arrayOfIntegers', ], [ - 'array{0: 1, 1: 1, 2: 1, 3: 1|2, 4: 1|3, 5?: 2|3, 6?: 3}', + 'array{1, 1, 1, 1, 1, 2, 3}|array{1, 1, 1, 1, 1}|array{1, 1, 1, 2, 3, 2, 3}|array{1, 1, 1, 2, 3}', '$conditionalArray + $unshiftedConditionalArray', ], [ - 'array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1, 4: 1, 5?: 2|3, 6?: 3}', + 'array{\'lorem\', stdClass, 1, 1, 1, 2, 3}|array{\'lorem\', stdClass, 1, 1, 1}', '$unshiftedConditionalArray + $conditionalArray', ], [ @@ -2645,70 +2643,6 @@ public function dataBinaryOperations(): array 'array{int, int, int}', '$anotherArray = $arrayOfIntegers', ], - [ - 'string|null', - 'var_export()', - ], - [ - 'null', - 'var_export($string)', - ], - [ - 'null', - 'var_export($string, false)', - ], - [ - 'string', - 'var_export($string, true)', - ], - [ - 'bool|string', - 'highlight_string()', - ], - [ - 'bool', - 'highlight_string($string)', - ], - [ - 'bool', - 'highlight_string($string, false)', - ], - [ - 'string', - 'highlight_string($string, true)', - ], - [ - 'bool|string', - 'highlight_file()', - ], - [ - 'bool', - 'highlight_file($string)', - ], - [ - 'bool', - 'highlight_file($string, false)', - ], - [ - 'string', - 'highlight_file($string, true)', - ], - [ - 'string|true', - 'print_r()', - ], - [ - 'true', - 'print_r($string)', - ], - [ - 'true', - 'print_r($string, false)', - ], - [ - 'string', - 'print_r($string, true)', - ], [ '1', '$one++', @@ -2766,7 +2700,7 @@ public function dataBinaryOperations(): array 'count($appendingToArrayInBranches)', ], [ - '3|4|5', + '3|5', 'count($conditionalArray)', ], [ @@ -2790,9 +2724,49 @@ public function dataBinaryOperations(): array '$mixed - $mixed', ], [ - '*ERROR*', + 'array', '$mixed + []', ], + [ + 'array|int', + '$intOrArray + $intOrArray', + ], + [ + 'float|int', + '$intOrFloat + $intOrFloat', + ], + [ + 'array|float', + '$floatOrArray + $floatOrArray', + ], + [ + 'array|bool|float|int|string', + '$plusable + $plusable', + ], + [ + 'array', + '$mixedNoFloat + []', + ], + [ + '(float|int)', + '$mixedNoFloat + 5', + ], + [ + '(float|int)', + '$mixedNoInt + 5', + ], + [ + '*ERROR*', + '$mixedNoArray + []', + ], + [ + '*ERROR*', + '$mixedNoArrayOrInt + []', + ], + [ + '*ERROR*', + '$integer + []', + ], [ '124', '1 + "123"', @@ -2838,9 +2812,17 @@ public function dataBinaryOperations(): array '5 & 3', ], [ - 'int', + 'int<0, 3>', '$integer & 3', ], + [ + 'int<0, 7>', + '7 & $integer', + ], + [ + 'int', + '$integer & $integer', + ], [ '\'x\'', '"x" & "y"', @@ -2890,7 +2872,7 @@ public function dataBinaryOperations(): array '$integer ^ 3', ], [ - '\'' . "\x01" . '\'', + '"\001"', '"x" ^ "y"', ], [ @@ -2906,7 +2888,7 @@ public function dataBinaryOperations(): array '"5" ^ 3', ], [ - 'int', + 'int<0, 3>', '$integer &= 3', ], [ @@ -2950,7 +2932,7 @@ public function dataBinaryOperations(): array '$fooString[4]', ], [ - 'string', + "''|'f'|'o'", '$fooString[$integer]', ], [ @@ -2962,7 +2944,7 @@ public function dataBinaryOperations(): array '"$fooString bar"', ], [ - '*ERROR*', + 'non-falsy-string', '"$std bar"', ], [ @@ -2982,11 +2964,11 @@ public function dataBinaryOperations(): array '$arrToUnshift2', ], [ - 'array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1, 4: 1, 5?: 2|3, 6?: 3}', + 'array{\'lorem\', stdClass, 1, 1, 1, 2, 3}|array{\'lorem\', stdClass, 1, 1, 1}', '$unshiftedConditionalArray', ], [ - 'array{dirname: string, basename: string, filename: string, extension?: string}', + 'array{dirname?: string, basename: string, extension?: string, filename: string}', 'pathinfo($string)', ], [ @@ -3002,19 +2984,19 @@ public function dataBinaryOperations(): array '$string--', ], [ - 'string', + '(float|int|string)', '++$string', ], [ - 'string', + '(float|int|string)', '--$string', ], [ - 'string', + '(float|int|string)', '$incrementedString', ], [ - 'string', + '(float|int|string)', '$decrementedString', ], [ @@ -3042,19 +3024,19 @@ public function dataBinaryOperations(): array '$decrementedFooString', ], [ - 'literal-string&non-empty-string', + "'barbar'|'barfoo'|'foobar'|'foofoo'", '$conditionalString . $conditionalString', ], [ - 'literal-string&non-empty-string', + "'baripsum'|'barlorem'|'fooipsum'|'foolorem'", '$conditionalString . $anotherConditionalString', ], [ - 'literal-string&non-empty-string', + "'ipsumbar'|'ipsumfoo'|'lorembar'|'loremfoo'", '$anotherConditionalString . $conditionalString', ], [ - '6|7|8', + '6|8', 'count($conditionalArray) + count($array)', ], [ @@ -3125,14 +3107,6 @@ public function dataBinaryOperations(): array 'bool', 'array_key_exists(\'foo\', $generalArray)', ], - [ - PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle', - 'curl_init()', - ], - [ - PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle|false', - 'curl_init($string)', - ], [ 'string', 'sprintf($string, $string, 1)', @@ -3142,17 +3116,25 @@ public function dataBinaryOperations(): array "sprintf('%s %s', 'foo', 'bar')", ], [ - 'array{}|array{0: \'password\'|\'username\', 1?: \'password\'}', + 'array{}|array{\'password\'}|array{0: \'username\', 1?: \'password\'}', '$coalesceArray', ], [ - 'array', + 'array{1, 2, 3}', '$arrayToBeUnset', ], [ - 'array', + 'array{1, 2, 3}', '$arrayToBeUnset2', ], + [ + 'array{0?: 1, 1?: 2, 2?: 3}', + '$arrayToBeUnset3', + ], + [ + 'array{0?: 1, 1?: 2, 2?: 3}', + '$arrayToBeUnset4', + ], [ 'array', '$shiftedNonEmptyArray', @@ -3174,7 +3156,7 @@ public function dataBinaryOperations(): array '$simpleXMLReturningXML', ], [ - 'non-empty-string', + 'non-falsy-string', '$xmlString', ], [ @@ -3186,15 +3168,15 @@ public function dataBinaryOperations(): array '$simpleXMLRightXpath', ], [ - 'array|false', + 'array|false|null', '$simpleXMLWrongXpath', ], [ - 'array|false', + 'array|false|null', '$simpleXMLUnknownXpath', ], [ - 'array|false', + 'array|false|null', '$namespacedXpath', ], ]; @@ -3374,7 +3356,7 @@ public function dataLiteralArraysKeys(): array "'BooleansArray'", ], [ - 'int|string', + '(int|string)', "'UnknownConstantArray'", ], ]; @@ -3469,7 +3451,7 @@ public function dataTypeFromFunctionPhpDocs(): array '$arrayParameterOne', ], [ - 'array', + 'array', '$arrayParameterOther', ], [ @@ -3658,6 +3640,23 @@ public function testTypeFromFunctionPhpDocsPhpstanPrefix( ); } + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromFunctionPrefixedPhpDocs + */ + public function testTypeFromFunctionPhpDocsPhanPrefix( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/functionPhpDocs-phanPrefix.php'; + $this->assertTypes( + __DIR__ . '/data/functionPhpDocs-phanPrefix.php', + $description, + $expression, + ); + } + public function dataTypeFromMethodPhpDocs(): array { return [ @@ -3867,6 +3866,31 @@ public function testTypeFromMethodPhpDocsPhpstanPrefix( ); } + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + */ + public function testTypeFromMethodPhpDocsPhanPrefix( + string $description, + string $expression, + bool $replaceClass = true, + ): void + { + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhanPrefix)', $description); + + if ($replaceClass && $expression !== '$this->doFoo()') { + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPhanPrefix)', $description); + if ($description === 'MethodPhpDocsNamespace\Foo') { + $description = 'MethodPhpDocsNamespace\FooPhanPrefix'; + } + } + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs-phanPrefix.php', + $description, + $expression, + ); + } + /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs @@ -3903,11 +3927,11 @@ public function testTypeFromMethodPhpDocsInheritDocWithoutCurlyBraces( ): void { if ($replaceClass) { - $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooInheritDocChild)', $description); - $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooInheritDocChild)', $description); + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooInheritDocChildWithoutCurly)', $description); + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooInheritDocChildWithoutCurly)', $description); $description = str_replace('MethodPhpDocsNamespace\FooParent', 'MethodPhpDocsNamespace\Foo', $description); if ($expression === '$inlineSelf') { - $description = 'MethodPhpDocsNamespace\FooInheritDocChild'; + $description = 'MethodPhpDocsNamespace\FooInheritDocChildWithoutCurly'; } } $this->assertTypes( @@ -4021,7 +4045,7 @@ public function testNotSwitchInstanceof(): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof-not.php', - '*ERROR*', + '*NEVER*', '$foo', ); } @@ -4268,7 +4292,7 @@ public function dataAnonymousFunction(): array '$str', ], [ - PHP_VERSION_ID < 80000 ? 'array' : 'array', + PHP_VERSION_ID < 80000 ? 'list' : 'array', '$arr', ], [ @@ -4397,7 +4421,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-iterable-with-specified-key-type.php', - 'ForeachWithGenericsPhpDoc\Bar|ForeachWithGenericsPhpDoc\Foo', + 'ForeachWithGenericsPhpDocIterable\Bar|ForeachWithGenericsPhpDocIterable\Foo', '$key', ], [ @@ -4407,7 +4431,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-iterable-with-complex-value-type.php', - 'float|ForeachWithComplexValueType\Foo', + 'float|ForeachIterableWithComplexValueType\Foo', '$value', ], [ @@ -4541,9 +4565,17 @@ public function dataArrayFunctions(): array '$filteredIntegers[0]', ], [ - '123', + '*ERROR*', '$filteredMixed[0]', ], + [ + '123', + '$filteredMixed[1]', + ], + [ + 'non-empty-array<0|1|2, 1|2|3>', + '$uniquedIntegers', + ], [ '1|2|3', '$uniquedIntegers[1]', @@ -4573,7 +4605,7 @@ public function dataArrayFunctions(): array '$reducedToInt', ], [ - 'array<0|1|2, 1|2|3>', + 'array{1, 2, 3}', 'array_change_key_case($integers)', ], [ @@ -4585,7 +4617,7 @@ public function dataArrayFunctions(): array 'array_combine([1], [2])', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', 'array_combine([1, 2], [3])', ], [ @@ -4645,16 +4677,16 @@ public function dataArrayFunctions(): array 'array_intersect_assoc($integers, [])', ], [ - 'array<0|1|2, 1|2|3>', + 'array{}', 'array_intersect_key($integers, [])', ], [ - 'array', + 'array{1, 2, 3}|array{4, 5, 6}', 'array_intersect_key(...[$integers, [4, 5, 6]])', ], [ 'array', - 'array_intersect_key(...$generalIntegersInAnotherArray, [])', + 'array_intersect_key(...$generalIntegersInAnotherArray)', ], [ 'array<0|1|2, 1|2|3>', @@ -4693,7 +4725,7 @@ public function dataArrayFunctions(): array '$filledIntegersWithKeys', ], [ - 'non-empty-array', + 'non-empty-list<\'foo\'>', '$filledNonEmptyArray', ], [ @@ -4705,11 +4737,11 @@ public function dataArrayFunctions(): array '$filledNegativeConstAlwaysFalse', ], [ - 'array|false', + PHP_VERSION_ID < 80000 ? 'list<1>|false' : 'list<1>', '$filledByMaybeNegativeRange', ], [ - 'non-empty-array', + 'non-empty-list<1>', '$filledByPositiveRange', ], [ @@ -4725,7 +4757,7 @@ public function dataArrayFunctions(): array 'array_keys($stringOrIntegerKeys)', ], [ - 'array', + 'list', 'array_keys($generalStringKeys)', ], [ @@ -4733,11 +4765,11 @@ public function dataArrayFunctions(): array 'array_values($integerKeys)', ], [ - 'array', + 'list', 'array_values($generalStringKeys)', ], [ - 'non-empty-array', + 'array{foo: stdClass, 0: stdClass}', 'array_merge($stringOrIntegerKeys)', ], [ @@ -4745,23 +4777,36 @@ public function dataArrayFunctions(): array 'array_merge($generalStringKeys, $generalDateTimeValues)', ], [ - 'non-empty-array', + 'non-empty-array<1|string, int|stdClass>', 'array_merge($generalStringKeys, $stringOrIntegerKeys)', ], [ - 'non-empty-array', + 'non-empty-array<1|string, int|stdClass>', 'array_merge($stringOrIntegerKeys, $generalStringKeys)', ], [ - 'non-empty-array', + 'array{foo: stdClass, bar: stdClass, 0: stdClass}', 'array_merge($stringKeys, $stringOrIntegerKeys)', ], [ - 'non-empty-array', + "array{foo: 'foo', 0: stdClass, bar: stdClass}", 'array_merge($stringOrIntegerKeys, $stringKeys)', ], [ - 'non-empty-array', + 'array{foo: 1, bar: 2, 0: 2, 1: 3}', + "array_merge(['foo' => 4, 'bar' => 5], ...[['foo' => 1, 'bar' => 2], [2, 3]])", + ], + [ + 'array{foo: 1, foo2: stdClass}', + 'array_merge([\'foo\' => new stdClass()], ...[[\'foo2\' => new stdClass()], [\'foo\' => 1]])', + ], + + [ + 'array{foo: 1, foo2: stdClass}', + 'array_merge([\'foo\' => new stdClass()], ...[[\'foo2\' => new stdClass()], [\'foo\' => 1]])', + ], + [ + "array{color: 'green', 0: 2, 1: 4, 2: 'a', 3: 'b', shape: 'trapezoid', 4: 4}", 'array_merge(array("color" => "red", 2, 4), array("a", "b", "color" => "green", "shape" => "trapezoid", 4))', ], [ @@ -4777,7 +4822,7 @@ public function dataArrayFunctions(): array 'array_fill(5, 6, \'banana\')', ], [ - 'non-empty-array', + 'non-empty-list<\'apple\'>', 'array_fill(0, 101, \'apple\')', ], [ @@ -4789,7 +4834,7 @@ public function dataArrayFunctions(): array 'array_fill($integer, 2, new \stdClass())', ], [ - 'array', + PHP_VERSION_ID < 80000 ? 'array|false' : 'array', 'array_fill(2, $integer, new \stdClass())', ], [ @@ -4817,11 +4862,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{foo: \'banana\', bar: \'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{foo: stdClass, bar: stdClass}', 'array_map(function (): \stdClass {}, $conditionalKeysArray)', ], [ @@ -4829,7 +4874,7 @@ public function dataArrayFunctions(): array 'array_pop($stringKeys)', ], [ - 'array&hasOffset(\'baz\')', + 'non-empty-array&hasOffsetValue(\'baz\', stdClass)', '$stdClassesWithIsset', ], [ @@ -4917,7 +4962,7 @@ public function dataArrayFunctions(): array 'array_search(9, $generalStringKeys)', ], [ - 'null', + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', 'array_search(999, $integer, true)', ], [ @@ -4965,19 +5010,19 @@ public function dataArrayFunctions(): array 'array_search(\'id\', $generalIntegerOrStringKeysMixedValues, true)', ], [ - 'int|string|false|null', + '*ERROR*', 'array_search(\'id\', doFoo() ? $generalIntegerOrStringKeys : false, true)', ], [ - 'false|null', + '*ERROR*', 'array_search(\'id\', doFoo() ? [] : false, true)', ], [ - 'null', + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', 'array_search(\'id\', false, true)', ], [ - 'null', + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', 'array_search(\'id\', false)', ], [ @@ -5228,6 +5273,14 @@ public function testArrayFunctions( public function dataFunctions(): array { + $strSplitDefaultReturnType = 'non-empty-list|false'; + if (PHP_VERSION_ID >= 80000) { + $strSplitDefaultReturnType = 'non-empty-list'; + } + if (PHP_VERSION_ID >= 80200) { + $strSplitDefaultReturnType = 'list'; + } + return [ [ 'string', @@ -5281,30 +5334,6 @@ public function dataFunctions(): array 'bool', '$versionCompare8', ], - [ - 'int<0, max>', - '$mbStrlenWithoutEncoding', - ], - [ - 'int<0, max>', - '$mbStrlenWithValidEncoding', - ], - [ - 'int<0, max>', - '$mbStrlenWithValidEncodingAlias', - ], - [ - PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', - '$mbStrlenWithInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'int<0, max>|false' : 'int<0, max>', - '$mbStrlenWithValidAndInvalidEncoding', - ], - [ - PHP_VERSION_ID < 80000 ? 'int<0, max>|false' : 'int<0, max>', - '$mbStrlenWithUnknownEncoding', - ], [ 'string', '$mbHttpOutputWithoutEncoding', @@ -5366,7 +5395,7 @@ public function dataFunctions(): array '$mbInternalEncodingWithUnknownEncoding', ], [ - 'array', + 'list', '$mbEncodingAliasesWithValidEncoding', ], [ @@ -5374,11 +5403,11 @@ public function dataFunctions(): array '$mbEncodingAliasesWithInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbEncodingAliasesWithValidAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbEncodingAliasesWithUnknownEncoding', ], [ @@ -5442,7 +5471,7 @@ public function dataFunctions(): array '$gettimeofdayBenevolent', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + $strSplitDefaultReturnType, '$strSplitConstantStringWithoutDefinedParameters', ], [ @@ -5450,7 +5479,7 @@ public function dataFunctions(): array '$strSplitConstantStringWithoutDefinedSplitLength', ], [ - 'non-empty-array', + PHP_VERSION_ID < 80200 ? 'non-empty-list' : 'list', '$strSplitStringWithoutDefinedSplitLength', ], [ @@ -5466,15 +5495,15 @@ public function dataFunctions(): array '$strSplitConstantStringWithFailureSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + $strSplitDefaultReturnType, '$strSplitConstantStringWithInvalidSplitLengthType', ], [ - 'non-empty-array', + "array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", '$strSplitConstantStringWithVariableStringAndConstantSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + $strSplitDefaultReturnType, '$strSplitConstantStringWithVariableStringAndVariableSplitLength', ], // parse_url @@ -5491,7 +5520,7 @@ public function dataFunctions(): array '$parseUrlConstantUrlWithoutComponent2', ], [ - 'array{scheme?: string, host?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', + 'array{scheme?: lowercase-string, host?: lowercase-string, port?: int<0, 65535>, user?: lowercase-string, pass?: lowercase-string, path?: lowercase-string, query?: lowercase-string, fragment?: lowercase-string}|int<0, 65535>|lowercase-string|false|null', '$parseUrlConstantUrlUnknownComponent', ], [ @@ -5511,11 +5540,11 @@ public function dataFunctions(): array '$parseUrlStringUrlWithComponentInvalid', ], [ - 'int|false|null', + 'int<0, 65535>|false|null', '$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<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', '$parseUrlStringUrlWithoutComponent', ], [ @@ -5685,7 +5714,11 @@ public function dataRangeFunction(): array 'range(2, 5, 2)', ], [ - 'array{2.0, 3.0, 4.0, 5.0}', + 'array{2, 0}', + "range(2, '', 2)", + ], + [ + PHP_VERSION_ID < 80300 ? 'array{2.0, 3.0, 4.0, 5.0}' : 'array{2, 3, 4, 5}', 'range(2, 5, 1.0)', ], [ @@ -5693,19 +5726,19 @@ public function dataRangeFunction(): array 'range(2.1, 5)', ], [ - 'array', + 'list', 'range(2, 5, $integer)', ], [ - 'array', + 'list', 'range($float, 5, $integer)', ], [ - 'array', + 'list<(float|int|string)>', 'range($float, $mixed, $integer)', ], [ - 'array', + 'list<(float|int|string)>', 'range($integer, $mixed)', ], [ @@ -5713,7 +5746,7 @@ public function dataRangeFunction(): array 'range(1, doFoo() ? 1 : 2)', ], [ - 'array{0: -1|1, 1?: 0|2, 2?: 1, 3?: 2}', + 'array{0: -1, 1: 0, 2: 1, 3?: 2}|array{0: 1, 1?: 2}', 'range(doFoo() ? -1 : 1, doFoo() ? 1 : 2)', ], [ @@ -5721,7 +5754,7 @@ public function dataRangeFunction(): array 'range(3, -1)', ], [ - 'non-empty-array>', + 'non-empty-list>', 'range(0, 50)', ], ]; @@ -5774,7 +5807,7 @@ public function dataSpecifiedTypesUsingIsFunctions(): array '$null', ], [ - 'array', + 'array', '$array', ], [ @@ -6105,15 +6138,15 @@ public function dataVoid(): array { return [ [ - 'void', + 'null', '$this->doFoo()', ], [ - 'void', + 'null', '$this->doBar()', ], [ - 'void', + 'null', '$this->doConflictingVoid()', ], ]; @@ -6430,7 +6463,7 @@ public function dataMisleadingTypes(): array '$foo->misleadingIntReturnType()', ], [ - 'mixed', + PHP_VERSION_ID >= 80000 ? 'mixed' : 'MisleadingTypes\mixed', '$foo->misleadingMixedReturnType()', ], ]; @@ -6917,12 +6950,12 @@ public function dataForeachLoopVariables(): array "'end'", ], [ - 'non-empty-array', + 'non-empty-list<1|2|3>', '$integers', "'end'", ], [ - 'array', + 'list<1|2|3>', '$integers', "'afterLoop'", ], @@ -7010,7 +7043,6 @@ public function dataWhileLoopVariables(): array ]; } - public function dataForLoopVariables(): array { return [ @@ -7047,8 +7079,6 @@ public function dataForLoopVariables(): array ]; } - - /** * @dataProvider dataLoopVariables * @dataProvider dataForeachLoopVariables @@ -7278,7 +7308,7 @@ public function dataExplode(): array { return [ [ - 'non-empty-array', + 'non-empty-list', '$sureArray', ], [ @@ -7286,15 +7316,15 @@ public function dataExplode(): array '$sureFalse', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'non-empty-list|false' : 'non-empty-list', '$arrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'non-empty-list|false' : 'non-empty-list', '$anotherArrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? '(non-empty-array|false)' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? '(non-empty-list|false)' : 'non-empty-list', '$benevolentArrayOrFalse', ], ]; @@ -7342,6 +7372,18 @@ public function dataArrayPointerFunctions(): array '\'baz\'|\'foo\'', 'reset($conditionalArray)', ], + [ + '0|1', + 'reset($constantArrayOptionalKeys1)', + ], + [ + '0', + 'reset($constantArrayOptionalKeys2)', + ], + [ + '0', + 'reset($constantArrayOptionalKeys3)', + ], [ 'mixed', 'end()', @@ -7366,6 +7408,18 @@ public function dataArrayPointerFunctions(): array '\'bar\'|\'baz\'', 'end($secondConditionalArray)', ], + [ + '2', + 'end($constantArrayOptionalKeys1)', + ], + [ + '2', + 'end($constantArrayOptionalKeys2)', + ], + [ + '1|2', + 'end($constantArrayOptionalKeys3)', + ], ]; } @@ -7388,7 +7442,7 @@ public function dataReplaceFunctions(): array { return [ [ - 'non-empty-string', + 'lowercase-string&non-falsy-string', '$expectedString', ], [ @@ -7396,7 +7450,7 @@ public function dataReplaceFunctions(): array '$expectedString2', ], [ - 'non-empty-string|null', + '(lowercase-string&non-falsy-string)|null', '$anotherExpectedString', ], [ @@ -7404,31 +7458,31 @@ public function dataReplaceFunctions(): array '$expectedArray', ], [ - 'array{a: string, b: string}|null', + 'array{a?: string, b?: string}', '$expectedArray2', ], [ - 'array{a: string, b: string}|null', + 'array{a?: string, b?: string}', '$anotherExpectedArray', ], [ - 'array|string', + 'list|string', '$expectedArrayOrString', ], [ - '(array|string)', + '(list|string)', '$expectedBenevolentArrayOrString', ], [ - 'array|string|null', + 'list|string|null', '$expectedArrayOrString2', ], [ - 'array|string|null', + 'list|string|null', '$anotherExpectedArrayOrString', ], [ - 'array{a: string, b: string}|null', + 'array{a?: string, b?: string}', 'preg_replace_callback_array($callbacks, $array)', ], [ @@ -7474,11 +7528,13 @@ public function dataFilterVar(): Generator 'FILTER_SANITIZE_SPECIAL_CHARS', 'FILTER_SANITIZE_STRING', 'FILTER_SANITIZE_URL', + 'FILTER_VALIDATE_REGEXP', + ], + 'non-falsy-string' => [ 'FILTER_VALIDATE_EMAIL', 'FILTER_VALIDATE_IP', '$filterIp', 'FILTER_VALIDATE_MAC', - 'FILTER_VALIDATE_REGEXP', 'FILTER_VALIDATE_URL', ], 'int' => ['FILTER_VALIDATE_INT'], @@ -7971,11 +8027,11 @@ public function dataPassedByReference(): array '$arr', ], [ - 'mixed', + 'array', '$matches', ], [ - 'mixed', + 'string', '$s', ], ]; @@ -8008,11 +8064,11 @@ public function dataCallables(): array '$closure()', ], [ - 'Callables\\Bar', + PHP_VERSION_ID < 80000 ? 'Callables\\Bar' : '*ERROR*', '$arrayWithStaticMethod()', ], [ - 'float', + PHP_VERSION_ID < 80000 ? 'float' : '*ERROR*', '$stringWithStaticMethod()', ], [ @@ -8045,15 +8101,15 @@ public function dataArrayKeysInBranches(): array { return [ [ - 'array{i: int<1, max>, j: int, k: int<1, max>, key: DateTimeImmutable, l: 1, m: 5, n?: \'str\'}', + 'array{i: int<1, max>, j: int, k: int<1, max>, l: 1, m: 5, key: DateTimeImmutable, n?: \'str\'}', '$array', ], [ - 'array', + 'non-empty-array&hasOffsetValue(\'key\', mixed~null)', '$generalArray', ], [ - 'mixed', // should be DateTimeImmutable + 'mixed~null', '$generalArray[\'key\']', ], [ @@ -8061,11 +8117,11 @@ public function dataArrayKeysInBranches(): array '$arrayAppendedInIf', ], [ - 'array', + 'non-empty-list<\'bar\'|\'baz\'|\'foo\'>', '$arrayAppendedInForeach', ], [ - 'array, literal-string&non-empty-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' + 'non-empty-array, literal-string&lowercase-string&non-falsy-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' '$anotherArrayAppendedInForeach', ], [ @@ -8377,6 +8433,44 @@ public function testAnonymousClassNameInTrait( ); } + public function dataAnonymousClassNameSameLine(): array + { + return [ + [ + 'AnonymousClass0d7d08272ba2f0a6ef324bb65c679e02', + '$foo', + '$bar', + ], + [ + 'AnonymousClass464f64cbdca25b4af842cae65615bca9', + '$bar', + '$baz', + ], + [ + 'AnonymousClassa9fb472ec9acc5cae3bee4355c296bfa', + '$baz', + 'die', + ], + ]; + } + + /** + * @dataProvider dataAnonymousClassNameSameLine + */ + public function testAnonymousClassNameSameLine( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/anonymous-class-name-same-line.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + public function dataDynamicConstants(): array { return [ @@ -8393,7 +8487,7 @@ public function dataDynamicConstants(): array 'DynamicConstants\NoDynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', ], [ - 'bool', + 'false', 'GLOBAL_DYNAMIC_CONSTANT', ], [ @@ -8423,6 +8517,52 @@ public function testDynamicConstants( ); } + public function dataDynamicConstantsWithNativeTypes(): array + { + return [ + [ + 'int', + 'DynamicConstantNativeTypes\Foo::FOO', + ], + [ + 'int|string', + 'DynamicConstantNativeTypes\Foo::BAR', + ], + [ + 'int', + '$foo::FOO', + ], + [ + 'int|string', + '$foo::BAR', + ], + ]; + } + + /** + * @dataProvider dataDynamicConstantsWithNativeTypes + */ + public function testDynamicConstantsWithNativeTypes( + string $description, + string $expression, + ): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->assertTypes( + __DIR__ . '/data/dynamic-constant-native-types.php', + $description, + $expression, + 'die', + [ + 'DynamicConstantNativeTypes\Foo::FOO', + 'DynamicConstantNativeTypes\Foo::BAR', + ], + ); + } + public function dataIsset(): array { return [ @@ -8431,19 +8571,19 @@ public function dataIsset(): array '$array[\'b\']', ], [ - 'array{a: 1|2|3, b: 2|3, c?: 4}', + 'array{a: 1, b: 2}|array{a: 3, b: 3, c: 4}', '$array', ], [ - 'array{a: 1|2|3, b: 2|3|null, c?: 4}', + 'array{a: 1, b: 2}|array{a: 3, b: 3, c: 4}|array{a: 3, b: null}', '$arrayCopy', ], [ - 'array{a: 1|2|3, c?: 4}', + 'array{a: 2}', '$anotherArrayCopy', ], [ - 'array', + 'array{a: 1, b: 2}|array{a: 2}|array{a: 3, b: 3, c: 4}|array{a: 3, b: null}', '$yetAnotherArrayCopy', ], [ @@ -8451,11 +8591,11 @@ public function dataIsset(): array '$mixedIsset', ], [ - 'array&hasOffset(\'a\')', + 'non-empty-array&hasOffset(\'a\')', '$mixedArrayKeyExists', ], [ - 'array&hasOffset(\'a\')', + 'non-empty-array&hasOffsetValue(\'a\', int)', '$integers', ], [ @@ -8475,7 +8615,7 @@ public function dataIsset(): array '$lookup[$a] ?? false', ], [ - '\'foo\'|false', + '\'foo\'', '$nullableArray[\'a\'] ?? false', ], [ @@ -8483,7 +8623,7 @@ public function dataIsset(): array '$nullableArray[\'b\'] ?? false', ], [ - '\'baz\'|false', + '\'baz\'', '$nullableArray[\'c\'] ?? false', ], ]; @@ -8552,43 +8692,6 @@ public function testPropertyArrayAssignment( ); } - public function dataInArray(): array - { - return [ - [ - '\'bar\'|\'foo\'', - '$s', - ], - [ - 'string', - '$mixed', - ], - [ - 'string', - '$r', - ], - [ - '\'foo\'', - '$fooOrBarOrBaz', - ], - ]; - } - - /** - * @dataProvider dataInArray - */ - public function testInArray( - string $description, - string $expression, - ): void - { - $this->assertTypes( - __DIR__ . '/data/in-array.php', - $description, - $expression, - ); - } - public function dataGetParentClass(): array { return [ @@ -8717,19 +8820,19 @@ public function dataPhp73Functions(): array { return [ [ - 'string|false', + 'non-empty-string|false', 'json_encode($mixed)', ], [ - 'string', + 'non-empty-string', 'json_encode($mixed, JSON_THROW_ON_ERROR)', ], [ - 'string', + 'non-empty-string', 'json_encode($mixed, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ - 'string', + 'non-empty-string', 'json_encode($mixed, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ @@ -8737,11 +8840,11 @@ public function dataPhp73Functions(): array 'json_decode($mixed)', ], [ - 'mixed~false', + 'mixed', 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ - 'mixed~false', + 'mixed', 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ @@ -8792,6 +8895,30 @@ public function dataPhp73Functions(): array '2|3', 'array_key_last($anotherLiteralArray)', ], + [ + "'a'|'b'", + 'array_key_first($constantArrayOptionalKeys1)', + ], + [ + "'c'", + 'array_key_last($constantArrayOptionalKeys1)', + ], + [ + "'a'", + 'array_key_first($constantArrayOptionalKeys2)', + ], + [ + "'c'", + 'array_key_last($constantArrayOptionalKeys2)', + ], + [ + "'a'", + 'array_key_first($constantArrayOptionalKeys3)', + ], + [ + "'b'|'c'", + 'array_key_last($constantArrayOptionalKeys3)', + ], [ 'array{int, int}', '$hrtime1', @@ -8819,9 +8946,6 @@ public function testPhp73Functions( string $expression, ): void { - if (PHP_VERSION_ID < 70300) { - $this->markTestSkipped('Test requires PHP 7.3'); - } $this->assertTypes( __DIR__ . '/data/php73_functions.php', $description, @@ -8833,7 +8957,7 @@ public function dataPhp74Functions(): array { return [ [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithoutDefinedParameters', ], [ @@ -8841,7 +8965,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithoutDefinedSplitLength', ], [ - 'non-empty-array', + 'list', '$mbStrSplitStringWithoutDefinedSplitLength', ], [ @@ -8857,15 +8981,15 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithFailureSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithInvalidSplitLengthType', ], [ - 'non-empty-array', + "array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLength', ], [ @@ -8877,7 +9001,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding', ], [ @@ -8889,7 +9013,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding', ], [ @@ -8905,7 +9029,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding', ], [ @@ -8913,11 +9037,11 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding', ], [ - 'non-empty-array', + "array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding', ], [ @@ -8925,11 +9049,11 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding', ], [ @@ -8937,7 +9061,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding', ], ]; @@ -8951,9 +9075,6 @@ public function testPhp74Functions( string $expression, ): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4'); - } $this->assertTypes( __DIR__ . '/data/php74_functions.php', $description, @@ -9048,7 +9169,7 @@ public function dataGeneralizeScope(): array { return [ [ - 'array, loadCount: int<0, max>, removeCount: int<0, max>, saveCount: int<0, max>}>>', + 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', '$statistics', ], ]; @@ -9073,7 +9194,7 @@ public function dataGeneralizeScopeRecursiveType(): array { return [ [ - 'array{}|array{foo: array}', + 'array{}|array{foo?: array}', '$data', ], ]; @@ -9159,7 +9280,7 @@ public function dataInferPrivatePropertyTypeFromConstructor(): array '$this->bool', ], [ - 'array', + 'array', '$this->array', ], ]; @@ -9206,9 +9327,6 @@ public function testPropertyNativeTypes( string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/property-native-types.php', $description, @@ -9242,9 +9360,6 @@ public function testArrowFunctions( string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/arrow-functions.php', $description, @@ -9278,9 +9393,6 @@ public function testArrowFunctionsInside( string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/arrow-functions-inside.php', $description, @@ -9338,9 +9450,6 @@ public function testCoalesceAssign( string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/coalesce-assign.php', $description, @@ -9352,11 +9461,11 @@ public function dataArraySpread(): array { return [ [ - 'non-empty-array', + 'non-empty-list', '$integersOne', ], [ - 'non-empty-array', + 'non-empty-list', '$integersTwo', ], [ @@ -9364,11 +9473,11 @@ public function dataArraySpread(): array '$integersThree', ], [ - 'non-empty-array', + 'non-empty-list', '$integersFour', ], [ - 'non-empty-array', + 'non-empty-list', '$integersFive', ], [ @@ -9390,9 +9499,6 @@ public function testArraySpread( string $expression, ): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/array-spread.php', $description, @@ -9400,39 +9506,11 @@ public function testArraySpread( ); } - public function dataPhp74FunctionsIn73(): array - { - return [ - [ - 'mixed', - 'password_algos()', - ], - ]; - } - - /** - * @dataProvider dataPhp74FunctionsIn73 - */ - public function testPhp74FunctionsIn73( - string $description, - string $expression, - ): void - { - if (PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test does not run on PHP >= 7.4.'); - } - $this->assertTypes( - __DIR__ . '/data/die-73.php', - $description, - $expression, - ); - } - public function dataPhp74FunctionsIn74(): array { return [ [ - 'array', + 'list', 'password_algos()', ], ]; @@ -9446,9 +9524,6 @@ public function testPhp74FunctionsIn74( string $expression, ): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->assertTypes( __DIR__ . '/data/die-74.php', $description, @@ -9522,24 +9597,25 @@ private function assertTypes( $assertType(self::$assertTypesCache[$file][$evaluatedPointExpression]); return; } - $this->processFile( - $file, - static function (Node $node, Scope $scope) use ($file, $evaluatedPointExpression, $assertType): void { - if ($node instanceof VirtualNode) { - return; - } - $printer = new Standard(); - $printedNode = $printer->prettyPrint([$node]); - if ($printedNode !== $evaluatedPointExpression) { - return; - } - - self::$assertTypesCache[$file][$evaluatedPointExpression] = $scope; - - $assertType($scope); - }, - $dynamicConstantNames, - ); + + self::processFile( + $file, + static function (Node $node, Scope $scope) use ($file, $evaluatedPointExpression, $assertType): void { + if ($node instanceof VirtualNode) { + return; + } + $printer = new Printer(); + $printedNode = $printer->prettyPrint([$node]); + if ($printedNode !== $evaluatedPointExpression) { + return; + } + + self::$assertTypesCache[$file][$evaluatedPointExpression] = $scope; + + $assertType($scope); + }, + $dynamicConstantNames, + ); } public static function getAdditionalConfigFiles(): array @@ -9573,7 +9649,7 @@ public function dataDeclareStrictTypes(): array */ public function testDeclareStrictTypes(string $file, bool $result): void { - $this->processFile($file, function (Node $node, Scope $scope) use ($result): void { + self::processFile($file, function (Node $node, Scope $scope) use ($result): void { if (!($node instanceof Exit_)) { return; } @@ -9584,7 +9660,7 @@ public function testDeclareStrictTypes(string $file, bool $result): void public function testEarlyTermination(): void { - $this->processFile(__DIR__ . '/data/early-termination.php', function (Node $node, Scope $scope): void { + self::processFile(__DIR__ . '/data/early-termination.php', function (Node $node, Scope $scope): void { if (!($node instanceof Exit_)) { return; } @@ -9595,7 +9671,7 @@ public function testEarlyTermination(): void }); } - protected function getEarlyTerminatingMethodCalls(): array + protected static function getEarlyTerminatingMethodCalls(): array { return [ \EarlyTermination\Foo::class => [ @@ -9605,7 +9681,7 @@ protected function getEarlyTerminatingMethodCalls(): array ]; } - protected function getEarlyTerminatingFunctionCalls(): array + protected static function getEarlyTerminatingFunctionCalls(): array { return ['baz']; } @@ -9625,7 +9701,7 @@ private function assertTypeDescribe( } /** @return string[] */ - protected function getAdditionalAnalysedFiles(): array + protected static function getAdditionalAnalysedFiles(): array { return [ __DIR__ . '/data/methodPhpDocs-trait-defined.php', diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php new file mode 100644 index 0000000000..83bb27b70b --- /dev/null +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php @@ -0,0 +1,40 @@ +> + */ + public function dataFileAsserts(): iterable + { + // compares constants according to the php-version phpstan configuration, + // _NOT_ the current php runtime version + yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/looseConstComparisonPhp7.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php new file mode 100644 index 0000000000..e765ca01d5 --- /dev/null +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php @@ -0,0 +1,40 @@ +> + */ + public function dataFileAsserts(): iterable + { + // compares constants according to the php-version phpstan configuration, + // _NOT_ the current php runtime version + yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php8.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/looseConstComparisonPhp8.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 8e2186cab3..a2e9ef0619 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -2,804 +2,274 @@ namespace PHPStan\Analyser; +use EnumTypeAssertions\Foo; +use PHPStan\File\FileHelper; use PHPStan\Testing\TypeInferenceTestCase; use stdClass; +use function array_shift; use function define; -use function extension_loaded; +use function dirname; +use function implode; +use function sprintf; +use function str_starts_with; +use function strlen; +use function substr; use const PHP_INT_SIZE; use const PHP_VERSION_ID; class NodeScopeResolverTest extends TypeInferenceTestCase { - public function dataFileAsserts(): iterable + /** + * @return iterable + */ + private static function findTestFiles(): 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'); - - require_once __DIR__ . '/data/bug2577.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2577.php'); - - require_once __DIR__ . '/data/generics.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics.php'); - - require_once __DIR__ . '/data/generic-class-string.php'; - - 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'); - 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'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/static-properties.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/static-methods.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2612.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2677.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2676.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/psalm-prefix-unresolvable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/complex-generics-example.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2648.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2740.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2822.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inheritdoc-parameter-remapping.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inheritdoc-constructors.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/list-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2835.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2443.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2750.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2850.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2863.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types.php'); - 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'); - } - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 70401) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-return-type.php'); - } - 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'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/const-expr-phpdoc-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3226.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2001.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2232.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3009.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-var.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-param.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-return.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-template.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3266.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3269.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assign-nested-arrays.php'); - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3276.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/shadowed-trait-methods.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions-namespaced.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/root-scope-maybe-defined.php'); - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3336.php'); - } - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/catch-without-variable.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/mixed-typehint.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'); - if (extension_loaded('gd')) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/graphics-draw-return-types.php'); - } - - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - require_once __DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/unionTypes.php'); - } - - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/mixedType.php'); - } - - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/staticReturnType.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-arrays.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array-key-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3133.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-2550.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2899.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_split.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bcmath-dynamic-return.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3875.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2611.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3548.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3866.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1014.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-pr-339.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/pow.php'); - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-expr.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array.php'); - - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-on-expr.php'); - } - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3961-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3961.php'); + foreach (self::findTestDataFilesFromDirectory(__DIR__ . '/nsrt') as $testFile) { + yield $testFile; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1924.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/extra-int-types.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/count-type.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816-2.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3985.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-slice.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3990.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3991.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3993.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3997.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4016.php'); - - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/promoted-properties-types.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/early-termination-phpdoc.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3915.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2378.php'); - - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/match-expr.php'); - } - - if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/nullsafe.php'); + if (PHP_VERSION_ID < 80200 && PHP_VERSION_ID >= 80100) { + yield __DIR__ . '/data/enum-reflection-php81.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/specified-types-closure-use.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/cast-to-numeric-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2539.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2733.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3132.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1233.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/comparison-operators.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3880.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inc-dec-in-conditions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4099.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3760.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2997.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1657.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2945.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4207.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4206.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-empty-array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4205.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variable-certainty.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1865.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-non-empty-array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/foreach-dependent-key-value.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variables-type-guard-same-as-type.php'); - - if (PHP_VERSION_ID >= 70400 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variables-arrow-function.php'); + if (PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80400) { + yield __DIR__ . '/data/enum-reflection-backed.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-801.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1209.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2980.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3986.php'); - - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4188.php'); - } - - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4339.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4343.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-method.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4351.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-use.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-declare.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4398.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4415.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/compact.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4500.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4504.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4436.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-3777.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2549.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1945.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2003.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-651.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1283.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4538.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/proc_get_status.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-4552.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1897.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1801.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2927.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4558.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4557.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4209.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4209-2.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2869.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3024.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3134.php'); - 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'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3321.php'); - - require_once __DIR__ . '/../Rules/Generics/data/bug-3769.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Generics/data/bug-3769.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/instanceof-class-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4498.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4587.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4606.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3922.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-types-unwrapping.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-types-unwrapping-covariant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-incomplete-constructor.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/iterator-iterator.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4642.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-4643.php'); - require_once __DIR__ . '/data/throw-points/helpers.php'; - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/php8/null-safe-method-call.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/and.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/array-dim-fetch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/assign.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/assign-op.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/do-while.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/for.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/foreach.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/func-call.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/if.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/method-call.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/or.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/property-fetch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/static-call.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/switch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/throw.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/try-catch-finally.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/variable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/while.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/try-catch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-override.php'); - require_once __DIR__ . '/data/phpdoc-pseudotype-namespace.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-namespace.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-global.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-traits.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4423.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-unions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-parent.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4247.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4267.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2231.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3558.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3351.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4213.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4657.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4707.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4545.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4714.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4725.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4733.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4326.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-987.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3677.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4215.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4695.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2977.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3190.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/ternary-specified-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-560.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/do-not-remember-impure-functions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4190.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/clear-stat-cache.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-object-argument.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-object-argument-static.php'); - - require_once __DIR__ . '/data/invalidate-object-argument-function.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-object-argument-function.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4588.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4091.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3382.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4177.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2288.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1157.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1597.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3617.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-778.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2969.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3004.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3710.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3822.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-505.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1670.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1219.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3302.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1511.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4434.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4231.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4287.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4700.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-in-closure-bind.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/multi-assign.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-reduce-types-first.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4803.php'); - - require_once __DIR__ . '/data/type-aliases.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/type-aliases.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4650.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2906.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/DateTimeDynamicReturnTypes.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4821.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4838.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4879.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4820.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4822.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4816.php'); - - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4757.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4814.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4982.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4761.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3331.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3106.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2640.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2413.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3446.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/getopt.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-default.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4985.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5000.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/number_format.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5140.php'); - - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-4857.php'); + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/bug-4902.php'; } - yield from $this->gatherAssertTypes(__DIR__ . '/data/empty-array-shape.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5089.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3158.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/unable-to-resolve-callback-parameter-type.php'); - - require_once __DIR__ . '/../Rules/Functions/data/varying-acceptor.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/varying-acceptor.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/uksort-bug.php'); - - if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-types.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902-php8.php'); + if (PHP_VERSION_ID < 80300) { + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/data/mb-strlen-php82.php'; + } elseif (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/data/mb-strlen-php8.php'; } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902.php'); + yield __DIR__ . '/data/mb-strlen-php73.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'); + yield __DIR__ . '/../Rules/Methods/data/bug-6856.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'); + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/explode-php74.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'); + yield __DIR__ . '/data/explode-php80.php'; } if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/reflectionclass-issue-5511-php8.php'); + yield __DIR__ . '/../Reflection/data/unionTypes.php'; + yield __DIR__ . '/../Reflection/data/mixedType.php'; + yield __DIR__ . '/../Reflection/data/staticReturnType.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'); + yield __DIR__ . '/data/predefined-constants-64bit.php'; } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5287.php'); + yield __DIR__ . '/data/predefined-constants-32bit.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'); - } + yield __DIR__ . '/../Rules/Variables/data/bug-10577.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-10610.php'; + yield __DIR__ . '/../Rules/Comparison/data/bug-2550.php'; + yield __DIR__ . '/../Rules/Properties/data/bug-3777.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-4552.php'; + yield __DIR__ . '/../Rules/Methods/data/infer-array-key.php'; + yield __DIR__ . '/../Rules/Generics/data/bug-3769.php'; + yield __DIR__ . '/../Rules/Generics/data/bug-6301.php'; + yield __DIR__ . '/../Rules/PhpDoc/data/bug-4643.php'; - if (PHP_VERSION_ID >= 80100 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-intersection.php'); + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Comparison/data/bug-4857.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'); - } - } + yield __DIR__ . '/../Rules/Methods/data/bug-5089.php'; + yield __DIR__ . '/../Rules/Methods/data/unable-to-resolve-callback-parameter-type.php'; - if (PHP_VERSION_ID >= 80100 || self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/first-class-callables.php'); - } + yield __DIR__ . '/../Rules/Functions/data/varying-acceptor.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-4415.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-5372.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-5372_2.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-5562.php'; if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-is-list-type-specifying.php'); + define('TEST_OBJECT_CONSTANT', new stdClass()); + define('TEST_NULL_CONSTANT', null); + define('TEST_TRUE_CONSTANT', true); + define('TEST_FALSE_CONSTANT', false); + define('TEST_ARRAY_CONSTANT', [true, false, null]); + define('TEST_ENUM_CONSTANT', Foo::ONE); + yield __DIR__ . '/data/new-in-initializers-runtime.php'; + yield __DIR__ . '/data/scope-in-enum-match-arm-body.php'; } - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-unpacking-string-keys.php'); - } + yield __DIR__ . '/../Rules/Comparison/data/bug-6473.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/filesystem-functions.php'); + yield __DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php'; - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/enums.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/enums-import-alias.php'); - } + yield __DIR__ . '/../Rules/Methods/data/bug-5749.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-5757.php'; if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6293.php'); + yield __DIR__ . '/../Rules/Methods/data/bug-6635.php'; } - if (PHP_VERSION_ID >= 70200) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-php72.php'); + if (PHP_VERSION_ID >= 80300) { + yield __DIR__ . '/../Rules/Constants/data/bug-10212.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'); + yield __DIR__ . '/../Rules/Methods/data/bug-3284.php'; - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-readonly-properties.php'); + if (PHP_VERSION_ID >= 80300) { + yield __DIR__ . '/../Rules/Methods/data/return-type-class-constant.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'); + //define('ALREADY_DEFINED_CONSTANT', true); + //yield from $this->gatherAssertTypes(__DIR__ . '/data/already-defined-constant.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'); - } + yield __DIR__ . '/../Rules/Methods/data/conditional-complex-templates.php'; - if (PHP_VERSION_ID >= 70304) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/date-period-return-types.php'); - } + yield __DIR__ . '/../Rules/Methods/data/bug-7511.php'; + yield __DIR__ . '/../Rules/Properties/data/trait-mixin.php'; + yield __DIR__ . '/../Rules/Methods/data/trait-mixin.php'; + yield __DIR__ . '/../Rules/Comparison/data/bug-4708.php'; + yield __DIR__ . '/../Rules/Functions/data/bug-7156.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-6364.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-5758.php'; + yield __DIR__ . '/../Rules/Functions/data/bug-3931.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-7417.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-7469.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-3391.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 __DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.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 >= 80200) { + yield __DIR__ . '/../Rules/Methods/data/true-typehint.php'; } + yield __DIR__ . '/../Rules/Arrays/data/bug-6000.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'); - } + yield __DIR__ . '/../Rules/Arrays/data/slevomat-foreach-unset-bug.php'; + yield __DIR__ . '/../Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.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'); + yield __DIR__ . '/../Rules/Comparison/data/bug-7898.php'; } if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6566-types.php'); + yield __DIR__ . '/../Rules/Functions/data/bug-7823.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'); + yield __DIR__ . '/../Analyser/data/is-resource-specified.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'); + yield __DIR__ . '/../Rules/Arrays/data/bug-7954.php'; + yield __DIR__ . '/../Rules/Comparison/data/docblock-assert-equality.php'; + yield __DIR__ . '/../Rules/Properties/data/bug-7839.php'; + yield __DIR__ . '/../Rules/Classes/data/bug-5333.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-8174.php'; - if (self::$useStaticReflectionProvider) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-in-union.php'); + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Comparison/data/bug-8169.php'; } + yield __DIR__ . '/../Rules/Functions/data/bug-8280.php'; + yield __DIR__ . '/../Rules/Comparison/data/bug-8277.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-8113.php'; + yield __DIR__ . '/../Rules/Functions/data/bug-8389.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-8467a.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'); + if (PHP_VERSION_ID >= 80100) { + yield __DIR__ . '/../Rules/Comparison/data/bug-8485.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 __DIR__ . '/../Rules/Comparison/data/bug-9007.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'); + yield __DIR__ . '/../Rules/DeadCode/data/bug-8620.php'; - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6505.php'); + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/../Rules/Constants/data/bug-8957.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'); + if (PHP_VERSION_ID >= 80100) { + yield __DIR__ . '/../Rules/Comparison/data/bug-9499.php'; + } + + yield __DIR__ . '/../Rules/PhpDoc/data/bug-8609-function.php'; + yield __DIR__ . '/../Rules/Comparison/data/bug-5365.php'; + yield __DIR__ . '/../Rules/Comparison/data/bug-6551.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-9403.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-9542.php'; + yield __DIR__ . '/../Rules/Functions/data/bug-9803.php'; + yield __DIR__ . '/../Rules/PhpDoc/data/bug-10594.php'; + yield __DIR__ . '/../Rules/Classes/data/bug-11591.php'; + yield __DIR__ . '/../Rules/Classes/data/bug-11591-method-tag.php'; + yield __DIR__ . '/../Rules/Classes/data/bug-11591-property-tag.php'; + yield __DIR__ . '/../Rules/Classes/data/mixin-trait-use.php'; + + yield __DIR__ . '/../Rules/Arrays/data/bug-11679.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; + yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.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'); + /** + * @return iterable + */ + public static function dataFile(): iterable + { + $base = dirname(__DIR__, 3) . '/'; + $baseLength = strlen($base); - 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'); - } + $fileHelper = new FileHelper($base); + foreach (self::findTestFiles() as $file) { + $file = $fileHelper->normalizePath($file); - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6591.php'); - } + $testName = $file; + if (str_starts_with($file, $base)) { + $testName = substr($file, $baseLength); + } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php'); + yield $testName => [$file]; + } } /** - * @dataProvider dataFileAsserts - * @param mixed ...$args + * @dataProvider dataFile */ - public function testFileAsserts( - string $assertType, - string $file, - ...$args, - ): void + public function testFile(string $file): void { - $this->assertFileAsserts($assertType, $file, ...$args); + $asserts = $this->gatherAssertTypes($file); + $this->assertNotCount(0, $asserts, sprintf('File %s has no asserts.', $file)); + $failures = []; + + foreach ($asserts as $args) { + $assertType = array_shift($args); + $file = array_shift($args); + + if ($assertType === 'type') { + $expected = $args[0]; + $actual = $args[1]; + + if ($expected !== $actual) { + $failures[] = sprintf("Line %d:\nExpected: %s\nActual: %s\n", $args[2], $expected, $actual); + } + } elseif ($assertType === 'variableCertainty') { + $expectedCertainty = $args[0]; + $actualCertainty = $args[1]; + $variableName = $args[2]; + + if ($expectedCertainty->equals($actualCertainty) !== true) { + $failures[] = sprintf("Certainty of %s on line %d:\nExpected: %s\nActual: %s\n", $variableName, $args[3], $expectedCertainty->describe(), $actualCertainty->describe()); + } + } + } + + if ($failures === []) { + return; + } + + self::fail(sprintf("Failed assertions in %s:\n\n%s", $file, implode("\n", $failures))); } public static function getAdditionalConfigFiles(): array diff --git a/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php new file mode 100644 index 0000000000..795707aabb --- /dev/null +++ b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/param-closure-this-stubs.php'); + } + + /** + * @dataProvider dataAsserts + * @param mixed ...$args + */ + public function testAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/param-closure-this-stubs.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ParamOutTypeTest.php b/tests/PHPStan/Analyser/ParamOutTypeTest.php new file mode 100644 index 0000000000..b29b6fbd0c --- /dev/null +++ b/tests/PHPStan/Analyser/ParamOutTypeTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/param-out.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/typeAliases.neon', + __DIR__ . '/param-out.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php new file mode 100644 index 0000000000..c3476c32fa --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/parameter-closure-type-extension-arrow-function.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__ . '/parameter-closure-type-extension-arrow-function.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php new file mode 100644 index 0000000000..4b990f1f72 --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/parameter-closure-type-extension.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__ . '/parameter-closure-type-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php b/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php new file mode 100644 index 0000000000..e32c7c3ca3 --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/param-out/parameter-out-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__ . '/parameter-out.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/PathConstantsTest.php b/tests/PHPStan/Analyser/PathConstantsTest.php new file mode 100644 index 0000000000..39832befa4 --- /dev/null +++ b/tests/PHPStan/Analyser/PathConstantsTest.php @@ -0,0 +1,40 @@ +gatherAssertTypes(__DIR__ . '/data/pathConstants-win.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/pathConstants.php'); + } + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/usePathConstantsAsConstantString.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ScopePhpVersionTest.php b/tests/PHPStan/Analyser/ScopePhpVersionTest.php new file mode 100644 index 0000000000..fac3b8a066 --- /dev/null +++ b/tests/PHPStan/Analyser/ScopePhpVersionTest.php @@ -0,0 +1,43 @@ +', + __DIR__ . '/data/scope-constants-global.php', + ], + [ + 'int<80000, 80499>', + __DIR__ . '/data/scope-constants-namespace.php', + ], + ]; + } + + /** + * @dataProvider dataTestPhpVersion + */ + public function testPhpVersion(string $expected, string $file): void + { + self::processFile($file, function (Node $node, Scope $scope) use ($expected): void { + if (!($node instanceof Exit_)) { + return; + } + $this->assertSame( + $expected, + $scope->getPhpVersion()->getType()->describe(VerbosityLevel::precise()), + ); + }); + } + +} diff --git a/tests/PHPStan/Analyser/ScopeTest.php b/tests/PHPStan/Analyser/ScopeTest.php index 4f9e0a4ce4..4612ef1596 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -5,16 +5,21 @@ use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Name\FullyQualified; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; 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\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +/** + * @covers \PHPStan\Analyser\MutatingScope + */ class ScopeTest extends PHPStanTestCase { @@ -29,7 +34,7 @@ public function dataGeneralize(): array [ new ConstantStringType('a'), new ConstantStringType('b'), - 'literal-string&non-empty-string', + 'literal-string&lowercase-string&non-falsy-string', ], [ new ConstantIntegerType(0), @@ -139,7 +144,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(1), ]), - 'array', + 'non-empty-array', ], [ new ConstantArrayType([ @@ -154,7 +159,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'array>', + 'non-empty-array>', ], [ new UnionType([ @@ -232,8 +237,8 @@ public function testGeneralize(Type $a, Type $b, string $expectedTypeDescription { /** @var ScopeFactory $scopeFactory */ $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); - $scopeA = $scopeFactory->create(ScopeContext::create('file.php'))->assignVariable('a', $a); - $scopeB = $scopeFactory->create(ScopeContext::create('file.php'))->assignVariable('a', $b); + $scopeA = $scopeFactory->create(ScopeContext::create('file.php'))->assignVariable('a', $a, $a, TrinaryLogic::createYes()); + $scopeB = $scopeFactory->create(ScopeContext::create('file.php'))->assignVariable('a', $b, $b, TrinaryLogic::createYes()); $resultScope = $scopeA->generalizeWith($scopeB); $this->assertSame($expectedTypeDescription, $resultScope->getVariableType('a')->describe(VerbosityLevel::precise())); } @@ -245,7 +250,29 @@ 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<0, max>', $type->describe(VerbosityLevel::precise())); + $this->assertSame('int<1, max>', $type->describe(VerbosityLevel::precise())); + } + + public function testDefinedVariables(): void + { + /** @var ScopeFactory $scopeFactory */ + $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); + $scope = $scopeFactory->create(ScopeContext::create('file.php')) + ->assignVariable('a', new ConstantStringType('a'), new StringType(), TrinaryLogic::createYes()) + ->assignVariable('b', new ConstantStringType('b'), new StringType(), TrinaryLogic::createMaybe()); + + $this->assertSame(['a'], $scope->getDefinedVariables()); + } + + public function testMaybeDefinedVariables(): void + { + /** @var ScopeFactory $scopeFactory */ + $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); + $scope = $scopeFactory->create(ScopeContext::create('file.php')) + ->assignVariable('a', new ConstantStringType('a'), new StringType(), TrinaryLogic::createYes()) + ->assignVariable('b', new ConstantStringType('b'), new StringType(), TrinaryLogic::createMaybe()); + + $this->assertSame(['b'], $scope->getMaybeDefinedVariables()); } } diff --git a/tests/PHPStan/Analyser/StatementResultTest.php b/tests/PHPStan/Analyser/StatementResultTest.php index 228ad689c6..da7d60bd3d 100644 --- a/tests/PHPStan/Analyser/StatementResultTest.php +++ b/tests/PHPStan/Analyser/StatementResultTest.php @@ -5,6 +5,7 @@ use PhpParser\Node\Stmt; use PHPStan\Parser\Parser; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; @@ -173,6 +174,138 @@ public function dataIsAlwaysTerminating(): array 'while (true) { break; }', false, ], + [ + 'while (true) { exit; }', + true, + ], + [ + 'while (true) { while (true) { } }', + true, + ], + [ + 'while (true) { while (true) { return; } }', + true, + ], + [ + 'while (true) { while (true) { break; } }', + true, + ], + [ + 'while (true) { while (true) { exit; } }', + true, + ], + [ + 'while (true) { while (true) { break 2; } }', + false, + ], + [ + 'while (true) { while ($x) { } }', + true, + ], + [ + 'while (true) { while ($x) { return; } }', + true, + ], + [ + 'while (true) { while ($x) { break; } }', + true, + ], + [ + 'while (true) { while ($x) { exit; } }', + true, + ], + [ + 'while (true) { while ($x) { break 2; } }', + false, + ], + [ + 'for (;;) { }', + true, + ], + [ + 'for (;;) { return; }', + true, + ], + [ + 'for (;;) { break; }', + false, + ], + [ + 'for (;;) { exit; }', + true, + ], + [ + 'for (;;) { for (;;) { } }', + true, + ], + [ + 'for (;;) { for (;;) { return; } }', + true, + ], + [ + 'for (;;) { for (;;) { break; } }', + true, + ], + [ + 'for (;;) { for (;;) { exit; } }', + true, + ], + [ + 'for (;;) { for (;;) { break 2; } }', + false, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { } }', + true, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { return; } }', + true, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { break; } }', + true, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { exit; } }', + true, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { break 2; } }', + false, + ], + [ + 'for ($i = 0; $i < 5;) { }', + true, + ], + [ + 'for ($i = 0; $i < 5; $i--) { }', + true, + ], + [ + 'for (; 0, 1;) { }', + true, + ], + [ + 'for (; 1, 0;) { }', + false, + ], + [ + 'for (; "", "a";) { }', + true, + ], + [ + 'for (; "a", "";) { }', + false, + ], + [ + 'for ($c = (0x80 | 0x40); $c & 0x80; $c = $c << 1) { }', + false, + ], + [ + 'for ($i = 0; $i < 10; $i++) { $i = 5; }', + true, + ], [ 'do { } while (doFoo());', false, @@ -231,7 +364,7 @@ public function dataIsAlwaysTerminating(): array ], [ 'for ($i = 0; $i < 10; $i++) { return; }', - false, // will be true with range types + true, ], [ 'for ($i = 0; $i < 0; $i++) { return; }', @@ -395,16 +528,17 @@ public function testIsAlwaysTerminating( /** @var ScopeFactory $scopeFactory */ $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); $scope = $scopeFactory->create(ScopeContext::create('test.php')) - ->assignVariable('string', new StringType()) - ->assignVariable('x', new IntegerType()) - ->assignVariable('cond', new MixedType()) - ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType())); + ->assignVariable('string', new StringType(), new StringType(), TrinaryLogic::createYes()) + ->assignVariable('x', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()) + ->assignVariable('cond', new MixedType(), new MixedType(), TrinaryLogic::createYes()) + ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), TrinaryLogic::createYes()); $result = $nodeScopeResolver->processStmtNodes( new Stmt\Namespace_(null, $stmts), $stmts, $scope, static function (): void { }, + StatementContext::createTopLevel(), ); $this->assertSame($expectedIsAlwaysTerminating, $result->isAlwaysTerminating()); } diff --git a/tests/PHPStan/Analyser/TestClosureTypeRule.php b/tests/PHPStan/Analyser/TestClosureTypeRule.php new file mode 100644 index 0000000000..9d3128b1c7 --- /dev/null +++ b/tests/PHPStan/Analyser/TestClosureTypeRule.php @@ -0,0 +1,39 @@ + + */ +class TestClosureTypeRule implements Rule +{ + + public function getNodeType(): string + { + return FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node instanceof Closure && !$node instanceof Node\Expr\ArrowFunction) { + return []; + } + + $type = $scope->getType($node); + + return [ + RuleErrorBuilder::message(sprintf('Closure type: %s', $type->describe(VerbosityLevel::precise()))) + ->identifier('tests.closureType') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php b/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php new file mode 100644 index 0000000000..aebfdc606f --- /dev/null +++ b/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class TestClosureTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TestClosureTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/nsrt/closure-passed-to-type.php'], [ + [ + 'Closure type: Closure(mixed): (1|2|3)', + 25, + ], + [ + 'Closure type: Closure(mixed): (1|2|3)', + 35, + ], + ]); + } + +} diff --git a/tests/PHPStan/Analyser/ThrowsTagFromNativeFunctionStubTest.php b/tests/PHPStan/Analyser/ThrowsTagFromNativeFunctionStubTest.php new file mode 100644 index 0000000000..a6f515560b --- /dev/null +++ b/tests/PHPStan/Analyser/ThrowsTagFromNativeFunctionStubTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/throws-tag-from-native-function-stub.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/throws-tag-from-native-function-stub.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TraitStubFilesTest.php b/tests/PHPStan/Analyser/TraitStubFilesTest.php new file mode 100644 index 0000000000..9107f5f45c --- /dev/null +++ b/tests/PHPStan/Analyser/TraitStubFilesTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/trait-stubs.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__ . '/trait-stubs.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 43af6df355..0957b36f22 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -18,7 +18,10 @@ use PhpParser\Node\Scalar\String_; use PhpParser\Node\VarLikeIdentifier; use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Printer\Printer; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -36,12 +39,13 @@ use function sprintf; use const PHP_INT_MAX; use const PHP_INT_MIN; +use const PHP_VERSION_ID; class TypeSpecifierTest extends PHPStanTestCase { private const FALSEY_TYPE_DESCRIPTION = '0|0.0|\'\'|\'0\'|array{}|false|null'; - private const TRUTHY_TYPE_DESCRIPTION = 'mixed~' . self::FALSEY_TYPE_DESCRIPTION; + 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; @@ -55,23 +59,24 @@ class TypeSpecifierTest extends PHPStanTestCase protected function setUp(): void { $reflectionProvider = $this->createReflectionProvider(); - $this->printer = new Standard(); + $this->printer = new Printer(); $this->typeSpecifier = self::getContainer()->getService('typeSpecifier'); $this->scope = $this->createScopeFactory($reflectionProvider, $this->typeSpecifier)->create(ScopeContext::create('')); $this->scope = $this->scope->enterClass($reflectionProvider->getClass('DateTime')); - $this->scope = $this->scope->assignVariable('bar', new ObjectType('Bar')); - $this->scope = $this->scope->assignVariable('stringOrNull', new UnionType([new StringType(), new NullType()])); - $this->scope = $this->scope->assignVariable('string', new StringType()); - $this->scope = $this->scope->assignVariable('barOrNull', new UnionType([new ObjectType('Bar'), new NullType()])); - $this->scope = $this->scope->assignVariable('barOrFalse', new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)])); - $this->scope = $this->scope->assignVariable('stringOrFalse', new UnionType([new StringType(), new ConstantBooleanType(false)])); - $this->scope = $this->scope->assignVariable('array', new ArrayType(new MixedType(), new MixedType())); - $this->scope = $this->scope->assignVariable('foo', new MixedType()); - $this->scope = $this->scope->assignVariable('classString', new ClassStringType()); - $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar'))); - $this->scope = $this->scope->assignVariable('object', new ObjectWithoutClassType()); - $this->scope = $this->scope->assignVariable('int', new IntegerType()); - $this->scope = $this->scope->assignVariable('float', new FloatType()); + $this->scope = $this->scope->assignVariable('bar', new ObjectType('Bar'), new ObjectType('Bar'), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('stringOrNull', new UnionType([new StringType(), new NullType()]), new UnionType([new StringType(), new NullType()]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('string', new StringType(), new StringType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('fooOrNull', new UnionType([new ObjectType('Foo'), new NullType()]), new UnionType([new ObjectType('Foo'), new NullType()]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('barOrNull', new UnionType([new ObjectType('Bar'), new NullType()]), new UnionType([new ObjectType('Bar'), new NullType()]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('barOrFalse', new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)]), new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('stringOrFalse', new UnionType([new StringType(), new ConstantBooleanType(false)]), new UnionType([new StringType(), new ConstantBooleanType(false)]), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('array', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('foo', new MixedType(), new MixedType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('classString', new ClassStringType(), new ClassStringType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar')), new GenericClassStringType(new ObjectType('Bar')), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('object', new ObjectWithoutClassType(), new ObjectWithoutClassType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('int', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()); + $this->scope = $this->scope->assignVariable('float', new FloatType(), new FloatType(), TrinaryLogic::createYes()); } /** @@ -90,9 +95,41 @@ public function testCondition(Expr $expr, array $expectedPositiveResult, array $ $this->assertSame($expectedNegatedResult, $actualResult, sprintf('if not (%s)', $this->printer->prettyPrintExpr($expr))); } - public function dataCondition(): array + public function dataCondition(): iterable { - return [ + if (PHP_VERSION_ID >= 80100) { + yield [ + new Identical( + new PropertyFetch(new Variable('foo'), 'bar'), + new Expr\ClassConstFetch(new Name('Bug9499\\FooEnum'), 'A'), + ), + [ + '$foo->bar' => 'Bug9499\FooEnum::A', + ], + [ + '$foo->bar' => '~Bug9499\FooEnum::A', + ], + ]; + yield [ + new Identical( + new AlwaysRememberedExpr( + new PropertyFetch(new Variable('foo'), 'bar'), + new ObjectType('Bug9499\\FooEnum'), + new ObjectType('Bug9499\\FooEnum'), + ), + new Expr\ClassConstFetch(new Name('Bug9499\\FooEnum'), 'A'), + ), + [ + '__phpstanRembered($foo->bar)' => 'Bug9499\FooEnum::A', + '$foo->bar' => 'Bug9499\FooEnum::A', + ], + [ + '__phpstanRembered($foo->bar)' => '~Bug9499\FooEnum::A', + '$foo->bar' => '~Bug9499\FooEnum::A', + ], + ]; + } + yield from [ [ $this->createFunctionCall('is_int'), ['$foo' => 'int'], @@ -101,7 +138,7 @@ public function dataCondition(): array [ $this->createFunctionCall('is_numeric'), ['$foo' => 'float|int|numeric-string'], - ['$foo' => '~float|int'], + ['$foo' => '~float|int|numeric-string'], ], [ $this->createFunctionCall('is_scalar'), @@ -192,8 +229,8 @@ public function dataCondition(): array ]), new String_('Foo'), ), - ['$foo' => 'Foo'], - ['$foo' => '~Foo'], + ['$foo' => 'Foo', 'get_class($foo)' => '\'Foo\''], + ['get_class($foo)' => '~\'Foo\''], ], [ new Equal( @@ -202,8 +239,28 @@ public function dataCondition(): array new Arg(new Variable('foo')), ]), ), - ['$foo' => 'Foo'], - ['$foo' => '~Foo'], + ['$foo' => 'Foo', 'get_class($foo)' => '\'Foo\''], + ['get_class($foo)' => '~\'Foo\''], + ], + [ + new Equal( + new FuncCall(new Name('get_debug_type'), [ + new Arg(new Variable('foo')), + ]), + new String_('Foo'), + ), + ['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''], + ['get_debug_type($foo)' => '~\'Foo\''], + ], + [ + new Equal( + new String_('Foo'), + new FuncCall(new Name('get_debug_type'), [ + new Arg(new Variable('foo')), + ]), + ), + ['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''], + ['get_debug_type($foo)' => '~\'Foo\''], ], [ new BooleanNot( @@ -376,6 +433,14 @@ public function dataCondition(): array ['is_int($foo)' => 'true', '$foo' => 'int'], ['is_int($foo)' => '~true', '$foo' => '~int'], ], + [ + new Identical( + $this->createFunctionCall('is_string'), + new Expr\ConstFetch(new Name('true')), + ), + ['is_string($foo)' => 'true', '$foo' => 'string'], + ['is_string($foo)' => '~true', '$foo' => '~string'], + ], [ new Identical( $this->createFunctionCall('is_int'), @@ -413,15 +478,15 @@ public function dataCondition(): array new Variable('foo'), new Expr\ConstFetch(new Name('null')), ), - ['$foo' => self::SURE_NOT_TRUTHY], - ['$foo' => self::SURE_NOT_FALSEY], + ['$foo' => '0|0.0|\'\'|array{}|false|null'], + ['$foo' => '~0|0.0|\'\'|array{}|false|null'], ], [ new Expr\BinaryOp\Identical( new Variable('foo'), new Variable('bar'), ), - ['$foo' => 'Bar', '$bar' => 'Bar'], + ['$foo' => 'Bar', '$bar' => 'mixed'], // could be '$bar' => 'Bar' [], ], [ @@ -527,6 +592,17 @@ public function dataCondition(): array ['$foo' => self::SURE_NOT_FALSEY], ['$foo' => self::SURE_NOT_TRUTHY], ], + [ + new Expr\Isset_([ + new Variable('stringOrNull'), + ]), + [ + '$stringOrNull' => '~null', + ], + [ + '$stringOrNull' => 'null', + ], + ], [ new Expr\Isset_([ new Variable('stringOrNull'), @@ -536,16 +612,29 @@ public function dataCondition(): array '$stringOrNull' => '~null', '$barOrNull' => '~null', ], + [], + ], + [ + new Expr\Isset_([ + new Variable('stringOrNull'), + new Variable('barOrNull'), + new Variable('fooOrNull'), + ]), [ - 'isset($stringOrNull, $barOrNull)' => self::SURE_NOT_TRUTHY, + '$stringOrNull' => '~null', + '$barOrNull' => '~null', + '$fooOrNull' => '~null', ], + [], ], [ new Expr\BooleanNot(new Expr\Empty_(new Variable('stringOrNull'))), [ '$stringOrNull' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], - [], + [ + '$stringOrNull' => '\'\'|\'0\'|null', + ], ], [ new Expr\BinaryOp\Identical( @@ -559,7 +648,9 @@ public function dataCondition(): array ], [ new Expr\Empty_(new Variable('array')), - [], + [ + '$array' => 'array{}|null', + ], [ '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], @@ -569,7 +660,9 @@ public function dataCondition(): array [ '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], - [], + [ + '$array' => 'array{}|null', + ], ], [ new FuncCall(new Name('count'), [ @@ -667,9 +760,7 @@ public function dataCondition(): array '$foo' => 'object&hasProperty(bar) & ~null', '$foo->bar' => '~null', ], - [ - 'isset($foo->bar)' => self::SURE_NOT_TRUTHY, - ], + [], ], [ new Expr\Isset_( @@ -680,9 +771,7 @@ public function dataCondition(): array [ 'Foo::$bar' => '~null', ], - [ - 'isset(Foo::$bar)' => self::SURE_NOT_TRUTHY, - ], + [], ], [ new Identical( @@ -706,9 +795,11 @@ public function dataCondition(): array ), [ '$notNullBar' => 'null', + '$barOrNull' => 'null', ], [ '$notNullBar' => '~null', + '$barOrNull' => '~null', ], ], [ @@ -729,10 +820,10 @@ public function dataCondition(): array new LNumber(3), ), [ - '$n' => 'mixed~int<3, max>|true', + '$n' => 'mixed~(int<3, max>|true)', ], [ - '$n' => 'mixed~int|false|null', + '$n' => 'mixed~(0.0|int|false|null)', ], ], [ @@ -741,10 +832,10 @@ public function dataCondition(): array new LNumber(PHP_INT_MIN), ), [ - '$n' => 'mixed~int<' . PHP_INT_MIN . ', max>|true', + '$n' => 'mixed~(int<' . PHP_INT_MIN . ', max>|true)', ], [ - '$n' => 'mixed~false|null', + '$n' => 'mixed~(0.0|false|null)', ], ], [ @@ -753,7 +844,7 @@ public function dataCondition(): array new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~bool|int|null', + '$n' => 'mixed~(0.0|bool|int|null)', ], [ '$n' => 'mixed', @@ -768,7 +859,7 @@ public function dataCondition(): array '$n' => 'mixed~int<' . (PHP_INT_MIN + 1) . ', max>', ], [ - '$n' => 'mixed~bool|int|null', + '$n' => 'mixed~(0.0|bool|int|null)', ], ], [ @@ -777,10 +868,10 @@ public function dataCondition(): array new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~int|false|null', + '$n' => 'mixed~(0.0|int|false|null)', ], [ - '$n' => 'mixed~int<' . PHP_INT_MAX . ', max>|true', + '$n' => 'mixed~(int<' . PHP_INT_MAX . ', max>|true)', ], ], [ @@ -795,10 +886,10 @@ public function dataCondition(): array ), ), [ - '$n' => 'mixed~int|int<6, max>|false|null', + '$n' => 'mixed~(0.0|int|int<6, max>|false|null)', ], [ - '$n' => 'mixed~int<3, 5>|true', + '$n' => 'mixed~(int<3, 5>|true)', ], ], [ @@ -828,9 +919,11 @@ public function dataCondition(): array ), [ '$notNullBar' => '~null', + '$barOrNull' => '~null', ], [ '$notNullBar' => 'null', + '$barOrNull' => 'null', ], ], [ @@ -855,9 +948,32 @@ public function dataCondition(): array ), [ '$notFalseBar' => 'false & ' . self::SURE_NOT_TRUTHY, + '$barOrFalse' => 'false', ], [ '$notFalseBar' => '~false', + '$barOrFalse' => '~false', + ], + ], + [ + new Identical( + new Expr\ConstFetch(new Name('null')), + new Expr\AssignOp\Coalesce( + new Variable('a'), + new Expr\Ternary( + new Variable('b'), + new Variable('b'), + new Expr\ConstFetch( + new Name('null'), + ), + ), + ), + ), + [ + '$a' => 'null', + ], + [ + '$a' => '~null', ], ], [ @@ -882,9 +998,11 @@ public function dataCondition(): array ), [ '$notFalseBar' => '~false', + '$barOrFalse' => '~false', ], [ '$notFalseBar' => 'false & ' . self::SURE_NOT_TRUTHY, + '$barOrFalse' => 'false', ], ], [ @@ -897,9 +1015,11 @@ public function dataCondition(): array ), [ '$notFalseBar' => 'Bar', + '$barOrFalse' => 'Bar', ], [ '$notFalseBar' => '~Bar', + '$barOrFalse' => '~Bar', ], ], [ @@ -914,7 +1034,7 @@ public function dataCondition(): array ]), ), [ - '$array' => 'array', + '$array' => 'non-empty-array', ], [ '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', @@ -935,7 +1055,7 @@ public function dataCondition(): array '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', ], [ - '$array' => 'array', + '$array' => 'non-empty-array', ], ], [ @@ -944,7 +1064,55 @@ public function dataCondition(): array new Arg(new Variable('array')), ]), [ - '$array' => 'array&hasOffset(\'foo\')', + '$array' => 'non-empty-array&hasOffset(\'foo\')', + ], + [ + '$array' => '~hasOffset(\'foo\')', + ], + ], + [ + new Expr\BinaryOp\BooleanOr( + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('foo')), + new Arg(new Variable('array')), + ]), + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('bar')), + new Arg(new Variable('array')), + ]), + ), + [ + '$array' => 'non-empty-array', + ], + [ + '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', + ], + ], + [ + new BooleanNot(new Expr\BinaryOp\BooleanOr( + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('foo')), + new Arg(new Variable('array')), + ]), + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('bar')), + new Arg(new Variable('array')), + ]), + )), + [ + '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', + ], + [ + '$array' => 'non-empty-array', + ], + ], + [ + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('foo')), + new Arg(new Variable('array')), + ]), + [ + '$array' => 'non-empty-array&hasOffset(\'foo\')', ], [ '$array' => '~hasOffset(\'foo\')', @@ -966,9 +1134,7 @@ public function dataCondition(): array new Arg(new Variable('stringOrNull')), new Arg(new Expr\ConstFetch(new Name('false'))), ]), - [ - '$object' => 'object', - ], + [], [], ], [ @@ -1050,8 +1216,8 @@ public function dataCondition(): array ), new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')), ), - ['$a' => 'non-empty-array|null'], - ['$a' => 'mixed~non-empty-array & ~null'], + ['$a' => 'non-empty-array|null'], + ['$a' => 'mixed~non-empty-array & ~null'], ], [ new Expr\BinaryOp\BooleanAnd( @@ -1066,7 +1232,7 @@ public function dataCondition(): array ), [ '$foo' => 'array', - 'array_filter($foo, \'is_string\', ARRAY_FILTER_USE_KEY)' => 'array', + 'array_filter($foo, \'is_string\', ARRAY_FILTER_USE_KEY)' => 'array', // could be 'array' ], [], ], @@ -1082,8 +1248,8 @@ public function dataCondition(): array ), ), [ - '$foo' => 'non-empty-array', - 'count($foo)' => 'mixed~int|false|null', + '$foo' => 'non-empty-array', + 'count($foo)' => 'mixed~(0.0|int|false|null)', ], [], ], @@ -1099,7 +1265,7 @@ public function dataCondition(): array ), ), [ - '$foo' => 'non-empty-array', + '$foo' => 'non-empty-array', 'count($foo)' => '2', ], [], @@ -1116,7 +1282,7 @@ public function dataCondition(): array ), ), [ - '$foo' => 'non-empty-string', + '$foo' => "string & ~''", 'strlen($foo)' => '~0', ], [ @@ -1177,11 +1343,17 @@ private function toReadableResult(SpecifiedTypes $specifiedTypes): array return $descriptions; } + /** + * @param non-empty-string $className + */ private function createInstanceOf(string $className, string $variableName = 'foo'): Expr\Instanceof_ { return new Expr\Instanceof_(new Variable($variableName), new Name($className)); } + /** + * @param non-empty-string $functionName + */ private function createFunctionCall(string $functionName, string $variableName = 'foo'): FuncCall { return new FuncCall(new Name($functionName), [new Arg(new Variable($variableName))]); diff --git a/tests/PHPStan/Analyser/UnknownMixedTypeOnOlderPhpTest.php b/tests/PHPStan/Analyser/UnknownMixedTypeOnOlderPhpTest.php new file mode 100644 index 0000000000..3c83a57e57 --- /dev/null +++ b/tests/PHPStan/Analyser/UnknownMixedTypeOnOlderPhpTest.php @@ -0,0 +1,42 @@ + + */ +class UnknownMixedTypeOnOlderPhpTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return self::getContainer()->getByType(ExistingClassesInTypehintsRule::class); + } + + public function testMixedUnknownType(): void + { + $this->analyse([__DIR__ . '/data/unknown-mixed-type.php'], [ + [ + 'Parameter $m of method UnknownMixedType\Foo::doFoo() has invalid type UnknownMixedType\mixed.', + 8, + ], + [ + 'Method UnknownMixedType\Foo::doFoo() has invalid return type UnknownMixedType\mixed.', + 8, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge(parent::getAdditionalConfigFiles(), [ + __DIR__ . '/unknown-mixed-type.neon', + ]); + } + +} diff --git a/tests/PHPStan/Analyser/assert-stub.neon b/tests/PHPStan/Analyser/assert-stub.neon new file mode 100644 index 0000000000..6c5d7b7d60 --- /dev/null +++ b/tests/PHPStan/Analyser/assert-stub.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/assert.stub diff --git a/tests/PHPStan/Analyser/bug-10922.neon b/tests/PHPStan/Analyser/bug-10922.neon new file mode 100644 index 0000000000..3ee516d3be --- /dev/null +++ b/tests/PHPStan/Analyser/bug-10922.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithAlwaysIterableForeach: false diff --git a/tests/PHPStan/Analyser/bug-11009.neon b/tests/PHPStan/Analyser/bug-11009.neon new file mode 100644 index 0000000000..d9a90c70b4 --- /dev/null +++ b/tests/PHPStan/Analyser/bug-11009.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/bug-11009.stub diff --git a/tests/PHPStan/Analyser/bug-9307.neon b/tests/PHPStan/Analyser/bug-9307.neon new file mode 100644 index 0000000000..c551b84f1f --- /dev/null +++ b/tests/PHPStan/Analyser/bug-9307.neon @@ -0,0 +1,2 @@ +parameters: + treatPhpDocTypesAsCertain: false diff --git a/tests/PHPStan/Analyser/conditional-return-type-stub.neon b/tests/PHPStan/Analyser/conditional-return-type-stub.neon new file mode 100644 index 0000000000..9fe5ad41fe --- /dev/null +++ b/tests/PHPStan/Analyser/conditional-return-type-stub.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/conditional-return-type.stub diff --git a/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php b/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php index 3ee8656661..b66f99e76b 100644 --- a/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php +++ b/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php @@ -1,7 +1,7 @@ name instanceof Identifier) { + return null; + } + + if ($expr->name->name !== 'methodReturningBoolNoMatterTheCallerUnlessReturnsString') { + return null; + } + + $methodReflection = $scope->getMethodReflection($scope->getType($expr->var), $expr->name->name); + + if ($methodReflection === null) { + return null; + } + + $returnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $methodReflection->getVariants() + )->getReturnType(); + + if ($returnType instanceof StringType) { + return null; + } + + return new BooleanType(); + } + +} diff --git a/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php index 3fd9f8b0b4..dd72c4525e 100644 --- a/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php +++ b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php @@ -2,17 +2,27 @@ namespace PHPStan\Tests; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ResolvedMethodReflection; +use PHPStan\Reflection\Type\CalledOnTypeUnresolvedMethodPrototypeReflection; +use PHPStan\Reflection\Type\UnionTypeMethodReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; +use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; class GetByPrimaryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -31,16 +41,28 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method { $args = $methodCall->args; if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } $arg = $args[0]->value; if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } if (!($arg->class instanceof \PhpParser\Node\Name)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } return new ObjectType((string) $arg->class); @@ -65,12 +87,20 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method { $args = $methodCall->args; if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } $argType = $scope->getType($args[0]->value); if (!$argType instanceof ConstantStringType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } return new ObjectType($argType->getValue()); @@ -95,16 +125,28 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, { $args = $methodCall->args; if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } $arg = $args[0]->value; if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } if (!($arg->class instanceof \PhpParser\Node\Name)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); } return new ObjectType((string) $arg->class); @@ -189,3 +231,71 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } } + + +class ConditionalGetSingle implements DynamicMethodReturnTypeExtension { + + public function getClass(): string + { + return \DynamicMethodReturnGetSingleConditional\Foo::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'get'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + +} + +class Bug7344DynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension +{ + public function getClass(): string + { + return \Bug7344\Model::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getModel'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + return new IntegerType(); + } + +} + +class Bug7391BDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +{ + public function getClass(): string + { + return \Bug7391B\Foo::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'm'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope + ): Type { + // return instantiated type from class string + return $scope->getType(new New_($methodCall->class)); + } +} diff --git a/tests/PHPStan/Analyser/data/already-defined-constant.php b/tests/PHPStan/Analyser/data/already-defined-constant.php new file mode 100644 index 0000000000..e37c747d22 --- /dev/null +++ b/tests/PHPStan/Analyser/data/already-defined-constant.php @@ -0,0 +1,15 @@ +> $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.php b/tests/PHPStan/Analyser/data/array-destructuring.php index abf9bff20c..69e8b03786 100644 --- a/tests/PHPStan/Analyser/data/array-destructuring.php +++ b/tests/PHPStan/Analyser/data/array-destructuring.php @@ -1,5 +1,5 @@ true, 'value' => '123']; diff --git a/tests/PHPStan/Analyser/data/array-filter.php b/tests/PHPStan/Analyser/data/array-filter.php deleted file mode 100644 index f5d1e181fe..0000000000 --- a/tests/PHPStan/Analyser/data/array-filter.php +++ /dev/null @@ -1,37 +0,0 @@ - $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 deleted file mode 100644 index 2275170d0d..0000000000 --- a/tests/PHPStan/Analyser/data/array-flip.php +++ /dev/null @@ -1,43 +0,0 @@ -', $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-is-list-type-specifying.php b/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php deleted file mode 100644 index d3779c9f66..0000000000 --- a/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php +++ /dev/null @@ -1,35 +0,0 @@ -', $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 deleted file mode 100644 index 7845d56c8a..0000000000 --- a/tests/PHPStan/Analyser/data/array-map.php +++ /dev/null @@ -1,62 +0,0 @@ - $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-pointer-functions.php b/tests/PHPStan/Analyser/data/array-pointer-functions.php index 6bae9731a0..60786d435c 100644 --- a/tests/PHPStan/Analyser/data/array-pointer-functions.php +++ b/tests/PHPStan/Analyser/data/array-pointer-functions.php @@ -2,6 +2,8 @@ namespace ResetDynamicReturnTypeExtension; +use function PHPStan\Testing\assertType; + class Foo { @@ -16,6 +18,12 @@ public function doFoo(array $generalArray, $somethingElse) 'a' => 1, 'b' => 2, ]; + /** @var array{a?: 0, b: 1, c: 2} $constantArrayOptionalKeys1 */ + $constantArrayOptionalKeys1 = []; + /** @var array{a: 0, b?: 1, c: 2} $constantArrayOptionalKeys2 */ + $constantArrayOptionalKeys2 = []; + /** @var array{a: 0, b: 1, c?: 2} $constantArrayOptionalKeys3 */ + $constantArrayOptionalKeys3 = []; $conditionalArray = ['foo', 'bar']; if (doFoo()) { diff --git a/tests/PHPStan/Analyser/data/array-slice.php b/tests/PHPStan/Analyser/data/array-slice.php deleted file mode 100644 index 291ffbdb9f..0000000000 --- a/tests/PHPStan/Analyser/data/array-slice.php +++ /dev/null @@ -1,38 +0,0 @@ - $arr1 - * @param array $arr2 - */ - public function preserveTypes(array $arr1, array $arr2): void - { - assertType('array', array_slice($arr1, 1, 2)); - assertType('array', array_slice($arr1, 1, 2, true)); - assertType('array', array_slice($arr2, 1, 2)); - assertType('array', array_slice($arr2, 1, 2, true)); - } - -} diff --git a/tests/PHPStan/Analyser/data/array-spread.php b/tests/PHPStan/Analyser/data/array-spread.php index 5bd12fdc38..f752a03270 100644 --- a/tests/PHPStan/Analyser/data/array-spread.php +++ b/tests/PHPStan/Analyser/data/array-spread.php @@ -1,4 +1,4 @@ -= 7.4 + $floatList - */ -function foo3($floatList) -{ - $sum = array_sum($floatList); - assertType('float', $sum); -} - -/** - * @param mixed[] $list - */ -function foo4($list) -{ - $sum = array_sum($list); - assertType('float|int', $sum); -} - -/** - * @param string[] $list - */ -function foo5($list) -{ - $sum = array_sum($list); - assertType('float|int', $sum); -} diff --git a/tests/PHPStan/Analyser/data/array-union.php b/tests/PHPStan/Analyser/data/array-union.php new file mode 100644 index 0000000000..dd70c5e642 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-union.php @@ -0,0 +1,24 @@ + 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 deleted file mode 100644 index ce73048a46..0000000000 --- a/tests/PHPStan/Analyser/data/array_map_multiple.php +++ /dev/null @@ -1,36 +0,0 @@ - $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-functions-inside.php b/tests/PHPStan/Analyser/data/arrow-functions-inside.php index 269e2bf8e7..258b951b63 100644 --- a/tests/PHPStan/Analyser/data/arrow-functions-inside.php +++ b/tests/PHPStan/Analyser/data/arrow-functions-inside.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 +doFoo($x)) { + assertType('int', $x); + } else { + assertType('mixed~int', $x); + } +}; + + +function (Bar $b, $x): void { + if ($b->doFoo($x)) { + assertType('int', $x); + } else { + assertType('mixed~int', $x); + } +}; diff --git a/tests/PHPStan/Analyser/data/assert.stub b/tests/PHPStan/Analyser/data/assert.stub new file mode 100644 index 0000000000..eeb68738cb --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert.stub @@ -0,0 +1,16 @@ + + */ +abstract class SimpleEntity +{ + /** + * @param SimpleTable $table + */ + public function __construct(protected readonly SimpleTable $table) + { + } +} + +/** + * @template-covariant E of SimpleEntity + */ +class SimpleTable +{ + /** + * @template ENTITY of SimpleEntity + * + * @param class-string $className + * + * @return SimpleTable + */ + public static function table(string $className, string $name): SimpleTable + { + return new SimpleTable($className, $name); + } + + /** + * @param class-string $className + */ + private function __construct(readonly string $className, readonly string $table) + { + } +} + +/** + * @template-extends SimpleEntity + */ +class TestEntity extends SimpleEntity +{ + public function __construct() + { + $table = SimpleTable::table(TestEntity::class, 'testentity'); + parent::__construct($table); + } +} + + +/** + * @template-extends SimpleEntity + */ +class AnotherEntity extends SimpleEntity +{ + public function __construct() + { + $table = SimpleTable::table(AnotherEntity::class, 'anotherentity'); + parent::__construct($table); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10086.php b/tests/PHPStan/Analyser/data/bug-10086.php new file mode 100644 index 0000000000..5b1fc7c235 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10086.php @@ -0,0 +1,12 @@ + +match($_GET['x']) { + 'x' => 'y', + default => 'z', +} +)(); + +define('x', $a); diff --git a/tests/PHPStan/Analyser/data/bug-10147.php b/tests/PHPStan/Analyser/data/bug-10147.php new file mode 100644 index 0000000000..8798de28e7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10147.php @@ -0,0 +1,13 @@ +busy); + assertType('bool', $b->busy2); +}; + +class ModelWithoutAllowDynamicProperties +{ + +} + +/** + * @property-read bool $busy + * @phpstan-require-extends ModelWithoutAllowDynamicProperties + */ +interface BatchAwareWithoutAllowDynamicProperties +{ + +} + +function (BatchAwareWithoutAllowDynamicProperties $b): void +{ + $result = $b->busy; // @phpstan-ignore-line + + assertType('*ERROR*', $result); +}; diff --git a/tests/PHPStan/Analyser/data/bug-10358.php b/tests/PHPStan/Analyser/data/bug-10358.php new file mode 100644 index 0000000000..fc8a94f01c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10358.php @@ -0,0 +1,8 @@ + + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-10538.php b/tests/PHPStan/Analyser/data/bug-10538.php new file mode 100644 index 0000000000..24fc1f1be2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10538.php @@ -0,0 +1,44 @@ + 1, + 'BY' => 2, + 'BE' => 3, + 'BB' => 4, + 'HB' => 5, + 'HH' => 6, + 'HE' => 7, + 'MV' => 8, + 'NI' => 9, + 'NW' => 10, + 'RP' => 11, + 'SL' => 12, + 'ST' => 13, + 'SN' => 14, + 'SH' => 15, + 'TH' => 16, + ]; + + protected static function test(): void + { + for ($i = 0; $i < 10; $i++) { + foreach (self::CHANGESET as $stateCode => $changesets) { + $stateId = self::STATES[$stateCode]; + foreach ($changesets as $changeset) { + echo sprintf( + '%s %s %s %s', + $changeset['new']['Gemarkung'], + $changeset['old']['Gemeinde'], + $changeset['old']['Gemarkung'], + $stateId + ); + } + } + } + } + + protected const CHANGESET = ['BB' => [['old' => ['Gemeinde' => 'Alt Zauche - Wußmerk', 'Gemarkung' => 'Alt Zauche'],'new' => ['Gemeinde' => 'Alt Zauche - Wußwerk', 'Gemarkung' => 'Alt Zauche'],],['old' => ['Gemeinde' => 'Alt Zauche - Wußmerk', 'Gemarkung' => 'Wußwerk'],'new' => ['Gemeinde' => 'Alt Zauche - Wußwerk', 'Gemarkung' => 'Wußwerk'],],['old' => ['Gemeinde' => 'Doberlug - Kirchhain', 'Gemarkung' => 'Doberlug-Kirchhainrchhain'],'new' => ['Gemeinde' => 'Doberlug - Kirchhain', 'Gemarkung' => 'Doberlug-Kirchhain'],],],'BE' => [['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Charlottenburg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Charlottenburg'],],['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Grunewald-Forst'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Grunewald-Forst'],],['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Schmargendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schmargendorf'],],['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Wilmersdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wilmersdorf'],],['old' => ['Gemeindeschluessel' => '11000002', 'Gemeinde' => 'Friedrichshain-Kreuzberg', 'Gemarkung' => 'Friedrichshain'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Friedrichshain'],],['old' => ['Gemeindeschluessel' => '11000002', 'Gemeinde' => 'Friedrichshain-Kreuzberg', 'Gemarkung' => 'Kreuzberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kreuzberg'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Falkenberg Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Falkenberg Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gut'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Hohenschönhausen'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Hohenschönhausen'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Lichtenberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lichtenberg'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Malchow Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Malchow Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Malchow Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Malchow Gut'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Wartenberg Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wartenberg Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Wartenberg Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wartenberg Gut'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Weißensee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Weißensee'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Ahrensfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Ahrensfelde'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Biesdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Biesdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Dahlwitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Dahlwitz'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Falkenberg Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Falkenberg Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gut'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Friedrichsfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Friedrichsfelde'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Hellersdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Hellersdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Kaulsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kaulsdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Köpenick'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Köpenick'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Mahlsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Mahlsdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Marzahn'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Marzahn'],],['old' => ['Gemeindeschluessel' => '11000001', 'Gemeinde' => 'Mitte', 'Gemarkung' => 'Mitte'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Mitte'],],['old' => ['Gemeindeschluessel' => '11000001', 'Gemeinde' => 'Mitte', 'Gemarkung' => 'Tiergarten'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tiergarten'],],['old' => ['Gemeindeschluessel' => '11000001', 'Gemeinde' => 'Mitte', 'Gemarkung' => 'Wedding'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wedding'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Britz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Britz'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Buckow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Buckow'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Neukölln'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Neukölln'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Rudow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Rudow'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow 01'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow 01'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow 02'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow 02'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow 03'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow 03'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Prenzlauer Berg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Prenzlauer Berg'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Weißensee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Weißensee'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Weißensee 01'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Weißensee 01'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Frohnau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Frohnau'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Heiligensee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Heiligensee'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Hermsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Hermsdorf'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Lübars'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lübars'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Reinickendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Reinickendorf'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Schulzendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schulzendorf'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Tegel-Forst'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tegel-Forst'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Tegel-Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tegel-Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Tegel-Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tegel-Gut'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Valentinswerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Valentinswerder'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Wilhelmsruh'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wilhelmsruh'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Wittenau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wittenau'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Eiswerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Eiswerder'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Gatow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Gatow'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Gewehrplan u. Pulverfabrik'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Gewehrplan u. Pulverfabrik'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Groß-Glienicke'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Groß-Glienicke'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Haselhorst'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Haselhorst'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Heerstraße'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Heerstraße'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Kladow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kladow'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Klosterfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Klosterfelde'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Pichelsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pichelsdorf'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Pichelswerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pichelswerder'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Seeburg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Seeburg'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Spandau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Spandau'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Staaken'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Staaken'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Teufelsbruch'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Teufelsbruch'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Tiefwerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tiefwerder'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Zitadelle'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Zitadelle'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Dahlem'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Dahlem'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Düppel'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Düppel'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Lankwitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lankwitz'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Lichterfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lichterfelde'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Nikolassee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Nikolassee'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Schwanenwerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schwanenwerder'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Steglitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Steglitz'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Wannsee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wannsee'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Zehlendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Zehlendorf'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Friedenau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Friedenau'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Lichtenrade'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lichtenrade'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Mariendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Mariendorf'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Marienfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Marienfelde'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Schöneberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schöneberg'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Tempelhof'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tempelhof'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Bohnsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Bohnsdorf'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Britz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Britz'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Buckow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Buckow'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Fahlenberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Fahlenberg'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Glienicke'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Glienicke'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Grünau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Grünau'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Johannisthal'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Johannisthal'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Kanne'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kanne'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Köpenick'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Köpenick'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Neukölln'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Neukölln'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Oberschöneweide'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Oberschöneweide'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Rudow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Rudow'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Schmöckwitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schmöckwitz'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Treptow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Treptow'],],],'HB' => [['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt1'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt 1'],],['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt3'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt 3'],],['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt4'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt 4'],],['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Überseehafen', 'Gemarkungsnummer' => '040008'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Überseehafen', 'Gemarkungsnummer' => '040009'],],],'HE' => [['old' => ['Gemeindeschluessel' => '06635005', 'Gemeinde' => 'Bromskirchen', 'Gemarkung' => 'Bromskirchen'],'new' => ['Gemeindeschluessel' => '06635001', 'Gemeinde' => 'Allendorf (Eder)', 'Gemarkung' => 'Bromskirchen'],],['old' => ['Gemeindeschluessel' => '06635005', 'Gemeinde' => 'Bromskirchen', 'Gemarkung' => 'Somplar'],'new' => ['Gemeindeschluessel' => '06635001', 'Gemeinde' => 'Allendorf (Eder)', 'Gemarkung' => 'Somplar'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Gelnhausen'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Gelnhausen'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Hailer'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Hailer'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Haitz'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Haitz'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Höchst'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Höchst'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Meerholz'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Meerholz'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Roth'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Roth'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Ahlbach'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Ahlbach'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Dietkirchen'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Dietkirchen'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Eschhofen'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Eschhofen'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Limburg'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Limburg'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Lindenholzhausen'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Lindenholzhausen'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Linter'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Linter'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Offheim'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Offheim'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Staffel'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Staffel'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Arnoldshain'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Arnoldshain'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Brombach'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Brombach'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Dorfweil'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Dorfweil'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Hunoldstal'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Hunoldstal'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Niederreifenberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Niederreifenberg'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Oberreifenberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Oberreifenberg'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Schmitten'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Schmitten'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Seelenberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Seelenberg'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Treisberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Treisberg'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Alraft'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Alraft'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Dehringhausen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Dehringhausen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Freienhagen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Freienhagen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Höringhausen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Höringhausen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Netze'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Netze'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Nieder-Werbe'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Nieder-Werbe'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Ober-Werbe'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Ober-Werbe'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Oberwerba'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Oberwerba'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Sachsenhausen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Sachsenhausen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Waldeck'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Waldeck'],],],'MV' => [['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Buschmühlen'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Buschmühlen'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Malpendorf'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Malpendorf'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Neubukow'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Neubukow'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Panzow'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Panzow'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Spriehusen'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Spriehusen'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Helmstorf'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Helmstorf'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Klein Tessin'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Klein Tessin'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Tessin'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Tessin'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Vilz'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Vilz'],],],'NI' => [['old' => ['Gemeindeschluessel' => '03153006', 'Gemeinde' => 'Hahausen', 'Gemarkung' => 'Hahausen'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Hahausen'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Astfeld'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Astfeld'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Bredelem'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Bredelem'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Langelsheim'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Langelsheim'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lautenthal'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lautenthal'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen-Mispliet'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen-Mispliet'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Lutter am Barenberge'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lutter am Barenberge'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Lutter-Westerberg'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lutter-Westerberg'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Nauen'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Nauen'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Ostlutter'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Ostlutter'],],['old' => ['Gemeindeschluessel' => '03153014', 'Gemeinde' => 'Wallmoden', 'Gemarkung' => 'Alt Wallmoden'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Alt Wallmoden'],],['old' => ['Gemeindeschluessel' => '03153014', 'Gemeinde' => 'Wallmoden', 'Gemarkung' => 'Bodenstein'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Bodenstein'],],['old' => ['Gemeindeschluessel' => '03153014', 'Gemeinde' => 'Wallmoden', 'Gemarkung' => 'Neuwallmoden'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Neuwallmoden'],],],'ST' => [['old' => ['Gemeinde' => 'Bad Dürrenberg, Stadt', 'Gemarkung' => 'Nempitz'],'new' => ['Gemeinde' => 'Bad Dürrenberg, Solestadt', 'Gemarkung' => 'Nempitz'],],['old' => ['Gemeinde' => 'Bad Dürrenberg, Stadt', 'Gemarkung' => 'Oebles-Schlechtewitz'],'new' => ['Gemeinde' => 'Bad Dürrenberg, Solestadt', 'Gemarkung' => 'Oebles-Schlechtewitz'],],['old' => ['Gemeinde' => 'Bad Dürrenberg, Stadt', 'Gemarkung' => 'Tollwitz'],'new' => ['Gemeinde' => 'Bad Dürrenberg, Solestadt', 'Gemarkung' => 'Tollwitz'],],['old' => ['Gemeinde' => 'Harsleben', 'Gemarkung' => 'Harsleben'],'new' => ['Gemeinde' => 'Harsleben / Harschlewe', 'Gemarkung' => 'Harsleben'],],['old' => ['Gemeindeschluessel' => '15083575', 'Gemeinde' => 'Westheide', 'Gemarkung' => 'Born'],'new' => ['Gemeindeschluessel' => '15083557', 'Gemeinde' => 'Westheide', 'Gemarkung' => 'Born'],],],'TH' => [['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Berteroda'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Berteroda'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Eisenach'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Eisenach'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Frohnishof'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Frohnishof'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Göringen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Göringen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hörschel'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hörschel'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hötzelsroda'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hötzelsroda'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Madelungen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Madelungen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neuenhof'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neuenhof'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neukirchen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neukirchen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stedtfeld'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stedtfeld'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stockhausen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stockhausen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stregda'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stregda'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Wartha'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Wartha'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Bliederstedt'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Bliederstedt'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Feldengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Feldengel'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Großenehrich'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Großenehrich'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Holzengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Holzengel'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Kirchengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Kirchengel'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Niederspier'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Niederspier'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Otterstedt'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Otterstedt'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Rohnstedt'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Rohnstedt'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Wenigenehrich'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Wenigenehrich'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Westerengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Westerengel'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Etterwinden'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Etterwinden'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Gräfen-Nitzendorf'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Gräfen-Nitzendorf'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Gumpelstadt'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Gumpelstadt'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Kupfersuhl'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Kupfersuhl'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Möhra'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Möhra'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Neuendorf'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Neuendorf'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Wackenhof'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Wackenhof'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Waldfisch'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Waldfisch'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Witzelroda'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Witzelroda'],],['old' => ['Gemeinde' => 'Roßleben-Wiehe, Stadt', 'Gemarkung' => 'Bottendorf'],'new' => ['Gemeinde' => 'Roßleben-Wiehe', 'Gemarkung' => 'Bottendorf'],],['old' => ['Gemeinde' => 'Wolferschwenda', 'Gemarkung' => 'Wolferschwenda'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Wolferschwenda'],],],]; +} diff --git a/tests/PHPStan/Analyser/data/bug-10772.php b/tests/PHPStan/Analyser/data/bug-10772.php new file mode 100644 index 0000000000..76ea079046 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10772.php @@ -0,0 +1,9393 @@ +getNameInLanguage(LanguageAlpha2::English); + + // part B, all right + // $value->toCountryAlpha2()->getNameInLanguage(LanguageAlpha2::English); +} + +enum CountryAlpha3: string +{ + case Afghanistan = 'AFG'; + case Aland_Islands = 'ALA'; + case Albania = 'ALB'; + case Algeria = 'DZA'; + case American_Samoa = 'ASM'; + case Andorra = 'AND'; + case Angola = 'AGO'; + case Anguilla = 'AIA'; + case Antarctica = 'ATA'; + case Antigua_and_Barbuda = 'ATG'; + case Argentina = 'ARG'; + case Armenia = 'ARM'; + case Aruba = 'ABW'; + case Australia = 'AUS'; + case Austria = 'AUT'; + case Azerbaijan = 'AZE'; + case Bahamas = 'BHS'; + case Bahrain = 'BHR'; + case Bangladesh = 'BGD'; + case Barbados = 'BRB'; + case Belarus = 'BLR'; + case Belgium = 'BEL'; + case Belize = 'BLZ'; + case Benin = 'BEN'; + case Bermuda = 'BMU'; + case Bhutan = 'BTN'; + case Bolivia = 'BOL'; + case Bonaire_Sint_Eustatius_and_Saba = 'BES'; + case Bosnia_and_Herzegovina = 'BIH'; + case Botswana = 'BWA'; + case Bouvet_Island = 'BVT'; + case Brazil = 'BRA'; + case British_Indian_Ocean_Territory = 'IOT'; + case Brunei_Darussalam = 'BRN'; + case Bulgaria = 'BGR'; + case Burkina_Faso = 'BFA'; + case Burundi = 'BDI'; + case Cabo_Verde = 'CPV'; + case Cambodia = 'KHM'; + case Cameroon = 'CMR'; + case Canada = 'CAN'; + case Cayman_Islands = 'CYM'; + case Central_African_Republic = 'CAF'; + case Chad = 'TCD'; + case Chile = 'CHL'; + case China = 'CHN'; + case Christmas_Island = 'CXR'; + case Cocos_Islands = 'CCK'; + case Colombia = 'COL'; + case Comoros = 'COM'; + case Congo = 'COG'; + case Congo_Democratic_Republic = 'COD'; + case Cook_Islands = 'COK'; + case Costa_Rica = 'CRI'; + case Cote_d_Ivoire = 'CIV'; + case Croatia = 'HRV'; + case Cuba = 'CUB'; + case Curacao = 'CUW'; + case Cyprus = 'CYP'; + case Czechia = 'CZE'; + case Denmark = 'DNK'; + case Djibouti = 'DJI'; + case Dominica = 'DMA'; + case Dominican_Republic = 'DOM'; + case Ecuador = 'ECU'; + case Egypt = 'EGY'; + case El_Salvador = 'SLV'; + case Equatorial_Guinea = 'GNQ'; + case Eritrea = 'ERI'; + case Estonia = 'EST'; + case Eswatini = 'SWZ'; + case Ethiopia = 'ETH'; + case Falkland_Islands = 'FLK'; + case Faroe_Islands = 'FRO'; + case Fiji = 'FJI'; + case Finland = 'FIN'; + case France = 'FRA'; + case French_Guiana = 'GUF'; + case French_Polynesia = 'PYF'; + case French_Southern_Territories = 'ATF'; + case Gabon = 'GAB'; + case Gambia = 'GMB'; + case Georgia = 'GEO'; + case Germany = 'DEU'; + case Ghana = 'GHA'; + case Gibraltar = 'GIB'; + case Greece = 'GRC'; + case Greenland = 'GRL'; + case Grenada = 'GRD'; + case Guadeloupe = 'GLP'; + case Guam = 'GUM'; + case Guatemala = 'GTM'; + case Guernsey = 'GGY'; + case Guinea = 'GIN'; + case Guinea_Bissau = 'GNB'; + case Guyana = 'GUY'; + case Haiti = 'HTI'; + case Heard_Island_and_McDonald_Islands = 'HMD'; + case Holy_See = 'VAT'; + case Honduras = 'HND'; + case Hong_Kong = 'HKG'; + case Hungary = 'HUN'; + case Iceland = 'ISL'; + case India = 'IND'; + case Indonesia = 'IDN'; + case Iran = 'IRN'; + case Iraq = 'IRQ'; + case Ireland = 'IRL'; + case Isle_of_Man = 'IMN'; + case Israel = 'ISR'; + case Italy = 'ITA'; + case Jamaica = 'JAM'; + case Japan = 'JPN'; + case Jersey = 'JEY'; + case Jordan = 'JOR'; + case Kazakhstan = 'KAZ'; + case Kenya = 'KEN'; + case Kiribati = 'KIR'; + case Korea_Democratic_Peoples_Republic = 'PRK'; + case Korea_Republic = 'KOR'; + case Kuwait = 'KWT'; + case Kyrgyzstan = 'KGZ'; + case Lao_Peoples_Democratic_Republic = 'LAO'; + case Latvia = 'LVA'; + case Lebanon = 'LBN'; + case Lesotho = 'LSO'; + case Liberia = 'LBR'; + case Libya = 'LBY'; + case Liechtenstein = 'LIE'; + case Lithuania = 'LTU'; + case Luxembourg = 'LUX'; + case Macao = 'MAC'; + case Madagascar = 'MDG'; + case Malawi = 'MWI'; + case Malaysia = 'MYS'; + case Maldives = 'MDV'; + case Mali = 'MLI'; + case Malta = 'MLT'; + case Marshall_Islands = 'MHL'; + case Martinique = 'MTQ'; + case Mauritania = 'MRT'; + case Mauritius = 'MUS'; + case Mayotte = 'MYT'; + case Mexico = 'MEX'; + case Micronesia = 'FSM'; + case Moldova = 'MDA'; + case Monaco = 'MCO'; + case Mongolia = 'MNG'; + case Montenegro = 'MNE'; + case Montserrat = 'MSR'; + case Morocco = 'MAR'; + case Mozambique = 'MOZ'; + case Myanmar = 'MMR'; + case Namibia = 'NAM'; + case Nauru = 'NRU'; + case Nepal = 'NPL'; + case Netherlands = 'NLD'; + case New_Caledonia = 'NCL'; + case New_Zealand = 'NZL'; + case Nicaragua = 'NIC'; + case Niger = 'NER'; + case Nigeria = 'NGA'; + case Niue = 'NIU'; + case Norfolk_Island = 'NFK'; + case North_Macedonia = 'MKD'; + case Northern_Mariana_Islands = 'MNP'; + case Norway = 'NOR'; + case Oman = 'OMN'; + case Pakistan = 'PAK'; + case Palau = 'PLW'; + case Palestine = 'PSE'; + case Panama = 'PAN'; + case Papua_New_Guinea = 'PNG'; + case Paraguay = 'PRY'; + case Peru = 'PER'; + case Philippines = 'PHL'; + case Pitcairn = 'PCN'; + case Poland = 'POL'; + case Portugal = 'PRT'; + case Puerto_Rico = 'PRI'; + case Qatar = 'QAT'; + case Reunion = 'REU'; + case Romania = 'ROU'; + case Russian_Federation = 'RUS'; + case Rwanda = 'RWA'; + case Saint_Barthelemy = 'BLM'; + case Saint_Helena_Ascension_Tristan_da_Cunha = 'SHN'; + case Saint_Kitts_and_Nevis = 'KNA'; + case Saint_Lucia = 'LCA'; + case Saint_Martin_French_part = 'MAF'; + case Saint_Pierre_and_Miquelon = 'SPM'; + case Saint_Vincent_and_the_Grenadines = 'VCT'; + case Samoa = 'WSM'; + case San_Marino = 'SMR'; + case Sao_Tome_and_Principe = 'STP'; + case Saudi_Arabia = 'SAU'; + case Senegal = 'SEN'; + case Serbia = 'SRB'; + case Seychelles = 'SYC'; + case Sierra_Leone = 'SLE'; + case Singapore = 'SGP'; + case Sint_Maarten_Dutch_part = 'SXM'; + case Slovakia = 'SVK'; + case Slovenia = 'SVN'; + case Solomon_Islands = 'SLB'; + case Somalia = 'SOM'; + case South_Africa = 'ZAF'; + case South_Georgia_South_Sandwich_Islands = 'SGS'; + case South_Sudan = 'SSD'; + case Spain = 'ESP'; + case Sri_Lanka = 'LKA'; + case Sudan = 'SDN'; + case Suriname = 'SUR'; + case Svalbard_Jan_Mayen = 'SJM'; + case Sweden = 'SWE'; + case Switzerland = 'CHE'; + case Syrian_Arab_Republic = 'SYR'; + case Taiwan_Province_of_China = 'TWN'; + case Tajikistan = 'TJK'; + case Tanzania = 'TZA'; + case Thailand = 'THA'; + case Timor_Leste = 'TLS'; + case Togo = 'TGO'; + case Tokelau = 'TKL'; + case Tonga = 'TON'; + case Trinidad_and_Tobago = 'TTO'; + case Tunisia = 'TUN'; + case Turkey = 'TUR'; + case Turkmenistan = 'TKM'; + case Turks_and_Caicos_Islands = 'TCA'; + case Tuvalu = 'TUV'; + case Uganda = 'UGA'; + case Ukraine = 'UKR'; + case United_Arab_Emirates = 'ARE'; + case United_Kingdom = 'GBR'; + case United_States_Outlying_Islands = 'UMI'; + case United_States_of_America = 'USA'; + case Uruguay = 'URY'; + case Uzbekistan = 'UZB'; + case Vanuatu = 'VUT'; + case Venezuela = 'VEN'; + case Viet_Nam = 'VNM'; + case Virgin_Islands_British = 'VGB'; + case Virgin_Islands_U_S = 'VIR'; + case Wallis_and_Futuna = 'WLF'; + case Western_Sahara = 'ESH'; + case Yemen = 'YEM'; + case Zambia = 'ZMB'; + case Zimbabwe = 'ZWE'; + + + + public function getNameInLanguage(LanguageAlpha2|LanguageAlpha3Terminology|LanguageAlpha3Bibliographic|LanguageAlpha3Extensive $language): ?string + { + return $this->toCountryAlpha2()->getNameInLanguage($language); + } + + public function toCountryAlpha2(): mixed + { + return BackedEnum::fromName('x', $this->name); + } +} + +enum LanguageAlpha2: string +{ + case Abkhazian = 'ab'; + case Afar = 'aa'; + case Afrikaans = 'af'; + case Akan = 'ak'; + case Albanian = 'sq'; + case Amharic = 'am'; + case Arabic = 'ar'; + case Aragonese = 'an'; + case Armenian = 'hy'; + case Assamese = 'as'; + case Avaric = 'av'; + case Avestan = 'ae'; + case Aymara = 'ay'; + case Azerbaijani = 'az'; + case Bambara = 'bm'; + case Bashkir = 'ba'; + case Basque = 'eu'; + case Belarusian = 'be'; + case Bengali = 'bn'; + case Bihari_languages = 'bh'; + case Bislama = 'bi'; + case Bokmal_Norwegian_Norwegian_Bokmal = 'nb'; + case Bosnian = 'bs'; + case Breton = 'br'; + case Bulgarian = 'bg'; + case Burmese = 'my'; + case Catalan_Valencian = 'ca'; + case Central_Khmer = 'km'; + case Chamorro = 'ch'; + case Chechen = 'ce'; + case Chichewa_Chewa_Nyanja = 'ny'; + case Chinese = 'zh'; + case Church_Slavic_Old_Slavonic_Church_Slavonic_Old_Bulgarian_Old_Church_Slavonic = 'cu'; + case Chuvash = 'cv'; + case Cornish = 'kw'; + case Corsican = 'co'; + case Cree = 'cr'; + case Croatian = 'hr'; + case Czech = 'cs'; + case Danish = 'da'; + case Divehi_Dhivehi_Maldivian = 'dv'; + case Dutch_Flemish = 'nl'; + case Dzongkha = 'dz'; + case English = 'en'; + case Esperanto = 'eo'; + case Estonian = 'et'; + case Ewe = 'ee'; + case Faroese = 'fo'; + case Fijian = 'fj'; + case Finnish = 'fi'; + case French = 'fr'; + case Fulah = 'ff'; + case Gaelic_Scottish_Gaelic = 'gd'; + case Galician = 'gl'; + case Ganda = 'lg'; + case Georgian = 'ka'; + case German = 'de'; + case Greek_Modern_1453 = 'el'; + case Guarani = 'gn'; + case Gujarati = 'gu'; + case Haitian_Haitian_Creole = 'ht'; + case Hausa = 'ha'; + case Hebrew = 'he'; + case Herero = 'hz'; + case Hindi = 'hi'; + case Hiri_Motu = 'ho'; + case Hungarian = 'hu'; + case Icelandic = 'is'; + case Ido = 'io'; + case Igbo = 'ig'; + case Indonesian = 'id'; + case Interlingua_International_Auxiliary_Language_Association = 'ia'; + case Interlingue_Occidental = 'ie'; + case Inuktitut = 'iu'; + case Inupiaq = 'ik'; + case Irish = 'ga'; + case Italian = 'it'; + case Japanese = 'ja'; + case Javanese = 'jv'; + case Kalaallisut_Greenlandic = 'kl'; + case Kannada = 'kn'; + case Kanuri = 'kr'; + case Kashmiri = 'ks'; + case Kazakh = 'kk'; + case Kikuyu_Gikuyu = 'ki'; + case Kinyarwanda = 'rw'; + case Kirghiz_Kyrgyz = 'ky'; + case Komi = 'kv'; + case Kongo = 'kg'; + case Korean = 'ko'; + case Kuanyama_Kwanyama = 'kj'; + case Kurdish = 'ku'; + case Lao = 'lo'; + case Latin = 'la'; + case Latvian = 'lv'; + case Limburgan_Limburger_Limburgish = 'li'; + case Lingala = 'ln'; + case Lithuanian = 'lt'; + case Luba_Katanga = 'lu'; + case Luxembourgish_Letzeburgesch = 'lb'; + case Macedonian = 'mk'; + case Malagasy = 'mg'; + case Malay = 'ms'; + case Malayalam = 'ml'; + case Maltese = 'mt'; + case Manx = 'gv'; + case Maori = 'mi'; + case Marathi = 'mr'; + case Marshallese = 'mh'; + case Mongolian = 'mn'; + case Nauru = 'na'; + case Navajo_Navaho = 'nv'; + case Ndebele_North_North_Ndebele = 'nd'; + case Ndebele_South_South_Ndebele = 'nr'; + case Ndonga = 'ng'; + case Nepali = 'ne'; + case Northern_Sami = 'se'; + case Norwegian = 'no'; + case Norwegian_Nynorsk_Nynorsk_Norwegian = 'nn'; + case Occitan_post_1500 = 'oc'; + case Ojibwa = 'oj'; + case Oriya = 'or'; + case Oromo = 'om'; + case Ossetian_Ossetic = 'os'; + case Pali = 'pi'; + case Panjabi_Punjabi = 'pa'; + case Persian = 'fa'; + case Polish = 'pl'; + case Portuguese = 'pt'; + case Pushto_Pashto = 'ps'; + case Quechua = 'qu'; + case Romanian_Moldavian_Moldovan = 'ro'; + case Romansh = 'rm'; + case Rundi = 'rn'; + case Russian = 'ru'; + case Samoan = 'sm'; + case Sango = 'sg'; + case Sanskrit = 'sa'; + case Sardinian = 'sc'; + case Serbian = 'sr'; + case Shona = 'sn'; + case Sichuan_Yi_Nuosu = 'ii'; + case Sindhi = 'sd'; + case Sinhala_Sinhalese = 'si'; + case Slovak = 'sk'; + case Slovenian = 'sl'; + case Somali = 'so'; + case Sotho_Southern = 'st'; + case Spanish_Castilian = 'es'; + case Sundanese = 'su'; + case Swahili = 'sw'; + case Swati = 'ss'; + case Swedish = 'sv'; + case Tagalog = 'tl'; + case Tahitian = 'ty'; + case Tajik = 'tg'; + case Tamil = 'ta'; + case Tatar = 'tt'; + case Telugu = 'te'; + case Thai = 'th'; + case Tibetan = 'bo'; + case Tigrinya = 'ti'; + case Tonga_Tonga_Islands = 'to'; + case Tsonga = 'ts'; + case Tswana = 'tn'; + case Turkish = 'tr'; + case Turkmen = 'tk'; + case Twi = 'tw'; + case Uighur_Uyghur = 'ug'; + case Ukrainian = 'uk'; + case Urdu = 'ur'; + case Uzbek = 'uz'; + case Venda = 've'; + case Vietnamese = 'vi'; + case Volapuk = 'vo'; + case Walloon = 'wa'; + case Welsh = 'cy'; + case Western_Frisian = 'fy'; + case Wolof = 'wo'; + case Xhosa = 'xh'; + case Yiddish = 'yi'; + case Yoruba = 'yo'; + case Zhuang_Chuang = 'za'; + case Zulu = 'zu'; + + /** @deprecated Will be removed in v4. Please use ::getNameInLanguage(LanguageAlpha2::English) instead */ + public function toLanguageName(): LanguageName + { + return BackedEnum::fromName(LanguageName::class, $this->name); + } +} + +enum LanguageAlpha3Terminology: string +{ + case Abkhazian = 'abk'; + case Achinese = 'ace'; + case Acoli = 'ach'; + case Adangme = 'ada'; + case Adyghe_Adygei = 'ady'; + case Afar = 'aar'; + case Afrihili = 'afh'; + case Afrikaans = 'afr'; + case Afro_Asiatic_languages = 'afa'; + case Ainu = 'ain'; + case Akan = 'aka'; + case Akkadian = 'akk'; + case Albanian = 'sqi'; + case Aleut = 'ale'; + case Algonquian_languages = 'alg'; + case Altaic_languages = 'tut'; + case Amharic = 'amh'; + case Angika = 'anp'; + case Apache_languages = 'apa'; + case Arabic = 'ara'; + case Aragonese = 'arg'; + case Arapaho = 'arp'; + case Arawak = 'arw'; + case Armenian = 'hye'; + case Aromanian_Arumanian_Macedo_Romanian = 'rup'; + case Artificial_languages = 'art'; + case Assamese = 'asm'; + case Asturian_Bable_Leonese_Asturleonese = 'ast'; + case Athapascan_languages = 'ath'; + case Australian_languages = 'aus'; + case Austronesian_languages = 'map'; + case Avaric = 'ava'; + case Avestan = 'ave'; + case Awadhi = 'awa'; + case Aymara = 'aym'; + case Azerbaijani = 'aze'; + case Balinese = 'ban'; + case Baltic_languages = 'bat'; + case Baluchi = 'bal'; + case Bambara = 'bam'; + case Bamileke_languages = 'bai'; + case Banda_languages = 'bad'; + case Bantu_languages = 'bnt'; + case Basa = 'bas'; + case Bashkir = 'bak'; + case Basque = 'eus'; + case Batak_languages = 'btk'; + case Beja_Bedawiyet = 'bej'; + case Belarusian = 'bel'; + case Bemba = 'bem'; + case Bengali = 'ben'; + case Berber_languages = 'ber'; + case Bhojpuri = 'bho'; + case Bihari_languages = 'bih'; + case Bikol = 'bik'; + case Bini_Edo = 'bin'; + case Bislama = 'bis'; + case Blin_Bilin = 'byn'; + case Blissymbols_Blissymbolics_Bliss = 'zbl'; + case Bokmal_Norwegian_Norwegian_Bokmal = 'nob'; + case Bosnian = 'bos'; + case Braj = 'bra'; + case Breton = 'bre'; + case Buginese = 'bug'; + case Bulgarian = 'bul'; + case Buriat = 'bua'; + case Burmese = 'mya'; + case Caddo = 'cad'; + case Catalan_Valencian = 'cat'; + case Caucasian_languages = 'cau'; + case Cebuano = 'ceb'; + case Celtic_languages = 'cel'; + case Central_American_Indian_languages = 'cai'; + case Central_Khmer = 'khm'; + case Chagatai = 'chg'; + case Chamic_languages = 'cmc'; + case Chamorro = 'cha'; + case Chechen = 'che'; + case Cherokee = 'chr'; + case Cheyenne = 'chy'; + case Chibcha = 'chb'; + case Chichewa_Chewa_Nyanja = 'nya'; + case Chinese = 'zho'; + case Chinook_jargon = 'chn'; + case Chipewyan_Dene_Suline = 'chp'; + case Choctaw = 'cho'; + case Church_Slavic_Old_Slavonic_Church_Slavonic_Old_Bulgarian_Old_Church_Slavonic = 'chu'; + case Chuukese = 'chk'; + case Chuvash = 'chv'; + case Classical_Newari_Old_Newari_Classical_Nepal_Bhasa = 'nwc'; + case Classical_Syriac = 'syc'; + case Coptic = 'cop'; + case Cornish = 'cor'; + case Corsican = 'cos'; + case Cree = 'cre'; + case Creek = 'mus'; + case Creoles_and_pidgins = 'crp'; + case Creoles_and_pidgins_English_based = 'cpe'; + case Creoles_and_pidgins_French_based = 'cpf'; + case Creoles_and_pidgins_Portuguese_based = 'cpp'; + case Crimean_Tatar_Crimean_Turkish = 'crh'; + case Croatian = 'hrv'; + case Cushitic_languages = 'cus'; + case Czech = 'ces'; + case Dakota = 'dak'; + case Danish = 'dan'; + case Dargwa = 'dar'; + case Delaware = 'del'; + case Dinka = 'din'; + case Divehi_Dhivehi_Maldivian = 'div'; + case Dogri = 'doi'; + case Dogrib = 'dgr'; + case Dravidian_languages = 'dra'; + case Duala = 'dua'; + case Dutch_Flemish = 'nld'; + case Dutch_Middle_ca_1050_1350 = 'dum'; + case Dyula = 'dyu'; + case Dzongkha = 'dzo'; + case Eastern_Frisian = 'frs'; + case Efik = 'efi'; + case Egyptian_Ancient = 'egy'; + case Ekajuk = 'eka'; + case Elamite = 'elx'; + case English = 'eng'; + case English_Middle_1100_1500 = 'enm'; + case English_Old_ca_450_1100 = 'ang'; + case Erzya = 'myv'; + case Esperanto = 'epo'; + case Estonian = 'est'; + case Ewe = 'ewe'; + case Ewondo = 'ewo'; + case Fang = 'fan'; + case Fanti = 'fat'; + case Faroese = 'fao'; + case Fijian = 'fij'; + case Filipino_Pilipino = 'fil'; + case Finnish = 'fin'; + case Finno_Ugrian_languages = 'fiu'; + case Fon = 'fon'; + case French = 'fra'; + case French_Middle_ca_1400_1600 = 'frm'; + case French_Old_842_ca_1400 = 'fro'; + case Friulian = 'fur'; + case Fulah = 'ful'; + case Ga = 'gaa'; + case Gaelic_Scottish_Gaelic = 'gla'; + case Galibi_Carib = 'car'; + case Galician = 'glg'; + case Ganda = 'lug'; + case Gayo = 'gay'; + case Gbaya = 'gba'; + case Geez = 'gez'; + case Georgian = 'kat'; + case German = 'deu'; + case German_Middle_High_ca_1050_1500 = 'gmh'; + case German_Old_High_ca_750_1050 = 'goh'; + case Germanic_languages = 'gem'; + case Gilbertese = 'gil'; + case Gondi = 'gon'; + case Gorontalo = 'gor'; + case Gothic = 'got'; + case Grebo = 'grb'; + case Greek_Ancient_to_1453 = 'grc'; + case Greek_Modern_1453 = 'ell'; + case Guarani = 'grn'; + case Gujarati = 'guj'; + case Gwich_in = 'gwi'; + case Haida = 'hai'; + case Haitian_Haitian_Creole = 'hat'; + case Hausa = 'hau'; + case Hawaiian = 'haw'; + case Hebrew = 'heb'; + case Herero = 'her'; + case Hiligaynon = 'hil'; + case Himachali_languages_Western_Pahari_languages = 'him'; + case Hindi = 'hin'; + case Hiri_Motu = 'hmo'; + case Hittite = 'hit'; + case Hmong_Mong = 'hmn'; + case Hungarian = 'hun'; + case Hupa = 'hup'; + case Iban = 'iba'; + case Icelandic = 'isl'; + case Ido = 'ido'; + case Igbo = 'ibo'; + case Ijo_languages = 'ijo'; + case Iloko = 'ilo'; + case Inari_Sami = 'smn'; + case Indic_languages = 'inc'; + case Indo_European_languages = 'ine'; + case Indonesian = 'ind'; + case Ingush = 'inh'; + case Interlingua_International_Auxiliary_Language_Association = 'ina'; + case Interlingue_Occidental = 'ile'; + case Inuktitut = 'iku'; + case Inupiaq = 'ipk'; + case Iranian_languages = 'ira'; + case Irish = 'gle'; + case Irish_Middle_900_1200 = 'mga'; + case Irish_Old_to_900 = 'sga'; + case Iroquoian_languages = 'iro'; + case Italian = 'ita'; + case Japanese = 'jpn'; + case Javanese = 'jav'; + case Judeo_Arabic = 'jrb'; + case Judeo_Persian = 'jpr'; + case Kabardian = 'kbd'; + case Kabyle = 'kab'; + case Kachin_Jingpho = 'kac'; + case Kalaallisut_Greenlandic = 'kal'; + case Kalmyk_Oirat = 'xal'; + case Kamba = 'kam'; + case Kannada = 'kan'; + case Kanuri = 'kau'; + case Kara_Kalpak = 'kaa'; + case Karachay_Balkar = 'krc'; + case Karelian = 'krl'; + case Karen_languages = 'kar'; + case Kashmiri = 'kas'; + case Kashubian = 'csb'; + case Kawi = 'kaw'; + case Kazakh = 'kaz'; + case Khasi = 'kha'; + case Khoisan_languages = 'khi'; + case Khotanese_Sakan = 'kho'; + case Kikuyu_Gikuyu = 'kik'; + case Kimbundu = 'kmb'; + case Kinyarwanda = 'kin'; + case Kirghiz_Kyrgyz = 'kir'; + case Klingon_tlhIngan_Hol = 'tlh'; + case Komi = 'kom'; + case Kongo = 'kon'; + case Konkani = 'kok'; + case Korean = 'kor'; + case Kosraean = 'kos'; + case Kpelle = 'kpe'; + case Kru_languages = 'kro'; + case Kuanyama_Kwanyama = 'kua'; + case Kumyk = 'kum'; + case Kurdish = 'kur'; + case Kurukh = 'kru'; + case Kutenai = 'kut'; + case Ladino = 'lad'; + case Lahnda = 'lah'; + case Lamba = 'lam'; + case Land_Dayak_languages = 'day'; + case Lao = 'lao'; + case Latin = 'lat'; + case Latvian = 'lav'; + case Lezghian = 'lez'; + case Limburgan_Limburger_Limburgish = 'lim'; + case Lingala = 'lin'; + case Lithuanian = 'lit'; + case Lojban = 'jbo'; + case Low_German_Low_Saxon_German_Low_Saxon_Low = 'nds'; + case Lower_Sorbian = 'dsb'; + case Lozi = 'loz'; + case Luba_Katanga = 'lub'; + case Luba_Lulua = 'lua'; + case Luiseno = 'lui'; + case Lule_Sami = 'smj'; + case Lunda = 'lun'; + case Luo_Kenya_and_Tanzania = 'luo'; + case Lushai = 'lus'; + case Luxembourgish_Letzeburgesch = 'ltz'; + case Macedonian = 'mkd'; + case Madurese = 'mad'; + case Magahi = 'mag'; + case Maithili = 'mai'; + case Makasar = 'mak'; + case Malagasy = 'mlg'; + case Malay = 'msa'; + case Malayalam = 'mal'; + case Maltese = 'mlt'; + case Manchu = 'mnc'; + case Mandar = 'mdr'; + case Mandingo = 'man'; + case Manipuri = 'mni'; + case Manobo_languages = 'mno'; + case Manx = 'glv'; + case Maori = 'mri'; + case Mapudungun_Mapuche = 'arn'; + case Marathi = 'mar'; + case Mari = 'chm'; + case Marshallese = 'mah'; + case Marwari = 'mwr'; + case Masai = 'mas'; + case Mayan_languages = 'myn'; + case Mende = 'men'; + case Mi_kmaq_Micmac = 'mic'; + case Minangkabau = 'min'; + case Mirandese = 'mwl'; + case Mohawk = 'moh'; + case Moksha = 'mdf'; + case Mon_Khmer_languages = 'mkh'; + case Mongo = 'lol'; + case Mongolian = 'mon'; + case Montenegrin = 'cnr'; + case Mossi = 'mos'; + case Multiple_languages = 'mul'; + case Munda_languages = 'mun'; + case N_Ko = 'nqo'; + case Nahuatl_languages = 'nah'; + case Nauru = 'nau'; + case Navajo_Navaho = 'nav'; + case Ndebele_North_North_Ndebele = 'nde'; + case Ndebele_South_South_Ndebele = 'nbl'; + case Ndonga = 'ndo'; + case Neapolitan = 'nap'; + case Nepal_Bhasa_Newari = 'new'; + case Nepali = 'nep'; + case Nias = 'nia'; + case Niger_Kordofanian_languages = 'nic'; + case Nilo_Saharan_languages = 'ssa'; + case Niuean = 'niu'; + case No_linguistic_content_Not_applicable = 'zxx'; + case Nogai = 'nog'; + case Norse_Old = 'non'; + case North_American_Indian_languages = 'nai'; + case Northern_Frisian = 'frr'; + case Northern_Sami = 'sme'; + case Norwegian = 'nor'; + case Norwegian_Nynorsk_Nynorsk_Norwegian = 'nno'; + case Nubian_languages = 'nub'; + case Nyamwezi = 'nym'; + case Nyankole = 'nyn'; + case Nyoro = 'nyo'; + case Nzima = 'nzi'; + case Occitan_post_1500 = 'oci'; + case Official_Aramaic_700_300_BCE_Imperial_Aramaic_700_300_BCE = 'arc'; + case Ojibwa = 'oji'; + case Oriya = 'ori'; + case Oromo = 'orm'; + case Osage = 'osa'; + case Ossetian_Ossetic = 'oss'; + case Otomian_languages = 'oto'; + case Pahlavi = 'pal'; + case Palauan = 'pau'; + case Pali = 'pli'; + case Pampanga_Kapampangan = 'pam'; + case Pangasinan = 'pag'; + case Panjabi_Punjabi = 'pan'; + case Papiamento = 'pap'; + case Papuan_languages = 'paa'; + case Pedi_Sepedi_Northern_Sotho = 'nso'; + case Persian = 'fas'; + case Persian_Old_ca_600_400_B_C = 'peo'; + case Philippine_languages = 'phi'; + case Phoenician = 'phn'; + case Pohnpeian = 'pon'; + case Polish = 'pol'; + case Portuguese = 'por'; + case Prakrit_languages = 'pra'; + case Provencal_Old_to_1500_Occitan_Old_to_1500 = 'pro'; + case Pushto_Pashto = 'pus'; + case Quechua = 'que'; + case Rajasthani = 'raj'; + case Rapanui = 'rap'; + case Rarotongan_Cook_Islands_Maori = 'rar'; + case Romance_languages = 'roa'; + case Romanian_Moldavian_Moldovan = 'ron'; + case Romansh = 'roh'; + case Romany = 'rom'; + case Rundi = 'run'; + case Russian = 'rus'; + case Salishan_languages = 'sal'; + case Samaritan_Aramaic = 'sam'; + case Sami_languages = 'smi'; + case Samoan = 'smo'; + case Sandawe = 'sad'; + case Sango = 'sag'; + case Sanskrit = 'san'; + case Santali = 'sat'; + case Sardinian = 'srd'; + case Sasak = 'sas'; + case Scots = 'sco'; + case Selkup = 'sel'; + case Semitic_languages = 'sem'; + case Serbian = 'srp'; + case Serer = 'srr'; + case Shan = 'shn'; + case Shona = 'sna'; + case Sichuan_Yi_Nuosu = 'iii'; + case Sicilian = 'scn'; + case Sidamo = 'sid'; + case Sign_Languages = 'sgn'; + case Siksika = 'bla'; + case Sindhi = 'snd'; + case Sinhala_Sinhalese = 'sin'; + case Sino_Tibetan_languages = 'sit'; + case Siouan_languages = 'sio'; + case Skolt_Sami = 'sms'; + case Slave_Athapascan = 'den'; + case Slavic_languages = 'sla'; + case Slovak = 'slk'; + case Slovenian = 'slv'; + case Sogdian = 'sog'; + case Somali = 'som'; + case Songhai_languages = 'son'; + case Soninke = 'snk'; + case Sorbian_languages = 'wen'; + case Sotho_Southern = 'sot'; + case South_American_Indian_languages = 'sai'; + case Southern_Altai = 'alt'; + case Southern_Sami = 'sma'; + case Spanish_Castilian = 'spa'; + case Sranan_Tongo = 'srn'; + case Standard_Moroccan_Tamazight = 'zgh'; + case Sukuma = 'suk'; + case Sumerian = 'sux'; + case Sundanese = 'sun'; + case Susu = 'sus'; + case Swahili = 'swa'; + case Swati = 'ssw'; + case Swedish = 'swe'; + case Swiss_German_Alemannic_Alsatian = 'gsw'; + case Syriac = 'syr'; + case Tagalog = 'tgl'; + case Tahitian = 'tah'; + case Tai_languages = 'tai'; + case Tajik = 'tgk'; + case Tamashek = 'tmh'; + case Tamil = 'tam'; + case Tatar = 'tat'; + case Telugu = 'tel'; + case Tereno = 'ter'; + case Tetum = 'tet'; + case Thai = 'tha'; + case Tibetan = 'bod'; + case Tigre = 'tig'; + case Tigrinya = 'tir'; + case Timne = 'tem'; + case Tiv = 'tiv'; + case Tlingit = 'tli'; + case Tok_Pisin = 'tpi'; + case Tokelau = 'tkl'; + case Tonga_Nyasa = 'tog'; + case Tonga_Tonga_Islands = 'ton'; + case Tsimshian = 'tsi'; + case Tsonga = 'tso'; + case Tswana = 'tsn'; + case Tumbuka = 'tum'; + case Tupi_languages = 'tup'; + case Turkish = 'tur'; + case Turkish_Ottoman_1500_1928 = 'ota'; + case Turkmen = 'tuk'; + case Tuvalu = 'tvl'; + case Tuvinian = 'tyv'; + case Twi = 'twi'; + case Udmurt = 'udm'; + case Ugaritic = 'uga'; + case Uighur_Uyghur = 'uig'; + case Ukrainian = 'ukr'; + case Umbundu = 'umb'; + case Uncoded_languages = 'mis'; + case Undetermined = 'und'; + case Upper_Sorbian = 'hsb'; + case Urdu = 'urd'; + case Uzbek = 'uzb'; + case Vai = 'vai'; + case Venda = 'ven'; + case Vietnamese = 'vie'; + case Volapuk = 'vol'; + case Votic = 'vot'; + case Wakashan_languages = 'wak'; + case Walloon = 'wln'; + case Waray = 'war'; + case Washo = 'was'; + case Welsh = 'cym'; + case Western_Frisian = 'fry'; + case Wolaitta_Wolaytta = 'wal'; + case Wolof = 'wol'; + case Xhosa = 'xho'; + case Yakut = 'sah'; + case Yao = 'yao'; + case Yapese = 'yap'; + case Yiddish = 'yid'; + case Yoruba = 'yor'; + case Yupik_languages = 'ypk'; + case Zande_languages = 'znd'; + case Zapotec = 'zap'; + case Zaza_Dimili_Dimli_Kirdki_Kirmanjki_Zazaki = 'zza'; + case Zenaga = 'zen'; + case Zhuang_Chuang = 'zha'; + case Zulu = 'zul'; + case Zuni = 'zun'; +} + + +enum LanguageAlpha3Bibliographic: string +{ + case Abkhazian = 'abk'; + case Achinese = 'ace'; + case Acoli = 'ach'; + case Adangme = 'ada'; + case Adyghe_Adygei = 'ady'; + case Afar = 'aar'; + case Afrihili = 'afh'; + case Afrikaans = 'afr'; + case Afro_Asiatic_languages = 'afa'; + case Ainu = 'ain'; + case Akan = 'aka'; + case Akkadian = 'akk'; + case Albanian = 'alb'; + case Aleut = 'ale'; + case Algonquian_languages = 'alg'; + case Altaic_languages = 'tut'; + case Amharic = 'amh'; + case Angika = 'anp'; + case Apache_languages = 'apa'; + case Arabic = 'ara'; + case Aragonese = 'arg'; + case Arapaho = 'arp'; + case Arawak = 'arw'; + case Armenian = 'arm'; + case Aromanian_Arumanian_Macedo_Romanian = 'rup'; + case Artificial_languages = 'art'; + case Assamese = 'asm'; + case Asturian_Bable_Leonese_Asturleonese = 'ast'; + case Athapascan_languages = 'ath'; + case Australian_languages = 'aus'; + case Austronesian_languages = 'map'; + case Avaric = 'ava'; + case Avestan = 'ave'; + case Awadhi = 'awa'; + case Aymara = 'aym'; + case Azerbaijani = 'aze'; + case Balinese = 'ban'; + case Baltic_languages = 'bat'; + case Baluchi = 'bal'; + case Bambara = 'bam'; + case Bamileke_languages = 'bai'; + case Banda_languages = 'bad'; + case Bantu_languages = 'bnt'; + case Basa = 'bas'; + case Bashkir = 'bak'; + case Basque = 'baq'; + case Batak_languages = 'btk'; + case Beja_Bedawiyet = 'bej'; + case Belarusian = 'bel'; + case Bemba = 'bem'; + case Bengali = 'ben'; + case Berber_languages = 'ber'; + case Bhojpuri = 'bho'; + case Bihari_languages = 'bih'; + case Bikol = 'bik'; + case Bini_Edo = 'bin'; + case Bislama = 'bis'; + case Blin_Bilin = 'byn'; + case Blissymbols_Blissymbolics_Bliss = 'zbl'; + case Bokmal_Norwegian_Norwegian_Bokmal = 'nob'; + case Bosnian = 'bos'; + case Braj = 'bra'; + case Breton = 'bre'; + case Buginese = 'bug'; + case Bulgarian = 'bul'; + case Buriat = 'bua'; + case Burmese = 'bur'; + case Caddo = 'cad'; + case Catalan_Valencian = 'cat'; + case Caucasian_languages = 'cau'; + case Cebuano = 'ceb'; + case Celtic_languages = 'cel'; + case Central_American_Indian_languages = 'cai'; + case Central_Khmer = 'khm'; + case Chagatai = 'chg'; + case Chamic_languages = 'cmc'; + case Chamorro = 'cha'; + case Chechen = 'che'; + case Cherokee = 'chr'; + case Cheyenne = 'chy'; + case Chibcha = 'chb'; + case Chichewa_Chewa_Nyanja = 'nya'; + case Chinese = 'chi'; + case Chinook_jargon = 'chn'; + case Chipewyan_Dene_Suline = 'chp'; + case Choctaw = 'cho'; + case Church_Slavic_Old_Slavonic_Church_Slavonic_Old_Bulgarian_Old_Church_Slavonic = 'chu'; + case Chuukese = 'chk'; + case Chuvash = 'chv'; + case Classical_Newari_Old_Newari_Classical_Nepal_Bhasa = 'nwc'; + case Classical_Syriac = 'syc'; + case Coptic = 'cop'; + case Cornish = 'cor'; + case Corsican = 'cos'; + case Cree = 'cre'; + case Creek = 'mus'; + case Creoles_and_pidgins = 'crp'; + case Creoles_and_pidgins_English_based = 'cpe'; + case Creoles_and_pidgins_French_based = 'cpf'; + case Creoles_and_pidgins_Portuguese_based = 'cpp'; + case Crimean_Tatar_Crimean_Turkish = 'crh'; + case Croatian = 'hrv'; + case Cushitic_languages = 'cus'; + case Czech = 'cze'; + case Dakota = 'dak'; + case Danish = 'dan'; + case Dargwa = 'dar'; + case Delaware = 'del'; + case Dinka = 'din'; + case Divehi_Dhivehi_Maldivian = 'div'; + case Dogri = 'doi'; + case Dogrib = 'dgr'; + case Dravidian_languages = 'dra'; + case Duala = 'dua'; + case Dutch_Flemish = 'dut'; + case Dutch_Middle_ca_1050_1350 = 'dum'; + case Dyula = 'dyu'; + case Dzongkha = 'dzo'; + case Eastern_Frisian = 'frs'; + case Efik = 'efi'; + case Egyptian_Ancient = 'egy'; + case Ekajuk = 'eka'; + case Elamite = 'elx'; + case English = 'eng'; + case English_Middle_1100_1500 = 'enm'; + case English_Old_ca_450_1100 = 'ang'; + case Erzya = 'myv'; + case Esperanto = 'epo'; + case Estonian = 'est'; + case Ewe = 'ewe'; + case Ewondo = 'ewo'; + case Fang = 'fan'; + case Fanti = 'fat'; + case Faroese = 'fao'; + case Fijian = 'fij'; + case Filipino_Pilipino = 'fil'; + case Finnish = 'fin'; + case Finno_Ugrian_languages = 'fiu'; + case Fon = 'fon'; + case French = 'fre'; + case French_Middle_ca_1400_1600 = 'frm'; + case French_Old_842_ca_1400 = 'fro'; + case Friulian = 'fur'; + case Fulah = 'ful'; + case Ga = 'gaa'; + case Gaelic_Scottish_Gaelic = 'gla'; + case Galibi_Carib = 'car'; + case Galician = 'glg'; + case Ganda = 'lug'; + case Gayo = 'gay'; + case Gbaya = 'gba'; + case Geez = 'gez'; + case Georgian = 'geo'; + case German = 'ger'; + case German_Middle_High_ca_1050_1500 = 'gmh'; + case German_Old_High_ca_750_1050 = 'goh'; + case Germanic_languages = 'gem'; + case Gilbertese = 'gil'; + case Gondi = 'gon'; + case Gorontalo = 'gor'; + case Gothic = 'got'; + case Grebo = 'grb'; + case Greek_Ancient_to_1453 = 'grc'; + case Greek_Modern_1453 = 'gre'; + case Guarani = 'grn'; + case Gujarati = 'guj'; + case Gwich_in = 'gwi'; + case Haida = 'hai'; + case Haitian_Haitian_Creole = 'hat'; + case Hausa = 'hau'; + case Hawaiian = 'haw'; + case Hebrew = 'heb'; + case Herero = 'her'; + case Hiligaynon = 'hil'; + case Himachali_languages_Western_Pahari_languages = 'him'; + case Hindi = 'hin'; + case Hiri_Motu = 'hmo'; + case Hittite = 'hit'; + case Hmong_Mong = 'hmn'; + case Hungarian = 'hun'; + case Hupa = 'hup'; + case Iban = 'iba'; + case Icelandic = 'ice'; + case Ido = 'ido'; + case Igbo = 'ibo'; + case Ijo_languages = 'ijo'; + case Iloko = 'ilo'; + case Inari_Sami = 'smn'; + case Indic_languages = 'inc'; + case Indo_European_languages = 'ine'; + case Indonesian = 'ind'; + case Ingush = 'inh'; + case Interlingua_International_Auxiliary_Language_Association = 'ina'; + case Interlingue_Occidental = 'ile'; + case Inuktitut = 'iku'; + case Inupiaq = 'ipk'; + case Iranian_languages = 'ira'; + case Irish = 'gle'; + case Irish_Middle_900_1200 = 'mga'; + case Irish_Old_to_900 = 'sga'; + case Iroquoian_languages = 'iro'; + case Italian = 'ita'; + case Japanese = 'jpn'; + case Javanese = 'jav'; + case Judeo_Arabic = 'jrb'; + case Judeo_Persian = 'jpr'; + case Kabardian = 'kbd'; + case Kabyle = 'kab'; + case Kachin_Jingpho = 'kac'; + case Kalaallisut_Greenlandic = 'kal'; + case Kalmyk_Oirat = 'xal'; + case Kamba = 'kam'; + case Kannada = 'kan'; + case Kanuri = 'kau'; + case Kara_Kalpak = 'kaa'; + case Karachay_Balkar = 'krc'; + case Karelian = 'krl'; + case Karen_languages = 'kar'; + case Kashmiri = 'kas'; + case Kashubian = 'csb'; + case Kawi = 'kaw'; + case Kazakh = 'kaz'; + case Khasi = 'kha'; + case Khoisan_languages = 'khi'; + case Khotanese_Sakan = 'kho'; + case Kikuyu_Gikuyu = 'kik'; + case Kimbundu = 'kmb'; + case Kinyarwanda = 'kin'; + case Kirghiz_Kyrgyz = 'kir'; + case Klingon_tlhIngan_Hol = 'tlh'; + case Komi = 'kom'; + case Kongo = 'kon'; + case Konkani = 'kok'; + case Korean = 'kor'; + case Kosraean = 'kos'; + case Kpelle = 'kpe'; + case Kru_languages = 'kro'; + case Kuanyama_Kwanyama = 'kua'; + case Kumyk = 'kum'; + case Kurdish = 'kur'; + case Kurukh = 'kru'; + case Kutenai = 'kut'; + case Ladino = 'lad'; + case Lahnda = 'lah'; + case Lamba = 'lam'; + case Land_Dayak_languages = 'day'; + case Lao = 'lao'; + case Latin = 'lat'; + case Latvian = 'lav'; + case Lezghian = 'lez'; + case Limburgan_Limburger_Limburgish = 'lim'; + case Lingala = 'lin'; + case Lithuanian = 'lit'; + case Lojban = 'jbo'; + case Low_German_Low_Saxon_German_Low_Saxon_Low = 'nds'; + case Lower_Sorbian = 'dsb'; + case Lozi = 'loz'; + case Luba_Katanga = 'lub'; + case Luba_Lulua = 'lua'; + case Luiseno = 'lui'; + case Lule_Sami = 'smj'; + case Lunda = 'lun'; + case Luo_Kenya_and_Tanzania = 'luo'; + case Lushai = 'lus'; + case Luxembourgish_Letzeburgesch = 'ltz'; + case Macedonian = 'mac'; + case Madurese = 'mad'; + case Magahi = 'mag'; + case Maithili = 'mai'; + case Makasar = 'mak'; + case Malagasy = 'mlg'; + case Malay = 'may'; + case Malayalam = 'mal'; + case Maltese = 'mlt'; + case Manchu = 'mnc'; + case Mandar = 'mdr'; + case Mandingo = 'man'; + case Manipuri = 'mni'; + case Manobo_languages = 'mno'; + case Manx = 'glv'; + case Maori = 'mao'; + case Mapudungun_Mapuche = 'arn'; + case Marathi = 'mar'; + case Mari = 'chm'; + case Marshallese = 'mah'; + case Marwari = 'mwr'; + case Masai = 'mas'; + case Mayan_languages = 'myn'; + case Mende = 'men'; + case Mi_kmaq_Micmac = 'mic'; + case Minangkabau = 'min'; + case Mirandese = 'mwl'; + case Mohawk = 'moh'; + case Moksha = 'mdf'; + case Mon_Khmer_languages = 'mkh'; + case Mongo = 'lol'; + case Mongolian = 'mon'; + case Montenegrin = 'cnr'; + case Mossi = 'mos'; + case Multiple_languages = 'mul'; + case Munda_languages = 'mun'; + case N_Ko = 'nqo'; + case Nahuatl_languages = 'nah'; + case Nauru = 'nau'; + case Navajo_Navaho = 'nav'; + case Ndebele_North_North_Ndebele = 'nde'; + case Ndebele_South_South_Ndebele = 'nbl'; + case Ndonga = 'ndo'; + case Neapolitan = 'nap'; + case Nepal_Bhasa_Newari = 'new'; + case Nepali = 'nep'; + case Nias = 'nia'; + case Niger_Kordofanian_languages = 'nic'; + case Nilo_Saharan_languages = 'ssa'; + case Niuean = 'niu'; + case No_linguistic_content_Not_applicable = 'zxx'; + case Nogai = 'nog'; + case Norse_Old = 'non'; + case North_American_Indian_languages = 'nai'; + case Northern_Frisian = 'frr'; + case Northern_Sami = 'sme'; + case Norwegian = 'nor'; + case Norwegian_Nynorsk_Nynorsk_Norwegian = 'nno'; + case Nubian_languages = 'nub'; + case Nyamwezi = 'nym'; + case Nyankole = 'nyn'; + case Nyoro = 'nyo'; + case Nzima = 'nzi'; + case Occitan_post_1500 = 'oci'; + case Official_Aramaic_700_300_BCE_Imperial_Aramaic_700_300_BCE = 'arc'; + case Ojibwa = 'oji'; + case Oriya = 'ori'; + case Oromo = 'orm'; + case Osage = 'osa'; + case Ossetian_Ossetic = 'oss'; + case Otomian_languages = 'oto'; + case Pahlavi = 'pal'; + case Palauan = 'pau'; + case Pali = 'pli'; + case Pampanga_Kapampangan = 'pam'; + case Pangasinan = 'pag'; + case Panjabi_Punjabi = 'pan'; + case Papiamento = 'pap'; + case Papuan_languages = 'paa'; + case Pedi_Sepedi_Northern_Sotho = 'nso'; + case Persian = 'per'; + case Persian_Old_ca_600_400_B_C = 'peo'; + case Philippine_languages = 'phi'; + case Phoenician = 'phn'; + case Pohnpeian = 'pon'; + case Polish = 'pol'; + case Portuguese = 'por'; + case Prakrit_languages = 'pra'; + case Provencal_Old_to_1500_Occitan_Old_to_1500 = 'pro'; + case Pushto_Pashto = 'pus'; + case Quechua = 'que'; + case Rajasthani = 'raj'; + case Rapanui = 'rap'; + case Rarotongan_Cook_Islands_Maori = 'rar'; + case Romance_languages = 'roa'; + case Romanian_Moldavian_Moldovan = 'rum'; + case Romansh = 'roh'; + case Romany = 'rom'; + case Rundi = 'run'; + case Russian = 'rus'; + case Salishan_languages = 'sal'; + case Samaritan_Aramaic = 'sam'; + case Sami_languages = 'smi'; + case Samoan = 'smo'; + case Sandawe = 'sad'; + case Sango = 'sag'; + case Sanskrit = 'san'; + case Santali = 'sat'; + case Sardinian = 'srd'; + case Sasak = 'sas'; + case Scots = 'sco'; + case Selkup = 'sel'; + case Semitic_languages = 'sem'; + case Serbian = 'srp'; + case Serer = 'srr'; + case Shan = 'shn'; + case Shona = 'sna'; + case Sichuan_Yi_Nuosu = 'iii'; + case Sicilian = 'scn'; + case Sidamo = 'sid'; + case Sign_Languages = 'sgn'; + case Siksika = 'bla'; + case Sindhi = 'snd'; + case Sinhala_Sinhalese = 'sin'; + case Sino_Tibetan_languages = 'sit'; + case Siouan_languages = 'sio'; + case Skolt_Sami = 'sms'; + case Slave_Athapascan = 'den'; + case Slavic_languages = 'sla'; + case Slovak = 'slo'; + case Slovenian = 'slv'; + case Sogdian = 'sog'; + case Somali = 'som'; + case Songhai_languages = 'son'; + case Soninke = 'snk'; + case Sorbian_languages = 'wen'; + case Sotho_Southern = 'sot'; + case South_American_Indian_languages = 'sai'; + case Southern_Altai = 'alt'; + case Southern_Sami = 'sma'; + case Spanish_Castilian = 'spa'; + case Sranan_Tongo = 'srn'; + case Standard_Moroccan_Tamazight = 'zgh'; + case Sukuma = 'suk'; + case Sumerian = 'sux'; + case Sundanese = 'sun'; + case Susu = 'sus'; + case Swahili = 'swa'; + case Swati = 'ssw'; + case Swedish = 'swe'; + case Swiss_German_Alemannic_Alsatian = 'gsw'; + case Syriac = 'syr'; + case Tagalog = 'tgl'; + case Tahitian = 'tah'; + case Tai_languages = 'tai'; + case Tajik = 'tgk'; + case Tamashek = 'tmh'; + case Tamil = 'tam'; + case Tatar = 'tat'; + case Telugu = 'tel'; + case Tereno = 'ter'; + case Tetum = 'tet'; + case Thai = 'tha'; + case Tibetan = 'tib'; + case Tigre = 'tig'; + case Tigrinya = 'tir'; + case Timne = 'tem'; + case Tiv = 'tiv'; + case Tlingit = 'tli'; + case Tok_Pisin = 'tpi'; + case Tokelau = 'tkl'; + case Tonga_Nyasa = 'tog'; + case Tonga_Tonga_Islands = 'ton'; + case Tsimshian = 'tsi'; + case Tsonga = 'tso'; + case Tswana = 'tsn'; + case Tumbuka = 'tum'; + case Tupi_languages = 'tup'; + case Turkish = 'tur'; + case Turkish_Ottoman_1500_1928 = 'ota'; + case Turkmen = 'tuk'; + case Tuvalu = 'tvl'; + case Tuvinian = 'tyv'; + case Twi = 'twi'; + case Udmurt = 'udm'; + case Ugaritic = 'uga'; + case Uighur_Uyghur = 'uig'; + case Ukrainian = 'ukr'; + case Umbundu = 'umb'; + case Uncoded_languages = 'mis'; + case Undetermined = 'und'; + case Upper_Sorbian = 'hsb'; + case Urdu = 'urd'; + case Uzbek = 'uzb'; + case Vai = 'vai'; + case Venda = 'ven'; + case Vietnamese = 'vie'; + case Volapuk = 'vol'; + case Votic = 'vot'; + case Wakashan_languages = 'wak'; + case Walloon = 'wln'; + case Waray = 'war'; + case Washo = 'was'; + case Welsh = 'wel'; + case Western_Frisian = 'fry'; + case Wolaitta_Wolaytta = 'wal'; + case Wolof = 'wol'; + case Xhosa = 'xho'; + case Yakut = 'sah'; + case Yao = 'yao'; + case Yapese = 'yap'; + case Yiddish = 'yid'; + case Yoruba = 'yor'; + case Yupik_languages = 'ypk'; + case Zande_languages = 'znd'; + case Zapotec = 'zap'; + case Zaza_Dimili_Dimli_Kirdki_Kirmanjki_Zazaki = 'zza'; + case Zenaga = 'zen'; + case Zhuang_Chuang = 'zha'; + case Zulu = 'zul'; + case Zuni = 'zun'; + +} + + +enum LanguageAlpha3Extensive: string +{ + case Ghotuo = 'aaa'; + case Alumu_Tesu = 'aab'; + case Ari = 'aac'; + case Amal = 'aad'; + case Arbereshe_Albanian = 'aae'; + case Aranadan = 'aaf'; + case Ambrak = 'aag'; + case Abu_Arapesh = 'aah'; + case Arifama_Miniafia = 'aai'; + case Ankave = 'aak'; + case Afade = 'aal'; + case Anambe = 'aan'; + case Algerian_Saharan_Arabic = 'aao'; + case Para_Arara = 'aap'; + case Eastern_Abnaki = 'aaq'; + case Afar = 'aar'; + case Aasax = 'aas'; + case Arvanitika_Albanian = 'aat'; + case Abau = 'aau'; + case Solong = 'aaw'; + case Mandobo_Atas = 'aax'; + case Amarasi = 'aaz'; + case Abe = 'aba'; + case Bankon = 'abb'; + case Ambala_Ayta = 'abc'; + case Manide = 'abd'; + case Western_Abnaki = 'abe'; + case Abai_Sungai = 'abf'; + case Abaga = 'abg'; + case Tajiki_Arabic = 'abh'; + case Abidji = 'abi'; + case Aka_Bea = 'abj'; + case Abkhazian = 'abk'; + case Lampung_Nyo = 'abl'; + case Abanyom = 'abm'; + case Abua = 'abn'; + case Abon = 'abo'; + case Abellen_Ayta = 'abp'; + case Abaza = 'abq'; + case Abron = 'abr'; + case Ambonese_Malay = 'abs'; + case Ambulas = 'abt'; + case Abure = 'abu'; + case Baharna_Arabic = 'abv'; + case Pal = 'abw'; + case Inabaknon = 'abx'; + case Aneme_Wake = 'aby'; + case Abui = 'abz'; + case Achagua = 'aca'; + case Anca = 'acb'; + case Gikyode = 'acd'; + case Achinese = 'ace'; + case Saint_Lucian_Creole_French = 'acf'; + case Acoli = 'ach'; + case Aka_Cari = 'aci'; + case Aka_Kora = 'ack'; + case Akar_Bale = 'acl'; + case Mesopotamian_Arabic = 'acm'; + case Achang = 'acn'; + case Eastern_Acipa = 'acp'; + case Ta_izzi_Adeni_Arabic = 'acq'; + case Achi = 'acr'; + case Acroa = 'acs'; + case Achterhoeks = 'act'; + case Achuar_Shiwiar = 'acu'; + case Achumawi = 'acv'; + case Hijazi_Arabic = 'acw'; + case Omani_Arabic = 'acx'; + case Cypriot_Arabic = 'acy'; + case Acheron = 'acz'; + case Adangme = 'ada'; + case Atauran = 'adb'; + case Lidzonka = 'add'; + case Adele = 'ade'; + case Dhofari_Arabic = 'adf'; + case Andegerebinha = 'adg'; + case Adhola = 'adh'; + case Adi = 'adi'; + case Adioukrou = 'adj'; + case Galo = 'adl'; + case Adang = 'adn'; + case Abu = 'ado'; + case Adangbe = 'adq'; + case Adonara = 'adr'; + case Adamorobe_Sign_Language = 'ads'; + case Adnyamathanha = 'adt'; + case Aduge = 'adu'; + case Amundava = 'adw'; + case Amdo_Tibetan = 'adx'; + case Adyghe = 'ady'; + case Adzera = 'adz'; + case Areba = 'aea'; + case Tunisian_Arabic = 'aeb'; + case Saidi_Arabic = 'aec'; + case Argentine_Sign_Language = 'aed'; + case Northeast_Pashai = 'aee'; + case Haeke = 'aek'; + case Ambele = 'ael'; + case Arem = 'aem'; + case Armenian_Sign_Language = 'aen'; + case Aer = 'aeq'; + case Eastern_Arrernte = 'aer'; + case Alsea = 'aes'; + case Akeu = 'aeu'; + case Ambakich = 'aew'; + case Amele = 'aey'; + case Aeka = 'aez'; + case Gulf_Arabic = 'afb'; + case Andai = 'afd'; + case Putukwam = 'afe'; + case Afghan_Sign_Language = 'afg'; + case Afrihili = 'afh'; + case Akrukay = 'afi'; + case Nanubae = 'afk'; + case Defaka = 'afn'; + case Eloyi = 'afo'; + case Tapei = 'afp'; + case Afrikaans = 'afr'; + case Afro_Seminole_Creole = 'afs'; + case Afitti = 'aft'; + case Awutu = 'afu'; + case Obokuitai = 'afz'; + case Aguano = 'aga'; + case Legbo = 'agb'; + case Agatu = 'agc'; + case Agarabi = 'agd'; + case Angal = 'age'; + case Arguni = 'agf'; + case Angor = 'agg'; + case Ngelima = 'agh'; + case Agariya = 'agi'; + case Argobba = 'agj'; + case Isarog_Agta = 'agk'; + case Fembe = 'agl'; + case Angaataha = 'agm'; + case Agutaynen = 'agn'; + case Tainae = 'ago'; + case Aghem = 'agq'; + case Aguaruna = 'agr'; + case Esimbi = 'ags'; + case Central_Cagayan_Agta = 'agt'; + case Aguacateco = 'agu'; + case Remontado_Dumagat = 'agv'; + case Kahua = 'agw'; + case Aghul = 'agx'; + case Southern_Alta = 'agy'; + case Mt_Iriga_Agta = 'agz'; + case Ahanta = 'aha'; + case Axamb = 'ahb'; + case Qimant = 'ahg'; + case Aghu = 'ahh'; + case Tiagbamrin_Aizi = 'ahi'; + case Akha = 'ahk'; + case Igo = 'ahl'; + case Mobumrin_Aizi = 'ahm'; + case Ahan = 'ahn'; + case Ahom = 'aho'; + case Aproumu_Aizi = 'ahp'; + case Ahirani = 'ahr'; + case Ashe = 'ahs'; + case Ahtena = 'aht'; + case Arosi = 'aia'; + case Ainu_China = 'aib'; + case Ainbai = 'aic'; + case Alngith = 'aid'; + case Amara = 'aie'; + case Agi = 'aif'; + case Antigua_and_Barbuda_Creole_English = 'aig'; + case Ai_Cham = 'aih'; + case Assyrian_Neo_Aramaic = 'aii'; + case Lishanid_Noshan = 'aij'; + case Ake = 'aik'; + case Aimele = 'ail'; + case Aimol = 'aim'; + case Ainu_Japan = 'ain'; + case Aiton = 'aio'; + case Burumakok = 'aip'; + case Aimaq = 'aiq'; + case Airoran = 'air'; + case Arikem = 'ait'; + case Aari = 'aiw'; + case Aighon = 'aix'; + case Ali = 'aiy'; + case Aja_South_Sudan = 'aja'; + case Aja_Benin = 'ajg'; + case Ajie = 'aji'; + case Andajin = 'ajn'; + case Algerian_Jewish_Sign_Language = 'ajs'; + case Judeo_Moroccan_Arabic = 'aju'; + case Ajawa = 'ajw'; + case Amri_Karbi = 'ajz'; + case Akan = 'aka'; + case Batak_Angkola = 'akb'; + case Mpur = 'akc'; + case Ukpet_Ehom = 'akd'; + case Akawaio = 'ake'; + case Akpa = 'akf'; + case Anakalangu = 'akg'; + case Angal_Heneng = 'akh'; + case Aiome = 'aki'; + case Aka_Jeru = 'akj'; + case Akkadian = 'akk'; + case Aklanon = 'akl'; + case Aka_Bo = 'akm'; + case Akurio = 'ako'; + case Siwu = 'akp'; + case Ak = 'akq'; + case Araki = 'akr'; + case Akaselem = 'aks'; + case Akolet = 'akt'; + case Akum = 'aku'; + case Akhvakh = 'akv'; + case Akwa = 'akw'; + case Aka_Kede = 'akx'; + case Aka_Kol = 'aky'; + case Alabama = 'akz'; + case Alago = 'ala'; + case Qawasqar = 'alc'; + case Alladian = 'ald'; + case Aleut = 'ale'; + case Alege = 'alf'; + case Alawa = 'alh'; + case Amaimon = 'ali'; + case Alangan = 'alj'; + case Alak = 'alk'; + case Allar = 'all'; + case Amblong = 'alm'; + case Gheg_Albanian = 'aln'; + case Larike_Wakasihu = 'alo'; + case Alune = 'alp'; + case Algonquin = 'alq'; + case Alutor = 'alr'; + case Tosk_Albanian = 'als'; + case Southern_Altai = 'alt'; + case Are_are = 'alu'; + case Alaba_K_abeena = 'alw'; + case Amol = 'alx'; + case Alyawarr = 'aly'; + case Alur = 'alz'; + case Amanaye = 'ama'; + case Ambo = 'amb'; + case Amahuaca = 'amc'; + case Yanesha = 'ame'; + case Hamer_Banna = 'amf'; + case Amurdak = 'amg'; + case Amharic = 'amh'; + case Amis = 'ami'; + case Amdang = 'amj'; + case Ambai = 'amk'; + case War_Jaintia = 'aml'; + case Ama_Papua_New_Guinea = 'amm'; + case Amanab = 'amn'; + case Amo = 'amo'; + case Alamblak = 'amp'; + case Amahai = 'amq'; + case Amarakaeri = 'amr'; + case Southern_Amami_Oshima = 'ams'; + case Amto = 'amt'; + case Guerrero_Amuzgo = 'amu'; + case Ambelau = 'amv'; + case Western_Neo_Aramaic = 'amw'; + case Anmatyerre = 'amx'; + case Ami = 'amy'; + case Atampaya = 'amz'; + case Andaqui = 'ana'; + case Andoa = 'anb'; + case Ngas = 'anc'; + case Ansus = 'and'; + case Xaracuu = 'ane'; + case Animere = 'anf'; + case Old_English_ca_450_1100 = 'ang'; + case Nend = 'anh'; + case Andi = 'ani'; + case Anor = 'anj'; + case Goemai = 'ank'; + case Anu_Hkongso_Chin = 'anl'; + case Anal = 'anm'; + case Obolo = 'ann'; + case Andoque = 'ano'; + case Angika = 'anp'; + case Jarawa_India = 'anq'; + case Andh = 'anr'; + case Anserma = 'ans'; + case Antakarinya = 'ant'; + case Anuak = 'anu'; + case Denya = 'anv'; + case Anaang = 'anw'; + case Andra_Hus = 'anx'; + case Anyin = 'any'; + case Anem = 'anz'; + case Angolar = 'aoa'; + case Abom = 'aob'; + case Pemon = 'aoc'; + case Andarum = 'aod'; + case Angal_Enen = 'aoe'; + case Bragat = 'aof'; + case Angoram = 'aog'; + case Anindilyakwa = 'aoi'; + case Mufian = 'aoj'; + case Arho = 'aok'; + case Alor = 'aol'; + case Omie = 'aom'; + case Bumbita_Arapesh = 'aon'; + case Aore = 'aor'; + case Taikat = 'aos'; + case Atong_India = 'aot'; + case A_ou = 'aou'; + case Atorada = 'aox'; + case Uab_Meto = 'aoz'; + case Sa_a = 'apb'; + case Levantine_Arabic = 'apc'; + case Sudanese_Arabic = 'apd'; + case Bukiyip = 'ape'; + case Pahanan_Agta = 'apf'; + case Ampanang = 'apg'; + case Athpariya = 'aph'; + case Apiaka = 'api'; + case Jicarilla_Apache = 'apj'; + case Kiowa_Apache = 'apk'; + case Lipan_Apache = 'apl'; + case Mescalero_Chiricahua_Apache = 'apm'; + case Apinaye = 'apn'; + case Ambul = 'apo'; + case Apma = 'app'; + case A_Pucikwar = 'apq'; + case Arop_Lokep = 'apr'; + case Arop_Sissano = 'aps'; + case Apatani = 'apt'; + case Apurina = 'apu'; + case Alapmunte = 'apv'; + case Western_Apache = 'apw'; + case Aputai = 'apx'; + case Apalai = 'apy'; + case Safeyoka = 'apz'; + case Archi = 'aqc'; + case Ampari_Dogon = 'aqd'; + case Arigidi = 'aqg'; + case Aninka = 'aqk'; + case Atohwaim = 'aqm'; + case Northern_Alta = 'aqn'; + case Atakapa = 'aqp'; + case Arha = 'aqr'; + case Angaite = 'aqt'; + case Akuntsu = 'aqz'; + case Arabic = 'ara'; + case Standard_Arabic = 'arb'; + case Official_Aramaic_700_300_BCE = 'arc'; + case Arabana = 'ard'; + case Western_Arrarnta = 'are'; + case Aragonese = 'arg'; + case Arhuaco = 'arh'; + case Arikara = 'ari'; + case Arapaso = 'arj'; + case Arikapu = 'ark'; + case Arabela = 'arl'; + case Mapudungun = 'arn'; + case Araona = 'aro'; + case Arapaho = 'arp'; + case Algerian_Arabic = 'arq'; + case Karo_Brazil = 'arr'; + case Najdi_Arabic = 'ars'; + case Arua_Amazonas_State = 'aru'; + case Arbore = 'arv'; + case Arawak = 'arw'; + case Arua_Rodonia_State = 'arx'; + case Moroccan_Arabic = 'ary'; + case Egyptian_Arabic = 'arz'; + case Asu_Tanzania = 'asa'; + case Assiniboine = 'asb'; + case Casuarina_Coast_Asmat = 'asc'; + case American_Sign_Language = 'ase'; + case Auslan = 'asf'; + case Cishingini = 'asg'; + case Abishira = 'ash'; + case Buruwai = 'asi'; + case Sari = 'asj'; + case Ashkun = 'ask'; + case Asilulu = 'asl'; + case Assamese = 'asm'; + case Xingu_Asurini = 'asn'; + case Dano = 'aso'; + case Algerian_Sign_Language = 'asp'; + case Austrian_Sign_Language = 'asq'; + case Asuri = 'asr'; + case Ipulo = 'ass'; + case Asturian = 'ast'; + case Tocantins_Asurini = 'asu'; + case Asoa = 'asv'; + case Australian_Aborigines_Sign_Language = 'asw'; + case Muratayak = 'asx'; + case Yaosakor_Asmat = 'asy'; + case As = 'asz'; + case Pele_Ata = 'ata'; + case Zaiwa = 'atb'; + case Atsahuaca = 'atc'; + case Ata_Manobo = 'atd'; + case Atemble = 'ate'; + case Ivbie_North_Okpela_Arhe = 'atg'; + case Attie = 'ati'; + case Atikamekw = 'atj'; + case Ati = 'atk'; + case Mt_Iraya_Agta = 'atl'; + case Ata = 'atm'; + case Ashtiani = 'atn'; + case Atong_Cameroon = 'ato'; + case Pudtol_Atta = 'atp'; + case Aralle_Tabulahan = 'atq'; + case Waimiri_Atroari = 'atr'; + case Gros_Ventre = 'ats'; + case Pamplona_Atta = 'att'; + case Reel = 'atu'; + case Northern_Altai = 'atv'; + case Atsugewi = 'atw'; + case Arutani = 'atx'; + case Aneityum = 'aty'; + case Arta = 'atz'; + case Asumboa = 'aua'; + case Alugu = 'aub'; + case Waorani = 'auc'; + case Anuta = 'aud'; + case Aguna = 'aug'; + case Aushi = 'auh'; + case Anuki = 'aui'; + case Awjilah = 'auj'; + case Heyo = 'auk'; + case Aulua = 'aul'; + case Asu_Nigeria = 'aum'; + case Molmo_One = 'aun'; + case Auyokawa = 'auo'; + case Makayam = 'aup'; + case Anus = 'auq'; + case Aruek = 'aur'; + case Austral = 'aut'; + case Auye = 'auu'; + case Awyi = 'auw'; + case Aura = 'aux'; + case Awiyaana = 'auy'; + case Uzbeki_Arabic = 'auz'; + case Avaric = 'ava'; + case Avau = 'avb'; + case Alviri_Vidari = 'avd'; + case Avestan = 'ave'; + case Avikam = 'avi'; + case Kotava = 'avk'; + case Eastern_Egyptian_Bedawi_Arabic = 'avl'; + case Angkamuthi = 'avm'; + case Avatime = 'avn'; + case Agavotaguerra = 'avo'; + case Aushiri = 'avs'; + case Au = 'avt'; + case Avokaya = 'avu'; + case Ava_Canoeiro = 'avv'; + case Awadhi = 'awa'; + case Awa_Papua_New_Guinea = 'awb'; + case Cicipu = 'awc'; + case Aweti = 'awe'; + case Anguthimri = 'awg'; + case Awbono = 'awh'; + case Aekyom = 'awi'; + case Awabakal = 'awk'; + case Arawum = 'awm'; + case Awngi = 'awn'; + case Awak = 'awo'; + case Awera = 'awr'; + case South_Awyu = 'aws'; + case Arawete = 'awt'; + case Central_Awyu = 'awu'; + case Jair_Awyu = 'awv'; + case Awun = 'aww'; + case Awara = 'awx'; + case Edera_Awyu = 'awy'; + case Abipon = 'axb'; + case Ayerrerenge = 'axe'; + case Mato_Grosso_Arara = 'axg'; + case Yaka_Central_African_Republic = 'axk'; + case Lower_Southern_Aranda = 'axl'; + case Middle_Armenian = 'axm'; + case Xaragure = 'axx'; + case Awar = 'aya'; + case Ayizo_Gbe = 'ayb'; + case Southern_Aymara = 'ayc'; + case Ayabadhu = 'ayd'; + case Ayere = 'aye'; + case Ginyanga = 'ayg'; + case Hadrami_Arabic = 'ayh'; + case Leyigha = 'ayi'; + case Akuku = 'ayk'; + case Libyan_Arabic = 'ayl'; + case Aymara = 'aym'; + case Sanaani_Arabic = 'ayn'; + case Ayoreo = 'ayo'; + case North_Mesopotamian_Arabic = 'ayp'; + case Ayi_Papua_New_Guinea = 'ayq'; + case Central_Aymara = 'ayr'; + case Sorsogon_Ayta = 'ays'; + case Magbukun_Ayta = 'ayt'; + case Ayu = 'ayu'; + case Mai_Brat = 'ayz'; + case Azha = 'aza'; + case South_Azerbaijani = 'azb'; + case Eastern_Durango_Nahuatl = 'azd'; + case Azerbaijani = 'aze'; + case San_Pedro_Amuzgos_Amuzgo = 'azg'; + case North_Azerbaijani = 'azj'; + case Ipalapa_Amuzgo = 'azm'; + case Western_Durango_Nahuatl = 'azn'; + case Awing = 'azo'; + case Faire_Atta = 'azt'; + case Highland_Puebla_Nahuatl = 'azz'; + case Babatana = 'baa'; + case Bainouk_Gunyuno = 'bab'; + case Badui = 'bac'; + case Bare = 'bae'; + case Nubaca = 'baf'; + case Tuki = 'bag'; + case Bahamas_Creole_English = 'bah'; + case Barakai = 'baj'; + case Bashkir = 'bak'; + case Baluchi = 'bal'; + case Bambara = 'bam'; + case Balinese = 'ban'; + case Waimaha = 'bao'; + case Bantawa = 'bap'; + case Bavarian = 'bar'; + case Basa_Cameroon = 'bas'; + case Bada_Nigeria = 'bau'; + case Vengo = 'bav'; + case Bambili_Bambui = 'baw'; + case Bamun = 'bax'; + case Batuley = 'bay'; + case Baatonum = 'bba'; + case Barai = 'bbb'; + case Batak_Toba = 'bbc'; + case Bau = 'bbd'; + case Bangba = 'bbe'; + case Baibai = 'bbf'; + case Barama = 'bbg'; + case Bugan = 'bbh'; + case Barombi = 'bbi'; + case Ghomala = 'bbj'; + case Babanki = 'bbk'; + case Bats = 'bbl'; + case Babango = 'bbm'; + case Uneapa = 'bbn'; + case Northern_Bobo_Madare = 'bbo'; + case West_Central_Banda = 'bbp'; + case Bamali = 'bbq'; + case Girawa = 'bbr'; + case Bakpinka = 'bbs'; + case Mburku = 'bbt'; + case Kulung_Nigeria = 'bbu'; + case Karnai = 'bbv'; + case Baba = 'bbw'; + case Bubia = 'bbx'; + case Befang = 'bby'; + case Central_Bai = 'bca'; + case Bainouk_Samik = 'bcb'; + case Southern_Balochi = 'bcc'; + case North_Babar = 'bcd'; + case Bamenyam = 'bce'; + case Bamu = 'bcf'; + case Baga_Pokur = 'bcg'; + case Bariai = 'bch'; + case Baoule = 'bci'; + case Bardi = 'bcj'; + case Bunuba = 'bck'; + case Central_Bikol = 'bcl'; + case Bannoni = 'bcm'; + case Bali_Nigeria = 'bcn'; + case Kaluli = 'bco'; + case Bali_Democratic_Republic_of_Congo = 'bcp'; + case Bench = 'bcq'; + case Babine = 'bcr'; + case Kohumono = 'bcs'; + case Bendi = 'bct'; + case Awad_Bing = 'bcu'; + case Shoo_Minda_Nye = 'bcv'; + case Bana = 'bcw'; + case Bacama = 'bcy'; + case Bainouk_Gunyaamolo = 'bcz'; + case Bayot = 'bda'; + case Basap = 'bdb'; + case Embera_Baudo = 'bdc'; + case Bunama = 'bdd'; + case Bade = 'bde'; + case Biage = 'bdf'; + case Bonggi = 'bdg'; + case Baka_South_Sudan = 'bdh'; + case Burun = 'bdi'; + case Bai_South_Sudan = 'bdj'; + case Budukh = 'bdk'; + case Indonesian_Bajau = 'bdl'; + case Buduma = 'bdm'; + case Baldemu = 'bdn'; + case Morom = 'bdo'; + case Bende = 'bdp'; + case Bahnar = 'bdq'; + case West_Coast_Bajau = 'bdr'; + case Burunge = 'bds'; + case Bokoto = 'bdt'; + case Oroko = 'bdu'; + case Bodo_Parja = 'bdv'; + case Baham = 'bdw'; + case Budong_Budong = 'bdx'; + case Bandjalang = 'bdy'; + case Badeshi = 'bdz'; + case Beaver = 'bea'; + case Bebele = 'beb'; + case Iceve_Maci = 'bec'; + case Bedoanas = 'bed'; + case Byangsi = 'bee'; + case Benabena = 'bef'; + case Belait = 'beg'; + case Biali = 'beh'; + case Bekati = 'bei'; + case Beja = 'bej'; + case Bebeli = 'bek'; + case Belarusian = 'bel'; + case Bemba_Zambia = 'bem'; + case Bengali = 'ben'; + case Beami = 'beo'; + case Besoa = 'bep'; + case Beembe = 'beq'; + case Besme = 'bes'; + case Guiberoua_Bete = 'bet'; + case Blagar = 'beu'; + case Daloa_Bete = 'bev'; + case Betawi = 'bew'; + case Jur_Modo = 'bex'; + case Beli_Papua_New_Guinea = 'bey'; + case Bena_Tanzania = 'bez'; + case Bari = 'bfa'; + case Pauri_Bareli = 'bfb'; + case Panyi_Bai = 'bfc'; + case Bafut = 'bfd'; + case Betaf = 'bfe'; + case Bofi = 'bff'; + case Busang_Kayan = 'bfg'; + case Blafe = 'bfh'; + case British_Sign_Language = 'bfi'; + case Bafanji = 'bfj'; + case Ban_Khor_Sign_Language = 'bfk'; + case Banda_Ndele = 'bfl'; + case Mmen = 'bfm'; + case Bunak = 'bfn'; + case Malba_Birifor = 'bfo'; + case Beba = 'bfp'; + case Badaga = 'bfq'; + case Bazigar = 'bfr'; + case Southern_Bai = 'bfs'; + case Balti = 'bft'; + case Gahri = 'bfu'; + case Bondo = 'bfw'; + case Bantayanon = 'bfx'; + case Bagheli = 'bfy'; + case Mahasu_Pahari = 'bfz'; + case Gwamhi_Wuri = 'bga'; + case Bobongko = 'bgb'; + case Haryanvi = 'bgc'; + case Rathwi_Bareli = 'bgd'; + case Bauria = 'bge'; + case Bangandu = 'bgf'; + case Bugun = 'bgg'; + case Giangan = 'bgi'; + case Bangolan = 'bgj'; + case Bit = 'bgk'; + case Bo_Laos = 'bgl'; + case Western_Balochi = 'bgn'; + case Baga_Koga = 'bgo'; + case Eastern_Balochi = 'bgp'; + case Bagri = 'bgq'; + case Bawm_Chin = 'bgr'; + case Tagabawa = 'bgs'; + case Bughotu = 'bgt'; + case Mbongno = 'bgu'; + case Warkay_Bipim = 'bgv'; + case Bhatri = 'bgw'; + case Balkan_Gagauz_Turkish = 'bgx'; + case Benggoi = 'bgy'; + case Banggai = 'bgz'; + case Bharia = 'bha'; + case Bhili = 'bhb'; + case Biga = 'bhc'; + case Bhadrawahi = 'bhd'; + case Bhaya = 'bhe'; + case Odiai = 'bhf'; + case Binandere = 'bhg'; + case Bukharic = 'bhh'; + case Bhilali = 'bhi'; + case Bahing = 'bhj'; + case Bimin = 'bhl'; + case Bathari = 'bhm'; + case Bohtan_Neo_Aramaic = 'bhn'; + case Bhojpuri = 'bho'; + case Bima = 'bhp'; + case Tukang_Besi_South = 'bhq'; + case Bara_Malagasy = 'bhr'; + case Buwal = 'bhs'; + case Bhattiyali = 'bht'; + case Bhunjia = 'bhu'; + case Bahau = 'bhv'; + case Biak = 'bhw'; + case Bhalay = 'bhx'; + case Bhele = 'bhy'; + case Bada_Indonesia = 'bhz'; + case Badimaya = 'bia'; + case Bissa = 'bib'; + case Bidiyo = 'bid'; + case Bepour = 'bie'; + case Biafada = 'bif'; + case Biangai = 'big'; + case Bikol = 'bik'; + case Bile = 'bil'; + case Bimoba = 'bim'; + case Bini = 'bin'; + case Nai = 'bio'; + case Bila = 'bip'; + case Bipi = 'biq'; + case Bisorio = 'bir'; + case Bislama = 'bis'; + case Berinomo = 'bit'; + case Biete = 'biu'; + case Southern_Birifor = 'biv'; + case Kol_Cameroon = 'biw'; + case Bijori = 'bix'; + case Birhor = 'biy'; + case Baloi = 'biz'; + case Budza = 'bja'; + case Banggarla = 'bjb'; + case Bariji = 'bjc'; + case Biao_Jiao_Mien = 'bje'; + case Barzani_Jewish_Neo_Aramaic = 'bjf'; + case Bidyogo = 'bjg'; + case Bahinemo = 'bjh'; + case Burji = 'bji'; + case Kanauji = 'bjj'; + case Barok = 'bjk'; + case Bulu_Papua_New_Guinea = 'bjl'; + case Bajelani = 'bjm'; + case Banjar = 'bjn'; + case Mid_Southern_Banda = 'bjo'; + case Fanamaket = 'bjp'; + case Binumarien = 'bjr'; + case Bajan = 'bjs'; + case Balanta_Ganja = 'bjt'; + case Busuu = 'bju'; + case Bedjond = 'bjv'; + case Bakwe = 'bjw'; + case Banao_Itneg = 'bjx'; + case Bayali = 'bjy'; + case Baruga = 'bjz'; + case Kyak = 'bka'; + case Baka_Cameroon = 'bkc'; + case Binukid = 'bkd'; + case Beeke = 'bkf'; + case Buraka = 'bkg'; + case Bakoko = 'bkh'; + case Baki = 'bki'; + case Pande = 'bkj'; + case Brokskat = 'bkk'; + case Berik = 'bkl'; + case Kom_Cameroon = 'bkm'; + case Bukitan = 'bkn'; + case Kwa = 'bko'; + case Boko_Democratic_Republic_of_Congo = 'bkp'; + case Bakairi = 'bkq'; + case Bakumpai = 'bkr'; + case Northern_Sorsoganon = 'bks'; + case Boloki = 'bkt'; + case Buhid = 'bku'; + case Bekwarra = 'bkv'; + case Bekwel = 'bkw'; + case Baikeno = 'bkx'; + case Bokyi = 'bky'; + case Bungku = 'bkz'; + case Siksika = 'bla'; + case Bilua = 'blb'; + case Bella_Coola = 'blc'; + case Bolango = 'bld'; + case Balanta_Kentohe = 'ble'; + case Buol = 'blf'; + case Kuwaa = 'blh'; + case Bolia = 'bli'; + case Bolongan = 'blj'; + case Pa_o_Karen = 'blk'; + case Biloxi = 'bll'; + case Beli_South_Sudan = 'blm'; + case Southern_Catanduanes_Bikol = 'bln'; + case Anii = 'blo'; + case Blablanga = 'blp'; + case Baluan_Pam = 'blq'; + case Blang = 'blr'; + case Balaesang = 'bls'; + case Tai_Dam = 'blt'; + case Kibala = 'blv'; + case Balangao = 'blw'; + case Mag_Indi_Ayta = 'blx'; + case Notre = 'bly'; + case Balantak = 'blz'; + case Lame = 'bma'; + case Bembe = 'bmb'; + case Biem = 'bmc'; + case Baga_Manduri = 'bmd'; + case Limassa = 'bme'; + case Bom_Kim = 'bmf'; + case Bamwe = 'bmg'; + case Kein = 'bmh'; + case Bagirmi = 'bmi'; + case Bote_Majhi = 'bmj'; + case Ghayavi = 'bmk'; + case Bomboli = 'bml'; + case Northern_Betsimisaraka_Malagasy = 'bmm'; + case Bina_Papua_New_Guinea = 'bmn'; + case Bambalang = 'bmo'; + case Bulgebi = 'bmp'; + case Bomu = 'bmq'; + case Muinane = 'bmr'; + case Bilma_Kanuri = 'bms'; + case Biao_Mon = 'bmt'; + case Somba_Siawari = 'bmu'; + case Bum = 'bmv'; + case Bomwali = 'bmw'; + case Baimak = 'bmx'; + case Baramu = 'bmz'; + case Bonerate = 'bna'; + case Bookan = 'bnb'; + case Bontok = 'bnc'; + case Banda_Indonesia = 'bnd'; + case Bintauna = 'bne'; + case Masiwang = 'bnf'; + case Benga = 'bng'; + case Bangi = 'bni'; + case Eastern_Tawbuid = 'bnj'; + case Bierebo = 'bnk'; + case Boon = 'bnl'; + case Batanga = 'bnm'; + case Bunun = 'bnn'; + case Bantoanon = 'bno'; + case Bola = 'bnp'; + case Bantik = 'bnq'; + case Butmas_Tur = 'bnr'; + case Bundeli = 'bns'; + case Bentong = 'bnu'; + case Bonerif = 'bnv'; + case Bisis = 'bnw'; + case Bangubangu = 'bnx'; + case Bintulu = 'bny'; + case Beezen = 'bnz'; + case Bora = 'boa'; + case Aweer = 'bob'; + case Tibetan = 'bod'; + case Mundabli = 'boe'; + case Bolon = 'bof'; + case Bamako_Sign_Language = 'bog'; + case Boma = 'boh'; + case Barbareno = 'boi'; + case Anjam = 'boj'; + case Bonjo = 'bok'; + case Bole = 'bol'; + case Berom = 'bom'; + case Bine = 'bon'; + case Tiemacewe_Bozo = 'boo'; + case Bonkiman = 'bop'; + case Bogaya = 'boq'; + case Bororo = 'bor'; + case Bosnian = 'bos'; + case Bongo = 'bot'; + case Bondei = 'bou'; + case Tuwuli = 'bov'; + case Rema = 'bow'; + case Buamu = 'box'; + case Bodo_Central_African_Republic = 'boy'; + case Tieyaxo_Bozo = 'boz'; + case Daakaka = 'bpa'; + case Mbuk = 'bpc'; + case Banda_Banda = 'bpd'; + case Bauni = 'bpe'; + case Bonggo = 'bpg'; + case Botlikh = 'bph'; + case Bagupi = 'bpi'; + case Binji = 'bpj'; + case Orowe = 'bpk'; + case Broome_Pearling_Lugger_Pidgin = 'bpl'; + case Biyom = 'bpm'; + case Dzao_Min = 'bpn'; + case Anasi = 'bpo'; + case Kaure = 'bpp'; + case Banda_Malay = 'bpq'; + case Koronadal_Blaan = 'bpr'; + case Sarangani_Blaan = 'bps'; + case Barrow_Point = 'bpt'; + case Bongu = 'bpu'; + case Bian_Marind = 'bpv'; + case Bo_Papua_New_Guinea = 'bpw'; + case Palya_Bareli = 'bpx'; + case Bishnupriya = 'bpy'; + case Bilba = 'bpz'; + case Tchumbuli = 'bqa'; + case Bagusa = 'bqb'; + case Boko_Benin = 'bqc'; + case Bung = 'bqd'; + case Baga_Kaloum = 'bqf'; + case Bago_Kusuntu = 'bqg'; + case Baima = 'bqh'; + case Bakhtiari = 'bqi'; + case Bandial = 'bqj'; + case Banda_Mbres = 'bqk'; + case Bilakura = 'bql'; + case Wumboko = 'bqm'; + case Bulgarian_Sign_Language = 'bqn'; + case Balo = 'bqo'; + case Busa = 'bqp'; + case Biritai = 'bqq'; + case Burusu = 'bqr'; + case Bosngun = 'bqs'; + case Bamukumbit = 'bqt'; + case Boguru = 'bqu'; + case Koro_Wachi = 'bqv'; + case Buru_Nigeria = 'bqw'; + case Baangi = 'bqx'; + case Bengkala_Sign_Language = 'bqy'; + case Bakaka = 'bqz'; + case Braj = 'bra'; + case Brao = 'brb'; + case Berbice_Creole_Dutch = 'brc'; + case Baraamu = 'brd'; + case Breton = 'bre'; + case Bira = 'brf'; + case Baure = 'brg'; + case Brahui = 'brh'; + case Mokpwe = 'bri'; + case Bieria = 'brj'; + case Birked = 'brk'; + case Birwa = 'brl'; + case Barambu = 'brm'; + case Boruca = 'brn'; + case Brokkat = 'bro'; + case Barapasi = 'brp'; + case Breri = 'brq'; + case Birao = 'brr'; + case Baras = 'brs'; + case Bitare = 'brt'; + case Eastern_Bru = 'bru'; + case Western_Bru = 'brv'; + case Bellari = 'brw'; + case Bodo_India = 'brx'; + case Burui = 'bry'; + case Bilbil = 'brz'; + case Abinomn = 'bsa'; + case Brunei_Bisaya = 'bsb'; + case Bassari = 'bsc'; + case Wushi = 'bse'; + case Bauchi = 'bsf'; + case Bashkardi = 'bsg'; + case Kati = 'bsh'; + case Bassossi = 'bsi'; + case Bangwinji = 'bsj'; + case Burushaski = 'bsk'; + case Basa_Gumna = 'bsl'; + case Busami = 'bsm'; + case Barasana_Eduria = 'bsn'; + case Buso = 'bso'; + case Baga_Sitemu = 'bsp'; + case Bassa = 'bsq'; + case Bassa_Kontagora = 'bsr'; + case Akoose = 'bss'; + case Basketo = 'bst'; + case Bahonsuai = 'bsu'; + case Baga_Sobane = 'bsv'; + case Baiso = 'bsw'; + case Yangkam = 'bsx'; + case Sabah_Bisaya = 'bsy'; + case Bata = 'bta'; + case Bati_Cameroon = 'btc'; + case Batak_Dairi = 'btd'; + case Gamo_Ningi = 'bte'; + case Birgit = 'btf'; + case Gagnoa_Bete = 'btg'; + case Biatah_Bidayuh = 'bth'; + case Burate = 'bti'; + case Bacanese_Malay = 'btj'; + case Batak_Mandailing = 'btm'; + case Ratagnon = 'btn'; + case Rinconada_Bikol = 'bto'; + case Budibud = 'btp'; + case Batek = 'btq'; + case Baetora = 'btr'; + case Batak_Simalungun = 'bts'; + case Bete_Bendi = 'btt'; + case Batu = 'btu'; + case Bateri = 'btv'; + case Butuanon = 'btw'; + case Batak_Karo = 'btx'; + case Bobot = 'bty'; + case Batak_Alas_Kluet = 'btz'; + case Buriat = 'bua'; + case Bua = 'bub'; + case Bushi = 'buc'; + case Ntcham = 'bud'; + case Beothuk = 'bue'; + case Bushoong = 'buf'; + case Buginese = 'bug'; + case Younuo_Bunu = 'buh'; + case Bongili = 'bui'; + case Basa_Gurmana = 'buj'; + case Bugawac = 'buk'; + case Bulgarian = 'bul'; + case Bulu_Cameroon = 'bum'; + case Sherbro = 'bun'; + case Terei = 'buo'; + case Busoa = 'bup'; + case Brem = 'buq'; + case Bokobaru = 'bus'; + case Bungain = 'but'; + case Budu = 'buu'; + case Bun = 'buv'; + case Bubi = 'buw'; + case Boghom = 'bux'; + case Bullom_So = 'buy'; + case Bukwen = 'buz'; + case Barein = 'bva'; + case Bube = 'bvb'; + case Baelelea = 'bvc'; + case Baeggu = 'bvd'; + case Berau_Malay = 'bve'; + case Boor = 'bvf'; + case Bonkeng = 'bvg'; + case Bure = 'bvh'; + case Belanda_Viri = 'bvi'; + case Baan = 'bvj'; + case Bukat = 'bvk'; + case Bolivian_Sign_Language = 'bvl'; + case Bamunka = 'bvm'; + case Buna = 'bvn'; + case Bolgo = 'bvo'; + case Bumang = 'bvp'; + case Birri = 'bvq'; + case Burarra = 'bvr'; + case Bati_Indonesia = 'bvt'; + case Bukit_Malay = 'bvu'; + case Baniva = 'bvv'; + case Boga = 'bvw'; + case Dibole = 'bvx'; + case Baybayanon = 'bvy'; + case Bauzi = 'bvz'; + case Bwatoo = 'bwa'; + case Namosi_Naitasiri_Serua = 'bwb'; + case Bwile = 'bwc'; + case Bwaidoka = 'bwd'; + case Bwe_Karen = 'bwe'; + case Boselewa = 'bwf'; + case Barwe = 'bwg'; + case Bishuo = 'bwh'; + case Baniwa = 'bwi'; + case Laa_Laa_Bwamu = 'bwj'; + case Bauwaki = 'bwk'; + case Bwela = 'bwl'; + case Biwat = 'bwm'; + case Wunai_Bunu = 'bwn'; + case Boro_Ethiopia = 'bwo'; + case Mandobo_Bawah = 'bwp'; + case Southern_Bobo_Madare = 'bwq'; + case Bura_Pabir = 'bwr'; + case Bomboma = 'bws'; + case Bafaw_Balong = 'bwt'; + case Buli_Ghana = 'bwu'; + case Bwa = 'bww'; + case Bu_Nao_Bunu = 'bwx'; + case Cwi_Bwamu = 'bwy'; + case Bwisi = 'bwz'; + case Tairaha = 'bxa'; + case Belanda_Bor = 'bxb'; + case Molengue = 'bxc'; + case Pela = 'bxd'; + case Birale = 'bxe'; + case Bilur = 'bxf'; + case Bangala = 'bxg'; + case Buhutu = 'bxh'; + case Pirlatapa = 'bxi'; + case Bayungu = 'bxj'; + case Bukusu = 'bxk'; + case Jalkunan = 'bxl'; + case Mongolia_Buriat = 'bxm'; + case Burduna = 'bxn'; + case Barikanchi = 'bxo'; + case Bebil = 'bxp'; + case Beele = 'bxq'; + case Russia_Buriat = 'bxr'; + case Busam = 'bxs'; + case China_Buriat = 'bxu'; + case Berakou = 'bxv'; + case Bankagooma = 'bxw'; + case Binahari = 'bxz'; + case Batak = 'bya'; + case Bikya = 'byb'; + case Ubaghara = 'byc'; + case Benyadu = 'byd'; + case Pouye = 'bye'; + case Bete = 'byf'; + case Baygo = 'byg'; + case Bhujel = 'byh'; + case Buyu = 'byi'; + case Bina_Nigeria = 'byj'; + case Biao = 'byk'; + case Bayono = 'byl'; + case Bidjara = 'bym'; + case Bilin = 'byn'; + case Biyo = 'byo'; + case Bumaji = 'byp'; + case Basay = 'byq'; + case Baruya = 'byr'; + case Burak = 'bys'; + case Berti = 'byt'; + case Medumba = 'byv'; + case Belhariya = 'byw'; + case Qaqet = 'byx'; + case Banaro = 'byz'; + case Bandi = 'bza'; + case Andio = 'bzb'; + case Southern_Betsimisaraka_Malagasy = 'bzc'; + case Bribri = 'bzd'; + case Jenaama_Bozo = 'bze'; + case Boikin = 'bzf'; + case Babuza = 'bzg'; + case Mapos_Buang = 'bzh'; + case Bisu = 'bzi'; + case Belize_Kriol_English = 'bzj'; + case Nicaragua_Creole_English = 'bzk'; + case Boano_Sulawesi = 'bzl'; + case Bolondo = 'bzm'; + case Boano_Maluku = 'bzn'; + case Bozaba = 'bzo'; + case Kemberano = 'bzp'; + case Buli_Indonesia = 'bzq'; + case Biri = 'bzr'; + case Brazilian_Sign_Language = 'bzs'; + case Brithenig = 'bzt'; + case Burmeso = 'bzu'; + case Naami = 'bzv'; + case Basa_Nigeria = 'bzw'; + case Kelengaxo_Bozo = 'bzx'; + case Obanliku = 'bzy'; + case Evant = 'bzz'; + case Chorti = 'caa'; + case Garifuna = 'cab'; + case Chuj = 'cac'; + case Caddo = 'cad'; + case Lehar = 'cae'; + case Southern_Carrier = 'caf'; + case Nivacle = 'cag'; + case Cahuarano = 'cah'; + case Chane = 'caj'; + case Kaqchikel = 'cak'; + case Carolinian = 'cal'; + case Cemuhi = 'cam'; + case Chambri = 'can'; + case Chacobo = 'cao'; + case Chipaya = 'cap'; + case Car_Nicobarese = 'caq'; + case Galibi_Carib = 'car'; + case Tsimane = 'cas'; + case Catalan = 'cat'; + case Cavinena = 'cav'; + case Callawalla = 'caw'; + case Chiquitano = 'cax'; + case Cayuga = 'cay'; + case Canichana = 'caz'; + case Cabiyari = 'cbb'; + case Carapana = 'cbc'; + case Carijona = 'cbd'; + case Chimila = 'cbg'; + case Chachi = 'cbi'; + case Ede_Cabe = 'cbj'; + case Chavacano = 'cbk'; + case Bualkhaw_Chin = 'cbl'; + case Nyahkur = 'cbn'; + case Izora = 'cbo'; + case Tsucuba = 'cbq'; + case Cashibo_Cacataibo = 'cbr'; + case Cashinahua = 'cbs'; + case Chayahuita = 'cbt'; + case Candoshi_Shapra = 'cbu'; + case Cacua = 'cbv'; + case Kinabalian = 'cbw'; + case Carabayo = 'cby'; + case Chamicuro = 'ccc'; + case Cafundo_Creole = 'ccd'; + case Chopi = 'cce'; + case Samba_Daka = 'ccg'; + case Atsam = 'cch'; + case Kasanga = 'ccj'; + case Cutchi_Swahili = 'ccl'; + case Malaccan_Creole_Malay = 'ccm'; + case Comaltepec_Chinantec = 'cco'; + case Chakma = 'ccp'; + case Cacaopera = 'ccr'; + case Choni = 'cda'; + case Chenchu = 'cde'; + case Chiru = 'cdf'; + case Chambeali = 'cdh'; + case Chodri = 'cdi'; + case Churahi = 'cdj'; + case Chepang = 'cdm'; + case Chaudangsi = 'cdn'; + case Min_Dong_Chinese = 'cdo'; + case Cinda_Regi_Tiyal = 'cdr'; + case Chadian_Sign_Language = 'cds'; + case Chadong = 'cdy'; + case Koda = 'cdz'; + case Lower_Chehalis = 'cea'; + case Cebuano = 'ceb'; + case Chamacoco = 'ceg'; + case Eastern_Khumi_Chin = 'cek'; + case Cen = 'cen'; + case Czech = 'ces'; + case Centuum = 'cet'; + case Ekai_Chin = 'cey'; + case Dijim_Bwilim = 'cfa'; + case Cara = 'cfd'; + case Como_Karim = 'cfg'; + case Falam_Chin = 'cfm'; + case Changriwa = 'cga'; + case Kagayanen = 'cgc'; + case Chiga = 'cgg'; + case Chocangacakha = 'cgk'; + case Chamorro = 'cha'; + case Chibcha = 'chb'; + case Catawba = 'chc'; + case Highland_Oaxaca_Chontal = 'chd'; + case Chechen = 'che'; + case Tabasco_Chontal = 'chf'; + case Chagatai = 'chg'; + case Chinook = 'chh'; + case Ojitlan_Chinantec = 'chj'; + case Chuukese = 'chk'; + case Cahuilla = 'chl'; + case Mari_Russia = 'chm'; + case Chinook_jargon = 'chn'; + case Choctaw = 'cho'; + case Chipewyan = 'chp'; + case Quiotepec_Chinantec = 'chq'; + case Cherokee = 'chr'; + case Cholon = 'cht'; + case Church_Slavic = 'chu'; + case Chuvash = 'chv'; + case Chuwabu = 'chw'; + case Chantyal = 'chx'; + case Cheyenne = 'chy'; + case Ozumacin_Chinantec = 'chz'; + case Cia_Cia = 'cia'; + case Ci_Gbe = 'cib'; + case Chickasaw = 'cic'; + case Chimariko = 'cid'; + case Cineni = 'cie'; + case Chinali = 'cih'; + case Chitkuli_Kinnauri = 'cik'; + case Cimbrian = 'cim'; + case Cinta_Larga = 'cin'; + case Chiapanec = 'cip'; + case Tiri = 'cir'; + case Chippewa = 'ciw'; + case Chaima = 'ciy'; + case Western_Cham = 'cja'; + case Chru = 'cje'; + case Upper_Chehalis = 'cjh'; + case Chamalal = 'cji'; + case Chokwe = 'cjk'; + case Eastern_Cham = 'cjm'; + case Chenapian = 'cjn'; + case Asheninka_Pajonal = 'cjo'; + case Cabecar = 'cjp'; + case Shor = 'cjs'; + case Chuave = 'cjv'; + case Jinyu_Chinese = 'cjy'; + case Central_Kurdish = 'ckb'; + case Chak = 'ckh'; + case Cibak = 'ckl'; + case Chakavian = 'ckm'; + case Kaang_Chin = 'ckn'; + case Anufo = 'cko'; + case Kajakse = 'ckq'; + case Kairak = 'ckr'; + case Tayo = 'cks'; + case Chukot = 'ckt'; + case Koasati = 'cku'; + case Kavalan = 'ckv'; + case Caka = 'ckx'; + case Cakfem_Mushere = 'cky'; + case Cakchiquel_Quiche_Mixed_Language = 'ckz'; + case Ron = 'cla'; + case Chilcotin = 'clc'; + case Chaldean_Neo_Aramaic = 'cld'; + case Lealao_Chinantec = 'cle'; + case Chilisso = 'clh'; + case Chakali = 'cli'; + case Laitu_Chin = 'clj'; + case Idu_Mishmi = 'clk'; + case Chala = 'cll'; + case Clallam = 'clm'; + case Lowland_Oaxaca_Chontal = 'clo'; + case Classical_Sanskrit = 'cls'; + case Lautu_Chin = 'clt'; + case Caluyanun = 'clu'; + case Chulym = 'clw'; + case Eastern_Highland_Chatino = 'cly'; + case Maa = 'cma'; + case Cerma = 'cme'; + case Classical_Mongolian = 'cmg'; + case Embera_Chami = 'cmi'; + case Campalagian = 'cml'; + case Michigamea = 'cmm'; + case Mandarin_Chinese = 'cmn'; + case Central_Mnong = 'cmo'; + case Mro_Khimi_Chin = 'cmr'; + case Messapic = 'cms'; + case Camtho = 'cmt'; + case Changthang = 'cna'; + case Chinbon_Chin = 'cnb'; + case Coong = 'cnc'; + case Northern_Qiang = 'cng'; + case Hakha_Chin = 'cnh'; + case Ashaninka = 'cni'; + case Khumi_Chin = 'cnk'; + case Lalana_Chinantec = 'cnl'; + case Con = 'cno'; + case Northern_Ping_Chinese = 'cnp'; + case Chung = 'cnq'; + case Montenegrin = 'cnr'; + case Central_Asmat = 'cns'; + case Tepetotutla_Chinantec = 'cnt'; + case Chenoua = 'cnu'; + case Ngawn_Chin = 'cnw'; + case Middle_Cornish = 'cnx'; + case Cocos_Islands_Malay = 'coa'; + case Chicomuceltec = 'cob'; + case Cocopa = 'coc'; + case Cocama_Cocamilla = 'cod'; + case Koreguaje = 'coe'; + case Colorado = 'cof'; + case Chong = 'cog'; + case Chonyi_Dzihana_Kauma = 'coh'; + case Cochimi = 'coj'; + case Santa_Teresa_Cora = 'cok'; + case Columbia_Wenatchi = 'col'; + case Comanche = 'com'; + case Cofan = 'con'; + case Comox = 'coo'; + case Coptic = 'cop'; + case Coquille = 'coq'; + case Cornish = 'cor'; + case Corsican = 'cos'; + case Caquinte = 'cot'; + case Wamey = 'cou'; + case Cao_Miao = 'cov'; + case Cowlitz = 'cow'; + case Nanti = 'cox'; + case Chochotec = 'coz'; + case Palantla_Chinantec = 'cpa'; + case Ucayali_Yurua_Asheninka = 'cpb'; + case Ajyininka_Apurucayali = 'cpc'; + case Cappadocian_Greek = 'cpg'; + case Chinese_Pidgin_English = 'cpi'; + case Cherepon = 'cpn'; + case Kpeego = 'cpo'; + case Capiznon = 'cps'; + case Pichis_Asheninka = 'cpu'; + case Pu_Xian_Chinese = 'cpx'; + case South_Ucayali_Asheninka = 'cpy'; + case Chuanqiandian_Cluster_Miao = 'cqd'; + case Chara = 'cra'; + case Island_Carib = 'crb'; + case Lonwolwol = 'crc'; + case Coeur_d_Alene = 'crd'; + case Cree = 'cre'; + case Caramanta = 'crf'; + case Michif = 'crg'; + case Crimean_Tatar = 'crh'; + case Saotomense = 'cri'; + case Southern_East_Cree = 'crj'; + case Plains_Cree = 'crk'; + case Northern_East_Cree = 'crl'; + case Moose_Cree = 'crm'; + case El_Nayar_Cora = 'crn'; + case Crow = 'cro'; + case Iyo_wujwa_Chorote = 'crq'; + case Carolina_Algonquian = 'crr'; + case Seselwa_Creole_French = 'crs'; + case Iyojwa_ja_Chorote = 'crt'; + case Chaura = 'crv'; + case Chrau = 'crw'; + case Carrier = 'crx'; + case Cori = 'cry'; + case Cruzeno = 'crz'; + case Chiltepec_Chinantec = 'csa'; + case Kashubian = 'csb'; + case Catalan_Sign_Language = 'csc'; + case Chiangmai_Sign_Language = 'csd'; + case Czech_Sign_Language = 'cse'; + case Cuba_Sign_Language = 'csf'; + case Chilean_Sign_Language = 'csg'; + case Asho_Chin = 'csh'; + case Coast_Miwok = 'csi'; + case Songlai_Chin = 'csj'; + case Jola_Kasa = 'csk'; + case Chinese_Sign_Language = 'csl'; + case Central_Sierra_Miwok = 'csm'; + case Colombian_Sign_Language = 'csn'; + case Sochiapam_Chinantec = 'cso'; + case Southern_Ping_Chinese = 'csp'; + case Croatia_Sign_Language = 'csq'; + case Costa_Rican_Sign_Language = 'csr'; + case Southern_Ohlone = 'css'; + case Northern_Ohlone = 'cst'; + case Sumtu_Chin = 'csv'; + case Swampy_Cree = 'csw'; + case Cambodian_Sign_Language = 'csx'; + case Siyin_Chin = 'csy'; + case Coos = 'csz'; + case Tataltepec_Chatino = 'cta'; + case Chetco = 'ctc'; + case Tedim_Chin = 'ctd'; + case Tepinapa_Chinantec = 'cte'; + case Chittagonian = 'ctg'; + case Thaiphum_Chin = 'cth'; + case Tlacoatzintepec_Chinantec = 'ctl'; + case Chitimacha = 'ctm'; + case Chhintange = 'ctn'; + case Embera_Catio = 'cto'; + case Western_Highland_Chatino = 'ctp'; + case Northern_Catanduanes_Bikol = 'cts'; + case Wayanad_Chetti = 'ctt'; + case Chol = 'ctu'; + case Moundadan_Chetty = 'cty'; + case Zacatepec_Chatino = 'ctz'; + case Cua = 'cua'; + case Cubeo = 'cub'; + case Usila_Chinantec = 'cuc'; + case Chuka = 'cuh'; + case Cuiba = 'cui'; + case Mashco_Piro = 'cuj'; + case San_Blas_Kuna = 'cuk'; + case Culina = 'cul'; + case Cumanagoto = 'cuo'; + case Cupeno = 'cup'; + case Cun = 'cuq'; + case Chhulung = 'cur'; + case Teutila_Cuicatec = 'cut'; + case Tai_Ya = 'cuu'; + case Cuvok = 'cuv'; + case Chukwa = 'cuw'; + case Tepeuxila_Cuicatec = 'cux'; + case Cuitlatec = 'cuy'; + case Chug = 'cvg'; + case Valle_Nacional_Chinantec = 'cvn'; + case Kabwa = 'cwa'; + case Maindo = 'cwb'; + case Woods_Cree = 'cwd'; + case Kwere = 'cwe'; + case Chewong = 'cwg'; + case Kuwaataay = 'cwt'; + case Cha_ari = 'cxh'; + case Nopala_Chatino = 'cya'; + case Cayubaba = 'cyb'; + case Welsh = 'cym'; + case Cuyonon = 'cyo'; + case Huizhou_Chinese = 'czh'; + case Knaanic = 'czk'; + case Zenzontepec_Chatino = 'czn'; + case Min_Zhong_Chinese = 'czo'; + case Zotung_Chin = 'czt'; + case Dangaleat = 'daa'; + case Dambi = 'dac'; + case Marik = 'dad'; + case Duupa = 'dae'; + case Dagbani = 'dag'; + case Gwahatike = 'dah'; + case Day = 'dai'; + case Dar_Fur_Daju = 'daj'; + case Dakota = 'dak'; + case Dahalo = 'dal'; + case Damakawa = 'dam'; + case Danish = 'dan'; + case Daai_Chin = 'dao'; + case Dandami_Maria = 'daq'; + case Dargwa = 'dar'; + case Daho_Doo = 'das'; + case Dar_Sila_Daju = 'dau'; + case Taita = 'dav'; + case Davawenyo = 'daw'; + case Dayi = 'dax'; + case Dao = 'daz'; + case Bangime = 'dba'; + case Deno = 'dbb'; + case Dadiya = 'dbd'; + case Dabe = 'dbe'; + case Edopi = 'dbf'; + case Dogul_Dom_Dogon = 'dbg'; + case Doka = 'dbi'; + case Ida_an = 'dbj'; + case Dyirbal = 'dbl'; + case Duguri = 'dbm'; + case Duriankere = 'dbn'; + case Dulbu = 'dbo'; + case Duwai = 'dbp'; + case Daba = 'dbq'; + case Dabarre = 'dbr'; + case Ben_Tey_Dogon = 'dbt'; + case Bondum_Dom_Dogon = 'dbu'; + case Dungu = 'dbv'; + case Bankan_Tey_Dogon = 'dbw'; + case Dibiyaso = 'dby'; + case Deccan = 'dcc'; + case Negerhollands = 'dcr'; + case Dadi_Dadi = 'dda'; + case Dongotono = 'ddd'; + case Doondo = 'dde'; + case Fataluku = 'ddg'; + case West_Goodenough = 'ddi'; + case Jaru = 'ddj'; + case Dendi_Benin = 'ddn'; + case Dido = 'ddo'; + case Dhudhuroa = 'ddr'; + case Donno_So_Dogon = 'dds'; + case Dawera_Daweloor = 'ddw'; + case Dagik = 'dec'; + case Dedua = 'ded'; + case Dewoin = 'dee'; + case Dezfuli = 'def'; + case Degema = 'deg'; + case Dehwari = 'deh'; + case Demisa = 'dei'; + case Dek = 'dek'; + case Delaware = 'del'; + case Dem = 'dem'; + case Slave_Athapascan = 'den'; + case Pidgin_Delaware = 'dep'; + case Dendi_Central_African_Republic = 'deq'; + case Deori = 'der'; + case Desano = 'des'; + case German = 'deu'; + case Domung = 'dev'; + case Dengese = 'dez'; + case Southern_Dagaare = 'dga'; + case Bunoge_Dogon = 'dgb'; + case Casiguran_Dumagat_Agta = 'dgc'; + case Dagaari_Dioula = 'dgd'; + case Degenan = 'dge'; + case Doga = 'dgg'; + case Dghwede = 'dgh'; + case Northern_Dagara = 'dgi'; + case Dagba = 'dgk'; + case Andaandi = 'dgl'; + case Dagoman = 'dgn'; + case Dogri_individual_language = 'dgo'; + case Dogrib = 'dgr'; + case Dogoso = 'dgs'; + case Ndra_ngith = 'dgt'; + case Daungwurrung = 'dgw'; + case Doghoro = 'dgx'; + case Daga = 'dgz'; + case Dhundari = 'dhd'; + case Dhangu_Djangu = 'dhg'; + case Dhimal = 'dhi'; + case Dhalandji = 'dhl'; + case Zemba = 'dhm'; + case Dhanki = 'dhn'; + case Dhodia = 'dho'; + case Dhargari = 'dhr'; + case Dhaiso = 'dhs'; + case Dhurga = 'dhu'; + case Dehu = 'dhv'; + case Dhanwar_Nepal = 'dhw'; + case Dhungaloo = 'dhx'; + case Dia = 'dia'; + case South_Central_Dinka = 'dib'; + case Lakota_Dida = 'dic'; + case Didinga = 'did'; + case Dieri = 'dif'; + case Digo = 'dig'; + case Kumiai = 'dih'; + case Dimbong = 'dii'; + case Dai = 'dij'; + case Southwestern_Dinka = 'dik'; + case Dilling = 'dil'; + case Dime = 'dim'; + case Dinka = 'din'; + case Dibo = 'dio'; + case Northeastern_Dinka = 'dip'; + case Dimli_individual_language = 'diq'; + case Dirim = 'dir'; + case Dimasa = 'dis'; + case Diriku = 'diu'; + case Dhivehi = 'div'; + case Northwestern_Dinka = 'diw'; + case Dixon_Reef = 'dix'; + case Diuwe = 'diy'; + case Ding = 'diz'; + case Djadjawurrung = 'dja'; + case Djinba = 'djb'; + case Dar_Daju_Daju = 'djc'; + case Djamindjung = 'djd'; + case Zarma = 'dje'; + case Djangun = 'djf'; + case Djinang = 'dji'; + case Djeebbana = 'djj'; + case Eastern_Maroon_Creole = 'djk'; + case Jamsay_Dogon = 'djm'; + case Jawoyn = 'djn'; + case Jangkang = 'djo'; + case Djambarrpuyngu = 'djr'; + case Kapriman = 'dju'; + case Djawi = 'djw'; + case Dakpakha = 'dka'; + case Kadung = 'dkg'; + case Dakka = 'dkk'; + case Kuijau = 'dkr'; + case Southeastern_Dinka = 'dks'; + case Mazagway = 'dkx'; + case Dolgan = 'dlg'; + case Dahalik = 'dlk'; + case Dalmatian = 'dlm'; + case Darlong = 'dln'; + case Duma = 'dma'; + case Mombo_Dogon = 'dmb'; + case Gavak = 'dmc'; + case Madhi_Madhi = 'dmd'; + case Dugwor = 'dme'; + case Medefaidrin = 'dmf'; + case Upper_Kinabatangan = 'dmg'; + case Domaaki = 'dmk'; + case Dameli = 'dml'; + case Dama = 'dmm'; + case Kemedzung = 'dmo'; + case East_Damar = 'dmr'; + case Dampelas = 'dms'; + case Dubu = 'dmu'; + case Dumpas = 'dmv'; + case Mudburra = 'dmw'; + case Dema = 'dmx'; + case Demta = 'dmy'; + case Upper_Grand_Valley_Dani = 'dna'; + case Daonda = 'dnd'; + case Ndendeule = 'dne'; + case Dungan = 'dng'; + case Lower_Grand_Valley_Dani = 'dni'; + case Dan = 'dnj'; + case Dengka = 'dnk'; + case Dzuungoo = 'dnn'; + case Ndrulo = 'dno'; + case Danaru = 'dnr'; + case Mid_Grand_Valley_Dani = 'dnt'; + case Danau = 'dnu'; + case Danu = 'dnv'; + case Western_Dani = 'dnw'; + case Deni = 'dny'; + case Dom = 'doa'; + case Dobu = 'dob'; + case Northern_Dong = 'doc'; + case Doe = 'doe'; + case Domu = 'dof'; + case Dong = 'doh'; + case Dogri_macrolanguage = 'doi'; + case Dondo = 'dok'; + case Doso = 'dol'; + case Toura_Papua_New_Guinea = 'don'; + case Dongo = 'doo'; + case Lukpa = 'dop'; + case Dominican_Sign_Language = 'doq'; + case Dori_o = 'dor'; + case Dogose = 'dos'; + case Dass = 'dot'; + case Dombe = 'dov'; + case Doyayo = 'dow'; + case Bussa = 'dox'; + case Dompo = 'doy'; + case Dorze = 'doz'; + case Papar = 'dpp'; + case Dair = 'drb'; + case Minderico = 'drc'; + case Darmiya = 'drd'; + case Dolpo = 'dre'; + case Rungus = 'drg'; + case C_Lela = 'dri'; + case Paakantyi = 'drl'; + case West_Damar = 'drn'; + case Daro_Matu_Melanau = 'dro'; + case Dura = 'drq'; + case Gedeo = 'drs'; + case Drents = 'drt'; + case Rukai = 'dru'; + case Darai = 'dry'; + case Lower_Sorbian = 'dsb'; + case Dutch_Sign_Language = 'dse'; + case Daasanach = 'dsh'; + case Disa = 'dsi'; + case Dokshi = 'dsk'; + case Danish_Sign_Language = 'dsl'; + case Dusner = 'dsn'; + case Desiya = 'dso'; + case Tadaksahak = 'dsq'; + case Mardin_Sign_Language = 'dsz'; + case Daur = 'dta'; + case Labuk_Kinabatangan_Kadazan = 'dtb'; + case Ditidaht = 'dtd'; + case Adithinngithigh = 'dth'; + case Ana_Tinga_Dogon = 'dti'; + case Tene_Kan_Dogon = 'dtk'; + case Tomo_Kan_Dogon = 'dtm'; + case Daats_iin = 'dtn'; + case Tommo_So_Dogon = 'dto'; + case Kadazan_Dusun = 'dtp'; + case Lotud = 'dtr'; + case Toro_So_Dogon = 'dts'; + case Toro_Tegu_Dogon = 'dtt'; + case Tebul_Ure_Dogon = 'dtu'; + case Dotyali = 'dty'; + case Duala = 'dua'; + case Dubli = 'dub'; + case Duna = 'duc'; + case Umiray_Dumaget_Agta = 'due'; + case Dumbea = 'duf'; + case Duruma = 'dug'; + case Dungra_Bhil = 'duh'; + case Dumun = 'dui'; + case Uyajitaya = 'duk'; + case Alabat_Island_Agta = 'dul'; + case Middle_Dutch_ca_1050_1350 = 'dum'; + case Dusun_Deyah = 'dun'; + case Dupaninan_Agta = 'duo'; + case Duano = 'dup'; + case Dusun_Malang = 'duq'; + case Dii = 'dur'; + case Dumi = 'dus'; + case Drung = 'duu'; + case Duvle = 'duv'; + case Dusun_Witu = 'duw'; + case Duungooma = 'dux'; + case Dicamay_Agta = 'duy'; + case Duli_Gey = 'duz'; + case Duau = 'dva'; + case Diri = 'dwa'; + case Dawik_Kui = 'dwk'; + case Dawro = 'dwr'; + case Dutton_World_Speedwords = 'dws'; + case Dhuwal = 'dwu'; + case Dawawa = 'dww'; + case Dhuwaya = 'dwy'; + case Dewas_Rai = 'dwz'; + case Dyan = 'dya'; + case Dyaberdyaber = 'dyb'; + case Dyugun = 'dyd'; + case Villa_Viciosa_Agta = 'dyg'; + case Djimini_Senoufo = 'dyi'; + case Yanda_Dom_Dogon = 'dym'; + case Dyangadi = 'dyn'; + case Jola_Fonyi = 'dyo'; + case Dyarim = 'dyr'; + case Dyula = 'dyu'; + case Djabugay = 'dyy'; + case Tunzu = 'dza'; + case Daza = 'dzd'; + case Djiwarli = 'dze'; + case Dazaga = 'dzg'; + case Dzalakha = 'dzl'; + case Dzando = 'dzn'; + case Dzongkha = 'dzo'; + case Karenggapa = 'eaa'; + case Beginci = 'ebc'; + case Ebughu = 'ebg'; + case Eastern_Bontok = 'ebk'; + case Teke_Ebo = 'ebo'; + case Ebrie = 'ebr'; + case Embu = 'ebu'; + case Eteocretan = 'ecr'; + case Ecuadorian_Sign_Language = 'ecs'; + case Eteocypriot = 'ecy'; + case E = 'eee'; + case Efai = 'efa'; + case Efe = 'efe'; + case Efik = 'efi'; + case Ega = 'ega'; + case Emilian = 'egl'; + case Benamanga = 'egm'; + case Eggon = 'ego'; + case Egyptian_Ancient = 'egy'; + case Miyakubo_Sign_Language = 'ehs'; + case Ehueun = 'ehu'; + case Eipomek = 'eip'; + case Eitiep = 'eit'; + case Askopan = 'eiv'; + case Ejamat = 'eja'; + case Ekajuk = 'eka'; + case Ekit = 'eke'; + case Ekari = 'ekg'; + case Eki = 'eki'; + case Standard_Estonian = 'ekk'; + case Kol_Bangladesh = 'ekl'; + case Elip = 'ekm'; + case Koti = 'eko'; + case Ekpeye = 'ekp'; + case Yace = 'ekr'; + case Eastern_Kayah = 'eky'; + case Elepi = 'ele'; + case El_Hugeirat = 'elh'; + case Nding = 'eli'; + case Elkei = 'elk'; + case Modern_Greek_1453 = 'ell'; + case Eleme = 'elm'; + case El_Molo = 'elo'; + case Elu = 'elu'; + case Elamite = 'elx'; + case Emai_Iuleha_Ora = 'ema'; + case Embaloh = 'emb'; + case Emerillon = 'eme'; + case Eastern_Meohang = 'emg'; + case Mussau_Emira = 'emi'; + case Eastern_Maninkakan = 'emk'; + case Mamulique = 'emm'; + case Eman = 'emn'; + case Northern_Embera = 'emp'; + case Eastern_Minyag = 'emq'; + case Pacific_Gulf_Yupik = 'ems'; + case Eastern_Muria = 'emu'; + case Emplawas = 'emw'; + case Erromintxela = 'emx'; + case Epigraphic_Mayan = 'emy'; + case Mbessa = 'emz'; + case Apali = 'ena'; + case Markweeta = 'enb'; + case En = 'enc'; + case Ende = 'end'; + case Forest_Enets = 'enf'; + case English = 'eng'; + case Tundra_Enets = 'enh'; + case Enlhet = 'enl'; + case Middle_English_1100_1500 = 'enm'; + case Engenni = 'enn'; + case Enggano = 'eno'; + case Enga = 'enq'; + case Emumu = 'enr'; + case Enu = 'enu'; + case Enwan_Edo_State = 'env'; + case Enwan_Akwa_Ibom_State = 'enw'; + case Enxet = 'enx'; + case Beti_Cote_d_Ivoire = 'eot'; + case Epie = 'epi'; + case Esperanto = 'epo'; + case Eravallan = 'era'; + case Sie = 'erg'; + case Eruwa = 'erh'; + case Ogea = 'eri'; + case South_Efate = 'erk'; + case Horpa = 'ero'; + case Erre = 'err'; + case Ersu = 'ers'; + case Eritai = 'ert'; + case Erokwanas = 'erw'; + case Ese_Ejja = 'ese'; + case Aheri_Gondi = 'esg'; + case Eshtehardi = 'esh'; + case North_Alaskan_Inupiatun = 'esi'; + case Northwest_Alaska_Inupiatun = 'esk'; + case Egypt_Sign_Language = 'esl'; + case Esuma = 'esm'; + case Salvadoran_Sign_Language = 'esn'; + case Estonian_Sign_Language = 'eso'; + case Esselen = 'esq'; + case Central_Siberian_Yupik = 'ess'; + case Estonian = 'est'; + case Central_Yupik = 'esu'; + case Eskayan = 'esy'; + case Etebi = 'etb'; + case Etchemin = 'etc'; + case Ethiopian_Sign_Language = 'eth'; + case Eton_Vanuatu = 'etn'; + case Eton_Cameroon = 'eto'; + case Edolo = 'etr'; + case Yekhee = 'ets'; + case Etruscan = 'ett'; + case Ejagham = 'etu'; + case Eten = 'etx'; + case Semimi = 'etz'; + case Eudeve = 'eud'; + case Basque = 'eus'; + case Even = 'eve'; + case Uvbie = 'evh'; + case Evenki = 'evn'; + case Ewe = 'ewe'; + case Ewondo = 'ewo'; + case Extremaduran = 'ext'; + case Eyak = 'eya'; + case Keiyo = 'eyo'; + case Ezaa = 'eza'; + case Uzekwe = 'eze'; + case Fasu = 'faa'; + case Fa_d_Ambu = 'fab'; + case Wagi = 'fad'; + case Fagani = 'faf'; + case Finongan = 'fag'; + case Baissa_Fali = 'fah'; + case Faiwol = 'fai'; + case Faita = 'faj'; + case Fang_Cameroon = 'fak'; + case South_Fali = 'fal'; + case Fam = 'fam'; + case Fang_Equatorial_Guinea = 'fan'; + case Faroese = 'fao'; + case Paloor = 'fap'; + case Fataleka = 'far'; + case Persian = 'fas'; + case Fanti = 'fat'; + case Fayu = 'fau'; + case Fala = 'fax'; + case Southwestern_Fars = 'fay'; + case Northwestern_Fars = 'faz'; + case West_Albay_Bikol = 'fbl'; + case Quebec_Sign_Language = 'fcs'; + case Feroge = 'fer'; + case Foia_Foia = 'ffi'; + case Maasina_Fulfulde = 'ffm'; + case Fongoro = 'fgr'; + case Nobiin = 'fia'; + case Fyer = 'fie'; + case Faifi = 'fif'; + case Fijian = 'fij'; + case Filipino = 'fil'; + case Finnish = 'fin'; + case Fipa = 'fip'; + case Firan = 'fir'; + case Tornedalen_Finnish = 'fit'; + case Fiwaga = 'fiw'; + case Kirya_Konzel = 'fkk'; + case Kven_Finnish = 'fkv'; + case Kalispel_Pend_d_Oreille = 'fla'; + case Foau = 'flh'; + case Fali = 'fli'; + case North_Fali = 'fll'; + case Flinders_Island = 'fln'; + case Fuliiru = 'flr'; + case Flaaitaal = 'fly'; + case Fe_fe = 'fmp'; + case Far_Western_Muria = 'fmu'; + case Fanbak = 'fnb'; + case Fanagalo = 'fng'; + case Fania = 'fni'; + case Foodo = 'fod'; + case Foi = 'foi'; + case Foma = 'fom'; + case Fon = 'fon'; + case Fore = 'for'; + case Siraya = 'fos'; + case Fernando_Po_Creole_English = 'fpe'; + case Fas = 'fqs'; + case French = 'fra'; + case Cajun_French = 'frc'; + case Fordata = 'frd'; + case Frankish = 'frk'; + case Middle_French_ca_1400_1600 = 'frm'; + case Old_French_842_ca_1400 = 'fro'; + case Arpitan = 'frp'; + case Forak = 'frq'; + case Northern_Frisian = 'frr'; + case Eastern_Frisian = 'frs'; + case Fortsenal = 'frt'; + case Western_Frisian = 'fry'; + case Finnish_Sign_Language = 'fse'; + case French_Sign_Language = 'fsl'; + case Finland_Swedish_Sign_Language = 'fss'; + case Adamawa_Fulfulde = 'fub'; + case Pulaar = 'fuc'; + case East_Futuna = 'fud'; + case Borgu_Fulfulde = 'fue'; + case Pular = 'fuf'; + case Western_Niger_Fulfulde = 'fuh'; + case Bagirmi_Fulfulde = 'fui'; + case Ko = 'fuj'; + case Fulah = 'ful'; + case Fum = 'fum'; + case Fulnio = 'fun'; + case Central_Eastern_Niger_Fulfulde = 'fuq'; + case Friulian = 'fur'; + case Futuna_Aniwa = 'fut'; + case Furu = 'fuu'; + case Nigerian_Fulfulde = 'fuv'; + case Fuyug = 'fuy'; + case Fur = 'fvr'; + case Fwai = 'fwa'; + case Fwe = 'fwe'; + case Ga = 'gaa'; + case Gabri = 'gab'; + case Mixed_Great_Andamanese = 'gac'; + case Gaddang = 'gad'; + case Guarequena = 'gae'; + case Gende = 'gaf'; + case Gagauz = 'gag'; + case Alekano = 'gah'; + case Borei = 'gai'; + case Gadsup = 'gaj'; + case Gamkonora = 'gak'; + case Galolen = 'gal'; + case Kandawo = 'gam'; + case Gan_Chinese = 'gan'; + case Gants = 'gao'; + case Gal = 'gap'; + case Gata = 'gaq'; + case Galeya = 'gar'; + case Adiwasi_Garasia = 'gas'; + case Kenati = 'gat'; + case Mudhili_Gadaba = 'gau'; + case Nobonob = 'gaw'; + case Borana_Arsi_Guji_Oromo = 'gax'; + case Gayo = 'gay'; + case West_Central_Oromo = 'gaz'; + case Gbaya_Central_African_Republic = 'gba'; + case Kaytetye = 'gbb'; + case Karajarri = 'gbd'; + case Niksek = 'gbe'; + case Gaikundi = 'gbf'; + case Gbanziri = 'gbg'; + case Defi_Gbe = 'gbh'; + case Galela = 'gbi'; + case Bodo_Gadaba = 'gbj'; + case Gaddi = 'gbk'; + case Gamit = 'gbl'; + case Garhwali = 'gbm'; + case Mo_da = 'gbn'; + case Northern_Grebo = 'gbo'; + case Gbaya_Bossangoa = 'gbp'; + case Gbaya_Bozoum = 'gbq'; + case Gbagyi = 'gbr'; + case Gbesi_Gbe = 'gbs'; + case Gagadu = 'gbu'; + case Gbanu = 'gbv'; + case Gabi_Gabi = 'gbw'; + case Eastern_Xwla_Gbe = 'gbx'; + case Gbari = 'gby'; + case Zoroastrian_Dari = 'gbz'; + case Mali = 'gcc'; + case Ganggalida = 'gcd'; + case Galice = 'gce'; + case Guadeloupean_Creole_French = 'gcf'; + case Grenadian_Creole_English = 'gcl'; + case Gaina = 'gcn'; + case Guianese_Creole_French = 'gcr'; + case Colonia_Tovar_German = 'gct'; + case Gade_Lohar = 'gda'; + case Pottangi_Ollar_Gadaba = 'gdb'; + case Gugu_Badhun = 'gdc'; + case Gedaged = 'gdd'; + case Gude = 'gde'; + case Guduf_Gava = 'gdf'; + case Ga_dang = 'gdg'; + case Gadjerawang = 'gdh'; + case Gundi = 'gdi'; + case Gurdjar = 'gdj'; + case Gadang = 'gdk'; + case Dirasha = 'gdl'; + case Laal = 'gdm'; + case Umanakaina = 'gdn'; + case Ghodoberi = 'gdo'; + case Mehri = 'gdq'; + case Wipi = 'gdr'; + case Ghandruk_Sign_Language = 'gds'; + case Kungardutyi = 'gdt'; + case Gudu = 'gdu'; + case Godwari = 'gdx'; + case Geruma = 'gea'; + case Kire = 'geb'; + case Gboloo_Grebo = 'gec'; + case Gade = 'ged'; + case Gerai = 'gef'; + case Gengle = 'geg'; + case Hutterite_German = 'geh'; + case Gebe = 'gei'; + case Gen = 'gej'; + case Ywom = 'gek'; + case ut_Ma_in = 'gel'; + case Geme = 'geq'; + case Geser_Gorom = 'ges'; + case Eviya = 'gev'; + case Gera = 'gew'; + case Garre = 'gex'; + case Enya = 'gey'; + case Geez = 'gez'; + case Patpatar = 'gfk'; + case Gafat = 'gft'; + case Gao = 'gga'; + case Gbii = 'ggb'; + case Gugadj = 'ggd'; + case Gurr_goni = 'gge'; + case Gurgula = 'ggg'; + case Kungarakany = 'ggk'; + case Ganglau = 'ggl'; + case Gitua = 'ggt'; + case Gagu = 'ggu'; + case Gogodala = 'ggw'; + case Ghadames = 'gha'; + case Hiberno_Scottish_Gaelic = 'ghc'; + case Southern_Ghale = 'ghe'; + case Northern_Ghale = 'ghh'; + case Geko_Karen = 'ghk'; + case Ghulfan = 'ghl'; + case Ghanongga = 'ghn'; + case Ghomara = 'gho'; + case Ghera = 'ghr'; + case Guhu_Samane = 'ghs'; + case Kuke = 'ght'; + case Kija = 'gia'; + case Gibanawa = 'gib'; + case Gail = 'gic'; + case Gidar = 'gid'; + case Gabogbo = 'gie'; + case Goaria = 'gig'; + case Githabul = 'gih'; + case Girirra = 'gii'; + case Gilbertese = 'gil'; + case Gimi_Eastern_Highlands = 'gim'; + case Hinukh = 'gin'; + case Gimi_West_New_Britain = 'gip'; + case Green_Gelao = 'giq'; + case Red_Gelao = 'gir'; + case North_Giziga = 'gis'; + case Gitxsan = 'git'; + case Mulao = 'giu'; + case White_Gelao = 'giw'; + case Gilima = 'gix'; + case Giyug = 'giy'; + case South_Giziga = 'giz'; + case Kachi_Koli = 'gjk'; + case Gunditjmara = 'gjm'; + case Gonja = 'gjn'; + case Gurindji_Kriol = 'gjr'; + case Gujari = 'gju'; + case Guya = 'gka'; + case Magi_Madang_Province = 'gkd'; + case Ndai = 'gke'; + case Gokana = 'gkn'; + case Kok_Nar = 'gko'; + case Guinea_Kpelle = 'gkp'; + case Ungkue = 'gku'; + case Scottish_Gaelic = 'gla'; + case Belning = 'glb'; + case Bon_Gula = 'glc'; + case Nanai = 'gld'; + case Irish = 'gle'; + case Galician = 'glg'; + case Northwest_Pashai = 'glh'; + case Gula_Iro = 'glj'; + case Gilaki = 'glk'; + case Garlali = 'gll'; + case Galambu = 'glo'; + case Glaro_Twabo = 'glr'; + case Gula_Chad = 'glu'; + case Manx = 'glv'; + case Glavda = 'glw'; + case Gule = 'gly'; + case Gambera = 'gma'; + case Gula_alaa = 'gmb'; + case Maghdi = 'gmd'; + case Magiyi = 'gmg'; + case Middle_High_German_ca_1050_1500 = 'gmh'; + case Middle_Low_German = 'gml'; + case Gbaya_Mbodomo = 'gmm'; + case Gimnime = 'gmn'; + case Mirning = 'gmr'; + case Gumalu = 'gmu'; + case Gamo = 'gmv'; + case Magoma = 'gmx'; + case Mycenaean_Greek = 'gmy'; + case Mgbolizhia = 'gmz'; + case Kaansa = 'gna'; + case Gangte = 'gnb'; + case Guanche = 'gnc'; + case Zulgo_Gemzek = 'gnd'; + case Ganang = 'gne'; + case Ngangam = 'gng'; + case Lere = 'gnh'; + case Gooniyandi = 'gni'; + case Ngen = 'gnj'; + case Gana = 'gnk'; + case Gangulu = 'gnl'; + case Ginuman = 'gnm'; + case Gumatj = 'gnn'; + case Northern_Gondi = 'gno'; + case Gana_2 = 'gnq'; + case Gureng_Gureng = 'gnr'; + case Guntai = 'gnt'; + case Gnau = 'gnu'; + case Western_Bolivian_Guarani = 'gnw'; + case Ganzi = 'gnz'; + case Guro = 'goa'; + case Playero = 'gob'; + case Gorakor = 'goc'; + case Godie = 'god'; + case Gongduk = 'goe'; + case Gofa = 'gof'; + case Gogo = 'gog'; + case Old_High_German_ca_750_1050 = 'goh'; + case Gobasi = 'goi'; + case Gowlan = 'goj'; + case Gowli = 'gok'; + case Gola = 'gol'; + case Goan_Konkani = 'gom'; + case Gondi = 'gon'; + case Gone_Dau = 'goo'; + case Yeretuar = 'gop'; + case Gorap = 'goq'; + case Gorontalo = 'gor'; + case Gronings = 'gos'; + case Gothic = 'got'; + case Gavar = 'gou'; + case Goo = 'gov'; + case Gorowa = 'gow'; + case Gobu = 'gox'; + case Goundo = 'goy'; + case Gozarkhani = 'goz'; + case Gupa_Abawa = 'gpa'; + case Ghanaian_Pidgin_English = 'gpe'; + case Taiap = 'gpn'; + case Ga_anda = 'gqa'; + case Guiqiong = 'gqi'; + case Guana_Brazil = 'gqn'; + case Gor = 'gqr'; + case Qau = 'gqu'; + case Rajput_Garasia = 'gra'; + case Grebo = 'grb'; + case Ancient_Greek_to_1453 = 'grc'; + case Guruntum_Mbaaru = 'grd'; + case Madi = 'grg'; + case Gbiri_Niragu = 'grh'; + case Ghari = 'gri'; + case Southern_Grebo = 'grj'; + case Kota_Marudu_Talantang = 'grm'; + case Guarani = 'grn'; + case Groma = 'gro'; + case Gorovu = 'grq'; + case Taznatit = 'grr'; + case Gresi = 'grs'; + case Garo = 'grt'; + case Kistane = 'gru'; + case Central_Grebo = 'grv'; + case Gweda = 'grw'; + case Guriaso = 'grx'; + case Barclayville_Grebo = 'gry'; + case Guramalum = 'grz'; + case Ghanaian_Sign_Language = 'gse'; + case German_Sign_Language = 'gsg'; + case Gusilay = 'gsl'; + case Guatemalan_Sign_Language = 'gsm'; + case Nema = 'gsn'; + case Southwest_Gbaya = 'gso'; + case Wasembo = 'gsp'; + case Greek_Sign_Language = 'gss'; + case Swiss_German = 'gsw'; + case Guato = 'gta'; + case Aghu_Tharnggala = 'gtu'; + case Shiki = 'gua'; + case Guajajara = 'gub'; + case Wayuu = 'guc'; + case Yocoboue_Dida = 'gud'; + case Gurindji = 'gue'; + case Gupapuyngu = 'guf'; + case Paraguayan_Guarani = 'gug'; + case Guahibo = 'guh'; + case Eastern_Bolivian_Guarani = 'gui'; + case Gujarati = 'guj'; + case Gumuz = 'guk'; + case Sea_Island_Creole_English = 'gul'; + case Guambiano = 'gum'; + case Mbya_Guarani = 'gun'; + case Guayabero = 'guo'; + case Gunwinggu = 'gup'; + case Ache = 'guq'; + case Farefare = 'gur'; + case Guinean_Sign_Language = 'gus'; + case Maleku_Jaika = 'gut'; + case Yanomamo = 'guu'; + case Gun = 'guw'; + case Gourmanchema = 'gux'; + case Gusii = 'guz'; + case Guana_Paraguay = 'gva'; + case Guanano = 'gvc'; + case Duwet = 'gve'; + case Golin = 'gvf'; + case Guaja = 'gvj'; + case Gulay = 'gvl'; + case Gurmana = 'gvm'; + case Kuku_Yalanji = 'gvn'; + case Gaviao_Do_Jiparana = 'gvo'; + case Para_Gaviao = 'gvp'; + case Gurung = 'gvr'; + case Gumawana = 'gvs'; + case Guyani = 'gvy'; + case Mbato = 'gwa'; + case Gwa = 'gwb'; + case Gawri = 'gwc'; + case Gawwada = 'gwd'; + case Gweno = 'gwe'; + case Gowro = 'gwf'; + case Moo = 'gwg'; + case Gwich_in = 'gwi'; + case Gwi = 'gwj'; + case Awngthim = 'gwm'; + case Gwandara = 'gwn'; + case Gwere = 'gwr'; + case Gawar_Bati = 'gwt'; + case Guwamu = 'gwu'; + case Kwini = 'gww'; + case Gua = 'gwx'; + case We_Southern = 'gxx'; + case Northwest_Gbaya = 'gya'; + case Garus = 'gyb'; + case Kayardild = 'gyd'; + case Gyem = 'gye'; + case Gungabula = 'gyf'; + case Gbayi = 'gyg'; + case Gyele = 'gyi'; + case Gayil = 'gyl'; + case Ngabere = 'gym'; + case Guyanese_Creole_English = 'gyn'; + case Gyalsumdo = 'gyo'; + case Guarayu = 'gyr'; + case Gunya = 'gyy'; + case Geji = 'gyz'; + case Ganza = 'gza'; + case Gazi = 'gzi'; + case Gane = 'gzn'; + case Han = 'haa'; + case Hanoi_Sign_Language = 'hab'; + case Gurani = 'hac'; + case Hatam = 'had'; + case Eastern_Oromo = 'hae'; + case Haiphong_Sign_Language = 'haf'; + case Hanga = 'hag'; + case Hahon = 'hah'; + case Haida = 'hai'; + case Hajong = 'haj'; + case Hakka_Chinese = 'hak'; + case Halang = 'hal'; + case Hewa = 'ham'; + case Hangaza = 'han'; + case Hako = 'hao'; + case Hupla = 'hap'; + case Ha = 'haq'; + case Harari = 'har'; + case Haisla = 'has'; + case Haitian = 'hat'; + case Hausa = 'hau'; + case Havu = 'hav'; + case Hawaiian = 'haw'; + case Southern_Haida = 'hax'; + case Haya = 'hay'; + case Hazaragi = 'haz'; + case Hamba = 'hba'; + case Huba = 'hbb'; + case Heiban = 'hbn'; + case Ancient_Hebrew = 'hbo'; + case Serbo_Croatian = 'hbs'; + case Habu = 'hbu'; + case Andaman_Creole_Hindi = 'hca'; + case Huichol = 'hch'; + case Northern_Haida = 'hdn'; + case Honduras_Sign_Language = 'hds'; + case Hadiyya = 'hdy'; + case Northern_Qiandong_Miao = 'hea'; + case Hebrew = 'heb'; + case Herde = 'hed'; + case Helong = 'heg'; + case Hehe = 'heh'; + case Heiltsuk = 'hei'; + case Hemba = 'hem'; + case Herero = 'her'; + case Hai_om = 'hgm'; + case Haigwai = 'hgw'; + case Hoia_Hoia = 'hhi'; + case Kerak = 'hhr'; + case Hoyahoya = 'hhy'; + case Lamang = 'hia'; + case Hibito = 'hib'; + case Hidatsa = 'hid'; + case Fiji_Hindi = 'hif'; + case Kamwe = 'hig'; + case Pamosu = 'hih'; + case Hinduri = 'hii'; + case Hijuk = 'hij'; + case Seit_Kaitetu = 'hik'; + case Hiligaynon = 'hil'; + case Hindi = 'hin'; + case Tsoa = 'hio'; + case Himarima = 'hir'; + case Hittite = 'hit'; + case Hiw = 'hiw'; + case Hixkaryana = 'hix'; + case Haji = 'hji'; + case Kahe = 'hka'; + case Hunde = 'hke'; + case Khah = 'hkh'; + case Hunjara_Kaina_Ke = 'hkk'; + case Mel_Khaonh = 'hkn'; + case Hong_Kong_Sign_Language = 'hks'; + case Halia = 'hla'; + case Halbi = 'hlb'; + case Halang_Doan = 'hld'; + case Hlersu = 'hle'; + case Matu_Chin = 'hlt'; + case Hieroglyphic_Luwian = 'hlu'; + case Southern_Mashan_Hmong = 'hma'; + case Humburi_Senni_Songhay = 'hmb'; + case Central_Huishui_Hmong = 'hmc'; + case Large_Flowery_Miao = 'hmd'; + case Eastern_Huishui_Hmong = 'hme'; + case Hmong_Don = 'hmf'; + case Southwestern_Guiyang_Hmong = 'hmg'; + case Southwestern_Huishui_Hmong = 'hmh'; + case Northern_Huishui_Hmong = 'hmi'; + case Ge = 'hmj'; + case Maek = 'hmk'; + case Luopohe_Hmong = 'hml'; + case Central_Mashan_Hmong = 'hmm'; + case Hmong = 'hmn'; + case Hiri_Motu = 'hmo'; + case Northern_Mashan_Hmong = 'hmp'; + case Eastern_Qiandong_Miao = 'hmq'; + case Hmar = 'hmr'; + case Southern_Qiandong_Miao = 'hms'; + case Hamtai = 'hmt'; + case Hamap = 'hmu'; + case Hmong_Do = 'hmv'; + case Western_Mashan_Hmong = 'hmw'; + case Southern_Guiyang_Hmong = 'hmy'; + case Hmong_Shua = 'hmz'; + case Mina_Cameroon = 'hna'; + case Southern_Hindko = 'hnd'; + case Chhattisgarhi = 'hne'; + case Hungu = 'hng'; + case Ani = 'hnh'; + case Hani = 'hni'; + case Hmong_Njua = 'hnj'; + case Hanunoo = 'hnn'; + case Northern_Hindko = 'hno'; + case Caribbean_Hindustani = 'hns'; + case Hung = 'hnu'; + case Hoava = 'hoa'; + case Mari_Madang_Province = 'hob'; + case Ho = 'hoc'; + case Holma = 'hod'; + case Horom = 'hoe'; + case Hobyot = 'hoh'; + case Holikachuk = 'hoi'; + case Hadothi = 'hoj'; + case Holu = 'hol'; + case Homa = 'hom'; + case Holoholo = 'hoo'; + case Hopi = 'hop'; + case Horo = 'hor'; + case Ho_Chi_Minh_City_Sign_Language = 'hos'; + case Hote = 'hot'; + case Hovongan = 'hov'; + case Honi = 'how'; + case Holiya = 'hoy'; + case Hozo = 'hoz'; + case Hpon = 'hpo'; + case Hawai_i_Sign_Language_HSL = 'hps'; + case Hrangkhol = 'hra'; + case Niwer_Mil = 'hrc'; + case Hre = 'hre'; + case Haruku = 'hrk'; + case Horned_Miao = 'hrm'; + case Haroi = 'hro'; + case Nhirrpi = 'hrp'; + case Hertevin = 'hrt'; + case Hruso = 'hru'; + case Croatian = 'hrv'; + case Warwar_Feni = 'hrw'; + case Hunsrik = 'hrx'; + case Harzani = 'hrz'; + case Upper_Sorbian = 'hsb'; + case Hungarian_Sign_Language = 'hsh'; + case Hausa_Sign_Language = 'hsl'; + case Xiang_Chinese = 'hsn'; + case Harsusi = 'hss'; + case Hoti = 'hti'; + case Minica_Huitoto = 'hto'; + case Hadza = 'hts'; + case Hitu = 'htu'; + case Middle_Hittite = 'htx'; + case Huambisa = 'hub'; + case Hua = 'huc'; + case Huaulu = 'hud'; + case San_Francisco_Del_Mar_Huave = 'hue'; + case Humene = 'huf'; + case Huachipaeri = 'hug'; + case Huilliche = 'huh'; + case Huli = 'hui'; + case Northern_Guiyang_Hmong = 'huj'; + case Hulung = 'huk'; + case Hula = 'hul'; + case Hungana = 'hum'; + case Hungarian = 'hun'; + case Hu = 'huo'; + case Hupa = 'hup'; + case Tsat = 'huq'; + case Halkomelem = 'hur'; + case Huastec = 'hus'; + case Humla = 'hut'; + case Murui_Huitoto = 'huu'; + case San_Mateo_Del_Mar_Huave = 'huv'; + case Hukumina = 'huw'; + case Nupode_Huitoto = 'hux'; + case Hulaula = 'huy'; + case Hunzib = 'huz'; + case Haitian_Vodoun_Culture_Language = 'hvc'; + case San_Dionisio_Del_Mar_Huave = 'hve'; + case Haveke = 'hvk'; + case Sabu = 'hvn'; + case Santa_Maria_Del_Mar_Huave = 'hvv'; + case Wane = 'hwa'; + case Hawai_i_Creole_English = 'hwc'; + case Hwana = 'hwo'; + case Hya = 'hya'; + case Armenian = 'hye'; + case Western_Armenian = 'hyw'; + case Iaai = 'iai'; + case Iatmul = 'ian'; + case Purari = 'iar'; + case Iban = 'iba'; + case Ibibio = 'ibb'; + case Iwaidja = 'ibd'; + case Akpes = 'ibe'; + case Ibanag = 'ibg'; + case Bih = 'ibh'; + case Ibaloi = 'ibl'; + case Agoi = 'ibm'; + case Ibino = 'ibn'; + case Igbo = 'ibo'; + case Ibuoro = 'ibr'; + case Ibu = 'ibu'; + case Ibani = 'iby'; + case Ede_Ica = 'ica'; + case Etkywan = 'ich'; + case Icelandic_Sign_Language = 'icl'; + case Islander_Creole_English = 'icr'; + case Idakho_Isukha_Tiriki = 'ida'; + case Indo_Portuguese = 'idb'; + case Idon = 'idc'; + case Ede_Idaca = 'idd'; + case Idere = 'ide'; + case Idi = 'idi'; + case Ido = 'ido'; + case Indri = 'idr'; + case Idesa = 'ids'; + case Idate = 'idt'; + case Idoma = 'idu'; + case Amganad_Ifugao = 'ifa'; + case Batad_Ifugao = 'ifb'; + case Ife = 'ife'; + case Ifo = 'iff'; + case Tuwali_Ifugao = 'ifk'; + case Teke_Fuumu = 'ifm'; + case Mayoyao_Ifugao = 'ifu'; + case Keley_I_Kallahan = 'ify'; + case Ebira = 'igb'; + case Igede = 'ige'; + case Igana = 'igg'; + case Igala = 'igl'; + case Kanggape = 'igm'; + case Ignaciano = 'ign'; + case Isebe = 'igo'; + case Interglossa = 'igs'; + case Igwe = 'igw'; + case Iha_Based_Pidgin = 'ihb'; + case Ihievbe = 'ihi'; + case Iha = 'ihp'; + case Bidhawal = 'ihw'; + case Sichuan_Yi = 'iii'; + case Thiin = 'iin'; + case Izon = 'ijc'; + case Biseni = 'ije'; + case Ede_Ije = 'ijj'; + case Kalabari = 'ijn'; + case Southeast_Ijo = 'ijs'; + case Eastern_Canadian_Inuktitut = 'ike'; + case Ikhin_Arokho = 'ikh'; + case Iko = 'iki'; + case Ika = 'ikk'; + case Ikulu = 'ikl'; + case Olulumo_Ikom = 'iko'; + case Ikpeshi = 'ikp'; + case Ikaranggal = 'ikr'; + case Inuit_Sign_Language = 'iks'; + case Inuinnaqtun = 'ikt'; + case Inuktitut = 'iku'; + case Iku_Gora_Ankwa = 'ikv'; + case Ikwere = 'ikw'; + case Ik = 'ikx'; + case Ikizu = 'ikz'; + case Ile_Ape = 'ila'; + case Ila = 'ilb'; + case Interlingue = 'ile'; + case Garig_Ilgar = 'ilg'; + case Ili_Turki = 'ili'; + case Ilongot = 'ilk'; + case Iranun_Malaysia = 'ilm'; + case Iloko = 'ilo'; + case Iranun_Philippines = 'ilp'; + case International_Sign = 'ils'; + case Ili_uun = 'ilu'; + case Ilue = 'ilv'; + case Mala_Malasar = 'ima'; + case Anamgura = 'imi'; + case Miluk = 'iml'; + case Imonda = 'imn'; + case Imbongu = 'imo'; + case Imroing = 'imr'; + case Marsian = 'ims'; + case Imotong = 'imt'; + case Milyan = 'imy'; + case Interlingua_International_Auxiliary_Language_Association = 'ina'; + case Inga = 'inb'; + case Indonesian = 'ind'; + case Degexit_an = 'ing'; + case Ingush = 'inh'; + case Jungle_Inga = 'inj'; + case Indonesian_Sign_Language = 'inl'; + case Minaean = 'inm'; + case Isinai = 'inn'; + case Inoke_Yate = 'ino'; + case Inapari = 'inp'; + case Indian_Sign_Language = 'ins'; + case Intha = 'int'; + case Ineseno = 'inz'; + case Inor = 'ior'; + case Tuma_Irumu = 'iou'; + case Iowa_Oto = 'iow'; + case Ipili = 'ipi'; + case Inupiaq = 'ipk'; + case Ipiko = 'ipo'; + case Iquito = 'iqu'; + case Ikwo = 'iqw'; + case Iresim = 'ire'; + case Irarutu = 'irh'; + case Rigwe = 'iri'; + case Iraqw = 'irk'; + case Irantxe = 'irn'; + case Ir = 'irr'; + case Irula = 'iru'; + case Kamberau = 'irx'; + case Iraya = 'iry'; + case Isabi = 'isa'; + case Isconahua = 'isc'; + case Isnag = 'isd'; + case Italian_Sign_Language = 'ise'; + case Irish_Sign_Language = 'isg'; + case Esan = 'ish'; + case Nkem_Nkum = 'isi'; + case Ishkashimi = 'isk'; + case Icelandic = 'isl'; + case Masimasi = 'ism'; + case Isanzu = 'isn'; + case Isoko = 'iso'; + case Israeli_Sign_Language = 'isr'; + case Istriot = 'ist'; + case Isu_Menchum_Division = 'isu'; + case Italian = 'ita'; + case Binongan_Itneg = 'itb'; + case Southern_Tidung = 'itd'; + case Itene = 'ite'; + case Inlaod_Itneg = 'iti'; + case Judeo_Italian = 'itk'; + case Itelmen = 'itl'; + case Itu_Mbon_Uzo = 'itm'; + case Itonama = 'ito'; + case Iteri = 'itr'; + case Isekiri = 'its'; + case Maeng_Itneg = 'itt'; + case Itawit = 'itv'; + case Ito = 'itw'; + case Itik = 'itx'; + case Moyadan_Itneg = 'ity'; + case Itza = 'itz'; + case Iu_Mien = 'ium'; + case Ibatan = 'ivb'; + case Ivatan = 'ivv'; + case I_Wak = 'iwk'; + case Iwam = 'iwm'; + case Iwur = 'iwo'; + case Sepik_Iwam = 'iws'; + case Ixcatec = 'ixc'; + case Ixil = 'ixl'; + case Iyayu = 'iya'; + case Mesaka = 'iyo'; + case Yaka_Congo = 'iyx'; + case Ingrian = 'izh'; + case Kizamani = 'izm'; + case Izere = 'izr'; + case Izii = 'izz'; + case Jamamadi = 'jaa'; + case Hyam = 'jab'; + case Popti = 'jac'; + case Jahanka = 'jad'; + case Yabem = 'jae'; + case Jara = 'jaf'; + case Jah_Hut = 'jah'; + case Zazao = 'jaj'; + case Jakun = 'jak'; + case Yalahatan = 'jal'; + case Jamaican_Creole_English = 'jam'; + case Jandai = 'jan'; + case Yanyuwa = 'jao'; + case Yaqay = 'jaq'; + case New_Caledonian_Javanese = 'jas'; + case Jakati = 'jat'; + case Yaur = 'jau'; + case Javanese = 'jav'; + case Jambi_Malay = 'jax'; + case Yan_nhangu = 'jay'; + case Jawe = 'jaz'; + case Judeo_Berber = 'jbe'; + case Badjiri = 'jbi'; + case Arandai = 'jbj'; + case Barikewa = 'jbk'; + case Bijim = 'jbm'; + case Nafusi = 'jbn'; + case Lojban = 'jbo'; + case Jofotek_Bromnya = 'jbr'; + case Jabuti = 'jbt'; + case Jukun_Takum = 'jbu'; + case Yawijibaya = 'jbw'; + case Jamaican_Country_Sign_Language = 'jcs'; + case Krymchak = 'jct'; + case Jad = 'jda'; + case Jadgali = 'jdg'; + case Judeo_Tat = 'jdt'; + case Jebero = 'jeb'; + case Jerung = 'jee'; + case Jeh = 'jeh'; + case Yei = 'jei'; + case Jeri_Kuo = 'jek'; + case Yelmek = 'jel'; + case Dza = 'jen'; + case Jere = 'jer'; + case Manem = 'jet'; + case Jonkor_Bourmataguil = 'jeu'; + case Ngbee = 'jgb'; + case Judeo_Georgian = 'jge'; + case Gwak = 'jgk'; + case Ngomba = 'jgo'; + case Jehai = 'jhi'; + case Jhankot_Sign_Language = 'jhs'; + case Jina = 'jia'; + case Jibu = 'jib'; + case Tol = 'jic'; + case Bu_Kaduna_State = 'jid'; + case Jilbe = 'jie'; + case Jingulu = 'jig'; + case sTodsde = 'jih'; + case Jiiddu = 'jii'; + case Jilim = 'jil'; + case Jimi_Cameroon = 'jim'; + case Jiamao = 'jio'; + case Guanyinqiao = 'jiq'; + case Jita = 'jit'; + case Youle_Jinuo = 'jiu'; + case Shuar = 'jiv'; + case Buyuan_Jinuo = 'jiy'; + case Jejueo = 'jje'; + case Bankal = 'jjr'; + case Kaera = 'jka'; + case Mobwa_Karen = 'jkm'; + case Kubo = 'jko'; + case Paku_Karen = 'jkp'; + case Koro_India = 'jkr'; + case Amami_Koniya_Sign_Language = 'jks'; + case Labir = 'jku'; + case Ngile = 'jle'; + case Jamaican_Sign_Language = 'jls'; + case Dima = 'jma'; + case Zumbun = 'jmb'; + case Machame = 'jmc'; + case Yamdena = 'jmd'; + case Jimi_Nigeria = 'jmi'; + case Jumli = 'jml'; + case Makuri_Naga = 'jmn'; + case Kamara = 'jmr'; + case Mashi_Nigeria = 'jms'; + case Mouwase = 'jmw'; + case Western_Juxtlahuaca_Mixtec = 'jmx'; + case Jangshung = 'jna'; + case Jandavra = 'jnd'; + case Yangman = 'jng'; + case Janji = 'jni'; + case Yemsa = 'jnj'; + case Rawat = 'jnl'; + case Jaunsari = 'jns'; + case Joba = 'job'; + case Wojenaka = 'jod'; + case Jogi = 'jog'; + case Jora = 'jor'; + case Jordanian_Sign_Language = 'jos'; + case Jowulu = 'jow'; + case Jewish_Palestinian_Aramaic = 'jpa'; + case Japanese = 'jpn'; + case Judeo_Persian = 'jpr'; + case Jaqaru = 'jqr'; + case Jarai = 'jra'; + case Judeo_Arabic = 'jrb'; + case Jiru = 'jrr'; + case Jakattoe = 'jrt'; + case Japreria = 'jru'; + case Japanese_Sign_Language = 'jsl'; + case Juma = 'jua'; + case Wannu = 'jub'; + case Jurchen = 'juc'; + case Worodougou = 'jud'; + case Hone = 'juh'; + case Ngadjuri = 'jui'; + case Wapan = 'juk'; + case Jirel = 'jul'; + case Jumjum = 'jum'; + case Juang = 'jun'; + case Jiba = 'juo'; + case Hupde = 'jup'; + case Juruna = 'jur'; + case Jumla_Sign_Language = 'jus'; + case Jutish = 'jut'; + case Ju = 'juu'; + case Wapha = 'juw'; + case Juray = 'juy'; + case Javindo = 'jvd'; + case Caribbean_Javanese = 'jvn'; + case Jwira_Pepesa = 'jwi'; + case Jiarong = 'jya'; + case Judeo_Yemeni_Arabic = 'jye'; + case Jaya = 'jyy'; + case Kara_Kalpak = 'kaa'; + case Kabyle = 'kab'; + case Kachin = 'kac'; + case Adara = 'kad'; + case Ketangalan = 'kae'; + case Katso = 'kaf'; + case Kajaman = 'kag'; + case Kara_Central_African_Republic = 'kah'; + case Karekare = 'kai'; + case Jju = 'kaj'; + case Kalanguya = 'kak'; + case Kalaallisut = 'kal'; + case Kamba_Kenya = 'kam'; + case Kannada = 'kan'; + case Xaasongaxango = 'kao'; + case Bezhta = 'kap'; + case Capanahua = 'kaq'; + case Kashmiri = 'kas'; + case Georgian = 'kat'; + case Kanuri = 'kau'; + case Katukina = 'kav'; + case Kawi = 'kaw'; + case Kao = 'kax'; + case Kamayura = 'kay'; + case Kazakh = 'kaz'; + case Kalarko = 'kba'; + case Kaxuiana = 'kbb'; + case Kadiweu = 'kbc'; + case Kabardian = 'kbd'; + case Kanju = 'kbe'; + case Khamba = 'kbg'; + case Camsa = 'kbh'; + case Kaptiau = 'kbi'; + case Kari = 'kbj'; + case Grass_Koiari = 'kbk'; + case Kanembu = 'kbl'; + case Iwal = 'kbm'; + case Kare_Central_African_Republic = 'kbn'; + case Keliko = 'kbo'; + case Kabiye = 'kbp'; + case Kamano = 'kbq'; + case Kafa = 'kbr'; + case Kande = 'kbs'; + case Abadi = 'kbt'; + case Kabutra = 'kbu'; + case Dera_Indonesia = 'kbv'; + case Kaiep = 'kbw'; + case Ap_Ma = 'kbx'; + case Manga_Kanuri = 'kby'; + case Duhwa = 'kbz'; + case Khanty = 'kca'; + case Kawacha = 'kcb'; + case Lubila = 'kcc'; + case Ngkalmpw_Kanum = 'kcd'; + case Kaivi = 'kce'; + case Ukaan = 'kcf'; + case Tyap = 'kcg'; + case Vono = 'kch'; + case Kamantan = 'kci'; + case Kobiana = 'kcj'; + case Kalanga = 'kck'; + case Kela_Papua_New_Guinea = 'kcl'; + case Gula_Central_African_Republic = 'kcm'; + case Nubi = 'kcn'; + case Kinalakna = 'kco'; + case Kanga = 'kcp'; + case Kamo = 'kcq'; + case Katla = 'kcr'; + case Koenoem = 'kcs'; + case Kaian = 'kct'; + case Kami_Tanzania = 'kcu'; + case Kete = 'kcv'; + case Kabwari = 'kcw'; + case Kachama_Ganjule = 'kcx'; + case Korandje = 'kcy'; + case Konongo = 'kcz'; + case Worimi = 'kda'; + case Kutu = 'kdc'; + case Yankunytjatjara = 'kdd'; + case Makonde = 'kde'; + case Mamusi = 'kdf'; + case Seba = 'kdg'; + case Tem = 'kdh'; + case Kumam = 'kdi'; + case Karamojong = 'kdj'; + case Numee = 'kdk'; + case Tsikimba = 'kdl'; + case Kagoma = 'kdm'; + case Kunda = 'kdn'; + case Kaningdon_Nindem = 'kdp'; + case Koch = 'kdq'; + case Karaim = 'kdr'; + case Kuy = 'kdt'; + case Kadaru = 'kdu'; + case Koneraw = 'kdw'; + case Kam = 'kdx'; + case Keder = 'kdy'; + case Kwaja = 'kdz'; + case Kabuverdianu = 'kea'; + case Kele = 'keb'; + case Keiga = 'kec'; + case Kerewe = 'ked'; + case Eastern_Keres = 'kee'; + case Kpessi = 'kef'; + case Tese = 'keg'; + case Keak = 'keh'; + case Kei = 'kei'; + case Kadar = 'kej'; + case Kekchi = 'kek'; + case Kela_Democratic_Republic_of_Congo = 'kel'; + case Kemak = 'kem'; + case Kenyang = 'ken'; + case Kakwa = 'keo'; + case Kaikadi = 'kep'; + case Kamar = 'keq'; + case Kera = 'ker'; + case Kugbo = 'kes'; + case Ket = 'ket'; + case Akebu = 'keu'; + case Kanikkaran = 'kev'; + case West_Kewa = 'kew'; + case Kukna = 'kex'; + case Kupia = 'key'; + case Kukele = 'kez'; + case Kodava = 'kfa'; + case Northwestern_Kolami = 'kfb'; + case Konda_Dora = 'kfc'; + case Korra_Koraga = 'kfd'; + case Kota_India = 'kfe'; + case Koya = 'kff'; + case Kudiya = 'kfg'; + case Kurichiya = 'kfh'; + case Kannada_Kurumba = 'kfi'; + case Kemiehua = 'kfj'; + case Kinnauri = 'kfk'; + case Kung = 'kfl'; + case Khunsari = 'kfm'; + case Kuk = 'kfn'; + case Koro_Cote_d_Ivoire = 'kfo'; + case Korwa = 'kfp'; + case Korku = 'kfq'; + case Kachhi = 'kfr'; + case Bilaspuri = 'kfs'; + case Kanjari = 'kft'; + case Katkari = 'kfu'; + case Kurmukar = 'kfv'; + case Kharam_Naga = 'kfw'; + case Kullu_Pahari = 'kfx'; + case Kumaoni = 'kfy'; + case Koromfe = 'kfz'; + case Koyaga = 'kga'; + case Kawe = 'kgb'; + case Komering = 'kge'; + case Kube = 'kgf'; + case Kusunda = 'kgg'; + case Selangor_Sign_Language = 'kgi'; + case Gamale_Kham = 'kgj'; + case Kaiwa = 'kgk'; + case Kunggari = 'kgl'; + case Karingani = 'kgn'; + case Krongo = 'kgo'; + case Kaingang = 'kgp'; + case Kamoro = 'kgq'; + case Abun = 'kgr'; + case Kumbainggar = 'kgs'; + case Somyev = 'kgt'; + case Kobol = 'kgu'; + case Karas = 'kgv'; + case Karon_Dori = 'kgw'; + case Kamaru = 'kgx'; + case Kyerung = 'kgy'; + case Khasi = 'kha'; + case Lu = 'khb'; + case Tukang_Besi_North = 'khc'; + case Badi_Kanum = 'khd'; + case Korowai = 'khe'; + case Khuen = 'khf'; + case Khams_Tibetan = 'khg'; + case Kehu = 'khh'; + case Kuturmi = 'khj'; + case Halh_Mongolian = 'khk'; + case Lusi = 'khl'; + case Khmer = 'khm'; + case Khandesi = 'khn'; + case Khotanese = 'kho'; + case Kapori = 'khp'; + case Koyra_Chiini_Songhay = 'khq'; + case Kharia = 'khr'; + case Kasua = 'khs'; + case Khamti = 'kht'; + case Nkhumbi = 'khu'; + case Khvarshi = 'khv'; + case Khowar = 'khw'; + case Kanu = 'khx'; + case Kele_Democratic_Republic_of_Congo = 'khy'; + case Keapara = 'khz'; + case Kim = 'kia'; + case Koalib = 'kib'; + case Kickapoo = 'kic'; + case Koshin = 'kid'; + case Kibet = 'kie'; + case Eastern_Parbate_Kham = 'kif'; + case Kimaama = 'kig'; + case Kilmeri = 'kih'; + case Kitsai = 'kii'; + case Kilivila = 'kij'; + case Kikuyu = 'kik'; + case Kariya = 'kil'; + case Karagas = 'kim'; + case Kinyarwanda = 'kin'; + case Kiowa = 'kio'; + case Sheshi_Kham = 'kip'; + case Kosadle = 'kiq'; + case Kirghiz = 'kir'; + case Kis = 'kis'; + case Agob = 'kit'; + case Kirmanjki_individual_language = 'kiu'; + case Kimbu = 'kiv'; + case Northeast_Kiwai = 'kiw'; + case Khiamniungan_Naga = 'kix'; + case Kirikiri = 'kiy'; + case Kisi = 'kiz'; + case Mlap = 'kja'; + case Q_anjob_al = 'kjb'; + case Coastal_Konjo = 'kjc'; + case Southern_Kiwai = 'kjd'; + case Kisar = 'kje'; + case Khmu = 'kjg'; + case Khakas = 'kjh'; + case Zabana = 'kji'; + case Khinalugh = 'kjj'; + case Highland_Konjo = 'kjk'; + case Western_Parbate_Kham = 'kjl'; + case Khang = 'kjm'; + case Kunjen = 'kjn'; + case Harijan_Kinnauri = 'kjo'; + case Pwo_Eastern_Karen = 'kjp'; + case Western_Keres = 'kjq'; + case Kurudu = 'kjr'; + case East_Kewa = 'kjs'; + case Phrae_Pwo_Karen = 'kjt'; + case Kashaya = 'kju'; + case Kaikavian_Literary_Language = 'kjv'; + case Ramopa = 'kjx'; + case Erave = 'kjy'; + case Bumthangkha = 'kjz'; + case Kakanda = 'kka'; + case Kwerisa = 'kkb'; + case Odoodee = 'kkc'; + case Kinuku = 'kkd'; + case Kakabe = 'kke'; + case Kalaktang_Monpa = 'kkf'; + case Mabaka_Valley_Kalinga = 'kkg'; + case Khun = 'kkh'; + case Kagulu = 'kki'; + case Kako = 'kkj'; + case Kokota = 'kkk'; + case Kosarek_Yale = 'kkl'; + case Kiong = 'kkm'; + case Kon_Keu = 'kkn'; + case Karko = 'kko'; + case Gugubera = 'kkp'; + case Kaeku = 'kkq'; + case Kir_Balar = 'kkr'; + case Giiwo = 'kks'; + case Koi = 'kkt'; + case Tumi = 'kku'; + case Kangean = 'kkv'; + case Teke_Kukuya = 'kkw'; + case Kohin = 'kkx'; + case Guugu_Yimidhirr = 'kky'; + case Kaska = 'kkz'; + case Klamath_Modoc = 'kla'; + case Kiliwa = 'klb'; + case Kolbila = 'klc'; + case Gamilaraay = 'kld'; + case Kulung_Nepal = 'kle'; + case Kendeje = 'klf'; + case Tagakaulo = 'klg'; + case Weliki = 'klh'; + case Kalumpang = 'kli'; + case Khalaj = 'klj'; + case Kono_Nigeria = 'klk'; + case Kagan_Kalagan = 'kll'; + case Migum = 'klm'; + case Kalenjin = 'kln'; + case Kapya = 'klo'; + case Kamasa = 'klp'; + case Rumu = 'klq'; + case Khaling = 'klr'; + case Kalasha = 'kls'; + case Nukna = 'klt'; + case Klao = 'klu'; + case Maskelynes = 'klv'; + case Tado = 'klw'; + case Koluwawa = 'klx'; + case Kalao = 'kly'; + case Kabola = 'klz'; + case Konni = 'kma'; + case Kimbundu = 'kmb'; + case Southern_Dong = 'kmc'; + case Majukayang_Kalinga = 'kmd'; + case Bakole = 'kme'; + case Kare_Papua_New_Guinea = 'kmf'; + case Kate = 'kmg'; + case Kalam = 'kmh'; + case Kami_Nigeria = 'kmi'; + case Kumarbhag_Paharia = 'kmj'; + case Limos_Kalinga = 'kmk'; + case Tanudan_Kalinga = 'kml'; + case Kom_India = 'kmm'; + case Awtuw = 'kmn'; + case Kwoma = 'kmo'; + case Gimme = 'kmp'; + case Kwama = 'kmq'; + case Northern_Kurdish = 'kmr'; + case Kamasau = 'kms'; + case Kemtuik = 'kmt'; + case Kanite = 'kmu'; + case Karipuna_Creole_French = 'kmv'; + case Komo_Democratic_Republic_of_Congo = 'kmw'; + case Waboda = 'kmx'; + case Koma = 'kmy'; + case Khorasani_Turkish = 'kmz'; + case Dera_Nigeria = 'kna'; + case Lubuagan_Kalinga = 'knb'; + case Central_Kanuri = 'knc'; + case Konda = 'knd'; + case Kankanaey = 'kne'; + case Mankanya = 'knf'; + case Koongo = 'kng'; + case Kanufi = 'kni'; + case Western_Kanjobal = 'knj'; + case Kuranko = 'knk'; + case Keninjal = 'knl'; + case Kanamari = 'knm'; + case Konkani_individual_language = 'knn'; + case Kono_Sierra_Leone = 'kno'; + case Kwanja = 'knp'; + case Kintaq = 'knq'; + case Kaningra = 'knr'; + case Kensiu = 'kns'; + case Panoan_Katukina = 'knt'; + case Kono_Guinea = 'knu'; + case Tabo = 'knv'; + case Kung_Ekoka = 'knw'; + case Kendayan = 'knx'; + case Kanyok = 'kny'; + case Kalamse = 'knz'; + case Konomala = 'koa'; + case Kpati = 'koc'; + case Kodi = 'kod'; + case Kacipo_Bale_Suri = 'koe'; + case Kubi = 'kof'; + case Cogui = 'kog'; + case Koyo = 'koh'; + case Komi_Permyak = 'koi'; + case Konkani_macrolanguage = 'kok'; + case Kol_Papua_New_Guinea = 'kol'; + case Komi = 'kom'; + case Kongo = 'kon'; + case Konzo = 'koo'; + case Waube = 'kop'; + case Kota_Gabon = 'koq'; + case Korean = 'kor'; + case Kosraean = 'kos'; + case Lagwan = 'kot'; + case Koke = 'kou'; + case Kudu_Camo = 'kov'; + case Kugama = 'kow'; + case Koyukon = 'koy'; + case Korak = 'koz'; + case Kutto = 'kpa'; + case Mullu_Kurumba = 'kpb'; + case Curripaco = 'kpc'; + case Koba = 'kpd'; + case Kpelle = 'kpe'; + case Komba = 'kpf'; + case Kapingamarangi = 'kpg'; + case Kplang = 'kph'; + case Kofei = 'kpi'; + case Karaja = 'kpj'; + case Kpan = 'kpk'; + case Kpala = 'kpl'; + case Koho = 'kpm'; + case Kepkiriwat = 'kpn'; + case Ikposo = 'kpo'; + case Korupun_Sela = 'kpq'; + case Korafe_Yegha = 'kpr'; + case Tehit = 'kps'; + case Karata = 'kpt'; + case Kafoa = 'kpu'; + case Komi_Zyrian = 'kpv'; + case Kobon = 'kpw'; + case Mountain_Koiali = 'kpx'; + case Koryak = 'kpy'; + case Kupsabiny = 'kpz'; + case Mum = 'kqa'; + case Kovai = 'kqb'; + case Doromu_Koki = 'kqc'; + case Koy_Sanjaq_Surat = 'kqd'; + case Kalagan = 'kqe'; + case Kakabai = 'kqf'; + case Khe = 'kqg'; + case Kisankasa = 'kqh'; + case Koitabu = 'kqi'; + case Koromira = 'kqj'; + case Kotafon_Gbe = 'kqk'; + case Kyenele = 'kql'; + case Khisa = 'kqm'; + case Kaonde = 'kqn'; + case Eastern_Krahn = 'kqo'; + case Kimre = 'kqp'; + case Krenak = 'kqq'; + case Kimaragang = 'kqr'; + case Northern_Kissi = 'kqs'; + case Klias_River_Kadazan = 'kqt'; + case Seroa = 'kqu'; + case Okolod = 'kqv'; + case Kandas = 'kqw'; + case Mser = 'kqx'; + case Koorete = 'kqy'; + case Korana = 'kqz'; + case Kumhali = 'kra'; + case Karkin = 'krb'; + case Karachay_Balkar = 'krc'; + case Kairui_Midiki = 'krd'; + case Panara = 'kre'; + case Koro_Vanuatu = 'krf'; + case Kurama = 'krh'; + case Krio = 'kri'; + case Kinaray_A = 'krj'; + case Kerek = 'krk'; + case Karelian = 'krl'; + case Sapo = 'krn'; + case Durop = 'krp'; + case Krung = 'krr'; + case Gbaya_Sudan = 'krs'; + case Tumari_Kanuri = 'krt'; + case Kurukh = 'kru'; + case Kavet = 'krv'; + case Western_Krahn = 'krw'; + case Karon = 'krx'; + case Kryts = 'kry'; + case Sota_Kanum = 'krz'; + case Shambala = 'ksb'; + case Southern_Kalinga = 'ksc'; + case Kuanua = 'ksd'; + case Kuni = 'kse'; + case Bafia = 'ksf'; + case Kusaghe = 'ksg'; + case Kolsch = 'ksh'; + case Krisa = 'ksi'; + case Uare = 'ksj'; + case Kansa = 'ksk'; + case Kumalu = 'ksl'; + case Kumba = 'ksm'; + case Kasiguranin = 'ksn'; + case Kofa = 'kso'; + case Kaba = 'ksp'; + case Kwaami = 'ksq'; + case Borong = 'ksr'; + case Southern_Kisi = 'kss'; + case Winye = 'kst'; + case Khamyang = 'ksu'; + case Kusu = 'ksv'; + case S_gaw_Karen = 'ksw'; + case Kedang = 'ksx'; + case Kharia_Thar = 'ksy'; + case Kodaku = 'ksz'; + case Katua = 'kta'; + case Kambaata = 'ktb'; + case Kholok = 'ktc'; + case Kokata = 'ktd'; + case Nubri = 'kte'; + case Kwami = 'ktf'; + case Kalkutung = 'ktg'; + case Karanga = 'kth'; + case North_Muyu = 'kti'; + case Plapo_Krumen = 'ktj'; + case Kaniet = 'ktk'; + case Koroshi = 'ktl'; + case Kurti = 'ktm'; + case Karitiana = 'ktn'; + case Kuot = 'kto'; + case Kaduo = 'ktp'; + case Katabaga = 'ktq'; + case South_Muyu = 'kts'; + case Ketum = 'ktt'; + case Kituba_Democratic_Republic_of_Congo = 'ktu'; + case Eastern_Katu = 'ktv'; + case Kato = 'ktw'; + case Kaxarari = 'ktx'; + case Kango_Bas_Uele_District = 'kty'; + case Ju_hoan = 'ktz'; + case Kuanyama = 'kua'; + case Kutep = 'kub'; + case Kwinsu = 'kuc'; + case Auhelawa = 'kud'; + case Kuman_Papua_New_Guinea = 'kue'; + case Western_Katu = 'kuf'; + case Kupa = 'kug'; + case Kushi = 'kuh'; + case Kuikuro_Kalapalo = 'kui'; + case Kuria = 'kuj'; + case Kepo = 'kuk'; + case Kulere = 'kul'; + case Kumyk = 'kum'; + case Kunama = 'kun'; + case Kumukio = 'kuo'; + case Kunimaipa = 'kup'; + case Karipuna = 'kuq'; + case Kurdish = 'kur'; + case Kusaal = 'kus'; + case Kutenai = 'kut'; + case Upper_Kuskokwim = 'kuu'; + case Kur = 'kuv'; + case Kpagua = 'kuw'; + case Kukatja = 'kux'; + case Kuuku_Ya_u = 'kuy'; + case Kunza = 'kuz'; + case Bagvalal = 'kva'; + case Kubu = 'kvb'; + case Kove = 'kvc'; + case Kui_Indonesia = 'kvd'; + case Kalabakan = 'kve'; + case Kabalai = 'kvf'; + case Kuni_Boazi = 'kvg'; + case Komodo = 'kvh'; + case Kwang = 'kvi'; + case Psikye = 'kvj'; + case Korean_Sign_Language = 'kvk'; + case Kayaw = 'kvl'; + case Kendem = 'kvm'; + case Border_Kuna = 'kvn'; + case Dobel = 'kvo'; + case Kompane = 'kvp'; + case Geba_Karen = 'kvq'; + case Kerinci = 'kvr'; + case Lahta_Karen = 'kvt'; + case Yinbaw_Karen = 'kvu'; + case Kola = 'kvv'; + case Wersing = 'kvw'; + case Parkari_Koli = 'kvx'; + case Yintale_Karen = 'kvy'; + case Tsakwambo = 'kvz'; + case Daw = 'kwa'; + case Kwa_2 = 'kwb'; + case Likwala = 'kwc'; + case Kwaio = 'kwd'; + case Kwerba = 'kwe'; + case Kwara_ae = 'kwf'; + case Sara_Kaba_Deme = 'kwg'; + case Kowiai = 'kwh'; + case Awa_Cuaiquer = 'kwi'; + case Kwanga = 'kwj'; + case Kwakiutl = 'kwk'; + case Kofyar = 'kwl'; + case Kwambi = 'kwm'; + case Kwangali = 'kwn'; + case Kwomtari = 'kwo'; + case Kodia = 'kwp'; + case Kwer = 'kwr'; + case Kwese = 'kws'; + case Kwesten = 'kwt'; + case Kwakum = 'kwu'; + case Sara_Kaba_Naa = 'kwv'; + case Kwinti = 'kww'; + case Khirwar = 'kwx'; + case San_Salvador_Kongo = 'kwy'; + case Kwadi = 'kwz'; + case Kairiru = 'kxa'; + case Krobu = 'kxb'; + case Konso = 'kxc'; + case Brunei = 'kxd'; + case Manumanaw_Karen = 'kxf'; + case Karo_Ethiopia = 'kxh'; + case Keningau_Murut = 'kxi'; + case Kulfa = 'kxj'; + case Zayein_Karen = 'kxk'; + case Northern_Khmer = 'kxm'; + case Kanowit_Tanjong_Melanau = 'kxn'; + case Kanoe = 'kxo'; + case Wadiyara_Koli = 'kxp'; + case Smarky_Kanum = 'kxq'; + case Koro_Papua_New_Guinea = 'kxr'; + case Kangjia = 'kxs'; + case Koiwat = 'kxt'; + case Kuvi = 'kxv'; + case Konai = 'kxw'; + case Likuba = 'kxx'; + case Kayong = 'kxy'; + case Kerewo = 'kxz'; + case Kwaya = 'kya'; + case Butbut_Kalinga = 'kyb'; + case Kyaka = 'kyc'; + case Karey = 'kyd'; + case Krache = 'kye'; + case Kouya = 'kyf'; + case Keyagana = 'kyg'; + case Karok = 'kyh'; + case Kiput = 'kyi'; + case Karao = 'kyj'; + case Kamayo = 'kyk'; + case Kalapuya = 'kyl'; + case Kpatili = 'kym'; + case Northern_Binukidnon = 'kyn'; + case Kelon = 'kyo'; + case Kang = 'kyp'; + case Kenga = 'kyq'; + case Kuruaya = 'kyr'; + case Baram_Kayan = 'kys'; + case Kayagar = 'kyt'; + case Western_Kayah = 'kyu'; + case Kayort = 'kyv'; + case Kudmali = 'kyw'; + case Rapoisi = 'kyx'; + case Kambaira = 'kyy'; + case Kayabi = 'kyz'; + case Western_Karaboro = 'kza'; + case Kaibobo = 'kzb'; + case Bondoukou_Kulango = 'kzc'; + case Kadai = 'kzd'; + case Kosena = 'kze'; + case Da_a_Kaili = 'kzf'; + case Kikai = 'kzg'; + case Kelabit = 'kzi'; + case Kazukuru = 'kzk'; + case Kayeli = 'kzl'; + case Kais = 'kzm'; + case Kokola = 'kzn'; + case Kaningi = 'kzo'; + case Kaidipang = 'kzp'; + case Kaike = 'kzq'; + case Karang = 'kzr'; + case Sugut_Dusun = 'kzs'; + case Kayupulau = 'kzu'; + case Komyandaret = 'kzv'; + case Kariri_Xoco = 'kzw'; + case Kamarian = 'kzx'; + case Kango_Tshopo_District = 'kzy'; + case Kalabra = 'kzz'; + case Southern_Subanen = 'laa'; + case Linear_A = 'lab'; + case Lacandon = 'lac'; + case Ladino = 'lad'; + case Pattani = 'lae'; + case Lafofa = 'laf'; + case Rangi = 'lag'; + case Lahnda = 'lah'; + case Lambya = 'lai'; + case Lango_Uganda = 'laj'; + case Lalia = 'lal'; + case Lamba = 'lam'; + case Laru = 'lan'; + case Lao = 'lao'; + case Laka_Chad = 'lap'; + case Qabiao = 'laq'; + case Larteh = 'lar'; + case Lama_Togo = 'las'; + case Latin = 'lat'; + case Laba = 'lau'; + case Latvian = 'lav'; + case Lauje = 'law'; + case Tiwa = 'lax'; + case Lama_Bai = 'lay'; + case Aribwatsa = 'laz'; + case Label = 'lbb'; + case Lakkia = 'lbc'; + case Lak = 'lbe'; + case Tinani = 'lbf'; + case Laopang = 'lbg'; + case La_bi = 'lbi'; + case Ladakhi = 'lbj'; + case Central_Bontok = 'lbk'; + case Libon_Bikol = 'lbl'; + case Lodhi = 'lbm'; + case Rmeet = 'lbn'; + case Laven = 'lbo'; + case Wampar = 'lbq'; + case Lohorung = 'lbr'; + case Libyan_Sign_Language = 'lbs'; + case Lachi = 'lbt'; + case Labu = 'lbu'; + case Lavatbura_Lamusong = 'lbv'; + case Tolaki = 'lbw'; + case Lawangan = 'lbx'; + case Lamalama = 'lby'; + case Lardil = 'lbz'; + case Legenyem = 'lcc'; + case Lola = 'lcd'; + case Loncong = 'lce'; + case Lubu = 'lcf'; + case Luchazi = 'lch'; + case Lisela = 'lcl'; + case Tungag = 'lcm'; + case Western_Lawa = 'lcp'; + case Luhu = 'lcq'; + case Lisabata_Nuniali = 'lcs'; + case Kla_Dan = 'lda'; + case Du_ya = 'ldb'; + case Luri = 'ldd'; + case Lenyima = 'ldg'; + case Lamja_Dengsa_Tola = 'ldh'; + case Laari = 'ldi'; + case Lemoro = 'ldj'; + case Leelau = 'ldk'; + case Kaan = 'ldl'; + case Landoma = 'ldm'; + case Laadan = 'ldn'; + case Loo = 'ldo'; + case Tso = 'ldp'; + case Lufu = 'ldq'; + case Lega_Shabunda = 'lea'; + case Lala_Bisa = 'leb'; + case Leco = 'lec'; + case Lendu = 'led'; + case Lyele = 'lee'; + case Lelemi = 'lef'; + case Lenje = 'leh'; + case Lemio = 'lei'; + case Lengola = 'lej'; + case Leipon = 'lek'; + case Lele_Democratic_Republic_of_Congo = 'lel'; + case Nomaande = 'lem'; + case Lenca = 'len'; + case Leti_Cameroon = 'leo'; + case Lepcha = 'lep'; + case Lembena = 'leq'; + case Lenkau = 'ler'; + case Lese = 'les'; + case Lesing_Gelimi = 'let'; + case Kara_Papua_New_Guinea = 'leu'; + case Lamma = 'lev'; + case Ledo_Kaili = 'lew'; + case Luang = 'lex'; + case Lemolang = 'ley'; + case Lezghian = 'lez'; + case Lefa = 'lfa'; + case Lingua_Franca_Nova = 'lfn'; + case Lungga = 'lga'; + case Laghu = 'lgb'; + case Lugbara = 'lgg'; + case Laghuu = 'lgh'; + case Lengilu = 'lgi'; + case Lingarak = 'lgk'; + case Wala = 'lgl'; + case Lega_Mwenga = 'lgm'; + case T_apo = 'lgn'; + case Lango_South_Sudan = 'lgo'; + case Logba = 'lgq'; + case Lengo = 'lgr'; + case Guinea_Bissau_Sign_Language = 'lgs'; + case Pahi = 'lgt'; + case Longgu = 'lgu'; + case Ligenza = 'lgz'; + case Laha_Viet_Nam = 'lha'; + case Laha_Indonesia = 'lhh'; + case Lahu_Shi = 'lhi'; + case Lahul_Lohar = 'lhl'; + case Lhomi = 'lhm'; + case Lahanan = 'lhn'; + case Lhokpu = 'lhp'; + case Mlahso = 'lhs'; + case Lo_Toga = 'lht'; + case Lahu = 'lhu'; + case West_Central_Limba = 'lia'; + case Likum = 'lib'; + case Hlai = 'lic'; + case Nyindrou = 'lid'; + case Likila = 'lie'; + case Limbu = 'lif'; + case Ligbi = 'lig'; + case Lihir = 'lih'; + case Ligurian = 'lij'; + case Lika = 'lik'; + case Lillooet = 'lil'; + case Limburgan = 'lim'; + case Lingala = 'lin'; + case Liki = 'lio'; + case Sekpele = 'lip'; + case Libido = 'liq'; + case Liberian_English = 'lir'; + case Lisu = 'lis'; + case Lithuanian = 'lit'; + case Logorik = 'liu'; + case Liv = 'liv'; + case Col = 'liw'; + case Liabuku = 'lix'; + case Banda_Bambari = 'liy'; + case Libinza = 'liz'; + case Golpa = 'lja'; + case Rampi = 'lje'; + case Laiyolo = 'lji'; + case Li_o = 'ljl'; + case Lampung_Api = 'ljp'; + case Yirandali = 'ljw'; + case Yuru = 'ljx'; + case Lakalei = 'lka'; + case Kabras = 'lkb'; + case Kucong = 'lkc'; + case Lakonde = 'lkd'; + case Kenyi = 'lke'; + case Lakha = 'lkh'; + case Laki = 'lki'; + case Remun = 'lkj'; + case Laeko_Libuat = 'lkl'; + case Kalaamaya = 'lkm'; + case Lakon = 'lkn'; + case Khayo = 'lko'; + case Pari = 'lkr'; + case Kisa = 'lks'; + case Lakota = 'lkt'; + case Kungkari = 'lku'; + case Lokoya = 'lky'; + case Lala_Roba = 'lla'; + case Lolo = 'llb'; + case Lele_Guinea = 'llc'; + case Ladin = 'lld'; + case Lele_Papua_New_Guinea = 'lle'; + case Hermit = 'llf'; + case Lole = 'llg'; + case Lamu = 'llh'; + case Teke_Laali = 'lli'; + case Ladji_Ladji = 'llj'; + case Lelak = 'llk'; + case Lilau = 'lll'; + case Lasalimu = 'llm'; + case Lele_Chad = 'lln'; + case North_Efate = 'llp'; + case Lolak = 'llq'; + case Lithuanian_Sign_Language = 'lls'; + case Lau = 'llu'; + case Lauan = 'llx'; + case East_Limba = 'lma'; + case Merei = 'lmb'; + case Limilngan = 'lmc'; + case Lumun = 'lmd'; + case Peve = 'lme'; + case South_Lembata = 'lmf'; + case Lamogai = 'lmg'; + case Lambichhong = 'lmh'; + case Lombi = 'lmi'; + case West_Lembata = 'lmj'; + case Lamkang = 'lmk'; + case Hano = 'lml'; + case Lambadi = 'lmn'; + case Lombard = 'lmo'; + case Limbum = 'lmp'; + case Lamatuka = 'lmq'; + case Lamalera = 'lmr'; + case Lamenu = 'lmu'; + case Lomaiviti = 'lmv'; + case Lake_Miwok = 'lmw'; + case Laimbue = 'lmx'; + case Lamboya = 'lmy'; + case Langbashe = 'lna'; + case Mbalanhu = 'lnb'; + case Lundayeh = 'lnd'; + case Langobardic = 'lng'; + case Lanoh = 'lnh'; + case Daantanai = 'lni'; + case Leningitij = 'lnj'; + case South_Central_Banda = 'lnl'; + case Langam = 'lnm'; + case Lorediakarkar = 'lnn'; + case Lamnso = 'lns'; + case Longuda = 'lnu'; + case Lanima = 'lnw'; + case Lonzo = 'lnz'; + case Loloda = 'loa'; + case Lobi = 'lob'; + case Inonhan = 'loc'; + case Saluan = 'loe'; + case Logol = 'lof'; + case Logo = 'log'; + case Laarim = 'loh'; + case Loma_Cote_d_Ivoire = 'loi'; + case Lou = 'loj'; + case Loko = 'lok'; + case Mongo = 'lol'; + case Loma_Liberia = 'lom'; + case Malawi_Lomwe = 'lon'; + case Lombo = 'loo'; + case Lopa = 'lop'; + case Lobala = 'loq'; + case Teen = 'lor'; + case Loniu = 'los'; + case Otuho = 'lot'; + case Louisiana_Creole = 'lou'; + case Lopi = 'lov'; + case Tampias_Lobu = 'low'; + case Loun = 'lox'; + case Loke = 'loy'; + case Lozi = 'loz'; + case Lelepa = 'lpa'; + case Lepki = 'lpe'; + case Long_Phuri_Naga = 'lpn'; + case Lipo = 'lpo'; + case Lopit = 'lpx'; + case Logir = 'lqr'; + case Rara_Bakati = 'lra'; + case Northern_Luri = 'lrc'; + case Laurentian = 'lre'; + case Laragia = 'lrg'; + case Marachi = 'lri'; + case Loarki = 'lrk'; + case Lari = 'lrl'; + case Marama = 'lrm'; + case Lorang = 'lrn'; + case Laro = 'lro'; + case Southern_Yamphu = 'lrr'; + case Larantuka_Malay = 'lrt'; + case Larevat = 'lrv'; + case Lemerig = 'lrz'; + case Lasgerdi = 'lsa'; + case Burundian_Sign_Language = 'lsb'; + case Albarradas_Sign_Language = 'lsc'; + case Lishana_Deni = 'lsd'; + case Lusengo = 'lse'; + case Lish = 'lsh'; + case Lashi = 'lsi'; + case Latvian_Sign_Language = 'lsl'; + case Saamia = 'lsm'; + case Tibetan_Sign_Language = 'lsn'; + case Laos_Sign_Language = 'lso'; + case Panamanian_Sign_Language = 'lsp'; + case Aruop = 'lsr'; + case Lasi = 'lss'; + case Trinidad_and_Tobago_Sign_Language = 'lst'; + case Sivia_Sign_Language = 'lsv'; + case Seychelles_Sign_Language = 'lsw'; + case Mauritian_Sign_Language = 'lsy'; + case Late_Middle_Chinese = 'ltc'; + case Latgalian = 'ltg'; + case Thur = 'lth'; + case Leti_Indonesia = 'lti'; + case Latunde = 'ltn'; + case Tsotso = 'lto'; + case Tachoni = 'lts'; + case Latu = 'ltu'; + case Luxembourgish = 'ltz'; + case Luba_Lulua = 'lua'; + case Luba_Katanga = 'lub'; + case Aringa = 'luc'; + case Ludian = 'lud'; + case Luvale = 'lue'; + case Laua = 'luf'; + case Ganda = 'lug'; + case Luiseno = 'lui'; + case Luna = 'luj'; + case Lunanakha = 'luk'; + case Olu_bo = 'lul'; + case Luimbi = 'lum'; + case Lunda = 'lun'; + case Luo_Kenya_and_Tanzania = 'luo'; + case Lumbu = 'lup'; + case Lucumi = 'luq'; + case Laura = 'lur'; + case Lushai = 'lus'; + case Lushootseed = 'lut'; + case Lumba_Yakkha = 'luu'; + case Luwati = 'luv'; + case Luo_Cameroon = 'luw'; + case Luyia = 'luy'; + case Southern_Luri = 'luz'; + case Maku_a = 'lva'; + case Lavi = 'lvi'; + case Lavukaleve = 'lvk'; + case Lwel = 'lvl'; + case Standard_Latvian = 'lvs'; + case Levuka = 'lvu'; + case Lwalu = 'lwa'; + case Lewo_Eleng = 'lwe'; + case Wanga = 'lwg'; + case White_Lachi = 'lwh'; + case Eastern_Lawa = 'lwl'; + case Laomian = 'lwm'; + case Luwo = 'lwo'; + case Malawian_Sign_Language = 'lws'; + case Lewotobi = 'lwt'; + case Lawu = 'lwu'; + case Lewo = 'lww'; + case Lakurumau = 'lxm'; + case Layakha = 'lya'; + case Lyngngam = 'lyg'; + case Luyana = 'lyn'; + case Literary_Chinese = 'lzh'; + case Litzlitz = 'lzl'; + case Leinong_Naga = 'lzn'; + case Laz = 'lzz'; + case San_Jeronimo_Tecoatl_Mazatec = 'maa'; + case Yutanduchi_Mixtec = 'mab'; + case Madurese = 'mad'; + case Bo_Rukul = 'mae'; + case Mafa = 'maf'; + case Magahi = 'mag'; + case Marshallese = 'mah'; + case Maithili = 'mai'; + case Jalapa_De_Diaz_Mazatec = 'maj'; + case Makasar = 'mak'; + case Malayalam = 'mal'; + case Mam = 'mam'; + case Mandingo = 'man'; + case Chiquihuitlan_Mazatec = 'maq'; + case Marathi = 'mar'; + case Masai = 'mas'; + case San_Francisco_Matlatzinca = 'mat'; + case Huautla_Mazatec = 'mau'; + case Satere_Mawe = 'mav'; + case Mampruli = 'maw'; + case North_Moluccan_Malay = 'max'; + case Central_Mazahua = 'maz'; + case Higaonon = 'mba'; + case Western_Bukidnon_Manobo = 'mbb'; + case Macushi = 'mbc'; + case Dibabawon_Manobo = 'mbd'; + case Molale = 'mbe'; + case Baba_Malay = 'mbf'; + case Mangseng = 'mbh'; + case Ilianen_Manobo = 'mbi'; + case Nadeb = 'mbj'; + case Malol = 'mbk'; + case Maxakali = 'mbl'; + case Ombamba = 'mbm'; + case Macaguan = 'mbn'; + case Mbo_Cameroon = 'mbo'; + case Malayo = 'mbp'; + case Maisin = 'mbq'; + case Nukak_Maku = 'mbr'; + case Sarangani_Manobo = 'mbs'; + case Matigsalug_Manobo = 'mbt'; + case Mbula_Bwazza = 'mbu'; + case Mbulungish = 'mbv'; + case Maring = 'mbw'; + case Mari_East_Sepik_Province = 'mbx'; + case Memoni = 'mby'; + case Amoltepec_Mixtec = 'mbz'; + case Maca = 'mca'; + case Machiguenga = 'mcb'; + case Bitur = 'mcc'; + case Sharanahua = 'mcd'; + case Itundujia_Mixtec = 'mce'; + case Matses = 'mcf'; + case Mapoyo = 'mcg'; + case Maquiritari = 'mch'; + case Mese = 'mci'; + case Mvanip = 'mcj'; + case Mbunda = 'mck'; + case Macaguaje = 'mcl'; + case Malaccan_Creole_Portuguese = 'mcm'; + case Masana = 'mcn'; + case Coatlan_Mixe = 'mco'; + case Makaa = 'mcp'; + case Ese = 'mcq'; + case Menya = 'mcr'; + case Mambai = 'mcs'; + case Mengisa = 'mct'; + case Cameroon_Mambila = 'mcu'; + case Minanibai = 'mcv'; + case Mawa_Chad = 'mcw'; + case Mpiemo = 'mcx'; + case South_Watut = 'mcy'; + case Mawan = 'mcz'; + case Mada_Nigeria = 'mda'; + case Morigi = 'mdb'; + case Male_Papua_New_Guinea = 'mdc'; + case Mbum = 'mdd'; + case Maba_Chad = 'mde'; + case Moksha = 'mdf'; + case Massalat = 'mdg'; + case Maguindanaon = 'mdh'; + case Mamvu = 'mdi'; + case Mangbetu = 'mdj'; + case Mangbutu = 'mdk'; + case Maltese_Sign_Language = 'mdl'; + case Mayogo = 'mdm'; + case Mbati = 'mdn'; + case Mbala = 'mdp'; + case Mbole = 'mdq'; + case Mandar = 'mdr'; + case Maria_Papua_New_Guinea = 'mds'; + case Mbere = 'mdt'; + case Mboko = 'mdu'; + case Santa_Lucia_Monteverde_Mixtec = 'mdv'; + case Mbosi = 'mdw'; + case Dizin = 'mdx'; + case Male_Ethiopia = 'mdy'; + case Surui_Do_Para = 'mdz'; + case Menka = 'mea'; + case Ikobi = 'meb'; + case Marra = 'mec'; + case Melpa = 'med'; + case Mengen = 'mee'; + case Megam = 'mef'; + case Southwestern_Tlaxiaco_Mixtec = 'meh'; + case Midob = 'mei'; + case Meyah = 'mej'; + case Mekeo = 'mek'; + case Central_Melanau = 'mel'; + case Mangala = 'mem'; + case Mende_Sierra_Leone = 'men'; + case Kedah_Malay = 'meo'; + case Miriwoong = 'mep'; + case Merey = 'meq'; + case Meru = 'mer'; + case Masmaje = 'mes'; + case Mato = 'met'; + case Motu = 'meu'; + case Mano = 'mev'; + case Maaka = 'mew'; + case Hassaniyya = 'mey'; + case Menominee = 'mez'; + case Pattani_Malay = 'mfa'; + case Bangka = 'mfb'; + case Mba = 'mfc'; + case Mendankwe_Nkwen = 'mfd'; + case Morisyen = 'mfe'; + case Naki = 'mff'; + case Mogofin = 'mfg'; + case Matal = 'mfh'; + case Wandala = 'mfi'; + case Mefele = 'mfj'; + case North_Mofu = 'mfk'; + case Putai = 'mfl'; + case Marghi_South = 'mfm'; + case Cross_River_Mbembe = 'mfn'; + case Mbe = 'mfo'; + case Makassar_Malay = 'mfp'; + case Moba = 'mfq'; + case Marrithiyel = 'mfr'; + case Mexican_Sign_Language = 'mfs'; + case Mokerang = 'mft'; + case Mbwela = 'mfu'; + case Mandjak = 'mfv'; + case Mulaha = 'mfw'; + case Melo = 'mfx'; + case Mayo = 'mfy'; + case Mabaan = 'mfz'; + case Middle_Irish_900_1200 = 'mga'; + case Mararit = 'mgb'; + case Morokodo = 'mgc'; + case Moru = 'mgd'; + case Mango = 'mge'; + case Maklew = 'mgf'; + case Mpumpong = 'mgg'; + case Makhuwa_Meetto = 'mgh'; + case Lijili = 'mgi'; + case Abureni = 'mgj'; + case Mawes = 'mgk'; + case Maleu_Kilenge = 'mgl'; + case Mambae = 'mgm'; + case Mbangi = 'mgn'; + case Meta = 'mgo'; + case Eastern_Magar = 'mgp'; + case Malila = 'mgq'; + case Mambwe_Lungu = 'mgr'; + case Manda_Tanzania = 'mgs'; + case Mongol = 'mgt'; + case Mailu = 'mgu'; + case Matengo = 'mgv'; + case Matumbi = 'mgw'; + case Mbunga = 'mgy'; + case Mbugwe = 'mgz'; + case Manda_India = 'mha'; + case Mahongwe = 'mhb'; + case Mocho = 'mhc'; + case Mbugu = 'mhd'; + case Besisi = 'mhe'; + case Mamaa = 'mhf'; + case Margu = 'mhg'; + case Ma_di = 'mhi'; + case Mogholi = 'mhj'; + case Mungaka = 'mhk'; + case Mauwake = 'mhl'; + case Makhuwa_Moniga = 'mhm'; + case Mocheno = 'mhn'; + case Mashi_Zambia = 'mho'; + case Balinese_Malay = 'mhp'; + case Mandan = 'mhq'; + case Eastern_Mari = 'mhr'; + case Buru_Indonesia = 'mhs'; + case Mandahuaca = 'mht'; + case Digaro_Mishmi = 'mhu'; + case Mbukushu = 'mhw'; + case Maru = 'mhx'; + case Ma_anyan = 'mhy'; + case Mor_Mor_Islands = 'mhz'; + case Miami = 'mia'; + case Atatlahuca_Mixtec = 'mib'; + case Mi_kmaq = 'mic'; + case Mandaic = 'mid'; + case Ocotepec_Mixtec = 'mie'; + case Mofu_Gudur = 'mif'; + case San_Miguel_El_Grande_Mixtec = 'mig'; + case Chayuco_Mixtec = 'mih'; + case Chigmecatitlan_Mixtec = 'mii'; + case Abar = 'mij'; + case Mikasuki = 'mik'; + case Penoles_Mixtec = 'mil'; + case Alacatlatzala_Mixtec = 'mim'; + case Minangkabau = 'min'; + case Pinotepa_Nacional_Mixtec = 'mio'; + case Apasco_Apoala_Mixtec = 'mip'; + case Miskito = 'miq'; + case Isthmus_Mixe = 'mir'; + case Uncoded_languages = 'mis'; + case Southern_Puebla_Mixtec = 'mit'; + case Cacaloxtepec_Mixtec = 'miu'; + case Akoye = 'miw'; + case Mixtepec_Mixtec = 'mix'; + case Ayutla_Mixtec = 'miy'; + case Coatzospan_Mixtec = 'miz'; + case Makalero = 'mjb'; + case San_Juan_Colorado_Mixtec = 'mjc'; + case Northwest_Maidu = 'mjd'; + case Muskum = 'mje'; + case Tu = 'mjg'; + case Mwera_Nyasa = 'mjh'; + case Kim_Mun = 'mji'; + case Mawak = 'mjj'; + case Matukar = 'mjk'; + case Mandeali = 'mjl'; + case Medebur = 'mjm'; + case Ma_Papua_New_Guinea = 'mjn'; + case Malankuravan = 'mjo'; + case Malapandaram = 'mjp'; + case Malaryan = 'mjq'; + case Malavedan = 'mjr'; + case Miship = 'mjs'; + case Sauria_Paharia = 'mjt'; + case Manna_Dora = 'mju'; + case Mannan = 'mjv'; + case Karbi = 'mjw'; + case Mahali = 'mjx'; + case Mahican = 'mjy'; + case Majhi = 'mjz'; + case Mbre = 'mka'; + case Mal_Paharia = 'mkb'; + case Siliput = 'mkc'; + case Macedonian = 'mkd'; + case Mawchi = 'mke'; + case Miya = 'mkf'; + case Mak_China = 'mkg'; + case Dhatki = 'mki'; + case Mokilese = 'mkj'; + case Byep = 'mkk'; + case Mokole = 'mkl'; + case Moklen = 'mkm'; + case Kupang_Malay = 'mkn'; + case Mingang_Doso = 'mko'; + case Moikodi = 'mkp'; + case Bay_Miwok = 'mkq'; + case Malas = 'mkr'; + case Silacayoapan_Mixtec = 'mks'; + case Vamale = 'mkt'; + case Konyanka_Maninka = 'mku'; + case Mafea = 'mkv'; + case Kituba_Congo = 'mkw'; + case Kinamiging_Manobo = 'mkx'; + case East_Makian = 'mky'; + case Makasae = 'mkz'; + case Malo = 'mla'; + case Mbule = 'mlb'; + case Cao_Lan = 'mlc'; + case Manambu = 'mle'; + case Mal = 'mlf'; + case Malagasy = 'mlg'; + case Mape = 'mlh'; + case Malimpung = 'mli'; + case Miltu = 'mlj'; + case Ilwana = 'mlk'; + case Malua_Bay = 'mll'; + case Mulam = 'mlm'; + case Malango = 'mln'; + case Mlomp = 'mlo'; + case Bargam = 'mlp'; + case Western_Maninkakan = 'mlq'; + case Vame = 'mlr'; + case Masalit = 'mls'; + case Maltese = 'mlt'; + case To_abaita = 'mlu'; + case Motlav = 'mlv'; + case Moloko = 'mlw'; + case Malfaxal = 'mlx'; + case Malaynon = 'mlz'; + case Mama = 'mma'; + case Momina = 'mmb'; + case Michoacan_Mazahua = 'mmc'; + case Maonan = 'mmd'; + case Mae = 'mme'; + case Mundat = 'mmf'; + case North_Ambrym = 'mmg'; + case Mehinaku = 'mmh'; + case Musar = 'mmi'; + case Majhwar = 'mmj'; + case Mukha_Dora = 'mmk'; + case Man_Met = 'mml'; + case Maii = 'mmm'; + case Mamanwa = 'mmn'; + case Mangga_Buang = 'mmo'; + case Siawi = 'mmp'; + case Musak = 'mmq'; + case Western_Xiangxi_Miao = 'mmr'; + case Malalamai = 'mmt'; + case Mmaala = 'mmu'; + case Miriti = 'mmv'; + case Emae = 'mmw'; + case Madak = 'mmx'; + case Migaama = 'mmy'; + case Mabaale = 'mmz'; + case Mbula = 'mna'; + case Muna = 'mnb'; + case Manchu = 'mnc'; + case Monde = 'mnd'; + case Naba = 'mne'; + case Mundani = 'mnf'; + case Eastern_Mnong = 'mng'; + case Mono_Democratic_Republic_of_Congo = 'mnh'; + case Manipuri = 'mni'; + case Munji = 'mnj'; + case Mandinka = 'mnk'; + case Tiale = 'mnl'; + case Mapena = 'mnm'; + case Southern_Mnong = 'mnn'; + case Min_Bei_Chinese = 'mnp'; + case Minriq = 'mnq'; + case Mono_USA = 'mnr'; + case Mansi = 'mns'; + case Mer = 'mnu'; + case Rennell_Bellona = 'mnv'; + case Mon = 'mnw'; + case Manikion = 'mnx'; + case Manyawa = 'mny'; + case Moni = 'mnz'; + case Mwan = 'moa'; + case Mocovi = 'moc'; + case Mobilian = 'mod'; + case Innu = 'moe'; + case Mongondow = 'mog'; + case Mohawk = 'moh'; + case Mboi = 'moi'; + case Monzombo = 'moj'; + case Morori = 'mok'; + case Mangue = 'mom'; + case Mongolian = 'mon'; + case Monom = 'moo'; + case Mopan_Maya = 'mop'; + case Mor_Bomberai_Peninsula = 'moq'; + case Moro = 'mor'; + case Mossi = 'mos'; + case Bari_2 = 'mot'; + case Mogum = 'mou'; + case Mohave = 'mov'; + case Moi_Congo = 'mow'; + case Molima = 'mox'; + case Shekkacho = 'moy'; + case Mukulu = 'moz'; + case Mpoto = 'mpa'; + case Malak_Malak = 'mpb'; + case Mangarrayi = 'mpc'; + case Machinere = 'mpd'; + case Majang = 'mpe'; + case Marba = 'mpg'; + case Maung = 'mph'; + case Mpade = 'mpi'; + case Martu_Wangka = 'mpj'; + case Mbara_Chad = 'mpk'; + case Middle_Watut = 'mpl'; + case Yosondua_Mixtec = 'mpm'; + case Mindiri = 'mpn'; + case Miu = 'mpo'; + case Migabac = 'mpp'; + case Matis = 'mpq'; + case Vangunu = 'mpr'; + case Dadibi = 'mps'; + case Mian = 'mpt'; + case Makurap = 'mpu'; + case Mungkip = 'mpv'; + case Mapidian = 'mpw'; + case Misima_Panaeati = 'mpx'; + case Mapia = 'mpy'; + case Mpi = 'mpz'; + case Maba_Indonesia = 'mqa'; + case Mbuko = 'mqb'; + case Mangole = 'mqc'; + case Matepi = 'mqe'; + case Momuna = 'mqf'; + case Kota_Bangun_Kutai_Malay = 'mqg'; + case Tlazoyaltepec_Mixtec = 'mqh'; + case Mariri = 'mqi'; + case Mamasa = 'mqj'; + case Rajah_Kabunsuwan_Manobo = 'mqk'; + case Mbelime = 'mql'; + case South_Marquesan = 'mqm'; + case Moronene = 'mqn'; + case Modole = 'mqo'; + case Manipa = 'mqp'; + case Minokok = 'mqq'; + case Mander = 'mqr'; + case West_Makian = 'mqs'; + case Mok = 'mqt'; + case Mandari = 'mqu'; + case Mosimo = 'mqv'; + case Murupi = 'mqw'; + case Mamuju = 'mqx'; + case Manggarai = 'mqy'; + case Pano = 'mqz'; + case Mlabri = 'mra'; + case Marino = 'mrb'; + case Maricopa = 'mrc'; + case Western_Magar = 'mrd'; + case Martha_s_Vineyard_Sign_Language = 'mre'; + case Elseng = 'mrf'; + case Mising = 'mrg'; + case Mara_Chin = 'mrh'; + case Maori = 'mri'; + case Western_Mari = 'mrj'; + case Hmwaveke = 'mrk'; + case Mortlockese = 'mrl'; + case Merlav = 'mrm'; + case Cheke_Holo = 'mrn'; + case Mru = 'mro'; + case Morouas = 'mrp'; + case North_Marquesan = 'mrq'; + case Maria_India = 'mrr'; + case Maragus = 'mrs'; + case Marghi_Central = 'mrt'; + case Mono_Cameroon = 'mru'; + case Mangareva = 'mrv'; + case Maranao = 'mrw'; + case Maremgi = 'mrx'; + case Mandaya = 'mry'; + case Marind = 'mrz'; + case Malay_macrolanguage = 'msa'; + case Masbatenyo = 'msb'; + case Sankaran_Maninka = 'msc'; + case Yucatec_Maya_Sign_Language = 'msd'; + case Musey = 'mse'; + case Mekwei = 'msf'; + case Moraid = 'msg'; + case Masikoro_Malagasy = 'msh'; + case Sabah_Malay = 'msi'; + case Ma_Democratic_Republic_of_Congo = 'msj'; + case Mansaka = 'msk'; + case Molof = 'msl'; + case Agusan_Manobo = 'msm'; + case Vures = 'msn'; + case Mombum = 'mso'; + case Maritsaua = 'msp'; + case Caac = 'msq'; + case Mongolian_Sign_Language = 'msr'; + case West_Masela = 'mss'; + case Musom = 'msu'; + case Maslam = 'msv'; + case Mansoanka = 'msw'; + case Moresada = 'msx'; + case Aruamu = 'msy'; + case Momare = 'msz'; + case Cotabato_Manobo = 'mta'; + case Anyin_Morofo = 'mtb'; + case Munit = 'mtc'; + case Mualang = 'mtd'; + case Mono_Solomon_Islands = 'mte'; + case Murik_Papua_New_Guinea = 'mtf'; + case Una = 'mtg'; + case Munggui = 'mth'; + case Maiwa_Papua_New_Guinea = 'mti'; + case Moskona = 'mtj'; + case Mbe_2 = 'mtk'; + case Montol = 'mtl'; + case Mator = 'mtm'; + case Matagalpa = 'mtn'; + case Totontepec_Mixe = 'mto'; + case Wichi_Lhamtes_Nocten = 'mtp'; + case Muong = 'mtq'; + case Mewari = 'mtr'; + case Yora = 'mts'; + case Mota = 'mtt'; + case Tututepec_Mixtec = 'mtu'; + case Asaro_o = 'mtv'; + case Southern_Binukidnon = 'mtw'; + case Tidaa_Mixtec = 'mtx'; + case Nabi = 'mty'; + case Mundang = 'mua'; + case Mubi = 'mub'; + case Ajumbu = 'muc'; + case Mednyj_Aleut = 'mud'; + case Media_Lengua = 'mue'; + case Musgu = 'mug'; + case Mundu = 'muh'; + case Musi = 'mui'; + case Mabire = 'muj'; + case Mugom = 'muk'; + case Multiple_languages = 'mul'; + case Maiwala = 'mum'; + case Nyong = 'muo'; + case Malvi = 'mup'; + case Eastern_Xiangxi_Miao = 'muq'; + case Murle = 'mur'; + case Creek = 'mus'; + case Western_Muria = 'mut'; + case Yaaku = 'muu'; + case Muthuvan = 'muv'; + case Bo_Ung = 'mux'; + case Muyang = 'muy'; + case Mursi = 'muz'; + case Manam = 'mva'; + case Mattole = 'mvb'; + case Mamboru = 'mvd'; + case Marwari_Pakistan = 'mve'; + case Peripheral_Mongolian = 'mvf'; + case Yucuane_Mixtec = 'mvg'; + case Mulgi = 'mvh'; + case Miyako = 'mvi'; + case Mekmek = 'mvk'; + case Mbara_Australia = 'mvl'; + case Minaveha = 'mvn'; + case Marovo = 'mvo'; + case Duri = 'mvp'; + case Moere = 'mvq'; + case Marau = 'mvr'; + case Massep = 'mvs'; + case Mpotovoro = 'mvt'; + case Marfa = 'mvu'; + case Tagal_Murut = 'mvv'; + case Machinga = 'mvw'; + case Meoswar = 'mvx'; + case Indus_Kohistani = 'mvy'; + case Mesqan = 'mvz'; + case Mwatebu = 'mwa'; + case Juwal = 'mwb'; + case Are = 'mwc'; + case Mwera_Chimwera = 'mwe'; + case Murrinh_Patha = 'mwf'; + case Aiklep = 'mwg'; + case Mouk_Aria = 'mwh'; + case Labo = 'mwi'; + case Kita_Maninkakan = 'mwk'; + case Mirandese = 'mwl'; + case Sar = 'mwm'; + case Nyamwanga = 'mwn'; + case Central_Maewo = 'mwo'; + case Kala_Lagaw_Ya = 'mwp'; + case Mun_Chin = 'mwq'; + case Marwari = 'mwr'; + case Mwimbi_Muthambi = 'mws'; + case Moken = 'mwt'; + case Mittu = 'mwu'; + case Mentawai = 'mwv'; + case Hmong_Daw = 'mww'; + case Moingi = 'mwz'; + case Northwest_Oaxaca_Mixtec = 'mxa'; + case Tezoatlan_Mixtec = 'mxb'; + case Manyika = 'mxc'; + case Modang = 'mxd'; + case Mele_Fila = 'mxe'; + case Malgbe = 'mxf'; + case Mbangala = 'mxg'; + case Mvuba = 'mxh'; + case Mozarabic = 'mxi'; + case Miju_Mishmi = 'mxj'; + case Monumbo = 'mxk'; + case Maxi_Gbe = 'mxl'; + case Meramera = 'mxm'; + case Moi_Indonesia = 'mxn'; + case Mbowe = 'mxo'; + case Tlahuitoltepec_Mixe = 'mxp'; + case Juquila_Mixe = 'mxq'; + case Murik_Malaysia = 'mxr'; + case Huitepec_Mixtec = 'mxs'; + case Jamiltepec_Mixtec = 'mxt'; + case Mada_Cameroon = 'mxu'; + case Metlatonoc_Mixtec = 'mxv'; + case Namo = 'mxw'; + case Mahou = 'mxx'; + case Southeastern_Nochixtlan_Mixtec = 'mxy'; + case Central_Masela = 'mxz'; + case Burmese = 'mya'; + case Mbay = 'myb'; + case Mayeka = 'myc'; + case Myene = 'mye'; + case Bambassi = 'myf'; + case Manta = 'myg'; + case Makah = 'myh'; + case Mangayat = 'myj'; + case Mamara_Senoufo = 'myk'; + case Moma = 'myl'; + case Me_en = 'mym'; + case Anfillo = 'myo'; + case Piraha = 'myp'; + case Muniche = 'myr'; + case Mesmes = 'mys'; + case Munduruku = 'myu'; + case Erzya = 'myv'; + case Muyuw = 'myw'; + case Masaaba = 'myx'; + case Macuna = 'myy'; + case Classical_Mandaic = 'myz'; + case Santa_Maria_Zacatepec_Mixtec = 'mza'; + case Tumzabt = 'mzb'; + case Madagascar_Sign_Language = 'mzc'; + case Malimba = 'mzd'; + case Morawa = 'mze'; + case Monastic_Sign_Language = 'mzg'; + case Wichi_Lhamtes_Guisnay = 'mzh'; + case Ixcatlan_Mazatec = 'mzi'; + case Manya = 'mzj'; + case Nigeria_Mambila = 'mzk'; + case Mazatlan_Mixe = 'mzl'; + case Mumuye = 'mzm'; + case Mazanderani = 'mzn'; + case Matipuhy = 'mzo'; + case Movima = 'mzp'; + case Mori_Atas = 'mzq'; + case Marubo = 'mzr'; + case Macanese = 'mzs'; + case Mintil = 'mzt'; + case Inapang = 'mzu'; + case Manza = 'mzv'; + case Deg = 'mzw'; + case Mawayana = 'mzx'; + case Mozambican_Sign_Language = 'mzy'; + case Maiadomu = 'mzz'; + case Namla = 'naa'; + case Southern_Nambikuara = 'nab'; + case Narak = 'nac'; + case Naka_ela = 'nae'; + case Nabak = 'naf'; + case Naga_Pidgin = 'nag'; + case Nalu = 'naj'; + case Nakanai = 'nak'; + case Nalik = 'nal'; + case Ngan_gityemerri = 'nam'; + case Min_Nan_Chinese = 'nan'; + case Naaba = 'nao'; + case Neapolitan = 'nap'; + case Khoekhoe = 'naq'; + case Iguta = 'nar'; + case Naasioi = 'nas'; + case Cahungwarya = 'nat'; + case Nauru = 'nau'; + case Navajo = 'nav'; + case Nawuri = 'naw'; + case Nakwi = 'nax'; + case Ngarrindjeri = 'nay'; + case Coatepec_Nahuatl = 'naz'; + case Nyemba = 'nba'; + case Ndoe = 'nbb'; + case Chang_Naga = 'nbc'; + case Ngbinda = 'nbd'; + case Konyak_Naga = 'nbe'; + case Nagarchal = 'nbg'; + case Ngamo = 'nbh'; + case Mao_Naga = 'nbi'; + case Ngarinyman = 'nbj'; + case Nake = 'nbk'; + case South_Ndebele = 'nbl'; + case Ngbaka_Ma_bo = 'nbm'; + case Kuri = 'nbn'; + case Nkukoli = 'nbo'; + case Nnam = 'nbp'; + case Nggem = 'nbq'; + case Numana = 'nbr'; + case Namibian_Sign_Language = 'nbs'; + case Na = 'nbt'; + case Rongmei_Naga = 'nbu'; + case Ngamambo = 'nbv'; + case Southern_Ngbandi = 'nbw'; + case Ningera = 'nby'; + case Iyo = 'nca'; + case Central_Nicobarese = 'ncb'; + case Ponam = 'ncc'; + case Nachering = 'ncd'; + case Yale = 'nce'; + case Notsi = 'ncf'; + case Nisga_a = 'ncg'; + case Central_Huasteca_Nahuatl = 'nch'; + case Classical_Nahuatl = 'nci'; + case Northern_Puebla_Nahuatl = 'ncj'; + case Na_kara = 'nck'; + case Michoacan_Nahuatl = 'ncl'; + case Nambo = 'ncm'; + case Nauna = 'ncn'; + case Sibe = 'nco'; + case Northern_Katang = 'ncq'; + case Ncane = 'ncr'; + case Nicaraguan_Sign_Language = 'ncs'; + case Chothe_Naga = 'nct'; + case Chumburung = 'ncu'; + case Central_Puebla_Nahuatl = 'ncx'; + case Natchez = 'ncz'; + case Ndasa = 'nda'; + case Kenswei_Nsei = 'ndb'; + case Ndau = 'ndc'; + case Nde_Nsele_Nta = 'ndd'; + case North_Ndebele = 'nde'; + case Nadruvian = 'ndf'; + case Ndengereko = 'ndg'; + case Ndali = 'ndh'; + case Samba_Leko = 'ndi'; + case Ndamba = 'ndj'; + case Ndaka = 'ndk'; + case Ndolo = 'ndl'; + case Ndam = 'ndm'; + case Ngundi = 'ndn'; + case Ndonga = 'ndo'; + case Ndo = 'ndp'; + case Ndombe = 'ndq'; + case Ndoola = 'ndr'; + case Low_German = 'nds'; + case Ndunga = 'ndt'; + case Dugun = 'ndu'; + case Ndut = 'ndv'; + case Ndobo = 'ndw'; + case Nduga = 'ndx'; + case Lutos = 'ndy'; + case Ndogo = 'ndz'; + case Eastern_Ngad_a = 'nea'; + case Toura_Cote_d_Ivoire = 'neb'; + case Nedebang = 'nec'; + case Nde_Gbite = 'ned'; + case Nelemwa_Nixumwak = 'nee'; + case Nefamese = 'nef'; + case Negidal = 'neg'; + case Nyenkha = 'neh'; + case Neo_Hittite = 'nei'; + case Neko = 'nej'; + case Neku = 'nek'; + case Nemi = 'nem'; + case Nengone = 'nen'; + case Na_Meo = 'neo'; + case Nepali_macrolanguage = 'nep'; + case North_Central_Mixe = 'neq'; + case Yahadian = 'ner'; + case Bhoti_Kinnauri = 'nes'; + case Nete = 'net'; + case Neo = 'neu'; + case Nyaheun = 'nev'; + case Newari = 'new'; + case Neme = 'nex'; + case Neyo = 'ney'; + case Nez_Perce = 'nez'; + case Dhao = 'nfa'; + case Ahwai = 'nfd'; + case Ayiwo = 'nfl'; + case Nafaanra = 'nfr'; + case Mfumte = 'nfu'; + case Ngbaka = 'nga'; + case Northern_Ngbandi = 'ngb'; + case Ngombe_Democratic_Republic_of_Congo = 'ngc'; + case Ngando_Central_African_Republic = 'ngd'; + case Ngemba = 'nge'; + case Ngbaka_Manza = 'ngg'; + case N_ng = 'ngh'; + case Ngizim = 'ngi'; + case Ngie = 'ngj'; + case Dalabon = 'ngk'; + case Lomwe = 'ngl'; + case Ngatik_Men_s_Creole = 'ngm'; + case Ngwo = 'ngn'; + case Ngulu = 'ngp'; + case Ngurimi = 'ngq'; + case Engdewu = 'ngr'; + case Gvoko = 'ngs'; + case Kriang = 'ngt'; + case Guerrero_Nahuatl = 'ngu'; + case Nagumi = 'ngv'; + case Ngwaba = 'ngw'; + case Nggwahyi = 'ngx'; + case Tibea = 'ngy'; + case Ngungwel = 'ngz'; + case Nhanda = 'nha'; + case Beng = 'nhb'; + case Tabasco_Nahuatl = 'nhc'; + case Chiripa = 'nhd'; + case Eastern_Huasteca_Nahuatl = 'nhe'; + case Nhuwala = 'nhf'; + case Tetelcingo_Nahuatl = 'nhg'; + case Nahari = 'nhh'; + case Zacatlan_Ahuacatlan_Tepetzintla_Nahuatl = 'nhi'; + case Isthmus_Cosoleacaque_Nahuatl = 'nhk'; + case Morelos_Nahuatl = 'nhm'; + case Central_Nahuatl = 'nhn'; + case Takuu = 'nho'; + case Isthmus_Pajapan_Nahuatl = 'nhp'; + case Huaxcaleca_Nahuatl = 'nhq'; + case Naro = 'nhr'; + case Ometepec_Nahuatl = 'nht'; + case Noone = 'nhu'; + case Temascaltepec_Nahuatl = 'nhv'; + case Western_Huasteca_Nahuatl = 'nhw'; + case Isthmus_Mecayapan_Nahuatl = 'nhx'; + case Northern_Oaxaca_Nahuatl = 'nhy'; + case Santa_Maria_La_Alta_Nahuatl = 'nhz'; + case Nias = 'nia'; + case Nakame = 'nib'; + case Ngandi = 'nid'; + case Niellim = 'nie'; + case Nek = 'nif'; + case Ngalakgan = 'nig'; + case Nyiha_Tanzania = 'nih'; + case Nii = 'nii'; + case Ngaju = 'nij'; + case Southern_Nicobarese = 'nik'; + case Nila = 'nil'; + case Nilamba = 'nim'; + case Ninzo = 'nin'; + case Nganasan = 'nio'; + case Nandi = 'niq'; + case Nimboran = 'nir'; + case Nimi = 'nis'; + case Southeastern_Kolami = 'nit'; + case Niuean = 'niu'; + case Gilyak = 'niv'; + case Nimo = 'niw'; + case Hema = 'nix'; + case Ngiti = 'niy'; + case Ningil = 'niz'; + case Nzanyi = 'nja'; + case Nocte_Naga = 'njb'; + case Ndonde_Hamba = 'njd'; + case Lotha_Naga = 'njh'; + case Gudanji = 'nji'; + case Njen = 'njj'; + case Njalgulgule = 'njl'; + case Angami_Naga = 'njm'; + case Liangmai_Naga = 'njn'; + case Ao_Naga = 'njo'; + case Njerep = 'njr'; + case Nisa = 'njs'; + case Ndyuka_Trio_Pidgin = 'njt'; + case Ngadjunmaya = 'nju'; + case Kunyi = 'njx'; + case Njyem = 'njy'; + case Nyishi = 'njz'; + case Nkoya = 'nka'; + case Khoibu_Naga = 'nkb'; + case Nkongho = 'nkc'; + case Koireng = 'nkd'; + case Duke = 'nke'; + case Inpui_Naga = 'nkf'; + case Nekgini = 'nkg'; + case Khezha_Naga = 'nkh'; + case Thangal_Naga = 'nki'; + case Nakai = 'nkj'; + case Nokuku = 'nkk'; + case Namat = 'nkm'; + case Nkangala = 'nkn'; + case Nkonya = 'nko'; + case Niuatoputapu = 'nkp'; + case Nkami = 'nkq'; + case Nukuoro = 'nkr'; + case North_Asmat = 'nks'; + case Nyika_Tanzania = 'nkt'; + case Bouna_Kulango = 'nku'; + case Nyika_Malawi_and_Zambia = 'nkv'; + case Nkutu = 'nkw'; + case Nkoroo = 'nkx'; + case Nkari = 'nkz'; + case Ngombale = 'nla'; + case Nalca = 'nlc'; + case Dutch = 'nld'; + case East_Nyala = 'nle'; + case Gela = 'nlg'; + case Grangali = 'nli'; + case Nyali = 'nlj'; + case Ninia_Yali = 'nlk'; + case Nihali = 'nll'; + case Mankiyali = 'nlm'; + case Ngul = 'nlo'; + case Lao_Naga = 'nlq'; + case Nchumbulu = 'nlu'; + case Orizaba_Nahuatl = 'nlv'; + case Walangama = 'nlw'; + case Nahali = 'nlx'; + case Nyamal = 'nly'; + case Nalogo = 'nlz'; + case Maram_Naga = 'nma'; + case Big_Nambas = 'nmb'; + case Ngam = 'nmc'; + case Ndumu = 'nmd'; + case Mzieme_Naga = 'nme'; + case Tangkhul_Naga_India = 'nmf'; + case Kwasio = 'nmg'; + case Monsang_Naga = 'nmh'; + case Nyam = 'nmi'; + case Ngombe_Central_African_Republic = 'nmj'; + case Namakura = 'nmk'; + case Ndemli = 'nml'; + case Manangba = 'nmm'; + case Xoo = 'nmn'; + case Moyon_Naga = 'nmo'; + case Nimanbur = 'nmp'; + case Nambya = 'nmq'; + case Nimbari = 'nmr'; + case Letemboi = 'nms'; + case Namonuito = 'nmt'; + case Northeast_Maidu = 'nmu'; + case Ngamini = 'nmv'; + case Nimoa = 'nmw'; + case Nama_Papua_New_Guinea = 'nmx'; + case Namuyi = 'nmy'; + case Nawdm = 'nmz'; + case Nyangumarta = 'nna'; + case Nande = 'nnb'; + case Nancere = 'nnc'; + case West_Ambae = 'nnd'; + case Ngandyera = 'nne'; + case Ngaing = 'nnf'; + case Maring_Naga = 'nng'; + case Ngiemboon = 'nnh'; + case North_Nuaulu = 'nni'; + case Nyangatom = 'nnj'; + case Nankina = 'nnk'; + case Northern_Rengma_Naga = 'nnl'; + case Namia = 'nnm'; + case Ngete = 'nnn'; + case Norwegian_Nynorsk = 'nno'; + case Wancho_Naga = 'nnp'; + case Ngindo = 'nnq'; + case Narungga = 'nnr'; + case Nanticoke = 'nnt'; + case Dwang = 'nnu'; + case Nugunu_Australia = 'nnv'; + case Southern_Nuni = 'nnw'; + case Nyangga = 'nny'; + case Nda_nda = 'nnz'; + case Woun_Meu = 'noa'; + case Norwegian_Bokmal = 'nob'; + case Nuk = 'noc'; + case Northern_Thai = 'nod'; + case Nimadi = 'noe'; + case Nomane = 'nof'; + case Nogai = 'nog'; + case Nomu = 'noh'; + case Noiri = 'noi'; + case Nonuya = 'noj'; + case Nooksack = 'nok'; + case Nomlaki = 'nol'; + case Old_Norse = 'non'; + case Numanggang = 'nop'; + case Ngongo = 'noq'; + case Norwegian = 'nor'; + case Eastern_Nisu = 'nos'; + case Nomatsiguenga = 'not'; + case Ewage_Notu = 'nou'; + case Novial = 'nov'; + case Nyambo = 'now'; + case Noy = 'noy'; + case Nayi = 'noz'; + case Nar_Phu = 'npa'; + case Nupbikha = 'npb'; + case Ponyo_Gongwang_Naga = 'npg'; + case Phom_Naga = 'nph'; + case Nepali_individual_language = 'npi'; + case Southeastern_Puebla_Nahuatl = 'npl'; + case Mondropolon = 'npn'; + case Pochuri_Naga = 'npo'; + case Nipsan = 'nps'; + case Puimei_Naga = 'npu'; + case Noipx = 'npx'; + case Napu = 'npy'; + case Southern_Nago = 'nqg'; + case Kura_Ede_Nago = 'nqk'; + case Ngendelengo = 'nql'; + case Ndom = 'nqm'; + case Nen = 'nqn'; + case N_Ko = 'nqo'; + case Kyan_Karyaw_Naga = 'nqq'; + case Nteng = 'nqt'; + case Akyaung_Ari_Naga = 'nqy'; + case Ngom = 'nra'; + case Nara = 'nrb'; + case Noric = 'nrc'; + case Southern_Rengma_Naga = 'nre'; + case Jerriais = 'nrf'; + case Narango = 'nrg'; + case Chokri_Naga = 'nri'; + case Ngarla = 'nrk'; + case Ngarluma = 'nrl'; + case Narom = 'nrm'; + case Norn = 'nrn'; + case North_Picene = 'nrp'; + case Norra = 'nrr'; + case Northern_Kalapuya = 'nrt'; + case Narua = 'nru'; + case Ngurmbur = 'nrx'; + case Lala = 'nrz'; + case Sangtam_Naga = 'nsa'; + case Lower_Nossob = 'nsb'; + case Nshi = 'nsc'; + case Southern_Nisu = 'nsd'; + case Nsenga = 'nse'; + case Northwestern_Nisu = 'nsf'; + case Ngasa = 'nsg'; + case Ngoshie = 'nsh'; + case Nigerian_Sign_Language = 'nsi'; + case Naskapi = 'nsk'; + case Norwegian_Sign_Language = 'nsl'; + case Sumi_Naga = 'nsm'; + case Nehan = 'nsn'; + case Pedi = 'nso'; + case Nepalese_Sign_Language = 'nsp'; + case Northern_Sierra_Miwok = 'nsq'; + case Maritime_Sign_Language = 'nsr'; + case Nali = 'nss'; + case Tase_Naga = 'nst'; + case Sierra_Negra_Nahuatl = 'nsu'; + case Southwestern_Nisu = 'nsv'; + case Navut = 'nsw'; + case Nsongo = 'nsx'; + case Nasal = 'nsy'; + case Nisenan = 'nsz'; + case Northern_Tidung = 'ntd'; + case Nathembo = 'nte'; + case Ngantangarra = 'ntg'; + case Natioro = 'nti'; + case Ngaanyatjarra = 'ntj'; + case Ikoma_Nata_Isenye = 'ntk'; + case Nateni = 'ntm'; + case Ntomba = 'nto'; + case Northern_Tepehuan = 'ntp'; + case Delo = 'ntr'; + case Natugu = 'ntu'; + case Nottoway = 'ntw'; + case Tangkhul_Naga_Myanmar = 'ntx'; + case Mantsi = 'nty'; + case Natanzi = 'ntz'; + case Yuanga = 'nua'; + case Nukuini = 'nuc'; + case Ngala = 'nud'; + case Ngundu = 'nue'; + case Nusu = 'nuf'; + case Nungali = 'nug'; + case Ndunda = 'nuh'; + case Ngumbi = 'nui'; + case Nyole = 'nuj'; + case Nuu_chah_nulth = 'nuk'; + case Nusa_Laut = 'nul'; + case Niuafo_ou = 'num'; + case Anong = 'nun'; + case Nguon = 'nuo'; + case Nupe_Nupe_Tako = 'nup'; + case Nukumanu = 'nuq'; + case Nukuria = 'nur'; + case Nuer = 'nus'; + case Nung_Viet_Nam = 'nut'; + case Ngbundu = 'nuu'; + case Northern_Nuni = 'nuv'; + case Nguluwan = 'nuw'; + case Mehek = 'nux'; + case Nunggubuyu = 'nuy'; + case Tlamacazapa_Nahuatl = 'nuz'; + case Nasarian = 'nvh'; + case Namiae = 'nvm'; + case Nyokon = 'nvo'; + case Nawathinehena = 'nwa'; + case Nyabwa = 'nwb'; + case Classical_Newari = 'nwc'; + case Ngwe = 'nwe'; + case Ngayawung = 'nwg'; + case Southwest_Tanna = 'nwi'; + case Nyamusa_Molo = 'nwm'; + case Nauo = 'nwo'; + case Nawaru = 'nwr'; + case Ndwewe = 'nww'; + case Middle_Newar = 'nwx'; + case Nottoway_Meherrin = 'nwy'; + case Nauete = 'nxa'; + case Ngando_Democratic_Republic_of_Congo = 'nxd'; + case Nage = 'nxe'; + case Ngad_a = 'nxg'; + case Nindi = 'nxi'; + case Koki_Naga = 'nxk'; + case South_Nuaulu = 'nxl'; + case Numidian = 'nxm'; + case Ngawun = 'nxn'; + case Ndambomo = 'nxo'; + case Naxi = 'nxq'; + case Ninggerum = 'nxr'; + case Nafri = 'nxx'; + case Nyanja = 'nya'; + case Nyangbo = 'nyb'; + case Nyanga_li = 'nyc'; + case Nyore = 'nyd'; + case Nyengo = 'nye'; + case Giryama = 'nyf'; + case Nyindu = 'nyg'; + case Nyikina = 'nyh'; + case Ama_Sudan = 'nyi'; + case Nyanga = 'nyj'; + case Nyaneka = 'nyk'; + case Nyeu = 'nyl'; + case Nyamwezi = 'nym'; + case Nyankole = 'nyn'; + case Nyoro = 'nyo'; + case Nyang_i = 'nyp'; + case Nayini = 'nyq'; + case Nyiha_Malawi = 'nyr'; + case Nyungar = 'nys'; + case Nyawaygi = 'nyt'; + case Nyungwe = 'nyu'; + case Nyulnyul = 'nyv'; + case Nyaw = 'nyw'; + case Nganyaywana = 'nyx'; + case Nyakyusa_Ngonde = 'nyy'; + case Tigon_Mbembe = 'nza'; + case Njebi = 'nzb'; + case Nzadi = 'nzd'; + case Nzima = 'nzi'; + case Nzakara = 'nzk'; + case Zeme_Naga = 'nzm'; + case Dir_Nyamzak_Mbarimi = 'nzr'; + case New_Zealand_Sign_Language = 'nzs'; + case Teke_Nzikou = 'nzu'; + case Nzakambay = 'nzy'; + case Nanga_Dama_Dogon = 'nzz'; + case Orok = 'oaa'; + case Oroch = 'oac'; + case Old_Aramaic_up_to_700_BCE = 'oar'; + case Old_Avar = 'oav'; + case Obispeno = 'obi'; + case Southern_Bontok = 'obk'; + case Oblo = 'obl'; + case Moabite = 'obm'; + case Obo_Manobo = 'obo'; + case Old_Burmese = 'obr'; + case Old_Breton = 'obt'; + case Obulom = 'obu'; + case Ocaina = 'oca'; + case Old_Chinese = 'och'; + case Occitan_post_1500 = 'oci'; + case Old_Cham = 'ocm'; + case Old_Cornish = 'oco'; + case Atzingo_Matlatzinca = 'ocu'; + case Odut = 'oda'; + case Od = 'odk'; + case Old_Dutch = 'odt'; + case Odual = 'odu'; + case Ofo = 'ofo'; + case Old_Frisian = 'ofs'; + case Efutop = 'ofu'; + case Ogbia = 'ogb'; + case Ogbah = 'ogc'; + case Old_Georgian = 'oge'; + case Ogbogolo = 'ogg'; + case Khana = 'ogo'; + case Ogbronuagum = 'ogu'; + case Old_Hittite = 'oht'; + case Old_Hungarian = 'ohu'; + case Oirata = 'oia'; + case Okolie = 'oie'; + case Inebu_One = 'oin'; + case Northwestern_Ojibwa = 'ojb'; + case Central_Ojibwa = 'ojc'; + case Eastern_Ojibwa = 'ojg'; + case Ojibwa = 'oji'; + case Old_Japanese = 'ojp'; + case Severn_Ojibwa = 'ojs'; + case Ontong_Java = 'ojv'; + case Western_Ojibwa = 'ojw'; + case Okanagan = 'oka'; + case Okobo = 'okb'; + case Kobo = 'okc'; + case Okodia = 'okd'; + case Okpe_Southwestern_Edo = 'oke'; + case Koko_Babangk = 'okg'; + case Koresh_e_Rostam = 'okh'; + case Okiek = 'oki'; + case Oko_Juwoi = 'okj'; + case Kwamtim_One = 'okk'; + case Old_Kentish_Sign_Language = 'okl'; + case Middle_Korean_10th_16th_cent = 'okm'; + case Oki_No_Erabu = 'okn'; + case Old_Korean_3rd_9th_cent = 'oko'; + case Kirike = 'okr'; + case Oko_Eni_Osayen = 'oks'; + case Oku = 'oku'; + case Orokaiva = 'okv'; + case Okpe_Northwestern_Edo = 'okx'; + case Old_Khmer = 'okz'; + case Walungge = 'ola'; + case Mochi = 'old'; + case Olekha = 'ole'; + case Olkol = 'olk'; + case Oloma = 'olm'; + case Livvi = 'olo'; + case Olrat = 'olr'; + case Old_Lithuanian = 'olt'; + case Kuvale = 'olu'; + case Omaha_Ponca = 'oma'; + case East_Ambae = 'omb'; + case Mochica = 'omc'; + case Omagua = 'omg'; + case Omi = 'omi'; + case Omok = 'omk'; + case Ombo = 'oml'; + case Minoan = 'omn'; + case Utarmbung = 'omo'; + case Old_Manipuri = 'omp'; + case Old_Marathi = 'omr'; + case Omotik = 'omt'; + case Omurano = 'omu'; + case South_Tairora = 'omw'; + case Old_Mon = 'omx'; + case Old_Malay = 'omy'; + case Ona = 'ona'; + case Lingao = 'onb'; + case Oneida = 'one'; + case Olo = 'ong'; + case Onin = 'oni'; + case Onjob = 'onj'; + case Kabore_One = 'onk'; + case Onobasulu = 'onn'; + case Onondaga = 'ono'; + case Sartang = 'onp'; + case Northern_One = 'onr'; + case Ono = 'ons'; + case Ontenu = 'ont'; + case Unua = 'onu'; + case Old_Nubian = 'onw'; + case Onin_Based_Pidgin = 'onx'; + case Tohono_O_odham = 'ood'; + case Ong = 'oog'; + case Onge = 'oon'; + case Oorlams = 'oor'; + case Old_Ossetic = 'oos'; + case Okpamheri = 'opa'; + case Kopkaka = 'opk'; + case Oksapmin = 'opm'; + case Opao = 'opo'; + case Opata = 'opt'; + case Ofaye = 'opy'; + case Oroha = 'ora'; + case Orma = 'orc'; + case Orejon = 'ore'; + case Oring = 'org'; + case Oroqen = 'orh'; + case Oriya_macrolanguage = 'ori'; + case Oromo = 'orm'; + case Orang_Kanaq = 'orn'; + case Orokolo = 'oro'; + case Oruma = 'orr'; + case Orang_Seletar = 'ors'; + case Adivasi_Oriya = 'ort'; + case Ormuri = 'oru'; + case Old_Russian = 'orv'; + case Oro_Win = 'orw'; + case Oro = 'orx'; + case Odia = 'ory'; + case Ormu = 'orz'; + case Osage = 'osa'; + case Oscan = 'osc'; + case Osing = 'osi'; + case Old_Sundanese = 'osn'; + case Ososo = 'oso'; + case Old_Spanish = 'osp'; + case Ossetian = 'oss'; + case Osatu = 'ost'; + case Southern_One = 'osu'; + case Old_Saxon = 'osx'; + case Ottoman_Turkish_1500_1928 = 'ota'; + case Old_Tibetan = 'otb'; + case Ot_Danum = 'otd'; + case Mezquital_Otomi = 'ote'; + case Oti = 'oti'; + case Old_Turkish = 'otk'; + case Tilapa_Otomi = 'otl'; + case Eastern_Highland_Otomi = 'otm'; + case Tenango_Otomi = 'otn'; + case Queretaro_Otomi = 'otq'; + case Otoro = 'otr'; + case Estado_de_Mexico_Otomi = 'ots'; + case Temoaya_Otomi = 'ott'; + case Otuke = 'otu'; + case Ottawa = 'otw'; + case Texcatepec_Otomi = 'otx'; + case Old_Tamil = 'oty'; + case Ixtenco_Otomi = 'otz'; + case Tagargrent = 'oua'; + case Glio_Oubi = 'oub'; + case Oune = 'oue'; + case Old_Uighur = 'oui'; + case Ouma = 'oum'; + case Elfdalian = 'ovd'; + case Owiniga = 'owi'; + case Old_Welsh = 'owl'; + case Oy = 'oyb'; + case Oyda = 'oyd'; + case Wayampi = 'oym'; + case Oya_oya = 'oyy'; + case Koonzime = 'ozm'; + case Parecis = 'pab'; + case Pacoh = 'pac'; + case Paumari = 'pad'; + case Pagibete = 'pae'; + case Paranawat = 'paf'; + case Pangasinan = 'pag'; + case Tenharim = 'pah'; + case Pe = 'pai'; + case Parakana = 'pak'; + case Pahlavi = 'pal'; + case Pampanga = 'pam'; + case Panjabi = 'pan'; + case Northern_Paiute = 'pao'; + case Papiamento = 'pap'; + case Parya = 'paq'; + case Panamint = 'par'; + case Papasena = 'pas'; + case Palauan = 'pau'; + case Pakaasnovos = 'pav'; + case Pawnee = 'paw'; + case Pankarare = 'pax'; + case Pech = 'pay'; + case Pankararu = 'paz'; + case Paez = 'pbb'; + case Patamona = 'pbc'; + case Mezontla_Popoloca = 'pbe'; + case Coyotepec_Popoloca = 'pbf'; + case Paraujano = 'pbg'; + case E_napa_Woromaipu = 'pbh'; + case Parkwa = 'pbi'; + case Mak_Nigeria = 'pbl'; + case Puebla_Mazatec = 'pbm'; + case Kpasam = 'pbn'; + case Papel = 'pbo'; + case Badyara = 'pbp'; + case Pangwa = 'pbr'; + case Central_Pame = 'pbs'; + case Southern_Pashto = 'pbt'; + case Northern_Pashto = 'pbu'; + case Pnar = 'pbv'; + case Pyu_Papua_New_Guinea = 'pby'; + case Santa_Ines_Ahuatempan_Popoloca = 'pca'; + case Pear = 'pcb'; + case Bouyei = 'pcc'; + case Picard = 'pcd'; + case Ruching_Palaung = 'pce'; + case Paliyan = 'pcf'; + case Paniya = 'pcg'; + case Pardhan = 'pch'; + case Duruwa = 'pci'; + case Parenga = 'pcj'; + case Paite_Chin = 'pck'; + case Pardhi = 'pcl'; + case Nigerian_Pidgin = 'pcm'; + case Piti = 'pcn'; + case Pacahuara = 'pcp'; + case Pyapun = 'pcw'; + case Anam = 'pda'; + case Pennsylvania_German = 'pdc'; + case Pa_Di = 'pdi'; + case Podena = 'pdn'; + case Padoe = 'pdo'; + case Plautdietsch = 'pdt'; + case Kayan = 'pdu'; + case Peranakan_Indonesian = 'pea'; + case Eastern_Pomo = 'peb'; + case Mala_Papua_New_Guinea = 'ped'; + case Taje = 'pee'; + case Northeastern_Pomo = 'pef'; + case Pengo = 'peg'; + case Bonan = 'peh'; + case Chichimeca_Jonaz = 'pei'; + case Northern_Pomo = 'pej'; + case Penchal = 'pek'; + case Pekal = 'pel'; + case Phende = 'pem'; + case Old_Persian_ca_600_400_B_C = 'peo'; + case Kunja = 'pep'; + case Southern_Pomo = 'peq'; + case Iranian_Persian = 'pes'; + case Pemono = 'pev'; + case Petats = 'pex'; + case Petjo = 'pey'; + case Eastern_Penan = 'pez'; + case Paafang = 'pfa'; + case Pere = 'pfe'; + case Pfaelzisch = 'pfl'; + case Sudanese_Creole_Arabic = 'pga'; + case Gandhari = 'pgd'; + case Pangwali = 'pgg'; + case Pagi = 'pgi'; + case Rerep = 'pgk'; + case Primitive_Irish = 'pgl'; + case Paelignian = 'pgn'; + case Pangseng = 'pgs'; + case Pagu = 'pgu'; + case Papua_New_Guinean_Sign_Language = 'pgz'; + case Pa_Hng = 'pha'; + case Phudagi = 'phd'; + case Phuong = 'phg'; + case Phukha = 'phh'; + case Pahari = 'phj'; + case Phake = 'phk'; + case Phalura = 'phl'; + case Phimbi = 'phm'; + case Phoenician = 'phn'; + case Phunoi = 'pho'; + case Phana = 'phq'; + case Pahari_Potwari = 'phr'; + case Phu_Thai = 'pht'; + case Phuan = 'phu'; + case Pahlavani = 'phv'; + case Phangduwali = 'phw'; + case Pima_Bajo = 'pia'; + case Yine = 'pib'; + case Pinji = 'pic'; + case Piaroa = 'pid'; + case Piro = 'pie'; + case Pingelapese = 'pif'; + case Pisabo = 'pig'; + case Pitcairn_Norfolk = 'pih'; + case Pijao = 'pij'; + case Yom = 'pil'; + case Powhatan = 'pim'; + case Piame = 'pin'; + case Piapoco = 'pio'; + case Pero = 'pip'; + case Piratapuyo = 'pir'; + case Pijin = 'pis'; + case Pitta_Pitta = 'pit'; + case Pintupi_Luritja = 'piu'; + case Pileni = 'piv'; + case Pimbwe = 'piw'; + case Piu = 'pix'; + case Piya_Kwonci = 'piy'; + case Pije = 'piz'; + case Pitjantjatjara = 'pjt'; + case Ardhamagadhi_Prakrit = 'pka'; + case Pokomo = 'pkb'; + case Paekche = 'pkc'; + case Pak_Tong = 'pkg'; + case Pankhu = 'pkh'; + case Pakanha = 'pkn'; + case Pokoot = 'pko'; + case Pukapuka = 'pkp'; + case Attapady_Kurumba = 'pkr'; + case Pakistan_Sign_Language = 'pks'; + case Maleng = 'pkt'; + case Paku = 'pku'; + case Miani = 'pla'; + case Polonombauk = 'plb'; + case Central_Palawano = 'plc'; + case Polari = 'pld'; + case Palu_e = 'ple'; + case Pilaga = 'plg'; + case Paulohi = 'plh'; + case Pali = 'pli'; + case Kohistani_Shina = 'plk'; + case Shwe_Palaung = 'pll'; + case Palenquero = 'pln'; + case Oluta_Popoluca = 'plo'; + case Palaic = 'plq'; + case Palaka_Senoufo = 'plr'; + case San_Marcos_Tlacoyalco_Popoloca = 'pls'; + case Plateau_Malagasy = 'plt'; + case Palikur = 'plu'; + case Southwest_Palawano = 'plv'; + case Brooke_s_Point_Palawano = 'plw'; + case Bolyu = 'ply'; + case Paluan = 'plz'; + case Paama = 'pma'; + case Pambia = 'pmb'; + case Pallanganmiddang = 'pmd'; + case Pwaamei = 'pme'; + case Pamona = 'pmf'; + case Maharastri_Prakrit = 'pmh'; + case Northern_Pumi = 'pmi'; + case Southern_Pumi = 'pmj'; + case Lingua_Franca = 'pml'; + case Pomo = 'pmm'; + case Pam = 'pmn'; + case Pom = 'pmo'; + case Northern_Pame = 'pmq'; + case Paynamar = 'pmr'; + case Piemontese = 'pms'; + case Tuamotuan = 'pmt'; + case Plains_Miwok = 'pmw'; + case Poumei_Naga = 'pmx'; + case Papuan_Malay = 'pmy'; + case Southern_Pame = 'pmz'; + case Punan_Bah_Biau = 'pna'; + case Western_Panjabi = 'pnb'; + case Pannei = 'pnc'; + case Mpinda = 'pnd'; + case Western_Penan = 'pne'; + case Pangu = 'png'; + case Penrhyn = 'pnh'; + case Aoheng = 'pni'; + case Pinjarup = 'pnj'; + case Paunaka = 'pnk'; + case Paleni = 'pnl'; + case Punan_Batu_1 = 'pnm'; + case Pinai_Hagahai = 'pnn'; + case Panobo = 'pno'; + case Pancana = 'pnp'; + case Pana_Burkina_Faso = 'pnq'; + case Panim = 'pnr'; + case Ponosakan = 'pns'; + case Pontic = 'pnt'; + case Jiongnai_Bunu = 'pnu'; + case Pinigura = 'pnv'; + case Banyjima = 'pnw'; + case Phong_Kniang = 'pnx'; + case Pinyin = 'pny'; + case Pana_Central_African_Republic = 'pnz'; + case Poqomam = 'poc'; + case San_Juan_Atzingo_Popoloca = 'poe'; + case Poke = 'pof'; + case Potiguara = 'pog'; + case Poqomchi = 'poh'; + case Highland_Popoluca = 'poi'; + case Pokanga = 'pok'; + case Polish = 'pol'; + case Southeastern_Pomo = 'pom'; + case Pohnpeian = 'pon'; + case Central_Pomo = 'poo'; + case Pwapwa = 'pop'; + case Texistepec_Popoluca = 'poq'; + case Portuguese = 'por'; + case Sayula_Popoluca = 'pos'; + case Potawatomi = 'pot'; + case Upper_Guinea_Crioulo = 'pov'; + case San_Felipe_Otlaltepec_Popoloca = 'pow'; + case Polabian = 'pox'; + case Pogolo = 'poy'; + case Papi = 'ppe'; + case Paipai = 'ppi'; + case Uma = 'ppk'; + case Pipil = 'ppl'; + case Papuma = 'ppm'; + case Papapana = 'ppn'; + case Folopa = 'ppo'; + case Pelende = 'ppp'; + case Pei = 'ppq'; + case San_Luis_Temalacayuca_Popoloca = 'pps'; + case Pare = 'ppt'; + case Papora = 'ppu'; + case Pa_a = 'pqa'; + case Malecite_Passamaquoddy = 'pqm'; + case Parachi = 'prc'; + case Parsi_Dari = 'prd'; + case Principense = 'pre'; + case Paranan = 'prf'; + case Prussian = 'prg'; + case Porohanon = 'prh'; + case Paici = 'pri'; + case Parauk = 'prk'; + case Peruvian_Sign_Language = 'prl'; + case Kibiri = 'prm'; + case Prasuni = 'prn'; + case Old_Provencal_to_1500 = 'pro'; + case Asheninka_Perene = 'prq'; + case Puri = 'prr'; + case Dari = 'prs'; + case Phai = 'prt'; + case Puragi = 'pru'; + case Parawen = 'prw'; + case Purik = 'prx'; + case Providencia_Sign_Language = 'prz'; + case Asue_Awyu = 'psa'; + case Iranian_Sign_Language = 'psc'; + case Plains_Indian_Sign_Language = 'psd'; + case Central_Malay = 'pse'; + case Penang_Sign_Language = 'psg'; + case Southwest_Pashai = 'psh'; + case Southeast_Pashai = 'psi'; + case Puerto_Rican_Sign_Language = 'psl'; + case Pauserna = 'psm'; + case Panasuan = 'psn'; + case Polish_Sign_Language = 'pso'; + case Philippine_Sign_Language = 'psp'; + case Pasi = 'psq'; + case Portuguese_Sign_Language = 'psr'; + case Kaulong = 'pss'; + case Central_Pashto = 'pst'; + case Sauraseni_Prakrit = 'psu'; + case Port_Sandwich = 'psw'; + case Piscataway = 'psy'; + case Pai_Tavytera = 'pta'; + case Pataxo_Ha_Ha_Hae = 'pth'; + case Pindiini = 'pti'; + case Patani = 'ptn'; + case Zo_e = 'pto'; + case Patep = 'ptp'; + case Pattapu = 'ptq'; + case Piamatsina = 'ptr'; + case Enrekang = 'ptt'; + case Bambam = 'ptu'; + case Port_Vato = 'ptv'; + case Pentlatch = 'ptw'; + case Pathiya = 'pty'; + case Western_Highland_Purepecha = 'pua'; + case Purum = 'pub'; + case Punan_Merap = 'puc'; + case Punan_Aput = 'pud'; + case Puelche = 'pue'; + case Punan_Merah = 'puf'; + case Phuie = 'pug'; + case Puinave = 'pui'; + case Punan_Tubu = 'puj'; + case Puma = 'pum'; + case Puoc = 'puo'; + case Pulabu = 'pup'; + case Puquina = 'puq'; + case Purubora = 'pur'; + case Pushto = 'pus'; + case Putoh = 'put'; + case Punu = 'puu'; + case Puluwatese = 'puw'; + case Puare = 'pux'; + case Purisimeno = 'puy'; + case Pawaia = 'pwa'; + case Panawa = 'pwb'; + case Gapapaiwa = 'pwg'; + case Patwin = 'pwi'; + case Molbog = 'pwm'; + case Paiwan = 'pwn'; + case Pwo_Western_Karen = 'pwo'; + case Powari = 'pwr'; + case Pwo_Northern_Karen = 'pww'; + case Quetzaltepec_Mixe = 'pxm'; + case Pye_Krumen = 'pye'; + case Fyam = 'pym'; + case Poyanawa = 'pyn'; + case Paraguayan_Sign_Language = 'pys'; + case Puyuma = 'pyu'; + case Pyu_Myanmar = 'pyx'; + case Pyen = 'pyy'; + case Pesse = 'pze'; + case Pazeh = 'pzh'; + case Jejara_Naga = 'pzn'; + case Quapaw = 'qua'; + case Huallaga_Huanuco_Quechua = 'qub'; + case K_iche = 'quc'; + case Calderon_Highland_Quichua = 'qud'; + case Quechua = 'que'; + case Lambayeque_Quechua = 'quf'; + case Chimborazo_Highland_Quichua = 'qug'; + case South_Bolivian_Quechua = 'quh'; + case Quileute = 'qui'; + case Chachapoyas_Quechua = 'quk'; + case North_Bolivian_Quechua = 'qul'; + case Sipacapense = 'qum'; + case Quinault = 'qun'; + case Southern_Pastaza_Quechua = 'qup'; + case Quinqui = 'quq'; + case Yanahuanca_Pasco_Quechua = 'qur'; + case Santiago_del_Estero_Quichua = 'qus'; + case Sacapulteco = 'quv'; + case Tena_Lowland_Quichua = 'quw'; + case Yauyos_Quechua = 'qux'; + case Ayacucho_Quechua = 'quy'; + case Cusco_Quechua = 'quz'; + case Ambo_Pasco_Quechua = 'qva'; + case Cajamarca_Quechua = 'qvc'; + case Eastern_Apurimac_Quechua = 'qve'; + case Huamalies_Dos_de_Mayo_Huanuco_Quechua = 'qvh'; + case Imbabura_Highland_Quichua = 'qvi'; + case Loja_Highland_Quichua = 'qvj'; + case Cajatambo_North_Lima_Quechua = 'qvl'; + case Margos_Yarowilca_Lauricocha_Quechua = 'qvm'; + case North_Junin_Quechua = 'qvn'; + case Napo_Lowland_Quechua = 'qvo'; + case Pacaraos_Quechua = 'qvp'; + case San_Martin_Quechua = 'qvs'; + case Huaylla_Wanca_Quechua = 'qvw'; + case Queyu = 'qvy'; + case Northern_Pastaza_Quichua = 'qvz'; + case Corongo_Ancash_Quechua = 'qwa'; + case Classical_Quechua = 'qwc'; + case Huaylas_Ancash_Quechua = 'qwh'; + case Kuman_Russia = 'qwm'; + case Sihuas_Ancash_Quechua = 'qws'; + case Kwalhioqua_Tlatskanai = 'qwt'; + case Chiquian_Ancash_Quechua = 'qxa'; + case Chincha_Quechua = 'qxc'; + case Panao_Huanuco_Quechua = 'qxh'; + case Salasaca_Highland_Quichua = 'qxl'; + case Northern_Conchucos_Ancash_Quechua = 'qxn'; + case Southern_Conchucos_Ancash_Quechua = 'qxo'; + case Puno_Quechua = 'qxp'; + case Qashqa_i = 'qxq'; + case Canar_Highland_Quichua = 'qxr'; + case Southern_Qiang = 'qxs'; + case Santa_Ana_de_Tusi_Pasco_Quechua = 'qxt'; + case Arequipa_La_Union_Quechua = 'qxu'; + case Jauja_Wanca_Quechua = 'qxw'; + case Quenya = 'qya'; + case Quiripi = 'qyp'; + case Dungmali = 'raa'; + case Camling = 'rab'; + case Rasawa = 'rac'; + case Rade = 'rad'; + case Western_Meohang = 'raf'; + case Logooli = 'rag'; + case Rabha = 'rah'; + case Ramoaaina = 'rai'; + case Rajasthani = 'raj'; + case Tulu_Bohuai = 'rak'; + case Ralte = 'ral'; + case Canela = 'ram'; + case Riantana = 'ran'; + case Rao = 'rao'; + case Rapanui = 'rap'; + case Saam = 'raq'; + case Rarotongan = 'rar'; + case Tegali = 'ras'; + case Razajerdi = 'rat'; + case Raute = 'rau'; + case Sampang = 'rav'; + case Rawang = 'raw'; + case Rang = 'rax'; + case Rapa = 'ray'; + case Rahambuu = 'raz'; + case Rumai_Palaung = 'rbb'; + case Northern_Bontok = 'rbk'; + case Miraya_Bikol = 'rbl'; + case Barababaraba = 'rbp'; + case Reunion_Creole_French = 'rcf'; + case Rudbari = 'rdb'; + case Rerau = 'rea'; + case Rembong = 'reb'; + case Rejang_Kayan = 'ree'; + case Kara_Tanzania = 'reg'; + case Reli = 'rei'; + case Rejang = 'rej'; + case Rendille = 'rel'; + case Remo = 'rem'; + case Rengao = 'ren'; + case Rer_Bare = 'rer'; + case Reshe = 'res'; + case Retta = 'ret'; + case Reyesano = 'rey'; + case Roria = 'rga'; + case Romano_Greek = 'rge'; + case Rangkas = 'rgk'; + case Romagnol = 'rgn'; + case Resigaro = 'rgr'; + case Southern_Roglai = 'rgs'; + case Ringgou = 'rgu'; + case Rohingya = 'rhg'; + case Yahang = 'rhp'; + case Riang_India = 'ria'; + case Bribri_Sign_Language = 'rib'; + case Tarifit = 'rif'; + case Riang_Lang = 'ril'; + case Nyaturu = 'rim'; + case Nungu = 'rin'; + case Ribun = 'rir'; + case Ritharrngu = 'rit'; + case Riung = 'riu'; + case Rajong = 'rjg'; + case Raji = 'rji'; + case Rajbanshi = 'rjs'; + case Kraol = 'rka'; + case Rikbaktsa = 'rkb'; + case Rakahanga_Manihiki = 'rkh'; + case Rakhine = 'rki'; + case Marka = 'rkm'; + case Rangpuri = 'rkt'; + case Arakwal = 'rkw'; + case Rama = 'rma'; + case Rembarrnga = 'rmb'; + case Carpathian_Romani = 'rmc'; + case Traveller_Danish = 'rmd'; + case Angloromani = 'rme'; + case Kalo_Finnish_Romani = 'rmf'; + case Traveller_Norwegian = 'rmg'; + case Murkim = 'rmh'; + case Lomavren = 'rmi'; + case Romkun = 'rmk'; + case Baltic_Romani = 'rml'; + case Roma = 'rmm'; + case Balkan_Romani = 'rmn'; + case Sinte_Romani = 'rmo'; + case Rempi = 'rmp'; + case Calo = 'rmq'; + case Romanian_Sign_Language = 'rms'; + case Domari = 'rmt'; + case Tavringer_Romani = 'rmu'; + case Romanova = 'rmv'; + case Welsh_Romani = 'rmw'; + case Romam = 'rmx'; + case Vlax_Romani = 'rmy'; + case Marma = 'rmz'; + case Brunca_Sign_Language = 'rnb'; + case Ruund = 'rnd'; + case Ronga = 'rng'; + case Ranglong = 'rnl'; + case Roon = 'rnn'; + case Rongpo = 'rnp'; + case Nari_Nari = 'rnr'; + case Rungwa = 'rnw'; + case Tae = 'rob'; + case Cacgia_Roglai = 'roc'; + case Rogo = 'rod'; + case Ronji = 'roe'; + case Rombo = 'rof'; + case Northern_Roglai = 'rog'; + case Romansh = 'roh'; + case Romblomanon = 'rol'; + case Romany = 'rom'; + case Romanian = 'ron'; + case Rotokas = 'roo'; + case Kriol = 'rop'; + case Rongga = 'ror'; + case Runga = 'rou'; + case Dela_Oenale = 'row'; + case Repanbitip = 'rpn'; + case Rapting = 'rpt'; + case Ririo = 'rri'; + case Moriori = 'rrm'; + case Waima = 'rro'; + case Arritinngithigh = 'rrt'; + case Romano_Serbian = 'rsb'; + case Ruthenian = 'rsk'; + case Russian_Sign_Language = 'rsl'; + case Miriwoong_Sign_Language = 'rsm'; + case Rwandan_Sign_Language = 'rsn'; + case Rishiwa = 'rsw'; + case Rungtu_Chin = 'rtc'; + case Ratahan = 'rth'; + case Rotuman = 'rtm'; + case Yurats = 'rts'; + case Rathawi = 'rtw'; + case Gungu = 'rub'; + case Ruuli = 'ruc'; + case Rusyn = 'rue'; + case Luguru = 'ruf'; + case Roviana = 'rug'; + case Ruga = 'ruh'; + case Rufiji = 'rui'; + case Che = 'ruk'; + case Rundi = 'run'; + case Istro_Romanian = 'ruo'; + case Macedo_Romanian = 'rup'; + case Megleno_Romanian = 'ruq'; + case Russian = 'rus'; + case Rutul = 'rut'; + case Lanas_Lobu = 'ruu'; + case Mala_Nigeria = 'ruy'; + case Ruma = 'ruz'; + case Rawo = 'rwa'; + case Rwa = 'rwk'; + case Ruwila = 'rwl'; + case Amba_Uganda = 'rwm'; + case Rawa = 'rwo'; + case Marwari_India = 'rwr'; + case Ngardi = 'rxd'; + case Karuwali = 'rxw'; + case Northern_Amami_Oshima = 'ryn'; + case Yaeyama = 'rys'; + case Central_Okinawan = 'ryu'; + case Razihi = 'rzh'; + case Saba = 'saa'; + case Buglere = 'sab'; + case Meskwaki = 'sac'; + case Sandawe = 'sad'; + case Sabane = 'sae'; + case Safaliba = 'saf'; + case Sango = 'sag'; + case Yakut = 'sah'; + case Sahu = 'saj'; + case Sake = 'sak'; + case Samaritan_Aramaic = 'sam'; + case Sanskrit = 'san'; + case Sause = 'sao'; + case Samburu = 'saq'; + case Saraveca = 'sar'; + case Sasak = 'sas'; + case Santali = 'sat'; + case Saleman = 'sau'; + case Saafi_Saafi = 'sav'; + case Sawi = 'saw'; + case Sa = 'sax'; + case Saya = 'say'; + case Saurashtra = 'saz'; + case Ngambay = 'sba'; + case Simbo = 'sbb'; + case Kele_Papua_New_Guinea = 'sbc'; + case Southern_Samo = 'sbd'; + case Saliba = 'sbe'; + case Chabu = 'sbf'; + case Seget = 'sbg'; + case Sori_Harengan = 'sbh'; + case Seti = 'sbi'; + case Surbakhal = 'sbj'; + case Safwa = 'sbk'; + case Botolan_Sambal = 'sbl'; + case Sagala = 'sbm'; + case Sindhi_Bhil = 'sbn'; + case Sabum = 'sbo'; + case Sangu_Tanzania = 'sbp'; + case Sileibi = 'sbq'; + case Sembakung_Murut = 'sbr'; + case Subiya = 'sbs'; + case Kimki = 'sbt'; + case Stod_Bhoti = 'sbu'; + case Sabine = 'sbv'; + case Simba = 'sbw'; + case Seberuang = 'sbx'; + case Soli = 'sby'; + case Sara_Kaba = 'sbz'; + case Chut = 'scb'; + case Dongxiang = 'sce'; + case San_Miguel_Creole_French = 'scf'; + case Sanggau = 'scg'; + case Sakachep = 'sch'; + case Sri_Lankan_Creole_Malay = 'sci'; + case Sadri = 'sck'; + case Shina = 'scl'; + case Sicilian = 'scn'; + case Scots = 'sco'; + case Hyolmo = 'scp'; + case Sa_och = 'scq'; + case North_Slavey = 'scs'; + case Southern_Katang = 'sct'; + case Shumcho = 'scu'; + case Sheni = 'scv'; + case Sha = 'scw'; + case Sicel = 'scx'; + case Toraja_Sa_dan = 'sda'; + case Shabak = 'sdb'; + case Sassarese_Sardinian = 'sdc'; + case Surubu = 'sde'; + case Sarli = 'sdf'; + case Savi = 'sdg'; + case Southern_Kurdish = 'sdh'; + case Suundi = 'sdj'; + case Sos_Kundi = 'sdk'; + case Saudi_Arabian_Sign_Language = 'sdl'; + case Gallurese_Sardinian = 'sdn'; + case Bukar_Sadung_Bidayuh = 'sdo'; + case Sherdukpen = 'sdp'; + case Semandang = 'sdq'; + case Oraon_Sadri = 'sdr'; + case Sened = 'sds'; + case Shuadit = 'sdt'; + case Sarudu = 'sdu'; + case Sibu_Melanau = 'sdx'; + case Sallands = 'sdz'; + case Semai = 'sea'; + case Shempire_Senoufo = 'seb'; + case Sechelt = 'sec'; + case Sedang = 'sed'; + case Seneca = 'see'; + case Cebaara_Senoufo = 'sef'; + case Segeju = 'seg'; + case Sena = 'seh'; + case Seri = 'sei'; + case Sene = 'sej'; + case Sekani = 'sek'; + case Selkup = 'sel'; + case Nanerige_Senoufo = 'sen'; + case Suarmin = 'seo'; + case Sicite_Senoufo = 'sep'; + case Senara_Senoufo = 'seq'; + case Serrano = 'ser'; + case Koyraboro_Senni_Songhai = 'ses'; + case Sentani = 'set'; + case Serui_Laut = 'seu'; + case Nyarafolo_Senoufo = 'sev'; + case Sewa_Bay = 'sew'; + case Secoya = 'sey'; + case Senthang_Chin = 'sez'; + case Langue_des_signes_de_Belgique_Francophone = 'sfb'; + case Eastern_Subanen = 'sfe'; + case Small_Flowery_Miao = 'sfm'; + case South_African_Sign_Language = 'sfs'; + case Sehwi = 'sfw'; + case Old_Irish_to_900 = 'sga'; + case Mag_antsi_Ayta = 'sgb'; + case Kipsigis = 'sgc'; + case Surigaonon = 'sgd'; + case Segai = 'sge'; + case Swiss_German_Sign_Language = 'sgg'; + case Shughni = 'sgh'; + case Suga = 'sgi'; + case Surgujia = 'sgj'; + case Sangkong = 'sgk'; + case Singa = 'sgm'; + case Singpho = 'sgp'; + case Sangisari = 'sgr'; + case Samogitian = 'sgs'; + case Brokpake = 'sgt'; + case Salas = 'sgu'; + case Sebat_Bet_Gurage = 'sgw'; + case Sierra_Leone_Sign_Language = 'sgx'; + case Sanglechi = 'sgy'; + case Sursurunga = 'sgz'; + case Shall_Zwall = 'sha'; + case Ninam = 'shb'; + case Sonde = 'shc'; + case Kundal_Shahi = 'shd'; + case Sheko = 'she'; + case Shua = 'shg'; + case Shoshoni = 'shh'; + case Tachelhit = 'shi'; + case Shatt = 'shj'; + case Shilluk = 'shk'; + case Shendu = 'shl'; + case Shahrudi = 'shm'; + case Shan = 'shn'; + case Shanga = 'sho'; + case Shipibo_Conibo = 'shp'; + case Sala = 'shq'; + case Shi = 'shr'; + case Shuswap = 'shs'; + case Shasta = 'sht'; + case Chadian_Arabic = 'shu'; + case Shehri = 'shv'; + case Shwai = 'shw'; + case She = 'shx'; + case Tachawit = 'shy'; + case Syenara_Senoufo = 'shz'; + case Akkala_Sami = 'sia'; + case Sebop = 'sib'; + case Sidamo = 'sid'; + case Simaa = 'sie'; + case Siamou = 'sif'; + case Paasaal = 'sig'; + case Zire = 'sih'; + case Shom_Peng = 'sii'; + case Numbami = 'sij'; + case Sikiana = 'sik'; + case Tumulung_Sisaala = 'sil'; + case Mende_Papua_New_Guinea = 'sim'; + case Sinhala = 'sin'; + case Sikkimese = 'sip'; + case Sonia = 'siq'; + case Siri = 'sir'; + case Siuslaw = 'sis'; + case Sinagen = 'siu'; + case Sumariup = 'siv'; + case Siwai = 'siw'; + case Sumau = 'six'; + case Sivandi = 'siy'; + case Siwi = 'siz'; + case Epena = 'sja'; + case Sajau_Basap = 'sjb'; + case Kildin_Sami = 'sjd'; + case Pite_Sami = 'sje'; + case Assangori = 'sjg'; + case Kemi_Sami = 'sjk'; + case Sajalong = 'sjl'; + case Mapun = 'sjm'; + case Sindarin = 'sjn'; + case Xibe = 'sjo'; + case Surjapuri = 'sjp'; + case Siar_Lak = 'sjr'; + case Senhaja_De_Srair = 'sjs'; + case Ter_Sami = 'sjt'; + case Ume_Sami = 'sju'; + case Shawnee = 'sjw'; + case Skagit = 'ska'; + case Saek = 'skb'; + case Ma_Manda = 'skc'; + case Southern_Sierra_Miwok = 'skd'; + case Seke_Vanuatu = 'ske'; + case Sakirabia = 'skf'; + case Sakalava_Malagasy = 'skg'; + case Sikule = 'skh'; + case Sika = 'ski'; + case Seke_Nepal = 'skj'; + case Kutong = 'skm'; + case Kolibugan_Subanon = 'skn'; + case Seko_Tengah = 'sko'; + case Sekapan = 'skp'; + case Sininkere = 'skq'; + case Saraiki = 'skr'; + case Maia = 'sks'; + case Sakata = 'skt'; + case Sakao = 'sku'; + case Skou = 'skv'; + case Skepi_Creole_Dutch = 'skw'; + case Seko_Padang = 'skx'; + case Sikaiana = 'sky'; + case Sekar = 'skz'; + case Saliba_2 = 'slc'; + case Sissala = 'sld'; + case Sholaga = 'sle'; + case Swiss_Italian_Sign_Language = 'slf'; + case Selungai_Murut = 'slg'; + case Southern_Puget_Sound_Salish = 'slh'; + case Lower_Silesian = 'sli'; + case Saluma = 'slj'; + case Slovak = 'slk'; + case Salt_Yui = 'sll'; + case Pangutaran_Sama = 'slm'; + case Salinan = 'sln'; + case Lamaholot = 'slp'; + case Salar = 'slr'; + case Singapore_Sign_Language = 'sls'; + case Sila = 'slt'; + case Selaru = 'slu'; + case Slovenian = 'slv'; + case Sialum = 'slw'; + case Salampasu = 'slx'; + case Selayar = 'sly'; + case Ma_ya = 'slz'; + case Southern_Sami = 'sma'; + case Simbari = 'smb'; + case Som = 'smc'; + case Northern_Sami = 'sme'; + case Auwe = 'smf'; + case Simbali = 'smg'; + case Samei = 'smh'; + case Lule_Sami = 'smj'; + case Bolinao = 'smk'; + case Central_Sama = 'sml'; + case Musasa = 'smm'; + case Inari_Sami = 'smn'; + case Samoan = 'smo'; + case Samaritan = 'smp'; + case Samo = 'smq'; + case Simeulue = 'smr'; + case Skolt_Sami = 'sms'; + case Simte = 'smt'; + case Somray = 'smu'; + case Samvedi = 'smv'; + case Sumbawa = 'smw'; + case Samba = 'smx'; + case Semnani = 'smy'; + case Simeku = 'smz'; + case Shona = 'sna'; + case Sinaugoro = 'snc'; + case Sindhi = 'snd'; + case Bau_Bidayuh = 'sne'; + case Noon = 'snf'; + case Sanga_Democratic_Republic_of_Congo = 'sng'; + case Sensi = 'sni'; + case Riverain_Sango = 'snj'; + case Soninke = 'snk'; + case Sangil = 'snl'; + case Southern_Ma_di = 'snm'; + case Siona = 'snn'; + case Snohomish = 'sno'; + case Siane = 'snp'; + case Sangu_Gabon = 'snq'; + case Sihan = 'snr'; + case South_West_Bay = 'sns'; + case Senggi = 'snu'; + case Sa_ban = 'snv'; + case Selee = 'snw'; + case Sam = 'snx'; + case Saniyo_Hiyewe = 'sny'; + case Kou = 'snz'; + case Thai_Song = 'soa'; + case Sobei = 'sob'; + case So_Democratic_Republic_of_Congo = 'soc'; + case Songoora = 'sod'; + case Songomeno = 'soe'; + case Sogdian = 'sog'; + case Aka = 'soh'; + case Sonha = 'soi'; + case Soi = 'soj'; + case Sokoro = 'sok'; + case Solos = 'sol'; + case Somali = 'som'; + case Songo = 'soo'; + case Songe = 'sop'; + case Kanasi = 'soq'; + case Somrai = 'sor'; + case Seeku = 'sos'; + case Southern_Sotho = 'sot'; + case Southern_Thai = 'sou'; + case Sonsorol = 'sov'; + case Sowanda = 'sow'; + case Swo = 'sox'; + case Miyobe = 'soy'; + case Temi = 'soz'; + case Spanish = 'spa'; + case Sepa_Indonesia = 'spb'; + case Sape = 'spc'; + case Saep = 'spd'; + case Sepa_Papua_New_Guinea = 'spe'; + case Sian = 'spg'; + case Saponi = 'spi'; + case Sengo = 'spk'; + case Selepet = 'spl'; + case Akukem = 'spm'; + case Sanapana = 'spn'; + case Spokane = 'spo'; + case Supyire_Senoufo = 'spp'; + case Loreto_Ucayali_Spanish = 'spq'; + case Saparua = 'spr'; + case Saposa = 'sps'; + case Spiti_Bhoti = 'spt'; + case Sapuan = 'spu'; + case Sambalpuri = 'spv'; + case South_Picene = 'spx'; + case Sabaot = 'spy'; + case Shama_Sambuga = 'sqa'; + case Shau = 'sqh'; + case Albanian = 'sqi'; + case Albanian_Sign_Language = 'sqk'; + case Suma = 'sqm'; + case Susquehannock = 'sqn'; + case Sorkhei = 'sqo'; + case Sou = 'sqq'; + case Siculo_Arabic = 'sqr'; + case Sri_Lankan_Sign_Language = 'sqs'; + case Soqotri = 'sqt'; + case Squamish = 'squ'; + case Kufr_Qassem_Sign_Language_KQSL = 'sqx'; + case Saruga = 'sra'; + case Sora = 'srb'; + case Logudorese_Sardinian = 'src'; + case Sardinian = 'srd'; + case Sara = 'sre'; + case Nafi = 'srf'; + case Sulod = 'srg'; + case Sarikoli = 'srh'; + case Siriano = 'sri'; + case Serudung_Murut = 'srk'; + case Isirawa = 'srl'; + case Saramaccan = 'srm'; + case Sranan_Tongo = 'srn'; + case Campidanese_Sardinian = 'sro'; + case Serbian = 'srp'; + case Siriono = 'srq'; + case Serer = 'srr'; + case Sarsi = 'srs'; + case Sauri = 'srt'; + case Surui = 'sru'; + case Southern_Sorsoganon = 'srv'; + case Serua = 'srw'; + case Sirmauri = 'srx'; + case Sera = 'sry'; + case Shahmirzadi = 'srz'; + case Southern_Sama = 'ssb'; + case Suba_Simbiti = 'ssc'; + case Siroi = 'ssd'; + case Balangingi = 'sse'; + case Thao = 'ssf'; + case Seimat = 'ssg'; + case Shihhi_Arabic = 'ssh'; + case Sansi = 'ssi'; + case Sausi = 'ssj'; + case Sunam = 'ssk'; + case Western_Sisaala = 'ssl'; + case Semnam = 'ssm'; + case Waata = 'ssn'; + case Sissano = 'sso'; + case Spanish_Sign_Language = 'ssp'; + case So_a = 'ssq'; + case Swiss_French_Sign_Language = 'ssr'; + case So = 'sss'; + case Sinasina = 'sst'; + case Susuami = 'ssu'; + case Shark_Bay = 'ssv'; + case Swati = 'ssw'; + case Samberigi = 'ssx'; + case Saho = 'ssy'; + case Sengseng = 'ssz'; + case Settla = 'sta'; + case Northern_Subanen = 'stb'; + case Sentinel = 'std'; + case Liana_Seti = 'ste'; + case Seta = 'stf'; + case Trieng = 'stg'; + case Shelta = 'sth'; + case Bulo_Stieng = 'sti'; + case Matya_Samo = 'stj'; + case Arammba = 'stk'; + case Stellingwerfs = 'stl'; + case Setaman = 'stm'; + case Owa = 'stn'; + case Stoney = 'sto'; + case Southeastern_Tepehuan = 'stp'; + case Saterfriesisch = 'stq'; + case Straits_Salish = 'str'; + case Shumashti = 'sts'; + case Budeh_Stieng = 'stt'; + case Samtao = 'stu'; + case Silt_e = 'stv'; + case Satawalese = 'stw'; + case Siberian_Tatar = 'sty'; + case Sulka = 'sua'; + case Suku = 'sub'; + case Western_Subanon = 'suc'; + case Suena = 'sue'; + case Suganga = 'sug'; + case Suki = 'sui'; + case Shubi = 'suj'; + case Sukuma = 'suk'; + case Sundanese = 'sun'; + case Bouni = 'suo'; + case Tirmaga_Chai_Suri = 'suq'; + case Mwaghavul = 'sur'; + case Susu = 'sus'; + case Subtiaba = 'sut'; + case Puroik = 'suv'; + case Sumbwa = 'suw'; + case Sumerian = 'sux'; + case Suya = 'suy'; + case Sunwar = 'suz'; + case Svan = 'sva'; + case Ulau_Suain = 'svb'; + case Vincentian_Creole_English = 'svc'; + case Serili = 'sve'; + case Slovakian_Sign_Language = 'svk'; + case Slavomolisano = 'svm'; + case Savosavo = 'svs'; + case Skalvian = 'svx'; + case Swahili_macrolanguage = 'swa'; + case Maore_Comorian = 'swb'; + case Congo_Swahili = 'swc'; + case Swedish = 'swe'; + case Sere = 'swf'; + case Swabian = 'swg'; + case Swahili_individual_language = 'swh'; + case Sui = 'swi'; + case Sira = 'swj'; + case Malawi_Sena = 'swk'; + case Swedish_Sign_Language = 'swl'; + case Samosa = 'swm'; + case Sawknah = 'swn'; + case Shanenawa = 'swo'; + case Suau = 'swp'; + case Sharwa = 'swq'; + case Saweru = 'swr'; + case Seluwasan = 'sws'; + case Sawila = 'swt'; + case Suwawa = 'swu'; + case Shekhawati = 'swv'; + case Sowa = 'sww'; + case Suruaha = 'swx'; + case Sarua = 'swy'; + case Suba = 'sxb'; + case Sicanian = 'sxc'; + case Sighu = 'sxe'; + case Shuhi = 'sxg'; + case Southern_Kalapuya = 'sxk'; + case Selian = 'sxl'; + case Samre = 'sxm'; + case Sangir = 'sxn'; + case Sorothaptic = 'sxo'; + case Saaroa = 'sxr'; + case Sasaru = 'sxs'; + case Upper_Saxon = 'sxu'; + case Saxwe_Gbe = 'sxw'; + case Siang = 'sya'; + case Central_Subanen = 'syb'; + case Classical_Syriac = 'syc'; + case Seki = 'syi'; + case Sukur = 'syk'; + case Sylheti = 'syl'; + case Maya_Samo = 'sym'; + case Senaya = 'syn'; + case Suoy = 'syo'; + case Syriac = 'syr'; + case Sinyar = 'sys'; + case Kagate = 'syw'; + case Samay = 'syx'; + case Al_Sayyid_Bedouin_Sign_Language = 'syy'; + case Semelai = 'sza'; + case Ngalum = 'szb'; + case Semaq_Beri = 'szc'; + case Seze = 'sze'; + case Sengele = 'szg'; + case Silesian = 'szl'; + case Sula = 'szn'; + case Suabo = 'szp'; + case Solomon_Islands_Sign_Language = 'szs'; + case Isu_Fako_Division = 'szv'; + case Sawai = 'szw'; + case Sakizaya = 'szy'; + case Lower_Tanana = 'taa'; + case Tabassaran = 'tab'; + case Lowland_Tarahumara = 'tac'; + case Tause = 'tad'; + case Tariana = 'tae'; + case Tapirape = 'taf'; + case Tagoi = 'tag'; + case Tahitian = 'tah'; + case Eastern_Tamang = 'taj'; + case Tala = 'tak'; + case Tal = 'tal'; + case Tamil = 'tam'; + case Tangale = 'tan'; + case Yami = 'tao'; + case Taabwa = 'tap'; + case Tamasheq = 'taq'; + case Central_Tarahumara = 'tar'; + case Tay_Boi = 'tas'; + case Tatar = 'tat'; + case Upper_Tanana = 'tau'; + case Tatuyo = 'tav'; + case Tai = 'taw'; + case Tamki = 'tax'; + case Atayal = 'tay'; + case Tocho = 'taz'; + case Aikana = 'tba'; + case Takia = 'tbc'; + case Kaki_Ae = 'tbd'; + case Tanimbili = 'tbe'; + case Mandara = 'tbf'; + case North_Tairora = 'tbg'; + case Dharawal = 'tbh'; + case Gaam = 'tbi'; + case Tiang = 'tbj'; + case Calamian_Tagbanwa = 'tbk'; + case Tboli = 'tbl'; + case Tagbu = 'tbm'; + case Barro_Negro_Tunebo = 'tbn'; + case Tawala = 'tbo'; + case Taworta = 'tbp'; + case Tumtum = 'tbr'; + case Tanguat = 'tbs'; + case Tembo_Kitembo = 'tbt'; + case Tubar = 'tbu'; + case Tobo = 'tbv'; + case Tagbanwa = 'tbw'; + case Kapin = 'tbx'; + case Tabaru = 'tby'; + case Ditammari = 'tbz'; + case Ticuna = 'tca'; + case Tanacross = 'tcb'; + case Datooga = 'tcc'; + case Tafi = 'tcd'; + case Southern_Tutchone = 'tce'; + case Malinaltepec_Me_phaa = 'tcf'; + case Tamagario = 'tcg'; + case Turks_And_Caicos_Creole_English = 'tch'; + case Wara = 'tci'; + case Tchitchege = 'tck'; + case Taman_Myanmar = 'tcl'; + case Tanahmerah = 'tcm'; + case Tichurong = 'tcn'; + case Taungyo = 'tco'; + case Tawr_Chin = 'tcp'; + case Kaiy = 'tcq'; + case Torres_Strait_Creole = 'tcs'; + case T_en = 'tct'; + case Southeastern_Tarahumara = 'tcu'; + case Tecpatlan_Totonac = 'tcw'; + case Toda = 'tcx'; + case Tulu = 'tcy'; + case Thado_Chin = 'tcz'; + case Tagdal = 'tda'; + case Panchpargania = 'tdb'; + case Embera_Tado = 'tdc'; + case Tai_Nua = 'tdd'; + case Tiranige_Diga_Dogon = 'tde'; + case Talieng = 'tdf'; + case Western_Tamang = 'tdg'; + case Thulung = 'tdh'; + case Tomadino = 'tdi'; + case Tajio = 'tdj'; + case Tambas = 'tdk'; + case Sur = 'tdl'; + case Taruma = 'tdm'; + case Tondano = 'tdn'; + case Teme = 'tdo'; + case Tita = 'tdq'; + case Todrah = 'tdr'; + case Doutai = 'tds'; + case Tetun_Dili = 'tdt'; + case Toro = 'tdv'; + case Tandroy_Mahafaly_Malagasy = 'tdx'; + case Tadyawan = 'tdy'; + case Temiar = 'tea'; + case Tetete = 'teb'; + case Terik = 'tec'; + case Tepo_Krumen = 'ted'; + case Huehuetla_Tepehua = 'tee'; + case Teressa = 'tef'; + case Teke_Tege = 'teg'; + case Tehuelche = 'teh'; + case Torricelli = 'tei'; + case Ibali_Teke = 'tek'; + case Telugu = 'tel'; + case Timne = 'tem'; + case Tama_Colombia = 'ten'; + case Teso = 'teo'; + case Tepecano = 'tep'; + case Temein = 'teq'; + case Tereno = 'ter'; + case Tengger = 'tes'; + case Tetum = 'tet'; + case Soo = 'teu'; + case Teor = 'tev'; + case Tewa_USA = 'tew'; + case Tennet = 'tex'; + case Tulishi = 'tey'; + case Tetserret = 'tez'; + case Tofin_Gbe = 'tfi'; + case Tanaina = 'tfn'; + case Tefaro = 'tfo'; + case Teribe = 'tfr'; + case Ternate = 'tft'; + case Sagalla = 'tga'; + case Tobilung = 'tgb'; + case Tigak = 'tgc'; + case Ciwogai = 'tgd'; + case Eastern_Gorkha_Tamang = 'tge'; + case Chalikha = 'tgf'; + case Tobagonian_Creole_English = 'tgh'; + case Lawunuia = 'tgi'; + case Tagin = 'tgj'; + case Tajik = 'tgk'; + case Tagalog = 'tgl'; + case Tandaganon = 'tgn'; + case Sudest = 'tgo'; + case Tangoa = 'tgp'; + case Tring = 'tgq'; + case Tareng = 'tgr'; + case Nume = 'tgs'; + case Central_Tagbanwa = 'tgt'; + case Tanggu = 'tgu'; + case Tingui_Boto = 'tgv'; + case Tagwana_Senoufo = 'tgw'; + case Tagish = 'tgx'; + case Togoyo = 'tgy'; + case Tagalaka = 'tgz'; + case Thai = 'tha'; + case Kuuk_Thaayorre = 'thd'; + case Chitwania_Tharu = 'the'; + case Thangmi = 'thf'; + case Northern_Tarahumara = 'thh'; + case Tai_Long = 'thi'; + case Tharaka = 'thk'; + case Dangaura_Tharu = 'thl'; + case Aheu = 'thm'; + case Thachanadan = 'thn'; + case Thompson = 'thp'; + case Kochila_Tharu = 'thq'; + case Rana_Tharu = 'thr'; + case Thakali = 'ths'; + case Tahltan = 'tht'; + case Thuri = 'thu'; + case Tahaggart_Tamahaq = 'thv'; + case Tha = 'thy'; + case Tayart_Tamajeq = 'thz'; + case Tidikelt_Tamazight = 'tia'; + case Tira = 'tic'; + case Tifal = 'tif'; + case Tigre = 'tig'; + case Timugon_Murut = 'tih'; + case Tiene = 'tii'; + case Tilung = 'tij'; + case Tikar = 'tik'; + case Tillamook = 'til'; + case Timbe = 'tim'; + case Tindi = 'tin'; + case Teop = 'tio'; + case Trimuris = 'tip'; + case Tiefo = 'tiq'; + case Tigrinya = 'tir'; + case Masadiit_Itneg = 'tis'; + case Tinigua = 'tit'; + case Adasen = 'tiu'; + case Tiv = 'tiv'; + case Tiwi = 'tiw'; + case Southern_Tiwa = 'tix'; + case Tiruray = 'tiy'; + case Tai_Hongjin = 'tiz'; + case Tajuasohn = 'tja'; + case Tunjung = 'tjg'; + case Northern_Tujia = 'tji'; + case Tjungundji = 'tjj'; + case Tai_Laing = 'tjl'; + case Timucua = 'tjm'; + case Tonjon = 'tjn'; + case Temacine_Tamazight = 'tjo'; + case Tjupany = 'tjp'; + case Southern_Tujia = 'tjs'; + case Tjurruru = 'tju'; + case Djabwurrung = 'tjw'; + case Truka = 'tka'; + case Buksa = 'tkb'; + case Tukudede = 'tkd'; + case Takwane = 'tke'; + case Tukumanfed = 'tkf'; + case Tesaka_Malagasy = 'tkg'; + case Tokelau = 'tkl'; + case Takelma = 'tkm'; + case Toku_No_Shima = 'tkn'; + case Tikopia = 'tkp'; + case Tee = 'tkq'; + case Tsakhur = 'tkr'; + case Takestani = 'tks'; + case Kathoriya_Tharu = 'tkt'; + case Upper_Necaxa_Totonac = 'tku'; + case Mur_Pano = 'tkv'; + case Teanu = 'tkw'; + case Tangko = 'tkx'; + case Takua = 'tkz'; + case Southwestern_Tepehuan = 'tla'; + case Tobelo = 'tlb'; + case Yecuatla_Totonac = 'tlc'; + case Talaud = 'tld'; + case Telefol = 'tlf'; + case Tofanma = 'tlg'; + case Klingon = 'tlh'; + case Tlingit = 'tli'; + case Talinga_Bwisi = 'tlj'; + case Taloki = 'tlk'; + case Tetela = 'tll'; + case Tolomako = 'tlm'; + case Talondo = 'tln'; + case Talodi = 'tlo'; + case Filomena_Mata_Coahuitlan_Totonac = 'tlp'; + case Tai_Loi = 'tlq'; + case Talise = 'tlr'; + case Tambotalo = 'tls'; + case Sou_Nama = 'tlt'; + case Tulehu = 'tlu'; + case Taliabu = 'tlv'; + case Khehek = 'tlx'; + case Talysh = 'tly'; + case Tama_Chad = 'tma'; + case Katbol = 'tmb'; + case Tumak = 'tmc'; + case Haruai = 'tmd'; + case Tremembe = 'tme'; + case Toba_Maskoy = 'tmf'; + case Ternateno = 'tmg'; + case Tamashek = 'tmh'; + case Tutuba = 'tmi'; + case Samarokena = 'tmj'; + case Tamnim_Citak = 'tml'; + case Tai_Thanh = 'tmm'; + case Taman_Indonesia = 'tmn'; + case Temoq = 'tmo'; + case Tumleo = 'tmq'; + case Jewish_Babylonian_Aramaic_ca_200_1200_CE = 'tmr'; + case Tima = 'tms'; + case Tasmate = 'tmt'; + case Iau = 'tmu'; + case Tembo_Motembo = 'tmv'; + case Temuan = 'tmw'; + case Tami = 'tmy'; + case Tamanaku = 'tmz'; + case Tacana = 'tna'; + case Western_Tunebo = 'tnb'; + case Tanimuca_Retuara = 'tnc'; + case Angosturas_Tunebo = 'tnd'; + case Tobanga = 'tng'; + case Maiani = 'tnh'; + case Tandia = 'tni'; + case Kwamera = 'tnk'; + case Lenakel = 'tnl'; + case Tabla = 'tnm'; + case North_Tanna = 'tnn'; + case Toromono = 'tno'; + case Whitesands = 'tnp'; + case Taino = 'tnq'; + case Menik = 'tnr'; + case Tenis = 'tns'; + case Tontemboan = 'tnt'; + case Tay_Khang = 'tnu'; + case Tangchangya = 'tnv'; + case Tonsawang = 'tnw'; + case Tanema = 'tnx'; + case Tongwe = 'tny'; + case Ten_edn = 'tnz'; + case Toba = 'tob'; + case Coyutla_Totonac = 'toc'; + case Toma = 'tod'; + case Gizrra = 'tof'; + case Tonga_Nyasa = 'tog'; + case Gitonga = 'toh'; + case Tonga_Zambia = 'toi'; + case Tojolabal = 'toj'; + case Toki_Pona = 'tok'; + case Tolowa = 'tol'; + case Tombulu = 'tom'; + case Tonga_Tonga_Islands = 'ton'; + case Xicotepec_De_Juarez_Totonac = 'too'; + case Papantla_Totonac = 'top'; + case Toposa = 'toq'; + case Togbo_Vara_Banda = 'tor'; + case Highland_Totonac = 'tos'; + case Tho = 'tou'; + case Upper_Taromi = 'tov'; + case Jemez = 'tow'; + case Tobian = 'tox'; + case Topoiyo = 'toy'; + case To = 'toz'; + case Taupota = 'tpa'; + case Azoyu_Me_phaa = 'tpc'; + case Tippera = 'tpe'; + case Tarpia = 'tpf'; + case Kula = 'tpg'; + case Tok_Pisin = 'tpi'; + case Tapiete = 'tpj'; + case Tupinikin = 'tpk'; + case Tlacoapa_Me_phaa = 'tpl'; + case Tampulma = 'tpm'; + case Tupinamba = 'tpn'; + case Tai_Pao = 'tpo'; + case Pisaflores_Tepehua = 'tpp'; + case Tukpa = 'tpq'; + case Tupari = 'tpr'; + case Tlachichilco_Tepehua = 'tpt'; + case Tampuan = 'tpu'; + case Tanapag = 'tpv'; + case Acatepec_Me_phaa = 'tpx'; + case Trumai = 'tpy'; + case Tinputz = 'tpz'; + case Tembe = 'tqb'; + case Lehali = 'tql'; + case Turumsa = 'tqm'; + case Tenino = 'tqn'; + case Toaripi = 'tqo'; + case Tomoip = 'tqp'; + case Tunni = 'tqq'; + case Torona = 'tqr'; + case Western_Totonac = 'tqt'; + case Touo = 'tqu'; + case Tonkawa = 'tqw'; + case Tirahi = 'tra'; + case Terebu = 'trb'; + case Copala_Triqui = 'trc'; + case Turi = 'trd'; + case East_Tarangan = 'tre'; + case Trinidadian_Creole_English = 'trf'; + case Lishan_Didan = 'trg'; + case Turaka = 'trh'; + case Trio = 'tri'; + case Toram = 'trj'; + case Traveller_Scottish = 'trl'; + case Tregami = 'trm'; + case Trinitario = 'trn'; + case Tarao_Naga = 'tro'; + case Kok_Borok = 'trp'; + case San_Martin_Itunyoso_Triqui = 'trq'; + case Taushiro = 'trr'; + case Chicahuaxtla_Triqui = 'trs'; + case Tunggare = 'trt'; + case Turoyo = 'tru'; + case Sediq = 'trv'; + case Torwali = 'trw'; + case Tringgus_Sembaan_Bidayuh = 'trx'; + case Turung = 'try'; + case Tora = 'trz'; + case Tsaangi = 'tsa'; + case Tsamai = 'tsb'; + case Tswa = 'tsc'; + case Tsakonian = 'tsd'; + case Tunisian_Sign_Language = 'tse'; + case Tausug = 'tsg'; + case Tsuvan = 'tsh'; + case Tsimshian = 'tsi'; + case Tshangla = 'tsj'; + case Tseku = 'tsk'; + case Ts_un_Lao = 'tsl'; + case Turkish_Sign_Language = 'tsm'; + case Tswana = 'tsn'; + case Tsonga = 'tso'; + case Northern_Toussian = 'tsp'; + case Thai_Sign_Language = 'tsq'; + case Akei = 'tsr'; + case Taiwan_Sign_Language = 'tss'; + case Tondi_Songway_Kiini = 'tst'; + case Tsou = 'tsu'; + case Tsogo = 'tsv'; + case Tsishingini = 'tsw'; + case Mubami = 'tsx'; + case Tebul_Sign_Language = 'tsy'; + case Purepecha = 'tsz'; + case Tutelo = 'tta'; + case Gaa = 'ttb'; + case Tektiteko = 'ttc'; + case Tauade = 'ttd'; + case Bwanabwana = 'tte'; + case Tuotomb = 'ttf'; + case Tutong = 'ttg'; + case Upper_Ta_oih = 'tth'; + case Tobati = 'tti'; + case Tooro = 'ttj'; + case Totoro = 'ttk'; + case Totela = 'ttl'; + case Northern_Tutchone = 'ttm'; + case Towei = 'ttn'; + case Lower_Ta_oih = 'tto'; + case Tombelala = 'ttp'; + case Tawallammat_Tamajaq = 'ttq'; + case Tera = 'ttr'; + case Northeastern_Thai = 'tts'; + case Muslim_Tat = 'ttt'; + case Torau = 'ttu'; + case Titan = 'ttv'; + case Long_Wat = 'ttw'; + case Sikaritai = 'tty'; + case Tsum = 'ttz'; + case Wiarumus = 'tua'; + case Tubatulabal = 'tub'; + case Mutu = 'tuc'; + case Tuxa = 'tud'; + case Tuyuca = 'tue'; + case Central_Tunebo = 'tuf'; + case Tunia = 'tug'; + case Taulil = 'tuh'; + case Tupuri = 'tui'; + case Tugutil = 'tuj'; + case Turkmen = 'tuk'; + case Tula = 'tul'; + case Tumbuka = 'tum'; + case Tunica = 'tun'; + case Tucano = 'tuo'; + case Tedaga = 'tuq'; + case Turkish = 'tur'; + case Tuscarora = 'tus'; + case Tututni = 'tuu'; + case Turkana = 'tuv'; + case Tuxinawa = 'tux'; + case Tugen = 'tuy'; + case Turka = 'tuz'; + case Vaghua = 'tva'; + case Tsuvadi = 'tvd'; + case Te_un = 'tve'; + case Tulai = 'tvi'; + case Southeast_Ambrym = 'tvk'; + case Tuvalu = 'tvl'; + case Tela_Masbuar = 'tvm'; + case Tavoyan = 'tvn'; + case Tidore = 'tvo'; + case Taveta = 'tvs'; + case Tutsa_Naga = 'tvt'; + case Tunen = 'tvu'; + case Sedoa = 'tvw'; + case Taivoan = 'tvx'; + case Timor_Pidgin = 'tvy'; + case Twana = 'twa'; + case Western_Tawbuid = 'twb'; + case Teshenawa = 'twc'; + case Twents = 'twd'; + case Tewa_Indonesia = 'twe'; + case Northern_Tiwa = 'twf'; + case Tereweng = 'twg'; + case Tai_Don = 'twh'; + case Twi = 'twi'; + case Tawara = 'twl'; + case Tawang_Monpa = 'twm'; + case Twendi = 'twn'; + case Tswapong = 'two'; + case Ere = 'twp'; + case Tasawaq = 'twq'; + case Southwestern_Tarahumara = 'twr'; + case Turiwara = 'twt'; + case Termanu = 'twu'; + case Tuwari = 'tww'; + case Tewe = 'twx'; + case Tawoyan = 'twy'; + case Tombonuo = 'txa'; + case Tokharian_B = 'txb'; + case Tsetsaut = 'txc'; + case Totoli = 'txe'; + case Tangut = 'txg'; + case Thracian = 'txh'; + case Ikpeng = 'txi'; + case Tarjumo = 'txj'; + case Tomini = 'txm'; + case West_Tarangan = 'txn'; + case Toto = 'txo'; + case Tii = 'txq'; + case Tartessian = 'txr'; + case Tonsea = 'txs'; + case Citak = 'txt'; + case Kayapo = 'txu'; + case Tatana = 'txx'; + case Tanosy_Malagasy = 'txy'; + case Tauya = 'tya'; + case Kyanga = 'tye'; + case O_du = 'tyh'; + case Teke_Tsaayi = 'tyi'; + case Tai_Do = 'tyj'; + case Thu_Lao = 'tyl'; + case Kombai = 'tyn'; + case Thaypan = 'typ'; + case Tai_Daeng = 'tyr'; + case Tay_Sa_Pa = 'tys'; + case Tay_Tac = 'tyt'; + case Kua = 'tyu'; + case Tuvinian = 'tyv'; + case Teke_Tyee = 'tyx'; + case Tiyaa = 'tyy'; + case Tay = 'tyz'; + case Tanzanian_Sign_Language = 'tza'; + case Tzeltal = 'tzh'; + case Tz_utujil = 'tzj'; + case Talossan = 'tzl'; + case Central_Atlas_Tamazight = 'tzm'; + case Tugun = 'tzn'; + case Tzotzil = 'tzo'; + case Tabriak = 'tzx'; + case Uamue = 'uam'; + case Kuan = 'uan'; + case Tairuma = 'uar'; + case Ubang = 'uba'; + case Ubi = 'ubi'; + case Buhi_non_Bikol = 'ubl'; + case Ubir = 'ubr'; + case Umbu_Ungu = 'ubu'; + case Ubykh = 'uby'; + case Uda = 'uda'; + case Udihe = 'ude'; + case Muduga = 'udg'; + case Udi = 'udi'; + case Ujir = 'udj'; + case Wuzlam = 'udl'; + case Udmurt = 'udm'; + case Uduk = 'udu'; + case Kioko = 'ues'; + case Ufim = 'ufi'; + case Ugaritic = 'uga'; + case Kuku_Ugbanh = 'ugb'; + case Ughele = 'uge'; + case Kubachi = 'ugh'; + case Ugandan_Sign_Language = 'ugn'; + case Ugong = 'ugo'; + case Uruguayan_Sign_Language = 'ugy'; + case Uhami = 'uha'; + case Damal = 'uhn'; + case Uighur = 'uig'; + case Uisai = 'uis'; + case Iyive = 'uiv'; + case Tanjijili = 'uji'; + case Kaburi = 'uka'; + case Ukuriguma = 'ukg'; + case Ukhwejo = 'ukh'; + case Kui_India = 'uki'; + case Muak_Sa_aak = 'ukk'; + case Ukrainian_Sign_Language = 'ukl'; + case Ukpe_Bayobiri = 'ukp'; + case Ukwa = 'ukq'; + case Ukrainian = 'ukr'; + case Urubu_Kaapor_Sign_Language = 'uks'; + case Ukue = 'uku'; + case Kuku = 'ukv'; + case Ukwuani_Aboh_Ndoni = 'ukw'; + case Kuuk_Yak = 'uky'; + case Fungwa = 'ula'; + case Ulukwumi = 'ulb'; + case Ulch = 'ulc'; + case Lule = 'ule'; + case Usku = 'ulf'; + case Ulithian = 'uli'; + case Meriam_Mir = 'ulk'; + case Ullatan = 'ull'; + case Ulumanda = 'ulm'; + case Unserdeutsch = 'uln'; + case Uma_Lung = 'ulu'; + case Ulwa = 'ulw'; + case Buli = 'uly'; + case Umatilla = 'uma'; + case Umbundu = 'umb'; + case Marrucinian = 'umc'; + case Umbindhamu = 'umd'; + case Morrobalama = 'umg'; + case Ukit = 'umi'; + case Umon = 'umm'; + case Makyan_Naga = 'umn'; + case Umotina = 'umo'; + case Umpila = 'ump'; + case Umbugarla = 'umr'; + case Pendau = 'ums'; + case Munsee = 'umu'; + case North_Watut = 'una'; + case Undetermined = 'und'; + case Uneme = 'une'; + case Ngarinyin = 'ung'; + case Uni = 'uni'; + case Enawene_Nawe = 'unk'; + case Unami = 'unm'; + case Kurnai = 'unn'; + case Mundari = 'unr'; + case Unubahe = 'unu'; + case Munda = 'unx'; + case Unde_Kaili = 'unz'; + case Kulon = 'uon'; + case Umeda = 'upi'; + case Uripiv_Wala_Rano_Atchin = 'upv'; + case Urarina = 'ura'; + case Urubu_Kaapor = 'urb'; + case Urningangg = 'urc'; + case Urdu = 'urd'; + case Uru = 'ure'; + case Uradhi = 'urf'; + case Urigina = 'urg'; + case Urhobo = 'urh'; + case Urim = 'uri'; + case Urak_Lawoi = 'urk'; + case Urali = 'url'; + case Urapmin = 'urm'; + case Uruangnirin = 'urn'; + case Ura_Papua_New_Guinea = 'uro'; + case Uru_Pa_In = 'urp'; + case Lehalurup = 'urr'; + case Urat = 'urt'; + case Urumi = 'uru'; + case Uruava = 'urv'; + case Sop = 'urw'; + case Urimo = 'urx'; + case Orya = 'ury'; + case Uru_Eu_Wau_Wau = 'urz'; + case Usarufa = 'usa'; + case Ushojo = 'ush'; + case Usui = 'usi'; + case Usaghade = 'usk'; + case Uspanteco = 'usp'; + case us_Saare = 'uss'; + case Uya = 'usu'; + case Otank = 'uta'; + case Ute_Southern_Paiute = 'ute'; + case ut_Hun = 'uth'; + case Amba_Solomon_Islands = 'utp'; + case Etulo = 'utr'; + case Utu = 'utu'; + case Urum = 'uum'; + case Ura_Vanuatu = 'uur'; + case U = 'uuu'; + case West_Uvean = 'uve'; + case Uri = 'uvh'; + case Lote = 'uvl'; + case Kuku_Uwanh = 'uwa'; + case Doko_Uyanga = 'uya'; + case Uzbek = 'uzb'; + case Northern_Uzbek = 'uzn'; + case Southern_Uzbek = 'uzs'; + case Vaagri_Booli = 'vaa'; + case Vale = 'vae'; + case Vafsi = 'vaf'; + case Vagla = 'vag'; + case Varhadi_Nagpuri = 'vah'; + case Vai = 'vai'; + case Sekele = 'vaj'; + case Vehes = 'val'; + case Vanimo = 'vam'; + case Valman = 'van'; + case Vao = 'vao'; + case Vaiphei = 'vap'; + case Huarijio = 'var'; + case Vasavi = 'vas'; + case Vanuma = 'vau'; + case Varli = 'vav'; + case Wayu = 'vay'; + case Southeast_Babar = 'vbb'; + case Southwestern_Bontok = 'vbk'; + case Venetian = 'vec'; + case Veddah = 'ved'; + case Veluws = 'vel'; + case Vemgo_Mabas = 'vem'; + case Venda = 'ven'; + case Ventureno = 'veo'; + case Veps = 'vep'; + case Mom_Jango = 'ver'; + case Vaghri = 'vgr'; + case Vlaamse_Gebarentaal = 'vgt'; + case Virgin_Islands_Creole_English = 'vic'; + case Vidunda = 'vid'; + case Vietnamese = 'vie'; + case Vili = 'vif'; + case Viemo = 'vig'; + case Vilela = 'vil'; + case Vinza = 'vin'; + case Vishavan = 'vis'; + case Viti = 'vit'; + case Iduna = 'viv'; + case Bajjika = 'vjk'; + case Kariyarra = 'vka'; + case Kujarge = 'vkj'; + case Kaur = 'vkk'; + case Kulisusu = 'vkl'; + case Kamakan = 'vkm'; + case Koro_Nulu = 'vkn'; + case Kodeoha = 'vko'; + case Korlai_Creole_Portuguese = 'vkp'; + case Tenggarong_Kutai_Malay = 'vkt'; + case Kurrama = 'vku'; + case Koro_Zuba = 'vkz'; + case Valpei = 'vlp'; + case Vlaams = 'vls'; + case Martuyhunira = 'vma'; + case Barbaram = 'vmb'; + case Juxtlahuaca_Mixtec = 'vmc'; + case Mudu_Koraga = 'vmd'; + case East_Masela = 'vme'; + case Mainfrankisch = 'vmf'; + case Lungalunga = 'vmg'; + case Maraghei = 'vmh'; + case Miwa = 'vmi'; + case Ixtayutla_Mixtec = 'vmj'; + case Makhuwa_Shirima = 'vmk'; + case Malgana = 'vml'; + case Mitlatongo_Mixtec = 'vmm'; + case Soyaltepec_Mazatec = 'vmp'; + case Soyaltepec_Mixtec = 'vmq'; + case Marenje = 'vmr'; + case Moksela = 'vms'; + case Muluridyi = 'vmu'; + case Valley_Maidu = 'vmv'; + case Makhuwa = 'vmw'; + case Tamazola_Mixtec = 'vmx'; + case Ayautla_Mazatec = 'vmy'; + case Mazatlan_Mazatec = 'vmz'; + case Vano = 'vnk'; + case Vinmavis = 'vnm'; + case Vunapu = 'vnp'; + case Volapuk = 'vol'; + case Voro = 'vor'; + case Votic = 'vot'; + case Vera_a = 'vra'; + case Voro_2 = 'vro'; + case Varisi = 'vrs'; + case Burmbar = 'vrt'; + case Moldova_Sign_Language = 'vsi'; + case Venezuelan_Sign_Language = 'vsl'; + case Vedic_Sanskrit = 'vsn'; + case Valencian_Sign_Language = 'vsv'; + case Vitou = 'vto'; + case Vumbu = 'vum'; + case Vunjo = 'vun'; + case Vute = 'vut'; + case Awa_China = 'vwa'; + case Walla_Walla = 'waa'; + case Wab = 'wab'; + case Wasco_Wishram = 'wac'; + case Wamesa = 'wad'; + case Walser = 'wae'; + case Wakona = 'waf'; + case Wa_ema = 'wag'; + case Watubela = 'wah'; + case Wares = 'wai'; + case Waffa = 'waj'; + case Wolaytta = 'wal'; + case Wampanoag = 'wam'; + case Wan = 'wan'; + case Wappo = 'wao'; + case Wapishana = 'wap'; + case Wagiman = 'waq'; + case Waray_Philippines = 'war'; + case Washo = 'was'; + case Kaninuwa = 'wat'; + case Waura = 'wau'; + case Waka = 'wav'; + case Waiwai = 'waw'; + case Watam = 'wax'; + case Wayana = 'way'; + case Wampur = 'waz'; + case Warao = 'wba'; + case Wabo = 'wbb'; + case Waritai = 'wbe'; + case Wara_2 = 'wbf'; + case Wanda = 'wbh'; + case Vwanji = 'wbi'; + case Alagwa = 'wbj'; + case Waigali = 'wbk'; + case Wakhi = 'wbl'; + case Wa = 'wbm'; + case Warlpiri = 'wbp'; + case Waddar = 'wbq'; + case Wagdi = 'wbr'; + case West_Bengal_Sign_Language = 'wbs'; + case Warnman = 'wbt'; + case Wajarri = 'wbv'; + case Woi = 'wbw'; + case Yanomami = 'wca'; + case Waci_Gbe = 'wci'; + case Wandji = 'wdd'; + case Wadaginam = 'wdg'; + case Wadjiginy = 'wdj'; + case Wadikali = 'wdk'; + case Wendat = 'wdt'; + case Wadjigu = 'wdu'; + case Wadjabangayi = 'wdy'; + case Wewaw = 'wea'; + case We_Western = 'wec'; + case Wedau = 'wed'; + case Wergaia = 'weg'; + case Weh = 'weh'; + case Kiunum = 'wei'; + case Weme_Gbe = 'wem'; + case Wemale = 'weo'; + case Westphalien = 'wep'; + case Weri = 'wer'; + case Cameroon_Pidgin = 'wes'; + case Perai = 'wet'; + case Rawngtu_Chin = 'weu'; + case Wejewa = 'wew'; + case Yafi = 'wfg'; + case Wagaya = 'wga'; + case Wagawaga = 'wgb'; + case Wangkangurru = 'wgg'; + case Wahgi = 'wgi'; + case Waigeo = 'wgo'; + case Wirangu = 'wgu'; + case Warrgamay = 'wgy'; + case Sou_Upaa = 'wha'; + case North_Wahgi = 'whg'; + case Wahau_Kenyah = 'whk'; + case Wahau_Kayan = 'whu'; + case Southern_Toussian = 'wib'; + case Wichita = 'wic'; + case Wik_Epa = 'wie'; + case Wik_Keyangan = 'wif'; + case Wik_Ngathan = 'wig'; + case Wik_Me_anha = 'wih'; + case Minidien = 'wii'; + case Wik_Iiyanh = 'wij'; + case Wikalkan = 'wik'; + case Wilawila = 'wil'; + case Wik_Mungkan = 'wim'; + case Ho_Chunk = 'win'; + case Wirafed = 'wir'; + case Wiru = 'wiu'; + case Vitu = 'wiv'; + case Wiyot = 'wiy'; + case Waja = 'wja'; + case Warji = 'wji'; + case Kw_adza = 'wka'; + case Kumbaran = 'wkb'; + case Wakde = 'wkd'; + case Kalanadi = 'wkl'; + case Keerray_Woorroong = 'wkr'; + case Kunduvadi = 'wku'; + case Wakawaka = 'wkw'; + case Wangkayutyuru = 'wky'; + case Walio = 'wla'; + case Mwali_Comorian = 'wlc'; + case Wolane = 'wle'; + case Kunbarlang = 'wlg'; + case Welaun = 'wlh'; + case Waioli = 'wli'; + case Wailaki = 'wlk'; + case Wali_Sudan = 'wll'; + case Middle_Welsh = 'wlm'; + case Walloon = 'wln'; + case Wolio = 'wlo'; + case Wailapa = 'wlr'; + case Wallisian = 'wls'; + case Wuliwuli = 'wlu'; + case Wichi_Lhamtes_Vejoz = 'wlv'; + case Walak = 'wlw'; + case Wali_Ghana = 'wlx'; + case Waling = 'wly'; + case Mawa_Nigeria = 'wma'; + case Wambaya = 'wmb'; + case Wamas = 'wmc'; + case Mamainde = 'wmd'; + case Wambule = 'wme'; + case Western_Minyag = 'wmg'; + case Waima_a = 'wmh'; + case Wamin = 'wmi'; + case Maiwa_Indonesia = 'wmm'; + case Waamwang = 'wmn'; + case Wom_Papua_New_Guinea = 'wmo'; + case Wambon = 'wms'; + case Walmajarri = 'wmt'; + case Mwani = 'wmw'; + case Womo = 'wmx'; + case Mokati = 'wnb'; + case Wantoat = 'wnc'; + case Wandarang = 'wnd'; + case Waneci = 'wne'; + case Wanggom = 'wng'; + case Ndzwani_Comorian = 'wni'; + case Wanukaka = 'wnk'; + case Wanggamala = 'wnm'; + case Wunumara = 'wnn'; + case Wano = 'wno'; + case Wanap = 'wnp'; + case Usan = 'wnu'; + case Wintu = 'wnw'; + case Wanyi = 'wny'; + case Kuwema = 'woa'; + case We_Northern = 'wob'; + case Wogeo = 'woc'; + case Wolani = 'wod'; + case Woleaian = 'woe'; + case Gambian_Wolof = 'wof'; + case Wogamusin = 'wog'; + case Kamang = 'woi'; + case Longto = 'wok'; + case Wolof = 'wol'; + case Wom_Nigeria = 'wom'; + case Wongo = 'won'; + case Manombai = 'woo'; + case Woria = 'wor'; + case Hanga_Hundi = 'wos'; + case Wawonii = 'wow'; + case Weyto = 'woy'; + case Maco = 'wpc'; + case Waluwarra = 'wrb'; + case Warungu = 'wrg'; + case Wiradjuri = 'wrh'; + case Wariyangga = 'wri'; + case Garrwa = 'wrk'; + case Warlmanpa = 'wrl'; + case Warumungu = 'wrm'; + case Warnang = 'wrn'; + case Worrorra = 'wro'; + case Waropen = 'wrp'; + case Wardaman = 'wrr'; + case Waris = 'wrs'; + case Waru = 'wru'; + case Waruna = 'wrv'; + case Gugu_Warra = 'wrw'; + case Wae_Rana = 'wrx'; + case Merwari = 'wry'; + case Waray_Australia = 'wrz'; + case Warembori = 'wsa'; + case Adilabad_Gondi = 'wsg'; + case Wusi = 'wsi'; + case Waskia = 'wsk'; + case Owenia = 'wsr'; + case Wasa = 'wss'; + case Wasu = 'wsu'; + case Wotapuri_Katarqalai = 'wsv'; + case Matambwe = 'wtb'; + case Watiwa = 'wtf'; + case Wathawurrung = 'wth'; + case Berta = 'wti'; + case Watakataui = 'wtk'; + case Mewati = 'wtm'; + case Wotu = 'wtw'; + case Wikngenchera = 'wua'; + case Wunambal = 'wub'; + case Wudu = 'wud'; + case Wutunhua = 'wuh'; + case Silimo = 'wul'; + case Wumbvu = 'wum'; + case Bungu = 'wun'; + case Wurrugu = 'wur'; + case Wutung = 'wut'; + case Wu_Chinese = 'wuu'; + case Wuvulu_Aua = 'wuv'; + case Wulna = 'wux'; + case Wauyai = 'wuy'; + case Waama = 'wwa'; + case Wakabunga = 'wwb'; + case Wetamut = 'wwo'; + case Warrwa = 'wwr'; + case Wawa = 'www'; + case Waxianghua = 'wxa'; + case Wardandi = 'wxw'; + case Wangaaybuwan_Ngiyambaa = 'wyb'; + case Woiwurrung = 'wyi'; + case Wymysorys = 'wym'; + case Wyandot = 'wyn'; + case Wayoro = 'wyr'; + case Western_Fijian = 'wyy'; + case Andalusian_Arabic = 'xaa'; + case Sambe = 'xab'; + case Kachari = 'xac'; + case Adai = 'xad'; + case Aequian = 'xae'; + case Aghwan = 'xag'; + case Kaimbe = 'xai'; + case Ararandewara = 'xaj'; + case Maku = 'xak'; + case Kalmyk = 'xal'; + case Xam = 'xam'; + case Xamtanga = 'xan'; + case Khao = 'xao'; + case Apalachee = 'xap'; + case Aquitanian = 'xaq'; + case Karami = 'xar'; + case Kamas = 'xas'; + case Katawixi = 'xat'; + case Kauwera = 'xau'; + case Xavante = 'xav'; + case Kawaiisu = 'xaw'; + case Kayan_Mahakam = 'xay'; + case Lower_Burdekin = 'xbb'; + case Bactrian = 'xbc'; + case Bindal = 'xbd'; + case Bigambal = 'xbe'; + case Bunganditj = 'xbg'; + case Kombio = 'xbi'; + case Birrpayi = 'xbj'; + case Middle_Breton = 'xbm'; + case Kenaboi = 'xbn'; + case Bolgarian = 'xbo'; + case Bibbulman = 'xbp'; + case Kambera = 'xbr'; + case Kambiwa = 'xbw'; + case Batjala = 'xby'; + case Cumbric = 'xcb'; + case Camunic = 'xcc'; + case Celtiberian = 'xce'; + case Cisalpine_Gaulish = 'xcg'; + case Chemakum = 'xch'; + case Classical_Armenian = 'xcl'; + case Comecrudo = 'xcm'; + case Cotoname = 'xcn'; + case Chorasmian = 'xco'; + case Carian = 'xcr'; + case Classical_Tibetan = 'xct'; + case Curonian = 'xcu'; + case Chuvantsy = 'xcv'; + case Coahuilteco = 'xcw'; + case Cayuse = 'xcy'; + case Darkinyung = 'xda'; + case Dacian = 'xdc'; + case Dharuk = 'xdk'; + case Edomite = 'xdm'; + case Kwandu = 'xdo'; + case Kaitag = 'xdq'; + case Malayic_Dayak = 'xdy'; + case Eblan = 'xeb'; + case Hdi = 'xed'; + case Xegwi = 'xeg'; + case Kelo = 'xel'; + case Kembayan = 'xem'; + case Epi_Olmec = 'xep'; + case Xerente = 'xer'; + case Kesawai = 'xes'; + case Xeta = 'xet'; + case Keoru_Ahia = 'xeu'; + case Faliscan = 'xfa'; + case Galatian = 'xga'; + case Gbin = 'xgb'; + case Gudang = 'xgd'; + case Gabrielino_Fernandeno = 'xgf'; + case Goreng = 'xgg'; + case Garingbal = 'xgi'; + case Galindan = 'xgl'; + case Dharumbal = 'xgm'; + case Garza = 'xgr'; + case Unggumi = 'xgu'; + case Guwa = 'xgw'; + case Harami = 'xha'; + case Hunnic = 'xhc'; + case Hadrami = 'xhd'; + case Khetrani = 'xhe'; + case Middle_Khmer_1400_to_1850_CE = 'xhm'; + case Xhosa = 'xho'; + case Hernican = 'xhr'; + case Hattic = 'xht'; + case Hurrian = 'xhu'; + case Khua = 'xhv'; + case Iberian = 'xib'; + case Xiri = 'xii'; + case Illyrian = 'xil'; + case Xinca = 'xin'; + case Xiriana = 'xir'; + case Kisan = 'xis'; + case Indus_Valley_Language = 'xiv'; + case Xipaya = 'xiy'; + case Minjungbal = 'xjb'; + case Jaitmatang = 'xjt'; + case Kalkoti = 'xka'; + case Northern_Nago = 'xkb'; + case Kho_ini = 'xkc'; + case Mendalam_Kayan = 'xkd'; + case Kereho = 'xke'; + case Khengkha = 'xkf'; + case Kagoro = 'xkg'; + case Kenyan_Sign_Language = 'xki'; + case Kajali = 'xkj'; + case Kachok = 'xkk'; + case Mainstream_Kenyah = 'xkl'; + case Kayan_River_Kayan = 'xkn'; + case Kiorr = 'xko'; + case Kabatei = 'xkp'; + case Koroni = 'xkq'; + case Xakriaba = 'xkr'; + case Kumbewaha = 'xks'; + case Kantosi = 'xkt'; + case Kaamba = 'xku'; + case Kgalagadi = 'xkv'; + case Kembra = 'xkw'; + case Karore = 'xkx'; + case Uma_Lasan = 'xky'; + case Kurtokha = 'xkz'; + case Kamula = 'xla'; + case Loup_B = 'xlb'; + case Lycian = 'xlc'; + case Lydian = 'xld'; + case Lemnian = 'xle'; + case Ligurian_Ancient = 'xlg'; + case Liburnian = 'xli'; + case Alanic = 'xln'; + case Loup_A = 'xlo'; + case Lepontic = 'xlp'; + case Lusitanian = 'xls'; + case Cuneiform_Luwian = 'xlu'; + case Elymian = 'xly'; + case Mushungulu = 'xma'; + case Mbonga = 'xmb'; + case Makhuwa_Marrevone = 'xmc'; + case Mbudum = 'xmd'; + case Median = 'xme'; + case Mingrelian = 'xmf'; + case Mengaka = 'xmg'; + case Kugu_Muminh = 'xmh'; + case Majera = 'xmj'; + case Ancient_Macedonian = 'xmk'; + case Malaysian_Sign_Language = 'xml'; + case Manado_Malay = 'xmm'; + case Manichaean_Middle_Persian = 'xmn'; + case Morerebi = 'xmo'; + case Kuku_Mu_inh = 'xmp'; + case Kuku_Mangk = 'xmq'; + case Meroitic = 'xmr'; + case Moroccan_Sign_Language = 'xms'; + case Matbat = 'xmt'; + case Kamu = 'xmu'; + case Antankarana_Malagasy = 'xmv'; + case Tsimihety_Malagasy = 'xmw'; + case Salawati = 'xmx'; + case Mayaguduna = 'xmy'; + case Mori_Bawah = 'xmz'; + case Ancient_North_Arabian = 'xna'; + case Kanakanabu = 'xnb'; + case Middle_Mongolian = 'xng'; + case Kuanhua = 'xnh'; + case Ngarigu = 'xni'; + case Ngoni_Tanzania = 'xnj'; + case Nganakarti = 'xnk'; + case Ngumbarl = 'xnm'; + case Northern_Kankanay = 'xnn'; + case Anglo_Norman = 'xno'; + case Ngoni_Mozambique = 'xnq'; + case Kangri = 'xnr'; + case Kanashi = 'xns'; + case Narragansett = 'xnt'; + case Nukunul = 'xnu'; + case Nyiyaparli = 'xny'; + case Kenzi = 'xnz'; + case O_chi_chi = 'xoc'; + case Kokoda = 'xod'; + case Soga = 'xog'; + case Kominimung = 'xoi'; + case Xokleng = 'xok'; + case Komo_Sudan = 'xom'; + case Konkomba = 'xon'; + case Xukuru = 'xoo'; + case Kopar = 'xop'; + case Korubo = 'xor'; + case Kowaki = 'xow'; + case Pirriya = 'xpa'; + case Northeastern_Tasmanian = 'xpb'; + case Pecheneg = 'xpc'; + case Oyster_Bay_Tasmanian = 'xpd'; + case Liberia_Kpelle = 'xpe'; + case Southeast_Tasmanian = 'xpf'; + case Phrygian = 'xpg'; + case North_Midlands_Tasmanian = 'xph'; + case Pictish = 'xpi'; + case Mpalitjanh = 'xpj'; + case Kulina_Pano = 'xpk'; + case Port_Sorell_Tasmanian = 'xpl'; + case Pumpokol = 'xpm'; + case Kapinawa = 'xpn'; + case Pochutec = 'xpo'; + case Puyo_Paekche = 'xpp'; + case Mohegan_Pequot = 'xpq'; + case Parthian = 'xpr'; + case Pisidian = 'xps'; + case Punthamara = 'xpt'; + case Punic = 'xpu'; + case Northern_Tasmanian = 'xpv'; + case Northwestern_Tasmanian = 'xpw'; + case Southwestern_Tasmanian = 'xpx'; + case Puyo = 'xpy'; + case Bruny_Island_Tasmanian = 'xpz'; + case Karakhanid = 'xqa'; + case Qatabanian = 'xqt'; + case Kraho = 'xra'; + case Eastern_Karaboro = 'xrb'; + case Gundungurra = 'xrd'; + case Kreye = 'xre'; + case Minang = 'xrg'; + case Krikati_Timbira = 'xri'; + case Armazic = 'xrm'; + case Arin = 'xrn'; + case Raetic = 'xrr'; + case Aranama_Tamique = 'xrt'; + case Marriammu = 'xru'; + case Karawa = 'xrw'; + case Sabaean = 'xsa'; + case Sambal = 'xsb'; + case Scythian = 'xsc'; + case Sidetic = 'xsd'; + case Sempan = 'xse'; + case Shamang = 'xsh'; + case Sio = 'xsi'; + case Subi = 'xsj'; + case South_Slavey = 'xsl'; + case Kasem = 'xsm'; + case Sanga_Nigeria = 'xsn'; + case Solano = 'xso'; + case Silopi = 'xsp'; + case Makhuwa_Saka = 'xsq'; + case Sherpa = 'xsr'; + case Sanuma = 'xsu'; + case Sudovian = 'xsv'; + case Saisiyat = 'xsy'; + case Alcozauca_Mixtec = 'xta'; + case Chazumba_Mixtec = 'xtb'; + case Katcha_Kadugli_Miri = 'xtc'; + case Diuxi_Tilantongo_Mixtec = 'xtd'; + case Ketengban = 'xte'; + case Transalpine_Gaulish = 'xtg'; + case Yitha_Yitha = 'xth'; + case Sinicahua_Mixtec = 'xti'; + case San_Juan_Teita_Mixtec = 'xtj'; + case Tijaltepec_Mixtec = 'xtl'; + case Magdalena_Penasco_Mixtec = 'xtm'; + case Northern_Tlaxiaco_Mixtec = 'xtn'; + case Tokharian_A = 'xto'; + case San_Miguel_Piedras_Mixtec = 'xtp'; + case Tumshuqese = 'xtq'; + case Early_Tripuri = 'xtr'; + case Sindihui_Mixtec = 'xts'; + case Tacahua_Mixtec = 'xtt'; + case Cuyamecalco_Mixtec = 'xtu'; + case Thawa = 'xtv'; + case Tawande = 'xtw'; + case Yoloxochitl_Mixtec = 'xty'; + case Alu_Kurumba = 'xua'; + case Betta_Kurumba = 'xub'; + case Umiida = 'xud'; + case Kunigami = 'xug'; + case Jennu_Kurumba = 'xuj'; + case Ngunawal = 'xul'; + case Umbrian = 'xum'; + case Unggaranggu = 'xun'; + case Kuo = 'xuo'; + case Upper_Umpqua = 'xup'; + case Urartian = 'xur'; + case Kuthant = 'xut'; + case Kxoe = 'xuu'; + case Venetic = 'xve'; + case Kamviri = 'xvi'; + case Vandalic = 'xvn'; + case Volscian = 'xvo'; + case Vestinian = 'xvs'; + case Kwaza = 'xwa'; + case Woccon = 'xwc'; + case Wadi_Wadi = 'xwd'; + case Xwela_Gbe = 'xwe'; + case Kwegu = 'xwg'; + case Wajuk = 'xwj'; + case Wangkumara = 'xwk'; + case Western_Xwla_Gbe = 'xwl'; + case Written_Oirat = 'xwo'; + case Kwerba_Mamberamo = 'xwr'; + case Wotjobaluk = 'xwt'; + case Wemba_Wemba = 'xww'; + case Boro_Ghana = 'xxb'; + case Ke_o = 'xxk'; + case Minkin = 'xxm'; + case Koropo = 'xxr'; + case Tambora = 'xxt'; + case Yaygir = 'xya'; + case Yandjibara = 'xyb'; + case Mayi_Yapi = 'xyj'; + case Mayi_Kulan = 'xyk'; + case Yalakalore = 'xyl'; + case Mayi_Thakurti = 'xyt'; + case Yorta_Yorta = 'xyy'; + case Zhang_Zhung = 'xzh'; + case Zemgalian = 'xzm'; + case Ancient_Zapotec = 'xzp'; + case Yaminahua = 'yaa'; + case Yuhup = 'yab'; + case Pass_Valley_Yali = 'yac'; + case Yagua = 'yad'; + case Pume = 'yae'; + case Yaka_Democratic_Republic_of_Congo = 'yaf'; + case Yamana = 'yag'; + case Yazgulyam = 'yah'; + case Yagnobi = 'yai'; + case Banda_Yangere = 'yaj'; + case Yakama = 'yak'; + case Yalunka = 'yal'; + case Yamba = 'yam'; + case Mayangna = 'yan'; + case Yao = 'yao'; + case Yapese = 'yap'; + case Yaqui = 'yaq'; + case Yabarana = 'yar'; + case Nugunu_Cameroon = 'yas'; + case Yambeta = 'yat'; + case Yuwana = 'yau'; + case Yangben = 'yav'; + case Yawalapiti = 'yaw'; + case Yauma = 'yax'; + case Agwagwune = 'yay'; + case Lokaa = 'yaz'; + case Yala = 'yba'; + case Yemba = 'ybb'; + case West_Yugur = 'ybe'; + case Yakha = 'ybh'; + case Yamphu = 'ybi'; + case Hasha = 'ybj'; + case Bokha = 'ybk'; + case Yukuben = 'ybl'; + case Yaben = 'ybm'; + case Yabaana = 'ybn'; + case Yabong = 'ybo'; + case Yawiyo = 'ybx'; + case Yaweyuha = 'yby'; + case Chesu = 'ych'; + case Lolopo = 'ycl'; + case Yucuna = 'ycn'; + case Chepya = 'ycp'; + case Yilan_Creole = 'ycr'; + case Yanda = 'yda'; + case Eastern_Yiddish = 'ydd'; + case Yangum_Dey = 'yde'; + case Yidgha = 'ydg'; + case Yoidik = 'ydk'; + case Ravula = 'yea'; + case Yeniche = 'yec'; + case Yimas = 'yee'; + case Yeni = 'yei'; + case Yevanic = 'yej'; + case Yela = 'yel'; + case Tarok = 'yer'; + case Nyankpa = 'yes'; + case Yetfa = 'yet'; + case Yerukula = 'yeu'; + case Yapunda = 'yev'; + case Yeyi = 'yey'; + case Malyangapa = 'yga'; + case Yiningayi = 'ygi'; + case Yangum_Gel = 'ygl'; + case Yagomi = 'ygm'; + case Gepo = 'ygp'; + case Yagaria = 'ygr'; + case Yol_u_Sign_Language = 'ygs'; + case Yugul = 'ygu'; + case Yagwoia = 'ygw'; + case Baha_Buyang = 'yha'; + case Judeo_Iraqi_Arabic = 'yhd'; + case Hlepho_Phowa = 'yhl'; + case Yan_nhangu_Sign_Language = 'yhs'; + case Yinggarda = 'yia'; + case Yiddish = 'yid'; + case Ache_2 = 'yif'; + case Wusa_Nasu = 'yig'; + case Western_Yiddish = 'yih'; + case Yidiny = 'yii'; + case Yindjibarndi = 'yij'; + case Dongshanba_Lalo = 'yik'; + case Yindjilandji = 'yil'; + case Yimchungru_Naga = 'yim'; + case Riang_Lai = 'yin'; + case Pholo = 'yip'; + case Miqie = 'yiq'; + case North_Awyu = 'yir'; + case Yis = 'yis'; + case Eastern_Lalu = 'yit'; + case Awu = 'yiu'; + case Northern_Nisu = 'yiv'; + case Axi_Yi = 'yix'; + case Azhe = 'yiz'; + case Yakan = 'yka'; + case Northern_Yukaghir = 'ykg'; + case Khamnigan_Mongol = 'ykh'; + case Yoke = 'yki'; + case Yakaikeke = 'ykk'; + case Khlula = 'ykl'; + case Kap = 'ykm'; + case Kua_nsi = 'ykn'; + case Yasa = 'yko'; + case Yekora = 'ykr'; + case Kathu = 'ykt'; + case Kuamasi = 'yku'; + case Yakoma = 'yky'; + case Yaul = 'yla'; + case Yaleba = 'ylb'; + case Yele = 'yle'; + case Yelogu = 'ylg'; + case Angguruk_Yali = 'yli'; + case Yil = 'yll'; + case Limi = 'ylm'; + case Langnian_Buyang = 'yln'; + case Naluo_Yi = 'ylo'; + case Yalarnnga = 'ylr'; + case Aribwaung = 'ylu'; + case Nyalayu = 'yly'; + case Yambes = 'ymb'; + case Southern_Muji = 'ymc'; + case Muda = 'ymd'; + case Yameo = 'yme'; + case Yamongeri = 'ymg'; + case Mili = 'ymh'; + case Moji = 'ymi'; + case Makwe = 'ymk'; + case Iamalele = 'yml'; + case Maay = 'ymm'; + case Yamna = 'ymn'; + case Yangum_Mon = 'ymo'; + case Yamap = 'ymp'; + case Qila_Muji = 'ymq'; + case Malasar = 'ymr'; + case Mysian = 'yms'; + case Northern_Muji = 'ymx'; + case Muzi = 'ymz'; + case Aluo = 'yna'; + case Yandruwandha = 'ynd'; + case Lang_e = 'yne'; + case Yango = 'yng'; + case Naukan_Yupik = 'ynk'; + case Yangulam = 'ynl'; + case Yana = 'ynn'; + case Yong = 'yno'; + case Yendang = 'ynq'; + case Yansi = 'yns'; + case Yahuna = 'ynu'; + case Yoba = 'yob'; + case Yogad = 'yog'; + case Yonaguni = 'yoi'; + case Yokuts = 'yok'; + case Yola = 'yol'; + case Yombe = 'yom'; + case Yongkom = 'yon'; + case Yoruba = 'yor'; + case Yotti = 'yot'; + case Yoron = 'yox'; + case Yoy = 'yoy'; + case Phala = 'ypa'; + case Labo_Phowa = 'ypb'; + case Phola = 'ypg'; + case Phupha = 'yph'; + case Phuma = 'ypm'; + case Ani_Phowa = 'ypn'; + case Alo_Phola = 'ypo'; + case Phupa = 'ypp'; + case Phuza = 'ypz'; + case Yerakai = 'yra'; + case Yareba = 'yrb'; + case Yaoure = 'yre'; + case Nenets = 'yrk'; + case Nhengatu = 'yrl'; + case Yirrk_Mel = 'yrm'; + case Yerong = 'yrn'; + case Yaroame = 'yro'; + case Yarsun = 'yrs'; + case Yarawata = 'yrw'; + case Yarluyandi = 'yry'; + case Yassic = 'ysc'; + case Samatao = 'ysd'; + case Sonaga = 'ysg'; + case Yugoslavian_Sign_Language = 'ysl'; + case Myanmar_Sign_Language = 'ysm'; + case Sani = 'ysn'; + case Nisi_China = 'yso'; + case Southern_Lolopo = 'ysp'; + case Sirenik_Yupik = 'ysr'; + case Yessan_Mayo = 'yss'; + case Sanie = 'ysy'; + case Talu = 'yta'; + case Tanglang = 'ytl'; + case Thopho = 'ytp'; + case Yout_Wam = 'ytw'; + case Yatay = 'yty'; + case Yucateco = 'yua'; + case Yugambal = 'yub'; + case Yuchi = 'yuc'; + case Judeo_Tripolitanian_Arabic = 'yud'; + case Yue_Chinese = 'yue'; + case Havasupai_Walapai_Yavapai = 'yuf'; + case Yug = 'yug'; + case Yuruti = 'yui'; + case Karkar_Yuri = 'yuj'; + case Yuki = 'yuk'; + case Yulu = 'yul'; + case Quechan = 'yum'; + case Bena_Nigeria = 'yun'; + case Yukpa = 'yup'; + case Yuqui = 'yuq'; + case Yurok = 'yur'; + case Yopno = 'yut'; + case Yau_Morobe_Province = 'yuw'; + case Southern_Yukaghir = 'yux'; + case East_Yugur = 'yuy'; + case Yuracare = 'yuz'; + case Yawa = 'yva'; + case Yavitero = 'yvt'; + case Kalou = 'ywa'; + case Yinhawangka = 'ywg'; + case Western_Lalu = 'ywl'; + case Yawanawa = 'ywn'; + case Wuding_Luquan_Yi = 'ywq'; + case Yawuru = 'ywr'; + case Xishanba_Lalo = 'ywt'; + case Wumeng_Nasu = 'ywu'; + case Yawarawarga = 'yww'; + case Mayawali = 'yxa'; + case Yagara = 'yxg'; + case Yardliyawarra = 'yxl'; + case Yinwum = 'yxm'; + case Yuyu = 'yxu'; + case Yabula_Yabula = 'yxy'; + case Yir_Yoront = 'yyr'; + case Yau_Sandaun_Province = 'yyu'; + case Ayizi = 'yyz'; + case E_ma_Buyang = 'yzg'; + case Zokhuo = 'yzk'; + case Sierra_de_Juarez_Zapotec = 'zaa'; + case Western_Tlacolula_Valley_Zapotec = 'zab'; + case Ocotlan_Zapotec = 'zac'; + case Cajonos_Zapotec = 'zad'; + case Yareni_Zapotec = 'zae'; + case Ayoquesco_Zapotec = 'zaf'; + case Zaghawa = 'zag'; + case Zangwal = 'zah'; + case Isthmus_Zapotec = 'zai'; + case Zaramo = 'zaj'; + case Zanaki = 'zak'; + case Zauzou = 'zal'; + case Miahuatlan_Zapotec = 'zam'; + case Ozolotepec_Zapotec = 'zao'; + case Zapotec = 'zap'; + case Aloapam_Zapotec = 'zaq'; + case Rincon_Zapotec = 'zar'; + case Santo_Domingo_Albarradas_Zapotec = 'zas'; + case Tabaa_Zapotec = 'zat'; + case Zangskari = 'zau'; + case Yatzachi_Zapotec = 'zav'; + case Mitla_Zapotec = 'zaw'; + case Xadani_Zapotec = 'zax'; + case Zayse_Zergulla = 'zay'; + case Zari = 'zaz'; + case Balaibalan = 'zba'; + case Central_Berawan = 'zbc'; + case East_Berawan = 'zbe'; + case Blissymbols = 'zbl'; + case Batui = 'zbt'; + case Bu_Bauchi_State = 'zbu'; + case West_Berawan = 'zbw'; + case Coatecas_Altas_Zapotec = 'zca'; + case Las_Delicias_Zapotec = 'zcd'; + case Central_Hongshuihe_Zhuang = 'zch'; + case Ngazidja_Comorian = 'zdj'; + case Zeeuws = 'zea'; + case Zenag = 'zeg'; + case Eastern_Hongshuihe_Zhuang = 'zeh'; + case Zeem = 'zem'; + case Zenaga = 'zen'; + case Kinga = 'zga'; + case Guibei_Zhuang = 'zgb'; + case Standard_Moroccan_Tamazight = 'zgh'; + case Minz_Zhuang = 'zgm'; + case Guibian_Zhuang = 'zgn'; + case Magori = 'zgr'; + case Zhuang = 'zha'; + case Zhaba = 'zhb'; + case Dai_Zhuang = 'zhd'; + case Zhire = 'zhi'; + case Nong_Zhuang = 'zhn'; + case Chinese = 'zho'; + case Zhoa = 'zhw'; + case Zia = 'zia'; + case Zimbabwe_Sign_Language = 'zib'; + case Zimakani = 'zik'; + case Zialo = 'zil'; + case Mesme = 'zim'; + case Zinza = 'zin'; + case Zigula = 'ziw'; + case Zizilivakan = 'ziz'; + case Kaimbulawa = 'zka'; + case Kadu = 'zkd'; + case Koguryo = 'zkg'; + case Khorezmian = 'zkh'; + case Karankawa = 'zkk'; + case Kanan = 'zkn'; + case Kott = 'zko'; + case Sao_Paulo_Kaingang = 'zkp'; + case Zakhring = 'zkr'; + case Kitan = 'zkt'; + case Kaurna = 'zku'; + case Krevinian = 'zkv'; + case Khazar = 'zkz'; + case Zula = 'zla'; + case Liujiang_Zhuang = 'zlj'; + case Malay_individual_language = 'zlm'; + case Lianshan_Zhuang = 'zln'; + case Liuqian_Zhuang = 'zlq'; + case Zul = 'zlu'; + case Manda_Australia = 'zma'; + case Zimba = 'zmb'; + case Margany = 'zmc'; + case Maridan = 'zmd'; + case Mangerr = 'zme'; + case Mfinu = 'zmf'; + case Marti_Ke = 'zmg'; + case Makolkol = 'zmh'; + case Negeri_Sembilan_Malay = 'zmi'; + case Maridjabin = 'zmj'; + case Mandandanyi = 'zmk'; + case Matngala = 'zml'; + case Marimanindji = 'zmm'; + case Mbangwe = 'zmn'; + case Molo = 'zmo'; + case Mpuono = 'zmp'; + case Mituku = 'zmq'; + case Maranunggu = 'zmr'; + case Mbesa = 'zms'; + case Maringarr = 'zmt'; + case Muruwari = 'zmu'; + case Mbariman_Gudhinma = 'zmv'; + case Mbo_Democratic_Republic_of_Congo = 'zmw'; + case Bomitaba = 'zmx'; + case Mariyedi = 'zmy'; + case Mbandja = 'zmz'; + case Zan_Gula = 'zna'; + case Zande_individual_language = 'zne'; + case Mang = 'zng'; + case Manangkari = 'znk'; + case Mangas = 'zns'; + case Copainala_Zoque = 'zoc'; + case Chimalapa_Zoque = 'zoh'; + case Zou = 'zom'; + case Asuncion_Mixtepec_Zapotec = 'zoo'; + case Tabasco_Zoque = 'zoq'; + case Rayon_Zoque = 'zor'; + case Francisco_Leon_Zoque = 'zos'; + case Lachiguiri_Zapotec = 'zpa'; + case Yautepec_Zapotec = 'zpb'; + case Choapan_Zapotec = 'zpc'; + case Southeastern_Ixtlan_Zapotec = 'zpd'; + case Petapa_Zapotec = 'zpe'; + case San_Pedro_Quiatoni_Zapotec = 'zpf'; + case Guevea_De_Humboldt_Zapotec = 'zpg'; + case Totomachapan_Zapotec = 'zph'; + case Santa_Maria_Quiegolani_Zapotec = 'zpi'; + case Quiavicuzas_Zapotec = 'zpj'; + case Tlacolulita_Zapotec = 'zpk'; + case Lachixio_Zapotec = 'zpl'; + case Mixtepec_Zapotec = 'zpm'; + case Santa_Ines_Yatzechi_Zapotec = 'zpn'; + case Amatlan_Zapotec = 'zpo'; + case El_Alto_Zapotec = 'zpp'; + case Zoogocho_Zapotec = 'zpq'; + case Santiago_Xanica_Zapotec = 'zpr'; + case Coatlan_Zapotec = 'zps'; + case San_Vicente_Coatlan_Zapotec = 'zpt'; + case Yalalag_Zapotec = 'zpu'; + case Chichicapan_Zapotec = 'zpv'; + case Zaniza_Zapotec = 'zpw'; + case San_Baltazar_Loxicha_Zapotec = 'zpx'; + case Mazaltepec_Zapotec = 'zpy'; + case Texmelucan_Zapotec = 'zpz'; + case Qiubei_Zhuang = 'zqe'; + case Kara_Korea = 'zra'; + case Mirgan = 'zrg'; + case Zerenkel = 'zrn'; + case Zaparo = 'zro'; + case Zarphatic = 'zrp'; + case Mairasi = 'zrs'; + case Sarasira = 'zsa'; + case Kaskean = 'zsk'; + case Zambian_Sign_Language = 'zsl'; + case Standard_Malay = 'zsm'; + case Southern_Rincon_Zapotec = 'zsr'; + case Sukurum = 'zsu'; + case Elotepec_Zapotec = 'zte'; + case Xanaguia_Zapotec = 'ztg'; + case Lapaguia_Guivini_Zapotec = 'ztl'; + case San_Agustin_Mixtepec_Zapotec = 'ztm'; + case Santa_Catarina_Albarradas_Zapotec = 'ztn'; + case Loxicha_Zapotec = 'ztp'; + case Quioquitani_Quieri_Zapotec = 'ztq'; + case Tilquiapan_Zapotec = 'zts'; + case Tejalapan_Zapotec = 'ztt'; + case Guila_Zapotec = 'ztu'; + case Zaachila_Zapotec = 'ztx'; + case Yatee_Zapotec = 'zty'; + case Tokano = 'zuh'; + case Zulu = 'zul'; + case Kumzari = 'zum'; + case Zuni = 'zun'; + case Zumaya = 'zuy'; + case Zay = 'zwa'; + case No_linguistic_content = 'zxx'; + case Yongbei_Zhuang = 'zyb'; + case Yang_Zhuang = 'zyg'; + case Youjiang_Zhuang = 'zyj'; + case Yongnan_Zhuang = 'zyn'; + case Zyphe_Chin = 'zyp'; + case Zaza = 'zza'; + case Zuojiang_Zhuang = 'zzj'; + +} + +class LanguageName {} + +class BackedEnum { + static public function fromName(string $s, string $t):mixed { + return null; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10847.php b/tests/PHPStan/Analyser/data/bug-10847.php new file mode 100644 index 0000000000..6a3dd0bbb0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10847.php @@ -0,0 +1,880 @@ +): Value>, url: string}> + */ + private const BUILTIN_FUNCTIONS = [ + // sass:color + 'red' => ['overloads' => ['$color' => [ColorFunctions::class, 'red']], 'url' => 'sass:color'], + 'green' => ['overloads' => ['$color' => [ColorFunctions::class, 'green']], 'url' => 'sass:color'], + 'blue' => ['overloads' => ['$color' => [ColorFunctions::class, 'blue']], 'url' => 'sass:color'], + 'mix' => ['overloads' => ['$color1, $color2, $weight: 50%' => [ColorFunctions::class, 'mix']], 'url' => 'sass:color'], + 'rgb' => ['overloads' => [ + '$red, $green, $blue, $alpha' => [ColorFunctions::class, 'rgb'], + '$red, $green, $blue' => [ColorFunctions::class, 'rgb'], + '$color, $alpha' => [ColorFunctions::class, 'rgbTwoArgs'], + '$channels' => [ColorFunctions::class, 'rgbOneArgs'], + ], 'url' => 'sass:color'], + 'rgba' => ['overloads' => [ + '$red, $green, $blue, $alpha' => [ColorFunctions::class, 'rgba'], + '$red, $green, $blue' => [ColorFunctions::class, 'rgba'], + '$color, $alpha' => [ColorFunctions::class, 'rgbaTwoArgs'], + '$channels' => [ColorFunctions::class, 'rgbaOneArgs'], + ], 'url' => 'sass:color'], + 'invert' => ['overloads' => ['$color, $weight: 100%' => [ColorFunctions::class, 'invert']], 'url' => 'sass:color'], + 'hue' => ['overloads' => ['$color' => [ColorFunctions::class, 'hue']], 'url' => 'sass:color'], + 'saturation' => ['overloads' => ['$color' => [ColorFunctions::class, 'saturation']], 'url' => 'sass:color'], + 'lightness' => ['overloads' => ['$color' => [ColorFunctions::class, 'lightness']], 'url' => 'sass:color'], + 'complement' => ['overloads' => ['$color' => [ColorFunctions::class, 'complement']], 'url' => 'sass:color'], + 'hsl' => ['overloads' => [ + '$hue, $saturation, $lightness, $alpha' => [ColorFunctions::class, 'hsl'], + '$hue, $saturation, $lightness' => [ColorFunctions::class, 'hsl'], + '$hue, $saturation' => [ColorFunctions::class, 'hslTwoArgs'], + '$channels' => [ColorFunctions::class, 'hslOneArgs'], + ], 'url' => 'sass:color'], + 'hsla' => ['overloads' => [ + '$hue, $saturation, $lightness, $alpha' => [ColorFunctions::class, 'hsla'], + '$hue, $saturation, $lightness' => [ColorFunctions::class, 'hsla'], + '$hue, $saturation' => [ColorFunctions::class, 'hslaTwoArgs'], + '$channels' => [ColorFunctions::class, 'hslaOneArgs'], + ], 'url' => 'sass:color'], + 'grayscale' => ['overloads' => ['$color' => [ColorFunctions::class, 'grayscale']], 'url' => 'sass:color'], + 'adjust-hue' => ['overloads' => ['$color, $degrees' => [ColorFunctions::class, 'adjustHue']], 'url' => 'sass:color'], + 'lighten' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'lighten']], 'url' => 'sass:color'], + 'darken' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'darken']], 'url' => 'sass:color'], + 'saturate' => ['overloads' => [ + '$amount' => [ColorFunctions::class, 'saturateCss'], + '$color, $amount' => [ColorFunctions::class, 'saturate'], + ], 'url' => 'sass:color'], + 'desaturate' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'desaturate']], 'url' => 'sass:color'], + 'opacify' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'opacify']], 'url' => 'sass:color'], + 'fade-in' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'opacify']], 'url' => 'sass:color'], + 'transparentize' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'transparentize']], 'url' => 'sass:color'], + 'fade-out' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'transparentize']], 'url' => 'sass:color'], + 'alpha' => ['overloads' => [ + '$color' => [ColorFunctions::class, 'alpha'], + '$args...' => [ColorFunctions::class, 'alphaMicrosoft'], + ], 'url' => 'sass:color'], + 'opacity' => ['overloads' => ['$color' => [ColorFunctions::class, 'opacity']], 'url' => 'sass:color'], + 'ie-hex-str' => ['overloads' => ['$color' => [ColorFunctions::class, 'ieHexStr']], 'url' => 'sass:color'], + 'adjust-color' => ['overloads' => ['$color, $kwargs...' => [ColorFunctions::class, 'adjust']], 'url' => 'sass:color'], + 'scale-color' => ['overloads' => ['$color, $kwargs...' => [ColorFunctions::class, 'scale']], 'url' => 'sass:color'], + 'change-color' => ['overloads' => ['$color, $kwargs...' => [ColorFunctions::class, 'change']], 'url' => 'sass:color'], + // sass:list + 'length' => ['overloads' => ['$list' => [ListFunctions::class, 'length']], 'url' => 'sass:list'], + 'nth' => ['overloads' => ['$list, $n' => [ListFunctions::class, 'nth']], 'url' => 'sass:list'], + 'set-nth' => ['overloads' => ['$list, $n, $value' => [ListFunctions::class, 'setNth']], 'url' => 'sass:list'], + 'join' => ['overloads' => ['$list1, $list2, $separator: auto, $bracketed: auto' => [ListFunctions::class, 'join']], 'url' => 'sass:list'], + 'append' => ['overloads' => ['$list, $val, $separator: auto' => [ListFunctions::class, 'append']], 'url' => 'sass:list'], + 'zip' => ['overloads' => ['$lists...' => [ListFunctions::class, 'zip']], 'url' => 'sass:list'], + 'index' => ['overloads' => ['$list, $value' => [ListFunctions::class, 'index']], 'url' => 'sass:list'], + 'is-bracketed' => ['overloads' => ['$list' => [ListFunctions::class, 'isBracketed']], 'url' => 'sass:list'], + 'list-separator' => ['overloads' => ['$list' => [ListFunctions::class, 'separator']], 'url' => 'sass:list'], + // sass:map + 'map-get' => ['overloads' => ['$map, $key, $keys...' => [MapFunctions::class, 'get']], 'url' => 'sass:map'], + 'map-merge' => ['overloads' => [ + '$map1, $map2' => [MapFunctions::class, 'mergeTwoArgs'], + '$map1, $args...' => [MapFunctions::class, 'mergeVariadic'], + ], 'url' => 'sass:map'], + 'map-remove' => ['overloads' => [ + // Because the signature below has an explicit `$key` argument, it doesn't + // allow zero keys to be passed. We want to allow that case, so we add an + // explicit overload for it. + '$map' => [MapFunctions::class, 'removeNoKeys'], + // The first argument has special handling so that the $key parameter can be + // passed by name. + '$map, $key, $keys...' => [MapFunctions::class, 'remove'], + ], 'url' => 'sass:map'], + 'map-keys' => ['overloads' => ['$map' => [MapFunctions::class, 'keys']], 'url' => 'sass:map'], + 'map-values' => ['overloads' => ['$map' => [MapFunctions::class, 'values']], 'url' => 'sass:map'], + 'map-has-key' => ['overloads' => ['map, $key, $keys...' => [MapFunctions::class, 'hasKey']], 'url' => 'sass:map'], + // sass:math + 'abs' => ['overloads' => ['$number' => [MathFunctions::class, 'abs']], 'url' => 'sass:math'], + 'ceil' => ['overloads' => ['$number' => [MathFunctions::class, 'ceil']], 'url' => 'sass:math'], + 'floor' => ['overloads' => ['$number' => [MathFunctions::class, 'floor']], 'url' => 'sass:math'], + 'max' => ['overloads' => ['$numbers...' => [MathFunctions::class, 'max']], 'url' => 'sass:math'], + 'min' => ['overloads' => ['$numbers...' => [MathFunctions::class, 'min']], 'url' => 'sass:math'], + 'random' => ['overloads' => ['$limit: null' => [MathFunctions::class, 'random']], 'url' => 'sass:math'], + 'percentage' => ['overloads' => ['$number' => [MathFunctions::class, 'percentage']], 'url' => 'sass:math'], + 'round' => ['overloads' => ['$number' => [MathFunctions::class, 'round']], 'url' => 'sass:math'], + 'unit' => ['overloads' => ['$number' => [MathFunctions::class, 'unit']], 'url' => 'sass:math'], + 'comparable' => ['overloads' => ['$number1, $number2' => [MathFunctions::class, 'compatible']], 'url' => 'sass:math'], + 'unitless' => ['overloads' => ['$number' => [MathFunctions::class, 'isUnitless']], 'url' => 'sass:math'], + // sass:meta + 'feature-exists' => ['overloads' => ['$feature' => [MetaFunctions::class, 'featureExists']], 'url' => 'sass:meta'], + 'inspect' => ['overloads' => ['$value' => [MetaFunctions::class, 'inspect']], 'url' => 'sass:meta'], + 'type-of' => ['overloads' => ['$value' => [MetaFunctions::class, 'typeof']], 'url' => 'sass:meta'], + // sass:selector + 'is-superselector' => ['overloads' => ['$super, $sub' => [SelectorFunctions::class, 'isSuperselector']], 'url' => 'sass:selector'], + 'simple-selectors' => ['overloads' => ['$selector' => [SelectorFunctions::class, 'simpleSelectors']], 'url' => 'sass:selector'], + 'selector-parse' => ['overloads' => ['$selector' => [SelectorFunctions::class, 'parse']], 'url' => 'sass:selector'], + 'selector-nest' => ['overloads' => ['$selectors...' => [SelectorFunctions::class, 'nest']], 'url' => 'sass:selector'], + 'selector-append' => ['overloads' => ['$selectors...' => [SelectorFunctions::class, 'append']], 'url' => 'sass:selector'], + 'selector-extend' => ['overloads' => ['$selector, $extendee, $extender' => [SelectorFunctions::class, 'extend']], 'url' => 'sass:selector'], + 'selector-replace' => ['overloads' => ['$selector, $original, $replacement' => [SelectorFunctions::class, 'replace']], 'url' => 'sass:selector'], + 'selector-unify' => ['overloads' => ['$selector1, $selector2' => [SelectorFunctions::class, 'unify']], 'url' => 'sass:selector'], + // sass:string + 'unquote' => ['overloads' => ['$string' => [StringFunctions::class, 'unquote']], 'url' => 'sass:string'], + 'quote' => ['overloads' => ['$string' => [StringFunctions::class, 'quote']], 'url' => 'sass:string'], + 'to-upper-case' => ['overloads' => ['$string' => [StringFunctions::class, 'toUpperCase']], 'url' => 'sass:string'], + 'to-lower-case' => ['overloads' => ['$string' => [StringFunctions::class, 'toLowerCase']], 'url' => 'sass:string'], + 'uniqueId' => ['overloads' => ['' => [StringFunctions::class, 'uniqueId']], 'url' => 'sass:string'], + 'str-length' => ['overloads' => ['$string' => [StringFunctions::class, 'length']], 'url' => 'sass:string'], + 'str-insert' => ['overloads' => ['$string, $insert, $index' => [StringFunctions::class, 'insert']], 'url' => 'sass:string'], + 'str-index' => ['overloads' => ['$string, $substring' => [StringFunctions::class, 'index']], 'url' => 'sass:string'], + 'str-slice' => ['overloads' => ['$string, $start-at, $end-at: -1' => [StringFunctions::class, 'slice']], 'url' => 'sass:string'], + ]; + + public static function has(string $name): bool + { + return isset(self::BUILTIN_FUNCTIONS[$name]); + } + + public static function get(string $name): BuiltInCallable + { + if (!isset(self::BUILTIN_FUNCTIONS[$name])) { + throw new \InvalidArgumentException("There is no builtin function named $name."); + } + + return BuiltInCallable::overloadedFunction($name, self::BUILTIN_FUNCTIONS[$name]['overloads'], self::BUILTIN_FUNCTIONS[$name]['url']); + } +} + +abstract class Value {} + +class BuiltInCallable +{ + /** + * @param array): Value> $overloads + */ + public static function overloadedFunction(string $name, array $overloads, ?string $url = null): BuiltInCallable + { + $processedOverloads = []; + + foreach ($overloads as $args => $callback) { + $overloads[] = [ + $args, + $callback, + ]; + } + + return new BuiltInCallable($name, $processedOverloads, $url); + } + + /** + * @param list): Value}> $overloads + */ + private function __construct(public readonly string $name, public readonly array $overloads, public readonly ?string $url) + { + } +} + +/** + * @internal + */ +class ColorFunctions +{ + /** + * @param list $arguments + */ + public static function rgb(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgbTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgbOneArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgba(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgbaTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgbaOneArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function invert(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hsl(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hslTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hslOneArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hsla(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hslaTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hslaOneArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function grayscale(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function adjustHue(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function lighten(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function darken(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function saturateCss(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function saturate(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function desaturate(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function alpha(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function alphaMicrosoft(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function opacity(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function red(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function green(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function blue(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function mix(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hue(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function saturation(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function lightness(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function complement(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function adjust(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function scale(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function change(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function ieHexStr(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function opacify(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function transparentize(array $arguments): Value + { + return $arguments[0]; + } +} +class ListFunctions +{ + /** + * @param list $arguments + */ + public static function length(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function nth(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function setNth(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function join(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function append(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function zip(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function index(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function separator(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function isBracketed(array $arguments): Value + { + return $arguments[0]; + } +} +class MapFunctions +{ + /** + * @param list $arguments + */ + public static function get(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function mergeTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function mergeVariadic(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function removeNoKeys(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function remove(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function keys(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function values(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hasKey(array $arguments): Value + { + return $arguments[0]; + } +} +final class MathFunctions +{ + /** + * @param list $arguments + */ + public static function abs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function ceil(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function floor(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function max(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function min(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function round(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function compatible(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function isUnitless(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function unit(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function percentage(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function random(array $arguments): Value + { + return $arguments[0]; + } +} +final class MetaFunctions +{ + /** + * @param list $arguments + */ + public static function featureExists(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function inspect(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function typeof(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function keywords(array $arguments): Value + { + return $arguments[0]; + } +} +final class SelectorFunctions +{ + /** + * @param list $arguments + */ + public static function nest(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function append(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function extend(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function replace(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function unify(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function isSuperselector(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function simpleSelectors(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function parse(array $arguments): Value + { + return $arguments[0]; + } +} +final class StringFunctions +{ + /** + * @param list $arguments + */ + public static function unquote(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function quote(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function length(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function insert(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function index(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function slice(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function toUpperCase(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function toLowerCase(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function uniqueId(array $arguments): Value + { + return $arguments[0]; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10867.php b/tests/PHPStan/Analyser/data/bug-10867.php new file mode 100644 index 0000000000..82620c277c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10867.php @@ -0,0 +1,10 @@ + + +

+ $array */ + public function sayHello(array $array): void + { + foreach ($array as $key => $item) { + $array[$key]['bar'] = ''; + } + assertType("array", $array); + } + + /** @param array $array */ + public function sayHello2(array $array): void + { + if (count($array) > 0) { + return; + } + + foreach ($array as $key => $item) { + $array[$key]['bar'] = ''; + } + assertType("array{}", $array); + } + + /** @param array $array */ + public function sayHello3(array $array): void + { + if (count($array) === 0) { + return; + } + + foreach ($array as $key => $item) { + $array[$key]['bar'] = ''; + } + assertType("non-empty-array", $array); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10952c.php b/tests/PHPStan/Analyser/data/bug-10952c.php new file mode 100644 index 0000000000..87d28e3827 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10952c.php @@ -0,0 +1,30 @@ +getString(); + + if ((strlen($string) > 1) === true) { + assertType('non-empty-string', $string); + } else { + assertType("string", $string); + } + + match (true) { + (strlen($string) > 1) => assertType('non-empty-string', $string), + default => assertType("string", $string), + }; + + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10979.php b/tests/PHPStan/Analyser/data/bug-10979.php new file mode 100644 index 0000000000..562d7b4eeb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10979.php @@ -0,0 +1,802 @@ + 'Guzzle', + self::PHPUnit => 'PHPUnit', + self::Monolog => 'Monolog', + self::PChart => 'pChart', + self::PHPStan => 'PHPStan', + self::PHPMailer => 'PHPMailer', + self::RespectValidation => 'RespectValidation', + self::Stripe => 'Stripe', + self::Ratchet => 'Ratchet', + self::Sentinel => 'Sentinel', + // Python + self::Matplotlib => 'Matplotlib', + self::Seaborn => 'Seaborn', + self::Selenium => 'Selenium', + self::OpenCV => 'OpenCV', + self::Keras => 'Keras', + self::PyTorch => 'PyTorch', + self::NumPy => 'NumPy', + self::Pandas => 'Pandas', + self::Plotly => 'Plotly', + // C++ + self::Cmake => 'CMake', + // Node.js + self::Playwright => 'Playwright', + + /** + * サーバーサイド(フレームワーク) + */ + // Laravel + self::LaravelScout => 'Laravel Scout', + self::LaravelCashier => 'Laravel Cashier', + self::LaravelJetstream => 'Laravel Jetstream', + self::LaravelSanctum => 'Laravel Sanctum', + // Java + self::SLF4J => 'SLF4J', + self::Mockito => 'Mockito', + self::OpenCSV => 'OpenCSV', + // Rails + self::Devise => 'Devise', + self::Capybara => 'Capybara', + + /** + * フロントエンド + */ + // JavaScript・TypeScript + self::JQuery => 'jQuery', + self::D3js => 'D3.js', + self::Lodash => 'Lodash', + self::Underscorejs => 'Underscore.js', + self::Animejs => 'Anime.js', + self::AnimateOnScroll => 'Animate On Scroll', + self::Videojs => 'Video.js', + self::Chartjs => 'Chart.js', + self::Cleavejs => 'Cleave.js', + self::FullPagejs => 'FullPage.js', + self::Leaflet => 'Leaflet', + self::Threejs => 'Three.js', + self::Screenfulljs => 'Screenfull.js', + self::Axios => 'Axios', + self::SocketIO => 'Socket.io', + self::TanStackQuery => 'TanStack Query', + self::Htmx => 'htmx', + self::GSAP => 'GSAP', + self::Swiper => 'Swiper', + self::EmblaCarousel => 'Embla Carousel', + self::Husky => 'husky', + self::MilionJs => 'Milion.js', + self::Biome => 'Biome', + self::Prettier => 'Prettier', + self::ESLint => 'ESLint', + self::SolidJS => 'SolidJS', + self::NextAuth => 'NextAuth', + self::InertiaJS => 'Inertia.js', + self::DrizzleORM => 'Drizzle ORM', // TS専用 + self::Zod => 'Zod', // TS専用 + self::TypeORM => 'TypeORM', // TS専用 + // Vue + self::VueChartjs => 'Vue Chart.js', + self::VeeValidate => 'VeeValidate', + self::VueDraggable => 'Vue Draggable', + self::Vuelidate => 'Vuelidate', + self::VueMultiselect => 'Vue Multiselect', + self::Vuex => 'Vuex', + self::Vuetify => 'Vuetify', + self::ElementUI => 'Element UI', + self::VueMaterial => 'Vue Material', + self::BootstrapVue => 'Bootstrap Vue', + self::Pinia => 'Pinia', + // React + self::Redux => 'Redux', + self::Tldraw => 'tldraw', + self::ShadcnUi => 'shadcn/ui', + self::MUI => 'MUI', + self::ChakraUI => 'Chakra UI', + self::Recoil => 'Recoil', + self::Jotai => 'Jotai', + self::Zustand => 'Zustand', + self::SWR => 'SWR', + self::ReactHookForm => 'React Hook Form', + self::RadixUI => 'Radix UI', + // Tailwind CSS + self::DaisyUI => 'DaisyUI', + + /** + * インフラ + */ + + /** + * モバイルアプリ + */ + + /** + * データベース + */ + self::DBeaver => 'DBeaver', + self::SequelPro => 'Sequel Pro', + self::SequelAce => 'Sequel Ace', + self::TablePlus => 'TablePlus', + self::Navicat => 'Navicat', + self::MySQLWorkbench => 'MySQL Workbench', + self::PHPMyAdmin => 'phpMyAdmin', + }; + } + + /** + * 関連する分野のIDを配列で返す + * ※複数の分野に関連する場合は複数のIDを返す + * + * @return array + */ + public function getFieldIds(): array + { + return match ($this) { + /** + * 複数に関連するライブラリ + */ + self::InertiaJS => [ + FieldMasterEnum::FrontEnd->value, + FieldMasterEnum::ServerSide->value, + ], + + /** + * サーバーサイド + */ + self::Guzzle, + self::PHPUnit, + self::Monolog, + self::PChart, + self::PHPStan, + self::PHPMailer, + self::RespectValidation, + self::Stripe, + self::Ratchet, + self::Sentinel, + self::Matplotlib, + self::Seaborn, + self::Selenium, + self::OpenCV, + self::Keras, + self::PyTorch, + self::NumPy, + self::Pandas, + self::Plotly, + self::Cmake, + self::LaravelScout, + self::LaravelCashier, + self::LaravelJetstream, + self::LaravelSanctum, + self::SLF4J, + self::Mockito, + self::OpenCSV, + self::Devise, + self::Capybara + => [ + FieldMasterEnum::ServerSide->value, + ], + + /** + * フロントエンド + */ + self::JQuery, + self::D3js, + self::Lodash, + self::Underscorejs, + self::Animejs, + self::AnimateOnScroll, + self::Videojs, + self::Chartjs, + self::Cleavejs, + self::FullPagejs, + self::Leaflet, + self::Threejs, + self::Screenfulljs, + self::Axios, + self::TypeORM, + self::VueChartjs, + self::VeeValidate, + self::VueDraggable, + self::Vuelidate, + self::VueMultiselect, + self::Vuex, + self::Vuetify, + self::ElementUI, + self::VueMaterial, + self::BootstrapVue, + self::SocketIO, + self::TanStackQuery, + self::Htmx, + self::Zod, + self::Redux, + self::Tldraw, + self::ShadcnUi, + self::MUI, + self::ChakraUI, + self::Recoil, + self::Jotai, + self::Zustand, + self::SWR, + self::ReactHookForm, + self::RadixUI, + self::GSAP, + self::Swiper, + self::EmblaCarousel, + self::Husky, + self::DrizzleORM, + self::MilionJs, + self::Biome, + self::Prettier, + self::ESLint, + self::DaisyUI, + self::Playwright, + self::Pinia, + self::SolidJS, + self::NextAuth + => [ + FieldMasterEnum::FrontEnd->value, + ], + + /** + * インフラ + */ + + /** + * モバイルアプリ + */ + + /** + * データベース + */ + self::DBeaver, + self::SequelPro, + self::SequelAce, + self::TablePlus, + self::Navicat, + self::MySQLWorkbench, + self::PHPMyAdmin + => [ + FieldMasterEnum::Database->value, + ], + }; + } + + /** + * 関連する言語・ツール、もしくはフレームワークのIDを配列で返す + * ※複数に関連する場合は複数のIDを返す + * + * @return array + */ + public function getMasterIds(): array + { + return match ($this) { + /** + * 複数に関連するライブラリ + */ + self::Stripe => [ + LanguageToolMasterEnum::PHP->value, + LanguageToolMasterEnum::Ruby->value, + LanguageToolMasterEnum::Java->value, + LanguageToolMasterEnum::Python->value, + LanguageToolMasterEnum::Go->value, + LanguageToolMasterEnum::NodeJS->value, + LanguageToolMasterEnum::JavaScript->value, + LanguageToolMasterEnum::TypeScript->value, + FrameworkMasterEnum::ReactNative->value, + ], + self::PyTorch => [ + LanguageToolMasterEnum::Python->value, + LanguageToolMasterEnum::CPlusPlus->value, + ], + self::OpenCV => [ + LanguageToolMasterEnum::Python->value, + LanguageToolMasterEnum::CPlusPlus->value, + LanguageToolMasterEnum::Java->value, + ], + self::InertiaJS => [ + FrameworkMasterEnum::Vue->value, + FrameworkMasterEnum::React->value, + FrameworkMasterEnum::Svelte->value, + FrameworkMasterEnum::Laravel->value, + FrameworkMasterEnum::RubyOnRails->value, + ], + + /** + * サーバーサイド(言語・ツール) + */ + // PHP + self::Guzzle, + self::PHPUnit, + self::Monolog, + self::PChart, + self::PHPStan, + self::PHPMailer, + self::RespectValidation, + self::Ratchet, + self::Sentinel => [ + LanguageToolMasterEnum::PHP->value, + ], + // C++ + self::Cmake => [ + LanguageToolMasterEnum::CPlusPlus->value, + ], + // Python + self::Matplotlib, + self::Seaborn, + self::Selenium, + self::Keras, + self::NumPy, + self::Pandas, + self::Plotly => [ + LanguageToolMasterEnum::Python->value, + ], + // Java + self::SLF4J, + self::Mockito, + self::OpenCSV => [ + LanguageToolMasterEnum::Java->value, + ], + // Node.js + self::Playwright => [ + LanguageToolMasterEnum::NodeJS->value, + ], + + /** + * サーバーサイド(フレームワーク) + */ + // Laravel + self::LaravelScout, + self::LaravelCashier, + self::LaravelJetstream, + self::LaravelSanctum => [ + FrameworkMasterEnum::Laravel->value, + ], + // Rails + self::Devise, + self::Capybara => [ + FrameworkMasterEnum::RubyOnRails->value, + ], + + /** + * フロントエンド + */ + // JavaScript・TypeScript + self::JQuery, + self::D3js, + self::Lodash, + self::Underscorejs, + self::Animejs, + self::AnimateOnScroll, + self::Videojs, + self::Chartjs, + self::Cleavejs, + self::FullPagejs, + self::Leaflet, + self::Threejs, + self::Screenfulljs, + self::Axios, + self::SocketIO, + self::TanStackQuery, + self::Htmx, + self::GSAP, + self::Swiper, + self::EmblaCarousel, + self::Husky, + self::Biome, + self::Prettier, + self::ESLint, + self::SolidJS, + self::NextAuth + => [ + LanguageToolMasterEnum::JavaScript->value, + LanguageToolMasterEnum::TypeScript->value, + ], + // TypeScript + self::Zod, + self::TypeORM, + self::DrizzleORM + => [ + LanguageToolMasterEnum::TypeScript->value, + ], + // Vue + self::VueChartjs, + self::VeeValidate, + self::VueDraggable, + self::Vuelidate, + self::VueMultiselect, + self::Vuex, + self::Vuetify, + self::ElementUI, + self::VueMaterial, + self::BootstrapVue, + self::Pinia + => [ + FrameworkMasterEnum::Vue->value, + ], + // React + self::Redux, + self::Tldraw, + self::ShadcnUi, + self::MUI, + self::ChakraUI, + self::Recoil, + self::Jotai, + self::Zustand, + self::SWR, + self::ReactHookForm, + self::RadixUI, + self::MilionJs + => [ + FrameworkMasterEnum::React->value, + ], + // Tailwind CSS + self::DaisyUI => [ + FrameworkMasterEnum::TailwindCSS->value, + ], + + /** + * インフラ + */ + + /** + * モバイルアプリ + */ + + /** + * データベース + */ + self::DBeaver => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + LanguageToolMasterEnum::PostgreSQL->value, + LanguageToolMasterEnum::SQLite->value, + LanguageToolMasterEnum::OracleDatabase->value, + LanguageToolMasterEnum::Db2->value, + LanguageToolMasterEnum::SQLServer->value, + LanguageToolMasterEnum::FirebirdSQL->value, + LanguageToolMasterEnum::MongoDB->value, + LanguageToolMasterEnum::ApacheCassandra->value, + LanguageToolMasterEnum::Redis->value, + LanguageToolMasterEnum::BigQuery->value, + LanguageToolMasterEnum::AmazonDynamoDB->value, + ], + self::SequelPro, + self::SequelAce => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + ], + self::TablePlus => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + LanguageToolMasterEnum::PostgreSQL->value, + LanguageToolMasterEnum::SQLite->value, + LanguageToolMasterEnum::OracleDatabase->value, + LanguageToolMasterEnum::Redis->value, + LanguageToolMasterEnum::ApacheCassandra->value, + LanguageToolMasterEnum::MongoDB->value, + LanguageToolMasterEnum::MariaDB->value, + LanguageToolMasterEnum::SQLServer->value, + LanguageToolMasterEnum::BigQuery->value, + LanguageToolMasterEnum::CockroachDB->value, + ], + self::Navicat => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + LanguageToolMasterEnum::PostgreSQL->value, + LanguageToolMasterEnum::SQLite->value, + LanguageToolMasterEnum::OracleDatabase->value, + LanguageToolMasterEnum::SQLServer->value, + LanguageToolMasterEnum::MariaDB->value, + ], + self::MySQLWorkbench => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + ], + self::PHPMyAdmin => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + LanguageToolMasterEnum::MariaDB->value, + ], + }; + } + + /** + * `language_tool` or `framework`いずれかのtypeを返す + * + * @return LibraryRelationTypeEnum + */ + public function getType(): LibraryRelationTypeEnum + { + return match ($this) { + /** + * 言語・ツール&フレームワークどちらにも関連するライブラリ + */ + + /** + * 言語・ツール + */ + self::Guzzle, + self::PHPUnit, + self::Monolog, + self::PChart, + self::PHPStan, + self::PHPMailer, + self::RespectValidation, + self::Stripe, + self::Ratchet, + self::Sentinel, + self::Matplotlib, + self::Seaborn, + self::Selenium, + self::OpenCV, + self::Keras, + self::PyTorch, + self::NumPy, + self::Pandas, + self::Plotly, + self::Cmake, + self::SLF4J, + self::Mockito, + self::OpenCSV, + self::JQuery, + self::D3js, + self::Lodash, + self::Underscorejs, + self::Animejs, + self::AnimateOnScroll, + self::Videojs, + self::Chartjs, + self::Cleavejs, + self::FullPagejs, + self::Leaflet, + self::Threejs, + self::Screenfulljs, + self::Axios, + self::SocketIO, + self::Htmx, + self::TanStackQuery, + self::Zod, + self::TypeORM, + self::DBeaver, + self::SequelPro, + self::SequelAce, + self::TablePlus, + self::Navicat, + self::MySQLWorkbench, + self::PHPMyAdmin, + self::GSAP, + self::Swiper, + self::EmblaCarousel, + self::Husky, + self::DrizzleORM, + self::MilionJs, + self::Biome, + self::Prettier, + self::ESLint, + self::DaisyUI, + self::Playwright, + self::SolidJS, + self::NextAuth + => LibraryRelationTypeEnum::LanguageTool, + + /** + * フレームワーク + */ + self::LaravelScout, + self::LaravelCashier, + self::LaravelJetstream, + self::LaravelSanctum, + self::VueChartjs, + self::VeeValidate, + self::VueDraggable, + self::Vuelidate, + self::VueMultiselect, + self::Vuex, + self::Vuetify, + self::ElementUI, + self::VueMaterial, + self::BootstrapVue, + self::Redux, + self::Tldraw, + self::Devise, + self::Capybara, + self::ShadcnUi, + self::MUI, + self::ChakraUI, + self::Recoil, + self::Jotai, + self::Zustand, + self::SWR, + self::ReactHookForm, + self::RadixUI, + self::Pinia, + self::InertiaJS + => LibraryRelationTypeEnum::Framework, + }; + } + + /** + * カテゴリIDがライブラリのIDかどうか判定 + * + * @param int $categoryId + * @return bool + */ + public static function isLibraryCategoryId(int $categoryId): bool + { + foreach (self::cases() as $case) { + if ($categoryId === $case->value) { + return true; + } + } + return false; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10980.php b/tests/PHPStan/Analyser/data/bug-10980.php new file mode 100644 index 0000000000..97e04e83e5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10980.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug10985; + +use function PHPStan\Testing\assertType; + +enum Test { + case ORIGINAL; +} + +function (): void { + $item = Test::class; + $result = ($item)::ORIGINAL; + assertType('Bug10985\\Test::ORIGINAL', $result); +}; diff --git a/tests/PHPStan/Analyser/data/bug-11009.php b/tests/PHPStan/Analyser/data/bug-11009.php new file mode 100644 index 0000000000..1eea19fe18 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11009.php @@ -0,0 +1,45 @@ +returnStatic()); + assertType(B::class, $b->returnSelf()); +}; diff --git a/tests/PHPStan/Analyser/data/bug-11009.stub b/tests/PHPStan/Analyser/data/bug-11009.stub new file mode 100644 index 0000000000..dce4347bb4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11009.stub @@ -0,0 +1,21 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug11263; + +enum FirstEnum: string +{ + + case XyzSaturdayStopDomestic = 'XYZ_DOMESTIC_300'; + case XyzSaturdayDeliveryAir = 'XYZ_AIR_300'; + case XyzAdditionalHandling = 'XYZ_100'; + case XyzCommercialDomesticAirDeliveryArea = 'XYZ_COMMERCIAL_AIR_376'; + case XyzCommercialDomesticAirExtendedDeliveryArea = 'XYZ_COMMERCIAL_AIR_EXTENDED_376'; + case XyzCommercialDomesticGroundDeliveryArea = 'XYZ_COMMERCIAL_GROUND_376'; + case XyzCommercialDomesticGroundExtendedDeliveryArea = 'XYZ_COMMERCIAL_GROUND_EXTENDED_376'; + case XyzResidentialDomesticAirDeliveryArea = 'XYZ_RESIDENTIAL_AIR_376'; + case XyzResidentialDomesticAirExtendedDeliveryArea = 'XYZ_RESIDENTIAL_AIR_EXTENDED_376'; + case XyzResidentialDomesticGroundDeliveryArea = 'XYZ_RESIDENTIAL_GROUND_376'; + case XyzResidentialDomesticGroundExtendedDeliveryArea = 'XYZ_RESIDENTIAL_GROUND_EXTENDED_376'; + case XyzDeliveryAreaSurchargeSurePost = 'XYZ_SURE_POST_376'; + case XyzDeliveryAreaSurchargeSurePostExtended = 'XYZ_SURE_POST_EXTENDED_376'; + case XyzDeliveryAreaSurchargeOther = 'XYZ_DELIVERY_AREA_OTHER_376'; + case XyzResidentialSurchargeAir = 'XYZ_RESIDENTIAL_SURCHARGE_AIR_270'; + case XyzResidentialSurchargeGround = 'XYZ_RESIDENTIAL_SURCHARGE_GROUND_270'; + case XyzSurchargeCodeFuel = 'XYZ_375'; + case XyzCod = 'XYZ_COD_110'; + case XyzDeliveryConfirmation = 'XYZ_DELIVERY_CONFIRMATION_120'; + case XyzShipDeliveryConfirmation = 'XYZ_SHIP_DELIVERY_CONFIRMATION_121'; + case XyzExtendedArea = 'XYZ_EXTENDED_AREA_190'; + case XyzHazMat = 'XYZ_HAZ_MAT_199'; + case XyzDryIce = 'XYZ_DRY_ICE_200'; + case XyzIscSeeds = 'XYZ_ISC_SEEDS_201'; + case XyzIscPerishables = 'XYZ_ISC_PERISHABLES_202'; + case XyzIscTobacco = 'XYZ_ISC_TOBACCO_203'; + case XyzIscPlants = 'XYZ_ISC_PLANTS_204'; + case XyzIscAlcoholicBeverages = 'XYZ_ISC_ALCOHOLIC_BEVERAGES_205'; + case XyzIscBiologicalSubstances = 'XYZ_ISC_BIOLOGICAL_SUBSTANCES_206'; + case XyzIscSpecialExceptions = 'XYZ_ISC_SPECIAL_EXCEPTIONS_207'; + case XyzHoldForPickup = 'XYZ_HOLD_FOR_PICKUP_220'; + case XyzOriginCertificate = 'XYZ_ORIGIN_CERTIFICATE_240'; + case XyzPrintReturnLabel = 'XYZ_PRINT_RETURN_LABEL_250'; + case XyzExportLicenseVerification = 'XYZ_EXPORT_LICENSE_VERIFICATION_258'; + case XyzPrintNMail = 'XYZ_PRINT_N_MAIL_260'; + case XyzReturnService1attempt = 'XYZ_RETURN_SERVICE_1ATTEMPT_280'; + case XyzReturnService3attempt = 'XYZ_RETURN_SERVICE_3ATTEMPT_290'; + case XyzSaturdayInternationalProcessingFee = 'XYZ_SATURDAY_INTERNATIONAL_PROCESSING_FEE_310'; + case XyzElectronicReturnLabel = 'XYZ_ELECTRONIC_RETURN_LABEL_350'; + case XyzPreparedSedForm = 'XYZ_PREPARED_SED_FORM_374'; + case XyzLargePackage = 'XYZ_LARGE_PACKAGE_377'; + case XyzShipperPaysDutyTax = 'XYZ_SHIPPER_PAYS_DUTY_TAX_378'; + case XyzShipperPaysDutyTaxUnpaid = 'XYZ_SHIPPER_PAYS_DUTY_TAX_UNPAID_379'; + case XyzExpressPlusSurcharge = 'XYZ_EXPRESS_PLUS_SURCHARGE_380'; + case XyzInsurance = 'XYZ_INSURANCE_400'; + case XyzShipAdditionalHandling = 'XYZ_SHIP_ADDITIONAL_HANDLING_401'; + case XyzShipperRelease = 'XYZ_SHIPPER_RELEASE_402'; + case XyzCheckToShipper = 'XYZ_CHECK_TO_SHIPPER_403'; + case XyzProactiveResponse = 'XYZ_PROACTIVE_RESPONSE_404'; + case XyzGermanPickup = 'XYZ_GERMAN_PICKUP_405'; + case XyzGermanRoadTax = 'XYZ_GERMAN_ROAD_TAX_406'; + case XyzExtendedAreaPickup = 'XYZ_EXTENDED_AREA_PICKUP_407'; + case XyzReturnOfDocument = 'XYZ_RETURN_OF_DOCUMENT_410'; + case XyzPeakSeason = 'XYZ_PEAK_SEASON_430'; + case XyzLargePackageSeasonalSurcharge = 'XYZ_LARGE_PACKAGE_SEASONAL_SURCHARGE_431'; + case XyzAdditionalHandlingSeasonalSurchargeDiscontinued = 'XYZ_ADDITIONAL_HANDLING_SEASONAL_SURCHARGE_432'; + case XyzShipLargePackage = 'XYZ_SHIP_LARGE_PACKAGE_440'; + case XyzCarbonNeutral = 'XYZ_CARBON_NEUTRAL_441'; + case XyzImportControl = 'XYZ_IMPORT_CONTROL_444'; + case XyzCommercialInvoiceRemoval = 'XYZ_COMMERCIAL_INVOICE_REMOVAL_445'; + case XyzImportControlElectronicLabel = 'XYZ_IMPORT_CONTROL_ELECTRONIC_LABEL_446'; + case XyzImportControlPrintLabel = 'XYZ_IMPORT_CONTROL_PRINT_LABEL_447'; + case XyzImportControlPrintAndMailLabel = 'XYZ_IMPORT_CONTROL_PRINT_AND_MAIL_LABEL_448'; + case XyzImportControlOnePickupAttemptLabel = 'XYZ_IMPORT_CONTROL_ONE_PICKUP_ATTEMPT_LABEL_449'; + case XyzImportControlThreePickUpAttemptLabel = 'XYZ_IMPORT_CONTROL_THREE_PICK_UP_ATTEMPT_LABEL_450'; + case XyzRefrigeration = 'XYZ_REFRIGERATION_452'; + case XyzExchangePrintReturnLabel = 'XYZ_EXCHANGE_PRINT_RETURN_LABEL_464'; + case XyzCommittedDeliveryWindow = 'XYZ_COMMITTED_DELIVERY_WINDOW_470'; + case XyzSecuritySurcharge = 'XYZ_SECURITY_SURCHARGE_480'; + case XyzNonMachinableCharge = 'XYZ_NON_MACHINABLE_CHARGE_490'; + case XyzCustomerTransactionFee = 'XYZ_CUSTOMER_TRANSACTION_FEE_492'; + case XyzSurePostNonStandardLength = 'XYZ_493'; + case XyzSurePostNonStandardExtraLength = 'XYZ_494'; + case XyzSurePostNonStandardCube = 'XYZ_NON_STANDARD_CUBE_CHARGE_495'; + case XyzShipmentCod = 'XYZ_SHIPMENT_COD_500'; + case XyzLiftGateForPickup = 'XYZ_LIFT_GATE_FOR_PICKUP_510'; + case XyzLiftGateForDelivery = 'XYZ_LIFT_GATE_FOR_DELIVERY_511'; + case XyzDropOffAtXyzFacility = 'XYZ_DROP_OFF_AT_XYZ_FACILITY_512'; + case XyzPremiumCare = 'XYZ_PREMIUM_CARE_515'; + case XyzOversizePallet = 'XYZ_OVERSIZE_PALLET_520'; + case XyzFreightDeliverySurcharge = 'XYZ_FREIGHT_DELIVERY_SURCHARGE_530'; + case XyzFreightPickxyzurcharge = 'XYZ_FREIGHT_PICKUP_SURCHARGE_531'; + case XyzDirectToRetail = 'XYZ_DIRECT_TO_RETAIL_540'; + case XyzDirectDeliveryOnly = 'XYZ_DIRECT_DELIVERY_ONLY_541'; + case XyzNoAccessPoint = 'XYZ_NO_ACCESS_POINT_541'; + case XyzDeliverToAddresseeOnly = 'XYZ_DELIVER_TO_ADDRESSEE_ONLY_542'; + case XyzDirectToRetailCod = 'XYZ_DIRECT_TO_RETAIL_COD_543'; + case XyzRetailAccessPoint = 'XYZ_RETAIL_ACCESS_POINT_544'; + case XyzElectronicPackageReleaseAuthentication = 'XYZ_ELECTRONIC_PACKAGE_RELEASE_AUTHENTICATION_546'; + case XyzPayAtStore = 'XYZ_PAY_AT_STORE_547'; + case XyzInsideDelivery = 'XYZ_INSIDE_DELIVERY_549'; + case XyzItemDisposal = 'XYZ_ITEM_DISPOSAL_550'; + case XyzAddressCorrections = 'XYZ_ADDRESS_CORRECTIONS'; + case XyzNotPreviouslyBilledFee = 'XYZ_NOT_PREVIOUSLY_BILLED_FEE'; + case XyzPickxyzurcharge = 'XYZ_PICKUP_SURCHARGE'; + case XyzChargeback = 'XYZ_CHARGEBACK'; + case XyzAdditionalHandlingPeakDemand = 'XYZ_ADDITIONAL_HANDLING_PEAK_DEMAND'; + case XyzOtherSurcharge = 'XYZ_OTHER_SURCHARGE'; + + case XyzRemoteAreaSurcharge = 'XYZ_REMOTE_AREA_SURCHARGE'; + case XyzRemoteAreaOtherSurcharge = 'XYZ_REMOTE_AREA_OTHER_SURCHARGE'; + + case CompanyEconomyResidentialSurchargeLightweight = 'COMPANY_ECONOMY_RESIDENTIAL_SURCHARGE_LIGHTWEIGHT'; + case CompanyEconomyDeliverySurchargeLightweight = 'COMPANY_ECONOMY_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyEconomyExtendedDeliverySurchargeLightweight = 'COMPANY_ECONOMY_EXTENDED_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyStandardResidentialSurchargeLightweight = 'COMPANY_STANDARD_RESIDENTIAL_SURCHARGE_LIGHTWEIGHT'; + case CompanyStandardDeliverySurchargeLightweight = 'COMPANY_STANDARD_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyStandardExtendedDeliverySurchargeLightweight = 'COMPANY_STANDARD_EXTENDED_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyEconomyResidentialSurchargePlus = 'COMPANY_ECONOMY_RESIDENTIAL_SURCHARGE_PLUS'; + case CompanyEconomyDeliverySurchargePlus = 'COMPANY_ECONOMY_DELIVERY_SURCHARGE_PLUS'; + case CompanyEconomyExtendedDeliverySurchargePlus = 'COMPANY_ECONOMY_EXTENDED_DELIVERY_SURCHARGE_PLUS'; + case CompanyEconomyPeakSurchargeLightweight = 'COMPANY_ECONOMY_PEAK_SURCHARGE_LIGHTWEIGHT'; + case CompanyEconomyPeakSurchargePlus = 'COMPANY_ECONOMY_PEAK_SURCHARGE_PLUS'; + case CompanyEconomyPeakSurchargeOver5Lbs = 'COMPANY_ECONOMY_PEAK_SURCHARGE_OVER_5_LBS'; + case CompanyStandardResidentialSurchargePlus = 'COMPANY_STANDARD_RESIDENTIAL_SURCHARGE_PLUS'; + case CompanyStandardDeliverySurchargePlus = 'COMPANY_STANDARD_DELIVERY_SURCHARGE_PLUS'; + case CompanyStandardExtendedDeliverySurchargePlus = 'COMPANY_STANDARD_EXTENDED_DELIVERY_SURCHARGE_PLUS'; + case CompanyStandardPeakSurchargeLightweight = 'COMPANY_STANDARD_PEAK_SURCHARGE_LIGHTWEIGHT'; + case CompanyStandardPeakSurchargePlus = 'COMPANY_STANDARD_PEAK_SURCHARGE_PLUS'; + case CompanyStandardPeakSurchargeOver5Lbs = 'COMPANY_STANDARD_PEAK_SURCHARGE_OVER_5_LBS'; + + case Company2DayResidentialSurcharge = 'COMPANY_2_DAY_RESIDENTIAL_SURCHARGE'; + case Company2DayDeliverySurcharge = 'COMPANY_2_DAY_DELIVERY_SURCHARGE'; + case Company2DayExtendedDeliverySurcharge = 'COMPANY_2_DAY_EXTENDED_DELIVERY_SURCHARGE'; + case Company2DayPeakSurcharge = 'COMPANY_2_DAY_PEAK_SURCHARGE'; + + case CompanyHazmatResidentialSurcharge = 'COMPANY_HAZMAT_RESIDENTIAL_SURCHARGE'; + case CompanyHazmatResidentialSurchargeLightweight = 'COMPANY_HAZMAT_RESIDENTIAL_SURCHARGE_LIGHTWEIGHT'; + case CompanyHazmatDeliverySurcharge = 'COMPANY_HAZMAT_DELIVERY_SURCHARGE'; + case CompanyHazmatDeliverySurchargeLightweight = 'COMPANY_HAZMAT_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyHazmatExtendedDeliverySurcharge = 'COMPANY_HAZMAT_EXTENDED_DELIVERY_SURCHARGE'; + case CompanyHazmatExtendedDeliverySurchargeLightweight = 'COMPANY_HAZMAT_EXTENDED_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyHazmatPeakSurchargeLightweight = 'COMPANY_HAZMAT_PEAK_SURCHARGE_LIGHTWEIGHT'; + case CompanyHazmatPeakSurcharge = 'COMPANY_HAZMAT_PEAK_SURCHARGE'; + case CompanyHazmatPeakSurchargePlus = 'COMPANY_HAZMAT_PEAK_SURCHARGE_PLUS'; + case CompanyHazmatPeakSurchargeOver5Lbs = 'COMPANY_HAZMAT_PEAK_SURCHARGE_OVER_5_LBS'; + + case CompanyFuelSurcharge = 'COMPANY_FUEL_SURCHARGE'; + + case Company2DayAdditionalHandlingSurchargeDimensions = 'COMPANY_2DAY_ADDITIONAL_HANDLING_SURCHARGE_DIMENSIONS'; + case Company2DayAdditionalHandlingSurchargeWeight = 'COMPANY_2DAY_ADDITIONAL_HANDLING_SURCHARGE_WEIGHT'; + case CompanyStandardAdditionalHandlingSurchargeDimensions = 'COMPANY_STANDARD_ADDITIONAL_HANDLING_SURCHARGE_DIMENSIONS'; + case CompanyStandardAdditionalHandlingSurchargeWeight = 'COMPANY_STANDARD_ADDITIONAL_HANDLING_SURCHARGE_WEIGHT'; + case CompanyEconomyAdditionalHandlingSurchargeDimensions = 'COMPANY_ECONOMY_ADDITIONAL_HANDLING_SURCHARGE_DIMENSIONS'; + case CompanyEconomyAdditionalHandlingSurchargeWeight = 'COMPANY_ECONOMY_ADDITIONAL_HANDLING_SURCHARGE_WEIGHT'; + case CompanyHazmatAdditionalHandlingSurchargeDimensions = 'COMPANY_HAZMAT_ADDITIONAL_HANDLING_SURCHARGE_DIMENSIONS'; + case CompanyHazmatAdditionalHandlingSurchargeWeight = 'COMPANY_HAZMAT_ADDITIONAL_HANDLING_SURCHARGE_WEIGHT'; + case Company2DayLargePackageSurcharge = 'COMPANY_2DAY_LARGE_PACKAGE_SURCHARGE'; + case CompanyStandardLargePackageSurcharge = 'COMPANY_STANDARD_LARGE_PACKAGE_SURCHARGE'; + case CompanyEconomyLargePackageSurcharge = 'COMPANY_ECONOMY_LARGE_PACKAGE_SURCHARGE'; + case CompanyHazmatLargePackageSurcharge = 'COMPANY_HAZMAT_LARGE_PACKAGE_SURCHARGE'; + case CompanyLargePackagePeakSurcharge = 'COMPANY_LARGE_PACKAGE_PEAK_SURCHARGE'; + + case CompanyUkSignatureSurcharge = 'COMPANY_UK_SIGNATURE_SURCHARGE'; + + case AciFuelSurcharge = 'ACI_FUEL_SURCHARGE'; + case AciUnmanifestedSurcharge = 'ACI_UNMANIFESTED_SURCHARGE'; + case AciPeakSurcharge = 'ACI_PEAK_SURCHARGE'; + case AciOversizeSurcharge = 'ACI_OVERSIZE_SURCHARGE'; + case AciUltraUrbanSurcharge = 'ACI_ULTRA_URBAN_SURCHARGE'; + case AciNonStandardSurcharge = 'ACI_NON_STANDARD_SURCHARGE'; + + case XyzmiFuelSurcharge = 'XYZMI_FUEL_SURCHARGE'; + case XyzmiNonStandardSurcharge = 'XYZMI_NON_STANDARD_SURCHARGE'; + + case FooEcommerceFuelSurcharge = 'FOO_ECOMMERCE_FUEL_SURCHARGE'; + case FooEcommercePeakSurcharge = 'FOO_ECOMMERCE_PEAK_SURCHARGE'; + case FooEcommerceOversizeSurcharge = 'FOO_ECOMMERCE_OVERSIZE_SURCHARGE'; + case FooEcommerceFutureUseSurcharge = 'FOO_ECOMMERCE_FUTURE_USE_SURCHARGE'; + case FooEcommerceDimLengthSurcharge = 'FOO_ECOMMERCE_DIM_LENGTH_SURCHARGE'; + + case HooFuelSurcharge = 'LASER_SHIP_FUEL_SURCHARGE'; + case HooResidentialSurcharge = 'LASER_SHIP_RESIDENTIAL_SURCHARGE'; + case HooDeliverySurcharge = 'LASER_SHIP_DELIVERY_SURCHARGE'; + case HooExtendedDeliverySurcharge = 'LASER_SHIP_EXTENDED_DELIVERY_SURCHARGE'; + + case MooSmartPostFuelSurcharge = 'MOO_SMART_POST_FUEL_SURCHARGE'; + case MooSmartPostDeliverySurcharge = 'MOO_SMART_POST_DELIVERY_SURCHARGE'; + case MooSmartPostExtendedDeliverySurcharge = 'MOO_SMART_POST_EXTENDED_DELIVERY_SURCHARGE'; + case MooSmartPostNonMachinableSurcharge = 'MOO_SMART_POST_NON_MACHINABLE_SURCHARGE'; + + case MooAdditionalHandlingDomesticDimensionSurcharge = 'MOO_ADDITIONAL_HANDLING_DOMESTIC_DIMENSION_SURCHARGE'; + case MooOversizeSurcharge = 'MOO_OVERSIZE_SURCHARGE'; + + case MooAdditionalHandling = 'MOO_ADDITIONAL_HANDLING'; + case MooAdditionalHandlingChargeDimensions = 'MOO_ADDITIONAL_HANDLING_CHARGE_DIMENSIONS'; + case MooAdditionalHandlingChargePackage = 'MOO_ADDITIONAL_HANDLING_CHARGE_PACKAGE'; + case MooAdditionalHandlingChargeWeight = 'MOO_ADDITIONAL_HANDLING_CHARGE_WEIGHT'; + case MooAdditionalWeightCharge = 'MOO_ADDITIONAL_WEIGHT_CHARGE'; + case MooAdvancementFee = 'MOO_ADVANCEMENT_FEE'; + case MooAhsDimensions = 'MOO_AHS_DIMENSIONS'; + case MooAhsWeight = 'MOO_AHS_WEIGHT'; + case MooAlaskaOrHawaiiOrPuertoRicoPkupOrDel = 'MOO_ALASKA/HAWAII/PUERTO_RICO_PKUP/DEL'; + case MooAppointment = 'MOO_APPOINTMENT'; + case MooBox24X24X18DblWalledProductQuantity2 = 'MOO_BOX_24_X_24_X_18_DBL_WALLED_PRODUCT_QUANTITY_2'; + case MooBox28X28X28DblWalledProductQuantity3 = 'MOO_BOX_28_X_28_X_28_DBL_WALLED_PRODUCT_QUANTITY_3'; + case MooBoxMultiDepth22X22X22ProductQuantity7 = 'MOO_BOX_MULTI_DEPTH_22_X_22_X_22_PRODUCT_QUANTITY_7'; + case MooBrokerDocumentTransferFee = 'MOO_BROKER_DOCUMENT_TRANSFER_FEE'; + case MooCallTag = 'MOO_CALL_TAG'; + case MooCustomsOvertimeFee = 'MOO_CUSTOMS_OVERTIME_FEE'; + case MooDasAlaskaComm = 'MOO_DAS_ALASKA_COMM'; + case MooDasAlaskaResi = 'MOO_DAS_ALASKA_RESI'; + case MooDasComm = 'MOO_DAS_COMM'; + case MooDasExtendedComm = 'MOO_DAS_EXTENDED_COMM'; + case MooDasExtendedResi = 'MOO_DAS_EXTENDED_RESI'; + case MooDasHawaiiComm = 'MOO_DAS_HAWAII_COMM'; + case MooDasHawaiiResi = 'MOO_DAS_HAWAII_RESI'; + case MooDasRemoteComm = 'MOO_DAS_REMOTE_COMM'; + case MooDasRemoteResi = 'MOO_DAS_REMOTE_RESI'; + case MooDasResi = 'MOO_DAS_RESI'; + case MooDateCertain = 'MOO_DATE_CERTAIN'; + case MooDeclaredValue = 'MOO_DECLARED_VALUE'; + case MooDeclaredValueCharge = 'MOO_DECLARED_VALUE_CHARGE'; + case MooDeliveryAndReturns = 'MOO_DELIVERY_AND_RETURNS'; + case MooDeliveryAreaSurcharge = 'MOO_DELIVERY_AREA_SURCHARGE'; + case MooDeliveryAreaSurchargeAlaska = 'MOO_DELIVERY_AREA_SURCHARGE_ALASKA'; + case MooDeliveryAreaSurchargeExtended = 'MOO_DELIVERY_AREA_SURCHARGE_EXTENDED'; + case MooDeliveryAreaSurchargeHawaii = 'MOO_DELIVERY_AREA_SURCHARGE_HAWAII'; + case MooElectronicEntryForFormalEntry = 'MOO_ELECTRONIC_ENTRY_FOR_FORMAL_ENTRY'; + case MooEvening = 'MOO_EVENING'; + case MooExtendedDeliveryArea = 'MOO_EXTENDED_DELIVERY_AREA'; + case MooFoodAndDrugAdministrationClearance = 'MOO_FOOD_AND_DRUG_ADMINISTRATION_CLEARANCE'; + case MooFragileLarge20X20X12ProductQuantity2 = 'MOO_FRAGILE_LARGE_20_X_20_X_12_PRODUCT_QUANTITY_2'; + case MooFragileLarge23X17X12ProductQuantity1 = 'MOO_FRAGILE_LARGE_23_X_17_X_12_PRODUCT_QUANTITY_1'; + case MooFreeTradeZone = 'MOO_FREE_TRADE_ZONE'; + case MooFuelSurcharge = 'MOO_FUEL_SURCHARGE'; + case MooHandlingFee = 'MOO_HANDLING_FEE'; + case MooHoldForPickup = 'MOO_HOLD_FOR_PICKUP'; + case MooImportPermitsAndLicensesFee = 'MOO_IMPORT_PERMITS_AND_LICENSES_FEE'; + case MooPeakAhsCharge = 'MOO_PEAK_AHS_CHARGE'; + case MooResidential = 'MOO_RESIDENTIAL'; + case MooOversizeCharge = 'MOO_OVERSIZE_CHARGE'; + case MooPeakOversizeSurcharge = 'MOO_PEAK_OVERSIZE_CHARGE'; + case MooAdditionalVat = 'MOO_ADDITIONAL_VAT'; + case MooGstOnDisbOrAncillaryServiceFees = 'MOO_GST_ON_DISB_OR_ANCILLARY_SERVICE_FEES'; + case MooHstOnAdvOrAncillaryServiceFees = 'MOO_HST_ON_ADV_OR_ANCILLARY_SERVICE_FEES'; + case MooHstOnDisbOrAncillaryServiceFees = 'MOO_HST_ON_DISB_OR_ANCILLARY_SERVICE_FEES'; + case MooIndiaCgst = 'MOO_INDIA_CGST'; + case MooIndiaSgst = 'MOO_INDIA_SGST'; + case MooMooAdditionalVat = 'MOO_MOO_ADDITIONAL_VAT'; + case MooMooAdditionalDuty = 'MOO_MOO_ADDITIONAL_DUTY'; + case MooEgyptVatOnFreight = 'MOO_EGYPT_VAT_ON_FREIGHT'; + case MooDutyAndTaxAmendmentFee = 'MOO_DUTY_AND_TAX_AMENDMENT_FEE'; + case MooCustomsDuty = 'MOO_CUSTOMS_DUTY'; + case MooCstAdditionalDuty = 'MOO_CST_ADDITIONAL_DUTY'; + case MooChinaVatDutyOrTax = 'MOO_CHINA_VAT_DUTY_OR_TAX'; + case MooArgentinaExportDuty = 'MOO_ARGENTINA_EXPORT_DUTY'; + case MooAustraliaGst = 'MOO_AUSTRALIA_GST'; + case MooBhFreightVat = 'MOO_BH_FREIGHT_VAT'; + case MooCanadaGst = 'MOO_CANADA_GST'; + case MooCanadaHst = 'MOO_CANADA_HST'; + case MooCanadaHstNb = 'MOO_CANADA_HST_NB'; + case MooCanadaHstOn = 'MOO_CANADA_HST_ON'; + case MooGstSingapore = 'MOO_GST_SINGAPORE'; + case MooBritishColumbiaPst = 'MOO_BRITISH_COLUMBIA_PST'; + case MooDisbursementFee = 'MOO_DISBURSEMENT_FEE'; + case MooVatOnDisbursementFee = 'MOO_VAT_ON_DISBURSEMENT_FEE'; + case MooResidentialRuralZone = 'MOO_RESIDENTIAL_RURAL_ZONE'; + case MooOriginalVat = 'MOO_ORIGINAL_VAT'; + case MooMexicoIvaFreight = 'MOO_MEXICO_IVA_FREIGHT'; + case MooOtherGovernmentAgencyFee = 'MOO_OTHER_GOVERNMENT_AGENCY_FEE'; + case MooCustodyFee = 'MOO_CUSTODY_FEE'; + case MooProcessingFee = 'MOO_PROCESSING_FEE'; + case MooStorageFee = 'MOO_STORAGE_FEE'; + case MooIndividualFormalEntry = 'MOO_INDIVIDUAL_FORMAL_ENTRY'; + case MooRebillDuty = 'MOO_REBILL_DUTY'; + case MooClearanceEntryFee = 'MOO_CLEARANCE_ENTRY_FEE'; + case MooCustomsClearanceFee = 'MOO_CUSTOMS_CLEARANCE_FEE'; + case MooRebillVAT = 'MOO_REBILL_VAT'; + case MooIdVatOnAncillaries = 'MOO_ID_VAT_ON_ANCILLARIES'; + + case MooPeakSurcharge = 'MOO_PEAK_CHARGE'; + case MooOutOfDeliveryAreaTier = 'MOO_OUT_OF_DELIVERY_AREA_TIER'; + case MooMerchandiseProcessingFee = 'MOO_MERCHANDISE_PROCESSING_FEE'; + case MooReturnOnCallSurcharge = 'MOO_RETURN_ON_CALL_SURCHARGE'; + case MooUnauthorizedOSSurcharge = 'MOO_UNAUTHORIZED_OS'; + case MooPeakUnauthCharge = 'MOO_PEAK_UNAUTH_CHARGE'; + case MooMissingAccountNumber = 'MOO_MISSING_ACCOUNT_NUMBER'; + case MooHazardousMaterial = 'MOO_HAZARDOUS_MATERIAL'; + case MooReturnPickupFee = 'MOO_RETURN_PICKUP_FEE'; + case MooPeakResiCharge = 'MOO_PEAK_RESI_CHARGE'; + case MooSalesTax = 'MOO_SALES_TAX'; + case MooOther = 'MOO_OTHER'; + + case ZooFuelSurcharge = 'CANADA_POST_FUEL_SURCHARGE'; + + case ZooGoodsAndServicesTaxSurcharge = 'CANADA_POST_GST_SURCHARGE'; + + case ZooHarmonizedSalesTaxSurcharge = 'CANADA_POST_HST_SURCHARGE'; + + case ZooProvincialSalesTaxSurcharge = 'CANADA_POST_PST_SURCHARGE'; + + case ZooPackageRedirectionSurcharge = 'CANADA_POST_REDIRECTION_SURCHARGE'; + + case ZooDeliveryConfirmationSurcharge = 'CANADA_POST_DELIVERY_CONFIRMATION'; + + case ZooSignatureOptionSurcharge = 'CANADA_POST_SIGNATURE_OPTION_SURCHARGE'; + + case ZooOnDemandPickxyzurcharge = 'CANADA_POST_ON_DEMAND_PICKUP'; + + case ZooOutOfSpecSurcharge = 'CANADA_POST_OUT_OF_SPEC_SURCHARGE'; + + case ZooAutoBillingSurcharge = 'CANADA_POST_AUTO_BILLING_SURCHARGE'; + + case ZooOversizeNotPackagedSurcharge = 'CANADA_POST_OVERSIZE_NOT_PACKAGED_SURCHARGE'; + + case EvriRelabellingSurcharge = 'EVRI_RELABELLING_SURCHARGE'; + + case EvriNetworkUndeliveredSurcharge = 'EVRI_NETWORK_UNDELIVERED_SURCHARGE'; + + case GooInvalidAddressCorrection = 'GOO_INVALID_ADDRESS_CORRECTION'; + case GooIncorrectAddressCorrection = 'GOO_INCORRECT_ADDRESS_CORRECTION'; + case GooGroupCZip = 'GOO_GROUP_CZIP'; + case GooDeliveryAreaSurcharge = 'GOO_DELIVERY_AREA_SURCHARGE'; + case GooExtendedDeliveryAreaSurcharge = 'GOO_EXTENDED_DELIVERY_AREA_SURCHARGE'; + case GooEnergySurcharge = 'GOO_ENERGY_SURCHARGE'; + case GooExtraPiece = 'GOO_EXTRA_PIECE'; + case GooResidentialCharge = 'GOO_RESIDENTIAL_CHARGE'; + case GooRelabelCharge = 'GOO_RELABEL_CHARGE'; + case GooWeekendPerPiece = 'GOO_WEEKEND_PER_PIECE'; + case GooExtraWeight = 'GOO_EXTRA_WEIGHT'; + case GooReturnBaseCharge = 'GOO_RETURN_BASE_CHARGE'; + case GooReturnExtraWeight = 'GOO_RETURN_EXTRA_WEIGHT'; + case GooPeakSurcharge = 'GOO_PEAK_SURCHARGE'; + case GooAdditionalHandling = 'GOO_ADDITIONAL_HANDLING'; + case GooVolumeRebate = 'GOO_VOLUME_REBATE'; + case GooOverMaxLimit = 'GOO_OVER_MAX_LIMIT'; + case GooRemoteDeliveryAreaSurcharge = 'GOO_REMOTE_DELIVERY_AREA_SURCHARGE'; + case GooOffHour = 'GOO_OFF_HOUR'; + case GooVolumeRebateBase = 'GOO_VOLUME_REBATE_BASE'; + case GooResidentialSignature = 'GOO_RESIDENTIAL_SIGNATURE'; + case GooAHDemandSurcharge = 'GOO_AHDEMAND_SURCHARGE'; + case GooOversizeDemandSurcharge = 'GOO_OVERSIZE_DEMAND_SURCHARGE'; + case GooAuditFee = 'GOO_AUDIT_FEE'; + case GooVolumeRebate2 = 'GOO_VOLUME_REBATE_2'; + case GooVolumeRebate3 = 'GOO_VOLUME_REBATE_3'; + case GooUnmappedSurcharge = 'GOO_UNMAPPED_SURCHARGE'; + + case LooDeliveryAreaSurcharge = 'PITNEY_BOWES_DELIVERY_AREA_SURCHARGE'; + case LooFuelSurcharge = 'PITNEY_BOWES_FUEL_SURCHARGE'; + + case FooExpressSaturdayDelivery = 'FOO_EXPRESS_SATURDAY_DELIVERY'; + case FooExpressElevatedRisk = 'FOO_EXPRESS_ELEVATED_RISK'; + case FooExpressEmergencySituation = 'FOO_EXPRESS_EMERGENCY_SITUATION'; + case FooExpressDutiesTaxesPaid = 'FOO_EXPRESS_DUTIES_TAXES_PAID'; + case FooExpressDutyTaxPaid = 'FOO_EXPRESS_DUTY_TAX_PAID'; + case FooExpressFuelSurcharge = 'FOO_EXPRESS_FUEL_SURCHARGE'; + case FooExpressShipmentValueProtection = 'FOO_EXPRESS_SHIPMENT_VALUE_PROTECTION'; + case FooExpressAddressCorrection = 'FOO_EXPRESS_ADDRESS_CORRECTION'; + case FooExpressNeutralDelivery = 'FOO_EXPRESS_NEUTRAL_DELIVERY'; + case FooExpressRemoteAreaPickup = 'FOO_EXPRESS_REMOTE_AREA_PICKUP'; + case FooExpressRemoteAreaDelivery = 'FOO_EXPRESS_REMOTE_AREA_DELIVERY'; + case FooExpressShipmentPreparation = 'FOO_EXPRESS_SHIPMENT_PREPARATION'; + case FooExpressStandardPickup = 'FOO_EXPRESS_STANDARD_PICKUP'; + case FooExpressNonStandardPickup = 'FOO_EXPRESS_NON_STANDARD_PICKUP'; + case FooExpressMonthlyPickxyzervice = 'FOO_EXPRESS_MONTHLY_PICKUP_SERVICE'; + case FooExpressResidentialAddress = 'FOO_EXPRESS_RESIDENTIAL_ADDRESS'; + case FooExpressResidentialDelivery = 'FOO_EXPRESS_RESIDENTIAL_DELIVERY'; + case FooExpressSingleClearance = 'FOO_EXPRESS_SINGLE_CLEARANCE'; + case FooExpressUnderBondGuarantee = 'FOO_EXPRESS_UNDER_BOND_GUARANTEE'; + case FooExpressFormalClearance = 'FOO_EXPRESS_FORMAL_CLEARANCE'; + case FooExpressNonRoutineEntry = 'FOO_EXPRESS_NON_ROUTINE_ENTRY'; + case FooExpressDisbursements = 'FOO_EXPRESS_DISBURSEMENTS'; + case FooExpressDutyTaxImporter = 'FOO_EXPRESS_DUTY_TAX_IMPORTER'; + case FooExpressDutyTaxProcessing = 'FOO_EXPRESS_DUTY_TAX_PROCESSING'; + case FooExpressMultilineEntry = 'FOO_EXPRESS_MULTILINE_ENTRY'; + case FooExpressOtherGovtAgcyBorderControls = 'FOO_EXPRESS_OTHER_GOVT_AGCY_BORDER_CONTROLS'; + case FooExpressPrintedInvoice = 'FOO_EXPRESS_PRINTED_INVOICE'; + case FooExpressObtainingPermitsLicenses = 'FOO_EXPRESS_OBTAINING_PERMITS_LICENSES'; + case FooExpressPermitsLicences = 'FOO_EXPRESS_PERMITS_LICENCES'; + case FooExpressBondedStorage = 'FOO_EXPRESS_BONDED_STORAGE'; + case FooExpressExportDeclaration = 'FOO_EXPRESS_EXPORT_DECLARATION'; + case FooExpressExporterValidation = 'FOO_EXPRESS_EXPORTER_VALIDATION'; + case FooExpressRestrictedDestination = 'FOO_EXPRESS_RESTRICTED_DESTINATION'; + case FooExpressAdditionalDuty = 'FOO_EXPRESS_ADDITIONAL_DUTY'; + case FooExpressImportExportTaxes = 'FOO_EXPRESS_IMPORT_EXPORT_TAXES'; + case FooExpressQuarantineInspection = 'FOO_EXPRESS_QUARANTINE_INSPECTION'; + case FooExpressMerchandiseProcessing = 'FOO_EXPRESS_MERCHANDISE_PROCESSING'; + case FooExpressMerchandiseProcess = 'FOO_EXPRESS_MERCHANDISE_PROCESS'; + case FooExpressImportPenalty = 'FOO_EXPRESS_IMPORT_PENALTY'; + case FooExpressTradeZoneProcess = 'FOO_EXPRESS_TRADE_ZONE_PROCESS'; + case FooExpressRegulatoryCharge = 'FOO_EXPRESS_REGULATORY_CHARGE'; + case FooExpressRegulatoryCharges = 'FOO_EXPRESS_REGULATORY_CHARGES'; + case FooExpressVatOnNonRevenueItem = 'FOO_EXPRESS_VAT_ON_NON_REVENUE_ITEM'; + case FooExpressExciseTax = 'FOO_EXPRESS_EXCISE_TAX'; + case FooExpressImportExportDuties = 'FOO_EXPRESS_IMPORT_EXPORT_DUTIES'; + case FooExpressOversizePieceDimension = 'FOO_EXPRESS_OVERSIZE_PIECE_DIMENSION'; + case FooExpressOversizePiece = 'FOO_EXPRESS_OVERSIZE_PIECE'; + case FooExpressNonStackablePallet = 'FOO_EXPRESS_NON_STACKABLE_PALLET'; + case FooExpressPremium900 = 'FOO_EXPRESS_PREMIUM_9_00'; + case FooExpressPremium1200 = 'FOO_EXPRESS_PREMIUM_12_00'; + case FooExpressOverweightPiece = 'FOO_EXPRESS_OVERWEIGHT_PIECE'; + case FooExpressCommercialGesture = 'FOO_EXPRESS_COMMERCIAL_GESTURE'; + + case PassportTaxes = 'PASSPORT_TAXES'; + case PassportDuties = 'PASSPORT_DUTIES'; + case PassportClearanceFee = 'PASSPORT_CLEARANCE_FEE'; + + case IooProvincialTax = 'IOO_PST'; + case IooGoodsAndServicesTax = 'IOO_GST'; + case IooHarmonizedTax = 'IOO_HST'; + case IooTaxes = 'IOO_TAXES'; + case IooDuties = 'IOO_DUTIES'; + + case FooExpressEuFuel = 'FOO_EXPRESS_EU_FUEL'; + case FooExpressEuRemoteAreaDelivery = 'FOO_EXPRESS_EU_REMOTE_AREA_DELIVERY'; + case FooExpressEuOverWeight = 'FOO_EXPRESS_EU_OVER_WEIGHT'; + +} + +enum SecondEnum: string +{ + + case Duties = 'duties'; + case ProcessingFees = 'processing_fees'; + case Taxes = 'taxes'; + + public function getLabel(): string + { + return match ($this) { + self::Duties => 'duties', + self::ProcessingFees => 'processing fees', + self::Taxes => 'taxes', + }; + } + + public static function fromFirstEnum(FirstEnum $FirstEnum): ?self + { + return match ($FirstEnum) { + FirstEnum::FooExpressExciseTax, + FirstEnum::FooExpressVatOnNonRevenueItem, + FirstEnum::FooExpressImportExportTaxes, + FirstEnum::IooTaxes, + FirstEnum::IooProvincialTax, + FirstEnum::IooHarmonizedTax, + FirstEnum::IooGoodsAndServicesTax, + FirstEnum::PassportTaxes => self::Taxes, + FirstEnum::FooExpressRegulatoryCharges, + FirstEnum::FooExpressRegulatoryCharge, + FirstEnum::FooExpressTradeZoneProcess, + FirstEnum::FooExpressImportPenalty, + FirstEnum::FooExpressMerchandiseProcess, + FirstEnum::FooExpressMerchandiseProcessing, + FirstEnum::FooExpressQuarantineInspection, + FirstEnum::FooExpressBondedStorage, + FirstEnum::FooExpressPermitsLicences, + FirstEnum::FooExpressObtainingPermitsLicenses, + FirstEnum::FooExpressPrintedInvoice, + FirstEnum::FooExpressOtherGovtAgcyBorderControls, + FirstEnum::FooExpressMultilineEntry, + FirstEnum::FooExpressDutyTaxProcessing, + FirstEnum::FooExpressDutyTaxImporter, + FirstEnum::FooExpressDisbursements, + FirstEnum::FooExpressNonRoutineEntry, + FirstEnum::FooExpressFormalClearance, + FirstEnum::FooExpressUnderBondGuarantee, + FirstEnum::FooExpressSingleClearance, + FirstEnum::FooExpressDutyTaxPaid, + FirstEnum::FooExpressDutiesTaxesPaid, + FirstEnum::PassportClearanceFee => self::ProcessingFees, + FirstEnum::FooExpressAdditionalDuty, + FirstEnum::FooExpressImportExportDuties, + FirstEnum::IooDuties, + FirstEnum::PassportDuties => self::Duties, + FirstEnum::XyzSaturdayStopDomestic, + FirstEnum::XyzSaturdayDeliveryAir, + FirstEnum::XyzAdditionalHandling, + FirstEnum::XyzCommercialDomesticAirDeliveryArea, + FirstEnum::XyzCommercialDomesticAirExtendedDeliveryArea, + FirstEnum::XyzCommercialDomesticGroundDeliveryArea, + FirstEnum::XyzCommercialDomesticGroundExtendedDeliveryArea, + FirstEnum::XyzResidentialDomesticAirDeliveryArea, + FirstEnum::XyzResidentialDomesticAirExtendedDeliveryArea, + FirstEnum::XyzResidentialDomesticGroundDeliveryArea, + FirstEnum::XyzResidentialDomesticGroundExtendedDeliveryArea, + FirstEnum::XyzDeliveryAreaSurchargeSurePost, + FirstEnum::XyzDeliveryAreaSurchargeSurePostExtended, + FirstEnum::XyzDeliveryAreaSurchargeOther, + FirstEnum::XyzResidentialSurchargeAir, + FirstEnum::XyzResidentialSurchargeGround, + FirstEnum::XyzSurchargeCodeFuel, + FirstEnum::XyzCod, + FirstEnum::XyzDeliveryConfirmation, + FirstEnum::XyzShipDeliveryConfirmation, + FirstEnum::XyzExtendedArea, + FirstEnum::XyzHazMat, + FirstEnum::XyzDryIce, + FirstEnum::XyzIscSeeds, + FirstEnum::XyzIscPerishables, + FirstEnum::XyzIscTobacco, + FirstEnum::XyzIscPlants, + FirstEnum::XyzIscAlcoholicBeverages, + FirstEnum::XyzIscBiologicalSubstances, + FirstEnum::XyzIscSpecialExceptions, + FirstEnum::XyzHoldForPickup, + FirstEnum::XyzOriginCertificate, + FirstEnum::XyzPrintReturnLabel, + FirstEnum::XyzExportLicenseVerification, + FirstEnum::XyzPrintNMail, + FirstEnum::XyzReturnService1attempt, + FirstEnum::XyzReturnService3attempt, + FirstEnum::XyzSaturdayInternationalProcessingFee, + FirstEnum::XyzElectronicReturnLabel, + FirstEnum::XyzPreparedSedForm, + FirstEnum::XyzLargePackage, + FirstEnum::XyzShipperPaysDutyTax, + FirstEnum::XyzShipperPaysDutyTaxUnpaid, + FirstEnum::XyzExpressPlusSurcharge, + FirstEnum::XyzInsurance, + FirstEnum::XyzShipAdditionalHandling, + FirstEnum::XyzShipperRelease, + FirstEnum::XyzCheckToShipper, + FirstEnum::XyzProactiveResponse, + FirstEnum::XyzGermanPickup, + FirstEnum::XyzGermanRoadTax, + FirstEnum::XyzExtendedAreaPickup, + FirstEnum::XyzReturnOfDocument, + FirstEnum::XyzPeakSeason, + FirstEnum::XyzLargePackageSeasonalSurcharge, + FirstEnum::XyzAdditionalHandlingSeasonalSurchargeDiscontinued, + FirstEnum::XyzShipLargePackage, + FirstEnum::XyzCarbonNeutral, + FirstEnum::XyzImportControl, + FirstEnum::XyzCommercialInvoiceRemoval, + FirstEnum::XyzImportControlElectronicLabel, + FirstEnum::XyzImportControlPrintLabel, + FirstEnum::XyzImportControlPrintAndMailLabel, + FirstEnum::XyzImportControlOnePickupAttemptLabel, + FirstEnum::XyzImportControlThreePickUpAttemptLabel, + FirstEnum::XyzRefrigeration, + FirstEnum::XyzExchangePrintReturnLabel, + FirstEnum::XyzCommittedDeliveryWindow, + FirstEnum::XyzSecuritySurcharge, + FirstEnum::XyzNonMachinableCharge, + FirstEnum::XyzCustomerTransactionFee, + FirstEnum::XyzSurePostNonStandardLength, + FirstEnum::XyzSurePostNonStandardExtraLength, + FirstEnum::XyzSurePostNonStandardCube, + FirstEnum::XyzShipmentCod, + FirstEnum::XyzLiftGateForPickup, + FirstEnum::XyzLiftGateForDelivery, + FirstEnum::XyzDropOffAtXyzFacility, + FirstEnum::XyzPremiumCare, + FirstEnum::XyzOversizePallet, + FirstEnum::XyzFreightDeliverySurcharge, + FirstEnum::XyzFreightPickxyzurcharge, + FirstEnum::XyzDirectToRetail, + FirstEnum::XyzDirectDeliveryOnly, + FirstEnum::XyzNoAccessPoint, + FirstEnum::XyzDeliverToAddresseeOnly, + FirstEnum::XyzDirectToRetailCod, + FirstEnum::XyzRetailAccessPoint, + FirstEnum::XyzElectronicPackageReleaseAuthentication, + FirstEnum::XyzPayAtStore, + FirstEnum::XyzInsideDelivery, + FirstEnum::XyzItemDisposal, + FirstEnum::XyzAddressCorrections, + FirstEnum::XyzNotPreviouslyBilledFee, + FirstEnum::XyzPickxyzurcharge, + FirstEnum::XyzChargeback, + FirstEnum::XyzAdditionalHandlingPeakDemand, + FirstEnum::XyzOtherSurcharge, + FirstEnum::XyzRemoteAreaSurcharge, + FirstEnum::XyzRemoteAreaOtherSurcharge, + FirstEnum::CompanyEconomyResidentialSurchargeLightweight, + FirstEnum::CompanyEconomyDeliverySurchargeLightweight, + FirstEnum::CompanyEconomyExtendedDeliverySurchargeLightweight, + FirstEnum::CompanyStandardResidentialSurchargeLightweight, + FirstEnum::CompanyStandardDeliverySurchargeLightweight, + FirstEnum::CompanyStandardExtendedDeliverySurchargeLightweight, + FirstEnum::CompanyEconomyResidentialSurchargePlus, + FirstEnum::CompanyEconomyDeliverySurchargePlus, + FirstEnum::CompanyEconomyExtendedDeliverySurchargePlus, + FirstEnum::CompanyEconomyPeakSurchargeLightweight, + FirstEnum::CompanyEconomyPeakSurchargePlus, + FirstEnum::CompanyEconomyPeakSurchargeOver5Lbs, + FirstEnum::CompanyStandardResidentialSurchargePlus, + FirstEnum::CompanyStandardDeliverySurchargePlus, + FirstEnum::CompanyStandardExtendedDeliverySurchargePlus, + FirstEnum::CompanyStandardPeakSurchargeLightweight, + FirstEnum::CompanyStandardPeakSurchargePlus, + FirstEnum::CompanyStandardPeakSurchargeOver5Lbs, + FirstEnum::Company2DayResidentialSurcharge, + FirstEnum::Company2DayDeliverySurcharge, + FirstEnum::Company2DayExtendedDeliverySurcharge, + FirstEnum::Company2DayPeakSurcharge, + FirstEnum::CompanyHazmatResidentialSurcharge, + FirstEnum::CompanyHazmatResidentialSurchargeLightweight, + FirstEnum::CompanyHazmatDeliverySurcharge, + FirstEnum::CompanyHazmatDeliverySurchargeLightweight, + FirstEnum::CompanyHazmatExtendedDeliverySurcharge, + FirstEnum::CompanyHazmatExtendedDeliverySurchargeLightweight, + FirstEnum::CompanyHazmatPeakSurchargeLightweight, + FirstEnum::CompanyHazmatPeakSurcharge, + FirstEnum::CompanyHazmatPeakSurchargePlus, + FirstEnum::CompanyHazmatPeakSurchargeOver5Lbs, + FirstEnum::CompanyFuelSurcharge, + FirstEnum::Company2DayAdditionalHandlingSurchargeDimensions, + FirstEnum::Company2DayAdditionalHandlingSurchargeWeight, + FirstEnum::CompanyStandardAdditionalHandlingSurchargeDimensions, + FirstEnum::CompanyStandardAdditionalHandlingSurchargeWeight, + FirstEnum::CompanyEconomyAdditionalHandlingSurchargeDimensions, + FirstEnum::CompanyEconomyAdditionalHandlingSurchargeWeight, + FirstEnum::CompanyHazmatAdditionalHandlingSurchargeDimensions, + FirstEnum::CompanyHazmatAdditionalHandlingSurchargeWeight, + FirstEnum::Company2DayLargePackageSurcharge, + FirstEnum::CompanyStandardLargePackageSurcharge, + FirstEnum::CompanyEconomyLargePackageSurcharge, + FirstEnum::CompanyHazmatLargePackageSurcharge, + FirstEnum::CompanyLargePackagePeakSurcharge, + FirstEnum::CompanyUkSignatureSurcharge, + FirstEnum::AciFuelSurcharge, + FirstEnum::AciUnmanifestedSurcharge, + FirstEnum::AciPeakSurcharge, + FirstEnum::AciOversizeSurcharge, + FirstEnum::AciUltraUrbanSurcharge, + FirstEnum::AciNonStandardSurcharge, + FirstEnum::XyzmiFuelSurcharge, + FirstEnum::XyzmiNonStandardSurcharge, + FirstEnum::FooEcommerceFuelSurcharge, + FirstEnum::FooEcommercePeakSurcharge, + FirstEnum::FooEcommerceOversizeSurcharge, + FirstEnum::FooEcommerceFutureUseSurcharge, + FirstEnum::FooEcommerceDimLengthSurcharge, + FirstEnum::HooFuelSurcharge, + FirstEnum::HooResidentialSurcharge, + FirstEnum::HooDeliverySurcharge, + FirstEnum::HooExtendedDeliverySurcharge, + FirstEnum::MooSmartPostFuelSurcharge, + FirstEnum::MooSmartPostDeliverySurcharge, + FirstEnum::MooSmartPostExtendedDeliverySurcharge, + FirstEnum::MooSmartPostNonMachinableSurcharge, + FirstEnum::MooAdditionalHandlingDomesticDimensionSurcharge, + FirstEnum::MooOversizeSurcharge, + FirstEnum::MooAdditionalHandling, + FirstEnum::MooAdditionalHandlingChargeDimensions, + FirstEnum::MooAdditionalHandlingChargePackage, + FirstEnum::MooAdditionalHandlingChargeWeight, + FirstEnum::MooAdditionalWeightCharge, + FirstEnum::MooAdvancementFee, + FirstEnum::MooAhsDimensions, + FirstEnum::MooAhsWeight, + FirstEnum::MooAlaskaOrHawaiiOrPuertoRicoPkupOrDel, + FirstEnum::MooAppointment, + FirstEnum::MooBox24X24X18DblWalledProductQuantity2, + FirstEnum::MooBox28X28X28DblWalledProductQuantity3, + FirstEnum::MooBoxMultiDepth22X22X22ProductQuantity7, + FirstEnum::MooBrokerDocumentTransferFee, + FirstEnum::MooCallTag, + FirstEnum::MooCustomsOvertimeFee, + FirstEnum::MooDasAlaskaComm, + FirstEnum::MooDasAlaskaResi, + FirstEnum::MooDasComm, + FirstEnum::MooDasExtendedComm, + FirstEnum::MooDasExtendedResi, + FirstEnum::MooDasHawaiiComm, + FirstEnum::MooDasHawaiiResi, + FirstEnum::MooDasRemoteComm, + FirstEnum::MooDasRemoteResi, + FirstEnum::MooDasResi, + FirstEnum::MooDateCertain, + FirstEnum::MooDeclaredValue, + FirstEnum::MooDeclaredValueCharge, + FirstEnum::MooDeliveryAndReturns, + FirstEnum::MooDeliveryAreaSurcharge, + FirstEnum::MooDeliveryAreaSurchargeAlaska, + FirstEnum::MooDeliveryAreaSurchargeExtended, + FirstEnum::MooDeliveryAreaSurchargeHawaii, + FirstEnum::MooElectronicEntryForFormalEntry, + FirstEnum::MooEvening, + FirstEnum::MooExtendedDeliveryArea, + FirstEnum::MooFoodAndDrugAdministrationClearance, + FirstEnum::MooFragileLarge20X20X12ProductQuantity2, + FirstEnum::MooFragileLarge23X17X12ProductQuantity1, + FirstEnum::MooFreeTradeZone, + FirstEnum::MooFuelSurcharge, + FirstEnum::MooHandlingFee, + FirstEnum::MooHoldForPickup, + FirstEnum::MooImportPermitsAndLicensesFee, + FirstEnum::MooPeakAhsCharge, + FirstEnum::MooResidential, + FirstEnum::MooOversizeCharge, + FirstEnum::MooPeakOversizeSurcharge, + FirstEnum::MooAdditionalVat, + FirstEnum::MooGstOnDisbOrAncillaryServiceFees, + FirstEnum::MooHstOnAdvOrAncillaryServiceFees, + FirstEnum::MooHstOnDisbOrAncillaryServiceFees, + FirstEnum::MooIndiaCgst, + FirstEnum::MooIndiaSgst, + FirstEnum::MooMooAdditionalVat, + FirstEnum::MooMooAdditionalDuty, + FirstEnum::MooEgyptVatOnFreight, + FirstEnum::MooDutyAndTaxAmendmentFee, + FirstEnum::MooCustomsDuty, + FirstEnum::MooCstAdditionalDuty, + FirstEnum::MooChinaVatDutyOrTax, + FirstEnum::MooArgentinaExportDuty, + FirstEnum::MooAustraliaGst, + FirstEnum::MooBhFreightVat, + FirstEnum::MooCanadaGst, + FirstEnum::MooCanadaHst, + FirstEnum::MooCanadaHstNb, + FirstEnum::MooCanadaHstOn, + FirstEnum::MooGstSingapore, + FirstEnum::MooBritishColumbiaPst, + FirstEnum::MooDisbursementFee, + FirstEnum::MooVatOnDisbursementFee, + FirstEnum::MooResidentialRuralZone, + FirstEnum::MooOriginalVat, + FirstEnum::MooMexicoIvaFreight, + FirstEnum::MooOtherGovernmentAgencyFee, + FirstEnum::MooCustodyFee, + FirstEnum::MooProcessingFee, + FirstEnum::MooStorageFee, + FirstEnum::MooIndividualFormalEntry, + FirstEnum::MooRebillDuty, + FirstEnum::MooClearanceEntryFee, + FirstEnum::MooCustomsClearanceFee, + FirstEnum::MooRebillVAT, + FirstEnum::MooIdVatOnAncillaries, + FirstEnum::MooPeakSurcharge, + FirstEnum::MooOutOfDeliveryAreaTier, + FirstEnum::MooMerchandiseProcessingFee, + FirstEnum::MooReturnOnCallSurcharge, + FirstEnum::MooUnauthorizedOSSurcharge, + FirstEnum::MooPeakUnauthCharge, + FirstEnum::MooMissingAccountNumber, + FirstEnum::MooHazardousMaterial, + FirstEnum::MooReturnPickupFee, + FirstEnum::MooPeakResiCharge, + FirstEnum::MooSalesTax, + FirstEnum::MooOther, + FirstEnum::ZooFuelSurcharge, + FirstEnum::ZooGoodsAndServicesTaxSurcharge, + FirstEnum::ZooHarmonizedSalesTaxSurcharge, + FirstEnum::ZooProvincialSalesTaxSurcharge, + FirstEnum::ZooPackageRedirectionSurcharge, + FirstEnum::ZooDeliveryConfirmationSurcharge, + FirstEnum::ZooSignatureOptionSurcharge, + FirstEnum::ZooOnDemandPickxyzurcharge, + FirstEnum::ZooOutOfSpecSurcharge, + FirstEnum::ZooAutoBillingSurcharge, + FirstEnum::ZooOversizeNotPackagedSurcharge, + FirstEnum::EvriRelabellingSurcharge, + FirstEnum::EvriNetworkUndeliveredSurcharge, + FirstEnum::GooInvalidAddressCorrection, + FirstEnum::GooIncorrectAddressCorrection, + FirstEnum::GooGroupCZip, + FirstEnum::GooDeliveryAreaSurcharge, + FirstEnum::GooExtendedDeliveryAreaSurcharge, + FirstEnum::GooEnergySurcharge, + FirstEnum::GooExtraPiece, + FirstEnum::GooResidentialCharge, + FirstEnum::GooRelabelCharge, + FirstEnum::GooWeekendPerPiece, + FirstEnum::GooExtraWeight, + FirstEnum::GooReturnBaseCharge, + FirstEnum::GooReturnExtraWeight, + FirstEnum::GooPeakSurcharge, + FirstEnum::GooAdditionalHandling, + FirstEnum::GooVolumeRebate, + FirstEnum::GooOverMaxLimit, + FirstEnum::GooRemoteDeliveryAreaSurcharge, + FirstEnum::GooOffHour, + FirstEnum::GooVolumeRebateBase, + FirstEnum::GooResidentialSignature, + FirstEnum::GooAHDemandSurcharge, + FirstEnum::GooOversizeDemandSurcharge, + FirstEnum::GooAuditFee, + FirstEnum::GooVolumeRebate2, + FirstEnum::GooVolumeRebate3, + FirstEnum::GooUnmappedSurcharge, + FirstEnum::LooDeliveryAreaSurcharge, + FirstEnum::LooFuelSurcharge, + FirstEnum::FooExpressSaturdayDelivery, + FirstEnum::FooExpressElevatedRisk, + FirstEnum::FooExpressEmergencySituation, + FirstEnum::FooExpressFuelSurcharge, + FirstEnum::FooExpressShipmentValueProtection, + FirstEnum::FooExpressAddressCorrection, + FirstEnum::FooExpressNeutralDelivery, + FirstEnum::FooExpressRemoteAreaPickup, + FirstEnum::FooExpressRemoteAreaDelivery, + FirstEnum::FooExpressShipmentPreparation, + FirstEnum::FooExpressStandardPickup, + FirstEnum::FooExpressNonStandardPickup, + FirstEnum::FooExpressMonthlyPickxyzervice, + FirstEnum::FooExpressResidentialAddress, + FirstEnum::FooExpressResidentialDelivery, + FirstEnum::FooExpressExportDeclaration, + FirstEnum::FooExpressExporterValidation, + FirstEnum::FooExpressRestrictedDestination, + FirstEnum::FooExpressOversizePieceDimension, + FirstEnum::FooExpressOversizePiece, + FirstEnum::FooExpressNonStackablePallet, + FirstEnum::FooExpressPremium900, + FirstEnum::FooExpressPremium1200, + FirstEnum::FooExpressOverweightPiece, + FirstEnum::FooExpressEuFuel, + FirstEnum::FooExpressEuOverWeight, + FirstEnum::FooExpressEuRemoteAreaDelivery, + FirstEnum::FooExpressCommercialGesture => null, + }; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-11283.php b/tests/PHPStan/Analyser/data/bug-11283.php new file mode 100644 index 0000000000..f0c4b3fc17 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11283.php @@ -0,0 +1,168 @@ +|TFulfilled)) $onFulfilled + * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected + * @return PromiseInterface<($onRejected is null ? ($onFulfilled is null ? T : TFulfilled) : ($onFulfilled is null ? T|TRejected : TFulfilled|TRejected))> + */ + public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface; + + /** + * @template TThrowable of \Throwable + * @template TRejected + * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected + * @return PromiseInterface + */ + public function catch(callable $onRejected): PromiseInterface; + + /** + * @param callable(): (void|PromiseInterface) $onFulfilledOrRejected + * @return PromiseInterface + */ + public function finally(callable $onFulfilledOrRejected): PromiseInterface; +} + +/** + * @template T + * @param PromiseInterface|T $promiseOrValue + * @return PromiseInterface + */ +function resolve($promiseOrValue): PromiseInterface +{ + return returnMixed(); +} + +class Demonstration +{ + public function parseMessage(): void + { + $params = []; + $packet = []; + $promise = resolve(null); + + $promise->then(function () use (&$packet, &$params) { + if (mt_rand(0, 1)) { + resolve(null)->then( + function () use ($packet, &$params) { + $packet['payload']['type'] = 0; + $this->groupNotify( + $packet, + function () use ($packet, &$params) { + $this->save($packet, $params)->then(function () use ($packet, &$params) { + if ($params['links']) { + $this->handle($params)->then(function ($result) use ($packet) { + if ($result) { + $packet['payload']['preview'] = $result; + } + }); + } + }); + } + ); + } + ); + } else { + $this->call(function () use (&$params) { + $packet['target'] = []; + + $this->asyncAction()->then(function ($value) use (&$packet, &$params) { + if (!$value) { + $packet['payload']['type'] = 0; + $packet['payload']['message'] = ''; + $this->selfNotify($packet); + return; + } + + $packet['payload']['type'] = 0; + $this->groupNotify( + $packet, + function () use ($packet, &$params) { + $this->save($packet, $params)->then(function () use ($packet, &$params) { + if ($params) { + $this->handle($params)->then(function ($result) use ($packet) { + if ($result) { + $packet['payload']['preview'] = $result; + $this->selfNotify($packet); + } + }); + } + }); + } + ); + }); + }); + } + }); + } + + /** + * @return PromiseInterface + */ + private function handle(mixed $params): PromiseInterface + { + return resolve(null); + } + + /** + * @param array $packet + * @param array $params + * @return PromiseInterface + */ + private function save(array $packet, array &$params): PromiseInterface + { + return resolve(0); + } + + /** + * @param array $packet + * @param (callable():void) $callback + * @return bool + */ + public function groupNotify(array $packet, callable $callback): bool + { + return true; + } + + /** + * @param array $packet + * @return bool + */ + public function selfNotify(array $packet): bool + { + return true; + } + + /** + * @return PromiseInterface + */ + private function asyncAction(): PromiseInterface + { + return resolve(''); + } + + /** + * @param callable():void $callback + */ + private function call(callable $callback): void + { + } +} + diff --git a/tests/PHPStan/Analyser/data/bug-11292.php b/tests/PHPStan/Analyser/data/bug-11292.php new file mode 100644 index 0000000000..7d317e9e6c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11292.php @@ -0,0 +1,13 @@ +[\\x7f-\\xff]{1,64})(:[^]\\\\\\x00-\\x20\\"(),:-<>[\\x7f-\\xff]{1,64})?@)?((?:[-a-zA-Z0-9\\x7f-\\xff]{1,63}\\.)+[a-zA-Z\\x7f-\\xff][-a-zA-Z0-9\\x7f-\\xff]{1,62})((:[0-9]{1,5})?(/[!$-/0-9:;=@_~\':;!a-zA-Z\\x7f-\\xff]*?)?(\\?[!$-/0-9:;=@_\':;!a-zA-Z\\x7f-\\xff]+?)?(#[!$-/0-9?:;=@_\':;!a-zA-Z\\x7f-\\xff]+?)?)(?=[)\'?.!,;:]*(' . $nonUrl . '|$))}'; + if (preg_match($pattern, $s, $matches, PREG_OFFSET_CAPTURE, 0)) { + assertType('array}>', $matches); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-11297.php b/tests/PHPStan/Analyser/data/bug-11297.php new file mode 100644 index 0000000000..5bc767581c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11297.php @@ -0,0 +1,251 @@ += 8.1 + +namespace Bug11297; + +class ClassA { + /** @param array $array */ + public static function doSomething(string $string, array $array): void + { + } +} + +enum Icon: string +{ + case CASE1 = 'case1'; + case CASE2 = 'case2'; + case CASE3 = 'case3'; + case CASE4 = 'case4'; + case CASE5 = 'case5'; + case CASE6 = 'case6'; + case CASE7 = 'case7'; + case CASE8 = 'case8'; + case CASE9 = 'case9'; + case CASE10 = 'case10'; + case CASE11 = 'case11'; + case CASE12 = 'case12'; + case CASE13 = 'case13'; + case CASE14 = 'case14'; + case CASE15 = 'case15'; + case CASE16 = 'case16'; + case CASE17 = 'case17'; + case CASE18 = 'case18'; + case CASE19 = 'case19'; + case CASE20 = 'case20'; + case CASE21 = 'case21'; + case CASE22 = 'case22'; + case CASE23 = 'case23'; + case CASE24 = 'case24'; + case CASE25 = 'case25'; + case CASE26 = 'case26'; + case CASE27 = 'case27'; + case CASE28 = 'case28'; + case CASE29 = 'case29'; + case CASE30 = 'case30'; + case CASE31 = 'case31'; + case CASE32 = 'case32'; + case CASE33 = 'case33'; + case CASE34 = 'case34'; + case CASE35 = 'case35'; + case CASE36 = 'case36'; + case CASE37 = 'case37'; + case CASE38 = 'case38'; + case CASE39 = 'case39'; + case CASE40 = 'case40'; + case CASE41 = 'case41'; + case CASE42 = 'case42'; + case CASE43 = 'case43'; + case CASE44 = 'case44'; + case CASE45 = 'case45'; + case CASE46 = 'case46'; + case CASE47 = 'case47'; + case CASE48 = 'case48'; + case CASE49 = 'case49'; + case CASE50 = 'case50'; + case CASE51 = 'case51'; + case CASE52 = 'case52'; + case CASE53 = 'case53'; + case CASE54 = 'case54'; + case CASE55 = 'case55'; + case CASE56 = 'case56'; + case CASE57 = 'case57'; + case CASE58 = 'case58'; + case CASE59 = 'case59'; + case CASE60 = 'case60'; + case CASE61 = 'case61'; + case CASE62 = 'case62'; + case CASE63 = 'case63'; + case CASE64 = 'case64'; + case CASE65 = 'case65'; + case CASE66 = 'case66'; + case CASE67 = 'case67'; + case CASE68 = 'case68'; + case CASE69 = 'case69'; + case CASE70 = 'case70'; + case CASE71 = 'case71'; + case CASE72 = 'case72'; + case CASE73 = 'case73'; + case CASE74 = 'case74'; + case CASE75 = 'case75'; + case CASE76 = 'case76'; + case CASE77 = 'case77'; + case CASE78 = 'case78'; + case CASE79 = 'case79'; + case CASE80 = 'case80'; + case CASE81 = 'case81'; + case CASE82 = 'case82'; + case CASE83 = 'case83'; + case CASE84 = 'case84'; + case CASE85 = 'case85'; + case CASE86 = 'case86'; + case CASE87 = 'case87'; + case CASE88 = 'case88'; + case CASE89 = 'case89'; + case CASE90 = 'case90'; + case CASE91 = 'case91'; + case CASE92 = 'case92'; + case CASE93 = 'case93'; + case CASE94 = 'case94'; + case CASE95 = 'case95'; + case CASE96 = 'case96'; + case CASE97 = 'case97'; + case CASE98 = 'case98'; + case CASE99 = 'case99'; + case CASE100 = 'case100'; + case CASE101 = 'case101'; + case CASE102 = 'case102'; + case CASE103 = 'case103'; + case CASE104 = 'case104'; + case CASE105 = 'case105'; + case CASE106 = 'case106'; + case CASE107 = 'case107'; + case CASE108 = 'case108'; + case CASE109 = 'case109'; + case CASE110 = 'case110'; + case CASE111 = 'case111'; + case CASE112 = 'case112'; + case CASE113 = 'case113'; + case CASE114 = 'case114'; + case CASE115 = 'case115'; + case CASE116 = 'case116'; + case CASE117 = 'case117'; + case CASE118 = 'case118'; + case CASE119 = 'case119'; + case CASE120 = 'case120'; + case CASE121 = 'case121'; + case CASE122 = 'case122'; + case CASE123 = 'case123'; + case CASE124 = 'case124'; + case CASE125 = 'case125'; + case CASE126 = 'case126'; + case CASE127 = 'case127'; + case CASE128 = 'case128'; + case CASE129 = 'case129'; + case CASE130 = 'case130'; + case CASE131 = 'case131'; + case CASE132 = 'case132'; + case CASE133 = 'case133'; + case CASE134 = 'case134'; + case CASE135 = 'case135'; + case CASE136 = 'case136'; + case CASE137 = 'case137'; + case CASE138 = 'case138'; + case CASE139 = 'case139'; + case CASE140 = 'case140'; + case CASE141 = 'case141'; + case CASE142 = 'case142'; + case CASE143 = 'case143'; + case CASE144 = 'case144'; + case CASE145 = 'case145'; + case CASE146 = 'case146'; + case CASE147 = 'case147'; + case CASE148 = 'case148'; + case CASE149 = 'case149'; + case CASE150 = 'case150'; + case CASE151 = 'case151'; + case CASE152 = 'case152'; + case CASE153 = 'case153'; + case CASE154 = 'case154'; + case CASE155 = 'case155'; + case CASE156 = 'case156'; + case CASE157 = 'case157'; + case CASE158 = 'case158'; + case CASE159 = 'case159'; + case CASE160 = 'case160'; + case CASE161 = 'case161'; + case CASE162 = 'case162'; + case CASE163 = 'case163'; + case CASE164 = 'case164'; + case CASE165 = 'case165'; + case CASE166 = 'case166'; + case CASE167 = 'case167'; + case CASE168 = 'case168'; + case CASE169 = 'case169'; + case CASE170 = 'case170'; + case CASE171 = 'case171'; + case CASE172 = 'case172'; + case CASE173 = 'case173'; + case CASE174 = 'case174'; + case CASE175 = 'case175'; + case CASE176 = 'case176'; + case CASE177 = 'case177'; + case CASE178 = 'case178'; + case CASE179 = 'case179'; + case CASE180 = 'case180'; + case CASE181 = 'case181'; + case CASE182 = 'case182'; + case CASE183 = 'case183'; + case CASE184 = 'case184'; + case CASE185 = 'case185'; + case CASE186 = 'case186'; + case CASE187 = 'case187'; + case CASE188 = 'case188'; + case CASE189 = 'case189'; + case CASE190 = 'case190'; + case CASE191 = 'case191'; + case CASE192 = 'case192'; + case CASE193 = 'case193'; + case CASE194 = 'case194'; + case CASE195 = 'case195'; + case CASE196 = 'case196'; + case CASE197 = 'case197'; + case CASE198 = 'case198'; + case CASE199 = 'case199'; + case CASE200 = 'case200'; + + public function getFileIdentifier(): string + { + return match ($this) { + default => $this->value + }; + } + + public function getBackendLabelIdentifier(): string + { + return 'foo:' . $this->getLocallangIdentifier(); + } + + private function getLocallangIdentifier(): string + { + return 'foo.icon.' . $this->value; + } +} + +(static function (string $table): void { + /** + * Add TCA for top bar field in pages. + */ + ClassA::doSomething($table, [ + 'foo' => [ + 'config' => [ + 'items' => [['', ''], ...array_map( + static fn (Icon $icon): array => [ + $icon->getBackendLabelIdentifier(), + $icon->value, + 'foo/' . $icon->getFileIdentifier() . '.svg', + ], + Icon::cases(), + )], + ], + ], + ]); +})('foo'); diff --git a/tests/PHPStan/Analyser/data/bug-11511.php b/tests/PHPStan/Analyser/data/bug-11511.php new file mode 100644 index 0000000000..7af5066cc1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11511.php @@ -0,0 +1,10 @@ += 8.0 + +namespace Bug11511; + +$myObject = new class (new class { public string $bar = 'test'; }) { + public function __construct(public object $foo) + { + } +}; +echo $myObject->foo->bar; diff --git a/tests/PHPStan/Analyser/data/bug-11640.php b/tests/PHPStan/Analyser/data/bug-11640.php new file mode 100644 index 0000000000..62b1748d2e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11640.php @@ -0,0 +1,9 @@ +(?:\\\[A-Za-z])+)|[' . self::DATE_FORMAT_CHARACTERS . ']|(?P[A-Za-z])/'; + + /** + * Formats a DateTime object using the current translation for weekdays and months + * @param mixed $translation + */ + public static function formatDateTime(DateTime $dateTime, string $format, ?string $language , $translation): ?string + { + return preg_replace_callback( + self::DATE_FORMAT_REGEX, + fn(array $matches): string => match ($matches[0]) { + 'M' => $translation->getStrings('date.months.short')[$dateTime->format('n') - 1], + 'F' => $translation->getStrings('date.months.long')[$dateTime->format('n') - 1], + 'D' => $translation->getStrings('date.weekdays.short')[(int) $dateTime->format('w')], + 'l' => $translation->getStrings('date.weekdays.long')[(int) $dateTime->format('w')], + 'r' => static::formatDateTime($dateTime, DateTime::RFC2822, null, $translation), + default => $dateTime->format($matches[1] ?? $matches[0]) + }, + $format + ); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-11709.php b/tests/PHPStan/Analyser/data/bug-11709.php new file mode 100644 index 0000000000..2515e3dcb8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11709.php @@ -0,0 +1,28 @@ + : mixed) $value + * @phpstan-assert array $value + */ +function isArrayWithStringKeys(mixed $value): void +{ + if (!is_array($value)) { + throw new \Exception('Not an array'); + } + + foreach (array_keys($value) as $key) { + if (!is_string($key)) { + throw new \Exception('Non-string key'); + } + } +} + +function ($m): void { + isArrayWithStringKeys($m); + assertType('array', $m); +}; diff --git a/tests/PHPStan/Analyser/data/bug-11913.php b/tests/PHPStan/Analyser/data/bug-11913.php new file mode 100644 index 0000000000..865023c725 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11913.php @@ -0,0 +1,274 @@ + "AF", "name" => "Afghanistan", "d_code" => "0093"); + $countries[] = array("code" => "AL", "name" => "Albania", "d_code" => "00355"); + $countries[] = array("code" => "DZ", "name" => "Algeria", "d_code" => "00213"); + $countries[] = array("code" => "AS", "name" => "American Samoa", "d_code" => "001"); + $countries[] = array("code" => "AD", "name" => "Andorra", "d_code" => "00376"); + $countries[] = array("code" => "AO", "name" => "Angola", "d_code" => "00244"); + $countries[] = array("code" => "AI", "name" => "Anguilla", "d_code" => "001"); + $countries[] = array("code" => "AG", "name" => "Antigua", "d_code" => "001"); + $countries[] = array("code" => "AR", "name" => "Argentina", "d_code" => "0054"); + $countries[] = array("code" => "AM", "name" => "Armenia", "d_code" => "00374"); + $countries[] = array("code" => "AW", "name" => "Aruba", "d_code" => "00297"); + $countries[] = array("code" => "AU", "name" => "Australia", "d_code" => "0061"); + $countries[] = array("code" => "AT", "name" => "Austria", "d_code" => "0043"); + $countries[] = array("code" => "AZ", "name" => "Azerbaijan", "d_code" => "00994"); + $countries[] = array("code" => "BH", "name" => "Bahrain", "d_code" => "00973"); + $countries[] = array("code" => "BD", "name" => "Bangladesh", "d_code" => "00880"); + $countries[] = array("code" => "BB", "name" => "Barbados", "d_code" => "001"); + $countries[] = array("code" => "BY", "name" => "Belarus", "d_code" => "00375"); + $countries[] = array("code" => "BE", "name" => "Belgium", "d_code" => "0032"); + $countries[] = array("code" => "BZ", "name" => "Belize", "d_code" => "00501"); + $countries[] = array("code" => "BJ", "name" => "Benin", "d_code" => "00229"); + $countries[] = array("code" => "BM", "name" => "Bermuda", "d_code" => "001"); + $countries[] = array("code" => "BT", "name" => "Bhutan", "d_code" => "00975"); + $countries[] = array("code" => "BO", "name" => "Bolivia", "d_code" => "00591"); + $countries[] = array("code" => "BA", "name" => "Bosnia and Herzegovina", "d_code" => "00387"); + $countries[] = array("code" => "BW", "name" => "Botswana", "d_code" => "00267"); + $countries[] = array("code" => "BR", "name" => "Brazil", "d_code" => "0055"); + $countries[] = array("code" => "IO", "name" => "British Indian Ocean Territory", "d_code" => "00246"); + $countries[] = array("code" => "VG", "name" => "British Virgin Islands", "d_code" => "001"); + $countries[] = array("code" => "BN", "name" => "Brunei", "d_code" => "00673"); + $countries[] = array("code" => "BG", "name" => "Bulgaria", "d_code" => "00359"); + $countries[] = array("code" => "BF", "name" => "Burkina Faso", "d_code" => "00226"); + $countries[] = array("code" => "MM", "name" => "Burma Myanmar", "d_code" => "0095"); + $countries[] = array("code" => "BI", "name" => "Burundi", "d_code" => "00257"); + $countries[] = array("code" => "KH", "name" => "Cambodia", "d_code" => "00855"); + $countries[] = array("code" => "CM", "name" => "Cameroon", "d_code" => "00237"); + $countries[] = array("code" => "CA", "name" => "Canada", "d_code" => "001"); + $countries[] = array("code" => "CV", "name" => "Cape Verde", "d_code" => "00238"); + $countries[] = array("code" => "KY", "name" => "Cayman Islands", "d_code" => "001"); + $countries[] = array("code" => "CF", "name" => "Central African Republic", "d_code" => "00236"); + $countries[] = array("code" => "TD", "name" => "Chad", "d_code" => "00235"); + $countries[] = array("code" => "CL", "name" => "Chile", "d_code" => "0056"); + $countries[] = array("code" => "CN", "name" => "China", "d_code" => "0086"); + $countries[] = array("code" => "CO", "name" => "Colombia", "d_code" => "0057"); + $countries[] = array("code" => "KM", "name" => "Comoros", "d_code" => "00269"); + $countries[] = array("code" => "CK", "name" => "Cook Islands", "d_code" => "00682"); + $countries[] = array("code" => "CR", "name" => "Costa Rica", "d_code" => "00506"); + $countries[] = array("code" => "CI", "name" => "Côte d'Ivoire", "d_code" => "00225"); + $countries[] = array("code" => "HR", "name" => "Croatia", "d_code" => "00385"); + $countries[] = array("code" => "CU", "name" => "Cuba", "d_code" => "0053"); + $countries[] = array("code" => "CY", "name" => "Cyprus", "d_code" => "00357"); + $countries[] = array("code" => "CZ", "name" => "Czech Republic", "d_code" => "00420"); + $countries[] = array("code" => "CD", "name" => "Democratic Republic of Congo", "d_code" => "00243"); + $countries[] = array("code" => "DK", "name" => "Denmark", "d_code" => "0045"); + $countries[] = array("code" => "DJ", "name" => "Djibouti", "d_code" => "00253"); + $countries[] = array("code" => "DM", "name" => "Dominica", "d_code" => "001"); + $countries[] = array("code" => "DO", "name" => "Dominican Republic", "d_code" => "001"); + $countries[] = array("code" => "EC", "name" => "Ecuador", "d_code" => "00593"); + $countries[] = array("code" => "EG", "name" => "Egypt", "d_code" => "0020"); + $countries[] = array("code" => "SV", "name" => "El Salvador", "d_code" => "00503"); + $countries[] = array("code" => "GQ", "name" => "Equatorial Guinea", "d_code" => "00240"); + $countries[] = array("code" => "ER", "name" => "Eritrea", "d_code" => "00291"); + $countries[] = array("code" => "EE", "name" => "Estonia", "d_code" => "00372"); + $countries[] = array("code" => "ET", "name" => "Ethiopia", "d_code" => "00251"); + $countries[] = array("code" => "FK", "name" => "Falkland Islands", "d_code" => "00500"); + $countries[] = array("code" => "FO", "name" => "Faroe Islands", "d_code" => "00298"); + $countries[] = array("code" => "FM", "name" => "Federated States of Micronesia", "d_code" => "00691"); + $countries[] = array("code" => "FJ", "name" => "Fiji", "d_code" => "00679"); + $countries[] = array("code" => "FI", "name" => "Finland", "d_code" => "00358"); + $countries[] = array("code" => "FR", "name" => "France", "d_code" => "0033"); + $countries[] = array("code" => "GF", "name" => "French Guiana", "d_code" => "00594"); + $countries[] = array("code" => "PF", "name" => "French Polynesia", "d_code" => "00689"); + $countries[] = array("code" => "GA", "name" => "Gabon", "d_code" => "00241"); + $countries[] = array("code" => "GE", "name" => "Georgia", "d_code" => "00995"); + $countries[] = array("code" => "DE", "name" => "Germany", "d_code" => "0049"); + $countries[] = array("code" => "GH", "name" => "Ghana", "d_code" => "00233"); + $countries[] = array("code" => "GI", "name" => "Gibraltar", "d_code" => "00350"); + $countries[] = array("code" => "GR", "name" => "Greece", "d_code" => "0030"); + $countries[] = array("code" => "GL", "name" => "Greenland", "d_code" => "00299"); + $countries[] = array("code" => "GD", "name" => "Grenada", "d_code" => "001"); + $countries[] = array("code" => "GP", "name" => "Guadeloupe", "d_code" => "00590"); + $countries[] = array("code" => "GU", "name" => "Guam", "d_code" => "001"); + $countries[] = array("code" => "GT", "name" => "Guatemala", "d_code" => "00502"); + $countries[] = array("code" => "GN", "name" => "Guinea", "d_code" => "00224"); + $countries[] = array("code" => "GW", "name" => "Guinea-Bissau", "d_code" => "00245"); + $countries[] = array("code" => "GY", "name" => "Guyana", "d_code" => "00592"); + $countries[] = array("code" => "HT", "name" => "Haiti", "d_code" => "00509"); + $countries[] = array("code" => "HN", "name" => "Honduras", "d_code" => "00504"); + $countries[] = array("code" => "HK", "name" => "Hong Kong", "d_code" => "00852"); + $countries[] = array("code" => "HU", "name" => "Hungary", "d_code" => "0036"); + $countries[] = array("code" => "IS", "name" => "Iceland", "d_code" => "00354"); + $countries[] = array("code" => "IN", "name" => "India", "d_code" => "0091"); + $countries[] = array("code" => "ID", "name" => "Indonesia", "d_code" => "0062"); + $countries[] = array("code" => "IR", "name" => "Iran", "d_code" => "0098"); + $countries[] = array("code" => "IQ", "name" => "Iraq", "d_code" => "00964"); + $countries[] = array("code" => "IE", "name" => "Ireland", "d_code" => "00353"); + $countries[] = array("code" => "IL", "name" => "Israel", "d_code" => "00972"); + $countries[] = array("code" => "IT", "name" => "Italy", "d_code" => "0039"); + $countries[] = array("code" => "JM", "name" => "Jamaica", "d_code" => "001"); + $countries[] = array("code" => "JP", "name" => "Japan", "d_code" => "0081"); + $countries[] = array("code" => "JO", "name" => "Jordan", "d_code" => "00962"); + $countries[] = array("code" => "KZ", "name" => "Kazakhstan", "d_code" => "007"); + $countries[] = array("code" => "KE", "name" => "Kenya", "d_code" => "00254"); + $countries[] = array("code" => "KI", "name" => "Kiribati", "d_code" => "00686"); + $countries[] = array("code" => "XK", "name" => "Kosovo", "d_code" => "00381"); + $countries[] = array("code" => "KW", "name" => "Kuwait", "d_code" => "00965"); + $countries[] = array("code" => "KG", "name" => "Kyrgyzstan", "d_code" => "00996"); + $countries[] = array("code" => "LA", "name" => "Laos", "d_code" => "00856"); + $countries[] = array("code" => "LV", "name" => "Latvia", "d_code" => "00371"); + $countries[] = array("code" => "LB", "name" => "Lebanon", "d_code" => "00961"); + $countries[] = array("code" => "LS", "name" => "Lesotho", "d_code" => "00266"); + $countries[] = array("code" => "LR", "name" => "Liberia", "d_code" => "00231"); + $countries[] = array("code" => "LY", "name" => "Libya", "d_code" => "00218"); + $countries[] = array("code" => "LI", "name" => "Liechtenstein", "d_code" => "00423"); + $countries[] = array("code" => "LT", "name" => "Lithuania", "d_code" => "00370"); + $countries[] = array("code" => "LU", "name" => "Luxembourg", "d_code" => "00352"); + $countries[] = array("code" => "MO", "name" => "Macau", "d_code" => "00853"); + $countries[] = array("code" => "MK", "name" => "Macedonia", "d_code" => "00389"); + $countries[] = array("code" => "MG", "name" => "Madagascar", "d_code" => "00261"); + $countries[] = array("code" => "MW", "name" => "Malawi", "d_code" => "00265"); + $countries[] = array("code" => "MY", "name" => "Malaysia", "d_code" => "0060"); + $countries[] = array("code" => "MV", "name" => "Maldives", "d_code" => "00960"); + $countries[] = array("code" => "ML", "name" => "Mali", "d_code" => "00223"); + $countries[] = array("code" => "MT", "name" => "Malta", "d_code" => "00356"); + $countries[] = array("code" => "MH", "name" => "Marshall Islands", "d_code" => "00692"); + $countries[] = array("code" => "MQ", "name" => "Martinique", "d_code" => "00596"); + $countries[] = array("code" => "MR", "name" => "Mauritania", "d_code" => "00222"); + $countries[] = array("code" => "MU", "name" => "Mauritius", "d_code" => "00230"); + $countries[] = array("code" => "YT", "name" => "Mayotte", "d_code" => "00262"); + $countries[] = array("code" => "MX", "name" => "Mexico", "d_code" => "0052"); + $countries[] = array("code" => "MD", "name" => "Moldova", "d_code" => "00373"); + $countries[] = array("code" => "MC", "name" => "Monaco", "d_code" => "00377"); + $countries[] = array("code" => "MN", "name" => "Mongolia", "d_code" => "00976"); + $countries[] = array("code" => "ME", "name" => "Montenegro", "d_code" => "00382"); + $countries[] = array("code" => "MS", "name" => "Montserrat", "d_code" => "001"); + $countries[] = array("code" => "MA", "name" => "Morocco", "d_code" => "00212"); + $countries[] = array("code" => "MZ", "name" => "Mozambique", "d_code" => "00258"); + $countries[] = array("code" => "NA", "name" => "Namibia", "d_code" => "00264"); + $countries[] = array("code" => "NR", "name" => "Nauru", "d_code" => "00674"); + $countries[] = array("code" => "NP", "name" => "Nepal", "d_code" => "00977"); + $countries[] = array("code" => "NL", "name" => "Netherlands", "d_code" => "0031"); + $countries[] = array("code" => "AN", "name" => "Netherlands Antilles", "d_code" => "00599"); + $countries[] = array("code" => "NC", "name" => "New Caledonia", "d_code" => "00687"); + $countries[] = array("code" => "NZ", "name" => "New Zealand", "d_code" => "0064"); + $countries[] = array("code" => "NI", "name" => "Nicaragua", "d_code" => "00505"); + $countries[] = array("code" => "NE", "name" => "Niger", "d_code" => "00227"); + $countries[] = array("code" => "NG", "name" => "Nigeria", "d_code" => "00234"); + $countries[] = array("code" => "NU", "name" => "Niue", "d_code" => "00683"); + $countries[] = array("code" => "NF", "name" => "Norfolk Island", "d_code" => "00672"); + $countries[] = array("code" => "KP", "name" => "North Korea", "d_code" => "00850"); + $countries[] = array("code" => "MP", "name" => "Northern Mariana Islands", "d_code" => "001"); + $countries[] = array("code" => "NO", "name" => "Norway", "d_code" => "0047"); + $countries[] = array("code" => "OM", "name" => "Oman", "d_code" => "00968"); + $countries[] = array("code" => "PK", "name" => "Pakistan", "d_code" => "0092"); + $countries[] = array("code" => "PW", "name" => "Palau", "d_code" => "00680"); + $countries[] = array("code" => "PS", "name" => "Palestine", "d_code" => "00970"); + $countries[] = array("code" => "PA", "name" => "Panama", "d_code" => "00507"); + $countries[] = array("code" => "PG", "name" => "Papua New Guinea", "d_code" => "00675"); + $countries[] = array("code" => "PY", "name" => "Paraguay", "d_code" => "00595"); + $countries[] = array("code" => "PE", "name" => "Peru", "d_code" => "0051"); + $countries[] = array("code" => "PH", "name" => "Philippines", "d_code" => "0063"); + $countries[] = array("code" => "PL", "name" => "Poland", "d_code" => "0048"); + $countries[] = array("code" => "PT", "name" => "Portugal", "d_code" => "00351"); + $countries[] = array("code" => "PR", "name" => "Puerto Rico", "d_code" => "001"); + $countries[] = array("code" => "QA", "name" => "Qatar", "d_code" => "00974"); + $countries[] = array("code" => "CG", "name" => "Republic of the Congo", "d_code" => "00242"); + $countries[] = array("code" => "RE", "name" => "Réunion", "d_code" => "00262"); + $countries[] = array("code" => "RO", "name" => "Romania", "d_code" => "0040"); + $countries[] = array("code" => "RU", "name" => "Russia", "d_code" => "007"); + $countries[] = array("code" => "RW", "name" => "Rwanda", "d_code" => "00250"); + $countries[] = array("code" => "BL", "name" => "Saint Barthélemy", "d_code" => "00590"); + $countries[] = array("code" => "SH", "name" => "Saint Helena", "d_code" => "00290"); + $countries[] = array("code" => "KN", "name" => "Saint Kitts and Nevis", "d_code" => "001"); + $countries[] = array("code" => "MF", "name" => "Saint Martin", "d_code" => "00590"); + $countries[] = array("code" => "PM", "name" => "Saint Pierre and Miquelon", "d_code" => "00508"); + $countries[] = array("code" => "VC", "name" => "Saint Vincent and the Grenadines", "d_code" => "001"); + $countries[] = array("code" => "WS", "name" => "Samoa", "d_code" => "00685"); + $countries[] = array("code" => "SM", "name" => "San Marino", "d_code" => "00378"); + $countries[] = array("code" => "ST", "name" => "São Tomé and Príncipe", "d_code" => "00239"); + $countries[] = array("code" => "SA", "name" => "Saudi Arabia", "d_code" => "00966"); + $countries[] = array("code" => "SN", "name" => "Senegal", "d_code" => "00221"); + $countries[] = array("code" => "RS", "name" => "Serbia", "d_code" => "00381"); + $countries[] = array("code" => "SC", "name" => "Seychelles", "d_code" => "00248"); + $countries[] = array("code" => "SL", "name" => "Sierra Leone", "d_code" => "00232"); + $countries[] = array("code" => "SG", "name" => "Singapore", "d_code" => "0065"); + $countries[] = array("code" => "SK", "name" => "Slovakia", "d_code" => "00421"); + $countries[] = array("code" => "SI", "name" => "Slovenia", "d_code" => "00386"); + $countries[] = array("code" => "SB", "name" => "Solomon Islands", "d_code" => "00677"); + $countries[] = array("code" => "SO", "name" => "Somalia", "d_code" => "00252"); + $countries[] = array("code" => "ZA", "name" => "South Africa", "d_code" => "0027"); + $countries[] = array("code" => "KR", "name" => "South Korea", "d_code" => "0082"); + $countries[] = array("code" => "ES", "name" => "Spain", "d_code" => "0034"); + $countries[] = array("code" => "LK", "name" => "Sri Lanka", "d_code" => "0094"); + $countries[] = array("code" => "LC", "name" => "St. Lucia", "d_code" => "001"); + $countries[] = array("code" => "SD", "name" => "Sudan", "d_code" => "00249"); + $countries[] = array("code" => "SR", "name" => "Suriname", "d_code" => "00597"); + $countries[] = array("code" => "SZ", "name" => "Swaziland", "d_code" => "00268"); + $countries[] = array("code" => "SE", "name" => "Sweden", "d_code" => "0046"); + $countries[] = array("code" => "CH", "name" => "Switzerland", "d_code" => "0041"); + $countries[] = array("code" => "SY", "name" => "Syria", "d_code" => "00963"); + $countries[] = array("code" => "TW", "name" => "Taiwan", "d_code" => "00886"); + $countries[] = array("code" => "TJ", "name" => "Tajikistan", "d_code" => "00992"); + $countries[] = array("code" => "TZ", "name" => "Tanzania", "d_code" => "00255"); + $countries[] = array("code" => "TH", "name" => "Thailand", "d_code" => "0066"); + $countries[] = array("code" => "BS", "name" => "The Bahamas", "d_code" => "001"); + $countries[] = array("code" => "GM", "name" => "The Gambia", "d_code" => "00220"); + $countries[] = array("code" => "TL", "name" => "Timor-Leste", "d_code" => "00670"); + $countries[] = array("code" => "TG", "name" => "Togo", "d_code" => "00228"); + $countries[] = array("code" => "TK", "name" => "Tokelau", "d_code" => "00690"); + $countries[] = array("code" => "TO", "name" => "Tonga", "d_code" => "00676"); + $countries[] = array("code" => "TT", "name" => "Trinidad and Tobago", "d_code" => "001"); + $countries[] = array("code" => "TN", "name" => "Tunisia", "d_code" => "00216"); + $countries[] = array("code" => "TR", "name" => "Turkey", "d_code" => "0090"); + $countries[] = array("code" => "TM", "name" => "Turkmenistan", "d_code" => "00993"); + $countries[] = array("code" => "TC", "name" => "Turks and Caicos Islands", "d_code" => "001"); + $countries[] = array("code" => "TV", "name" => "Tuvalu", "d_code" => "00688"); + $countries[] = array("code" => "UG", "name" => "Uganda", "d_code" => "00256"); + $countries[] = array("code" => "UA", "name" => "Ukraine", "d_code" => "00380"); + $countries[] = array("code" => "AE", "name" => "United Arab Emirates", "d_code" => "00971"); + $countries[] = array("code" => "GB", "name" => "United Kingdom", "d_code" => "0044"); + $countries[] = array("code" => "US", "name" => "United States", "d_code" => "001"); + $countries[] = array("code" => "UY", "name" => "Uruguay", "d_code" => "00598"); + $countries[] = array("code" => "VI", "name" => "US Virgin Islands", "d_code" => "001"); + $countries[] = array("code" => "UZ", "name" => "Uzbekistan", "d_code" => "00998"); + $countries[] = array("code" => "VU", "name" => "Vanuatu", "d_code" => "00678"); + $countries[] = array("code" => "VA", "name" => "Vatican City", "d_code" => "0039"); + $countries[] = array("code" => "VE", "name" => "Venezuela", "d_code" => "0058"); + $countries[] = array("code" => "VN", "name" => "Vietnam", "d_code" => "0084"); + $countries[] = array("code" => "WF", "name" => "Wallis and Futuna", "d_code" => "00681"); + $countries[] = array("code" => "YE", "name" => "Yemen", "d_code" => "00967"); + $countries[] = array("code" => "ZM", "name" => "Zambia", "d_code" => "00260"); + $countries[] = array("code" => "ZW", "name" => "Zimbabwe", "d_code" => "00263"); + return $countries; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12159.php b/tests/PHPStan/Analyser/data/bug-12159.php new file mode 100644 index 0000000000..d39ae6bd94 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12159.php @@ -0,0 +1,2681 @@ + TestMatrix::Values[ $Point ][ 0 ]; +$foo = + [ + [ 'val' => $func( 1237123 ) ], + [ 'val' => $func( 4379284 ) ], + [ 'val' => $func( 4534895 ) ], + [ 'val' => $func( 9483754 ) ], + [ 'val' => $func( 8127361 ) ], + [ 'val' => $func( 1287129 ) ], + [ 'val' => $func( 7244590 ) ], + ]; + +//for( $i = 0; $i < 100; $i++ ) +//{ +if( $_GET['a'] < $foo[ 1 ][ 'val' ] ) echo '1'; +if( $_GET['a'] < $foo[ 2 ][ 'val' ] ) echo '2'; +if( $_GET['a'] < $foo[ 3 ][ 'val' ] ) echo '3'; +if( $_GET['a'] < $foo[ 4 ][ 'val' ] ) echo '4'; +if( $_GET['a'] < $foo[ 5 ][ 'val' ] ) echo '5'; +if( $_GET['a'] < $foo[ 6 ][ 'val' ] ) echo '6'; +//} + +class TestMatrix +{ + public const array Values = array ( + 0 => + array ( + 0 => 5874481165396689108, + 1 => 8662405580715299972, + 2 => 1838729323137802481, + 3 => 1296254215171686394, + 4 => 240787718805128243, + 5 => 2569399932576470543, + 6 => 2666865512562476674, + 7 => 7440800791997798335, + 8 => 9017504029234684124, + 9 => 1943700815897404988, + 10 => 4807266916823040232, + 11 => 5651791337534958850, + 12 => 7002607381155865437, + 13 => 4533265986128849713, + 14 => 5376300349620761130, + 15 => 7905874742842521971, + 16 => 909744888026452130, + 17 => 7282766239447087930, + 18 => 1346776530840371545, + 19 => 5241686368013809035, + 20 => 4960581668501873164, + 21 => 4216999787816457923, + 22 => 7206618997006790711, + 23 => 1737316659480734612, + 24 => 1396564421397776612, + 25 => 1225052620751257798, + 26 => 5524782971343881599, + 27 => 2259152306650062736, + 28 => 3668358132158286281, + 29 => 6329278711234406504, + 30 => 1398072019509396341, + 31 => 8955588514252493507, + 32 => 1767105836397175082, + 33 => 7034230021779190326, + 34 => 6905169987039336897, + 35 => 389472364053965244, + 36 => 2784078665352126084, + 37 => 2778618770698283740, + 38 => 1378766762279037262, + 39 => 3618227099446145118, + 40 => 1276484677607692690, + 41 => 3099202919675399195, + 42 => 8794553594722463315, + 43 => 9220965608516075037, + 44 => 464961969218490186, + 45 => 8431789941543532605, + 46 => 2220000829371936407, + 47 => 673824151036998803, + 48 => 5433256145723805103, + 49 => 3825003899632634051, + ), + 1 => + array ( + 0 => 8154052500937462248, + 1 => 5576807385765339137, + 2 => 1100518481621993286, + 3 => 3205600232505774719, + 4 => 239730811793443707, + 5 => 4054412049366275439, + 6 => 941723216813015420, + 7 => 6470087431894049191, + 8 => 2716337345328343897, + 9 => 953683961010207742, + 10 => 2738362684680615246, + 11 => 3535184723439979851, + 12 => 4105453969139039388, + 13 => 1769008182819306978, + 14 => 7161610946609102827, + 15 => 7459169462458964034, + 16 => 7200589149413721938, + 17 => 1842332918081972441, + 18 => 7021770893406632400, + 19 => 8342890679809897170, + 20 => 3229267769836612196, + 21 => 8621895098320821041, + 22 => 8192020378402459973, + 23 => 646879134901493937, + 24 => 7644597118411382877, + 25 => 2669227552611432887, + 26 => 4264746695750690265, + 27 => 830616027307888589, + 28 => 6989343698662199702, + 29 => 220940944081390977, + 30 => 2991501672354298249, + 31 => 8565280316848910077, + 32 => 7453367854425505710, + 33 => 8407476888139000249, + 34 => 9141118524169411532, + 35 => 3417565007599042997, + 36 => 7929540029455947300, + 37 => 6341525159423457135, + 38 => 1136401477976290415, + 39 => 815721375348001867, + 40 => 9122672261212021062, + 41 => 3038993244577792661, + 42 => 8902537870044219933, + 43 => 6742257712143705646, + 44 => 305160917138533628, + 45 => 1944434793172827222, + 46 => 5335522480652404622, + 47 => 6226560700086665665, + 48 => 8307974418320309240, + 49 => 6806303928471061067, + ), + 2 => + array ( + 0 => 4975290592490926991, + 1 => 6131701571914965130, + 2 => 9127520861288523557, + 3 => 1405655716825922037, + 4 => 2953339211295438483, + 5 => 7640526281049794652, + 6 => 1453014071818130187, + 7 => 2738989913949892618, + 8 => 6000021482380697144, + 9 => 1037973965313154250, + 10 => 528984535358733067, + 11 => 3417642461931931383, + 12 => 8343011794702923220, + 13 => 7507168997195646060, + 14 => 1831280245495928841, + 15 => 6774996787168120603, + 16 => 99498811159382374, + 17 => 336866722933543741, + 18 => 8971742337733516403, + 19 => 3959481408560435649, + 20 => 4194447658835901918, + 21 => 4698189036403840281, + 22 => 1868877229552777436, + 23 => 3782558119442101296, + 24 => 8612829831567636140, + 25 => 2999918364775393717, + 26 => 4457456312209359735, + 27 => 1911400307152511590, + 28 => 5342632524118518101, + 29 => 7582753306401387624, + 30 => 2891552232599249434, + 31 => 6722331618838222538, + 32 => 1863871167267174088, + 33 => 4721864064741949167, + 34 => 6921608495105963351, + 35 => 2787258830853121593, + 36 => 6318006494535492932, + 37 => 8758213181123797132, + 38 => 2817595964341381484, + 39 => 2189508344516984329, + 40 => 8595851620765258356, + 41 => 4675797867402162161, + 42 => 8664216558549206169, + 43 => 8392353657675228864, + 44 => 3523827866624970939, + 45 => 3125081307911204903, + 46 => 7613092314757778536, + 47 => 5262826170155900761, + 48 => 5156363701596412744, + 49 => 4334292640529862435, + ), + 3 => + array ( + 0 => 6680271612749645767, + 1 => 1038897265710563469, + 2 => 3125268357134460497, + 3 => 3448035616856209350, + 4 => 2290547007394087177, + 5 => 3202782379344553998, + 6 => 8856642337182845360, + 7 => 7006619529055284271, + 8 => 7469279615622781778, + 9 => 3271987266513004287, + 10 => 5561282669998343625, + 11 => 2124822921183299442, + 12 => 5756164387055634612, + 13 => 5552937428984643025, + 14 => 7064113750641600855, + 15 => 5328246101339893619, + 16 => 7333438201129908387, + 17 => 3828772120818252593, + 18 => 8174834386774866076, + 19 => 7786829975211333555, + 20 => 3981203765539870334, + 21 => 7797235689763652673, + 22 => 4165615128733961575, + 23 => 5981144219284475327, + 24 => 3418001781831286846, + 25 => 1492200888573448114, + 26 => 2317318866594527246, + 27 => 2688445214897280589, + 28 => 8929138296967524205, + 29 => 2942491267302746123, + 30 => 4529371813136470715, + 31 => 8181894960585438448, + 32 => 4403301414553068732, + 33 => 3650365933794415107, + 34 => 1802263228403420039, + 35 => 2837949245046582415, + 36 => 8103859399717457751, + 37 => 6523233038597037591, + 38 => 2417678247431759747, + 39 => 8539067974167032946, + 40 => 7239446630166406222, + 41 => 953227842772238130, + 42 => 2061891981074579091, + 43 => 9197132456777724388, + 44 => 4195535321569363259, + 45 => 7802646953768156569, + 46 => 1214202025857093854, + 47 => 2732892716731283275, + 48 => 6422702740355331603, + 49 => 314586223118274101, + ), + 4 => + array ( + 0 => 8932746737511960046, + 1 => 4420639939134831872, + 2 => 4015851428934836080, + 3 => 226942641444362166, + 4 => 7379063053453580291, + 5 => 5408297350760256023, + 6 => 7097728592049579553, + 7 => 2088253461945304456, + 8 => 2832527342827628633, + 9 => 4095360511140466509, + 10 => 8915545429197506654, + 11 => 7454633280949469211, + 12 => 426687349009436650, + 13 => 7558889905023459316, + 14 => 8409879617073507015, + 15 => 3709130785449676075, + 16 => 3916481028234348945, + 17 => 2080313258004748980, + 18 => 8454584147558376449, + 19 => 955650473035618219, + 20 => 8403398466426708496, + 21 => 7925840390455252607, + 22 => 6538000854800071609, + 23 => 5234246074356462331, + 24 => 1419480003652519257, + 25 => 4717025934655073480, + 26 => 7133440962553291054, + 27 => 1216670874596372868, + 28 => 3415520219011084806, + 29 => 6371251457962253684, + 30 => 7343082864680649875, + 31 => 1922360266759830594, + 32 => 2974660376862656509, + 33 => 858418194500090476, + 34 => 6356554697026476948, + 35 => 5114619950190199429, + 36 => 1904140895976090164, + 37 => 3593944879807201662, + 38 => 5719530069829191694, + 39 => 3031668473907288497, + 40 => 3448169104312841979, + 41 => 8122204554627926094, + 42 => 8518863644712353970, + 43 => 8169769218626969757, + 44 => 1659634164765638384, + 45 => 3477793331064103721, + 46 => 5434872090056290754, + 47 => 4276460341063621887, + 48 => 4640639099260040225, + 49 => 9009468365945428002, + ), + 5 => + array ( + 0 => 4931694814167754074, + 1 => 7183568584903649742, + 2 => 8275777720925639086, + 3 => 8419817439480667175, + 4 => 8604323712237915569, + 5 => 3352541925538437922, + 6 => 4199420257337885962, + 7 => 1106468391590120959, + 8 => 8507355052862836844, + 9 => 4331895772301917877, + 10 => 7920254998068325755, + 11 => 8996973071477628119, + 12 => 455008091719671736, + 13 => 3815293412644646732, + 14 => 436955111200922011, + 15 => 7013986275832485803, + 16 => 5688297970433003962, + 17 => 2011158629907362985, + 18 => 7951175882360459923, + 19 => 5742765642824123605, + 20 => 1216836110798583420, + 21 => 8679387052777060181, + 22 => 1985688926071711354, + 23 => 1831808276654186998, + 24 => 102085979594198107, + 25 => 2340187189218681369, + 26 => 1925730779370056452, + 27 => 4041628961826241780, + 28 => 4907429270936661782, + 29 => 999802994114419710, + 30 => 8230876938618101144, + 31 => 7777792290797940668, + 32 => 8058836789085030797, + 33 => 9145042694186638609, + 34 => 1490700942820470088, + 35 => 8080486113090366780, + 36 => 9012927814276117762, + 37 => 4817168063146379030, + 38 => 5887513675240220051, + 39 => 2170648251352279216, + 40 => 8660441599773259447, + 41 => 2566734480158371883, + 42 => 7877381935713445782, + 43 => 3424535784008708938, + 44 => 6138477295423731789, + 45 => 531408866931128281, + 46 => 8118255972970448317, + 47 => 6658517893844506220, + 48 => 5192765725571041390, + 49 => 4484573374403032898, + ), + 6 => + array ( + 0 => 7817725724153064283, + 1 => 676044540944783015, + 2 => 165795045891931505, + 3 => 8628574277625575660, + 4 => 4623749438938771988, + 5 => 7377760708842913949, + 6 => 4835799984105487685, + 7 => 8736269977412248326, + 8 => 5713305870673689087, + 9 => 1512747349895463886, + 10 => 1297398582506845786, + 11 => 4536497787871480498, + 12 => 7859883546974534383, + 13 => 7451785769304448658, + 14 => 1566047154522047144, + 15 => 2681185467507861604, + 16 => 3282533773867812887, + 17 => 7710783454818853777, + 18 => 757388808200637902, + 19 => 1267453957031758382, + 20 => 3067184561283064874, + 21 => 1075927338235603309, + 22 => 5040384382667600121, + 23 => 4470519589820835295, + 24 => 6446347166686380336, + 25 => 5133211229242848498, + 26 => 4799307086528142991, + 27 => 2417256161533702584, + 28 => 5004748314990362737, + 29 => 5654624457458575102, + 30 => 7831168243158770416, + 31 => 2438361643495966584, + 32 => 3331080805559396049, + 33 => 2332998025596953248, + 34 => 4955322642679292607, + 35 => 2823206402722454329, + 36 => 7864363481035388949, + 37 => 3972282083392565017, + 38 => 7397491981841336255, + 39 => 2077760290151467781, + 40 => 7444037508733373992, + 41 => 5693183530497128762, + 42 => 8635051873130500468, + 43 => 2725415837048413228, + 44 => 8350394208673414293, + 45 => 6573719025342292543, + 46 => 7882248774735967651, + 47 => 3621593747653440124, + 48 => 1381288049786560954, + 49 => 4271094963880511320, + ), + 7 => + array ( + 0 => 7374574356170927085, + 1 => 7717377238321849930, + 2 => 1617648016491363337, + 3 => 3920182728377977038, + 4 => 4864055338550692898, + 5 => 3852374904000741108, + 6 => 8014130603489499273, + 7 => 1266780406787288918, + 8 => 5745877767288766328, + 9 => 3755007514011162686, + 10 => 4988518671877958110, + 11 => 2765009033270152436, + 12 => 2933921152637177103, + 13 => 6289527477470818356, + 14 => 1566901334856634296, + 15 => 3847215702911572949, + 16 => 2464205579185886331, + 17 => 567922664555183884, + 18 => 8419671061374781092, + 19 => 5347381431422400203, + 20 => 2474093747941658240, + 21 => 947490060863904786, + 22 => 7728796299957089994, + 23 => 1958274075678411402, + 24 => 5787113707153405868, + 25 => 4770823972103532340, + 26 => 2782424094528669314, + 27 => 3927604835193670320, + 28 => 5880856123044238820, + 29 => 6247063793366641001, + 30 => 1003445960799983811, + 31 => 189188513499196933, + 32 => 6931085745898288806, + 33 => 5494959985724584020, + 34 => 6299501471987452338, + 35 => 6409426315745727087, + 36 => 6827715490929856161, + 37 => 144065419718686829, + 38 => 3427330871133407325, + 39 => 6708849578158375260, + 40 => 7821502350946541873, + 41 => 2579683792204579398, + 42 => 2174599388328183916, + 43 => 4939750476289377673, + 44 => 6195818835433786206, + 45 => 7844070692861182367, + 46 => 8223755928113469032, + 47 => 4630348997781506319, + 48 => 6784991557794232291, + 49 => 2456684460705773063, + ), + 8 => + array ( + 0 => 4165061665861516758, + 1 => 9208696398120276634, + 2 => 617283688467018612, + 3 => 8866309294196812992, + 4 => 8468066831561872950, + 5 => 5496080959195095216, + 6 => 3043457951940840139, + 7 => 107430864991081073, + 8 => 4854421891895873824, + 9 => 312505545755152719, + 10 => 5466206170978851220, + 11 => 7331656214488538183, + 12 => 8861441230750933712, + 13 => 1440020651481286548, + 14 => 8744438879784230686, + 15 => 7373332827225771759, + 16 => 858317805219293532, + 17 => 4035142104918609164, + 18 => 7415794421717864075, + 19 => 524830805408747363, + 20 => 9104056005409942822, + 21 => 5515188152570953140, + 22 => 7936119942383904460, + 23 => 9196672433903288853, + 24 => 4323078042756619284, + 25 => 8709662277893494773, + 26 => 7774114341997140065, + 27 => 326561711760595822, + 28 => 5659596638817237154, + 29 => 1800665601458267317, + 30 => 4226834709095391595, + 31 => 1934477928162442275, + 32 => 7861332517555509512, + 33 => 7305864724756284932, + 34 => 2107144327061685536, + 35 => 808722588857488630, + 36 => 2539437580044035869, + 37 => 3832604034556654476, + 38 => 4736821570536711823, + 39 => 8426922577642729511, + 40 => 7833992549454389473, + 41 => 1997039369012839251, + 42 => 5639683077508943280, + 43 => 8229475878766589103, + 44 => 2485173465855721469, + 45 => 974771202823843715, + 46 => 7104091963794213783, + 47 => 5736613206714171302, + 48 => 1452529717609521722, + 49 => 2512573977897891429, + ), + 9 => + array ( + 0 => 2723583737373680948, + 1 => 1942679677192434943, + 2 => 4992464429137244820, + 3 => 2108955603623244091, + 4 => 6715661544124588869, + 5 => 6784211158418344847, + 6 => 2174143361816980918, + 7 => 3159957296428237653, + 8 => 4642033571093997804, + 9 => 4516721609521486085, + 10 => 513419668552982043, + 11 => 8225856962710238974, + 12 => 76645791112297512, + 13 => 3838900370577780978, + 14 => 4377406039801675939, + 15 => 4248126498854180353, + 16 => 8514256144540280083, + 17 => 6624238216265012006, + 18 => 5512630561682499018, + 19 => 3151801592612715911, + 20 => 5682544206404299992, + 21 => 625026099893613569, + 22 => 5598008756980903917, + 23 => 4096496250937305812, + 24 => 542097768283614600, + 25 => 4214286372500783945, + 26 => 1065561831812197596, + 27 => 28230230818721266, + 28 => 7776756249499921877, + 29 => 7812792067516739818, + 30 => 1215883148035906041, + 31 => 2293132077185823077, + 32 => 2759052538028995446, + 33 => 5016491194647680439, + 34 => 6818634536467227486, + 35 => 4768244996115591062, + 36 => 7628778154079405816, + 37 => 1512766685186132967, + 38 => 1002281579513027848, + 39 => 4585799281945843823, + 40 => 7731707092844578819, + 41 => 4828769242619016876, + 42 => 2316143876529283991, + 43 => 7528436633751214560, + 44 => 1924628512711298773, + 45 => 6926054778707896318, + 46 => 3389519864922952866, + 47 => 3128371853095208573, + 48 => 50187235618483355, + 49 => 6349194033693131776, + ), + 10 => + array ( + 0 => 6322682526646271316, + 1 => 3095227202547366285, + 2 => 7395278893937465675, + 3 => 5200574266009884104, + 4 => 6279574000505636735, + 5 => 3352978839878696682, + 6 => 9191320712818604654, + 7 => 2262271016363943052, + 8 => 3214808318418256558, + 9 => 7853553360971957989, + 10 => 6297850452490597028, + 11 => 6224291870945590443, + 12 => 907950940123667978, + 13 => 8059430599577153641, + 14 => 3965322572900601193, + 15 => 8152944950051729202, + 16 => 5468985755335978628, + 17 => 6253800414625619123, + 18 => 4796881012575806886, + 19 => 809498396850796356, + 20 => 8761295074351369989, + 21 => 8211306778988175688, + 22 => 6425079682030866983, + 23 => 2208897775637467049, + 24 => 4037060769503045276, + 25 => 5982687341576200957, + 26 => 7321281395460426978, + 27 => 6813789423889591740, + 28 => 8652271626734070437, + 29 => 7655412007544743994, + 30 => 6318582516903548321, + 31 => 8120943312182510842, + 32 => 898459381905385629, + 33 => 1006272515095367404, + 34 => 5853432631494184641, + 35 => 2488930447849334827, + 36 => 5627991830205858315, + 37 => 8435986012941786135, + 38 => 500021810061656317, + 39 => 6086585656093606353, + 40 => 7799777209506835195, + 41 => 2564479240266255407, + 42 => 5830890894601088186, + 43 => 875317478781921464, + 44 => 4890435028615059637, + 45 => 6066524404263227777, + 46 => 8796437456649755382, + 47 => 671650050322048833, + 48 => 2996153661244103038, + 49 => 6141392984453555407, + ), + 11 => + array ( + 0 => 2113829968014464580, + 1 => 3604420855252515310, + 2 => 5566530360687933014, + 3 => 2638942722197379719, + 4 => 6197686530435577362, + 5 => 8804367326165731912, + 6 => 1374734978881384983, + 7 => 4121531290119521118, + 8 => 7025324650905800704, + 9 => 8632620634376756999, + 10 => 3493769733810379690, + 11 => 1446564299587766735, + 12 => 1548774894197112857, + 13 => 8755145460632063828, + 14 => 1599414219607213507, + 15 => 8326310746484899674, + 16 => 1438171968793473616, + 17 => 5739936335339518886, + 18 => 1230631109087403411, + 19 => 6085929453678720567, + 20 => 5517475317864770480, + 21 => 7544841164146387441, + 22 => 4413366135606076191, + 23 => 474656466728891395, + 24 => 1777603850640216995, + 25 => 3913561919378601733, + 26 => 5990372623719211725, + 27 => 8127855600690678186, + 28 => 7991862497915474195, + 29 => 4883200076616379029, + 30 => 5001010733372830540, + 31 => 6545802952205727101, + 32 => 8579592114269580287, + 33 => 1719858225414994089, + 34 => 2914370630968622228, + 35 => 6487456062856131622, + 36 => 1457230405126956623, + 37 => 5450438075766678977, + 38 => 4316797174379978326, + 39 => 356289589153760201, + 40 => 6152162952764411308, + 41 => 2095918233946250545, + 42 => 6846022177534448180, + 43 => 3138034230639707092, + 44 => 9076383662453007017, + 45 => 7766302103119169599, + 46 => 7318895974015143966, + 47 => 7844345536610967416, + 48 => 303771157892538553, + 49 => 1830013023076642241, + ), + 12 => + array ( + 0 => 8296851030827013358, + 1 => 3112251186986342163, + 2 => 1670409722450829600, + 3 => 7761113342030329019, + 4 => 8460561445500753222, + 5 => 4908257398338387298, + 6 => 1778895275579039127, + 7 => 3380500509985904841, + 8 => 5879289665279498918, + 9 => 1553159928549418822, + 10 => 311430609625452179, + 11 => 394936916444712045, + 12 => 5127641876166108248, + 13 => 6568988002955423611, + 14 => 8650085268993266854, + 15 => 5903427408450114483, + 16 => 2263226697604701659, + 17 => 8727279632415987896, + 18 => 3842911696821754254, + 19 => 5490803589488953024, + 20 => 7936352037551275551, + 21 => 1802719271321128297, + 22 => 7959093330975432496, + 23 => 1557009731146154818, + 24 => 1473872816908980020, + 25 => 1418764498156927753, + 26 => 2301176459661145867, + 27 => 2286352418548464686, + 28 => 1194621763940317472, + 29 => 6606061027604696484, + 30 => 8084518688858422568, + 31 => 2208900834543651741, + 32 => 5755194898079572507, + 33 => 7320839167101439960, + 34 => 9029972412529258306, + 35 => 5889791403139418397, + 36 => 6344044519932199509, + 37 => 5662962995408376380, + 38 => 1793535773221710787, + 39 => 6776030508990122856, + 40 => 7477111423046883661, + 41 => 3028777341102868090, + 42 => 4057757640110568728, + 43 => 5986048017637921779, + 44 => 9125552214661206232, + 45 => 7852264129484078269, + 46 => 4446147301234138628, + 47 => 5507063673112794235, + 48 => 6332855026822695011, + 49 => 4020513967214987505, + ), + 13 => + array ( + 0 => 2837617673724062427, + 1 => 7125850334112735147, + 2 => 6063426842568747128, + 3 => 1449956004993771688, + 4 => 8233038711924343980, + 5 => 1624050510578207334, + 6 => 201045653760070683, + 7 => 6425618561397260897, + 8 => 1736775056718544457, + 9 => 4283281796416168155, + 10 => 8943407918198470419, + 11 => 5174416738774162884, + 12 => 8282242448652142434, + 13 => 3483110946752360937, + 14 => 9172098532505635523, + 15 => 4919860276458045393, + 16 => 1508811892472366358, + 17 => 2543702316937780378, + 18 => 5391494775097463950, + 19 => 1646737894557870150, + 20 => 3840251377981664631, + 21 => 5557055980319270631, + 22 => 614458087357624962, + 23 => 3049172204044066528, + 24 => 4147916760406968728, + 25 => 8609446583426508961, + 26 => 2242391100589192563, + 27 => 5436112318641652346, + 28 => 4618310365458346019, + 29 => 2077318216555261390, + 30 => 3059989963664577310, + 31 => 7848793921431254972, + 32 => 1203430412948043756, + 33 => 2729600696821765392, + 34 => 791147694547888137, + 35 => 3707975566214340037, + 36 => 8601861547198440141, + 37 => 8535418355338386385, + 38 => 7608939352612737337, + 39 => 329873792714069411, + 40 => 2476061428301616271, + 41 => 8636330979861967347, + 42 => 4895768550130850937, + 43 => 4385109140267411446, + 44 => 8630975950112663906, + 45 => 6540002935581557630, + 46 => 3308964414877219337, + 47 => 5153842433409053720, + 48 => 253675177384576905, + 49 => 8423529847500341694, + ), + 14 => + array ( + 0 => 6993713031298341935, + 1 => 5326414593476770012, + 2 => 5440814550066802105, + 3 => 141762543875879518, + 4 => 5685816950122979356, + 5 => 3092600055577005256, + 6 => 484524073179592456, + 7 => 3023390118292269707, + 8 => 6864350520979702465, + 9 => 164326004277162557, + 10 => 1061461362432115174, + 11 => 2224051270026522509, + 12 => 6168787883393523744, + 13 => 7674793873689286403, + 14 => 1911946231027664781, + 15 => 8744291606724379208, + 16 => 9014519428529976331, + 17 => 3879593031828012380, + 18 => 619709744505846015, + 19 => 9116163054436980499, + 20 => 7832149942441221423, + 21 => 8108528699446988884, + 22 => 1971792629433296522, + 23 => 2640898620660261083, + 24 => 4826688299073541883, + 25 => 8208909046876680841, + 26 => 5721944470113654305, + 27 => 4086878983333595985, + 28 => 3777491165352231027, + 29 => 8919919327161482714, + 30 => 1411839390869133003, + 31 => 6507835402545136011, + 32 => 6630143811048135593, + 33 => 9162986904570452659, + 34 => 2158137837160408572, + 35 => 8083368029763836496, + 36 => 1089926883319054315, + 37 => 8268575358599231390, + 38 => 8199472199423672208, + 39 => 2280879658381489781, + 40 => 5576217042829441238, + 41 => 1546113314666207528, + 42 => 314235395477009613, + 43 => 1154159462456870581, + 44 => 6430125602104326521, + 45 => 4141336619453788776, + 46 => 8123765325147860838, + 47 => 1072475769909743664, + 48 => 3275082594725811702, + 49 => 35188985155813154, + ), + 15 => + array ( + 0 => 4491251610507865005, + 1 => 5013670103317501847, + 2 => 1908586816547780374, + 3 => 5528080847159743054, + 4 => 5104328648855753448, + 5 => 7599385267220236891, + 6 => 2776409469017349441, + 7 => 4575596226800948948, + 8 => 6369321928571414671, + 9 => 1618971068284703013, + 10 => 6277448308413490415, + 11 => 511988212940164645, + 12 => 3099316290169034108, + 13 => 3954426873623717044, + 14 => 4442835296439196398, + 15 => 8527786574257820049, + 16 => 541480700139692845, + 17 => 7258546318137865130, + 18 => 2111094668206075978, + 19 => 7746803879177003947, + 20 => 807752852058787647, + 21 => 6303558981146631063, + 22 => 1612288856991150333, + 23 => 3477957171986545461, + 24 => 2903449324702960216, + 25 => 4847163341110332855, + 26 => 8152405596867347396, + 27 => 8338399885984045224, + 28 => 5649959999977342668, + 29 => 5720423269116660296, + 30 => 965246675443819514, + 31 => 4402398597112098409, + 32 => 7574584563321041436, + 33 => 5672360046774743378, + 34 => 2546837547808280354, + 35 => 7971394139153078563, + 36 => 7369689550706069809, + 37 => 8866894908552724322, + 38 => 764751270312312614, + 39 => 3417051346281355094, + 40 => 7229557916866124768, + 41 => 7261498631961135330, + 42 => 5400611949698217702, + 43 => 4379197429476731239, + 44 => 944076077759636497, + 45 => 3343096502531647942, + 46 => 1460414845122217807, + 47 => 5886003542955764528, + 48 => 294146151341598816, + 49 => 7553441789861934638, + ), + 16 => + array ( + 0 => 8741958986469974724, + 1 => 6215975541860594564, + 2 => 2030793673351821656, + 3 => 7664541364665664906, + 4 => 8470810402228401978, + 5 => 4313164655146288908, + 6 => 4839977850635283703, + 7 => 4651535922908649829, + 8 => 81623039571201672, + 9 => 5879786151984355685, + 10 => 2652375748969362868, + 11 => 1412377869821067484, + 12 => 7764752987880077980, + 13 => 3232608468180411697, + 14 => 5219774171360183259, + 15 => 276757970441762536, + 16 => 2157050254663254778, + 17 => 4772180464617334572, + 18 => 4850998942845193572, + 19 => 2543311538514698065, + 20 => 8050994584586108828, + 21 => 2815479474551748381, + 22 => 5971023239458235291, + 23 => 4067859276180314903, + 24 => 7748875825149588576, + 25 => 7607843825928354150, + 26 => 1115863343729652284, + 27 => 968665230690300207, + 28 => 2344103572208289990, + 29 => 4915922776603825251, + 30 => 7899341581173719583, + 31 => 3270638032084051342, + 32 => 7922829911756040174, + 33 => 6901237696263042089, + 34 => 103197869659722557, + 35 => 527606972626448062, + 36 => 205932143123493544, + 37 => 4666962621159430172, + 38 => 6025147156756276603, + 39 => 2569017618097790149, + 40 => 2782270428022887692, + 41 => 2110342899579201191, + 42 => 4866511434611918196, + 43 => 8287772446542779705, + 44 => 1240825666152673689, + 45 => 7318857828118583203, + 46 => 7395325360634556807, + 47 => 1537320824630196486, + 48 => 6236055319334356730, + 49 => 8913567671596838634, + ), + 17 => + array ( + 0 => 3043470933854885224, + 1 => 6714301203475713128, + 2 => 860257064799129134, + 3 => 9041321571746388930, + 4 => 288738229336661630, + 5 => 3371616536887951610, + 6 => 1598608002069517570, + 7 => 5345879053451417291, + 8 => 4605770882480547648, + 9 => 8046129185750429146, + 10 => 7471568780314310293, + 11 => 1891596127269858319, + 12 => 2648872195739917662, + 13 => 2923983151863274145, + 14 => 78950419940827592, + 15 => 5925091477994177417, + 16 => 5731829744992297031, + 17 => 4296592622666844395, + 18 => 2419286681494585306, + 19 => 7283688448528986472, + 20 => 3321477450763978371, + 21 => 3064657579201514684, + 22 => 5374614556206587782, + 23 => 9107664630361570410, + 24 => 6890980300156013295, + 25 => 1165636160761295363, + 26 => 7068550182564021171, + 27 => 1118884285637398925, + 28 => 4520356901520518371, + 29 => 3906256068453096126, + 30 => 5334730629419585704, + 31 => 8867104621512809498, + 32 => 3070185485814636491, + 33 => 200199337477590437, + 34 => 4949494895124137875, + 35 => 8951288005981893499, + 36 => 2222242921160594363, + 37 => 4156807003305590083, + 38 => 3482462562024041320, + 39 => 2635205157596707857, + 40 => 840204241790569977, + 41 => 7563496981822937374, + 42 => 1582663658368798766, + 43 => 2736234992581107731, + 44 => 7016727431215779519, + 45 => 4968847729299064149, + 46 => 1216274414653489790, + 47 => 353213425186274929, + 48 => 4727317845209199005, + 49 => 8297576197853361424, + ), + 18 => + array ( + 0 => 159828459226828317, + 1 => 5228910350354088371, + 2 => 3800521216652551335, + 3 => 2253147546468586805, + 4 => 106796071918441752, + 5 => 2814225221080495171, + 6 => 3053238596951743089, + 7 => 4477943349369572147, + 8 => 8952510351557581107, + 9 => 2368476941075762366, + 10 => 561925318975977237, + 11 => 329670233355618662, + 12 => 5208937722910779587, + 13 => 3060450901088187935, + 14 => 4659097015012886378, + 15 => 5039174786713818080, + 16 => 3018568769194342498, + 17 => 1240769854944228955, + 18 => 2817285542073135861, + 19 => 3934900710974648820, + 20 => 8482919410301897152, + 21 => 8481234644320051096, + 22 => 4171591109421777684, + 23 => 5034695506354661667, + 24 => 4092817754517451666, + 25 => 4560986042376585682, + 26 => 3054876512309742094, + 27 => 6753222229261142602, + 28 => 5849041337477188797, + 29 => 7938201530168412349, + 30 => 6670314596868727397, + 31 => 7259960116747972664, + 32 => 2061159009576901210, + 33 => 5516856451150141519, + 34 => 5562142725910270159, + 35 => 4428036293610195147, + 36 => 7825136895944119800, + 37 => 6528157703864613968, + 38 => 7077699025224950556, + 39 => 3958424612440598778, + 40 => 4382670869650741676, + 41 => 4907831461290595051, + 42 => 2955573740056960677, + 43 => 2467864051452085508, + 44 => 2771440083868176870, + 45 => 2126983384946487140, + 46 => 694858885292569525, + 47 => 7420632173785167556, + 48 => 17990672710647105, + 49 => 2041959591437784652, + ), + 19 => + array ( + 0 => 7317139211076919878, + 1 => 1282655490899029874, + 2 => 7762517756959954308, + 3 => 7307406843013483032, + 4 => 8440361575264531800, + 5 => 4557610895832592743, + 6 => 4647166194384492730, + 7 => 7836965747539165421, + 8 => 661449650495111113, + 9 => 5905003857595880068, + 10 => 6058292247968883017, + 11 => 7707813779197067451, + 12 => 2765003876415774360, + 13 => 1642519878811525518, + 14 => 6488644034703432506, + 15 => 443516601408995930, + 16 => 8681252700158220179, + 17 => 7213878451268575925, + 18 => 4957060309915020583, + 19 => 8614133085282346831, + 20 => 4469738621889306141, + 21 => 3619072342991403987, + 22 => 1288952461195174914, + 23 => 3127547882180992688, + 24 => 5243033781121657206, + 25 => 430262612273204082, + 26 => 351924028121170974, + 27 => 290830022617614644, + 28 => 4426032873476367010, + 29 => 3298187746051695086, + 30 => 3300882353921382357, + 31 => 2998867997974943651, + 32 => 6335244123367408722, + 33 => 1562080616401434152, + 34 => 3622026051437015416, + 35 => 3063104137993823287, + 36 => 4908105192604607913, + 37 => 8108507327674564482, + 38 => 8078582610832559796, + 39 => 4545970688996026128, + 40 => 7575511062471729436, + 41 => 6668469679406886222, + 42 => 2055949106569645003, + 43 => 8084940231047228149, + 44 => 7809492702601105062, + 45 => 5958456976930763443, + 46 => 4479320839357515450, + 47 => 1286213746222420831, + 48 => 1329535666848852083, + 49 => 5437370777345146572, + ), + 20 => + array ( + 0 => 4857706934552326839, + 1 => 8604356504209431307, + 2 => 5916200389864426149, + 3 => 1972127778835616323, + 4 => 4466838903146615187, + 5 => 5189584875258487647, + 6 => 8206235570971558685, + 7 => 5664557861400693721, + 8 => 76554600264032963, + 9 => 1375414045028523191, + 10 => 314604821407701077, + 11 => 542962474657268177, + 12 => 3763797168773875653, + 13 => 7696660931594638607, + 14 => 7657860041931331157, + 15 => 3684023238413049415, + 16 => 1288136826482098114, + 17 => 6538391815793689011, + 18 => 1539691100482100899, + 19 => 6697889143180391350, + 20 => 689391216106492212, + 21 => 8558737790467168778, + 22 => 9114955107747374239, + 23 => 3516848329603263424, + 24 => 7951243507168588495, + 25 => 1278745189874536837, + 26 => 1110763008585829835, + 27 => 387695939753230271, + 28 => 6327450490177456303, + 29 => 4763569094147725981, + 30 => 4431527363687509033, + 31 => 2176672561786634376, + 32 => 4103216092069297204, + 33 => 1012903945494380106, + 34 => 6519217886324143112, + 35 => 891551299177755208, + 36 => 5286097396474065445, + 37 => 4872647425260736893, + 38 => 5504723327489075283, + 39 => 5240238322856169756, + 40 => 2121810588737684596, + 41 => 2995943731837790863, + 42 => 6363242933036886665, + 43 => 6437009869752649523, + 44 => 6010597810129509157, + 45 => 6031054356983231858, + 46 => 7604333500356670964, + 47 => 2040711769022116167, + 48 => 1223016982760333922, + 49 => 5656644529208310713, + ), + 21 => + array ( + 0 => 3692883075057948045, + 1 => 8341745748715868269, + 2 => 4153798986434369105, + 3 => 6190685996571843058, + 4 => 4581011959289663915, + 5 => 6889228844290451861, + 6 => 386651216501620503, + 7 => 2641657213163165536, + 8 => 1335417413810890798, + 9 => 1195325223121027856, + 10 => 1950382984503804487, + 11 => 2018980923444633939, + 12 => 4535200343609863955, + 13 => 4532391651609183606, + 14 => 3091765872963829161, + 15 => 1725514701875724129, + 16 => 8802608053136199660, + 17 => 2501886360038703766, + 18 => 7936720140765753419, + 19 => 6148499603267943045, + 20 => 7684043930850667486, + 21 => 7670255701399237573, + 22 => 8188367993869462016, + 23 => 8735440608656363427, + 24 => 5410649862262562695, + 25 => 4925080728400948351, + 26 => 2176929635680360748, + 27 => 4807048413318271132, + 28 => 6010622872835781146, + 29 => 6303972123625327278, + 30 => 8749397688527702840, + 31 => 5314599595601066296, + 32 => 4101592221080075628, + 33 => 5839125295374380379, + 34 => 4446671680471125606, + 35 => 7858211664287691753, + 36 => 5246910991839856164, + 37 => 4482566883724413138, + 38 => 4817024681224994802, + 39 => 7185912174789012378, + 40 => 1962027438001045790, + 41 => 2609804510626860868, + 42 => 4880788808493006624, + 43 => 8013142836916761691, + 44 => 7099794532571876632, + 45 => 5714190209300556809, + 46 => 4074292082754804563, + 47 => 7118110499688626233, + 48 => 3740645108594423970, + 49 => 3319563052739345108, + ), + 22 => + array ( + 0 => 3635597747626063519, + 1 => 2164524562859423435, + 2 => 5400922439277929330, + 3 => 5638755949943251895, + 4 => 345060876821584997, + 5 => 6346953969578339165, + 6 => 1258767325705790159, + 7 => 5557965573836848627, + 8 => 3701462982527702467, + 9 => 617315811096399620, + 10 => 6224692550136567962, + 11 => 6933758326188267012, + 12 => 1349620962154589916, + 13 => 3090293583685603526, + 14 => 3138811343989032784, + 15 => 4085195644063384467, + 16 => 1750741553651055209, + 17 => 1375307368490389063, + 18 => 2676576903521551168, + 19 => 2480373025920539306, + 20 => 2382891362135228642, + 21 => 7945241691905708930, + 22 => 1298017934480368845, + 23 => 5446902565524747023, + 24 => 1729116730968347711, + 25 => 4147150133130736401, + 26 => 1427843070559773159, + 27 => 1780551772808485451, + 28 => 7917259692730601273, + 29 => 7349523907545971585, + 30 => 2123698404678043325, + 31 => 2028478562293619435, + 32 => 3650204844478402782, + 33 => 1048742987380935661, + 34 => 4919093645065853713, + 35 => 4735521395278711667, + 36 => 4263061631352778668, + 37 => 4990281965597796595, + 38 => 6572930134587784857, + 39 => 6345249396950527073, + 40 => 5357728545494608011, + 41 => 2251117625226850611, + 42 => 9094453220809515443, + 43 => 589604802378396739, + 44 => 6612910280471354751, + 45 => 321052347772933560, + 46 => 3531910257691624990, + 47 => 5723107334369389887, + 48 => 1934550046285941562, + 49 => 2408405055455205691, + ), + 23 => + array ( + 0 => 3182544926194816683, + 1 => 4135791120284976973, + 2 => 9038384596036099199, + 3 => 3360257051495387930, + 4 => 3067116657795906868, + 5 => 9189263530066581983, + 6 => 8810029068987713437, + 7 => 4181405060040733093, + 8 => 6789036062736737414, + 9 => 4180258664806222317, + 10 => 5206301288833582003, + 11 => 7404138723681179874, + 12 => 7584189287131670526, + 13 => 2431867746107884339, + 14 => 1875792223611432089, + 15 => 3459055032035616268, + 16 => 86592156086271429, + 17 => 8483421072516128642, + 18 => 8294151068735231921, + 19 => 5802441801608907744, + 20 => 8382169087571134445, + 21 => 6175256394403582016, + 22 => 8680936151108964764, + 23 => 8028075470000659146, + 24 => 3934209999818180592, + 25 => 2376976355793312353, + 26 => 7412806587346857250, + 27 => 3271699019268501922, + 28 => 8643725002057836189, + 29 => 5272966637925117582, + 30 => 1956416735411967379, + 31 => 2276572757067924478, + 32 => 5452481299602682727, + 33 => 2879185636264199317, + 34 => 3746042541108156691, + 35 => 1429252009136254500, + 36 => 2743586749321822426, + 37 => 7671817618041762252, + 38 => 5465526680667937836, + 39 => 1408302065483439410, + 40 => 3146408973387714635, + 41 => 9144752839124785415, + 42 => 3055389789080167063, + 43 => 2920916448116928028, + 44 => 5096541788581167409, + 45 => 9140954567743705011, + 46 => 8334927526779853673, + 47 => 26254271172604416, + 48 => 6044180175352828659, + 49 => 4905444378844812527, + ), + 24 => + array ( + 0 => 8778804630631233758, + 1 => 2128773536485951925, + 2 => 3156292813293586100, + 3 => 5479506868479360061, + 4 => 5255521102514434059, + 5 => 6127102471628856136, + 6 => 3445428007543458351, + 7 => 4552536685857991488, + 8 => 181461191819877432, + 9 => 7659452559481153647, + 10 => 6208548587363259414, + 11 => 871845240942600698, + 12 => 1566686596856397432, + 13 => 5085136758745300568, + 14 => 48239442416834900, + 15 => 5249326187208137968, + 16 => 6679940152503118125, + 17 => 5672910834000683796, + 18 => 5654888840313725373, + 19 => 2964751030779185994, + 20 => 2596948428062872680, + 21 => 3886836164888421968, + 22 => 2801687144774483114, + 23 => 1435564309420727411, + 24 => 7823551093266275640, + 25 => 8317900161982747716, + 26 => 7670105986978675742, + 27 => 3293880832832782226, + 28 => 6852392738548947525, + 29 => 2399226343154695689, + 30 => 4623705354315297526, + 31 => 1337335133852864718, + 32 => 3142692742052820504, + 33 => 1110904463022289965, + 34 => 1709325244942754172, + 35 => 7781064800373664699, + 36 => 5538479197098539926, + 37 => 2601033748890917211, + 38 => 2003881498784293691, + 39 => 2112085745486960080, + 40 => 4310240847154287634, + 41 => 2308476327942257873, + 42 => 4962068776322463085, + 43 => 9219870942954359516, + 44 => 275448173853618795, + 45 => 3636000360048724646, + 46 => 6515795951916562220, + 47 => 6592664636514711945, + 48 => 5553810843268514647, + 49 => 7475257716026832493, + ), + 25 => + array ( + 0 => 3934912217800888461, + 1 => 7374561905709187569, + 2 => 6362524244007135673, + 3 => 7545292000266069826, + 4 => 3688385979393175809, + 5 => 8944760284319862423, + 6 => 5719514110377594126, + 7 => 2687367137215149026, + 8 => 373362793523307917, + 9 => 4037229058581099439, + 10 => 2760450080990531277, + 11 => 1331755606287071328, + 12 => 8903956594658100019, + 13 => 3017060200190361567, + 14 => 9067522733796185567, + 15 => 7841088764654616386, + 16 => 3325815798413485528, + 17 => 2008486325220794910, + 18 => 8175990495435770767, + 19 => 8700862870804434417, + 20 => 3037197994434502453, + 21 => 2612473879337278307, + 22 => 6960714636653288891, + 23 => 2599077756695892188, + 24 => 1117179736310225927, + 25 => 4567773530476414377, + 26 => 4647243747058620445, + 27 => 2321813451349409720, + 28 => 3865738658487181873, + 29 => 605370897901752710, + 30 => 3561298430528888930, + 31 => 482212088563126217, + 32 => 1123821138794575444, + 33 => 3644559737915817503, + 34 => 3169168436100744951, + 35 => 6684837151528598524, + 36 => 949624257943655438, + 37 => 2363265038683742192, + 38 => 6975100778960739566, + 39 => 1088106952368155082, + 40 => 9031071114875912453, + 41 => 7186957180246026588, + 42 => 748047347237757320, + 43 => 1829271522380212151, + 44 => 5948031981348174897, + 45 => 8287940031417741995, + 46 => 2505752838050649804, + 47 => 5099014870862750432, + 48 => 8588087635974285280, + 49 => 6421123552582880483, + ), + 26 => + array ( + 0 => 5075038906546001565, + 1 => 5575772665085918239, + 2 => 7690213268403706123, + 3 => 2561367337985348087, + 4 => 9198633483003604625, + 5 => 8176611681804800368, + 6 => 1034749991224969288, + 7 => 5413951329712953070, + 8 => 6843474764774872589, + 9 => 2988363423107848960, + 10 => 6905745081169982630, + 11 => 3584472635889546143, + 12 => 2868065303409280569, + 13 => 5721763934857011046, + 14 => 51272945170672251, + 15 => 110898137231783043, + 16 => 3261624826775449864, + 17 => 4290905888212127901, + 18 => 3598331731128937800, + 19 => 5485918646765189403, + 20 => 3199925657249673765, + 21 => 4687523998068607431, + 22 => 3547242790293341951, + 23 => 5878605187637781812, + 24 => 1329701316071700626, + 25 => 3852165965733157158, + 26 => 5568308703939857000, + 27 => 712159736152581729, + 28 => 3942040367433932618, + 29 => 7579188707060844698, + 30 => 699748792621735028, + 31 => 8984741049761024565, + 32 => 3630987657323419332, + 33 => 6921833013055677001, + 34 => 5427985014679601453, + 35 => 8808271519225071503, + 36 => 4711070269125981849, + 37 => 3373227369288191129, + 38 => 6126028385690479496, + 39 => 5863162538755040589, + 40 => 260615166567030749, + 41 => 6169978680851501167, + 42 => 4358818732555163540, + 43 => 8518740114556884065, + 44 => 7958754409966373094, + 45 => 573152438257673709, + 46 => 331267994190726417, + 47 => 8356096694878241479, + 48 => 1272080648927188078, + 49 => 8719394796985858664, + ), + 27 => + array ( + 0 => 1713112773270000284, + 1 => 3674491511347012363, + 2 => 2816944677731995110, + 3 => 8169516782327556549, + 4 => 1079881425235210838, + 5 => 7358305538760468281, + 6 => 5817013438134320577, + 7 => 8544277047549920689, + 8 => 2612693494334873504, + 9 => 2410205570754317675, + 10 => 4765074328332257479, + 11 => 3200927192423204576, + 12 => 993571942634740218, + 13 => 4127024137323817041, + 14 => 7931819328137732593, + 15 => 6004980101535875403, + 16 => 2593996430156591924, + 17 => 3344245560034769530, + 18 => 7758136653194498132, + 19 => 8094110195572556176, + 20 => 3118267944711071984, + 21 => 7186275536405421706, + 22 => 7796826921442172507, + 23 => 456647124663036567, + 24 => 2295505108146214194, + 25 => 845993445877474996, + 26 => 2281582727100964735, + 27 => 8590622392767984276, + 28 => 5335525485978198826, + 29 => 6532961240982760621, + 30 => 618136707885506589, + 31 => 2277579219937808739, + 32 => 1847684410351490936, + 33 => 3121950859776251309, + 34 => 1373846454651465108, + 35 => 8429372726308291726, + 36 => 4202058483673705428, + 37 => 2102701678608686168, + 38 => 5292586743616572656, + 39 => 1141103091656692614, + 40 => 2452537960493978322, + 41 => 1799252082873228399, + 42 => 8139542680960213645, + 43 => 2220323688842873613, + 44 => 6085583203942625976, + 45 => 1390191550131234271, + 46 => 2556428103448636739, + 47 => 7978764410120570984, + 48 => 7452825238242091899, + 49 => 4906989274116857274, + ), + 28 => + array ( + 0 => 1462805255444649928, + 1 => 8343560722428820573, + 2 => 3858360165264612091, + 3 => 5987775446304932519, + 4 => 8243926019807501861, + 5 => 259792547847858263, + 6 => 8523293594423809996, + 7 => 1022732337636159834, + 8 => 3213715358666985280, + 9 => 5868573469829409213, + 10 => 8466678775818920229, + 11 => 5868366253057791812, + 12 => 6208045679919712986, + 13 => 4828029670603764478, + 14 => 1536764228551143006, + 15 => 7944654398075334736, + 16 => 6540004857400283412, + 17 => 8356652291598372276, + 18 => 7473778899420566941, + 19 => 12515907380719664, + 20 => 3045657915092005947, + 21 => 9076819206325981963, + 22 => 5523885183662623808, + 23 => 3643583187697051931, + 24 => 6047814813088565655, + 25 => 6607907412556680407, + 26 => 3704065470050326981, + 27 => 4943669158459086917, + 28 => 5364952723168287348, + 29 => 3462662330667826688, + 30 => 8701473005455226322, + 31 => 1758611190548459715, + 32 => 4406707928828976418, + 33 => 4888811657431037264, + 34 => 4957013862266587794, + 35 => 2524559341906780414, + 36 => 7047810700820786417, + 37 => 7771528433217430898, + 38 => 8370077425980690940, + 39 => 6794459757583249402, + 40 => 7352324777543603408, + 41 => 6524367095060281956, + 42 => 5781828331196203330, + 43 => 9003794765183902323, + 44 => 2773806512634632982, + 45 => 2478330167704433223, + 46 => 5133311010011475355, + 47 => 6062915138609666101, + 48 => 1366333151612500973, + 49 => 2633440997389232656, + ), + 29 => + array ( + 0 => 2946701720143929750, + 1 => 979159154486554783, + 2 => 3800430233049834405, + 3 => 2403077969463716904, + 4 => 5684238811566745468, + 5 => 733901519881574856, + 6 => 7982886017501491491, + 7 => 5095751087179294980, + 8 => 5458658444971789613, + 9 => 3558216986214111636, + 10 => 1421140469558251152, + 11 => 5589596901034330396, + 12 => 660126764196440588, + 13 => 1626742210305838928, + 14 => 3004209297107836086, + 15 => 4339709620670315423, + 16 => 4601546315149483637, + 17 => 3300906351479838877, + 18 => 8818742378911532918, + 19 => 7650541207121115820, + 20 => 2467475790644867462, + 21 => 8212973278184867174, + 22 => 6170747021793458782, + 23 => 554473080159560355, + 24 => 8109061402513869721, + 25 => 4935184950522172622, + 26 => 2836070912624371173, + 27 => 5863104186443070794, + 28 => 5066322367034512452, + 29 => 7515904293548439617, + 30 => 859595352069190526, + 31 => 5444872038822103090, + 32 => 3909093526439632101, + 33 => 4778069109418293990, + 34 => 1050678055158717675, + 35 => 6090768910048938533, + 36 => 1999585673717811001, + 37 => 5599213870610749390, + 38 => 5985534876910914488, + 39 => 1280817401435980253, + 40 => 1607456235317077015, + 41 => 4706933717031109339, + 42 => 4063064640509200643, + 43 => 8093028299255604600, + 44 => 5250545038454364236, + 45 => 7822988978679330097, + 46 => 1432284631859890921, + 47 => 6076775734848570758, + 48 => 5016187889233898325, + 49 => 6200896567050378142, + ), + 30 => + array ( + 0 => 886091043385175502, + 1 => 6107304329680384151, + 2 => 4487808133923722915, + 3 => 1572718359237314418, + 4 => 7182589849836822147, + 5 => 8552310449666824121, + 6 => 839834575767730160, + 7 => 3704725190344708521, + 8 => 3419433146617460448, + 9 => 4583683278208878013, + 10 => 4287173717136287019, + 11 => 2023580108484110495, + 12 => 139396265302617537, + 13 => 2695133350856059405, + 14 => 3375601802434923130, + 15 => 7543307316188800487, + 16 => 166137300174459592, + 17 => 2037951619903114622, + 18 => 6556111035886676158, + 19 => 6842491202596981334, + 20 => 6427960512489248432, + 21 => 1366064619968751026, + 22 => 3380087751220567269, + 23 => 1844240660623953672, + 24 => 8917750134472624943, + 25 => 3529706961223209031, + 26 => 413567163584414095, + 27 => 7204467989140882562, + 28 => 2600697917552335595, + 29 => 5504588681600388754, + 30 => 5185102754012983553, + 31 => 8437022723812659702, + 32 => 8946155770277578791, + 33 => 5364720908803041297, + 34 => 561598278573040523, + 35 => 2372698569561196055, + 36 => 4633419157760179115, + 37 => 3220279843598497436, + 38 => 155088913438442781, + 39 => 4858459018003785580, + 40 => 2683868126975220053, + 41 => 8232077000531421659, + 42 => 5622386414546408187, + 43 => 3723224767708117380, + 44 => 681397607067024437, + 45 => 3412495988269800265, + 46 => 5291514015537221343, + 47 => 4827703663950925572, + 48 => 465582164685264367, + 49 => 185016645248110044, + ), + 31 => + array ( + 0 => 8937537024875823665, + 1 => 1710633894284092377, + 2 => 8894642741914578266, + 3 => 8664119568507171411, + 4 => 2379779599812168746, + 5 => 4205412394192548097, + 6 => 7956809385605578280, + 7 => 8996315485331930942, + 8 => 6111233478685486620, + 9 => 8498569945704516150, + 10 => 2297507561583664303, + 11 => 8972169406416037499, + 12 => 1195619691522435232, + 13 => 4717523340848578881, + 14 => 2232481570914083203, + 15 => 3150101794125719823, + 16 => 6354655699945953482, + 17 => 4318642052172430270, + 18 => 5106084537983843572, + 19 => 1664777159510717072, + 20 => 2751967262693138443, + 21 => 5773248984841535745, + 22 => 4209805512870706679, + 23 => 898477103160193176, + 24 => 4666007108426825973, + 25 => 7211869303597657401, + 26 => 2666974192884884367, + 27 => 3480320594345135329, + 28 => 7950389503094974352, + 29 => 1336265817527650754, + 30 => 1310171618122281865, + 31 => 2291592408450733899, + 32 => 8959026177580877450, + 33 => 6740618986473816432, + 34 => 2615501683646827626, + 35 => 8729274792135371503, + 36 => 7327723571053828489, + 37 => 2576476113940077551, + 38 => 1363992834319357767, + 39 => 6831197456638087042, + 40 => 4166364003648427849, + 41 => 3320269676092112238, + 42 => 1124856159597645905, + 43 => 6031181692767874674, + 44 => 587104996978489946, + 45 => 4609207886116121379, + 46 => 8030301603141448722, + 47 => 8714941587912486385, + 48 => 2397527085071463971, + 49 => 8713607253404744721, + ), + 32 => + array ( + 0 => 7361044476014135852, + 1 => 5943379752510709458, + 2 => 7063923696520971270, + 3 => 1098056062291977944, + 4 => 8751701111653162376, + 5 => 8299307866581014768, + 6 => 531487442231113133, + 7 => 800424898181663787, + 8 => 3572053471303813275, + 9 => 5820132396104405712, + 10 => 5231045148117457196, + 11 => 8794966624701729985, + 12 => 4426083511481337255, + 13 => 6684150774213996097, + 14 => 6195586616970758831, + 15 => 4540547196657677940, + 16 => 3176763528575786006, + 17 => 3016704037695981978, + 18 => 6744125090676683568, + 19 => 7314666039928310381, + 20 => 6776756960956103555, + 21 => 2819577541088759121, + 22 => 808394245928260098, + 23 => 7623836821615698462, + 24 => 9181513438115673210, + 25 => 4841213581850083788, + 26 => 1702688065381194280, + 27 => 5055789798320038739, + 28 => 3380105065975209047, + 29 => 4599440295762820917, + 30 => 9177830387841628385, + 31 => 6834565555694329853, + 32 => 8205003401511565956, + 33 => 270865630133328421, + 34 => 3209340440005987283, + 35 => 6274281444150890611, + 36 => 6624332540249377414, + 37 => 2587527519812771104, + 38 => 7332647062080436425, + 39 => 5005771960338902691, + 40 => 3468699152815339036, + 41 => 1049194951778930910, + 42 => 3935584475848725335, + 43 => 9085753827045089064, + 44 => 9005661771391728938, + 45 => 5200913379481214357, + 46 => 3232030195284048767, + 47 => 7473765017672637593, + 48 => 8372990784366189599, + 49 => 900533435598411787, + ), + 33 => + array ( + 0 => 8594859403573976797, + 1 => 1056469051697018343, + 2 => 7981819807984249663, + 3 => 6700723553123759699, + 4 => 7901581591457502220, + 5 => 1310800509390325152, + 6 => 499750275117256505, + 7 => 1071702412840450245, + 8 => 3633047946110581667, + 9 => 6585929724644917875, + 10 => 3416110053601876100, + 11 => 4136922603327478165, + 12 => 1198179981647639256, + 13 => 5364443461276255613, + 14 => 2584348601509212450, + 15 => 5120324315937730250, + 16 => 522836777497002224, + 17 => 552034138319415545, + 18 => 4587724350149825427, + 19 => 5710345816705425453, + 20 => 2214162093303859919, + 21 => 7551406637754997327, + 22 => 801129753984345927, + 23 => 1694443285187760235, + 24 => 1607467601520276272, + 25 => 2446055389537726020, + 26 => 1269354020728556364, + 27 => 7711661596009824915, + 28 => 9071667294651872920, + 29 => 4913652187114691065, + 30 => 3050691115879208133, + 31 => 6934534687990289192, + 32 => 6067912219752470094, + 33 => 8220841418446711910, + 34 => 972116675376438178, + 35 => 1344284661582616006, + 36 => 1513685892785327687, + 37 => 164825221202889849, + 38 => 68873197765246129, + 39 => 6777363252419567909, + 40 => 825244377168104549, + 41 => 2681971304420594537, + 42 => 3883311224134497028, + 43 => 2672973131901906080, + 44 => 6820460877454352312, + 45 => 3037320540320603458, + 46 => 3664002611712155615, + 47 => 6952694747682406149, + 48 => 2596464038043400667, + 49 => 7366177260837591685, + ), + 34 => + array ( + 0 => 292350062840987499, + 1 => 7344240818539750031, + 2 => 1609631560856080955, + 3 => 7228986135093966777, + 4 => 8608191835886862036, + 5 => 1163066080309899072, + 6 => 70532433777463622, + 7 => 924319004301312418, + 8 => 2140581494331315574, + 9 => 7169352686836314056, + 10 => 638443241571752306, + 11 => 853780255615345105, + 12 => 1739147682244541523, + 13 => 8567050146095229043, + 14 => 4779753847101001513, + 15 => 5875986820860264955, + 16 => 4779366679965644631, + 17 => 3400913573370824391, + 18 => 6562992562650988324, + 19 => 1686033803026870867, + 20 => 3253521295475704978, + 21 => 5825470707331639173, + 22 => 1796220638478598798, + 23 => 7270350964116185434, + 24 => 991647800200356728, + 25 => 3606088830973856550, + 26 => 443145163029070989, + 27 => 7183190472191538757, + 28 => 15901392383005115, + 29 => 5758362578091923125, + 30 => 8971571545023320382, + 31 => 1971232438561079318, + 32 => 1430868415766661534, + 33 => 3332871680198107404, + 34 => 7743844596465737135, + 35 => 6711974713948763843, + 36 => 7944739695979211083, + 37 => 3612624505570525766, + 38 => 6683708597460602577, + 39 => 4247736755630511721, + 40 => 448594595469079611, + 41 => 4045026055920591555, + 42 => 2292968395929078788, + 43 => 6306296644449068379, + 44 => 3706306833466702788, + 45 => 6665090138939911651, + 46 => 7888274755113851365, + 47 => 6086132437729850665, + 48 => 3839356044209629608, + 49 => 985183048708961512, + ), + 35 => + array ( + 0 => 3325224500063830265, + 1 => 8065460522493303762, + 2 => 2150977162718404844, + 3 => 2513095501676670864, + 4 => 8233290220021652200, + 5 => 8463376693561504977, + 6 => 6027691299865433222, + 7 => 4331413006856090578, + 8 => 2113829432426161123, + 9 => 1835559938513239524, + 10 => 3760589369569864168, + 11 => 1322344057131535225, + 12 => 3357990062355066404, + 13 => 4121143615077418688, + 14 => 7327001177941823182, + 15 => 9173437920051811149, + 16 => 7016399979746488251, + 17 => 1850523048176725335, + 18 => 3983576576818988739, + 19 => 5840312242176026548, + 20 => 1259214195820192258, + 21 => 6447647888325876110, + 22 => 4470490824147858804, + 23 => 6784267568304408636, + 24 => 821472665125293996, + 25 => 664019338943997056, + 26 => 4076926184150142025, + 27 => 4319387561386893749, + 28 => 8201171442534002237, + 29 => 5644788835480447906, + 30 => 2649447979175035529, + 31 => 2468996022215338736, + 32 => 5728463485280084198, + 33 => 8394329974141246714, + 34 => 1352483190536383684, + 35 => 8736243844338540400, + 36 => 790017721772835965, + 37 => 6338377763947825681, + 38 => 4264781310792118564, + 39 => 2705874621155713282, + 40 => 2593771310831765496, + 41 => 6634404399979259606, + 42 => 1294697555944562153, + 43 => 7529861579645268978, + 44 => 2078202749952120215, + 45 => 1396951686711735132, + 46 => 7171446141795263687, + 47 => 1516242630777191319, + 48 => 2210141497417437861, + 49 => 2744225556700338124, + ), + 36 => + array ( + 0 => 4600032135784053351, + 1 => 6713153269903552616, + 2 => 1524432997499412451, + 3 => 9085663892181184132, + 4 => 5140890333166193573, + 5 => 6415370570842750449, + 6 => 605209017130950974, + 7 => 778544994861874783, + 8 => 3713470051168290047, + 9 => 6851658133011496782, + 10 => 7521089360523036379, + 11 => 55470468240461548, + 12 => 4424723957480851091, + 13 => 3847530157992312256, + 14 => 8823616067821477758, + 15 => 8222436034397097533, + 16 => 2778414665128527248, + 17 => 7369251459457795788, + 18 => 4071388805764854010, + 19 => 4287747405081384406, + 20 => 6793671831132673172, + 21 => 3114148111762093180, + 22 => 384788139844541605, + 23 => 8249529710893372789, + 24 => 2157714201190551670, + 25 => 3314092267069056806, + 26 => 5433532917470695439, + 27 => 4060442883699127140, + 28 => 8771445583039981773, + 29 => 660319066543258122, + 30 => 1439816300286172314, + 31 => 7548093856347548369, + 32 => 1176802104448457513, + 33 => 2151633963287867818, + 34 => 7069149822341864080, + 35 => 3586634261392759215, + 36 => 2115719774775525555, + 37 => 3750140748264703566, + 38 => 5440490869326034310, + 39 => 5562736766219798862, + 40 => 8671375179968291392, + 41 => 499467303889880326, + 42 => 3052671933762117788, + 43 => 4654562233905362880, + 44 => 246193436748305982, + 45 => 6081020084404882682, + 46 => 1890761200179428934, + 47 => 847208909396042167, + 48 => 660301253897618064, + 49 => 6043165264656513894, + ), + 37 => + array ( + 0 => 6757639381915063463, + 1 => 4555324169353791928, + 2 => 8453433396043507231, + 3 => 7367975479284018964, + 4 => 2045542141502434678, + 5 => 2417676962307568300, + 6 => 186796869025639582, + 7 => 6246410055497566153, + 8 => 5849524973108094068, + 9 => 7106427541300957855, + 10 => 7262467490409045349, + 11 => 2625620544100850907, + 12 => 5171818870073954920, + 13 => 4623173366355609469, + 14 => 6630131502569099114, + 15 => 440063833278550381, + 16 => 6849776851137649663, + 17 => 2923628760438352403, + 18 => 5161648914320285977, + 19 => 9012034856361683200, + 20 => 4809767844500753446, + 21 => 635793370674531070, + 22 => 8444782472750918628, + 23 => 625119645145838644, + 24 => 1711050135195889262, + 25 => 6050146214025761878, + 26 => 2961578381937462178, + 27 => 4950745578012323237, + 28 => 8058763614061937163, + 29 => 6706111980476478039, + 30 => 5630792510411418295, + 31 => 8540298519551869302, + 32 => 5260738543895906421, + 33 => 5752971984351257314, + 34 => 1029160814473166884, + 35 => 1070704459591052615, + 36 => 3163994785792740338, + 37 => 1720500792367303567, + 38 => 4882806516087351766, + 39 => 8332253764632666699, + 40 => 5408659686407782182, + 41 => 5747746595516938623, + 42 => 3091560264708997801, + 43 => 5031101333688462343, + 44 => 8668987231476535653, + 45 => 5512602314817607471, + 46 => 7034669407608555298, + 47 => 2753102589796145363, + 48 => 1040884794404786919, + 49 => 3301428685472618392, + ), + 38 => + array ( + 0 => 5806979224365754074, + 1 => 1823229587162456230, + 2 => 2409728391803915007, + 3 => 3100954313368161394, + 4 => 6748311504469801793, + 5 => 2356721932680740467, + 6 => 2902595942118706670, + 7 => 9051556021252197471, + 8 => 8962436333537015158, + 9 => 1517751113887997394, + 10 => 7225950201013444459, + 11 => 2546087118437497882, + 12 => 4762377893858011208, + 13 => 779517291694285424, + 14 => 591839542358627284, + 15 => 5367686008521738386, + 16 => 783759746685788842, + 17 => 6116167441793213306, + 18 => 365815152326631591, + 19 => 8538677316958860389, + 20 => 5763153366233756599, + 21 => 5172130961491337193, + 22 => 6476396834007136821, + 23 => 6029836709564845298, + 24 => 408859033973015916, + 25 => 2209578524914201998, + 26 => 1127460121968579950, + 27 => 4246938370582055905, + 28 => 7297154488844828783, + 29 => 887923979202184226, + 30 => 3797040851850177616, + 31 => 8422393952121634468, + 32 => 20093478403899945, + 33 => 4886029618655351877, + 34 => 1864670258343019198, + 35 => 6183162109700895400, + 36 => 8119022212844878386, + 37 => 6370184042463066692, + 38 => 2157074710623202259, + 39 => 5195630894378703024, + 40 => 8360267727451888827, + 41 => 5613959301389216697, + 42 => 9200021631961180630, + 43 => 3011698433767435578, + 44 => 2646864648891248756, + 45 => 875594231654088324, + 46 => 5829254964574199142, + 47 => 5122073606137572977, + 48 => 6311992841960630320, + 49 => 3643912288953336149, + ), + 39 => + array ( + 0 => 2016566775146603089, + 1 => 8155079696619330380, + 2 => 2349389095752690292, + 3 => 4151708271097970529, + 4 => 5956829747782558827, + 5 => 8010100026456592115, + 6 => 2505786602051303335, + 7 => 4300295331001627854, + 8 => 6463313572684094538, + 9 => 3188827801685229116, + 10 => 7166293507027402765, + 11 => 5308514333273976656, + 12 => 4329555584359014245, + 13 => 5827346015406135785, + 14 => 6082988395039652687, + 15 => 2040516223980318253, + 16 => 2355417926414154923, + 17 => 4421881569720670438, + 18 => 1254048473373079942, + 19 => 143797357942920964, + 20 => 927159441847638900, + 21 => 9125656825790404665, + 22 => 3124874662529471393, + 23 => 237811715952482964, + 24 => 4997756459605186732, + 25 => 7348703874299424624, + 26 => 3127587511289385911, + 27 => 1838541085162730889, + 28 => 4971047307131513593, + 29 => 4496474097197643920, + 30 => 367900424813379579, + 31 => 6775300006857949729, + 32 => 7619709866842274577, + 33 => 2775169379458413451, + 34 => 5284696862615186585, + 35 => 98821901901066233, + 36 => 17527048592384211, + 37 => 4082619363789565760, + 38 => 1364416818122311591, + 39 => 8702556109554689709, + 40 => 7532793199729130150, + 41 => 4429646224542368281, + 42 => 8073534022127423854, + 43 => 6676615686384802225, + 44 => 919929188796408866, + 45 => 286463990828219372, + 46 => 8005591956436239675, + 47 => 3561122318417636367, + 48 => 7141613405689295298, + 49 => 4674503104866902121, + ), + 40 => + array ( + 0 => 3813891411377184995, + 1 => 7632928882910031881, + 2 => 1768137945981350311, + 3 => 6967634063234579249, + 4 => 2797976415019455260, + 5 => 7699168543238033240, + 6 => 7432355352439791743, + 7 => 3645360421841482982, + 8 => 8718943265536988858, + 9 => 7860220420066677853, + 10 => 4763524853016814130, + 11 => 746840427234091900, + 12 => 5163828695552919478, + 13 => 1914579265302922277, + 14 => 689044418060530410, + 15 => 3618489164297541063, + 16 => 61740947671020857, + 17 => 69467365830002889, + 18 => 2671124054414225336, + 19 => 6973968054449700665, + 20 => 5299840293325810092, + 21 => 4406112937255197737, + 22 => 7381541188822397852, + 23 => 3851762677347053740, + 24 => 7774469060249240663, + 25 => 6570726928612528603, + 26 => 3395723399289035971, + 27 => 93132544109401309, + 28 => 4181666026703237142, + 29 => 4173220807245945620, + 30 => 1233658091284053386, + 31 => 7500417950540395060, + 32 => 5162558696816248917, + 33 => 4738704079029496989, + 34 => 5295465061085161680, + 35 => 2592686572074882270, + 36 => 5178062672665528753, + 37 => 3178681861046476155, + 38 => 8142717135943049359, + 39 => 7783366835369323136, + 40 => 4456894141017658808, + 41 => 7842953574286147550, + 42 => 6810660917429813178, + 43 => 1554967904521840183, + 44 => 2713208514599819836, + 45 => 8532571816947514100, + 46 => 2089061501092911099, + 47 => 5321751266006750346, + 48 => 4895100493652081813, + 49 => 7234927795872127236, + ), + 41 => + array ( + 0 => 570088149450372795, + 1 => 1164232867661654054, + 2 => 2051552701357333167, + 3 => 5748419436007641146, + 4 => 7783682776645521105, + 5 => 6819552605786190114, + 6 => 7318884777199516126, + 7 => 5612491547583719061, + 8 => 763362897480930168, + 9 => 2463527279394751288, + 10 => 2611183880210585068, + 11 => 2986123354152687234, + 12 => 536686882744942805, + 13 => 5785237831191992332, + 14 => 2353350353624615635, + 15 => 461563559902372411, + 16 => 3175928654331211916, + 17 => 8080764706949616609, + 18 => 1410104464983705407, + 19 => 3262924143780834586, + 20 => 302129610278924770, + 21 => 3322796116246466854, + 22 => 4557521703859536401, + 23 => 3106092074572377085, + 24 => 4061779031248296017, + 25 => 1052804171374219391, + 26 => 2933561146296323762, + 27 => 1547346571335950347, + 28 => 6670520874064353354, + 29 => 283284163449062547, + 30 => 5896134536118292404, + 31 => 2209186875345740824, + 32 => 7444825709765803627, + 33 => 8953938364148905200, + 34 => 529033946703848914, + 35 => 7617202261886253770, + 36 => 9002313668944222518, + 37 => 4420623052744828643, + 38 => 2815968007325006508, + 39 => 2897293259084092647, + 40 => 7647576686317618160, + 41 => 7857788746596263074, + 42 => 6613528439398696350, + 43 => 4549926203279553663, + 44 => 415197260070137892, + 45 => 353967515100095697, + 46 => 1581348573539914790, + 47 => 4575328744085652766, + 48 => 5652419264096532247, + 49 => 7564682658483551039, + ), + 42 => + array ( + 0 => 3368031215514411691, + 1 => 6403987853643742458, + 2 => 7829107364149630452, + 3 => 2525493651196271018, + 4 => 2605186910814181395, + 5 => 757360054341229592, + 6 => 4614500769664121530, + 7 => 2773523826174019485, + 8 => 785515330228690897, + 9 => 1678326496442532536, + 10 => 4767587424228283278, + 11 => 1566791991750875373, + 12 => 4194593116166275321, + 13 => 101803838930387949, + 14 => 6025597034517161086, + 15 => 9039555063501814438, + 16 => 7516747679772046036, + 17 => 7337849071970777609, + 18 => 7048274146685765803, + 19 => 2490912036172562890, + 20 => 1430944179399369897, + 21 => 925714316602446056, + 22 => 213612474969070880, + 23 => 8424214291092099066, + 24 => 8128177527341340913, + 25 => 8877880304739835373, + 26 => 7348226364467796897, + 27 => 7912285523981974709, + 28 => 6684442297345397749, + 29 => 8027317014132309176, + 30 => 2949299310666118859, + 31 => 4606455232748928322, + 32 => 537190475010555064, + 33 => 6794274034248069286, + 34 => 4905834084409988810, + 35 => 5856514390852846329, + 36 => 676904489229921322, + 37 => 7847259829335428809, + 38 => 603314800360596006, + 39 => 7638251964110811153, + 40 => 1371516712379551103, + 41 => 455159667265152652, + 42 => 2852656949187768322, + 43 => 8754867695377231411, + 44 => 4566890769600741779, + 45 => 6528607434570235134, + 46 => 5624469704540827131, + 47 => 8228007257195895475, + 48 => 7630458432617230689, + 49 => 2078246302561992743, + ), + 43 => + array ( + 0 => 3249494586804550869, + 1 => 7910382034839657896, + 2 => 5902349133912960872, + 3 => 8762482164340726662, + 4 => 1417781288491979680, + 5 => 844189552183628289, + 6 => 105413071487348004, + 7 => 8048457843956318159, + 8 => 5673311589853157392, + 9 => 6394325769311212492, + 10 => 4670193095650677760, + 11 => 8507743819188635864, + 12 => 139715791730778277, + 13 => 2828117459503705560, + 14 => 341500275608800353, + 15 => 3179155592867479561, + 16 => 3844098293834220289, + 17 => 6312133208951470938, + 18 => 4244537775216380097, + 19 => 3534019033218030565, + 20 => 531876127245908088, + 21 => 6356952659942689745, + 22 => 6903196638771436642, + 23 => 6264480881865375007, + 24 => 3043474304227856010, + 25 => 7204330142071132784, + 26 => 3258647143114570790, + 27 => 37530236043757607, + 28 => 8551339878345091900, + 29 => 3299033420331349394, + 30 => 1978400900541748461, + 31 => 4540820036346133652, + 32 => 5769958510090889842, + 33 => 4938302165368205991, + 34 => 2113170122425780104, + 35 => 3098919758489925708, + 36 => 7470625425112337200, + 37 => 3975646892811123257, + 38 => 1645901075149631834, + 39 => 9070335151525780158, + 40 => 321397114888729766, + 41 => 4858454928755911814, + 42 => 3981083195911464670, + 43 => 1395664503592753426, + 44 => 7609480547019305390, + 45 => 3144647483655816046, + 46 => 5367370826883676615, + 47 => 5691916664283445633, + 48 => 4097634742120685165, + 49 => 6206863622131666366, + ), + 44 => + array ( + 0 => 4639603337079770337, + 1 => 6548770155749868574, + 2 => 2398214353718128319, + 3 => 7769879793141674175, + 4 => 6653347574412558883, + 5 => 7662016057612580511, + 6 => 7398655524569027645, + 7 => 6851643413417681191, + 8 => 767893431760439718, + 9 => 7062231732012763354, + 10 => 1298522423205031553, + 11 => 9195400374449744963, + 12 => 7494564530328238174, + 13 => 4099393498420092502, + 14 => 7660141477782417762, + 15 => 7571561870936051249, + 16 => 2033832064371960684, + 17 => 1357940161911104815, + 18 => 4527552584379370680, + 19 => 4920386769880277627, + 20 => 7886876756994925893, + 21 => 5832098476387845665, + 22 => 5512731950665409254, + 23 => 2043217959321410708, + 24 => 2083802338082358116, + 25 => 1384683054614545685, + 26 => 4839557418307744826, + 27 => 5661331976091812887, + 28 => 4804139735155990158, + 29 => 8840757128394199723, + 30 => 6994106153308457833, + 31 => 6154002278329028450, + 32 => 1087677666205341750, + 33 => 244141496875194498, + 34 => 1749204419113682048, + 35 => 421165274352980092, + 36 => 5872506030742618511, + 37 => 1200060348916246255, + 38 => 3454491290431278359, + 39 => 1219257449633606432, + 40 => 4229125875404665514, + 41 => 6551830068625350328, + 42 => 5372851860553691735, + 43 => 6345832380887692654, + 44 => 3971581644890599026, + 45 => 866215337358616677, + 46 => 6222937111762488779, + 47 => 4170730568607952413, + 48 => 4775735845523718382, + 49 => 7698396865646553849, + ), + 45 => + array ( + 0 => 6761524961601885404, + 1 => 3414416428243153487, + 2 => 3476888942937972132, + 3 => 7718425341207686339, + 4 => 8419186405106913149, + 5 => 8181554895640464429, + 6 => 4539063265997904631, + 7 => 1341007880286744441, + 8 => 6587067129391736831, + 9 => 7595362866891711786, + 10 => 2619190805311603421, + 11 => 5894249203443113671, + 12 => 5146393704896672701, + 13 => 4168043008359189211, + 14 => 9192964933144107984, + 15 => 3826491577932810596, + 16 => 7457298538606960883, + 17 => 4631755125743095501, + 18 => 161044355279124271, + 19 => 3010202514295283717, + 20 => 8059680371160325698, + 21 => 6595863138615917742, + 22 => 7386436897584571650, + 23 => 4701072006369199271, + 24 => 5996028913549021625, + 25 => 1385370845897888047, + 26 => 1397103345833615254, + 27 => 6535851722157254355, + 28 => 5421499465701269131, + 29 => 4306592338655903107, + 30 => 2593858251430297414, + 31 => 2205415075000559542, + 32 => 7461708601258226738, + 33 => 6407679053280239442, + 34 => 2564946185453642548, + 35 => 6475278799604297299, + 36 => 6152028983295708415, + 37 => 4983683457561829607, + 38 => 1178635810974040117, + 39 => 5759567876755428883, + 40 => 1322053749775238205, + 41 => 555619562242405497, + 42 => 6807341749451169414, + 43 => 7728241440378147950, + 44 => 5319688939770816921, + 45 => 8330530233447972957, + 46 => 6805864273766790458, + 47 => 6855715184295822442, + 48 => 2702091193560632332, + 49 => 4825710705888288991, + ), + 46 => + array ( + 0 => 7913695790750124353, + 1 => 8653387588604917050, + 2 => 4501536607873700808, + 3 => 2843454544178669405, + 4 => 7545222646060711543, + 5 => 5801657953352687385, + 6 => 3498405413117909679, + 7 => 2011575796963252385, + 8 => 5578854291917523326, + 9 => 1365804745939196076, + 10 => 1357356852128713007, + 11 => 2068518443315367314, + 12 => 5421584461688818784, + 13 => 9198502025719176988, + 14 => 8520230833383963213, + 15 => 5976540176867322880, + 16 => 484326789728010926, + 17 => 8808985841675418815, + 18 => 5659291383374410947, + 19 => 4861489845790677877, + 20 => 4055288565625686302, + 21 => 4161104036273753697, + 22 => 7529431841640584049, + 23 => 3780989685567154300, + 24 => 6401764981519833149, + 25 => 1197899620746247058, + 26 => 3863318676314471965, + 27 => 8795731749285657215, + 28 => 7747084535925253860, + 29 => 2655012824519025259, + 30 => 7682684206889080095, + 31 => 6025078434347081324, + 32 => 1615255103987886735, + 33 => 2259104565619831085, + 34 => 8709526609996605559, + 35 => 3216850528061239271, + 36 => 7915732078002834192, + 37 => 1720325163337754822, + 38 => 4501251746367269130, + 39 => 1025003535384033006, + 40 => 4493455961601113968, + 41 => 7225901443203618256, + 42 => 6616030311715042976, + 43 => 8669939462384114992, + 44 => 2383786445621405332, + 45 => 6929317520695291133, + 46 => 8937147448915996388, + 47 => 912539491837161693, + 48 => 6697268964085367094, + 49 => 8420369060589102425, + ), + 47 => + array ( + 0 => 3700850510367048260, + 1 => 8461232830075913452, + 2 => 4033262673722395063, + 3 => 7356544964393530878, + 4 => 2888676921803219667, + 5 => 7156299039118135574, + 6 => 8202406955783229598, + 7 => 2478528791317009258, + 8 => 4041439523008240005, + 9 => 6517161524906758763, + 10 => 7560096391704973842, + 11 => 6918879401634146730, + 12 => 401187887246168573, + 13 => 3195446384835002696, + 14 => 3367835345440063506, + 15 => 5116864212349157532, + 16 => 7218461634201387045, + 17 => 5906860779382038858, + 18 => 1319187094568417224, + 19 => 646696649961507734, + 20 => 6775047794651263682, + 21 => 9210598354519468496, + 22 => 814342682513204669, + 23 => 5028007859672831065, + 24 => 8673166025973962669, + 25 => 7143844887140264089, + 26 => 7149779640084513266, + 27 => 5327255293644503614, + 28 => 7740041835523082539, + 29 => 7157231891001033180, + 30 => 6880425606155238561, + 31 => 616395685636568226, + 32 => 1506343751416503448, + 33 => 6764045249172563223, + 34 => 4152136705025707998, + 35 => 3882959765415441419, + 36 => 3429371676537325573, + 37 => 96371800125123629, + 38 => 2044610538400451737, + 39 => 5934883755423690609, + 40 => 3088928761050459291, + 41 => 9166606688652394872, + 42 => 3172278448305727210, + 43 => 6258859854653782146, + 44 => 2370253363001932727, + 45 => 888613293568417738, + 46 => 566878523618938599, + 47 => 6807770796752629799, + 48 => 5268390502059375586, + 49 => 1369560507235967025, + ), + 48 => + array ( + 0 => 5969062734480091517, + 1 => 4258635298619589230, + 2 => 1239915403139647092, + 3 => 5090551864665397530, + 4 => 4983253304814937482, + 5 => 615571454853585349, + 6 => 1591783394356870228, + 7 => 3856456619073176967, + 8 => 4163682545845256068, + 9 => 6190387025904069066, + 10 => 6778629022847096049, + 11 => 7466609877102224863, + 12 => 1943975059967845995, + 13 => 7095909378083018591, + 14 => 2455897788796317876, + 15 => 5856674661467767271, + 16 => 2508324967447032828, + 17 => 1353417238913596355, + 18 => 4084639979570954526, + 19 => 3989307496196329143, + 20 => 3632865819435603525, + 21 => 8882842337316352089, + 22 => 7977727799862244196, + 23 => 806283432102605935, + 24 => 1545497087649195615, + 25 => 3557464393355285756, + 26 => 1703046612747353147, + 27 => 6901053312597805596, + 28 => 3193951683541817674, + 29 => 7142082117921271648, + 30 => 535259647425267513, + 31 => 1896436629335605015, + 32 => 4301705775874893248, + 33 => 8739743395429789192, + 34 => 6384324837173611357, + 35 => 6184503691135320603, + 36 => 332261142286366890, + 37 => 1207159795507319703, + 38 => 6098310336187064859, + 39 => 7657813151254044828, + 40 => 5694573187490330619, + 41 => 4325278518662817383, + 42 => 7159800258223705431, + 43 => 7983853345760488509, + 44 => 3245495653004469177, + 45 => 3887580662207195375, + 46 => 5890827052852695685, + 47 => 128559302317612711, + 48 => 3228480891169079160, + 49 => 1174439836486132859, + ), + 49 => + array ( + 0 => 4864039696068472075, + 1 => 8834124669575979344, + 2 => 5652678881475382548, + 3 => 2379635065223177717, + 4 => 293009543092963596, + 5 => 7945471327883416577, + 6 => 6689198029790926423, + 7 => 1921885854372196611, + 8 => 7525825208427230268, + 9 => 6893487881916577839, + 10 => 2286912634732295118, + 11 => 3638013130157988052, + 12 => 3440656755054773459, + 13 => 86991074017549167, + 14 => 6849234719062098564, + 15 => 368261341327680170, + 16 => 2398309716273344165, + 17 => 1995084157513314738, + 18 => 8199815484722779866, + 19 => 4555122545756624787, + 20 => 8438849263629566283, + 21 => 2220359438567702571, + 22 => 8177509177722963039, + 23 => 2999854020616151054, + 24 => 6824704403354290481, + 25 => 6275807546493426104, + 26 => 9090799789147344934, + 27 => 7949534743488954263, + 28 => 5624411741410589483, + 29 => 7277332826188252059, + 30 => 6459979453897856951, + 31 => 1648740848473399197, + 32 => 403884512548425336, + 33 => 4874507963546786037, + 34 => 1320751360825837637, + 35 => 8588053377246754896, + 36 => 1046831925576638044, + 37 => 7651453133008971076, + 38 => 5081048334666394086, + 39 => 8573555156262460241, + 40 => 2011704186137013088, + 41 => 7716460786009597267, + 42 => 8041214376909827919, + 43 => 2860046413430702208, + 44 => 8698080270427320899, + 45 => 7104210477142509900, + 46 => 2288000021596943068, + 47 => 9032553461555290826, + 48 => 1211098135104524011, + 49 => 494075524174801193, + ), + ); +} diff --git a/tests/PHPStan/Analyser/data/bug-12327.php b/tests/PHPStan/Analyser/data/bug-12327.php new file mode 100644 index 0000000000..7a61985ff6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12327.php @@ -0,0 +1,18 @@ +', $value); - return iterator_to_array($value); - } - - assertType('mixed~array', $value); - - throw new \LogicException(); - } -} diff --git a/tests/PHPStan/Analyser/data/bug-12512.php b/tests/PHPStan/Analyser/data/bug-12512.php new file mode 100644 index 0000000000..612927a31a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12512.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug12512; + +enum FooBarEnum: string +{ + case CASE_ONE = 'case_one'; + case CASE_TWO = 'case_two'; + case CASE_THREE = 'case_three'; + case CASE_FOUR = 'case_four'; + + public function matchFunction(): string + { + return match ($this) { + self::CASE_ONE => 'one', + self::CASE_TWO => 'two', + default => throw new \Exception( + sprintf('"%s" is not implemented yet', get_debug_type($this)) + ) + }; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12549.php b/tests/PHPStan/Analyser/data/bug-12549.php new file mode 100644 index 0000000000..e1bd8c5f0c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12549.php @@ -0,0 +1,17 @@ +bar(self::OPTION_ROUNDING_MODE); + } + + private function bar(string $v): void + { + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12627.php b/tests/PHPStan/Analyser/data/bug-12627.php new file mode 100644 index 0000000000..ce75be8225 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12627.php @@ -0,0 +1,17 @@ +b(); + } + + private function b(): void + { + } +} + +$c = new class() {}; diff --git a/tests/PHPStan/Analyser/data/bug-12671.php b/tests/PHPStan/Analyser/data/bug-12671.php new file mode 100644 index 0000000000..8066316400 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12671.php @@ -0,0 +1,1198 @@ + [], + // Angola. + 'AO' => [], + // Argentina. + 'AR' => [ + 'C' => ['Ciudad Autónoma de Buenos Aires', 'Ciudad Autónoma de Buenos Aires', NULL], + 'B' => ['Buenos Aires', 'Buenos Aires', NULL], + 'K' => ['Catamarca', 'Catamarca', NULL], + 'H' => ['Chaco', 'Chaco', NULL], + 'U' => ['Chubut', 'Chubut', NULL], + 'X' => ['Córdoba', 'Córdoba', NULL], + 'W' => ['Corrientes', 'Corrientes', NULL], + 'E' => ['Entre Ríos', 'Entre Ríos', NULL], + 'P' => ['Formosa', 'Formosa', NULL], + 'Y' => ['Jujuy', 'Jujuy', NULL], + 'L' => ['La Pampa', 'La Pampa', NULL], + 'F' => ['La Rioja', 'La Rioja', NULL], + 'M' => ['Mendoza', 'Mendoza', NULL], + 'N' => ['Misiones', 'Misiones', NULL], + 'Q' => ['Neuquén', 'Neuquén', NULL], + 'R' => ['Río Negro', 'Río Negro', NULL], + 'A' => ['Salta', 'Salta', NULL], + 'J' => ['San Juan', 'San Juan', NULL], + 'D' => ['San Luis', 'San Luis', NULL], + 'Z' => ['Santa Cruz', 'Santa Cruz', NULL], + 'S' => ['Santa Fe', 'Santa Fe', NULL], + 'G' => ['Santiago del Estero', 'Santiago del Estero', NULL], + 'V' => ['Tierra del Fuego', 'Tierra del Fuego', NULL], + 'T' => ['Tucumán', 'Tucumán', NULL], + ], + // Austria. + 'AT' => [], + // Australia. + 'AU' => [ + 'ACT' => ['ACT', 'Australian Capital Territory', NULL], + 'NSW' => ['NSW', 'New South Wales', NULL], + 'NT' => ['NT', 'Northern Territory', NULL], + 'QLD' => ['QLD', 'Queensland', NULL], + 'SA' => ['SA', 'South Australia', NULL], + 'TAS' => ['TAS', 'Tasmania', NULL], + 'VIC' => ['VIC', 'Victoria', NULL], + 'WA' => ['WA', 'Western Australia', NULL], + // [ 'JBT', 'Jervis Bay Territory', NULL ], + ], + // Aland Islands. + 'AX' => [], + // Bangladesh. + 'BD' => [], + // Belgium. + 'BE' => [], + // Bulgaria. + 'BG' => [], + // Bahrain. + 'BH' => [], + // Burundi. + 'BI' => [], + // Benin. + 'BJ' => [], + // Bolivia. + 'BO' => [], + // Brazil. + 'BR' => [ + 'AC' => ['AC', 'Acre', NULL], + 'AL' => ['AL', 'Alagoas', NULL], + 'AP' => ['AP', 'Amapá', NULL], + 'AM' => ['AM', 'Amazonas', NULL], + 'BA' => ['BA', 'Bahia', NULL], + 'CE' => ['CE', 'Ceará', NULL], + 'DF' => ['DF', 'Distrito Federal', NULL], + 'ES' => ['ES', 'Espírito Santo', NULL], + 'GO' => ['GO', 'Goiás', NULL], + 'MA' => ['MA', 'Maranhão', NULL], + 'MT' => ['MT', 'Mato Grosso', NULL], + 'MS' => ['MS', 'Mato Grosso do Sul', NULL], + 'MG' => ['MG', 'Minas Gerais', NULL], + 'PA' => ['PA', 'Pará', NULL], + 'PB' => ['PB', 'Paraíba', NULL], + 'PR' => ['PR', 'Paraná', NULL], + 'PE' => ['PE', 'Pernambuco', NULL], + 'PI' => ['PI', 'Piauí', NULL], + 'RJ' => ['RJ', 'Rio de Janeiro', NULL], + 'RN' => ['RN', 'Rio Grande do Norte', NULL], + 'RS' => ['RS', 'Rio Grande do Sul', NULL], + 'RO' => ['RO', 'Rondônia', NULL], + 'RR' => ['RR', 'Roraima', NULL], + 'SC' => ['SC', 'Santa Catarina', NULL], + 'SP' => ['SP', 'São Paulo', NULL], + 'SE' => ['SE', 'Sergipe', NULL], + 'TO' => ['TO', 'Tocantins', NULL], + ], + // Canada. + 'CA' => [ + 'AB' => ['AB', 'Alberta', 'Alberta'], + 'BC' => ['BC', 'British Columbia', 'Colombie-Britannique'], + 'MB' => ['MB', 'Manitoba', 'Manitoba'], + 'NB' => ['NB', 'New Brunswick', 'Nouveau-Brunswick'], + 'NL' => ['NL', 'Newfoundland and Labrador', 'Terre-Neuve-et-Labrador'], + 'NT' => ['NT', 'Northwest Territories', 'Territoires du Nord-Ouest'], + 'NS' => ['NS', 'Nova Scotia', 'Nouvelle-Écosse'], + 'NU' => ['NU', 'Nunavut', 'Nunavut'], + 'ON' => ['ON', 'Ontario', 'Ontario'], + 'PE' => ['PE', 'Prince Edward Island', 'Île-du-Prince-Édouard'], + 'QC' => ['QC', 'Quebec', 'Québec'], + 'SK' => ['SK', 'Saskatchewan', 'Saskatchewan'], + 'YT' => ['YT', 'Yukon', 'Yukon'], + ], + // Switzerland. + 'CH' => [], + // China. + 'CN' => [ + 'CN1' => ['Yunnan Sheng', 'Yunnan Sheng', '云南省'], + 'CN2' => ['Beijing Shi', 'Beijing Shi', '北京市'], + 'CN3' => ['Tianjin Shi', 'Tianjin Shi', '天津市'], + 'CN4' => ['Hebei Sheng', 'Hebei Sheng', '河北省'], + 'CN5' => ['Shanxi Sheng', 'Shanxi Sheng', '山西省'], + 'CN6' => ['Neimenggu Zizhiqu', 'Neimenggu Zizhiqu', '内蒙古'], + 'CN7' => ['Liaoning Sheng', 'Liaoning Sheng', '辽宁省'], + 'CN8' => ['Jilin Sheng', 'Jilin Sheng', '吉林省'], + 'CN9' => ['Heilongjiang Sheng', 'Heilongjiang Sheng', '黑龙江省'], + 'CN10' => ['Shanghai Shi', 'Shanghai Shi', '上海市'], + 'CN11' => ['Jiangsu Sheng', 'Jiangsu Sheng', '江苏省'], + 'CN12' => ['Zhejiang Sheng', 'Zhejiang Sheng', '浙江省'], + 'CN13' => ['Anhui Sheng', 'Anhui Sheng', '安徽省'], + 'CN14' => ['Fujian Sheng', 'Fujian Sheng', '福建省'], + 'CN15' => ['Jiangxi Sheng', 'Jiangxi Sheng', '江西省'], + 'CN16' => ['Shandong Sheng', 'Shandong Sheng', '山东省'], + 'CN17' => ['Henan Sheng', 'Henan Sheng', '河南省'], + 'CN18' => ['Hubei Sheng', 'Hubei Sheng', '湖北省'], + 'CN19' => ['Hunan Sheng', 'Hunan Sheng', '湖南省'], + 'CN20' => ['Guangdong Sheng', 'Guangdong Sheng', '广东省'], + 'CN21' => ['Guangxi Zhuangzuzizhiqu', 'Guangxi Zhuangzuzizhiqu', '广西'], + 'CN22' => ['Hainan Sheng', 'Hainan Sheng', '海南省'], + 'CN23' => ['Chongqing Shi', 'Chongqing Shi', '重庆市'], + 'CN24' => ['Sichuan Sheng', 'Sichuan Sheng', '四川省'], + 'CN25' => ['Guizhou Sheng', 'Guizhou Sheng', '贵州省'], + 'CN26' => ['Shaanxi Sheng', 'Shaanxi Sheng', '陕西省'], + 'CN27' => ['Gansu Sheng', 'Gansu Sheng', '甘肃省'], + 'CN28' => ['Qinghai Sheng', 'Qinghai Sheng', '青海省'], + 'CN29' => ['Ningxia Huizuzizhiqu', 'Ningxia Huizuzizhiqu', '宁夏'], + 'CN30' => ['Macau', 'Macau', '澳门'], + 'CN31' => ['Xizang Zizhiqu', 'Xizang Zizhiqu', '西藏'], + 'CN32' => ['Xinjiang Weiwuerzizhiqu', 'Xinjiang Weiwuerzizhiqu', '新疆'], + // [ 'Taiwan', 'Taiwan', '台湾' ], + // [ 'Hong Kong', 'Hong Kong', '香港' ], + ], + // Czech Republic. + 'CZ' => [], + // Germany. + 'DE' => [], + // Denmark. + 'DK' => [], + // Dominican Republic. + 'DO' => [], + // Algeria. + 'DZ' => [], + // Estonia. + 'EE' => [], + // Egypt. + 'EG' => [ + 'EGALX' => ['Alexandria Governorate', 'Alexandria Governorate', 'الإسكندرية'], + 'EGASN' => ['Aswan Governorate', 'Aswan Governorate', 'أسوان'], + 'EGAST' => ['Asyut Governorate', 'Asyut Governorate', 'أسيوط'], + 'EGBA' => ['Red Sea Governorate', 'Red Sea Governorate', 'البحر الأحمر'], + 'EGBH' => ['El Beheira Governorate', 'El Beheira Governorate', 'البحيرة'], + 'EGBNS' => ['Beni Suef Governorate', 'Beni Suef Governorate', 'بني سويف'], + 'EGC' => ['Cairo Governorate', 'Cairo Governorate', 'القاهرة'], + 'EGDK' => ['Dakahlia Governorate', 'Dakahlia Governorate', 'الدقهلية'], + 'EGDT' => ['Damietta Governorate', 'Damietta Governorate', 'دمياط'], + 'EGFYM' => ['Faiyum Governorate', 'Faiyum Governorate', 'الفيوم'], + 'EGGH' => ['Gharbia Governorate', 'Gharbia Governorate', 'الغربية'], + 'EGGZ' => ['Giza Governorate', 'Giza Governorate', 'الجيزة'], + 'EGIS' => ['Ismailia Governorate', 'Ismailia Governorate', 'الإسماعيلية'], + 'EGJS' => ['South Sinai Governorate', 'South Sinai Governorate', 'جنوب سيناء'], + 'EGKB' => ['Qalyubia Governorate', 'Qalyubia Governorate', 'القليوبية'], + 'EGKFS' => ['Kafr El Sheikh Governorate', 'Kafr El Sheikh Governorate', 'كفر الشيخ'], + 'EGKN' => ['Qena Governorate', 'Qena Governorate', 'قنا'], + 'EGLX' => ['Luxor Governorate', 'Luxor Governorate', 'الأقصر'], + 'EGMN' => ['Menia Governorate', 'Menia Governorate', 'المنيا'], + 'EGMNF' => ['Menofia Governorate', 'Menofia Governorate', 'المنوفية'], + 'EGMT' => ['Matrouh Governorate', 'Matrouh Governorate', 'مطروح'], + 'EGPTS' => ['Port Said Governorate', 'Port Said Governorate', 'بورسعيد'], + 'EGSHG' => ['Sohag Governorate', 'Sohag Governorate', 'سوهاج'], + 'EGSHR' => ['Ash Sharqia Governorate', 'Ash Sharqia Governorate', 'الشرقية'], + 'EGSIN' => ['North Sinai Governorate', 'North Sinai Governorate', 'شمال سيناء'], + 'EGSUZ' => ['Suez Governorate', 'Suez Governorate', 'السويس'], + 'EGWAD' => ['New Valley Governorate', 'New Valley Governorate', 'الوادي الجديد'], + ], + // Spain. + 'ES' => [ + 'C' => ['A Coruña', 'A Coruña', NULL], + 'VI' => ['Álava', 'Álava', NULL], + 'AB' => ['Albacete', 'Albacete', NULL], + 'A' => ['Alicante', 'Alicante', NULL], + 'AL' => ['Almería', 'Almería', NULL], + 'O' => ['Asturias', 'Asturias', NULL], + 'AV' => ['Ávila', 'Ávila', NULL], + 'BA' => ['Badajoz', 'Badajoz', NULL], + 'PM' => ['Balears', 'Balears', NULL], + 'B' => ['Barcelona', 'Barcelona', NULL], + 'BU' => ['Burgos', 'Burgos', NULL], + 'CC' => ['Cáceres', 'Cáceres', NULL], + 'CA' => ['Cádiz', 'Cádiz', NULL], + 'S' => ['Cantabria', 'Cantabria', NULL], + 'CS' => ['Castellón', 'Castellón', NULL], + 'CE' => ['Ceuta', 'Ceuta', NULL], + 'CR' => ['Ciudad Real', 'Ciudad Real', NULL], + 'CO' => ['Córdoba', 'Córdoba', NULL], + 'CU' => ['Cuenca', 'Cuenca', NULL], + 'GI' => ['Girona', 'Girona', NULL], + 'GR' => ['Granada', 'Granada', NULL], + 'GU' => ['Guadalajara', 'Guadalajara', NULL], + 'SS' => ['Guipúzcoa', 'Guipúzcoa', NULL], + 'H' => ['Huelva', 'Huelva', NULL], + 'HU' => ['Huesca', 'Huesca', NULL], + 'J' => ['Jaén', 'Jaén', NULL], + 'LO' => ['La Rioja', 'La Rioja', NULL], + 'GC' => ['Las Palmas', 'Las Palmas', NULL], + 'LE' => ['León', 'León', NULL], + 'L' => ['Lleida', 'Lleida', NULL], + 'LU' => ['Lugo', 'Lugo', NULL], + 'M' => ['Madrid', 'Madrid', NULL], + 'MA' => ['Málaga', 'Málaga', NULL], + 'ML' => ['Melilla', 'Melilla', NULL], + 'MU' => ['Murcia', 'Murcia', NULL], + 'NA' => ['Navarra', 'Navarra', NULL], + 'OR' => ['Ourense', 'Ourense', NULL], + 'P' => ['Palencia', 'Palencia', NULL], + 'PO' => ['Pontevedra', 'Pontevedra', NULL], + 'SA' => ['Salamanca', 'Salamanca', NULL], + 'TF' => ['Santa Cruz de Tenerife', 'Santa Cruz de Tenerife', NULL], + 'SG' => ['Segovia', 'Segovia', NULL], + 'SE' => ['Sevilla', 'Sevilla', NULL], + 'SO' => ['Soria', 'Soria', NULL], + 'T' => ['Tarragona', 'Tarragona', NULL], + 'TE' => ['Teruel', 'Teruel', NULL], + 'TO' => ['Toledo', 'Toledo', NULL], + 'V' => ['Valencia', 'Valencia', NULL], + 'VA' => ['Valladolid', 'Valladolid', NULL], + 'BI' => ['Vizcaya', 'Vizcaya', NULL], + 'ZA' => ['Zamora', 'Zamora', NULL], + 'Z' => ['Zaragoza', 'Zaragoza', NULL], + ], + // Finland. + 'FI' => [], + // France. + 'FR' => [], + // French Guiana. + 'GF' => [], + // Ghana. + 'GH' => [], + // Guadeloupe. + 'GP' => [], + // Greece. + 'GR' => [], + // Guatemala. + 'GT' => [], + // Hong Kong. + 'HK' => [ + 'HONG KONG' => ['Hong Kong Island', 'Hong Kong Island', '香港島'], + 'KOWLOON' => ['Kowloon', 'Kowloon', '九龍'], + 'NEW TERRITORIES' => ['New Territories', 'New Territories', '新界'], + ], + // Hungary. + 'HU' => [], + // Indonesia. + 'ID' => [ + 'AC' => ['Aceh', 'Aceh', NULL], + 'SU' => ['Sumatera Utara', 'Sumatera Utara', NULL], + 'SB' => ['Sumatera Barat', 'Sumatera Barat', NULL], + 'RI' => ['Riau', 'Riau', NULL], + 'KR' => ['Kepulauan Riau', 'Kepulauan Riau', NULL], + 'JA' => ['Jambi', 'Jambi', NULL], + 'SS' => ['Sumatera Selatan', 'Sumatera Selatan', NULL], + 'BB' => ['Kepulauan Bangka Belitung', 'Kepulauan Bangka Belitung', NULL], + 'BE' => ['Bengkulu', 'Bengkulu', NULL], + 'LA' => ['Lampung', 'Lampung', NULL], + 'JK' => ['DKI Jakarta', 'DKI Jakarta', NULL], + 'JB' => ['Jawa Barat', 'Jawa Barat', NULL], + 'BT' => ['Banten', 'Banten', NULL], + 'JT' => ['Jawa Tengah', 'Jawa Tengah', NULL], + 'JI' => ['Jawa Timur', 'Jawa Timur', NULL], + 'YO' => ['Daerah Istimewa Yogyakarta', 'Daerah Istimewa Yogyakarta', NULL], + 'BA' => ['Bali', 'Bali', NULL], + 'NB' => ['Nusa Tenggara Barat', 'Nusa Tenggara Barat', NULL], + 'NT' => ['Nusa Tenggara Timur', 'Nusa Tenggara Timur', NULL], + 'KB' => ['Kalimantan Barat', 'Kalimantan Barat', NULL], + 'KT' => ['Kalimantan Tengah', 'Kalimantan Tengah', NULL], + 'KI' => ['Kalimantan Timur', 'Kalimantan Timur', NULL], + 'KS' => ['Kalimantan Selatan', 'Kalimantan Selatan', NULL], + 'KU' => ['Kalimantan Utara', 'Kalimantan Utara', NULL], + 'SA' => ['Sulawesi Utara', 'Sulawesi Utara', NULL], + 'ST' => ['Sulawesi Tengah', 'Sulawesi Tengah', NULL], + 'SG' => ['Sulawesi Tenggara', 'Sulawesi Tenggara', NULL], + 'SR' => ['Sulawesi Barat', 'Sulawesi Barat', NULL], + 'SN' => ['Sulawesi Selatan', 'Sulawesi Selatan', NULL], + 'GO' => ['Gorontalo', 'Gorontalo', NULL], + 'MA' => ['Maluku', 'Maluku', NULL], + 'MU' => ['Maluku Utara', 'Maluku Utara', NULL], + 'PA' => ['Papua', 'Papua', NULL], + 'PB' => ['Papua Barat', 'Papua Barat', NULL], + // [ 'Kalimantan Tengah', 'Kalimantan Tengah', NULL ], + // [ 'Kalimantan Timur', 'Kalimantan Timur', NULL ], + ], + // Ireland. + 'IE' => [ + 'CW' => ['Co. Carlow', 'Co. Carlow', NULL], + 'CN' => ['Co. Cavan', 'Co. Cavan', NULL], + 'CE' => ['Co. Clare', 'Co. Clare', NULL], + 'CO' => ['Co. Cork', 'Co. Cork', NULL], + 'DL' => ['Co. Donegal', 'Co. Donegal', NULL], + 'D' => ['Co. Dublin', 'Co. Dublin', NULL], + 'G' => ['Co. Galway', 'Co. Galway', NULL], + 'KY' => ['Co. Kerry', 'Co. Kerry', NULL], + 'KE' => ['Co. Kildare', 'Co. Kildare', NULL], + 'KK' => ['Co. Kilkenny', 'Co. Kilkenny', NULL], + 'LS' => ['Co. Laois', 'Co. Laois', NULL], + 'LM' => ['Co. Leitrim', 'Co. Leitrim', NULL], + 'LK' => ['Co. Limerick', 'Co. Limerick', NULL], + 'LD' => ['Co. Longford', 'Co. Longford', NULL], + 'LH' => ['Co. Louth', 'Co. Louth', NULL], + 'MO' => ['Co. Mayo', 'Co. Mayo', NULL], + 'MH' => ['Co. Meath', 'Co. Meath', NULL], + 'MN' => ['Co. Monaghan', 'Co. Monaghan', NULL], + 'OY' => ['Co. Offaly', 'Co. Offaly', NULL], + 'RN' => ['Co. Roscommon', 'Co. Roscommon', NULL], + 'SO' => ['Co. Sligo', 'Co. Sligo', NULL], + 'TA' => ['Co. Tipperary', 'Co. Tipperary', NULL], + 'WD' => ['Co. Waterford', 'Co. Waterford', NULL], + 'WH' => ['Co. Westmeath', 'Co. Westmeath', NULL], + 'WX' => ['Co. Wexford', 'Co. Wexford', NULL], + 'WW' => ['Co. Wicklow', 'Co. Wicklow', NULL], + ], + // Israel. + 'IL' => [], + // Isle of Man. + 'IM' => [], + // India. + 'IN' => [ + 'AP' => ['Andhra Pradesh', 'Andhra Pradesh', NULL], + 'AR' => ['Arunachal Pradesh', 'Arunachal Pradesh', NULL], + 'AS' => ['Assam', 'Assam', NULL], + 'BR' => ['Bihar', 'Bihar', NULL], + 'CT' => ['Chhattisgarh', 'Chhattisgarh', NULL], + 'GA' => ['Goa', 'Goa', NULL], + 'GJ' => ['Gujarat', 'Gujarat', NULL], + 'HR' => ['Haryana', 'Haryana', NULL], + 'HP' => ['Himachal Pradesh', 'Himachal Pradesh', NULL], + 'JK' => ['Jammu and Kashmir', 'Jammu & Kashmir', NULL], + 'JH' => ['Jharkhand', 'Jharkhand', NULL], + 'KA' => ['Karnataka', 'Karnataka', NULL], + 'KL' => ['Kerala', 'Kerala', NULL], + // 'LA' => __( 'Ladakh', 'woocommerce' ), + 'MP' => ['Madhya Pradesh', 'Madhya Pradesh', NULL], + 'MH' => ['Maharashtra', 'Maharashtra', NULL], + 'MN' => ['Manipur', 'Manipur', NULL], + 'ML' => ['Meghalaya', 'Meghalaya', NULL], + 'MZ' => ['Mizoram', 'Mizoram', NULL], + 'NL' => ['Nagaland', 'Nagaland', NULL], + 'OR' => ['Odisha', 'Odisha', NULL], + 'PB' => ['Punjab', 'Punjab', NULL], + 'RJ' => ['Rajasthan', 'Rajasthan', NULL], + 'SK' => ['Sikkim', 'Sikkim', NULL], + 'TN' => ['Tamil Nadu', 'Tamil Nadu', NULL], + 'TS' => ['Telangana', 'Telangana', NULL], + 'TR' => ['Tripura', 'Tripura', NULL], + 'UK' => ['Uttarakhand', 'Uttarakhand', NULL], + 'UP' => ['Uttar Pradesh', 'Uttar Pradesh', NULL], + 'WB' => ['West Bengal', 'West Bengal', NULL], + 'AN' => ['Andaman and Nicobar Islands', 'Andaman & Nicobar', NULL], + 'CH' => ['Chandigarh', 'Chandigarh', NULL], + 'DN' => ['Dadra and Nagar Haveli', 'Dadra & Nagar Haveli', NULL], + 'DD' => ['Daman and Diu', 'Daman & Diu', NULL], + 'DL' => ['Delhi', 'Delhi', NULL], + 'LD' => ['Lakshadweep', 'Lakshadweep', NULL], + 'PY' => ['Puducherry', 'Puducherry', NULL], + ], + // Iran. + 'IR' => [ + 'KHZ' => ['Khuzestan Province', 'Khuzestan Province', 'استان خوزستان'], + 'THR' => ['Tehran Province', 'Tehran Province', 'استان تهران'], + 'ILM' => ['Ilam Province', 'Ilam Province', 'استان ایلام'], + 'BHR' => ['Bushehr Province', 'Bushehr Province', 'استان بوشهر'], + 'ADL' => ['Ardabil Province', 'Ardabil Province', 'استان اردبیل'], + 'ESF' => ['Isfahan Province', 'Isfahan Province', 'استان اصفهان'], + 'YZD' => ['Yazd Province', 'Yazd Province', 'استان یزد'], + 'KRH' => ['Kermanshah Province', 'Kermanshah Province', 'استان کرمانشاه'], + 'KRN' => ['Kerman Province', 'Kerman Province', 'استان کرمان'], + 'HDN' => ['Hamadan Province', 'Hamadan Province', 'استان همدان'], + 'GZN' => ['Qazvin Province', 'Qazvin Province', 'استان قزوین'], + 'ZJN' => ['Zanjan Province', 'Zanjan Province', 'استان زنجان'], + 'LRS' => ['Lorestan Province', 'Lorestan Province', 'استان لرستان'], + 'ABZ' => ['Alborz Province', 'Alborz Province', 'استان البرز'], + 'EAZ' => ['East Azerbaijan Province', 'East Azerbaijan Province', 'استان آذربایجان شرقی'], + 'WAZ' => ['West Azerbaijan Province', 'West Azerbaijan Province', 'استان آذربایجان غربی'], + 'CHB' => ['Chaharmahal and Bakhtiari Province', 'Chaharmahal and Bakhtiari Province', 'استان چهارمحال و بختیاری'], + 'SKH' => ['South Khorasan Province', 'South Khorasan Province', 'استان خراسان جنوبی'], + 'RKH' => ['Razavi Khorasan Province', 'Razavi Khorasan Province', 'استان خراسان رضوی'], + 'NKH' => ['North Khorasan Province', 'North Khorasan Province', 'استان خراسان شمالی'], + 'SMN' => ['Semnan Province', 'Semnan Province', 'استان سمنان'], + 'FRS' => ['Fars Province', 'Fars Province', 'استان فارس'], + 'QHM' => ['Qom Province', 'Qom Province', 'استان قم'], + 'KRD' => ['Kurdistan Province', 'Kurdistan Province', 'استان کردستان'], + 'KBD' => ['Kohgiluyeh and Boyer-Ahmad Province', 'Kohgiluyeh and Boyer-Ahmad Province', 'استان کهگیلویه و بویراحمد'], + 'GLS' => ['Golestan Province', 'Golestan Province', 'استان گلستان'], + 'GIL' => ['Gilan Province', 'Gilan Province', 'استان گیلان'], + 'MZN' => ['Mazandaran Province', 'Mazandaran Province', 'استان مازندران'], + 'MKZ' => ['Markazi Province', 'Markazi Province', 'استان مرکزی'], + 'HRZ' => ['Hormozgan Province', 'Hormozgan Province', 'استان هرمزگان'], + 'SBN' => ['Sistan and Baluchestan Province', 'Sistan and Baluchestan Province', 'استان سیستان و بلوچستان'], + ], + // Iceland. + 'IS' => [], + // Italy. + 'IT' => [ + 'AG' => ['AG', 'Agrigento', NULL], + 'AL' => ['AL', 'Alessandria', NULL], + 'AN' => ['AN', 'Ancona', NULL], + 'AO' => ['AO', 'Aosta', NULL], + 'AR' => ['AR', 'Arezzo', NULL], + 'AP' => ['AP', 'Ascoli Piceno', NULL], + 'AT' => ['AT', 'Asti', NULL], + 'AV' => ['AV', 'Avellino', NULL], + 'BA' => ['BA', 'Bari', NULL], + 'BT' => ['BT', 'Barletta-Andria-Trani', NULL], + 'BL' => ['BL', 'Belluno', NULL], + 'BN' => ['BN', 'Benevento', NULL], + 'BG' => ['BG', 'Bergamo', NULL], + 'BI' => ['BI', 'Biella', NULL], + 'BO' => ['BO', 'Bologna', NULL], + 'BZ' => ['BZ', 'Bolzano', NULL], + 'BS' => ['BS', 'Brescia', NULL], + 'BR' => ['BR', 'Brindisi', NULL], + 'CA' => ['CA', 'Cagliari', NULL], + 'CL' => ['CL', 'Caltanissetta', NULL], + 'CB' => ['CB', 'Campobasso', NULL], + 'CE' => ['CE', 'Caserta', NULL], + 'CT' => ['CT', 'Catania', NULL], + 'CZ' => ['CZ', 'Catanzaro', NULL], + 'CH' => ['CH', 'Chieti', NULL], + 'CO' => ['CO', 'Como', NULL], + 'CS' => ['CS', 'Cosenza', NULL], + 'CR' => ['CR', 'Cremona', NULL], + 'KR' => ['KR', 'Crotone', NULL], + 'CN' => ['CN', 'Cuneo', NULL], + 'EN' => ['EN', 'Enna', NULL], + 'FM' => ['FM', 'Fermo', NULL], + 'FE' => ['FE', 'Ferrara', NULL], + 'FI' => ['FI', 'Firenze', NULL], + 'FG' => ['FG', 'Foggia', NULL], + 'FC' => ['FC', 'Forlì-Cesena', NULL], + 'FR' => ['FR', 'Frosinone', NULL], + 'GE' => ['GE', 'Genova', NULL], + 'GO' => ['GO', 'Gorizia', NULL], + 'GR' => ['GR', 'Grosseto', NULL], + 'IM' => ['IM', 'Imperia', NULL], + 'IS' => ['IS', 'Isernia', NULL], + 'SP' => ['SP', 'La Spezia', NULL], + 'AQ' => ['AQ', "L'Aquila", NULL], + 'LT' => ['LT', 'Latina', NULL], + 'LE' => ['LE', 'Lecce', NULL], + 'LC' => ['LC', 'Lecco', NULL], + 'LI' => ['LI', 'Livorno', NULL], + 'LO' => ['LO', 'Lodi', NULL], + 'LU' => ['LU', 'Lucca', NULL], + 'MC' => ['MC', 'Macerata', NULL], + 'MN' => ['MN', 'Mantova', NULL], + 'MS' => ['MS', 'Massa-Carrara', NULL], + 'MT' => ['MT', 'Matera', NULL], + 'ME' => ['ME', 'Messina', NULL], + 'MI' => ['MI', 'Milano', NULL], + 'MO' => ['MO', 'Modena', NULL], + 'MB' => ['MB', 'Monza e Brianza', NULL], + 'NA' => ['NA', 'Napoli', NULL], + 'NO' => ['NO', 'Novara', NULL], + 'NU' => ['NU', 'Nuoro', NULL], + 'OR' => ['OR', 'Oristano', NULL], + 'PD' => ['PD', 'Padova', NULL], + 'PA' => ['PA', 'Palermo', NULL], + 'PR' => ['PR', 'Parma', NULL], + 'PV' => ['PV', 'Pavia', NULL], + 'PG' => ['PG', 'Perugia', NULL], + 'PU' => ['PU', 'Pesaro e Urbino', NULL], + 'PE' => ['PE', 'Pescara', NULL], + 'PC' => ['PC', 'Piacenza', NULL], + 'PI' => ['PI', 'Pisa', NULL], + 'PT' => ['PT', 'Pistoia', NULL], + 'PN' => ['PN', 'Pordenone', NULL], + 'PZ' => ['PZ', 'Potenza', NULL], + 'PO' => ['PO', 'Prato', NULL], + 'RG' => ['RG', 'Ragusa', NULL], + 'RA' => ['RA', 'Ravenna', NULL], + 'RC' => ['RC', 'Reggio Calabria', NULL], + 'RE' => ['RE', 'Reggio Emilia', NULL], + 'RI' => ['RI', 'Rieti', NULL], + 'RN' => ['RN', 'Rimini', NULL], + 'RM' => ['RM', 'Roma', NULL], + 'RO' => ['RO', 'Rovigo', NULL], + 'SA' => ['SA', 'Salerno', NULL], + 'SS' => ['SS', 'Sassari', NULL], + 'SV' => ['SV', 'Savona', NULL], + 'SI' => ['SI', 'Siena', NULL], + 'SR' => ['SR', 'Siracusa', NULL], + 'SO' => ['SO', 'Sondrio', NULL], + 'SU' => ['SU', 'Sud Sardegna', NULL], + 'TA' => ['TA', 'Taranto', NULL], + 'TE' => ['TE', 'Teramo', NULL], + 'TR' => ['TR', 'Terni', NULL], + 'TO' => ['TO', 'Torino', NULL], + 'TP' => ['TP', 'Trapani', NULL], + 'TN' => ['TN', 'Trento', NULL], + 'TV' => ['TV', 'Treviso', NULL], + 'TS' => ['TS', 'Trieste', NULL], + 'UD' => ['UD', 'Udine', NULL], + 'VA' => ['VA', 'Varese', NULL], + 'VE' => ['VE', 'Venezia', NULL], + 'VB' => ['VB', 'Verbano-Cusio-Ossola', NULL], + 'VC' => ['VC', 'Vercelli', NULL], + 'VR' => ['VR', 'Verona', NULL], + 'VV' => ['VV', 'Vibo Valentia', NULL], + 'VI' => ['VI', 'Vicenza', NULL], + 'VT' => ['VT', 'Viterbo', NULL], + ], + // Jamaica. + 'JM' => [ + 'JM-01' => ['Kingston', 'Kingston', NULL], + 'JM-02' => ['St. Andrew', 'St. Andrew', NULL], + 'JM-03' => ['St. Thomas', 'St. Thomas', NULL], + 'JM-04' => ['Portland', 'Portland', NULL], + 'JM-05' => ['St. Mary', 'St. Mary', NULL], + 'JM-06' => ['St. Ann', 'St. Ann', NULL], + 'JM-07' => ['Trelawny', 'Trelawny', NULL], + 'JM-08' => ['St. James', 'St. James', NULL], + 'JM-09' => ['Hanover', 'Hanover', NULL], + 'JM-10' => ['Westmoreland', 'Westmoreland', NULL], + 'JM-11' => ['St. Elizabeth', 'St. Elizabeth', NULL], + 'JM-12' => ['Manchester', 'Manchester', NULL], + 'JM-13' => ['Clarendon', 'Clarendon', NULL], + 'JM-14' => ['St. Catherine', 'St. Catherine', NULL], + ], + // Japan. + 'JP' => [ + 'JP01' => ['Hokkaido', 'Hokkaido', '北海道'], + 'JP02' => ['Aomori', 'Aomori', '青森県'], + 'JP03' => ['Iwate', 'Iwate', '岩手県'], + 'JP04' => ['Miyagi', 'Miyagi', '宮城県'], + 'JP05' => ['Akita', 'Akita', '秋田県'], + 'JP06' => ['Yamagata', 'Yamagata', '山形県'], + 'JP07' => ['Fukushima', 'Fukushima', '福島県'], + 'JP08' => ['Ibaraki', 'Ibaraki', '茨城県'], + 'JP09' => ['Tochigi', 'Tochigi', '栃木県'], + 'JP10' => ['Gunma', 'Gunma', '群馬県'], + 'JP11' => ['Saitama', 'Saitama', '埼玉県'], + 'JP12' => ['Chiba', 'Chiba', '千葉県'], + 'JP13' => ['Tokyo', 'Tokyo', '東京都'], + 'JP14' => ['Kanagawa', 'Kanagawa', '神奈川県'], + 'JP15' => ['Niigata', 'Niigata', '新潟県'], + 'JP16' => ['Toyama', 'Toyama', '富山県'], + 'JP17' => ['Ishikawa', 'Ishikawa', '石川県'], + 'JP18' => ['Fukui', 'Fukui', '福井県'], + 'JP19' => ['Yamanashi', 'Yamanashi', '山梨県'], + 'JP20' => ['Nagano', 'Nagano', '長野県'], + 'JP21' => ['Gifu', 'Gifu', '岐阜県'], + 'JP22' => ['Shizuoka', 'Shizuoka', '静岡県'], + 'JP23' => ['Aichi', 'Aichi', '愛知県'], + 'JP24' => ['Mie', 'Mie', '三重県'], + 'JP25' => ['Shiga', 'Shiga', '滋賀県'], + 'JP26' => ['Kyoto', 'Kyoto', '京都府'], + 'JP27' => ['Osaka', 'Osaka', '大阪府'], + 'JP28' => ['Hyogo', 'Hyogo', '兵庫県'], + 'JP29' => ['Nara', 'Nara', '奈良県'], + 'JP30' => ['Wakayama', 'Wakayama', '和歌山県'], + 'JP31' => ['Tottori', 'Tottori', '鳥取県'], + 'JP32' => ['Shimane', 'Shimane', '島根県'], + 'JP33' => ['Okayama', 'Okayama', '岡山県'], + 'JP34' => ['Hiroshima', 'Hiroshima', '広島県'], + 'JP35' => ['Yamaguchi', 'Yamaguchi', '山口県'], + 'JP36' => ['Tokushima', 'Tokushima', '徳島県'], + 'JP37' => ['Kagawa', 'Kagawa', '香川県'], + 'JP38' => ['Ehime', 'Ehime', '愛媛県'], + 'JP39' => ['Kochi', 'Kochi', '高知県'], + 'JP40' => ['Fukuoka', 'Fukuoka', '福岡県'], + 'JP41' => ['Saga', 'Saga', '佐賀県'], + 'JP42' => ['Nagasaki', 'Nagasaki', '長崎県'], + 'JP43' => ['Kumamoto', 'Kumamoto', '熊本県'], + 'JP44' => ['Oita', 'Oita', '大分県'], + 'JP45' => ['Miyazaki', 'Miyazaki', '宮崎県'], + 'JP46' => ['Kagoshima', 'Kagoshima', '鹿児島県'], + 'JP47' => ['Okinawa', 'Okinawa', '沖縄県'], + ], + // Kenya. + 'KE' => [], + // South Korea. + 'KR' => [], + // Kuwait. + 'KW' => [], + // Laos. + 'LA' => [], + // Lebanon. + 'LB' => [], + // Sri Lanka. + 'LK' => [], + // Liberia. + 'LR' => [], + // Luxembourg. + 'LU' => [], + // Moldova. + 'MD' => [], + // Martinique. + 'MQ' => [], + // Malta. + 'MT' => [], + // Mexico. + 'MX' => [ + 'DF' => ['CDMX', 'Ciudad de México', NULL], + 'JA' => ['Jal.', 'Jalisco', NULL], + 'NL' => ['N.L.', 'Nuevo León', NULL], + 'AG' => ['Ags.', 'Aguascalientes', NULL], + 'BC' => ['B.C.', 'Baja California', NULL], + 'BS' => ['B.C.S.', 'Baja California Sur', NULL], + 'CM' => ['Camp.', 'Campeche', NULL], + 'CS' => ['Chis.', 'Chiapas', NULL], + 'CH' => ['Chih.', 'Chihuahua', NULL], + 'CO' => ['Coah.', 'Coahuila de Zaragoza', NULL], + 'CL' => ['Col.', 'Colima', NULL], + 'DG' => ['Dgo.', 'Durango', NULL], + 'GT' => ['Gto.', 'Guanajuato', NULL], + 'GR' => ['Gro.', 'Guerrero', NULL], + 'HG' => ['Hgo.', 'Hidalgo', NULL], + 'MX' => ['Méx.', 'Estado de México', NULL], + 'MI' => ['Mich.', 'Michoacán', NULL], + 'MO' => ['Mor.', 'Morelos', NULL], + 'NA' => ['Nay.', 'Nayarit', NULL], + 'OA' => ['Oax.', 'Oaxaca', NULL], + 'PU' => ['Pue.', 'Puebla', NULL], + 'QT' => ['Qro.', 'Querétaro', NULL], + 'QR' => ['Q.R.', 'Quintana Roo', NULL], + 'SL' => ['S.L.P.', 'San Luis Potosí', NULL], + 'SI' => ['Sin.', 'Sinaloa', NULL], + 'SO' => ['Son.', 'Sonora', NULL], + 'TB' => ['Tab.', 'Tabasco', NULL], + 'TM' => ['Tamps.', 'Tamaulipas', NULL], + 'TL' => ['Tlax.', 'Tlaxcala', NULL], + 'VE' => ['Ver.', 'Veracruz', NULL], + 'YU' => ['Yuc.', 'Yucatán', NULL], + 'ZA' => ['Zac.', 'Zacatecas', NULL], + ], + // Malaysia. + 'MY' => [ + 'JHR' => ['Johor', 'Johor', NULL], + 'KDH' => ['Kedah', 'Kedah', NULL], + 'KTN' => ['Kelantan', 'Kelantan', NULL], + 'LBN' => ['Labuan', 'Labuan', NULL], + 'MLK' => ['Melaka', 'Melaka', NULL], + 'NSN' => ['Negeri Sembilan', 'Negeri Sembilan', NULL], + 'PHG' => ['Pahang', 'Pahang', NULL], + 'PNG' => ['Pulau Pinang', 'Pulau Pinang', NULL], + 'PRK' => ['Perak', 'Perak', NULL], + 'PLS' => ['Perlis', 'Perlis', NULL], + 'SBH' => ['Sabah', 'Sabah', NULL], + 'SWK' => ['Sarawak', 'Sarawak', NULL], + 'SGR' => ['Selangor', 'Selangor', NULL], + 'TRG' => ['Terengganu', 'Terengganu', NULL], + 'PJY' => ['Putrajaya', 'Putrajaya', NULL], + 'KUL' => ['Kuala Lumpur', 'Kuala Lumpur', NULL], + ], + // Mozambique. + 'MZ' => [ + 'MZP' => ['Cabo Delgado', 'Cabo Delgado', NULL], + 'MZG' => ['Gaza', 'Gaza', NULL], + 'MZI' => ['Inhambane', 'Inhambane', NULL], + 'MZB' => ['Manica', 'Manica', NULL], + 'MZL' => ['Maputo', 'Maputo', NULL], + 'MZMPM' => ['Cidade de Maputo', 'Cidade de Maputo', NULL], + 'MZN' => ['Nampula', 'Nampula', NULL], + 'MZA' => ['Niassa', 'Niassa', NULL], + 'MZS' => ['Sofala', 'Sofala', NULL], + 'MZT' => ['Tete', 'Tete', NULL], + 'MZQ' => ['Zambezia', 'Zambezia', NULL], + ], + // Namibia. + 'NA' => [], + // Nigeria. + 'NG' => [ + 'AB' => ['Abia', 'Abia', NULL], + 'FC' => ['Federal Capital Territory', 'Federal Capital Territory', NULL], + 'AD' => ['Adamawa', 'Adamawa', NULL], + 'AK' => ['Akwa Ibom', 'Akwa Ibom', NULL], + 'AN' => ['Anambra', 'Anambra', NULL], + 'BA' => ['Bauchi', 'Bauchi', NULL], + 'BY' => ['Bayelsa', 'Bayelsa', NULL], + 'BE' => ['Benue', 'Benue', NULL], + 'BO' => ['Borno', 'Borno', NULL], + 'CR' => ['Cross River', 'Cross River', NULL], + 'DE' => ['Delta', 'Delta', NULL], + 'EB' => ['Ebonyi', 'Ebonyi', NULL], + 'ED' => ['Edo', 'Edo', NULL], + 'EK' => ['Ekiti', 'Ekiti', NULL], + 'EN' => ['Enugu', 'Enugu', NULL], + 'GO' => ['Gombe', 'Gombe', NULL], + 'IM' => ['Imo', 'Imo', NULL], + 'JI' => ['Jigawa', 'Jigawa', NULL], + 'KD' => ['Kaduna', 'Kaduna', NULL], + 'KN' => ['Kano', 'Kano', NULL], + 'KT' => ['Katsina', 'Katsina', NULL], + 'KE' => ['Kebbi', 'Kebbi', NULL], + 'KO' => ['Kogi', 'Kogi', NULL], + 'KW' => ['Kwara', 'Kwara', NULL], + 'LA' => ['Lagos', 'Lagos', NULL], + 'NA' => ['Nasarawa', 'Nasarawa', NULL], + 'NI' => ['Niger', 'Niger', NULL], + 'OG' => ['Ogun State', 'Ogun State', NULL], + 'ON' => ['Ondo', 'Ondo', NULL], + 'OS' => ['Osun', 'Osun', NULL], + 'OY' => ['Oyo', 'Oyo', NULL], + 'PL' => ['Plateau', 'Plateau', NULL], + 'RI' => ['Rivers', 'Rivers', NULL], + 'SO' => ['Sokoto', 'Sokoto', NULL], + 'TA' => ['Taraba', 'Taraba', NULL], + 'YO' => ['Yobe', 'Yobe', NULL], + 'ZA' => ['Zamfara', 'Zamfara', NULL], + ], + // Netherlands. + 'NL' => [], + // Norway. + 'NO' => [], + // Nepal. + 'NP' => [], + // New Zealand. + 'NZ' => [], + // Peru. + 'PE' => [ + 'CAL' => ['Callao', 'Callao', NULL], + 'LMA' => ['Municipalidad Metropolitana de Lima', 'Municipalidad Metropolitana de Lima', NULL], + 'AMA' => ['Amazonas', 'Amazonas', NULL], + 'ANC' => ['Áncash', 'Áncash', NULL], + 'APU' => ['Apurímac', 'Apurímac', NULL], + 'ARE' => ['Arequipa', 'Arequipa', NULL], + 'AYA' => ['Ayacucho', 'Ayacucho', NULL], + 'CAJ' => ['Cajamarca', 'Cajamarca', NULL], + 'CUS' => ['Cuzco', 'Cuzco', NULL], + 'HUV' => ['Huancavelica', 'Huancavelica', NULL], + 'HUC' => ['Huánuco', 'Huánuco', NULL], + 'ICA' => ['Ica', 'Ica', NULL], + 'JUN' => ['Junín', 'Junín', NULL], + 'LAL' => ['La Libertad', 'La Libertad', NULL], + 'LAM' => ['Lambayeque', 'Lambayeque', NULL], + 'LIM' => ['Gobierno Regional de Lima', 'Gobierno Regional de Lima', NULL], + 'LOR' => ['Loreto', 'Loreto', NULL], + 'MDD' => ['Madre de Dios', 'Madre de Dios', NULL], + 'MOQ' => ['Moquegua', 'Moquegua', NULL], + 'PAS' => ['Pasco', 'Pasco', NULL], + 'PIU' => ['Piura', 'Piura', NULL], + 'PUN' => ['Puno', 'Puno', NULL], + 'SAM' => ['San Martín', 'San Martín', NULL], + 'TAC' => ['Tacna', 'Tacna', NULL], + 'TUM' => ['Tumbes', 'Tumbes', NULL], + 'UCA' => ['Ucayali', 'Ucayali', NULL], + ], + // Philippines. + 'PH' => [ + 'ABR' => ['Abra', 'Abra', NULL], + 'AGN' => ['Agusan del Norte', 'Agusan del Norte', NULL], + 'AGS' => ['Agusan del Sur', 'Agusan del Sur', NULL], + 'AKL' => ['Aklan', 'Aklan', NULL], + 'ALB' => ['Albay', 'Albay', NULL], + 'ANT' => ['Antique', 'Antique', NULL], + 'APA' => ['Apayao', 'Apayao', NULL], + 'AUR' => ['Aurora', 'Aurora', NULL], + 'BAS' => ['Basilan', 'Basilan', NULL], + 'BAN' => ['Bataan', 'Bataan', NULL], + 'BTN' => ['Batanes', 'Batanes', NULL], + 'BTG' => ['Batangas', 'Batangas', NULL], + 'BEN' => ['Benguet', 'Benguet', NULL], + 'BIL' => ['Biliran', 'Biliran', NULL], + 'BOH' => ['Bohol', 'Bohol', NULL], + 'BUK' => ['Bukidnon', 'Bukidnon', NULL], + 'BUL' => ['Bulacan', 'Bulacan', NULL], + 'CAG' => ['Cagayan', 'Cagayan', NULL], + 'CAN' => ['Camarines Norte', 'Camarines Norte', NULL], + 'CAS' => ['Camarines Sur', 'Camarines Sur', NULL], + 'CAM' => ['Camiguin', 'Camiguin', NULL], + 'CAP' => ['Capiz', 'Capiz', NULL], + 'CAT' => ['Catanduanes', 'Catanduanes', NULL], + 'CAV' => ['Cavite', 'Cavite', NULL], + 'CEB' => ['Cebu', 'Cebu', NULL], + 'COM' => ['Compostela Valley', 'Compostela Valley', NULL], + 'NCO' => ['Cotabato', 'Cotabato', NULL], + 'DAV' => ['Davao del Norte', 'Davao del Norte', NULL], + 'DAS' => ['Davao del Sur', 'Davao del Sur', NULL], + 'DAC' => ['Davao Occidental', 'Davao Occidental', NULL], + 'DAO' => ['Davao Oriental', 'Davao Oriental', NULL], + 'DIN' => ['Dinagat Islands', 'Dinagat Islands', NULL], + 'EAS' => ['Eastern Samar', 'Eastern Samar', NULL], + 'GUI' => ['Guimaras', 'Guimaras', NULL], + 'IFU' => ['Ifugao', 'Ifugao', NULL], + 'ILN' => ['Ilocos Norte', 'Ilocos Norte', NULL], + 'ILS' => ['Ilocos Sur', 'Ilocos Sur', NULL], + 'ILI' => ['Iloilo', 'Iloilo', NULL], + 'ISA' => ['Isabela', 'Isabela', NULL], + 'KAL' => ['Kalinga', 'Kalinga', NULL], + 'LUN' => ['La Union', 'La Union', NULL], + 'LAG' => ['Laguna', 'Laguna', NULL], + 'LAN' => ['Lanao del Norte', 'Lanao del Norte', NULL], + 'LAS' => ['Lanao del Sur', 'Lanao del Sur', NULL], + 'LEY' => ['Leyte', 'Leyte', NULL], + 'MAG' => ['Maguindanao', 'Maguindanao', NULL], + 'MAD' => ['Marinduque', 'Marinduque', NULL], + 'MAS' => ['Masbate', 'Masbate', NULL], + 'MSC' => ['Misamis Occidental', 'Misamis Occidental', NULL], + 'MSR' => ['Misamis Oriental', 'Misamis Oriental', NULL], + 'MOU' => ['Mountain Province', 'Mountain Province', NULL], + 'NEC' => ['Negros Occidental', 'Negros Occidental', NULL], + 'NER' => ['Negros Oriental', 'Negros Oriental', NULL], + 'NSA' => ['Northern Samar', 'Northern Samar', NULL], + 'NUE' => ['Nueva Ecija', 'Nueva Ecija', NULL], + 'NUV' => ['Nueva Vizcaya', 'Nueva Vizcaya', NULL], + 'MDC' => ['Mindoro Occidental', 'Mindoro Occidental', NULL], + 'MDR' => ['Mindoro Oriental', 'Mindoro Oriental', NULL], + 'PLW' => ['Palawan', 'Palawan', NULL], + 'PAM' => ['Pampanga', 'Pampanga', NULL], + 'PAN' => ['Pangasinan', 'Pangasinan', NULL], + 'QUE' => ['Quezon Province', 'Quezon Province', NULL], + 'QUI' => ['Quirino', 'Quirino', NULL], + 'RIZ' => ['Rizal', 'Rizal', NULL], + 'ROM' => ['Romblon', 'Romblon', NULL], + 'WSA' => ['Samar', 'Samar', NULL], + 'SAR' => ['Sarangani', 'Sarangani', NULL], + 'SIQ' => ['Siquijor', 'Siquijor', NULL], + 'SOR' => ['Sorsogon', 'Sorsogon', NULL], + 'SCO' => ['South Cotabato', 'South Cotabato', NULL], + 'SLE' => ['Southern Leyte', 'Southern Leyte', NULL], + 'SUK' => ['Sultan Kudarat', 'Sultan Kudarat', NULL], + 'SLU' => ['Sulu', 'Sulu', NULL], + 'SUN' => ['Surigao del Norte', 'Surigao del Norte', NULL], + 'SUR' => ['Surigao del Sur', 'Surigao del Sur', NULL], + 'TAR' => ['Tarlac', 'Tarlac', NULL], + 'TAW' => ['Tawi-Tawi', 'Tawi-Tawi', NULL], + 'ZMB' => ['Zambales', 'Zambales', NULL], + 'ZAN' => ['Zamboanga del Norte', 'Zamboanga del Norte', NULL], + 'ZAS' => ['Zamboanga del Sur', 'Zamboanga del Sur', NULL], + 'ZSI' => ['Zamboanga Sibuguey', 'Zamboanga Sibuguey', NULL], + '00' => ['Metro Manila', 'Metro Manila', NULL], + ], + // Pakistan. + 'PK' => [], + // Poland. + 'PL' => [], + // Puerto Rico. + 'PR' => [], + // Portugal. + 'PT' => [], + // Paraguay. + 'PY' => [], + // Reunion. + 'RE' => [], + // Romania. + 'RO' => [], + // Serbia. + 'RS' => [], + // Sweden. + 'SE' => [], + // Singapore. + 'SG' => [], + // Slovenia. + 'SI' => [], + // Slovakia. + 'SK' => [], + // Thailand. + 'TH' => [ + 'TH-37' => ['Amnat Charoen', 'Amnat Charoen', 'อำนาจเจริญ'], + 'TH-15' => ['Ang Thong', 'Ang Thong', 'อ่างทอง'], + 'TH-14' => ['Phra Nakhon Si Ayutthaya', 'Phra Nakhon Si Ayutthaya', 'พระนครศรีอยุธยา'], + 'TH-10' => ['Bangkok', 'Bangkok', 'กรุงเทพมหานคร'], + 'TH-38' => ['Bueng Kan', 'Bueng Kan', 'จังหวัด บึงกาฬ'], + 'TH-31' => ['Buri Ram', 'Buri Ram', 'บุรีรัมย์'], + 'TH-24' => ['Chachoengsao', 'Chachoengsao', 'ฉะเชิงเทรา'], + 'TH-18' => ['Chai Nat', 'Chai Nat', 'ชัยนาท'], + 'TH-36' => ['Chaiyaphum', 'Chaiyaphum', 'ชัยภูมิ'], + 'TH-22' => ['Chanthaburi', 'Chanthaburi', 'จันทบุรี'], + 'TH-50' => ['Chiang Rai', 'Chiang Rai', 'เชียงราย'], + 'TH-57' => ['Chiang Mai', 'Chiang Mai', 'เชียงใหม่'], + 'TH-20' => ['Chon Buri', 'Chon Buri', 'ชลบุรี'], + 'TH-86' => ['Chumpon', 'Chumpon', 'ชุมพร'], + 'TH-46' => ['Kalasin', 'Kalasin', 'กาฬสินธุ์'], + 'TH-62' => ['Kamphaeng Phet', 'Kamphaeng Phet', 'กำแพงเพชร'], + 'TH-71' => ['Kanchanaburi', 'Kanchanaburi', 'กาญจนบุรี'], + 'TH-40' => ['Khon Kaen', 'Khon Kaen', 'ขอนแก่น'], + 'TH-81' => ['Krabi', 'Krabi', 'กระบี่'], + 'TH-52' => ['Lampang', 'Lampang', 'ลำปาง'], + 'TH-51' => ['Lamphun', 'Lamphun', 'ลำพูน'], + 'TH-42' => ['Loei', 'Loei', 'เลย'], + 'TH-16' => ['Lop Buri', 'Lop Buri', 'ลพบุรี'], + 'TH-58' => ['Mae Hong Son', 'Mae Hong Son', 'แม่ฮ่องสอน'], + 'TH-44' => ['Maha Sarakham', 'Maha Sarakham', 'มหาสารคาม'], + 'TH-49' => ['Mukdahan', 'Mukdahan', 'มุกดาหาร'], + 'TH-26' => ['Nakhon Nayok', 'Nakhon Nayok', 'นครนายก'], + 'TH-73' => ['Nakhon Pathom', 'Nakhon Pathom', 'นครปฐม'], + 'TH-48' => ['Nakhon Phanom', 'Nakhon Phanom', 'นครพนม'], + 'TH-30' => ['Nakhon Ratchasima', 'Nakhon Ratchasima', 'นครราชสีมา'], + 'TH-60' => ['Nakhon Sawan', 'Nakhon Sawan', 'นครสวรรค์'], + 'TH-80' => ['Nakhon Si Thammarat', 'Nakhon Si Thammarat', 'นครศรีธรรมราช'], + 'TH-55' => ['Nan', 'Nan', 'น่าน'], + 'TH-96' => ['Narathiwat', 'Narathiwat', 'นราธิวาส'], + 'TH-39' => ['Nong Bua Lam Phu', 'Nong Bua Lam Phu', 'หนองบัวลำภู'], + 'TH-43' => ['Nong Khai', 'Nong Khai', 'หนองคาย'], + 'TH-12' => ['Nonthaburi', 'Nonthaburi', 'นนทบุรี'], + 'TH-13' => ['Pathum Thani', 'Pathum Thani', 'ปทุมธานี'], + 'TH-94' => ['Pattani', 'Pattani', 'ปัตตานี'], + 'TH-82' => ['Phang Nga', 'Phang Nga', 'พังงา'], + 'TH-93' => ['Phattalung', 'Phattalung', 'พัทลุง'], + 'TH-56' => ['Phayao', 'Phayao', 'พะเยา'], + 'TH-67' => ['Phetchabun', 'Phetchabun', 'เพชรบูรณ์'], + 'TH-76' => ['Phetchaburi', 'Phetchaburi', 'เพชรบุรี'], + 'TH-66' => ['Phichit', 'Phichit', 'พิจิตร'], + 'TH-65' => ['Phitsanulok', 'Phitsanulok', 'พิษณุโลก'], + 'TH-54' => ['Phrae', 'Phrae', 'แพร่'], + 'TH-83' => ['Phuket', 'Phuket', 'ภูเก็ต'], + 'TH-25' => ['Prachin Buri', 'Prachin Buri', 'ปราจีนบุรี'], + 'TH-77' => ['Prachuap Khiri Khan', 'Prachuap Khiri Khan', 'ประจวบคีรีขันธ์'], + 'TH-85' => ['Ranong', 'Ranong', 'ระนอง'], + 'TH-70' => ['Ratchaburi', 'Ratchaburi', 'ราชบุรี'], + 'TH-21' => ['Rayong', 'Rayong', 'ระยอง'], + 'TH-45' => ['Roi Et', 'Roi Et', 'ร้อยเอ็ด'], + 'TH-27' => ['Sa Kaeo', 'Sa Kaeo', 'สระแก้ว'], + 'TH-47' => ['Sakon Nakhon', 'Sakon Nakhon', 'สกลนคร'], + 'TH-11' => ['Samut Prakan', 'Samut Prakan', 'สมุทรปราการ'], + 'TH-74' => ['Samut Sakhon', 'Samut Sakhon', 'สมุทรสาคร'], + 'TH-75' => ['Samut Songkhram', 'Samut Songkhram', 'สมุทรสงคราม'], + 'TH-19' => ['Saraburi', 'Saraburi', 'สระบุรี'], + 'TH-91' => ['Satun', 'Satun', 'สตูล'], + 'TH-17' => ['Sing Buri', 'Sing Buri', 'สิงห์บุรี'], + 'TH-33' => ['Si Sa Ket', 'Si Sa Ket', 'ศรีสะเกษ'], + 'TH-90' => ['Songkhla', 'Songkhla', 'สงขลา'], + 'TH-64' => ['Sukhothai', 'Sukhothai', 'สุโขทัย'], + 'TH-72' => ['Suphanburi', 'Suphanburi', 'สุพรรณบุรี'], + 'TH-84' => ['Surat Thani', 'Surat Thani', 'สุราษฎร์ธานี'], + 'TH-32' => ['Surin', 'Surin', 'สุรินทร์'], + 'TH-63' => ['Tak', 'Tak', 'ตาก'], + 'TH-92' => ['Trang', 'Trang', 'ตรัง'], + 'TH-23' => ['Trat', 'Trat', 'ตราด'], + 'TH-34' => ['Ubon Ratchathani', 'Ubon Ratchathani', 'อุบลราชธานี'], + 'TH-41' => ['Udon Thani', 'Udon Thani', 'อุดรธานี'], + 'TH-61' => ['Uthai Thani', 'Uthai Thani', 'อุทัยธานี'], + 'TH-53' => ['Uttaradit', 'Uttaradit', 'อุตรดิตถ์'], + 'TH-95' => ['Yala', 'Yala', 'ยะลา'], + 'TH-35' => ['Yasothon', 'Yasothon', 'ยโสธร'], + ], + // Turkey. + 'TR' => [ + 'TR01' => ['Adana', 'Adana', NULL], + 'TR02' => ['Adıyaman', 'Adıyaman', NULL], + 'TR03' => ['Afyon', 'Afyon', NULL], + 'TR04' => ['Ağrı', 'Ağrı', NULL], + 'TR05' => ['Amasya', 'Amasya', NULL], + 'TR06' => ['Ankara', 'Ankara', NULL], + 'TR07' => ['Antalya', 'Antalya', NULL], + 'TR08' => ['Artvin', 'Artvin', NULL], + 'TR09' => ['Aydın', 'Aydın', NULL], + 'TR10' => ['Balıkesir', 'Balıkesir', NULL], + 'TR11' => ['Bilecik', 'Bilecik', NULL], + 'TR12' => ['Bingöl', 'Bingöl', NULL], + 'TR13' => ['Bitlis', 'Bitlis', NULL], + 'TR14' => ['Bolu', 'Bolu', NULL], + 'TR15' => ['Burdur', 'Burdur', NULL], + 'TR16' => ['Bursa', 'Bursa', NULL], + 'TR17' => ['Çanakkale', 'Çanakkale', NULL], + 'TR18' => ['Çankırı', 'Çankırı', NULL], + 'TR19' => ['Çorum', 'Çorum', NULL], + 'TR20' => ['Denizli', 'Denizli', NULL], + 'TR21' => ['Diyarbakır', 'Diyarbakır', NULL], + 'TR22' => ['Edirne', 'Edirne', NULL], + 'TR23' => ['Elazığ', 'Elazığ', NULL], + 'TR24' => ['Erzincan', 'Erzincan', NULL], + 'TR25' => ['Erzurum', 'Erzurum', NULL], + 'TR26' => ['Eskişehir', 'Eskişehir', NULL], + 'TR27' => ['Gaziantep', 'Gaziantep', NULL], + 'TR28' => ['Giresun', 'Giresun', NULL], + 'TR29' => ['Gümüşhane', 'Gümüşhane', NULL], + 'TR30' => ['Hakkari', 'Hakkari', NULL], + 'TR31' => ['Hatay', 'Hatay', NULL], + 'TR32' => ['Isparta', 'Isparta', NULL], + 'TR33' => ['Mersin', 'Mersin', NULL], + 'TR34' => ['İstanbul', 'İstanbul', NULL], + 'TR35' => ['İzmir', 'İzmir', NULL], + 'TR36' => ['Kars', 'Kars', NULL], + 'TR37' => ['Kastamonu', 'Kastamonu', NULL], + 'TR38' => ['Kayseri', 'Kayseri', NULL], + 'TR39' => ['Kırklareli', 'Kırklareli', NULL], + 'TR40' => ['Kırşehir', 'Kırşehir', NULL], + 'TR41' => ['Kocaeli', 'Kocaeli', NULL], + 'TR42' => ['Konya', 'Konya', NULL], + 'TR43' => ['Kütahya', 'Kütahya', NULL], + 'TR44' => ['Malatya', 'Malatya', NULL], + 'TR45' => ['Manisa', 'Manisa', NULL], + 'TR46' => ['Kahramanmaraş', 'Kahramanmaraş', NULL], + 'TR47' => ['Mardin', 'Mardin', NULL], + 'TR48' => ['Muğla', 'Muğla', NULL], + 'TR49' => ['Muş', 'Muş', NULL], + 'TR50' => ['Nevşehir', 'Nevşehir', NULL], + 'TR51' => ['Niğde', 'Niğde', NULL], + 'TR52' => ['Ordu', 'Ordu', NULL], + 'TR53' => ['Rize', 'Rize', NULL], + 'TR54' => ['Sakarya', 'Sakarya', NULL], + 'TR55' => ['Samsun', 'Samsun', NULL], + 'TR56' => ['Siirt', 'Siirt', NULL], + 'TR57' => ['Sinop', 'Sinop', NULL], + 'TR58' => ['Sivas', 'Sivas', NULL], + 'TR59' => ['Tekirdağ', 'Tekirdağ', NULL], + 'TR60' => ['Tokat', 'Tokat', NULL], + 'TR61' => ['Trabzon', 'Trabzon', NULL], + 'TR62' => ['Tunceli', 'Tunceli', NULL], + 'TR63' => ['Şanlıurfa', 'Şanlıurfa', NULL], + 'TR64' => ['Uşak', 'Uşak', NULL], + 'TR65' => ['Van', 'Van', NULL], + 'TR66' => ['Yozgat', 'Yozgat', NULL], + 'TR67' => ['Zonguldak', 'Zonguldak', NULL], + 'TR68' => ['Aksaray', 'Aksaray', NULL], + 'TR69' => ['Bayburt', 'Bayburt', NULL], + 'TR70' => ['Karaman', 'Karaman', NULL], + 'TR71' => ['Kırıkkale', 'Kırıkkale', NULL], + 'TR72' => ['Batman', 'Batman', NULL], + 'TR73' => ['Şırnak', 'Şırnak', NULL], + 'TR74' => ['Bartın', 'Bartın', NULL], + 'TR75' => ['Ardahan', 'Ardahan', NULL], + 'TR76' => ['Iğdır', 'Iğdır', NULL], + 'TR77' => ['Yalova', 'Yalova', NULL], + 'TR78' => ['Karabük', 'Karabük', NULL], + 'TR79' => ['Kilis', 'Kilis', NULL], + 'TR80' => ['Osmaniye', 'Osmaniye', NULL], + 'TR81' => ['Düzce', 'Düzce', NULL], + ], + // Tanzania. + 'TZ' => [], + // Uganda. + 'UG' => [], + // United States Minor Outlying Islands. + 'UM' => [], + // United States. + 'US' => [ + 'AL' => ['AL', 'Alabama', NULL], + 'AK' => ['AK', 'Alaska', NULL], + 'AZ' => ['AZ', 'Arizona', NULL], + 'AR' => ['AR', 'Arkansas', NULL], + 'CA' => ['CA', 'California', NULL], + 'CO' => ['CO', 'Colorado', NULL], + 'CT' => ['CT', 'Connecticut', NULL], + 'DE' => ['DE', 'Delaware', NULL], + 'DC' => ['DC', 'District of Columbia', NULL], + 'FL' => ['FL', 'Florida', NULL], + 'GA' => ['GA', 'Georgia', NULL], + 'HI' => ['HI', 'Hawaii', NULL], + 'ID' => ['ID', 'Idaho', NULL], + 'IL' => ['IL', 'Illinois', NULL], + 'IN' => ['IN', 'Indiana', NULL], + 'IA' => ['IA', 'Iowa', NULL], + 'KS' => ['KS', 'Kansas', NULL], + 'KY' => ['KY', 'Kentucky', NULL], + 'LA' => ['LA', 'Louisiana', NULL], + 'ME' => ['ME', 'Maine', NULL], + 'MD' => ['MD', 'Maryland', NULL], + 'MA' => ['MA', 'Massachusetts', NULL], + 'MI' => ['MI', 'Michigan', NULL], + 'MN' => ['MN', 'Minnesota', NULL], + 'MS' => ['MS', 'Mississippi', NULL], + 'MO' => ['MO', 'Missouri', NULL], + 'MT' => ['MT', 'Montana', NULL], + 'NE' => ['NE', 'Nebraska', NULL], + 'NV' => ['NV', 'Nevada', NULL], + 'NH' => ['NH', 'New Hampshire', NULL], + 'NJ' => ['NJ', 'New Jersey', NULL], + 'NM' => ['NM', 'New Mexico', NULL], + 'NY' => ['NY', 'New York', NULL], + 'NC' => ['NC', 'North Carolina', NULL], + 'ND' => ['ND', 'North Dakota', NULL], + 'OH' => ['OH', 'Ohio', NULL], + 'OK' => ['OK', 'Oklahoma', NULL], + 'OR' => ['OR', 'Oregon', NULL], + 'PA' => ['PA', 'Pennsylvania', NULL], + 'RI' => ['RI', 'Rhode Island', NULL], + 'SC' => ['SC', 'South Carolina', NULL], + 'SD' => ['SD', 'South Dakota', NULL], + 'TN' => ['TN', 'Tennessee', NULL], + 'TX' => ['TX', 'Texas', NULL], + 'UT' => ['UT', 'Utah', NULL], + 'VT' => ['VT', 'Vermont', NULL], + 'VA' => ['VA', 'Virginia', NULL], + 'WA' => ['WA', 'Washington', NULL], + 'WV' => ['WV', 'West Virginia', NULL], + 'WI' => ['WI', 'Wisconsin', NULL], + 'WY' => ['WY', 'Wyoming', NULL], + 'AA' => ['AA', 'Armed Forces (AA)', NULL], + 'AE' => ['AE', 'Armed Forces (AE)', NULL], + 'AP' => ['AP', 'Armed Forces (AP)', NULL], + //[ 'AS', 'American Samoa', NULL ], + //[ 'GU', 'Guam', NULL ], + //[ 'MH', 'Marshall Islands', NULL ], + //[ 'FM', 'Micronesia', NULL ], + //[ 'MP', 'Northern Mariana Islands', NULL ], + //[ 'PW', 'Palau', NULL ], + //[ 'PR', 'Puerto Rico', NULL ], + //[ 'VI', 'Virgin Islands', NULL ], + ], + // Vietnam. + 'VN' => [], + // Mayotte. + 'YT' => [], + // South Africa. + 'ZA' => [], + // Zambia. + 'ZM' => [], + ]; + // phpcs:enable +} + +/** + * WC_Stripe_Express_Checkout_Helper class. + */ +class WC_Stripe_Express_Checkout_Helper { + + /** + * Sanitize string for comparison. + * + * @param string $string String to be sanitized. + * + * @return string The sanitized string. + */ + public function sanitize_string( $string ) { + return trim( strtolower( $string ) ); + } + + /** + * Get normalized state from express checkout API dropdown list of states. + * + * @param string $state Full state name or state code. + * @param string $country Two-letter country code. + * + * @return string Normalized state or original state input value. + */ + public function get_normalized_state_from_pr_states( $state, $country ) { + // Include Payment Request API State list for compatibility with WC countries/states. + $pr_states = WC_Stripe_Payment_Request_Button_States::STATES; + + if ( ! isset( $pr_states[ $country ] ) ) { + return $state; + } + + foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) { + $sanitized_state_string = $this->sanitize_string( $state ); + // Checks if input state matches with Payment Request state code (0), name (1) or localName (2). + if ( + ( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[0] ) ) || + ( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[1] ) ) || + ( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[2] ) ) + ) { + return $wc_state_abbr; + } + } + + return $state; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12767.php b/tests/PHPStan/Analyser/data/bug-12767.php new file mode 100644 index 0000000000..8ba79bff66 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12767.php @@ -0,0 +1,19 @@ + ['dd1' => 1, 'dd2' => 2]]; + + for ($i=1; $i <= 2; $i++) { + ${'field'.$i} = $employee->data['dd'.$i]; + + assertType('int', ${'field'.$i}); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12778.php b/tests/PHPStan/Analyser/data/bug-12778.php new file mode 100644 index 0000000000..23e4039715 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12778.php @@ -0,0 +1,13 @@ +{''}; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12787.php b/tests/PHPStan/Analyser/data/bug-12787.php new file mode 100644 index 0000000000..189d88cc8b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12787.php @@ -0,0 +1,22 @@ + $labels + */ + public function b(array $labels, \stdClass $payment): bool + { + $fullData = ' + { + "additionalData": { + "acquirerAccountCode": "TestPmmAcquirerAccount", + "authorisationMid": "1009", + "cvcResult": "1 Matches", + "avsResult": "4 AVS not supported for this card type", + "authCode": "25595", + "acquirerReference": "acquirerReference", + "expiryDate": "8/2018", + "avsResultRaw": "Y", + "cvcResultRaw": "M", + "refusalReasonRaw": "00 : Approved or completed successfully", + "refusalCodeRaw": "00", + "acquirerCode": "TestPmmAcquirer", + "inferredRefusalReason": "3D Secure Mandated", + "networkTxReference": "MCC123456789012", + "cardHolderName": "Test Cardholder", + "issuerCountry": "NL", + "countryCode": "NL", + "cardBin": "411111", + "issuerBin": "41111101", + "cardSchemeCommercial": "true", + "cardPaymentMethod": "visa", + "cardIssuingBank": "Bank of America", + "cardIssuingCountry": "US", + "cardIssuingCurrency": "USD", + "fundingSource": "PREPAID_RELOADABLE", + "cardSummary": "1111", + "isCardCommercial": "true", + "paymentMethodVariant": "visadebit", + "paymentMethod": "visa", + "coBrandedWith": "visa", + "businessTypeIdentifier": "PP", + "cardProductId": "P", + "bankSummary": "1111", + "bankAccount.ownerName": "A. Klaassen", + "bankAccount.iban": "NL13TEST0123456789", + "cavv": "AQIDBAUGBw", + "xid": "ODgxNDc2MDg2", + "cavvAlgorithm": "3", + "eci": "02", + "dsTransID": "f8062b92-66e9-4c5a-979a-f465e66a6e48", + "threeDSVersion": "2.1.0", + "threeDAuthenticatedResponse": "Y", + "liabilityShift": "true", + "threeDOffered": "true", + "threeDAuthenticated": "false", + "challengeCancel": "01", + "fraudResultType": "FRAUD", + "fraudManualReview": "false" + }, + "fraudResult": { + "accountScore": 10, + "result": { + "fraudCheckResult": { + "accountScore": "10", + "checkId": "26", + "name": "ShopperEmailRefCheck" + } + } + }, + "response": "[cancelOrRefund-received]" + }'; + + $result = json_decode($fullData, true); + + $r = $labels['result_code'] === '' + && $labels['merchant_reference'] === $payment->merchant_reference + && $labels['brand_code'] === $payment->brand_code + && $labels['acquirer_account_code'] === $result['additionalData']['acquirerAccountCode'] + && $labels['authorisation_mid'] === $result['additionalData']['authorisationMid'] + && $labels['cvc_result'] === $result['additionalData']['cvcResult'] + && $labels['auth_code'] === $result['additionalData']['authCode'] + && $labels['acquirer_reference'] === $result['additionalData']['acquirerReference'] + && $labels['expiry_date'] === $result['additionalData']['expiryDate'] + && $labels['avs_result_raw'] === $result['additionalData']['avsResultRaw'] + && $labels['cvc_result_raw'] === $result['additionalData']['cvcResultRaw'] + && $labels['acquirer_code'] === $result['additionalData']['acquirerCode'] + && $labels['inferred_refusal_reason'] === $result['additionalData']['inferredRefusalReason'] + && $labels['network_tx_reference'] === $result['additionalData']['networkTxReference'] + && $labels['issuer_country'] === $result['additionalData']['issuerCountry'] + && $labels['country_code'] === $result['additionalData']['countryCode'] + && $labels['card_bin'] === $result['additionalData']['cardBin'] + && $labels['issuer_bin'] === $result['additionalData']['issuerBin'] + && $labels['card_scheme_commercial'] === $result['additionalData']['cardSchemeCommercial'] + && $labels['card_payment_method'] === $result['additionalData']['cardPaymentMethod'] + && $labels['card_issuing_bank'] === $result['additionalData']['cardIssuingBank'] + && $labels['card_issuing_country'] === $result['additionalData']['cardIssuingCountry'] + && $labels['card_issuing_currency'] === $result['additionalData']['cardIssuingCurrency'] + && $labels['card_summary'] === $result['additionalData']['cardSummary'] + && $labels['payment_method_variant'] === $result['additionalData']['paymentMethodVariant'] + && $labels['payment_method'] === $result['additionalData']['paymentMethod'] + && $labels['co_branded_with'] === $result['additionalData']['coBrandedWith'] + && $labels['business_type_identifier'] === $result['additionalData']['businessTypeIdentifier'] + && $labels['card_product_id'] === $result['additionalData']['cardProductId'] + && $labels['bank_summary'] === $result['additionalData']['bankSummary'] + && $labels['cavv'] === $result['additionalData']['cavv'] + && $labels['xid'] === $result['additionalData']['xid'] + && $labels['cavv_algorithm'] === $result['additionalData']['cavvAlgorithm'] + && $labels['eci'] === $result['additionalData']['eci'] + && $labels['ds_trans_id'] === $result['additionalData']['dsTransID'] + && $labels['liability_shift'] === $result['additionalData']['liabilityShift'] + && $labels['fraud_result_type'] === $result['additionalData']['fraudResultType'] + && $labels['fraud_manual_review'] === $result['additionalData']['fraudManualReview'] + && $labels['fraud_result_account_score'] === $result['fraudResult']['accountScore'] + && $labels['fraud_result_check_id'] === $result['fraudResult']['result']['fraudCheckResult']['checkId'] + && $labels['fraud_result_name'] === $result['fraudResult']['result']['fraudCheckResult']['name'] + && $labels['response'] === $result['response']; + return $r; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12803.php b/tests/PHPStan/Analyser/data/bug-12803.php new file mode 100644 index 0000000000..60e6ecf05c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12803.php @@ -0,0 +1,27 @@ + $a */ + $a = $this->c(fn() => (object) ['bar' => 1, 'foo' => 2]); + $b = $this->c(fn() => (object) ['bar' => 1, 'foo' => 2]); + } + + /** + * @template T + * @param callable(): T $callback + * @return Generic + */ + public function c(callable $callback): Generic + { + return new Generic(); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-12934.php b/tests/PHPStan/Analyser/data/bug-12934.php new file mode 100644 index 0000000000..36109899a8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12934.php @@ -0,0 +1,9 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug12934; + +function(string $path): void { + session_set_cookie_params(0, path: $path, secure: true, httponly: true); +}; diff --git a/tests/PHPStan/Analyser/data/bug-12949.php b/tests/PHPStan/Analyser/data/bug-12949.php new file mode 100644 index 0000000000..eeafccb0de --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12949.php @@ -0,0 +1,19 @@ +{$b}(); + $o::{$b}(); + echo $o::{$b}; + + echo ""; +} diff --git a/tests/PHPStan/Analyser/data/bug-1388.php b/tests/PHPStan/Analyser/data/bug-1388.php new file mode 100644 index 0000000000..7f85a04c17 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-1388.php @@ -0,0 +1,38 @@ +s; + $sId = $s->id; + + $data[$sId]['1'] = '1'; + $data[$sId]['2'] = '2'; + $data[$sId]['3']['31'] = false; + $data[$sId]['4']['41']['411'] = false; + foreach ($s->c as $c) { + $cId = $c->id; + + $data[$sId]['nodes'][$cId]['1'] = $c->name; + $data[$sId]['nodes'][$cId]['2'] = '2'; + $data[$sId]['nodes'][$cId]['3']['31'] = false; + $data[$sId]['nodes'][$cId]['4']['41']['411'] = false; + foreach ($c->d as $d) { + $dId = $d->id; + + $data[$sId]['nodes'][$cId]['nodes'][$dId]['1'] = $d->name; + $data[$sId]['nodes'][$cId]['nodes'][$dId]['2'] = '2'; + $data[$sId]['nodes'][$cId]['nodes'][$dId]['3']['31'] = false; + $data[$sId]['nodes'][$cId]['nodes'][$dId]['4']['41']['411'] = false; + } + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-1447.php b/tests/PHPStan/Analyser/data/bug-1447.php new file mode 100644 index 0000000000..53cffc3366 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-1447.php @@ -0,0 +1,27 @@ + $v) { + if ($v === 'a') $e = true; + else if ($v === 'b') $e = true; + else if ($v === 'c') $e = true; + else if ($v === 'd') $e = true; + else if ($v === 'e') $e = true; + else if ($v === 'f') $e = true; + else if ($v === 'g') $e = true; + else if ($v === 'h') $e = true; + else if ($v === 'i') $e = true; + else if ($v === 'j') $e = true; + else if ($v === 'k') $e = true; + else if ($v === 'l') $e = true; + else if ($v === 'm') $e = true; + else if ($v === 'n') $e = true; + else if ($v === 'o') $e = true; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2001.php b/tests/PHPStan/Analyser/data/bug-2001.php deleted file mode 100644 index 22db42086a..0000000000 --- a/tests/PHPStan/Analyser/data/bug-2001.php +++ /dev/null @@ -1,51 +0,0 @@ - $i]; - if (rand(0, 1)) { - $a['b'] = $i; - } - - if (rand(0,1)) { - $a = ['d' => $i]; - } - - assertType('array{a: int, b?: int}|array{d: int}', $a); - } -} diff --git a/tests/PHPStan/Analyser/data/bug-3009.php b/tests/PHPStan/Analyser/data/bug-3009.php deleted file mode 100644 index 5d9b2cdd12..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3009.php +++ /dev/null @@ -1,30 +0,0 @@ - 'TextType::class', + 'group' => 'EntityManagerFormType::class', + 'number' => 'IntegerType::class', + 'select' => 'ChoiceType::class', + 'radio' => 'ChoiceType::class', + 'checkbox' => 'ChoiceType::class', + 'bool' => 'CheckboxType::class', + ]; + + /** + * @param string $class + * + * @return string + * + * @throws \Exception + */ + public static function getTypeFromClass(string $class): string + { + $type = array_keys(self::TYPE_TO_CLASS_MAP, $class, true); + + if (0 === count($type)) { + throw new \Exception(sprintf('No type matched class %s', $class)); + } + if (1 < count($type)) { + throw new \Exception( + sprintf('Multiple types found, did you mean any of %s', implode(', ', $type)) + ); + } + + return $type[0]; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3382.php b/tests/PHPStan/Analyser/data/bug-3382.php deleted file mode 100644 index 973b489f4c..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3382.php +++ /dev/null @@ -1,9 +0,0 @@ - + */ +class RecursiveClass extends EntityRepository +{ + +} diff --git a/tests/PHPStan/Analyser/data/bug-3961-php8.php b/tests/PHPStan/Analyser/data/bug-3961-php8.php deleted file mode 100644 index 93b19e3857..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3961-php8.php +++ /dev/null @@ -1,21 +0,0 @@ -', explode('.', $v)); - assertType('*NEVER*', explode('', $v)); - assertType('array', explode('.', $v, -2)); - assertType('non-empty-array', explode('.', $v, 0)); - assertType('non-empty-array', explode('.', $v, 1)); - assertType('non-empty-array', explode($d, $v)); - assertType('non-empty-array', explode($m, $v)); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-3961.php b/tests/PHPStan/Analyser/data/bug-3961.php deleted file mode 100644 index 35a1a2ba11..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3961.php +++ /dev/null @@ -1,21 +0,0 @@ -', explode('.', $v)); - assertType('false', explode('', $v)); - assertType('array', explode('.', $v, -2)); - assertType('non-empty-array', explode('.', $v, 0)); - assertType('non-empty-array', explode('.', $v, 1)); - assertType('non-empty-array|false', explode($d, $v)); - assertType('(non-empty-array|false)', explode($m, $v)); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-3981.php b/tests/PHPStan/Analyser/data/bug-3981.php deleted file mode 100644 index 2a1929cf1a..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3981.php +++ /dev/null @@ -1,25 +0,0 @@ - $a - */ - public function doFoo(array $a): void - { - assertType('array', $a); - $a[] = 2; - assertType('non-empty-array', $a); - - unset($a[0]); - assertType('array', $a); - } - - /** - * @param array $a - */ - public function doBar(array $a): void - { - assertType('array', $a); - $a[1] = 2; - assertType('non-empty-array', $a); - - unset($a[1]); - assertType('array', $a); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-4091.php b/tests/PHPStan/Analyser/data/bug-4091.php deleted file mode 100644 index 0361c4eb4e..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4091.php +++ /dev/null @@ -1,10 +0,0 @@ - 3) { - echo 'Fizz'; - assertType('int', mt_rand(0,10)); -} diff --git a/tests/PHPStan/Analyser/data/bug-4099.php b/tests/PHPStan/Analyser/data/bug-4099.php deleted file mode 100644 index 2e49179969..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4099.php +++ /dev/null @@ -1,41 +0,0 @@ - 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); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-4207.php b/tests/PHPStan/Analyser/data/bug-4207.php deleted file mode 100644 index 0a7ae998d4..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4207.php +++ /dev/null @@ -1,10 +0,0 @@ ->', range(1, 10000)); - assertType('non-empty-array>', range(10000, 1)); -}; diff --git a/tests/PHPStan/Analyser/data/bug-4308.php b/tests/PHPStan/Analyser/data/bug-4308.php new file mode 100644 index 0000000000..9584ebaf12 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4308.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug4308; + +class Test +{ + /** + * @var (string|int|null)[] + * @phpstan-var array{ + * prop1?: string, prop2?: string, prop3?: string, + * prop4?: string, prop5?: string, prop6?: string, + * prop7?: string, prop8?: int, prop9?: int + * } + */ + protected array $updateData = []; + + /** + * @phpstan-param array{ + * prop1?: string, prop2?: string, prop3?: string, + * prop4?: string, prop5?: string, prop6?: string, + * prop7?: string, prop8?: int, prop9?: int + * } $data + */ + public function update(array $data): void + { + $this->updateData = $data + $this->updateData; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4398.php b/tests/PHPStan/Analyser/data/bug-4398.php deleted file mode 100644 index ee0c2a2565..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4398.php +++ /dev/null @@ -1,18 +0,0 @@ -', 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 deleted file mode 100644 index 7e0b97d7fb..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4434.php +++ /dev/null @@ -1,42 +0,0 @@ -', 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<5, 6>|int<8, max>', PHP_MAJOR_VERSION); - assertType('int<5, 6>|int<8, max>', \PHP_MAJOR_VERSION); - } - } - } -} - -class HelloWorld2 -{ - public function testSendEmailToLog(): void - { - foreach ([1] as $emailFile) { - 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<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-4587.php b/tests/PHPStan/Analyser/data/bug-4587.php deleted file mode 100644 index b0643a055e..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4587.php +++ /dev/null @@ -1,37 +0,0 @@ - $results */ - $results = []; - - $type = array_map(static function (array $result): array { - assertType('array{a: int}', $result); - return $result; - }, $results); - - assertType('array', $type); - } - - public function b(): void - { - /** @var list $results */ - $results = []; - - $type = array_map(static function (array $result): array { - assertType('array{a: int}', $result); - $result['a'] = (string) $result['a']; - assertType('array{a: numeric-string}', $result); - - return $result; - }, $results); - - assertType('array', $type); - } -} diff --git a/tests/PHPStan/Analyser/data/bug-4657.php b/tests/PHPStan/Analyser/data/bug-4657.php deleted file mode 100644 index 3cc65c9ed9..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4657.php +++ /dev/null @@ -1,22 +0,0 @@ -', $count); - - $a = []; - if (isset($array['a'])) $a[] = $array['a']; - if (isset($array['b'])) $a[] = $array['b']; - if (isset($array['c'])) $a[] = $array['c']; - if (isset($array['d'])) $a[] = $array['d']; - 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); - } 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); - } -}; - -function(array $array, int $count): void { - if ($count < 1) { - return; - } - - assertType('int<1, max>', $count); - - $a = []; - if (isset($array['a'])) $a[] = $array['a']; - if (isset($array['b'])) $a[] = $array['b']; - if (isset($array['c'])) $a[] = $array['c']; - if (isset($array['d'])) $a[] = $array['d']; - 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); - } 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); - } -}; diff --git a/tests/PHPStan/Analyser/data/bug-4711.php b/tests/PHPStan/Analyser/data/bug-4711.php deleted file mode 100644 index 8d25957907..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4711.php +++ /dev/null @@ -1,19 +0,0 @@ -', 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 d51a97b3e4..508320fb8b 100644 --- a/tests/PHPStan/Analyser/data/bug-4715.php +++ b/tests/PHPStan/Analyser/data/bug-4715.php @@ -30,7 +30,7 @@ class Administration {} class Company { /** - * @var Collection|Administration[] + * @var Collection */ protected Collection $administrations; @@ -40,7 +40,7 @@ public function __construct() } /** - * @return Collection|Administration[] + * @return Collection */ public function getAdministrations() : Collection { diff --git a/tests/PHPStan/Analyser/data/bug-4732.php b/tests/PHPStan/Analyser/data/bug-4732.php new file mode 100644 index 0000000000..46f401919e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4732.php @@ -0,0 +1,23 @@ + $flags bitflags options + */ + public static function sayHello(int $flags): void + { + } + + public static function test(): void + { + HelloWorld::sayHello(HelloWorld::FOO_BAR | HelloWorld::FOO_BAZ); + HelloWorld::sayHello(HelloWorld::FOO_BAR); + HelloWorld::sayHello(HelloWorld::FOO_BAZ); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4902-php8.php b/tests/PHPStan/Analyser/data/bug-4902-php8.php deleted file mode 100644 index 016ac2586f..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4902-php8.php +++ /dev/null @@ -1,53 +0,0 @@ -= 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-4902.php b/tests/PHPStan/Analyser/data/bug-4902.php index e56cd001dd..fc84a47d33 100644 --- a/tests/PHPStan/Analyser/data/bug-4902.php +++ b/tests/PHPStan/Analyser/data/bug-4902.php @@ -1,4 +1,4 @@ -= 7.4 + ...$wrappers */ function unwrapAllAndWrapAgain(Wrapper ...$wrappers): void { - assertType('array', array_map(function (Wrapper $item) { + assertType('list', array_map(function (Wrapper $item) { return $this->unwrap($item); }, $wrappers)); - assertType('array', array_map(fn (Wrapper $item) => $this->unwrap($item), $wrappers)); + assertType('list', array_map(fn (Wrapper $item) => $this->unwrap($item), $wrappers)); } } diff --git a/tests/PHPStan/Analyser/data/bug-5081.php b/tests/PHPStan/Analyser/data/bug-5081.php new file mode 100644 index 0000000000..25c35dcc2b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5081.php @@ -0,0 +1,506 @@ + Preferences) in the final carrier price.'; +$_LANGADM['AdminCarrierWizard29aa46cc3d2677c7e0f216910df600ff'] = 'Free shipping'; +$_LANGADM['AdminCarrierWizardbafd7322c6e97d25b6299b5d6fe8920b'] = 'No'; +$_LANGADM['AdminCarrierWizard93cba07454f06a4a960172bbd6e2a435'] = 'Yes'; +$_LANGADM['AdminCarrierWizard780c462e85ba4399a5d42e88f69a15ca'] = 'Billing'; +$_LANGADM['AdminCarrierWizard0f696253cf9dacf6079bf5060e60da06'] = 'According to total price.'; +$_LANGADM['AdminCarrierWizarda083cb6637472c81ec701d3342320adf'] = 'According to total weight.'; +$_LANGADM['AdminCarrierWizard4b78ac8eb158840e9638a3aeb26c4a9d'] = 'Tax'; +$_LANGADM['AdminCarrierWizard6f3455d187a23443796efdcbe044096b'] = 'No tax'; +$_LANGADM['AdminCarrierWizard082ebbb29b5ba59c293a00a55581679b'] = 'Out-of-range behavior'; +$_LANGADM['AdminCarrierWizard482836cce404046ca7dc34fb0a6fc526'] = 'Apply the cost of the highest defined range'; +$_LANGADM['AdminCarrierWizard4f890cf6a72112cad95093baecf39831'] = 'Disable carrier'; +$_LANGADM['AdminCarrierWizard885ef9bdb910d1379b853075daf44e43'] = 'Out-of-range behavior occurs when no defined range matches the customer\'s cart (e.g. when the weight of the cart is greater than the highest weight limit defined by the weight ranges).'; +$_LANGADM['AdminCarrierWizard9c3448f86be5ee19015f4ecce4bbd6fe'] = 'Maximum package width (%s)'; +$_LANGADM['AdminCarrierWizard2f79e7f703f8cd0258b0ef7e0237a4be'] = 'Maximum width managed by this carrier. Set the value to "0", or leave this field blank to ignore.'; +$_LANGADM['AdminCarrierWizard497876c111e98a20564817545518f829'] = 'The value must be an integer.'; +$_LANGADM['AdminCarrierWizard65a0cd2bca5d0a980a5582a548d79900'] = 'Maximum package height (%s)'; +$_LANGADM['AdminCarrierWizard5929a4e1d04d4653b6dbe2aac59d8a41'] = 'Maximum height managed by this carrier. Set the value to "0", or leave this field blank to ignore.'; +$_LANGADM['AdminCarrierWizard8317f5bb182c1e92c11221955592b518'] = 'Maximum package depth (%s)'; +$_LANGADM['AdminCarrierWizardaacaecfacce577935cf83eeb01bcac40'] = 'Maximum depth managed by this carrier. Set the value to "0", or leave this field blank to ignore.'; +$_LANGADM['AdminCarrierWizardda5c987cbda47de7a6b09406b0840ec4'] = 'Maximum package weight (%s)'; +$_LANGADM['AdminCarrierWizard82ef5a4b25d9debf587900797b0b9619'] = 'Maximum weight managed by this carrier. Set the value to "0", or leave this field blank to ignore.'; +$_LANGADM['AdminCarrierWizard920bd1fb6d54c93fca528ce941464225'] = 'Group access'; +$_LANGADM['AdminCarrierWizardd7049d8a068769eb32177e404639b8ce'] = 'Mark the groups that are allowed access to this carrier.'; +$_LANGADM['AdminCarrierWizard1c6c9d089ce4b751673e3dd09e97b935'] = 'Enable the carrier in the front office.'; +$_LANGADM['AdminCarrierWizard6305822e6fd3b92120ee6f23552164c4'] = 'You must choose at least one shop or group shop.'; +$_LANGADM['AdminCarrierWizard9ef70769595c35cca03dae49ac1f31d1'] = 'An error occurred while saving this carrier.'; +$_LANGADM['AdminCarrierWizardcfabe09befdc8289f6ca5fbc6887ffe5'] = 'An error occurred while saving carrier groups.'; +$_LANGADM['AdminCarrierWizard2222c64a45d69edbf16dd5fb81db904b'] = 'An error occurred while saving carrier zones.'; +$_LANGADM['AdminCarrierWizardbae6cceb9789ee48445a0ddc8c143f0b'] = 'An error occurred while saving carrier ranges.'; +$_LANGADM['AdminCarrierWizardbe78233fdb6fe537e065a0d8650c0e84'] = 'An error occurred while saving associations of shops.'; +$_LANGADM['AdminCarrierWizard5b26cf06b6165264574bf9e097f062bc'] = 'An error occurred while saving the tax rules group.'; +$_LANGADM['AdminCarrierWizard08c490a8c2d633b012b63dccd00cc719'] = 'An error occurred while saving carrier logo.'; +$_LANGADM['AdminCartRulesd3b206d196cd6be3a2764c1fb90b200f'] = 'Delete selected'; +$_LANGADM['AdminCartRulese25f0ecd41211b01c83e5fec41df4fe7'] = 'Delete selected items?'; +$_LANGADM['AdminCartRulesb718adec73e04ce3ec720dd11a06a308'] = 'ID'; +$_LANGADM['AdminCartRules49ee3087348e8d44e1feda1917443987'] = 'Name'; +$_LANGADM['AdminCartRules502996d9790340c5fd7b86a5b93b1c9f'] = 'Priority'; +$_LANGADM['AdminCartRulesca0dbad92a874b2f69b549293387925e'] = 'Code'; +$_LANGADM['AdminCartRules694e8d1f2ee056f98ee488bdc4982d73'] = 'Quantity'; +$_LANGADM['AdminCartRules8c1279db4db86553e4b9682f78cf500e'] = 'Expiration date'; +$_LANGADM['AdminCartRulesec53a8c4f07baed5d8825072c89799be'] = 'Status'; +$_LANGADM['AdminCartRules447da4af35bd09b4d501afb8a2090909'] = 'Add new cart rule'; +$_LANGADM['AdminCartRulesf7de1b71605a10ef04416effa4c6e09e'] = 'Save and Stay'; +$_LANGADM['AdminCartRulesbd0e34e5be6447844e6f262d51f1a9dc'] = 'Payment'; +$_LANGADM['AdminCartRules65b7eaeb9ba4e9903f82297face9f7cd'] = 'Cart Rules'; +$_LANGADM['AdminCarts90855df1b2d1240c62d81bd35d4cfb06'] = 'Non ordered'; +$_LANGADM['AdminCarts121401ccf0e3e23bcefe6a454f0f0601'] = 'Abandoned cart'; +$_LANGADM['AdminCartsb718adec73e04ce3ec720dd11a06a308'] = 'ID'; +$_LANGADM['AdminCartsd79cf3f429596f77db95c65074663a54'] = 'Order ID'; +$_LANGADM['AdminCartsce26601dac0dea138b7295f02b7620a7'] = 'Customer'; +$_LANGADM['AdminCarts96b0141273eabab320119c467cdcaf17'] = 'Total'; +$_LANGADM['AdminCarts914419aa32f04011357d3b604a86d7eb'] = 'Carrier'; +$_LANGADM['AdminCarts44749712dbec183e983dcd78a7736c41'] = 'Date'; +$_LANGADM['AdminCarts54f664c70c22054ea0d8d26fc3997ce7'] = 'Online'; +$_LANGADM['AdminCartsd3b206d196cd6be3a2764c1fb90b200f'] = 'Delete selected'; +$_LANGADM['AdminCartse25f0ecd41211b01c83e5fec41df4fe7'] = 'Delete selected items?'; +$_LANGADM['AdminCartsf9b01554c32cc580b7380302f22613de'] = 'Export carts'; +$_LANGADM['AdminCartse4c3da18c66c0147144767efeb59198f'] = 'Conversion Rate'; +$_LANGADM['AdminCarts947d8520f04473da621f2718138f3bc6'] = '30 days'; +$_LANGADM['AdminCarts54e85d70ea67acdcc86963b14d6223a8'] = 'Abandoned Carts'; +$_LANGADM['AdminCarts915000b6f3e7bb451a6ed4ffc2839ab6'] = 'From %s to %s'; +$_LANGADM['AdminCartsffbb5322a3702b0d8d9c7f506209c540'] = 'Average Order Value'; +$_LANGADM['AdminCarts0ec8109e3ffa61bcc147c89d9a396cd7'] = '%s tax excl.'; +$_LANGADM['AdminCarts4d9e1e12ad8a61ea2a5554407488d91a'] = 'Net Profit per Visitor'; +$_LANGADM['AdminCartsc595d2957600891ad3063a9b13dda4b0'] = 'Cart #%06d'; +$_LANGADM['AdminCarts0b91ef9198a761459c595de4b12ca109'] = 'Total Cart'; +$_LANGADM['AdminCartsb00b85425e74ed2c85dc3119b78ff2c3'] = 'Free Shipping'; +$_LANGADM['AdminCartsee77ea46b0c548ed60eadf31bdd68613'] = 'Bad SQL query'; +$_LANGADM['AdminCategoriesb718adec73e04ce3ec720dd11a06a308'] = 'ID'; +$_LANGADM['AdminCategories49ee3087348e8d44e1feda1917443987'] = 'Name'; +$_LANGADM['AdminCategoriesb5a7adde1af5c87d7fd797b6245c2a39'] = 'Description'; +$_LANGADM['AdminCategories52f5e0bc3859bc5f5e25130b6c7e8881'] = 'Position'; +$_LANGADM['AdminCategories86754577897acfb25deb69039d49d9a7'] = 'Displayed'; +$_LANGADM['AdminCategoriesd3b206d196cd6be3a2764c1fb90b200f'] = 'Delete selected'; +$_LANGADM['AdminCategoriese25f0ecd41211b01c83e5fec41df4fe7'] = 'Delete selected items?'; +$_LANGADM['AdminCategories5f573e91e5eaa092e00a4c4df393c0cb'] = 'Add new root category'; +$_LANGADM['AdminCategoriesd0d4e3688fdaee5afa292083b855e143'] = 'Add new category'; +$_LANGADM['AdminCategoriesde9ced9bf5e9829de4a93ad8c9d7a170'] = 'Add New'; +$_LANGADM['AdminCategories72d6d7a1885885bb55a565fd1070581a'] = 'Import'; +$_LANGADM['AdminCategories7dce122004969d56ae2e0245cb754d35'] = 'Edit'; +$_LANGADM['AdminCategories630f6dc397fe74e52d5189e2c80f282b'] = 'Back to list'; +$_LANGADM['AdminCategories42c9e94e8e5c29861de422525262ff17'] = 'Disabled Categories'; +$_LANGADM['AdminCategories850da4810ae3771d696d504d7346caa6'] = 'Empty Categories'; +$_LANGADM['AdminCategories3b449120fdb2867c000d7bba671aead3'] = 'Top Category'; +$_LANGADM['AdminCategories947d8520f04473da621f2718138f3bc6'] = '30 days'; +$_LANGADM['AdminCategoriesa6398f9bbc9739ed67ca273b82da0a55'] = 'Average number of products per category'; +$_LANGADM['AdminCategories86c34fe1588fab846f096e74c989972f'] = '%s - All people without a valid customer account.'; +$_LANGADM['AdminCategories728b291abe64a8db2e524340d3a5ad4a'] = '%s - Customer who placed an order with the guest checkout.'; +$_LANGADM['AdminCategoriesfe731b8039502b7b8a526edc4e232785'] = '%s - All people who have created an account on this site.'; +$_LANGADM['AdminCategories3adbdb3ac060038aa0e6e6c138ef9873'] = 'Category'; +$_LANGADM['AdminCategories6252c0f2c2ed83b7b06dfca86d4650bb'] = 'Invalid characters'; +$_LANGADM['AdminCategories00d23a76e43b46dae9ec7aa9dcbebb32'] = 'Enabled'; +$_LANGADM['AdminCategoriesb9f5c797ebbf55adccdd8539a65a0241'] = 'Disabled'; +$_LANGADM['AdminCategories52b68aaa602d202c340d9e4e9157f276'] = 'Parent category'; +$_LANGADM['AdminCategories2028f52eb6d12dc1814f92f18c7365a0'] = 'Category Cover Image'; +$_LANGADM['AdminCategories42f9ee5026d32792987af851a2ea0343'] = 'This is the main image for your category, displayed in the category page. The category description will overlap this image and appear in its top-left corner.'; +$_LANGADM['AdminCategories4ae362f049719078c429941bed5dd440'] = 'Category thumbnail'; +$_LANGADM['AdminCategories9e11e4b371570340ca07913bc4783a7a'] = 'Meta title'; +$_LANGADM['AdminCategories3e053943605d9e4bf7dd7588ea19e9d2'] = 'Forbidden characters'; +$_LANGADM['AdminCategories3f64b2beede1082fd32ddb0bf11a641f'] = 'Meta description'; +$_LANGADM['AdminCategories7d7559ccac6bc30a4d985db11cb34a3a'] = 'Meta keywords'; +$_LANGADM['AdminCategories7e35726fb991605ab3d0e6406599e6ef'] = 'To add "tags," click in the field, write something, and then press "Enter."'; +$_LANGADM['AdminCategories1dec4f55522b828fe5dacf8478021a9e'] = 'Friendly URL'; +$_LANGADM['AdminCategories09e2683b6b92b326691cd992f6e5684b'] = 'Only letters, numbers, underscore (_) and the minus (-) character are allowed.'; +$_LANGADM['AdminCategories920bd1fb6d54c93fca528ce941464225'] = 'Group access'; +$_LANGADM['AdminCategories53d98bd116f47fdfe15c8eb4525c5e99'] = 'You now have three default customer groups.'; +$_LANGADM['AdminCategories463848257c086c4816d9f4c020a8d19e'] = 'Mark all of the customer groups which you would like to have access to this category.'; +$_LANGADM['AdminCategoriesc9cc8cce247e49bae79f15173ce97354'] = 'Save'; +$_LANGADM['AdminCategories154b6e494bf56cc4c787bfee6deac113'] = 'Root Category'; +$_LANGADM['AdminCategories93cba07454f06a4a960172bbd6e2a435'] = 'Yes'; +$_LANGADM['AdminCategoriesbafd7322c6e97d25b6299b5d6fe8920b'] = 'No'; +$_LANGADM['AdminCategories9d55fc80bbb875322aa67fd22fc98469'] = 'Shop association'; +$_LANGADM['AdminCategoriesf86f7b91afe27e79305a6b07bdb0d3c0'] = 'Failed to update the status'; +$_LANGADM['AdminCategoriesde360c8b5dd9a9fdd592b1c08b3b4a62'] = 'The status has been updated successfully'; +$_LANGADM['AdminCmsCategoriesd3b206d196cd6be3a2764c1fb90b200f'] = 'Delete selected'; +$_LANGADM['AdminCmsCategoriese25f0ecd41211b01c83e5fec41df4fe7'] = 'Delete selected items?'; +$_LANGADM['AdminCmsCategoriesaf1b98adf7f686b84cd0b443e022b7a0'] = 'Categories'; +$_LANGADM['AdminCmsCategoriesb718adec73e04ce3ec720dd11a06a308'] = 'ID'; +$_LANGADM['AdminCmsCategories49ee3087348e8d44e1feda1917443987'] = 'Name'; +$_LANGADM['AdminCmsCategoriesb5a7adde1af5c87d7fd797b6245c2a39'] = 'Description'; +$_LANGADM['AdminCmsCategories52f5e0bc3859bc5f5e25130b6c7e8881'] = 'Position'; +$_LANGADM['AdminCmsCategories86754577897acfb25deb69039d49d9a7'] = 'Displayed'; +$_LANGADM['AdminCmsCategories789ca3cc9e29e7ef767619e13c6b2f9e'] = 'CMS Category'; +$_LANGADM['AdminCmsCategories6252c0f2c2ed83b7b06dfca86d4650bb'] = 'Invalid characters'; +$_LANGADM['AdminCmsCategories00d23a76e43b46dae9ec7aa9dcbebb32'] = 'Enabled'; +$_LANGADM['AdminCmsCategoriesb9f5c797ebbf55adccdd8539a65a0241'] = 'Disabled'; +$_LANGADM['AdminCmsCategories57bd1d8ace15f17054281d1e88336b97'] = 'Parent CMS Category'; +$_LANGADM['AdminCmsCategories9e11e4b371570340ca07913bc4783a7a'] = 'Meta title'; +$_LANGADM['AdminCmsCategories3f64b2beede1082fd32ddb0bf11a641f'] = 'Meta description'; +$_LANGADM['AdminCmsCategories7d7559ccac6bc30a4d985db11cb34a3a'] = 'Meta keywords'; +$_LANGADM['AdminCmsCategories1dec4f55522b828fe5dacf8478021a9e'] = 'Friendly URL'; +$_LANGADM['AdminCmsCategoriesbed3b3133d292db46a0d28c5d91811b9'] = 'Only letters and the minus (-) character are allowed.'; +$_LANGADM['AdminCmsCategoriesc9cc8cce247e49bae79f15173ce97354'] = 'Save'; +$_LANGADM['AdminCmsCategories9d55fc80bbb875322aa67fd22fc98469'] = 'Shop association'; +$_LANGADM['AdminCmsContentd3b206d196cd6be3a2764c1fb90b200f'] = 'Delete selected'; +$_LANGADM['AdminCmsContente25f0ecd41211b01c83e5fec41df4fe7'] = 'Delete selected items?'; +$_LANGADM['AdminCmsContentef61fb324d729c341ea8ab9901e23566'] = 'Add new'; +$_LANGADM['AdminCmsContentf7931413dee107ddf5289c8886baf7ec'] = 'Edit: %s'; +$_LANGADM['AdminCmsContentc7da501f54544eba6787960200d9efdb'] = 'CMS'; +$_LANGADM['AdminCmsContentaf83e3b9f5d8398fc7b9e88cd6105bde'] = 'Add new CMS category'; +$_LANGADM['AdminCmsContentd0ce974814566418b6ad509f305f319a'] = 'Add new CMS page'; +$_LANGADM['AdminCmsd3b206d196cd6be3a2764c1fb90b200f'] = 'Delete selected'; +$_LANGADM['AdminCmse25f0ecd41211b01c83e5fec41df4fe7'] = 'Delete selected items?'; +$_LANGADM['AdminCmsb718adec73e04ce3ec720dd11a06a308'] = 'ID'; +$_LANGADM['AdminCmse6b391a8d2c4d45902a23a8b6585703d'] = 'URL'; +$_LANGADM['AdminCmsb78a3223503896721cca1303f776159b'] = 'Title'; +$_LANGADM['AdminCms52f5e0bc3859bc5f5e25130b6c7e8881'] = 'Position'; +$_LANGADM['AdminCms86754577897acfb25deb69039d49d9a7'] = 'Displayed'; +$_LANGADM['AdminCms7101cb00c6057071c3f5e52bcb31336b'] = 'Pages in category "%s"'; +$_LANGADM['AdminCmsf8825c9f08ff15b5ef6bc3a3898817e8'] = 'Save and preview'; +$_LANGADM['AdminCms9ea67be453eaccf020697b4654fc021a'] = 'Save and stay'; +$_LANGADM['AdminCms87d49200bfc48e0bcfd3bae27d5616f3'] = 'CMS Page'; +$_LANGADM['AdminCms789ca3cc9e29e7ef767619e13c6b2f9e'] = 'CMS Category'; +$_LANGADM['AdminCms9e11e4b371570340ca07913bc4783a7a'] = 'Meta title'; +$_LANGADM['AdminCms6252c0f2c2ed83b7b06dfca86d4650bb'] = 'Invalid characters'; +$_LANGADM['AdminCms3f64b2beede1082fd32ddb0bf11a641f'] = 'Meta description'; +$_LANGADM['AdminCms7d7559ccac6bc30a4d985db11cb34a3a'] = 'Meta keywords'; +$_LANGADM['AdminCms3ed349365d718a59eadb9df9d5c339f2'] = 'To add "tags" click in the field, write something, and then press "Enter."'; +$_LANGADM['AdminCms1dec4f55522b828fe5dacf8478021a9e'] = 'Friendly URL'; +$_LANGADM['AdminCms21f93401134586a6c481422bf01fccfd'] = 'Only letters and the hyphen (-) character are allowed.'; +$_LANGADM['AdminCms45b1bce0ceb1e155fc99d59a21761b9e'] = 'Page content'; +$_LANGADM['AdminCmsce1e51212c9df52777620dc9de246da0'] = 'Indexation by search engines'; +$_LANGADM['AdminCms00d23a76e43b46dae9ec7aa9dcbebb32'] = 'Enabled'; +$_LANGADM['AdminCmsb9f5c797ebbf55adccdd8539a65a0241'] = 'Disabled'; +$_LANGADM['AdminCmsc9cc8cce247e49bae79f15173ce97354'] = 'Save'; +$_LANGADM['AdminCms9d55fc80bbb875322aa67fd22fc98469'] = 'Shop association'; +$_LANGADM['AdminCmscc4fbd30d676ea2f9994b7063a8ada15'] = 'Pages in this category'; +$_LANGADM['AdminCmsef61fb324d729c341ea8ab9901e23566'] = 'Add new'; +$_LANGADM['AdminCms5ece607071fe59ddc4c88dc6abfe2310'] = 'No items found'; +$_LANGADM['AdminContactsd3b206d196cd6be3a2764c1fb90b200f'] = 'Delete selected'; +$_LANGADM['AdminContactse25f0ecd41211b01c83e5fec41df4fe7'] = 'Delete selected items?'; +$_LANGADM['AdminContactsb718adec73e04ce3ec720dd11a06a308'] = 'ID'; +$_LANGADM['AdminContactsb78a3223503896721cca1303f776159b'] = 'Title'; +$_LANGADM['AdminContactsb357b524e740bc85b9790a0712d84a30'] = 'Email address'; +$_LANGADM['AdminContactsb5a7adde1af5c87d7fd797b6245c2a39'] = 'Description'; +$_LANGADM['AdminContacts9aa698f602b1e5694855cee73a683488'] = 'Contacts'; +$_LANGADM['AdminContacts9cd9efd3eb168071eb0a199972c54aab'] = 'Contact name (e.g. Customer Support).'; +$_LANGADM['AdminContactsdaedf9c5c8f38ac4cf641f3fb3e1bdc4'] = 'Emails will be sent to this address.'; +$_LANGADM['AdminContactsa4cd3191fdeea29906a113c78d4c0e26'] = 'Save messages?'; +$_LANGADM['AdminContacts0f28459fa87b1b3ce6e8b17932f08c3a'] = 'If enabled, all messages will be saved in the "Customer Service" page under the "Customer" menu.'; +$_LANGADM['AdminContacts00d23a76e43b46dae9ec7aa9dcbebb32'] = 'Enabled'; +$_LANGADM['AdminContactsb9f5c797ebbf55adccdd8539a65a0241'] = 'Disabled'; +$_LANGADM['AdminContactsa2b086325f59e6c2fbd410511f4fdfb3'] = 'Further information regarding this contact.'; +$_LANGADM['AdminContactsc9cc8cce247e49bae79f15173ce97354'] = 'Save'; +$_LANGADM['AdminContacts9d55fc80bbb875322aa67fd22fc98469'] = 'Shop association'; +$_LANGADM['AdminContactsc41f67055a184ed2e895681336572761'] = 'Add new contact'; +$_LANGADM['AdminCountriesd3b206d196cd6be3a2764c1fb90b200f'] = 'Delete selected'; +$_LANGADM['AdminCountriese25f0ecd41211b01c83e5fec41df4fe7'] = 'Delete selected items?'; +$_LANGADM['AdminCountries10d30c6319cf61386c878e4d9a3e09a2'] = 'Assign to a new zone'; +$_LANGADM['AdminCountriesf52c1ff75f69fa46ae947f0a3f653641'] = 'Country options'; +$_LANGADM['AdminCountriesabb056fd74a8bdf858dbe3e68c5ea97c'] = 'Restrict country selections in front office to those covered by active carriers'; +$_LANGADM['AdminCountriesc9cc8cce247e49bae79f15173ce97354'] = 'Save'; +$_LANGADM['AdminCountriesb718adec73e04ce3ec720dd11a06a308'] = 'ID'; +$_LANGADM['AdminCountries59716c97497eb9694541f7c3d37b1a4d'] = 'Country'; +$_LANGADM['AdminCountriesad68f9bafd9bf2dcf3865dac55662fd5'] = 'ISO code'; +$_LANGADM['AdminCountriesd8ec51bf63378409b1d40cc45c80f926'] = 'Call prefix'; +$_LANGADM['AdminCountriesb3ff996fe5c77610359114835baf9b38'] = 'Zone'; +$_LANGADM['AdminCountries00d23a76e43b46dae9ec7aa9dcbebb32'] = 'Enabled'; +$_LANGADM['AdminCountries21e6a1298ab4cd040464d67a19d0f957'] = 'Add new country'; +$_LANGADM['AdminCountries790d59ef178acbc75d233bf4211763c6'] = 'Countries'; +$_LANGADM['AdminCountries3be0efaecb3514a14757b8beb4b5dbb3'] = 'Country name'; +$_LANGADM['AdminCountries6252c0f2c2ed83b7b06dfca86d4650bb'] = 'Invalid characters'; +$_LANGADM['AdminCountries3a58c76da4f48aaeb46af20f34caac1b'] = 'Two -- or three -- letter ISO code (e.g. "us for United States).'; +$_LANGADM['AdminCountriesab81f235de173b2d7c0b69009dc6d492'] = 'Two -- or three -- letter ISO code (e.g. U.S. for United States)'; +$_LANGADM['AdminCountriesd3c5d8339f3840b75b4031c2b1e508de'] = 'Official list here'; +$_LANGADM['AdminCountriescd2f7b9409e0f1527766ad35aa8bd3c5'] = 'International call prefix, (e.g. 1 for United States).'; +$_LANGADM['AdminCountries94d7422ba3c5b0f2a35f50b048e51c6d'] = 'Default currency'; +$_LANGADM['AdminCountriesa4f164d8b1b72c87b8ce558827bcd423'] = 'Default store currency'; +$_LANGADM['AdminCountries92de0162cbdfa60f671ba3cad1d392a1'] = 'Geographical region.'; +$_LANGADM['AdminCountriesecefe3def8a2d034d80f6a8876c3d4b1'] = 'Does it need Zip/postal code?'; +$_LANGADM['AdminCountries93cba07454f06a4a960172bbd6e2a435'] = 'Yes'; +$_LANGADM['AdminCountriesbafd7322c6e97d25b6299b5d6fe8920b'] = 'No'; +$_LANGADM['AdminCountries25d176f9d01ba273d1097ca7b298d281'] = 'Zip/postal code format'; +$_LANGADM['AdminCountries3477a6086401c89ab72387673c777af2'] = 'Indicate the format of the postal code: use L for a letter, N for a number, and C for the country\'s ISO 3166-1 alpha-2 code. For example, NNNNN for the United States, France, Poland and many other; LNNNNLLL for Argentina, etc. If you do not want PrestaShop to verify the postal code for this country, leave it blank.'; +$_LANGADM['AdminCountries665e1ad1c6657791cecb5b68008c7c00'] = 'Address format'; +$_LANGADM['AdminCountries4d3d769b812b6faa6b76e1a8abaece2d'] = 'Active'; +$_LANGADM['AdminCountriesb9f5c797ebbf55adccdd8539a65a0241'] = 'Disabled'; +$_LANGADM['AdminCountriesa2ddbdfb29a0708bd711601f9277435c'] = 'Display this country to your customers (the selected country will always be displayed in the Back Office).'; +$_LANGADM['AdminCountries0bd345b58335589d4c2fa1e50ae38619'] = 'Contains states'; +$_LANGADM['AdminCountries0c750dacc725ba4047374d2efc56ce3a'] = 'Do you need a tax identification number?'; +$_LANGADM['AdminCountries05820ffcf621269347a1c14d81d20b77'] = 'Display tax label (e.g. "Tax incl.")'; +$_LANGADM['AdminCountries9d55fc80bbb875322aa67fd22fc98469'] = 'Shop association'; +$_LANGADM['AdminCountries01e93e9457d86c646965decd586dc5ea'] = 'Address format invalid'; +$_LANGADM['AdminCountriesce26601dac0dea138b7295f02b7620a7'] = 'Customer'; +$_LANGADM['AdminCountries6416e8cb5fc0a208d94fa7f5a300dbc4'] = 'Warehouse'; +$_LANGADM['AdminCountries46a2a41cc6e552044816a2d04634545d'] = 'State'; +$_LANGADM['AdminCountriesdd7bf230fde8d4836917806aff6a6b27'] = 'Address'; +$_LANGADM['AdminCurrenciesb718adec73e04ce3ec720dd11a06a308'] = 'ID'; +$_LANGADM['AdminCurrencies386c339d37e737a436499d423a77df0c'] = 'Currency'; +$_LANGADM['AdminCurrenciesad68f9bafd9bf2dcf3865dac55662fd5'] = 'ISO code'; +$_LANGADM['AdminCurrencies5f838bbd088886f09f67b904e414f0e7'] = 'ISO code number'; +$_LANGADM['AdminCurrencies02c86eb2792f3262c21d030a87e19793'] = 'Symbol'; +$_LANGADM['AdminCurrenciese75e316ab3a0a8c0c5fc4b48d1a7033f'] = 'Exchange rate'; +$_LANGADM['AdminCurrencies00d23a76e43b46dae9ec7aa9dcbebb32'] = 'Enabled'; +$_LANGADM['AdminCurrenciesd3b206d196cd6be3a2764c1fb90b200f'] = 'Delete selected'; +$_LANGADM['AdminCurrenciese25f0ecd41211b01c83e5fec41df4fe7'] = 'Delete selected items?'; +$_LANGADM['AdminCurrencies77428b04a1847555eb9bc52422a377b0'] = 'Currency rates'; +$_LANGADM['AdminCurrenciesc1eaa657dda2892e5fec322ac710133a'] = 'Use PrestaShop\'s webservice to update your currency\'s exchange rates. However, please use caution: rates are provided as-is.'; +$_LANGADM['AdminCurrencies876ca43ba50351d4e492970f40632661'] = 'Update currency rates'; +$_LANGADM['AdminCurrenciesf6536046d7af41c3f3975868d6963179'] = 'Automatically update currency rates'; +$_LANGADM['AdminCurrenciesae2fce768106a7b6a61e57943b7a2143'] = 'Use PrestaShop\'s webservice to update your currency exchange rates. However, please use caution: rates are provided as-is.'; +$_LANGADM['AdminCurrenciesabe69e8e4b387562a767a6adaf112ed8'] = 'You can place the following URL in your crontab file, or you can click it yourself regularly'; +$_LANGADM['AdminCurrenciesdfcfc43722eef1eab1e4a12e50a068b1'] = 'Currencies'; +$_LANGADM['AdminCurrencies586e27d3575f00e51ad43b66eb34e49f'] = 'Currency name'; +$_LANGADM['AdminCurrencies5f41116581201a5ef32656b7d4a51e88'] = 'Only letters and the minus character are allowed.'; +$_LANGADM['AdminCurrencies4ef5571f164a6a7fcc9f4625d14e260b'] = 'ISO code (e.g. USD for Dollars, EUR for Euros, etc.).'; +$_LANGADM['AdminCurrencies462fdc88328b3c9d31c63fa01b4f00b1'] = 'Numeric ISO code'; +$_LANGADM['AdminCurrenciesf90c17fdefeeca8b0d55b80a7bc3cb34'] = 'Numeric ISO code (e.g. 840 for Dollars, 978 for Euros, etc.).'; +$_LANGADM['AdminCurrencies7559b7b096e0579368ca2ac7c187ba52'] = 'Will appear in front office (e.g. $, €, etc.)'; +$_LANGADM['AdminCurrencies4abafa9686f98e398e29e46fd388fa36'] = 'Exchange rates are calculated from one unit of your shop\'s default currency. For example, if the default currency is euros and your chosen currency is dollars, type "1.20" (1€ = $1.20).'; +$_LANGADM['AdminCurrencies597d44d65d4c76fe8cc8127b5b9b98bc'] = 'Currency format'; +$_LANGADM['AdminCurrencies188b945338e1d6582c845dfebb469a45'] = 'Applies to all prices (e.g. $1,240.15).'; +$_LANGADM['AdminCurrenciesfca4f4976817baa4a25858a3d6d5274d'] = 'Such as with Dollars'; +$_LANGADM['AdminCurrencies05f78b95fd31ed10def4d0c1ef8e4751'] = 'Such as with Euros'; +$_LANGADM['AdminCurrencies2b417805040de3a3df31c8fd3626b57c'] = 'Decimals'; +$_LANGADM['AdminCurrencies9b9e87b59be497e92da8d2208f9914a0'] = 'Display decimals in prices.'; +$_LANGADM['AdminCurrenciesb9f5c797ebbf55adccdd8539a65a0241'] = 'Disabled'; +$_LANGADM['AdminCurrencies627b24abf27e2d03d38537f84e81cb2e'] = 'Spacing'; +$_LANGADM['AdminCurrencies1d10d84822a63187918311cb3a4e0c87'] = 'Include a space between symbol and price (e.g. $1,240.15 -> $ 1,240.15).'; +$_LANGADM['AdminCurrencies2faec1f9f8cc7f8f40d521c4dd574f49'] = 'Enable'; +$_LANGADM['AdminCurrencies9d55fc80bbb875322aa67fd22fc98469'] = 'Shop association'; +$_LANGADM['AdminCurrenciesc9cc8cce247e49bae79f15173ce97354'] = 'Save'; +$_LANGADM['AdminCurrencies8246d0c794e7db090587c4797b2a234f'] = 'You cannot delete the default currency'; +$_LANGADM['AdminCurrencies7c77e53206853cb381e91e037554faa3'] = 'You cannot disable the default currency'; +$_LANGADM['AdminCurrencies076b68505282c6c0654708db343d6673'] = 'Add new currency'; +$_LANGADM['AdminCustomerPreferences9f7a304fd501ed0e4d06b899fed739d0'] = 'Only account creation'; +$_LANGADM['AdminCustomerPreferencesf2c822352f0e0a62e2de6d716475911b'] = 'Standard (account creation and address creation)'; +$_LANGADM['AdminCustomerPreferences0db377921f4ce762c62526131097968f'] = 'General'; +$_LANGADM['AdminCustomerPreferencesbcb9adf1d2347258b5c65483e34cf86f'] = 'Registration process type'; +assertType("non-empty-array<'AdminAddresses1c76cbfe21c6f44c1d1e59d54f3e4420'|'AdminAddresses284b47b0bb63ae2df3b29f0e691d6fcf'|'AdminAddresses3e053943605d9e4bf7dd7588ea19e9d2'|'AdminAddresses41c2fff4867cc204120f001e7af20f7a'|'AdminAddresses46a2a41cc6e552044816a2d04634545d'|'AdminAddresses57d056ed0984166336b7879c2af3657f'|'AdminAddresses59716c97497eb9694541f7c3d37b1a4d'|'AdminAddresses6252c0f2c2ed83b7b06dfca86d4650bb'|'AdminAddresses6311ae17c1ee52b36e68aaf4ad066387'|'AdminAddresses72d6d7a1885885bb55a565fd1070581a'|'AdminAddresses77587239bf4c54ea493c7033e1dbf636'|'AdminAddresses7cb32e708d6b961d476baced73d362bb'|'AdminAddresses919d1ffe6c1855e790a416efa7b4cc4e'|'AdminAddressesb718adec73e04ce3ec720dd11a06a308'|'AdminAddressesbaa31a65f29121c32b637bb845d41acf'|'AdminAddressesbc910f8bdf70f29374f496f05be0330c'|'AdminAddressesbed08e8af70a98c1a8361f13ec477be0'|'AdminAddressesc9cc8cce247e49bae79f15173ce97354'|'AdminAddressesce26601dac0dea138b7295f02b7620a7'|'AdminAddressesd3b206d196cd6be3a2764c1fb90b200f'|'AdminAddressesdd7bf230fde8d4836917806aff6a6b27'|'AdminAddressese25f0ecd41211b01c83e5fec41df4fe7'|'AdminAddressese4eb5dadb6ee84c5c55a8edf53f6e554'|'AdminAddressesea318a4ad37f0c2d2c368e6c958ed551'|'AdminAddresseseeabead01c6c6f25f22bf0b041df58a9'|'AdminAddressesfe66abce284ec8589e7d791185b5c442'|'AdminAdminPreferences0db377921f4ce762c62526131097968f'|'AdminAdminPreferences0f81567617bb8ebc23f48e74d8ae8acf'|'AdminAdminPreferences11b3df1e92b11e2d899494d3cdf4dd13'|'AdminAdminPreferences1b1befcb86d487715da458117710dfeb'|'AdminAdminPreferences20d6b6498eab9f749d55c9b53151e00a'|'AdminAdminPreferences2c111a587b8e6a65856ac7933d76bdce'|'AdminAdminPreferences46f18d3960afc01e5a1a5a0e0e9d571b'|'AdminAdminPreferences4ae386b852a3ee22324e8922e50c9aec'|'AdminAdminPreferences4e7ff7ca556a7ac8329ab27834e9631b'|'AdminAdminPreferences694c63d4a2b60499f7ba524fb639811f'|'AdminAdminPreferences73cdddd7730abfc13a55efb9f5685a3b'|'AdminAdminPreferences8004e61ca76ff500d1e6ee92f7cb7f93'|'AdminAdminPreferences99059a2047f475cdc6428076e3360134'|'AdminAdminPreferencesa274f4d4670213a9045ce258c6c56b80'|'AdminAdminPreferencesa676520f8296be0319ad6268657471ea'|'AdminAdminPreferencesade28d54bcdbc7c4cfd45d84ad517f7b'|'AdminAdminPreferencesb32a8e98434105bcfe4f234aa4c7b28b'|'AdminAdminPreferencesb48de7251c23e4b0eb0975b1c7bf9bc5'|'AdminAdminPreferencesb8a8fa662505e278031049e4990e428a'|'AdminAdminPreferencesc9cc8cce247e49bae79f15173ce97354'|'AdminAdminPreferencescabcb35221054c8ad296eb4e406e2cd7'|'AdminAdminPreferencesdcfba1534995899d2ca36cda978da215'|'AdminAdminPreferencese0853b619fbd24fdabc3ae78beb81193'|'AdminAdminPreferencese0c9f1de766b906e5660ea07af8a02ec'|'AdminAdminPreferencese62d77475fe6318731b4411ba1181dca'|'AdminAdminPreferencese78f32f514dbd49e570066db36343d13'|'AdminAdminPreferencese7fe6b70f4558e23f0254d80f52ae6d8'|'AdminAttachments0b27918290ff5323bea1e3b78a9cf04e'|'AdminAttachments0c6c7ccc80b3bfb8fcb57dc63405f599'|'AdminAttachments1351017ac6423911223bc19a8cb7c653'|'AdminAttachments1f66f9472666b18b19c22fd0f1a6a07b'|'AdminAttachments49ee3087348e8d44e1feda1917443987'|'AdminAttachments5251010ec9e364492c236bf8b9983928'|'AdminAttachments6f6cb72d544962fa333e2e34ce64f719'|'AdminAttachments8a23b9ee3a4502a0de3fc32c5ba7aa65'|'AdminAttachments8ecfb7c46cc91aaa98cc88b3f43cfffc'|'AdminAttachmentsb5a7adde1af5c87d7fd797b6245c2a39'|'AdminAttachmentsb718adec73e04ce3ec720dd11a06a308'|'AdminAttachmentsbdf4f1da184f2dc052c75ad7e1afbd4a'|'AdminAttachmentsc9cc8cce247e49bae79f15173ce97354'|'AdminAttachmentsd3b206d196cd6be3a2764c1fb90b200f'|'AdminAttachmentsd647666a6c4cef994b4fa1a540ba4481'|'AdminAttachmentse25f0ecd41211b01c83e5fec41df4fe7'|'AdminAttachmentse9cb217697088a98b1937d111d936281'|'AdminAttachmentseefad10f0e06ebfb6a27344408e54660'|'AdminAttachmentsfc1ff5390ecc7efd695f697f3d6b7e4b'|'AdminAttributeGenerator402784f5f14c30e7309a135ba6be531f'|'AdminAttributeGenerator81315cfd898aada1e99e0034b4b078c3'|'AdminAttributeGenerator9446a98ad14416153cc4d45ab8b531bf'|'AdminAttributeGeneratorced303d99586792bb560b5e1d35ea220'|'AdminAttributesGroups00039b674d8ced58313546dcab88a032'|'AdminAttributesGroups0e010c6b3fb88bf4277c880d1657787a'|'AdminAttributesGroups170269305ed04c49b26b2d5dbe053dc6'|'AdminAttributesGroups1736c2a3dfbe74f884bf5c9750bd4606'|'AdminAttributesGroups17af8baa9b3f90e936589069e4223280'|'AdminAttributesGroups1f40023e11d8401b0bffadc419135247'|'AdminAttributesGroups22cbf85c41427960736dc10cfec5faf4'|'AdminAttributesGroups287234a1ff35a314b5b6bc4e5828e745'|'AdminAttributesGroups2dce4461e5743f3b01acd4599a38d646'|'AdminAttributesGroups49ee3087348e8d44e1feda1917443987'|'AdminAttributesGroups5204077231fc7164e2269e96b584dd95'|'AdminAttributesGroups52729803b243ea9693a892161d5b8e38'|'AdminAttributesGroups52f5e0bc3859bc5f5e25130b6c7e8881'|'AdminAttributesGroups561f47d9c8a6153b011def4fd72386d5'|'AdminAttributesGroups577cf2cf1be74419ac04093a2b4cd64d'|'AdminAttributesGroups6252c0f2c2ed83b7b06dfca86d4650bb'|'AdminAttributesGroups630f6dc397fe74e52d5189e2c80f282b'|'AdminAttributesGroups689202409e48743b914713f96d93947c'|'AdminAttributesGroups713271e705e5269fc82684445cd063a8'|'AdminAttributesGroups71c476c94d0a0e3dfc0826afd03d2dda'|'AdminAttributesGroups71e8f8a090925f75719dfa0a5eae059e'|'AdminAttributesGroups72d6d7a1885885bb55a565fd1070581a'|'AdminAttributesGroups7d5672f569de406c85249db6f1c99ec0'|'AdminAttributesGroups8bd90a6d76a77fe0b160e8abd85c8590'|'AdminAttributesGroups9446a98ad14416153cc4d45ab8b531bf'|'AdminAttributesGroups9d55fc80bbb875322aa67fd22fc98469'|'AdminAttributesGroupsa3e8ae43188ae76d38f414b2bdb0077b'|'AdminAttributesGroupsb5e6921c2d093fbcb0088c9466ee9983'|'AdminAttributesGroupsb718adec73e04ce3ec720dd11a06a308'|'AdminAttributesGroupsba353198430b2004efeb1ac6d1f410d0'|'AdminAttributesGroupsc82a6100dace2b41087ba6cf99a5976a'|'AdminAttributesGroupsc9cc8cce247e49bae79f15173ce97354'|'AdminAttributesGroupscb5feb1b7314637725a2e73bdc9f7295'|'AdminAttributesGroupsced303d99586792bb560b5e1d35ea220'|'AdminAttributesGroupsd274013ea65428454962a59b7b373a41'|'AdminAttributesGroupsd3b206d196cd6be3a2764c1fb90b200f'|'AdminAttributesGroupsdd24a1142c1070a0efbdf43b4f0167cc'|'AdminAttributesGroupse25f0ecd41211b01c83e5fec41df4fe7'|'AdminAttributesGroupsf2d1c5443636295e9720caac90ea8d93'|'AdminAttributesGroupsf68b27443f6e6f685cce3f9f422a2b84'|'AdminAttributesGroupsf7931413dee107ddf5289c8886baf7ec'|'AdminAttributesGroupsfce2e84f3cce0e5351e85e9f0cb20107'|'AdminBackup03727ac48595a24daed975559c944a44'|'AdminBackup1589ac76f2f88749f51028f09b23f9d4'|'AdminBackup1908624a0bca678cd26b99bfd405324e'|'AdminBackup2c7338ad06a6bb0747b0d432c33464ce'|'AdminBackup2e25562aa49c13b17e979d826fecc25f'|'AdminBackup30c210e0173f2ff607cc84dc01ffc1f0'|'AdminBackup34082694d21dbdcfc31e6e32d9fb2b9f'|'AdminBackup44749712dbec183e983dcd78a7736c41'|'AdminBackup6a7e73161603d87b26a8eac49dab0a9c'|'AdminBackup6afc2b40f9acff2a4d1e67f2dfcd8a30'|'AdminBackup8859ec81a77f2f2b165bf5ea9858ecfc'|'AdminBackup9d8d2d5ab12b515182a505f54db7f538'|'AdminBackupb07ccf1ffff29007509d45dbcc13f923'|'AdminBackupb55e509c697e4cca0e1d160a7806698f'|'AdminBackupc9cc8cce247e49bae79f15173ce97354'|'AdminBackupd3b206d196cd6be3a2764c1fb90b200f'|'AdminBackupe25f0ecd41211b01c83e5fec41df4fe7'|'AdminBackupe807d3ccf8d24c8c1a3d86db5da78da8'|'AdminBackupea4788705e6873b424c65e91c2846b19'|'AdminBackupf36c9a20c2ce51f491c944e41fde5ace'|'AdminCarriers00d23a76e43b46dae9ec7aa9dcbebb32'|'AdminCarriers049de64decc4aa8fa5aa89cf8b17470c'|'AdminCarriers0687bb4ca6cc1c51d79684159f91ff11'|'AdminCarriers082ebbb29b5ba59c293a00a55581679b'|'AdminCarriers0cce6348a3d85f52a44d053f542afcbc'|'AdminCarriers1412292b09d3cd39f32549afb1f5f102'|'AdminCarriers1935671a637346f67b485596b9fcba2c'|'AdminCarriers1c0e287237d8c352c6ead633b019c047'|'AdminCarriers1c6c9d089ce4b751673e3dd09e97b935'|'AdminCarriers1c76cbfe21c6f44c1d1e59d54f3e4420'|'AdminCarriers1d6af794b2599c1407a83029a09d1ecf'|'AdminCarriers3194ebe40c7a8c29c78ea79066b6e05c'|'AdminCarriers324029d06c6bfe85489099f6e69b7637'|'AdminCarriers3e86ececa46af50900510892f94c4ed6'|'AdminCarriers482836cce404046ca7dc34fb0a6fc526'|'AdminCarriers49ee3087348e8d44e1feda1917443987'|'AdminCarriers49fec5c86a3b43821fdf0d9aa7bbd935'|'AdminCarriers4b78ac8eb158840e9638a3aeb26c4a9d'|'AdminCarriers4ca4a355318f45dac9fb0ee632d8dc3c'|'AdminCarriers4e140ba723a03baa6948340bf90e2ef6'|'AdminCarriers4f890cf6a72112cad95093baecf39831'|'AdminCarriers52f5e0bc3859bc5f5e25130b6c7e8881'|'AdminCarriers590f6d9a5885f042982c9a911f76abda'|'AdminCarriers5e6b7c069d71052ffc8c4410c0c46992'|'AdminCarriers6803abe0c8347830d574da8e04fa78e5'|'AdminCarriers6e6fbb3d274ac15210f6b7892c7d24c1'|'AdminCarriers7475ec0d41372a307c497acb7eeea8c4'|'AdminCarriers7589dfa9a5a899e9701335164c9ab520'|'AdminCarriers780c462e85ba4399a5d42e88f69a15ca'|'AdminCarriers7dce122004969d56ae2e0245cb754d35'|'AdminCarriers8a52ca34a90eb8486886815e62958ac1'|'AdminCarriers8c2857a9ad1d8f31659e35e904e20fa6'|'AdminCarriers8f497c1a3d15af9e0c215019f26b887d'|'AdminCarriers91aa2e3b1cd071ba7031bf4263e11821'|'AdminCarriers920bd1fb6d54c93fca528ce941464225'|'AdminCarriers93cba07454f06a4a960172bbd6e2a435'|'AdminCarriers9d55fc80bbb875322aa67fd22fc98469'|'AdminCarriers9e93aab109e30d26aa231a49385c99db'|'AdminCarriersa414ac63c6b29218661d1fa2c6e21b5b'|'AdminCarriersa788f81b3aa0ef9c9efcb1fb67708d82'|'AdminCarriersb00b85425e74ed2c85dc3119b78ff2c3'|'AdminCarriersb3ff996fe5c77610359114835baf9b38'|'AdminCarriersb718adec73e04ce3ec720dd11a06a308'|'AdminCarriersb9f5c797ebbf55adccdd8539a65a0241'|'AdminCarriersbafd7322c6e97d25b6299b5d6fe8920b'|'AdminCarriersc26732c157d7b353c1be9f7ba8962e57'|'AdminCarriersc8b462f779749d2e27abed2e9501b2bd'|'AdminCarriersc9cc8cce247e49bae79f15173ce97354'|'AdminCarrierscdaa245d6e50b5647bfd9fcb77ac9a21'|'AdminCarriersd3b206d196cd6be3a2764c1fb90b200f'|'AdminCarriersd7049d8a068769eb32177e404639b8ce'|'AdminCarriersdde695268ea519ababd83f0ca3d274fc'|'AdminCarrierse1bcd0aa73dbc610f1fc628499244d8f'|'AdminCarrierse25f0ecd41211b01c83e5fec41df4fe7'|'AdminCarrierse29e90d06dc78b1a6b2e5e9d61f2f724'|'AdminCarrierse3d29a6f3d7588301aa04429e686b260'|'AdminCarrierse6b391a8d2c4d45902a23a8b6585703d'|'AdminCarrierse81c4e4f2b7b93b481e13a8553c2ae1b'|'AdminCarriersec53a8c4f07baed5d8825072c89799be'|'AdminCarriersf2a6c498fb90ee345d997f888fce3b18'|'AdminCarriersf8af50e8f2eb39dc8581b4943d6ec59f'|'AdminCarriersff5e2cfc010955358f7ff264d9e58398'|'AdminCarrierWizard00d23a76e43b46dae9ec7aa9dcbebb32'|'AdminCarrierWizard0668ec4bb8d6bcb27d283b2af9bc5888'|'AdminCarrierWizard082ebbb29b5ba59c293a00a55581679b'|'AdminCarrierWizard08c490a8c2d633b012b63dccd00cc719'|'AdminCarrierWizard0979779c4569141b98591d326d343ec2'|'AdminCarrierWizard0f696253cf9dacf6079bf5060e60da06'|'AdminCarrierWizard10ac3d04253ef7e1ddc73e6091c0cd55'|'AdminCarrierWizard1778e0e8555dc044231c1d615b41ddea'|'AdminCarrierWizard1c6c9d089ce4b751673e3dd09e97b935'|'AdminCarrierWizard2222c64a45d69edbf16dd5fb81db904b'|'AdminCarrierWizard290612199861c31d1036b185b4e69b75'|'AdminCarrierWizard29aa46cc3d2677c7e0f216910df600ff'|'AdminCarrierWizard2f79e7f703f8cd0258b0ef7e0237a4be'|'AdminCarrierWizard40fe120d89217e6f04a27723136b8601'|'AdminCarrierWizard482836cce404046ca7dc34fb0a6fc526'|'AdminCarrierWizard497876c111e98a20564817545518f829'|'AdminCarrierWizard4b78ac8eb158840e9638a3aeb26c4a9d'|'AdminCarrierWizard4ca4a355318f45dac9fb0ee632d8dc3c'|'AdminCarrierWizard4f890cf6a72112cad95093baecf39831'|'AdminCarrierWizard5929a4e1d04d4653b6dbe2aac59d8a41'|'AdminCarrierWizard5b26cf06b6165264574bf9e097f062bc'|'AdminCarrierWizard6305822e6fd3b92120ee6f23552164c4'|'AdminCarrierWizard65a0cd2bca5d0a980a5582a548d79900'|'AdminCarrierWizard6f3455d187a23443796efdcbe044096b'|'AdminCarrierWizard756eb8cebeb953f5ae47235ff2e183b5'|'AdminCarrierWizard780c462e85ba4399a5d42e88f69a15ca'|'AdminCarrierWizard7cee91acc888d490e2622f3eca17cd37'|'AdminCarrierWizard81e24bc79af497d9e9c486bfa24742be'|'AdminCarrierWizard829c7cc5ed48e11df7ac9b05e236a12c'|'AdminCarrierWizard82ef5a4b25d9debf587900797b0b9619'|'AdminCarrierWizard8317f5bb182c1e92c11221955592b518'|'AdminCarrierWizard885ef9bdb910d1379b853075daf44e43'|'AdminCarrierWizard8c2857a9ad1d8f31659e35e904e20fa6'|'AdminCarrierWizard920bd1fb6d54c93fca528ce941464225'|'AdminCarrierWizard93cba07454f06a4a960172bbd6e2a435'|'AdminCarrierWizard9c3448f86be5ee19015f4ecce4bbd6fe'|'AdminCarrierWizard9d55fc80bbb875322aa67fd22fc98469'|'AdminCarrierWizard9ef70769595c35cca03dae49ac1f31d1'|'AdminCarrierWizarda083cb6637472c81ec701d3342320adf'|'AdminCarrierWizarda20ddccbb6f808ec42cd66323e6c6061'|'AdminCarrierWizarda788f81b3aa0ef9c9efcb1fb67708d82'|'AdminCarrierWizardaacaecfacce577935cf83eeb01bcac40'|'AdminCarrierWizardb9f5c797ebbf55adccdd8539a65a0241'|'AdminCarrierWizardbae6cceb9789ee48445a0ddc8c143f0b'|'AdminCarrierWizardbafd7322c6e97d25b6299b5d6fe8920b'|'AdminCarrierWizardbe78233fdb6fe537e065a0d8650c0e84'|'AdminCarrierWizardc8b462f779749d2e27abed2e9501b2bd'|'AdminCarrierWizardc91e596246bbf8fdff9dae7b349d71d9'|'AdminCarrierWizardcfabe09befdc8289f6ca5fbc6887ffe5'|'AdminCarrierWizardd7049d8a068769eb32177e404639b8ce'|'AdminCarrierWizardda5c987cbda47de7a6b09406b0840ec4'|'AdminCarrierWizarddd1f775e443ff3b9a89270713580a51b'|'AdminCarrierWizarddde695268ea519ababd83f0ca3d274fc'|'AdminCarrierWizardde62775a71fc2bf7a13d7530ae24a7ed'|'AdminCarrierWizarde0c892f1ca1fb503987c2db8fd250a43'|'AdminCarrierWizarde2fb9fa6091dd9f779b98efdf998a00a'|'AdminCarrierWizardea4788705e6873b424c65e91c2846b19'|'AdminCarrierWizardf1fe3b3625cdded65fc740dd16b978a6'|'AdminCartRules447da4af35bd09b4d501afb8a2090909'|'AdminCartRules49ee3087348e8d44e1feda1917443987'|'AdminCartRules502996d9790340c5fd7b86a5b93b1c9f'|'AdminCartRules65b7eaeb9ba4e9903f82297face9f7cd'|'AdminCartRules694e8d1f2ee056f98ee488bdc4982d73'|'AdminCartRules8c1279db4db86553e4b9682f78cf500e'|'AdminCartRulesb718adec73e04ce3ec720dd11a06a308'|'AdminCartRulesbd0e34e5be6447844e6f262d51f1a9dc'|'AdminCartRulesca0dbad92a874b2f69b549293387925e'|'AdminCartRulesd3b206d196cd6be3a2764c1fb90b200f'|'AdminCartRulese25f0ecd41211b01c83e5fec41df4fe7'|'AdminCartRulesec53a8c4f07baed5d8825072c89799be'|'AdminCartRulesf7de1b71605a10ef04416effa4c6e09e'|'AdminCarts0b91ef9198a761459c595de4b12ca109'|'AdminCarts0ec8109e3ffa61bcc147c89d9a396cd7'|'AdminCarts121401ccf0e3e23bcefe6a454f0f0601'|'AdminCarts44749712dbec183e983dcd78a7736c41'|'AdminCarts4d9e1e12ad8a61ea2a5554407488d91a'|'AdminCarts54e85d70ea67acdcc86963b14d6223a8'|'AdminCarts54f664c70c22054ea0d8d26fc3997ce7'|'AdminCarts90855df1b2d1240c62d81bd35d4cfb06'|'AdminCarts914419aa32f04011357d3b604a86d7eb'|'AdminCarts915000b6f3e7bb451a6ed4ffc2839ab6'|'AdminCarts947d8520f04473da621f2718138f3bc6'|'AdminCarts96b0141273eabab320119c467cdcaf17'|'AdminCartsb00b85425e74ed2c85dc3119b78ff2c3'|'AdminCartsb718adec73e04ce3ec720dd11a06a308'|'AdminCartsc595d2957600891ad3063a9b13dda4b0'|'AdminCartsce26601dac0dea138b7295f02b7620a7'|'AdminCartsd3b206d196cd6be3a2764c1fb90b200f'|'AdminCartsd79cf3f429596f77db95c65074663a54'|'AdminCartse25f0ecd41211b01c83e5fec41df4fe7'|'AdminCartse4c3da18c66c0147144767efeb59198f'|'AdminCartsee77ea46b0c548ed60eadf31bdd68613'|'AdminCartsf9b01554c32cc580b7380302f22613de'|'AdminCartsffbb5322a3702b0d8d9c7f506209c540'|'AdminCategories00d23a76e43b46dae9ec7aa9dcbebb32'|'AdminCategories09e2683b6b92b326691cd992f6e5684b'|'AdminCategories154b6e494bf56cc4c787bfee6deac113'|'AdminCategories1dec4f55522b828fe5dacf8478021a9e'|'AdminCategories2028f52eb6d12dc1814f92f18c7365a0'|'AdminCategories3adbdb3ac060038aa0e6e6c138ef9873'|'AdminCategories3b449120fdb2867c000d7bba671aead3'|'AdminCategories3e053943605d9e4bf7dd7588ea19e9d2'|'AdminCategories3f64b2beede1082fd32ddb0bf11a641f'|'AdminCategories42c9e94e8e5c29861de422525262ff17'|'AdminCategories42f9ee5026d32792987af851a2ea0343'|'AdminCategories463848257c086c4816d9f4c020a8d19e'|'AdminCategories49ee3087348e8d44e1feda1917443987'|'AdminCategories4ae362f049719078c429941bed5dd440'|'AdminCategories52b68aaa602d202c340d9e4e9157f276'|'AdminCategories52f5e0bc3859bc5f5e25130b6c7e8881'|'AdminCategories53d98bd116f47fdfe15c8eb4525c5e99'|'AdminCategories5f573e91e5eaa092e00a4c4df393c0cb'|'AdminCategories6252c0f2c2ed83b7b06dfca86d4650bb'|'AdminCategories630f6dc397fe74e52d5189e2c80f282b'|'AdminCategories728b291abe64a8db2e524340d3a5ad4a'|'AdminCategories72d6d7a1885885bb55a565fd1070581a'|'AdminCategories7d7559ccac6bc30a4d985db11cb34a3a'|'AdminCategories7dce122004969d56ae2e0245cb754d35'|'AdminCategories7e35726fb991605ab3d0e6406599e6ef'|'AdminCategories850da4810ae3771d696d504d7346caa6'|'AdminCategories86754577897acfb25deb69039d49d9a7'|'AdminCategories86c34fe1588fab846f096e74c989972f'|'AdminCategories920bd1fb6d54c93fca528ce941464225'|'AdminCategories93cba07454f06a4a960172bbd6e2a435'|'AdminCategories947d8520f04473da621f2718138f3bc6'|'AdminCategories9d55fc80bbb875322aa67fd22fc98469'|'AdminCategories9e11e4b371570340ca07913bc4783a7a'|'AdminCategoriesa6398f9bbc9739ed67ca273b82da0a55'|'AdminCategoriesb5a7adde1af5c87d7fd797b6245c2a39'|'AdminCategoriesb718adec73e04ce3ec720dd11a06a308'|'AdminCategoriesb9f5c797ebbf55adccdd8539a65a0241'|'AdminCategoriesbafd7322c6e97d25b6299b5d6fe8920b'|'AdminCategoriesc9cc8cce247e49bae79f15173ce97354'|'AdminCategoriesd0d4e3688fdaee5afa292083b855e143'|'AdminCategoriesd3b206d196cd6be3a2764c1fb90b200f'|'AdminCategoriesde360c8b5dd9a9fdd592b1c08b3b4a62'|'AdminCategoriesde9ced9bf5e9829de4a93ad8c9d7a170'|'AdminCategoriese25f0ecd41211b01c83e5fec41df4fe7'|'AdminCategoriesf86f7b91afe27e79305a6b07bdb0d3c0'|'AdminCategoriesfe731b8039502b7b8a526edc4e232785'|'AdminCms00d23a76e43b46dae9ec7aa9dcbebb32'|'AdminCms1dec4f55522b828fe5dacf8478021a9e'|'AdminCms21f93401134586a6c481422bf01fccfd'|'AdminCms3ed349365d718a59eadb9df9d5c339f2'|'AdminCms3f64b2beede1082fd32ddb0bf11a641f'|'AdminCms45b1bce0ceb1e155fc99d59a21761b9e'|'AdminCms52f5e0bc3859bc5f5e25130b6c7e8881'|'AdminCms5ece607071fe59ddc4c88dc6abfe2310'|'AdminCms6252c0f2c2ed83b7b06dfca86d4650bb'|'AdminCms7101cb00c6057071c3f5e52bcb31336b'|'AdminCms789ca3cc9e29e7ef767619e13c6b2f9e'|'AdminCms7d7559ccac6bc30a4d985db11cb34a3a'|'AdminCms86754577897acfb25deb69039d49d9a7'|'AdminCms87d49200bfc48e0bcfd3bae27d5616f3'|'AdminCms9d55fc80bbb875322aa67fd22fc98469'|'AdminCms9e11e4b371570340ca07913bc4783a7a'|'AdminCms9ea67be453eaccf020697b4654fc021a'|'AdminCmsb718adec73e04ce3ec720dd11a06a308'|'AdminCmsb78a3223503896721cca1303f776159b'|'AdminCmsb9f5c797ebbf55adccdd8539a65a0241'|'AdminCmsc9cc8cce247e49bae79f15173ce97354'|'AdminCmsCategories00d23a76e43b46dae9ec7aa9dcbebb32'|'AdminCmsCategories1dec4f55522b828fe5dacf8478021a9e'|'AdminCmsCategories3f64b2beede1082fd32ddb0bf11a641f'|'AdminCmsCategories49ee3087348e8d44e1feda1917443987'|'AdminCmsCategories52f5e0bc3859bc5f5e25130b6c7e8881'|'AdminCmsCategories57bd1d8ace15f17054281d1e88336b97'|'AdminCmsCategories6252c0f2c2ed83b7b06dfca86d4650bb'|'AdminCmsCategories789ca3cc9e29e7ef767619e13c6b2f9e'|'AdminCmsCategories7d7559ccac6bc30a4d985db11cb34a3a'|'AdminCmsCategories86754577897acfb25deb69039d49d9a7'|'AdminCmsCategories9d55fc80bbb875322aa67fd22fc98469'|'AdminCmsCategories9e11e4b371570340ca07913bc4783a7a'|'AdminCmsCategoriesaf1b98adf7f686b84cd0b443e022b7a0'|'AdminCmsCategoriesb5a7adde1af5c87d7fd797b6245c2a39'|'AdminCmsCategoriesb718adec73e04ce3ec720dd11a06a308'|'AdminCmsCategoriesb9f5c797ebbf55adccdd8539a65a0241'|'AdminCmsCategoriesbed3b3133d292db46a0d28c5d91811b9'|'AdminCmsCategoriesc9cc8cce247e49bae79f15173ce97354'|'AdminCmsCategoriesd3b206d196cd6be3a2764c1fb90b200f'|'AdminCmsCategoriese25f0ecd41211b01c83e5fec41df4fe7'|'AdminCmscc4fbd30d676ea2f9994b7063a8ada15'|'AdminCmsce1e51212c9df52777620dc9de246da0'|'AdminCmsContentaf83e3b9f5d8398fc7b9e88cd6105bde'|'AdminCmsContentc7da501f54544eba6787960200d9efdb'|'AdminCmsContentd0ce974814566418b6ad509f305f319a'|'AdminCmsContentd3b206d196cd6be3a2764c1fb90b200f'|'AdminCmsContente25f0ecd41211b01c83e5fec41df4fe7'|'AdminCmsContentef61fb324d729c341ea8ab9901e23566'|'AdminCmsContentf7931413dee107ddf5289c8886baf7ec'|'AdminCmsd3b206d196cd6be3a2764c1fb90b200f'|'AdminCmse25f0ecd41211b01c83e5fec41df4fe7'|'AdminCmse6b391a8d2c4d45902a23a8b6585703d'|'AdminCmsef61fb324d729c341ea8ab9901e23566'|'AdminCmsf8825c9f08ff15b5ef6bc3a3898817e8'|'AdminContacts00d23a76e43b46dae9ec7aa9dcbebb32'|'AdminContacts0f28459fa87b1b3ce6e8b17932f08c3a'|'AdminContacts9aa698f602b1e5694855cee73a683488'|'AdminContacts9cd9efd3eb168071eb0a199972c54aab'|'AdminContacts9d55fc80bbb875322aa67fd22fc98469'|'AdminContactsa2b086325f59e6c2fbd410511f4fdfb3'|'AdminContactsa4cd3191fdeea29906a113c78d4c0e26'|'AdminContactsb357b524e740bc85b9790a0712d84a30'|'AdminContactsb5a7adde1af5c87d7fd797b6245c2a39'|'AdminContactsb718adec73e04ce3ec720dd11a06a308'|'AdminContactsb78a3223503896721cca1303f776159b'|'AdminContactsb9f5c797ebbf55adccdd8539a65a0241'|'AdminContactsc41f67055a184ed2e895681336572761'|'AdminContactsc9cc8cce247e49bae79f15173ce97354'|'AdminContactsd3b206d196cd6be3a2764c1fb90b200f'|'AdminContactsdaedf9c5c8f38ac4cf641f3fb3e1bdc4'|'AdminContactse25f0ecd41211b01c83e5fec41df4fe7'|'AdminCountries00d23a76e43b46dae9ec7aa9dcbebb32'|'AdminCountries01e93e9457d86c646965decd586dc5ea'|'AdminCountries05820ffcf621269347a1c14d81d20b77'|'AdminCountries0bd345b58335589d4c2fa1e50ae38619'|'AdminCountries0c750dacc725ba4047374d2efc56ce3a'|'AdminCountries10d30c6319cf61386c878e4d9a3e09a2'|'AdminCountries21e6a1298ab4cd040464d67a19d0f957'|'AdminCountries25d176f9d01ba273d1097ca7b298d281'|'AdminCountries3477a6086401c89ab72387673c777af2'|'AdminCountries3a58c76da4f48aaeb46af20f34caac1b'|'AdminCountries3be0efaecb3514a14757b8beb4b5dbb3'|'AdminCountries46a2a41cc6e552044816a2d04634545d'|'AdminCountries4d3d769b812b6faa6b76e1a8abaece2d'|'AdminCountries59716c97497eb9694541f7c3d37b1a4d'|'AdminCountries6252c0f2c2ed83b7b06dfca86d4650bb'|'AdminCountries6416e8cb5fc0a208d94fa7f5a300dbc4'|'AdminCountries665e1ad1c6657791cecb5b68008c7c00'|'AdminCountries790d59ef178acbc75d233bf4211763c6'|'AdminCountries92de0162cbdfa60f671ba3cad1d392a1'|'AdminCountries93cba07454f06a4a960172bbd6e2a435'|'AdminCountries94d7422ba3c5b0f2a35f50b048e51c6d'|'AdminCountries9d55fc80bbb875322aa67fd22fc98469'|'AdminCountriesa2ddbdfb29a0708bd711601f9277435c'|'AdminCountriesa4f164d8b1b72c87b8ce558827bcd423'|'AdminCountriesab81f235de173b2d7c0b69009dc6d492'|'AdminCountriesabb056fd74a8bdf858dbe3e68c5ea97c'|'AdminCountriesad68f9bafd9bf2dcf3865dac55662fd5'|'AdminCountriesb3ff996fe5c77610359114835baf9b38'|'AdminCountriesb718adec73e04ce3ec720dd11a06a308'|'AdminCountriesb9f5c797ebbf55adccdd8539a65a0241'|'AdminCountriesbafd7322c6e97d25b6299b5d6fe8920b'|'AdminCountriesc9cc8cce247e49bae79f15173ce97354'|'AdminCountriescd2f7b9409e0f1527766ad35aa8bd3c5'|'AdminCountriesce26601dac0dea138b7295f02b7620a7'|'AdminCountriesd3b206d196cd6be3a2764c1fb90b200f'|'AdminCountriesd3c5d8339f3840b75b4031c2b1e508de'|'AdminCountriesd8ec51bf63378409b1d40cc45c80f926'|'AdminCountriesdd7bf230fde8d4836917806aff6a6b27'|'AdminCountriese25f0ecd41211b01c83e5fec41df4fe7'|'AdminCountriesecefe3def8a2d034d80f6a8876c3d4b1'|'AdminCountriesf52c1ff75f69fa46ae947f0a3f653641'|'AdminCurrencies00d23a76e43b46dae9ec7aa9dcbebb32'|'AdminCurrencies02c86eb2792f3262c21d030a87e19793'|'AdminCurrencies05f78b95fd31ed10def4d0c1ef8e4751'|'AdminCurrencies076b68505282c6c0654708db343d6673'|'AdminCurrencies188b945338e1d6582c845dfebb469a45'|'AdminCurrencies1d10d84822a63187918311cb3a4e0c87'|'AdminCurrencies2b417805040de3a3df31c8fd3626b57c'|'AdminCurrencies2faec1f9f8cc7f8f40d521c4dd574f49'|'AdminCurrencies386c339d37e737a436499d423a77df0c'|'AdminCurrencies462fdc88328b3c9d31c63fa01b4f00b1'|'AdminCurrencies4abafa9686f98e398e29e46fd388fa36'|'AdminCurrencies4ef5571f164a6a7fcc9f4625d14e260b'|'AdminCurrencies586e27d3575f00e51ad43b66eb34e49f'|'AdminCurrencies597d44d65d4c76fe8cc8127b5b9b98bc'|'AdminCurrencies5f41116581201a5ef32656b7d4a51e88'|'AdminCurrencies5f838bbd088886f09f67b904e414f0e7'|'AdminCurrencies627b24abf27e2d03d38537f84e81cb2e'|'AdminCurrencies7559b7b096e0579368ca2ac7c187ba52'|'AdminCurrencies77428b04a1847555eb9bc52422a377b0'|'AdminCurrencies7c77e53206853cb381e91e037554faa3'|'AdminCurrencies8246d0c794e7db090587c4797b2a234f'|'AdminCurrencies876ca43ba50351d4e492970f40632661'|'AdminCurrencies9b9e87b59be497e92da8d2208f9914a0'|'AdminCurrencies9d55fc80bbb875322aa67fd22fc98469'|'AdminCurrenciesabe69e8e4b387562a767a6adaf112ed8'|'AdminCurrenciesad68f9bafd9bf2dcf3865dac55662fd5'|'AdminCurrenciesae2fce768106a7b6a61e57943b7a2143'|'AdminCurrenciesb718adec73e04ce3ec720dd11a06a308'|'AdminCurrenciesb9f5c797ebbf55adccdd8539a65a0241'|'AdminCurrenciesc1eaa657dda2892e5fec322ac710133a'|'AdminCurrenciesc9cc8cce247e49bae79f15173ce97354'|'AdminCurrenciesd3b206d196cd6be3a2764c1fb90b200f'|'AdminCurrenciesdfcfc43722eef1eab1e4a12e50a068b1'|'AdminCurrenciese25f0ecd41211b01c83e5fec41df4fe7'|'AdminCurrenciese75e316ab3a0a8c0c5fc4b48d1a7033f'|'AdminCurrenciesf6536046d7af41c3f3975868d6963179'|'AdminCurrenciesf90c17fdefeeca8b0d55b80a7bc3cb34'|'AdminCurrenciesfca4f4976817baa4a25858a3d6d5274d'|'AdminCustomerPreferences0db377921f4ce762c62526131097968f'|'AdminCustomerPreferences9f7a304fd501ed0e4d06b899fed739d0'|'AdminCustomerPreferencesbcb9adf1d2347258b5c65483e34cf86f'|'AdminCustomerPreferencesf2c822352f0e0a62e2de6d716475911b', '%s - All people who have created an account on this site.'|'%s - All people without a valid customer account.'|'%s - Customer who placed an order with the guest checkout.'|'%s tax excl.'|'(ie. "DROP TABLE IF EXISTS")'|'30 days'|'Abandoned cart'|'Abandoned Carts'|'According to total price'|'According to total price.'|'According to total weight'|'According to total weight.'|'Active'|'Add handling costs'|'Add New'|'Add new'|'Add new address'|'Add new attachment'|'Add New Attribute'|'Add new attribute'|'Add New Attributes'|'Add new carrier'|'Add new cart rule'|'Add new category'|'Add new CMS category'|'Add new CMS page'|'Add new contact'|'Add new country'|'Add new currency'|'Add new root category'|'Add New Value'|'Add new value'|'Add New Values'|'Address'|'Address alias'|'Address format'|'Address format invalid'|'Addresses'|'Age'|'Allowed characters: letters, spaces and %s'|'Allowed characters: letters, spaces and "%s".'|'An error occurred while saving associations of shops.'|'An error occurred while saving carrier groups.'|'An error occurred while saving carrier logo.'|'An error occurred while saving carrier ranges.'|'An error occurred while saving carrier zones.'|'An error occurred while saving the tax rules group.'|'An error occurred while saving this carrier.'|'Applies to all prices (e.g. $1,240.15).'|'Apply both regular shipping cost and product-specific shipping costs.'|'Apply shipping cost'|'Apply the cost of the highest defined range'|'Assign to a new zone'|'Associated with'|'Attachment'|'Attribute group'|'Attribute type'|'Attributes'|'Attributes generator'|'Automatically check for module updates'|'Automatically update currency rates'|'Average number of products per category'|'Average Order Value'|'Back to list'|'Back to the product'|'Backup options'|'Bad SQL query'|'Billing'|'Call prefix'|'Cancel'|'Carrier'|'Carrier name'|'Carrier name displayed during checkout'|'Carriers'|'Cart #%06d'|'Cart Rules'|'Categories'|'Category'|'Category Cover Image'|'Category thumbnail'|'Check the cookie\'s IP address'|'Check the IP address of the cookie in order to prevent your cookie from being stolen.'|'Choose a color with the color picker, or enter an HTML color (e.g. "lightblue", "#CC6600").'|'Choose the attribute group for this value.'|'City'|'CMS'|'CMS Category'|'CMS Page'|'Code'|'Color'|'Color or texture'|'Company'|'Contact name (e.g. Customer Support).'|'Contacts'|'Contains states'|'Conversion Rate'|'Countries'|'Country'|'Country name'|'Country options'|'Currencies'|'Currency'|'Currency format'|'Currency name'|'Currency rates'|'Current texture'|'Customer'|'Date'|'Day'|'Days'|'Decimals'|'Default behavior'|'Default currency'|'Default store currency'|'Define the upload limit for a downloadable product (in megabytes). This value has to be lower or equal to the maximum file upload allotted by your server (currently: %s MB).'|'Define the upload limit for an image (in megabytes). This value has to be lower or equal to the maximum file upload allotted by your server (currently: %s MB).'|'Delay'|'Delete'|'Delete selected'|'Delete selected item?'|'Delete selected items?'|'Delivery tracking URL: Type \'@\' where the tracking number should appear. It will be automatically replaced by the tracking number.'|'Delivery tracking URL: Type \'@\' where the tracking number should appear. It will then be automatically replaced by the tracking number.'|'Description'|'Disable carrier'|'Disabled'|'Disabled Categories'|'Display decimals in prices.'|'Display tax label (e.g. "Tax incl.")'|'Display this country to your customers (the selected country will always be displayed in the Back Office).'|'Displayed'|'DNI / NIF / NIE'|'Do you need a tax identification number?'|'Does it need Zip/postal code?'|'Drop existing tables during import'|'Drop existing tables during import.'|'Drop-down list'|'Edit'|'Edit New Attribute'|'Edit Value'|'Edit: %s'|'Email address'|'Emails will be sent to this address.'|'Empty Categories'|'Enable'|'Enable the carrier in the front office.'|'Enabled'|'Enter "0" for a longest shipping delay, or "9" for the shortest shipping delay.'|'Estimated delivery time will be displayed during checkout.'|'Exchange rate'|'Exchange rates are calculated from one unit of your shop\'s default currency. For example, if the default currency is euros and your chosen currency is dollars, type "1.20" (1€ = $1.20).'|'Expiration date'|'Export carts'|'Failed to copy the file.'|'Failed to update the status'|'File'|'File name'|'File not found'|'File size'|'Filename'|'Finish'|'First Name'|'For example: \'http://example.com/track.php?num=@\' with \'@\' where the tracking number should appear.'|'For in-store pickup, enter 0 to replace the carrier name with your shop name.'|'Forbidden characters'|'Free Shipping'|'Free shipping'|'Friendly URL'|'From %s to %s'|'Further information regarding this contact.'|'General'|'General settings'|'Geographical region.'|'Group access'|'Home phone'|'Hour'|'Hours'|'hours'|'ID'|'Identification Number'|'If enabled, all messages will be saved in the "Customer Service" page under the "Customer" menu.'|'If enabled, the backup script will drop your tables prior to restoring data.'|'Ignore statistics tables'|'Import'|'Include a space between symbol and price (e.g. $1,240.15 -> $ 1,240.15).'|'Include the handling costs (as set in Shipping > Preferences) in the final carrier price.'|'Include the shipping and handling costs in the carrier price.'|'Indexation by search engines'|'Indicate the format of the postal code: use L for a letter, N for a number, and C for the country\'s ISO 3166-1 alpha-2 code. For example, NNNNN for the United States, France, Poland and many other; LNNNNLLL for Argentina, etc. If you do not want PrestaShop to verify the postal code for this country, leave it blank.'|'International call prefix, (e.g. 1 for United States).'|'Invalid characters'|'ISO code'|'ISO code (e.g. USD for Dollars, EUR for Euros, etc.).'|'ISO code number'|'It appears the backup was successful, however you must download and carefully verify the backup file before proceeding.'|'Last Name'|'Lifetime of back office cookies'|'Lifetime of front office cookies'|'Logo'|'Mark all of the customer groups which you would like to have access to this category.'|'Mark the groups that are allowed access to this carrier.'|'Maximum depth managed by this carrier. Set the value to "0", or leave this field blank to ignore.'|'Maximum depth managed by this carrier. Set the value to "0," or leave this field blank to ignore.'|'Maximum height managed by this carrier. Set the value to "0", or leave this field blank to ignore.'|'Maximum height managed by this carrier. Set the value to "0," or leave this field blank to ignore.'|'Maximum package depth'|'Maximum package depth (%s)'|'Maximum package height'|'Maximum package height (%s)'|'Maximum package weight'|'Maximum package weight (%s)'|'Maximum package width'|'Maximum package width (%s)'|'Maximum size for a downloadable product'|'Maximum size for a product\'s image'|'Maximum size for attachment'|'Maximum weight managed by this carrier. Set the value to "0", or leave this field blank to ignore.'|'Maximum weight managed by this carrier. Set the value to "0," or leave this field blank to ignore.'|'Maximum width managed by this carrier. Set the value to "0", or leave this field blank to ignore.'|'Maximum width managed by this carrier. Set the value to "0," or leave this field blank to ignore.'|'megabytes'|'Meta description'|'Meta keywords'|'Meta title'|'Mobile phone'|'MultiStore'|'Name'|'Net Profit per Visitor'|'New modules and updates are displayed on the modules page.'|'Next'|'No'|'No items found'|'No Tax'|'No tax'|'Non ordered'|'Notifications'|'Notifications are numbered bubbles displayed at the very top of your back office, right next to the shop\'s name. They display the number of new items since you last clicked on them.'|'Numeric ISO code'|'Numeric ISO code (e.g. 840 for Dollars, 978 for Euros, etc.).'|'Official list here'|'Online'|'Only account creation'|'Only letters and the hyphen (-) character are allowed.'|'Only letters and the minus (-) character are allowed.'|'Only letters and the minus character are allowed.'|'Only letters, numbers, underscore (_) and the minus (-) character are allowed.'|'or'|'Order ID'|'Other'|'Out-of-range behavior'|'Out-of-range behavior occurs when no defined range matches the customer\'s cart (e.g. when the weight of the cart is greater than the highest weight limit defined by the weight ranges).'|'Out-of-range behavior occurs when none is defined (e.g. when a customer\'s cart weight is greater than the highest range limit).'|'Page content'|'Pages in category "%s"'|'Pages in this category'|'Parent category'|'Parent CMS Category'|'Payment'|'Performance'|'Please set another carrier as default before deleting this one.'|'Position'|'Previous'|'Priority'|'product(s)'|'Public name'|'Quantity'|'Radio buttons'|'Registration process type'|'Restrict country selections in front office to those covered by active carriers'|'Root Category'|'Save'|'Save and preview'|'Save and Stay'|'Save and stay'|'Save messages?'|'Save then add another value'|'Set the amount of hours during which the back office cookies are valid. After that amount of time, the PrestaShop user will have to log in again.'|'Set the amount of hours during which the front office cookies are valid. After that amount of time, the customer will have to log in again.'|'Set the maximum size allowed for attachment files (in megabytes). This value has to be lower or equal to the maximum file upload allotted by your server (currently: %s MB).'|'Shipping and handling'|'Shipping locations and costs'|'Shop association'|'Show notifications for new customers'|'Show notifications for new messages'|'Show notifications for new orders'|'Size'|'Size, weight, and group access'|'Spacing'|'Speed grade'|'Standard (account creation and address creation)'|'State'|'Status'|'Such as with Dollars'|'Such as with Euros'|'Summary'|'Symbol'|'Tax'|'Texture'|'The "Backups" directory located in the admin directory must be writable (CHMOD 755 / 777).'|'The carrier\'s name will be displayed during checkout.'|'The estimated delivery time will be displayed during checkout.'|'The file %1dollars exceeds the size allowed by the server. The limit is set to %2dollard MB.'|'The file is too large. Maximum size allowed is: %1dollard kB. The file you are trying to upload is %2dollard kB.'|'The public name for this attribute, displayed to the customers.'|'The status has been updated successfully'|'The value must be an integer.'|'The way the attribute\'s values will be presented to the customers in the product\'s page.'|'The zones in which this carrier will be used.'|'This attachment is associated with the following products, do you really want to delete it?'|'This feature has been disabled. You can activate it here: %s.'|'This is the main image for your category, displayed in the category page. The category description will overlap this image and appear in its top-left corner.'|'This will display notifications every time a new customer registers in your shop.'|'This will display notifications when new messages are posted in your shop.'|'This will display notifications when new orders are made in your shop.'|'This will override the HTML color!'|'Title'|'To add "tags" click in the field, write something, and then press "Enter."'|'To add "tags," click in the field, write something, and then press "Enter."'|'Top Category'|'Total'|'Total Cart'|'Tracking URL'|'Transit time'|'Two -- or three -- letter ISO code (e.g. "us for United States).'|'Two -- or three -- letter ISO code (e.g. U.S. for United States)'|'Update currency rates'|'Upload a logo from your computer.'|'Upload an image file containing the color texture from your computer.'|'Upload error. Please check your server configurations for the maximum upload size allowed.'|'Upload quota'|'URL'|'Use one of our recommended carrier modules'|'Use PrestaShop\'s webservice to update your currency exchange rates. However, please use caution: rates are provided as-is.'|'Use PrestaShop\'s webservice to update your currency\'s exchange rates. However, please use caution: rates are provided as-is.'|'Value'|'Values'|'Values count'|'VAT number'|'Warehouse'|'Will appear in front office (e.g. $, €, etc.)'|'Yes'|'You can place the following URL in your crontab file, or you can click it yourself regularly'|'You cannot delete the default currency'|'You cannot disable the default currency'|'You must choose at least one shop or group shop.'|'You must register at least one phone number.'|'You now have three default customer groups.'|'Your internal name for this attribute.'|'Zip/Postal Code'|'Zip/postal code format'|'Zone'>&oversized-array", $_LANGADM); diff --git a/tests/PHPStan/Analyser/data/bug-5091.php b/tests/PHPStan/Analyser/data/bug-5091.php new file mode 100644 index 0000000000..626843d9cf --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5091.php @@ -0,0 +1,175 @@ + '']; + } + } +} + +namespace Bug5091 { + + /** + * @phpstan-type MyType array{foobar: string} + */ + trait MyTrait + { + /** + * @return array + */ + public function MyMethod(): array + { + return [['foobar' => 'foo']]; + } + } + + class MyClass + { + use MyTrait; + } + + /** + * @phpstan-type TypeArrayAjaxResponse array{ + * message : string, + * status : int, + * success : bool, + * value : null|float|int|string, + * } + */ + trait MyTrait2 + { + /** @return TypeArrayAjaxResponse */ + protected function getAjaxResponse(): array + { + return [ + "message" => "test", + "status" => 200, + "success" => true, + "value" => 5, + ]; + } + } + + class MyController + { + use MyTrait2; + } + + + /** + * @phpstan-type X string + */ + class Types {} + + /** + * @phpstan-import-type X from Types + */ + trait t { + /** @return X */ + public function getX() { + return "123"; + } + } + + class aClass + { + use t; + } + + /** + * @phpstan-import-type X from Types + */ + class Z { + /** @return X */ + public function getX() { // works as expected + return "123"; + } + } + + /** + * @phpstan-type SomePhpstanType array{ + * property: mixed + * } + */ + trait TraitWithType + { + /** + * @phpstan-return SomePhpstanType + */ + protected function get(): array + { + return [ + 'property' => 'something', + ]; + } + } + + /** + * @phpstan-import-type SomePhpstanType from TraitWithType + */ + class ClassWithTraitWithType + { + use TraitWithType; + + /** + * @phpstan-return SomePhpstanType + */ + public function SomeMethod(): array + { + return $this->get(); + } + } + + /** + * @phpstan-type FooJson array{bar: string} + */ + trait Foo { + /** + * @phpstan-return FooJson + */ + public function sayHello(\DateTime $date): array + { + return [ + 'bar'=> 'baz' + ]; + } + } + + /** + * @phpstan-import-type FooJson from Foo + */ + class HelloWorld + { + use Foo; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5287-php81.php b/tests/PHPStan/Analyser/data/bug-5287-php81.php deleted file mode 100644 index 21920ff3da..0000000000 --- a/tests/PHPStan/Analyser/data/bug-5287-php81.php +++ /dev/null @@ -1,59 +0,0 @@ - $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 deleted file mode 100644 index 3bcb5eb4fb..0000000000 --- a/tests/PHPStan/Analyser/data/bug-5287.php +++ /dev/null @@ -1,59 +0,0 @@ - $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-5312.php b/tests/PHPStan/Analyser/data/bug-5312.php new file mode 100644 index 0000000000..95428adc4b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5312.php @@ -0,0 +1,14 @@ + + */ +interface Updatable +{ + /** + * @param T $object + */ + public function update(Updatable $object): void; +} diff --git a/tests/PHPStan/Analyser/data/bug-5390.php b/tests/PHPStan/Analyser/data/bug-5390.php new file mode 100644 index 0000000000..57332bebe3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5390.php @@ -0,0 +1,19 @@ +b->someMethod(); + } +} +/** @mixin A */ +class B +{ + +} diff --git a/tests/PHPStan/Analyser/data/bug-5597.php b/tests/PHPStan/Analyser/data/bug-5597.php new file mode 100644 index 0000000000..19720c8a17 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5597.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug5597; + +interface InterfaceA {} + +class ClassA implements InterfaceA {} + +class ClassB +{ + public function __construct( + private InterfaceA $parameterA, + ) { + } + + public function test() : InterfaceA + { + return $this->parameterA; + } +} + +$classA = new class() extends ClassA {}; +$thisWorks = new class($classA) extends ClassB {}; + +$thisFailsWithTwoErrors = new class(new class() extends ClassA {}) extends ClassB {}; diff --git a/tests/PHPStan/Analyser/data/bug-5668.php b/tests/PHPStan/Analyser/data/bug-5668.php deleted file mode 100644 index 5633ce0ab0..0000000000 --- a/tests/PHPStan/Analyser/data/bug-5668.php +++ /dev/null @@ -1,44 +0,0 @@ - $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-5698-php7.php b/tests/PHPStan/Analyser/data/bug-5698-php7.php deleted file mode 100644 index 10d2ceb899..0000000000 --- a/tests/PHPStan/Analyser/data/bug-5698-php7.php +++ /dev/null @@ -1,16 +0,0 @@ -', $foo); - assertNativeType('array', $foo); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-5843.php b/tests/PHPStan/Analyser/data/bug-5843.php deleted file mode 100644 index d9793fe2c8..0000000000 --- a/tests/PHPStan/Analyser/data/bug-5843.php +++ /dev/null @@ -1,36 +0,0 @@ -= 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-6160.php b/tests/PHPStan/Analyser/data/bug-6160.php new file mode 100644 index 0000000000..b0ac5850d1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6160.php @@ -0,0 +1,25 @@ + + */ + public static function split($flags = 0){ + return []; + } + + public static function test(): void + { + $a = self::split(94561); // should error + $a = self::split(PREG_SPLIT_NO_EMPTY); // should work + $a = self::split(PREG_SPLIT_DELIM_CAPTURE); // should work + $a = self::split(PREG_SPLIT_NO_EMPTY_COPY); // should work + $a = self::split("sdf"); // should error + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6192.php b/tests/PHPStan/Analyser/data/bug-6192.php new file mode 100644 index 0000000000..65c0b20226 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6192.php @@ -0,0 +1,34 @@ += 8.1 + +namespace Bug6192; + +class Foo { + public function __construct( + public string $value = 'default foo foo' + ) {} +} + +class Bar { + public function __construct( + public Foo $foo = new Foo('default bar foo') + ) {} +} + +class Baz +{ + + public function doFoo(): void + { + echo "Testing Foo\n"; + var_export(new Foo('testing foo')); + echo "\n"; + } + + public function doBar(): void + { + echo "Testing Bar\n"; + var_export(new Bar(new Foo('testing bar'))); + echo "\n"; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-6265.php b/tests/PHPStan/Analyser/data/bug-6265.php new file mode 100644 index 0000000000..a50d962c3f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6265.php @@ -0,0 +1,67 @@ +comments['#' . $comment['thread_parentid']])) + { + if (in_array('#' . $comment['parentid'], $lv1_keys)) + { + if (!$match) + { + for ($ii = 0;$ii < count($lv2_keys);$ii++) + { + if (!$match3) + { + for ($iii = 0;$iii < count($lv3_keys_all);$iii++) + { + if (!$match4) + { + for ($iiii = 0;$iiii < count($lv4_keys_all);$iiii++) + { + if (!$match5) + { + for ($i6 = 0;$i6 < count($lv5_keys_all);$i6++) + { + if (!$match6) + { + for ($i7 = 0;$i7 < count($lv6_keys_all);$i7++) + { + if (!$match7) + { + for ($i8 = 0;$i8 < count($lv7_keys_all);$i8++) + { + if (!$match8) + { + for ($i9 = 0;$i9 < count($lv8_keys_all);$i9++) + { + if (!$match9) + { + + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + return true; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-6433.php b/tests/PHPStan/Analyser/data/bug-6433.php deleted file mode 100644 index 9a31de7ef1..0000000000 --- a/tests/PHPStan/Analyser/data/bug-6433.php +++ /dev/null @@ -1,21 +0,0 @@ -= 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-6442.php b/tests/PHPStan/Analyser/data/bug-6442.php index 413826daa7..2bcd2309ff 100644 --- a/tests/PHPStan/Analyser/data/bug-6442.php +++ b/tests/PHPStan/Analyser/data/bug-6442.php @@ -17,7 +17,7 @@ class B extends A use T; } -new class() extends B +$a = new class() extends B { use T; }; diff --git a/tests/PHPStan/Analyser/data/bug-6494.php b/tests/PHPStan/Analyser/data/bug-6494.php new file mode 100644 index 0000000000..ba325b86d1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6494.php @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug6494; + +use function PHPStan\Testing\assertType; + +// To get rid of warnings about using new static() +interface SomeInterface { + public function __construct(); +} + +class Base implements SomeInterface { + + public function __construct() {} + + /** + * @return \Generator + */ + public static function instances() { + yield new static(); + } +} + +function (): void { + foreach ((new Base())::instances() as $h) { + assertType(Base::class, $h); + } +}; + +class Extension extends Base { + +} + +function (): void { + foreach ((new Extension())::instances() as $h) { + assertType(Extension::class, $h); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-6649.php b/tests/PHPStan/Analyser/data/bug-6649.php new file mode 100644 index 0000000000..9340cfb4e4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6649.php @@ -0,0 +1,30 @@ + + */ +class SubCollection extends Collection { + /** @param TKey $key */ + public function __construct($key) { + assertType('TKey of Bug6649\Bar&Bug6649\Foo (class Bug6649\SubCollection, argument)', $key); + } + + public static function test(): void { + assertType('Bug6649\SubCollection', new SubCollection(new FooBar())); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6842.php b/tests/PHPStan/Analyser/data/bug-6842.php new file mode 100644 index 0000000000..7565f71c85 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6842.php @@ -0,0 +1,71 @@ + + */ + public function getScheduledEvents( + \DateTimeInterface $startDate, + \DateTimeInterface $endDate + ): \Iterator { + $interval = \DateInterval::createFromDateString('1 day'); + + /** @var \Iterator $datePeriod */ + $datePeriod = new \DatePeriod($startDate, $interval, $endDate); + + foreach ($datePeriod as $dateTime) { + $scheduledEvent = $this->createScheduledEventFromSchedule($dateTime); + + if ($scheduledEvent >= $startDate) { + yield $scheduledEvent; + } + } + } + + /** + * @template T of \DateTimeInterface|\DateTime|\DateTimeImmutable + * + * @param T $startDate + * @param T $endDate + * + * @return \Iterator + */ + public function getScheduledEvents2( + \DateTimeInterface $startDate, + \DateTimeInterface $endDate + ): \Iterator { + $interval = \DateInterval::createFromDateString('1 day'); + + /** @var \DatePeriod<\DateTimeInterface, \DateTimeInterface, null>&iterable $datePeriod */ + $datePeriod = new \DatePeriod($startDate, $interval, $endDate); + + foreach ($datePeriod as $dateTime) { + $scheduledEvent = $this->createScheduledEventFromSchedule($dateTime); + + if ($scheduledEvent >= $startDate) { + yield $scheduledEvent; + } + } + } + + /** + * @template T of \DateTimeInterface + * + * @param T|\DateTime|\DateTimeImmutable $dateTime + * + * @return T|\DateTime|\DateTimeImmutable + */ + protected function createScheduledEventFromSchedule( + \DateTimeInterface $dateTime + ): \DateTimeInterface { + return $dateTime; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6866.php b/tests/PHPStan/Analyser/data/bug-6866.php new file mode 100644 index 0000000000..f268da3f2f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6866.php @@ -0,0 +1,9 @@ += 8.0 + +namespace Bug6872; + +/** + * @template TState of object + */ +abstract class Bug6872 { + public function __construct() { + // empty + } + + /** + * @param TState $state + */ + protected function saveState(object $state): void { + $this->set($state); + } + + /** + * @param object|array|string|float|int|bool|null $value + */ + public function set(object|array|string|float|int|bool|null $value): mixed { + return $value; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6896.php b/tests/PHPStan/Analyser/data/bug-6896.php new file mode 100644 index 0000000000..3e5e15eae8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6896.php @@ -0,0 +1,44 @@ += 8.0 + +namespace Bug6896; + +use IteratorIterator; +use LimitIterator; +use Traversable; +use ArrayObject; + +/** + * @template TKey as array-key + * @template TValue + * + * @extends ArrayObject + * + * Basic generic iterator, with additional helper functions. + */ +abstract class XIterator extends ArrayObject +{ +} + +final class RandHelper +{ + + /** + * @template TRandKey as array-key + * @template TRandVal + * @template TRandList as array|XIterator|Traversable + * + * @param TRandList $list + * + * @return ( + * TRandList is array ? array : ( + * TRandList is XIterator ? XIterator : + * IteratorIterator|LimitIterator + * )) + */ + public static function getPseudoRandomWithUrl( + array|XIterator|Traversable $list, + ): array|XIterator|IteratorIterator|LimitIterator + { + return $list; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6936.php b/tests/PHPStan/Analyser/data/bug-6936.php new file mode 100644 index 0000000000..93c30a4ade --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6936.php @@ -0,0 +1,112 @@ + $adansch) { + $aktueller_endkunde = new x(); + $avk = new avk(); + $change = 0; + $col = []; + if ($adansch['telefon'] != $aktueller_endkunde->telefon && '' != $adansch['telefon']) { + $col['telefon'] = $adansch['telefon']; + $change = 1; + } + if ($adansch['email'] != $aktueller_endkunde->email && '' != $adansch['email']) { + $col['email'] = $adansch['email']; + $change = 1; + } + if ($adansch['fa_gruendungsjahr'] != $aktueller_endkunde->fa_gruendungsjahr) { + $col['fa_gruendungsjahr'] = $adansch['fa_gruendungsjahr']; + $change = 1; + } + if ($adansch['fa_geschaeftsfuehrer'] != $aktueller_endkunde->fa_geschaeftsfuehrer) { + $col['fa_geschaeftsfuehrer'] = $adansch['fa_geschaeftsfuehrer']; + $change = 1; + } + if ($adansch['handelregnr'] != $aktueller_endkunde->handelregnr) { + $col['handelregnr'] = $adansch['handelregnr']; + $change = 1; + } + if ($adansch['amtsgericht'] != $aktueller_endkunde->amtsgericht) { + $col['amtsgericht'] = $adansch['amtsgericht']; + $change = 1; + } + if ($adansch['ustid'] != $aktueller_endkunde->ustid) { + $col['ustid'] = $adansch['ustid']; + $change = 1; + } + if ($adansch['ustnr'] != $aktueller_endkunde->ustnr) { + $col['ustnr'] = $adansch['ustnr']; + $change = 1; + } + + if ($adansch['firma'] != $aktueller_endkunde->firma) { + $col['firma'] = $adansch['firma']; + $change = 1; + } + + if (1 == $change) { + // MobisHelper::createXmlDataJob("ada",(int)$aktueller_endkunde->adaid, $col); + if (!isset($_SENDJOB[$avk->avkid]['ada'][$aktueller_endkunde->adaid])) { + $_SENDJOB[$avk->avkid]['ada'][$aktueller_endkunde->adaid] = []; + } + + $_SENDJOB[$avk->avkid]['ada'][$aktueller_endkunde->adaid] = $_SENDJOB[$avk->avkid]['ada'][$aktueller_endkunde->adaid] + $col; + } + } + } +} + + +class x { + /** + * @var int + */ + public $adaid; + /** + * @var string + */ + public $telefon; + /** + * @var string + */ + public $email; + /** + * @var string + */ + public $fa_gruendungsjahr; + /** + * @var string + */ + public $fa_geschaeftsfuehrer; + /** + * @var string + */ + public $handelregnr; + /** + * @var string + */ + public $amtsgericht; + /** + * @var string + */ + public $ustid; + /** + * @var string + */ + public $ustnr; + /** + * @var string + */ + public $firma; +} +class avk { + /** + * @var int + */ + public $avkid; +} diff --git a/tests/PHPStan/Analyser/data/bug-6940.php b/tests/PHPStan/Analyser/data/bug-6940.php new file mode 100644 index 0000000000..e62dbc3e19 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6940.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug7012; + +enum Foo +{ + case BAR; +} + +function test(Foo $f = Foo::BAR): void +{ + echo 'test'; +} + +function test2(): void +{ + test(); +} diff --git a/tests/PHPStan/Analyser/data/bug-7030.php b/tests/PHPStan/Analyser/data/bug-7030.php new file mode 100644 index 0000000000..12c1ab586a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7030.php @@ -0,0 +1,18 @@ += 0) { + $dt->add($interval); + } else { + $dt->sub($interval); + } + + return $dt->format('Y-m-d H:i:s'); +} + +function date_add_day(?string $date, ?int $days): ?string { + return date_add('D', $days, $date); +} + +function date_add_month(?string $date, ?int $months): ?string { + return date_add('M', $months, $date); +} diff --git a/tests/PHPStan/Analyser/data/bug-7094.php b/tests/PHPStan/Analyser/data/bug-7094.php new file mode 100644 index 0000000000..bcf26cb62a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7094.php @@ -0,0 +1,79 @@ + + */ +trait AttributeTrait +{ + /** + * @param key-of $key + */ + public function hasAttribute(string $key): bool + { + $attr = $this->getAttributes(); + + return isset($attr[$key]); + } + + /** + * @template K of key-of + * @param K $key + * @param T[K] $val + */ + public function setAttribute(string $key, $val): void + { + $attr = $this->getAttributes(); + $attr[$key] = $val; + $this->setAttributes($attr); + } + + /** + * @template K of key-of + * @param K $key + * @return T[K]|null + */ + public function getAttribute(string $key) + { + return $this->getAttributes()[$key] ?? null; + } + + /** + * @param key-of $key + */ + public function unsetAttribute(string $key): void + { + $attr = $this->getAttributes(); + unset($attr[$key]); + $this->setAttributes($attr); + } +} + +/** + * @phpstan-type Attrs array{foo?: string, bar?: 5|6|7, baz?: bool} + */ +class Foo { + /** @use AttributeTrait */ + use AttributeTrait; + + /** @return Attrs */ + public function getAttributes(): array + { + return []; + } + + /** @param Attrs $attr */ + public function setAttributes(array $attr): void + { + } +} + + +$f = new Foo; +$f->setAttribute('unknown-attr-err', 3); +$f->setAttribute('foo', 3); // invalid type should error! +$f->setAttribute('bar', 3); // invalid type should error! +$f->setAttribute('bar', 5); // valid! +$f->setAttribute('foo', 5); // NOT VALID +$f->getAttribute('unknown-attr-err'); diff --git a/tests/PHPStan/Analyser/data/bug-7110.php b/tests/PHPStan/Analyser/data/bug-7110.php new file mode 100644 index 0000000000..9a81a94852 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7110.php @@ -0,0 +1,39 @@ += 8.1 + +namespace Bug7135; + +class HelloWorld +{ + private \Closure $closure; + public function sayHello(callable $callable): void + { + $this->closure = $callable(...); + } + public function sayHello2(callable $callable): void + { + $this->closure = $this->sayHello(...); + } + public function sayHello3(callable $callable): void + { + $this->closure = strlen(...); + } + public function sayHello4(callable $callable): void + { + $this->closure = new HelloWorld(...); + } + public function sayHello5(callable $callable): void + { + $this->closure = self::doFoo(...); + } + + public static function doFoo(): void + { + + } + + public function getClosure(): \Closure + { + return $this->closure; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7140.php b/tests/PHPStan/Analyser/data/bug-7140.php new file mode 100644 index 0000000000..ede86286ad --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7140.php @@ -0,0 +1,46 @@ +getFoo()->getFoo()); +}; diff --git a/tests/PHPStan/Analyser/data/bug-7215.php b/tests/PHPStan/Analyser/data/bug-7215.php new file mode 100644 index 0000000000..4e278c3bb1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7215.php @@ -0,0 +1,26 @@ + $array + * @return (T is int ? ($array is non-empty-array ? non-empty-list : list) : ($array is non-empty-array ? non-empty-list : list)) +*/ +function keysAsString(array $array): array +{ + $keys = []; + + foreach ($array as $k => $_) { + $keys[] = (string)$k; + } + + return $keys; +} + +function () { + assertType('list', keysAsString([])); + assertType('non-empty-list', keysAsString(['' => ''])); +}; diff --git a/tests/PHPStan/Analyser/data/bug-7248.php b/tests/PHPStan/Analyser/data/bug-7248.php new file mode 100644 index 0000000000..7c04a0d406 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7248.php @@ -0,0 +1,24 @@ +, + * extensions?: array + * } + */ +class HelloWorld +{ + /** + * @phpstan-return A + */ + public function toArray(): array { + return []; + } +} + +function () { + $hw = new HelloWorld; + assert(['extensions' => ['foo' => 'bar']] === $hw->toArray()); +}; diff --git a/tests/PHPStan/Analyser/data/bug-7275.php b/tests/PHPStan/Analyser/data/bug-7275.php new file mode 100644 index 0000000000..4f4224e313 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7275.php @@ -0,0 +1,28 @@ + $collectionWithPotentialNulls + * + * @return mixed[] + */ + public function doSomething(array $collectionWithPotentialNulls): array + { + return !in_array(null, $collectionWithPotentialNulls, true) + ? $this->doSomethingElse($collectionWithPotentialNulls) + : []; + } + + /** + * @param array $collectionWithoutNulls + * + * @return mixed[] + */ + public function doSomethingElse(array $collectionWithoutNulls): array + { + return []; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7320.php b/tests/PHPStan/Analyser/data/bug-7320.php new file mode 100644 index 0000000000..d365481ae0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7320.php @@ -0,0 +1,14 @@ +getModel()); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7351.php b/tests/PHPStan/Analyser/data/bug-7351.php new file mode 100644 index 0000000000..59d5ba6422 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7351.php @@ -0,0 +1,20 @@ + + */ +trait AttributeTrait +{ + /** + * @template K of key-of + * @param K $key + * @return T[K]|null + */ + public function getAttribute(string $key) + { + return $this->getAttributes()[$key] ?? null; + } +} + +/** + * @phpstan-type Attrs array{foo?: string} + */ +class Foo { + /** @use AttributeTrait */ + use AttributeTrait; + + /** @return Attrs */ + public function getAttributes(): array + { + return []; + } +} + +/** + * @phpstan-type Attrs array{foo?: string, bar?: string} + */ +class Bar { + /** @use AttributeTrait */ + use AttributeTrait; + + /** @return Attrs */ + public function getAttributes(): array + { + return []; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7391b.php b/tests/PHPStan/Analyser/data/bug-7391b.php new file mode 100644 index 0000000000..9970ec7d47 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7391b.php @@ -0,0 +1,30 @@ + $tgs + * + * @return array + * + * @throws \Exception + */ + public function computeForFrontByPosition($tgs) + { + /** @phpstan-var array $res */ + $res = []; + + foreach ($tgs as $tgItem) { + $position = $tgItem->getPosition(); + + if (!isset($res[$position])) { + $res[$position] = $tgItem; + } else { + $tgItemToKeep = $this->compare($tgItem, $res[$position]); + $res[$position] = $tgItemToKeep; + } + } + ksort($res); + + return $res; + } + + /** + * @phpstan-template T of TgEntityInterface + * @phpstan-param T $nextTg + * @phpstan-param T $currentTg + * @phpstan-return T + */ + abstract protected function compare(TgEntityInterface $nextTg, TgEntityInterface $currentTg): TgEntityInterface; +} diff --git a/tests/PHPStan/Analyser/data/bug-7554.php b/tests/PHPStan/Analyser/data/bug-7554.php new file mode 100644 index 0000000000..d81ffe37ff --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7554.php @@ -0,0 +1,32 @@ + $val): + switch ($tag): + case 'A0': + $data['A0'] = ''; + break; + case 'A1': + $data['A1'] = ''; + break; + case 'A2': + $data['A2'] = ''; + break; + case 'A3': + $data['A3'] = ''; + break; + case 'A4': + $data['A4'] = ''; + break; + case 'A5': + $data['A5'] = ''; + break; + case 'A6': + $data['A6'] = ''; + break; + case 'A7': + $data['A7'] = ''; + break; + case 'A8': + $data['A8'] = ''; + break; + case 'A9': + $data['A9'] = ''; + break; + case 'A10': + $data['A10'] = ''; + break; + case 'A11': + $data['A11'] = ''; + break; + case 'A12': + $data['A12'] = ''; + break; + case 'A13': + $data['A13'] = ''; + break; + case 'A14': + $data['A14'] = ''; + break; + case 'A15': + $data['A15'] = ''; + break; + case 'A16': + $data['A16'] = ''; + break; + case 'A17': + $data['A17'] = ''; + break; + case 'A18': + $data['A18'] = ''; + break; + case 'A19': + $data['A19'] = ''; + break; + case 'A20': + $data['A20'] = ''; + break; + case 'A21': + $data['A21'] = ''; + break; + case 'A22': + $data['A22'] = ''; + break; + case 'A23': + $data['A23'] = ''; + break; + case 'A24': + $data['A24'] = ''; + break; + case 'A25': + $data['A25'] = ''; + break; + case 'A26': + $data['A26'] = ''; + break; + case 'A27': + $data['A27'] = ''; + break; + case 'A28': + $data['A28'] = ''; + break; + case 'A29': + $data['A29'] = ''; + break; + case 'A30': + $data['A30'] = ''; + break; + case 'A31': + $data['A31'] = ''; + break; + case 'A32': + $data['A32'] = ''; + break; + endswitch; + endforeach; + + echo 'test'; +} diff --git a/tests/PHPStan/Analyser/data/bug-7637.php b/tests/PHPStan/Analyser/data/bug-7637.php new file mode 100644 index 0000000000..16e12dcfe4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7637.php @@ -0,0 +1,71 @@ + : + * ($key is 'editor' ? string|null : + * ($key is 'editor_basepath' ? string|null : + * ($key is 'timer' ? rex_timer : + * ($key is 'timezone' ? string : + * ($key is 'table_prefix' ? non-empty-string : + * ($key is 'temp_prefix' ? non-empty-string : + * ($key is 'version' ? string : + * ($key is 'server' ? string : + * ($key is 'servername' ? string : + * ($key is 'error_email' ? string : + * ($key is 'lang' ? non-empty-string : + * ($key is 'instname' ? non-empty-string : + * ($key is 'theme' ? non-empty-string : + * ($key is 'start_page' ? non-empty-string : + * ($key is 'socket_proxy' ? non-empty-string|null : + * ($key is 'password_policy' ? array : + * ($key is 'backend_login_policy' ? array : + * ($key is 'db' ? array : + * ($key is 'setup' ? bool|array : + * ($key is 'system_addons' ? non-empty-string[] : + * ($key is 'setup_addons' ? non-empty-string[] : + * mixed|null + * ))))))))))))))))))))))))) + * ) + */ + public static function getProperty($key, $default = null) + { + /** @psalm-suppress TypeDoesNotContainType **/ + if (!is_string($key)) { + throw new InvalidArgumentException('Expecting $key to be string, but ' . gettype($key) . ' given!'); + } + /** @psalm-suppress MixedReturnStatement **/ + if (isset(self::$properties[$key])) { + return self::$properties[$key]; + } + /** @psalm-suppress MixedReturnStatement **/ + return $default; + } +} + +function () { + assertType('array>', HelloWorld::getProperty('db')); +}; diff --git a/tests/PHPStan/Analyser/data/bug-7737.php b/tests/PHPStan/Analyser/data/bug-7737.php new file mode 100644 index 0000000000..f50a9b287e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7737.php @@ -0,0 +1,27 @@ + [1, 2, 3], + ]; + foreach ($context['values'] as $context["_key"] => $context["value"]) { + echo sprintf("Key: %s, Value: %s\n", $context["_key"], $context["value"]); + } + + unset($context["_key"]); +}; + +function () { + $context = [ + 'values' => [1, 2, 3], + ]; + foreach ($context['values'] as $context["_key"] => $value) { + echo sprintf("Key: %s, Value: %s\n", $context["_key"], $value); + } + + unset($context["_key"]); +}; diff --git a/tests/PHPStan/Analyser/data/bug-7762.php b/tests/PHPStan/Analyser/data/bug-7762.php new file mode 100644 index 0000000000..8dd2d25be1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7762.php @@ -0,0 +1,6 @@ + 0, + 'ccccc_yyyy' => 0, + 'oooo_rrrrr' => 0, + 'oooo_yyyy' => 0, + 'ssss' => 0, + 'ssss_next' => 0, + ]; + + $jjjjjjxxxx = [ + 'ccccc' => 0, + 'ccccc_eur' => 0, + 'oooo' => 0, + 'oooo_eur' => 0, + ]; + + $resultsTemplate = [ + 'ccccc' => 0, + 'oooo' => 0, + ]; + + $vvvvTemplate = [ + 0 => $jjjjjjxxxx, + self::F => $jjjjjjxxxx, + self::I => $jjjjjjxxxx, + self::H => $jjjjjjxxxx, + self::G => $jjjjjjxxxx, + ]; + + $kkkkkkkTemplate = [ + 0 => $ggggxxxx, + self::C => $ggggxxxx, + self::D => $ggggxxxx, + self::E => $ggggxxxx, + ]; + + $results = [ + 'ddddd' => [ + 'bbbbb' => [0 => $kkkkkkkTemplate], + 'eeeee' => [0 => $kkkkkkkTemplate], + 'zzzz' => [0 => $kkkkkkkTemplate], + ], + 'qqqqq' => [ + 'bbbbb' => $kkkkkkkTemplate, + 'eeeee' => $kkkkkkkTemplate, + 'zzzz' => $kkkkkkkTemplate, + ], + 'jjjjjj' => [ + 'bbbbb' => [ + 0 => $vvvvTemplate, + self::A => $vvvvTemplate, + self::B => $vvvvTemplate, + ], + 'eeeee' => $jjjjjjxxxx, + 'zzzz' => $jjjjjjxxxx, + ], + 'aaaaaaa' => [ + 'bbbbb' => $resultsTemplate, + 'eeeee' => $resultsTemplate, + 'wwww' => $resultsTemplate, + 'nnnnn' => $resultsTemplate, + ], + 'iiiii' => $resultsTemplate, + ]; + + /** @var mixed[] $llllllgggg */ + $llllllgggg = []; + + foreach ($llllllgggg as $llllllmmmmmm) { + if ((bool)$llllllmmmmmm['a']) { + $results['aaaaaaa']['wwww']['oooo'] += (float)$llllllmmmmmm['b']; + + continue; + } + + if ((bool)$llllllmmmmmm['c']) { + $results['aaaaaaa']['nnnnn']['oooo'] += (float)$llllllmmmmmm['d']; + + continue; + } + + $tttttId = $llllllmmmmmm['e']; + + $tttttuuuuu = (float)$llllllmmmmmm['b']; + $tttttuuuuuNet = (float)$llllllmmmmmm['f']; + $ssss = (float)$llllllmmmmmm['g']; + $ssssNext = (float)$llllllmmmmmm['h']; + $kkkkkkkId = (int)$llllllmmmmmm['i']; + $isbbbbb = (bool)$llllllmmmmmm['j']; + $key = $isbbbbb ? 'bbbbb' : 'eeeee'; + + if ((bool)$llllllmmmmmm['k']) { + $results['ddddd']['zzzz'][0][0]['oooo_rrrrr'] += $tttttuuuuu; + $results['ddddd']['zzzz'][0][0]['oooo_yyyy'] += $tttttuuuuuNet; + $results['ddddd']['zzzz'][0][0]['ssss'] += $ssss; + $results['ddddd']['zzzz'][0][0]['ssss_next'] += $ssssNext; + + $results['ddddd']['zzzz'][0][$tttttId]['oooo_rrrrr'] += $tttttuuuuu; + $results['ddddd']['zzzz'][0][$tttttId]['oooo_yyyy'] += $tttttuuuuuNet; + $results['ddddd']['zzzz'][0][$tttttId]['ssss'] += $ssss; + $results['ddddd']['zzzz'][0][$tttttId]['ssss_next'] += $ssssNext; + + $results['ddddd'][$key][0][0]['oooo_rrrrr'] += $tttttuuuuu; + $results['ddddd'][$key][0][0]['oooo_yyyy'] += $tttttuuuuuNet; + $results['ddddd'][$key][0][0]['ssss'] += $ssss; + $results['ddddd'][$key][0][0]['ssss_next'] += $ssssNext; + + $results['ddddd'][$key][0][$tttttId]['oooo_rrrrr'] += $tttttuuuuu; + $results['ddddd'][$key][0][$tttttId]['oooo_yyyy'] += $tttttuuuuuNet; + $results['ddddd'][$key][0][$tttttId]['ssss'] += $ssss; + $results['ddddd'][$key][0][$tttttId]['ssss_next'] += $ssssNext; + + $results['ddddd'][$key][$kkkkkkkId][0]['oooo_rrrrr'] += $tttttuuuuu; + $results['ddddd'][$key][$kkkkkkkId][0]['oooo_yyyy'] += $tttttuuuuuNet; + $results['ddddd'][$key][$kkkkkkkId][0]['ssss'] += $ssss; + $results['ddddd'][$key][$kkkkkkkId][0]['ssss_next'] += $ssssNext; + + $results['ddddd'][$key][$kkkkkkkId][$tttttId]['oooo_rrrrr'] = $tttttuuuuu; + $results['ddddd'][$key][$kkkkkkkId][$tttttId]['oooo_yyyy'] = $tttttuuuuuNet; + $results['ddddd'][$key][$kkkkkkkId][$tttttId]['ssss'] = $ssss; + $results['ddddd'][$key][$kkkkkkkId][$tttttId]['ssss_next'] = $ssssNext; + } elseif ((bool)$llllllmmmmmm['l']) { + if (!$isbbbbb) { + continue; + } + + $results['qqqqq']['zzzz'][0]['oooo_yyyy'] += $tttttuuuuu; + $results['qqqqq']['zzzz'][$tttttId]['oooo_yyyy'] += $tttttuuuuu; + $results['qqqqq']['bbbbb'][0]['oooo_yyyy'] += $tttttuuuuu; + $results['qqqqq']['bbbbb'][$tttttId]['oooo_yyyy'] = $tttttuuuuu; + } else { + throw new \LogicException(''); + } + } + + /** @var mixed[] $aaa */ + $aaa = []; + + foreach ($aaa as $row) { + $tttttId = $row['tttttId']; + $kkkkkkkId = $row['kkkkkkkId']; + $key = $row['key']; + + $tttttuuuuu = (float)$row['tttttuuuuuggggEurorrrrr']; + $tttttuuuuuNet = (float)$row['tttttuuuuuggggEuroNet']; + + if ($row['isddddd']) { + $results['ddddd']['zzzz'][0][0]['ccccc_rrrrr'] += $tttttuuuuu; + $results['ddddd']['zzzz'][0][0]['ccccc_yyyy'] += $tttttuuuuuNet; + $results['ddddd']['zzzz'][0][$tttttId]['ccccc_rrrrr'] += $tttttuuuuu; + $results['ddddd']['zzzz'][0][$tttttId]['ccccc_yyyy'] += $tttttuuuuuNet; + $results['ddddd'][$key][0][0]['ccccc_rrrrr'] += $tttttuuuuu; + $results['ddddd'][$key][0][0]['ccccc_yyyy'] += $tttttuuuuuNet; + $results['ddddd'][$key][0][$tttttId]['ccccc_rrrrr'] += $tttttuuuuu; + $results['ddddd'][$key][0][$tttttId]['ccccc_yyyy'] += $tttttuuuuuNet; + $results['ddddd'][$key][$kkkkkkkId][0]['ccccc_rrrrr'] += $tttttuuuuu; + $results['ddddd'][$key][$kkkkkkkId][0]['ccccc_yyyy'] += $tttttuuuuuNet; + $results['ddddd'][$key][$kkkkkkkId][$tttttId]['ccccc_rrrrr'] = $tttttuuuuu; + $results['ddddd'][$key][$kkkkkkkId][$tttttId]['ccccc_yyyy'] = $tttttuuuuuNet; + } else { + $results['qqqqq']['zzzz'][0]['ccccc_yyyy'] += $tttttuuuuuNet; + $results['qqqqq']['zzzz'][$tttttId]['ccccc_yyyy'] += $tttttuuuuuNet; + $results['qqqqq']['bbbbb'][0]['ccccc_yyyy'] += $tttttuuuuuNet; + $results['qqqqq']['bbbbb'][$tttttId]['ccccc_yyyy'] += $tttttuuuuuNet; + } + } + + $results['jjjjjj']['zzzz']['ccccc_eur'] = $results['jjjjjj']['bbbbb'][0][0]['ccccc_eur'] + + $results['jjjjjj']['eeeee']['ccccc_eur']; + $results['jjjjjj']['zzzz']['oooo_eur'] = $results['jjjjjj']['bbbbb'][0][0]['oooo_eur'] + + $results['jjjjjj']['eeeee']['oooo_eur']; + + $bbbbbggggccccc = $results['ddddd']['bbbbb'][0][0]['ccccc_yyyy'] + + $results['qqqqq']['bbbbb'][0]['ccccc_yyyy']; + $bbbbbjjjjjjccccc = $results['jjjjjj']['bbbbb'][0][0]['ccccc_eur']; + + $bbbbbggggoooo = $results['ddddd']['bbbbb'][0][0]['oooo_yyyy'] + + $results['ddddd']['bbbbb'][0][0]['ssss_next'] + + $results['qqqqq']['bbbbb'][0]['oooo_yyyy']; + $bbbbbjjjjjjoooo = $results['jjjjjj']['bbbbb'][0][0]['oooo_eur']; + + $ffffffggggccccc = $results['ddddd']['eeeee'][0][0]['ccccc_yyyy'] + + $results['qqqqq']['eeeee'][0]['ccccc_yyyy']; + $ffffffjjjjjjccccc = $results['jjjjjj']['eeeee']['ccccc_eur']; + + $ffffffggggoooo = $results['ddddd']['eeeee'][0][0]['oooo_yyyy'] + + $results['ddddd']['eeeee'][0][0]['ssss_next'] + + $results['qqqqq']['eeeee'][0]['oooo_yyyy']; + $ffffffjjjjjjoooo = $results['jjjjjj']['eeeee']['oooo_eur']; + + $results['aaaaaaa']['bbbbbbbbbbbb']['ccccc'] = $bbbbbjjjjjjccccc > 0 ? $bbbbbggggccccc - $bbbbbjjjjjjccccc : 0; + $results['aaaaaaa']['bbbbb']['oooo'] = $bbbbbjjjjjjoooo > 0 ? $bbbbbggggoooo - $bbbbbjjjjjjoooo : 0; + $results['aaaaaaa']['eeeee']['ccccc'] = $ffffffjjjjjjccccc > 0 ? $ffffffggggccccc - $ffffffjjjjjjccccc : 0; + $results['aaaaaaa']['eeeee']['oooo'] = $ffffffjjjjjjoooo > 0 ? $ffffffggggoooo - $ffffffjjjjjjoooo : 0; + + $results['aaaaaaa']['zzzz']['ccccc'] = + $results['aaaaaaa']['bbbbb']['ccccc'] + + $results['aaaaaaa']['eeeee']['ccccc'] + + $results['aaaaaaa']['wwww']['oooo'] + + $results['aaaaaaa']['nnnnn']['oooo']; + + $results['aaaaaaa']['zzzz']['oooo'] = + $results['aaaaaaa']['bbbbb']['oooo'] + + $results['aaaaaaa']['eeeee']['oooo'] + + $results['aaaaaaa']['wwww']['oooo'] + + $results['aaaaaaa']['nnnnn']['oooo']; + + $results['iiiii']['ccccc'] = $bbbbbjjjjjjccccc > 0 ? ($bbbbbggggccccc / $bbbbbjjjjjjccccc * 100) - 100 : 0; + $results['iiiii']['oooo'] = $bbbbbjjjjjjoooo > 0 ? ($bbbbbggggoooo / $bbbbbjjjjjjoooo * 100) - 100 : 0; + + return $results; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7918.php b/tests/PHPStan/Analyser/data/bug-7918.php new file mode 100644 index 0000000000..2c021d53db --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7918.php @@ -0,0 +1,135 @@ + */ + private function someFunc(): array + { + return []; + } + + private function rand(): bool + { + return random_int(0, 1) > 0; + } + + /** + * @phpstan-impure + */ + private function randImpure(): bool + { + return random_int(0, 1) > 0; + } + + /** @return list> */ + public function run(): array + { + $arr3 = []; + foreach ($this->someFunc() as $id => $arr2) { + // Solution 1 - Specify $result type + // /** @var array $result */ + $result = [ + 'val1' => false, + 'val2' => false, + 'val3' => false, + 'val4' => false, + 'val5' => false, + 'val6' => false, + 'val7' => false, + ]; + + if ($this->rand()) { + $result['val1'] = true; + } + if ($this->rand()) { + $result['val2'] = true; + } + + if ($this->rand()) { + $result['val3'] = true; + } + + if ($this->rand()) { + $result['val4'] = true; + } + + if ($this->rand()) { + $result['val5'] = true; + } + + if ($this->rand()) { + $result['val6'] = true; + } + + // Solution 2 - reduce cyclomatic complexity by replacing above statements with the following + // $result['val1'] = $this->rand(); + // $result['val2'] = $this->rand(); + // $result['val3'] = $this->rand(); + // $result['val4'] = $this->rand(); + // $result['val5'] = $this->rand(); + // $result['val6'] = $this->rand(); + + + $arr3[] = $result; + assertType('non-empty-list', $arr3); + } + + assertType('list', $arr3); + + return $arr3; + } + + /** @return list> */ + public function runImpure(): array + { + $arr3 = []; + foreach ($this->someFunc() as $id => $arr2) { + // Solution 1 - Specify $result type + // /** @var array $result */ + $result = [ + 'val1' => false, + 'val2' => false, + 'val3' => false, + 'val4' => false, + 'val5' => false, + 'val6' => false, + 'val7' => false, + ]; + + if ($this->randImpure()) { + $result['val1'] = true; + } + if ($this->randImpure()) { + $result['val2'] = true; + } + + if ($this->randImpure()) { + $result['val3'] = true; + } + + if ($this->randImpure()) { + $result['val4'] = true; + } + + if ($this->randImpure()) { + $result['val5'] = true; + } + + if ($this->randImpure()) { + $result['val6'] = true; + } + + $arr3[] = $result; + assertType('non-empty-list', $arr3); + } + + assertType('list', $arr3); + + return $arr3; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7963-two.php b/tests/PHPStan/Analyser/data/bug-7963-two.php new file mode 100644 index 0000000000..833df28131 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7963-two.php @@ -0,0 +1,1693 @@ + + */ + const Data = array ( + 'accessibility' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'alert' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'apps' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'archive' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'arrow-both' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'arrow-down' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'arrow-left' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'arrow-right' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'arrow-switch' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'arrow-up' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'beaker' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'bell' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'bell-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'bell-slash' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'blocked' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'bold' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'book' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'bookmark' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'bookmark-slash' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'briefcase' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'broadcast' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'browser' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'bug' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'calendar' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'check' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'check-circle' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'check-circle-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'checklist' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'chevron-down' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'chevron-left' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'chevron-right' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'chevron-up' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'circle' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'circle-slash' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'clock' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'cloud' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'cloud-offline' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'code' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'code-of-conduct' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'code-review' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'code-square' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'codescan' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'codescan-checkmark' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'codespaces' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'columns' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'comment' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'comment-discussion' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'container' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'copilot' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'copilot-error' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'copilot-warning' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'copy' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'cpu' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'credit-card' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'cross-reference' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'dash' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'database' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'dependabot' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'desktop-download' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'device-camera' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'device-camera-video' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'device-desktop' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'device-mobile' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'diamond' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'diff' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'diff-added' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'diff-ignored' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'diff-modified' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'diff-removed' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'diff-renamed' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'dot' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'dot-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'download' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'duplicate' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'ellipsis' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'eye' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'eye-closed' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'facebook' => + array ( + 'width' => 512, + 'height' => 512, + 'path' => '', + ), + 'feed-discussion' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'feed-forked' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'feed-heart' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'feed-merged' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'feed-person' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'feed-repo' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'feed-rocket' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'feed-star' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'feed-tag' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'feed-trophy' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-added' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-badge' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-binary' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-code' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-diff' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-directory' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-directory-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-directory-open-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-moved' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-removed' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-submodule' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-symlink-file' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'file-zip' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'filter' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'flame' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'fold' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'fold-down' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'fold-up' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'gear' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'gift' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'git-branch' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'git-commit' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'git-compare' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'git-merge' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'git-pull-request' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'git-pull-request-closed' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'git-pull-request-draft' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'globe' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'grabber' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'graph' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'hash' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'heading' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'heart' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'heart-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'history' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'home' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'horizontal-rule' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'hourglass' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'hubot' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'id-badge' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'image' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'inbox' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'infinity' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'info' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'issue-closed' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'issue-draft' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'issue-opened' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'issue-reopened' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'italic' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'iterations' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'kebab-horizontal' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'key' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'key-asterisk' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'law' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'light-bulb' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'link' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'link-external' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'linux' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'list-ordered' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'list-unordered' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'location' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'lock' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'log' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'logo-gist' => + array ( + 'width' => 25, + 'height' => 16, + 'path' => '', + ), + 'logo-github' => + array ( + 'width' => 45, + 'height' => 16, + 'path' => '', + ), + 'macos' => + array ( + 'width' => 412, + 'height' => 412, + 'path' => '', + ), + 'mail' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'mark-github' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'markdown' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'megaphone' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'mention' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'meter' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'milestone' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'mirror' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'moon' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'mortar-board' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'multi-select' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'mute' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'no-entry' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'north-star' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'note' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'number' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'organization' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'package' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'package-dependencies' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'package-dependents' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'paintbrush' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'paper-airplane' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'paste' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'pencil' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'people' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'person' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'person-add' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'person-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'pin' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'play' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'plug' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'plus' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'plus-circle' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'project' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'pulse' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'question' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'quote' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'reply' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'repo' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'repo-clone' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'repo-deleted' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'repo-forked' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'repo-locked' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'repo-pull' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'repo-push' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'repo-template' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'report' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'rocket' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'rows' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'rss' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'ruby' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'screen-full' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'screen-normal' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'search' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'server' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'share' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'share-android' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'shield' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'shield-check' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'shield-lock' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'shield-x' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'sidebar-collapse' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'sidebar-expand' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'sign-in' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'sign-out' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'single-select' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'skip' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'sliders' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'smiley' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'sort-asc' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'sort-desc' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'square' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'square-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'squirrel' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'stack' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'star' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'star-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'steam' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'steamdb' => + array ( + 'width' => 128, + 'height' => 128, + 'path' => '', + ), + 'steamdeck' => + array ( + 'width' => 20, + 'height' => 20, + 'path' => '', + ), + 'steamdeck_playable' => + array ( + 'width' => 20, + 'height' => 20, + 'path' => '', + ), + 'steamdeck_unsupported' => + array ( + 'width' => 20, + 'height' => 20, + 'path' => '', + ), + 'steamdeck_verified' => + array ( + 'width' => 20, + 'height' => 20, + 'path' => '', + ), + 'steamworks' => + array ( + 'width' => 45, + 'height' => 19, + 'path' => '', + ), + 'stop' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'stopwatch' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'strikethrough' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'sun' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'sync' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'tab-external' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'table' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'tag' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'tasklist' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'telescope' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'telescope-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'terminal' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'three-bars' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'thumbsdown' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'thumbsup' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'tools' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'trash' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'triangle-down' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'triangle-left' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'triangle-right' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'triangle-up' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'trophy' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'twitch' => + array ( + 'width' => 512, + 'height' => 512, + 'path' => '', + ), + 'twitter' => + array ( + 'width' => 512, + 'height' => 512, + 'path' => '', + ), + 'typography' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'unfold' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'unlock' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'unmute' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'unverified' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'upload' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'verified' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'versions' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'video' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'webhook' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'windows' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'workflow' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'x' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'x-circle' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'x-circle-fill' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + 'youtube' => + array ( + 'width' => 576, + 'height' => 576, + 'path' => '', + ), + 'zap' => + array ( + 'width' => 16, + 'height' => 16, + 'path' => '', + ), + ); +} diff --git a/tests/PHPStan/Analyser/data/bug-7963.php b/tests/PHPStan/Analyser/data/bug-7963.php new file mode 100644 index 0000000000..ac7d433943 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7963.php @@ -0,0 +1,547 @@ +}> + */ + public function getRenderViewElementTests(): array + { + $elements = [ + ['Data Example', FieldDescriptionInterface::TYPE_STRING, 'Example', ['safe' => false]], + ['Data Example', FieldDescriptionInterface::TYPE_STRING, 'Example', ['safe' => false]], + ['Data Example', FieldDescriptionInterface::TYPE_TEXTAREA, 'Example', ['safe' => false]], + [ + 'Data ', + FieldDescriptionInterface::TYPE_DATETIME, + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), [], + ], + [ + 'Data ', + FieldDescriptionInterface::TYPE_DATETIME, + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + ['format' => 'd.m.Y H:i:s'], + ], + [ + 'Data ', + FieldDescriptionInterface::TYPE_DATETIME, + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('UTC')), + ['timezone' => 'Asia/Hong_Kong'], + ], + [ + 'Data ', + FieldDescriptionInterface::TYPE_DATE, + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + [], + ], + [ + 'Data ', + FieldDescriptionInterface::TYPE_DATE, + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + ['format' => 'd.m.Y'], + ], + [ + 'Data ', + FieldDescriptionInterface::TYPE_TIME, + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('Europe/London')), + [], + ], + [ + 'Data ', + FieldDescriptionInterface::TYPE_TIME, + new \DateTime('2013-12-24 10:11:12', new \DateTimeZone('UTC')), + ['timezone' => 'Asia/Hong_Kong'], + ], + ['Data 10.746135', FieldDescriptionInterface::TYPE_FLOAT, 10.746135, ['safe' => false]], + ['Data 5678', FieldDescriptionInterface::TYPE_INTEGER, 5678, ['safe' => false]], + ['Data 1074.6135 %', FieldDescriptionInterface::TYPE_PERCENT, 10.746135, []], + ['Data 0 %', FieldDescriptionInterface::TYPE_PERCENT, 0, []], + ['Data EUR 10.746135', FieldDescriptionInterface::TYPE_CURRENCY, 10.746135, ['currency' => 'EUR']], + ['Data GBP 51.23456', FieldDescriptionInterface::TYPE_CURRENCY, 51.23456, ['currency' => 'GBP']], + ['Data EUR 0', FieldDescriptionInterface::TYPE_CURRENCY, 0, ['currency' => 'EUR']], + [ + 'Data
  • 1 => First
  • 2 => Second
', + FieldDescriptionInterface::TYPE_ARRAY, + [1 => 'First', 2 => 'Second'], + ['safe' => false], + ], + [ + 'Data [1 => First, 2 => Second] ', + FieldDescriptionInterface::TYPE_ARRAY, + [1 => 'First', 2 => 'Second'], + ['safe' => false, 'inline' => true], + ], + [ + 'Data yes', + FieldDescriptionInterface::TYPE_BOOLEAN, + true, + [], + ], + [ + 'Data yes', + FieldDescriptionInterface::TYPE_BOOLEAN, + true, + ['inverse' => true], + ], + ['Data no', FieldDescriptionInterface::TYPE_BOOLEAN, false, []], + [ + 'Data no', + FieldDescriptionInterface::TYPE_BOOLEAN, + false, + ['inverse' => true], + ], + [ + 'Data Delete', + FieldDescriptionInterface::TYPE_TRANS, + 'action_delete', + ['safe' => false, 'catalogue' => 'SonataAdminBundle'], + ], + [ + 'Data Delete', + FieldDescriptionInterface::TYPE_TRANS, + 'delete', + ['safe' => false, 'catalogue' => 'SonataAdminBundle', 'format' => 'action_%s'], + ], + ['Data Status1', FieldDescriptionInterface::TYPE_CHOICE, 'Status1', ['safe' => false]], + [ + 'Data Alias1', + FieldDescriptionInterface::TYPE_CHOICE, + 'Status1', + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ]], + ], + [ + 'Data NoValidKeyInChoices', + FieldDescriptionInterface::TYPE_CHOICE, + 'NoValidKeyInChoices', + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ]], + ], + [ + 'Data Delete', + FieldDescriptionInterface::TYPE_CHOICE, + 'Foo', + ['safe' => false, 'catalogue' => 'SonataAdminBundle', 'choices' => [ + 'Foo' => 'action_delete', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ]], + ], + [ + 'Data NoValidKeyInChoices', + FieldDescriptionInterface::TYPE_CHOICE, + ['NoValidKeyInChoices'], + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data NoValidKeyInChoices, Alias2', + FieldDescriptionInterface::TYPE_CHOICE, + ['NoValidKeyInChoices', 'Status2'], + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data Alias1, Alias3', + FieldDescriptionInterface::TYPE_CHOICE, + ['Status1', 'Status3'], + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data Alias1 | Alias3', + FieldDescriptionInterface::TYPE_CHOICE, + ['Status1', 'Status3'], ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true, 'delimiter' => ' | '], + ], + [ + 'Data Delete, Alias3', + FieldDescriptionInterface::TYPE_CHOICE, + ['Foo', 'Status3'], + ['safe' => false, 'catalogue' => 'SonataAdminBundle', 'choices' => [ + 'Foo' => 'action_delete', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data Alias1, Alias3', + FieldDescriptionInterface::TYPE_CHOICE, + ['Status1', 'Status3'], + ['safe' => true, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data <b>Alias1</b>, <b>Alias3</b>', + FieldDescriptionInterface::TYPE_CHOICE, + ['Status1', 'Status3'], + ['safe' => false, 'choices' => [ + 'Status1' => 'Alias1', + 'Status2' => 'Alias2', + 'Status3' => 'Alias3', + ], 'multiple' => true], + ], + [ + 'Data http://example.com', + FieldDescriptionInterface::TYPE_URL, + 'http://example.com/', + ['safe' => false], + ], + [ + 'Data http://example.com', + FieldDescriptionInterface::TYPE_URL, + 'http://example.com/', + ['safe' => false, 'attributes' => ['target' => '_blank']], + ], + [ + 'Data http://example.com', + FieldDescriptionInterface::TYPE_URL, + 'http://example.com/', + ['safe' => false, 'attributes' => ['target' => '_blank', 'class' => 'fooLink']], + ], + [ + 'Data https://example.com', + FieldDescriptionInterface::TYPE_URL, + 'https://example.com/', + ['safe' => false], + ], + [ + 'Data example.com', + FieldDescriptionInterface::TYPE_URL, + 'http://example.com/', + ['safe' => false, 'hide_protocol' => true], + ], + [ + 'Data example.com', + FieldDescriptionInterface::TYPE_URL, + 'https://example.com/', + ['safe' => false, 'hide_protocol' => true], + ], + [ + 'Data http://example.com', + FieldDescriptionInterface::TYPE_URL, + 'http://example.com/', + ['safe' => false, 'hide_protocol' => false], + ], + [ + 'Data https://example.com', + FieldDescriptionInterface::TYPE_URL, + 'https://example.com/', + ['safe' => false, + 'hide_protocol' => false, ], + ], + [ + 'Data Foo', + FieldDescriptionInterface::TYPE_URL, + 'Foo', + ['safe' => false, 'url' => 'http://example.com/'], + ], + [ + 'Data <b>Foo</b>', + FieldDescriptionInterface::TYPE_URL, + 'Foo', + ['safe' => false, 'url' => 'http://example.com/'], + ], + [ + 'Data Foo', + FieldDescriptionInterface::TYPE_URL, + 'Foo', + ['safe' => true, 'url' => 'http://example.com/'], + ], + [ + 'Data Foo', + FieldDescriptionInterface::TYPE_URL, + 'Foo', + ['safe' => false, 'route' => ['name' => 'sonata_admin_foo']], + ], + [ + 'Data Foo', + FieldDescriptionInterface::TYPE_URL, + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo', + 'absolute' => true, + ]], + ], + [ + 'Data foo/bar?a=b&c=123456789', + FieldDescriptionInterface::TYPE_URL, + 'http://foo/bar?a=b&c=123456789', + [ + 'safe' => false, + 'route' => ['name' => 'sonata_admin_foo'], + 'hide_protocol' => true, + ], + ], + [ + 'Data foo/bar?a=b&c=123456789', + FieldDescriptionInterface::TYPE_URL, + 'http://foo/bar?a=b&c=123456789', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo', + 'absolute' => true, + ], 'hide_protocol' => true], + ], + [ + 'Data Foo', + FieldDescriptionInterface::TYPE_URL, + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo_param', + 'parameters' => ['param1' => 'abcd', 'param2' => 'efgh', 'param3' => 'ijkl'], + ]], + ], + [ + 'Data Foo', + FieldDescriptionInterface::TYPE_URL, + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo_param', + 'absolute' => true, + 'parameters' => [ + 'param1' => 'abcd', + 'param2' => 'efgh', + 'param3' => 'ijkl', + ], + ]], + ], + [ + 'Data Foo', + FieldDescriptionInterface::TYPE_URL, + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo_object', + 'parameters' => [ + 'param1' => 'abcd', + 'param2' => 'efgh', + 'param3' => 'ijkl', + ], + 'identifier_parameter_name' => 'barId', + ]], + ], + [ + 'Data Foo', + FieldDescriptionInterface::TYPE_URL, + 'Foo', + ['safe' => false, 'route' => [ + 'name' => 'sonata_admin_foo_object', + 'absolute' => true, + 'parameters' => [ + 'param1' => 'abcd', + 'param2' => 'efgh', + 'param3' => 'ijkl', + ], + 'identifier_parameter_name' => 'barId', + ]], + ], + [ + 'Data  ', + FieldDescriptionInterface::TYPE_EMAIL, + null, + [], + ], + [ + 'Data admin@admin.com', + FieldDescriptionInterface::TYPE_EMAIL, + 'admin@admin.com', + [], + ], + [ + 'Data admin@admin.com', + FieldDescriptionInterface::TYPE_EMAIL, + 'admin@admin.com', + ['subject' => 'Main Theme', 'body' => 'Message Body'], + ], + [ + 'Data admin@admin.com', + FieldDescriptionInterface::TYPE_EMAIL, + 'admin@admin.com', + ['subject' => 'Main Theme'], + ], + [ + 'Data admin@admin.com', + FieldDescriptionInterface::TYPE_EMAIL, + 'admin@admin.com', + ['body' => 'Message Body'], + ], + [ + 'Data admin@admin.com', + FieldDescriptionInterface::TYPE_EMAIL, + 'admin@admin.com', + ['as_string' => true, 'subject' => 'Main Theme', 'body' => 'Message Body'], + ], + [ + 'Data admin@admin.com', + FieldDescriptionInterface::TYPE_EMAIL, + 'admin@admin.com', + ['as_string' => true, 'subject' => 'Main Theme'], + ], + [ + 'Data admin@admin.com', + FieldDescriptionInterface::TYPE_EMAIL, + 'admin@admin.com', + ['as_string' => true, 'body' => 'Message Body'], + ], + [ + 'Data admin@admin.com', + FieldDescriptionInterface::TYPE_EMAIL, + 'admin@admin.com', + ['as_string' => false], + ], + [ + 'Data admin@admin.com', + FieldDescriptionInterface::TYPE_EMAIL, + 'admin@admin.com', + ['as_string' => true], + ], + [ + 'Data

Creating a Template for the Field and form

', + FieldDescriptionInterface::TYPE_HTML, + '

Creating a Template for the Field and form

', + [], + ], + [ + 'Data Creating a Template for the Field and form', + FieldDescriptionInterface::TYPE_HTML, + '

Creating a Template for the Field and form

', + ['strip' => true], + ], + [ + 'Data Creating a Template for the...', + FieldDescriptionInterface::TYPE_HTML, + '

Creating a Template for the Field and form

', + ['truncate' => true], + ], + [ + 'Data Creatin...', + FieldDescriptionInterface::TYPE_HTML, + '

Creating a Template for the Field and form

', + ['truncate' => ['length' => 10]], + ], + [ + 'Data Creating a Template for the Field...', + FieldDescriptionInterface::TYPE_HTML, + '

Creating a Template for the Field and form

', + ['truncate' => ['cut' => false]], + ], + [ + 'Data Creating a Template for t etc.', + FieldDescriptionInterface::TYPE_HTML, + '

Creating a Template for the Field and form

', + ['truncate' => ['ellipsis' => ' etc.']], + ], + [ + 'Data Creating a Template[...]', + FieldDescriptionInterface::TYPE_HTML, + '

Creating a Template for the Field and form

', + [ + 'truncate' => [ + 'length' => 20, + 'cut' => false, + 'ellipsis' => '[...]', + ], + ], + ], + [ + <<<'EOT' + Data
+ A very long string +
+ EOT + , + FieldDescriptionInterface::TYPE_STRING, + ' A very long string ', + [ + 'collapse' => true, + 'safe' => false, + ], + ], + [ + <<<'EOT' + Data
+ A very long string +
+ EOT + , + FieldDescriptionInterface::TYPE_STRING, + ' A very long string ', + [ + 'collapse' => [ + 'height' => 10, + 'more' => 'More', + 'less' => 'Less', + ], + 'safe' => false, + ], + ], + ]; + + if (\PHP_VERSION_ID >= 80100) { + $elements[] = [ + 'Data Hearts', + FieldDescriptionInterface::TYPE_ENUM, + '', + [], + ]; + } + + return $elements; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7980.php b/tests/PHPStan/Analyser/data/bug-7980.php new file mode 100644 index 0000000000..1e2f3c0601 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7980.php @@ -0,0 +1,28 @@ +value) ? $valueObj->value : 0; + +test($value, $valueObj?->ttl); diff --git a/tests/PHPStan/Analyser/data/bug-8004.php b/tests/PHPStan/Analyser/data/bug-8004.php new file mode 100644 index 0000000000..938f6f1111 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8004.php @@ -0,0 +1,114 @@ +} $importQuiz + * + * @return list + */ + public function getErrorsOnInvalidQuestions(array $importQuiz, int $key): array + { + $errors = []; + + foreach ($importQuiz['questions'] as $index => $question) { + if (empty($question['question']) && empty($question['answer_1']) && empty($question['answer_2']) && empty($question['answer_3']) && empty($question['answer_4']) && empty($question['right_answer']) && empty($question['right_answer_explanation'])) { + continue; + } + + if (empty($question['question'])) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::EMPTY_QUESTION, 'value' => $index + 1]; + } elseif (255 < mb_strlen($question['question'])) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_QUESTION_TOO_LONG, 'value' => $index + 1]; + } + + if (null === $question['answer_1'] || '' === $question['answer_1'] || null === $question['answer_2'] || '' === $question['answer_2']) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::EMPTY_ANSWER, 'value' => $index + 1]; + } + + if (null !== $question['answer_1'] && '' !== $question['answer_1']) { + if (\is_string($question['answer_1']) && 150 < mb_strlen($question['answer_1'])) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_ANSWER_1_TOO_LONG, 'value' => $index + 1]; + } elseif ($question['answer_1'] instanceof \DateTimeInterface) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_ANSWER_1_TYPE, 'value' => $index + 1]; + } + } + + if (null !== $question['answer_2'] && '' !== $question['answer_2']) { + if (\is_string($question['answer_2']) && 150 < mb_strlen($question['answer_2'])) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_ANSWER_2_TOO_LONG, 'value' => $index + 1]; + } elseif ($question['answer_2'] instanceof \DateTimeInterface) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_ANSWER_2_TYPE, 'value' => $index + 1]; + } + } + + $hasQuestion3 = isset($question['answer_3']) && null !== $question['answer_3'] && '' !== $question['answer_3']; + + if ($hasQuestion3) { + if (\is_string($question['answer_3']) && 150 < mb_strlen($question['answer_3'])) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_ANSWER_3_TOO_LONG, 'value' => $index + 1]; + } elseif ($question['answer_3'] instanceof \DateTimeInterface) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_ANSWER_3_TYPE, 'value' => $index + 1]; + } + } + + $hasQuestion4 = isset($question['answer_4']) && null !== $question['answer_4'] && '' !== $question['answer_4']; + + if ($hasQuestion4) { + if (\is_string($question['answer_4']) && 150 < mb_strlen($question['answer_4'])) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_ANSWER_4_TOO_LONG, 'value' => $index + 1]; + } elseif ($question['answer_4'] instanceof \DateTimeInterface) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_ANSWER_4_TYPE, 'value' => $index + 1]; + } + } + + $answerCount = 2 + ($hasQuestion3 ? 1 : 0) + ($hasQuestion4 ? 1 : 0); + + if (empty($question['right_answer']) || !is_numeric($question['right_answer']) || $question['right_answer'] <= 0 || (int) $question['right_answer'] > $answerCount) { + $errors[] = ['line' => $key, 'type' => QuizImportErrorType::INVALID_RIGHT_ANSWER, 'value' => $index + 1]; + } + } + + assertType("list&oversized-array", $errors); + + return $errors; + } +} + +class QuizImportErrorType +{ + public const EMPTY_ANSWER = 'empty_answer'; + public const EMPTY_BONUS_DURATION = 'empty_bonus_duration'; + public const EMPTY_BONUS_POINTS = 'empty_bonus_points'; + public const EMPTY_COLLECTION = 'empty_collection'; + public const EMPTY_INTRODUCTION = 'empty_introduction'; + public const EMPTY_QUESTION = 'empty_question'; + public const EMPTY_TITLE = 'empty_title'; + public const INVALID_ANSWER_1_TOO_LONG = 'invalid_answer_1_too_long'; + public const INVALID_ANSWER_2_TOO_LONG = 'invalid_answer_2_too_long'; + public const INVALID_ANSWER_3_TOO_LONG = 'invalid_answer_3_too_long'; + public const INVALID_ANSWER_4_TOO_LONG = 'invalid_answer_4_too_long'; + public const INVALID_ANSWER_1_TYPE = 'invalid_answer_1_type'; + public const INVALID_ANSWER_2_TYPE = 'invalid_answer_2_type'; + public const INVALID_ANSWER_3_TYPE = 'invalid_answer_3_type'; + public const INVALID_ANSWER_4_TYPE = 'invalid_answer_4_type'; + public const INVALID_AUTHOR = 'invalid_author'; + public const INVALID_BONUS_DURATION = 'invalid_bonus_duration'; + public const INVALID_BONUS_POINTS = 'invalid_bonus_points'; + public const INVALID_CLOSING_DATE = 'invalid_closing_date'; + public const INVALID_COLLECTION = 'invalid_collection'; + public const INVALID_NEWS_FEED = 'invalid_news_feed'; + public const INVALID_PARTICIPATION_POINTS = 'invalid_participation_points'; + public const INVALID_PERFECT_POINTS = 'invalid_perfect_points'; + public const INVALID_PUBLICATION_DATE = 'invalid_publication_date'; + public const INVALID_QUESTION_TOO_LONG = 'invalid_question_too_long'; + public const INVALID_RESPONSE_TIME = 'invalid_response_time'; + public const INVALID_RIGHT_ANSWER = 'invalid_right_answer'; + public const INVALID_RIGHT_ANSWER_POINTS = 'invalid_right_answer_points'; + public const INVALID_TITLE_TOO_LONG = 'invalid_title_too_long'; + public const INVALID_WRONG_ANSWER_POINTS = 'invalid_wrong_answer_points'; +} diff --git a/tests/PHPStan/Analyser/data/bug-8072.php b/tests/PHPStan/Analyser/data/bug-8072.php new file mode 100644 index 0000000000..2f9b881057 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8072.php @@ -0,0 +1,20 @@ + 'Hi'); + echo say((fn (?string $name = null) => 'Hi')(...)); + + echo say(function (?string $name = null) { + return 'Hi'; + }); + echo say((function (?string $name = null) { + return 'Hi'; + })(...)); +}; diff --git a/tests/PHPStan/Analyser/data/bug-8078.php b/tests/PHPStan/Analyser/data/bug-8078.php new file mode 100644 index 0000000000..79c2b4f7e5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8078.php @@ -0,0 +1,11 @@ += 8.1 + +namespace Bug8078; + +class HelloWorld +{ + public function test(): void + { + $closure = (static fn (): string => 'evaluated Closure')(...); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8146a.php b/tests/PHPStan/Analyser/data/bug-8146a.php new file mode 100644 index 0000000000..3d0e10a65f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8146a.php @@ -0,0 +1,152 @@ +session = $session; + $this->object = $object; + } + + public function sayHello(): void + { + $changeLog = []; + + $firstname = $this->session->get('firstname'); + if ($firstname !== $this->object->getFirstname()) { + $changelog['firstname_old'] = $this->object->getFirstname(); + $changelog['firstname_new'] = $firstname; + } + + $lastname = $this->session->get('lastname'); + if ($lastname !== $this->object->getLastname()) { + $changelog['lastname_old'] = $this->object->getLastname(); + $changelog['lastname_new'] = $lastname; + } + + $street = $this->session->get('street'); + if ($street !== $this->object->getStreet()) { + $changelog['street_old'] = $this->object->getStreet(); + $changelog['street_new'] = $street; + } + + $zip = $this->session->get('zip'); + if ($zip !== $this->object->getZip()) { + $changelog['zip_old'] = $this->object->getZip(); + $changelog['zip_new'] = $zip; + } + + $city = $this->session->get('city'); + if ($city !== $this->object->getCity()) { + $changelog['city_old'] = $this->object->getCity(); + $changelog['city_new'] = $city; + } + + $phonenumber = $this->session->get('phonenumber'); + if ($phonenumber !== $this->object->getPhonenumber()) { + $changelog['phonenumber_old'] = $this->object->getPhonenumber(); + $changelog['phonenumber_new'] = $phonenumber; + } + + $email = $this->session->get('email'); + if ($email !== $this->object->getEmail()) { + $changelog['email_old'] = $this->object->getEmail(); + $changelog['email_new'] = $email; + } + + $deliveryFirstname = $this->session->get('deliveryFirstname'); + if ($deliveryFirstname !== $this->object->getDeliveryFirstname()) { + $changelog['deliveryFirstname_old'] = $this->object->getDeliveryFirstname(); + $changelog['deliveryFirstname_new'] = $deliveryFirstname; + } + + $deliveryLastname = $this->session->get('deliveryLastname'); + if ($deliveryLastname !== $this->object->getDeliveryLastname()) { + $changelog['deliveryLastname_old'] = $this->object->getDeliveryLastname(); + $changelog['deliveryLastname_new'] = $deliveryLastname; + } + $deliveryStreet = $this->session->get('deliveryStreet'); + if ($deliveryStreet !== $this->object->getDeliveryStreet()) { + $changelog['deliveryStreet_old'] = $this->object->getDeliveryStreet(); + $changelog['deliveryStreet_new'] = $deliveryStreet; + } + $deliveryZip = $this->session->get('deliveryZip'); + if ($deliveryZip !== $this->object->getDeliveryZip()) { + $changelog['deliveryZip_old'] = $this->object->getDeliveryZip(); + $changelog['deliveryZip_new'] = $deliveryZip; + } + $deliveryCity = $this->session->get('deliveryCity'); + if ($deliveryCity !== $this->object->getDeliveryCity()) { + $changelog['deliveryCity_old'] = $this->object->getDeliveryCity(); + $changelog['deliveryCity_new'] = $deliveryCity; + } + + } +} + +interface SessionInterface +{ + /** + * @return mixed + */ + public function get(string $key); +} + +interface DataObject +{ + /** + * @return string|null + */ + public function getFirstname(); + /** + * @return string|null + */ + public function getLastname(); + /** + * @return string|null + */ + public function getStreet(); + /** + * @return string|null + */ + public function getZip(); + /** + * @return string|null + */ + public function getCity(); + /** + * @return string|null + */ + public function getPhonenumber(); + /** + * @return string|null + */ + public function getEmail(); + /** + * @return string|null + */ + public function getDeliveryFirstname(); + /** + * @return string|null + */ + public function getDeliveryLastname(); + /** + * @return string|null + */ + public function getDeliveryStreet(); + /** + * @return string|null + */ + public function getDeliveryZip(); + /** + * @return string|null + */ + public function getDeliveryCity(); +} diff --git a/tests/PHPStan/Analyser/data/bug-8146b.php b/tests/PHPStan/Analyser/data/bug-8146b.php new file mode 100644 index 0000000000..6d3ed34290 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8146b.php @@ -0,0 +1,5544 @@ +, coordinates: array{lat: float, lng: float}}>> */ + public function getData(): array + { + return [ + 'Bács-Kiskun' => [ + 'Ágasegyháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8386043, 'lng' => 19.4502899], + ], + 'Akasztó' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6898175, 'lng' => 19.205086], + ], + 'Apostag' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8812652, 'lng' => 18.9648478], + ], + 'Bácsalmás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1250396, 'lng' => 19.3357509], + ], + 'Bácsbokod' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1234737, 'lng' => 19.155708], + ], + 'Bácsborsód' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0989373, 'lng' => 19.1566725], + ], + 'Bácsszentgyörgy' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9746039, 'lng' => 19.0398066], + ], + 'Bácsszőlős' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1352003, 'lng' => 19.4215997], + ], + 'Baja' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1817951, 'lng' => 18.9543051], + ], + 'Ballószög' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8619947, 'lng' => 19.5726144], + ], + 'Balotaszállás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3512041, 'lng' => 19.5403558], + ], + 'Bátmonostor' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1057304, 'lng' => 18.9238311], + ], + 'Bátya' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4891741, 'lng' => 18.9579127], + ], + 'Bócsa' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6113504, 'lng' => 19.4826419], + ], + 'Borota' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2657107, 'lng' => 19.2233598], + ], + 'Bugac' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6883076, 'lng' => 19.6833655], + ], + 'Bugacpusztaháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7022143, 'lng' => 19.6356538], + ], + 'Császártöltés' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4222869, 'lng' => 19.1815532], + ], + 'Csátalja' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0363238, 'lng' => 18.9469006], + ], + 'Csávoly' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1912599, 'lng' => 19.1451178], + ], + 'Csengőd' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.71532, 'lng' => 19.2660933], + ], + 'Csikéria' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.121679, 'lng' => 19.473777], + ], + 'Csólyospálos' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4180837, 'lng' => 19.8402638], + ], + 'Dávod' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9976187, 'lng' => 18.9176479], + ], + 'Drágszél' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4653889, 'lng' => 19.0382659], + ], + 'Dunaegyháza' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8383215, 'lng' => 18.9605216], + ], + 'Dunafalva' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.081562, 'lng' => 18.7782526], + ], + 'Dunapataj' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6422106, 'lng' => 18.9989393], + ], + 'Dunaszentbenedek' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.593856, 'lng' => 18.8935322], + ], + 'Dunatetétlen' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.7578624, 'lng' => 19.0932563], + ], + 'Dunavecse' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.9133047, 'lng' => 18.9731873], + ], + 'Dusnok' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3893659, 'lng' => 18.960842], + ], + 'Érsekcsanád' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2541554, 'lng' => 18.9835293], + ], + 'Érsekhalma' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3472701, 'lng' => 19.1247379], + ], + 'Fajsz' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.4157936, 'lng' => 18.9191954], + ], + 'Felsőlajos' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0647473, 'lng' => 19.4944348], + ], + 'Felsőszentiván' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1966179, 'lng' => 19.1873616], + ], + 'Foktő' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5268759, 'lng' => 18.9196874], + ], + 'Fülöpháza' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8914016, 'lng' => 19.4432493], + ], + 'Fülöpjakab' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.742058, 'lng' => 19.7227232], + ], + 'Fülöpszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8195701, 'lng' => 19.2372115], + ], + 'Gara' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0349999, 'lng' => 19.0393411], + ], + 'Gátér' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.680435, 'lng' => 19.9596412], + ], + 'Géderlak' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6072512, 'lng' => 18.9135762], + ], + 'Hajós' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4001409, 'lng' => 19.1193255], + ], + 'Harkakötöny' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4634053, 'lng' => 19.6069951], + ], + 'Harta' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6960997, 'lng' => 19.0328195], + ], + 'Helvécia' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8360977, 'lng' => 19.620438], + ], + 'Hercegszántó' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9482057, 'lng' => 18.9389127], + ], + 'Homokmégy' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4892762, 'lng' => 19.0730421], + ], + 'Imrehegy' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4867668, 'lng' => 19.3056372], + ], + 'Izsák' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8020009, 'lng' => 19.3546225], + ], + 'Jakabszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7602785, 'lng' => 19.6055301], + ], + 'Jánoshalma' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2974544, 'lng' => 19.3250656], + ], + 'Jászszentlászló' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.5672659, 'lng' => 19.7590541], + ], + 'Kalocsa' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5281229, 'lng' => 18.9840376], + ], + 'Kaskantyú' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6711891, 'lng' => 19.3895391], + ], + 'Katymár' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0344636, 'lng' => 19.2087609], + ], + 'Kecel' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5243135, 'lng' => 19.2451963], + ], + 'Kecskemét' => [ + 'constituencies' => ['Bács-Kiskun 2.', 'Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8963711, 'lng' => 19.6896861], + ], + 'Kelebia' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1958608, 'lng' => 19.6066291], + ], + 'Kéleshalom' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3641795, 'lng' => 19.2831241], + ], + 'Kerekegyháza' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9385747, 'lng' => 19.4770208], + ], + 'Kiskőrös' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6224967, 'lng' => 19.2874568], + ], + 'Kiskunfélegyháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7112802, 'lng' => 19.8515196], + ], + 'Kiskunhalas' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4354409, 'lng' => 19.4834284], + ], + 'Kiskunmajsa' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4904848, 'lng' => 19.7366569], + ], + 'Kisszállás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2791272, 'lng' => 19.4908079], + ], + 'Kömpöc' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4640167, 'lng' => 19.8665681], + ], + 'Kunadacs' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.956503, 'lng' => 19.2880496], + ], + 'Kunbaja' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.0848391, 'lng' => 19.4213713], + ], + 'Kunbaracs' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9891493, 'lng' => 19.3999584], + ], + 'Kunfehértó' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.362671, 'lng' => 19.4141949], + ], + 'Kunpeszér' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0611502, 'lng' => 19.2753764], + ], + 'Kunszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7627801, 'lng' => 19.7532925], + ], + 'Kunszentmiklós' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0244473, 'lng' => 19.1235997], + ], + 'Ladánybene' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0344239, 'lng' => 19.456807], + ], + 'Lajosmizse' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0248225, 'lng' => 19.5559232], + ], + 'Lakitelek' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8710339, 'lng' => 19.9930216], + ], + 'Madaras' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0554833, 'lng' => 19.2633403], + ], + 'Mátételke' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1614675, 'lng' => 19.2802263], + ], + 'Mélykút' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2132295, 'lng' => 19.3814176], + ], + 'Miske' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4434918, 'lng' => 19.0315752], + ], + 'Móricgát' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6233704, 'lng' => 19.6885382], + ], + 'Nagybaracska' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0444015, 'lng' => 18.9048387], + ], + 'Nemesnádudvar' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3348444, 'lng' => 19.0542114], + ], + 'Nyárlőrinc' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8611255, 'lng' => 19.8773125], + ], + 'Ordas' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6364524, 'lng' => 18.9504602], + ], + 'Öregcsertő' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.515272, 'lng' => 19.1090595], + ], + 'Orgovány' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7497582, 'lng' => 19.4746024], + ], + 'Páhi' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.7136232, 'lng' => 19.3856937], + ], + 'Pálmonostora' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6265115, 'lng' => 19.9425525], + ], + 'Petőfiszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6243457, 'lng' => 19.8596537], + ], + 'Pirtó' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5139604, 'lng' => 19.4301958], + ], + 'Rém' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2470804, 'lng' => 19.1416684], + ], + 'Solt' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8021967, 'lng' => 19.0108147], + ], + 'Soltszentimre' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.769786, 'lng' => 19.2840433], + ], + 'Soltvadkert' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5789287, 'lng' => 19.3938029], + ], + 'Sükösd' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2832039, 'lng' => 18.9942907], + ], + 'Szabadszállás' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8763076, 'lng' => 19.2232539], + ], + 'Szakmár' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5543652, 'lng' => 19.0742847], + ], + 'Szalkszentmárton' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9754928, 'lng' => 19.0171018], + ], + 'Szank' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.5557842, 'lng' => 19.6668956], + ], + 'Szentkirály' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.9169398, 'lng' => 19.9175371], + ], + 'Szeremle' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1436504, 'lng' => 18.8810207], + ], + 'Tabdi' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6818019, 'lng' => 19.3042672], + ], + 'Tass' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0184485, 'lng' => 19.0281253], + ], + 'Tataháza' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.173167, 'lng' => 19.3024716], + ], + 'Tázlár' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5509533, 'lng' => 19.5159844], + ], + 'Tiszaalpár' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8140236, 'lng' => 19.9936556], + ], + 'Tiszakécske' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.9358726, 'lng' => 20.0969279], + ], + 'Tiszaug' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8537215, 'lng' => 20.052921], + ], + 'Tompa' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2060507, 'lng' => 19.5389553], + ], + 'Újsolt' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8706098, 'lng' => 19.1186222], + ], + 'Újtelek' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5911716, 'lng' => 19.0564597], + ], + 'Uszód' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5704972, 'lng' => 18.9038275], + ], + 'Városföld' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8174844, 'lng' => 19.7597893], + ], + 'Vaskút' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1080968, 'lng' => 18.9861524], + ], + 'Zsana' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3802847, 'lng' => 19.6600846], + ], + ], + 'Baranya' => [ + 'Abaliget' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1428711, 'lng' => 18.1152298], + ], + 'Adorjás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8509119, 'lng' => 18.0617924], + ], + 'Ág' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2962836, 'lng' => 18.2023275], + ], + 'Almamellék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1603198, 'lng' => 17.8765681], + ], + 'Almáskeresztúr' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1199547, 'lng' => 17.8958453], + ], + 'Alsómocsolád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.313518, 'lng' => 18.2481993], + ], + 'Alsószentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7912208, 'lng' => 18.3065816], + ], + 'Apátvarasd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1856469, 'lng' => 18.47932], + ], + 'Aranyosgadány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.007757, 'lng' => 18.1195466], + ], + 'Áta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9367366, 'lng' => 18.2985608], + ], + 'Babarc' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0042229, 'lng' => 18.5527511], + ], + 'Babarcszőlős' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.898699, 'lng' => 18.1360284], + ], + 'Bakóca' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2074891, 'lng' => 18.0002016], + ], + 'Bakonya' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0850942, 'lng' => 18.082286], + ], + 'Baksa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9554293, 'lng' => 18.0909794], + ], + 'Bánfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.994691, 'lng' => 17.8798792], + ], + 'Bár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0482419, 'lng' => 18.7119502], + ], + 'Baranyahídvég' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8461886, 'lng' => 18.0229597], + ], + 'Baranyajenő' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2734519, 'lng' => 18.0469416], + ], + 'Baranyaszentgyörgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2461345, 'lng' => 18.0119839], + ], + 'Basal' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0734372, 'lng' => 17.7832659], + ], + 'Belvárdgyula' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9750659, 'lng' => 18.4288438], + ], + 'Beremend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7877528, 'lng' => 18.4322322], + ], + 'Berkesd' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0766759, 'lng' => 18.4078442], + ], + 'Besence' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8956421, 'lng' => 17.9654588], + ], + 'Bezedek' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8653948, 'lng' => 18.5854023], + ], + 'Bicsérd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0216488, 'lng' => 18.0779429], + ], + 'Bikal' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3329154, 'lng' => 18.2845332], + ], + 'Birján' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0007461, 'lng' => 18.3739733], + ], + 'Bisse' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9082449, 'lng' => 18.2603363], + ], + 'Boda' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0796449, 'lng' => 18.0477749], + ], + 'Bodolyabér' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.196906, 'lng' => 18.1189705], + ], + 'Bogád' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0858618, 'lng' => 18.3215439], + ], + 'Bogádmindszent' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9069292, 'lng' => 18.0382456], + ], + 'Bogdása' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8756825, 'lng' => 17.7892759], + ], + 'Boldogasszonyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1826055, 'lng' => 17.8379176], + ], + 'Bóly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9654045, 'lng' => 18.5166166], + ], + 'Borjád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9356423, 'lng' => 18.4708549], + ], + 'Bosta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9500492, 'lng' => 18.2104193], + ], + 'Botykapeterd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0499466, 'lng' => 17.8662441], + ], + 'Bükkösd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1100188, 'lng' => 17.9925218], + ], + 'Bürüs' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9653278, 'lng' => 17.7591739], + ], + 'Csányoszró' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8810774, 'lng' => 17.9101381], + ], + 'Csarnóta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8949174, 'lng' => 18.2163121], + ], + 'Csebény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1893582, 'lng' => 17.9275209], + ], + 'Cserdi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0808529, 'lng' => 17.9911191], + ], + 'Cserkút' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0756664, 'lng' => 18.1340119], + ], + 'Csertő' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.093457, 'lng' => 17.8034587], + ], + 'Csonkamindszent' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0518017, 'lng' => 17.9658056], + ], + 'Cún' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8122974, 'lng' => 18.0678543], + ], + 'Dencsháza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.993512, 'lng' => 17.8347772], + ], + 'Dinnyeberki' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0972962, 'lng' => 17.9563165], + ], + 'Diósviszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8774861, 'lng' => 18.1640495], + ], + 'Drávacsehi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8130167, 'lng' => 18.1666181], + ], + 'Drávacsepely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8308297, 'lng' => 18.1352308], + ], + 'Drávafok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8860365, 'lng' => 17.7636317], + ], + 'Drávaiványi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8470684, 'lng' => 17.8159164], + ], + 'Drávakeresztúr' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8386967, 'lng' => 17.7580104], + ], + 'Drávapalkonya' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8033438, 'lng' => 18.1790753], + ], + 'Drávapiski' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8396577, 'lng' => 18.0989657], + ], + 'Drávaszabolcs' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.803275, 'lng' => 18.2093234], + ], + 'Drávaszerdahely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8363562, 'lng' => 18.1638527], + ], + 'Drávasztára' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8230964, 'lng' => 17.8220692], + ], + 'Dunaszekcső' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0854783, 'lng' => 18.7542203], + ], + 'Egerág' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9834452, 'lng' => 18.3039561], + ], + 'Egyházasharaszti' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8097356, 'lng' => 18.3314381], + ], + 'Egyházaskozár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3319023, 'lng' => 18.3178591], + ], + 'Ellend' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0580138, 'lng' => 18.3760682], + ], + 'Endrőc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9296401, 'lng' => 17.7621758], + ], + 'Erdősmárok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.055568, 'lng' => 18.5458091], + ], + 'Erdősmecske' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1768439, 'lng' => 18.5109755], + ], + 'Erzsébet' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1004339, 'lng' => 18.4587621], + ], + 'Fazekasboda' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1230108, 'lng' => 18.4850924], + ], + 'Feked' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1626797, 'lng' => 18.5588015], + ], + 'Felsőegerszeg' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2539122, 'lng' => 18.1335751], + ], + 'Felsőszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8513101, 'lng' => 17.7034033], + ], + 'Garé' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9180881, 'lng' => 18.1956808], + ], + 'Gerde' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9904428, 'lng' => 18.0255496], + ], + 'Gerényes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.3070289, 'lng' => 18.1848981], + ], + 'Geresdlak' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1107897, 'lng' => 18.5268599], + ], + 'Gilvánfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9184356, 'lng' => 17.9622098], + ], + 'Gödre' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2899579, 'lng' => 17.9723779], + ], + 'Görcsöny' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9709725, 'lng' => 18.133486], + ], + 'Görcsönydoboka' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0709275, 'lng' => 18.6275109], + ], + 'Gordisa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7970748, 'lng' => 18.2354868], + ], + 'Gyód' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9979549, 'lng' => 18.1781638], + ], + 'Gyöngyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9601196, 'lng' => 17.9506649], + ], + 'Gyöngyösmellék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9868644, 'lng' => 17.7014751], + ], + 'Harkány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8534053, 'lng' => 18.2348372], + ], + 'Hásságy' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0330172, 'lng' => 18.388848], + ], + 'Hegyhátmaróc' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3109929, 'lng' => 18.3362487], + ], + 'Hegyszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9036373, 'lng' => 18.086797], + ], + 'Helesfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0894523, 'lng' => 17.9770167], + ], + 'Hetvehely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1332155, 'lng' => 18.0432466], + ], + 'Hidas' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2574631, 'lng' => 18.4937015], + ], + 'Himesháza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0797595, 'lng' => 18.5805933], + ], + 'Hirics' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8247516, 'lng' => 17.9934259], + ], + 'Hobol' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0197823, 'lng' => 17.7724266], + ], + 'Homorúd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.981847, 'lng' => 18.7887766], + ], + 'Horváthertelend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1751748, 'lng' => 17.9272893], + ], + 'Hosszúhetény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1583167, 'lng' => 18.3520974], + ], + 'Husztót' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1711511, 'lng' => 18.0932139], + ], + 'Ibafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1552456, 'lng' => 17.9179873], + ], + 'Illocska' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.800591, 'lng' => 18.5233576], + ], + 'Ipacsfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8345382, 'lng' => 18.2055561], + ], + 'Ivánbattyán' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9077809, 'lng' => 18.4176354], + ], + 'Ivándárda' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.831643, 'lng' => 18.5922589], + ], + 'Kacsóta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0390809, 'lng' => 17.9544689], + ], + 'Kákics' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9028359, 'lng' => 17.8568313], + ], + 'Kárász' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2667559, 'lng' => 18.3188548], + ], + 'Kásád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7793743, 'lng' => 18.3991912], + ], + 'Katádfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9970924, 'lng' => 17.8692171], + ], + 'Kátoly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0634292, 'lng' => 18.4496796], + ], + 'Kékesd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1007579, 'lng' => 18.4720006], + ], + 'Kémes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8241919, 'lng' => 18.1031607], + ], + 'Kemse' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8237775, 'lng' => 17.9119613], + ], + 'Keszü' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 46.0160053, 'lng' => 18.1918765], + ], + 'Kétújfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9643465, 'lng' => 17.7128738], + ], + 'Királyegyháza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9975029, 'lng' => 17.9670799], + ], + 'Kisasszonyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9467478, 'lng' => 18.0062386], + ], + 'Kisbeszterce' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2054937, 'lng' => 18.033257], + ], + 'Kisbudmér' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9132933, 'lng' => 18.4468642], + ], + 'Kisdér' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9397014, 'lng' => 18.1280256], + ], + 'Kisdobsza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0279686, 'lng' => 17.654966], + ], + 'Kishajmás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2000972, 'lng' => 18.0807394], + ], + 'Kisharsány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8597428, 'lng' => 18.3628602], + ], + 'Kisherend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9657006, 'lng' => 18.3308199], + ], + 'Kisjakabfalva' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8961294, 'lng' => 18.4347874], + ], + 'Kiskassa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9532763, 'lng' => 18.3984025], + ], + 'Kislippó' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8309942, 'lng' => 18.5387451], + ], + 'Kisnyárád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0369956, 'lng' => 18.5642298], + ], + 'Kisszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8245119, 'lng' => 18.0223384], + ], + 'Kistamási' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0118086, 'lng' => 17.7210893], + ], + 'Kistapolca' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8215113, 'lng' => 18.383003], + ], + 'Kistótfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9080691, 'lng' => 18.3097841], + ], + 'Kisvaszar' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2748571, 'lng' => 18.2126962], + ], + 'Köblény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2948258, 'lng' => 18.303697], + ], + 'Kökény' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9995372, 'lng' => 18.2057648], + ], + 'Kölked' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9489796, 'lng' => 18.7058024], + ], + 'Komló' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1929788, 'lng' => 18.2512139], + ], + 'Kórós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8666591, 'lng' => 18.0818986], + ], + 'Kovácshida' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8322528, 'lng' => 18.1852847], + ], + 'Kovácsszénája' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1714525, 'lng' => 18.1099753], + ], + 'Kővágószőlős' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0824433, 'lng' => 18.1242335], + ], + 'Kővágótöttös' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0859181, 'lng' => 18.1005597], + ], + 'Kozármisleny' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0412574, 'lng' => 18.2872228], + ], + 'Lánycsók' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0073964, 'lng' => 18.624077], + ], + 'Lapáncsa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8187417, 'lng' => 18.4965793], + ], + 'Liget' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2346633, 'lng' => 18.1924669], + ], + 'Lippó' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.863493, 'lng' => 18.5702136], + ], + 'Liptód' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.044203, 'lng' => 18.5153709], + ], + 'Lothárd' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0015129, 'lng' => 18.3534664], + ], + 'Lovászhetény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1573687, 'lng' => 18.4736022], + ], + 'Lúzsok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8386895, 'lng' => 17.9448893], + ], + 'Mágocs' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3507989, 'lng' => 18.2282954], + ], + 'Magyarbóly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8424536, 'lng' => 18.4905327], + ], + 'Magyaregregy' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2497645, 'lng' => 18.3080926], + ], + 'Magyarhertelend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1887919, 'lng' => 18.1496193], + ], + 'Magyarlukafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1692382, 'lng' => 17.7566367], + ], + 'Magyarmecske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9444333, 'lng' => 17.963957], + ], + 'Magyarsarlós' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0412482, 'lng' => 18.3527956], + ], + 'Magyarszék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1966719, 'lng' => 18.1955889], + ], + 'Magyartelek' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9438384, 'lng' => 17.9834231], + ], + 'Majs' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9090894, 'lng' => 18.59764], + ], + 'Mánfa' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1620219, 'lng' => 18.2424376], + ], + 'Maráza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0767639, 'lng' => 18.5102704], + ], + 'Márfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8597093, 'lng' => 18.184506], + ], + 'Máriakéménd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0275242, 'lng' => 18.4616888], + ], + 'Markóc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8633597, 'lng' => 17.7628134], + ], + 'Marócsa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9143499, 'lng' => 17.8155625], + ], + 'Márok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8776725, 'lng' => 18.5052153], + ], + 'Martonfa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1162762, 'lng' => 18.373108], + ], + 'Matty' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7959854, 'lng' => 18.2646823], + ], + 'Máza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2674701, 'lng' => 18.3987184], + ], + 'Mecseknádasd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.22466, 'lng' => 18.4653855], + ], + 'Mecsekpölöske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2232838, 'lng' => 18.2117379], + ], + 'Mekényes' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3905907, 'lng' => 18.3338629], + ], + 'Merenye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.069313, 'lng' => 17.6981454], + ], + 'Meződ' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2898147, 'lng' => 18.1028572], + ], + 'Mindszentgodisa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2270491, 'lng' => 18.070952], + ], + 'Mohács' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0046295, 'lng' => 18.6794304], + ], + 'Molvány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0294158, 'lng' => 17.7455964], + ], + 'Monyoród' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0115276, 'lng' => 18.4781726], + ], + 'Mozsgó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1148249, 'lng' => 17.8457585], + ], + 'Nagybudmér' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9378397, 'lng' => 18.4443309], + ], + 'Nagycsány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.871837, 'lng' => 17.9441308], + ], + 'Nagydobsza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0290366, 'lng' => 17.6672107], + ], + 'Nagyhajmás' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.372206, 'lng' => 18.2898052], + ], + 'Nagyharsány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8466947, 'lng' => 18.3947776], + ], + 'Nagykozár' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.067814, 'lng' => 18.316561], + ], + 'Nagynyárád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9447148, 'lng' => 18.578055], + ], + 'Nagypall' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1474016, 'lng' => 18.4539234], + ], + 'Nagypeterd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0459728, 'lng' => 17.8979423], + ], + 'Nagytótfalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8638406, 'lng' => 18.3426767], + ], + 'Nagyváty' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0617075, 'lng' => 17.93209], + ], + 'Nemeske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.020198, 'lng' => 17.7129695], + ], + 'Nyugotszenterzsébet' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0747959, 'lng' => 17.9096635], + ], + 'Óbánya' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2220338, 'lng' => 18.4084838], + ], + 'Ócsárd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9341296, 'lng' => 18.1533436], + ], + 'Ófalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2210918, 'lng' => 18.534029], + ], + 'Okorág' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9262423, 'lng' => 17.8761913], + ], + 'Okorvölgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.15235, 'lng' => 18.0600392], + ], + 'Olasz' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0128298, 'lng' => 18.4122965], + ], + 'Old' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7893924, 'lng' => 18.3526547], + ], + 'Orfű' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1504207, 'lng' => 18.1423992], + ], + 'Oroszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2201904, 'lng' => 18.122659], + ], + 'Ózdfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9288431, 'lng' => 18.0210679], + ], + 'Palé' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2603608, 'lng' => 18.0690432], + ], + 'Palkonya' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8968607, 'lng' => 18.3899099], + ], + 'Palotabozsok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1275672, 'lng' => 18.6416844], + ], + 'Páprád' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8927275, 'lng' => 18.0103745], + ], + 'Patapoklosi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0753051, 'lng' => 17.7415323], + ], + 'Pécs' => [ + 'constituencies' => ['Baranya 2.', 'Baranya 1.'], + 'coordinates' => ['lat' => 46.0727345, 'lng' => 18.232266], + ], + 'Pécsbagota' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9906469, 'lng' => 18.0728758], + ], + 'Pécsdevecser' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9585177, 'lng' => 18.3839237], + ], + 'Pécsudvard' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 46.0108323, 'lng' => 18.2750737], + ], + 'Pécsvárad' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1591341, 'lng' => 18.4185199], + ], + 'Pellérd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.034172, 'lng' => 18.1551531], + ], + 'Pereked' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0940085, 'lng' => 18.3768639], + ], + 'Peterd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9726228, 'lng' => 18.3606704], + ], + 'Pettend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0001576, 'lng' => 17.7011535], + ], + 'Piskó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8112973, 'lng' => 17.9384454], + ], + 'Pócsa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9100922, 'lng' => 18.4699792], + ], + 'Pogány' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9827333, 'lng' => 18.2568939], + ], + 'Rádfalva' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8598624, 'lng' => 18.1252323], + ], + 'Regenye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.969783, 'lng' => 18.1685228], + ], + 'Romonya' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0871177, 'lng' => 18.3391112], + ], + 'Rózsafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0227215, 'lng' => 17.8889708], + ], + 'Sámod' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8536384, 'lng' => 18.0384521], + ], + 'Sárok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8414254, 'lng' => 18.6119412], + ], + 'Sásd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2563232, 'lng' => 18.1024778], + ], + 'Sátorhely' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9417452, 'lng' => 18.6330768], + ], + 'Sellye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.873291, 'lng' => 17.8494986], + ], + 'Siklós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8555814, 'lng' => 18.2979721], + ], + 'Siklósbodony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9105251, 'lng' => 18.1202589], + ], + 'Siklósnagyfalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.820428, 'lng' => 18.3636246], + ], + 'Somberek' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0812348, 'lng' => 18.6586781], + ], + 'Somogyapáti' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0920041, 'lng' => 17.7506787], + ], + 'Somogyhárságy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1623103, 'lng' => 17.7731873], + ], + 'Somogyhatvan' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1120284, 'lng' => 17.7126553], + ], + 'Somogyviszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1146313, 'lng' => 17.7636375], + ], + 'Sósvertike' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8340815, 'lng' => 17.8614028], + ], + 'Sumony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9675435, 'lng' => 17.9146319], + ], + 'Szabadszentkirály' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0059012, 'lng' => 18.0435247], + ], + 'Szágy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2244706, 'lng' => 17.9469817], + ], + 'Szajk' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9921175, 'lng' => 18.5328986], + ], + 'Szalánta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9471908, 'lng' => 18.2376181], + ], + 'Szalatnak' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2903675, 'lng' => 18.2809735], + ], + 'Szaporca' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8135724, 'lng' => 18.1045054], + ], + 'Szárász' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3487743, 'lng' => 18.3727487], + ], + 'Szászvár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2739639, 'lng' => 18.3774781], + ], + 'Szava' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9024581, 'lng' => 18.1738569], + ], + 'Szebény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1296283, 'lng' => 18.5879918], + ], + 'Szederkény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9986735, 'lng' => 18.4530663], + ], + 'Székelyszabar' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0471326, 'lng' => 18.6012321], + ], + 'Szellő' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0744167, 'lng' => 18.4609549], + ], + 'Szemely' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0083381, 'lng' => 18.3256717], + ], + 'Szentdénes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0079644, 'lng' => 17.9271651], + ], + 'Szentegát' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9754975, 'lng' => 17.8244079], + ], + 'Szentkatalin' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.174384, 'lng' => 18.0505714], + ], + 'Szentlászló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1540417, 'lng' => 17.8331512], + ], + 'Szentlőrinc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0403123, 'lng' => 17.9897756], + ], + 'Szigetvár' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0487727, 'lng' => 17.7983466], + ], + 'Szilágy' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1009525, 'lng' => 18.4065405], + ], + 'Szilvás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9616358, 'lng' => 18.1981701], + ], + 'Szőke' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9604273, 'lng' => 18.1867423], + ], + 'Szőkéd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9645154, 'lng' => 18.2884592], + ], + 'Szörény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9683861, 'lng' => 17.6819713], + ], + 'Szulimán' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1264433, 'lng' => 17.805449], + ], + 'Szűr' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.099254, 'lng' => 18.5809615], + ], + 'Tarrós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2806564, 'lng' => 18.1425225], + ], + 'Tékes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2866262, 'lng' => 18.1744149], + ], + 'Teklafalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9493136, 'lng' => 17.7287585], + ], + 'Tengeri' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9263477, 'lng' => 18.087938], + ], + 'Tésenfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8127763, 'lng' => 18.1178921], + ], + 'Téseny' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9515499, 'lng' => 18.0479966], + ], + 'Tófű' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3094872, 'lng' => 18.3576794], + ], + 'Tormás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2309543, 'lng' => 17.9937201], + ], + 'Tótszentgyörgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0521798, 'lng' => 17.7178541], + ], + 'Töttös' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9150433, 'lng' => 18.5407584], + ], + 'Túrony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9054082, 'lng' => 18.2309533], + ], + 'Udvar' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.900472, 'lng' => 18.6594842], + ], + 'Újpetre' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.934779, 'lng' => 18.3636323], + ], + 'Vajszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8592442, 'lng' => 17.9868205], + ], + 'Várad' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9743574, 'lng' => 17.7456586], + ], + 'Varga' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2475508, 'lng' => 18.1424694], + ], + 'Vásárosbéc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1825351, 'lng' => 17.7246441], + ], + 'Vásárosdombó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.3064752, 'lng' => 18.1334675], + ], + 'Vázsnok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2653395, 'lng' => 18.1253751], + ], + 'Vejti' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8096089, 'lng' => 17.9682522], + ], + 'Vékény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2695945, 'lng' => 18.3423454], + ], + 'Velény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9807601, 'lng' => 18.0514344], + ], + 'Véménd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1551161, 'lng' => 18.6190866], + ], + 'Versend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9953039, 'lng' => 18.5115869], + ], + 'Villány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8700399, 'lng' => 18.453201], + ], + 'Villánykövesd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8823189, 'lng' => 18.425812], + ], + 'Vokány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9133714, 'lng' => 18.3364685], + ], + 'Zádor' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9623692, 'lng' => 17.6579278], + ], + 'Zaláta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8111976, 'lng' => 17.8901202], + ], + 'Zengővárkony' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1728638, 'lng' => 18.4320077], + ], + 'Zók' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0104261, 'lng' => 18.0965422], + ], + ], + 'Békés' => [ + 'Almáskamarás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4617785, 'lng' => 21.092448], + ], + 'Battonya' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.2902462, 'lng' => 21.0199215], + ], + 'Békés' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.6704899, 'lng' => 21.0434996], + ], + 'Békéscsaba' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6735939, 'lng' => 21.0877309], + ], + 'Békéssámson' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4208677, 'lng' => 20.6176498], + ], + 'Békésszentandrás' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8715996, 'lng' => 20.48336], + ], + 'Bélmegyer' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8726019, 'lng' => 21.1832832], + ], + 'Biharugra' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9691009, 'lng' => 21.5987651], + ], + 'Bucsa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.2047017, 'lng' => 20.9970391], + ], + 'Csabacsűd' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8244161, 'lng' => 20.6485242], + ], + 'Csabaszabadi' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.574811, 'lng' => 20.951145], + ], + 'Csanádapáca' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5409397, 'lng' => 20.8852553], + ], + 'Csárdaszállás' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8647568, 'lng' => 20.9374853], + ], + 'Csorvás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6308376, 'lng' => 20.8340929], + ], + 'Dévaványa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.0313217, 'lng' => 20.9595443], + ], + 'Doboz' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7343152, 'lng' => 21.2420659], + ], + 'Dombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3415879, 'lng' => 21.1342664], + ], + 'Dombiratos' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4195218, 'lng' => 21.1178789], + ], + 'Ecsegfalva' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.14789, 'lng' => 20.9239261], + ], + 'Elek' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.5291929, 'lng' => 21.2487556], + ], + 'Füzesgyarmat' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.1051107, 'lng' => 21.2108329], + ], + 'Gádoros' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.6667476, 'lng' => 20.5961159], + ], + 'Gerendás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5969212, 'lng' => 20.8593687], + ], + 'Geszt' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8831763, 'lng' => 21.5794915], + ], + 'Gyomaendrőd' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.9317797, 'lng' => 20.8113125], + ], + 'Gyula' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.6473027, 'lng' => 21.2784255], + ], + 'Hunya' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.812869, 'lng' => 20.8458337], + ], + 'Kamut' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.7619186, 'lng' => 20.9798143], + ], + 'Kardos' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.7941712, 'lng' => 20.715629], + ], + 'Kardoskút' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.498573, 'lng' => 20.7040158], + ], + 'Kaszaper' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4598817, 'lng' => 20.8251944], + ], + 'Kertészsziget' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.1542945, 'lng' => 21.0610234], + ], + 'Kétegyháza' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5417887, 'lng' => 21.1810736], + ], + 'Kétsoprony' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.7208319, 'lng' => 20.8870273], + ], + 'Kevermes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4167579, 'lng' => 21.1818484], + ], + 'Kisdombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3693244, 'lng' => 21.0996778], + ], + 'Kondoros' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.7574628, 'lng' => 20.7972363], + ], + 'Körösladány' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.9607513, 'lng' => 21.0767574], + ], + 'Körösnagyharsány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.0080391, 'lng' => 21.6417355], + ], + 'Köröstarcsa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8780314, 'lng' => 21.02402], + ], + 'Körösújfalu' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9659419, 'lng' => 21.3988486], + ], + 'Kötegyán' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.738284, 'lng' => 21.481692], + ], + 'Kunágota' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4234015, 'lng' => 21.0467553], + ], + 'Lőkösháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4297019, 'lng' => 21.2318793], + ], + 'Magyarbánhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4577279, 'lng' => 20.968734], + ], + 'Magyardombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3794548, 'lng' => 21.0743712], + ], + 'Medgyesbodzás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5186797, 'lng' => 20.9596371], + ], + 'Medgyesegyháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4967576, 'lng' => 21.0271996], + ], + 'Méhkerék' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7735176, 'lng' => 21.4435935], + ], + 'Mezőberény' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.825687, 'lng' => 21.0243614], + ], + 'Mezőgyán' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8709809, 'lng' => 21.5257366], + ], + 'Mezőhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3172449, 'lng' => 20.8173892], + ], + 'Mezőkovácsháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4093003, 'lng' => 20.9112692], + ], + 'Murony' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.760463, 'lng' => 21.0411739], + ], + 'Nagybánhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.460095, 'lng' => 20.902578], + ], + 'Nagykamarás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4727168, 'lng' => 21.1213871], + ], + 'Nagyszénás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.6722161, 'lng' => 20.6734381], + ], + 'Okány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8982798, 'lng' => 21.3467384], + ], + 'Örménykút' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.830573, 'lng' => 20.7344497], + ], + 'Orosháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5684222, 'lng' => 20.6544927], + ], + 'Pusztaföldvár' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5251751, 'lng' => 20.8024526], + ], + 'Pusztaottlaka' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5386606, 'lng' => 21.0060316], + ], + 'Sarkad' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7374245, 'lng' => 21.3810771], + ], + 'Sarkadkeresztúr' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8107081, 'lng' => 21.3841932], + ], + 'Szabadkígyós' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.601522, 'lng' => 21.0753003], + ], + 'Szarvas' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8635641, 'lng' => 20.5526535], + ], + 'Szeghalom' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.0239347, 'lng' => 21.1666571], + ], + 'Tarhos' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8132012, 'lng' => 21.2109597], + ], + 'Telekgerendás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6566167, 'lng' => 20.9496242], + ], + 'Tótkomlós' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4107596, 'lng' => 20.7363644], + ], + 'Újkígyós' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5899757, 'lng' => 21.0242728], + ], + 'Újszalonta' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8128247, 'lng' => 21.4908762], + ], + 'Végegyháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3882623, 'lng' => 20.8699923], + ], + 'Vésztő' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9244546, 'lng' => 21.2628502], + ], + 'Zsadány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9230248, 'lng' => 21.4873156], + ], + ], + 'Borsod-Abaúj-Zemplén' => [ + 'Abaújalpár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3065157, 'lng' => 21.232147], + ], + 'Abaújkér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3033478, 'lng' => 21.2013068], + ], + 'Abaújlak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4051818, 'lng' => 20.9548056], + ], + 'Abaújszántó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2792184, 'lng' => 21.1874523], + ], + 'Abaújszolnok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3730791, 'lng' => 20.9749255], + ], + 'Abaújvár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5266538, 'lng' => 21.3150208], + ], + 'Abod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3928646, 'lng' => 20.7923344], + ], + 'Aggtelek' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4686657, 'lng' => 20.5040699], + ], + 'Alacska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2157484, 'lng' => 20.6502945], + ], + 'Alsóberecki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3437614, 'lng' => 21.6905164], + ], + 'Alsódobsza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1799523, 'lng' => 21.0026817], + ], + 'Alsógagy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4052855, 'lng' => 21.0255485], + ], + 'Alsóregmec' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4634336, 'lng' => 21.6181953], + ], + 'Alsószuha' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3726027, 'lng' => 20.5044038], + ], + 'Alsótelekes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4105212, 'lng' => 20.6547156], + ], + 'Alsóvadász' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2401438, 'lng' => 20.9043765], + ], + 'Alsózsolca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.0748263, 'lng' => 20.8850624], + ], + 'Arka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3562385, 'lng' => 21.252529], + ], + 'Arló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1746548, 'lng' => 20.2560308], + ], + 'Arnót' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1319962, 'lng' => 20.859401], + ], + 'Ároktő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7284812, 'lng' => 20.9423131], + ], + 'Aszaló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2177554, 'lng' => 20.9624804], + ], + 'Baktakék' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3675199, 'lng' => 21.0288911], + ], + 'Balajt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3210349, 'lng' => 20.7866111], + ], + 'Bánhorváti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2260139, 'lng' => 20.504815], + ], + 'Bánréve' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2986902, 'lng' => 20.3560194], + ], + 'Baskó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3326787, 'lng' => 21.336418], + ], + 'Becskeháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5294979, 'lng' => 20.8354743], + ], + 'Bekecs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1534102, 'lng' => 21.1762263], + ], + 'Berente' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2385836, 'lng' => 20.6700776], + ], + 'Beret' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3458722, 'lng' => 21.0235103], + ], + 'Berzék' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0240535, 'lng' => 20.9528886], + ], + 'Bőcs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0442332, 'lng' => 20.9683874], + ], + 'Bodroghalom' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3009977, 'lng' => 21.707044], + ], + 'Bodrogkeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1630176, 'lng' => 21.3595899], + ], + 'Bodrogkisfalud' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1789303, 'lng' => 21.3617788], + ], + 'Bodrogolaszi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2867085, 'lng' => 21.5160527], + ], + 'Bódvalenke' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5424028, 'lng' => 20.8041838], + ], + 'Bódvarákó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5111514, 'lng' => 20.7358047], + ], + 'Bódvaszilas' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5377629, 'lng' => 20.7312757], + ], + 'Bogács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9030764, 'lng' => 20.5312356], + ], + 'Boldogkőújfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3193629, 'lng' => 21.242022], + ], + 'Boldogkőváralja' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3380634, 'lng' => 21.2367554], + ], + 'Boldva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.218091, 'lng' => 20.7886144], + ], + 'Borsodbóta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2121829, 'lng' => 20.3960602], + ], + 'Borsodgeszt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9559428, 'lng' => 20.6944004], + ], + 'Borsodivánka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.701045, 'lng' => 20.6547148], + ], + 'Borsodnádasd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1191717, 'lng' => 20.2529566], + ], + 'Borsodszentgyörgy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1892068, 'lng' => 20.2073894], + ], + 'Borsodszirák' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2610318, 'lng' => 20.7676252], + ], + 'Bózsva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4743356, 'lng' => 21.468268], + ], + 'Bükkábrány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8884157, 'lng' => 20.6810544], + ], + 'Bükkaranyos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9866329, 'lng' => 20.7794609], + ], + 'Bükkmogyorósd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1291531, 'lng' => 20.3563552], + ], + 'Bükkszentkereszt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0668164, 'lng' => 20.6324773], + ], + 'Bükkzsérc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9587559, 'lng' => 20.5025627], + ], + 'Büttös' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4783127, 'lng' => 21.0110122], + ], + 'Cigánd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2558937, 'lng' => 21.8889241], + ], + 'Csenyéte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4345165, 'lng' => 21.0412334], + ], + 'Cserépfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9413093, 'lng' => 20.5347083], + ], + 'Cserépváralja' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9325883, 'lng' => 20.5598918], + ], + 'Csernely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1438586, 'lng' => 20.3390005], + ], + 'Csincse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8883234, 'lng' => 20.768705], + ], + 'Csobád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2796877, 'lng' => 21.0269782], + ], + 'Csobaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0485163, 'lng' => 21.3382189], + ], + 'Csokvaomány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1666711, 'lng' => 20.3744746], + ], + 'Damak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3168034, 'lng' => 20.8216124], + ], + 'Dámóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3748294, 'lng' => 22.0336128], + ], + 'Debréte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5000066, 'lng' => 20.8661035], + ], + 'Dédestapolcsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1804582, 'lng' => 20.4850166], + ], + 'Detek' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3336841, 'lng' => 21.0176305], + ], + 'Domaháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1836193, 'lng' => 20.1055583], + ], + 'Dövény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3469512, 'lng' => 20.5431344], + ], + 'Dubicsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2837745, 'lng' => 20.4940325], + ], + 'Edelény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2934391, 'lng' => 20.7385817], + ], + 'Egerlövő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7203221, 'lng' => 20.6175935], + ], + 'Égerszög' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.442896, 'lng' => 20.5875195], + ], + 'Emőd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9380038, 'lng' => 20.8154444], + ], + 'Encs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3259442, 'lng' => 21.1133006], + ], + 'Erdőbénye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2662769, 'lng' => 21.3547995], + ], + 'Erdőhorváti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3158739, 'lng' => 21.4272709], + ], + 'Fáj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4219028, 'lng' => 21.0747972], + ], + 'Fancsal' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3552347, 'lng' => 21.064671], + ], + 'Farkaslyuk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1876627, 'lng' => 20.3086509], + ], + 'Felsőberecki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3595718, 'lng' => 21.6950761], + ], + 'Felsődobsza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2555859, 'lng' => 21.0764245], + ], + 'Felsőgagy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4289932, 'lng' => 21.0128468], + ], + 'Felsőkelecsény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3600051, 'lng' => 20.5939689], + ], + 'Felsőnyárád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3299583, 'lng' => 20.5995966], + ], + 'Felsőregmec' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4915243, 'lng' => 21.6056225], + ], + 'Felsőtelekes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4058831, 'lng' => 20.6352386], + ], + 'Felsővadász' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3709811, 'lng' => 20.9195765], + ], + 'Felsőzsolca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1041265, 'lng' => 20.8595396], + ], + 'Filkeháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4960919, 'lng' => 21.4888024], + ], + 'Fony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3910341, 'lng' => 21.2865504], + ], + 'Forró' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3233535, 'lng' => 21.0880493], + ], + 'Fulókércs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4308674, 'lng' => 21.1049891], + ], + 'Füzér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.539654, 'lng' => 21.4547936], + ], + 'Füzérkajata' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5182556, 'lng' => 21.5000318], + ], + 'Füzérkomlós' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5126205, 'lng' => 21.4532344], + ], + 'Füzérradvány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.483741, 'lng' => 21.530474], + ], + 'Gadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4006289, 'lng' => 20.9296444], + ], + 'Gagyapáti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.409096, 'lng' => 21.0017182], + ], + 'Gagybátor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.433303, 'lng' => 20.94859], + ], + 'Gagyvendégi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4285166, 'lng' => 20.972405], + ], + 'Galvács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4190767, 'lng' => 20.7767621], + ], + 'Garadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4174625, 'lng' => 21.17463], + ], + 'Gelej' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.828655, 'lng' => 20.7755503], + ], + 'Gesztely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1026673, 'lng' => 20.9654647], + ], + 'Gibárt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3153245, 'lng' => 21.1603909], + ], + 'Girincs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9691368, 'lng' => 20.9846965], + ], + 'Golop' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2374312, 'lng' => 21.1893372], + ], + 'Gömörszőlős' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3730427, 'lng' => 20.4276758], + ], + 'Gönc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4727097, 'lng' => 21.2735417], + ], + 'Göncruszka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4488786, 'lng' => 21.239774], + ], + 'Györgytarló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2053902, 'lng' => 21.6316333], + ], + 'Halmaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2464584, 'lng' => 20.9983349], + ], + 'Hangács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2896949, 'lng' => 20.8314625], + ], + 'Hangony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2290868, 'lng' => 20.198029], + ], + 'Háromhuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3780662, 'lng' => 21.4283347], + ], + 'Harsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9679177, 'lng' => 20.7418041], + ], + 'Hegymeg' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3314259, 'lng' => 20.8614048], + ], + 'Hejce' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4234865, 'lng' => 21.2816978], + ], + 'Hejőbába' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9059201, 'lng' => 20.9452436], + ], + 'Hejőkeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9610209, 'lng' => 20.8772681], + ], + 'Hejőkürt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8564708, 'lng' => 20.9930661], + ], + 'Hejőpapi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8972354, 'lng' => 20.9054713], + ], + 'Hejőszalonta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9388389, 'lng' => 20.8822344], + ], + 'Hercegkút' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3340476, 'lng' => 21.5301233], + ], + 'Hernádbűd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2966038, 'lng' => 21.137896], + ], + 'Hernádcéce' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3587807, 'lng' => 21.1976117], + ], + 'Hernádkak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0892117, 'lng' => 20.9635617], + ], + 'Hernádkércs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2420151, 'lng' => 21.0501362], + ], + 'Hernádnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0716822, 'lng' => 20.9742345], + ], + 'Hernádpetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4815086, 'lng' => 21.1622472], + ], + 'Hernádszentandrás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2890724, 'lng' => 21.0949074], + ], + 'Hernádszurdok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.48169, 'lng' => 21.2071561], + ], + 'Hernádvécse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4406714, 'lng' => 21.1687099], + ], + 'Hét' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.282992, 'lng' => 20.3875674], + ], + 'Hidasnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5029778, 'lng' => 21.2293013], + ], + 'Hidvégardó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5598883, 'lng' => 20.8395348], + ], + 'Hollóháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5393716, 'lng' => 21.4144474], + ], + 'Homrogd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2834505, 'lng' => 20.9125329], + ], + 'Igrici' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8673926, 'lng' => 20.8831705], + ], + 'Imola' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4201572, 'lng' => 20.5516409], + ], + 'Ináncs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2861362, 'lng' => 21.0681971], + ], + 'Irota' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3964482, 'lng' => 20.8752667], + ], + 'Izsófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3087892, 'lng' => 20.6536072], + ], + 'Jákfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3316408, 'lng' => 20.569496], + ], + 'Járdánháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1551033, 'lng' => 20.2477262], + ], + 'Jósvafő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4826254, 'lng' => 20.5504479], + ], + 'Kács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9574786, 'lng' => 20.6145847], + ], + 'Kánó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4276397, 'lng' => 20.5991681], + ], + 'Kány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5151651, 'lng' => 21.0143542], + ], + 'Karcsa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3131571, 'lng' => 21.7953512], + ], + 'Karos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3312141, 'lng' => 21.7406654], + ], + 'Kazincbarcika' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2489437, 'lng' => 20.6189771], + ], + 'Kázsmárk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2728658, 'lng' => 20.9760294], + ], + 'Kéked' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5447244, 'lng' => 21.3500526], + ], + 'Kelemér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3551802, 'lng' => 20.4296357], + ], + 'Kenézlő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2004193, 'lng' => 21.5311235], + ], + 'Keresztéte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4989547, 'lng' => 20.950696], + ], + 'Kesznyéten' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9694339, 'lng' => 21.0413905], + ], + 'Királd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2393694, 'lng' => 20.3764361], + ], + 'Kiscsécs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9678112, 'lng' => 21.011133], + ], + 'Kisgyőr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0096251, 'lng' => 20.6874073], + ], + 'Kishuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4503449, 'lng' => 21.4814089], + ], + 'Kiskinizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2508135, 'lng' => 21.0345918], + ], + 'Kisrozvágy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3491303, 'lng' => 21.9390758], + ], + 'Kissikátor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1946631, 'lng' => 20.1302306], + ], + 'Kistokaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0397115, 'lng' => 20.8410079], + ], + 'Komjáti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5452009, 'lng' => 20.7618268], + ], + 'Komlóska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3404486, 'lng' => 21.4622875], + ], + 'Kondó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1880491, 'lng' => 20.6438586], + ], + 'Korlát' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3779667, 'lng' => 21.2457327], + ], + 'Köröm' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9842491, 'lng' => 20.9545886], + ], + 'Kovácsvágás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.45352, 'lng' => 21.5283164], + ], + 'Krasznokvajda' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4705256, 'lng' => 20.9714153], + ], + 'Kupa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3316226, 'lng' => 20.9145594], + ], + 'Kurityán' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.310505, 'lng' => 20.62573], + ], + 'Lácacséke' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3664002, 'lng' => 21.9934562], + ], + 'Ládbesenyő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3432268, 'lng' => 20.7859308], + ], + 'Lak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3480907, 'lng' => 20.8662135], + ], + 'Legyesbénye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1564545, 'lng' => 21.1530692], + ], + 'Léh' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2906948, 'lng' => 20.9807054], + ], + 'Lénárddaróc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1486722, 'lng' => 20.3728301], + ], + 'Litka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4544802, 'lng' => 21.0584273], + ], + 'Mád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1922445, 'lng' => 21.2759773], + ], + 'Makkoshotyka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3571928, 'lng' => 21.5164187], + ], + 'Mályi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0175678, 'lng' => 20.8292414], + ], + 'Mályinka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1545567, 'lng' => 20.4958901], + ], + 'Martonyi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4702379, 'lng' => 20.7660532], + ], + 'Megyaszó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1875185, 'lng' => 21.0547033], + ], + 'Méra' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3565901, 'lng' => 21.1469291], + ], + 'Meszes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.438651, 'lng' => 20.7950688], + ], + 'Mezőcsát' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8207081, 'lng' => 20.9051607], + ], + 'Mezőkeresztes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8262301, 'lng' => 20.6884043], + ], + 'Mezőkövesd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8074617, 'lng' => 20.5698525], + ], + 'Mezőnagymihály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8062776, 'lng' => 20.7308177], + ], + 'Mezőnyárád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8585625, 'lng' => 20.6764688], + ], + 'Mezőzombor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1501209, 'lng' => 21.2575954], + ], + 'Mikóháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4617944, 'lng' => 21.592572], + ], + 'Miskolc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.', 'Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1034775, 'lng' => 20.7784384], + ], + 'Mogyoróska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3759799, 'lng' => 21.3296401], + ], + 'Monaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3061021, 'lng' => 20.9348205], + ], + 'Monok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2099439, 'lng' => 21.149252], + ], + 'Múcsony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2758139, 'lng' => 20.6716209], + ], + 'Muhi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9778997, 'lng' => 20.9293321], + ], + 'Nagybarca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2476865, 'lng' => 20.5280319], + ], + 'Nagycsécs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9601505, 'lng' => 20.9482798], + ], + 'Nagyhuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4290026, 'lng' => 21.492424], + ], + 'Nagykinizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2344766, 'lng' => 21.0335706], + ], + 'Nagyrozvágy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3404683, 'lng' => 21.9228458], + ], + 'Négyes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7013, 'lng' => 20.7040224], + ], + 'Nekézseny' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1689694, 'lng' => 20.4291357], + ], + 'Nemesbikk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8876867, 'lng' => 20.9661155], + ], + 'Novajidrány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.396674, 'lng' => 21.1688256], + ], + 'Nyékládháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9933002, 'lng' => 20.8429935], + ], + 'Nyésta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3702622, 'lng' => 20.9514276], + ], + 'Nyíri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4986982, 'lng' => 21.440883], + ], + 'Nyomár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.275559, 'lng' => 20.8198353], + ], + 'Olaszliszka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2419377, 'lng' => 21.4279754], + ], + 'Onga' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1194769, 'lng' => 20.9065655], + ], + 'Ónod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0024425, 'lng' => 20.9146535], + ], + 'Ormosbánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3322064, 'lng' => 20.6493181], + ], + 'Oszlár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.8740321, 'lng' => 21.0332202], + ], + 'Ózd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2241439, 'lng' => 20.2888698], + ], + 'Pácin' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3306334, 'lng' => 21.8337743], + ], + 'Pálháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4717353, 'lng' => 21.507078], + ], + 'Pamlény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.493024, 'lng' => 20.9282949], + ], + 'Pányok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5298401, 'lng' => 21.3478472], + ], + 'Parasznya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1688229, 'lng' => 20.6402064], + ], + 'Pere' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2845544, 'lng' => 21.1211586], + ], + 'Perecse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5027869, 'lng' => 20.9845634], + ], + 'Perkupa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4712725, 'lng' => 20.6862819], + ], + 'Prügy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0824191, 'lng' => 21.2428751], + ], + 'Pusztafalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5439277, 'lng' => 21.4860599], + ], + 'Pusztaradvány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4679248, 'lng' => 21.1338715], + ], + 'Putnok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2939007, 'lng' => 20.4333508], + ], + 'Radostyán' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1787774, 'lng' => 20.6532017], + ], + 'Ragály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4041753, 'lng' => 20.5211463], + ], + 'Rakaca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4617206, 'lng' => 20.8848555], + ], + 'Rakacaszend' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4611034, 'lng' => 20.8378744], + ], + 'Rásonysápberencs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.304802, 'lng' => 20.9934828], + ], + 'Rátka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2156932, 'lng' => 21.2267141], + ], + 'Regéc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.392191, 'lng' => 21.3436481], + ], + 'Répáshuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0507939, 'lng' => 20.5254934], + ], + 'Révleányvár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3230427, 'lng' => 22.0416695], + ], + 'Ricse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3251432, 'lng' => 21.9687588], + ], + 'Rudabánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3747405, 'lng' => 20.6206118], + ], + 'Rudolftelep' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3092868, 'lng' => 20.6711602], + ], + 'Sajóbábony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1742691, 'lng' => 20.734572], + ], + 'Sajóecseg' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.190065, 'lng' => 20.772827], + ], + 'Sajógalgóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2929878, 'lng' => 20.5323886], + ], + 'Sajóhídvég' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0026817, 'lng' => 20.9495863], + ], + 'Sajóivánka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2654174, 'lng' => 20.5799268], + ], + 'Sajókápolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1952827, 'lng' => 20.6848853], + ], + 'Sajókaza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2864119, 'lng' => 20.5851277], + ], + 'Sajókeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1694996, 'lng' => 20.7768886], + ], + 'Sajólád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0402765, 'lng' => 20.9024513], + ], + 'Sajólászlófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1848765, 'lng' => 20.6736002], + ], + 'Sajómercse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2461305, 'lng' => 20.414773], + ], + 'Sajónémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.270659, 'lng' => 20.3811845], + ], + 'Sajóörös' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9515653, 'lng' => 21.0219599], + ], + 'Sajópálfala' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.163139, 'lng' => 20.8458093], + ], + 'Sajópetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0351497, 'lng' => 20.8878767], + ], + 'Sajópüspöki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.280186, 'lng' => 20.3400614], + ], + 'Sajósenye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1960682, 'lng' => 20.8185281], + ], + 'Sajószentpéter' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2188772, 'lng' => 20.7092248], + ], + 'Sajószöged' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9458004, 'lng' => 20.9946112], + ], + 'Sajóvámos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1802021, 'lng' => 20.8298154], + ], + 'Sajóvelezd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2714818, 'lng' => 20.4593985], + ], + 'Sály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9527979, 'lng' => 20.6597197], + ], + 'Sárazsadány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2684871, 'lng' => 21.497789], + ], + 'Sárospatak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3196929, 'lng' => 21.5687308], + ], + 'Sáta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1876567, 'lng' => 20.3914051], + ], + 'Sátoraljaújhely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3960601, 'lng' => 21.6551122], + ], + 'Selyeb' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3381582, 'lng' => 20.9541317], + ], + 'Semjén' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3521396, 'lng' => 21.9671011], + ], + 'Serényfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3071589, 'lng' => 20.3852844], + ], + 'Sima' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2996969, 'lng' => 21.3030527], + ], + 'Sóstófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.156243, 'lng' => 20.9870638], + ], + 'Szakácsi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3820531, 'lng' => 20.8614571], + ], + 'Szakáld' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9431182, 'lng' => 20.908997], + ], + 'Szalaszend' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3859709, 'lng' => 21.1243501], + ], + 'Szalonna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4500484, 'lng' => 20.7394926], + ], + 'Szászfa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4704359, 'lng' => 20.9418168], + ], + 'Szegi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.1953737, 'lng' => 21.3795562], + ], + 'Szegilong' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2162488, 'lng' => 21.3965639], + ], + 'Szemere' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4661495, 'lng' => 21.099542], + ], + 'Szendrő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4046962, 'lng' => 20.7282046], + ], + 'Szendrőlád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3433366, 'lng' => 20.7419436], + ], + 'Szentistván' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7737632, 'lng' => 20.6579694], + ], + 'Szentistvánbaksa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2227558, 'lng' => 21.0276456], + ], + 'Szerencs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1590429, 'lng' => 21.2048872], + ], + 'Szikszó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1989312, 'lng' => 20.9298039], + ], + 'Szin' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4972791, 'lng' => 20.6601922], + ], + 'Szinpetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4847097, 'lng' => 20.625043], + ], + 'Szirmabesenyő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1509585, 'lng' => 20.7957903], + ], + 'Szögliget' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5215045, 'lng' => 20.6770697], + ], + 'Szőlősardó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.443484, 'lng' => 20.6278686], + ], + 'Szomolya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8919105, 'lng' => 20.4949334], + ], + 'Szuhafő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4082703, 'lng' => 20.4515974], + ], + 'Szuhakálló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2835218, 'lng' => 20.6523991], + ], + 'Szuhogy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3842029, 'lng' => 20.6731282], + ], + 'Taktabáj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0621903, 'lng' => 21.3112131], + ], + 'Taktaharkány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0876121, 'lng' => 21.129918], + ], + 'Taktakenéz' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0508677, 'lng' => 21.2167146], + ], + 'Taktaszada' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1103437, 'lng' => 21.1735733], + ], + 'Tállya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2352295, 'lng' => 21.2260996], + ], + 'Tarcal' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1311328, 'lng' => 21.3418021], + ], + 'Tard' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8784711, 'lng' => 20.598937], + ], + 'Tardona' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1699442, 'lng' => 20.531454], + ], + 'Telkibánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4854061, 'lng' => 21.3574907], + ], + 'Teresztenye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4463436, 'lng' => 20.6031689], + ], + 'Tibolddaróc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9206758, 'lng' => 20.6355357], + ], + 'Tiszabábolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.689752, 'lng' => 20.813906], + ], + 'Tiszacsermely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2336812, 'lng' => 21.7945686], + ], + 'Tiszadorogma' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.6839826, 'lng' => 20.8661184], + ], + 'Tiszakarád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2061184, 'lng' => 21.7213149], + ], + 'Tiszakeszi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7879554, 'lng' => 20.9904672], + ], + 'Tiszaladány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0621067, 'lng' => 21.4101619], + ], + 'Tiszalúc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0358262, 'lng' => 21.0648204], + ], + 'Tiszapalkonya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.8849204, 'lng' => 21.0557818], + ], + 'Tiszatardos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0406385, 'lng' => 21.379655], + ], + 'Tiszatarján' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8329217, 'lng' => 21.0014346], + ], + 'Tiszaújváros' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9159846, 'lng' => 21.0427447], + ], + 'Tiszavalk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.6888504, 'lng' => 20.751499], + ], + 'Tokaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1172148, 'lng' => 21.4089015], + ], + 'Tolcsva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2841513, 'lng' => 21.4488452], + ], + 'Tomor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3258904, 'lng' => 20.8823733], + ], + 'Tornabarakony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4922432, 'lng' => 20.8192157], + ], + 'Tornakápolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4616855, 'lng' => 20.617706], + ], + 'Tornanádaska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5611186, 'lng' => 20.7846392], + ], + 'Tornaszentandrás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5226438, 'lng' => 20.7790226], + ], + 'Tornaszentjakab' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5244312, 'lng' => 20.8729813], + ], + 'Tornyosnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5202757, 'lng' => 21.2506927], + ], + 'Trizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4251253, 'lng' => 20.4958645], + ], + 'Újcsanálos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1380468, 'lng' => 21.0036907], + ], + 'Uppony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2155013, 'lng' => 20.434654], + ], + 'Vadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2733247, 'lng' => 20.5552218], + ], + 'Vágáshuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4264605, 'lng' => 21.545222], + ], + 'Vajdácska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3196383, 'lng' => 21.6541401], + ], + 'Vámosújfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2575496, 'lng' => 21.4524394], + ], + 'Varbó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1631678, 'lng' => 20.6217693], + ], + 'Varbóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4644075, 'lng' => 20.6450152], + ], + 'Vatta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9228447, 'lng' => 20.7389995], + ], + 'Vilmány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4166062, 'lng' => 21.2302229], + ], + 'Vilyvitány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4952223, 'lng' => 21.5589737], + ], + 'Viss' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2176861, 'lng' => 21.5069652], + ], + 'Viszló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4939386, 'lng' => 20.8862569], + ], + 'Vizsoly' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3845496, 'lng' => 21.2158416], + ], + 'Zádorfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3860789, 'lng' => 20.4852484], + ], + 'Zalkod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.1857296, 'lng' => 21.4592752], + ], + 'Zemplénagárd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.36024, 'lng' => 22.0709646], + ], + 'Ziliz' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2511796, 'lng' => 20.7922106], + ], + 'Zsujta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4997896, 'lng' => 21.2789138], + ], + 'Zubogy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3792388, 'lng' => 20.5758141], + ], + ], + 'Budapest' => [ + 'Budapest I. ker.' => [ + 'constituencies' => ['Budapest 01.'], + 'coordinates' => ['lat' => 47.4968219, 'lng' => 19.037458], + ], + 'Budapest II. ker.' => [ + 'constituencies' => ['Budapest 03.', 'Budapest 04.'], + 'coordinates' => ['lat' => 47.5393329, 'lng' => 18.986934], + ], + 'Budapest III. ker.' => [ + 'constituencies' => ['Budapest 04.', 'Budapest 10.'], + 'coordinates' => ['lat' => 47.5671768, 'lng' => 19.0368517], + ], + 'Budapest IV. ker.' => [ + 'constituencies' => ['Budapest 11.', 'Budapest 12.'], + 'coordinates' => ['lat' => 47.5648915, 'lng' => 19.0913149], + ], + 'Budapest V. ker.' => [ + 'constituencies' => ['Budapest 01.'], + 'coordinates' => ['lat' => 47.5002319, 'lng' => 19.0520181], + ], + 'Budapest VI. ker.' => [ + 'constituencies' => ['Budapest 05.'], + 'coordinates' => ['lat' => 47.509863, 'lng' => 19.0625813], + ], + 'Budapest VII. ker.' => [ + 'constituencies' => ['Budapest 05.'], + 'coordinates' => ['lat' => 47.5027289, 'lng' => 19.073376], + ], + 'Budapest VIII. ker.' => [ + 'constituencies' => ['Budapest 01.', 'Budapest 06.'], + 'coordinates' => ['lat' => 47.4894184, 'lng' => 19.070668], + ], + 'Budapest IX. ker.' => [ + 'constituencies' => ['Budapest 01.', 'Budapest 06.'], + 'coordinates' => ['lat' => 47.4649279, 'lng' => 19.0916229], + ], + 'Budapest X. ker.' => [ + 'constituencies' => ['Budapest 09.', 'Budapest 14.'], + 'coordinates' => ['lat' => 47.4820909, 'lng' => 19.1575028], + ], + 'Budapest XI. ker.' => [ + 'constituencies' => ['Budapest 02.', 'Budapest 18.'], + 'coordinates' => ['lat' => 47.4593099, 'lng' => 19.0187389], + ], + 'Budapest XII. ker.' => [ + 'constituencies' => ['Budapest 03.'], + 'coordinates' => ['lat' => 47.4991199, 'lng' => 18.990459], + ], + 'Budapest XIII. ker.' => [ + 'constituencies' => ['Budapest 11.', 'Budapest 07.'], + 'coordinates' => ['lat' => 47.5355105, 'lng' => 19.0709266], + ], + 'Budapest XIV. ker.' => [ + 'constituencies' => ['Budapest 08.', 'Budapest 13.'], + 'coordinates' => ['lat' => 47.5224569, 'lng' => 19.114709], + ], + 'Budapest XV. ker.' => [ + 'constituencies' => ['Budapest 12.'], + 'coordinates' => ['lat' => 47.5589, 'lng' => 19.1193], + ], + 'Budapest XVI. ker.' => [ + 'constituencies' => ['Budapest 13.'], + 'coordinates' => ['lat' => 47.5183029, 'lng' => 19.191941], + ], + 'Budapest XVII. ker.' => [ + 'constituencies' => ['Budapest 14.'], + 'coordinates' => ['lat' => 47.4803, 'lng' => 19.2667001], + ], + 'Budapest XVIII. ker.' => [ + 'constituencies' => ['Budapest 15.'], + 'coordinates' => ['lat' => 47.4281229, 'lng' => 19.2098429], + ], + 'Budapest XIX. ker.' => [ + 'constituencies' => ['Budapest 09.', 'Budapest 16.'], + 'coordinates' => ['lat' => 47.4457289, 'lng' => 19.1430149], + ], + 'Budapest XX. ker.' => [ + 'constituencies' => ['Budapest 16.'], + 'coordinates' => ['lat' => 47.4332879, 'lng' => 19.1193169], + ], + 'Budapest XXI. ker.' => [ + 'constituencies' => ['Budapest 17.'], + 'coordinates' => ['lat' => 47.4243579, 'lng' => 19.066142], + ], + 'Budapest XXII. ker.' => [ + 'constituencies' => ['Budapest 18.'], + 'coordinates' => ['lat' => 47.425, 'lng' => 19.031667], + ], + 'Budapest XXIII. ker.' => [ + 'constituencies' => ['Budapest 17.'], + 'coordinates' => ['lat' => 47.3939599, 'lng' => 19.122523], + ], + ], + 'Csongrád-Csanád' => [ + 'Algyő' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3329625, 'lng' => 20.207889], + ], + 'Ambrózfalva' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3501417, 'lng' => 20.7313995], + ], + 'Apátfalva' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.173317, 'lng' => 20.5800472], + ], + 'Árpádhalom' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6158286, 'lng' => 20.547733], + ], + 'Ásotthalom' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.1995983, 'lng' => 19.7833756], + ], + 'Baks' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5518708, 'lng' => 20.1064166], + ], + 'Balástya' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4261828, 'lng' => 20.004933], + ], + 'Bordány' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3194213, 'lng' => 19.9227063], + ], + 'Csanádalberti' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3267872, 'lng' => 20.7068631], + ], + 'Csanádpalota' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2407708, 'lng' => 20.7228873], + ], + 'Csanytelek' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6014883, 'lng' => 20.1114379], + ], + 'Csengele' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5411505, 'lng' => 19.8644533], + ], + 'Csongrád-Csanád' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7084264, 'lng' => 20.1436061], + ], + 'Derekegyház' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.580238, 'lng' => 20.3549845], + ], + 'Deszk' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2179603, 'lng' => 20.2404106], + ], + 'Dóc' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.437292, 'lng' => 20.1363129], + ], + 'Domaszék' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2466283, 'lng' => 19.9990365], + ], + 'Eperjes' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7076258, 'lng' => 20.5621489], + ], + 'Fábiánsebestyén' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6748615, 'lng' => 20.455037], + ], + 'Felgyő' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6616513, 'lng' => 20.1097394], + ], + 'Ferencszállás' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2158295, 'lng' => 20.3553359], + ], + 'Földeák' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3184223, 'lng' => 20.4929019], + ], + 'Forráskút' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3655956, 'lng' => 19.9089055], + ], + 'Hódmezővásárhely' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.4181262, 'lng' => 20.3300315], + ], + 'Királyhegyes' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2717114, 'lng' => 20.6126302], + ], + 'Kistelek' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4694781, 'lng' => 19.9804365], + ], + 'Kiszombor' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1856953, 'lng' => 20.4265486], + ], + 'Klárafalva' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.220953, 'lng' => 20.3255224], + ], + 'Kövegy' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2246141, 'lng' => 20.6840764], + ], + 'Kübekháza' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1500892, 'lng' => 20.276983], + ], + 'Magyarcsanád' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1698824, 'lng' => 20.6132706], + ], + 'Makó' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2219071, 'lng' => 20.4809265], + ], + 'Maroslele' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2698362, 'lng' => 20.3418589], + ], + 'Mártély' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.4682451, 'lng' => 20.2416146], + ], + 'Mindszent' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5227585, 'lng' => 20.1895798], + ], + 'Mórahalom' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2179218, 'lng' => 19.88372], + ], + 'Nagyér' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3703008, 'lng' => 20.729605], + ], + 'Nagylak' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1737713, 'lng' => 20.7111982], + ], + 'Nagymágocs' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5857132, 'lng' => 20.4833875], + ], + 'Nagytőke' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7552639, 'lng' => 20.2860999], + ], + 'Óföldeák' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2985957, 'lng' => 20.4369086], + ], + 'Ópusztaszer' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4957061, 'lng' => 20.0665358], + ], + 'Öttömös' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2808756, 'lng' => 19.6826038], + ], + 'Pitvaros' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3194853, 'lng' => 20.7385996], + ], + 'Pusztamérges' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3280134, 'lng' => 19.6849699], + ], + 'Pusztaszer' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5515959, 'lng' => 19.9870098], + ], + 'Röszke' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.1873773, 'lng' => 20.037455], + ], + 'Ruzsa' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2890678, 'lng' => 19.7481121], + ], + 'Sándorfalva' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.3635951, 'lng' => 20.1032227], + ], + 'Szatymaz' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.3426558, 'lng' => 20.0391941], + ], + 'Szeged' => [ + 'constituencies' => ['Csongrád-Csanád 2.', 'Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2530102, 'lng' => 20.1414253], + ], + 'Szegvár' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5816447, 'lng' => 20.2266415], + ], + 'Székkutas' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.5063976, 'lng' => 20.537673], + ], + 'Szentes' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.654789, 'lng' => 20.2637492], + ], + 'Tiszasziget' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1720458, 'lng' => 20.1618289], + ], + 'Tömörkény' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6166243, 'lng' => 20.0436896], + ], + 'Újszentiván' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1859286, 'lng' => 20.1835123], + ], + 'Üllés' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3355015, 'lng' => 19.8489644], + ], + 'Zákányszék' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2752726, 'lng' => 19.8883111], + ], + 'Zsombó' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3284014, 'lng' => 19.9766186], + ], + ], + 'Fejér' => [ + 'Aba' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0328193, 'lng' => 18.522359], + ], + 'Adony' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.119831, 'lng' => 18.8612469], + ], + 'Alap' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8075763, 'lng' => 18.684028], + ], + 'Alcsútdoboz' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4277067, 'lng' => 18.6030325], + ], + 'Alsószentiván' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7910573, 'lng' => 18.732161], + ], + 'Bakonycsernye' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.321719, 'lng' => 18.0907379], + ], + 'Bakonykúti' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2458464, 'lng' => 18.195769], + ], + 'Balinka' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3135736, 'lng' => 18.1907168], + ], + 'Baracs' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9049033, 'lng' => 18.8752931], + ], + 'Baracska' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2824737, 'lng' => 18.7598901], + ], + 'Beloiannisz' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.183143, 'lng' => 18.8245727], + ], + 'Besnyő' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.1892568, 'lng' => 18.7936832], + ], + 'Bicske' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4911792, 'lng' => 18.6370142], + ], + 'Bodajk' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3209663, 'lng' => 18.2339242], + ], + 'Bodmér' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4489857, 'lng' => 18.5383832], + ], + 'Cece' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7698199, 'lng' => 18.6336808], + ], + 'Csabdi' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.5229299, 'lng' => 18.6085371], + ], + 'Csákberény' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3506861, 'lng' => 18.3265064], + ], + 'Csákvár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3941468, 'lng' => 18.4602445], + ], + 'Csókakő' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3533961, 'lng' => 18.2693867], + ], + 'Csór' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2049913, 'lng' => 18.2557813], + ], + 'Csősz' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0382791, 'lng' => 18.414533], + ], + 'Daruszentmiklós' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.87194, 'lng' => 18.8568642], + ], + 'Dég' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8707664, 'lng' => 18.4445717], + ], + 'Dunaújváros' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 46.9619059, 'lng' => 18.9355227], + ], + 'Előszállás' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8276091, 'lng' => 18.8280627], + ], + 'Enying' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9326943, 'lng' => 18.2414807], + ], + 'Ercsi' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.2482238, 'lng' => 18.8912626], + ], + 'Etyek' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4467098, 'lng' => 18.751179], + ], + 'Fehérvárcsurgó' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2904264, 'lng' => 18.2645262], + ], + 'Felcsút' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4541851, 'lng' => 18.5865775], + ], + 'Füle' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0535367, 'lng' => 18.2480871], + ], + 'Gánt' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3902121, 'lng' => 18.387061], + ], + 'Gárdony' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.196537, 'lng' => 18.6115195], + ], + 'Gyúró' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3700577, 'lng' => 18.7384824], + ], + 'Hantos' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9943127, 'lng' => 18.6989263], + ], + 'Igar' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7757642, 'lng' => 18.5137348], + ], + 'Iszkaszentgyörgy' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2399338, 'lng' => 18.2987232], + ], + 'Isztimér' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2787058, 'lng' => 18.1955966], + ], + 'Iváncsa' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.153376, 'lng' => 18.8270434], + ], + 'Jenő' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1047531, 'lng' => 18.2453199], + ], + 'Kajászó' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3234883, 'lng' => 18.7221054], + ], + 'Káloz' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9568415, 'lng' => 18.4853961], + ], + 'Kápolnásnyék' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2398554, 'lng' => 18.6764288], + ], + 'Kincsesbánya' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2632477, 'lng' => 18.2764679], + ], + 'Kisapostag' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8940766, 'lng' => 18.9323135], + ], + 'Kisláng' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9598173, 'lng' => 18.3860884], + ], + 'Kőszárhegy' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0926048, 'lng' => 18.341234], + ], + 'Kulcs' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0541246, 'lng' => 18.9197178], + ], + 'Lajoskomárom' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.841585, 'lng' => 18.3355393], + ], + 'Lepsény' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9918514, 'lng' => 18.2469618], + ], + 'Lovasberény' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3109278, 'lng' => 18.5527924], + ], + 'Magyaralmás' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2913027, 'lng' => 18.3245512], + ], + 'Mány' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.5321762, 'lng' => 18.6555811], + ], + 'Martonvásár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3164516, 'lng' => 18.7877558], + ], + 'Mátyásdomb' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9228626, 'lng' => 18.3470929], + ], + 'Mezőfalva' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9323938, 'lng' => 18.7771045], + ], + 'Mezőkomárom' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8276482, 'lng' => 18.2934472], + ], + 'Mezőszentgyörgy' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9920267, 'lng' => 18.2795568], + ], + 'Mezőszilas' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8166957, 'lng' => 18.4754679], + ], + 'Moha' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2437717, 'lng' => 18.3313907], + ], + 'Mór' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.374928, 'lng' => 18.2036035], + ], + 'Nadap' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2585056, 'lng' => 18.6167437], + ], + 'Nádasdladány' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1341786, 'lng' => 18.2394077], + ], + 'Nagykarácsony' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8706425, 'lng' => 18.7725518], + ], + 'Nagylók' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9764964, 'lng' => 18.64115], + ], + 'Nagyveleg' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.361797, 'lng' => 18.111061], + ], + 'Nagyvenyim' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 46.9571015, 'lng' => 18.8576229], + ], + 'Óbarok' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4922397, 'lng' => 18.5681206], + ], + 'Pákozd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2172004, 'lng' => 18.5430768], + ], + 'Pátka' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2752462, 'lng' => 18.4950339], + ], + 'Pázmánd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.283645, 'lng' => 18.654854], + ], + 'Perkáta' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0482285, 'lng' => 18.784294], + ], + 'Polgárdi' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0601257, 'lng' => 18.2993645], + ], + 'Pusztaszabolcs' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.1408918, 'lng' => 18.7601638], + ], + 'Pusztavám' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.4297438, 'lng' => 18.2317401], + ], + 'Rácalmás' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0243223, 'lng' => 18.9350709], + ], + 'Ráckeresztúr' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2729155, 'lng' => 18.8330106], + ], + 'Sárbogárd' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.879104, 'lng' => 18.6213353], + ], + 'Sáregres' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.783236, 'lng' => 18.5935136], + ], + 'Sárkeresztes' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2517488, 'lng' => 18.3541822], + ], + 'Sárkeresztúr' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0025252, 'lng' => 18.5479461], + ], + 'Sárkeszi' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1582764, 'lng' => 18.284968], + ], + 'Sárosd' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0414738, 'lng' => 18.6488144], + ], + 'Sárszentágota' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9706742, 'lng' => 18.5634969], + ], + 'Sárszentmihály' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1537282, 'lng' => 18.3235014], + ], + 'Seregélyes' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.1100586, 'lng' => 18.5788431], + ], + 'Soponya' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0120427, 'lng' => 18.4543505], + ], + 'Söréd' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.322683, 'lng' => 18.280508], + ], + 'Sukoró' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2425436, 'lng' => 18.6022803], + ], + 'Szabadbattyán' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1175572, 'lng' => 18.3681061], + ], + 'Szabadegyháza' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0770131, 'lng' => 18.6912379], + ], + 'Szabadhídvég' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8210159, 'lng' => 18.2798938], + ], + 'Szár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4791911, 'lng' => 18.5158147], + ], + 'Székesfehérvár' => [ + 'constituencies' => ['Fejér 2.', 'Fejér 1.'], + 'coordinates' => ['lat' => 47.1860262, 'lng' => 18.4221358], + ], + 'Tabajd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4045316, 'lng' => 18.6302011], + ], + 'Tác' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0794264, 'lng' => 18.403381], + ], + 'Tordas' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3440943, 'lng' => 18.7483302], + ], + 'Újbarok' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4791337, 'lng' => 18.5585574], + ], + 'Úrhida' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1298384, 'lng' => 18.3321437], + ], + 'Vajta' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7227758, 'lng' => 18.6618091], + ], + 'Vál' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3624339, 'lng' => 18.6766737], + ], + 'Velence' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2300924, 'lng' => 18.6506424], + ], + 'Vereb' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.318485, 'lng' => 18.6197301], + ], + 'Vértesacsa' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3700218, 'lng' => 18.5792793], + ], + 'Vértesboglár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4291347, 'lng' => 18.5235823], + ], + 'Zámoly' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3168103, 'lng' => 18.408371], + ], + 'Zichyújfalu' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.1291991, 'lng' => 18.6692222], + ], + ], + 'Győr-Moson-Sopron' => [ + 'Abda' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6962149, 'lng' => 17.5445786], + ], + 'Acsalag' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.676095, 'lng' => 17.1977771], + ], + 'Ágfalva' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.688862, 'lng' => 16.5110233], + ], + 'Agyagosszergény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.608545, 'lng' => 16.9409912], + ], + 'Árpás' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5134127, 'lng' => 17.3931579], + ], + 'Ásványráró' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8287695, 'lng' => 17.499195], + ], + 'Babót' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5752269, 'lng' => 17.0758604], + ], + 'Bágyogszovát' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5866036, 'lng' => 17.3617273], + ], + 'Bakonygyirót' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4181388, 'lng' => 17.8055502], + ], + 'Bakonypéterd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4667076, 'lng' => 17.7967619], + ], + 'Bakonyszentlászló' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.3892006, 'lng' => 17.8032754], + ], + 'Barbacs' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6455476, 'lng' => 17.297216], + ], + 'Beled' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4662675, 'lng' => 17.0959263], + ], + 'Bezenye' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9609867, 'lng' => 17.216211], + ], + 'Bezi' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6737572, 'lng' => 17.3921093], + ], + 'Bodonhely' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5655752, 'lng' => 17.4072124], + ], + 'Bogyoszló' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5609657, 'lng' => 17.1850606], + ], + 'Bőny' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6516279, 'lng' => 17.8703841], + ], + 'Börcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6862052, 'lng' => 17.4988893], + ], + 'Bősárkány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6881947, 'lng' => 17.2507143], + ], + 'Cakóháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6967121, 'lng' => 17.2863758], + ], + 'Cirák' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4779219, 'lng' => 17.0282338], + ], + 'Csáfordjánosfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4151998, 'lng' => 16.9510595], + ], + 'Csapod' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5162077, 'lng' => 16.9234546], + ], + 'Csér' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4169765, 'lng' => 16.9330737], + ], + 'Csikvánd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4666335, 'lng' => 17.4546305], + ], + 'Csorna' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6103234, 'lng' => 17.2462444], + ], + 'Darnózseli' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8493957, 'lng' => 17.4273958], + ], + 'Dénesfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4558445, 'lng' => 17.0335351], + ], + 'Dör' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5979168, 'lng' => 17.2991911], + ], + 'Dunakiliti' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9659588, 'lng' => 17.2882641], + ], + 'Dunaremete' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8761957, 'lng' => 17.4375005], + ], + 'Dunaszeg' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7692554, 'lng' => 17.5407805], + ], + 'Dunaszentpál' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7771623, 'lng' => 17.5043978], + ], + 'Dunasziget' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9359671, 'lng' => 17.3617867], + ], + 'Ebergőc' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5635832, 'lng' => 16.81167], + ], + 'Écs' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5604415, 'lng' => 17.7072193], + ], + 'Edve' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4551126, 'lng' => 17.135508], + ], + 'Egyed' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5192845, 'lng' => 17.3396861], + ], + 'Egyházasfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.46243, 'lng' => 16.7679871], + ], + 'Enese' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6461219, 'lng' => 17.4235267], + ], + 'Farád' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6064483, 'lng' => 17.2003347], + ], + 'Fehértó' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6759514, 'lng' => 17.3453497], + ], + 'Feketeerdő' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9355702, 'lng' => 17.2783691], + ], + 'Felpéc' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5225976, 'lng' => 17.5993517], + ], + 'Fenyőfő' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.3490387, 'lng' => 17.7656259], + ], + 'Fertőboz' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.633426, 'lng' => 16.6998899], + ], + 'Fertőd' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.61818, 'lng' => 16.8741418], + ], + 'Fertőendréd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6054618, 'lng' => 16.9085891], + ], + 'Fertőhomok' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6196363, 'lng' => 16.7710445], + ], + 'Fertőrákos' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.7209654, 'lng' => 16.6488128], + ], + 'Fertőszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5895578, 'lng' => 16.8730712], + ], + 'Fertőszéplak' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6172442, 'lng' => 16.8405708], + ], + 'Gönyű' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.7334344, 'lng' => 17.8243403], + ], + 'Gyalóka' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4427372, 'lng' => 16.696223], + ], + 'Gyarmat' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4604024, 'lng' => 17.4964917], + ], + 'Gyömöre' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4982876, 'lng' => 17.564804], + ], + 'Győr' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.', 'Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.6874569, 'lng' => 17.6503974], + ], + 'Győrasszonyfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4950098, 'lng' => 17.8072327], + ], + 'Győrladamér' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7545651, 'lng' => 17.5633004], + ], + 'Gyóró' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4916519, 'lng' => 17.0236667], + ], + 'Győrság' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5751529, 'lng' => 17.7515893], + ], + 'Győrsövényház' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6909394, 'lng' => 17.3734235], + ], + 'Győrszemere' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.551813, 'lng' => 17.5635661], + ], + 'Győrújbarát' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6076284, 'lng' => 17.6389745], + ], + 'Győrújfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.722197, 'lng' => 17.6054524], + ], + 'Győrzámoly' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7434268, 'lng' => 17.5770199], + ], + 'Halászi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8903231, 'lng' => 17.3256673], + ], + 'Harka' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6339566, 'lng' => 16.5986264], + ], + 'Hédervár' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.831062, 'lng' => 17.4541026], + ], + 'Hegyeshalom' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9117445, 'lng' => 17.156071], + ], + 'Hegykő' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6188466, 'lng' => 16.7940292], + ], + 'Hidegség' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6253847, 'lng' => 16.740935], + ], + 'Himod' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5200248, 'lng' => 17.0064434], + ], + 'Hövej' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5524954, 'lng' => 17.0166402], + ], + 'Ikrény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6539897, 'lng' => 17.5281764], + ], + 'Iván' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.445549, 'lng' => 16.9096056], + ], + 'Jánossomorja' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7847917, 'lng' => 17.1298642], + ], + 'Jobaháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5799316, 'lng' => 17.1886952], + ], + 'Kajárpéc' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4888221, 'lng' => 17.6350057], + ], + 'Kapuvár' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5912437, 'lng' => 17.0301952], + ], + 'Károlyháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8032696, 'lng' => 17.3446363], + ], + 'Kimle' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8172115, 'lng' => 17.3676625], + ], + 'Kisbabot' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5551791, 'lng' => 17.4149558], + ], + 'Kisbajcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7450615, 'lng' => 17.6800942], + ], + 'Kisbodak' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8963234, 'lng' => 17.4196192], + ], + 'Kisfalud' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.2041959, 'lng' => 18.494568], + ], + 'Kóny' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6307264, 'lng' => 17.3596093], + ], + 'Kópháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6385359, 'lng' => 16.6451629], + ], + 'Koroncó' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5999604, 'lng' => 17.5284792], + ], + 'Kunsziget' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7385858, 'lng' => 17.5176565], + ], + 'Lázi' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4661979, 'lng' => 17.8346909], + ], + 'Lébény' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7360651, 'lng' => 17.3905652], + ], + 'Levél' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8949275, 'lng' => 17.2001946], + ], + 'Lipót' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8615868, 'lng' => 17.4603528], + ], + 'Lövő' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5107966, 'lng' => 16.7898395], + ], + 'Maglóca' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6625685, 'lng' => 17.2751221], + ], + 'Magyarkeresztúr' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5200063, 'lng' => 17.1660121], + ], + 'Máriakálnok' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8596905, 'lng' => 17.3237666], + ], + 'Markotabödöge' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6815136, 'lng' => 17.3116772], + ], + 'Mecsér' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.796671, 'lng' => 17.4744842], + ], + 'Mérges' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6012809, 'lng' => 17.4438455], + ], + 'Mezőörs' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.568844, 'lng' => 17.8821253], + ], + 'Mihályi' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5142703, 'lng' => 17.0958265], + ], + 'Mórichida' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5127896, 'lng' => 17.4218174], + ], + 'Mosonmagyaróvár' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8681469, 'lng' => 17.2689169], + ], + 'Mosonszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7294576, 'lng' => 17.4242231], + ], + 'Mosonszolnok' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8511108, 'lng' => 17.1735793], + ], + 'Mosonudvar' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8435379, 'lng' => 17.224348], + ], + 'Nagybajcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7639168, 'lng' => 17.686613], + ], + 'Nagycenk' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6081549, 'lng' => 16.6979223], + ], + 'Nagylózs' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5654858, 'lng' => 16.76965], + ], + 'Nagyszentjános' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.7100868, 'lng' => 17.8681808], + ], + 'Nemeskér' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.483855, 'lng' => 16.8050771], + ], + 'Nyalka' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5443407, 'lng' => 17.8091081], + ], + 'Nyúl' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5832389, 'lng' => 17.6862095], + ], + 'Osli' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6385609, 'lng' => 17.0755158], + ], + 'Öttevény' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7255506, 'lng' => 17.4899552], + ], + 'Páli' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4774264, 'lng' => 17.1695082], + ], + 'Pannonhalma' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.549497, 'lng' => 17.7552412], + ], + 'Pásztori' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5553919, 'lng' => 17.2696728], + ], + 'Pázmándfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5710798, 'lng' => 17.7810865], + ], + 'Pér' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6111604, 'lng' => 17.8049747], + ], + 'Pereszteg' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.594289, 'lng' => 16.7354028], + ], + 'Petőháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5965785, 'lng' => 16.8954138], + ], + 'Pinnye' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5855193, 'lng' => 16.7706082], + ], + 'Potyond' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.549377, 'lng' => 17.1821874], + ], + 'Püski' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8846385, 'lng' => 17.4070152], + ], + 'Pusztacsalád' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4853081, 'lng' => 16.9013644], + ], + 'Rábacsanak' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5256113, 'lng' => 17.2902872], + ], + 'Rábacsécsény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5879598, 'lng' => 17.4227941], + ], + 'Rábakecöl' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4324946, 'lng' => 17.1126349], + ], + 'Rábapatona' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6314656, 'lng' => 17.4797584], + ], + 'Rábapordány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5574649, 'lng' => 17.3262502], + ], + 'Rábasebes' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4392738, 'lng' => 17.2423807], + ], + 'Rábaszentandrás' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4596327, 'lng' => 17.3272097], + ], + 'Rábaszentmihály' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5775103, 'lng' => 17.4312379], + ], + 'Rábaszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5381909, 'lng' => 17.417513], + ], + 'Rábatamási' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5893387, 'lng' => 17.1699767], + ], + 'Rábcakapi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7079835, 'lng' => 17.2755839], + ], + 'Rajka' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9977901, 'lng' => 17.1983996], + ], + 'Ravazd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5162349, 'lng' => 17.7512699], + ], + 'Répceszemere' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4282026, 'lng' => 16.9738943], + ], + 'Répcevis' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4427966, 'lng' => 16.6731972], + ], + 'Rétalap' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6072246, 'lng' => 17.9071507], + ], + 'Röjtökmuzsaj' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5543502, 'lng' => 16.8363467], + ], + 'Románd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4484049, 'lng' => 17.7909987], + ], + 'Sarród' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6315873, 'lng' => 16.8613408], + ], + 'Sikátor' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4370828, 'lng' => 17.8510581], + ], + 'Sobor' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4768368, 'lng' => 17.3752902], + ], + 'Sokorópátka' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4892381, 'lng' => 17.6953943], + ], + 'Sopron' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6816619, 'lng' => 16.5844795], + ], + 'Sopronhorpács' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4831854, 'lng' => 16.7359058], + ], + 'Sopronkövesd' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5460504, 'lng' => 16.7432859], + ], + 'Sopronnémeti' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5364397, 'lng' => 17.2070182], + ], + 'Szakony' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4262848, 'lng' => 16.7154462], + ], + 'Szany' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4620733, 'lng' => 17.3027671], + ], + 'Szárföld' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5933239, 'lng' => 17.1221243], + ], + 'Szerecseny' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4628425, 'lng' => 17.5536197], + ], + 'Szil' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.501622, 'lng' => 17.233297], + ], + 'Szilsárkány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5396552, 'lng' => 17.2545808], + ], + 'Táp' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5168299, 'lng' => 17.8292989], + ], + 'Tápszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4930151, 'lng' => 17.8524913], + ], + 'Tarjánpuszta' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5062161, 'lng' => 17.7869857], + ], + 'Tárnokréti' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.7217546, 'lng' => 17.3078226], + ], + 'Tényő' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5407376, 'lng' => 17.6490009], + ], + 'Tét' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5198967, 'lng' => 17.5108553], + ], + 'Töltéstava' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6273335, 'lng' => 17.7343778], + ], + 'Újkér' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4573295, 'lng' => 16.8187647], + ], + 'Újrónafő' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8101728, 'lng' => 17.2015241], + ], + 'Und' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.488856, 'lng' => 16.6961552], + ], + 'Vadosfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4986805, 'lng' => 17.1287654], + ], + 'Vág' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4469264, 'lng' => 17.2121765], + ], + 'Vámosszabadi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7571476, 'lng' => 17.6507532], + ], + 'Várbalog' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8347267, 'lng' => 17.0720923], + ], + 'Vásárosfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4537986, 'lng' => 17.1158473], + ], + 'Vének' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7392272, 'lng' => 17.7556608], + ], + 'Veszkény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5969056, 'lng' => 17.0891913], + ], + 'Veszprémvarsány' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4290248, 'lng' => 17.8287245], + ], + 'Vitnyéd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5863882, 'lng' => 16.9832151], + ], + 'Völcsej' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.496503, 'lng' => 16.7604595], + ], + 'Zsebeháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.511293, 'lng' => 17.191017], + ], + 'Zsira' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4580482, 'lng' => 16.6766466], + ], + ], + 'Hajdú-Bihar' => [ + 'Álmosd' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4167788, 'lng' => 21.9806107], + ], + 'Ártánd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1241958, 'lng' => 21.7568167], + ], + 'Bagamér' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4498231, 'lng' => 21.9942012], + ], + 'Bakonszeg' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1900613, 'lng' => 21.4442102], + ], + 'Balmazújváros' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.6145296, 'lng' => 21.3417333], + ], + 'Báránd' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2936964, 'lng' => 21.2288584], + ], + 'Bedő' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1634194, 'lng' => 21.7502785], + ], + 'Berekböszörmény' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0615952, 'lng' => 21.6782301], + ], + 'Berettyóújfalu' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2196438, 'lng' => 21.5362812], + ], + 'Bihardancsháza' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2291246, 'lng' => 21.3159659], + ], + 'Biharkeresztes' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1301236, 'lng' => 21.7219423], + ], + 'Biharnagybajom' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2108104, 'lng' => 21.2302309], + ], + 'Bihartorda' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.215994, 'lng' => 21.3526252], + ], + 'Bocskaikert' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6435949, 'lng' => 21.659878], + ], + 'Bojt' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1927968, 'lng' => 21.7327485], + ], + 'Csökmő' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0315111, 'lng' => 21.2892817], + ], + 'Darvas' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1017037, 'lng' => 21.3374554], + ], + 'Debrecen' => [ + 'constituencies' => ['Hajdú-Bihar 3.', 'Hajdú-Bihar 1.', 'Hajdú-Bihar 2.'], + 'coordinates' => ['lat' => 47.5316049, 'lng' => 21.6273124], + ], + 'Derecske' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3533886, 'lng' => 21.5658524], + ], + 'Ebes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4709086, 'lng' => 21.490457], + ], + 'Egyek' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.6258313, 'lng' => 20.8907463], + ], + 'Esztár' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2837051, 'lng' => 21.7744117], + ], + 'Földes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2896801, 'lng' => 21.3633025], + ], + 'Folyás' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8086696, 'lng' => 21.1371809], + ], + 'Fülöp' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.5981409, 'lng' => 22.0546557], + ], + 'Furta' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1300357, 'lng' => 21.460144], + ], + 'Gáborján' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2360716, 'lng' => 21.6622765], + ], + 'Görbeháza' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8200025, 'lng' => 21.2359976], + ], + 'Hajdúbagos' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3947066, 'lng' => 21.6643329], + ], + 'Hajdúböszörmény' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.6718908, 'lng' => 21.5126637], + ], + 'Hajdúdorog' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8166047, 'lng' => 21.4980694], + ], + 'Hajdúhadház' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6802292, 'lng' => 21.6675179], + ], + 'Hajdúnánás' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.843004, 'lng' => 21.4242691], + ], + 'Hajdúsámson' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6049148, 'lng' => 21.7597325], + ], + 'Hajdúszoboszló' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4435369, 'lng' => 21.3965516], + ], + 'Hajdúszovát' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3903463, 'lng' => 21.4764161], + ], + 'Hencida' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2507004, 'lng' => 21.6989732], + ], + 'Hortobágy' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.5868751, 'lng' => 21.1560332], + ], + 'Hosszúpályi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3947673, 'lng' => 21.7346539], + ], + 'Kaba' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3565391, 'lng' => 21.2726765], + ], + 'Kismarja' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2463277, 'lng' => 21.8214627], + ], + 'Kokad' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4054409, 'lng' => 21.9336174], + ], + 'Komádi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0055271, 'lng' => 21.4944772], + ], + 'Konyár' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3213954, 'lng' => 21.6691634], + ], + 'Körösszakál' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0178012, 'lng' => 21.5932398], + ], + 'Körösszegapáti' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0396539, 'lng' => 21.6317831], + ], + 'Létavértes' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.3835171, 'lng' => 21.8798767], + ], + 'Magyarhomorog' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0222187, 'lng' => 21.5480518], + ], + 'Mezőpeterd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.165025, 'lng' => 21.6200633], + ], + 'Mezősas' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1104156, 'lng' => 21.5671344], + ], + 'Mikepércs' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.4406335, 'lng' => 21.6366773], + ], + 'Monostorpályi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3984198, 'lng' => 21.7764527], + ], + 'Nádudvar' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4259381, 'lng' => 21.1616779], + ], + 'Nagyhegyes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.539228, 'lng' => 21.345552], + ], + 'Nagykereki' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1863168, 'lng' => 21.7922805], + ], + 'Nagyrábé' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2043078, 'lng' => 21.3306582], + ], + 'Nyírábrány' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.541423, 'lng' => 22.0128317], + ], + 'Nyíracsád' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6039774, 'lng' => 21.9715154], + ], + 'Nyíradony' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6899404, 'lng' => 21.9085991], + ], + 'Nyírmártonfalva' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.5862503, 'lng' => 21.8964914], + ], + 'Pocsaj' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2851817, 'lng' => 21.8122198], + ], + 'Polgár' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8679381, 'lng' => 21.1141038], + ], + 'Püspökladány' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3216529, 'lng' => 21.1185953], + ], + 'Sáp' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2549739, 'lng' => 21.3555868], + ], + 'Sáránd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.4062312, 'lng' => 21.6290631], + ], + 'Sárrétudvari' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2406806, 'lng' => 21.1866058], + ], + 'Szentpéterszeg' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2386719, 'lng' => 21.6178971], + ], + 'Szerep' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2278774, 'lng' => 21.1407795], + ], + 'Téglás' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.7109686, 'lng' => 21.6727776], + ], + 'Tépe' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.32046, 'lng' => 21.5714076], + ], + 'Tetétlen' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3148595, 'lng' => 21.3069162], + ], + 'Tiszacsege' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.6997085, 'lng' => 20.9917041], + ], + 'Tiszagyulaháza' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.942524, 'lng' => 21.1428152], + ], + 'Told' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1180165, 'lng' => 21.6413048], + ], + 'Újiráz' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 46.9870862, 'lng' => 21.3556353], + ], + 'Újléta' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4650261, 'lng' => 21.8733489], + ], + 'Újszentmargita' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.7266767, 'lng' => 21.1047788], + ], + 'Újtikos' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.9176202, 'lng' => 21.171571], + ], + 'Vámospércs' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.525345, 'lng' => 21.8992474], + ], + 'Váncsod' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2011182, 'lng' => 21.6400459], + ], + 'Vekerd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0959975, 'lng' => 21.4017741], + ], + 'Zsáka' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1340418, 'lng' => 21.4307824], + ], + ], + 'Heves' => [ + 'Abasár' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7989023, 'lng' => 20.0036779], + ], + 'Adács' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6922284, 'lng' => 19.9779484], + ], + 'Aldebrő' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7891428, 'lng' => 20.2302555], + ], + 'Andornaktálya' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8499325, 'lng' => 20.4105243], + ], + 'Apc' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7933298, 'lng' => 19.6955737], + ], + 'Átány' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.6156875, 'lng' => 20.3620368], + ], + 'Atkár' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7209651, 'lng' => 19.8912361], + ], + 'Balaton' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 46.8302679, 'lng' => 17.7340438], + ], + 'Bátor' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.99076, 'lng' => 20.2627351], + ], + 'Bekölce' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0804457, 'lng' => 20.268156], + ], + 'Bélapátfalva' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0578657, 'lng' => 20.3500536], + ], + 'Besenyőtelek' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.6994693, 'lng' => 20.4300342], + ], + 'Boconád' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6414895, 'lng' => 20.1877312], + ], + 'Bodony' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9420912, 'lng' => 20.0199927], + ], + 'Boldog' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6031287, 'lng' => 19.687521], + ], + 'Bükkszék' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9915393, 'lng' => 20.1765126], + ], + 'Bükkszenterzsébet' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0532811, 'lng' => 20.1622924], + ], + 'Bükkszentmárton' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0715382, 'lng' => 20.3310312], + ], + 'Csány' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6474142, 'lng' => 19.8259607], + ], + 'Demjén' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8317294, 'lng' => 20.3313872], + ], + 'Detk' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7489442, 'lng' => 20.0983332], + ], + 'Domoszló' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8288666, 'lng' => 20.1172988], + ], + 'Dormánd' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7203119, 'lng' => 20.4174779], + ], + 'Ecséd' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7307237, 'lng' => 19.7684767], + ], + 'Eger' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9025348, 'lng' => 20.3772284], + ], + 'Egerbakta' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9341404, 'lng' => 20.2918134], + ], + 'Egerbocs' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0263467, 'lng' => 20.2598999], + ], + 'Egercsehi' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0545478, 'lng' => 20.261522], + ], + 'Egerfarmos' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7177802, 'lng' => 20.5358914], + ], + 'Egerszalók' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8702275, 'lng' => 20.3241673], + ], + 'Egerszólát' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8902473, 'lng' => 20.2669774], + ], + 'Erdőkövesd' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0391241, 'lng' => 20.1013656], + ], + 'Erdőtelek' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6852656, 'lng' => 20.3115369], + ], + 'Erk' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6101796, 'lng' => 20.076668], + ], + 'Fedémes' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0320282, 'lng' => 20.1878653], + ], + 'Feldebrő' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8128253, 'lng' => 20.2363322], + ], + 'Felsőtárkány' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9734513, 'lng' => 20.41906], + ], + 'Füzesabony' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7495339, 'lng' => 20.4150668], + ], + 'Gyöngyös' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7772651, 'lng' => 19.9294927], + ], + 'Gyöngyöshalász' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7413068, 'lng' => 19.9227242], + ], + 'Gyöngyösoroszi' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8263987, 'lng' => 19.8928817], + ], + 'Gyöngyöspata' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8140904, 'lng' => 19.7923335], + ], + 'Gyöngyössolymos' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8160489, 'lng' => 19.9338831], + ], + 'Gyöngyöstarján' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8132903, 'lng' => 19.8664265], + ], + 'Halmajugra' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7634173, 'lng' => 20.0523104], + ], + 'Hatvan' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6656965, 'lng' => 19.676666], + ], + 'Heréd' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7081485, 'lng' => 19.6327042], + ], + 'Heves' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.5971694, 'lng' => 20.280156], + ], + 'Hevesaranyos' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0109153, 'lng' => 20.2342809], + ], + 'Hevesvezekény' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.5570546, 'lng' => 20.3580453], + ], + 'Hort' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6890439, 'lng' => 19.7842632], + ], + 'Istenmezeje' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0845673, 'lng' => 20.0515347], + ], + 'Ivád' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0203013, 'lng' => 20.0612654], + ], + 'Kál' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7318239, 'lng' => 20.2608866], + ], + 'Kápolna' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7584202, 'lng' => 20.2459749], + ], + 'Karácsond' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7282318, 'lng' => 20.0282488], + ], + 'Kerecsend' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7947277, 'lng' => 20.3444695], + ], + 'Kerekharaszt' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6623104, 'lng' => 19.6253721], + ], + 'Kisfüzes' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9881653, 'lng' => 20.1267373], + ], + 'Kisköre' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.4984608, 'lng' => 20.4973609], + ], + 'Kisnána' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8506469, 'lng' => 20.1457821], + ], + 'Kömlő' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 46.1929788, 'lng' => 18.2512139], + ], + 'Kompolt' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7415463, 'lng' => 20.2406377], + ], + 'Lőrinci' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7390261, 'lng' => 19.6756557], + ], + 'Ludas' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7300788, 'lng' => 20.0910629], + ], + 'Maklár' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8054074, 'lng' => 20.410901], + ], + 'Markaz' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8222206, 'lng' => 20.0582311], + ], + 'Mátraballa' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9843833, 'lng' => 20.0225017], + ], + + ], + ]; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8147.php b/tests/PHPStan/Analyser/data/bug-8147.php new file mode 100644 index 0000000000..a30cacf9a1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8147.php @@ -0,0 +1,4021 @@ + */ + private const HUGE_MAP = [ + 'value0001' => 1, + 'value0002' => 2, + 'value0003' => 3, + 'value0004' => 4, + 'value0005' => 5, + 'value0006' => 6, + 'value0007' => 7, + 'value0008' => 8, + 'value0009' => 9, + 'value0010' => 10, + 'value0011' => 11, + 'value0012' => 12, + 'value0013' => 13, + 'value0014' => 14, + 'value0015' => 15, + 'value0016' => 16, + 'value0017' => 17, + 'value0018' => 18, + 'value0019' => 19, + 'value0020' => 20, + 'value0021' => 21, + 'value0022' => 22, + 'value0023' => 23, + 'value0024' => 24, + 'value0025' => 25, + 'value0026' => 26, + 'value0027' => 27, + 'value0028' => 28, + 'value0029' => 29, + 'value0030' => 30, + 'value0031' => 31, + 'value0032' => 32, + 'value0033' => 33, + 'value0034' => 34, + 'value0035' => 35, + 'value0036' => 36, + 'value0037' => 37, + 'value0038' => 38, + 'value0039' => 39, + 'value0040' => 40, + 'value0041' => 41, + 'value0042' => 42, + 'value0043' => 43, + 'value0044' => 44, + 'value0045' => 45, + 'value0046' => 46, + 'value0047' => 47, + 'value0048' => 48, + 'value0049' => 49, + 'value0050' => 50, + 'value0051' => 51, + 'value0052' => 52, + 'value0053' => 53, + 'value0054' => 54, + 'value0055' => 55, + 'value0056' => 56, + 'value0057' => 57, + 'value0058' => 58, + 'value0059' => 59, + 'value0060' => 60, + 'value0061' => 61, + 'value0062' => 62, + 'value0063' => 63, + 'value0064' => 64, + 'value0065' => 65, + 'value0066' => 66, + 'value0067' => 67, + 'value0068' => 68, + 'value0069' => 69, + 'value0070' => 70, + 'value0071' => 71, + 'value0072' => 72, + 'value0073' => 73, + 'value0074' => 74, + 'value0075' => 75, + 'value0076' => 76, + 'value0077' => 77, + 'value0078' => 78, + 'value0079' => 79, + 'value0080' => 80, + 'value0081' => 81, + 'value0082' => 82, + 'value0083' => 83, + 'value0084' => 84, + 'value0085' => 85, + 'value0086' => 86, + 'value0087' => 87, + 'value0088' => 88, + 'value0089' => 89, + 'value0090' => 90, + 'value0091' => 91, + 'value0092' => 92, + 'value0093' => 93, + 'value0094' => 94, + 'value0095' => 95, + 'value0096' => 96, + 'value0097' => 97, + 'value0098' => 98, + 'value0099' => 99, + 'value0100' => 100, + 'value0101' => 101, + 'value0102' => 102, + 'value0103' => 103, + 'value0104' => 104, + 'value0105' => 105, + 'value0106' => 106, + 'value0107' => 107, + 'value0108' => 108, + 'value0109' => 109, + 'value0110' => 110, + 'value0111' => 111, + 'value0112' => 112, + 'value0113' => 113, + 'value0114' => 114, + 'value0115' => 115, + 'value0116' => 116, + 'value0117' => 117, + 'value0118' => 118, + 'value0119' => 119, + 'value0120' => 120, + 'value0121' => 121, + 'value0122' => 122, + 'value0123' => 123, + 'value0124' => 124, + 'value0125' => 125, + 'value0126' => 126, + 'value0127' => 127, + 'value0128' => 128, + 'value0129' => 129, + 'value0130' => 130, + 'value0131' => 131, + 'value0132' => 132, + 'value0133' => 133, + 'value0134' => 134, + 'value0135' => 135, + 'value0136' => 136, + 'value0137' => 137, + 'value0138' => 138, + 'value0139' => 139, + 'value0140' => 140, + 'value0141' => 141, + 'value0142' => 142, + 'value0143' => 143, + 'value0144' => 144, + 'value0145' => 145, + 'value0146' => 146, + 'value0147' => 147, + 'value0148' => 148, + 'value0149' => 149, + 'value0150' => 150, + 'value0151' => 151, + 'value0152' => 152, + 'value0153' => 153, + 'value0154' => 154, + 'value0155' => 155, + 'value0156' => 156, + 'value0157' => 157, + 'value0158' => 158, + 'value0159' => 159, + 'value0160' => 160, + 'value0161' => 161, + 'value0162' => 162, + 'value0163' => 163, + 'value0164' => 164, + 'value0165' => 165, + 'value0166' => 166, + 'value0167' => 167, + 'value0168' => 168, + 'value0169' => 169, + 'value0170' => 170, + 'value0171' => 171, + 'value0172' => 172, + 'value0173' => 173, + 'value0174' => 174, + 'value0175' => 175, + 'value0176' => 176, + 'value0177' => 177, + 'value0178' => 178, + 'value0179' => 179, + 'value0180' => 180, + 'value0181' => 181, + 'value0182' => 182, + 'value0183' => 183, + 'value0184' => 184, + 'value0185' => 185, + 'value0186' => 186, + 'value0187' => 187, + 'value0188' => 188, + 'value0189' => 189, + 'value0190' => 190, + 'value0191' => 191, + 'value0192' => 192, + 'value0193' => 193, + 'value0194' => 194, + 'value0195' => 195, + 'value0196' => 196, + 'value0197' => 197, + 'value0198' => 198, + 'value0199' => 199, + 'value0200' => 200, + 'value0201' => 201, + 'value0202' => 202, + 'value0203' => 203, + 'value0204' => 204, + 'value0205' => 205, + 'value0206' => 206, + 'value0207' => 207, + 'value0208' => 208, + 'value0209' => 209, + 'value0210' => 210, + 'value0211' => 211, + 'value0212' => 212, + 'value0213' => 213, + 'value0214' => 214, + 'value0215' => 215, + 'value0216' => 216, + 'value0217' => 217, + 'value0218' => 218, + 'value0219' => 219, + 'value0220' => 220, + 'value0221' => 221, + 'value0222' => 222, + 'value0223' => 223, + 'value0224' => 224, + 'value0225' => 225, + 'value0226' => 226, + 'value0227' => 227, + 'value0228' => 228, + 'value0229' => 229, + 'value0230' => 230, + 'value0231' => 231, + 'value0232' => 232, + 'value0233' => 233, + 'value0234' => 234, + 'value0235' => 235, + 'value0236' => 236, + 'value0237' => 237, + 'value0238' => 238, + 'value0239' => 239, + 'value0240' => 240, + 'value0241' => 241, + 'value0242' => 242, + 'value0243' => 243, + 'value0244' => 244, + 'value0245' => 245, + 'value0246' => 246, + 'value0247' => 247, + 'value0248' => 248, + 'value0249' => 249, + 'value0250' => 250, + 'value0251' => 251, + 'value0252' => 252, + 'value0253' => 253, + 'value0254' => 254, + 'value0255' => 255, + 'value0256' => 256, + 'value0257' => 257, + 'value0258' => 258, + 'value0259' => 259, + 'value0260' => 260, + 'value0261' => 261, + 'value0262' => 262, + 'value0263' => 263, + 'value0264' => 264, + 'value0265' => 265, + 'value0266' => 266, + 'value0267' => 267, + 'value0268' => 268, + 'value0269' => 269, + 'value0270' => 270, + 'value0271' => 271, + 'value0272' => 272, + 'value0273' => 273, + 'value0274' => 274, + 'value0275' => 275, + 'value0276' => 276, + 'value0277' => 277, + 'value0278' => 278, + 'value0279' => 279, + 'value0280' => 280, + 'value0281' => 281, + 'value0282' => 282, + 'value0283' => 283, + 'value0284' => 284, + 'value0285' => 285, + 'value0286' => 286, + 'value0287' => 287, + 'value0288' => 288, + 'value0289' => 289, + 'value0290' => 290, + 'value0291' => 291, + 'value0292' => 292, + 'value0293' => 293, + 'value0294' => 294, + 'value0295' => 295, + 'value0296' => 296, + 'value0297' => 297, + 'value0298' => 298, + 'value0299' => 299, + 'value0300' => 300, + 'value0301' => 301, + 'value0302' => 302, + 'value0303' => 303, + 'value0304' => 304, + 'value0305' => 305, + 'value0306' => 306, + 'value0307' => 307, + 'value0308' => 308, + 'value0309' => 309, + 'value0310' => 310, + 'value0311' => 311, + 'value0312' => 312, + 'value0313' => 313, + 'value0314' => 314, + 'value0315' => 315, + 'value0316' => 316, + 'value0317' => 317, + 'value0318' => 318, + 'value0319' => 319, + 'value0320' => 320, + 'value0321' => 321, + 'value0322' => 322, + 'value0323' => 323, + 'value0324' => 324, + 'value0325' => 325, + 'value0326' => 326, + 'value0327' => 327, + 'value0328' => 328, + 'value0329' => 329, + 'value0330' => 330, + 'value0331' => 331, + 'value0332' => 332, + 'value0333' => 333, + 'value0334' => 334, + 'value0335' => 335, + 'value0336' => 336, + 'value0337' => 337, + 'value0338' => 338, + 'value0339' => 339, + 'value0340' => 340, + 'value0341' => 341, + 'value0342' => 342, + 'value0343' => 343, + 'value0344' => 344, + 'value0345' => 345, + 'value0346' => 346, + 'value0347' => 347, + 'value0348' => 348, + 'value0349' => 349, + 'value0350' => 350, + 'value0351' => 351, + 'value0352' => 352, + 'value0353' => 353, + 'value0354' => 354, + 'value0355' => 355, + 'value0356' => 356, + 'value0357' => 357, + 'value0358' => 358, + 'value0359' => 359, + 'value0360' => 360, + 'value0361' => 361, + 'value0362' => 362, + 'value0363' => 363, + 'value0364' => 364, + 'value0365' => 365, + 'value0366' => 366, + 'value0367' => 367, + 'value0368' => 368, + 'value0369' => 369, + 'value0370' => 370, + 'value0371' => 371, + 'value0372' => 372, + 'value0373' => 373, + 'value0374' => 374, + 'value0375' => 375, + 'value0376' => 376, + 'value0377' => 377, + 'value0378' => 378, + 'value0379' => 379, + 'value0380' => 380, + 'value0381' => 381, + 'value0382' => 382, + 'value0383' => 383, + 'value0384' => 384, + 'value0385' => 385, + 'value0386' => 386, + 'value0387' => 387, + 'value0388' => 388, + 'value0389' => 389, + 'value0390' => 390, + 'value0391' => 391, + 'value0392' => 392, + 'value0393' => 393, + 'value0394' => 394, + 'value0395' => 395, + 'value0396' => 396, + 'value0397' => 397, + 'value0398' => 398, + 'value0399' => 399, + 'value0400' => 400, + 'value0401' => 401, + 'value0402' => 402, + 'value0403' => 403, + 'value0404' => 404, + 'value0405' => 405, + 'value0406' => 406, + 'value0407' => 407, + 'value0408' => 408, + 'value0409' => 409, + 'value0410' => 410, + 'value0411' => 411, + 'value0412' => 412, + 'value0413' => 413, + 'value0414' => 414, + 'value0415' => 415, + 'value0416' => 416, + 'value0417' => 417, + 'value0418' => 418, + 'value0419' => 419, + 'value0420' => 420, + 'value0421' => 421, + 'value0422' => 422, + 'value0423' => 423, + 'value0424' => 424, + 'value0425' => 425, + 'value0426' => 426, + 'value0427' => 427, + 'value0428' => 428, + 'value0429' => 429, + 'value0430' => 430, + 'value0431' => 431, + 'value0432' => 432, + 'value0433' => 433, + 'value0434' => 434, + 'value0435' => 435, + 'value0436' => 436, + 'value0437' => 437, + 'value0438' => 438, + 'value0439' => 439, + 'value0440' => 440, + 'value0441' => 441, + 'value0442' => 442, + 'value0443' => 443, + 'value0444' => 444, + 'value0445' => 445, + 'value0446' => 446, + 'value0447' => 447, + 'value0448' => 448, + 'value0449' => 449, + 'value0450' => 450, + 'value0451' => 451, + 'value0452' => 452, + 'value0453' => 453, + 'value0454' => 454, + 'value0455' => 455, + 'value0456' => 456, + 'value0457' => 457, + 'value0458' => 458, + 'value0459' => 459, + 'value0460' => 460, + 'value0461' => 461, + 'value0462' => 462, + 'value0463' => 463, + 'value0464' => 464, + 'value0465' => 465, + 'value0466' => 466, + 'value0467' => 467, + 'value0468' => 468, + 'value0469' => 469, + 'value0470' => 470, + 'value0471' => 471, + 'value0472' => 472, + 'value0473' => 473, + 'value0474' => 474, + 'value0475' => 475, + 'value0476' => 476, + 'value0477' => 477, + 'value0478' => 478, + 'value0479' => 479, + 'value0480' => 480, + 'value0481' => 481, + 'value0482' => 482, + 'value0483' => 483, + 'value0484' => 484, + 'value0485' => 485, + 'value0486' => 486, + 'value0487' => 487, + 'value0488' => 488, + 'value0489' => 489, + 'value0490' => 490, + 'value0491' => 491, + 'value0492' => 492, + 'value0493' => 493, + 'value0494' => 494, + 'value0495' => 495, + 'value0496' => 496, + 'value0497' => 497, + 'value0498' => 498, + 'value0499' => 499, + 'value0500' => 500, + 'value0501' => 501, + 'value0502' => 502, + 'value0503' => 503, + 'value0504' => 504, + 'value0505' => 505, + 'value0506' => 506, + 'value0507' => 507, + 'value0508' => 508, + 'value0509' => 509, + 'value0510' => 510, + 'value0511' => 511, + 'value0512' => 512, + 'value0513' => 513, + 'value0514' => 514, + 'value0515' => 515, + 'value0516' => 516, + 'value0517' => 517, + 'value0518' => 518, + 'value0519' => 519, + 'value0520' => 520, + 'value0521' => 521, + 'value0522' => 522, + 'value0523' => 523, + 'value0524' => 524, + 'value0525' => 525, + 'value0526' => 526, + 'value0527' => 527, + 'value0528' => 528, + 'value0529' => 529, + 'value0530' => 530, + 'value0531' => 531, + 'value0532' => 532, + 'value0533' => 533, + 'value0534' => 534, + 'value0535' => 535, + 'value0536' => 536, + 'value0537' => 537, + 'value0538' => 538, + 'value0539' => 539, + 'value0540' => 540, + 'value0541' => 541, + 'value0542' => 542, + 'value0543' => 543, + 'value0544' => 544, + 'value0545' => 545, + 'value0546' => 546, + 'value0547' => 547, + 'value0548' => 548, + 'value0549' => 549, + 'value0550' => 550, + 'value0551' => 551, + 'value0552' => 552, + 'value0553' => 553, + 'value0554' => 554, + 'value0555' => 555, + 'value0556' => 556, + 'value0557' => 557, + 'value0558' => 558, + 'value0559' => 559, + 'value0560' => 560, + 'value0561' => 561, + 'value0562' => 562, + 'value0563' => 563, + 'value0564' => 564, + 'value0565' => 565, + 'value0566' => 566, + 'value0567' => 567, + 'value0568' => 568, + 'value0569' => 569, + 'value0570' => 570, + 'value0571' => 571, + 'value0572' => 572, + 'value0573' => 573, + 'value0574' => 574, + 'value0575' => 575, + 'value0576' => 576, + 'value0577' => 577, + 'value0578' => 578, + 'value0579' => 579, + 'value0580' => 580, + 'value0581' => 581, + 'value0582' => 582, + 'value0583' => 583, + 'value0584' => 584, + 'value0585' => 585, + 'value0586' => 586, + 'value0587' => 587, + 'value0588' => 588, + 'value0589' => 589, + 'value0590' => 590, + 'value0591' => 591, + 'value0592' => 592, + 'value0593' => 593, + 'value0594' => 594, + 'value0595' => 595, + 'value0596' => 596, + 'value0597' => 597, + 'value0598' => 598, + 'value0599' => 599, + 'value0600' => 600, + 'value0601' => 601, + 'value0602' => 602, + 'value0603' => 603, + 'value0604' => 604, + 'value0605' => 605, + 'value0606' => 606, + 'value0607' => 607, + 'value0608' => 608, + 'value0609' => 609, + 'value0610' => 610, + 'value0611' => 611, + 'value0612' => 612, + 'value0613' => 613, + 'value0614' => 614, + 'value0615' => 615, + 'value0616' => 616, + 'value0617' => 617, + 'value0618' => 618, + 'value0619' => 619, + 'value0620' => 620, + 'value0621' => 621, + 'value0622' => 622, + 'value0623' => 623, + 'value0624' => 624, + 'value0625' => 625, + 'value0626' => 626, + 'value0627' => 627, + 'value0628' => 628, + 'value0629' => 629, + 'value0630' => 630, + 'value0631' => 631, + 'value0632' => 632, + 'value0633' => 633, + 'value0634' => 634, + 'value0635' => 635, + 'value0636' => 636, + 'value0637' => 637, + 'value0638' => 638, + 'value0639' => 639, + 'value0640' => 640, + 'value0641' => 641, + 'value0642' => 642, + 'value0643' => 643, + 'value0644' => 644, + 'value0645' => 645, + 'value0646' => 646, + 'value0647' => 647, + 'value0648' => 648, + 'value0649' => 649, + 'value0650' => 650, + 'value0651' => 651, + 'value0652' => 652, + 'value0653' => 653, + 'value0654' => 654, + 'value0655' => 655, + 'value0656' => 656, + 'value0657' => 657, + 'value0658' => 658, + 'value0659' => 659, + 'value0660' => 660, + 'value0661' => 661, + 'value0662' => 662, + 'value0663' => 663, + 'value0664' => 664, + 'value0665' => 665, + 'value0666' => 666, + 'value0667' => 667, + 'value0668' => 668, + 'value0669' => 669, + 'value0670' => 670, + 'value0671' => 671, + 'value0672' => 672, + 'value0673' => 673, + 'value0674' => 674, + 'value0675' => 675, + 'value0676' => 676, + 'value0677' => 677, + 'value0678' => 678, + 'value0679' => 679, + 'value0680' => 680, + 'value0681' => 681, + 'value0682' => 682, + 'value0683' => 683, + 'value0684' => 684, + 'value0685' => 685, + 'value0686' => 686, + 'value0687' => 687, + 'value0688' => 688, + 'value0689' => 689, + 'value0690' => 690, + 'value0691' => 691, + 'value0692' => 692, + 'value0693' => 693, + 'value0694' => 694, + 'value0695' => 695, + 'value0696' => 696, + 'value0697' => 697, + 'value0698' => 698, + 'value0699' => 699, + 'value0700' => 700, + 'value0701' => 701, + 'value0702' => 702, + 'value0703' => 703, + 'value0704' => 704, + 'value0705' => 705, + 'value0706' => 706, + 'value0707' => 707, + 'value0708' => 708, + 'value0709' => 709, + 'value0710' => 710, + 'value0711' => 711, + 'value0712' => 712, + 'value0713' => 713, + 'value0714' => 714, + 'value0715' => 715, + 'value0716' => 716, + 'value0717' => 717, + 'value0718' => 718, + 'value0719' => 719, + 'value0720' => 720, + 'value0721' => 721, + 'value0722' => 722, + 'value0723' => 723, + 'value0724' => 724, + 'value0725' => 725, + 'value0726' => 726, + 'value0727' => 727, + 'value0728' => 728, + 'value0729' => 729, + 'value0730' => 730, + 'value0731' => 731, + 'value0732' => 732, + 'value0733' => 733, + 'value0734' => 734, + 'value0735' => 735, + 'value0736' => 736, + 'value0737' => 737, + 'value0738' => 738, + 'value0739' => 739, + 'value0740' => 740, + 'value0741' => 741, + 'value0742' => 742, + 'value0743' => 743, + 'value0744' => 744, + 'value0745' => 745, + 'value0746' => 746, + 'value0747' => 747, + 'value0748' => 748, + 'value0749' => 749, + 'value0750' => 750, + 'value0751' => 751, + 'value0752' => 752, + 'value0753' => 753, + 'value0754' => 754, + 'value0755' => 755, + 'value0756' => 756, + 'value0757' => 757, + 'value0758' => 758, + 'value0759' => 759, + 'value0760' => 760, + 'value0761' => 761, + 'value0762' => 762, + 'value0763' => 763, + 'value0764' => 764, + 'value0765' => 765, + 'value0766' => 766, + 'value0767' => 767, + 'value0768' => 768, + 'value0769' => 769, + 'value0770' => 770, + 'value0771' => 771, + 'value0772' => 772, + 'value0773' => 773, + 'value0774' => 774, + 'value0775' => 775, + 'value0776' => 776, + 'value0777' => 777, + 'value0778' => 778, + 'value0779' => 779, + 'value0780' => 780, + 'value0781' => 781, + 'value0782' => 782, + 'value0783' => 783, + 'value0784' => 784, + 'value0785' => 785, + 'value0786' => 786, + 'value0787' => 787, + 'value0788' => 788, + 'value0789' => 789, + 'value0790' => 790, + 'value0791' => 791, + 'value0792' => 792, + 'value0793' => 793, + 'value0794' => 794, + 'value0795' => 795, + 'value0796' => 796, + 'value0797' => 797, + 'value0798' => 798, + 'value0799' => 799, + 'value0800' => 800, + 'value0801' => 801, + 'value0802' => 802, + 'value0803' => 803, + 'value0804' => 804, + 'value0805' => 805, + 'value0806' => 806, + 'value0807' => 807, + 'value0808' => 808, + 'value0809' => 809, + 'value0810' => 810, + 'value0811' => 811, + 'value0812' => 812, + 'value0813' => 813, + 'value0814' => 814, + 'value0815' => 815, + 'value0816' => 816, + 'value0817' => 817, + 'value0818' => 818, + 'value0819' => 819, + 'value0820' => 820, + 'value0821' => 821, + 'value0822' => 822, + 'value0823' => 823, + 'value0824' => 824, + 'value0825' => 825, + 'value0826' => 826, + 'value0827' => 827, + 'value0828' => 828, + 'value0829' => 829, + 'value0830' => 830, + 'value0831' => 831, + 'value0832' => 832, + 'value0833' => 833, + 'value0834' => 834, + 'value0835' => 835, + 'value0836' => 836, + 'value0837' => 837, + 'value0838' => 838, + 'value0839' => 839, + 'value0840' => 840, + 'value0841' => 841, + 'value0842' => 842, + 'value0843' => 843, + 'value0844' => 844, + 'value0845' => 845, + 'value0846' => 846, + 'value0847' => 847, + 'value0848' => 848, + 'value0849' => 849, + 'value0850' => 850, + 'value0851' => 851, + 'value0852' => 852, + 'value0853' => 853, + 'value0854' => 854, + 'value0855' => 855, + 'value0856' => 856, + 'value0857' => 857, + 'value0858' => 858, + 'value0859' => 859, + 'value0860' => 860, + 'value0861' => 861, + 'value0862' => 862, + 'value0863' => 863, + 'value0864' => 864, + 'value0865' => 865, + 'value0866' => 866, + 'value0867' => 867, + 'value0868' => 868, + 'value0869' => 869, + 'value0870' => 870, + 'value0871' => 871, + 'value0872' => 872, + 'value0873' => 873, + 'value0874' => 874, + 'value0875' => 875, + 'value0876' => 876, + 'value0877' => 877, + 'value0878' => 878, + 'value0879' => 879, + 'value0880' => 880, + 'value0881' => 881, + 'value0882' => 882, + 'value0883' => 883, + 'value0884' => 884, + 'value0885' => 885, + 'value0886' => 886, + 'value0887' => 887, + 'value0888' => 888, + 'value0889' => 889, + 'value0890' => 890, + 'value0891' => 891, + 'value0892' => 892, + 'value0893' => 893, + 'value0894' => 894, + 'value0895' => 895, + 'value0896' => 896, + 'value0897' => 897, + 'value0898' => 898, + 'value0899' => 899, + 'value0900' => 900, + 'value0901' => 901, + 'value0902' => 902, + 'value0903' => 903, + 'value0904' => 904, + 'value0905' => 905, + 'value0906' => 906, + 'value0907' => 907, + 'value0908' => 908, + 'value0909' => 909, + 'value0910' => 910, + 'value0911' => 911, + 'value0912' => 912, + 'value0913' => 913, + 'value0914' => 914, + 'value0915' => 915, + 'value0916' => 916, + 'value0917' => 917, + 'value0918' => 918, + 'value0919' => 919, + 'value0920' => 920, + 'value0921' => 921, + 'value0922' => 922, + 'value0923' => 923, + 'value0924' => 924, + 'value0925' => 925, + 'value0926' => 926, + 'value0927' => 927, + 'value0928' => 928, + 'value0929' => 929, + 'value0930' => 930, + 'value0931' => 931, + 'value0932' => 932, + 'value0933' => 933, + 'value0934' => 934, + 'value0935' => 935, + 'value0936' => 936, + 'value0937' => 937, + 'value0938' => 938, + 'value0939' => 939, + 'value0940' => 940, + 'value0941' => 941, + 'value0942' => 942, + 'value0943' => 943, + 'value0944' => 944, + 'value0945' => 945, + 'value0946' => 946, + 'value0947' => 947, + 'value0948' => 948, + 'value0949' => 949, + 'value0950' => 950, + 'value0951' => 951, + 'value0952' => 952, + 'value0953' => 953, + 'value0954' => 954, + 'value0955' => 955, + 'value0956' => 956, + 'value0957' => 957, + 'value0958' => 958, + 'value0959' => 959, + 'value0960' => 960, + 'value0961' => 961, + 'value0962' => 962, + 'value0963' => 963, + 'value0964' => 964, + 'value0965' => 965, + 'value0966' => 966, + 'value0967' => 967, + 'value0968' => 968, + 'value0969' => 969, + 'value0970' => 970, + 'value0971' => 971, + 'value0972' => 972, + 'value0973' => 973, + 'value0974' => 974, + 'value0975' => 975, + 'value0976' => 976, + 'value0977' => 977, + 'value0978' => 978, + 'value0979' => 979, + 'value0980' => 980, + 'value0981' => 981, + 'value0982' => 982, + 'value0983' => 983, + 'value0984' => 984, + 'value0985' => 985, + 'value0986' => 986, + 'value0987' => 987, + 'value0988' => 988, + 'value0989' => 989, + 'value0990' => 990, + 'value0991' => 991, + 'value0992' => 992, + 'value0993' => 993, + 'value0994' => 994, + 'value0995' => 995, + 'value0996' => 996, + 'value0997' => 997, + 'value0998' => 998, + 'value0999' => 999, + 'value1000' => 1000, + 'value1001' => 1001, + 'value1002' => 1002, + 'value1003' => 1003, + 'value1004' => 1004, + 'value1005' => 1005, + 'value1006' => 1006, + 'value1007' => 1007, + 'value1008' => 1008, + 'value1009' => 1009, + 'value1010' => 1010, + 'value1011' => 1011, + 'value1012' => 1012, + 'value1013' => 1013, + 'value1014' => 1014, + 'value1015' => 1015, + 'value1016' => 1016, + 'value1017' => 1017, + 'value1018' => 1018, + 'value1019' => 1019, + 'value1020' => 1020, + 'value1021' => 1021, + 'value1022' => 1022, + 'value1023' => 1023, + 'value1024' => 1024, + 'value1025' => 1025, + 'value1026' => 1026, + 'value1027' => 1027, + 'value1028' => 1028, + 'value1029' => 1029, + 'value1030' => 1030, + 'value1031' => 1031, + 'value1032' => 1032, + 'value1033' => 1033, + 'value1034' => 1034, + 'value1035' => 1035, + 'value1036' => 1036, + 'value1037' => 1037, + 'value1038' => 1038, + 'value1039' => 1039, + 'value1040' => 1040, + 'value1041' => 1041, + 'value1042' => 1042, + 'value1043' => 1043, + 'value1044' => 1044, + 'value1045' => 1045, + 'value1046' => 1046, + 'value1047' => 1047, + 'value1048' => 1048, + 'value1049' => 1049, + 'value1050' => 1050, + 'value1051' => 1051, + 'value1052' => 1052, + 'value1053' => 1053, + 'value1054' => 1054, + 'value1055' => 1055, + 'value1056' => 1056, + 'value1057' => 1057, + 'value1058' => 1058, + 'value1059' => 1059, + 'value1060' => 1060, + 'value1061' => 1061, + 'value1062' => 1062, + 'value1063' => 1063, + 'value1064' => 1064, + 'value1065' => 1065, + 'value1066' => 1066, + 'value1067' => 1067, + 'value1068' => 1068, + 'value1069' => 1069, + 'value1070' => 1070, + 'value1071' => 1071, + 'value1072' => 1072, + 'value1073' => 1073, + 'value1074' => 1074, + 'value1075' => 1075, + 'value1076' => 1076, + 'value1077' => 1077, + 'value1078' => 1078, + 'value1079' => 1079, + 'value1080' => 1080, + 'value1081' => 1081, + 'value1082' => 1082, + 'value1083' => 1083, + 'value1084' => 1084, + 'value1085' => 1085, + 'value1086' => 1086, + 'value1087' => 1087, + 'value1088' => 1088, + 'value1089' => 1089, + 'value1090' => 1090, + 'value1091' => 1091, + 'value1092' => 1092, + 'value1093' => 1093, + 'value1094' => 1094, + 'value1095' => 1095, + 'value1096' => 1096, + 'value1097' => 1097, + 'value1098' => 1098, + 'value1099' => 1099, + 'value1100' => 1100, + 'value1101' => 1101, + 'value1102' => 1102, + 'value1103' => 1103, + 'value1104' => 1104, + 'value1105' => 1105, + 'value1106' => 1106, + 'value1107' => 1107, + 'value1108' => 1108, + 'value1109' => 1109, + 'value1110' => 1110, + 'value1111' => 1111, + 'value1112' => 1112, + 'value1113' => 1113, + 'value1114' => 1114, + 'value1115' => 1115, + 'value1116' => 1116, + 'value1117' => 1117, + 'value1118' => 1118, + 'value1119' => 1119, + 'value1120' => 1120, + 'value1121' => 1121, + 'value1122' => 1122, + 'value1123' => 1123, + 'value1124' => 1124, + 'value1125' => 1125, + 'value1126' => 1126, + 'value1127' => 1127, + 'value1128' => 1128, + 'value1129' => 1129, + 'value1130' => 1130, + 'value1131' => 1131, + 'value1132' => 1132, + 'value1133' => 1133, + 'value1134' => 1134, + 'value1135' => 1135, + 'value1136' => 1136, + 'value1137' => 1137, + 'value1138' => 1138, + 'value1139' => 1139, + 'value1140' => 1140, + 'value1141' => 1141, + 'value1142' => 1142, + 'value1143' => 1143, + 'value1144' => 1144, + 'value1145' => 1145, + 'value1146' => 1146, + 'value1147' => 1147, + 'value1148' => 1148, + 'value1149' => 1149, + 'value1150' => 1150, + 'value1151' => 1151, + 'value1152' => 1152, + 'value1153' => 1153, + 'value1154' => 1154, + 'value1155' => 1155, + 'value1156' => 1156, + 'value1157' => 1157, + 'value1158' => 1158, + 'value1159' => 1159, + 'value1160' => 1160, + 'value1161' => 1161, + 'value1162' => 1162, + 'value1163' => 1163, + 'value1164' => 1164, + 'value1165' => 1165, + 'value1166' => 1166, + 'value1167' => 1167, + 'value1168' => 1168, + 'value1169' => 1169, + 'value1170' => 1170, + 'value1171' => 1171, + 'value1172' => 1172, + 'value1173' => 1173, + 'value1174' => 1174, + 'value1175' => 1175, + 'value1176' => 1176, + 'value1177' => 1177, + 'value1178' => 1178, + 'value1179' => 1179, + 'value1180' => 1180, + 'value1181' => 1181, + 'value1182' => 1182, + 'value1183' => 1183, + 'value1184' => 1184, + 'value1185' => 1185, + 'value1186' => 1186, + 'value1187' => 1187, + 'value1188' => 1188, + 'value1189' => 1189, + 'value1190' => 1190, + 'value1191' => 1191, + 'value1192' => 1192, + 'value1193' => 1193, + 'value1194' => 1194, + 'value1195' => 1195, + 'value1196' => 1196, + 'value1197' => 1197, + 'value1198' => 1198, + 'value1199' => 1199, + 'value1200' => 1200, + 'value1201' => 1201, + 'value1202' => 1202, + 'value1203' => 1203, + 'value1204' => 1204, + 'value1205' => 1205, + 'value1206' => 1206, + 'value1207' => 1207, + 'value1208' => 1208, + 'value1209' => 1209, + 'value1210' => 1210, + 'value1211' => 1211, + 'value1212' => 1212, + 'value1213' => 1213, + 'value1214' => 1214, + 'value1215' => 1215, + 'value1216' => 1216, + 'value1217' => 1217, + 'value1218' => 1218, + 'value1219' => 1219, + 'value1220' => 1220, + 'value1221' => 1221, + 'value1222' => 1222, + 'value1223' => 1223, + 'value1224' => 1224, + 'value1225' => 1225, + 'value1226' => 1226, + 'value1227' => 1227, + 'value1228' => 1228, + 'value1229' => 1229, + 'value1230' => 1230, + 'value1231' => 1231, + 'value1232' => 1232, + 'value1233' => 1233, + 'value1234' => 1234, + 'value1235' => 1235, + 'value1236' => 1236, + 'value1237' => 1237, + 'value1238' => 1238, + 'value1239' => 1239, + 'value1240' => 1240, + 'value1241' => 1241, + 'value1242' => 1242, + 'value1243' => 1243, + 'value1244' => 1244, + 'value1245' => 1245, + 'value1246' => 1246, + 'value1247' => 1247, + 'value1248' => 1248, + 'value1249' => 1249, + 'value1250' => 1250, + 'value1251' => 1251, + 'value1252' => 1252, + 'value1253' => 1253, + 'value1254' => 1254, + 'value1255' => 1255, + 'value1256' => 1256, + 'value1257' => 1257, + 'value1258' => 1258, + 'value1259' => 1259, + 'value1260' => 1260, + 'value1261' => 1261, + 'value1262' => 1262, + 'value1263' => 1263, + 'value1264' => 1264, + 'value1265' => 1265, + 'value1266' => 1266, + 'value1267' => 1267, + 'value1268' => 1268, + 'value1269' => 1269, + 'value1270' => 1270, + 'value1271' => 1271, + 'value1272' => 1272, + 'value1273' => 1273, + 'value1274' => 1274, + 'value1275' => 1275, + 'value1276' => 1276, + 'value1277' => 1277, + 'value1278' => 1278, + 'value1279' => 1279, + 'value1280' => 1280, + 'value1281' => 1281, + 'value1282' => 1282, + 'value1283' => 1283, + 'value1284' => 1284, + 'value1285' => 1285, + 'value1286' => 1286, + 'value1287' => 1287, + 'value1288' => 1288, + 'value1289' => 1289, + 'value1290' => 1290, + 'value1291' => 1291, + 'value1292' => 1292, + 'value1293' => 1293, + 'value1294' => 1294, + 'value1295' => 1295, + 'value1296' => 1296, + 'value1297' => 1297, + 'value1298' => 1298, + 'value1299' => 1299, + 'value1300' => 1300, + 'value1301' => 1301, + 'value1302' => 1302, + 'value1303' => 1303, + 'value1304' => 1304, + 'value1305' => 1305, + 'value1306' => 1306, + 'value1307' => 1307, + 'value1308' => 1308, + 'value1309' => 1309, + 'value1310' => 1310, + 'value1311' => 1311, + 'value1312' => 1312, + 'value1313' => 1313, + 'value1314' => 1314, + 'value1315' => 1315, + 'value1316' => 1316, + 'value1317' => 1317, + 'value1318' => 1318, + 'value1319' => 1319, + 'value1320' => 1320, + 'value1321' => 1321, + 'value1322' => 1322, + 'value1323' => 1323, + 'value1324' => 1324, + 'value1325' => 1325, + 'value1326' => 1326, + 'value1327' => 1327, + 'value1328' => 1328, + 'value1329' => 1329, + 'value1330' => 1330, + 'value1331' => 1331, + 'value1332' => 1332, + 'value1333' => 1333, + 'value1334' => 1334, + 'value1335' => 1335, + 'value1336' => 1336, + 'value1337' => 1337, + 'value1338' => 1338, + 'value1339' => 1339, + 'value1340' => 1340, + 'value1341' => 1341, + 'value1342' => 1342, + 'value1343' => 1343, + 'value1344' => 1344, + 'value1345' => 1345, + 'value1346' => 1346, + 'value1347' => 1347, + 'value1348' => 1348, + 'value1349' => 1349, + 'value1350' => 1350, + 'value1351' => 1351, + 'value1352' => 1352, + 'value1353' => 1353, + 'value1354' => 1354, + 'value1355' => 1355, + 'value1356' => 1356, + 'value1357' => 1357, + 'value1358' => 1358, + 'value1359' => 1359, + 'value1360' => 1360, + 'value1361' => 1361, + 'value1362' => 1362, + 'value1363' => 1363, + 'value1364' => 1364, + 'value1365' => 1365, + 'value1366' => 1366, + 'value1367' => 1367, + 'value1368' => 1368, + 'value1369' => 1369, + 'value1370' => 1370, + 'value1371' => 1371, + 'value1372' => 1372, + 'value1373' => 1373, + 'value1374' => 1374, + 'value1375' => 1375, + 'value1376' => 1376, + 'value1377' => 1377, + 'value1378' => 1378, + 'value1379' => 1379, + 'value1380' => 1380, + 'value1381' => 1381, + 'value1382' => 1382, + 'value1383' => 1383, + 'value1384' => 1384, + 'value1385' => 1385, + 'value1386' => 1386, + 'value1387' => 1387, + 'value1388' => 1388, + 'value1389' => 1389, + 'value1390' => 1390, + 'value1391' => 1391, + 'value1392' => 1392, + 'value1393' => 1393, + 'value1394' => 1394, + 'value1395' => 1395, + 'value1396' => 1396, + 'value1397' => 1397, + 'value1398' => 1398, + 'value1399' => 1399, + 'value1400' => 1400, + 'value1401' => 1401, + 'value1402' => 1402, + 'value1403' => 1403, + 'value1404' => 1404, + 'value1405' => 1405, + 'value1406' => 1406, + 'value1407' => 1407, + 'value1408' => 1408, + 'value1409' => 1409, + 'value1410' => 1410, + 'value1411' => 1411, + 'value1412' => 1412, + 'value1413' => 1413, + 'value1414' => 1414, + 'value1415' => 1415, + 'value1416' => 1416, + 'value1417' => 1417, + 'value1418' => 1418, + 'value1419' => 1419, + 'value1420' => 1420, + 'value1421' => 1421, + 'value1422' => 1422, + 'value1423' => 1423, + 'value1424' => 1424, + 'value1425' => 1425, + 'value1426' => 1426, + 'value1427' => 1427, + 'value1428' => 1428, + 'value1429' => 1429, + 'value1430' => 1430, + 'value1431' => 1431, + 'value1432' => 1432, + 'value1433' => 1433, + 'value1434' => 1434, + 'value1435' => 1435, + 'value1436' => 1436, + 'value1437' => 1437, + 'value1438' => 1438, + 'value1439' => 1439, + 'value1440' => 1440, + 'value1441' => 1441, + 'value1442' => 1442, + 'value1443' => 1443, + 'value1444' => 1444, + 'value1445' => 1445, + 'value1446' => 1446, + 'value1447' => 1447, + 'value1448' => 1448, + 'value1449' => 1449, + 'value1450' => 1450, + 'value1451' => 1451, + 'value1452' => 1452, + 'value1453' => 1453, + 'value1454' => 1454, + 'value1455' => 1455, + 'value1456' => 1456, + 'value1457' => 1457, + 'value1458' => 1458, + 'value1459' => 1459, + 'value1460' => 1460, + 'value1461' => 1461, + 'value1462' => 1462, + 'value1463' => 1463, + 'value1464' => 1464, + 'value1465' => 1465, + 'value1466' => 1466, + 'value1467' => 1467, + 'value1468' => 1468, + 'value1469' => 1469, + 'value1470' => 1470, + 'value1471' => 1471, + 'value1472' => 1472, + 'value1473' => 1473, + 'value1474' => 1474, + 'value1475' => 1475, + 'value1476' => 1476, + 'value1477' => 1477, + 'value1478' => 1478, + 'value1479' => 1479, + 'value1480' => 1480, + 'value1481' => 1481, + 'value1482' => 1482, + 'value1483' => 1483, + 'value1484' => 1484, + 'value1485' => 1485, + 'value1486' => 1486, + 'value1487' => 1487, + 'value1488' => 1488, + 'value1489' => 1489, + 'value1490' => 1490, + 'value1491' => 1491, + 'value1492' => 1492, + 'value1493' => 1493, + 'value1494' => 1494, + 'value1495' => 1495, + 'value1496' => 1496, + 'value1497' => 1497, + 'value1498' => 1498, + 'value1499' => 1499, + 'value1500' => 1500, + 'value1501' => 1501, + 'value1502' => 1502, + 'value1503' => 1503, + 'value1504' => 1504, + 'value1505' => 1505, + 'value1506' => 1506, + 'value1507' => 1507, + 'value1508' => 1508, + 'value1509' => 1509, + 'value1510' => 1510, + 'value1511' => 1511, + 'value1512' => 1512, + 'value1513' => 1513, + 'value1514' => 1514, + 'value1515' => 1515, + 'value1516' => 1516, + 'value1517' => 1517, + 'value1518' => 1518, + 'value1519' => 1519, + 'value1520' => 1520, + 'value1521' => 1521, + 'value1522' => 1522, + 'value1523' => 1523, + 'value1524' => 1524, + 'value1525' => 1525, + 'value1526' => 1526, + 'value1527' => 1527, + 'value1528' => 1528, + 'value1529' => 1529, + 'value1530' => 1530, + 'value1531' => 1531, + 'value1532' => 1532, + 'value1533' => 1533, + 'value1534' => 1534, + 'value1535' => 1535, + 'value1536' => 1536, + 'value1537' => 1537, + 'value1538' => 1538, + 'value1539' => 1539, + 'value1540' => 1540, + 'value1541' => 1541, + 'value1542' => 1542, + 'value1543' => 1543, + 'value1544' => 1544, + 'value1545' => 1545, + 'value1546' => 1546, + 'value1547' => 1547, + 'value1548' => 1548, + 'value1549' => 1549, + 'value1550' => 1550, + 'value1551' => 1551, + 'value1552' => 1552, + 'value1553' => 1553, + 'value1554' => 1554, + 'value1555' => 1555, + 'value1556' => 1556, + 'value1557' => 1557, + 'value1558' => 1558, + 'value1559' => 1559, + 'value1560' => 1560, + 'value1561' => 1561, + 'value1562' => 1562, + 'value1563' => 1563, + 'value1564' => 1564, + 'value1565' => 1565, + 'value1566' => 1566, + 'value1567' => 1567, + 'value1568' => 1568, + 'value1569' => 1569, + 'value1570' => 1570, + 'value1571' => 1571, + 'value1572' => 1572, + 'value1573' => 1573, + 'value1574' => 1574, + 'value1575' => 1575, + 'value1576' => 1576, + 'value1577' => 1577, + 'value1578' => 1578, + 'value1579' => 1579, + 'value1580' => 1580, + 'value1581' => 1581, + 'value1582' => 1582, + 'value1583' => 1583, + 'value1584' => 1584, + 'value1585' => 1585, + 'value1586' => 1586, + 'value1587' => 1587, + 'value1588' => 1588, + 'value1589' => 1589, + 'value1590' => 1590, + 'value1591' => 1591, + 'value1592' => 1592, + 'value1593' => 1593, + 'value1594' => 1594, + 'value1595' => 1595, + 'value1596' => 1596, + 'value1597' => 1597, + 'value1598' => 1598, + 'value1599' => 1599, + 'value1600' => 1600, + 'value1601' => 1601, + 'value1602' => 1602, + 'value1603' => 1603, + 'value1604' => 1604, + 'value1605' => 1605, + 'value1606' => 1606, + 'value1607' => 1607, + 'value1608' => 1608, + 'value1609' => 1609, + 'value1610' => 1610, + 'value1611' => 1611, + 'value1612' => 1612, + 'value1613' => 1613, + 'value1614' => 1614, + 'value1615' => 1615, + 'value1616' => 1616, + 'value1617' => 1617, + 'value1618' => 1618, + 'value1619' => 1619, + 'value1620' => 1620, + 'value1621' => 1621, + 'value1622' => 1622, + 'value1623' => 1623, + 'value1624' => 1624, + 'value1625' => 1625, + 'value1626' => 1626, + 'value1627' => 1627, + 'value1628' => 1628, + 'value1629' => 1629, + 'value1630' => 1630, + 'value1631' => 1631, + 'value1632' => 1632, + 'value1633' => 1633, + 'value1634' => 1634, + 'value1635' => 1635, + 'value1636' => 1636, + 'value1637' => 1637, + 'value1638' => 1638, + 'value1639' => 1639, + 'value1640' => 1640, + 'value1641' => 1641, + 'value1642' => 1642, + 'value1643' => 1643, + 'value1644' => 1644, + 'value1645' => 1645, + 'value1646' => 1646, + 'value1647' => 1647, + 'value1648' => 1648, + 'value1649' => 1649, + 'value1650' => 1650, + 'value1651' => 1651, + 'value1652' => 1652, + 'value1653' => 1653, + 'value1654' => 1654, + 'value1655' => 1655, + 'value1656' => 1656, + 'value1657' => 1657, + 'value1658' => 1658, + 'value1659' => 1659, + 'value1660' => 1660, + 'value1661' => 1661, + 'value1662' => 1662, + 'value1663' => 1663, + 'value1664' => 1664, + 'value1665' => 1665, + 'value1666' => 1666, + 'value1667' => 1667, + 'value1668' => 1668, + 'value1669' => 1669, + 'value1670' => 1670, + 'value1671' => 1671, + 'value1672' => 1672, + 'value1673' => 1673, + 'value1674' => 1674, + 'value1675' => 1675, + 'value1676' => 1676, + 'value1677' => 1677, + 'value1678' => 1678, + 'value1679' => 1679, + 'value1680' => 1680, + 'value1681' => 1681, + 'value1682' => 1682, + 'value1683' => 1683, + 'value1684' => 1684, + 'value1685' => 1685, + 'value1686' => 1686, + 'value1687' => 1687, + 'value1688' => 1688, + 'value1689' => 1689, + 'value1690' => 1690, + 'value1691' => 1691, + 'value1692' => 1692, + 'value1693' => 1693, + 'value1694' => 1694, + 'value1695' => 1695, + 'value1696' => 1696, + 'value1697' => 1697, + 'value1698' => 1698, + 'value1699' => 1699, + 'value1700' => 1700, + 'value1701' => 1701, + 'value1702' => 1702, + 'value1703' => 1703, + 'value1704' => 1704, + 'value1705' => 1705, + 'value1706' => 1706, + 'value1707' => 1707, + 'value1708' => 1708, + 'value1709' => 1709, + 'value1710' => 1710, + 'value1711' => 1711, + 'value1712' => 1712, + 'value1713' => 1713, + 'value1714' => 1714, + 'value1715' => 1715, + 'value1716' => 1716, + 'value1717' => 1717, + 'value1718' => 1718, + 'value1719' => 1719, + 'value1720' => 1720, + 'value1721' => 1721, + 'value1722' => 1722, + 'value1723' => 1723, + 'value1724' => 1724, + 'value1725' => 1725, + 'value1726' => 1726, + 'value1727' => 1727, + 'value1728' => 1728, + 'value1729' => 1729, + 'value1730' => 1730, + 'value1731' => 1731, + 'value1732' => 1732, + 'value1733' => 1733, + 'value1734' => 1734, + 'value1735' => 1735, + 'value1736' => 1736, + 'value1737' => 1737, + 'value1738' => 1738, + 'value1739' => 1739, + 'value1740' => 1740, + 'value1741' => 1741, + 'value1742' => 1742, + 'value1743' => 1743, + 'value1744' => 1744, + 'value1745' => 1745, + 'value1746' => 1746, + 'value1747' => 1747, + 'value1748' => 1748, + 'value1749' => 1749, + 'value1750' => 1750, + 'value1751' => 1751, + 'value1752' => 1752, + 'value1753' => 1753, + 'value1754' => 1754, + 'value1755' => 1755, + 'value1756' => 1756, + 'value1757' => 1757, + 'value1758' => 1758, + 'value1759' => 1759, + 'value1760' => 1760, + 'value1761' => 1761, + 'value1762' => 1762, + 'value1763' => 1763, + 'value1764' => 1764, + 'value1765' => 1765, + 'value1766' => 1766, + 'value1767' => 1767, + 'value1768' => 1768, + 'value1769' => 1769, + 'value1770' => 1770, + 'value1771' => 1771, + 'value1772' => 1772, + 'value1773' => 1773, + 'value1774' => 1774, + 'value1775' => 1775, + 'value1776' => 1776, + 'value1777' => 1777, + 'value1778' => 1778, + 'value1779' => 1779, + 'value1780' => 1780, + 'value1781' => 1781, + 'value1782' => 1782, + 'value1783' => 1783, + 'value1784' => 1784, + 'value1785' => 1785, + 'value1786' => 1786, + 'value1787' => 1787, + 'value1788' => 1788, + 'value1789' => 1789, + 'value1790' => 1790, + 'value1791' => 1791, + 'value1792' => 1792, + 'value1793' => 1793, + 'value1794' => 1794, + 'value1795' => 1795, + 'value1796' => 1796, + 'value1797' => 1797, + 'value1798' => 1798, + 'value1799' => 1799, + 'value1800' => 1800, + 'value1801' => 1801, + 'value1802' => 1802, + 'value1803' => 1803, + 'value1804' => 1804, + 'value1805' => 1805, + 'value1806' => 1806, + 'value1807' => 1807, + 'value1808' => 1808, + 'value1809' => 1809, + 'value1810' => 1810, + 'value1811' => 1811, + 'value1812' => 1812, + 'value1813' => 1813, + 'value1814' => 1814, + 'value1815' => 1815, + 'value1816' => 1816, + 'value1817' => 1817, + 'value1818' => 1818, + 'value1819' => 1819, + 'value1820' => 1820, + 'value1821' => 1821, + 'value1822' => 1822, + 'value1823' => 1823, + 'value1824' => 1824, + 'value1825' => 1825, + 'value1826' => 1826, + 'value1827' => 1827, + 'value1828' => 1828, + 'value1829' => 1829, + 'value1830' => 1830, + 'value1831' => 1831, + 'value1832' => 1832, + 'value1833' => 1833, + 'value1834' => 1834, + 'value1835' => 1835, + 'value1836' => 1836, + 'value1837' => 1837, + 'value1838' => 1838, + 'value1839' => 1839, + 'value1840' => 1840, + 'value1841' => 1841, + 'value1842' => 1842, + 'value1843' => 1843, + 'value1844' => 1844, + 'value1845' => 1845, + 'value1846' => 1846, + 'value1847' => 1847, + 'value1848' => 1848, + 'value1849' => 1849, + 'value1850' => 1850, + 'value1851' => 1851, + 'value1852' => 1852, + 'value1853' => 1853, + 'value1854' => 1854, + 'value1855' => 1855, + 'value1856' => 1856, + 'value1857' => 1857, + 'value1858' => 1858, + 'value1859' => 1859, + 'value1860' => 1860, + 'value1861' => 1861, + 'value1862' => 1862, + 'value1863' => 1863, + 'value1864' => 1864, + 'value1865' => 1865, + 'value1866' => 1866, + 'value1867' => 1867, + 'value1868' => 1868, + 'value1869' => 1869, + 'value1870' => 1870, + 'value1871' => 1871, + 'value1872' => 1872, + 'value1873' => 1873, + 'value1874' => 1874, + 'value1875' => 1875, + 'value1876' => 1876, + 'value1877' => 1877, + 'value1878' => 1878, + 'value1879' => 1879, + 'value1880' => 1880, + 'value1881' => 1881, + 'value1882' => 1882, + 'value1883' => 1883, + 'value1884' => 1884, + 'value1885' => 1885, + 'value1886' => 1886, + 'value1887' => 1887, + 'value1888' => 1888, + 'value1889' => 1889, + 'value1890' => 1890, + 'value1891' => 1891, + 'value1892' => 1892, + 'value1893' => 1893, + 'value1894' => 1894, + 'value1895' => 1895, + 'value1896' => 1896, + 'value1897' => 1897, + 'value1898' => 1898, + 'value1899' => 1899, + 'value1900' => 1900, + 'value1901' => 1901, + 'value1902' => 1902, + 'value1903' => 1903, + 'value1904' => 1904, + 'value1905' => 1905, + 'value1906' => 1906, + 'value1907' => 1907, + 'value1908' => 1908, + 'value1909' => 1909, + 'value1910' => 1910, + 'value1911' => 1911, + 'value1912' => 1912, + 'value1913' => 1913, + 'value1914' => 1914, + 'value1915' => 1915, + 'value1916' => 1916, + 'value1917' => 1917, + 'value1918' => 1918, + 'value1919' => 1919, + 'value1920' => 1920, + 'value1921' => 1921, + 'value1922' => 1922, + 'value1923' => 1923, + 'value1924' => 1924, + 'value1925' => 1925, + 'value1926' => 1926, + 'value1927' => 1927, + 'value1928' => 1928, + 'value1929' => 1929, + 'value1930' => 1930, + 'value1931' => 1931, + 'value1932' => 1932, + 'value1933' => 1933, + 'value1934' => 1934, + 'value1935' => 1935, + 'value1936' => 1936, + 'value1937' => 1937, + 'value1938' => 1938, + 'value1939' => 1939, + 'value1940' => 1940, + 'value1941' => 1941, + 'value1942' => 1942, + 'value1943' => 1943, + 'value1944' => 1944, + 'value1945' => 1945, + 'value1946' => 1946, + 'value1947' => 1947, + 'value1948' => 1948, + 'value1949' => 1949, + 'value1950' => 1950, + 'value1951' => 1951, + 'value1952' => 1952, + 'value1953' => 1953, + 'value1954' => 1954, + 'value1955' => 1955, + 'value1956' => 1956, + 'value1957' => 1957, + 'value1958' => 1958, + 'value1959' => 1959, + 'value1960' => 1960, + 'value1961' => 1961, + 'value1962' => 1962, + 'value1963' => 1963, + 'value1964' => 1964, + 'value1965' => 1965, + 'value1966' => 1966, + 'value1967' => 1967, + 'value1968' => 1968, + 'value1969' => 1969, + 'value1970' => 1970, + 'value1971' => 1971, + 'value1972' => 1972, + 'value1973' => 1973, + 'value1974' => 1974, + 'value1975' => 1975, + 'value1976' => 1976, + 'value1977' => 1977, + 'value1978' => 1978, + 'value1979' => 1979, + 'value1980' => 1980, + 'value1981' => 1981, + 'value1982' => 1982, + 'value1983' => 1983, + 'value1984' => 1984, + 'value1985' => 1985, + 'value1986' => 1986, + 'value1987' => 1987, + 'value1988' => 1988, + 'value1989' => 1989, + 'value1990' => 1990, + 'value1991' => 1991, + 'value1992' => 1992, + 'value1993' => 1993, + 'value1994' => 1994, + 'value1995' => 1995, + 'value1996' => 1996, + 'value1997' => 1997, + 'value1998' => 1998, + 'value1999' => 1999, + 'value2000' => 2000, + 'value2001' => 2001, + 'value2002' => 2002, + 'value2003' => 2003, + 'value2004' => 2004, + 'value2005' => 2005, + 'value2006' => 2006, + 'value2007' => 2007, + 'value2008' => 2008, + 'value2009' => 2009, + 'value2010' => 2010, + 'value2011' => 2011, + 'value2012' => 2012, + 'value2013' => 2013, + 'value2014' => 2014, + 'value2015' => 2015, + 'value2016' => 2016, + 'value2017' => 2017, + 'value2018' => 2018, + 'value2019' => 2019, + 'value2020' => 2020, + 'value2021' => 2021, + 'value2022' => 2022, + 'value2023' => 2023, + 'value2024' => 2024, + 'value2025' => 2025, + 'value2026' => 2026, + 'value2027' => 2027, + 'value2028' => 2028, + 'value2029' => 2029, + 'value2030' => 2030, + 'value2031' => 2031, + 'value2032' => 2032, + 'value2033' => 2033, + 'value2034' => 2034, + 'value2035' => 2035, + 'value2036' => 2036, + 'value2037' => 2037, + 'value2038' => 2038, + 'value2039' => 2039, + 'value2040' => 2040, + 'value2041' => 2041, + 'value2042' => 2042, + 'value2043' => 2043, + 'value2044' => 2044, + 'value2045' => 2045, + 'value2046' => 2046, + 'value2047' => 2047, + 'value2048' => 2048, + 'value2049' => 2049, + 'value2050' => 2050, + 'value2051' => 2051, + 'value2052' => 2052, + 'value2053' => 2053, + 'value2054' => 2054, + 'value2055' => 2055, + 'value2056' => 2056, + 'value2057' => 2057, + 'value2058' => 2058, + 'value2059' => 2059, + 'value2060' => 2060, + 'value2061' => 2061, + 'value2062' => 2062, + 'value2063' => 2063, + 'value2064' => 2064, + 'value2065' => 2065, + 'value2066' => 2066, + 'value2067' => 2067, + 'value2068' => 2068, + 'value2069' => 2069, + 'value2070' => 2070, + 'value2071' => 2071, + 'value2072' => 2072, + 'value2073' => 2073, + 'value2074' => 2074, + 'value2075' => 2075, + 'value2076' => 2076, + 'value2077' => 2077, + 'value2078' => 2078, + 'value2079' => 2079, + 'value2080' => 2080, + 'value2081' => 2081, + 'value2082' => 2082, + 'value2083' => 2083, + 'value2084' => 2084, + 'value2085' => 2085, + 'value2086' => 2086, + 'value2087' => 2087, + 'value2088' => 2088, + 'value2089' => 2089, + 'value2090' => 2090, + 'value2091' => 2091, + 'value2092' => 2092, + 'value2093' => 2093, + 'value2094' => 2094, + 'value2095' => 2095, + 'value2096' => 2096, + 'value2097' => 2097, + 'value2098' => 2098, + 'value2099' => 2099, + 'value2100' => 2100, + 'value2101' => 2101, + 'value2102' => 2102, + 'value2103' => 2103, + 'value2104' => 2104, + 'value2105' => 2105, + 'value2106' => 2106, + 'value2107' => 2107, + 'value2108' => 2108, + 'value2109' => 2109, + 'value2110' => 2110, + 'value2111' => 2111, + 'value2112' => 2112, + 'value2113' => 2113, + 'value2114' => 2114, + 'value2115' => 2115, + 'value2116' => 2116, + 'value2117' => 2117, + 'value2118' => 2118, + 'value2119' => 2119, + 'value2120' => 2120, + 'value2121' => 2121, + 'value2122' => 2122, + 'value2123' => 2123, + 'value2124' => 2124, + 'value2125' => 2125, + 'value2126' => 2126, + 'value2127' => 2127, + 'value2128' => 2128, + 'value2129' => 2129, + 'value2130' => 2130, + 'value2131' => 2131, + 'value2132' => 2132, + 'value2133' => 2133, + 'value2134' => 2134, + 'value2135' => 2135, + 'value2136' => 2136, + 'value2137' => 2137, + 'value2138' => 2138, + 'value2139' => 2139, + 'value2140' => 2140, + 'value2141' => 2141, + 'value2142' => 2142, + 'value2143' => 2143, + 'value2144' => 2144, + 'value2145' => 2145, + 'value2146' => 2146, + 'value2147' => 2147, + 'value2148' => 2148, + 'value2149' => 2149, + 'value2150' => 2150, + 'value2151' => 2151, + 'value2152' => 2152, + 'value2153' => 2153, + 'value2154' => 2154, + 'value2155' => 2155, + 'value2156' => 2156, + 'value2157' => 2157, + 'value2158' => 2158, + 'value2159' => 2159, + 'value2160' => 2160, + 'value2161' => 2161, + 'value2162' => 2162, + 'value2163' => 2163, + 'value2164' => 2164, + 'value2165' => 2165, + 'value2166' => 2166, + 'value2167' => 2167, + 'value2168' => 2168, + 'value2169' => 2169, + 'value2170' => 2170, + 'value2171' => 2171, + 'value2172' => 2172, + 'value2173' => 2173, + 'value2174' => 2174, + 'value2175' => 2175, + 'value2176' => 2176, + 'value2177' => 2177, + 'value2178' => 2178, + 'value2179' => 2179, + 'value2180' => 2180, + 'value2181' => 2181, + 'value2182' => 2182, + 'value2183' => 2183, + 'value2184' => 2184, + 'value2185' => 2185, + 'value2186' => 2186, + 'value2187' => 2187, + 'value2188' => 2188, + 'value2189' => 2189, + 'value2190' => 2190, + 'value2191' => 2191, + 'value2192' => 2192, + 'value2193' => 2193, + 'value2194' => 2194, + 'value2195' => 2195, + 'value2196' => 2196, + 'value2197' => 2197, + 'value2198' => 2198, + 'value2199' => 2199, + 'value2200' => 2200, + 'value2201' => 2201, + 'value2202' => 2202, + 'value2203' => 2203, + 'value2204' => 2204, + 'value2205' => 2205, + 'value2206' => 2206, + 'value2207' => 2207, + 'value2208' => 2208, + 'value2209' => 2209, + 'value2210' => 2210, + 'value2211' => 2211, + 'value2212' => 2212, + 'value2213' => 2213, + 'value2214' => 2214, + 'value2215' => 2215, + 'value2216' => 2216, + 'value2217' => 2217, + 'value2218' => 2218, + 'value2219' => 2219, + 'value2220' => 2220, + 'value2221' => 2221, + 'value2222' => 2222, + 'value2223' => 2223, + 'value2224' => 2224, + 'value2225' => 2225, + 'value2226' => 2226, + 'value2227' => 2227, + 'value2228' => 2228, + 'value2229' => 2229, + 'value2230' => 2230, + 'value2231' => 2231, + 'value2232' => 2232, + 'value2233' => 2233, + 'value2234' => 2234, + 'value2235' => 2235, + 'value2236' => 2236, + 'value2237' => 2237, + 'value2238' => 2238, + 'value2239' => 2239, + 'value2240' => 2240, + 'value2241' => 2241, + 'value2242' => 2242, + 'value2243' => 2243, + 'value2244' => 2244, + 'value2245' => 2245, + 'value2246' => 2246, + 'value2247' => 2247, + 'value2248' => 2248, + 'value2249' => 2249, + 'value2250' => 2250, + 'value2251' => 2251, + 'value2252' => 2252, + 'value2253' => 2253, + 'value2254' => 2254, + 'value2255' => 2255, + 'value2256' => 2256, + 'value2257' => 2257, + 'value2258' => 2258, + 'value2259' => 2259, + 'value2260' => 2260, + 'value2261' => 2261, + 'value2262' => 2262, + 'value2263' => 2263, + 'value2264' => 2264, + 'value2265' => 2265, + 'value2266' => 2266, + 'value2267' => 2267, + 'value2268' => 2268, + 'value2269' => 2269, + 'value2270' => 2270, + 'value2271' => 2271, + 'value2272' => 2272, + 'value2273' => 2273, + 'value2274' => 2274, + 'value2275' => 2275, + 'value2276' => 2276, + 'value2277' => 2277, + 'value2278' => 2278, + 'value2279' => 2279, + 'value2280' => 2280, + 'value2281' => 2281, + 'value2282' => 2282, + 'value2283' => 2283, + 'value2284' => 2284, + 'value2285' => 2285, + 'value2286' => 2286, + 'value2287' => 2287, + 'value2288' => 2288, + 'value2289' => 2289, + 'value2290' => 2290, + 'value2291' => 2291, + 'value2292' => 2292, + 'value2293' => 2293, + 'value2294' => 2294, + 'value2295' => 2295, + 'value2296' => 2296, + 'value2297' => 2297, + 'value2298' => 2298, + 'value2299' => 2299, + 'value2300' => 2300, + 'value2301' => 2301, + 'value2302' => 2302, + 'value2303' => 2303, + 'value2304' => 2304, + 'value2305' => 2305, + 'value2306' => 2306, + 'value2307' => 2307, + 'value2308' => 2308, + 'value2309' => 2309, + 'value2310' => 2310, + 'value2311' => 2311, + 'value2312' => 2312, + 'value2313' => 2313, + 'value2314' => 2314, + 'value2315' => 2315, + 'value2316' => 2316, + 'value2317' => 2317, + 'value2318' => 2318, + 'value2319' => 2319, + 'value2320' => 2320, + 'value2321' => 2321, + 'value2322' => 2322, + 'value2323' => 2323, + 'value2324' => 2324, + 'value2325' => 2325, + 'value2326' => 2326, + 'value2327' => 2327, + 'value2328' => 2328, + 'value2329' => 2329, + 'value2330' => 2330, + 'value2331' => 2331, + 'value2332' => 2332, + 'value2333' => 2333, + 'value2334' => 2334, + 'value2335' => 2335, + 'value2336' => 2336, + 'value2337' => 2337, + 'value2338' => 2338, + 'value2339' => 2339, + 'value2340' => 2340, + 'value2341' => 2341, + 'value2342' => 2342, + 'value2343' => 2343, + 'value2344' => 2344, + 'value2345' => 2345, + 'value2346' => 2346, + 'value2347' => 2347, + 'value2348' => 2348, + 'value2349' => 2349, + 'value2350' => 2350, + 'value2351' => 2351, + 'value2352' => 2352, + 'value2353' => 2353, + 'value2354' => 2354, + 'value2355' => 2355, + 'value2356' => 2356, + 'value2357' => 2357, + 'value2358' => 2358, + 'value2359' => 2359, + 'value2360' => 2360, + 'value2361' => 2361, + 'value2362' => 2362, + 'value2363' => 2363, + 'value2364' => 2364, + 'value2365' => 2365, + 'value2366' => 2366, + 'value2367' => 2367, + 'value2368' => 2368, + 'value2369' => 2369, + 'value2370' => 2370, + 'value2371' => 2371, + 'value2372' => 2372, + 'value2373' => 2373, + 'value2374' => 2374, + 'value2375' => 2375, + 'value2376' => 2376, + 'value2377' => 2377, + 'value2378' => 2378, + 'value2379' => 2379, + 'value2380' => 2380, + 'value2381' => 2381, + 'value2382' => 2382, + 'value2383' => 2383, + 'value2384' => 2384, + 'value2385' => 2385, + 'value2386' => 2386, + 'value2387' => 2387, + 'value2388' => 2388, + 'value2389' => 2389, + 'value2390' => 2390, + 'value2391' => 2391, + 'value2392' => 2392, + 'value2393' => 2393, + 'value2394' => 2394, + 'value2395' => 2395, + 'value2396' => 2396, + 'value2397' => 2397, + 'value2398' => 2398, + 'value2399' => 2399, + 'value2400' => 2400, + 'value2401' => 2401, + 'value2402' => 2402, + 'value2403' => 2403, + 'value2404' => 2404, + 'value2405' => 2405, + 'value2406' => 2406, + 'value2407' => 2407, + 'value2408' => 2408, + 'value2409' => 2409, + 'value2410' => 2410, + 'value2411' => 2411, + 'value2412' => 2412, + 'value2413' => 2413, + 'value2414' => 2414, + 'value2415' => 2415, + 'value2416' => 2416, + 'value2417' => 2417, + 'value2418' => 2418, + 'value2419' => 2419, + 'value2420' => 2420, + 'value2421' => 2421, + 'value2422' => 2422, + 'value2423' => 2423, + 'value2424' => 2424, + 'value2425' => 2425, + 'value2426' => 2426, + 'value2427' => 2427, + 'value2428' => 2428, + 'value2429' => 2429, + 'value2430' => 2430, + 'value2431' => 2431, + 'value2432' => 2432, + 'value2433' => 2433, + 'value2434' => 2434, + 'value2435' => 2435, + 'value2436' => 2436, + 'value2437' => 2437, + 'value2438' => 2438, + 'value2439' => 2439, + 'value2440' => 2440, + 'value2441' => 2441, + 'value2442' => 2442, + 'value2443' => 2443, + 'value2444' => 2444, + 'value2445' => 2445, + 'value2446' => 2446, + 'value2447' => 2447, + 'value2448' => 2448, + 'value2449' => 2449, + 'value2450' => 2450, + 'value2451' => 2451, + 'value2452' => 2452, + 'value2453' => 2453, + 'value2454' => 2454, + 'value2455' => 2455, + 'value2456' => 2456, + 'value2457' => 2457, + 'value2458' => 2458, + 'value2459' => 2459, + 'value2460' => 2460, + 'value2461' => 2461, + 'value2462' => 2462, + 'value2463' => 2463, + 'value2464' => 2464, + 'value2465' => 2465, + 'value2466' => 2466, + 'value2467' => 2467, + 'value2468' => 2468, + 'value2469' => 2469, + 'value2470' => 2470, + 'value2471' => 2471, + 'value2472' => 2472, + 'value2473' => 2473, + 'value2474' => 2474, + 'value2475' => 2475, + 'value2476' => 2476, + 'value2477' => 2477, + 'value2478' => 2478, + 'value2479' => 2479, + 'value2480' => 2480, + 'value2481' => 2481, + 'value2482' => 2482, + 'value2483' => 2483, + 'value2484' => 2484, + 'value2485' => 2485, + 'value2486' => 2486, + 'value2487' => 2487, + 'value2488' => 2488, + 'value2489' => 2489, + 'value2490' => 2490, + 'value2491' => 2491, + 'value2492' => 2492, + 'value2493' => 2493, + 'value2494' => 2494, + 'value2495' => 2495, + 'value2496' => 2496, + 'value2497' => 2497, + 'value2498' => 2498, + 'value2499' => 2499, + 'value2500' => 2500, + 'value2501' => 2501, + 'value2502' => 2502, + 'value2503' => 2503, + 'value2504' => 2504, + 'value2505' => 2505, + 'value2506' => 2506, + 'value2507' => 2507, + 'value2508' => 2508, + 'value2509' => 2509, + 'value2510' => 2510, + 'value2511' => 2511, + 'value2512' => 2512, + 'value2513' => 2513, + 'value2514' => 2514, + 'value2515' => 2515, + 'value2516' => 2516, + 'value2517' => 2517, + 'value2518' => 2518, + 'value2519' => 2519, + 'value2520' => 2520, + 'value2521' => 2521, + 'value2522' => 2522, + 'value2523' => 2523, + 'value2524' => 2524, + 'value2525' => 2525, + 'value2526' => 2526, + 'value2527' => 2527, + 'value2528' => 2528, + 'value2529' => 2529, + 'value2530' => 2530, + 'value2531' => 2531, + 'value2532' => 2532, + 'value2533' => 2533, + 'value2534' => 2534, + 'value2535' => 2535, + 'value2536' => 2536, + 'value2537' => 2537, + 'value2538' => 2538, + 'value2539' => 2539, + 'value2540' => 2540, + 'value2541' => 2541, + 'value2542' => 2542, + 'value2543' => 2543, + 'value2544' => 2544, + 'value2545' => 2545, + 'value2546' => 2546, + 'value2547' => 2547, + 'value2548' => 2548, + 'value2549' => 2549, + 'value2550' => 2550, + 'value2551' => 2551, + 'value2552' => 2552, + 'value2553' => 2553, + 'value2554' => 2554, + 'value2555' => 2555, + 'value2556' => 2556, + 'value2557' => 2557, + 'value2558' => 2558, + 'value2559' => 2559, + 'value2560' => 2560, + 'value2561' => 2561, + 'value2562' => 2562, + 'value2563' => 2563, + 'value2564' => 2564, + 'value2565' => 2565, + 'value2566' => 2566, + 'value2567' => 2567, + 'value2568' => 2568, + 'value2569' => 2569, + 'value2570' => 2570, + 'value2571' => 2571, + 'value2572' => 2572, + 'value2573' => 2573, + 'value2574' => 2574, + 'value2575' => 2575, + 'value2576' => 2576, + 'value2577' => 2577, + 'value2578' => 2578, + 'value2579' => 2579, + 'value2580' => 2580, + 'value2581' => 2581, + 'value2582' => 2582, + 'value2583' => 2583, + 'value2584' => 2584, + 'value2585' => 2585, + 'value2586' => 2586, + 'value2587' => 2587, + 'value2588' => 2588, + 'value2589' => 2589, + 'value2590' => 2590, + 'value2591' => 2591, + 'value2592' => 2592, + 'value2593' => 2593, + 'value2594' => 2594, + 'value2595' => 2595, + 'value2596' => 2596, + 'value2597' => 2597, + 'value2598' => 2598, + 'value2599' => 2599, + 'value2600' => 2600, + 'value2601' => 2601, + 'value2602' => 2602, + 'value2603' => 2603, + 'value2604' => 2604, + 'value2605' => 2605, + 'value2606' => 2606, + 'value2607' => 2607, + 'value2608' => 2608, + 'value2609' => 2609, + 'value2610' => 2610, + 'value2611' => 2611, + 'value2612' => 2612, + 'value2613' => 2613, + 'value2614' => 2614, + 'value2615' => 2615, + 'value2616' => 2616, + 'value2617' => 2617, + 'value2618' => 2618, + 'value2619' => 2619, + 'value2620' => 2620, + 'value2621' => 2621, + 'value2622' => 2622, + 'value2623' => 2623, + 'value2624' => 2624, + 'value2625' => 2625, + 'value2626' => 2626, + 'value2627' => 2627, + 'value2628' => 2628, + 'value2629' => 2629, + 'value2630' => 2630, + 'value2631' => 2631, + 'value2632' => 2632, + 'value2633' => 2633, + 'value2634' => 2634, + 'value2635' => 2635, + 'value2636' => 2636, + 'value2637' => 2637, + 'value2638' => 2638, + 'value2639' => 2639, + 'value2640' => 2640, + 'value2641' => 2641, + 'value2642' => 2642, + 'value2643' => 2643, + 'value2644' => 2644, + 'value2645' => 2645, + 'value2646' => 2646, + 'value2647' => 2647, + 'value2648' => 2648, + 'value2649' => 2649, + 'value2650' => 2650, + 'value2651' => 2651, + 'value2652' => 2652, + 'value2653' => 2653, + 'value2654' => 2654, + 'value2655' => 2655, + 'value2656' => 2656, + 'value2657' => 2657, + 'value2658' => 2658, + 'value2659' => 2659, + 'value2660' => 2660, + 'value2661' => 2661, + 'value2662' => 2662, + 'value2663' => 2663, + 'value2664' => 2664, + 'value2665' => 2665, + 'value2666' => 2666, + 'value2667' => 2667, + 'value2668' => 2668, + 'value2669' => 2669, + 'value2670' => 2670, + 'value2671' => 2671, + 'value2672' => 2672, + 'value2673' => 2673, + 'value2674' => 2674, + 'value2675' => 2675, + 'value2676' => 2676, + 'value2677' => 2677, + 'value2678' => 2678, + 'value2679' => 2679, + 'value2680' => 2680, + 'value2681' => 2681, + 'value2682' => 2682, + 'value2683' => 2683, + 'value2684' => 2684, + 'value2685' => 2685, + 'value2686' => 2686, + 'value2687' => 2687, + 'value2688' => 2688, + 'value2689' => 2689, + 'value2690' => 2690, + 'value2691' => 2691, + 'value2692' => 2692, + 'value2693' => 2693, + 'value2694' => 2694, + 'value2695' => 2695, + 'value2696' => 2696, + 'value2697' => 2697, + 'value2698' => 2698, + 'value2699' => 2699, + 'value2700' => 2700, + 'value2701' => 2701, + 'value2702' => 2702, + 'value2703' => 2703, + 'value2704' => 2704, + 'value2705' => 2705, + 'value2706' => 2706, + 'value2707' => 2707, + 'value2708' => 2708, + 'value2709' => 2709, + 'value2710' => 2710, + 'value2711' => 2711, + 'value2712' => 2712, + 'value2713' => 2713, + 'value2714' => 2714, + 'value2715' => 2715, + 'value2716' => 2716, + 'value2717' => 2717, + 'value2718' => 2718, + 'value2719' => 2719, + 'value2720' => 2720, + 'value2721' => 2721, + 'value2722' => 2722, + 'value2723' => 2723, + 'value2724' => 2724, + 'value2725' => 2725, + 'value2726' => 2726, + 'value2727' => 2727, + 'value2728' => 2728, + 'value2729' => 2729, + 'value2730' => 2730, + 'value2731' => 2731, + 'value2732' => 2732, + 'value2733' => 2733, + 'value2734' => 2734, + 'value2735' => 2735, + 'value2736' => 2736, + 'value2737' => 2737, + 'value2738' => 2738, + 'value2739' => 2739, + 'value2740' => 2740, + 'value2741' => 2741, + 'value2742' => 2742, + 'value2743' => 2743, + 'value2744' => 2744, + 'value2745' => 2745, + 'value2746' => 2746, + 'value2747' => 2747, + 'value2748' => 2748, + 'value2749' => 2749, + 'value2750' => 2750, + 'value2751' => 2751, + 'value2752' => 2752, + 'value2753' => 2753, + 'value2754' => 2754, + 'value2755' => 2755, + 'value2756' => 2756, + 'value2757' => 2757, + 'value2758' => 2758, + 'value2759' => 2759, + 'value2760' => 2760, + 'value2761' => 2761, + 'value2762' => 2762, + 'value2763' => 2763, + 'value2764' => 2764, + 'value2765' => 2765, + 'value2766' => 2766, + 'value2767' => 2767, + 'value2768' => 2768, + 'value2769' => 2769, + 'value2770' => 2770, + 'value2771' => 2771, + 'value2772' => 2772, + 'value2773' => 2773, + 'value2774' => 2774, + 'value2775' => 2775, + 'value2776' => 2776, + 'value2777' => 2777, + 'value2778' => 2778, + 'value2779' => 2779, + 'value2780' => 2780, + 'value2781' => 2781, + 'value2782' => 2782, + 'value2783' => 2783, + 'value2784' => 2784, + 'value2785' => 2785, + 'value2786' => 2786, + 'value2787' => 2787, + 'value2788' => 2788, + 'value2789' => 2789, + 'value2790' => 2790, + 'value2791' => 2791, + 'value2792' => 2792, + 'value2793' => 2793, + 'value2794' => 2794, + 'value2795' => 2795, + 'value2796' => 2796, + 'value2797' => 2797, + 'value2798' => 2798, + 'value2799' => 2799, + 'value2800' => 2800, + 'value2801' => 2801, + 'value2802' => 2802, + 'value2803' => 2803, + 'value2804' => 2804, + 'value2805' => 2805, + 'value2806' => 2806, + 'value2807' => 2807, + 'value2808' => 2808, + 'value2809' => 2809, + 'value2810' => 2810, + 'value2811' => 2811, + 'value2812' => 2812, + 'value2813' => 2813, + 'value2814' => 2814, + 'value2815' => 2815, + 'value2816' => 2816, + 'value2817' => 2817, + 'value2818' => 2818, + 'value2819' => 2819, + 'value2820' => 2820, + 'value2821' => 2821, + 'value2822' => 2822, + 'value2823' => 2823, + 'value2824' => 2824, + 'value2825' => 2825, + 'value2826' => 2826, + 'value2827' => 2827, + 'value2828' => 2828, + 'value2829' => 2829, + 'value2830' => 2830, + 'value2831' => 2831, + 'value2832' => 2832, + 'value2833' => 2833, + 'value2834' => 2834, + 'value2835' => 2835, + 'value2836' => 2836, + 'value2837' => 2837, + 'value2838' => 2838, + 'value2839' => 2839, + 'value2840' => 2840, + 'value2841' => 2841, + 'value2842' => 2842, + 'value2843' => 2843, + 'value2844' => 2844, + 'value2845' => 2845, + 'value2846' => 2846, + 'value2847' => 2847, + 'value2848' => 2848, + 'value2849' => 2849, + 'value2850' => 2850, + 'value2851' => 2851, + 'value2852' => 2852, + 'value2853' => 2853, + 'value2854' => 2854, + 'value2855' => 2855, + 'value2856' => 2856, + 'value2857' => 2857, + 'value2858' => 2858, + 'value2859' => 2859, + 'value2860' => 2860, + 'value2861' => 2861, + 'value2862' => 2862, + 'value2863' => 2863, + 'value2864' => 2864, + 'value2865' => 2865, + 'value2866' => 2866, + 'value2867' => 2867, + 'value2868' => 2868, + 'value2869' => 2869, + 'value2870' => 2870, + 'value2871' => 2871, + 'value2872' => 2872, + 'value2873' => 2873, + 'value2874' => 2874, + 'value2875' => 2875, + 'value2876' => 2876, + 'value2877' => 2877, + 'value2878' => 2878, + 'value2879' => 2879, + 'value2880' => 2880, + 'value2881' => 2881, + 'value2882' => 2882, + 'value2883' => 2883, + 'value2884' => 2884, + 'value2885' => 2885, + 'value2886' => 2886, + 'value2887' => 2887, + 'value2888' => 2888, + 'value2889' => 2889, + 'value2890' => 2890, + 'value2891' => 2891, + 'value2892' => 2892, + 'value2893' => 2893, + 'value2894' => 2894, + 'value2895' => 2895, + 'value2896' => 2896, + 'value2897' => 2897, + 'value2898' => 2898, + 'value2899' => 2899, + 'value2900' => 2900, + 'value2901' => 2901, + 'value2902' => 2902, + 'value2903' => 2903, + 'value2904' => 2904, + 'value2905' => 2905, + 'value2906' => 2906, + 'value2907' => 2907, + 'value2908' => 2908, + 'value2909' => 2909, + 'value2910' => 2910, + 'value2911' => 2911, + 'value2912' => 2912, + 'value2913' => 2913, + 'value2914' => 2914, + 'value2915' => 2915, + 'value2916' => 2916, + 'value2917' => 2917, + 'value2918' => 2918, + 'value2919' => 2919, + 'value2920' => 2920, + 'value2921' => 2921, + 'value2922' => 2922, + 'value2923' => 2923, + 'value2924' => 2924, + 'value2925' => 2925, + 'value2926' => 2926, + 'value2927' => 2927, + 'value2928' => 2928, + 'value2929' => 2929, + 'value2930' => 2930, + 'value2931' => 2931, + 'value2932' => 2932, + 'value2933' => 2933, + 'value2934' => 2934, + 'value2935' => 2935, + 'value2936' => 2936, + 'value2937' => 2937, + 'value2938' => 2938, + 'value2939' => 2939, + 'value2940' => 2940, + 'value2941' => 2941, + 'value2942' => 2942, + 'value2943' => 2943, + 'value2944' => 2944, + 'value2945' => 2945, + 'value2946' => 2946, + 'value2947' => 2947, + 'value2948' => 2948, + 'value2949' => 2949, + 'value2950' => 2950, + 'value2951' => 2951, + 'value2952' => 2952, + 'value2953' => 2953, + 'value2954' => 2954, + 'value2955' => 2955, + 'value2956' => 2956, + 'value2957' => 2957, + 'value2958' => 2958, + 'value2959' => 2959, + 'value2960' => 2960, + 'value2961' => 2961, + 'value2962' => 2962, + 'value2963' => 2963, + 'value2964' => 2964, + 'value2965' => 2965, + 'value2966' => 2966, + 'value2967' => 2967, + 'value2968' => 2968, + 'value2969' => 2969, + 'value2970' => 2970, + 'value2971' => 2971, + 'value2972' => 2972, + 'value2973' => 2973, + 'value2974' => 2974, + 'value2975' => 2975, + 'value2976' => 2976, + 'value2977' => 2977, + 'value2978' => 2978, + 'value2979' => 2979, + 'value2980' => 2980, + 'value2981' => 2981, + 'value2982' => 2982, + 'value2983' => 2983, + 'value2984' => 2984, + 'value2985' => 2985, + 'value2986' => 2986, + 'value2987' => 2987, + 'value2988' => 2988, + 'value2989' => 2989, + 'value2990' => 2990, + 'value2991' => 2991, + 'value2992' => 2992, + 'value2993' => 2993, + 'value2994' => 2994, + 'value2995' => 2995, + 'value2996' => 2996, + 'value2997' => 2997, + 'value2998' => 2998, + 'value2999' => 2999, + 'value3000' => 3000, + 'value3001' => 3001, + 'value3002' => 3002, + 'value3003' => 3003, + 'value3004' => 3004, + 'value3005' => 3005, + 'value3006' => 3006, + 'value3007' => 3007, + 'value3008' => 3008, + 'value3009' => 3009, + 'value3010' => 3010, + 'value3011' => 3011, + 'value3012' => 3012, + 'value3013' => 3013, + 'value3014' => 3014, + 'value3015' => 3015, + 'value3016' => 3016, + 'value3017' => 3017, + 'value3018' => 3018, + 'value3019' => 3019, + 'value3020' => 3020, + 'value3021' => 3021, + 'value3022' => 3022, + 'value3023' => 3023, + 'value3024' => 3024, + 'value3025' => 3025, + 'value3026' => 3026, + 'value3027' => 3027, + 'value3028' => 3028, + 'value3029' => 3029, + 'value3030' => 3030, + 'value3031' => 3031, + 'value3032' => 3032, + 'value3033' => 3033, + 'value3034' => 3034, + 'value3035' => 3035, + 'value3036' => 3036, + 'value3037' => 3037, + 'value3038' => 3038, + 'value3039' => 3039, + 'value3040' => 3040, + 'value3041' => 3041, + 'value3042' => 3042, + 'value3043' => 3043, + 'value3044' => 3044, + 'value3045' => 3045, + 'value3046' => 3046, + 'value3047' => 3047, + 'value3048' => 3048, + 'value3049' => 3049, + 'value3050' => 3050, + 'value3051' => 3051, + 'value3052' => 3052, + 'value3053' => 3053, + 'value3054' => 3054, + 'value3055' => 3055, + 'value3056' => 3056, + 'value3057' => 3057, + 'value3058' => 3058, + 'value3059' => 3059, + 'value3060' => 3060, + 'value3061' => 3061, + 'value3062' => 3062, + 'value3063' => 3063, + 'value3064' => 3064, + 'value3065' => 3065, + 'value3066' => 3066, + 'value3067' => 3067, + 'value3068' => 3068, + 'value3069' => 3069, + 'value3070' => 3070, + 'value3071' => 3071, + 'value3072' => 3072, + 'value3073' => 3073, + 'value3074' => 3074, + 'value3075' => 3075, + 'value3076' => 3076, + 'value3077' => 3077, + 'value3078' => 3078, + 'value3079' => 3079, + 'value3080' => 3080, + 'value3081' => 3081, + 'value3082' => 3082, + 'value3083' => 3083, + 'value3084' => 3084, + 'value3085' => 3085, + 'value3086' => 3086, + 'value3087' => 3087, + 'value3088' => 3088, + 'value3089' => 3089, + 'value3090' => 3090, + 'value3091' => 3091, + 'value3092' => 3092, + 'value3093' => 3093, + 'value3094' => 3094, + 'value3095' => 3095, + 'value3096' => 3096, + 'value3097' => 3097, + 'value3098' => 3098, + 'value3099' => 3099, + 'value3100' => 3100, + 'value3101' => 3101, + 'value3102' => 3102, + 'value3103' => 3103, + 'value3104' => 3104, + 'value3105' => 3105, + 'value3106' => 3106, + 'value3107' => 3107, + 'value3108' => 3108, + 'value3109' => 3109, + 'value3110' => 3110, + 'value3111' => 3111, + 'value3112' => 3112, + 'value3113' => 3113, + 'value3114' => 3114, + 'value3115' => 3115, + 'value3116' => 3116, + 'value3117' => 3117, + 'value3118' => 3118, + 'value3119' => 3119, + 'value3120' => 3120, + 'value3121' => 3121, + 'value3122' => 3122, + 'value3123' => 3123, + 'value3124' => 3124, + 'value3125' => 3125, + 'value3126' => 3126, + 'value3127' => 3127, + 'value3128' => 3128, + 'value3129' => 3129, + 'value3130' => 3130, + 'value3131' => 3131, + 'value3132' => 3132, + 'value3133' => 3133, + 'value3134' => 3134, + 'value3135' => 3135, + 'value3136' => 3136, + 'value3137' => 3137, + 'value3138' => 3138, + 'value3139' => 3139, + 'value3140' => 3140, + 'value3141' => 3141, + 'value3142' => 3142, + 'value3143' => 3143, + 'value3144' => 3144, + 'value3145' => 3145, + 'value3146' => 3146, + 'value3147' => 3147, + 'value3148' => 3148, + 'value3149' => 3149, + 'value3150' => 3150, + 'value3151' => 3151, + 'value3152' => 3152, + 'value3153' => 3153, + 'value3154' => 3154, + 'value3155' => 3155, + 'value3156' => 3156, + 'value3157' => 3157, + 'value3158' => 3158, + 'value3159' => 3159, + 'value3160' => 3160, + 'value3161' => 3161, + 'value3162' => 3162, + 'value3163' => 3163, + 'value3164' => 3164, + 'value3165' => 3165, + 'value3166' => 3166, + 'value3167' => 3167, + 'value3168' => 3168, + 'value3169' => 3169, + 'value3170' => 3170, + 'value3171' => 3171, + 'value3172' => 3172, + 'value3173' => 3173, + 'value3174' => 3174, + 'value3175' => 3175, + 'value3176' => 3176, + 'value3177' => 3177, + 'value3178' => 3178, + 'value3179' => 3179, + 'value3180' => 3180, + 'value3181' => 3181, + 'value3182' => 3182, + 'value3183' => 3183, + 'value3184' => 3184, + 'value3185' => 3185, + 'value3186' => 3186, + 'value3187' => 3187, + 'value3188' => 3188, + 'value3189' => 3189, + 'value3190' => 3190, + 'value3191' => 3191, + 'value3192' => 3192, + 'value3193' => 3193, + 'value3194' => 3194, + 'value3195' => 3195, + 'value3196' => 3196, + 'value3197' => 3197, + 'value3198' => 3198, + 'value3199' => 3199, + 'value3200' => 3200, + 'value3201' => 3201, + 'value3202' => 3202, + 'value3203' => 3203, + 'value3204' => 3204, + 'value3205' => 3205, + 'value3206' => 3206, + 'value3207' => 3207, + 'value3208' => 3208, + 'value3209' => 3209, + 'value3210' => 3210, + 'value3211' => 3211, + 'value3212' => 3212, + 'value3213' => 3213, + 'value3214' => 3214, + 'value3215' => 3215, + 'value3216' => 3216, + 'value3217' => 3217, + 'value3218' => 3218, + 'value3219' => 3219, + 'value3220' => 3220, + 'value3221' => 3221, + 'value3222' => 3222, + 'value3223' => 3223, + 'value3224' => 3224, + 'value3225' => 3225, + 'value3226' => 3226, + 'value3227' => 3227, + 'value3228' => 3228, + 'value3229' => 3229, + 'value3230' => 3230, + 'value3231' => 3231, + 'value3232' => 3232, + 'value3233' => 3233, + 'value3234' => 3234, + 'value3235' => 3235, + 'value3236' => 3236, + 'value3237' => 3237, + 'value3238' => 3238, + 'value3239' => 3239, + 'value3240' => 3240, + 'value3241' => 3241, + 'value3242' => 3242, + 'value3243' => 3243, + 'value3244' => 3244, + 'value3245' => 3245, + 'value3246' => 3246, + 'value3247' => 3247, + 'value3248' => 3248, + 'value3249' => 3249, + 'value3250' => 3250, + 'value3251' => 3251, + 'value3252' => 3252, + 'value3253' => 3253, + 'value3254' => 3254, + 'value3255' => 3255, + 'value3256' => 3256, + 'value3257' => 3257, + 'value3258' => 3258, + 'value3259' => 3259, + 'value3260' => 3260, + 'value3261' => 3261, + 'value3262' => 3262, + 'value3263' => 3263, + 'value3264' => 3264, + 'value3265' => 3265, + 'value3266' => 3266, + 'value3267' => 3267, + 'value3268' => 3268, + 'value3269' => 3269, + 'value3270' => 3270, + 'value3271' => 3271, + 'value3272' => 3272, + 'value3273' => 3273, + 'value3274' => 3274, + 'value3275' => 3275, + 'value3276' => 3276, + 'value3277' => 3277, + 'value3278' => 3278, + 'value3279' => 3279, + 'value3280' => 3280, + 'value3281' => 3281, + 'value3282' => 3282, + 'value3283' => 3283, + 'value3284' => 3284, + 'value3285' => 3285, + 'value3286' => 3286, + 'value3287' => 3287, + 'value3288' => 3288, + 'value3289' => 3289, + 'value3290' => 3290, + 'value3291' => 3291, + 'value3292' => 3292, + 'value3293' => 3293, + 'value3294' => 3294, + 'value3295' => 3295, + 'value3296' => 3296, + 'value3297' => 3297, + 'value3298' => 3298, + 'value3299' => 3299, + 'value3300' => 3300, + 'value3301' => 3301, + 'value3302' => 3302, + 'value3303' => 3303, + 'value3304' => 3304, + 'value3305' => 3305, + 'value3306' => 3306, + 'value3307' => 3307, + 'value3308' => 3308, + 'value3309' => 3309, + 'value3310' => 3310, + 'value3311' => 3311, + 'value3312' => 3312, + 'value3313' => 3313, + 'value3314' => 3314, + 'value3315' => 3315, + 'value3316' => 3316, + 'value3317' => 3317, + 'value3318' => 3318, + 'value3319' => 3319, + 'value3320' => 3320, + 'value3321' => 3321, + 'value3322' => 3322, + 'value3323' => 3323, + 'value3324' => 3324, + 'value3325' => 3325, + 'value3326' => 3326, + 'value3327' => 3327, + 'value3328' => 3328, + 'value3329' => 3329, + 'value3330' => 3330, + 'value3331' => 3331, + 'value3332' => 3332, + 'value3333' => 3333, + 'value3334' => 3334, + 'value3335' => 3335, + 'value3336' => 3336, + 'value3337' => 3337, + 'value3338' => 3338, + 'value3339' => 3339, + 'value3340' => 3340, + 'value3341' => 3341, + 'value3342' => 3342, + 'value3343' => 3343, + 'value3344' => 3344, + 'value3345' => 3345, + 'value3346' => 3346, + 'value3347' => 3347, + 'value3348' => 3348, + 'value3349' => 3349, + 'value3350' => 3350, + 'value3351' => 3351, + 'value3352' => 3352, + 'value3353' => 3353, + 'value3354' => 3354, + 'value3355' => 3355, + 'value3356' => 3356, + 'value3357' => 3357, + 'value3358' => 3358, + 'value3359' => 3359, + 'value3360' => 3360, + 'value3361' => 3361, + 'value3362' => 3362, + 'value3363' => 3363, + 'value3364' => 3364, + 'value3365' => 3365, + 'value3366' => 3366, + 'value3367' => 3367, + 'value3368' => 3368, + 'value3369' => 3369, + 'value3370' => 3370, + 'value3371' => 3371, + 'value3372' => 3372, + 'value3373' => 3373, + 'value3374' => 3374, + 'value3375' => 3375, + 'value3376' => 3376, + 'value3377' => 3377, + 'value3378' => 3378, + 'value3379' => 3379, + 'value3380' => 3380, + 'value3381' => 3381, + 'value3382' => 3382, + 'value3383' => 3383, + 'value3384' => 3384, + 'value3385' => 3385, + 'value3386' => 3386, + 'value3387' => 3387, + 'value3388' => 3388, + 'value3389' => 3389, + 'value3390' => 3390, + 'value3391' => 3391, + 'value3392' => 3392, + 'value3393' => 3393, + 'value3394' => 3394, + 'value3395' => 3395, + 'value3396' => 3396, + 'value3397' => 3397, + 'value3398' => 3398, + 'value3399' => 3399, + 'value3400' => 3400, + 'value3401' => 3401, + 'value3402' => 3402, + 'value3403' => 3403, + 'value3404' => 3404, + 'value3405' => 3405, + 'value3406' => 3406, + 'value3407' => 3407, + 'value3408' => 3408, + 'value3409' => 3409, + 'value3410' => 3410, + 'value3411' => 3411, + 'value3412' => 3412, + 'value3413' => 3413, + 'value3414' => 3414, + 'value3415' => 3415, + 'value3416' => 3416, + 'value3417' => 3417, + 'value3418' => 3418, + 'value3419' => 3419, + 'value3420' => 3420, + 'value3421' => 3421, + 'value3422' => 3422, + 'value3423' => 3423, + 'value3424' => 3424, + 'value3425' => 3425, + 'value3426' => 3426, + 'value3427' => 3427, + 'value3428' => 3428, + 'value3429' => 3429, + 'value3430' => 3430, + 'value3431' => 3431, + 'value3432' => 3432, + 'value3433' => 3433, + 'value3434' => 3434, + 'value3435' => 3435, + 'value3436' => 3436, + 'value3437' => 3437, + 'value3438' => 3438, + 'value3439' => 3439, + 'value3440' => 3440, + 'value3441' => 3441, + 'value3442' => 3442, + 'value3443' => 3443, + 'value3444' => 3444, + 'value3445' => 3445, + 'value3446' => 3446, + 'value3447' => 3447, + 'value3448' => 3448, + 'value3449' => 3449, + 'value3450' => 3450, + 'value3451' => 3451, + 'value3452' => 3452, + 'value3453' => 3453, + 'value3454' => 3454, + 'value3455' => 3455, + 'value3456' => 3456, + 'value3457' => 3457, + 'value3458' => 3458, + 'value3459' => 3459, + 'value3460' => 3460, + 'value3461' => 3461, + 'value3462' => 3462, + 'value3463' => 3463, + 'value3464' => 3464, + 'value3465' => 3465, + 'value3466' => 3466, + 'value3467' => 3467, + 'value3468' => 3468, + 'value3469' => 3469, + 'value3470' => 3470, + 'value3471' => 3471, + 'value3472' => 3472, + 'value3473' => 3473, + 'value3474' => 3474, + 'value3475' => 3475, + 'value3476' => 3476, + 'value3477' => 3477, + 'value3478' => 3478, + 'value3479' => 3479, + 'value3480' => 3480, + 'value3481' => 3481, + 'value3482' => 3482, + 'value3483' => 3483, + 'value3484' => 3484, + 'value3485' => 3485, + 'value3486' => 3486, + 'value3487' => 3487, + 'value3488' => 3488, + 'value3489' => 3489, + 'value3490' => 3490, + 'value3491' => 3491, + 'value3492' => 3492, + 'value3493' => 3493, + 'value3494' => 3494, + 'value3495' => 3495, + 'value3496' => 3496, + 'value3497' => 3497, + 'value3498' => 3498, + 'value3499' => 3499, + 'value3500' => 3500, + 'value3501' => 3501, + 'value3502' => 3502, + 'value3503' => 3503, + 'value3504' => 3504, + 'value3505' => 3505, + 'value3506' => 3506, + 'value3507' => 3507, + 'value3508' => 3508, + 'value3509' => 3509, + 'value3510' => 3510, + 'value3511' => 3511, + 'value3512' => 3512, + 'value3513' => 3513, + 'value3514' => 3514, + 'value3515' => 3515, + 'value3516' => 3516, + 'value3517' => 3517, + 'value3518' => 3518, + 'value3519' => 3519, + 'value3520' => 3520, + 'value3521' => 3521, + 'value3522' => 3522, + 'value3523' => 3523, + 'value3524' => 3524, + 'value3525' => 3525, + 'value3526' => 3526, + 'value3527' => 3527, + 'value3528' => 3528, + 'value3529' => 3529, + 'value3530' => 3530, + 'value3531' => 3531, + 'value3532' => 3532, + 'value3533' => 3533, + 'value3534' => 3534, + 'value3535' => 3535, + 'value3536' => 3536, + 'value3537' => 3537, + 'value3538' => 3538, + 'value3539' => 3539, + 'value3540' => 3540, + 'value3541' => 3541, + 'value3542' => 3542, + 'value3543' => 3543, + 'value3544' => 3544, + 'value3545' => 3545, + 'value3546' => 3546, + 'value3547' => 3547, + 'value3548' => 3548, + 'value3549' => 3549, + 'value3550' => 3550, + 'value3551' => 3551, + 'value3552' => 3552, + 'value3553' => 3553, + 'value3554' => 3554, + 'value3555' => 3555, + 'value3556' => 3556, + 'value3557' => 3557, + 'value3558' => 3558, + 'value3559' => 3559, + 'value3560' => 3560, + 'value3561' => 3561, + 'value3562' => 3562, + 'value3563' => 3563, + 'value3564' => 3564, + 'value3565' => 3565, + 'value3566' => 3566, + 'value3567' => 3567, + 'value3568' => 3568, + 'value3569' => 3569, + 'value3570' => 3570, + 'value3571' => 3571, + 'value3572' => 3572, + 'value3573' => 3573, + 'value3574' => 3574, + 'value3575' => 3575, + 'value3576' => 3576, + 'value3577' => 3577, + 'value3578' => 3578, + 'value3579' => 3579, + 'value3580' => 3580, + 'value3581' => 3581, + 'value3582' => 3582, + 'value3583' => 3583, + 'value3584' => 3584, + 'value3585' => 3585, + 'value3586' => 3586, + 'value3587' => 3587, + 'value3588' => 3588, + 'value3589' => 3589, + 'value3590' => 3590, + 'value3591' => 3591, + 'value3592' => 3592, + 'value3593' => 3593, + 'value3594' => 3594, + 'value3595' => 3595, + 'value3596' => 3596, + 'value3597' => 3597, + 'value3598' => 3598, + 'value3599' => 3599, + 'value3600' => 3600, + 'value3601' => 3601, + 'value3602' => 3602, + 'value3603' => 3603, + 'value3604' => 3604, + 'value3605' => 3605, + 'value3606' => 3606, + 'value3607' => 3607, + 'value3608' => 3608, + 'value3609' => 3609, + 'value3610' => 3610, + 'value3611' => 3611, + 'value3612' => 3612, + 'value3613' => 3613, + 'value3614' => 3614, + 'value3615' => 3615, + 'value3616' => 3616, + 'value3617' => 3617, + 'value3618' => 3618, + 'value3619' => 3619, + 'value3620' => 3620, + 'value3621' => 3621, + 'value3622' => 3622, + 'value3623' => 3623, + 'value3624' => 3624, + 'value3625' => 3625, + 'value3626' => 3626, + 'value3627' => 3627, + 'value3628' => 3628, + 'value3629' => 3629, + 'value3630' => 3630, + 'value3631' => 3631, + 'value3632' => 3632, + 'value3633' => 3633, + 'value3634' => 3634, + 'value3635' => 3635, + 'value3636' => 3636, + 'value3637' => 3637, + 'value3638' => 3638, + 'value3639' => 3639, + 'value3640' => 3640, + 'value3641' => 3641, + 'value3642' => 3642, + 'value3643' => 3643, + 'value3644' => 3644, + 'value3645' => 3645, + 'value3646' => 3646, + 'value3647' => 3647, + 'value3648' => 3648, + 'value3649' => 3649, + 'value3650' => 3650, + 'value3651' => 3651, + 'value3652' => 3652, + 'value3653' => 3653, + 'value3654' => 3654, + 'value3655' => 3655, + 'value3656' => 3656, + 'value3657' => 3657, + 'value3658' => 3658, + 'value3659' => 3659, + 'value3660' => 3660, + 'value3661' => 3661, + 'value3662' => 3662, + 'value3663' => 3663, + 'value3664' => 3664, + 'value3665' => 3665, + 'value3666' => 3666, + 'value3667' => 3667, + 'value3668' => 3668, + 'value3669' => 3669, + 'value3670' => 3670, + 'value3671' => 3671, + 'value3672' => 3672, + 'value3673' => 3673, + 'value3674' => 3674, + 'value3675' => 3675, + 'value3676' => 3676, + 'value3677' => 3677, + 'value3678' => 3678, + 'value3679' => 3679, + 'value3680' => 3680, + 'value3681' => 3681, + 'value3682' => 3682, + 'value3683' => 3683, + 'value3684' => 3684, + 'value3685' => 3685, + 'value3686' => 3686, + 'value3687' => 3687, + 'value3688' => 3688, + 'value3689' => 3689, + 'value3690' => 3690, + 'value3691' => 3691, + 'value3692' => 3692, + 'value3693' => 3693, + 'value3694' => 3694, + 'value3695' => 3695, + 'value3696' => 3696, + 'value3697' => 3697, + 'value3698' => 3698, + 'value3699' => 3699, + 'value3700' => 3700, + 'value3701' => 3701, + 'value3702' => 3702, + 'value3703' => 3703, + 'value3704' => 3704, + 'value3705' => 3705, + 'value3706' => 3706, + 'value3707' => 3707, + 'value3708' => 3708, + 'value3709' => 3709, + 'value3710' => 3710, + 'value3711' => 3711, + 'value3712' => 3712, + 'value3713' => 3713, + 'value3714' => 3714, + 'value3715' => 3715, + 'value3716' => 3716, + 'value3717' => 3717, + 'value3718' => 3718, + 'value3719' => 3719, + 'value3720' => 3720, + 'value3721' => 3721, + 'value3722' => 3722, + 'value3723' => 3723, + 'value3724' => 3724, + 'value3725' => 3725, + 'value3726' => 3726, + 'value3727' => 3727, + 'value3728' => 3728, + 'value3729' => 3729, + 'value3730' => 3730, + 'value3731' => 3731, + 'value3732' => 3732, + 'value3733' => 3733, + 'value3734' => 3734, + 'value3735' => 3735, + 'value3736' => 3736, + 'value3737' => 3737, + 'value3738' => 3738, + 'value3739' => 3739, + 'value3740' => 3740, + 'value3741' => 3741, + 'value3742' => 3742, + 'value3743' => 3743, + 'value3744' => 3744, + 'value3745' => 3745, + 'value3746' => 3746, + 'value3747' => 3747, + 'value3748' => 3748, + 'value3749' => 3749, + 'value3750' => 3750, + 'value3751' => 3751, + 'value3752' => 3752, + 'value3753' => 3753, + 'value3754' => 3754, + 'value3755' => 3755, + 'value3756' => 3756, + 'value3757' => 3757, + 'value3758' => 3758, + 'value3759' => 3759, + 'value3760' => 3760, + 'value3761' => 3761, + 'value3762' => 3762, + 'value3763' => 3763, + 'value3764' => 3764, + 'value3765' => 3765, + 'value3766' => 3766, + 'value3767' => 3767, + 'value3768' => 3768, + 'value3769' => 3769, + 'value3770' => 3770, + 'value3771' => 3771, + 'value3772' => 3772, + 'value3773' => 3773, + 'value3774' => 3774, + 'value3775' => 3775, + 'value3776' => 3776, + 'value3777' => 3777, + 'value3778' => 3778, + 'value3779' => 3779, + 'value3780' => 3780, + 'value3781' => 3781, + 'value3782' => 3782, + 'value3783' => 3783, + 'value3784' => 3784, + 'value3785' => 3785, + 'value3786' => 3786, + 'value3787' => 3787, + 'value3788' => 3788, + 'value3789' => 3789, + 'value3790' => 3790, + 'value3791' => 3791, + 'value3792' => 3792, + 'value3793' => 3793, + 'value3794' => 3794, + 'value3795' => 3795, + 'value3796' => 3796, + 'value3797' => 3797, + 'value3798' => 3798, + 'value3799' => 3799, + 'value3800' => 3800, + 'value3801' => 3801, + 'value3802' => 3802, + 'value3803' => 3803, + 'value3804' => 3804, + 'value3805' => 3805, + 'value3806' => 3806, + 'value3807' => 3807, + 'value3808' => 3808, + 'value3809' => 3809, + 'value3810' => 3810, + 'value3811' => 3811, + 'value3812' => 3812, + 'value3813' => 3813, + 'value3814' => 3814, + 'value3815' => 3815, + 'value3816' => 3816, + 'value3817' => 3817, + 'value3818' => 3818, + 'value3819' => 3819, + 'value3820' => 3820, + 'value3821' => 3821, + 'value3822' => 3822, + 'value3823' => 3823, + 'value3824' => 3824, + 'value3825' => 3825, + 'value3826' => 3826, + 'value3827' => 3827, + 'value3828' => 3828, + 'value3829' => 3829, + 'value3830' => 3830, + 'value3831' => 3831, + 'value3832' => 3832, + 'value3833' => 3833, + 'value3834' => 3834, + 'value3835' => 3835, + 'value3836' => 3836, + 'value3837' => 3837, + 'value3838' => 3838, + 'value3839' => 3839, + 'value3840' => 3840, + 'value3841' => 3841, + 'value3842' => 3842, + 'value3843' => 3843, + 'value3844' => 3844, + 'value3845' => 3845, + 'value3846' => 3846, + 'value3847' => 3847, + 'value3848' => 3848, + 'value3849' => 3849, + 'value3850' => 3850, + 'value3851' => 3851, + 'value3852' => 3852, + 'value3853' => 3853, + 'value3854' => 3854, + 'value3855' => 3855, + 'value3856' => 3856, + 'value3857' => 3857, + 'value3858' => 3858, + 'value3859' => 3859, + 'value3860' => 3860, + 'value3861' => 3861, + 'value3862' => 3862, + 'value3863' => 3863, + 'value3864' => 3864, + 'value3865' => 3865, + 'value3866' => 3866, + 'value3867' => 3867, + 'value3868' => 3868, + 'value3869' => 3869, + 'value3870' => 3870, + 'value3871' => 3871, + 'value3872' => 3872, + 'value3873' => 3873, + 'value3874' => 3874, + 'value3875' => 3875, + 'value3876' => 3876, + 'value3877' => 3877, + 'value3878' => 3878, + 'value3879' => 3879, + 'value3880' => 3880, + 'value3881' => 3881, + 'value3882' => 3882, + 'value3883' => 3883, + 'value3884' => 3884, + 'value3885' => 3885, + 'value3886' => 3886, + 'value3887' => 3887, + 'value3888' => 3888, + 'value3889' => 3889, + 'value3890' => 3890, + 'value3891' => 3891, + 'value3892' => 3892, + 'value3893' => 3893, + 'value3894' => 3894, + 'value3895' => 3895, + 'value3896' => 3896, + 'value3897' => 3897, + 'value3898' => 3898, + 'value3899' => 3899, + 'value3900' => 3900, + 'value3901' => 3901, + 'value3902' => 3902, + 'value3903' => 3903, + 'value3904' => 3904, + 'value3905' => 3905, + 'value3906' => 3906, + 'value3907' => 3907, + 'value3908' => 3908, + 'value3909' => 3909, + 'value3910' => 3910, + 'value3911' => 3911, + 'value3912' => 3912, + 'value3913' => 3913, + 'value3914' => 3914, + 'value3915' => 3915, + 'value3916' => 3916, + 'value3917' => 3917, + 'value3918' => 3918, + 'value3919' => 3919, + 'value3920' => 3920, + 'value3921' => 3921, + 'value3922' => 3922, + 'value3923' => 3923, + 'value3924' => 3924, + 'value3925' => 3925, + 'value3926' => 3926, + 'value3927' => 3927, + 'value3928' => 3928, + 'value3929' => 3929, + 'value3930' => 3930, + 'value3931' => 3931, + 'value3932' => 3932, + 'value3933' => 3933, + 'value3934' => 3934, + 'value3935' => 3935, + 'value3936' => 3936, + 'value3937' => 3937, + 'value3938' => 3938, + 'value3939' => 3939, + 'value3940' => 3940, + 'value3941' => 3941, + 'value3942' => 3942, + 'value3943' => 3943, + 'value3944' => 3944, + 'value3945' => 3945, + 'value3946' => 3946, + 'value3947' => 3947, + 'value3948' => 3948, + 'value3949' => 3949, + 'value3950' => 3950, + 'value3951' => 3951, + 'value3952' => 3952, + 'value3953' => 3953, + 'value3954' => 3954, + 'value3955' => 3955, + 'value3956' => 3956, + 'value3957' => 3957, + 'value3958' => 3958, + 'value3959' => 3959, + 'value3960' => 3960, + 'value3961' => 3961, + 'value3962' => 3962, + 'value3963' => 3963, + 'value3964' => 3964, + 'value3965' => 3965, + 'value3966' => 3966, + 'value3967' => 3967, + 'value3968' => 3968, + 'value3969' => 3969, + 'value3970' => 3970, + 'value3971' => 3971, + 'value3972' => 3972, + 'value3973' => 3973, + 'value3974' => 3974, + 'value3975' => 3975, + 'value3976' => 3976, + 'value3977' => 3977, + 'value3978' => 3978, + 'value3979' => 3979, + 'value3980' => 3980, + 'value3981' => 3981, + 'value3982' => 3982, + 'value3983' => 3983, + 'value3984' => 3984, + 'value3985' => 3985, + 'value3986' => 3986, + 'value3987' => 3987, + 'value3988' => 3988, + 'value3989' => 3989, + 'value3990' => 3990, + 'value3991' => 3991, + 'value3992' => 3992, + 'value3993' => 3993, + 'value3994' => 3994, + 'value3995' => 3995, + 'value3996' => 3996, + 'value3997' => 3997, + 'value3998' => 3998, + 'value3999' => 3999, + ]; + + public function getIdForReference(string $name): int + { + return self::HUGE_MAP[$name] ?? throw new RuntimeException('not found'); + } + + public function getReferenceForId(int $id): string + { + return array_search($id, self::HUGE_MAP, true) ?: throw new RuntimeException('not found'); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8215.php b/tests/PHPStan/Analyser/data/bug-8215.php new file mode 100644 index 0000000000..3cecf0ce1f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8215.php @@ -0,0 +1,13012 @@ + 10001, + 2 => 10002, + 3 => 10003, + 4 => 10004, + 5 => 10005, + 6 => 10006, + 7 => 10007, + 8 => 10008, + 9 => 10009, + 10 => 10010, + 11 => 10011, + 12 => 10012, + 13 => 10013, + 14 => 10014, + 15 => 10015, + 16 => 10016, + 17 => 10017, + 18 => 10018, + 19 => 10019, + 20 => 10020, + 21 => 10021, + 22 => 10022, + 23 => 10023, + 24 => 10024, + 25 => 10025, + 26 => 10026, + 27 => 10027, + 28 => 10028, + 29 => 10029, + 30 => 10030, + 31 => 10031, + 32 => 10032, + 33 => 10033, + 34 => 10034, + 35 => 10035, + 36 => 10036, + 37 => 10037, + 38 => 10038, + 39 => 10039, + 40 => 10040, + 41 => 10041, + 42 => 10042, + 43 => 10043, + 44 => 10044, + 45 => 10045, + 46 => 10046, + 47 => 10047, + 48 => 10048, + 49 => 10049, + 50 => 10050, + 51 => 10051, + 52 => 10052, + 53 => 10053, + 54 => 10054, + 55 => 10055, + 56 => 10056, + 57 => 10057, + 58 => 10058, + 59 => 10059, + 60 => 10060, + 61 => 10061, + 62 => 10062, + 63 => 10063, + 64 => 10064, + 65 => 10065, + 66 => 10066, + 67 => 10067, + 68 => 10068, + 69 => 10069, + 70 => 10070, + 71 => 10071, + 72 => 10072, + 73 => 10073, + 74 => 10074, + 75 => 10075, + 76 => 10076, + 77 => 10077, + 78 => 10078, + 79 => 10079, + 80 => 10080, + 81 => 10081, + 82 => 10082, + 83 => 10083, + 84 => 10084, + 85 => 10085, + 86 => 10086, + 87 => 10087, + 88 => 10088, + 89 => 10089, + 90 => 10090, + 91 => 10091, + 92 => 10092, + 93 => 10093, + 94 => 10094, + 95 => 10095, + 96 => 10096, + 97 => 10097, + 98 => 10098, + 99 => 10099, + 100 => 10100, + 101 => 10101, + 102 => 10102, + 103 => 10103, + 104 => 10104, + 105 => 10105, + 106 => 10106, + 107 => 10107, + 108 => 10108, + 109 => 10109, + 110 => 10110, + 111 => 10111, + 112 => 10112, + 113 => 10113, + 114 => 10114, + 115 => 10115, + 116 => 10116, + 117 => 10117, + 118 => 10118, + 119 => 10119, + 120 => 10120, + 121 => 10121, + 122 => 10122, + 123 => 10123, + 124 => 10124, + 125 => 10125, + 126 => 10126, + 127 => 10127, + 128 => 10128, + 129 => 10129, + 130 => 10130, + 131 => 10131, + 132 => 10132, + 133 => 10133, + 134 => 10134, + 135 => 10135, + 136 => 10136, + 137 => 10137, + 138 => 10138, + 139 => 10139, + 140 => 10140, + 141 => 10141, + 142 => 10142, + 143 => 10143, + 144 => 10144, + 145 => 10145, + 146 => 10146, + 147 => 10147, + 148 => 10148, + 149 => 10149, + 150 => 10150, + 151 => 10151, + 152 => 10152, + 153 => 10153, + 154 => 10154, + 155 => 10155, + 156 => 10156, + 157 => 10157, + 158 => 10158, + 159 => 10159, + 160 => 10160, + 161 => 10161, + 162 => 10162, + 163 => 10163, + 164 => 10164, + 165 => 10165, + 166 => 10166, + 167 => 10167, + 168 => 10168, + 169 => 10169, + 170 => 10170, + 171 => 10171, + 172 => 10172, + 173 => 10173, + 174 => 10174, + 175 => 10175, + 176 => 10176, + 177 => 10177, + 178 => 10178, + 179 => 10179, + 180 => 10180, + 181 => 10181, + 182 => 10182, + 183 => 10183, + 184 => 10184, + 185 => 10185, + 186 => 10186, + 187 => 10187, + 188 => 10188, + 189 => 10189, + 190 => 10190, + 191 => 10191, + 192 => 10192, + 193 => 10193, + 194 => 10194, + 195 => 10195, + 196 => 10196, + 197 => 10197, + 198 => 10198, + 199 => 10199, + 200 => 10200, + 201 => 10201, + 202 => 10202, + 203 => 10203, + 204 => 10204, + 205 => 10205, + 206 => 10206, + 207 => 10207, + 208 => 10208, + 209 => 10209, + 210 => 10210, + 211 => 10211, + 212 => 10212, + 213 => 10213, + 214 => 10214, + 215 => 10215, + 216 => 10216, + 217 => 10217, + 218 => 10218, + 219 => 10219, + 220 => 10220, + 221 => 10221, + 222 => 10222, + 223 => 10223, + 224 => 10224, + 225 => 10225, + 226 => 10226, + 227 => 10227, + 228 => 10228, + 229 => 10229, + 230 => 10230, + 231 => 10231, + 232 => 10232, + 233 => 10233, + 234 => 10234, + 235 => 10235, + 236 => 10236, + 237 => 10237, + 238 => 10238, + 239 => 10239, + 240 => 10240, + 241 => 10241, + 242 => 10242, + 243 => 10243, + 244 => 10244, + 245 => 10245, + 246 => 10246, + 247 => 10247, + 248 => 10248, + 249 => 10249, + 250 => 10250, + 251 => 10251, + 252 => 10252, + 253 => 10253, + 254 => 10254, + 255 => 10255, + 256 => 10256, + 257 => 10257, + 258 => 10258, + 259 => 10259, + 260 => 10260, + 261 => 10261, + 262 => 10262, + 263 => 10263, + 264 => 10264, + 265 => 10265, + 266 => 10266, + 267 => 10267, + 268 => 10268, + 269 => 10269, + 270 => 10270, + 271 => 10271, + 272 => 10272, + 273 => 10273, + 274 => 10274, + 275 => 10275, + 276 => 10276, + 277 => 10277, + 278 => 10278, + 279 => 10279, + 280 => 10280, + 281 => 10281, + 282 => 10282, + 283 => 10283, + 284 => 10284, + 285 => 10285, + 286 => 10286, + 287 => 10287, + 288 => 10288, + 289 => 10289, + 290 => 10290, + 291 => 10291, + 292 => 10292, + 293 => 10293, + 294 => 10294, + 295 => 10295, + 296 => 10296, + 297 => 10297, + 298 => 10298, + 299 => 10299, + 300 => 10300, + 301 => 10301, + 302 => 10302, + 303 => 10303, + 304 => 10304, + 305 => 10305, + 306 => 10306, + 307 => 10307, + 308 => 10308, + 309 => 10309, + 310 => 10310, + 311 => 10311, + 312 => 10312, + 313 => 10313, + 314 => 10314, + 315 => 10315, + 316 => 10316, + 317 => 10317, + 318 => 10318, + 319 => 10319, + 320 => 10320, + 321 => 10321, + 322 => 10322, + 323 => 10323, + 324 => 10324, + 325 => 10325, + 326 => 10326, + 327 => 10327, + 328 => 10328, + 329 => 10329, + 330 => 10330, + 331 => 10331, + 332 => 10332, + 333 => 10333, + 334 => 10334, + 335 => 10335, + 336 => 10336, + 337 => 10337, + 338 => 10338, + 339 => 10339, + 340 => 10340, + 341 => 10341, + 342 => 10342, + 343 => 10343, + 344 => 10344, + 345 => 10345, + 346 => 10346, + 347 => 10347, + 348 => 10348, + 349 => 10349, + 350 => 10350, + 351 => 10351, + 352 => 10352, + 353 => 10353, + 354 => 10354, + 355 => 10355, + 356 => 10356, + 357 => 10357, + 358 => 10358, + 359 => 10359, + 360 => 10360, + 361 => 10361, + 362 => 10362, + 363 => 10363, + 364 => 10364, + 365 => 10365, + 366 => 10366, + 367 => 10367, + 368 => 10368, + 369 => 10369, + 370 => 10370, + 371 => 10371, + 372 => 10372, + 373 => 10373, + 374 => 10374, + 375 => 10375, + 376 => 10376, + 377 => 10377, + 378 => 10378, + 379 => 10379, + 380 => 10380, + 381 => 10381, + 382 => 10382, + 383 => 10383, + 384 => 10384, + 385 => 10385, + 386 => 10386, + 387 => 10387, + 388 => 10388, + 389 => 10389, + 390 => 10390, + 391 => 10391, + 392 => 10392, + 393 => 10393, + 394 => 10394, + 395 => 10395, + 396 => 10396, + 397 => 10397, + 398 => 10398, + 399 => 10399, + 400 => 10400, + 401 => 10401, + 402 => 10402, + 403 => 10403, + 404 => 10404, + 405 => 10405, + 406 => 10406, + 407 => 10407, + 408 => 10408, + 409 => 10409, + 410 => 10410, + 411 => 10411, + 412 => 10412, + 413 => 10413, + 414 => 10414, + 415 => 10415, + 416 => 10416, + 417 => 10417, + 418 => 10418, + 419 => 10419, + 420 => 10420, + 421 => 10421, + 422 => 10422, + 423 => 10423, + 424 => 10424, + 425 => 10425, + 426 => 10426, + 427 => 10427, + 428 => 10428, + 429 => 10429, + 430 => 10430, + 431 => 10431, + 432 => 10432, + 433 => 10433, + 434 => 10434, + 435 => 10435, + 436 => 10436, + 437 => 10437, + 438 => 10438, + 439 => 10439, + 440 => 10440, + 441 => 10441, + 442 => 10442, + 443 => 10443, + 444 => 10444, + 445 => 10445, + 446 => 10446, + 447 => 10447, + 448 => 10448, + 449 => 10449, + 450 => 10450, + 451 => 10451, + 452 => 10452, + 453 => 10453, + 454 => 10454, + 455 => 10455, + 456 => 10456, + 457 => 10457, + 458 => 10458, + 459 => 10459, + 460 => 10460, + 461 => 10461, + 462 => 10462, + 463 => 10463, + 464 => 10464, + 465 => 10465, + 466 => 10466, + 467 => 10467, + 468 => 10468, + 469 => 10469, + 470 => 10470, + 471 => 10471, + 472 => 10472, + 473 => 10473, + 474 => 10474, + 475 => 10475, + 476 => 10476, + 477 => 10477, + 478 => 10478, + 479 => 10479, + 480 => 10480, + 481 => 10481, + 482 => 10482, + 483 => 10483, + 484 => 10484, + 485 => 10485, + 486 => 10486, + 487 => 10487, + 488 => 10488, + 489 => 10489, + 490 => 10490, + 491 => 10491, + 492 => 10492, + 493 => 10493, + 494 => 10494, + 495 => 10495, + 496 => 10496, + 497 => 10497, + 498 => 10498, + 499 => 10499, + 500 => 10500, + 501 => 10501, + 502 => 10502, + 503 => 10503, + 504 => 10504, + 505 => 10505, + 506 => 10506, + 507 => 10507, + 508 => 10508, + 509 => 10509, + 510 => 10510, + 511 => 10511, + 512 => 10512, + 513 => 10513, + 514 => 10514, + 515 => 10515, + 516 => 10516, + 517 => 10517, + 518 => 10518, + 519 => 10519, + 520 => 10520, + 521 => 10521, + 522 => 10522, + 523 => 10523, + 524 => 10524, + 525 => 10525, + 526 => 10526, + 527 => 10527, + 528 => 10528, + 529 => 10529, + 530 => 10530, + 531 => 10531, + 532 => 10532, + 533 => 10533, + 534 => 10534, + 535 => 10535, + 536 => 10536, + 537 => 10537, + 538 => 10538, + 539 => 10539, + 540 => 10540, + 541 => 10541, + 542 => 10542, + 543 => 10543, + 544 => 10544, + 545 => 10545, + 546 => 10546, + 547 => 10547, + 548 => 10548, + 549 => 10549, + 550 => 10550, + 551 => 10551, + 552 => 10552, + 553 => 10553, + 554 => 10554, + 555 => 10555, + 556 => 10556, + 557 => 10557, + 558 => 10558, + 559 => 10559, + 560 => 10560, + 561 => 10561, + 562 => 10562, + 563 => 10563, + 564 => 10564, + 565 => 10565, + 566 => 10566, + 567 => 10567, + 568 => 10568, + 569 => 10569, + 570 => 10570, + 571 => 10571, + 572 => 10572, + 573 => 10573, + 574 => 10574, + 575 => 10575, + 576 => 10576, + 577 => 10577, + 578 => 10578, + 579 => 10579, + 580 => 10580, + 581 => 10581, + 582 => 10582, + 583 => 10583, + 584 => 10584, + 585 => 10585, + 586 => 10586, + 587 => 10587, + 588 => 10588, + 589 => 10589, + 590 => 10590, + 591 => 10591, + 592 => 10592, + 593 => 10593, + 594 => 10594, + 595 => 10595, + 596 => 10596, + 597 => 10597, + 598 => 10598, + 599 => 10599, + 600 => 10600, + 601 => 10601, + 602 => 10602, + 603 => 10603, + 604 => 10604, + 605 => 10605, + 606 => 10606, + 607 => 10607, + 608 => 10608, + 609 => 10609, + 610 => 10610, + 611 => 10611, + 612 => 10612, + 613 => 10613, + 614 => 10614, + 615 => 10615, + 616 => 10616, + 617 => 10617, + 618 => 10618, + 619 => 10619, + 620 => 10620, + 621 => 10621, + 622 => 10622, + 623 => 10623, + 624 => 10624, + 625 => 10625, + 626 => 10626, + 627 => 10627, + 628 => 10628, + 629 => 10629, + 630 => 10630, + 631 => 10631, + 632 => 10632, + 633 => 10633, + 634 => 10634, + 635 => 10635, + 636 => 10636, + 637 => 10637, + 638 => 10638, + 639 => 10639, + 640 => 10640, + 641 => 10641, + 642 => 10642, + 643 => 10643, + 644 => 10644, + 645 => 10645, + 646 => 10646, + 647 => 10647, + 648 => 10648, + 649 => 10649, + 650 => 10650, + 651 => 10651, + 652 => 10652, + 653 => 10653, + 654 => 10654, + 655 => 10655, + 656 => 10656, + 657 => 10657, + 658 => 10658, + 659 => 10659, + 660 => 10660, + 661 => 10661, + 662 => 10662, + 663 => 10663, + 664 => 10664, + 665 => 10665, + 666 => 10666, + 667 => 10667, + 668 => 10668, + 669 => 10669, + 670 => 10670, + 671 => 10671, + 672 => 10672, + 673 => 10673, + 674 => 10674, + 675 => 10675, + 676 => 10676, + 677 => 10677, + 678 => 10678, + 679 => 10679, + 680 => 10680, + 681 => 10681, + 682 => 10682, + 683 => 10683, + 684 => 10684, + 685 => 10685, + 686 => 10686, + 687 => 10687, + 688 => 10688, + 689 => 10689, + 690 => 10690, + 691 => 10691, + 692 => 10692, + 693 => 10693, + 694 => 10694, + 695 => 10695, + 696 => 10696, + 697 => 10697, + 698 => 10698, + 699 => 10699, + 700 => 10700, + 701 => 10701, + 702 => 10702, + 703 => 10703, + 704 => 10704, + 705 => 10705, + 706 => 10706, + 707 => 10707, + 708 => 10708, + 709 => 10709, + 710 => 10710, + 711 => 10711, + 712 => 10712, + 713 => 10713, + 714 => 10714, + 715 => 10715, + 716 => 10716, + 717 => 10717, + 718 => 10718, + 719 => 10719, + 720 => 10720, + 721 => 10721, + 722 => 10722, + 723 => 10723, + 724 => 10724, + 725 => 10725, + 726 => 10726, + 727 => 10727, + 728 => 10728, + 729 => 10729, + 730 => 10730, + 731 => 10731, + 732 => 10732, + 733 => 10733, + 734 => 10734, + 735 => 10735, + 736 => 10736, + 737 => 10737, + 738 => 10738, + 739 => 10739, + 740 => 10740, + 741 => 10741, + 742 => 10742, + 743 => 10743, + 744 => 10744, + 745 => 10745, + 746 => 10746, + 747 => 10747, + 748 => 10748, + 749 => 10749, + 750 => 10750, + 751 => 10751, + 752 => 10752, + 753 => 10753, + 754 => 10754, + 755 => 10755, + 756 => 10756, + 757 => 10757, + 758 => 10758, + 759 => 10759, + 760 => 10760, + 761 => 10761, + 762 => 10762, + 763 => 10763, + 764 => 10764, + 765 => 10765, + 766 => 10766, + 767 => 10767, + 768 => 10768, + 769 => 10769, + 770 => 10770, + 771 => 10771, + 772 => 10772, + 773 => 10773, + 774 => 10774, + 775 => 10775, + 776 => 10776, + 777 => 10777, + 778 => 10778, + 779 => 10779, + 780 => 10780, + 781 => 10781, + 782 => 10782, + 783 => 10783, + 784 => 10784, + 785 => 10785, + 786 => 10786, + 787 => 10787, + 788 => 10788, + 789 => 10789, + 790 => 10790, + 791 => 10791, + 792 => 10792, + 793 => 10793, + 794 => 10794, + 795 => 10795, + 796 => 10796, + 797 => 10797, + 798 => 10798, + 799 => 10799, + 800 => 10800, + 801 => 10801, + 802 => 10802, + 803 => 10803, + 804 => 10804, + 805 => 10805, + 806 => 10806, + 807 => 10807, + 808 => 10808, + 809 => 10809, + 810 => 10810, + 811 => 10811, + 812 => 10812, + 813 => 10813, + 814 => 10814, + 815 => 10815, + 816 => 10816, + 817 => 10817, + 818 => 10818, + 819 => 10819, + 820 => 10820, + 821 => 10821, + 822 => 10822, + 823 => 10823, + 824 => 10824, + 825 => 10825, + 826 => 10826, + 827 => 10827, + 828 => 10828, + 829 => 10829, + 830 => 10830, + 831 => 10831, + 832 => 10832, + 833 => 10833, + 834 => 10834, + 835 => 10835, + 836 => 10836, + 837 => 10837, + 838 => 10838, + 839 => 10839, + 840 => 10840, + 841 => 10841, + 842 => 10842, + 843 => 10843, + 844 => 10844, + 845 => 10845, + 846 => 10846, + 847 => 10847, + 848 => 10848, + 849 => 10849, + 850 => 10850, + 851 => 10851, + 852 => 10852, + 853 => 10853, + 854 => 10854, + 855 => 10855, + 856 => 10856, + 857 => 10857, + 858 => 10858, + 859 => 10859, + 860 => 10860, + 861 => 10861, + 862 => 10862, + 863 => 10863, + 864 => 10864, + 865 => 10865, + 866 => 10866, + 867 => 10867, + 868 => 10868, + 869 => 10869, + 870 => 10870, + 871 => 10871, + 872 => 10872, + 873 => 10873, + 874 => 10874, + 875 => 10875, + 876 => 10876, + 877 => 10877, + 878 => 10878, + 879 => 10879, + 880 => 10880, + 881 => 10881, + 882 => 10882, + 883 => 10883, + 884 => 10884, + 885 => 10885, + 886 => 10886, + 887 => 10887, + 888 => 10888, + 889 => 10889, + 890 => 10890, + 891 => 10891, + 892 => 10892, + 893 => 10893, + 894 => 10894, + 895 => 10895, + 896 => 10896, + 897 => 10897, + 898 => 10898, + 899 => 10899, + 900 => 10900, + 901 => 10901, + 902 => 10902, + 903 => 10903, + 904 => 10904, + 905 => 10905, + 906 => 10906, + 907 => 10907, + 908 => 10908, + 909 => 10909, + 910 => 10910, + 911 => 10911, + 912 => 10912, + 913 => 10913, + 914 => 10914, + 915 => 10915, + 916 => 10916, + 917 => 10917, + 918 => 10918, + 919 => 10919, + 920 => 10920, + 921 => 10921, + 922 => 10922, + 923 => 10923, + 924 => 10924, + 925 => 10925, + 926 => 10926, + 927 => 10927, + 928 => 10928, + 929 => 10929, + 930 => 10930, + 931 => 10931, + 932 => 10932, + 933 => 10933, + 934 => 10934, + 935 => 10935, + 936 => 10936, + 937 => 10937, + 938 => 10938, + 939 => 10939, + 940 => 10940, + 941 => 10941, + 942 => 10942, + 943 => 10943, + 944 => 10944, + 945 => 10945, + 946 => 10946, + 947 => 10947, + 948 => 10948, + 949 => 10949, + 950 => 10950, + 951 => 10951, + 952 => 10952, + 953 => 10953, + 954 => 10954, + 955 => 10955, + 956 => 10956, + 957 => 10957, + 958 => 10958, + 959 => 10959, + 960 => 10960, + 961 => 10961, + 962 => 10962, + 963 => 10963, + 964 => 10964, + 965 => 10965, + 966 => 10966, + 967 => 10967, + 968 => 10968, + 969 => 10969, + 970 => 10970, + 971 => 10971, + 972 => 10972, + 973 => 10973, + 974 => 10974, + 975 => 10975, + 976 => 10976, + 977 => 10977, + 978 => 10978, + 979 => 10979, + 980 => 10980, + 981 => 10981, + 982 => 10982, + 983 => 10983, + 984 => 10984, + 985 => 10985, + 986 => 10986, + 987 => 10987, + 988 => 10988, + 989 => 10989, + 990 => 10990, + 991 => 10991, + 992 => 10992, + 993 => 10993, + 994 => 10994, + 995 => 10995, + 996 => 10996, + 997 => 10997, + 998 => 10998, + 999 => 10999, + 1000 => 11000, + 1001 => 11001, + 1002 => 11002, + 1003 => 11003, + 1004 => 11004, + 1005 => 11005, + 1006 => 11006, + 1007 => 11007, + 1008 => 11008, + 1009 => 11009, + 1010 => 11010, + 1011 => 11011, + 1012 => 11012, + 1013 => 11013, + 1014 => 11014, + 1015 => 11015, + 1016 => 11016, + 1017 => 11017, + 1018 => 11018, + 1019 => 11019, + 1020 => 11020, + 1021 => 11021, + 1022 => 11022, + 1023 => 11023, + 1024 => 11024, + 1025 => 11025, + 1026 => 11026, + 1027 => 11027, + 1028 => 11028, + 1029 => 11029, + 1030 => 11030, + 1031 => 11031, + 1032 => 11032, + 1033 => 11033, + 1034 => 11034, + 1035 => 11035, + 1036 => 11036, + 1037 => 11037, + 1038 => 11038, + 1039 => 11039, + 1040 => 11040, + 1041 => 11041, + 1042 => 11042, + 1043 => 11043, + 1044 => 11044, + 1045 => 11045, + 1046 => 11046, + 1047 => 11047, + 1048 => 11048, + 1049 => 11049, + 1050 => 11050, + 1051 => 11051, + 1052 => 11052, + 1053 => 11053, + 1054 => 11054, + 1055 => 11055, + 1056 => 11056, + 1057 => 11057, + 1058 => 11058, + 1059 => 11059, + 1060 => 11060, + 1061 => 11061, + 1062 => 11062, + 1063 => 11063, + 1064 => 11064, + 1065 => 11065, + 1066 => 11066, + 1067 => 11067, + 1068 => 11068, + 1069 => 11069, + 1070 => 11070, + 1071 => 11071, + 1072 => 11072, + 1073 => 11073, + 1074 => 11074, + 1075 => 11075, + 1076 => 11076, + 1077 => 11077, + 1078 => 11078, + 1079 => 11079, + 1080 => 11080, + 1081 => 11081, + 1082 => 11082, + 1083 => 11083, + 1084 => 11084, + 1085 => 11085, + 1086 => 11086, + 1087 => 11087, + 1088 => 11088, + 1089 => 11089, + 1090 => 11090, + 1091 => 11091, + 1092 => 11092, + 1093 => 11093, + 1094 => 11094, + 1095 => 11095, + 1096 => 11096, + 1097 => 11097, + 1098 => 11098, + 1099 => 11099, + 1100 => 11100, + 1101 => 11101, + 1102 => 11102, + 1103 => 11103, + 1104 => 11104, + 1105 => 11105, + 1106 => 11106, + 1107 => 11107, + 1108 => 11108, + 1109 => 11109, + 1110 => 11110, + 1111 => 11111, + 1112 => 11112, + 1113 => 11113, + 1114 => 11114, + 1115 => 11115, + 1116 => 11116, + 1117 => 11117, + 1118 => 11118, + 1119 => 11119, + 1120 => 11120, + 1121 => 11121, + 1122 => 11122, + 1123 => 11123, + 1124 => 11124, + 1125 => 11125, + 1126 => 11126, + 1127 => 11127, + 1128 => 11128, + 1129 => 11129, + 1130 => 11130, + 1131 => 11131, + 1132 => 11132, + 1133 => 11133, + 1134 => 11134, + 1135 => 11135, + 1136 => 11136, + 1137 => 11137, + 1138 => 11138, + 1139 => 11139, + 1140 => 11140, + 1141 => 11141, + 1142 => 11142, + 1143 => 11143, + 1144 => 11144, + 1145 => 11145, + 1146 => 11146, + 1147 => 11147, + 1148 => 11148, + 1149 => 11149, + 1150 => 11150, + 1151 => 11151, + 1152 => 11152, + 1153 => 11153, + 1154 => 11154, + 1155 => 11155, + 1156 => 11156, + 1157 => 11157, + 1158 => 11158, + 1159 => 11159, + 1160 => 11160, + 1161 => 11161, + 1162 => 11162, + 1163 => 11163, + 1164 => 11164, + 1165 => 11165, + 1166 => 11166, + 1167 => 11167, + 1168 => 11168, + 1169 => 11169, + 1170 => 11170, + 1171 => 11171, + 1172 => 11172, + 1173 => 11173, + 1174 => 11174, + 1175 => 11175, + 1176 => 11176, + 1177 => 11177, + 1178 => 11178, + 1179 => 11179, + 1180 => 11180, + 1181 => 11181, + 1182 => 11182, + 1183 => 11183, + 1184 => 11184, + 1185 => 11185, + 1186 => 11186, + 1187 => 11187, + 1188 => 11188, + 1189 => 11189, + 1190 => 11190, + 1191 => 11191, + 1192 => 11192, + 1193 => 11193, + 1194 => 11194, + 1195 => 11195, + 1196 => 11196, + 1197 => 11197, + 1198 => 11198, + 1199 => 11199, + 1200 => 11200, + 1201 => 11201, + 1202 => 11202, + 1203 => 11203, + 1204 => 11204, + 1205 => 11205, + 1206 => 11206, + 1207 => 11207, + 1208 => 11208, + 1209 => 11209, + 1210 => 11210, + 1211 => 11211, + 1212 => 11212, + 1213 => 11213, + 1214 => 11214, + 1215 => 11215, + 1216 => 11216, + 1217 => 11217, + 1218 => 11218, + 1219 => 11219, + 1220 => 11220, + 1221 => 11221, + 1222 => 11222, + 1223 => 11223, + 1224 => 11224, + 1225 => 11225, + 1226 => 11226, + 1227 => 11227, + 1228 => 11228, + 1229 => 11229, + 1230 => 11230, + 1231 => 11231, + 1232 => 11232, + 1233 => 11233, + 1234 => 11234, + 1235 => 11235, + 1236 => 11236, + 1237 => 11237, + 1238 => 11238, + 1239 => 11239, + 1240 => 11240, + 1241 => 11241, + 1242 => 11242, + 1243 => 11243, + 1244 => 11244, + 1245 => 11245, + 1246 => 11246, + 1247 => 11247, + 1248 => 11248, + 1249 => 11249, + 1250 => 11250, + 1251 => 11251, + 1252 => 11252, + 1253 => 11253, + 1254 => 11254, + 1255 => 11255, + 1256 => 11256, + 1257 => 11257, + 1258 => 11258, + 1259 => 11259, + 1260 => 11260, + 1261 => 11261, + 1262 => 11262, + 1263 => 11263, + 1264 => 11264, + 1265 => 11265, + 1266 => 11266, + 1267 => 11267, + 1268 => 11268, + 1269 => 11269, + 1270 => 11270, + 1271 => 11271, + 1272 => 11272, + 1273 => 11273, + 1274 => 11274, + 1275 => 11275, + 1276 => 11276, + 1277 => 11277, + 1278 => 11278, + 1279 => 11279, + 1280 => 11280, + 1281 => 11281, + 1282 => 11282, + 1283 => 11283, + 1284 => 11284, + 1285 => 11285, + 1286 => 11286, + 1287 => 11287, + 1288 => 11288, + 1289 => 11289, + 1290 => 11290, + 1291 => 11291, + 1292 => 11292, + 1293 => 11293, + 1294 => 11294, + 1295 => 11295, + 1296 => 11296, + 1297 => 11297, + 1298 => 11298, + 1299 => 11299, + 1300 => 11300, + 1301 => 11301, + 1302 => 11302, + 1303 => 11303, + 1304 => 11304, + 1305 => 11305, + 1306 => 11306, + 1307 => 11307, + 1308 => 11308, + 1309 => 11309, + 1310 => 11310, + 1311 => 11311, + 1312 => 11312, + 1313 => 11313, + 1314 => 11314, + 1315 => 11315, + 1316 => 11316, + 1317 => 11317, + 1318 => 11318, + 1319 => 11319, + 1320 => 11320, + 1321 => 11321, + 1322 => 11322, + 1323 => 11323, + 1324 => 11324, + 1325 => 11325, + 1326 => 11326, + 1327 => 11327, + 1328 => 11328, + 1329 => 11329, + 1330 => 11330, + 1331 => 11331, + 1332 => 11332, + 1333 => 11333, + 1334 => 11334, + 1335 => 11335, + 1336 => 11336, + 1337 => 11337, + 1338 => 11338, + 1339 => 11339, + 1340 => 11340, + 1341 => 11341, + 1342 => 11342, + 1343 => 11343, + 1344 => 11344, + 1345 => 11345, + 1346 => 11346, + 1347 => 11347, + 1348 => 11348, + 1349 => 11349, + 1350 => 11350, + 1351 => 11351, + 1352 => 11352, + 1353 => 11353, + 1354 => 11354, + 1355 => 11355, + 1356 => 11356, + 1357 => 11357, + 1358 => 11358, + 1359 => 11359, + 1360 => 11360, + 1361 => 11361, + 1362 => 11362, + 1363 => 11363, + 1364 => 11364, + 1365 => 11365, + 1366 => 11366, + 1367 => 11367, + 1368 => 11368, + 1369 => 11369, + 1370 => 11370, + 1371 => 11371, + 1372 => 11372, + 1373 => 11373, + 1374 => 11374, + 1375 => 11375, + 1376 => 11376, + 1377 => 11377, + 1378 => 11378, + 1379 => 11379, + 1380 => 11380, + 1381 => 11381, + 1382 => 11382, + 1383 => 11383, + 1384 => 11384, + 1385 => 11385, + 1386 => 11386, + 1387 => 11387, + 1388 => 11388, + 1389 => 11389, + 1390 => 11390, + 1391 => 11391, + 1392 => 11392, + 1393 => 11393, + 1394 => 11394, + 1395 => 11395, + 1396 => 11396, + 1397 => 11397, + 1398 => 11398, + 1399 => 11399, + 1400 => 11400, + 1401 => 11401, + 1402 => 11402, + 1403 => 11403, + 1404 => 11404, + 1405 => 11405, + 1406 => 11406, + 1407 => 11407, + 1408 => 11408, + 1409 => 11409, + 1410 => 11410, + 1411 => 11411, + 1412 => 11412, + 1413 => 11413, + 1414 => 11414, + 1415 => 11415, + 1416 => 11416, + 1417 => 11417, + 1418 => 11418, + 1419 => 11419, + 1420 => 11420, + 1421 => 11421, + 1422 => 11422, + 1423 => 11423, + 1424 => 11424, + 1425 => 11425, + 1426 => 11426, + 1427 => 11427, + 1428 => 11428, + 1429 => 11429, + 1430 => 11430, + 1431 => 11431, + 1432 => 11432, + 1433 => 11433, + 1434 => 11434, + 1435 => 11435, + 1436 => 11436, + 1437 => 11437, + 1438 => 11438, + 1439 => 11439, + 1440 => 11440, + 1441 => 11441, + 1442 => 11442, + 1443 => 11443, + 1444 => 11444, + 1445 => 11445, + 1446 => 11446, + 1447 => 11447, + 1448 => 11448, + 1449 => 11449, + 1450 => 11450, + 1451 => 11451, + 1452 => 11452, + 1453 => 11453, + 1454 => 11454, + 1455 => 11455, + 1456 => 11456, + 1457 => 11457, + 1458 => 11458, + 1459 => 11459, + 1460 => 11460, + 1461 => 11461, + 1462 => 11462, + 1463 => 11463, + 1464 => 11464, + 1465 => 11465, + 1466 => 11466, + 1467 => 11467, + 1468 => 11468, + 1469 => 11469, + 1470 => 11470, + 1471 => 11471, + 1472 => 11472, + 1473 => 11473, + 1474 => 11474, + 1475 => 11475, + 1476 => 11476, + 1477 => 11477, + 1478 => 11478, + 1479 => 11479, + 1480 => 11480, + 1481 => 11481, + 1482 => 11482, + 1483 => 11483, + 1484 => 11484, + 1485 => 11485, + 1486 => 11486, + 1487 => 11487, + 1488 => 11488, + 1489 => 11489, + 1490 => 11490, + 1491 => 11491, + 1492 => 11492, + 1493 => 11493, + 1494 => 11494, + 1495 => 11495, + 1496 => 11496, + 1497 => 11497, + 1498 => 11498, + 1499 => 11499, + 1500 => 11500, + 1501 => 11501, + 1502 => 11502, + 1503 => 11503, + 1504 => 11504, + 1505 => 11505, + 1506 => 11506, + 1507 => 11507, + 1508 => 11508, + 1509 => 11509, + 1510 => 11510, + 1511 => 11511, + 1512 => 11512, + 1513 => 11513, + 1514 => 11514, + 1515 => 11515, + 1516 => 11516, + 1517 => 11517, + 1518 => 11518, + 1519 => 11519, + 1520 => 11520, + 1521 => 11521, + 1522 => 11522, + 1523 => 11523, + 1524 => 11524, + 1525 => 11525, + 1526 => 11526, + 1527 => 11527, + 1528 => 11528, + 1529 => 11529, + 1530 => 11530, + 1531 => 11531, + 1532 => 11532, + 1533 => 11533, + 1534 => 11534, + 1535 => 11535, + 1536 => 11536, + 1537 => 11537, + 1538 => 11538, + 1539 => 11539, + 1540 => 11540, + 1541 => 11541, + 1542 => 11542, + 1543 => 11543, + 1544 => 11544, + 1545 => 11545, + 1546 => 11546, + 1547 => 11547, + 1548 => 11548, + 1549 => 11549, + 1550 => 11550, + 1551 => 11551, + 1552 => 11552, + 1553 => 11553, + 1554 => 11554, + 1555 => 11555, + 1556 => 11556, + 1557 => 11557, + 1558 => 11558, + 1559 => 11559, + 1560 => 11560, + 1561 => 11561, + 1562 => 11562, + 1563 => 11563, + 1564 => 11564, + 1565 => 11565, + 1566 => 11566, + 1567 => 11567, + 1568 => 11568, + 1569 => 11569, + 1570 => 11570, + 1571 => 11571, + 1572 => 11572, + 1573 => 11573, + 1574 => 11574, + 1575 => 11575, + 1576 => 11576, + 1577 => 11577, + 1578 => 11578, + 1579 => 11579, + 1580 => 11580, + 1581 => 11581, + 1582 => 11582, + 1583 => 11583, + 1584 => 11584, + 1585 => 11585, + 1586 => 11586, + 1587 => 11587, + 1588 => 11588, + 1589 => 11589, + 1590 => 11590, + 1591 => 11591, + 1592 => 11592, + 1593 => 11593, + 1594 => 11594, + 1595 => 11595, + 1596 => 11596, + 1597 => 11597, + 1598 => 11598, + 1599 => 11599, + 1600 => 11600, + 1601 => 11601, + 1602 => 11602, + 1603 => 11603, + 1604 => 11604, + 1605 => 11605, + 1606 => 11606, + 1607 => 11607, + 1608 => 11608, + 1609 => 11609, + 1610 => 11610, + 1611 => 11611, + 1612 => 11612, + 1613 => 11613, + 1614 => 11614, + 1615 => 11615, + 1616 => 11616, + 1617 => 11617, + 1618 => 11618, + 1619 => 11619, + 1620 => 11620, + 1621 => 11621, + 1622 => 11622, + 1623 => 11623, + 1624 => 11624, + 1625 => 11625, + 1626 => 11626, + 1627 => 11627, + 1628 => 11628, + 1629 => 11629, + 1630 => 11630, + 1631 => 11631, + 1632 => 11632, + 1633 => 11633, + 1634 => 11634, + 1635 => 11635, + 1636 => 11636, + 1637 => 11637, + 1638 => 11638, + 1639 => 11639, + 1640 => 11640, + 1641 => 11641, + 1642 => 11642, + 1643 => 11643, + 1644 => 11644, + 1645 => 11645, + 1646 => 11646, + 1647 => 11647, + 1648 => 11648, + 1649 => 11649, + 1650 => 11650, + 1651 => 11651, + 1652 => 11652, + 1653 => 11653, + 1654 => 11654, + 1655 => 11655, + 1656 => 11656, + 1657 => 11657, + 1658 => 11658, + 1659 => 11659, + 1660 => 11660, + 1661 => 11661, + 1662 => 11662, + 1663 => 11663, + 1664 => 11664, + 1665 => 11665, + 1666 => 11666, + 1667 => 11667, + 1668 => 11668, + 1669 => 11669, + 1670 => 11670, + 1671 => 11671, + 1672 => 11672, + 1673 => 11673, + 1674 => 11674, + 1675 => 11675, + 1676 => 11676, + 1677 => 11677, + 1678 => 11678, + 1679 => 11679, + 1680 => 11680, + 1681 => 11681, + 1682 => 11682, + 1683 => 11683, + 1684 => 11684, + 1685 => 11685, + 1686 => 11686, + 1687 => 11687, + 1688 => 11688, + 1689 => 11689, + 1690 => 11690, + 1691 => 11691, + 1692 => 11692, + 1693 => 11693, + 1694 => 11694, + 1695 => 11695, + 1696 => 11696, + 1697 => 11697, + 1698 => 11698, + 1699 => 11699, + 1700 => 11700, + 1701 => 11701, + 1702 => 11702, + 1703 => 11703, + 1704 => 11704, + 1705 => 11705, + 1706 => 11706, + 1707 => 11707, + 1708 => 11708, + 1709 => 11709, + 1710 => 11710, + 1711 => 11711, + 1712 => 11712, + 1713 => 11713, + 1714 => 11714, + 1715 => 11715, + 1716 => 11716, + 1717 => 11717, + 1718 => 11718, + 1719 => 11719, + 1720 => 11720, + 1721 => 11721, + 1722 => 11722, + 1723 => 11723, + 1724 => 11724, + 1725 => 11725, + 1726 => 11726, + 1727 => 11727, + 1728 => 11728, + 1729 => 11729, + 1730 => 11730, + 1731 => 11731, + 1732 => 11732, + 1733 => 11733, + 1734 => 11734, + 1735 => 11735, + 1736 => 11736, + 1737 => 11737, + 1738 => 11738, + 1739 => 11739, + 1740 => 11740, + 1741 => 11741, + 1742 => 11742, + 1743 => 11743, + 1744 => 11744, + 1745 => 11745, + 1746 => 11746, + 1747 => 11747, + 1748 => 11748, + 1749 => 11749, + 1750 => 11750, + 1751 => 11751, + 1752 => 11752, + 1753 => 11753, + 1754 => 11754, + 1755 => 11755, + 1756 => 11756, + 1757 => 11757, + 1758 => 11758, + 1759 => 11759, + 1760 => 11760, + 1761 => 11761, + 1762 => 11762, + 1763 => 11763, + 1764 => 11764, + 1765 => 11765, + 1766 => 11766, + 1767 => 11767, + 1768 => 11768, + 1769 => 11769, + 1770 => 11770, + 1771 => 11771, + 1772 => 11772, + 1773 => 11773, + 1774 => 11774, + 1775 => 11775, + 1776 => 11776, + 1777 => 11777, + 1778 => 11778, + 1779 => 11779, + 1780 => 11780, + 1781 => 11781, + 1782 => 11782, + 1783 => 11783, + 1784 => 11784, + 1785 => 11785, + 1786 => 11786, + 1787 => 11787, + 1788 => 11788, + 1789 => 11789, + 1790 => 11790, + 1791 => 11791, + 1792 => 11792, + 1793 => 11793, + 1794 => 11794, + 1795 => 11795, + 1796 => 11796, + 1797 => 11797, + 1798 => 11798, + 1799 => 11799, + 1800 => 11800, + 1801 => 11801, + 1802 => 11802, + 1803 => 11803, + 1804 => 11804, + 1805 => 11805, + 1806 => 11806, + 1807 => 11807, + 1808 => 11808, + 1809 => 11809, + 1810 => 11810, + 1811 => 11811, + 1812 => 11812, + 1813 => 11813, + 1814 => 11814, + 1815 => 11815, + 1816 => 11816, + 1817 => 11817, + 1818 => 11818, + 1819 => 11819, + 1820 => 11820, + 1821 => 11821, + 1822 => 11822, + 1823 => 11823, + 1824 => 11824, + 1825 => 11825, + 1826 => 11826, + 1827 => 11827, + 1828 => 11828, + 1829 => 11829, + 1830 => 11830, + 1831 => 11831, + 1832 => 11832, + 1833 => 11833, + 1834 => 11834, + 1835 => 11835, + 1836 => 11836, + 1837 => 11837, + 1838 => 11838, + 1839 => 11839, + 1840 => 11840, + 1841 => 11841, + 1842 => 11842, + 1843 => 11843, + 1844 => 11844, + 1845 => 11845, + 1846 => 11846, + 1847 => 11847, + 1848 => 11848, + 1849 => 11849, + 1850 => 11850, + 1851 => 11851, + 1852 => 11852, + 1853 => 11853, + 1854 => 11854, + 1855 => 11855, + 1856 => 11856, + 1857 => 11857, + 1858 => 11858, + 1859 => 11859, + 1860 => 11860, + 1861 => 11861, + 1862 => 11862, + 1863 => 11863, + 1864 => 11864, + 1865 => 11865, + 1866 => 11866, + 1867 => 11867, + 1868 => 11868, + 1869 => 11869, + 1870 => 11870, + 1871 => 11871, + 1872 => 11872, + 1873 => 11873, + 1874 => 11874, + 1875 => 11875, + 1876 => 11876, + 1877 => 11877, + 1878 => 11878, + 1879 => 11879, + 1880 => 11880, + 1881 => 11881, + 1882 => 11882, + 1883 => 11883, + 1884 => 11884, + 1885 => 11885, + 1886 => 11886, + 1887 => 11887, + 1888 => 11888, + 1889 => 11889, + 1890 => 11890, + 1891 => 11891, + 1892 => 11892, + 1893 => 11893, + 1894 => 11894, + 1895 => 11895, + 1896 => 11896, + 1897 => 11897, + 1898 => 11898, + 1899 => 11899, + 1900 => 11900, + 1901 => 11901, + 1902 => 11902, + 1903 => 11903, + 1904 => 11904, + 1905 => 11905, + 1906 => 11906, + 1907 => 11907, + 1908 => 11908, + 1909 => 11909, + 1910 => 11910, + 1911 => 11911, + 1912 => 11912, + 1913 => 11913, + 1914 => 11914, + 1915 => 11915, + 1916 => 11916, + 1917 => 11917, + 1918 => 11918, + 1919 => 11919, + 1920 => 11920, + 1921 => 11921, + 1922 => 11922, + 1923 => 11923, + 1924 => 11924, + 1925 => 11925, + 1926 => 11926, + 1927 => 11927, + 1928 => 11928, + 1929 => 11929, + 1930 => 11930, + 1931 => 11931, + 1932 => 11932, + 1933 => 11933, + 1934 => 11934, + 1935 => 11935, + 1936 => 11936, + 1937 => 11937, + 1938 => 11938, + 1939 => 11939, + 1940 => 11940, + 1941 => 11941, + 1942 => 11942, + 1943 => 11943, + 1944 => 11944, + 1945 => 11945, + 1946 => 11946, + 1947 => 11947, + 1948 => 11948, + 1949 => 11949, + 1950 => 11950, + 1951 => 11951, + 1952 => 11952, + 1953 => 11953, + 1954 => 11954, + 1955 => 11955, + 1956 => 11956, + 1957 => 11957, + 1958 => 11958, + 1959 => 11959, + 1960 => 11960, + 1961 => 11961, + 1962 => 11962, + 1963 => 11963, + 1964 => 11964, + 1965 => 11965, + 1966 => 11966, + 1967 => 11967, + 1968 => 11968, + 1969 => 11969, + 1970 => 11970, + 1971 => 11971, + 1972 => 11972, + 1973 => 11973, + 1974 => 11974, + 1975 => 11975, + 1976 => 11976, + 1977 => 11977, + 1978 => 11978, + 1979 => 11979, + 1980 => 11980, + 1981 => 11981, + 1982 => 11982, + 1983 => 11983, + 1984 => 11984, + 1985 => 11985, + 1986 => 11986, + 1987 => 11987, + 1988 => 11988, + 1989 => 11989, + 1990 => 11990, + 1991 => 11991, + 1992 => 11992, + 1993 => 11993, + 1994 => 11994, + 1995 => 11995, + 1996 => 11996, + 1997 => 11997, + 1998 => 11998, + 1999 => 11999, + 2000 => 12000, + 2001 => 12001, + 2002 => 12002, + 2003 => 12003, + 2004 => 12004, + 2005 => 12005, + 2006 => 12006, + 2007 => 12007, + 2008 => 12008, + 2009 => 12009, + 2010 => 12010, + 2011 => 12011, + 2012 => 12012, + 2013 => 12013, + 2014 => 12014, + 2015 => 12015, + 2016 => 12016, + 2017 => 12017, + 2018 => 12018, + 2019 => 12019, + 2020 => 12020, + 2021 => 12021, + 2022 => 12022, + 2023 => 12023, + 2024 => 12024, + 2025 => 12025, + 2026 => 12026, + 2027 => 12027, + 2028 => 12028, + 2029 => 12029, + 2030 => 12030, + 2031 => 12031, + 2032 => 12032, + 2033 => 12033, + 2034 => 12034, + 2035 => 12035, + 2036 => 12036, + 2037 => 12037, + 2038 => 12038, + 2039 => 12039, + 2040 => 12040, + 2041 => 12041, + 2042 => 12042, + 2043 => 12043, + 2044 => 12044, + 2045 => 12045, + 2046 => 12046, + 2047 => 12047, + 2048 => 12048, + 2049 => 12049, + 2050 => 12050, + 2051 => 12051, + 2052 => 12052, + 2053 => 12053, + 2054 => 12054, + 2055 => 12055, + 2056 => 12056, + 2057 => 12057, + 2058 => 12058, + 2059 => 12059, + 2060 => 12060, + 2061 => 12061, + 2062 => 12062, + 2063 => 12063, + 2064 => 12064, + 2065 => 12065, + 2066 => 12066, + 2067 => 12067, + 2068 => 12068, + 2069 => 12069, + 2070 => 12070, + 2071 => 12071, + 2072 => 12072, + 2073 => 12073, + 2074 => 12074, + 2075 => 12075, + 2076 => 12076, + 2077 => 12077, + 2078 => 12078, + 2079 => 12079, + 2080 => 12080, + 2081 => 12081, + 2082 => 12082, + 2083 => 12083, + 2084 => 12084, + 2085 => 12085, + 2086 => 12086, + 2087 => 12087, + 2088 => 12088, + 2089 => 12089, + 2090 => 12090, + 2091 => 12091, + 2092 => 12092, + 2093 => 12093, + 2094 => 12094, + 2095 => 12095, + 2096 => 12096, + 2097 => 12097, + 2098 => 12098, + 2099 => 12099, + 2100 => 12100, + 2101 => 12101, + 2102 => 12102, + 2103 => 12103, + 2104 => 12104, + 2105 => 12105, + 2106 => 12106, + 2107 => 12107, + 2108 => 12108, + 2109 => 12109, + 2110 => 12110, + 2111 => 12111, + 2112 => 12112, + 2113 => 12113, + 2114 => 12114, + 2115 => 12115, + 2116 => 12116, + 2117 => 12117, + 2118 => 12118, + 2119 => 12119, + 2120 => 12120, + 2121 => 12121, + 2122 => 12122, + 2123 => 12123, + 2124 => 12124, + 2125 => 12125, + 2126 => 12126, + 2127 => 12127, + 2128 => 12128, + 2129 => 12129, + 2130 => 12130, + 2131 => 12131, + 2132 => 12132, + 2133 => 12133, + 2134 => 12134, + 2135 => 12135, + 2136 => 12136, + 2137 => 12137, + 2138 => 12138, + 2139 => 12139, + 2140 => 12140, + 2141 => 12141, + 2142 => 12142, + 2143 => 12143, + 2144 => 12144, + 2145 => 12145, + 2146 => 12146, + 2147 => 12147, + 2148 => 12148, + 2149 => 12149, + 2150 => 12150, + 2151 => 12151, + 2152 => 12152, + 2153 => 12153, + 2154 => 12154, + 2155 => 12155, + 2156 => 12156, + 2157 => 12157, + 2158 => 12158, + 2159 => 12159, + 2160 => 12160, + 2161 => 12161, + 2162 => 12162, + 2163 => 12163, + 2164 => 12164, + 2165 => 12165, + 2166 => 12166, + 2167 => 12167, + 2168 => 12168, + 2169 => 12169, + 2170 => 12170, + 2171 => 12171, + 2172 => 12172, + 2173 => 12173, + 2174 => 12174, + 2175 => 12175, + 2176 => 12176, + 2177 => 12177, + 2178 => 12178, + 2179 => 12179, + 2180 => 12180, + 2181 => 12181, + 2182 => 12182, + 2183 => 12183, + 2184 => 12184, + 2185 => 12185, + 2186 => 12186, + 2187 => 12187, + 2188 => 12188, + 2189 => 12189, + 2190 => 12190, + 2191 => 12191, + 2192 => 12192, + 2193 => 12193, + 2194 => 12194, + 2195 => 12195, + 2196 => 12196, + 2197 => 12197, + 2198 => 12198, + 2199 => 12199, + 2200 => 12200, + 2201 => 12201, + 2202 => 12202, + 2203 => 12203, + 2204 => 12204, + 2205 => 12205, + 2206 => 12206, + 2207 => 12207, + 2208 => 12208, + 2209 => 12209, + 2210 => 12210, + 2211 => 12211, + 2212 => 12212, + 2213 => 12213, + 2214 => 12214, + 2215 => 12215, + 2216 => 12216, + 2217 => 12217, + 2218 => 12218, + 2219 => 12219, + 2220 => 12220, + 2221 => 12221, + 2222 => 12222, + 2223 => 12223, + 2224 => 12224, + 2225 => 12225, + 2226 => 12226, + 2227 => 12227, + 2228 => 12228, + 2229 => 12229, + 2230 => 12230, + 2231 => 12231, + 2232 => 12232, + 2233 => 12233, + 2234 => 12234, + 2235 => 12235, + 2236 => 12236, + 2237 => 12237, + 2238 => 12238, + 2239 => 12239, + 2240 => 12240, + 2241 => 12241, + 2242 => 12242, + 2243 => 12243, + 2244 => 12244, + 2245 => 12245, + 2246 => 12246, + 2247 => 12247, + 2248 => 12248, + 2249 => 12249, + 2250 => 12250, + 2251 => 12251, + 2252 => 12252, + 2253 => 12253, + 2254 => 12254, + 2255 => 12255, + 2256 => 12256, + 2257 => 12257, + 2258 => 12258, + 2259 => 12259, + 2260 => 12260, + 2261 => 12261, + 2262 => 12262, + 2263 => 12263, + 2264 => 12264, + 2265 => 12265, + 2266 => 12266, + 2267 => 12267, + 2268 => 12268, + 2269 => 12269, + 2270 => 12270, + 2271 => 12271, + 2272 => 12272, + 2273 => 12273, + 2274 => 12274, + 2275 => 12275, + 2276 => 12276, + 2277 => 12277, + 2278 => 12278, + 2279 => 12279, + 2280 => 12280, + 2281 => 12281, + 2282 => 12282, + 2283 => 12283, + 2284 => 12284, + 2285 => 12285, + 2286 => 12286, + 2287 => 12287, + 2288 => 12288, + 2289 => 12289, + 2290 => 12290, + 2291 => 12291, + 2292 => 12292, + 2293 => 12293, + 2294 => 12294, + 2295 => 12295, + 2296 => 12296, + 2297 => 12297, + 2298 => 12298, + 2299 => 12299, + 2300 => 12300, + 2301 => 12301, + 2302 => 12302, + 2303 => 12303, + 2304 => 12304, + 2305 => 12305, + 2306 => 12306, + 2307 => 12307, + 2308 => 12308, + 2309 => 12309, + 2310 => 12310, + 2311 => 12311, + 2312 => 12312, + 2313 => 12313, + 2314 => 12314, + 2315 => 12315, + 2316 => 12316, + 2317 => 12317, + 2318 => 12318, + 2319 => 12319, + 2320 => 12320, + 2321 => 12321, + 2322 => 12322, + 2323 => 12323, + 2324 => 12324, + 2325 => 12325, + 2326 => 12326, + 2327 => 12327, + 2328 => 12328, + 2329 => 12329, + 2330 => 12330, + 2331 => 12331, + 2332 => 12332, + 2333 => 12333, + 2334 => 12334, + 2335 => 12335, + 2336 => 12336, + 2337 => 12337, + 2338 => 12338, + 2339 => 12339, + 2340 => 12340, + 2341 => 12341, + 2342 => 12342, + 2343 => 12343, + 2344 => 12344, + 2345 => 12345, + 2346 => 12346, + 2347 => 12347, + 2348 => 12348, + 2349 => 12349, + 2350 => 12350, + 2351 => 12351, + 2352 => 12352, + 2353 => 12353, + 2354 => 12354, + 2355 => 12355, + 2356 => 12356, + 2357 => 12357, + 2358 => 12358, + 2359 => 12359, + 2360 => 12360, + 2361 => 12361, + 2362 => 12362, + 2363 => 12363, + 2364 => 12364, + 2365 => 12365, + 2366 => 12366, + 2367 => 12367, + 2368 => 12368, + 2369 => 12369, + 2370 => 12370, + 2371 => 12371, + 2372 => 12372, + 2373 => 12373, + 2374 => 12374, + 2375 => 12375, + 2376 => 12376, + 2377 => 12377, + 2378 => 12378, + 2379 => 12379, + 2380 => 12380, + 2381 => 12381, + 2382 => 12382, + 2383 => 12383, + 2384 => 12384, + 2385 => 12385, + 2386 => 12386, + 2387 => 12387, + 2388 => 12388, + 2389 => 12389, + 2390 => 12390, + 2391 => 12391, + 2392 => 12392, + 2393 => 12393, + 2394 => 12394, + 2395 => 12395, + 2396 => 12396, + 2397 => 12397, + 2398 => 12398, + 2399 => 12399, + 2400 => 12400, + 2401 => 12401, + 2402 => 12402, + 2403 => 12403, + 2404 => 12404, + 2405 => 12405, + 2406 => 12406, + 2407 => 12407, + 2408 => 12408, + 2409 => 12409, + 2410 => 12410, + 2411 => 12411, + 2412 => 12412, + 2413 => 12413, + 2414 => 12414, + 2415 => 12415, + 2416 => 12416, + 2417 => 12417, + 2418 => 12418, + 2419 => 12419, + 2420 => 12420, + 2421 => 12421, + 2422 => 12422, + 2423 => 12423, + 2424 => 12424, + 2425 => 12425, + 2426 => 12426, + 2427 => 12427, + 2428 => 12428, + 2429 => 12429, + 2430 => 12430, + 2431 => 12431, + 2432 => 12432, + 2433 => 12433, + 2434 => 12434, + 2435 => 12435, + 2436 => 12436, + 2437 => 12437, + 2438 => 12438, + 2439 => 12439, + 2440 => 12440, + 2441 => 12441, + 2442 => 12442, + 2443 => 12443, + 2444 => 12444, + 2445 => 12445, + 2446 => 12446, + 2447 => 12447, + 2448 => 12448, + 2449 => 12449, + 2450 => 12450, + 2451 => 12451, + 2452 => 12452, + 2453 => 12453, + 2454 => 12454, + 2455 => 12455, + 2456 => 12456, + 2457 => 12457, + 2458 => 12458, + 2459 => 12459, + 2460 => 12460, + 2461 => 12461, + 2462 => 12462, + 2463 => 12463, + 2464 => 12464, + 2465 => 12465, + 2466 => 12466, + 2467 => 12467, + 2468 => 12468, + 2469 => 12469, + 2470 => 12470, + 2471 => 12471, + 2472 => 12472, + 2473 => 12473, + 2474 => 12474, + 2475 => 12475, + 2476 => 12476, + 2477 => 12477, + 2478 => 12478, + 2479 => 12479, + 2480 => 12480, + 2481 => 12481, + 2482 => 12482, + 2483 => 12483, + 2484 => 12484, + 2485 => 12485, + 2486 => 12486, + 2487 => 12487, + 2488 => 12488, + 2489 => 12489, + 2490 => 12490, + 2491 => 12491, + 2492 => 12492, + 2493 => 12493, + 2494 => 12494, + 2495 => 12495, + 2496 => 12496, + 2497 => 12497, + 2498 => 12498, + 2499 => 12499, + 2500 => 12500, + 2501 => 12501, + 2502 => 12502, + 2503 => 12503, + 2504 => 12504, + 2505 => 12505, + 2506 => 12506, + 2507 => 12507, + 2508 => 12508, + 2509 => 12509, + 2510 => 12510, + 2511 => 12511, + 2512 => 12512, + 2513 => 12513, + 2514 => 12514, + 2515 => 12515, + 2516 => 12516, + 2517 => 12517, + 2518 => 12518, + 2519 => 12519, + 2520 => 12520, + 2521 => 12521, + 2522 => 12522, + 2523 => 12523, + 2524 => 12524, + 2525 => 12525, + 2526 => 12526, + 2527 => 12527, + 2528 => 12528, + 2529 => 12529, + 2530 => 12530, + 2531 => 12531, + 2532 => 12532, + 2533 => 12533, + 2534 => 12534, + 2535 => 12535, + 2536 => 12536, + 2537 => 12537, + 2538 => 12538, + 2539 => 12539, + 2540 => 12540, + 2541 => 12541, + 2542 => 12542, + 2543 => 12543, + 2544 => 12544, + 2545 => 12545, + 2546 => 12546, + 2547 => 12547, + 2548 => 12548, + 2549 => 12549, + 2550 => 12550, + 2551 => 12551, + 2552 => 12552, + 2553 => 12553, + 2554 => 12554, + 2555 => 12555, + 2556 => 12556, + 2557 => 12557, + 2558 => 12558, + 2559 => 12559, + 2560 => 12560, + 2561 => 12561, + 2562 => 12562, + 2563 => 12563, + 2564 => 12564, + 2565 => 12565, + 2566 => 12566, + 2567 => 12567, + 2568 => 12568, + 2569 => 12569, + 2570 => 12570, + 2571 => 12571, + 2572 => 12572, + 2573 => 12573, + 2574 => 12574, + 2575 => 12575, + 2576 => 12576, + 2577 => 12577, + 2578 => 12578, + 2579 => 12579, + 2580 => 12580, + 2581 => 12581, + 2582 => 12582, + 2583 => 12583, + 2584 => 12584, + 2585 => 12585, + 2586 => 12586, + 2587 => 12587, + 2588 => 12588, + 2589 => 12589, + 2590 => 12590, + 2591 => 12591, + 2592 => 12592, + 2593 => 12593, + 2594 => 12594, + 2595 => 12595, + 2596 => 12596, + 2597 => 12597, + 2598 => 12598, + 2599 => 12599, + 2600 => 12600, + 2601 => 12601, + 2602 => 12602, + 2603 => 12603, + 2604 => 12604, + 2605 => 12605, + 2606 => 12606, + 2607 => 12607, + 2608 => 12608, + 2609 => 12609, + 2610 => 12610, + 2611 => 12611, + 2612 => 12612, + 2613 => 12613, + 2614 => 12614, + 2615 => 12615, + 2616 => 12616, + 2617 => 12617, + 2618 => 12618, + 2619 => 12619, + 2620 => 12620, + 2621 => 12621, + 2622 => 12622, + 2623 => 12623, + 2624 => 12624, + 2625 => 12625, + 2626 => 12626, + 2627 => 12627, + 2628 => 12628, + 2629 => 12629, + 2630 => 12630, + 2631 => 12631, + 2632 => 12632, + 2633 => 12633, + 2634 => 12634, + 2635 => 12635, + 2636 => 12636, + 2637 => 12637, + 2638 => 12638, + 2639 => 12639, + 2640 => 12640, + 2641 => 12641, + 2642 => 12642, + 2643 => 12643, + 2644 => 12644, + 2645 => 12645, + 2646 => 12646, + 2647 => 12647, + 2648 => 12648, + 2649 => 12649, + 2650 => 12650, + 2651 => 12651, + 2652 => 12652, + 2653 => 12653, + 2654 => 12654, + 2655 => 12655, + 2656 => 12656, + 2657 => 12657, + 2658 => 12658, + 2659 => 12659, + 2660 => 12660, + 2661 => 12661, + 2662 => 12662, + 2663 => 12663, + 2664 => 12664, + 2665 => 12665, + 2666 => 12666, + 2667 => 12667, + 2668 => 12668, + 2669 => 12669, + 2670 => 12670, + 2671 => 12671, + 2672 => 12672, + 2673 => 12673, + 2674 => 12674, + 2675 => 12675, + 2676 => 12676, + 2677 => 12677, + 2678 => 12678, + 2679 => 12679, + 2680 => 12680, + 2681 => 12681, + 2682 => 12682, + 2683 => 12683, + 2684 => 12684, + 2685 => 12685, + 2686 => 12686, + 2687 => 12687, + 2688 => 12688, + 2689 => 12689, + 2690 => 12690, + 2691 => 12691, + 2692 => 12692, + 2693 => 12693, + 2694 => 12694, + 2695 => 12695, + 2696 => 12696, + 2697 => 12697, + 2698 => 12698, + 2699 => 12699, + 2700 => 12700, + 2701 => 12701, + 2702 => 12702, + 2703 => 12703, + 2704 => 12704, + 2705 => 12705, + 2706 => 12706, + 2707 => 12707, + 2708 => 12708, + 2709 => 12709, + 2710 => 12710, + 2711 => 12711, + 2712 => 12712, + 2713 => 12713, + 2714 => 12714, + 2715 => 12715, + 2716 => 12716, + 2717 => 12717, + 2718 => 12718, + 2719 => 12719, + 2720 => 12720, + 2721 => 12721, + 2722 => 12722, + 2723 => 12723, + 2724 => 12724, + 2725 => 12725, + 2726 => 12726, + 2727 => 12727, + 2728 => 12728, + 2729 => 12729, + 2730 => 12730, + 2731 => 12731, + 2732 => 12732, + 2733 => 12733, + 2734 => 12734, + 2735 => 12735, + 2736 => 12736, + 2737 => 12737, + 2738 => 12738, + 2739 => 12739, + 2740 => 12740, + 2741 => 12741, + 2742 => 12742, + 2743 => 12743, + 2744 => 12744, + 2745 => 12745, + 2746 => 12746, + 2747 => 12747, + 2748 => 12748, + 2749 => 12749, + 2750 => 12750, + 2751 => 12751, + 2752 => 12752, + 2753 => 12753, + 2754 => 12754, + 2755 => 12755, + 2756 => 12756, + 2757 => 12757, + 2758 => 12758, + 2759 => 12759, + 2760 => 12760, + 2761 => 12761, + 2762 => 12762, + 2763 => 12763, + 2764 => 12764, + 2765 => 12765, + 2766 => 12766, + 2767 => 12767, + 2768 => 12768, + 2769 => 12769, + 2770 => 12770, + 2771 => 12771, + 2772 => 12772, + 2773 => 12773, + 2774 => 12774, + 2775 => 12775, + 2776 => 12776, + 2777 => 12777, + 2778 => 12778, + 2779 => 12779, + 2780 => 12780, + 2781 => 12781, + 2782 => 12782, + 2783 => 12783, + 2784 => 12784, + 2785 => 12785, + 2786 => 12786, + 2787 => 12787, + 2788 => 12788, + 2789 => 12789, + 2790 => 12790, + 2791 => 12791, + 2792 => 12792, + 2793 => 12793, + 2794 => 12794, + 2795 => 12795, + 2796 => 12796, + 2797 => 12797, + 2798 => 12798, + 2799 => 12799, + 2800 => 12800, + 2801 => 12801, + 2802 => 12802, + 2803 => 12803, + 2804 => 12804, + 2805 => 12805, + 2806 => 12806, + 2807 => 12807, + 2808 => 12808, + 2809 => 12809, + 2810 => 12810, + 2811 => 12811, + 2812 => 12812, + 2813 => 12813, + 2814 => 12814, + 2815 => 12815, + 2816 => 12816, + 2817 => 12817, + 2818 => 12818, + 2819 => 12819, + 2820 => 12820, + 2821 => 12821, + 2822 => 12822, + 2823 => 12823, + 2824 => 12824, + 2825 => 12825, + 2826 => 12826, + 2827 => 12827, + 2828 => 12828, + 2829 => 12829, + 2830 => 12830, + 2831 => 12831, + 2832 => 12832, + 2833 => 12833, + 2834 => 12834, + 2835 => 12835, + 2836 => 12836, + 2837 => 12837, + 2838 => 12838, + 2839 => 12839, + 2840 => 12840, + 2841 => 12841, + 2842 => 12842, + 2843 => 12843, + 2844 => 12844, + 2845 => 12845, + 2846 => 12846, + 2847 => 12847, + 2848 => 12848, + 2849 => 12849, + 2850 => 12850, + 2851 => 12851, + 2852 => 12852, + 2853 => 12853, + 2854 => 12854, + 2855 => 12855, + 2856 => 12856, + 2857 => 12857, + 2858 => 12858, + 2859 => 12859, + 2860 => 12860, + 2861 => 12861, + 2862 => 12862, + 2863 => 12863, + 2864 => 12864, + 2865 => 12865, + 2866 => 12866, + 2867 => 12867, + 2868 => 12868, + 2869 => 12869, + 2870 => 12870, + 2871 => 12871, + 2872 => 12872, + 2873 => 12873, + 2874 => 12874, + 2875 => 12875, + 2876 => 12876, + 2877 => 12877, + 2878 => 12878, + 2879 => 12879, + 2880 => 12880, + 2881 => 12881, + 2882 => 12882, + 2883 => 12883, + 2884 => 12884, + 2885 => 12885, + 2886 => 12886, + 2887 => 12887, + 2888 => 12888, + 2889 => 12889, + 2890 => 12890, + 2891 => 12891, + 2892 => 12892, + 2893 => 12893, + 2894 => 12894, + 2895 => 12895, + 2896 => 12896, + 2897 => 12897, + 2898 => 12898, + 2899 => 12899, + 2900 => 12900, + 2901 => 12901, + 2902 => 12902, + 2903 => 12903, + 2904 => 12904, + 2905 => 12905, + 2906 => 12906, + 2907 => 12907, + 2908 => 12908, + 2909 => 12909, + 2910 => 12910, + 2911 => 12911, + 2912 => 12912, + 2913 => 12913, + 2914 => 12914, + 2915 => 12915, + 2916 => 12916, + 2917 => 12917, + 2918 => 12918, + 2919 => 12919, + 2920 => 12920, + 2921 => 12921, + 2922 => 12922, + 2923 => 12923, + 2924 => 12924, + 2925 => 12925, + 2926 => 12926, + 2927 => 12927, + 2928 => 12928, + 2929 => 12929, + 2930 => 12930, + 2931 => 12931, + 2932 => 12932, + 2933 => 12933, + 2934 => 12934, + 2935 => 12935, + 2936 => 12936, + 2937 => 12937, + 2938 => 12938, + 2939 => 12939, + 2940 => 12940, + 2941 => 12941, + 2942 => 12942, + 2943 => 12943, + 2944 => 12944, + 2945 => 12945, + 2946 => 12946, + 2947 => 12947, + 2948 => 12948, + 2949 => 12949, + 2950 => 12950, + 2951 => 12951, + 2952 => 12952, + 2953 => 12953, + 2954 => 12954, + 2955 => 12955, + 2956 => 12956, + 2957 => 12957, + 2958 => 12958, + 2959 => 12959, + 2960 => 12960, + 2961 => 12961, + 2962 => 12962, + 2963 => 12963, + 2964 => 12964, + 2965 => 12965, + 2966 => 12966, + 2967 => 12967, + 2968 => 12968, + 2969 => 12969, + 2970 => 12970, + 2971 => 12971, + 2972 => 12972, + 2973 => 12973, + 2974 => 12974, + 2975 => 12975, + 2976 => 12976, + 2977 => 12977, + 2978 => 12978, + 2979 => 12979, + 2980 => 12980, + 2981 => 12981, + 2982 => 12982, + 2983 => 12983, + 2984 => 12984, + 2985 => 12985, + 2986 => 12986, + 2987 => 12987, + 2988 => 12988, + 2989 => 12989, + 2990 => 12990, + 2991 => 12991, + 2992 => 12992, + 2993 => 12993, + 2994 => 12994, + 2995 => 12995, + 2996 => 12996, + 2997 => 12997, + 2998 => 12998, + 2999 => 12999, + 3000 => 13000, + 3001 => 13001, + 3002 => 13002, + 3003 => 13003, + 3004 => 13004, + 3005 => 13005, + 3006 => 13006, + 3007 => 13007, + 3008 => 13008, + 3009 => 13009, + 3010 => 13010, + 3011 => 13011, + 3012 => 13012, + 3013 => 13013, + 3014 => 13014, + 3015 => 13015, + 3016 => 13016, + 3017 => 13017, + 3018 => 13018, + 3019 => 13019, + 3020 => 13020, + 3021 => 13021, + 3022 => 13022, + 3023 => 13023, + 3024 => 13024, + 3025 => 13025, + 3026 => 13026, + 3027 => 13027, + 3028 => 13028, + 3029 => 13029, + 3030 => 13030, + 3031 => 13031, + 3032 => 13032, + 3033 => 13033, + 3034 => 13034, + 3035 => 13035, + 3036 => 13036, + 3037 => 13037, + 3038 => 13038, + 3039 => 13039, + 3040 => 13040, + 3041 => 13041, + 3042 => 13042, + 3043 => 13043, + 3044 => 13044, + 3045 => 13045, + 3046 => 13046, + 3047 => 13047, + 3048 => 13048, + 3049 => 13049, + 3050 => 13050, + 3051 => 13051, + 3052 => 13052, + 3053 => 13053, + 3054 => 13054, + 3055 => 13055, + 3056 => 13056, + 3057 => 13057, + 3058 => 13058, + 3059 => 13059, + 3060 => 13060, + 3061 => 13061, + 3062 => 13062, + 3063 => 13063, + 3064 => 13064, + 3065 => 13065, + 3066 => 13066, + 3067 => 13067, + 3068 => 13068, + 3069 => 13069, + 3070 => 13070, + 3071 => 13071, + 3072 => 13072, + 3073 => 13073, + 3074 => 13074, + 3075 => 13075, + 3076 => 13076, + 3077 => 13077, + 3078 => 13078, + 3079 => 13079, + 3080 => 13080, + 3081 => 13081, + 3082 => 13082, + 3083 => 13083, + 3084 => 13084, + 3085 => 13085, + 3086 => 13086, + 3087 => 13087, + 3088 => 13088, + 3089 => 13089, + 3090 => 13090, + 3091 => 13091, + 3092 => 13092, + 3093 => 13093, + 3094 => 13094, + 3095 => 13095, + 3096 => 13096, + 3097 => 13097, + 3098 => 13098, + 3099 => 13099, + 3100 => 13100, + 3101 => 13101, + 3102 => 13102, + 3103 => 13103, + 3104 => 13104, + 3105 => 13105, + 3106 => 13106, + 3107 => 13107, + 3108 => 13108, + 3109 => 13109, + 3110 => 13110, + 3111 => 13111, + 3112 => 13112, + 3113 => 13113, + 3114 => 13114, + 3115 => 13115, + 3116 => 13116, + 3117 => 13117, + 3118 => 13118, + 3119 => 13119, + 3120 => 13120, + 3121 => 13121, + 3122 => 13122, + 3123 => 13123, + 3124 => 13124, + 3125 => 13125, + 3126 => 13126, + 3127 => 13127, + 3128 => 13128, + 3129 => 13129, + 3130 => 13130, + 3131 => 13131, + 3132 => 13132, + 3133 => 13133, + 3134 => 13134, + 3135 => 13135, + 3136 => 13136, + 3137 => 13137, + 3138 => 13138, + 3139 => 13139, + 3140 => 13140, + 3141 => 13141, + 3142 => 13142, + 3143 => 13143, + 3144 => 13144, + 3145 => 13145, + 3146 => 13146, + 3147 => 13147, + 3148 => 13148, + 3149 => 13149, + 3150 => 13150, + 3151 => 13151, + 3152 => 13152, + 3153 => 13153, + 3154 => 13154, + 3155 => 13155, + 3156 => 13156, + 3157 => 13157, + 3158 => 13158, + 3159 => 13159, + 3160 => 13160, + 3161 => 13161, + 3162 => 13162, + 3163 => 13163, + 3164 => 13164, + 3165 => 13165, + 3166 => 13166, + 3167 => 13167, + 3168 => 13168, + 3169 => 13169, + 3170 => 13170, + 3171 => 13171, + 3172 => 13172, + 3173 => 13173, + 3174 => 13174, + 3175 => 13175, + 3176 => 13176, + 3177 => 13177, + 3178 => 13178, + 3179 => 13179, + 3180 => 13180, + 3181 => 13181, + 3182 => 13182, + 3183 => 13183, + 3184 => 13184, + 3185 => 13185, + 3186 => 13186, + 3187 => 13187, + 3188 => 13188, + 3189 => 13189, + 3190 => 13190, + 3191 => 13191, + 3192 => 13192, + 3193 => 13193, + 3194 => 13194, + 3195 => 13195, + 3196 => 13196, + 3197 => 13197, + 3198 => 13198, + 3199 => 13199, + 3200 => 13200, + 3201 => 13201, + 3202 => 13202, + 3203 => 13203, + 3204 => 13204, + 3205 => 13205, + 3206 => 13206, + 3207 => 13207, + 3208 => 13208, + 3209 => 13209, + 3210 => 13210, + 3211 => 13211, + 3212 => 13212, + 3213 => 13213, + 3214 => 13214, + 3215 => 13215, + 3216 => 13216, + 3217 => 13217, + 3218 => 13218, + 3219 => 13219, + 3220 => 13220, + 3221 => 13221, + 3222 => 13222, + 3223 => 13223, + 3224 => 13224, + 3225 => 13225, + 3226 => 13226, + 3227 => 13227, + 3228 => 13228, + 3229 => 13229, + 3230 => 13230, + 3231 => 13231, + 3232 => 13232, + 3233 => 13233, + 3234 => 13234, + 3235 => 13235, + 3236 => 13236, + 3237 => 13237, + 3238 => 13238, + 3239 => 13239, + 3240 => 13240, + 3241 => 13241, + 3242 => 13242, + 3243 => 13243, + 3244 => 13244, + 3245 => 13245, + 3246 => 13246, + 3247 => 13247, + 3248 => 13248, + 3249 => 13249, + 3250 => 13250, + 3251 => 13251, + 3252 => 13252, + 3253 => 13253, + 3254 => 13254, + 3255 => 13255, + 3256 => 13256, + 3257 => 13257, + 3258 => 13258, + 3259 => 13259, + 3260 => 13260, + 3261 => 13261, + 3262 => 13262, + 3263 => 13263, + 3264 => 13264, + 3265 => 13265, + 3266 => 13266, + 3267 => 13267, + 3268 => 13268, + 3269 => 13269, + 3270 => 13270, + 3271 => 13271, + 3272 => 13272, + 3273 => 13273, + 3274 => 13274, + 3275 => 13275, + 3276 => 13276, + 3277 => 13277, + 3278 => 13278, + 3279 => 13279, + 3280 => 13280, + 3281 => 13281, + 3282 => 13282, + 3283 => 13283, + 3284 => 13284, + 3285 => 13285, + 3286 => 13286, + 3287 => 13287, + 3288 => 13288, + 3289 => 13289, + 3290 => 13290, + 3291 => 13291, + 3292 => 13292, + 3293 => 13293, + 3294 => 13294, + 3295 => 13295, + 3296 => 13296, + 3297 => 13297, + 3298 => 13298, + 3299 => 13299, + 3300 => 13300, + 3301 => 13301, + 3302 => 13302, + 3303 => 13303, + 3304 => 13304, + 3305 => 13305, + 3306 => 13306, + 3307 => 13307, + 3308 => 13308, + 3309 => 13309, + 3310 => 13310, + 3311 => 13311, + 3312 => 13312, + 3313 => 13313, + 3314 => 13314, + 3315 => 13315, + 3316 => 13316, + 3317 => 13317, + 3318 => 13318, + 3319 => 13319, + 3320 => 13320, + 3321 => 13321, + 3322 => 13322, + 3323 => 13323, + 3324 => 13324, + 3325 => 13325, + 3326 => 13326, + 3327 => 13327, + 3328 => 13328, + 3329 => 13329, + 3330 => 13330, + 3331 => 13331, + 3332 => 13332, + 3333 => 13333, + 3334 => 13334, + 3335 => 13335, + 3336 => 13336, + 3337 => 13337, + 3338 => 13338, + 3339 => 13339, + 3340 => 13340, + 3341 => 13341, + 3342 => 13342, + 3343 => 13343, + 3344 => 13344, + 3345 => 13345, + 3346 => 13346, + 3347 => 13347, + 3348 => 13348, + 3349 => 13349, + 3350 => 13350, + 3351 => 13351, + 3352 => 13352, + 3353 => 13353, + 3354 => 13354, + 3355 => 13355, + 3356 => 13356, + 3357 => 13357, + 3358 => 13358, + 3359 => 13359, + 3360 => 13360, + 3361 => 13361, + 3362 => 13362, + 3363 => 13363, + 3364 => 13364, + 3365 => 13365, + 3366 => 13366, + 3367 => 13367, + 3368 => 13368, + 3369 => 13369, + 3370 => 13370, + 3371 => 13371, + 3372 => 13372, + 3373 => 13373, + 3374 => 13374, + 3375 => 13375, + 3376 => 13376, + 3377 => 13377, + 3378 => 13378, + 3379 => 13379, + 3380 => 13380, + 3381 => 13381, + 3382 => 13382, + 3383 => 13383, + 3384 => 13384, + 3385 => 13385, + 3386 => 13386, + 3387 => 13387, + 3388 => 13388, + 3389 => 13389, + 3390 => 13390, + 3391 => 13391, + 3392 => 13392, + 3393 => 13393, + 3394 => 13394, + 3395 => 13395, + 3396 => 13396, + 3397 => 13397, + 3398 => 13398, + 3399 => 13399, + 3400 => 13400, + 3401 => 13401, + 3402 => 13402, + 3403 => 13403, + 3404 => 13404, + 3405 => 13405, + 3406 => 13406, + 3407 => 13407, + 3408 => 13408, + 3409 => 13409, + 3410 => 13410, + 3411 => 13411, + 3412 => 13412, + 3413 => 13413, + 3414 => 13414, + 3415 => 13415, + 3416 => 13416, + 3417 => 13417, + 3418 => 13418, + 3419 => 13419, + 3420 => 13420, + 3421 => 13421, + 3422 => 13422, + 3423 => 13423, + 3424 => 13424, + 3425 => 13425, + 3426 => 13426, + 3427 => 13427, + 3428 => 13428, + 3429 => 13429, + 3430 => 13430, + 3431 => 13431, + 3432 => 13432, + 3433 => 13433, + 3434 => 13434, + 3435 => 13435, + 3436 => 13436, + 3437 => 13437, + 3438 => 13438, + 3439 => 13439, + 3440 => 13440, + 3441 => 13441, + 3442 => 13442, + 3443 => 13443, + 3444 => 13444, + 3445 => 13445, + 3446 => 13446, + 3447 => 13447, + 3448 => 13448, + 3449 => 13449, + 3450 => 13450, + 3451 => 13451, + 3452 => 13452, + 3453 => 13453, + 3454 => 13454, + 3455 => 13455, + 3456 => 13456, + 3457 => 13457, + 3458 => 13458, + 3459 => 13459, + 3460 => 13460, + 3461 => 13461, + 3462 => 13462, + 3463 => 13463, + 3464 => 13464, + 3465 => 13465, + 3466 => 13466, + 3467 => 13467, + 3468 => 13468, + 3469 => 13469, + 3470 => 13470, + 3471 => 13471, + 3472 => 13472, + 3473 => 13473, + 3474 => 13474, + 3475 => 13475, + 3476 => 13476, + 3477 => 13477, + 3478 => 13478, + 3479 => 13479, + 3480 => 13480, + 3481 => 13481, + 3482 => 13482, + 3483 => 13483, + 3484 => 13484, + 3485 => 13485, + 3486 => 13486, + 3487 => 13487, + 3488 => 13488, + 3489 => 13489, + 3490 => 13490, + 3491 => 13491, + 3492 => 13492, + 3493 => 13493, + 3494 => 13494, + 3495 => 13495, + 3496 => 13496, + 3497 => 13497, + 3498 => 13498, + 3499 => 13499, + 3500 => 13500, + 3501 => 13501, + 3502 => 13502, + 3503 => 13503, + 3504 => 13504, + 3505 => 13505, + 3506 => 13506, + 3507 => 13507, + 3508 => 13508, + 3509 => 13509, + 3510 => 13510, + 3511 => 13511, + 3512 => 13512, + 3513 => 13513, + 3514 => 13514, + 3515 => 13515, + 3516 => 13516, + 3517 => 13517, + 3518 => 13518, + 3519 => 13519, + 3520 => 13520, + 3521 => 13521, + 3522 => 13522, + 3523 => 13523, + 3524 => 13524, + 3525 => 13525, + 3526 => 13526, + 3527 => 13527, + 3528 => 13528, + 3529 => 13529, + 3530 => 13530, + 3531 => 13531, + 3532 => 13532, + 3533 => 13533, + 3534 => 13534, + 3535 => 13535, + 3536 => 13536, + 3537 => 13537, + 3538 => 13538, + 3539 => 13539, + 3540 => 13540, + 3541 => 13541, + 3542 => 13542, + 3543 => 13543, + 3544 => 13544, + 3545 => 13545, + 3546 => 13546, + 3547 => 13547, + 3548 => 13548, + 3549 => 13549, + 3550 => 13550, + 3551 => 13551, + 3552 => 13552, + 3553 => 13553, + 3554 => 13554, + 3555 => 13555, + 3556 => 13556, + 3557 => 13557, + 3558 => 13558, + 3559 => 13559, + 3560 => 13560, + 3561 => 13561, + 3562 => 13562, + 3563 => 13563, + 3564 => 13564, + 3565 => 13565, + 3566 => 13566, + 3567 => 13567, + 3568 => 13568, + 3569 => 13569, + 3570 => 13570, + 3571 => 13571, + 3572 => 13572, + 3573 => 13573, + 3574 => 13574, + 3575 => 13575, + 3576 => 13576, + 3577 => 13577, + 3578 => 13578, + 3579 => 13579, + 3580 => 13580, + 3581 => 13581, + 3582 => 13582, + 3583 => 13583, + 3584 => 13584, + 3585 => 13585, + 3586 => 13586, + 3587 => 13587, + 3588 => 13588, + 3589 => 13589, + 3590 => 13590, + 3591 => 13591, + 3592 => 13592, + 3593 => 13593, + 3594 => 13594, + 3595 => 13595, + 3596 => 13596, + 3597 => 13597, + 3598 => 13598, + 3599 => 13599, + 3600 => 13600, + 3601 => 13601, + 3602 => 13602, + 3603 => 13603, + 3604 => 13604, + 3605 => 13605, + 3606 => 13606, + 3607 => 13607, + 3608 => 13608, + 3609 => 13609, + 3610 => 13610, + 3611 => 13611, + 3612 => 13612, + 3613 => 13613, + 3614 => 13614, + 3615 => 13615, + 3616 => 13616, + 3617 => 13617, + 3618 => 13618, + 3619 => 13619, + 3620 => 13620, + 3621 => 13621, + 3622 => 13622, + 3623 => 13623, + 3624 => 13624, + 3625 => 13625, + 3626 => 13626, + 3627 => 13627, + 3628 => 13628, + 3629 => 13629, + 3630 => 13630, + 3631 => 13631, + 3632 => 13632, + 3633 => 13633, + 3634 => 13634, + 3635 => 13635, + 3636 => 13636, + 3637 => 13637, + 3638 => 13638, + 3639 => 13639, + 3640 => 13640, + 3641 => 13641, + 3642 => 13642, + 3643 => 13643, + 3644 => 13644, + 3645 => 13645, + 3646 => 13646, + 3647 => 13647, + 3648 => 13648, + 3649 => 13649, + 3650 => 13650, + 3651 => 13651, + 3652 => 13652, + 3653 => 13653, + 3654 => 13654, + 3655 => 13655, + 3656 => 13656, + 3657 => 13657, + 3658 => 13658, + 3659 => 13659, + 3660 => 13660, + 3661 => 13661, + 3662 => 13662, + 3663 => 13663, + 3664 => 13664, + 3665 => 13665, + 3666 => 13666, + 3667 => 13667, + 3668 => 13668, + 3669 => 13669, + 3670 => 13670, + 3671 => 13671, + 3672 => 13672, + 3673 => 13673, + 3674 => 13674, + 3675 => 13675, + 3676 => 13676, + 3677 => 13677, + 3678 => 13678, + 3679 => 13679, + 3680 => 13680, + 3681 => 13681, + 3682 => 13682, + 3683 => 13683, + 3684 => 13684, + 3685 => 13685, + 3686 => 13686, + 3687 => 13687, + 3688 => 13688, + 3689 => 13689, + 3690 => 13690, + 3691 => 13691, + 3692 => 13692, + 3693 => 13693, + 3694 => 13694, + 3695 => 13695, + 3696 => 13696, + 3697 => 13697, + 3698 => 13698, + 3699 => 13699, + 3700 => 13700, + 3701 => 13701, + 3702 => 13702, + 3703 => 13703, + 3704 => 13704, + 3705 => 13705, + 3706 => 13706, + 3707 => 13707, + 3708 => 13708, + 3709 => 13709, + 3710 => 13710, + 3711 => 13711, + 3712 => 13712, + 3713 => 13713, + 3714 => 13714, + 3715 => 13715, + 3716 => 13716, + 3717 => 13717, + 3718 => 13718, + 3719 => 13719, + 3720 => 13720, + 3721 => 13721, + 3722 => 13722, + 3723 => 13723, + 3724 => 13724, + 3725 => 13725, + 3726 => 13726, + 3727 => 13727, + 3728 => 13728, + 3729 => 13729, + 3730 => 13730, + 3731 => 13731, + 3732 => 13732, + 3733 => 13733, + 3734 => 13734, + 3735 => 13735, + 3736 => 13736, + 3737 => 13737, + 3738 => 13738, + 3739 => 13739, + 3740 => 13740, + 3741 => 13741, + 3742 => 13742, + 3743 => 13743, + 3744 => 13744, + 3745 => 13745, + 3746 => 13746, + 3747 => 13747, + 3748 => 13748, + 3749 => 13749, + 3750 => 13750, + 3751 => 13751, + 3752 => 13752, + 3753 => 13753, + 3754 => 13754, + 3755 => 13755, + 3756 => 13756, + 3757 => 13757, + 3758 => 13758, + 3759 => 13759, + 3760 => 13760, + 3761 => 13761, + 3762 => 13762, + 3763 => 13763, + 3764 => 13764, + 3765 => 13765, + 3766 => 13766, + 3767 => 13767, + 3768 => 13768, + 3769 => 13769, + 3770 => 13770, + 3771 => 13771, + 3772 => 13772, + 3773 => 13773, + 3774 => 13774, + 3775 => 13775, + 3776 => 13776, + 3777 => 13777, + 3778 => 13778, + 3779 => 13779, + 3780 => 13780, + 3781 => 13781, + 3782 => 13782, + 3783 => 13783, + 3784 => 13784, + 3785 => 13785, + 3786 => 13786, + 3787 => 13787, + 3788 => 13788, + 3789 => 13789, + 3790 => 13790, + 3791 => 13791, + 3792 => 13792, + 3793 => 13793, + 3794 => 13794, + 3795 => 13795, + 3796 => 13796, + 3797 => 13797, + 3798 => 13798, + 3799 => 13799, + 3800 => 13800, + 3801 => 13801, + 3802 => 13802, + 3803 => 13803, + 3804 => 13804, + 3805 => 13805, + 3806 => 13806, + 3807 => 13807, + 3808 => 13808, + 3809 => 13809, + 3810 => 13810, + 3811 => 13811, + 3812 => 13812, + 3813 => 13813, + 3814 => 13814, + 3815 => 13815, + 3816 => 13816, + 3817 => 13817, + 3818 => 13818, + 3819 => 13819, + 3820 => 13820, + 3821 => 13821, + 3822 => 13822, + 3823 => 13823, + 3824 => 13824, + 3825 => 13825, + 3826 => 13826, + 3827 => 13827, + 3828 => 13828, + 3829 => 13829, + 3830 => 13830, + 3831 => 13831, + 3832 => 13832, + 3833 => 13833, + 3834 => 13834, + 3835 => 13835, + 3836 => 13836, + 3837 => 13837, + 3838 => 13838, + 3839 => 13839, + 3840 => 13840, + 3841 => 13841, + 3842 => 13842, + 3843 => 13843, + 3844 => 13844, + 3845 => 13845, + 3846 => 13846, + 3847 => 13847, + 3848 => 13848, + 3849 => 13849, + 3850 => 13850, + 3851 => 13851, + 3852 => 13852, + 3853 => 13853, + 3854 => 13854, + 3855 => 13855, + 3856 => 13856, + 3857 => 13857, + 3858 => 13858, + 3859 => 13859, + 3860 => 13860, + 3861 => 13861, + 3862 => 13862, + 3863 => 13863, + 3864 => 13864, + 3865 => 13865, + 3866 => 13866, + 3867 => 13867, + 3868 => 13868, + 3869 => 13869, + 3870 => 13870, + 3871 => 13871, + 3872 => 13872, + 3873 => 13873, + 3874 => 13874, + 3875 => 13875, + 3876 => 13876, + 3877 => 13877, + 3878 => 13878, + 3879 => 13879, + 3880 => 13880, + 3881 => 13881, + 3882 => 13882, + 3883 => 13883, + 3884 => 13884, + 3885 => 13885, + 3886 => 13886, + 3887 => 13887, + 3888 => 13888, + 3889 => 13889, + 3890 => 13890, + 3891 => 13891, + 3892 => 13892, + 3893 => 13893, + 3894 => 13894, + 3895 => 13895, + 3896 => 13896, + 3897 => 13897, + 3898 => 13898, + 3899 => 13899, + 3900 => 13900, + 3901 => 13901, + 3902 => 13902, + 3903 => 13903, + 3904 => 13904, + 3905 => 13905, + 3906 => 13906, + 3907 => 13907, + 3908 => 13908, + 3909 => 13909, + 3910 => 13910, + 3911 => 13911, + 3912 => 13912, + 3913 => 13913, + 3914 => 13914, + 3915 => 13915, + 3916 => 13916, + 3917 => 13917, + 3918 => 13918, + 3919 => 13919, + 3920 => 13920, + 3921 => 13921, + 3922 => 13922, + 3923 => 13923, + 3924 => 13924, + 3925 => 13925, + 3926 => 13926, + 3927 => 13927, + 3928 => 13928, + 3929 => 13929, + 3930 => 13930, + 3931 => 13931, + 3932 => 13932, + 3933 => 13933, + 3934 => 13934, + 3935 => 13935, + 3936 => 13936, + 3937 => 13937, + 3938 => 13938, + 3939 => 13939, + 3940 => 13940, + 3941 => 13941, + 3942 => 13942, + 3943 => 13943, + 3944 => 13944, + 3945 => 13945, + 3946 => 13946, + 3947 => 13947, + 3948 => 13948, + 3949 => 13949, + 3950 => 13950, + 3951 => 13951, + 3952 => 13952, + 3953 => 13953, + 3954 => 13954, + 3955 => 13955, + 3956 => 13956, + 3957 => 13957, + 3958 => 13958, + 3959 => 13959, + 3960 => 13960, + 3961 => 13961, + 3962 => 13962, + 3963 => 13963, + 3964 => 13964, + 3965 => 13965, + 3966 => 13966, + 3967 => 13967, + 3968 => 13968, + 3969 => 13969, + 3970 => 13970, + 3971 => 13971, + 3972 => 13972, + 3973 => 13973, + 3974 => 13974, + 3975 => 13975, + 3976 => 13976, + 3977 => 13977, + 3978 => 13978, + 3979 => 13979, + 3980 => 13980, + 3981 => 13981, + 3982 => 13982, + 3983 => 13983, + 3984 => 13984, + 3985 => 13985, + 3986 => 13986, + 3987 => 13987, + 3988 => 13988, + 3989 => 13989, + 3990 => 13990, + 3991 => 13991, + 3992 => 13992, + 3993 => 13993, + 3994 => 13994, + 3995 => 13995, + 3996 => 13996, + 3997 => 13997, + 3998 => 13998, + 3999 => 13999, + 4000 => 14000, + 4001 => 14001, + 4002 => 14002, + 4003 => 14003, + 4004 => 14004, + 4005 => 14005, + 4006 => 14006, + 4007 => 14007, + 4008 => 14008, + 4009 => 14009, + 4010 => 14010, + 4011 => 14011, + 4012 => 14012, + 4013 => 14013, + 4014 => 14014, + 4015 => 14015, + 4016 => 14016, + 4017 => 14017, + 4018 => 14018, + 4019 => 14019, + 4020 => 14020, + 4021 => 14021, + 4022 => 14022, + 4023 => 14023, + 4024 => 14024, + 4025 => 14025, + 4026 => 14026, + 4027 => 14027, + 4028 => 14028, + 4029 => 14029, + 4030 => 14030, + 4031 => 14031, + 4032 => 14032, + 4033 => 14033, + 4034 => 14034, + 4035 => 14035, + 4036 => 14036, + 4037 => 14037, + 4038 => 14038, + 4039 => 14039, + 4040 => 14040, + 4041 => 14041, + 4042 => 14042, + 4043 => 14043, + 4044 => 14044, + 4045 => 14045, + 4046 => 14046, + 4047 => 14047, + 4048 => 14048, + 4049 => 14049, + 4050 => 14050, + 4051 => 14051, + 4052 => 14052, + 4053 => 14053, + 4054 => 14054, + 4055 => 14055, + 4056 => 14056, + 4057 => 14057, + 4058 => 14058, + 4059 => 14059, + 4060 => 14060, + 4061 => 14061, + 4062 => 14062, + 4063 => 14063, + 4064 => 14064, + 4065 => 14065, + 4066 => 14066, + 4067 => 14067, + 4068 => 14068, + 4069 => 14069, + 4070 => 14070, + 4071 => 14071, + 4072 => 14072, + 4073 => 14073, + 4074 => 14074, + 4075 => 14075, + 4076 => 14076, + 4077 => 14077, + 4078 => 14078, + 4079 => 14079, + 4080 => 14080, + 4081 => 14081, + 4082 => 14082, + 4083 => 14083, + 4084 => 14084, + 4085 => 14085, + 4086 => 14086, + 4087 => 14087, + 4088 => 14088, + 4089 => 14089, + 4090 => 14090, + 4091 => 14091, + 4092 => 14092, + 4093 => 14093, + 4094 => 14094, + 4095 => 14095, + 4096 => 14096, + 4097 => 14097, + 4098 => 14098, + 4099 => 14099, + 4100 => 14100, + 4101 => 14101, + 4102 => 14102, + 4103 => 14103, + 4104 => 14104, + 4105 => 14105, + 4106 => 14106, + 4107 => 14107, + 4108 => 14108, + 4109 => 14109, + 4110 => 14110, + 4111 => 14111, + 4112 => 14112, + 4113 => 14113, + 4114 => 14114, + 4115 => 14115, + 4116 => 14116, + 4117 => 14117, + 4118 => 14118, + 4119 => 14119, + 4120 => 14120, + 4121 => 14121, + 4122 => 14122, + 4123 => 14123, + 4124 => 14124, + 4125 => 14125, + 4126 => 14126, + 4127 => 14127, + 4128 => 14128, + 4129 => 14129, + 4130 => 14130, + 4131 => 14131, + 4132 => 14132, + 4133 => 14133, + 4134 => 14134, + 4135 => 14135, + 4136 => 14136, + 4137 => 14137, + 4138 => 14138, + 4139 => 14139, + 4140 => 14140, + 4141 => 14141, + 4142 => 14142, + 4143 => 14143, + 4144 => 14144, + 4145 => 14145, + 4146 => 14146, + 4147 => 14147, + 4148 => 14148, + 4149 => 14149, + 4150 => 14150, + 4151 => 14151, + 4152 => 14152, + 4153 => 14153, + 4154 => 14154, + 4155 => 14155, + 4156 => 14156, + 4157 => 14157, + 4158 => 14158, + 4159 => 14159, + 4160 => 14160, + 4161 => 14161, + 4162 => 14162, + 4163 => 14163, + 4164 => 14164, + 4165 => 14165, + 4166 => 14166, + 4167 => 14167, + 4168 => 14168, + 4169 => 14169, + 4170 => 14170, + 4171 => 14171, + 4172 => 14172, + 4173 => 14173, + 4174 => 14174, + 4175 => 14175, + 4176 => 14176, + 4177 => 14177, + 4178 => 14178, + 4179 => 14179, + 4180 => 14180, + 4181 => 14181, + 4182 => 14182, + 4183 => 14183, + 4184 => 14184, + 4185 => 14185, + 4186 => 14186, + 4187 => 14187, + 4188 => 14188, + 4189 => 14189, + 4190 => 14190, + 4191 => 14191, + 4192 => 14192, + 4193 => 14193, + 4194 => 14194, + 4195 => 14195, + 4196 => 14196, + 4197 => 14197, + 4198 => 14198, + 4199 => 14199, + 4200 => 14200, + 4201 => 14201, + 4202 => 14202, + 4203 => 14203, + 4204 => 14204, + 4205 => 14205, + 4206 => 14206, + 4207 => 14207, + 4208 => 14208, + 4209 => 14209, + 4210 => 14210, + 4211 => 14211, + 4212 => 14212, + 4213 => 14213, + 4214 => 14214, + 4215 => 14215, + 4216 => 14216, + 4217 => 14217, + 4218 => 14218, + 4219 => 14219, + 4220 => 14220, + 4221 => 14221, + 4222 => 14222, + 4223 => 14223, + 4224 => 14224, + 4225 => 14225, + 4226 => 14226, + 4227 => 14227, + 4228 => 14228, + 4229 => 14229, + 4230 => 14230, + 4231 => 14231, + 4232 => 14232, + 4233 => 14233, + 4234 => 14234, + 4235 => 14235, + 4236 => 14236, + 4237 => 14237, + 4238 => 14238, + 4239 => 14239, + 4240 => 14240, + 4241 => 14241, + 4242 => 14242, + 4243 => 14243, + 4244 => 14244, + 4245 => 14245, + 4246 => 14246, + 4247 => 14247, + 4248 => 14248, + 4249 => 14249, + 4250 => 14250, + 4251 => 14251, + 4252 => 14252, + 4253 => 14253, + 4254 => 14254, + 4255 => 14255, + 4256 => 14256, + 4257 => 14257, + 4258 => 14258, + 4259 => 14259, + 4260 => 14260, + 4261 => 14261, + 4262 => 14262, + 4263 => 14263, + 4264 => 14264, + 4265 => 14265, + 4266 => 14266, + 4267 => 14267, + 4268 => 14268, + 4269 => 14269, + 4270 => 14270, + 4271 => 14271, + 4272 => 14272, + 4273 => 14273, + 4274 => 14274, + 4275 => 14275, + 4276 => 14276, + 4277 => 14277, + 4278 => 14278, + 4279 => 14279, + 4280 => 14280, + 4281 => 14281, + 4282 => 14282, + 4283 => 14283, + 4284 => 14284, + 4285 => 14285, + 4286 => 14286, + 4287 => 14287, + 4288 => 14288, + 4289 => 14289, + 4290 => 14290, + 4291 => 14291, + 4292 => 14292, + 4293 => 14293, + 4294 => 14294, + 4295 => 14295, + 4296 => 14296, + 4297 => 14297, + 4298 => 14298, + 4299 => 14299, + 4300 => 14300, + 4301 => 14301, + 4302 => 14302, + 4303 => 14303, + 4304 => 14304, + 4305 => 14305, + 4306 => 14306, + 4307 => 14307, + 4308 => 14308, + 4309 => 14309, + 4310 => 14310, + 4311 => 14311, + 4312 => 14312, + 4313 => 14313, + 4314 => 14314, + 4315 => 14315, + 4316 => 14316, + 4317 => 14317, + 4318 => 14318, + 4319 => 14319, + 4320 => 14320, + 4321 => 14321, + 4322 => 14322, + 4323 => 14323, + 4324 => 14324, + 4325 => 14325, + 4326 => 14326, + 4327 => 14327, + 4328 => 14328, + 4329 => 14329, + 4330 => 14330, + 4331 => 14331, + 4332 => 14332, + 4333 => 14333, + 4334 => 14334, + 4335 => 14335, + 4336 => 14336, + 4337 => 14337, + 4338 => 14338, + 4339 => 14339, + 4340 => 14340, + 4341 => 14341, + 4342 => 14342, + 4343 => 14343, + 4344 => 14344, + 4345 => 14345, + 4346 => 14346, + 4347 => 14347, + 4348 => 14348, + 4349 => 14349, + 4350 => 14350, + 4351 => 14351, + 4352 => 14352, + 4353 => 14353, + 4354 => 14354, + 4355 => 14355, + 4356 => 14356, + 4357 => 14357, + 4358 => 14358, + 4359 => 14359, + 4360 => 14360, + 4361 => 14361, + 4362 => 14362, + 4363 => 14363, + 4364 => 14364, + 4365 => 14365, + 4366 => 14366, + 4367 => 14367, + 4368 => 14368, + 4369 => 14369, + 4370 => 14370, + 4371 => 14371, + 4372 => 14372, + 4373 => 14373, + 4374 => 14374, + 4375 => 14375, + 4376 => 14376, + 4377 => 14377, + 4378 => 14378, + 4379 => 14379, + 4380 => 14380, + 4381 => 14381, + 4382 => 14382, + 4383 => 14383, + 4384 => 14384, + 4385 => 14385, + 4386 => 14386, + 4387 => 14387, + 4388 => 14388, + 4389 => 14389, + 4390 => 14390, + 4391 => 14391, + 4392 => 14392, + 4393 => 14393, + 4394 => 14394, + 4395 => 14395, + 4396 => 14396, + 4397 => 14397, + 4398 => 14398, + 4399 => 14399, + 4400 => 14400, + 4401 => 14401, + 4402 => 14402, + 4403 => 14403, + 4404 => 14404, + 4405 => 14405, + 4406 => 14406, + 4407 => 14407, + 4408 => 14408, + 4409 => 14409, + 4410 => 14410, + 4411 => 14411, + 4412 => 14412, + 4413 => 14413, + 4414 => 14414, + 4415 => 14415, + 4416 => 14416, + 4417 => 14417, + 4418 => 14418, + 4419 => 14419, + 4420 => 14420, + 4421 => 14421, + 4422 => 14422, + 4423 => 14423, + 4424 => 14424, + 4425 => 14425, + 4426 => 14426, + 4427 => 14427, + 4428 => 14428, + 4429 => 14429, + 4430 => 14430, + 4431 => 14431, + 4432 => 14432, + 4433 => 14433, + 4434 => 14434, + 4435 => 14435, + 4436 => 14436, + 4437 => 14437, + 4438 => 14438, + 4439 => 14439, + 4440 => 14440, + 4441 => 14441, + 4442 => 14442, + 4443 => 14443, + 4444 => 14444, + 4445 => 14445, + 4446 => 14446, + 4447 => 14447, + 4448 => 14448, + 4449 => 14449, + 4450 => 14450, + 4451 => 14451, + 4452 => 14452, + 4453 => 14453, + 4454 => 14454, + 4455 => 14455, + 4456 => 14456, + 4457 => 14457, + 4458 => 14458, + 4459 => 14459, + 4460 => 14460, + 4461 => 14461, + 4462 => 14462, + 4463 => 14463, + 4464 => 14464, + 4465 => 14465, + 4466 => 14466, + 4467 => 14467, + 4468 => 14468, + 4469 => 14469, + 4470 => 14470, + 4471 => 14471, + 4472 => 14472, + 4473 => 14473, + 4474 => 14474, + 4475 => 14475, + 4476 => 14476, + 4477 => 14477, + 4478 => 14478, + 4479 => 14479, + 4480 => 14480, + 4481 => 14481, + 4482 => 14482, + 4483 => 14483, + 4484 => 14484, + 4485 => 14485, + 4486 => 14486, + 4487 => 14487, + 4488 => 14488, + 4489 => 14489, + 4490 => 14490, + 4491 => 14491, + 4492 => 14492, + 4493 => 14493, + 4494 => 14494, + 4495 => 14495, + 4496 => 14496, + 4497 => 14497, + 4498 => 14498, + 4499 => 14499, + 4500 => 14500, + 4501 => 14501, + 4502 => 14502, + 4503 => 14503, + 4504 => 14504, + 4505 => 14505, + 4506 => 14506, + 4507 => 14507, + 4508 => 14508, + 4509 => 14509, + 4510 => 14510, + 4511 => 14511, + 4512 => 14512, + 4513 => 14513, + 4514 => 14514, + 4515 => 14515, + 4516 => 14516, + 4517 => 14517, + 4518 => 14518, + 4519 => 14519, + 4520 => 14520, + 4521 => 14521, + 4522 => 14522, + 4523 => 14523, + 4524 => 14524, + 4525 => 14525, + 4526 => 14526, + 4527 => 14527, + 4528 => 14528, + 4529 => 14529, + 4530 => 14530, + 4531 => 14531, + 4532 => 14532, + 4533 => 14533, + 4534 => 14534, + 4535 => 14535, + 4536 => 14536, + 4537 => 14537, + 4538 => 14538, + 4539 => 14539, + 4540 => 14540, + 4541 => 14541, + 4542 => 14542, + 4543 => 14543, + 4544 => 14544, + 4545 => 14545, + 4546 => 14546, + 4547 => 14547, + 4548 => 14548, + 4549 => 14549, + 4550 => 14550, + 4551 => 14551, + 4552 => 14552, + 4553 => 14553, + 4554 => 14554, + 4555 => 14555, + 4556 => 14556, + 4557 => 14557, + 4558 => 14558, + 4559 => 14559, + 4560 => 14560, + 4561 => 14561, + 4562 => 14562, + 4563 => 14563, + 4564 => 14564, + 4565 => 14565, + 4566 => 14566, + 4567 => 14567, + 4568 => 14568, + 4569 => 14569, + 4570 => 14570, + 4571 => 14571, + 4572 => 14572, + 4573 => 14573, + 4574 => 14574, + 4575 => 14575, + 4576 => 14576, + 4577 => 14577, + 4578 => 14578, + 4579 => 14579, + 4580 => 14580, + 4581 => 14581, + 4582 => 14582, + 4583 => 14583, + 4584 => 14584, + 4585 => 14585, + 4586 => 14586, + 4587 => 14587, + 4588 => 14588, + 4589 => 14589, + 4590 => 14590, + 4591 => 14591, + 4592 => 14592, + 4593 => 14593, + 4594 => 14594, + 4595 => 14595, + 4596 => 14596, + 4597 => 14597, + 4598 => 14598, + 4599 => 14599, + 4600 => 14600, + 4601 => 14601, + 4602 => 14602, + 4603 => 14603, + 4604 => 14604, + 4605 => 14605, + 4606 => 14606, + 4607 => 14607, + 4608 => 14608, + 4609 => 14609, + 4610 => 14610, + 4611 => 14611, + 4612 => 14612, + 4613 => 14613, + 4614 => 14614, + 4615 => 14615, + 4616 => 14616, + 4617 => 14617, + 4618 => 14618, + 4619 => 14619, + 4620 => 14620, + 4621 => 14621, + 4622 => 14622, + 4623 => 14623, + 4624 => 14624, + 4625 => 14625, + 4626 => 14626, + 4627 => 14627, + 4628 => 14628, + 4629 => 14629, + 4630 => 14630, + 4631 => 14631, + 4632 => 14632, + 4633 => 14633, + 4634 => 14634, + 4635 => 14635, + 4636 => 14636, + 4637 => 14637, + 4638 => 14638, + 4639 => 14639, + 4640 => 14640, + 4641 => 14641, + 4642 => 14642, + 4643 => 14643, + 4644 => 14644, + 4645 => 14645, + 4646 => 14646, + 4647 => 14647, + 4648 => 14648, + 4649 => 14649, + 4650 => 14650, + 4651 => 14651, + 4652 => 14652, + 4653 => 14653, + 4654 => 14654, + 4655 => 14655, + 4656 => 14656, + 4657 => 14657, + 4658 => 14658, + 4659 => 14659, + 4660 => 14660, + 4661 => 14661, + 4662 => 14662, + 4663 => 14663, + 4664 => 14664, + 4665 => 14665, + 4666 => 14666, + 4667 => 14667, + 4668 => 14668, + 4669 => 14669, + 4670 => 14670, + 4671 => 14671, + 4672 => 14672, + 4673 => 14673, + 4674 => 14674, + 4675 => 14675, + 4676 => 14676, + 4677 => 14677, + 4678 => 14678, + 4679 => 14679, + 4680 => 14680, + 4681 => 14681, + 4682 => 14682, + 4683 => 14683, + 4684 => 14684, + 4685 => 14685, + 4686 => 14686, + 4687 => 14687, + 4688 => 14688, + 4689 => 14689, + 4690 => 14690, + 4691 => 14691, + 4692 => 14692, + 4693 => 14693, + 4694 => 14694, + 4695 => 14695, + 4696 => 14696, + 4697 => 14697, + 4698 => 14698, + 4699 => 14699, + 4700 => 14700, + 4701 => 14701, + 4702 => 14702, + 4703 => 14703, + 4704 => 14704, + 4705 => 14705, + 4706 => 14706, + 4707 => 14707, + 4708 => 14708, + 4709 => 14709, + 4710 => 14710, + 4711 => 14711, + 4712 => 14712, + 4713 => 14713, + 4714 => 14714, + 4715 => 14715, + 4716 => 14716, + 4717 => 14717, + 4718 => 14718, + 4719 => 14719, + 4720 => 14720, + 4721 => 14721, + 4722 => 14722, + 4723 => 14723, + 4724 => 14724, + 4725 => 14725, + 4726 => 14726, + 4727 => 14727, + 4728 => 14728, + 4729 => 14729, + 4730 => 14730, + 4731 => 14731, + 4732 => 14732, + 4733 => 14733, + 4734 => 14734, + 4735 => 14735, + 4736 => 14736, + 4737 => 14737, + 4738 => 14738, + 4739 => 14739, + 4740 => 14740, + 4741 => 14741, + 4742 => 14742, + 4743 => 14743, + 4744 => 14744, + 4745 => 14745, + 4746 => 14746, + 4747 => 14747, + 4748 => 14748, + 4749 => 14749, + 4750 => 14750, + 4751 => 14751, + 4752 => 14752, + 4753 => 14753, + 4754 => 14754, + 4755 => 14755, + 4756 => 14756, + 4757 => 14757, + 4758 => 14758, + 4759 => 14759, + 4760 => 14760, + 4761 => 14761, + 4762 => 14762, + 4763 => 14763, + 4764 => 14764, + 4765 => 14765, + 4766 => 14766, + 4767 => 14767, + 4768 => 14768, + 4769 => 14769, + 4770 => 14770, + 4771 => 14771, + 4772 => 14772, + 4773 => 14773, + 4774 => 14774, + 4775 => 14775, + 4776 => 14776, + 4777 => 14777, + 4778 => 14778, + 4779 => 14779, + 4780 => 14780, + 4781 => 14781, + 4782 => 14782, + 4783 => 14783, + 4784 => 14784, + 4785 => 14785, + 4786 => 14786, + 4787 => 14787, + 4788 => 14788, + 4789 => 14789, + 4790 => 14790, + 4791 => 14791, + 4792 => 14792, + 4793 => 14793, + 4794 => 14794, + 4795 => 14795, + 4796 => 14796, + 4797 => 14797, + 4798 => 14798, + 4799 => 14799, + 4800 => 14800, + 4801 => 14801, + 4802 => 14802, + 4803 => 14803, + 4804 => 14804, + 4805 => 14805, + 4806 => 14806, + 4807 => 14807, + 4808 => 14808, + 4809 => 14809, + 4810 => 14810, + 4811 => 14811, + 4812 => 14812, + 4813 => 14813, + 4814 => 14814, + 4815 => 14815, + 4816 => 14816, + 4817 => 14817, + 4818 => 14818, + 4819 => 14819, + 4820 => 14820, + 4821 => 14821, + 4822 => 14822, + 4823 => 14823, + 4824 => 14824, + 4825 => 14825, + 4826 => 14826, + 4827 => 14827, + 4828 => 14828, + 4829 => 14829, + 4830 => 14830, + 4831 => 14831, + 4832 => 14832, + 4833 => 14833, + 4834 => 14834, + 4835 => 14835, + 4836 => 14836, + 4837 => 14837, + 4838 => 14838, + 4839 => 14839, + 4840 => 14840, + 4841 => 14841, + 4842 => 14842, + 4843 => 14843, + 4844 => 14844, + 4845 => 14845, + 4846 => 14846, + 4847 => 14847, + 4848 => 14848, + 4849 => 14849, + 4850 => 14850, + 4851 => 14851, + 4852 => 14852, + 4853 => 14853, + 4854 => 14854, + 4855 => 14855, + 4856 => 14856, + 4857 => 14857, + 4858 => 14858, + 4859 => 14859, + 4860 => 14860, + 4861 => 14861, + 4862 => 14862, + 4863 => 14863, + 4864 => 14864, + 4865 => 14865, + 4866 => 14866, + 4867 => 14867, + 4868 => 14868, + 4869 => 14869, + 4870 => 14870, + 4871 => 14871, + 4872 => 14872, + 4873 => 14873, + 4874 => 14874, + 4875 => 14875, + 4876 => 14876, + 4877 => 14877, + 4878 => 14878, + 4879 => 14879, + 4880 => 14880, + 4881 => 14881, + 4882 => 14882, + 4883 => 14883, + 4884 => 14884, + 4885 => 14885, + 4886 => 14886, + 4887 => 14887, + 4888 => 14888, + 4889 => 14889, + 4890 => 14890, + 4891 => 14891, + 4892 => 14892, + 4893 => 14893, + 4894 => 14894, + 4895 => 14895, + 4896 => 14896, + 4897 => 14897, + 4898 => 14898, + 4899 => 14899, + 4900 => 14900, + 4901 => 14901, + 4902 => 14902, + 4903 => 14903, + 4904 => 14904, + 4905 => 14905, + 4906 => 14906, + 4907 => 14907, + 4908 => 14908, + 4909 => 14909, + 4910 => 14910, + 4911 => 14911, + 4912 => 14912, + 4913 => 14913, + 4914 => 14914, + 4915 => 14915, + 4916 => 14916, + 4917 => 14917, + 4918 => 14918, + 4919 => 14919, + 4920 => 14920, + 4921 => 14921, + 4922 => 14922, + 4923 => 14923, + 4924 => 14924, + 4925 => 14925, + 4926 => 14926, + 4927 => 14927, + 4928 => 14928, + 4929 => 14929, + 4930 => 14930, + 4931 => 14931, + 4932 => 14932, + 4933 => 14933, + 4934 => 14934, + 4935 => 14935, + 4936 => 14936, + 4937 => 14937, + 4938 => 14938, + 4939 => 14939, + 4940 => 14940, + 4941 => 14941, + 4942 => 14942, + 4943 => 14943, + 4944 => 14944, + 4945 => 14945, + 4946 => 14946, + 4947 => 14947, + 4948 => 14948, + 4949 => 14949, + 4950 => 14950, + 4951 => 14951, + 4952 => 14952, + 4953 => 14953, + 4954 => 14954, + 4955 => 14955, + 4956 => 14956, + 4957 => 14957, + 4958 => 14958, + 4959 => 14959, + 4960 => 14960, + 4961 => 14961, + 4962 => 14962, + 4963 => 14963, + 4964 => 14964, + 4965 => 14965, + 4966 => 14966, + 4967 => 14967, + 4968 => 14968, + 4969 => 14969, + 4970 => 14970, + 4971 => 14971, + 4972 => 14972, + 4973 => 14973, + 4974 => 14974, + 4975 => 14975, + 4976 => 14976, + 4977 => 14977, + 4978 => 14978, + 4979 => 14979, + 4980 => 14980, + 4981 => 14981, + 4982 => 14982, + 4983 => 14983, + 4984 => 14984, + 4985 => 14985, + 4986 => 14986, + 4987 => 14987, + 4988 => 14988, + 4989 => 14989, + 4990 => 14990, + 4991 => 14991, + 4992 => 14992, + 4993 => 14993, + 4994 => 14994, + 4995 => 14995, + 4996 => 14996, + 4997 => 14997, + 4998 => 14998, + 4999 => 14999, + 5000 => 15000, + 5001 => 15001, + 5002 => 15002, + 5003 => 15003, + 5004 => 15004, + 5005 => 15005, + 5006 => 15006, + 5007 => 15007, + 5008 => 15008, + 5009 => 15009, + 5010 => 15010, + 5011 => 15011, + 5012 => 15012, + 5013 => 15013, + 5014 => 15014, + 5015 => 15015, + 5016 => 15016, + 5017 => 15017, + 5018 => 15018, + 5019 => 15019, + 5020 => 15020, + 5021 => 15021, + 5022 => 15022, + 5023 => 15023, + 5024 => 15024, + 5025 => 15025, + 5026 => 15026, + 5027 => 15027, + 5028 => 15028, + 5029 => 15029, + 5030 => 15030, + 5031 => 15031, + 5032 => 15032, + 5033 => 15033, + 5034 => 15034, + 5035 => 15035, + 5036 => 15036, + 5037 => 15037, + 5038 => 15038, + 5039 => 15039, + 5040 => 15040, + 5041 => 15041, + 5042 => 15042, + 5043 => 15043, + 5044 => 15044, + 5045 => 15045, + 5046 => 15046, + 5047 => 15047, + 5048 => 15048, + 5049 => 15049, + 5050 => 15050, + 5051 => 15051, + 5052 => 15052, + 5053 => 15053, + 5054 => 15054, + 5055 => 15055, + 5056 => 15056, + 5057 => 15057, + 5058 => 15058, + 5059 => 15059, + 5060 => 15060, + 5061 => 15061, + 5062 => 15062, + 5063 => 15063, + 5064 => 15064, + 5065 => 15065, + 5066 => 15066, + 5067 => 15067, + 5068 => 15068, + 5069 => 15069, + 5070 => 15070, + 5071 => 15071, + 5072 => 15072, + 5073 => 15073, + 5074 => 15074, + 5075 => 15075, + 5076 => 15076, + 5077 => 15077, + 5078 => 15078, + 5079 => 15079, + 5080 => 15080, + 5081 => 15081, + 5082 => 15082, + 5083 => 15083, + 5084 => 15084, + 5085 => 15085, + 5086 => 15086, + 5087 => 15087, + 5088 => 15088, + 5089 => 15089, + 5090 => 15090, + 5091 => 15091, + 5092 => 15092, + 5093 => 15093, + 5094 => 15094, + 5095 => 15095, + 5096 => 15096, + 5097 => 15097, + 5098 => 15098, + 5099 => 15099, + 5100 => 15100, + 5101 => 15101, + 5102 => 15102, + 5103 => 15103, + 5104 => 15104, + 5105 => 15105, + 5106 => 15106, + 5107 => 15107, + 5108 => 15108, + 5109 => 15109, + 5110 => 15110, + 5111 => 15111, + 5112 => 15112, + 5113 => 15113, + 5114 => 15114, + 5115 => 15115, + 5116 => 15116, + 5117 => 15117, + 5118 => 15118, + 5119 => 15119, + 5120 => 15120, + 5121 => 15121, + 5122 => 15122, + 5123 => 15123, + 5124 => 15124, + 5125 => 15125, + 5126 => 15126, + 5127 => 15127, + 5128 => 15128, + 5129 => 15129, + 5130 => 15130, + 5131 => 15131, + 5132 => 15132, + 5133 => 15133, + 5134 => 15134, + 5135 => 15135, + 5136 => 15136, + 5137 => 15137, + 5138 => 15138, + 5139 => 15139, + 5140 => 15140, + 5141 => 15141, + 5142 => 15142, + 5143 => 15143, + 5144 => 15144, + 5145 => 15145, + 5146 => 15146, + 5147 => 15147, + 5148 => 15148, + 5149 => 15149, + 5150 => 15150, + 5151 => 15151, + 5152 => 15152, + 5153 => 15153, + 5154 => 15154, + 5155 => 15155, + 5156 => 15156, + 5157 => 15157, + 5158 => 15158, + 5159 => 15159, + 5160 => 15160, + 5161 => 15161, + 5162 => 15162, + 5163 => 15163, + 5164 => 15164, + 5165 => 15165, + 5166 => 15166, + 5167 => 15167, + 5168 => 15168, + 5169 => 15169, + 5170 => 15170, + 5171 => 15171, + 5172 => 15172, + 5173 => 15173, + 5174 => 15174, + 5175 => 15175, + 5176 => 15176, + 5177 => 15177, + 5178 => 15178, + 5179 => 15179, + 5180 => 15180, + 5181 => 15181, + 5182 => 15182, + 5183 => 15183, + 5184 => 15184, + 5185 => 15185, + 5186 => 15186, + 5187 => 15187, + 5188 => 15188, + 5189 => 15189, + 5190 => 15190, + 5191 => 15191, + 5192 => 15192, + 5193 => 15193, + 5194 => 15194, + 5195 => 15195, + 5196 => 15196, + 5197 => 15197, + 5198 => 15198, + 5199 => 15199, + 5200 => 15200, + 5201 => 15201, + 5202 => 15202, + 5203 => 15203, + 5204 => 15204, + 5205 => 15205, + 5206 => 15206, + 5207 => 15207, + 5208 => 15208, + 5209 => 15209, + 5210 => 15210, + 5211 => 15211, + 5212 => 15212, + 5213 => 15213, + 5214 => 15214, + 5215 => 15215, + 5216 => 15216, + 5217 => 15217, + 5218 => 15218, + 5219 => 15219, + 5220 => 15220, + 5221 => 15221, + 5222 => 15222, + 5223 => 15223, + 5224 => 15224, + 5225 => 15225, + 5226 => 15226, + 5227 => 15227, + 5228 => 15228, + 5229 => 15229, + 5230 => 15230, + 5231 => 15231, + 5232 => 15232, + 5233 => 15233, + 5234 => 15234, + 5235 => 15235, + 5236 => 15236, + 5237 => 15237, + 5238 => 15238, + 5239 => 15239, + 5240 => 15240, + 5241 => 15241, + 5242 => 15242, + 5243 => 15243, + 5244 => 15244, + 5245 => 15245, + 5246 => 15246, + 5247 => 15247, + 5248 => 15248, + 5249 => 15249, + 5250 => 15250, + 5251 => 15251, + 5252 => 15252, + 5253 => 15253, + 5254 => 15254, + 5255 => 15255, + 5256 => 15256, + 5257 => 15257, + 5258 => 15258, + 5259 => 15259, + 5260 => 15260, + 5261 => 15261, + 5262 => 15262, + 5263 => 15263, + 5264 => 15264, + 5265 => 15265, + 5266 => 15266, + 5267 => 15267, + 5268 => 15268, + 5269 => 15269, + 5270 => 15270, + 5271 => 15271, + 5272 => 15272, + 5273 => 15273, + 5274 => 15274, + 5275 => 15275, + 5276 => 15276, + 5277 => 15277, + 5278 => 15278, + 5279 => 15279, + 5280 => 15280, + 5281 => 15281, + 5282 => 15282, + 5283 => 15283, + 5284 => 15284, + 5285 => 15285, + 5286 => 15286, + 5287 => 15287, + 5288 => 15288, + 5289 => 15289, + 5290 => 15290, + 5291 => 15291, + 5292 => 15292, + 5293 => 15293, + 5294 => 15294, + 5295 => 15295, + 5296 => 15296, + 5297 => 15297, + 5298 => 15298, + 5299 => 15299, + 5300 => 15300, + 5301 => 15301, + 5302 => 15302, + 5303 => 15303, + 5304 => 15304, + 5305 => 15305, + 5306 => 15306, + 5307 => 15307, + 5308 => 15308, + 5309 => 15309, + 5310 => 15310, + 5311 => 15311, + 5312 => 15312, + 5313 => 15313, + 5314 => 15314, + 5315 => 15315, + 5316 => 15316, + 5317 => 15317, + 5318 => 15318, + 5319 => 15319, + 5320 => 15320, + 5321 => 15321, + 5322 => 15322, + 5323 => 15323, + 5324 => 15324, + 5325 => 15325, + 5326 => 15326, + 5327 => 15327, + 5328 => 15328, + 5329 => 15329, + 5330 => 15330, + 5331 => 15331, + 5332 => 15332, + 5333 => 15333, + 5334 => 15334, + 5335 => 15335, + 5336 => 15336, + 5337 => 15337, + 5338 => 15338, + 5339 => 15339, + 5340 => 15340, + 5341 => 15341, + 5342 => 15342, + 5343 => 15343, + 5344 => 15344, + 5345 => 15345, + 5346 => 15346, + 5347 => 15347, + 5348 => 15348, + 5349 => 15349, + 5350 => 15350, + 5351 => 15351, + 5352 => 15352, + 5353 => 15353, + 5354 => 15354, + 5355 => 15355, + 5356 => 15356, + 5357 => 15357, + 5358 => 15358, + 5359 => 15359, + 5360 => 15360, + 5361 => 15361, + 5362 => 15362, + 5363 => 15363, + 5364 => 15364, + 5365 => 15365, + 5366 => 15366, + 5367 => 15367, + 5368 => 15368, + 5369 => 15369, + 5370 => 15370, + 5371 => 15371, + 5372 => 15372, + 5373 => 15373, + 5374 => 15374, + 5375 => 15375, + 5376 => 15376, + 5377 => 15377, + 5378 => 15378, + 5379 => 15379, + 5380 => 15380, + 5381 => 15381, + 5382 => 15382, + 5383 => 15383, + 5384 => 15384, + 5385 => 15385, + 5386 => 15386, + 5387 => 15387, + 5388 => 15388, + 5389 => 15389, + 5390 => 15390, + 5391 => 15391, + 5392 => 15392, + 5393 => 15393, + 5394 => 15394, + 5395 => 15395, + 5396 => 15396, + 5397 => 15397, + 5398 => 15398, + 5399 => 15399, + 5400 => 15400, + 5401 => 15401, + 5402 => 15402, + 5403 => 15403, + 5404 => 15404, + 5405 => 15405, + 5406 => 15406, + 5407 => 15407, + 5408 => 15408, + 5409 => 15409, + 5410 => 15410, + 5411 => 15411, + 5412 => 15412, + 5413 => 15413, + 5414 => 15414, + 5415 => 15415, + 5416 => 15416, + 5417 => 15417, + 5418 => 15418, + 5419 => 15419, + 5420 => 15420, + 5421 => 15421, + 5422 => 15422, + 5423 => 15423, + 5424 => 15424, + 5425 => 15425, + 5426 => 15426, + 5427 => 15427, + 5428 => 15428, + 5429 => 15429, + 5430 => 15430, + 5431 => 15431, + 5432 => 15432, + 5433 => 15433, + 5434 => 15434, + 5435 => 15435, + 5436 => 15436, + 5437 => 15437, + 5438 => 15438, + 5439 => 15439, + 5440 => 15440, + 5441 => 15441, + 5442 => 15442, + 5443 => 15443, + 5444 => 15444, + 5445 => 15445, + 5446 => 15446, + 5447 => 15447, + 5448 => 15448, + 5449 => 15449, + 5450 => 15450, + 5451 => 15451, + 5452 => 15452, + 5453 => 15453, + 5454 => 15454, + 5455 => 15455, + 5456 => 15456, + 5457 => 15457, + 5458 => 15458, + 5459 => 15459, + 5460 => 15460, + 5461 => 15461, + 5462 => 15462, + 5463 => 15463, + 5464 => 15464, + 5465 => 15465, + 5466 => 15466, + 5467 => 15467, + 5468 => 15468, + 5469 => 15469, + 5470 => 15470, + 5471 => 15471, + 5472 => 15472, + 5473 => 15473, + 5474 => 15474, + 5475 => 15475, + 5476 => 15476, + 5477 => 15477, + 5478 => 15478, + 5479 => 15479, + 5480 => 15480, + 5481 => 15481, + 5482 => 15482, + 5483 => 15483, + 5484 => 15484, + 5485 => 15485, + 5486 => 15486, + 5487 => 15487, + 5488 => 15488, + 5489 => 15489, + 5490 => 15490, + 5491 => 15491, + 5492 => 15492, + 5493 => 15493, + 5494 => 15494, + 5495 => 15495, + 5496 => 15496, + 5497 => 15497, + 5498 => 15498, + 5499 => 15499, + 5500 => 15500, + 5501 => 15501, + 5502 => 15502, + 5503 => 15503, + 5504 => 15504, + 5505 => 15505, + 5506 => 15506, + 5507 => 15507, + 5508 => 15508, + 5509 => 15509, + 5510 => 15510, + 5511 => 15511, + 5512 => 15512, + 5513 => 15513, + 5514 => 15514, + 5515 => 15515, + 5516 => 15516, + 5517 => 15517, + 5518 => 15518, + 5519 => 15519, + 5520 => 15520, + 5521 => 15521, + 5522 => 15522, + 5523 => 15523, + 5524 => 15524, + 5525 => 15525, + 5526 => 15526, + 5527 => 15527, + 5528 => 15528, + 5529 => 15529, + 5530 => 15530, + 5531 => 15531, + 5532 => 15532, + 5533 => 15533, + 5534 => 15534, + 5535 => 15535, + 5536 => 15536, + 5537 => 15537, + 5538 => 15538, + 5539 => 15539, + 5540 => 15540, + 5541 => 15541, + 5542 => 15542, + 5543 => 15543, + 5544 => 15544, + 5545 => 15545, + 5546 => 15546, + 5547 => 15547, + 5548 => 15548, + 5549 => 15549, + 5550 => 15550, + 5551 => 15551, + 5552 => 15552, + 5553 => 15553, + 5554 => 15554, + 5555 => 15555, + 5556 => 15556, + 5557 => 15557, + 5558 => 15558, + 5559 => 15559, + 5560 => 15560, + 5561 => 15561, + 5562 => 15562, + 5563 => 15563, + 5564 => 15564, + 5565 => 15565, + 5566 => 15566, + 5567 => 15567, + 5568 => 15568, + 5569 => 15569, + 5570 => 15570, + 5571 => 15571, + 5572 => 15572, + 5573 => 15573, + 5574 => 15574, + 5575 => 15575, + 5576 => 15576, + 5577 => 15577, + 5578 => 15578, + 5579 => 15579, + 5580 => 15580, + 5581 => 15581, + 5582 => 15582, + 5583 => 15583, + 5584 => 15584, + 5585 => 15585, + 5586 => 15586, + 5587 => 15587, + 5588 => 15588, + 5589 => 15589, + 5590 => 15590, + 5591 => 15591, + 5592 => 15592, + 5593 => 15593, + 5594 => 15594, + 5595 => 15595, + 5596 => 15596, + 5597 => 15597, + 5598 => 15598, + 5599 => 15599, + 5600 => 15600, + 5601 => 15601, + 5602 => 15602, + 5603 => 15603, + 5604 => 15604, + 5605 => 15605, + 5606 => 15606, + 5607 => 15607, + 5608 => 15608, + 5609 => 15609, + 5610 => 15610, + 5611 => 15611, + 5612 => 15612, + 5613 => 15613, + 5614 => 15614, + 5615 => 15615, + 5616 => 15616, + 5617 => 15617, + 5618 => 15618, + 5619 => 15619, + 5620 => 15620, + 5621 => 15621, + 5622 => 15622, + 5623 => 15623, + 5624 => 15624, + 5625 => 15625, + 5626 => 15626, + 5627 => 15627, + 5628 => 15628, + 5629 => 15629, + 5630 => 15630, + 5631 => 15631, + 5632 => 15632, + 5633 => 15633, + 5634 => 15634, + 5635 => 15635, + 5636 => 15636, + 5637 => 15637, + 5638 => 15638, + 5639 => 15639, + 5640 => 15640, + 5641 => 15641, + 5642 => 15642, + 5643 => 15643, + 5644 => 15644, + 5645 => 15645, + 5646 => 15646, + 5647 => 15647, + 5648 => 15648, + 5649 => 15649, + 5650 => 15650, + 5651 => 15651, + 5652 => 15652, + 5653 => 15653, + 5654 => 15654, + 5655 => 15655, + 5656 => 15656, + 5657 => 15657, + 5658 => 15658, + 5659 => 15659, + 5660 => 15660, + 5661 => 15661, + 5662 => 15662, + 5663 => 15663, + 5664 => 15664, + 5665 => 15665, + 5666 => 15666, + 5667 => 15667, + 5668 => 15668, + 5669 => 15669, + 5670 => 15670, + 5671 => 15671, + 5672 => 15672, + 5673 => 15673, + 5674 => 15674, + 5675 => 15675, + 5676 => 15676, + 5677 => 15677, + 5678 => 15678, + 5679 => 15679, + 5680 => 15680, + 5681 => 15681, + 5682 => 15682, + 5683 => 15683, + 5684 => 15684, + 5685 => 15685, + 5686 => 15686, + 5687 => 15687, + 5688 => 15688, + 5689 => 15689, + 5690 => 15690, + 5691 => 15691, + 5692 => 15692, + 5693 => 15693, + 5694 => 15694, + 5695 => 15695, + 5696 => 15696, + 5697 => 15697, + 5698 => 15698, + 5699 => 15699, + 5700 => 15700, + 5701 => 15701, + 5702 => 15702, + 5703 => 15703, + 5704 => 15704, + 5705 => 15705, + 5706 => 15706, + 5707 => 15707, + 5708 => 15708, + 5709 => 15709, + 5710 => 15710, + 5711 => 15711, + 5712 => 15712, + 5713 => 15713, + 5714 => 15714, + 5715 => 15715, + 5716 => 15716, + 5717 => 15717, + 5718 => 15718, + 5719 => 15719, + 5720 => 15720, + 5721 => 15721, + 5722 => 15722, + 5723 => 15723, + 5724 => 15724, + 5725 => 15725, + 5726 => 15726, + 5727 => 15727, + 5728 => 15728, + 5729 => 15729, + 5730 => 15730, + 5731 => 15731, + 5732 => 15732, + 5733 => 15733, + 5734 => 15734, + 5735 => 15735, + 5736 => 15736, + 5737 => 15737, + 5738 => 15738, + 5739 => 15739, + 5740 => 15740, + 5741 => 15741, + 5742 => 15742, + 5743 => 15743, + 5744 => 15744, + 5745 => 15745, + 5746 => 15746, + 5747 => 15747, + 5748 => 15748, + 5749 => 15749, + 5750 => 15750, + 5751 => 15751, + 5752 => 15752, + 5753 => 15753, + 5754 => 15754, + 5755 => 15755, + 5756 => 15756, + 5757 => 15757, + 5758 => 15758, + 5759 => 15759, + 5760 => 15760, + 5761 => 15761, + 5762 => 15762, + 5763 => 15763, + 5764 => 15764, + 5765 => 15765, + 5766 => 15766, + 5767 => 15767, + 5768 => 15768, + 5769 => 15769, + 5770 => 15770, + 5771 => 15771, + 5772 => 15772, + 5773 => 15773, + 5774 => 15774, + 5775 => 15775, + 5776 => 15776, + 5777 => 15777, + 5778 => 15778, + 5779 => 15779, + 5780 => 15780, + 5781 => 15781, + 5782 => 15782, + 5783 => 15783, + 5784 => 15784, + 5785 => 15785, + 5786 => 15786, + 5787 => 15787, + 5788 => 15788, + 5789 => 15789, + 5790 => 15790, + 5791 => 15791, + 5792 => 15792, + 5793 => 15793, + 5794 => 15794, + 5795 => 15795, + 5796 => 15796, + 5797 => 15797, + 5798 => 15798, + 5799 => 15799, + 5800 => 15800, + 5801 => 15801, + 5802 => 15802, + 5803 => 15803, + 5804 => 15804, + 5805 => 15805, + 5806 => 15806, + 5807 => 15807, + 5808 => 15808, + 5809 => 15809, + 5810 => 15810, + 5811 => 15811, + 5812 => 15812, + 5813 => 15813, + 5814 => 15814, + 5815 => 15815, + 5816 => 15816, + 5817 => 15817, + 5818 => 15818, + 5819 => 15819, + 5820 => 15820, + 5821 => 15821, + 5822 => 15822, + 5823 => 15823, + 5824 => 15824, + 5825 => 15825, + 5826 => 15826, + 5827 => 15827, + 5828 => 15828, + 5829 => 15829, + 5830 => 15830, + 5831 => 15831, + 5832 => 15832, + 5833 => 15833, + 5834 => 15834, + 5835 => 15835, + 5836 => 15836, + 5837 => 15837, + 5838 => 15838, + 5839 => 15839, + 5840 => 15840, + 5841 => 15841, + 5842 => 15842, + 5843 => 15843, + 5844 => 15844, + 5845 => 15845, + 5846 => 15846, + 5847 => 15847, + 5848 => 15848, + 5849 => 15849, + 5850 => 15850, + 5851 => 15851, + 5852 => 15852, + 5853 => 15853, + 5854 => 15854, + 5855 => 15855, + 5856 => 15856, + 5857 => 15857, + 5858 => 15858, + 5859 => 15859, + 5860 => 15860, + 5861 => 15861, + 5862 => 15862, + 5863 => 15863, + 5864 => 15864, + 5865 => 15865, + 5866 => 15866, + 5867 => 15867, + 5868 => 15868, + 5869 => 15869, + 5870 => 15870, + 5871 => 15871, + 5872 => 15872, + 5873 => 15873, + 5874 => 15874, + 5875 => 15875, + 5876 => 15876, + 5877 => 15877, + 5878 => 15878, + 5879 => 15879, + 5880 => 15880, + 5881 => 15881, + 5882 => 15882, + 5883 => 15883, + 5884 => 15884, + 5885 => 15885, + 5886 => 15886, + 5887 => 15887, + 5888 => 15888, + 5889 => 15889, + 5890 => 15890, + 5891 => 15891, + 5892 => 15892, + 5893 => 15893, + 5894 => 15894, + 5895 => 15895, + 5896 => 15896, + 5897 => 15897, + 5898 => 15898, + 5899 => 15899, + 5900 => 15900, + 5901 => 15901, + 5902 => 15902, + 5903 => 15903, + 5904 => 15904, + 5905 => 15905, + 5906 => 15906, + 5907 => 15907, + 5908 => 15908, + 5909 => 15909, + 5910 => 15910, + 5911 => 15911, + 5912 => 15912, + 5913 => 15913, + 5914 => 15914, + 5915 => 15915, + 5916 => 15916, + 5917 => 15917, + 5918 => 15918, + 5919 => 15919, + 5920 => 15920, + 5921 => 15921, + 5922 => 15922, + 5923 => 15923, + 5924 => 15924, + 5925 => 15925, + 5926 => 15926, + 5927 => 15927, + 5928 => 15928, + 5929 => 15929, + 5930 => 15930, + 5931 => 15931, + 5932 => 15932, + 5933 => 15933, + 5934 => 15934, + 5935 => 15935, + 5936 => 15936, + 5937 => 15937, + 5938 => 15938, + 5939 => 15939, + 5940 => 15940, + 5941 => 15941, + 5942 => 15942, + 5943 => 15943, + 5944 => 15944, + 5945 => 15945, + 5946 => 15946, + 5947 => 15947, + 5948 => 15948, + 5949 => 15949, + 5950 => 15950, + 5951 => 15951, + 5952 => 15952, + 5953 => 15953, + 5954 => 15954, + 5955 => 15955, + 5956 => 15956, + 5957 => 15957, + 5958 => 15958, + 5959 => 15959, + 5960 => 15960, + 5961 => 15961, + 5962 => 15962, + 5963 => 15963, + 5964 => 15964, + 5965 => 15965, + 5966 => 15966, + 5967 => 15967, + 5968 => 15968, + 5969 => 15969, + 5970 => 15970, + 5971 => 15971, + 5972 => 15972, + 5973 => 15973, + 5974 => 15974, + 5975 => 15975, + 5976 => 15976, + 5977 => 15977, + 5978 => 15978, + 5979 => 15979, + 5980 => 15980, + 5981 => 15981, + 5982 => 15982, + 5983 => 15983, + 5984 => 15984, + 5985 => 15985, + 5986 => 15986, + 5987 => 15987, + 5988 => 15988, + 5989 => 15989, + 5990 => 15990, + 5991 => 15991, + 5992 => 15992, + 5993 => 15993, + 5994 => 15994, + 5995 => 15995, + 5996 => 15996, + 5997 => 15997, + 5998 => 15998, + 5999 => 15999, + 6000 => 16000, + 6001 => 16001, + 6002 => 16002, + 6003 => 16003, + 6004 => 16004, + 6005 => 16005, + 6006 => 16006, + 6007 => 16007, + 6008 => 16008, + 6009 => 16009, + 6010 => 16010, + 6011 => 16011, + 6012 => 16012, + 6013 => 16013, + 6014 => 16014, + 6015 => 16015, + 6016 => 16016, + 6017 => 16017, + 6018 => 16018, + 6019 => 16019, + 6020 => 16020, + 6021 => 16021, + 6022 => 16022, + 6023 => 16023, + 6024 => 16024, + 6025 => 16025, + 6026 => 16026, + 6027 => 16027, + 6028 => 16028, + 6029 => 16029, + 6030 => 16030, + 6031 => 16031, + 6032 => 16032, + 6033 => 16033, + 6034 => 16034, + 6035 => 16035, + 6036 => 16036, + 6037 => 16037, + 6038 => 16038, + 6039 => 16039, + 6040 => 16040, + 6041 => 16041, + 6042 => 16042, + 6043 => 16043, + 6044 => 16044, + 6045 => 16045, + 6046 => 16046, + 6047 => 16047, + 6048 => 16048, + 6049 => 16049, + 6050 => 16050, + 6051 => 16051, + 6052 => 16052, + 6053 => 16053, + 6054 => 16054, + 6055 => 16055, + 6056 => 16056, + 6057 => 16057, + 6058 => 16058, + 6059 => 16059, + 6060 => 16060, + 6061 => 16061, + 6062 => 16062, + 6063 => 16063, + 6064 => 16064, + 6065 => 16065, + 6066 => 16066, + 6067 => 16067, + 6068 => 16068, + 6069 => 16069, + 6070 => 16070, + 6071 => 16071, + 6072 => 16072, + 6073 => 16073, + 6074 => 16074, + 6075 => 16075, + 6076 => 16076, + 6077 => 16077, + 6078 => 16078, + 6079 => 16079, + 6080 => 16080, + 6081 => 16081, + 6082 => 16082, + 6083 => 16083, + 6084 => 16084, + 6085 => 16085, + 6086 => 16086, + 6087 => 16087, + 6088 => 16088, + 6089 => 16089, + 6090 => 16090, + 6091 => 16091, + 6092 => 16092, + 6093 => 16093, + 6094 => 16094, + 6095 => 16095, + 6096 => 16096, + 6097 => 16097, + 6098 => 16098, + 6099 => 16099, + 6100 => 16100, + 6101 => 16101, + 6102 => 16102, + 6103 => 16103, + 6104 => 16104, + 6105 => 16105, + 6106 => 16106, + 6107 => 16107, + 6108 => 16108, + 6109 => 16109, + 6110 => 16110, + 6111 => 16111, + 6112 => 16112, + 6113 => 16113, + 6114 => 16114, + 6115 => 16115, + 6116 => 16116, + 6117 => 16117, + 6118 => 16118, + 6119 => 16119, + 6120 => 16120, + 6121 => 16121, + 6122 => 16122, + 6123 => 16123, + 6124 => 16124, + 6125 => 16125, + 6126 => 16126, + 6127 => 16127, + 6128 => 16128, + 6129 => 16129, + 6130 => 16130, + 6131 => 16131, + 6132 => 16132, + 6133 => 16133, + 6134 => 16134, + 6135 => 16135, + 6136 => 16136, + 6137 => 16137, + 6138 => 16138, + 6139 => 16139, + 6140 => 16140, + 6141 => 16141, + 6142 => 16142, + 6143 => 16143, + 6144 => 16144, + 6145 => 16145, + 6146 => 16146, + 6147 => 16147, + 6148 => 16148, + 6149 => 16149, + 6150 => 16150, + 6151 => 16151, + 6152 => 16152, + 6153 => 16153, + 6154 => 16154, + 6155 => 16155, + 6156 => 16156, + 6157 => 16157, + 6158 => 16158, + 6159 => 16159, + 6160 => 16160, + 6161 => 16161, + 6162 => 16162, + 6163 => 16163, + 6164 => 16164, + 6165 => 16165, + 6166 => 16166, + 6167 => 16167, + 6168 => 16168, + 6169 => 16169, + 6170 => 16170, + 6171 => 16171, + 6172 => 16172, + 6173 => 16173, + 6174 => 16174, + 6175 => 16175, + 6176 => 16176, + 6177 => 16177, + 6178 => 16178, + 6179 => 16179, + 6180 => 16180, + 6181 => 16181, + 6182 => 16182, + 6183 => 16183, + 6184 => 16184, + 6185 => 16185, + 6186 => 16186, + 6187 => 16187, + 6188 => 16188, + 6189 => 16189, + 6190 => 16190, + 6191 => 16191, + 6192 => 16192, + 6193 => 16193, + 6194 => 16194, + 6195 => 16195, + 6196 => 16196, + 6197 => 16197, + 6198 => 16198, + 6199 => 16199, + 6200 => 16200, + 6201 => 16201, + 6202 => 16202, + 6203 => 16203, + 6204 => 16204, + 6205 => 16205, + 6206 => 16206, + 6207 => 16207, + 6208 => 16208, + 6209 => 16209, + 6210 => 16210, + 6211 => 16211, + 6212 => 16212, + 6213 => 16213, + 6214 => 16214, + 6215 => 16215, + 6216 => 16216, + 6217 => 16217, + 6218 => 16218, + 6219 => 16219, + 6220 => 16220, + 6221 => 16221, + 6222 => 16222, + 6223 => 16223, + 6224 => 16224, + 6225 => 16225, + 6226 => 16226, + 6227 => 16227, + 6228 => 16228, + 6229 => 16229, + 6230 => 16230, + 6231 => 16231, + 6232 => 16232, + 6233 => 16233, + 6234 => 16234, + 6235 => 16235, + 6236 => 16236, + 6237 => 16237, + 6238 => 16238, + 6239 => 16239, + 6240 => 16240, + 6241 => 16241, + 6242 => 16242, + 6243 => 16243, + 6244 => 16244, + 6245 => 16245, + 6246 => 16246, + 6247 => 16247, + 6248 => 16248, + 6249 => 16249, + 6250 => 16250, + 6251 => 16251, + 6252 => 16252, + 6253 => 16253, + 6254 => 16254, + 6255 => 16255, + 6256 => 16256, + 6257 => 16257, + 6258 => 16258, + 6259 => 16259, + 6260 => 16260, + 6261 => 16261, + 6262 => 16262, + 6263 => 16263, + 6264 => 16264, + 6265 => 16265, + 6266 => 16266, + 6267 => 16267, + 6268 => 16268, + 6269 => 16269, + 6270 => 16270, + 6271 => 16271, + 6272 => 16272, + 6273 => 16273, + 6274 => 16274, + 6275 => 16275, + 6276 => 16276, + 6277 => 16277, + 6278 => 16278, + 6279 => 16279, + 6280 => 16280, + 6281 => 16281, + 6282 => 16282, + 6283 => 16283, + 6284 => 16284, + 6285 => 16285, + 6286 => 16286, + 6287 => 16287, + 6288 => 16288, + 6289 => 16289, + 6290 => 16290, + 6291 => 16291, + 6292 => 16292, + 6293 => 16293, + 6294 => 16294, + 6295 => 16295, + 6296 => 16296, + 6297 => 16297, + 6298 => 16298, + 6299 => 16299, + 6300 => 16300, + 6301 => 16301, + 6302 => 16302, + 6303 => 16303, + 6304 => 16304, + 6305 => 16305, + 6306 => 16306, + 6307 => 16307, + 6308 => 16308, + 6309 => 16309, + 6310 => 16310, + 6311 => 16311, + 6312 => 16312, + 6313 => 16313, + 6314 => 16314, + 6315 => 16315, + 6316 => 16316, + 6317 => 16317, + 6318 => 16318, + 6319 => 16319, + 6320 => 16320, + 6321 => 16321, + 6322 => 16322, + 6323 => 16323, + 6324 => 16324, + 6325 => 16325, + 6326 => 16326, + 6327 => 16327, + 6328 => 16328, + 6329 => 16329, + 6330 => 16330, + 6331 => 16331, + 6332 => 16332, + 6333 => 16333, + 6334 => 16334, + 6335 => 16335, + 6336 => 16336, + 6337 => 16337, + 6338 => 16338, + 6339 => 16339, + 6340 => 16340, + 6341 => 16341, + 6342 => 16342, + 6343 => 16343, + 6344 => 16344, + 6345 => 16345, + 6346 => 16346, + 6347 => 16347, + 6348 => 16348, + 6349 => 16349, + 6350 => 16350, + 6351 => 16351, + 6352 => 16352, + 6353 => 16353, + 6354 => 16354, + 6355 => 16355, + 6356 => 16356, + 6357 => 16357, + 6358 => 16358, + 6359 => 16359, + 6360 => 16360, + 6361 => 16361, + 6362 => 16362, + 6363 => 16363, + 6364 => 16364, + 6365 => 16365, + 6366 => 16366, + 6367 => 16367, + 6368 => 16368, + 6369 => 16369, + 6370 => 16370, + 6371 => 16371, + 6372 => 16372, + 6373 => 16373, + 6374 => 16374, + 6375 => 16375, + 6376 => 16376, + 6377 => 16377, + 6378 => 16378, + 6379 => 16379, + 6380 => 16380, + 6381 => 16381, + 6382 => 16382, + 6383 => 16383, + 6384 => 16384, + 6385 => 16385, + 6386 => 16386, + 6387 => 16387, + 6388 => 16388, + 6389 => 16389, + 6390 => 16390, + 6391 => 16391, + 6392 => 16392, + 6393 => 16393, + 6394 => 16394, + 6395 => 16395, + 6396 => 16396, + 6397 => 16397, + 6398 => 16398, + 6399 => 16399, + 6400 => 16400, + 6401 => 16401, + 6402 => 16402, + 6403 => 16403, + 6404 => 16404, + 6405 => 16405, + 6406 => 16406, + 6407 => 16407, + 6408 => 16408, + 6409 => 16409, + 6410 => 16410, + 6411 => 16411, + 6412 => 16412, + 6413 => 16413, + 6414 => 16414, + 6415 => 16415, + 6416 => 16416, + 6417 => 16417, + 6418 => 16418, + 6419 => 16419, + 6420 => 16420, + 6421 => 16421, + 6422 => 16422, + 6423 => 16423, + 6424 => 16424, + 6425 => 16425, + 6426 => 16426, + 6427 => 16427, + 6428 => 16428, + 6429 => 16429, + 6430 => 16430, + 6431 => 16431, + 6432 => 16432, + 6433 => 16433, + 6434 => 16434, + 6435 => 16435, + 6436 => 16436, + 6437 => 16437, + 6438 => 16438, + 6439 => 16439, + 6440 => 16440, + 6441 => 16441, + 6442 => 16442, + 6443 => 16443, + 6444 => 16444, + 6445 => 16445, + 6446 => 16446, + 6447 => 16447, + 6448 => 16448, + 6449 => 16449, + 6450 => 16450, + 6451 => 16451, + 6452 => 16452, + 6453 => 16453, + 6454 => 16454, + 6455 => 16455, + 6456 => 16456, + 6457 => 16457, + 6458 => 16458, + 6459 => 16459, + 6460 => 16460, + 6461 => 16461, + 6462 => 16462, + 6463 => 16463, + 6464 => 16464, + 6465 => 16465, + 6466 => 16466, + 6467 => 16467, + 6468 => 16468, + 6469 => 16469, + 6470 => 16470, + 6471 => 16471, + 6472 => 16472, + 6473 => 16473, + 6474 => 16474, + 6475 => 16475, + 6476 => 16476, + 6477 => 16477, + 6478 => 16478, + 6479 => 16479, + 6480 => 16480, + 6481 => 16481, + 6482 => 16482, + 6483 => 16483, + 6484 => 16484, + 6485 => 16485, + 6486 => 16486, + 6487 => 16487, + 6488 => 16488, + 6489 => 16489, + 6490 => 16490, + 6491 => 16491, + 6492 => 16492, + 6493 => 16493, + 6494 => 16494, + 6495 => 16495, + 6496 => 16496, + 6497 => 16497, + 6498 => 16498, + 6499 => 16499, + 6500 => 16500, +]; + +const TEST_ARRAY_2 = [ + 10001 => 20001, + 10002 => 20002, + 10003 => 20003, + 10004 => 20004, + 10005 => 20005, + 10006 => 20006, + 10007 => 20007, + 10008 => 20008, + 10009 => 20009, + 10010 => 20010, + 10011 => 20011, + 10012 => 20012, + 10013 => 20013, + 10014 => 20014, + 10015 => 20015, + 10016 => 20016, + 10017 => 20017, + 10018 => 20018, + 10019 => 20019, + 10020 => 20020, + 10021 => 20021, + 10022 => 20022, + 10023 => 20023, + 10024 => 20024, + 10025 => 20025, + 10026 => 20026, + 10027 => 20027, + 10028 => 20028, + 10029 => 20029, + 10030 => 20030, + 10031 => 20031, + 10032 => 20032, + 10033 => 20033, + 10034 => 20034, + 10035 => 20035, + 10036 => 20036, + 10037 => 20037, + 10038 => 20038, + 10039 => 20039, + 10040 => 20040, + 10041 => 20041, + 10042 => 20042, + 10043 => 20043, + 10044 => 20044, + 10045 => 20045, + 10046 => 20046, + 10047 => 20047, + 10048 => 20048, + 10049 => 20049, + 10050 => 20050, + 10051 => 20051, + 10052 => 20052, + 10053 => 20053, + 10054 => 20054, + 10055 => 20055, + 10056 => 20056, + 10057 => 20057, + 10058 => 20058, + 10059 => 20059, + 10060 => 20060, + 10061 => 20061, + 10062 => 20062, + 10063 => 20063, + 10064 => 20064, + 10065 => 20065, + 10066 => 20066, + 10067 => 20067, + 10068 => 20068, + 10069 => 20069, + 10070 => 20070, + 10071 => 20071, + 10072 => 20072, + 10073 => 20073, + 10074 => 20074, + 10075 => 20075, + 10076 => 20076, + 10077 => 20077, + 10078 => 20078, + 10079 => 20079, + 10080 => 20080, + 10081 => 20081, + 10082 => 20082, + 10083 => 20083, + 10084 => 20084, + 10085 => 20085, + 10086 => 20086, + 10087 => 20087, + 10088 => 20088, + 10089 => 20089, + 10090 => 20090, + 10091 => 20091, + 10092 => 20092, + 10093 => 20093, + 10094 => 20094, + 10095 => 20095, + 10096 => 20096, + 10097 => 20097, + 10098 => 20098, + 10099 => 20099, + 10100 => 20100, + 10101 => 20101, + 10102 => 20102, + 10103 => 20103, + 10104 => 20104, + 10105 => 20105, + 10106 => 20106, + 10107 => 20107, + 10108 => 20108, + 10109 => 20109, + 10110 => 20110, + 10111 => 20111, + 10112 => 20112, + 10113 => 20113, + 10114 => 20114, + 10115 => 20115, + 10116 => 20116, + 10117 => 20117, + 10118 => 20118, + 10119 => 20119, + 10120 => 20120, + 10121 => 20121, + 10122 => 20122, + 10123 => 20123, + 10124 => 20124, + 10125 => 20125, + 10126 => 20126, + 10127 => 20127, + 10128 => 20128, + 10129 => 20129, + 10130 => 20130, + 10131 => 20131, + 10132 => 20132, + 10133 => 20133, + 10134 => 20134, + 10135 => 20135, + 10136 => 20136, + 10137 => 20137, + 10138 => 20138, + 10139 => 20139, + 10140 => 20140, + 10141 => 20141, + 10142 => 20142, + 10143 => 20143, + 10144 => 20144, + 10145 => 20145, + 10146 => 20146, + 10147 => 20147, + 10148 => 20148, + 10149 => 20149, + 10150 => 20150, + 10151 => 20151, + 10152 => 20152, + 10153 => 20153, + 10154 => 20154, + 10155 => 20155, + 10156 => 20156, + 10157 => 20157, + 10158 => 20158, + 10159 => 20159, + 10160 => 20160, + 10161 => 20161, + 10162 => 20162, + 10163 => 20163, + 10164 => 20164, + 10165 => 20165, + 10166 => 20166, + 10167 => 20167, + 10168 => 20168, + 10169 => 20169, + 10170 => 20170, + 10171 => 20171, + 10172 => 20172, + 10173 => 20173, + 10174 => 20174, + 10175 => 20175, + 10176 => 20176, + 10177 => 20177, + 10178 => 20178, + 10179 => 20179, + 10180 => 20180, + 10181 => 20181, + 10182 => 20182, + 10183 => 20183, + 10184 => 20184, + 10185 => 20185, + 10186 => 20186, + 10187 => 20187, + 10188 => 20188, + 10189 => 20189, + 10190 => 20190, + 10191 => 20191, + 10192 => 20192, + 10193 => 20193, + 10194 => 20194, + 10195 => 20195, + 10196 => 20196, + 10197 => 20197, + 10198 => 20198, + 10199 => 20199, + 10200 => 20200, + 10201 => 20201, + 10202 => 20202, + 10203 => 20203, + 10204 => 20204, + 10205 => 20205, + 10206 => 20206, + 10207 => 20207, + 10208 => 20208, + 10209 => 20209, + 10210 => 20210, + 10211 => 20211, + 10212 => 20212, + 10213 => 20213, + 10214 => 20214, + 10215 => 20215, + 10216 => 20216, + 10217 => 20217, + 10218 => 20218, + 10219 => 20219, + 10220 => 20220, + 10221 => 20221, + 10222 => 20222, + 10223 => 20223, + 10224 => 20224, + 10225 => 20225, + 10226 => 20226, + 10227 => 20227, + 10228 => 20228, + 10229 => 20229, + 10230 => 20230, + 10231 => 20231, + 10232 => 20232, + 10233 => 20233, + 10234 => 20234, + 10235 => 20235, + 10236 => 20236, + 10237 => 20237, + 10238 => 20238, + 10239 => 20239, + 10240 => 20240, + 10241 => 20241, + 10242 => 20242, + 10243 => 20243, + 10244 => 20244, + 10245 => 20245, + 10246 => 20246, + 10247 => 20247, + 10248 => 20248, + 10249 => 20249, + 10250 => 20250, + 10251 => 20251, + 10252 => 20252, + 10253 => 20253, + 10254 => 20254, + 10255 => 20255, + 10256 => 20256, + 10257 => 20257, + 10258 => 20258, + 10259 => 20259, + 10260 => 20260, + 10261 => 20261, + 10262 => 20262, + 10263 => 20263, + 10264 => 20264, + 10265 => 20265, + 10266 => 20266, + 10267 => 20267, + 10268 => 20268, + 10269 => 20269, + 10270 => 20270, + 10271 => 20271, + 10272 => 20272, + 10273 => 20273, + 10274 => 20274, + 10275 => 20275, + 10276 => 20276, + 10277 => 20277, + 10278 => 20278, + 10279 => 20279, + 10280 => 20280, + 10281 => 20281, + 10282 => 20282, + 10283 => 20283, + 10284 => 20284, + 10285 => 20285, + 10286 => 20286, + 10287 => 20287, + 10288 => 20288, + 10289 => 20289, + 10290 => 20290, + 10291 => 20291, + 10292 => 20292, + 10293 => 20293, + 10294 => 20294, + 10295 => 20295, + 10296 => 20296, + 10297 => 20297, + 10298 => 20298, + 10299 => 20299, + 10300 => 20300, + 10301 => 20301, + 10302 => 20302, + 10303 => 20303, + 10304 => 20304, + 10305 => 20305, + 10306 => 20306, + 10307 => 20307, + 10308 => 20308, + 10309 => 20309, + 10310 => 20310, + 10311 => 20311, + 10312 => 20312, + 10313 => 20313, + 10314 => 20314, + 10315 => 20315, + 10316 => 20316, + 10317 => 20317, + 10318 => 20318, + 10319 => 20319, + 10320 => 20320, + 10321 => 20321, + 10322 => 20322, + 10323 => 20323, + 10324 => 20324, + 10325 => 20325, + 10326 => 20326, + 10327 => 20327, + 10328 => 20328, + 10329 => 20329, + 10330 => 20330, + 10331 => 20331, + 10332 => 20332, + 10333 => 20333, + 10334 => 20334, + 10335 => 20335, + 10336 => 20336, + 10337 => 20337, + 10338 => 20338, + 10339 => 20339, + 10340 => 20340, + 10341 => 20341, + 10342 => 20342, + 10343 => 20343, + 10344 => 20344, + 10345 => 20345, + 10346 => 20346, + 10347 => 20347, + 10348 => 20348, + 10349 => 20349, + 10350 => 20350, + 10351 => 20351, + 10352 => 20352, + 10353 => 20353, + 10354 => 20354, + 10355 => 20355, + 10356 => 20356, + 10357 => 20357, + 10358 => 20358, + 10359 => 20359, + 10360 => 20360, + 10361 => 20361, + 10362 => 20362, + 10363 => 20363, + 10364 => 20364, + 10365 => 20365, + 10366 => 20366, + 10367 => 20367, + 10368 => 20368, + 10369 => 20369, + 10370 => 20370, + 10371 => 20371, + 10372 => 20372, + 10373 => 20373, + 10374 => 20374, + 10375 => 20375, + 10376 => 20376, + 10377 => 20377, + 10378 => 20378, + 10379 => 20379, + 10380 => 20380, + 10381 => 20381, + 10382 => 20382, + 10383 => 20383, + 10384 => 20384, + 10385 => 20385, + 10386 => 20386, + 10387 => 20387, + 10388 => 20388, + 10389 => 20389, + 10390 => 20390, + 10391 => 20391, + 10392 => 20392, + 10393 => 20393, + 10394 => 20394, + 10395 => 20395, + 10396 => 20396, + 10397 => 20397, + 10398 => 20398, + 10399 => 20399, + 10400 => 20400, + 10401 => 20401, + 10402 => 20402, + 10403 => 20403, + 10404 => 20404, + 10405 => 20405, + 10406 => 20406, + 10407 => 20407, + 10408 => 20408, + 10409 => 20409, + 10410 => 20410, + 10411 => 20411, + 10412 => 20412, + 10413 => 20413, + 10414 => 20414, + 10415 => 20415, + 10416 => 20416, + 10417 => 20417, + 10418 => 20418, + 10419 => 20419, + 10420 => 20420, + 10421 => 20421, + 10422 => 20422, + 10423 => 20423, + 10424 => 20424, + 10425 => 20425, + 10426 => 20426, + 10427 => 20427, + 10428 => 20428, + 10429 => 20429, + 10430 => 20430, + 10431 => 20431, + 10432 => 20432, + 10433 => 20433, + 10434 => 20434, + 10435 => 20435, + 10436 => 20436, + 10437 => 20437, + 10438 => 20438, + 10439 => 20439, + 10440 => 20440, + 10441 => 20441, + 10442 => 20442, + 10443 => 20443, + 10444 => 20444, + 10445 => 20445, + 10446 => 20446, + 10447 => 20447, + 10448 => 20448, + 10449 => 20449, + 10450 => 20450, + 10451 => 20451, + 10452 => 20452, + 10453 => 20453, + 10454 => 20454, + 10455 => 20455, + 10456 => 20456, + 10457 => 20457, + 10458 => 20458, + 10459 => 20459, + 10460 => 20460, + 10461 => 20461, + 10462 => 20462, + 10463 => 20463, + 10464 => 20464, + 10465 => 20465, + 10466 => 20466, + 10467 => 20467, + 10468 => 20468, + 10469 => 20469, + 10470 => 20470, + 10471 => 20471, + 10472 => 20472, + 10473 => 20473, + 10474 => 20474, + 10475 => 20475, + 10476 => 20476, + 10477 => 20477, + 10478 => 20478, + 10479 => 20479, + 10480 => 20480, + 10481 => 20481, + 10482 => 20482, + 10483 => 20483, + 10484 => 20484, + 10485 => 20485, + 10486 => 20486, + 10487 => 20487, + 10488 => 20488, + 10489 => 20489, + 10490 => 20490, + 10491 => 20491, + 10492 => 20492, + 10493 => 20493, + 10494 => 20494, + 10495 => 20495, + 10496 => 20496, + 10497 => 20497, + 10498 => 20498, + 10499 => 20499, + 10500 => 20500, + 10501 => 20501, + 10502 => 20502, + 10503 => 20503, + 10504 => 20504, + 10505 => 20505, + 10506 => 20506, + 10507 => 20507, + 10508 => 20508, + 10509 => 20509, + 10510 => 20510, + 10511 => 20511, + 10512 => 20512, + 10513 => 20513, + 10514 => 20514, + 10515 => 20515, + 10516 => 20516, + 10517 => 20517, + 10518 => 20518, + 10519 => 20519, + 10520 => 20520, + 10521 => 20521, + 10522 => 20522, + 10523 => 20523, + 10524 => 20524, + 10525 => 20525, + 10526 => 20526, + 10527 => 20527, + 10528 => 20528, + 10529 => 20529, + 10530 => 20530, + 10531 => 20531, + 10532 => 20532, + 10533 => 20533, + 10534 => 20534, + 10535 => 20535, + 10536 => 20536, + 10537 => 20537, + 10538 => 20538, + 10539 => 20539, + 10540 => 20540, + 10541 => 20541, + 10542 => 20542, + 10543 => 20543, + 10544 => 20544, + 10545 => 20545, + 10546 => 20546, + 10547 => 20547, + 10548 => 20548, + 10549 => 20549, + 10550 => 20550, + 10551 => 20551, + 10552 => 20552, + 10553 => 20553, + 10554 => 20554, + 10555 => 20555, + 10556 => 20556, + 10557 => 20557, + 10558 => 20558, + 10559 => 20559, + 10560 => 20560, + 10561 => 20561, + 10562 => 20562, + 10563 => 20563, + 10564 => 20564, + 10565 => 20565, + 10566 => 20566, + 10567 => 20567, + 10568 => 20568, + 10569 => 20569, + 10570 => 20570, + 10571 => 20571, + 10572 => 20572, + 10573 => 20573, + 10574 => 20574, + 10575 => 20575, + 10576 => 20576, + 10577 => 20577, + 10578 => 20578, + 10579 => 20579, + 10580 => 20580, + 10581 => 20581, + 10582 => 20582, + 10583 => 20583, + 10584 => 20584, + 10585 => 20585, + 10586 => 20586, + 10587 => 20587, + 10588 => 20588, + 10589 => 20589, + 10590 => 20590, + 10591 => 20591, + 10592 => 20592, + 10593 => 20593, + 10594 => 20594, + 10595 => 20595, + 10596 => 20596, + 10597 => 20597, + 10598 => 20598, + 10599 => 20599, + 10600 => 20600, + 10601 => 20601, + 10602 => 20602, + 10603 => 20603, + 10604 => 20604, + 10605 => 20605, + 10606 => 20606, + 10607 => 20607, + 10608 => 20608, + 10609 => 20609, + 10610 => 20610, + 10611 => 20611, + 10612 => 20612, + 10613 => 20613, + 10614 => 20614, + 10615 => 20615, + 10616 => 20616, + 10617 => 20617, + 10618 => 20618, + 10619 => 20619, + 10620 => 20620, + 10621 => 20621, + 10622 => 20622, + 10623 => 20623, + 10624 => 20624, + 10625 => 20625, + 10626 => 20626, + 10627 => 20627, + 10628 => 20628, + 10629 => 20629, + 10630 => 20630, + 10631 => 20631, + 10632 => 20632, + 10633 => 20633, + 10634 => 20634, + 10635 => 20635, + 10636 => 20636, + 10637 => 20637, + 10638 => 20638, + 10639 => 20639, + 10640 => 20640, + 10641 => 20641, + 10642 => 20642, + 10643 => 20643, + 10644 => 20644, + 10645 => 20645, + 10646 => 20646, + 10647 => 20647, + 10648 => 20648, + 10649 => 20649, + 10650 => 20650, + 10651 => 20651, + 10652 => 20652, + 10653 => 20653, + 10654 => 20654, + 10655 => 20655, + 10656 => 20656, + 10657 => 20657, + 10658 => 20658, + 10659 => 20659, + 10660 => 20660, + 10661 => 20661, + 10662 => 20662, + 10663 => 20663, + 10664 => 20664, + 10665 => 20665, + 10666 => 20666, + 10667 => 20667, + 10668 => 20668, + 10669 => 20669, + 10670 => 20670, + 10671 => 20671, + 10672 => 20672, + 10673 => 20673, + 10674 => 20674, + 10675 => 20675, + 10676 => 20676, + 10677 => 20677, + 10678 => 20678, + 10679 => 20679, + 10680 => 20680, + 10681 => 20681, + 10682 => 20682, + 10683 => 20683, + 10684 => 20684, + 10685 => 20685, + 10686 => 20686, + 10687 => 20687, + 10688 => 20688, + 10689 => 20689, + 10690 => 20690, + 10691 => 20691, + 10692 => 20692, + 10693 => 20693, + 10694 => 20694, + 10695 => 20695, + 10696 => 20696, + 10697 => 20697, + 10698 => 20698, + 10699 => 20699, + 10700 => 20700, + 10701 => 20701, + 10702 => 20702, + 10703 => 20703, + 10704 => 20704, + 10705 => 20705, + 10706 => 20706, + 10707 => 20707, + 10708 => 20708, + 10709 => 20709, + 10710 => 20710, + 10711 => 20711, + 10712 => 20712, + 10713 => 20713, + 10714 => 20714, + 10715 => 20715, + 10716 => 20716, + 10717 => 20717, + 10718 => 20718, + 10719 => 20719, + 10720 => 20720, + 10721 => 20721, + 10722 => 20722, + 10723 => 20723, + 10724 => 20724, + 10725 => 20725, + 10726 => 20726, + 10727 => 20727, + 10728 => 20728, + 10729 => 20729, + 10730 => 20730, + 10731 => 20731, + 10732 => 20732, + 10733 => 20733, + 10734 => 20734, + 10735 => 20735, + 10736 => 20736, + 10737 => 20737, + 10738 => 20738, + 10739 => 20739, + 10740 => 20740, + 10741 => 20741, + 10742 => 20742, + 10743 => 20743, + 10744 => 20744, + 10745 => 20745, + 10746 => 20746, + 10747 => 20747, + 10748 => 20748, + 10749 => 20749, + 10750 => 20750, + 10751 => 20751, + 10752 => 20752, + 10753 => 20753, + 10754 => 20754, + 10755 => 20755, + 10756 => 20756, + 10757 => 20757, + 10758 => 20758, + 10759 => 20759, + 10760 => 20760, + 10761 => 20761, + 10762 => 20762, + 10763 => 20763, + 10764 => 20764, + 10765 => 20765, + 10766 => 20766, + 10767 => 20767, + 10768 => 20768, + 10769 => 20769, + 10770 => 20770, + 10771 => 20771, + 10772 => 20772, + 10773 => 20773, + 10774 => 20774, + 10775 => 20775, + 10776 => 20776, + 10777 => 20777, + 10778 => 20778, + 10779 => 20779, + 10780 => 20780, + 10781 => 20781, + 10782 => 20782, + 10783 => 20783, + 10784 => 20784, + 10785 => 20785, + 10786 => 20786, + 10787 => 20787, + 10788 => 20788, + 10789 => 20789, + 10790 => 20790, + 10791 => 20791, + 10792 => 20792, + 10793 => 20793, + 10794 => 20794, + 10795 => 20795, + 10796 => 20796, + 10797 => 20797, + 10798 => 20798, + 10799 => 20799, + 10800 => 20800, + 10801 => 20801, + 10802 => 20802, + 10803 => 20803, + 10804 => 20804, + 10805 => 20805, + 10806 => 20806, + 10807 => 20807, + 10808 => 20808, + 10809 => 20809, + 10810 => 20810, + 10811 => 20811, + 10812 => 20812, + 10813 => 20813, + 10814 => 20814, + 10815 => 20815, + 10816 => 20816, + 10817 => 20817, + 10818 => 20818, + 10819 => 20819, + 10820 => 20820, + 10821 => 20821, + 10822 => 20822, + 10823 => 20823, + 10824 => 20824, + 10825 => 20825, + 10826 => 20826, + 10827 => 20827, + 10828 => 20828, + 10829 => 20829, + 10830 => 20830, + 10831 => 20831, + 10832 => 20832, + 10833 => 20833, + 10834 => 20834, + 10835 => 20835, + 10836 => 20836, + 10837 => 20837, + 10838 => 20838, + 10839 => 20839, + 10840 => 20840, + 10841 => 20841, + 10842 => 20842, + 10843 => 20843, + 10844 => 20844, + 10845 => 20845, + 10846 => 20846, + 10847 => 20847, + 10848 => 20848, + 10849 => 20849, + 10850 => 20850, + 10851 => 20851, + 10852 => 20852, + 10853 => 20853, + 10854 => 20854, + 10855 => 20855, + 10856 => 20856, + 10857 => 20857, + 10858 => 20858, + 10859 => 20859, + 10860 => 20860, + 10861 => 20861, + 10862 => 20862, + 10863 => 20863, + 10864 => 20864, + 10865 => 20865, + 10866 => 20866, + 10867 => 20867, + 10868 => 20868, + 10869 => 20869, + 10870 => 20870, + 10871 => 20871, + 10872 => 20872, + 10873 => 20873, + 10874 => 20874, + 10875 => 20875, + 10876 => 20876, + 10877 => 20877, + 10878 => 20878, + 10879 => 20879, + 10880 => 20880, + 10881 => 20881, + 10882 => 20882, + 10883 => 20883, + 10884 => 20884, + 10885 => 20885, + 10886 => 20886, + 10887 => 20887, + 10888 => 20888, + 10889 => 20889, + 10890 => 20890, + 10891 => 20891, + 10892 => 20892, + 10893 => 20893, + 10894 => 20894, + 10895 => 20895, + 10896 => 20896, + 10897 => 20897, + 10898 => 20898, + 10899 => 20899, + 10900 => 20900, + 10901 => 20901, + 10902 => 20902, + 10903 => 20903, + 10904 => 20904, + 10905 => 20905, + 10906 => 20906, + 10907 => 20907, + 10908 => 20908, + 10909 => 20909, + 10910 => 20910, + 10911 => 20911, + 10912 => 20912, + 10913 => 20913, + 10914 => 20914, + 10915 => 20915, + 10916 => 20916, + 10917 => 20917, + 10918 => 20918, + 10919 => 20919, + 10920 => 20920, + 10921 => 20921, + 10922 => 20922, + 10923 => 20923, + 10924 => 20924, + 10925 => 20925, + 10926 => 20926, + 10927 => 20927, + 10928 => 20928, + 10929 => 20929, + 10930 => 20930, + 10931 => 20931, + 10932 => 20932, + 10933 => 20933, + 10934 => 20934, + 10935 => 20935, + 10936 => 20936, + 10937 => 20937, + 10938 => 20938, + 10939 => 20939, + 10940 => 20940, + 10941 => 20941, + 10942 => 20942, + 10943 => 20943, + 10944 => 20944, + 10945 => 20945, + 10946 => 20946, + 10947 => 20947, + 10948 => 20948, + 10949 => 20949, + 10950 => 20950, + 10951 => 20951, + 10952 => 20952, + 10953 => 20953, + 10954 => 20954, + 10955 => 20955, + 10956 => 20956, + 10957 => 20957, + 10958 => 20958, + 10959 => 20959, + 10960 => 20960, + 10961 => 20961, + 10962 => 20962, + 10963 => 20963, + 10964 => 20964, + 10965 => 20965, + 10966 => 20966, + 10967 => 20967, + 10968 => 20968, + 10969 => 20969, + 10970 => 20970, + 10971 => 20971, + 10972 => 20972, + 10973 => 20973, + 10974 => 20974, + 10975 => 20975, + 10976 => 20976, + 10977 => 20977, + 10978 => 20978, + 10979 => 20979, + 10980 => 20980, + 10981 => 20981, + 10982 => 20982, + 10983 => 20983, + 10984 => 20984, + 10985 => 20985, + 10986 => 20986, + 10987 => 20987, + 10988 => 20988, + 10989 => 20989, + 10990 => 20990, + 10991 => 20991, + 10992 => 20992, + 10993 => 20993, + 10994 => 20994, + 10995 => 20995, + 10996 => 20996, + 10997 => 20997, + 10998 => 20998, + 10999 => 20999, + 11000 => 21000, + 11001 => 21001, + 11002 => 21002, + 11003 => 21003, + 11004 => 21004, + 11005 => 21005, + 11006 => 21006, + 11007 => 21007, + 11008 => 21008, + 11009 => 21009, + 11010 => 21010, + 11011 => 21011, + 11012 => 21012, + 11013 => 21013, + 11014 => 21014, + 11015 => 21015, + 11016 => 21016, + 11017 => 21017, + 11018 => 21018, + 11019 => 21019, + 11020 => 21020, + 11021 => 21021, + 11022 => 21022, + 11023 => 21023, + 11024 => 21024, + 11025 => 21025, + 11026 => 21026, + 11027 => 21027, + 11028 => 21028, + 11029 => 21029, + 11030 => 21030, + 11031 => 21031, + 11032 => 21032, + 11033 => 21033, + 11034 => 21034, + 11035 => 21035, + 11036 => 21036, + 11037 => 21037, + 11038 => 21038, + 11039 => 21039, + 11040 => 21040, + 11041 => 21041, + 11042 => 21042, + 11043 => 21043, + 11044 => 21044, + 11045 => 21045, + 11046 => 21046, + 11047 => 21047, + 11048 => 21048, + 11049 => 21049, + 11050 => 21050, + 11051 => 21051, + 11052 => 21052, + 11053 => 21053, + 11054 => 21054, + 11055 => 21055, + 11056 => 21056, + 11057 => 21057, + 11058 => 21058, + 11059 => 21059, + 11060 => 21060, + 11061 => 21061, + 11062 => 21062, + 11063 => 21063, + 11064 => 21064, + 11065 => 21065, + 11066 => 21066, + 11067 => 21067, + 11068 => 21068, + 11069 => 21069, + 11070 => 21070, + 11071 => 21071, + 11072 => 21072, + 11073 => 21073, + 11074 => 21074, + 11075 => 21075, + 11076 => 21076, + 11077 => 21077, + 11078 => 21078, + 11079 => 21079, + 11080 => 21080, + 11081 => 21081, + 11082 => 21082, + 11083 => 21083, + 11084 => 21084, + 11085 => 21085, + 11086 => 21086, + 11087 => 21087, + 11088 => 21088, + 11089 => 21089, + 11090 => 21090, + 11091 => 21091, + 11092 => 21092, + 11093 => 21093, + 11094 => 21094, + 11095 => 21095, + 11096 => 21096, + 11097 => 21097, + 11098 => 21098, + 11099 => 21099, + 11100 => 21100, + 11101 => 21101, + 11102 => 21102, + 11103 => 21103, + 11104 => 21104, + 11105 => 21105, + 11106 => 21106, + 11107 => 21107, + 11108 => 21108, + 11109 => 21109, + 11110 => 21110, + 11111 => 21111, + 11112 => 21112, + 11113 => 21113, + 11114 => 21114, + 11115 => 21115, + 11116 => 21116, + 11117 => 21117, + 11118 => 21118, + 11119 => 21119, + 11120 => 21120, + 11121 => 21121, + 11122 => 21122, + 11123 => 21123, + 11124 => 21124, + 11125 => 21125, + 11126 => 21126, + 11127 => 21127, + 11128 => 21128, + 11129 => 21129, + 11130 => 21130, + 11131 => 21131, + 11132 => 21132, + 11133 => 21133, + 11134 => 21134, + 11135 => 21135, + 11136 => 21136, + 11137 => 21137, + 11138 => 21138, + 11139 => 21139, + 11140 => 21140, + 11141 => 21141, + 11142 => 21142, + 11143 => 21143, + 11144 => 21144, + 11145 => 21145, + 11146 => 21146, + 11147 => 21147, + 11148 => 21148, + 11149 => 21149, + 11150 => 21150, + 11151 => 21151, + 11152 => 21152, + 11153 => 21153, + 11154 => 21154, + 11155 => 21155, + 11156 => 21156, + 11157 => 21157, + 11158 => 21158, + 11159 => 21159, + 11160 => 21160, + 11161 => 21161, + 11162 => 21162, + 11163 => 21163, + 11164 => 21164, + 11165 => 21165, + 11166 => 21166, + 11167 => 21167, + 11168 => 21168, + 11169 => 21169, + 11170 => 21170, + 11171 => 21171, + 11172 => 21172, + 11173 => 21173, + 11174 => 21174, + 11175 => 21175, + 11176 => 21176, + 11177 => 21177, + 11178 => 21178, + 11179 => 21179, + 11180 => 21180, + 11181 => 21181, + 11182 => 21182, + 11183 => 21183, + 11184 => 21184, + 11185 => 21185, + 11186 => 21186, + 11187 => 21187, + 11188 => 21188, + 11189 => 21189, + 11190 => 21190, + 11191 => 21191, + 11192 => 21192, + 11193 => 21193, + 11194 => 21194, + 11195 => 21195, + 11196 => 21196, + 11197 => 21197, + 11198 => 21198, + 11199 => 21199, + 11200 => 21200, + 11201 => 21201, + 11202 => 21202, + 11203 => 21203, + 11204 => 21204, + 11205 => 21205, + 11206 => 21206, + 11207 => 21207, + 11208 => 21208, + 11209 => 21209, + 11210 => 21210, + 11211 => 21211, + 11212 => 21212, + 11213 => 21213, + 11214 => 21214, + 11215 => 21215, + 11216 => 21216, + 11217 => 21217, + 11218 => 21218, + 11219 => 21219, + 11220 => 21220, + 11221 => 21221, + 11222 => 21222, + 11223 => 21223, + 11224 => 21224, + 11225 => 21225, + 11226 => 21226, + 11227 => 21227, + 11228 => 21228, + 11229 => 21229, + 11230 => 21230, + 11231 => 21231, + 11232 => 21232, + 11233 => 21233, + 11234 => 21234, + 11235 => 21235, + 11236 => 21236, + 11237 => 21237, + 11238 => 21238, + 11239 => 21239, + 11240 => 21240, + 11241 => 21241, + 11242 => 21242, + 11243 => 21243, + 11244 => 21244, + 11245 => 21245, + 11246 => 21246, + 11247 => 21247, + 11248 => 21248, + 11249 => 21249, + 11250 => 21250, + 11251 => 21251, + 11252 => 21252, + 11253 => 21253, + 11254 => 21254, + 11255 => 21255, + 11256 => 21256, + 11257 => 21257, + 11258 => 21258, + 11259 => 21259, + 11260 => 21260, + 11261 => 21261, + 11262 => 21262, + 11263 => 21263, + 11264 => 21264, + 11265 => 21265, + 11266 => 21266, + 11267 => 21267, + 11268 => 21268, + 11269 => 21269, + 11270 => 21270, + 11271 => 21271, + 11272 => 21272, + 11273 => 21273, + 11274 => 21274, + 11275 => 21275, + 11276 => 21276, + 11277 => 21277, + 11278 => 21278, + 11279 => 21279, + 11280 => 21280, + 11281 => 21281, + 11282 => 21282, + 11283 => 21283, + 11284 => 21284, + 11285 => 21285, + 11286 => 21286, + 11287 => 21287, + 11288 => 21288, + 11289 => 21289, + 11290 => 21290, + 11291 => 21291, + 11292 => 21292, + 11293 => 21293, + 11294 => 21294, + 11295 => 21295, + 11296 => 21296, + 11297 => 21297, + 11298 => 21298, + 11299 => 21299, + 11300 => 21300, + 11301 => 21301, + 11302 => 21302, + 11303 => 21303, + 11304 => 21304, + 11305 => 21305, + 11306 => 21306, + 11307 => 21307, + 11308 => 21308, + 11309 => 21309, + 11310 => 21310, + 11311 => 21311, + 11312 => 21312, + 11313 => 21313, + 11314 => 21314, + 11315 => 21315, + 11316 => 21316, + 11317 => 21317, + 11318 => 21318, + 11319 => 21319, + 11320 => 21320, + 11321 => 21321, + 11322 => 21322, + 11323 => 21323, + 11324 => 21324, + 11325 => 21325, + 11326 => 21326, + 11327 => 21327, + 11328 => 21328, + 11329 => 21329, + 11330 => 21330, + 11331 => 21331, + 11332 => 21332, + 11333 => 21333, + 11334 => 21334, + 11335 => 21335, + 11336 => 21336, + 11337 => 21337, + 11338 => 21338, + 11339 => 21339, + 11340 => 21340, + 11341 => 21341, + 11342 => 21342, + 11343 => 21343, + 11344 => 21344, + 11345 => 21345, + 11346 => 21346, + 11347 => 21347, + 11348 => 21348, + 11349 => 21349, + 11350 => 21350, + 11351 => 21351, + 11352 => 21352, + 11353 => 21353, + 11354 => 21354, + 11355 => 21355, + 11356 => 21356, + 11357 => 21357, + 11358 => 21358, + 11359 => 21359, + 11360 => 21360, + 11361 => 21361, + 11362 => 21362, + 11363 => 21363, + 11364 => 21364, + 11365 => 21365, + 11366 => 21366, + 11367 => 21367, + 11368 => 21368, + 11369 => 21369, + 11370 => 21370, + 11371 => 21371, + 11372 => 21372, + 11373 => 21373, + 11374 => 21374, + 11375 => 21375, + 11376 => 21376, + 11377 => 21377, + 11378 => 21378, + 11379 => 21379, + 11380 => 21380, + 11381 => 21381, + 11382 => 21382, + 11383 => 21383, + 11384 => 21384, + 11385 => 21385, + 11386 => 21386, + 11387 => 21387, + 11388 => 21388, + 11389 => 21389, + 11390 => 21390, + 11391 => 21391, + 11392 => 21392, + 11393 => 21393, + 11394 => 21394, + 11395 => 21395, + 11396 => 21396, + 11397 => 21397, + 11398 => 21398, + 11399 => 21399, + 11400 => 21400, + 11401 => 21401, + 11402 => 21402, + 11403 => 21403, + 11404 => 21404, + 11405 => 21405, + 11406 => 21406, + 11407 => 21407, + 11408 => 21408, + 11409 => 21409, + 11410 => 21410, + 11411 => 21411, + 11412 => 21412, + 11413 => 21413, + 11414 => 21414, + 11415 => 21415, + 11416 => 21416, + 11417 => 21417, + 11418 => 21418, + 11419 => 21419, + 11420 => 21420, + 11421 => 21421, + 11422 => 21422, + 11423 => 21423, + 11424 => 21424, + 11425 => 21425, + 11426 => 21426, + 11427 => 21427, + 11428 => 21428, + 11429 => 21429, + 11430 => 21430, + 11431 => 21431, + 11432 => 21432, + 11433 => 21433, + 11434 => 21434, + 11435 => 21435, + 11436 => 21436, + 11437 => 21437, + 11438 => 21438, + 11439 => 21439, + 11440 => 21440, + 11441 => 21441, + 11442 => 21442, + 11443 => 21443, + 11444 => 21444, + 11445 => 21445, + 11446 => 21446, + 11447 => 21447, + 11448 => 21448, + 11449 => 21449, + 11450 => 21450, + 11451 => 21451, + 11452 => 21452, + 11453 => 21453, + 11454 => 21454, + 11455 => 21455, + 11456 => 21456, + 11457 => 21457, + 11458 => 21458, + 11459 => 21459, + 11460 => 21460, + 11461 => 21461, + 11462 => 21462, + 11463 => 21463, + 11464 => 21464, + 11465 => 21465, + 11466 => 21466, + 11467 => 21467, + 11468 => 21468, + 11469 => 21469, + 11470 => 21470, + 11471 => 21471, + 11472 => 21472, + 11473 => 21473, + 11474 => 21474, + 11475 => 21475, + 11476 => 21476, + 11477 => 21477, + 11478 => 21478, + 11479 => 21479, + 11480 => 21480, + 11481 => 21481, + 11482 => 21482, + 11483 => 21483, + 11484 => 21484, + 11485 => 21485, + 11486 => 21486, + 11487 => 21487, + 11488 => 21488, + 11489 => 21489, + 11490 => 21490, + 11491 => 21491, + 11492 => 21492, + 11493 => 21493, + 11494 => 21494, + 11495 => 21495, + 11496 => 21496, + 11497 => 21497, + 11498 => 21498, + 11499 => 21499, + 11500 => 21500, + 11501 => 21501, + 11502 => 21502, + 11503 => 21503, + 11504 => 21504, + 11505 => 21505, + 11506 => 21506, + 11507 => 21507, + 11508 => 21508, + 11509 => 21509, + 11510 => 21510, + 11511 => 21511, + 11512 => 21512, + 11513 => 21513, + 11514 => 21514, + 11515 => 21515, + 11516 => 21516, + 11517 => 21517, + 11518 => 21518, + 11519 => 21519, + 11520 => 21520, + 11521 => 21521, + 11522 => 21522, + 11523 => 21523, + 11524 => 21524, + 11525 => 21525, + 11526 => 21526, + 11527 => 21527, + 11528 => 21528, + 11529 => 21529, + 11530 => 21530, + 11531 => 21531, + 11532 => 21532, + 11533 => 21533, + 11534 => 21534, + 11535 => 21535, + 11536 => 21536, + 11537 => 21537, + 11538 => 21538, + 11539 => 21539, + 11540 => 21540, + 11541 => 21541, + 11542 => 21542, + 11543 => 21543, + 11544 => 21544, + 11545 => 21545, + 11546 => 21546, + 11547 => 21547, + 11548 => 21548, + 11549 => 21549, + 11550 => 21550, + 11551 => 21551, + 11552 => 21552, + 11553 => 21553, + 11554 => 21554, + 11555 => 21555, + 11556 => 21556, + 11557 => 21557, + 11558 => 21558, + 11559 => 21559, + 11560 => 21560, + 11561 => 21561, + 11562 => 21562, + 11563 => 21563, + 11564 => 21564, + 11565 => 21565, + 11566 => 21566, + 11567 => 21567, + 11568 => 21568, + 11569 => 21569, + 11570 => 21570, + 11571 => 21571, + 11572 => 21572, + 11573 => 21573, + 11574 => 21574, + 11575 => 21575, + 11576 => 21576, + 11577 => 21577, + 11578 => 21578, + 11579 => 21579, + 11580 => 21580, + 11581 => 21581, + 11582 => 21582, + 11583 => 21583, + 11584 => 21584, + 11585 => 21585, + 11586 => 21586, + 11587 => 21587, + 11588 => 21588, + 11589 => 21589, + 11590 => 21590, + 11591 => 21591, + 11592 => 21592, + 11593 => 21593, + 11594 => 21594, + 11595 => 21595, + 11596 => 21596, + 11597 => 21597, + 11598 => 21598, + 11599 => 21599, + 11600 => 21600, + 11601 => 21601, + 11602 => 21602, + 11603 => 21603, + 11604 => 21604, + 11605 => 21605, + 11606 => 21606, + 11607 => 21607, + 11608 => 21608, + 11609 => 21609, + 11610 => 21610, + 11611 => 21611, + 11612 => 21612, + 11613 => 21613, + 11614 => 21614, + 11615 => 21615, + 11616 => 21616, + 11617 => 21617, + 11618 => 21618, + 11619 => 21619, + 11620 => 21620, + 11621 => 21621, + 11622 => 21622, + 11623 => 21623, + 11624 => 21624, + 11625 => 21625, + 11626 => 21626, + 11627 => 21627, + 11628 => 21628, + 11629 => 21629, + 11630 => 21630, + 11631 => 21631, + 11632 => 21632, + 11633 => 21633, + 11634 => 21634, + 11635 => 21635, + 11636 => 21636, + 11637 => 21637, + 11638 => 21638, + 11639 => 21639, + 11640 => 21640, + 11641 => 21641, + 11642 => 21642, + 11643 => 21643, + 11644 => 21644, + 11645 => 21645, + 11646 => 21646, + 11647 => 21647, + 11648 => 21648, + 11649 => 21649, + 11650 => 21650, + 11651 => 21651, + 11652 => 21652, + 11653 => 21653, + 11654 => 21654, + 11655 => 21655, + 11656 => 21656, + 11657 => 21657, + 11658 => 21658, + 11659 => 21659, + 11660 => 21660, + 11661 => 21661, + 11662 => 21662, + 11663 => 21663, + 11664 => 21664, + 11665 => 21665, + 11666 => 21666, + 11667 => 21667, + 11668 => 21668, + 11669 => 21669, + 11670 => 21670, + 11671 => 21671, + 11672 => 21672, + 11673 => 21673, + 11674 => 21674, + 11675 => 21675, + 11676 => 21676, + 11677 => 21677, + 11678 => 21678, + 11679 => 21679, + 11680 => 21680, + 11681 => 21681, + 11682 => 21682, + 11683 => 21683, + 11684 => 21684, + 11685 => 21685, + 11686 => 21686, + 11687 => 21687, + 11688 => 21688, + 11689 => 21689, + 11690 => 21690, + 11691 => 21691, + 11692 => 21692, + 11693 => 21693, + 11694 => 21694, + 11695 => 21695, + 11696 => 21696, + 11697 => 21697, + 11698 => 21698, + 11699 => 21699, + 11700 => 21700, + 11701 => 21701, + 11702 => 21702, + 11703 => 21703, + 11704 => 21704, + 11705 => 21705, + 11706 => 21706, + 11707 => 21707, + 11708 => 21708, + 11709 => 21709, + 11710 => 21710, + 11711 => 21711, + 11712 => 21712, + 11713 => 21713, + 11714 => 21714, + 11715 => 21715, + 11716 => 21716, + 11717 => 21717, + 11718 => 21718, + 11719 => 21719, + 11720 => 21720, + 11721 => 21721, + 11722 => 21722, + 11723 => 21723, + 11724 => 21724, + 11725 => 21725, + 11726 => 21726, + 11727 => 21727, + 11728 => 21728, + 11729 => 21729, + 11730 => 21730, + 11731 => 21731, + 11732 => 21732, + 11733 => 21733, + 11734 => 21734, + 11735 => 21735, + 11736 => 21736, + 11737 => 21737, + 11738 => 21738, + 11739 => 21739, + 11740 => 21740, + 11741 => 21741, + 11742 => 21742, + 11743 => 21743, + 11744 => 21744, + 11745 => 21745, + 11746 => 21746, + 11747 => 21747, + 11748 => 21748, + 11749 => 21749, + 11750 => 21750, + 11751 => 21751, + 11752 => 21752, + 11753 => 21753, + 11754 => 21754, + 11755 => 21755, + 11756 => 21756, + 11757 => 21757, + 11758 => 21758, + 11759 => 21759, + 11760 => 21760, + 11761 => 21761, + 11762 => 21762, + 11763 => 21763, + 11764 => 21764, + 11765 => 21765, + 11766 => 21766, + 11767 => 21767, + 11768 => 21768, + 11769 => 21769, + 11770 => 21770, + 11771 => 21771, + 11772 => 21772, + 11773 => 21773, + 11774 => 21774, + 11775 => 21775, + 11776 => 21776, + 11777 => 21777, + 11778 => 21778, + 11779 => 21779, + 11780 => 21780, + 11781 => 21781, + 11782 => 21782, + 11783 => 21783, + 11784 => 21784, + 11785 => 21785, + 11786 => 21786, + 11787 => 21787, + 11788 => 21788, + 11789 => 21789, + 11790 => 21790, + 11791 => 21791, + 11792 => 21792, + 11793 => 21793, + 11794 => 21794, + 11795 => 21795, + 11796 => 21796, + 11797 => 21797, + 11798 => 21798, + 11799 => 21799, + 11800 => 21800, + 11801 => 21801, + 11802 => 21802, + 11803 => 21803, + 11804 => 21804, + 11805 => 21805, + 11806 => 21806, + 11807 => 21807, + 11808 => 21808, + 11809 => 21809, + 11810 => 21810, + 11811 => 21811, + 11812 => 21812, + 11813 => 21813, + 11814 => 21814, + 11815 => 21815, + 11816 => 21816, + 11817 => 21817, + 11818 => 21818, + 11819 => 21819, + 11820 => 21820, + 11821 => 21821, + 11822 => 21822, + 11823 => 21823, + 11824 => 21824, + 11825 => 21825, + 11826 => 21826, + 11827 => 21827, + 11828 => 21828, + 11829 => 21829, + 11830 => 21830, + 11831 => 21831, + 11832 => 21832, + 11833 => 21833, + 11834 => 21834, + 11835 => 21835, + 11836 => 21836, + 11837 => 21837, + 11838 => 21838, + 11839 => 21839, + 11840 => 21840, + 11841 => 21841, + 11842 => 21842, + 11843 => 21843, + 11844 => 21844, + 11845 => 21845, + 11846 => 21846, + 11847 => 21847, + 11848 => 21848, + 11849 => 21849, + 11850 => 21850, + 11851 => 21851, + 11852 => 21852, + 11853 => 21853, + 11854 => 21854, + 11855 => 21855, + 11856 => 21856, + 11857 => 21857, + 11858 => 21858, + 11859 => 21859, + 11860 => 21860, + 11861 => 21861, + 11862 => 21862, + 11863 => 21863, + 11864 => 21864, + 11865 => 21865, + 11866 => 21866, + 11867 => 21867, + 11868 => 21868, + 11869 => 21869, + 11870 => 21870, + 11871 => 21871, + 11872 => 21872, + 11873 => 21873, + 11874 => 21874, + 11875 => 21875, + 11876 => 21876, + 11877 => 21877, + 11878 => 21878, + 11879 => 21879, + 11880 => 21880, + 11881 => 21881, + 11882 => 21882, + 11883 => 21883, + 11884 => 21884, + 11885 => 21885, + 11886 => 21886, + 11887 => 21887, + 11888 => 21888, + 11889 => 21889, + 11890 => 21890, + 11891 => 21891, + 11892 => 21892, + 11893 => 21893, + 11894 => 21894, + 11895 => 21895, + 11896 => 21896, + 11897 => 21897, + 11898 => 21898, + 11899 => 21899, + 11900 => 21900, + 11901 => 21901, + 11902 => 21902, + 11903 => 21903, + 11904 => 21904, + 11905 => 21905, + 11906 => 21906, + 11907 => 21907, + 11908 => 21908, + 11909 => 21909, + 11910 => 21910, + 11911 => 21911, + 11912 => 21912, + 11913 => 21913, + 11914 => 21914, + 11915 => 21915, + 11916 => 21916, + 11917 => 21917, + 11918 => 21918, + 11919 => 21919, + 11920 => 21920, + 11921 => 21921, + 11922 => 21922, + 11923 => 21923, + 11924 => 21924, + 11925 => 21925, + 11926 => 21926, + 11927 => 21927, + 11928 => 21928, + 11929 => 21929, + 11930 => 21930, + 11931 => 21931, + 11932 => 21932, + 11933 => 21933, + 11934 => 21934, + 11935 => 21935, + 11936 => 21936, + 11937 => 21937, + 11938 => 21938, + 11939 => 21939, + 11940 => 21940, + 11941 => 21941, + 11942 => 21942, + 11943 => 21943, + 11944 => 21944, + 11945 => 21945, + 11946 => 21946, + 11947 => 21947, + 11948 => 21948, + 11949 => 21949, + 11950 => 21950, + 11951 => 21951, + 11952 => 21952, + 11953 => 21953, + 11954 => 21954, + 11955 => 21955, + 11956 => 21956, + 11957 => 21957, + 11958 => 21958, + 11959 => 21959, + 11960 => 21960, + 11961 => 21961, + 11962 => 21962, + 11963 => 21963, + 11964 => 21964, + 11965 => 21965, + 11966 => 21966, + 11967 => 21967, + 11968 => 21968, + 11969 => 21969, + 11970 => 21970, + 11971 => 21971, + 11972 => 21972, + 11973 => 21973, + 11974 => 21974, + 11975 => 21975, + 11976 => 21976, + 11977 => 21977, + 11978 => 21978, + 11979 => 21979, + 11980 => 21980, + 11981 => 21981, + 11982 => 21982, + 11983 => 21983, + 11984 => 21984, + 11985 => 21985, + 11986 => 21986, + 11987 => 21987, + 11988 => 21988, + 11989 => 21989, + 11990 => 21990, + 11991 => 21991, + 11992 => 21992, + 11993 => 21993, + 11994 => 21994, + 11995 => 21995, + 11996 => 21996, + 11997 => 21997, + 11998 => 21998, + 11999 => 21999, + 12000 => 22000, + 12001 => 22001, + 12002 => 22002, + 12003 => 22003, + 12004 => 22004, + 12005 => 22005, + 12006 => 22006, + 12007 => 22007, + 12008 => 22008, + 12009 => 22009, + 12010 => 22010, + 12011 => 22011, + 12012 => 22012, + 12013 => 22013, + 12014 => 22014, + 12015 => 22015, + 12016 => 22016, + 12017 => 22017, + 12018 => 22018, + 12019 => 22019, + 12020 => 22020, + 12021 => 22021, + 12022 => 22022, + 12023 => 22023, + 12024 => 22024, + 12025 => 22025, + 12026 => 22026, + 12027 => 22027, + 12028 => 22028, + 12029 => 22029, + 12030 => 22030, + 12031 => 22031, + 12032 => 22032, + 12033 => 22033, + 12034 => 22034, + 12035 => 22035, + 12036 => 22036, + 12037 => 22037, + 12038 => 22038, + 12039 => 22039, + 12040 => 22040, + 12041 => 22041, + 12042 => 22042, + 12043 => 22043, + 12044 => 22044, + 12045 => 22045, + 12046 => 22046, + 12047 => 22047, + 12048 => 22048, + 12049 => 22049, + 12050 => 22050, + 12051 => 22051, + 12052 => 22052, + 12053 => 22053, + 12054 => 22054, + 12055 => 22055, + 12056 => 22056, + 12057 => 22057, + 12058 => 22058, + 12059 => 22059, + 12060 => 22060, + 12061 => 22061, + 12062 => 22062, + 12063 => 22063, + 12064 => 22064, + 12065 => 22065, + 12066 => 22066, + 12067 => 22067, + 12068 => 22068, + 12069 => 22069, + 12070 => 22070, + 12071 => 22071, + 12072 => 22072, + 12073 => 22073, + 12074 => 22074, + 12075 => 22075, + 12076 => 22076, + 12077 => 22077, + 12078 => 22078, + 12079 => 22079, + 12080 => 22080, + 12081 => 22081, + 12082 => 22082, + 12083 => 22083, + 12084 => 22084, + 12085 => 22085, + 12086 => 22086, + 12087 => 22087, + 12088 => 22088, + 12089 => 22089, + 12090 => 22090, + 12091 => 22091, + 12092 => 22092, + 12093 => 22093, + 12094 => 22094, + 12095 => 22095, + 12096 => 22096, + 12097 => 22097, + 12098 => 22098, + 12099 => 22099, + 12100 => 22100, + 12101 => 22101, + 12102 => 22102, + 12103 => 22103, + 12104 => 22104, + 12105 => 22105, + 12106 => 22106, + 12107 => 22107, + 12108 => 22108, + 12109 => 22109, + 12110 => 22110, + 12111 => 22111, + 12112 => 22112, + 12113 => 22113, + 12114 => 22114, + 12115 => 22115, + 12116 => 22116, + 12117 => 22117, + 12118 => 22118, + 12119 => 22119, + 12120 => 22120, + 12121 => 22121, + 12122 => 22122, + 12123 => 22123, + 12124 => 22124, + 12125 => 22125, + 12126 => 22126, + 12127 => 22127, + 12128 => 22128, + 12129 => 22129, + 12130 => 22130, + 12131 => 22131, + 12132 => 22132, + 12133 => 22133, + 12134 => 22134, + 12135 => 22135, + 12136 => 22136, + 12137 => 22137, + 12138 => 22138, + 12139 => 22139, + 12140 => 22140, + 12141 => 22141, + 12142 => 22142, + 12143 => 22143, + 12144 => 22144, + 12145 => 22145, + 12146 => 22146, + 12147 => 22147, + 12148 => 22148, + 12149 => 22149, + 12150 => 22150, + 12151 => 22151, + 12152 => 22152, + 12153 => 22153, + 12154 => 22154, + 12155 => 22155, + 12156 => 22156, + 12157 => 22157, + 12158 => 22158, + 12159 => 22159, + 12160 => 22160, + 12161 => 22161, + 12162 => 22162, + 12163 => 22163, + 12164 => 22164, + 12165 => 22165, + 12166 => 22166, + 12167 => 22167, + 12168 => 22168, + 12169 => 22169, + 12170 => 22170, + 12171 => 22171, + 12172 => 22172, + 12173 => 22173, + 12174 => 22174, + 12175 => 22175, + 12176 => 22176, + 12177 => 22177, + 12178 => 22178, + 12179 => 22179, + 12180 => 22180, + 12181 => 22181, + 12182 => 22182, + 12183 => 22183, + 12184 => 22184, + 12185 => 22185, + 12186 => 22186, + 12187 => 22187, + 12188 => 22188, + 12189 => 22189, + 12190 => 22190, + 12191 => 22191, + 12192 => 22192, + 12193 => 22193, + 12194 => 22194, + 12195 => 22195, + 12196 => 22196, + 12197 => 22197, + 12198 => 22198, + 12199 => 22199, + 12200 => 22200, + 12201 => 22201, + 12202 => 22202, + 12203 => 22203, + 12204 => 22204, + 12205 => 22205, + 12206 => 22206, + 12207 => 22207, + 12208 => 22208, + 12209 => 22209, + 12210 => 22210, + 12211 => 22211, + 12212 => 22212, + 12213 => 22213, + 12214 => 22214, + 12215 => 22215, + 12216 => 22216, + 12217 => 22217, + 12218 => 22218, + 12219 => 22219, + 12220 => 22220, + 12221 => 22221, + 12222 => 22222, + 12223 => 22223, + 12224 => 22224, + 12225 => 22225, + 12226 => 22226, + 12227 => 22227, + 12228 => 22228, + 12229 => 22229, + 12230 => 22230, + 12231 => 22231, + 12232 => 22232, + 12233 => 22233, + 12234 => 22234, + 12235 => 22235, + 12236 => 22236, + 12237 => 22237, + 12238 => 22238, + 12239 => 22239, + 12240 => 22240, + 12241 => 22241, + 12242 => 22242, + 12243 => 22243, + 12244 => 22244, + 12245 => 22245, + 12246 => 22246, + 12247 => 22247, + 12248 => 22248, + 12249 => 22249, + 12250 => 22250, + 12251 => 22251, + 12252 => 22252, + 12253 => 22253, + 12254 => 22254, + 12255 => 22255, + 12256 => 22256, + 12257 => 22257, + 12258 => 22258, + 12259 => 22259, + 12260 => 22260, + 12261 => 22261, + 12262 => 22262, + 12263 => 22263, + 12264 => 22264, + 12265 => 22265, + 12266 => 22266, + 12267 => 22267, + 12268 => 22268, + 12269 => 22269, + 12270 => 22270, + 12271 => 22271, + 12272 => 22272, + 12273 => 22273, + 12274 => 22274, + 12275 => 22275, + 12276 => 22276, + 12277 => 22277, + 12278 => 22278, + 12279 => 22279, + 12280 => 22280, + 12281 => 22281, + 12282 => 22282, + 12283 => 22283, + 12284 => 22284, + 12285 => 22285, + 12286 => 22286, + 12287 => 22287, + 12288 => 22288, + 12289 => 22289, + 12290 => 22290, + 12291 => 22291, + 12292 => 22292, + 12293 => 22293, + 12294 => 22294, + 12295 => 22295, + 12296 => 22296, + 12297 => 22297, + 12298 => 22298, + 12299 => 22299, + 12300 => 22300, + 12301 => 22301, + 12302 => 22302, + 12303 => 22303, + 12304 => 22304, + 12305 => 22305, + 12306 => 22306, + 12307 => 22307, + 12308 => 22308, + 12309 => 22309, + 12310 => 22310, + 12311 => 22311, + 12312 => 22312, + 12313 => 22313, + 12314 => 22314, + 12315 => 22315, + 12316 => 22316, + 12317 => 22317, + 12318 => 22318, + 12319 => 22319, + 12320 => 22320, + 12321 => 22321, + 12322 => 22322, + 12323 => 22323, + 12324 => 22324, + 12325 => 22325, + 12326 => 22326, + 12327 => 22327, + 12328 => 22328, + 12329 => 22329, + 12330 => 22330, + 12331 => 22331, + 12332 => 22332, + 12333 => 22333, + 12334 => 22334, + 12335 => 22335, + 12336 => 22336, + 12337 => 22337, + 12338 => 22338, + 12339 => 22339, + 12340 => 22340, + 12341 => 22341, + 12342 => 22342, + 12343 => 22343, + 12344 => 22344, + 12345 => 22345, + 12346 => 22346, + 12347 => 22347, + 12348 => 22348, + 12349 => 22349, + 12350 => 22350, + 12351 => 22351, + 12352 => 22352, + 12353 => 22353, + 12354 => 22354, + 12355 => 22355, + 12356 => 22356, + 12357 => 22357, + 12358 => 22358, + 12359 => 22359, + 12360 => 22360, + 12361 => 22361, + 12362 => 22362, + 12363 => 22363, + 12364 => 22364, + 12365 => 22365, + 12366 => 22366, + 12367 => 22367, + 12368 => 22368, + 12369 => 22369, + 12370 => 22370, + 12371 => 22371, + 12372 => 22372, + 12373 => 22373, + 12374 => 22374, + 12375 => 22375, + 12376 => 22376, + 12377 => 22377, + 12378 => 22378, + 12379 => 22379, + 12380 => 22380, + 12381 => 22381, + 12382 => 22382, + 12383 => 22383, + 12384 => 22384, + 12385 => 22385, + 12386 => 22386, + 12387 => 22387, + 12388 => 22388, + 12389 => 22389, + 12390 => 22390, + 12391 => 22391, + 12392 => 22392, + 12393 => 22393, + 12394 => 22394, + 12395 => 22395, + 12396 => 22396, + 12397 => 22397, + 12398 => 22398, + 12399 => 22399, + 12400 => 22400, + 12401 => 22401, + 12402 => 22402, + 12403 => 22403, + 12404 => 22404, + 12405 => 22405, + 12406 => 22406, + 12407 => 22407, + 12408 => 22408, + 12409 => 22409, + 12410 => 22410, + 12411 => 22411, + 12412 => 22412, + 12413 => 22413, + 12414 => 22414, + 12415 => 22415, + 12416 => 22416, + 12417 => 22417, + 12418 => 22418, + 12419 => 22419, + 12420 => 22420, + 12421 => 22421, + 12422 => 22422, + 12423 => 22423, + 12424 => 22424, + 12425 => 22425, + 12426 => 22426, + 12427 => 22427, + 12428 => 22428, + 12429 => 22429, + 12430 => 22430, + 12431 => 22431, + 12432 => 22432, + 12433 => 22433, + 12434 => 22434, + 12435 => 22435, + 12436 => 22436, + 12437 => 22437, + 12438 => 22438, + 12439 => 22439, + 12440 => 22440, + 12441 => 22441, + 12442 => 22442, + 12443 => 22443, + 12444 => 22444, + 12445 => 22445, + 12446 => 22446, + 12447 => 22447, + 12448 => 22448, + 12449 => 22449, + 12450 => 22450, + 12451 => 22451, + 12452 => 22452, + 12453 => 22453, + 12454 => 22454, + 12455 => 22455, + 12456 => 22456, + 12457 => 22457, + 12458 => 22458, + 12459 => 22459, + 12460 => 22460, + 12461 => 22461, + 12462 => 22462, + 12463 => 22463, + 12464 => 22464, + 12465 => 22465, + 12466 => 22466, + 12467 => 22467, + 12468 => 22468, + 12469 => 22469, + 12470 => 22470, + 12471 => 22471, + 12472 => 22472, + 12473 => 22473, + 12474 => 22474, + 12475 => 22475, + 12476 => 22476, + 12477 => 22477, + 12478 => 22478, + 12479 => 22479, + 12480 => 22480, + 12481 => 22481, + 12482 => 22482, + 12483 => 22483, + 12484 => 22484, + 12485 => 22485, + 12486 => 22486, + 12487 => 22487, + 12488 => 22488, + 12489 => 22489, + 12490 => 22490, + 12491 => 22491, + 12492 => 22492, + 12493 => 22493, + 12494 => 22494, + 12495 => 22495, + 12496 => 22496, + 12497 => 22497, + 12498 => 22498, + 12499 => 22499, + 12500 => 22500, + 12501 => 22501, + 12502 => 22502, + 12503 => 22503, + 12504 => 22504, + 12505 => 22505, + 12506 => 22506, + 12507 => 22507, + 12508 => 22508, + 12509 => 22509, + 12510 => 22510, + 12511 => 22511, + 12512 => 22512, + 12513 => 22513, + 12514 => 22514, + 12515 => 22515, + 12516 => 22516, + 12517 => 22517, + 12518 => 22518, + 12519 => 22519, + 12520 => 22520, + 12521 => 22521, + 12522 => 22522, + 12523 => 22523, + 12524 => 22524, + 12525 => 22525, + 12526 => 22526, + 12527 => 22527, + 12528 => 22528, + 12529 => 22529, + 12530 => 22530, + 12531 => 22531, + 12532 => 22532, + 12533 => 22533, + 12534 => 22534, + 12535 => 22535, + 12536 => 22536, + 12537 => 22537, + 12538 => 22538, + 12539 => 22539, + 12540 => 22540, + 12541 => 22541, + 12542 => 22542, + 12543 => 22543, + 12544 => 22544, + 12545 => 22545, + 12546 => 22546, + 12547 => 22547, + 12548 => 22548, + 12549 => 22549, + 12550 => 22550, + 12551 => 22551, + 12552 => 22552, + 12553 => 22553, + 12554 => 22554, + 12555 => 22555, + 12556 => 22556, + 12557 => 22557, + 12558 => 22558, + 12559 => 22559, + 12560 => 22560, + 12561 => 22561, + 12562 => 22562, + 12563 => 22563, + 12564 => 22564, + 12565 => 22565, + 12566 => 22566, + 12567 => 22567, + 12568 => 22568, + 12569 => 22569, + 12570 => 22570, + 12571 => 22571, + 12572 => 22572, + 12573 => 22573, + 12574 => 22574, + 12575 => 22575, + 12576 => 22576, + 12577 => 22577, + 12578 => 22578, + 12579 => 22579, + 12580 => 22580, + 12581 => 22581, + 12582 => 22582, + 12583 => 22583, + 12584 => 22584, + 12585 => 22585, + 12586 => 22586, + 12587 => 22587, + 12588 => 22588, + 12589 => 22589, + 12590 => 22590, + 12591 => 22591, + 12592 => 22592, + 12593 => 22593, + 12594 => 22594, + 12595 => 22595, + 12596 => 22596, + 12597 => 22597, + 12598 => 22598, + 12599 => 22599, + 12600 => 22600, + 12601 => 22601, + 12602 => 22602, + 12603 => 22603, + 12604 => 22604, + 12605 => 22605, + 12606 => 22606, + 12607 => 22607, + 12608 => 22608, + 12609 => 22609, + 12610 => 22610, + 12611 => 22611, + 12612 => 22612, + 12613 => 22613, + 12614 => 22614, + 12615 => 22615, + 12616 => 22616, + 12617 => 22617, + 12618 => 22618, + 12619 => 22619, + 12620 => 22620, + 12621 => 22621, + 12622 => 22622, + 12623 => 22623, + 12624 => 22624, + 12625 => 22625, + 12626 => 22626, + 12627 => 22627, + 12628 => 22628, + 12629 => 22629, + 12630 => 22630, + 12631 => 22631, + 12632 => 22632, + 12633 => 22633, + 12634 => 22634, + 12635 => 22635, + 12636 => 22636, + 12637 => 22637, + 12638 => 22638, + 12639 => 22639, + 12640 => 22640, + 12641 => 22641, + 12642 => 22642, + 12643 => 22643, + 12644 => 22644, + 12645 => 22645, + 12646 => 22646, + 12647 => 22647, + 12648 => 22648, + 12649 => 22649, + 12650 => 22650, + 12651 => 22651, + 12652 => 22652, + 12653 => 22653, + 12654 => 22654, + 12655 => 22655, + 12656 => 22656, + 12657 => 22657, + 12658 => 22658, + 12659 => 22659, + 12660 => 22660, + 12661 => 22661, + 12662 => 22662, + 12663 => 22663, + 12664 => 22664, + 12665 => 22665, + 12666 => 22666, + 12667 => 22667, + 12668 => 22668, + 12669 => 22669, + 12670 => 22670, + 12671 => 22671, + 12672 => 22672, + 12673 => 22673, + 12674 => 22674, + 12675 => 22675, + 12676 => 22676, + 12677 => 22677, + 12678 => 22678, + 12679 => 22679, + 12680 => 22680, + 12681 => 22681, + 12682 => 22682, + 12683 => 22683, + 12684 => 22684, + 12685 => 22685, + 12686 => 22686, + 12687 => 22687, + 12688 => 22688, + 12689 => 22689, + 12690 => 22690, + 12691 => 22691, + 12692 => 22692, + 12693 => 22693, + 12694 => 22694, + 12695 => 22695, + 12696 => 22696, + 12697 => 22697, + 12698 => 22698, + 12699 => 22699, + 12700 => 22700, + 12701 => 22701, + 12702 => 22702, + 12703 => 22703, + 12704 => 22704, + 12705 => 22705, + 12706 => 22706, + 12707 => 22707, + 12708 => 22708, + 12709 => 22709, + 12710 => 22710, + 12711 => 22711, + 12712 => 22712, + 12713 => 22713, + 12714 => 22714, + 12715 => 22715, + 12716 => 22716, + 12717 => 22717, + 12718 => 22718, + 12719 => 22719, + 12720 => 22720, + 12721 => 22721, + 12722 => 22722, + 12723 => 22723, + 12724 => 22724, + 12725 => 22725, + 12726 => 22726, + 12727 => 22727, + 12728 => 22728, + 12729 => 22729, + 12730 => 22730, + 12731 => 22731, + 12732 => 22732, + 12733 => 22733, + 12734 => 22734, + 12735 => 22735, + 12736 => 22736, + 12737 => 22737, + 12738 => 22738, + 12739 => 22739, + 12740 => 22740, + 12741 => 22741, + 12742 => 22742, + 12743 => 22743, + 12744 => 22744, + 12745 => 22745, + 12746 => 22746, + 12747 => 22747, + 12748 => 22748, + 12749 => 22749, + 12750 => 22750, + 12751 => 22751, + 12752 => 22752, + 12753 => 22753, + 12754 => 22754, + 12755 => 22755, + 12756 => 22756, + 12757 => 22757, + 12758 => 22758, + 12759 => 22759, + 12760 => 22760, + 12761 => 22761, + 12762 => 22762, + 12763 => 22763, + 12764 => 22764, + 12765 => 22765, + 12766 => 22766, + 12767 => 22767, + 12768 => 22768, + 12769 => 22769, + 12770 => 22770, + 12771 => 22771, + 12772 => 22772, + 12773 => 22773, + 12774 => 22774, + 12775 => 22775, + 12776 => 22776, + 12777 => 22777, + 12778 => 22778, + 12779 => 22779, + 12780 => 22780, + 12781 => 22781, + 12782 => 22782, + 12783 => 22783, + 12784 => 22784, + 12785 => 22785, + 12786 => 22786, + 12787 => 22787, + 12788 => 22788, + 12789 => 22789, + 12790 => 22790, + 12791 => 22791, + 12792 => 22792, + 12793 => 22793, + 12794 => 22794, + 12795 => 22795, + 12796 => 22796, + 12797 => 22797, + 12798 => 22798, + 12799 => 22799, + 12800 => 22800, + 12801 => 22801, + 12802 => 22802, + 12803 => 22803, + 12804 => 22804, + 12805 => 22805, + 12806 => 22806, + 12807 => 22807, + 12808 => 22808, + 12809 => 22809, + 12810 => 22810, + 12811 => 22811, + 12812 => 22812, + 12813 => 22813, + 12814 => 22814, + 12815 => 22815, + 12816 => 22816, + 12817 => 22817, + 12818 => 22818, + 12819 => 22819, + 12820 => 22820, + 12821 => 22821, + 12822 => 22822, + 12823 => 22823, + 12824 => 22824, + 12825 => 22825, + 12826 => 22826, + 12827 => 22827, + 12828 => 22828, + 12829 => 22829, + 12830 => 22830, + 12831 => 22831, + 12832 => 22832, + 12833 => 22833, + 12834 => 22834, + 12835 => 22835, + 12836 => 22836, + 12837 => 22837, + 12838 => 22838, + 12839 => 22839, + 12840 => 22840, + 12841 => 22841, + 12842 => 22842, + 12843 => 22843, + 12844 => 22844, + 12845 => 22845, + 12846 => 22846, + 12847 => 22847, + 12848 => 22848, + 12849 => 22849, + 12850 => 22850, + 12851 => 22851, + 12852 => 22852, + 12853 => 22853, + 12854 => 22854, + 12855 => 22855, + 12856 => 22856, + 12857 => 22857, + 12858 => 22858, + 12859 => 22859, + 12860 => 22860, + 12861 => 22861, + 12862 => 22862, + 12863 => 22863, + 12864 => 22864, + 12865 => 22865, + 12866 => 22866, + 12867 => 22867, + 12868 => 22868, + 12869 => 22869, + 12870 => 22870, + 12871 => 22871, + 12872 => 22872, + 12873 => 22873, + 12874 => 22874, + 12875 => 22875, + 12876 => 22876, + 12877 => 22877, + 12878 => 22878, + 12879 => 22879, + 12880 => 22880, + 12881 => 22881, + 12882 => 22882, + 12883 => 22883, + 12884 => 22884, + 12885 => 22885, + 12886 => 22886, + 12887 => 22887, + 12888 => 22888, + 12889 => 22889, + 12890 => 22890, + 12891 => 22891, + 12892 => 22892, + 12893 => 22893, + 12894 => 22894, + 12895 => 22895, + 12896 => 22896, + 12897 => 22897, + 12898 => 22898, + 12899 => 22899, + 12900 => 22900, + 12901 => 22901, + 12902 => 22902, + 12903 => 22903, + 12904 => 22904, + 12905 => 22905, + 12906 => 22906, + 12907 => 22907, + 12908 => 22908, + 12909 => 22909, + 12910 => 22910, + 12911 => 22911, + 12912 => 22912, + 12913 => 22913, + 12914 => 22914, + 12915 => 22915, + 12916 => 22916, + 12917 => 22917, + 12918 => 22918, + 12919 => 22919, + 12920 => 22920, + 12921 => 22921, + 12922 => 22922, + 12923 => 22923, + 12924 => 22924, + 12925 => 22925, + 12926 => 22926, + 12927 => 22927, + 12928 => 22928, + 12929 => 22929, + 12930 => 22930, + 12931 => 22931, + 12932 => 22932, + 12933 => 22933, + 12934 => 22934, + 12935 => 22935, + 12936 => 22936, + 12937 => 22937, + 12938 => 22938, + 12939 => 22939, + 12940 => 22940, + 12941 => 22941, + 12942 => 22942, + 12943 => 22943, + 12944 => 22944, + 12945 => 22945, + 12946 => 22946, + 12947 => 22947, + 12948 => 22948, + 12949 => 22949, + 12950 => 22950, + 12951 => 22951, + 12952 => 22952, + 12953 => 22953, + 12954 => 22954, + 12955 => 22955, + 12956 => 22956, + 12957 => 22957, + 12958 => 22958, + 12959 => 22959, + 12960 => 22960, + 12961 => 22961, + 12962 => 22962, + 12963 => 22963, + 12964 => 22964, + 12965 => 22965, + 12966 => 22966, + 12967 => 22967, + 12968 => 22968, + 12969 => 22969, + 12970 => 22970, + 12971 => 22971, + 12972 => 22972, + 12973 => 22973, + 12974 => 22974, + 12975 => 22975, + 12976 => 22976, + 12977 => 22977, + 12978 => 22978, + 12979 => 22979, + 12980 => 22980, + 12981 => 22981, + 12982 => 22982, + 12983 => 22983, + 12984 => 22984, + 12985 => 22985, + 12986 => 22986, + 12987 => 22987, + 12988 => 22988, + 12989 => 22989, + 12990 => 22990, + 12991 => 22991, + 12992 => 22992, + 12993 => 22993, + 12994 => 22994, + 12995 => 22995, + 12996 => 22996, + 12997 => 22997, + 12998 => 22998, + 12999 => 22999, + 13000 => 23000, + 13001 => 23001, + 13002 => 23002, + 13003 => 23003, + 13004 => 23004, + 13005 => 23005, + 13006 => 23006, + 13007 => 23007, + 13008 => 23008, + 13009 => 23009, + 13010 => 23010, + 13011 => 23011, + 13012 => 23012, + 13013 => 23013, + 13014 => 23014, + 13015 => 23015, + 13016 => 23016, + 13017 => 23017, + 13018 => 23018, + 13019 => 23019, + 13020 => 23020, + 13021 => 23021, + 13022 => 23022, + 13023 => 23023, + 13024 => 23024, + 13025 => 23025, + 13026 => 23026, + 13027 => 23027, + 13028 => 23028, + 13029 => 23029, + 13030 => 23030, + 13031 => 23031, + 13032 => 23032, + 13033 => 23033, + 13034 => 23034, + 13035 => 23035, + 13036 => 23036, + 13037 => 23037, + 13038 => 23038, + 13039 => 23039, + 13040 => 23040, + 13041 => 23041, + 13042 => 23042, + 13043 => 23043, + 13044 => 23044, + 13045 => 23045, + 13046 => 23046, + 13047 => 23047, + 13048 => 23048, + 13049 => 23049, + 13050 => 23050, + 13051 => 23051, + 13052 => 23052, + 13053 => 23053, + 13054 => 23054, + 13055 => 23055, + 13056 => 23056, + 13057 => 23057, + 13058 => 23058, + 13059 => 23059, + 13060 => 23060, + 13061 => 23061, + 13062 => 23062, + 13063 => 23063, + 13064 => 23064, + 13065 => 23065, + 13066 => 23066, + 13067 => 23067, + 13068 => 23068, + 13069 => 23069, + 13070 => 23070, + 13071 => 23071, + 13072 => 23072, + 13073 => 23073, + 13074 => 23074, + 13075 => 23075, + 13076 => 23076, + 13077 => 23077, + 13078 => 23078, + 13079 => 23079, + 13080 => 23080, + 13081 => 23081, + 13082 => 23082, + 13083 => 23083, + 13084 => 23084, + 13085 => 23085, + 13086 => 23086, + 13087 => 23087, + 13088 => 23088, + 13089 => 23089, + 13090 => 23090, + 13091 => 23091, + 13092 => 23092, + 13093 => 23093, + 13094 => 23094, + 13095 => 23095, + 13096 => 23096, + 13097 => 23097, + 13098 => 23098, + 13099 => 23099, + 13100 => 23100, + 13101 => 23101, + 13102 => 23102, + 13103 => 23103, + 13104 => 23104, + 13105 => 23105, + 13106 => 23106, + 13107 => 23107, + 13108 => 23108, + 13109 => 23109, + 13110 => 23110, + 13111 => 23111, + 13112 => 23112, + 13113 => 23113, + 13114 => 23114, + 13115 => 23115, + 13116 => 23116, + 13117 => 23117, + 13118 => 23118, + 13119 => 23119, + 13120 => 23120, + 13121 => 23121, + 13122 => 23122, + 13123 => 23123, + 13124 => 23124, + 13125 => 23125, + 13126 => 23126, + 13127 => 23127, + 13128 => 23128, + 13129 => 23129, + 13130 => 23130, + 13131 => 23131, + 13132 => 23132, + 13133 => 23133, + 13134 => 23134, + 13135 => 23135, + 13136 => 23136, + 13137 => 23137, + 13138 => 23138, + 13139 => 23139, + 13140 => 23140, + 13141 => 23141, + 13142 => 23142, + 13143 => 23143, + 13144 => 23144, + 13145 => 23145, + 13146 => 23146, + 13147 => 23147, + 13148 => 23148, + 13149 => 23149, + 13150 => 23150, + 13151 => 23151, + 13152 => 23152, + 13153 => 23153, + 13154 => 23154, + 13155 => 23155, + 13156 => 23156, + 13157 => 23157, + 13158 => 23158, + 13159 => 23159, + 13160 => 23160, + 13161 => 23161, + 13162 => 23162, + 13163 => 23163, + 13164 => 23164, + 13165 => 23165, + 13166 => 23166, + 13167 => 23167, + 13168 => 23168, + 13169 => 23169, + 13170 => 23170, + 13171 => 23171, + 13172 => 23172, + 13173 => 23173, + 13174 => 23174, + 13175 => 23175, + 13176 => 23176, + 13177 => 23177, + 13178 => 23178, + 13179 => 23179, + 13180 => 23180, + 13181 => 23181, + 13182 => 23182, + 13183 => 23183, + 13184 => 23184, + 13185 => 23185, + 13186 => 23186, + 13187 => 23187, + 13188 => 23188, + 13189 => 23189, + 13190 => 23190, + 13191 => 23191, + 13192 => 23192, + 13193 => 23193, + 13194 => 23194, + 13195 => 23195, + 13196 => 23196, + 13197 => 23197, + 13198 => 23198, + 13199 => 23199, + 13200 => 23200, + 13201 => 23201, + 13202 => 23202, + 13203 => 23203, + 13204 => 23204, + 13205 => 23205, + 13206 => 23206, + 13207 => 23207, + 13208 => 23208, + 13209 => 23209, + 13210 => 23210, + 13211 => 23211, + 13212 => 23212, + 13213 => 23213, + 13214 => 23214, + 13215 => 23215, + 13216 => 23216, + 13217 => 23217, + 13218 => 23218, + 13219 => 23219, + 13220 => 23220, + 13221 => 23221, + 13222 => 23222, + 13223 => 23223, + 13224 => 23224, + 13225 => 23225, + 13226 => 23226, + 13227 => 23227, + 13228 => 23228, + 13229 => 23229, + 13230 => 23230, + 13231 => 23231, + 13232 => 23232, + 13233 => 23233, + 13234 => 23234, + 13235 => 23235, + 13236 => 23236, + 13237 => 23237, + 13238 => 23238, + 13239 => 23239, + 13240 => 23240, + 13241 => 23241, + 13242 => 23242, + 13243 => 23243, + 13244 => 23244, + 13245 => 23245, + 13246 => 23246, + 13247 => 23247, + 13248 => 23248, + 13249 => 23249, + 13250 => 23250, + 13251 => 23251, + 13252 => 23252, + 13253 => 23253, + 13254 => 23254, + 13255 => 23255, + 13256 => 23256, + 13257 => 23257, + 13258 => 23258, + 13259 => 23259, + 13260 => 23260, + 13261 => 23261, + 13262 => 23262, + 13263 => 23263, + 13264 => 23264, + 13265 => 23265, + 13266 => 23266, + 13267 => 23267, + 13268 => 23268, + 13269 => 23269, + 13270 => 23270, + 13271 => 23271, + 13272 => 23272, + 13273 => 23273, + 13274 => 23274, + 13275 => 23275, + 13276 => 23276, + 13277 => 23277, + 13278 => 23278, + 13279 => 23279, + 13280 => 23280, + 13281 => 23281, + 13282 => 23282, + 13283 => 23283, + 13284 => 23284, + 13285 => 23285, + 13286 => 23286, + 13287 => 23287, + 13288 => 23288, + 13289 => 23289, + 13290 => 23290, + 13291 => 23291, + 13292 => 23292, + 13293 => 23293, + 13294 => 23294, + 13295 => 23295, + 13296 => 23296, + 13297 => 23297, + 13298 => 23298, + 13299 => 23299, + 13300 => 23300, + 13301 => 23301, + 13302 => 23302, + 13303 => 23303, + 13304 => 23304, + 13305 => 23305, + 13306 => 23306, + 13307 => 23307, + 13308 => 23308, + 13309 => 23309, + 13310 => 23310, + 13311 => 23311, + 13312 => 23312, + 13313 => 23313, + 13314 => 23314, + 13315 => 23315, + 13316 => 23316, + 13317 => 23317, + 13318 => 23318, + 13319 => 23319, + 13320 => 23320, + 13321 => 23321, + 13322 => 23322, + 13323 => 23323, + 13324 => 23324, + 13325 => 23325, + 13326 => 23326, + 13327 => 23327, + 13328 => 23328, + 13329 => 23329, + 13330 => 23330, + 13331 => 23331, + 13332 => 23332, + 13333 => 23333, + 13334 => 23334, + 13335 => 23335, + 13336 => 23336, + 13337 => 23337, + 13338 => 23338, + 13339 => 23339, + 13340 => 23340, + 13341 => 23341, + 13342 => 23342, + 13343 => 23343, + 13344 => 23344, + 13345 => 23345, + 13346 => 23346, + 13347 => 23347, + 13348 => 23348, + 13349 => 23349, + 13350 => 23350, + 13351 => 23351, + 13352 => 23352, + 13353 => 23353, + 13354 => 23354, + 13355 => 23355, + 13356 => 23356, + 13357 => 23357, + 13358 => 23358, + 13359 => 23359, + 13360 => 23360, + 13361 => 23361, + 13362 => 23362, + 13363 => 23363, + 13364 => 23364, + 13365 => 23365, + 13366 => 23366, + 13367 => 23367, + 13368 => 23368, + 13369 => 23369, + 13370 => 23370, + 13371 => 23371, + 13372 => 23372, + 13373 => 23373, + 13374 => 23374, + 13375 => 23375, + 13376 => 23376, + 13377 => 23377, + 13378 => 23378, + 13379 => 23379, + 13380 => 23380, + 13381 => 23381, + 13382 => 23382, + 13383 => 23383, + 13384 => 23384, + 13385 => 23385, + 13386 => 23386, + 13387 => 23387, + 13388 => 23388, + 13389 => 23389, + 13390 => 23390, + 13391 => 23391, + 13392 => 23392, + 13393 => 23393, + 13394 => 23394, + 13395 => 23395, + 13396 => 23396, + 13397 => 23397, + 13398 => 23398, + 13399 => 23399, + 13400 => 23400, + 13401 => 23401, + 13402 => 23402, + 13403 => 23403, + 13404 => 23404, + 13405 => 23405, + 13406 => 23406, + 13407 => 23407, + 13408 => 23408, + 13409 => 23409, + 13410 => 23410, + 13411 => 23411, + 13412 => 23412, + 13413 => 23413, + 13414 => 23414, + 13415 => 23415, + 13416 => 23416, + 13417 => 23417, + 13418 => 23418, + 13419 => 23419, + 13420 => 23420, + 13421 => 23421, + 13422 => 23422, + 13423 => 23423, + 13424 => 23424, + 13425 => 23425, + 13426 => 23426, + 13427 => 23427, + 13428 => 23428, + 13429 => 23429, + 13430 => 23430, + 13431 => 23431, + 13432 => 23432, + 13433 => 23433, + 13434 => 23434, + 13435 => 23435, + 13436 => 23436, + 13437 => 23437, + 13438 => 23438, + 13439 => 23439, + 13440 => 23440, + 13441 => 23441, + 13442 => 23442, + 13443 => 23443, + 13444 => 23444, + 13445 => 23445, + 13446 => 23446, + 13447 => 23447, + 13448 => 23448, + 13449 => 23449, + 13450 => 23450, + 13451 => 23451, + 13452 => 23452, + 13453 => 23453, + 13454 => 23454, + 13455 => 23455, + 13456 => 23456, + 13457 => 23457, + 13458 => 23458, + 13459 => 23459, + 13460 => 23460, + 13461 => 23461, + 13462 => 23462, + 13463 => 23463, + 13464 => 23464, + 13465 => 23465, + 13466 => 23466, + 13467 => 23467, + 13468 => 23468, + 13469 => 23469, + 13470 => 23470, + 13471 => 23471, + 13472 => 23472, + 13473 => 23473, + 13474 => 23474, + 13475 => 23475, + 13476 => 23476, + 13477 => 23477, + 13478 => 23478, + 13479 => 23479, + 13480 => 23480, + 13481 => 23481, + 13482 => 23482, + 13483 => 23483, + 13484 => 23484, + 13485 => 23485, + 13486 => 23486, + 13487 => 23487, + 13488 => 23488, + 13489 => 23489, + 13490 => 23490, + 13491 => 23491, + 13492 => 23492, + 13493 => 23493, + 13494 => 23494, + 13495 => 23495, + 13496 => 23496, + 13497 => 23497, + 13498 => 23498, + 13499 => 23499, + 13500 => 23500, + 13501 => 23501, + 13502 => 23502, + 13503 => 23503, + 13504 => 23504, + 13505 => 23505, + 13506 => 23506, + 13507 => 23507, + 13508 => 23508, + 13509 => 23509, + 13510 => 23510, + 13511 => 23511, + 13512 => 23512, + 13513 => 23513, + 13514 => 23514, + 13515 => 23515, + 13516 => 23516, + 13517 => 23517, + 13518 => 23518, + 13519 => 23519, + 13520 => 23520, + 13521 => 23521, + 13522 => 23522, + 13523 => 23523, + 13524 => 23524, + 13525 => 23525, + 13526 => 23526, + 13527 => 23527, + 13528 => 23528, + 13529 => 23529, + 13530 => 23530, + 13531 => 23531, + 13532 => 23532, + 13533 => 23533, + 13534 => 23534, + 13535 => 23535, + 13536 => 23536, + 13537 => 23537, + 13538 => 23538, + 13539 => 23539, + 13540 => 23540, + 13541 => 23541, + 13542 => 23542, + 13543 => 23543, + 13544 => 23544, + 13545 => 23545, + 13546 => 23546, + 13547 => 23547, + 13548 => 23548, + 13549 => 23549, + 13550 => 23550, + 13551 => 23551, + 13552 => 23552, + 13553 => 23553, + 13554 => 23554, + 13555 => 23555, + 13556 => 23556, + 13557 => 23557, + 13558 => 23558, + 13559 => 23559, + 13560 => 23560, + 13561 => 23561, + 13562 => 23562, + 13563 => 23563, + 13564 => 23564, + 13565 => 23565, + 13566 => 23566, + 13567 => 23567, + 13568 => 23568, + 13569 => 23569, + 13570 => 23570, + 13571 => 23571, + 13572 => 23572, + 13573 => 23573, + 13574 => 23574, + 13575 => 23575, + 13576 => 23576, + 13577 => 23577, + 13578 => 23578, + 13579 => 23579, + 13580 => 23580, + 13581 => 23581, + 13582 => 23582, + 13583 => 23583, + 13584 => 23584, + 13585 => 23585, + 13586 => 23586, + 13587 => 23587, + 13588 => 23588, + 13589 => 23589, + 13590 => 23590, + 13591 => 23591, + 13592 => 23592, + 13593 => 23593, + 13594 => 23594, + 13595 => 23595, + 13596 => 23596, + 13597 => 23597, + 13598 => 23598, + 13599 => 23599, + 13600 => 23600, + 13601 => 23601, + 13602 => 23602, + 13603 => 23603, + 13604 => 23604, + 13605 => 23605, + 13606 => 23606, + 13607 => 23607, + 13608 => 23608, + 13609 => 23609, + 13610 => 23610, + 13611 => 23611, + 13612 => 23612, + 13613 => 23613, + 13614 => 23614, + 13615 => 23615, + 13616 => 23616, + 13617 => 23617, + 13618 => 23618, + 13619 => 23619, + 13620 => 23620, + 13621 => 23621, + 13622 => 23622, + 13623 => 23623, + 13624 => 23624, + 13625 => 23625, + 13626 => 23626, + 13627 => 23627, + 13628 => 23628, + 13629 => 23629, + 13630 => 23630, + 13631 => 23631, + 13632 => 23632, + 13633 => 23633, + 13634 => 23634, + 13635 => 23635, + 13636 => 23636, + 13637 => 23637, + 13638 => 23638, + 13639 => 23639, + 13640 => 23640, + 13641 => 23641, + 13642 => 23642, + 13643 => 23643, + 13644 => 23644, + 13645 => 23645, + 13646 => 23646, + 13647 => 23647, + 13648 => 23648, + 13649 => 23649, + 13650 => 23650, + 13651 => 23651, + 13652 => 23652, + 13653 => 23653, + 13654 => 23654, + 13655 => 23655, + 13656 => 23656, + 13657 => 23657, + 13658 => 23658, + 13659 => 23659, + 13660 => 23660, + 13661 => 23661, + 13662 => 23662, + 13663 => 23663, + 13664 => 23664, + 13665 => 23665, + 13666 => 23666, + 13667 => 23667, + 13668 => 23668, + 13669 => 23669, + 13670 => 23670, + 13671 => 23671, + 13672 => 23672, + 13673 => 23673, + 13674 => 23674, + 13675 => 23675, + 13676 => 23676, + 13677 => 23677, + 13678 => 23678, + 13679 => 23679, + 13680 => 23680, + 13681 => 23681, + 13682 => 23682, + 13683 => 23683, + 13684 => 23684, + 13685 => 23685, + 13686 => 23686, + 13687 => 23687, + 13688 => 23688, + 13689 => 23689, + 13690 => 23690, + 13691 => 23691, + 13692 => 23692, + 13693 => 23693, + 13694 => 23694, + 13695 => 23695, + 13696 => 23696, + 13697 => 23697, + 13698 => 23698, + 13699 => 23699, + 13700 => 23700, + 13701 => 23701, + 13702 => 23702, + 13703 => 23703, + 13704 => 23704, + 13705 => 23705, + 13706 => 23706, + 13707 => 23707, + 13708 => 23708, + 13709 => 23709, + 13710 => 23710, + 13711 => 23711, + 13712 => 23712, + 13713 => 23713, + 13714 => 23714, + 13715 => 23715, + 13716 => 23716, + 13717 => 23717, + 13718 => 23718, + 13719 => 23719, + 13720 => 23720, + 13721 => 23721, + 13722 => 23722, + 13723 => 23723, + 13724 => 23724, + 13725 => 23725, + 13726 => 23726, + 13727 => 23727, + 13728 => 23728, + 13729 => 23729, + 13730 => 23730, + 13731 => 23731, + 13732 => 23732, + 13733 => 23733, + 13734 => 23734, + 13735 => 23735, + 13736 => 23736, + 13737 => 23737, + 13738 => 23738, + 13739 => 23739, + 13740 => 23740, + 13741 => 23741, + 13742 => 23742, + 13743 => 23743, + 13744 => 23744, + 13745 => 23745, + 13746 => 23746, + 13747 => 23747, + 13748 => 23748, + 13749 => 23749, + 13750 => 23750, + 13751 => 23751, + 13752 => 23752, + 13753 => 23753, + 13754 => 23754, + 13755 => 23755, + 13756 => 23756, + 13757 => 23757, + 13758 => 23758, + 13759 => 23759, + 13760 => 23760, + 13761 => 23761, + 13762 => 23762, + 13763 => 23763, + 13764 => 23764, + 13765 => 23765, + 13766 => 23766, + 13767 => 23767, + 13768 => 23768, + 13769 => 23769, + 13770 => 23770, + 13771 => 23771, + 13772 => 23772, + 13773 => 23773, + 13774 => 23774, + 13775 => 23775, + 13776 => 23776, + 13777 => 23777, + 13778 => 23778, + 13779 => 23779, + 13780 => 23780, + 13781 => 23781, + 13782 => 23782, + 13783 => 23783, + 13784 => 23784, + 13785 => 23785, + 13786 => 23786, + 13787 => 23787, + 13788 => 23788, + 13789 => 23789, + 13790 => 23790, + 13791 => 23791, + 13792 => 23792, + 13793 => 23793, + 13794 => 23794, + 13795 => 23795, + 13796 => 23796, + 13797 => 23797, + 13798 => 23798, + 13799 => 23799, + 13800 => 23800, + 13801 => 23801, + 13802 => 23802, + 13803 => 23803, + 13804 => 23804, + 13805 => 23805, + 13806 => 23806, + 13807 => 23807, + 13808 => 23808, + 13809 => 23809, + 13810 => 23810, + 13811 => 23811, + 13812 => 23812, + 13813 => 23813, + 13814 => 23814, + 13815 => 23815, + 13816 => 23816, + 13817 => 23817, + 13818 => 23818, + 13819 => 23819, + 13820 => 23820, + 13821 => 23821, + 13822 => 23822, + 13823 => 23823, + 13824 => 23824, + 13825 => 23825, + 13826 => 23826, + 13827 => 23827, + 13828 => 23828, + 13829 => 23829, + 13830 => 23830, + 13831 => 23831, + 13832 => 23832, + 13833 => 23833, + 13834 => 23834, + 13835 => 23835, + 13836 => 23836, + 13837 => 23837, + 13838 => 23838, + 13839 => 23839, + 13840 => 23840, + 13841 => 23841, + 13842 => 23842, + 13843 => 23843, + 13844 => 23844, + 13845 => 23845, + 13846 => 23846, + 13847 => 23847, + 13848 => 23848, + 13849 => 23849, + 13850 => 23850, + 13851 => 23851, + 13852 => 23852, + 13853 => 23853, + 13854 => 23854, + 13855 => 23855, + 13856 => 23856, + 13857 => 23857, + 13858 => 23858, + 13859 => 23859, + 13860 => 23860, + 13861 => 23861, + 13862 => 23862, + 13863 => 23863, + 13864 => 23864, + 13865 => 23865, + 13866 => 23866, + 13867 => 23867, + 13868 => 23868, + 13869 => 23869, + 13870 => 23870, + 13871 => 23871, + 13872 => 23872, + 13873 => 23873, + 13874 => 23874, + 13875 => 23875, + 13876 => 23876, + 13877 => 23877, + 13878 => 23878, + 13879 => 23879, + 13880 => 23880, + 13881 => 23881, + 13882 => 23882, + 13883 => 23883, + 13884 => 23884, + 13885 => 23885, + 13886 => 23886, + 13887 => 23887, + 13888 => 23888, + 13889 => 23889, + 13890 => 23890, + 13891 => 23891, + 13892 => 23892, + 13893 => 23893, + 13894 => 23894, + 13895 => 23895, + 13896 => 23896, + 13897 => 23897, + 13898 => 23898, + 13899 => 23899, + 13900 => 23900, + 13901 => 23901, + 13902 => 23902, + 13903 => 23903, + 13904 => 23904, + 13905 => 23905, + 13906 => 23906, + 13907 => 23907, + 13908 => 23908, + 13909 => 23909, + 13910 => 23910, + 13911 => 23911, + 13912 => 23912, + 13913 => 23913, + 13914 => 23914, + 13915 => 23915, + 13916 => 23916, + 13917 => 23917, + 13918 => 23918, + 13919 => 23919, + 13920 => 23920, + 13921 => 23921, + 13922 => 23922, + 13923 => 23923, + 13924 => 23924, + 13925 => 23925, + 13926 => 23926, + 13927 => 23927, + 13928 => 23928, + 13929 => 23929, + 13930 => 23930, + 13931 => 23931, + 13932 => 23932, + 13933 => 23933, + 13934 => 23934, + 13935 => 23935, + 13936 => 23936, + 13937 => 23937, + 13938 => 23938, + 13939 => 23939, + 13940 => 23940, + 13941 => 23941, + 13942 => 23942, + 13943 => 23943, + 13944 => 23944, + 13945 => 23945, + 13946 => 23946, + 13947 => 23947, + 13948 => 23948, + 13949 => 23949, + 13950 => 23950, + 13951 => 23951, + 13952 => 23952, + 13953 => 23953, + 13954 => 23954, + 13955 => 23955, + 13956 => 23956, + 13957 => 23957, + 13958 => 23958, + 13959 => 23959, + 13960 => 23960, + 13961 => 23961, + 13962 => 23962, + 13963 => 23963, + 13964 => 23964, + 13965 => 23965, + 13966 => 23966, + 13967 => 23967, + 13968 => 23968, + 13969 => 23969, + 13970 => 23970, + 13971 => 23971, + 13972 => 23972, + 13973 => 23973, + 13974 => 23974, + 13975 => 23975, + 13976 => 23976, + 13977 => 23977, + 13978 => 23978, + 13979 => 23979, + 13980 => 23980, + 13981 => 23981, + 13982 => 23982, + 13983 => 23983, + 13984 => 23984, + 13985 => 23985, + 13986 => 23986, + 13987 => 23987, + 13988 => 23988, + 13989 => 23989, + 13990 => 23990, + 13991 => 23991, + 13992 => 23992, + 13993 => 23993, + 13994 => 23994, + 13995 => 23995, + 13996 => 23996, + 13997 => 23997, + 13998 => 23998, + 13999 => 23999, + 14000 => 24000, + 14001 => 24001, + 14002 => 24002, + 14003 => 24003, + 14004 => 24004, + 14005 => 24005, + 14006 => 24006, + 14007 => 24007, + 14008 => 24008, + 14009 => 24009, + 14010 => 24010, + 14011 => 24011, + 14012 => 24012, + 14013 => 24013, + 14014 => 24014, + 14015 => 24015, + 14016 => 24016, + 14017 => 24017, + 14018 => 24018, + 14019 => 24019, + 14020 => 24020, + 14021 => 24021, + 14022 => 24022, + 14023 => 24023, + 14024 => 24024, + 14025 => 24025, + 14026 => 24026, + 14027 => 24027, + 14028 => 24028, + 14029 => 24029, + 14030 => 24030, + 14031 => 24031, + 14032 => 24032, + 14033 => 24033, + 14034 => 24034, + 14035 => 24035, + 14036 => 24036, + 14037 => 24037, + 14038 => 24038, + 14039 => 24039, + 14040 => 24040, + 14041 => 24041, + 14042 => 24042, + 14043 => 24043, + 14044 => 24044, + 14045 => 24045, + 14046 => 24046, + 14047 => 24047, + 14048 => 24048, + 14049 => 24049, + 14050 => 24050, + 14051 => 24051, + 14052 => 24052, + 14053 => 24053, + 14054 => 24054, + 14055 => 24055, + 14056 => 24056, + 14057 => 24057, + 14058 => 24058, + 14059 => 24059, + 14060 => 24060, + 14061 => 24061, + 14062 => 24062, + 14063 => 24063, + 14064 => 24064, + 14065 => 24065, + 14066 => 24066, + 14067 => 24067, + 14068 => 24068, + 14069 => 24069, + 14070 => 24070, + 14071 => 24071, + 14072 => 24072, + 14073 => 24073, + 14074 => 24074, + 14075 => 24075, + 14076 => 24076, + 14077 => 24077, + 14078 => 24078, + 14079 => 24079, + 14080 => 24080, + 14081 => 24081, + 14082 => 24082, + 14083 => 24083, + 14084 => 24084, + 14085 => 24085, + 14086 => 24086, + 14087 => 24087, + 14088 => 24088, + 14089 => 24089, + 14090 => 24090, + 14091 => 24091, + 14092 => 24092, + 14093 => 24093, + 14094 => 24094, + 14095 => 24095, + 14096 => 24096, + 14097 => 24097, + 14098 => 24098, + 14099 => 24099, + 14100 => 24100, + 14101 => 24101, + 14102 => 24102, + 14103 => 24103, + 14104 => 24104, + 14105 => 24105, + 14106 => 24106, + 14107 => 24107, + 14108 => 24108, + 14109 => 24109, + 14110 => 24110, + 14111 => 24111, + 14112 => 24112, + 14113 => 24113, + 14114 => 24114, + 14115 => 24115, + 14116 => 24116, + 14117 => 24117, + 14118 => 24118, + 14119 => 24119, + 14120 => 24120, + 14121 => 24121, + 14122 => 24122, + 14123 => 24123, + 14124 => 24124, + 14125 => 24125, + 14126 => 24126, + 14127 => 24127, + 14128 => 24128, + 14129 => 24129, + 14130 => 24130, + 14131 => 24131, + 14132 => 24132, + 14133 => 24133, + 14134 => 24134, + 14135 => 24135, + 14136 => 24136, + 14137 => 24137, + 14138 => 24138, + 14139 => 24139, + 14140 => 24140, + 14141 => 24141, + 14142 => 24142, + 14143 => 24143, + 14144 => 24144, + 14145 => 24145, + 14146 => 24146, + 14147 => 24147, + 14148 => 24148, + 14149 => 24149, + 14150 => 24150, + 14151 => 24151, + 14152 => 24152, + 14153 => 24153, + 14154 => 24154, + 14155 => 24155, + 14156 => 24156, + 14157 => 24157, + 14158 => 24158, + 14159 => 24159, + 14160 => 24160, + 14161 => 24161, + 14162 => 24162, + 14163 => 24163, + 14164 => 24164, + 14165 => 24165, + 14166 => 24166, + 14167 => 24167, + 14168 => 24168, + 14169 => 24169, + 14170 => 24170, + 14171 => 24171, + 14172 => 24172, + 14173 => 24173, + 14174 => 24174, + 14175 => 24175, + 14176 => 24176, + 14177 => 24177, + 14178 => 24178, + 14179 => 24179, + 14180 => 24180, + 14181 => 24181, + 14182 => 24182, + 14183 => 24183, + 14184 => 24184, + 14185 => 24185, + 14186 => 24186, + 14187 => 24187, + 14188 => 24188, + 14189 => 24189, + 14190 => 24190, + 14191 => 24191, + 14192 => 24192, + 14193 => 24193, + 14194 => 24194, + 14195 => 24195, + 14196 => 24196, + 14197 => 24197, + 14198 => 24198, + 14199 => 24199, + 14200 => 24200, + 14201 => 24201, + 14202 => 24202, + 14203 => 24203, + 14204 => 24204, + 14205 => 24205, + 14206 => 24206, + 14207 => 24207, + 14208 => 24208, + 14209 => 24209, + 14210 => 24210, + 14211 => 24211, + 14212 => 24212, + 14213 => 24213, + 14214 => 24214, + 14215 => 24215, + 14216 => 24216, + 14217 => 24217, + 14218 => 24218, + 14219 => 24219, + 14220 => 24220, + 14221 => 24221, + 14222 => 24222, + 14223 => 24223, + 14224 => 24224, + 14225 => 24225, + 14226 => 24226, + 14227 => 24227, + 14228 => 24228, + 14229 => 24229, + 14230 => 24230, + 14231 => 24231, + 14232 => 24232, + 14233 => 24233, + 14234 => 24234, + 14235 => 24235, + 14236 => 24236, + 14237 => 24237, + 14238 => 24238, + 14239 => 24239, + 14240 => 24240, + 14241 => 24241, + 14242 => 24242, + 14243 => 24243, + 14244 => 24244, + 14245 => 24245, + 14246 => 24246, + 14247 => 24247, + 14248 => 24248, + 14249 => 24249, + 14250 => 24250, + 14251 => 24251, + 14252 => 24252, + 14253 => 24253, + 14254 => 24254, + 14255 => 24255, + 14256 => 24256, + 14257 => 24257, + 14258 => 24258, + 14259 => 24259, + 14260 => 24260, + 14261 => 24261, + 14262 => 24262, + 14263 => 24263, + 14264 => 24264, + 14265 => 24265, + 14266 => 24266, + 14267 => 24267, + 14268 => 24268, + 14269 => 24269, + 14270 => 24270, + 14271 => 24271, + 14272 => 24272, + 14273 => 24273, + 14274 => 24274, + 14275 => 24275, + 14276 => 24276, + 14277 => 24277, + 14278 => 24278, + 14279 => 24279, + 14280 => 24280, + 14281 => 24281, + 14282 => 24282, + 14283 => 24283, + 14284 => 24284, + 14285 => 24285, + 14286 => 24286, + 14287 => 24287, + 14288 => 24288, + 14289 => 24289, + 14290 => 24290, + 14291 => 24291, + 14292 => 24292, + 14293 => 24293, + 14294 => 24294, + 14295 => 24295, + 14296 => 24296, + 14297 => 24297, + 14298 => 24298, + 14299 => 24299, + 14300 => 24300, + 14301 => 24301, + 14302 => 24302, + 14303 => 24303, + 14304 => 24304, + 14305 => 24305, + 14306 => 24306, + 14307 => 24307, + 14308 => 24308, + 14309 => 24309, + 14310 => 24310, + 14311 => 24311, + 14312 => 24312, + 14313 => 24313, + 14314 => 24314, + 14315 => 24315, + 14316 => 24316, + 14317 => 24317, + 14318 => 24318, + 14319 => 24319, + 14320 => 24320, + 14321 => 24321, + 14322 => 24322, + 14323 => 24323, + 14324 => 24324, + 14325 => 24325, + 14326 => 24326, + 14327 => 24327, + 14328 => 24328, + 14329 => 24329, + 14330 => 24330, + 14331 => 24331, + 14332 => 24332, + 14333 => 24333, + 14334 => 24334, + 14335 => 24335, + 14336 => 24336, + 14337 => 24337, + 14338 => 24338, + 14339 => 24339, + 14340 => 24340, + 14341 => 24341, + 14342 => 24342, + 14343 => 24343, + 14344 => 24344, + 14345 => 24345, + 14346 => 24346, + 14347 => 24347, + 14348 => 24348, + 14349 => 24349, + 14350 => 24350, + 14351 => 24351, + 14352 => 24352, + 14353 => 24353, + 14354 => 24354, + 14355 => 24355, + 14356 => 24356, + 14357 => 24357, + 14358 => 24358, + 14359 => 24359, + 14360 => 24360, + 14361 => 24361, + 14362 => 24362, + 14363 => 24363, + 14364 => 24364, + 14365 => 24365, + 14366 => 24366, + 14367 => 24367, + 14368 => 24368, + 14369 => 24369, + 14370 => 24370, + 14371 => 24371, + 14372 => 24372, + 14373 => 24373, + 14374 => 24374, + 14375 => 24375, + 14376 => 24376, + 14377 => 24377, + 14378 => 24378, + 14379 => 24379, + 14380 => 24380, + 14381 => 24381, + 14382 => 24382, + 14383 => 24383, + 14384 => 24384, + 14385 => 24385, + 14386 => 24386, + 14387 => 24387, + 14388 => 24388, + 14389 => 24389, + 14390 => 24390, + 14391 => 24391, + 14392 => 24392, + 14393 => 24393, + 14394 => 24394, + 14395 => 24395, + 14396 => 24396, + 14397 => 24397, + 14398 => 24398, + 14399 => 24399, + 14400 => 24400, + 14401 => 24401, + 14402 => 24402, + 14403 => 24403, + 14404 => 24404, + 14405 => 24405, + 14406 => 24406, + 14407 => 24407, + 14408 => 24408, + 14409 => 24409, + 14410 => 24410, + 14411 => 24411, + 14412 => 24412, + 14413 => 24413, + 14414 => 24414, + 14415 => 24415, + 14416 => 24416, + 14417 => 24417, + 14418 => 24418, + 14419 => 24419, + 14420 => 24420, + 14421 => 24421, + 14422 => 24422, + 14423 => 24423, + 14424 => 24424, + 14425 => 24425, + 14426 => 24426, + 14427 => 24427, + 14428 => 24428, + 14429 => 24429, + 14430 => 24430, + 14431 => 24431, + 14432 => 24432, + 14433 => 24433, + 14434 => 24434, + 14435 => 24435, + 14436 => 24436, + 14437 => 24437, + 14438 => 24438, + 14439 => 24439, + 14440 => 24440, + 14441 => 24441, + 14442 => 24442, + 14443 => 24443, + 14444 => 24444, + 14445 => 24445, + 14446 => 24446, + 14447 => 24447, + 14448 => 24448, + 14449 => 24449, + 14450 => 24450, + 14451 => 24451, + 14452 => 24452, + 14453 => 24453, + 14454 => 24454, + 14455 => 24455, + 14456 => 24456, + 14457 => 24457, + 14458 => 24458, + 14459 => 24459, + 14460 => 24460, + 14461 => 24461, + 14462 => 24462, + 14463 => 24463, + 14464 => 24464, + 14465 => 24465, + 14466 => 24466, + 14467 => 24467, + 14468 => 24468, + 14469 => 24469, + 14470 => 24470, + 14471 => 24471, + 14472 => 24472, + 14473 => 24473, + 14474 => 24474, + 14475 => 24475, + 14476 => 24476, + 14477 => 24477, + 14478 => 24478, + 14479 => 24479, + 14480 => 24480, + 14481 => 24481, + 14482 => 24482, + 14483 => 24483, + 14484 => 24484, + 14485 => 24485, + 14486 => 24486, + 14487 => 24487, + 14488 => 24488, + 14489 => 24489, + 14490 => 24490, + 14491 => 24491, + 14492 => 24492, + 14493 => 24493, + 14494 => 24494, + 14495 => 24495, + 14496 => 24496, + 14497 => 24497, + 14498 => 24498, + 14499 => 24499, + 14500 => 24500, + 14501 => 24501, + 14502 => 24502, + 14503 => 24503, + 14504 => 24504, + 14505 => 24505, + 14506 => 24506, + 14507 => 24507, + 14508 => 24508, + 14509 => 24509, + 14510 => 24510, + 14511 => 24511, + 14512 => 24512, + 14513 => 24513, + 14514 => 24514, + 14515 => 24515, + 14516 => 24516, + 14517 => 24517, + 14518 => 24518, + 14519 => 24519, + 14520 => 24520, + 14521 => 24521, + 14522 => 24522, + 14523 => 24523, + 14524 => 24524, + 14525 => 24525, + 14526 => 24526, + 14527 => 24527, + 14528 => 24528, + 14529 => 24529, + 14530 => 24530, + 14531 => 24531, + 14532 => 24532, + 14533 => 24533, + 14534 => 24534, + 14535 => 24535, + 14536 => 24536, + 14537 => 24537, + 14538 => 24538, + 14539 => 24539, + 14540 => 24540, + 14541 => 24541, + 14542 => 24542, + 14543 => 24543, + 14544 => 24544, + 14545 => 24545, + 14546 => 24546, + 14547 => 24547, + 14548 => 24548, + 14549 => 24549, + 14550 => 24550, + 14551 => 24551, + 14552 => 24552, + 14553 => 24553, + 14554 => 24554, + 14555 => 24555, + 14556 => 24556, + 14557 => 24557, + 14558 => 24558, + 14559 => 24559, + 14560 => 24560, + 14561 => 24561, + 14562 => 24562, + 14563 => 24563, + 14564 => 24564, + 14565 => 24565, + 14566 => 24566, + 14567 => 24567, + 14568 => 24568, + 14569 => 24569, + 14570 => 24570, + 14571 => 24571, + 14572 => 24572, + 14573 => 24573, + 14574 => 24574, + 14575 => 24575, + 14576 => 24576, + 14577 => 24577, + 14578 => 24578, + 14579 => 24579, + 14580 => 24580, + 14581 => 24581, + 14582 => 24582, + 14583 => 24583, + 14584 => 24584, + 14585 => 24585, + 14586 => 24586, + 14587 => 24587, + 14588 => 24588, + 14589 => 24589, + 14590 => 24590, + 14591 => 24591, + 14592 => 24592, + 14593 => 24593, + 14594 => 24594, + 14595 => 24595, + 14596 => 24596, + 14597 => 24597, + 14598 => 24598, + 14599 => 24599, + 14600 => 24600, + 14601 => 24601, + 14602 => 24602, + 14603 => 24603, + 14604 => 24604, + 14605 => 24605, + 14606 => 24606, + 14607 => 24607, + 14608 => 24608, + 14609 => 24609, + 14610 => 24610, + 14611 => 24611, + 14612 => 24612, + 14613 => 24613, + 14614 => 24614, + 14615 => 24615, + 14616 => 24616, + 14617 => 24617, + 14618 => 24618, + 14619 => 24619, + 14620 => 24620, + 14621 => 24621, + 14622 => 24622, + 14623 => 24623, + 14624 => 24624, + 14625 => 24625, + 14626 => 24626, + 14627 => 24627, + 14628 => 24628, + 14629 => 24629, + 14630 => 24630, + 14631 => 24631, + 14632 => 24632, + 14633 => 24633, + 14634 => 24634, + 14635 => 24635, + 14636 => 24636, + 14637 => 24637, + 14638 => 24638, + 14639 => 24639, + 14640 => 24640, + 14641 => 24641, + 14642 => 24642, + 14643 => 24643, + 14644 => 24644, + 14645 => 24645, + 14646 => 24646, + 14647 => 24647, + 14648 => 24648, + 14649 => 24649, + 14650 => 24650, + 14651 => 24651, + 14652 => 24652, + 14653 => 24653, + 14654 => 24654, + 14655 => 24655, + 14656 => 24656, + 14657 => 24657, + 14658 => 24658, + 14659 => 24659, + 14660 => 24660, + 14661 => 24661, + 14662 => 24662, + 14663 => 24663, + 14664 => 24664, + 14665 => 24665, + 14666 => 24666, + 14667 => 24667, + 14668 => 24668, + 14669 => 24669, + 14670 => 24670, + 14671 => 24671, + 14672 => 24672, + 14673 => 24673, + 14674 => 24674, + 14675 => 24675, + 14676 => 24676, + 14677 => 24677, + 14678 => 24678, + 14679 => 24679, + 14680 => 24680, + 14681 => 24681, + 14682 => 24682, + 14683 => 24683, + 14684 => 24684, + 14685 => 24685, + 14686 => 24686, + 14687 => 24687, + 14688 => 24688, + 14689 => 24689, + 14690 => 24690, + 14691 => 24691, + 14692 => 24692, + 14693 => 24693, + 14694 => 24694, + 14695 => 24695, + 14696 => 24696, + 14697 => 24697, + 14698 => 24698, + 14699 => 24699, + 14700 => 24700, + 14701 => 24701, + 14702 => 24702, + 14703 => 24703, + 14704 => 24704, + 14705 => 24705, + 14706 => 24706, + 14707 => 24707, + 14708 => 24708, + 14709 => 24709, + 14710 => 24710, + 14711 => 24711, + 14712 => 24712, + 14713 => 24713, + 14714 => 24714, + 14715 => 24715, + 14716 => 24716, + 14717 => 24717, + 14718 => 24718, + 14719 => 24719, + 14720 => 24720, + 14721 => 24721, + 14722 => 24722, + 14723 => 24723, + 14724 => 24724, + 14725 => 24725, + 14726 => 24726, + 14727 => 24727, + 14728 => 24728, + 14729 => 24729, + 14730 => 24730, + 14731 => 24731, + 14732 => 24732, + 14733 => 24733, + 14734 => 24734, + 14735 => 24735, + 14736 => 24736, + 14737 => 24737, + 14738 => 24738, + 14739 => 24739, + 14740 => 24740, + 14741 => 24741, + 14742 => 24742, + 14743 => 24743, + 14744 => 24744, + 14745 => 24745, + 14746 => 24746, + 14747 => 24747, + 14748 => 24748, + 14749 => 24749, + 14750 => 24750, + 14751 => 24751, + 14752 => 24752, + 14753 => 24753, + 14754 => 24754, + 14755 => 24755, + 14756 => 24756, + 14757 => 24757, + 14758 => 24758, + 14759 => 24759, + 14760 => 24760, + 14761 => 24761, + 14762 => 24762, + 14763 => 24763, + 14764 => 24764, + 14765 => 24765, + 14766 => 24766, + 14767 => 24767, + 14768 => 24768, + 14769 => 24769, + 14770 => 24770, + 14771 => 24771, + 14772 => 24772, + 14773 => 24773, + 14774 => 24774, + 14775 => 24775, + 14776 => 24776, + 14777 => 24777, + 14778 => 24778, + 14779 => 24779, + 14780 => 24780, + 14781 => 24781, + 14782 => 24782, + 14783 => 24783, + 14784 => 24784, + 14785 => 24785, + 14786 => 24786, + 14787 => 24787, + 14788 => 24788, + 14789 => 24789, + 14790 => 24790, + 14791 => 24791, + 14792 => 24792, + 14793 => 24793, + 14794 => 24794, + 14795 => 24795, + 14796 => 24796, + 14797 => 24797, + 14798 => 24798, + 14799 => 24799, + 14800 => 24800, + 14801 => 24801, + 14802 => 24802, + 14803 => 24803, + 14804 => 24804, + 14805 => 24805, + 14806 => 24806, + 14807 => 24807, + 14808 => 24808, + 14809 => 24809, + 14810 => 24810, + 14811 => 24811, + 14812 => 24812, + 14813 => 24813, + 14814 => 24814, + 14815 => 24815, + 14816 => 24816, + 14817 => 24817, + 14818 => 24818, + 14819 => 24819, + 14820 => 24820, + 14821 => 24821, + 14822 => 24822, + 14823 => 24823, + 14824 => 24824, + 14825 => 24825, + 14826 => 24826, + 14827 => 24827, + 14828 => 24828, + 14829 => 24829, + 14830 => 24830, + 14831 => 24831, + 14832 => 24832, + 14833 => 24833, + 14834 => 24834, + 14835 => 24835, + 14836 => 24836, + 14837 => 24837, + 14838 => 24838, + 14839 => 24839, + 14840 => 24840, + 14841 => 24841, + 14842 => 24842, + 14843 => 24843, + 14844 => 24844, + 14845 => 24845, + 14846 => 24846, + 14847 => 24847, + 14848 => 24848, + 14849 => 24849, + 14850 => 24850, + 14851 => 24851, + 14852 => 24852, + 14853 => 24853, + 14854 => 24854, + 14855 => 24855, + 14856 => 24856, + 14857 => 24857, + 14858 => 24858, + 14859 => 24859, + 14860 => 24860, + 14861 => 24861, + 14862 => 24862, + 14863 => 24863, + 14864 => 24864, + 14865 => 24865, + 14866 => 24866, + 14867 => 24867, + 14868 => 24868, + 14869 => 24869, + 14870 => 24870, + 14871 => 24871, + 14872 => 24872, + 14873 => 24873, + 14874 => 24874, + 14875 => 24875, + 14876 => 24876, + 14877 => 24877, + 14878 => 24878, + 14879 => 24879, + 14880 => 24880, + 14881 => 24881, + 14882 => 24882, + 14883 => 24883, + 14884 => 24884, + 14885 => 24885, + 14886 => 24886, + 14887 => 24887, + 14888 => 24888, + 14889 => 24889, + 14890 => 24890, + 14891 => 24891, + 14892 => 24892, + 14893 => 24893, + 14894 => 24894, + 14895 => 24895, + 14896 => 24896, + 14897 => 24897, + 14898 => 24898, + 14899 => 24899, + 14900 => 24900, + 14901 => 24901, + 14902 => 24902, + 14903 => 24903, + 14904 => 24904, + 14905 => 24905, + 14906 => 24906, + 14907 => 24907, + 14908 => 24908, + 14909 => 24909, + 14910 => 24910, + 14911 => 24911, + 14912 => 24912, + 14913 => 24913, + 14914 => 24914, + 14915 => 24915, + 14916 => 24916, + 14917 => 24917, + 14918 => 24918, + 14919 => 24919, + 14920 => 24920, + 14921 => 24921, + 14922 => 24922, + 14923 => 24923, + 14924 => 24924, + 14925 => 24925, + 14926 => 24926, + 14927 => 24927, + 14928 => 24928, + 14929 => 24929, + 14930 => 24930, + 14931 => 24931, + 14932 => 24932, + 14933 => 24933, + 14934 => 24934, + 14935 => 24935, + 14936 => 24936, + 14937 => 24937, + 14938 => 24938, + 14939 => 24939, + 14940 => 24940, + 14941 => 24941, + 14942 => 24942, + 14943 => 24943, + 14944 => 24944, + 14945 => 24945, + 14946 => 24946, + 14947 => 24947, + 14948 => 24948, + 14949 => 24949, + 14950 => 24950, + 14951 => 24951, + 14952 => 24952, + 14953 => 24953, + 14954 => 24954, + 14955 => 24955, + 14956 => 24956, + 14957 => 24957, + 14958 => 24958, + 14959 => 24959, + 14960 => 24960, + 14961 => 24961, + 14962 => 24962, + 14963 => 24963, + 14964 => 24964, + 14965 => 24965, + 14966 => 24966, + 14967 => 24967, + 14968 => 24968, + 14969 => 24969, + 14970 => 24970, + 14971 => 24971, + 14972 => 24972, + 14973 => 24973, + 14974 => 24974, + 14975 => 24975, + 14976 => 24976, + 14977 => 24977, + 14978 => 24978, + 14979 => 24979, + 14980 => 24980, + 14981 => 24981, + 14982 => 24982, + 14983 => 24983, + 14984 => 24984, + 14985 => 24985, + 14986 => 24986, + 14987 => 24987, + 14988 => 24988, + 14989 => 24989, + 14990 => 24990, + 14991 => 24991, + 14992 => 24992, + 14993 => 24993, + 14994 => 24994, + 14995 => 24995, + 14996 => 24996, + 14997 => 24997, + 14998 => 24998, + 14999 => 24999, + 15000 => 25000, + 15001 => 25001, + 15002 => 25002, + 15003 => 25003, + 15004 => 25004, + 15005 => 25005, + 15006 => 25006, + 15007 => 25007, + 15008 => 25008, + 15009 => 25009, + 15010 => 25010, + 15011 => 25011, + 15012 => 25012, + 15013 => 25013, + 15014 => 25014, + 15015 => 25015, + 15016 => 25016, + 15017 => 25017, + 15018 => 25018, + 15019 => 25019, + 15020 => 25020, + 15021 => 25021, + 15022 => 25022, + 15023 => 25023, + 15024 => 25024, + 15025 => 25025, + 15026 => 25026, + 15027 => 25027, + 15028 => 25028, + 15029 => 25029, + 15030 => 25030, + 15031 => 25031, + 15032 => 25032, + 15033 => 25033, + 15034 => 25034, + 15035 => 25035, + 15036 => 25036, + 15037 => 25037, + 15038 => 25038, + 15039 => 25039, + 15040 => 25040, + 15041 => 25041, + 15042 => 25042, + 15043 => 25043, + 15044 => 25044, + 15045 => 25045, + 15046 => 25046, + 15047 => 25047, + 15048 => 25048, + 15049 => 25049, + 15050 => 25050, + 15051 => 25051, + 15052 => 25052, + 15053 => 25053, + 15054 => 25054, + 15055 => 25055, + 15056 => 25056, + 15057 => 25057, + 15058 => 25058, + 15059 => 25059, + 15060 => 25060, + 15061 => 25061, + 15062 => 25062, + 15063 => 25063, + 15064 => 25064, + 15065 => 25065, + 15066 => 25066, + 15067 => 25067, + 15068 => 25068, + 15069 => 25069, + 15070 => 25070, + 15071 => 25071, + 15072 => 25072, + 15073 => 25073, + 15074 => 25074, + 15075 => 25075, + 15076 => 25076, + 15077 => 25077, + 15078 => 25078, + 15079 => 25079, + 15080 => 25080, + 15081 => 25081, + 15082 => 25082, + 15083 => 25083, + 15084 => 25084, + 15085 => 25085, + 15086 => 25086, + 15087 => 25087, + 15088 => 25088, + 15089 => 25089, + 15090 => 25090, + 15091 => 25091, + 15092 => 25092, + 15093 => 25093, + 15094 => 25094, + 15095 => 25095, + 15096 => 25096, + 15097 => 25097, + 15098 => 25098, + 15099 => 25099, + 15100 => 25100, + 15101 => 25101, + 15102 => 25102, + 15103 => 25103, + 15104 => 25104, + 15105 => 25105, + 15106 => 25106, + 15107 => 25107, + 15108 => 25108, + 15109 => 25109, + 15110 => 25110, + 15111 => 25111, + 15112 => 25112, + 15113 => 25113, + 15114 => 25114, + 15115 => 25115, + 15116 => 25116, + 15117 => 25117, + 15118 => 25118, + 15119 => 25119, + 15120 => 25120, + 15121 => 25121, + 15122 => 25122, + 15123 => 25123, + 15124 => 25124, + 15125 => 25125, + 15126 => 25126, + 15127 => 25127, + 15128 => 25128, + 15129 => 25129, + 15130 => 25130, + 15131 => 25131, + 15132 => 25132, + 15133 => 25133, + 15134 => 25134, + 15135 => 25135, + 15136 => 25136, + 15137 => 25137, + 15138 => 25138, + 15139 => 25139, + 15140 => 25140, + 15141 => 25141, + 15142 => 25142, + 15143 => 25143, + 15144 => 25144, + 15145 => 25145, + 15146 => 25146, + 15147 => 25147, + 15148 => 25148, + 15149 => 25149, + 15150 => 25150, + 15151 => 25151, + 15152 => 25152, + 15153 => 25153, + 15154 => 25154, + 15155 => 25155, + 15156 => 25156, + 15157 => 25157, + 15158 => 25158, + 15159 => 25159, + 15160 => 25160, + 15161 => 25161, + 15162 => 25162, + 15163 => 25163, + 15164 => 25164, + 15165 => 25165, + 15166 => 25166, + 15167 => 25167, + 15168 => 25168, + 15169 => 25169, + 15170 => 25170, + 15171 => 25171, + 15172 => 25172, + 15173 => 25173, + 15174 => 25174, + 15175 => 25175, + 15176 => 25176, + 15177 => 25177, + 15178 => 25178, + 15179 => 25179, + 15180 => 25180, + 15181 => 25181, + 15182 => 25182, + 15183 => 25183, + 15184 => 25184, + 15185 => 25185, + 15186 => 25186, + 15187 => 25187, + 15188 => 25188, + 15189 => 25189, + 15190 => 25190, + 15191 => 25191, + 15192 => 25192, + 15193 => 25193, + 15194 => 25194, + 15195 => 25195, + 15196 => 25196, + 15197 => 25197, + 15198 => 25198, + 15199 => 25199, + 15200 => 25200, + 15201 => 25201, + 15202 => 25202, + 15203 => 25203, + 15204 => 25204, + 15205 => 25205, + 15206 => 25206, + 15207 => 25207, + 15208 => 25208, + 15209 => 25209, + 15210 => 25210, + 15211 => 25211, + 15212 => 25212, + 15213 => 25213, + 15214 => 25214, + 15215 => 25215, + 15216 => 25216, + 15217 => 25217, + 15218 => 25218, + 15219 => 25219, + 15220 => 25220, + 15221 => 25221, + 15222 => 25222, + 15223 => 25223, + 15224 => 25224, + 15225 => 25225, + 15226 => 25226, + 15227 => 25227, + 15228 => 25228, + 15229 => 25229, + 15230 => 25230, + 15231 => 25231, + 15232 => 25232, + 15233 => 25233, + 15234 => 25234, + 15235 => 25235, + 15236 => 25236, + 15237 => 25237, + 15238 => 25238, + 15239 => 25239, + 15240 => 25240, + 15241 => 25241, + 15242 => 25242, + 15243 => 25243, + 15244 => 25244, + 15245 => 25245, + 15246 => 25246, + 15247 => 25247, + 15248 => 25248, + 15249 => 25249, + 15250 => 25250, + 15251 => 25251, + 15252 => 25252, + 15253 => 25253, + 15254 => 25254, + 15255 => 25255, + 15256 => 25256, + 15257 => 25257, + 15258 => 25258, + 15259 => 25259, + 15260 => 25260, + 15261 => 25261, + 15262 => 25262, + 15263 => 25263, + 15264 => 25264, + 15265 => 25265, + 15266 => 25266, + 15267 => 25267, + 15268 => 25268, + 15269 => 25269, + 15270 => 25270, + 15271 => 25271, + 15272 => 25272, + 15273 => 25273, + 15274 => 25274, + 15275 => 25275, + 15276 => 25276, + 15277 => 25277, + 15278 => 25278, + 15279 => 25279, + 15280 => 25280, + 15281 => 25281, + 15282 => 25282, + 15283 => 25283, + 15284 => 25284, + 15285 => 25285, + 15286 => 25286, + 15287 => 25287, + 15288 => 25288, + 15289 => 25289, + 15290 => 25290, + 15291 => 25291, + 15292 => 25292, + 15293 => 25293, + 15294 => 25294, + 15295 => 25295, + 15296 => 25296, + 15297 => 25297, + 15298 => 25298, + 15299 => 25299, + 15300 => 25300, + 15301 => 25301, + 15302 => 25302, + 15303 => 25303, + 15304 => 25304, + 15305 => 25305, + 15306 => 25306, + 15307 => 25307, + 15308 => 25308, + 15309 => 25309, + 15310 => 25310, + 15311 => 25311, + 15312 => 25312, + 15313 => 25313, + 15314 => 25314, + 15315 => 25315, + 15316 => 25316, + 15317 => 25317, + 15318 => 25318, + 15319 => 25319, + 15320 => 25320, + 15321 => 25321, + 15322 => 25322, + 15323 => 25323, + 15324 => 25324, + 15325 => 25325, + 15326 => 25326, + 15327 => 25327, + 15328 => 25328, + 15329 => 25329, + 15330 => 25330, + 15331 => 25331, + 15332 => 25332, + 15333 => 25333, + 15334 => 25334, + 15335 => 25335, + 15336 => 25336, + 15337 => 25337, + 15338 => 25338, + 15339 => 25339, + 15340 => 25340, + 15341 => 25341, + 15342 => 25342, + 15343 => 25343, + 15344 => 25344, + 15345 => 25345, + 15346 => 25346, + 15347 => 25347, + 15348 => 25348, + 15349 => 25349, + 15350 => 25350, + 15351 => 25351, + 15352 => 25352, + 15353 => 25353, + 15354 => 25354, + 15355 => 25355, + 15356 => 25356, + 15357 => 25357, + 15358 => 25358, + 15359 => 25359, + 15360 => 25360, + 15361 => 25361, + 15362 => 25362, + 15363 => 25363, + 15364 => 25364, + 15365 => 25365, + 15366 => 25366, + 15367 => 25367, + 15368 => 25368, + 15369 => 25369, + 15370 => 25370, + 15371 => 25371, + 15372 => 25372, + 15373 => 25373, + 15374 => 25374, + 15375 => 25375, + 15376 => 25376, + 15377 => 25377, + 15378 => 25378, + 15379 => 25379, + 15380 => 25380, + 15381 => 25381, + 15382 => 25382, + 15383 => 25383, + 15384 => 25384, + 15385 => 25385, + 15386 => 25386, + 15387 => 25387, + 15388 => 25388, + 15389 => 25389, + 15390 => 25390, + 15391 => 25391, + 15392 => 25392, + 15393 => 25393, + 15394 => 25394, + 15395 => 25395, + 15396 => 25396, + 15397 => 25397, + 15398 => 25398, + 15399 => 25399, + 15400 => 25400, + 15401 => 25401, + 15402 => 25402, + 15403 => 25403, + 15404 => 25404, + 15405 => 25405, + 15406 => 25406, + 15407 => 25407, + 15408 => 25408, + 15409 => 25409, + 15410 => 25410, + 15411 => 25411, + 15412 => 25412, + 15413 => 25413, + 15414 => 25414, + 15415 => 25415, + 15416 => 25416, + 15417 => 25417, + 15418 => 25418, + 15419 => 25419, + 15420 => 25420, + 15421 => 25421, + 15422 => 25422, + 15423 => 25423, + 15424 => 25424, + 15425 => 25425, + 15426 => 25426, + 15427 => 25427, + 15428 => 25428, + 15429 => 25429, + 15430 => 25430, + 15431 => 25431, + 15432 => 25432, + 15433 => 25433, + 15434 => 25434, + 15435 => 25435, + 15436 => 25436, + 15437 => 25437, + 15438 => 25438, + 15439 => 25439, + 15440 => 25440, + 15441 => 25441, + 15442 => 25442, + 15443 => 25443, + 15444 => 25444, + 15445 => 25445, + 15446 => 25446, + 15447 => 25447, + 15448 => 25448, + 15449 => 25449, + 15450 => 25450, + 15451 => 25451, + 15452 => 25452, + 15453 => 25453, + 15454 => 25454, + 15455 => 25455, + 15456 => 25456, + 15457 => 25457, + 15458 => 25458, + 15459 => 25459, + 15460 => 25460, + 15461 => 25461, + 15462 => 25462, + 15463 => 25463, + 15464 => 25464, + 15465 => 25465, + 15466 => 25466, + 15467 => 25467, + 15468 => 25468, + 15469 => 25469, + 15470 => 25470, + 15471 => 25471, + 15472 => 25472, + 15473 => 25473, + 15474 => 25474, + 15475 => 25475, + 15476 => 25476, + 15477 => 25477, + 15478 => 25478, + 15479 => 25479, + 15480 => 25480, + 15481 => 25481, + 15482 => 25482, + 15483 => 25483, + 15484 => 25484, + 15485 => 25485, + 15486 => 25486, + 15487 => 25487, + 15488 => 25488, + 15489 => 25489, + 15490 => 25490, + 15491 => 25491, + 15492 => 25492, + 15493 => 25493, + 15494 => 25494, + 15495 => 25495, + 15496 => 25496, + 15497 => 25497, + 15498 => 25498, + 15499 => 25499, + 15500 => 25500, + 15501 => 25501, + 15502 => 25502, + 15503 => 25503, + 15504 => 25504, + 15505 => 25505, + 15506 => 25506, + 15507 => 25507, + 15508 => 25508, + 15509 => 25509, + 15510 => 25510, + 15511 => 25511, + 15512 => 25512, + 15513 => 25513, + 15514 => 25514, + 15515 => 25515, + 15516 => 25516, + 15517 => 25517, + 15518 => 25518, + 15519 => 25519, + 15520 => 25520, + 15521 => 25521, + 15522 => 25522, + 15523 => 25523, + 15524 => 25524, + 15525 => 25525, + 15526 => 25526, + 15527 => 25527, + 15528 => 25528, + 15529 => 25529, + 15530 => 25530, + 15531 => 25531, + 15532 => 25532, + 15533 => 25533, + 15534 => 25534, + 15535 => 25535, + 15536 => 25536, + 15537 => 25537, + 15538 => 25538, + 15539 => 25539, + 15540 => 25540, + 15541 => 25541, + 15542 => 25542, + 15543 => 25543, + 15544 => 25544, + 15545 => 25545, + 15546 => 25546, + 15547 => 25547, + 15548 => 25548, + 15549 => 25549, + 15550 => 25550, + 15551 => 25551, + 15552 => 25552, + 15553 => 25553, + 15554 => 25554, + 15555 => 25555, + 15556 => 25556, + 15557 => 25557, + 15558 => 25558, + 15559 => 25559, + 15560 => 25560, + 15561 => 25561, + 15562 => 25562, + 15563 => 25563, + 15564 => 25564, + 15565 => 25565, + 15566 => 25566, + 15567 => 25567, + 15568 => 25568, + 15569 => 25569, + 15570 => 25570, + 15571 => 25571, + 15572 => 25572, + 15573 => 25573, + 15574 => 25574, + 15575 => 25575, + 15576 => 25576, + 15577 => 25577, + 15578 => 25578, + 15579 => 25579, + 15580 => 25580, + 15581 => 25581, + 15582 => 25582, + 15583 => 25583, + 15584 => 25584, + 15585 => 25585, + 15586 => 25586, + 15587 => 25587, + 15588 => 25588, + 15589 => 25589, + 15590 => 25590, + 15591 => 25591, + 15592 => 25592, + 15593 => 25593, + 15594 => 25594, + 15595 => 25595, + 15596 => 25596, + 15597 => 25597, + 15598 => 25598, + 15599 => 25599, + 15600 => 25600, + 15601 => 25601, + 15602 => 25602, + 15603 => 25603, + 15604 => 25604, + 15605 => 25605, + 15606 => 25606, + 15607 => 25607, + 15608 => 25608, + 15609 => 25609, + 15610 => 25610, + 15611 => 25611, + 15612 => 25612, + 15613 => 25613, + 15614 => 25614, + 15615 => 25615, + 15616 => 25616, + 15617 => 25617, + 15618 => 25618, + 15619 => 25619, + 15620 => 25620, + 15621 => 25621, + 15622 => 25622, + 15623 => 25623, + 15624 => 25624, + 15625 => 25625, + 15626 => 25626, + 15627 => 25627, + 15628 => 25628, + 15629 => 25629, + 15630 => 25630, + 15631 => 25631, + 15632 => 25632, + 15633 => 25633, + 15634 => 25634, + 15635 => 25635, + 15636 => 25636, + 15637 => 25637, + 15638 => 25638, + 15639 => 25639, + 15640 => 25640, + 15641 => 25641, + 15642 => 25642, + 15643 => 25643, + 15644 => 25644, + 15645 => 25645, + 15646 => 25646, + 15647 => 25647, + 15648 => 25648, + 15649 => 25649, + 15650 => 25650, + 15651 => 25651, + 15652 => 25652, + 15653 => 25653, + 15654 => 25654, + 15655 => 25655, + 15656 => 25656, + 15657 => 25657, + 15658 => 25658, + 15659 => 25659, + 15660 => 25660, + 15661 => 25661, + 15662 => 25662, + 15663 => 25663, + 15664 => 25664, + 15665 => 25665, + 15666 => 25666, + 15667 => 25667, + 15668 => 25668, + 15669 => 25669, + 15670 => 25670, + 15671 => 25671, + 15672 => 25672, + 15673 => 25673, + 15674 => 25674, + 15675 => 25675, + 15676 => 25676, + 15677 => 25677, + 15678 => 25678, + 15679 => 25679, + 15680 => 25680, + 15681 => 25681, + 15682 => 25682, + 15683 => 25683, + 15684 => 25684, + 15685 => 25685, + 15686 => 25686, + 15687 => 25687, + 15688 => 25688, + 15689 => 25689, + 15690 => 25690, + 15691 => 25691, + 15692 => 25692, + 15693 => 25693, + 15694 => 25694, + 15695 => 25695, + 15696 => 25696, + 15697 => 25697, + 15698 => 25698, + 15699 => 25699, + 15700 => 25700, + 15701 => 25701, + 15702 => 25702, + 15703 => 25703, + 15704 => 25704, + 15705 => 25705, + 15706 => 25706, + 15707 => 25707, + 15708 => 25708, + 15709 => 25709, + 15710 => 25710, + 15711 => 25711, + 15712 => 25712, + 15713 => 25713, + 15714 => 25714, + 15715 => 25715, + 15716 => 25716, + 15717 => 25717, + 15718 => 25718, + 15719 => 25719, + 15720 => 25720, + 15721 => 25721, + 15722 => 25722, + 15723 => 25723, + 15724 => 25724, + 15725 => 25725, + 15726 => 25726, + 15727 => 25727, + 15728 => 25728, + 15729 => 25729, + 15730 => 25730, + 15731 => 25731, + 15732 => 25732, + 15733 => 25733, + 15734 => 25734, + 15735 => 25735, + 15736 => 25736, + 15737 => 25737, + 15738 => 25738, + 15739 => 25739, + 15740 => 25740, + 15741 => 25741, + 15742 => 25742, + 15743 => 25743, + 15744 => 25744, + 15745 => 25745, + 15746 => 25746, + 15747 => 25747, + 15748 => 25748, + 15749 => 25749, + 15750 => 25750, + 15751 => 25751, + 15752 => 25752, + 15753 => 25753, + 15754 => 25754, + 15755 => 25755, + 15756 => 25756, + 15757 => 25757, + 15758 => 25758, + 15759 => 25759, + 15760 => 25760, + 15761 => 25761, + 15762 => 25762, + 15763 => 25763, + 15764 => 25764, + 15765 => 25765, + 15766 => 25766, + 15767 => 25767, + 15768 => 25768, + 15769 => 25769, + 15770 => 25770, + 15771 => 25771, + 15772 => 25772, + 15773 => 25773, + 15774 => 25774, + 15775 => 25775, + 15776 => 25776, + 15777 => 25777, + 15778 => 25778, + 15779 => 25779, + 15780 => 25780, + 15781 => 25781, + 15782 => 25782, + 15783 => 25783, + 15784 => 25784, + 15785 => 25785, + 15786 => 25786, + 15787 => 25787, + 15788 => 25788, + 15789 => 25789, + 15790 => 25790, + 15791 => 25791, + 15792 => 25792, + 15793 => 25793, + 15794 => 25794, + 15795 => 25795, + 15796 => 25796, + 15797 => 25797, + 15798 => 25798, + 15799 => 25799, + 15800 => 25800, + 15801 => 25801, + 15802 => 25802, + 15803 => 25803, + 15804 => 25804, + 15805 => 25805, + 15806 => 25806, + 15807 => 25807, + 15808 => 25808, + 15809 => 25809, + 15810 => 25810, + 15811 => 25811, + 15812 => 25812, + 15813 => 25813, + 15814 => 25814, + 15815 => 25815, + 15816 => 25816, + 15817 => 25817, + 15818 => 25818, + 15819 => 25819, + 15820 => 25820, + 15821 => 25821, + 15822 => 25822, + 15823 => 25823, + 15824 => 25824, + 15825 => 25825, + 15826 => 25826, + 15827 => 25827, + 15828 => 25828, + 15829 => 25829, + 15830 => 25830, + 15831 => 25831, + 15832 => 25832, + 15833 => 25833, + 15834 => 25834, + 15835 => 25835, + 15836 => 25836, + 15837 => 25837, + 15838 => 25838, + 15839 => 25839, + 15840 => 25840, + 15841 => 25841, + 15842 => 25842, + 15843 => 25843, + 15844 => 25844, + 15845 => 25845, + 15846 => 25846, + 15847 => 25847, + 15848 => 25848, + 15849 => 25849, + 15850 => 25850, + 15851 => 25851, + 15852 => 25852, + 15853 => 25853, + 15854 => 25854, + 15855 => 25855, + 15856 => 25856, + 15857 => 25857, + 15858 => 25858, + 15859 => 25859, + 15860 => 25860, + 15861 => 25861, + 15862 => 25862, + 15863 => 25863, + 15864 => 25864, + 15865 => 25865, + 15866 => 25866, + 15867 => 25867, + 15868 => 25868, + 15869 => 25869, + 15870 => 25870, + 15871 => 25871, + 15872 => 25872, + 15873 => 25873, + 15874 => 25874, + 15875 => 25875, + 15876 => 25876, + 15877 => 25877, + 15878 => 25878, + 15879 => 25879, + 15880 => 25880, + 15881 => 25881, + 15882 => 25882, + 15883 => 25883, + 15884 => 25884, + 15885 => 25885, + 15886 => 25886, + 15887 => 25887, + 15888 => 25888, + 15889 => 25889, + 15890 => 25890, + 15891 => 25891, + 15892 => 25892, + 15893 => 25893, + 15894 => 25894, + 15895 => 25895, + 15896 => 25896, + 15897 => 25897, + 15898 => 25898, + 15899 => 25899, + 15900 => 25900, + 15901 => 25901, + 15902 => 25902, + 15903 => 25903, + 15904 => 25904, + 15905 => 25905, + 15906 => 25906, + 15907 => 25907, + 15908 => 25908, + 15909 => 25909, + 15910 => 25910, + 15911 => 25911, + 15912 => 25912, + 15913 => 25913, + 15914 => 25914, + 15915 => 25915, + 15916 => 25916, + 15917 => 25917, + 15918 => 25918, + 15919 => 25919, + 15920 => 25920, + 15921 => 25921, + 15922 => 25922, + 15923 => 25923, + 15924 => 25924, + 15925 => 25925, + 15926 => 25926, + 15927 => 25927, + 15928 => 25928, + 15929 => 25929, + 15930 => 25930, + 15931 => 25931, + 15932 => 25932, + 15933 => 25933, + 15934 => 25934, + 15935 => 25935, + 15936 => 25936, + 15937 => 25937, + 15938 => 25938, + 15939 => 25939, + 15940 => 25940, + 15941 => 25941, + 15942 => 25942, + 15943 => 25943, + 15944 => 25944, + 15945 => 25945, + 15946 => 25946, + 15947 => 25947, + 15948 => 25948, + 15949 => 25949, + 15950 => 25950, + 15951 => 25951, + 15952 => 25952, + 15953 => 25953, + 15954 => 25954, + 15955 => 25955, + 15956 => 25956, + 15957 => 25957, + 15958 => 25958, + 15959 => 25959, + 15960 => 25960, + 15961 => 25961, + 15962 => 25962, + 15963 => 25963, + 15964 => 25964, + 15965 => 25965, + 15966 => 25966, + 15967 => 25967, + 15968 => 25968, + 15969 => 25969, + 15970 => 25970, + 15971 => 25971, + 15972 => 25972, + 15973 => 25973, + 15974 => 25974, + 15975 => 25975, + 15976 => 25976, + 15977 => 25977, + 15978 => 25978, + 15979 => 25979, + 15980 => 25980, + 15981 => 25981, + 15982 => 25982, + 15983 => 25983, + 15984 => 25984, + 15985 => 25985, + 15986 => 25986, + 15987 => 25987, + 15988 => 25988, + 15989 => 25989, + 15990 => 25990, + 15991 => 25991, + 15992 => 25992, + 15993 => 25993, + 15994 => 25994, + 15995 => 25995, + 15996 => 25996, + 15997 => 25997, + 15998 => 25998, + 15999 => 25999, + 16000 => 26000, + 16001 => 26001, + 16002 => 26002, + 16003 => 26003, + 16004 => 26004, + 16005 => 26005, + 16006 => 26006, + 16007 => 26007, + 16008 => 26008, + 16009 => 26009, + 16010 => 26010, + 16011 => 26011, + 16012 => 26012, + 16013 => 26013, + 16014 => 26014, + 16015 => 26015, + 16016 => 26016, + 16017 => 26017, + 16018 => 26018, + 16019 => 26019, + 16020 => 26020, + 16021 => 26021, + 16022 => 26022, + 16023 => 26023, + 16024 => 26024, + 16025 => 26025, + 16026 => 26026, + 16027 => 26027, + 16028 => 26028, + 16029 => 26029, + 16030 => 26030, + 16031 => 26031, + 16032 => 26032, + 16033 => 26033, + 16034 => 26034, + 16035 => 26035, + 16036 => 26036, + 16037 => 26037, + 16038 => 26038, + 16039 => 26039, + 16040 => 26040, + 16041 => 26041, + 16042 => 26042, + 16043 => 26043, + 16044 => 26044, + 16045 => 26045, + 16046 => 26046, + 16047 => 26047, + 16048 => 26048, + 16049 => 26049, + 16050 => 26050, + 16051 => 26051, + 16052 => 26052, + 16053 => 26053, + 16054 => 26054, + 16055 => 26055, + 16056 => 26056, + 16057 => 26057, + 16058 => 26058, + 16059 => 26059, + 16060 => 26060, + 16061 => 26061, + 16062 => 26062, + 16063 => 26063, + 16064 => 26064, + 16065 => 26065, + 16066 => 26066, + 16067 => 26067, + 16068 => 26068, + 16069 => 26069, + 16070 => 26070, + 16071 => 26071, + 16072 => 26072, + 16073 => 26073, + 16074 => 26074, + 16075 => 26075, + 16076 => 26076, + 16077 => 26077, + 16078 => 26078, + 16079 => 26079, + 16080 => 26080, + 16081 => 26081, + 16082 => 26082, + 16083 => 26083, + 16084 => 26084, + 16085 => 26085, + 16086 => 26086, + 16087 => 26087, + 16088 => 26088, + 16089 => 26089, + 16090 => 26090, + 16091 => 26091, + 16092 => 26092, + 16093 => 26093, + 16094 => 26094, + 16095 => 26095, + 16096 => 26096, + 16097 => 26097, + 16098 => 26098, + 16099 => 26099, + 16100 => 26100, + 16101 => 26101, + 16102 => 26102, + 16103 => 26103, + 16104 => 26104, + 16105 => 26105, + 16106 => 26106, + 16107 => 26107, + 16108 => 26108, + 16109 => 26109, + 16110 => 26110, + 16111 => 26111, + 16112 => 26112, + 16113 => 26113, + 16114 => 26114, + 16115 => 26115, + 16116 => 26116, + 16117 => 26117, + 16118 => 26118, + 16119 => 26119, + 16120 => 26120, + 16121 => 26121, + 16122 => 26122, + 16123 => 26123, + 16124 => 26124, + 16125 => 26125, + 16126 => 26126, + 16127 => 26127, + 16128 => 26128, + 16129 => 26129, + 16130 => 26130, + 16131 => 26131, + 16132 => 26132, + 16133 => 26133, + 16134 => 26134, + 16135 => 26135, + 16136 => 26136, + 16137 => 26137, + 16138 => 26138, + 16139 => 26139, + 16140 => 26140, + 16141 => 26141, + 16142 => 26142, + 16143 => 26143, + 16144 => 26144, + 16145 => 26145, + 16146 => 26146, + 16147 => 26147, + 16148 => 26148, + 16149 => 26149, + 16150 => 26150, + 16151 => 26151, + 16152 => 26152, + 16153 => 26153, + 16154 => 26154, + 16155 => 26155, + 16156 => 26156, + 16157 => 26157, + 16158 => 26158, + 16159 => 26159, + 16160 => 26160, + 16161 => 26161, + 16162 => 26162, + 16163 => 26163, + 16164 => 26164, + 16165 => 26165, + 16166 => 26166, + 16167 => 26167, + 16168 => 26168, + 16169 => 26169, + 16170 => 26170, + 16171 => 26171, + 16172 => 26172, + 16173 => 26173, + 16174 => 26174, + 16175 => 26175, + 16176 => 26176, + 16177 => 26177, + 16178 => 26178, + 16179 => 26179, + 16180 => 26180, + 16181 => 26181, + 16182 => 26182, + 16183 => 26183, + 16184 => 26184, + 16185 => 26185, + 16186 => 26186, + 16187 => 26187, + 16188 => 26188, + 16189 => 26189, + 16190 => 26190, + 16191 => 26191, + 16192 => 26192, + 16193 => 26193, + 16194 => 26194, + 16195 => 26195, + 16196 => 26196, + 16197 => 26197, + 16198 => 26198, + 16199 => 26199, + 16200 => 26200, + 16201 => 26201, + 16202 => 26202, + 16203 => 26203, + 16204 => 26204, + 16205 => 26205, + 16206 => 26206, + 16207 => 26207, + 16208 => 26208, + 16209 => 26209, + 16210 => 26210, + 16211 => 26211, + 16212 => 26212, + 16213 => 26213, + 16214 => 26214, + 16215 => 26215, + 16216 => 26216, + 16217 => 26217, + 16218 => 26218, + 16219 => 26219, + 16220 => 26220, + 16221 => 26221, + 16222 => 26222, + 16223 => 26223, + 16224 => 26224, + 16225 => 26225, + 16226 => 26226, + 16227 => 26227, + 16228 => 26228, + 16229 => 26229, + 16230 => 26230, + 16231 => 26231, + 16232 => 26232, + 16233 => 26233, + 16234 => 26234, + 16235 => 26235, + 16236 => 26236, + 16237 => 26237, + 16238 => 26238, + 16239 => 26239, + 16240 => 26240, + 16241 => 26241, + 16242 => 26242, + 16243 => 26243, + 16244 => 26244, + 16245 => 26245, + 16246 => 26246, + 16247 => 26247, + 16248 => 26248, + 16249 => 26249, + 16250 => 26250, + 16251 => 26251, + 16252 => 26252, + 16253 => 26253, + 16254 => 26254, + 16255 => 26255, + 16256 => 26256, + 16257 => 26257, + 16258 => 26258, + 16259 => 26259, + 16260 => 26260, + 16261 => 26261, + 16262 => 26262, + 16263 => 26263, + 16264 => 26264, + 16265 => 26265, + 16266 => 26266, + 16267 => 26267, + 16268 => 26268, + 16269 => 26269, + 16270 => 26270, + 16271 => 26271, + 16272 => 26272, + 16273 => 26273, + 16274 => 26274, + 16275 => 26275, + 16276 => 26276, + 16277 => 26277, + 16278 => 26278, + 16279 => 26279, + 16280 => 26280, + 16281 => 26281, + 16282 => 26282, + 16283 => 26283, + 16284 => 26284, + 16285 => 26285, + 16286 => 26286, + 16287 => 26287, + 16288 => 26288, + 16289 => 26289, + 16290 => 26290, + 16291 => 26291, + 16292 => 26292, + 16293 => 26293, + 16294 => 26294, + 16295 => 26295, + 16296 => 26296, + 16297 => 26297, + 16298 => 26298, + 16299 => 26299, + 16300 => 26300, + 16301 => 26301, + 16302 => 26302, + 16303 => 26303, + 16304 => 26304, + 16305 => 26305, + 16306 => 26306, + 16307 => 26307, + 16308 => 26308, + 16309 => 26309, + 16310 => 26310, + 16311 => 26311, + 16312 => 26312, + 16313 => 26313, + 16314 => 26314, + 16315 => 26315, + 16316 => 26316, + 16317 => 26317, + 16318 => 26318, + 16319 => 26319, + 16320 => 26320, + 16321 => 26321, + 16322 => 26322, + 16323 => 26323, + 16324 => 26324, + 16325 => 26325, + 16326 => 26326, + 16327 => 26327, + 16328 => 26328, + 16329 => 26329, + 16330 => 26330, + 16331 => 26331, + 16332 => 26332, + 16333 => 26333, + 16334 => 26334, + 16335 => 26335, + 16336 => 26336, + 16337 => 26337, + 16338 => 26338, + 16339 => 26339, + 16340 => 26340, + 16341 => 26341, + 16342 => 26342, + 16343 => 26343, + 16344 => 26344, + 16345 => 26345, + 16346 => 26346, + 16347 => 26347, + 16348 => 26348, + 16349 => 26349, + 16350 => 26350, + 16351 => 26351, + 16352 => 26352, + 16353 => 26353, + 16354 => 26354, + 16355 => 26355, + 16356 => 26356, + 16357 => 26357, + 16358 => 26358, + 16359 => 26359, + 16360 => 26360, + 16361 => 26361, + 16362 => 26362, + 16363 => 26363, + 16364 => 26364, + 16365 => 26365, + 16366 => 26366, + 16367 => 26367, + 16368 => 26368, + 16369 => 26369, + 16370 => 26370, + 16371 => 26371, + 16372 => 26372, + 16373 => 26373, + 16374 => 26374, + 16375 => 26375, + 16376 => 26376, + 16377 => 26377, + 16378 => 26378, + 16379 => 26379, + 16380 => 26380, + 16381 => 26381, + 16382 => 26382, + 16383 => 26383, + 16384 => 26384, + 16385 => 26385, + 16386 => 26386, + 16387 => 26387, + 16388 => 26388, + 16389 => 26389, + 16390 => 26390, + 16391 => 26391, + 16392 => 26392, + 16393 => 26393, + 16394 => 26394, + 16395 => 26395, + 16396 => 26396, + 16397 => 26397, + 16398 => 26398, + 16399 => 26399, + 16400 => 26400, + 16401 => 26401, + 16402 => 26402, + 16403 => 26403, + 16404 => 26404, + 16405 => 26405, + 16406 => 26406, + 16407 => 26407, + 16408 => 26408, + 16409 => 26409, + 16410 => 26410, + 16411 => 26411, + 16412 => 26412, + 16413 => 26413, + 16414 => 26414, + 16415 => 26415, + 16416 => 26416, + 16417 => 26417, + 16418 => 26418, + 16419 => 26419, + 16420 => 26420, + 16421 => 26421, + 16422 => 26422, + 16423 => 26423, + 16424 => 26424, + 16425 => 26425, + 16426 => 26426, + 16427 => 26427, + 16428 => 26428, + 16429 => 26429, + 16430 => 26430, + 16431 => 26431, + 16432 => 26432, + 16433 => 26433, + 16434 => 26434, + 16435 => 26435, + 16436 => 26436, + 16437 => 26437, + 16438 => 26438, + 16439 => 26439, + 16440 => 26440, + 16441 => 26441, + 16442 => 26442, + 16443 => 26443, + 16444 => 26444, + 16445 => 26445, + 16446 => 26446, + 16447 => 26447, + 16448 => 26448, + 16449 => 26449, + 16450 => 26450, + 16451 => 26451, + 16452 => 26452, + 16453 => 26453, + 16454 => 26454, + 16455 => 26455, + 16456 => 26456, + 16457 => 26457, + 16458 => 26458, + 16459 => 26459, + 16460 => 26460, + 16461 => 26461, + 16462 => 26462, + 16463 => 26463, + 16464 => 26464, + 16465 => 26465, + 16466 => 26466, + 16467 => 26467, + 16468 => 26468, + 16469 => 26469, + 16470 => 26470, + 16471 => 26471, + 16472 => 26472, + 16473 => 26473, + 16474 => 26474, + 16475 => 26475, + 16476 => 26476, + 16477 => 26477, + 16478 => 26478, + 16479 => 26479, + 16480 => 26480, + 16481 => 26481, + 16482 => 26482, + 16483 => 26483, + 16484 => 26484, + 16485 => 26485, + 16486 => 26486, + 16487 => 26487, + 16488 => 26488, + 16489 => 26489, + 16490 => 26490, + 16491 => 26491, + 16492 => 26492, + 16493 => 26493, + 16494 => 26494, + 16495 => 26495, + 16496 => 26496, + 16497 => 26497, + 16498 => 26498, + 16499 => 26499, + 16500 => 26500, +]; + +$firstArrayValue = TEST_ARRAY_1[rand(1, 6500)]; +$secondArrayValue = TEST_ARRAY_2[$firstArrayValue] ?? null; diff --git a/tests/PHPStan/Analyser/data/bug-8376.php b/tests/PHPStan/Analyser/data/bug-8376.php new file mode 100644 index 0000000000..482cd26698 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8376.php @@ -0,0 +1,11 @@ +prepare('SELECT x FROM z'); + $rows = $qry->fetchAll() ?: []; + + foreach($rows as $row) { + $matrix[$row['x']] = []; + + foreach($rows as $row2) { + $matrix[$row['x']][$row2['x']] = []; + + foreach($rows as $row3) { + $matrix[$row['x']][$row2['x']][$row3['x']] = []; + + foreach($rows as $row4) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']] = []; + + foreach($rows as $row5) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']][$row5['x']] = []; + + foreach($rows as $row6) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']][$row5['x']][$row6['x']] = []; + + foreach($rows as $row7) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']][$row5['x']][$row6['x']][$row7['x']] = []; + + foreach($rows as $row8) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']][$row5['x']][$row6['x']][$row7['x']][$row8['x']] = []; + } + } + } + } + } + } + } + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8537.php b/tests/PHPStan/Analyser/data/bug-8537.php new file mode 100644 index 0000000000..b0f36e6623 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8537.php @@ -0,0 +1,39 @@ += 8.0 + +namespace Bug8537; + +/** + * @property int $x + */ +interface SampleInterface +{ +} + +class Sample implements SampleInterface +{ + /** @param array $data */ + public function __construct(private array $data = []) + { + } + + public function __set(string $key, mixed $value): void + { + $this->data[$key] = $value; + } + + public function __get(string $key): mixed + { + return $this->data[$key] ?? null; + } + + public function __isset(string $key): bool + { + return array_key_exists($key, $this->data); + } +} + +function (): void { + $test = new Sample(); + $test->x = 3; + echo $test->x; +}; diff --git a/tests/PHPStan/Analyser/data/bug-8664.php b/tests/PHPStan/Analyser/data/bug-8664.php new file mode 100644 index 0000000000..30f8ef38e8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8664.php @@ -0,0 +1,68 @@ +id = $id; + } + + public function getId(): ?int + { + return $this->id; + } + + public function setUsername(?string $username = null): void + { + $this->username = $username; + } + + public function getUsername(): ?string + { + return $this->username; + } +} + +class DataObject +{ + protected ?UserObject $user = null; + + public function setUser(?UserObject $user = null): void + { + $this->user = $user; + } + + public function getUser(): ?UserObject + { + return $this->user; + } +} + +class Test +{ + public function test(): void + { + $data = new DataObject(); + + $userObject = $data->getUser(); + + if ($userObject?->getId() > 0) { + $userId = $userObject->getId(); + + var_dump($userId); + } + + if (null !== $userObject?->getUsername()) { + $userUsername = $userObject->getUsername(); + + var_dump($userUsername); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8983.php b/tests/PHPStan/Analyser/data/bug-8983.php new file mode 100644 index 0000000000..d75242220d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8983.php @@ -0,0 +1,30 @@ += 8.1 + +namespace Bug8983; + +use function PHPStan\Testing\assertType; + +enum Enum1: string +{ + + case FOO = 'foo'; + +} + +enum Enum2: string +{ + + case BAR = 'bar'; + +} + +class Foo +{ + + /** @param value-of $bar */ + public function doFoo($bar): void + { + assertType("'bar'|'foo'", $bar); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-9008.php b/tests/PHPStan/Analyser/data/bug-9008.php new file mode 100644 index 0000000000..fb71337bd5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9008.php @@ -0,0 +1,69 @@ +shouldWorkOne($alpha)); + assertType('Bug9008\\A|Bug9008\\B|Bug9008\\C', $this->shouldWorkTwo($alpha)); + assertType('Bug9008\\A|Bug9008\\B|Bug9008\\C', $this->worksButExtraVerboseOne($alpha)); + assertType('Bug9008\\A|Bug9008\\B|Bug9008\\C', $this->worksButExtraVerboseTwo($alpha)); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9039.php b/tests/PHPStan/Analyser/data/bug-9039.php new file mode 100644 index 0000000000..9411171099 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9039.php @@ -0,0 +1,19 @@ + + */ +class Test extends Voter +{ + public const FOO = 'Foo'; + private const RULES = [self::FOO]; +} diff --git a/tests/PHPStan/Analyser/data/bug-9307.php b/tests/PHPStan/Analyser/data/bug-9307.php new file mode 100644 index 0000000000..f5b2cb7906 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9307.php @@ -0,0 +1,56 @@ +getIds() as $id) { + if (! array_key_exists($id, $itemCache)) { + $items = $this->getObjects(); + $itemCache[$id] = $items; + } else { + $items = $itemCache[$id]; + } + + // It works when the following line is uncommented. + //$items = $this->getObjects(); + + foreach ($items as $item) { + $objects[$item->id] = $item; + } + } + + assertType('array', $objects); + + $this->acceptObjects($objects); + } + + /** @return array */ + public function getIds(): array + { + return []; + } + + /** @return array */ + public function getObjects(): array + { + return []; + } + + /** @param array $objects */ + public function acceptObjects(array $objects): void + { + + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9428.php b/tests/PHPStan/Analyser/data/bug-9428.php new file mode 100644 index 0000000000..90d47479f1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9428.php @@ -0,0 +1,11 @@ + */ + public array $array; + + /** + * @param positive-int $count + */ + public function __construct(int $count) { + $this->array = range(1, $count); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-9690.php b/tests/PHPStan/Analyser/data/bug-9690.php new file mode 100644 index 0000000000..547285c2cb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9690.php @@ -0,0 +1,174 @@ + new ANode(), + $b instanceof B => new BNode(), + default => new CNode(), + }; + } + + public function test(): void { + assertType('Bug9860\\ANode', $this->b(new A())); + assertType('Bug9860\\BNode', $this->b(new B())); + assertType('Bug9860\\ANode|Bug9860\\BNode', $this->b($this->a())); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9994.php b/tests/PHPStan/Analyser/data/bug-9994.php new file mode 100644 index 0000000000..f87e4efdde --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9994.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug9994; + +function (): void { + + $arr = [ + 1, + 2, + 3, + null, + ]; + + + var_dump(array_filter($arr, !is_null(...))); +}; diff --git a/tests/PHPStan/Analyser/data/callable-in-union.php b/tests/PHPStan/Analyser/data/callable-in-union.php deleted file mode 100644 index b72c0725cc..0000000000 --- a/tests/PHPStan/Analyser/data/callable-in-union.php +++ /dev/null @@ -1,17 +0,0 @@ -|(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 deleted file mode 100644 index 5014f87302..0000000000 --- a/tests/PHPStan/Analyser/data/cast-to-numeric-string.php +++ /dev/null @@ -1,61 +0,0 @@ -', $std::class); - assertType('*ERROR*', $string::class); - assertType('class-string', $stdOrNull::class); - assertType('*ERROR*', $stringOrNull::class); - } - -} diff --git a/tests/PHPStan/Analyser/data/closure-types.php b/tests/PHPStan/Analyser/data/closure-types.php deleted file mode 100644 index 72adfe762a..0000000000 --- a/tests/PHPStan/Analyser/data/closure-types.php +++ /dev/null @@ -1,50 +0,0 @@ - */ - private $arrayShapes; - - public function doFoo(): void - { - $a = array_map(function (array $a): array { - assertType('array{foo: string, bar: int}', $a); - - return $a; - }, $this->arrayShapes); - assertType('array', $a); - - $b = array_map(function ($b) { - assertType('array{foo: string, bar: int}', $b); - - return $b['foo']; - }, $this->arrayShapes); - assertType('array', $b); - } - - 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); - - return 1; - }); - } - - 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); - - return 1; - }); - } - -} diff --git a/tests/PHPStan/Analyser/data/coalesce-assign.php b/tests/PHPStan/Analyser/data/coalesce-assign.php index fd700ec914..e6940f5fbf 100644 --- a/tests/PHPStan/Analyser/data/coalesce-assign.php +++ b/tests/PHPStan/Analyser/data/coalesce-assign.php @@ -1,4 +1,4 @@ -= 7.4 + $foo = 1, + $isFoo || $isBar => $foo = 2, + default => $foo = null, + }; + } +} diff --git a/tests/PHPStan/Analyser/data/conditional-return-type-stub.php b/tests/PHPStan/Analyser/data/conditional-return-type-stub.php new file mode 100644 index 0000000000..0280837a8e --- /dev/null +++ b/tests/PHPStan/Analyser/data/conditional-return-type-stub.php @@ -0,0 +1,36 @@ +doFoo(1)); + assertType('string', $f->doFoo("foo")); +}; + + +function (Bar $b): void { + assertType('int', $b->doFoo(1)); + assertType('string', $b->doFoo("foo")); +}; diff --git a/tests/PHPStan/Analyser/data/conditional-return-type.stub b/tests/PHPStan/Analyser/data/conditional-return-type.stub new file mode 100644 index 0000000000..41532de782 --- /dev/null +++ b/tests/PHPStan/Analyser/data/conditional-return-type.stub @@ -0,0 +1,16 @@ +', count($nonEmpty)); - assertType('int<1, max>', sizeof($nonEmpty)); - } - -} diff --git a/tests/PHPStan/Analyser/data/countable.php b/tests/PHPStan/Analyser/data/countable.php deleted file mode 100644 index 2d18e25faa..0000000000 --- a/tests/PHPStan/Analyser/data/countable.php +++ /dev/null @@ -1,17 +0,0 @@ -', $foo->count()); - } -} - diff --git a/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php b/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php index f79aea7dd3..420d5e089c 100644 --- a/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php +++ b/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php @@ -2,5 +2,5 @@ function bcompiler_write_file(): void { - + echo 'test'; } diff --git a/tests/PHPStan/Analyser/data/die-73.php b/tests/PHPStan/Analyser/data/die-73.php deleted file mode 100644 index b1bce4861f..0000000000 --- a/tests/PHPStan/Analyser/data/die-73.php +++ /dev/null @@ -1,3 +0,0 @@ - $array + * @param ( + * $mode is ARRAY_FILTER_USE_BOTH + * ? (callable(T, K=): bool) + * : ( + * $mode is ARRAY_FILTER_USE_KEY + * ? (callable(K): bool) + * : ( + * $mode is 0 + * ? (callable(T): bool) + * : null + * ) + * ) + * ) $callback + * @param ARRAY_FILTER_USE_BOTH|ARRAY_FILTER_USE_KEY|0 $mode + * + * @return array + */ +function filter(array $array, ?callable $callback = null, int $mode = ARRAY_FILTER_USE_BOTH): array +{ + return null !== $callback + ? array_filter($array, $callback, $mode) + : array_filter($array); +} + +function () { + // This one does fail, as both the value + key is asked and the key + value is used + filter( + [false, true, false], + static fn (int $key, bool $value): bool => 0 === $key % 2 && $value, + mode: ARRAY_FILTER_USE_BOTH, + ); + + // This one should fail, as both the value + key is asked but only the key is used + filter( + [false, true, false], + static fn (int $key): bool => 0 === $key % 2, + mode: ARRAY_FILTER_USE_BOTH, + ); + + // This one should fail, as only the key is asked but the value is used + filter( + [false, true, false], + static fn (bool $value): bool => $value, + mode: ARRAY_FILTER_USE_KEY, + ); + + // This one should fail, as only the value is asked but the key is used + filter( + [false, true, false], + static fn (int $key): bool => 0 === $key % 2, + mode: 0, + ); +}; diff --git a/tests/PHPStan/Analyser/data/discussion-9053.php b/tests/PHPStan/Analyser/data/discussion-9053.php new file mode 100644 index 0000000000..e02627602c --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-9053.php @@ -0,0 +1,110 @@ += 8.0 + +namespace Discussion9053; + +use function PHPStan\Testing\assertType; + +/** + * @template TChild of ChildInterface + */ +interface ModelInterface { + /** + * @return TChild[] + */ + public function getChildren(): array; +} + +/** + * @implements ModelInterface + */ +class Model implements ModelInterface +{ + /** + * @var Child[] + */ + public array $children; + + public function getChildren(): array + { + return $this->children; + } +} + +/** + * @template T of ModelInterface + */ +interface ChildInterface { + /** + * @return T + */ + public function getModel(): ModelInterface; +} + + +/** + * @implements ChildInterface + */ +class Child implements ChildInterface +{ + public function __construct(private Model $model) + { + } + + public function getModel(): Model + { + return $this->model; + } +} + +/** + * @template T of ModelInterface + */ +class Helper +{ + /** + * @param T $model + */ + public function __construct(private ModelInterface $model) + {} + + /** + * @return template-type + */ + public function getFirstChildren(): ChildInterface + { + $firstChildren = $this->model->getChildren()[0] ?? null; + + if (!$firstChildren) { + throw new \RuntimeException('No first child found.'); + } + + return $firstChildren; + } +} + +class Other { + /** + * @template TChild of ChildInterface + * @template TModel of ModelInterface + * @param Helper $helper + * @return TChild + */ + public function getFirstChildren(Helper $helper): ChildInterface { + $child = $helper->getFirstChildren(); + assertType('TChild of Discussion9053\ChildInterface (method Discussion9053\Other::getFirstChildren(), argument)', $child); + + return $child; + } +} + +function (): void { + $model = new Model(); + $helper = new Helper($model); + assertType('Discussion9053\Helper', $helper); + $child = $helper->getFirstChildren(); + assertType('Discussion9053\Child', $child); + + $other = new Other(); + $child2 = $other->getFirstChildren($helper); + assertType('Discussion9053\Child', $child2); +}; diff --git a/tests/PHPStan/Analyser/data/div-by-zero.php b/tests/PHPStan/Analyser/data/div-by-zero.php deleted file mode 100644 index 2dae4d4767..0000000000 --- a/tests/PHPStan/Analyser/data/div-by-zero.php +++ /dev/null @@ -1,28 +0,0 @@ - $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-pollute-scope-with-block.php b/tests/PHPStan/Analyser/data/do-not-pollute-scope-with-block.php new file mode 100644 index 0000000000..d1569b1d5b --- /dev/null +++ b/tests/PHPStan/Analyser/data/do-not-pollute-scope-with-block.php @@ -0,0 +1,26 @@ +', rand(0, 1)); - } - }; - - function (): void { - if (rand(0, 1) === 0) { - assertType('int<0, 1>', rand(0, 1)); - } - }; - function (): void { - assertType('1|\'foo\'', rand(0, 1) ?: 'foo'); - assertType('\'foo\'|int<0, 1>', rand(0, 1) ? rand(0, 1) : 'foo'); - }; - } - - public function doBar(): bool - { - - } - - /** @phpstan-pure */ - public function doBaz(): bool - { - - } - - /** @phpstan-impure */ - public function doLorem(): bool - { - - } - - public function doIpsum() - { - if ($this->doBar() === true) { - assertType('true', $this->doBar()); - } - - if ($this->doBaz() === true) { - assertType('true', $this->doBaz()); - } - - if ($this->doLorem() === true) { - assertType('bool', $this->doLorem()); - } - } - - public function doDolor() - { - if ($this->doBar()) { - assertType('true', $this->doBar()); - } - - if ($this->doBaz()) { - assertType('true', $this->doBaz()); - } - - if ($this->doLorem()) { - assertType('bool', $this->doLorem()); - } - } - -} diff --git a/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php new file mode 100644 index 0000000000..5adbbc5220 --- /dev/null +++ b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php @@ -0,0 +1,111 @@ +pure() === 1) { + assertType('1', $this->pure()); + } + + if ($this->maybePure() === 1) { + assertType('int', $this->maybePure()); + } + + if ($this->impure() === 1) { + assertType('int', $this->impure()); + } + } + +} + +class FooStatic +{ + + /** @phpstan-pure */ + public static function pure(): int + { + return 1; + } + + public static function maybePure(): int + { + return 1; + } + + /** @phpstan-impure */ + public static function impure(): int + { + return rand(0, 1); + } + + public function test(): void + { + if (self::pure() === 1) { + assertType('1', self::pure()); + } + + if (self::maybePure() === 1) { + assertType('int', self::maybePure()); + } + + if (self::impure() === 1) { + assertType('int', self::impure()); + } + } + +} + +/** @phpstan-pure */ +function pure(): int +{ + return 1; +} + +function maybePure(): int +{ + return 1; +} + +/** @phpstan-impure */ +function impure(): int +{ + return rand(0, 1); +} + +function test(): void +{ + if (pure() === 1) { + assertType('1', pure()); + } + + if (maybePure() === 1) { + assertType('int', maybePure()); + } + + if (impure() === 1) { + assertType('int', impure()); + } +} diff --git a/tests/PHPStan/Analyser/data/dynamic-constant-native-types.php b/tests/PHPStan/Analyser/data/dynamic-constant-native-types.php new file mode 100644 index 0000000000..c39dca6e35 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-constant-native-types.php @@ -0,0 +1,15 @@ += 8.3 + +namespace DynamicConstantNativeTypes; + +final class Foo +{ + + public const int FOO = 123; + public const int|string BAR = 123; + +} + +function (Foo $foo): void { + die; +}; diff --git a/tests/PHPStan/Analyser/data/dynamic-method-return-types-named-args.php b/tests/PHPStan/Analyser/data/dynamic-method-return-types-named-args.php new file mode 100644 index 0000000000..7c0b75f766 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-method-return-types-named-args.php @@ -0,0 +1,26 @@ += 8.0 + +namespace DynamicMethodReturnTypesNamespace; + +use function PHPStan\Testing\assertType; + +class FooNamedArgs +{ + + public function __construct() + { + } + + public function doFoo() + { + $em = new EntityManager(); + $iem = new InheritedEntityManager(); + + assertType('DynamicMethodReturnTypesNamespace\Foo', $em->getByPrimary(className: \DynamicMethodReturnTypesNamespace\Foo::class)); + assertType('DynamicMethodReturnTypesNamespace\Foo', $iem->getByPrimary(className: \DynamicMethodReturnTypesNamespace\Foo::class)); + + assertType('DynamicMethodReturnTypesNamespace\Foo', \DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity(className: \DynamicMethodReturnTypesNamespace\Foo::class)); + assertType('DynamicMethodReturnTypesNamespace\Foo', \DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity(className: \DynamicMethodReturnTypesNamespace\Foo::class)); + } + +} diff --git a/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args-fixture.php b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args-fixture.php new file mode 100644 index 0000000000..7da45ec443 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args-fixture.php @@ -0,0 +1,68 @@ += 8.0 + +namespace DynamicMethodThrowTypeExtensionNamedArgs; + +use PHPStan\TrinaryLogic; +use function PHPStan\Testing\assertVariableCertainty; + +class Foo +{ + + /** @throws \Exception */ + public function throwOrNot(bool $need): int + { + if ($need) { + throw new \Exception(); + } + + return 1; + } + + /** @throws \Exception */ + public static function staticThrowOrNot(bool $need): int + { + if ($need) { + throw new \Exception(); + } + + return 1; + } + + public function doFoo1() + { + try { + $result = $this->throwOrNot(need: true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo2() + { + try { + $result = $this->throwOrNot(need: false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + + public function doFoo3() + { + try { + $result = self::staticThrowOrNot(need: true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo4() + { + try { + $result = self::staticThrowOrNot(need: false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + +} + diff --git a/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php new file mode 100644 index 0000000000..727352a332 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php @@ -0,0 +1,60 @@ += 8.0 + +namespace DynamicMethodThrowTypeExtensionNamedArgs; + +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PHPStan\Analyser\Scope; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicMethodThrowTypeExtension; +use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; +use PHPStan\Type\Type; + +class MethodThrowTypeExtension implements DynamicMethodThrowTypeExtension +{ + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'throwOrNot'; + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->args) < 1) { + return $methodReflection->getThrowType(); + } + + $argType = $scope->getType($methodCall->args[0]->value); + if ((new ConstantBooleanType(true))->isSuperTypeOf($argType)->yes()) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} + +class StaticMethodThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +{ + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticThrowOrNot'; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->args) < 1) { + return $methodReflection->getThrowType(); + } + + $argType = $scope->getType($methodCall->args[0]->value); + if ((new ConstantBooleanType(true))->isSuperTypeOf($argType)->yes()) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} diff --git a/tests/PHPStan/Analyser/data/enum-reflection-backed.php b/tests/PHPStan/Analyser/data/enum-reflection-backed.php new file mode 100644 index 0000000000..00f1b9634f --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum-reflection-backed.php @@ -0,0 +1,16 @@ + $class */ +function testNarrowGetNameTypeAfterIsBacked(string $class) { + $r = new ReflectionEnum($class); + assertType('class-string', $r->getName()); + if ($r->isBacked()) { + assertType('class-string', $r->getName()); + } +} diff --git a/tests/PHPStan/Analyser/data/enum-reflection-php81.php b/tests/PHPStan/Analyser/data/enum-reflection-php81.php new file mode 100644 index 0000000000..502de6eebd --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum-reflection-php81.php @@ -0,0 +1,23 @@ += 8.1 + +namespace EnumReflection81; + +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionEnumUnitCase; +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + + case FOO = 1; + case BAR = 2; +} + +function testNarrowGetBackingTypeAfterIsBacked() { + $r = new ReflectionEnum(Foo::class); + assertType('ReflectionType|null', $r->getBackingType()); + if ($r->isBacked()) { + assertType('ReflectionType', $r->getBackingType()); + } +} diff --git a/tests/PHPStan/Analyser/data/enums.php b/tests/PHPStan/Analyser/data/enums.php deleted file mode 100644 index 1545a53afc..0000000000 --- a/tests/PHPStan/Analyser/data/enums.php +++ /dev/null @@ -1,266 +0,0 @@ -= 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/explode-php74.php b/tests/PHPStan/Analyser/data/explode-php74.php new file mode 100644 index 0000000000..b205b1d0be --- /dev/null +++ b/tests/PHPStan/Analyser/data/explode-php74.php @@ -0,0 +1,15 @@ +|false', explode($s, 'foo')); + assertType('non-empty-list|false', explode($s, 'FOO')); + assertType('non-empty-list|false', explode($s, 'Foo')); + } +} diff --git a/tests/PHPStan/Analyser/data/explode-php80.php b/tests/PHPStan/Analyser/data/explode-php80.php new file mode 100644 index 0000000000..1c01239587 --- /dev/null +++ b/tests/PHPStan/Analyser/data/explode-php80.php @@ -0,0 +1,15 @@ +', explode($s, 'foo')); + assertType('non-empty-list', explode($s, 'FOO')); + assertType('non-empty-list', explode($s, 'Foo')); + } +} diff --git a/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php b/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php new file mode 100644 index 0000000000..76b6b00389 --- /dev/null +++ b/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php @@ -0,0 +1,21 @@ +methodReturningBoolNoMatterTheCallerUnlessReturnsString()); +assertType('bool', (new WhateverClass2)->methodReturningBoolNoMatterTheCallerUnlessReturnsString()); +assertType('string', (new WhateverClass3)->methodReturningBoolNoMatterTheCallerUnlessReturnsString()); diff --git a/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php deleted file mode 100644 index bcdf815694..0000000000 --- a/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php +++ /dev/null @@ -1,64 +0,0 @@ -doFluentUnionIterable() as $fluentUnionIterableBaz) { + die; + } +} diff --git a/tests/PHPStan/Analyser/data/functions.php b/tests/PHPStan/Analyser/data/functions.php index 6ef64e58fc..ff580ad6a4 100644 --- a/tests/PHPStan/Analyser/data/functions.php +++ b/tests/PHPStan/Analyser/data/functions.php @@ -15,13 +15,6 @@ $versionCompare7 = version_compare(doFoo() ? '7.0.0' : '6.0.5', doBar() ? '7.0.1' : '6.0.0', '<'); $versionCompare8 = version_compare('7.0.0', doFoo(), '<'); -$mbStrlenWithoutEncoding = mb_strlen(''); -$mbStrlenWithValidEncoding = mb_strlen('', 'utf-8'); -$mbStrlenWithValidEncodingAlias = mb_strlen('', 'utf8'); -$mbStrlenWithInvalidEncoding = mb_strlen('', 'foo'); -$mbStrlenWithValidAndInvalidEncoding = mb_strlen('', doFoo() ? 'utf-8' : 'foo'); -$mbStrlenWithUnknownEncoding = mb_strlen('', doFoo()); - $mbHttpOutputWithoutEncoding = mb_http_output(); $mbHttpOutputWithValidEncoding = mb_http_output('utf-8'); $mbHttpOutputWithInvalidEncoding = mb_http_output('foo'); diff --git a/tests/PHPStan/Analyser/data/generic-unions.php b/tests/PHPStan/Analyser/data/generic-unions.php deleted file mode 100644 index 4fe7aac9f5..0000000000 --- a/tests/PHPStan/Analyser/data/generic-unions.php +++ /dev/null @@ -1,135 +0,0 @@ -doFoo($nullableString)); - assertType('int|string', $this->doFoo($stringOrInt)); - - assertType('string|null', $this->doBar($nullableString)); - - assertType('int', $this->doBaz(1)); - assertType('string', $this->doBaz('foo')); - assertType('float', $this->doBaz(1.2)); - assertType('string', $this->doBaz($stringOrInt)); - } - -} - -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/getopt.php b/tests/PHPStan/Analyser/data/getopt.php deleted file mode 100644 index 142c986f57..0000000000 --- a/tests/PHPStan/Analyser/data/getopt.php +++ /dev/null @@ -1,9 +0,0 @@ -|string|false>|false)', $opts); diff --git a/tests/PHPStan/Analyser/data/hash-functions-74.php b/tests/PHPStan/Analyser/data/hash-functions-74.php deleted file mode 100644 index 85915248d7..0000000000 --- a/tests/PHPStan/Analyser/data/hash-functions-74.php +++ /dev/null @@ -1,53 +0,0 @@ - 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/ignore-identifiers.php b/tests/PHPStan/Analyser/data/ignore-identifiers.php new file mode 100644 index 0000000000..27961f1108 --- /dev/null +++ b/tests/PHPStan/Analyser/data/ignore-identifiers.php @@ -0,0 +1,21 @@ +noThrow(...), []); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/implode.php b/tests/PHPStan/Analyser/data/implode.php deleted file mode 100644 index b2e2b9aa7c..0000000000 --- a/tests/PHPStan/Analyser/data/implode.php +++ /dev/null @@ -1,24 +0,0 @@ -fooProp = rand(0, 1); - } - - public function ordinaryMethod(): int - { - return 1; - } - - /** - * @phpstan-impure - * @return int - */ - public function impureMethod(): int - { - $this->fooProp = rand(0, 1); - - return $this->fooProp; - } - - /** - * @impure - * @return int - */ - public function impureMethod2(): int - { - $this->fooProp = rand(0, 1); - - return $this->fooProp; - } - - public function doFoo(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->voidMethod(); - assertType('int', $this->fooProp); - } - - public function doBar(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->ordinaryMethod(); - assertType('1', $this->fooProp); - } - - public function doBaz(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->impureMethod(); - assertType('int', $this->fooProp); - } - - public function doLorem(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->impureMethod2(); - assertType('int', $this->fooProp); - } - -} diff --git a/tests/PHPStan/Analyser/data/in-array.php b/tests/PHPStan/Analyser/data/in-array.php deleted file mode 100644 index ca8246d0d1..0000000000 --- a/tests/PHPStan/Analyser/data/in-array.php +++ /dev/null @@ -1,47 +0,0 @@ -', $i); - - $i++; - assertType('int', $i); - } else { - assertType('int<3, max>', $i); - } - - if ($i < 3) { - assertType('int', $i); - - $i--; - assertType('int', $i); - } - - assertType('int|int<3, max>', $i); - - if ($i < 3 && $i > 5) { - assertType('*NEVER*', $i); - } else { - assertType('int|int<3, max>', $i); - } - - if ($i > 3 && $i < 5) { - assertType('4', $i); - } else { - assertType('3|int|int<5, max>', $i); - } - - if ($i >= 3 && $i <= 5) { - assertType('int<3, 5>', $i); - - if ($i === 2) { - assertType('*NEVER*', $i); - } else { - assertType('int<3, 5>', $i); - } - - if ($i !== 3) { - assertType('int<4, 5>', $i); - } else { - assertType('3', $i); - } - } -}; - - -function () { - for ($i = 0; $i < 5; $i++) { - assertType('int<0, 4>', $i); - } - - $i = 0; - while ($i < 5) { - assertType('int<0, 4>', $i); - $i++; - } - - $i = 0; - while ($i++ < 5) { - assertType('int<1, 5>', $i); - } - - $i = 0; - while (++$i < 5) { - assertType('int<1, 4>', $i); - } - - $i = 5; - while ($i-- > 0) { - assertType('int<0, 4>', $i); - } - - $i = 5; - while (--$i > 0) { - assertType('int<1, 4>', $i); - } -}; - - -function (int $j) { - $i = 1; - - assertType('true', $i > 0); - assertType('true', $i >= 1); - assertType('true', $i <= 1); - assertType('true', $i < 2); - - assertType('false', $i < 1); - assertType('false', $i <= 0); - assertType('false', $i >= 2); - assertType('false', $i > 1); - - assertType('true', 0 < $i); - assertType('true', 1 <= $i); - assertType('true', 1 >= $i); - assertType('true', 2 > $i); - - assertType('bool', $j > 0); - assertType('bool', $j >= 0); - assertType('bool', $j <= 0); - assertType('bool', $j < 0); - - if ($j < 5) { - assertType('bool', $j > 0); - assertType('false', $j > 4); - assertType('bool', 0 < $j); - assertType('false', 4 < $j); - - assertType('bool', $j >= 0); - assertType('false', $j >= 5); - assertType('bool', 0 <= $j); - assertType('false', 5 <= $j); - - assertType('true', $j <= 4); - assertType('bool', $j <= 3); - assertType('true', 4 >= $j); - assertType('bool', 3 >= $j); - - assertType('true', $j < 5); - assertType('bool', $j < 4); - assertType('true', 5 > $j); - assertType('bool', 4 > $j); - } -}; - -function (int $a, int $b, int $c): void { - - if ($a <= 11) { - return; - } - - assertType('int<12, max>', $a); - - if ($b <= 12) { - return; - } - - assertType('int<13, max>', $b); - - if ($c <= 13) { - return; - } - - assertType('int<14, max>', $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/is-resource-specified.php b/tests/PHPStan/Analyser/data/is-resource-specified.php new file mode 100644 index 0000000000..c78d5ad75a --- /dev/null +++ b/tests/PHPStan/Analyser/data/is-resource-specified.php @@ -0,0 +1,11 @@ +|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 deleted file mode 100644 index d27bde20f6..0000000000 --- a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-pre-81.php +++ /dev/null @@ -1,7 +0,0 @@ -', $ref->name ?? false); -} diff --git a/tests/PHPStan/Analyser/data/iterator_to_array.php b/tests/PHPStan/Analyser/data/iterator_to_array.php deleted file mode 100644 index e72040ad8c..0000000000 --- a/tests/PHPStan/Analyser/data/iterator_to_array.php +++ /dev/null @@ -1,34 +0,0 @@ - $foo - */ - public function testDefaultBehavior(Traversable $foo) - { - 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/list-type.php b/tests/PHPStan/Analyser/data/list-type.php deleted file mode 100644 index 592814aa9f..0000000000 --- a/tests/PHPStan/Analyser/data/list-type.php +++ /dev/null @@ -1,97 +0,0 @@ -', $list); - } - - /** @param list $list */ - public function directAssertionParamHint(array $list): void - { - assertType('array', $list); - } - - /** @param list $list */ - public function directAssertionNullableParamHint(array $list = null): void - { - assertType('array|null', $list); - } - - /** @param list<\DateTime> $list */ - public function directAssertionObjectParamHint($list): void - { - assertType('array', $list); - } - - public function withoutGenerics(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list[] = true; - $list[] = new \stdClass(); - assertType('non-empty-array', $list); - } - - - public function withMixedType(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list[] = true; - $list[] = new \stdClass(); - assertType('non-empty-array', $list); - } - - public function withObjectType(): void - { - /** @var list<\DateTime> $list */ - $list = []; - $list[] = new \DateTime(); - assertType('non-empty-array', $list); - } - - /** @return list */ - public function withScalarGoodContent(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list[] = true; - assertType('non-empty-array', $list); - } - - public function withNumericKey(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list['1'] = true; - assertType('non-empty-array', $list); - } - - public function withFullListFunctionality(): void - { - // These won't output errors for now but should when list type will be fully implemented - /** @var list $list */ - $list = []; - $list[] = '1'; - $list[] = '2'; - unset($list[0]);//break list behaviour - assertType('array', $list); - - /** @var list $list2 */ - $list2 = []; - $list2[2] = '1';//Most likely to create a gap in indexes - assertType('non-empty-array', $list2); - } - -} diff --git a/tests/PHPStan/Analyser/data/literal-string.php b/tests/PHPStan/Analyser/data/literal-string.php deleted file mode 100644 index c55c34feff..0000000000 --- a/tests/PHPStan/Analyser/data/literal-string.php +++ /dev/null @@ -1,56 +0,0 @@ -= 8.0 - -namespace MatchExpr; - -use function PHPStan\Testing\assertType; - -class Foo -{ - - /** - * @param 1|2|3|4 $i - */ - public function doFoo(int $i): void - { - assertType('*NEVER*', match ($i) { - 0 => $i, - }); - assertType('1|2|3|4', $i); - assertType('1', match ($i) { - 1 => $i, - }); - assertType('1|2|3|4', $i); - assertType('1|2', match ($i) { - 1, 2 => $i, - }); - assertType('1|2|3|4', $i); - assertType('1|2|3', match ($i) { - 1, 2, 3 => $i, - }); - assertType('1|2|3|4', $i); - assertType('2|3', match ($i) { - 1 => exit(), - 2, 3 => $i, - }); - assertType('1|2|3|4', $i); - } - - /** - * @param 1|2|3|4 $i - */ - public function doBar(int $i): void - { - match ($i) { - 0 => assertType('*NEVER*', $i), - default => assertType('1|2|3|4', $i), - }; - assertType('1|2|3|4', $i); - match ($i) { - 1 => assertType('1', $i), - default => assertType('2|3|4', $i), - }; - assertType('1|2|3|4', $i); - match ($i) { - 1, 2 => assertType('1|2', $i), - default => assertType('3|4', $i), - }; - assertType('1|2|3|4', $i); - match ($i) { - 1, 2, 3 => assertType('1|2|3', $i), - default => assertType('4', $i), - }; - assertType('1|2|3|4', $i); - - match ($i) { - assertType('1|2|3|4', $i), 1, assertType('2|3|4', $i) => null, - assertType('2|3|4', $i) => null, - default => assertType('2|3|4', $i), - }; - } - -} diff --git a/tests/PHPStan/Analyser/data/match-performance-issue.php b/tests/PHPStan/Analyser/data/match-performance-issue.php new file mode 100644 index 0000000000..b15e813423 --- /dev/null +++ b/tests/PHPStan/Analyser/data/match-performance-issue.php @@ -0,0 +1,1029 @@ += 8.1 + +namespace MatchPerformanceIssue; + +enum Country: string +{ + + case CODE_AFG = 'AF'; + case CODE_ALA = 'AX'; + case CODE_ALB = 'AL'; + case CODE_DZA = 'DZ'; + case CODE_ASM = 'AS'; + case CODE_AND = 'AD'; + case CODE_AGO = 'AO'; + case CODE_AIA = 'AI'; + case CODE_ATA = 'AQ'; + case CODE_ATG = 'AG'; + case CODE_ARG = 'AR'; + case CODE_ARM = 'AM'; + case CODE_ABW = 'AW'; + case CODE_AUS = 'AU'; + case CODE_AUT = 'AT'; + case CODE_AZE = 'AZ'; + case CODE_BHS = 'BS'; + case CODE_BHR = 'BH'; + case CODE_BGD = 'BD'; + case CODE_BRB = 'BB'; + case CODE_BLR = 'BY'; + case CODE_BEL = 'BE'; + case CODE_BLZ = 'BZ'; + case CODE_BEN = 'BJ'; + case CODE_BMU = 'BM'; + case CODE_BTN = 'BT'; + case CODE_BOL = 'BO'; + case CODE_BIH = 'BA'; + case CODE_BES = 'BQ'; + case CODE_BWA = 'BW'; + case CODE_BVT = 'BV'; + case CODE_BRA = 'BR'; + case CODE_IOT = 'IO'; + case CODE_BRN = 'BN'; + case CODE_BGR = 'BG'; + case CODE_BFA = 'BF'; + case CODE_BDI = 'BI'; + case CODE_KHM = 'KH'; + case CODE_CMR = 'CM'; + case CODE_CAN = 'CA'; + case CODE_CPV = 'CV'; + case CODE_CYM = 'KY'; + case CODE_CAF = 'CF'; + case CODE_TCD = 'TD'; + case CODE_CHL = 'CL'; + case CODE_CHN = 'CN'; + case CODE_CXR = 'CX'; + case CODE_CCK = 'CC'; + case CODE_COL = 'CO'; + case CODE_COM = 'KM'; + case CODE_COG = 'CG'; + case CODE_COK = 'CK'; + case CODE_CRI = 'CR'; + case CODE_CIV = 'CI'; + case CODE_HRV = 'HR'; + case CODE_CUB = 'CU'; + case CODE_CUW = 'CW'; + case CODE_CYP = 'CY'; + case CODE_CZE = 'CZ'; + case CODE_COD = 'CD'; + case CODE_DNK = 'DK'; + case CODE_DJI = 'DJ'; + case CODE_DMA = 'DM'; + case CODE_DOM = 'DO'; + case CODE_ECU = 'EC'; + case CODE_EGY = 'EG'; + case CODE_SLV = 'SV'; + case CODE_GNQ = 'GQ'; + case CODE_ERI = 'ER'; + case CODE_EST = 'EE'; + case CODE_ETH = 'ET'; + case CODE_FLK = 'FK'; + case CODE_FRO = 'FO'; + case CODE_FJI = 'FJ'; + case CODE_FIN = 'FI'; + case CODE_FRA = 'FR'; + case CODE_GUF = 'GF'; + case CODE_PYF = 'PF'; + case CODE_ATF = 'TF'; + case CODE_GAB = 'GA'; + case CODE_GMB = 'GM'; + case CODE_GEO = 'GE'; + case CODE_DEU = 'DE'; + case CODE_GHA = 'GH'; + case CODE_GIB = 'GI'; + case CODE_GRC = 'GR'; + case CODE_GRL = 'GL'; + case CODE_GRD = 'GD'; + case CODE_GLP = 'GP'; + case CODE_GUM = 'GU'; + case CODE_GTM = 'GT'; + case CODE_GGY = 'GG'; + case CODE_GIN = 'GN'; + case CODE_GNB = 'GW'; + case CODE_GUY = 'GY'; + case CODE_HTI = 'HT'; + case CODE_HMD = 'HM'; + case CODE_HND = 'HN'; + case CODE_HKG = 'HK'; + case CODE_HUN = 'HU'; + case CODE_ISL = 'IS'; + case CODE_IND = 'IN'; + case CODE_IDN = 'ID'; + case CODE_IRQ = 'IQ'; + case CODE_IRL = 'IE'; + case CODE_IRN = 'IR'; + case CODE_IMN = 'IM'; + case CODE_ISR = 'IL'; + case CODE_ITA = 'IT'; + case CODE_JAM = 'JM'; + case CODE_JPN = 'JP'; + case CODE_JEY = 'JE'; + case CODE_JOR = 'JO'; + case CODE_KAZ = 'KZ'; + case CODE_KEN = 'KE'; + case CODE_KIR = 'KI'; + case CODE_XKX = 'XK'; + case CODE_KWT = 'KW'; + case CODE_KGZ = 'KG'; + case CODE_LAO = 'LA'; + case CODE_LVA = 'LV'; + case CODE_LBN = 'LB'; + case CODE_LSO = 'LS'; + case CODE_LBR = 'LR'; + case CODE_LBY = 'LY'; + case CODE_LIE = 'LI'; + case CODE_LTU = 'LT'; + case CODE_LUX = 'LU'; + case CODE_MAC = 'MO'; + case CODE_MKD = 'MK'; + case CODE_MDG = 'MG'; + case CODE_MWI = 'MW'; + case CODE_MYS = 'MY'; + case CODE_MDV = 'MV'; + case CODE_MLI = 'ML'; + case CODE_MLT = 'MT'; + case CODE_MHL = 'MH'; + case CODE_MTQ = 'MQ'; + case CODE_MRT = 'MR'; + case CODE_MUS = 'MU'; + case CODE_MYT = 'YT'; + case CODE_MEX = 'MX'; + case CODE_FSM = 'FM'; + case CODE_MDA = 'MD'; + case CODE_MCO = 'MC'; + case CODE_MNG = 'MN'; + case CODE_MNE = 'ME'; + case CODE_MSR = 'MS'; + case CODE_MAR = 'MA'; + case CODE_MOZ = 'MZ'; + case CODE_MMR = 'MM'; + case CODE_NAM = 'NA'; + case CODE_NRU = 'NR'; + case CODE_NPL = 'NP'; + case CODE_NLD = 'NL'; + case CODE_NCL = 'NC'; + case CODE_NZL = 'NZ'; + case CODE_NIC = 'NI'; + case CODE_NER = 'NE'; + case CODE_NGA = 'NG'; + case CODE_NIU = 'NU'; + case CODE_NFK = 'NF'; + case CODE_MNP = 'MP'; + case CODE_PRK = 'KP'; + case CODE_NOR = 'NO'; + case CODE_OMN = 'OM'; + case CODE_PAK = 'PK'; + case CODE_PLW = 'PW'; + case CODE_PSE = 'PS'; + case CODE_PAN = 'PA'; + case CODE_PNG = 'PG'; + case CODE_PRY = 'PY'; + case CODE_PER = 'PE'; + case CODE_PHL = 'PH'; + case CODE_PCN = 'PN'; + case CODE_POL = 'PL'; + case CODE_PRT = 'PT'; + case CODE_PRI = 'PR'; + case CODE_QAT = 'QA'; + case CODE_REU = 'RE'; + case CODE_ROU = 'RO'; + case CODE_RUS = 'RU'; + case CODE_RWA = 'RW'; + case CODE_SHN = 'SH'; + case CODE_KNA = 'KN'; + case CODE_LCA = 'LC'; + case CODE_SXM = 'SX'; + case CODE_MAF = 'MF'; + case CODE_SPM = 'PM'; + case CODE_VCT = 'VC'; + case CODE_WSM = 'WS'; + case CODE_SMR = 'SM'; + case CODE_STP = 'ST'; + case CODE_SAU = 'SA'; + case CODE_SEN = 'SN'; + case CODE_SRB = 'RS'; + case CODE_SYC = 'SC'; + case CODE_SLE = 'SL'; + case CODE_SGP = 'SG'; + case CODE_SVK = 'SK'; + case CODE_SVN = 'SI'; + case CODE_SLB = 'SB'; + case CODE_SOM = 'SO'; + case CODE_ZAF = 'ZA'; + case CODE_SGS = 'GS'; + case CODE_KOR = 'KR'; + case CODE_SSU = 'SS'; + case CODE_ESP = 'ES'; + case CODE_LKA = 'LK'; + case CODE_SDN = 'SD'; + case CODE_SUR = 'SR'; + case CODE_SJM = 'SJ'; + case CODE_SWZ = 'SZ'; + case CODE_SWE = 'SE'; + case CODE_CHE = 'CH'; + case CODE_SYR = 'SY'; + case CODE_TWN = 'TW'; + case CODE_TJK = 'TJ'; + case CODE_TZA = 'TZ'; + case CODE_THA = 'TH'; + case CODE_TLS = 'TL'; + case CODE_TGO = 'TG'; + case CODE_TKL = 'TK'; + case CODE_TON = 'TO'; + case CODE_TTO = 'TT'; + case CODE_TUN = 'TN'; + case CODE_TUR = 'TR'; + case CODE_TKM = 'TM'; + case CODE_TCA = 'TC'; + case CODE_TUV = 'TV'; + case CODE_UGA = 'UG'; + case CODE_UKR = 'UA'; + case CODE_ARE = 'AE'; + case CODE_GBR = 'GB'; + case CODE_USA = 'US'; + case CODE_UMI = 'UM'; + case CODE_URY = 'UY'; + case CODE_UZB = 'UZ'; + case CODE_VUT = 'VU'; + case CODE_VAT = 'VA'; + case CODE_VEN = 'VE'; + case CODE_VNM = 'VN'; + case CODE_VGB = 'VG'; + case CODE_VIR = 'VI'; + case CODE_WLF = 'WF'; + case CODE_ESH = 'EH'; + case CODE_YEM = 'YE'; + case CODE_ZMB = 'ZM'; + case CODE_ZWE = 'ZW'; + + /** + * @return string[] + */ + public static function getLabelDefinitions(): array + { + return [ + self::CODE_ABW->value => _('Aruba'), + self::CODE_AFG->value => _('Islámský stát Afghánistán'), + self::CODE_AGO->value => _('Angolská republika'), + self::CODE_AIA->value => _('Anguilla'), + self::CODE_ALA->value => _('Alandské ostrovy'), + self::CODE_ALB->value => _('Albánská republika'), + self::CODE_AND->value => _('Andorrské knížectví'), + self::CODE_ARE->value => _('Spojené arabské emiráty'), + self::CODE_ARG->value => _('Argentinská republika'), + self::CODE_ARM->value => _('Arménská republika'), + self::CODE_ASM->value => _('Americká Samoa'), + self::CODE_ATA->value => _('Antarktida'), + self::CODE_ATF->value => _('Francouzská jižní území'), + self::CODE_ATG->value => _('Antigua a Barbuda'), + self::CODE_AUS->value => _('Austrálie'), + self::CODE_AUT->value => _('Rakouská republika'), + self::CODE_AZE->value => _('Ázerbájdžánská republika'), + self::CODE_BDI->value => _('Burundská republika'), + self::CODE_BEL->value => _('Belgické království'), + self::CODE_BEN->value => _('Beninská republika'), + self::CODE_BFA->value => _('Burkina Faso'), + self::CODE_BGD->value => _('Bangladéšská lidová republika'), + self::CODE_BGR->value => _('Bulharská republika'), + self::CODE_BHR->value => _('Bahrajnské království'), + self::CODE_BHS->value => _('Bahamské společenství'), + self::CODE_BIH->value => _('Bosna a Hercegovina'), + self::CODE_BES->value => _('Karibské Nizozemsko'), + self::CODE_BLR->value => _('Běloruská republika'), + self::CODE_BLZ->value => _('Belize'), + self::CODE_BMU->value => _('Bermudy'), + self::CODE_BOL->value => _('Bolivijská republika'), + self::CODE_BRA->value => _('Brazilská federativní republika'), + self::CODE_BRB->value => _('Barbados'), + self::CODE_BRN->value => _('Brunej Darussalam'), + self::CODE_BTN->value => _('Bhútánské království'), + self::CODE_BVT->value => _('Bouvetův ostrov'), + self::CODE_BWA->value => _('Botswanská republika'), + self::CODE_CAF->value => _('Středoafrická republika'), + self::CODE_CAN->value => _('Kanada'), + self::CODE_CCK->value => _('Kokosové ostrovy'), + self::CODE_CHE->value => _('Švýcarská konfederace'), + self::CODE_CHL->value => _('Chilská republika'), + self::CODE_CHN->value => _('Čínská lidová republika'), + self::CODE_CIV->value => _('Republika Pobřeží slonoviny'), + self::CODE_CMR->value => _('Kamerunská republika'), + self::CODE_COD->value => _('Konžská demokratická republika'), + self::CODE_COG->value => _('Konžská republika'), + self::CODE_COK->value => _('Cookovy ostrovy'), + self::CODE_COL->value => _('Kolumbijská republika'), + self::CODE_COM->value => _('Komorský svaz'), + self::CODE_CPV->value => _('Kapverdská republika'), + self::CODE_CRI->value => _('Kostarická republika'), + self::CODE_CUB->value => _('Kubánská republika'), + self::CODE_CUW->value => _('Curaçao'), + self::CODE_CXR->value => _('Vánoční ostrov'), + self::CODE_CYM->value => _('Kajmanské ostrovy'), + self::CODE_CYP->value => _('Kyperská republika'), + self::CODE_CZE->value => _('Česká republika'), + self::CODE_DEU->value => _('Spolková republika Německo'), + self::CODE_DJI->value => _('Džibutská republika'), + self::CODE_DMA->value => _('Dominické společenství'), + self::CODE_DNK->value => _('Dánské království'), + self::CODE_DOM->value => _('Dominikánská republika'), + self::CODE_DZA->value => _('Alžírská lidová demokratická republika'), + self::CODE_ECU->value => _('Ekvádorská republika'), + self::CODE_EGY->value => _('Egyptská arabská republika'), + self::CODE_ERI->value => _('Eritrea'), + self::CODE_ESH->value => _('Západní Sahara'), + self::CODE_ESP->value => _('Španělské království'), + self::CODE_EST->value => _('Estonská republika'), + self::CODE_ETH->value => _('Etiopská federativní demokratická republika'), + self::CODE_FIN->value => _('Finská republika'), + self::CODE_FJI->value => _('Republika Fidžijské ostrovy'), + self::CODE_FLK->value => _('Falklandy (Malvíny)'), + self::CODE_FRA->value => _('Francouzská republika'), + self::CODE_FRO->value => _('Faerské ostrovy'), + self::CODE_FSM->value => _('Federativní státy Mikronésie'), + self::CODE_GAB->value => _('Gabonská republika'), + self::CODE_GBR->value => _('Spojené království Velké Británie a Severního Irska'), + self::CODE_GEO->value => _('Gruzie'), + self::CODE_GGY->value => _('Guernsey'), + self::CODE_GHA->value => _('Ghanská republika'), + self::CODE_GIB->value => _('Gibraltar'), + self::CODE_GIN->value => _('Guinejská republika'), + self::CODE_GLP->value => _('Guadeloupe'), + self::CODE_GMB->value => _('Gambijská republika'), + self::CODE_GNB->value => _('Republika Guinea-Bissau'), + self::CODE_GNQ->value => _('Republika Rovníková Guinea'), + self::CODE_GRC->value => _('Řecká republika'), + self::CODE_GRD->value => _('Grenada'), + self::CODE_GRL->value => _('Grónsko'), + self::CODE_GTM->value => _('Guatemalská republika'), + self::CODE_GUF->value => _('Francouzská Guyana'), + self::CODE_GUM->value => _('Guam'), + self::CODE_GUY->value => _('Guyanská republika'), + self::CODE_HKG->value => _('Hongkong, zvláštní administrativní oblast Čínské lidové republiky'), + self::CODE_HMD->value => _('Heardův ostrov a McDonaldovy ostrovy'), + self::CODE_HND->value => _('Honduraská republika'), + self::CODE_HRV->value => _('Chorvatská republika'), + self::CODE_HTI->value => _('Haitská republika'), + self::CODE_HUN->value => _('Maďarská republika'), + self::CODE_IDN->value => _('Indonéská republika'), + self::CODE_IMN->value => _('Ostrov Man'), + self::CODE_IND->value => _('Indická republika'), + self::CODE_IOT->value => _('Britské indickooceánské území'), + self::CODE_IRL->value => _('Irsko'), + self::CODE_IRN->value => _('Íránská islámská republika'), + self::CODE_IRQ->value => _('Irácká republika'), + self::CODE_ISL->value => _('Islandská republika'), + self::CODE_ISR->value => _('Izraelský stát'), + self::CODE_ITA->value => _('Italská republika'), + self::CODE_JAM->value => _('Jamajka'), + self::CODE_JEY->value => _('Jersey'), + self::CODE_JOR->value => _('Jordánské hášimovské království'), + self::CODE_JPN->value => _('Japonsko'), + self::CODE_KAZ->value => _('Republika Kazachstán'), + self::CODE_KEN->value => _('Keňská republika'), + self::CODE_KGZ->value => _('Republika Kyrgyzstán'), + self::CODE_KHM->value => _('Kambodžské království'), + self::CODE_KIR->value => _('Republika Kiribati'), + self::CODE_KNA->value => _('Svatý Kryštof a Nevis'), + self::CODE_KOR->value => _('Korejská republika'), + self::CODE_KWT->value => _('Kuvajtský stát'), + self::CODE_LAO->value => _('Laoská lidově demokratická republika'), + self::CODE_LBN->value => _('Libanonská republika'), + self::CODE_LBR->value => _('Liberijská republika'), + self::CODE_LBY->value => _('Libyjská arabská lidová socialistická džamáhírije'), + self::CODE_LCA->value => _('Svatá Lucie'), + self::CODE_LIE->value => _('Lichtenštejnské knížectví'), + self::CODE_LKA->value => _('Srílanská demokratická socialistická republika'), + self::CODE_LSO->value => _('Lesothské království'), + self::CODE_LTU->value => _('Litevská republika'), + self::CODE_LUX->value => _('Lucemburské velkovévodství'), + self::CODE_LVA->value => _('Lotyšská republika'), + self::CODE_MAC->value => _('Macao, zvláštní administrativní oblast Čínské lidové republiky'), + self::CODE_MAF->value => _('Svatý Martin (Francie)'), + self::CODE_MAR->value => _('Marocké království'), + self::CODE_MCO->value => _('Monacké knížectví'), + self::CODE_MDA->value => _('Moldavská republika'), + self::CODE_MDG->value => _('Madagaskarská republika'), + self::CODE_MDV->value => _('Maledivská republika'), + self::CODE_MEX->value => _('Spojené státy mexické'), + self::CODE_MHL->value => _('Republika Marshallovy ostrovy'), + self::CODE_MKD->value => _('Bývalá jugoslávská republika Makedonie'), + self::CODE_MLI->value => _('Maliská republika'), + self::CODE_MLT->value => _('Maltská republika'), + self::CODE_MMR->value => _('Myanmarský svaz'), + self::CODE_MNE->value => _('Republika Černá Hora'), + self::CODE_MNG->value => _('Mongolsko'), + self::CODE_MNP->value => _('Společenství Severních Marian'), + self::CODE_MOZ->value => _('Mosambická republika'), + self::CODE_MRT->value => _('Mauritánská islámská republika'), + self::CODE_MSR->value => _('Montserrat'), + self::CODE_MTQ->value => _('Martinik'), + self::CODE_MUS->value => _('Mauricijská republika'), + self::CODE_MWI->value => _('Malawská republika'), + self::CODE_MYS->value => _('Malajsie'), + self::CODE_MYT->value => _('Mayotte'), + self::CODE_NAM->value => _('Namibijská republika'), + self::CODE_NCL->value => _('Nová Kaledonie'), + self::CODE_NER->value => _('Nigerská republika'), + self::CODE_NFK->value => _('Norfolk'), + self::CODE_NGA->value => _('Nigérijská federativní republika'), + self::CODE_NIC->value => _('Nikaragujská republika'), + self::CODE_NIU->value => _('Niue'), + self::CODE_NLD->value => _('Nizozemské království'), + self::CODE_NOR->value => _('Norské království'), + self::CODE_NPL->value => _('Nepálské království'), + self::CODE_NRU->value => _('Nauruská republika'), + self::CODE_NZL->value => _('Nový Zéland'), + self::CODE_OMN->value => _('Sultanát Omán'), + self::CODE_PAK->value => _('Pákistánská islámská republika'), + self::CODE_PAN->value => _('Panamská republika'), + self::CODE_PCN->value => _('Pitcairn'), + self::CODE_PER->value => _('Peruánská republika'), + self::CODE_PHL->value => _('Filipínská republika'), + self::CODE_PLW->value => _('Palauská republika'), + self::CODE_PNG->value => _('Papua Nová Guinea'), + self::CODE_POL->value => _('Polská republika'), + self::CODE_PRI->value => _('Portoriko'), + self::CODE_PRK->value => _('Korejská lidově demokratická republika'), + self::CODE_PRT->value => _('Portugalská republika'), + self::CODE_PRY->value => _('Paraguayská republika'), + self::CODE_PSE->value => _('Palestinská samospráva'), + self::CODE_PYF->value => _('Francouzská Polynésie'), + self::CODE_QAT->value => _('Stát Katar'), + self::CODE_REU->value => _('Réunion'), + self::CODE_ROU->value => _('Rumunsko'), + self::CODE_RUS->value => _('Ruská federace'), + self::CODE_RWA->value => _('Rwandská republika'), + self::CODE_SAU->value => _('Saúdskoarabské království'), + self::CODE_SDN->value => _('Súdánská republika'), + self::CODE_SEN->value => _('Senegalská republika'), + self::CODE_SGP->value => _('Singapurská republika'), + self::CODE_SGS->value => _('Jižní Georgie a Jižní Sandwichovy ostrovy'), + self::CODE_SHN->value => _('Svatá Helena'), + self::CODE_SJM->value => _('Svalbard a ostrov Jan Mayen'), + self::CODE_SLB->value => _('Šalamounovy ostrovy'), + self::CODE_SLE->value => _('Republika Sierra Leone'), + self::CODE_SLV->value => _('Salvadorská republika'), + self::CODE_SMR->value => _('Sanmarinská republika'), + self::CODE_SOM->value => _('Somálská republika'), + self::CODE_SPM->value => _('Saint Pierre a Miquelon'), + self::CODE_SRB->value => _('Republika Srbsko'), + self::CODE_SSU->value => _('Jihosúdánská republika'), + self::CODE_STP->value => _('Demokratická republika Svatý Tomáš a Princův ostrov'), + self::CODE_SUR->value => _('Surinamská republika'), + self::CODE_SVK->value => _('Slovenská republika'), + self::CODE_SVN->value => _('Slovinská republika'), + self::CODE_SWE->value => _('Švédské království'), + self::CODE_SWZ->value => _('Svazijské království'), + self::CODE_SXM->value => _('Svatý Martin (Nizozemsko)'), + self::CODE_SYC->value => _('Seychelská republika'), + self::CODE_SYR->value => _('Syrská arabská republika'), + self::CODE_TCA->value => _('Turks a Caicos'), + self::CODE_TCD->value => _('Čadská republika'), + self::CODE_TGO->value => _('Tožská republika'), + self::CODE_THA->value => _('Thajské království'), + self::CODE_TJK->value => _('Republika Tádžikistán'), + self::CODE_TKL->value => _('Tokelau'), + self::CODE_TKM->value => _('Turkmenistán'), + self::CODE_TLS->value => _('Demokratická republika Východní Timor'), + self::CODE_TON->value => _('Království Tonga'), + self::CODE_TTO->value => _('Republika Trinidad a Tobago'), + self::CODE_TUN->value => _('Tuniská republika'), + self::CODE_TUR->value => _('Turecká republika'), + self::CODE_TUV->value => _('Tuvalu'), + self::CODE_TWN->value => _('Tchaj-wan, čínská provincie'), + self::CODE_TZA->value => _('Sjednocená republika Tanzanie'), + self::CODE_UGA->value => _('Ugandská republika'), + self::CODE_UKR->value => _('Ukrajina'), + self::CODE_UMI->value => _('Menší odlehlé ostrovy USA'), + self::CODE_URY->value => _('Uruguayská východní republika'), + self::CODE_USA->value => _('Spojené státy americké'), + self::CODE_UZB->value => _('Republika Uzbekistán'), + self::CODE_VAT->value => _('Svatý stolec (Vatikánský městský stát)'), + self::CODE_VCT->value => _('Svatý Vincenc a Grenadiny'), + self::CODE_VEN->value => _('Bolívarovská republika Venezuela'), + self::CODE_VGB->value => _('Britské Panenské ostrovy'), + self::CODE_VIR->value => _('Americké Panenské ostrovy'), + self::CODE_VNM->value => _('Vietnamská socialistická republika'), + self::CODE_VUT->value => _('Vanuatská republika'), + self::CODE_WLF->value => _('Wallis a Futuna'), + self::CODE_WSM->value => _('Nezávislý stát Samoa'), + self::CODE_XKX->value => _('Kosovská republika'), + self::CODE_YEM->value => _('Jemenská republika'), + self::CODE_ZAF->value => _('Jihoafrická republika'), + self::CODE_ZMB->value => _('Zambijská republika'), + self::CODE_ZWE->value => _('Zimbabwská republika'), + ]; + } + + /** + * @return string[] + */ + public static function getShortLabelDefinitions(): array + { + return [ + self::CODE_ABW->value => _('Aruba'), + self::CODE_AFG->value => _('Afghánistán'), + self::CODE_AGO->value => _('Angola'), + self::CODE_AIA->value => _('Anguilla'), + self::CODE_ALA->value => _('Alandské ostrovy'), + self::CODE_ALB->value => _('Albánie'), + self::CODE_AND->value => _('Andorra'), + self::CODE_ARE->value => _('Spojené arabské emiráty'), + self::CODE_ARG->value => _('Argentina'), + self::CODE_ARM->value => _('Arménie'), + self::CODE_ASM->value => _('Americká Samoa'), + self::CODE_ATA->value => _('Antarktida'), + self::CODE_ATF->value => _('Francouzská jižní území'), + self::CODE_ATG->value => _('Antigua a Barbuda'), + self::CODE_AUS->value => _('Austrálie'), + self::CODE_AUT->value => _('Rakousko'), + self::CODE_AZE->value => _('Ázerbájdžán'), + self::CODE_BDI->value => _('Burundi'), + self::CODE_BEL->value => _('Belgie'), + self::CODE_BEN->value => _('Benin'), + self::CODE_BFA->value => _('Burkina Faso'), + self::CODE_BGD->value => _('Bangladéš'), + self::CODE_BGR->value => _('Bulharsko'), + self::CODE_BHR->value => _('Bahrajn'), + self::CODE_BHS->value => _('Bahamy'), + self::CODE_BIH->value => _('Bosna a Hercegovina'), + self::CODE_BES->value => _('Karibské Nizozemsko'), + self::CODE_BLR->value => _('Bělorusko'), + self::CODE_BLZ->value => _('Belize'), + self::CODE_BMU->value => _('Bermudy'), + self::CODE_BOL->value => _('Bolívie'), + self::CODE_BRA->value => _('Brazílie'), + self::CODE_BRB->value => _('Barbados'), + self::CODE_BRN->value => _('Brunej Darussalam'), + self::CODE_BTN->value => _('Bhútán'), + self::CODE_BVT->value => _('Bouvetův ostrov'), + self::CODE_BWA->value => _('Botswana'), + self::CODE_CAF->value => _('Středoafrická republika'), + self::CODE_CAN->value => _('Kanada'), + self::CODE_CCK->value => _('Kokosové ostrovy'), + self::CODE_CHE->value => _('Švýcarsko'), + self::CODE_CHL->value => _('Chile'), + self::CODE_CHN->value => _('Čína'), + self::CODE_CIV->value => _('Pobřeží slonoviny'), + self::CODE_CMR->value => _('Kamerun'), + self::CODE_COD->value => _('Kongo, demokratická republika'), + self::CODE_COG->value => _('Kongo'), + self::CODE_COK->value => _('Cookovy ostrovy'), + self::CODE_COL->value => _('Kolumbie'), + self::CODE_COM->value => _('Komory'), + self::CODE_CPV->value => _('Kapverdy'), + self::CODE_CRI->value => _('Kostarika'), + self::CODE_CUB->value => _('Kuba'), + self::CODE_CUW->value => _('Curaçao'), + self::CODE_CXR->value => _('Vánoční ostrov'), + self::CODE_CYM->value => _('Kajmanské ostrovy'), + self::CODE_CYP->value => _('Kypr'), + self::CODE_CZE->value => _('Česko'), + self::CODE_DEU->value => _('Německo'), + self::CODE_DJI->value => _('Džibutsko'), + self::CODE_DMA->value => _('Dominika'), + self::CODE_DNK->value => _('Dánsko'), + self::CODE_DOM->value => _('Dominikánská republika'), + self::CODE_DZA->value => _('Alžírsko'), + self::CODE_ECU->value => _('Ekvádor'), + self::CODE_EGY->value => _('Egypt'), + self::CODE_ERI->value => _('Eritrea'), + self::CODE_ESH->value => _('Západní Sahara'), + self::CODE_ESP->value => _('Španělsko'), + self::CODE_EST->value => _('Estonsko'), + self::CODE_ETH->value => _('Etiopie'), + self::CODE_FIN->value => _('Finsko'), + self::CODE_FJI->value => _('Fidži'), + self::CODE_FLK->value => _('Falklandy (Malvíny)'), + self::CODE_FRA->value => _('Francie'), + self::CODE_FRO->value => _('Faerské ostrovy'), + self::CODE_FSM->value => _('Mikronésie'), + self::CODE_GAB->value => _('Gabon'), + self::CODE_GBR->value => _('Velká Británie'), + self::CODE_GEO->value => _('Gruzie'), + self::CODE_GGY->value => _('Guernsey'), + self::CODE_GHA->value => _('Ghana'), + self::CODE_GIB->value => _('Gibraltar'), + self::CODE_GIN->value => _('Guinea'), + self::CODE_GLP->value => _('Guadeloupe'), + self::CODE_GMB->value => _('Gambie'), + self::CODE_GNB->value => _('Guinea-Bissau'), + self::CODE_GNQ->value => _('Rovníková Guinea'), + self::CODE_GRC->value => _('Řecko'), + self::CODE_GRD->value => _('Grenada'), + self::CODE_GRL->value => _('Grónsko'), + self::CODE_GTM->value => _('Guatemala'), + self::CODE_GUF->value => _('Francouzská Guyana'), + self::CODE_GUM->value => _('Guam'), + self::CODE_GUY->value => _('Guyana'), + self::CODE_HKG->value => _('Hongkong'), + self::CODE_HMD->value => _('Heardův ostrov a McDonaldovy ostrovy'), + self::CODE_HND->value => _('Honduras'), + self::CODE_HRV->value => _('Chorvatsko'), + self::CODE_HTI->value => _('Haiti'), + self::CODE_HUN->value => _('Maďarsko'), + self::CODE_IDN->value => _('Indonésie'), + self::CODE_IMN->value => _('Ostrov Man'), + self::CODE_IND->value => _('Indie'), + self::CODE_IOT->value => _('Britské indickooceánské území'), + self::CODE_IRL->value => _('Irsko'), + self::CODE_IRN->value => _('Írán'), + self::CODE_IRQ->value => _('Irák'), + self::CODE_ISL->value => _('Island'), + self::CODE_ISR->value => _('Izrael'), + self::CODE_ITA->value => _('Itálie'), + self::CODE_JAM->value => _('Jamajka'), + self::CODE_JEY->value => _('Jersey'), + self::CODE_JOR->value => _('Jordánsko'), + self::CODE_JPN->value => _('Japonsko'), + self::CODE_KAZ->value => _('Kazachstán'), + self::CODE_KEN->value => _('Keňa'), + self::CODE_KGZ->value => _('Kyrgyzstán'), + self::CODE_KHM->value => _('Kambodža'), + self::CODE_KIR->value => _('Kiribati'), + self::CODE_KNA->value => _('Svatý Kryštof a Nevis'), + self::CODE_KOR->value => _('Jižní Korea'), + self::CODE_KWT->value => _('Kuvajt'), + self::CODE_LAO->value => _('Laos'), + self::CODE_LBN->value => _('Libanon'), + self::CODE_LBR->value => _('Libérie'), + self::CODE_LBY->value => _('Libye'), + self::CODE_LCA->value => _('Svatá Lucie'), + self::CODE_LIE->value => _('Lichtenštejnsko'), + self::CODE_LKA->value => _('Srí Lanka'), + self::CODE_LSO->value => _('Lesotho'), + self::CODE_LTU->value => _('Litva'), + self::CODE_LUX->value => _('Lucembursko'), + self::CODE_LVA->value => _('Lotyšsko'), + self::CODE_MAC->value => _('Macao'), + self::CODE_MAF->value => _('Svatý Martin (Francie)'), + self::CODE_MAR->value => _('Maroko'), + self::CODE_MCO->value => _('Monako'), + self::CODE_MDA->value => _('Moldavsko'), + self::CODE_MDG->value => _('Madagaskar'), + self::CODE_MDV->value => _('Maledivy'), + self::CODE_MEX->value => _('Mexiko'), + self::CODE_MHL->value => _('Marshallovy ostrovy'), + self::CODE_MKD->value => _('Makedonie'), + self::CODE_MLI->value => _('Mali'), + self::CODE_MLT->value => _('Malta'), + self::CODE_MMR->value => _('Myanmar'), + self::CODE_MNE->value => _('Černá Hora'), + self::CODE_MNG->value => _('Mongolsko'), + self::CODE_MNP->value => _('Severní Mariany'), + self::CODE_MOZ->value => _('Mosambik'), + self::CODE_MRT->value => _('Mauritánie'), + self::CODE_MSR->value => _('Montserrat'), + self::CODE_MTQ->value => _('Martinik'), + self::CODE_MUS->value => _('Mauricius'), + self::CODE_MWI->value => _('Malawi'), + self::CODE_MYS->value => _('Malajsie'), + self::CODE_MYT->value => _('Mayotte'), + self::CODE_NAM->value => _('Namibie'), + self::CODE_NCL->value => _('Nová Kaledonie'), + self::CODE_NER->value => _('Niger'), + self::CODE_NFK->value => _('Norfolk'), + self::CODE_NGA->value => _('Nigérie'), + self::CODE_NIC->value => _('Nikaragua'), + self::CODE_NIU->value => _('Niue'), + self::CODE_NLD->value => _('Nizozemsko'), + self::CODE_NOR->value => _('Norsko'), + self::CODE_NPL->value => _('Nepál'), + self::CODE_NRU->value => _('Nauru'), + self::CODE_NZL->value => _('Nový Zéland'), + self::CODE_OMN->value => _('Omán'), + self::CODE_PAK->value => _('Pákistán'), + self::CODE_PAN->value => _('Panama'), + self::CODE_PCN->value => _('Pitcairn'), + self::CODE_PER->value => _('Peru'), + self::CODE_PHL->value => _('Filipíny'), + self::CODE_PLW->value => _('Palau'), + self::CODE_PNG->value => _('Papua Nová Guinea'), + self::CODE_POL->value => _('Polsko'), + self::CODE_PRI->value => _('Portoriko'), + self::CODE_PRK->value => _('Severní Korea'), + self::CODE_PRT->value => _('Portugalsko'), + self::CODE_PRY->value => _('Paraguay'), + self::CODE_PSE->value => _('Palestina'), + self::CODE_PYF->value => _('Francouzská Polynésie'), + self::CODE_QAT->value => _('Katar'), + self::CODE_REU->value => _('Réunion'), + self::CODE_ROU->value => _('Rumunsko'), + self::CODE_RUS->value => _('Rusko'), + self::CODE_RWA->value => _('Rwanda'), + self::CODE_SAU->value => _('Saúdská Arábie'), + self::CODE_SDN->value => _('Súdán'), + self::CODE_SEN->value => _('Senegal'), + self::CODE_SGP->value => _('Singapur'), + self::CODE_SGS->value => _('Jižní Georgie a Jižní Sandwichovy ostrovy'), + self::CODE_SHN->value => _('Svatá Helena'), + self::CODE_SJM->value => _('Svalbard a ostrov Jan Mayen'), + self::CODE_SLB->value => _('Šalamounovy ostrovy'), + self::CODE_SLE->value => _('Sierra Leone'), + self::CODE_SLV->value => _('Salvador'), + self::CODE_SMR->value => _('San Marino'), + self::CODE_SOM->value => _('Somálsko'), + self::CODE_SPM->value => _('Saint Pierre a Miquelon'), + self::CODE_SRB->value => _('Srbsko'), + self::CODE_SSU->value => _('Jižní Súdán'), + self::CODE_STP->value => _('Svatý Tomáš'), + self::CODE_SUR->value => _('Surinam'), + self::CODE_SVK->value => _('Slovensko'), + self::CODE_SVN->value => _('Slovinsko'), + self::CODE_SWE->value => _('Švédsko'), + self::CODE_SWZ->value => _('Svazijsko'), + self::CODE_SXM->value => _('Svatý Martin (Nizozemsko)'), + self::CODE_SYC->value => _('Seychely'), + self::CODE_SYR->value => _('Sýrie'), + self::CODE_TCA->value => _('Turks a Caicos'), + self::CODE_TCD->value => _('Čad'), + self::CODE_TGO->value => _('Togo'), + self::CODE_THA->value => _('Thajsko'), + self::CODE_TJK->value => _('Tádžikistán'), + self::CODE_TKL->value => _('Tokelau'), + self::CODE_TKM->value => _('Turkmenistán'), + self::CODE_TLS->value => _('Východní Timor'), + self::CODE_TON->value => _('Tonga'), + self::CODE_TTO->value => _('Trinidad a Tobago'), + self::CODE_TUN->value => _('Tunisko'), + self::CODE_TUR->value => _('Turecko'), + self::CODE_TUV->value => _('Tuvalu'), + self::CODE_TWN->value => _('Tchaj-wan'), + self::CODE_TZA->value => _('Tanzanie'), + self::CODE_UGA->value => _('Uganda'), + self::CODE_UKR->value => _('Ukrajina'), + self::CODE_UMI->value => _('Menší odlehlé ostrovy USA'), + self::CODE_URY->value => _('Uruguay'), + self::CODE_USA->value => _('Spojené státy'), + self::CODE_UZB->value => _('Uzbekistán'), + self::CODE_VAT->value => _('Vatikán'), + self::CODE_VCT->value => _('Svatý Vincenc a Grenadiny'), + self::CODE_VEN->value => _('Venezuela'), + self::CODE_VGB->value => _('Britské Panenské ostrovy'), + self::CODE_VIR->value => _('Americké Panenské ostrovy'), + self::CODE_VNM->value => _('Vietnam'), + self::CODE_VUT->value => _('Vanuatu'), + self::CODE_WLF->value => _('Wallis a Futuna'), + self::CODE_WSM->value => _('Samoa'), + self::CODE_XKX->value => _('Kosovo'), + self::CODE_YEM->value => _('Jemen'), + self::CODE_ZAF->value => _('Jihoafrická republika'), + self::CODE_ZMB->value => _('Zambie'), + self::CODE_ZWE->value => _('Zimbabwe'), + ]; + } + + public function getIdent(): string + { + return match ($this) { + self::CODE_ABW => _('Aruba'), + self::CODE_AFG => _('Afghánistán'), + self::CODE_AGO => _('Angola'), + self::CODE_AIA => _('Anguilla'), + self::CODE_ALA => _('Alandské ostrovy'), + self::CODE_ALB => _('Albánie'), + self::CODE_AND => _('Andorra'), + self::CODE_ARE => _('Spojené arabské emiráty'), + self::CODE_ARG => _('Argentina'), + self::CODE_ARM => _('Arménie'), + self::CODE_ASM => _('Americká Samoa'), + self::CODE_ATA => _('Antarktida'), + self::CODE_ATF => _('Francouzská jižní území'), + self::CODE_ATG => _('Antigua a Barbuda'), + self::CODE_AUS => _('Austrálie'), + self::CODE_AUT => _('Rakousko'), + self::CODE_AZE => _('Ázerbájdžán'), + self::CODE_BDI => _('Burundi'), + self::CODE_BEL => _('Belgie'), + self::CODE_BEN => _('Benin'), + self::CODE_BFA => _('Burkina Faso'), + self::CODE_BGD => _('Bangladéš'), + self::CODE_BGR => _('Bulharsko'), + self::CODE_BHR => _('Bahrajn'), + self::CODE_BHS => _('Bahamy'), + self::CODE_BIH => _('Bosna a Hercegovina'), + self::CODE_BES => _('Karibské Nizozemsko'), + self::CODE_BLR => _('Bělorusko'), + self::CODE_BLZ => _('Belize'), + self::CODE_BMU => _('Bermudy'), + self::CODE_BOL => _('Bolívie'), + self::CODE_BRA => _('Brazílie'), + self::CODE_BRB => _('Barbados'), + self::CODE_BRN => _('Brunej Darussalam'), + self::CODE_BTN => _('Bhútán'), + self::CODE_BVT => _('Bouvetův ostrov'), + self::CODE_BWA => _('Botswana'), + self::CODE_CAF => _('Středoafrická republika'), + self::CODE_CAN => _('Kanada'), + self::CODE_CCK => _('Kokosové ostrovy'), + self::CODE_CHE => _('Švýcarsko'), + self::CODE_CHL => _('Chile'), + self::CODE_CHN => _('Čína'), + self::CODE_CIV => _('Pobřeží slonoviny'), + self::CODE_CMR => _('Kamerun'), + self::CODE_COD => _('Kongo, demokratická republika'), + self::CODE_COG => _('Kongo'), + self::CODE_COK => _('Cookovy ostrovy'), + self::CODE_COL => _('Kolumbie'), + self::CODE_COM => _('Komory'), + self::CODE_CPV => _('Kapverdy'), + self::CODE_CRI => _('Kostarika'), + self::CODE_CUB => _('Kuba'), + self::CODE_CUW => _('Curaçao'), + self::CODE_CXR => _('Vánoční ostrov'), + self::CODE_CYM => _('Kajmanské ostrovy'), + self::CODE_CYP => _('Kypr'), + self::CODE_CZE => _('Česko'), + self::CODE_DEU => _('Německo'), + self::CODE_DJI => _('Džibutsko'), + self::CODE_DMA => _('Dominika'), + self::CODE_DNK => _('Dánsko'), + self::CODE_DOM => _('Dominikánská republika'), + self::CODE_DZA => _('Alžírsko'), + self::CODE_ECU => _('Ekvádor'), + self::CODE_EGY => _('Egypt'), + self::CODE_ERI => _('Eritrea'), + self::CODE_ESH => _('Západní Sahara'), + self::CODE_ESP => _('Španělsko'), + self::CODE_EST => _('Estonsko'), + self::CODE_ETH => _('Etiopie'), + self::CODE_FIN => _('Finsko'), + self::CODE_FJI => _('Fidži'), + self::CODE_FLK => _('Falklandy (Malvíny)'), + self::CODE_FRA => _('Francie'), + self::CODE_FRO => _('Faerské ostrovy'), + self::CODE_FSM => _('Mikronésie'), + self::CODE_GAB => _('Gabon'), + self::CODE_GBR => _('Velká Británie'), + self::CODE_GEO => _('Gruzie'), + self::CODE_GGY => _('Guernsey'), + self::CODE_GHA => _('Ghana'), + self::CODE_GIB => _('Gibraltar'), + self::CODE_GIN => _('Guinea'), + self::CODE_GLP => _('Guadeloupe'), + self::CODE_GMB => _('Gambie'), + self::CODE_GNB => _('Guinea-Bissau'), + self::CODE_GNQ => _('Rovníková Guinea'), + self::CODE_GRC => _('Řecko'), + self::CODE_GRD => _('Grenada'), + self::CODE_GRL => _('Grónsko'), + self::CODE_GTM => _('Guatemala'), + self::CODE_GUF => _('Francouzská Guyana'), + self::CODE_GUM => _('Guam'), + self::CODE_GUY => _('Guyana'), + self::CODE_HKG => _('Hongkong'), + self::CODE_HMD => _('Heardův ostrov a McDonaldovy ostrovy'), + self::CODE_HND => _('Honduras'), + self::CODE_HRV => _('Chorvatsko'), + self::CODE_HTI => _('Haiti'), + self::CODE_HUN => _('Maďarsko'), + self::CODE_IDN => _('Indonésie'), + self::CODE_IMN => _('Ostrov Man'), + self::CODE_IND => _('Indie'), + self::CODE_IOT => _('Britské indickooceánské území'), + self::CODE_IRL => _('Irsko'), + self::CODE_IRN => _('Írán'), + self::CODE_IRQ => _('Irák'), + self::CODE_ISL => _('Island'), + self::CODE_ISR => _('Izrael'), + self::CODE_ITA => _('Itálie'), + self::CODE_JAM => _('Jamajka'), + self::CODE_JEY => _('Jersey'), + self::CODE_JOR => _('Jordánsko'), + self::CODE_JPN => _('Japonsko'), + self::CODE_KAZ => _('Kazachstán'), + self::CODE_KEN => _('Keňa'), + self::CODE_KGZ => _('Kyrgyzstán'), + self::CODE_KHM => _('Kambodža'), + self::CODE_KIR => _('Kiribati'), + self::CODE_KNA => _('Svatý Kryštof a Nevis'), + self::CODE_KOR => _('Jižní Korea'), + self::CODE_KWT => _('Kuvajt'), + self::CODE_LAO => _('Laos'), + self::CODE_LBN => _('Libanon'), + self::CODE_LBR => _('Libérie'), + self::CODE_LBY => _('Libye'), + self::CODE_LCA => _('Svatá Lucie'), + self::CODE_LIE => _('Lichtenštejnsko'), + self::CODE_LKA => _('Srí Lanka'), + self::CODE_LSO => _('Lesotho'), + self::CODE_LTU => _('Litva'), + self::CODE_LUX => _('Lucembursko'), + self::CODE_LVA => _('Lotyšsko'), + self::CODE_MAC => _('Macao'), + self::CODE_MAF => _('Svatý Martin (Francie)'), + self::CODE_MAR => _('Maroko'), + self::CODE_MCO => _('Monako'), + self::CODE_MDA => _('Moldavsko'), + self::CODE_MDG => _('Madagaskar'), + self::CODE_MDV => _('Maledivy'), + self::CODE_MEX => _('Mexiko'), + self::CODE_MHL => _('Marshallovy ostrovy'), + self::CODE_MKD => _('Makedonie'), + self::CODE_MLI => _('Mali'), + self::CODE_MLT => _('Malta'), + self::CODE_MMR => _('Myanmar'), + self::CODE_MNE => _('Černá Hora'), + self::CODE_MNG => _('Mongolsko'), + self::CODE_MNP => _('Severní Mariany'), + self::CODE_MOZ => _('Mosambik'), + self::CODE_MRT => _('Mauritánie'), + self::CODE_MSR => _('Montserrat'), + self::CODE_MTQ => _('Martinik'), + self::CODE_MUS => _('Mauricius'), + self::CODE_MWI => _('Malawi'), + self::CODE_MYS => _('Malajsie'), + self::CODE_MYT => _('Mayotte'), + self::CODE_NAM => _('Namibie'), + self::CODE_NCL => _('Nová Kaledonie'), + self::CODE_NER => _('Niger'), + self::CODE_NFK => _('Norfolk'), + self::CODE_NGA => _('Nigérie'), + self::CODE_NIC => _('Nikaragua'), + self::CODE_NIU => _('Niue'), + self::CODE_NLD => _('Nizozemsko'), + self::CODE_NOR => _('Norsko'), + self::CODE_NPL => _('Nepál'), + self::CODE_NRU => _('Nauru'), + self::CODE_NZL => _('Nový Zéland'), + self::CODE_OMN => _('Omán'), + self::CODE_PAK => _('Pákistán'), + self::CODE_PAN => _('Panama'), + self::CODE_PCN => _('Pitcairn'), + self::CODE_PER => _('Peru'), + self::CODE_PHL => _('Filipíny'), + self::CODE_PLW => _('Palau'), + self::CODE_PNG => _('Papua Nová Guinea'), + self::CODE_POL => _('Polsko'), + self::CODE_PRI => _('Portoriko'), + self::CODE_PRK => _('Severní Korea'), + self::CODE_PRT => _('Portugalsko'), + self::CODE_PRY => _('Paraguay'), + self::CODE_PSE => _('Palestina'), + self::CODE_PYF => _('Francouzská Polynésie'), + self::CODE_QAT => _('Katar'), + self::CODE_REU => _('Réunion'), + self::CODE_ROU => _('Rumunsko'), + self::CODE_RUS => _('Rusko'), + self::CODE_RWA => _('Rwanda'), + self::CODE_SAU => _('Saúdská Arábie'), + self::CODE_SDN => _('Súdán'), + self::CODE_SEN => _('Senegal'), + self::CODE_SGP => _('Singapur'), + self::CODE_SGS => _('Jižní Georgie a Jižní Sandwichovy ostrovy'), + self::CODE_SHN => _('Svatá Helena'), + self::CODE_SJM => _('Svalbard a ostrov Jan Mayen'), + self::CODE_SLB => _('Šalamounovy ostrovy'), + self::CODE_SLE => _('Sierra Leone'), + self::CODE_SLV => _('Salvador'), + self::CODE_SMR => _('San Marino'), + self::CODE_SOM => _('Somálsko'), + self::CODE_SPM => _('Saint Pierre a Miquelon'), + self::CODE_SRB => _('Srbsko'), + self::CODE_SSU => _('Jižní Súdán'), + self::CODE_STP => _('Svatý Tomáš'), + self::CODE_SUR => _('Surinam'), + self::CODE_SVK => _('Slovensko'), + self::CODE_SVN => _('Slovinsko'), + self::CODE_SWE => _('Švédsko'), + self::CODE_SWZ => _('Svazijsko'), + self::CODE_SXM => _('Svatý Martin (Nizozemsko)'), + self::CODE_SYC => _('Seychely'), + self::CODE_SYR => _('Sýrie'), + self::CODE_TCA => _('Turks a Caicos'), + self::CODE_TCD => _('Čad'), + self::CODE_TGO => _('Togo'), + self::CODE_THA => _('Thajsko'), + self::CODE_TJK => _('Tádžikistán'), + self::CODE_TKL => _('Tokelau'), + self::CODE_TKM => _('Turkmenistán'), + self::CODE_TLS => _('Východní Timor'), + self::CODE_TON => _('Tonga'), + self::CODE_TTO => _('Trinidad a Tobago'), + self::CODE_TUN => _('Tunisko'), + self::CODE_TUR => _('Turecko'), + self::CODE_TUV => _('Tuvalu'), + self::CODE_TWN => _('Tchaj-wan'), + self::CODE_TZA => _('Tanzanie'), + self::CODE_UGA => _('Uganda'), + self::CODE_UKR => _('Ukrajina'), + self::CODE_UMI => _('Menší odlehlé ostrovy USA'), + self::CODE_URY => _('Uruguay'), + self::CODE_USA => _('Spojené státy'), + self::CODE_UZB => _('Uzbekistán'), + self::CODE_VAT => _('Vatikán'), + self::CODE_VCT => _('Svatý Vincenc a Grenadiny'), + self::CODE_VEN => _('Venezuela'), + self::CODE_VGB => _('Britské Panenské ostrovy'), + self::CODE_VIR => _('Americké Panenské ostrovy'), + self::CODE_VNM => _('Vietnam'), + self::CODE_VUT => _('Vanuatu'), + self::CODE_WLF => _('Wallis a Futuna'), + self::CODE_WSM => _('Samoa'), + self::CODE_XKX => _('Kosovo'), + self::CODE_YEM => _('Jemen'), + self::CODE_ZAF => _('Jihoafrická republika'), + self::CODE_ZMB => _('Zambie'), + self::CODE_ZWE => _('Zimbabwe'), + }; + } + +} diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php73.php b/tests/PHPStan/Analyser/data/mb-strlen-php73.php new file mode 100644 index 0000000000..45fad0364b --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-strlen-php73.php @@ -0,0 +1,56 @@ += 7.3 + +namespace MbStrlenPhp73; + +use function PHPStan\Testing\assertType; + +class MbStrlenPhp73 +{ + + /** + * @param non-empty-string $nonEmpty + * @param 'utf-8'|'8bit' $utf8And8bit + * @param 'utf-8'|'foo' $utf8AndInvalidEncoding + * @param '1'|'2'|'5'|'10' $constUnion + * @param 1|2|5|10|123|'1234'|false $constUnionMixed + * @param int|float $intFloat + * @param non-empty-string|int|float $nonEmptyStringIntFloat + * @param ""|false|null $emptyStringFalseNull + * @param ""|bool|null $emptyStringBoolNull + * @param "pass"|"none" $encodingsValidOnlyUntilPhp72 + */ + public function doFoo(int $i, string $s, bool $bool, float $float, $intFloat, $nonEmpty, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull, $constUnion, $constUnionMixed, $utf8And8bit, $utf8AndInvalidEncoding, string $unknownEncoding, $encodingsValidOnlyUntilPhp72) + { + assertType('0', mb_strlen('')); + assertType('5', mb_strlen('hallo')); + assertType('int<0, 1>', mb_strlen($bool)); + assertType('int<1, max>', mb_strlen($i)); + assertType('int<0, max>', mb_strlen($s)); + assertType('int<1, max>', mb_strlen($nonEmpty)); + assertType('int<1, 2>', mb_strlen($constUnion)); + assertType('int<0, 4>', mb_strlen($constUnionMixed)); + assertType('3', mb_strlen(123)); + assertType('1', mb_strlen(true)); + assertType('0', mb_strlen(false)); + assertType('0', mb_strlen(null)); + assertType('1', mb_strlen(1.0)); + assertType('4', mb_strlen(1.23)); + assertType('int<1, max>', mb_strlen($float)); + assertType('int<1, max>', mb_strlen($intFloat)); + assertType('int<1, max>', mb_strlen($nonEmptyStringIntFloat)); + assertType('0', mb_strlen($emptyStringFalseNull)); + assertType('int<0, 1>', mb_strlen($emptyStringBoolNull)); + assertType('8', mb_strlen('паляниця', 'utf-8')); + assertType('11', mb_strlen('alias test🤔', 'utf8')); + assertType('false', mb_strlen('', 'invalid encoding')); + assertType('int<5, 6>', mb_strlen('école', $utf8And8bit)); + assertType('5|false', mb_strlen('école', $utf8AndInvalidEncoding)); + assertType('1|3|5|6|false', mb_strlen('école', $unknownEncoding)); + assertType('2|4|5|6|8|false', mb_strlen('מזגן', $unknownEncoding)); + assertType('6|8|12|13|15|18|24|false', mb_strlen('いい天気ですね〜', $unknownEncoding)); + assertType('3|false', mb_strlen(123, $utf8AndInvalidEncoding)); + assertType('false', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); + } + +} + diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php8.php b/tests/PHPStan/Analyser/data/mb-strlen-php8.php new file mode 100644 index 0000000000..3fb7f73706 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-strlen-php8.php @@ -0,0 +1,55 @@ += 8.0 + +namespace MbStrlenPhp8; + +use function PHPStan\Testing\assertType; + +class MbStrlenPhp8 +{ + + /** + * @param non-empty-string $nonEmpty + * @param 'utf-8'|'8bit' $utf8And8bit + * @param 'utf-8'|'foo' $utf8AndInvalidEncoding + * @param '1'|'2'|'5'|'10' $constUnion + * @param 1|2|5|10|123|'1234'|false $constUnionMixed + * @param int|float $intFloat + * @param non-empty-string|int|float $nonEmptyStringIntFloat + * @param ""|false|null $emptyStringFalseNull + * @param ""|bool|null $emptyStringBoolNull + * @param "pass"|"none" $encodingsValidOnlyUntilPhp72 + */ + public function doFoo(int $i, string $s, bool $bool, float $float, $intFloat, $nonEmpty, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull, $constUnion, $constUnionMixed, $utf8And8bit, $utf8AndInvalidEncoding, string $unknownEncoding, $encodingsValidOnlyUntilPhp72) + { + assertType('0', mb_strlen('')); + assertType('5', mb_strlen('hallo')); + assertType('int<0, 1>', mb_strlen($bool)); + assertType('int<1, max>', mb_strlen($i)); + assertType('int<0, max>', mb_strlen($s)); + assertType('int<1, max>', mb_strlen($nonEmpty)); + assertType('int<1, 2>', mb_strlen($constUnion)); + assertType('int<0, 4>', mb_strlen($constUnionMixed)); + assertType('3', mb_strlen(123)); + assertType('1', mb_strlen(true)); + assertType('0', mb_strlen(false)); + assertType('0', mb_strlen(null)); + assertType('1', mb_strlen(1.0)); + assertType('4', mb_strlen(1.23)); + assertType('int<1, max>', mb_strlen($float)); + assertType('int<1, max>', mb_strlen($intFloat)); + assertType('int<1, max>', mb_strlen($nonEmptyStringIntFloat)); + assertType('0', mb_strlen($emptyStringFalseNull)); + assertType('int<0, 1>', mb_strlen($emptyStringBoolNull)); + assertType('8', mb_strlen('паляниця', 'utf-8')); + assertType('11', mb_strlen('alias test🤔', 'utf8')); + assertType('*NEVER*', mb_strlen('', 'invalid encoding')); + assertType('int<5, 6>', mb_strlen('école', $utf8And8bit)); + assertType('5', mb_strlen('école', $utf8AndInvalidEncoding)); + assertType('1|3|5|6', mb_strlen('école', $unknownEncoding)); + assertType('2|4|5|6|8', mb_strlen('מזגן', $unknownEncoding)); + assertType('6|8|12|13|15|18|24', mb_strlen('いい天気ですね〜', $unknownEncoding)); + assertType('3', mb_strlen(123, $utf8AndInvalidEncoding)); + assertType('*NEVER*', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); + } + +} diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php82.php b/tests/PHPStan/Analyser/data/mb-strlen-php82.php new file mode 100644 index 0000000000..7424e938a4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-strlen-php82.php @@ -0,0 +1,57 @@ += 8.2 + +declare(strict_types=1); + +namespace MbStrlenPhp82; + +use function PHPStan\Testing\assertType; + +class MbStrlenPhp82 +{ + + /** + * @param non-empty-string $nonEmpty + * @param 'utf-8'|'8bit' $utf8And8bit + * @param 'utf-8'|'foo' $utf8AndInvalidEncoding + * @param '1'|'2'|'5'|'10' $constUnion + * @param 1|2|5|10|123|'1234'|false $constUnionMixed + * @param int|float $intFloat + * @param non-empty-string|int|float $nonEmptyStringIntFloat + * @param ""|false|null $emptyStringFalseNull + * @param ""|bool|null $emptyStringBoolNull + * @param "pass"|"none" $encodingsValidOnlyUntilPhp72 + */ + public function doFoo(int $i, string $s, bool $bool, float $float, $intFloat, $nonEmpty, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull, $constUnion, $constUnionMixed, $utf8And8bit, $utf8AndInvalidEncoding, string $unknownEncoding, $encodingsValidOnlyUntilPhp72) + { + assertType('0', mb_strlen('')); + assertType('5', mb_strlen('hallo')); + assertType('int<0, 1>', mb_strlen($bool)); + assertType('int<1, max>', mb_strlen($i)); + assertType('int<0, max>', mb_strlen($s)); + assertType('int<1, max>', mb_strlen($nonEmpty)); + assertType('int<1, 2>', mb_strlen($constUnion)); + assertType('int<0, 4>', mb_strlen($constUnionMixed)); + assertType('3', mb_strlen(123)); + assertType('1', mb_strlen(true)); + assertType('0', mb_strlen(false)); + assertType('0', mb_strlen(null)); + assertType('1', mb_strlen(1.0)); + assertType('4', mb_strlen(1.23)); + assertType('int<1, max>', mb_strlen($float)); + assertType('int<1, max>', mb_strlen($intFloat)); + assertType('int<1, max>', mb_strlen($nonEmptyStringIntFloat)); + assertType('0', mb_strlen($emptyStringFalseNull)); + assertType('int<0, 1>', mb_strlen($emptyStringBoolNull)); + assertType('8', mb_strlen('паляниця', 'utf-8')); + assertType('11', mb_strlen('alias test🤔', 'utf8')); + assertType('*NEVER*', mb_strlen('', 'invalid encoding')); + assertType('int<5, 6>', mb_strlen('école', $utf8And8bit)); + assertType('5', mb_strlen('école', $utf8AndInvalidEncoding)); + assertType('1|3|5|6', mb_strlen('école', $unknownEncoding)); + assertType('2|4|5|8', mb_strlen('מזגן', $unknownEncoding)); + assertType('6|8|12|13|15|24', mb_strlen('いい天気ですね〜', $unknownEncoding)); + assertType('3', mb_strlen(123, $utf8AndInvalidEncoding)); + assertType('*NEVER*', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); + } + +} diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character-php71.php b/tests/PHPStan/Analyser/data/mb_substitute_character-php71.php deleted file mode 100644 index 0ba0e9ab4e..0000000000 --- a/tests/PHPStan/Analyser/data/mb_substitute_character-php71.php +++ /dev/null @@ -1,21 +0,0 @@ -', 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/method-phpDocs-inheritdoc-without-curly-braces.php b/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php index c0dff8a645..254d321d7f 100644 --- a/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php +++ b/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php @@ -5,7 +5,7 @@ use SomeNamespace\Amet as Dolor; use SomeNamespace\Consecteur; -class FooInheritDocChild extends Foo +class FooInheritDocChildWithoutCurly extends Foo { /** diff --git a/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php b/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php new file mode 100644 index 0000000000..5a44bfa784 --- /dev/null +++ b/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php @@ -0,0 +1,174 @@ +doFluentUnionIterable() as $fluentUnionIterableBaz) { + die; + } + } + + /** + * @phan-return self[] + */ + public function doBar(): array + { + + } + + public function returnParent(): parent + { + + } + + /** + * @phan-return parent + */ + public function returnPhpDocParent() + { + + } + + /** + * @phan-return NULL[] + */ + public function returnNulls(): array + { + + } + + public function returnObject(): object + { + + } + + public function phpDocVoidMethod(): self + { + + } + + public function phpDocVoidMethodFromInterface(): self + { + + } + + public function phpDocVoidParentMethod(): self + { + + } + + public function phpDocWithoutCurlyBracesVoidParentMethod(): self + { + + } + + /** + * @phan-return string[] + */ + public function returnsStringArray(): array + { + + } + + private function privateMethodWithPhpDoc() + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/minmax-arrays.php b/tests/PHPStan/Analyser/data/minmax-arrays.php deleted file mode 100644 index cffe5de439..0000000000 --- a/tests/PHPStan/Analyser/data/minmax-arrays.php +++ /dev/null @@ -1,152 +0,0 @@ - 0) { - assertType('int', min($ints)); - assertType('int', max($ints)); - } else { - assertType('false', min($ints)); - assertType('false', max($ints)); - } - if (count($ints) >= 1) { - assertType('int', min($ints)); - assertType('int', max($ints)); - } else { - assertType('false', min($ints)); - assertType('false', max($ints)); - } - if (count($ints) >= 2) { - assertType('int', min($ints)); - assertType('int', max($ints)); - } else { - assertType('int|false', min($ints)); - assertType('int|false', max($ints)); - } - if (count($ints) <= 0) { - assertType('false', min($ints)); - assertType('false', max($ints)); - } else { - assertType('int', min($ints)); - assertType('int', max($ints)); - } - if (count($ints) < 1) { - assertType('false', min($ints)); - assertType('false', max($ints)); - } else { - assertType('int', min($ints)); - assertType('int', max($ints)); - } - if (count($ints) < 2) { - assertType('int|false', min($ints)); - assertType('int|false', max($ints)); - } else { - assertType('int', min($ints)); - assertType('int', max($ints)); - } -} - -/** - * @param int[] $ints - */ -function dummy3(array $ints): void -{ - assertType('int|false', min($ints)); - assertType('int|false', max($ints)); -} - - -function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void -{ - assertType('array{0: DateTimeInterface, 1?: DateTimeInterface}', array_filter([$dateA, $dateB])); - assertType('DateTimeInterface', min(array_filter([$dateA, $dateB]))); - assertType('DateTimeInterface', max(array_filter([$dateA, $dateB]))); - assertType('array{0?: DateTimeInterface}', array_filter([$dateB])); - assertType('DateTimeInterface|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])); -} - -function dummy6(string $s, string $t): void { - 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/native-types.php b/tests/PHPStan/Analyser/data/native-types.php deleted file mode 100644 index 86f0ad7cd1..0000000000 --- a/tests/PHPStan/Analyser/data/native-types.php +++ /dev/null @@ -1,204 +0,0 @@ - $array - */ - public function doForeach(array $array): void - { - assertType('array', $array); - assertNativeType('array', $array); - - foreach ($array as $key => $value) { - assertType('non-empty-array', $array); - assertNativeType('non-empty-array', $array); - - assertType('string', $key); - assertNativeType('(int|string)', $key); - - assertType('int', $value); - assertNativeType('mixed', $value); - } - } - - /** - * @param self $foo - */ - public function doCatch($foo): void - { - assertType(Foo::class, $foo); - assertNativeType('mixed', $foo); - - try { - throw new \Exception(); - } catch (\InvalidArgumentException $foo) { - assertType(\InvalidArgumentException::class, $foo); - assertNativeType(\InvalidArgumentException::class, $foo); - } catch (\Exception $e) { - assertType('Exception~InvalidArgumentException', $e); - assertNativeType('Exception~InvalidArgumentException', $e); - - assertType(Foo::class, $foo); - assertNativeType('mixed', $foo); - } - } - - /** - * @param array $array - */ - public function doForeachArrayDestructuring(array $array) - { - assertType('array', $array); - assertNativeType('array', $array); - foreach ($array as $key => [$i, $s]) { - assertType('non-empty-array', $array); - assertNativeType('non-empty-array', $array); - - assertType('string', $key); - assertNativeType('(int|string)', $key); - - assertType('int', $i); - // assertNativeType('mixed', $i); - - assertType('string', $s); - // assertNativeType('mixed', $s); - } - } - - /** - * @param \DateTimeImmutable $date - */ - public function doIfElse(\DateTimeInterface $date): void - { - if ($date instanceof \DateTimeInterface) { - assertType(\DateTimeImmutable::class, $date); - assertNativeType(\DateTimeInterface::class, $date); - } else { - assertType('*NEVER*', $date); - assertNativeType('*NEVER*', $date); - } - - assertType(\DateTimeImmutable::class, $date); - assertNativeType(\DateTimeInterface::class, $date); - - if ($date instanceof \DateTimeImmutable) { - assertType(\DateTimeImmutable::class, $date); - assertNativeType(\DateTimeImmutable::class, $date); - } else { - assertType('*NEVER*', $date); - assertNativeType('DateTime', $date); - } - - assertType(\DateTimeImmutable::class, $date); - assertNativeType(\DateTimeImmutable::class, $date); // could be DateTimeInterface - - if ($date instanceof \DateTime) { - - } - } - -} - -/** - * @param Foo $foo - * @param \DateTimeImmutable $dateTime - * @param \DateTimeImmutable $dateTimeMutable - * @param string $nullableString - * @param string|null $nonNullableString - */ -function fooFunction( - $foo, - \DateTimeInterface $dateTime, - \DateTime $dateTimeMutable, - ?string $nullableString, - string $nonNullableString -): void -{ - assertType(Foo::class, $foo); - assertNativeType('mixed', $foo); - - assertType(\DateTimeImmutable::class, $dateTime); - assertNativeType(\DateTimeInterface::class, $dateTime); - - assertType(\DateTime::class, $dateTimeMutable); - assertNativeType(\DateTime::class, $dateTimeMutable); - - assertType('string|null', $nullableString); - assertNativeType('string|null', $nullableString); - - assertType('string', $nonNullableString); - assertNativeType('string', $nonNullableString); -} diff --git a/tests/PHPStan/Analyser/data/nested-functions.php b/tests/PHPStan/Analyser/data/nested-functions.php index 1d12b75157..b33ed3150a 100644 --- a/tests/PHPStan/Analyser/data/nested-functions.php +++ b/tests/PHPStan/Analyser/data/nested-functions.php @@ -12,8 +12,7 @@ public function doFoo(): self } -function () { - $foo = new Foo(); +function (Foo $foo) { $foo->doFoo() ->doFoo() ->doFoo() diff --git a/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php b/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php index 4fc08b50be..c46e1b2a91 100644 --- a/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php +++ b/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php @@ -5,3 +5,8 @@ use function PHPStan\Testing\assertType; assertType('stdClass', TEST_OBJECT_CONSTANT); +assertType('null', TEST_NULL_CONSTANT); +assertType('true', TEST_TRUE_CONSTANT); +assertType('false', TEST_FALSE_CONSTANT); +assertType('array{true, false, null}', TEST_ARRAY_CONSTANT); +assertType('EnumTypeAssertions\\Foo::ONE', TEST_ENUM_CONSTANT); diff --git a/tests/PHPStan/Analyser/data/non-empty-array.php b/tests/PHPStan/Analyser/data/non-empty-array.php deleted file mode 100644 index 2e3a24f305..0000000000 --- a/tests/PHPStan/Analyser/data/non-empty-array.php +++ /dev/null @@ -1,55 +0,0 @@ - $arrayOfStrings - * @param non-empty-list<\stdClass> $listOfStd - * @param non-empty-list<\stdClass> $listOfStd2 - * @param non-empty-list $invalidList - */ - public function doFoo( - array $array, - array $list, - array $arrayOfStrings, - array $listOfStd, - $listOfStd2, - array $invalidList, - $invalidList2 - ): void - { - 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.php b/tests/PHPStan/Analyser/data/non-empty-string.php deleted file mode 100644 index 03969d8cb9..0000000000 --- a/tests/PHPStan/Analyser/data/non-empty-string.php +++ /dev/null @@ -1,376 +0,0 @@ - 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/param-closure-this-stubs.php b/tests/PHPStan/Analyser/data/param-closure-this-stubs.php new file mode 100644 index 0000000000..61b0ab9b59 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-closure-this-stubs.php @@ -0,0 +1,60 @@ +transactional(function () { + assertType(EntityManagerParamClosureThis::class, $this); + }); + } + + public function doFoo2(): void + { + \MyFunctionClosureThis\doFoo(function () { + assertType(\MyFunctionClosureThis\Foo::class, $this); + }); + } + + public function doFoo3(array $a): void + { + uksort($a, function () { + assertType(\stdClass::class, $this); + }); + } + + /** + * @param \Ds\Deque $deque + */ + public function doFoo4(\Ds\Deque $deque): void + { + $deque->filter(function () { + assertType('Ds\Deque', $this); + }); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/param-closure-this-stubs.stub b/tests/PHPStan/Analyser/data/param-closure-this-stubs.stub new file mode 100644 index 0000000000..ec35c140a2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-closure-this-stubs.stub @@ -0,0 +1,54 @@ + + * @param-closure-this $this $callback + */ + public function filter(callable $callback = null): Deque + { + } + } +} diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php new file mode 100644 index 0000000000..e34e1c9082 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -0,0 +1,503 @@ + + */ +class ExtendsFooBar extends FooBar { + /** + * @param-out string $s + */ + function subMethod(?string &$s): void + { + } + + /** + * @param-out string $s + */ + function overriddenMethod(?string &$s): void + { + } + + function overriddenButinheritedPhpDocMethod(?string &$s): void + { + } + + public function renamedParams(int $x, int &$y) { + parent::renamedParams($x, $y); + } + + /** + * @param-out array $b + */ + public function paramOutOverridden(int $a, int &$b) { + } + +} + +class OutFromStub { + function stringOut(string &$string): void + { + } +} + +/** + * @param-out bool $s + */ +function takesNullableBool(?bool &$s) : void { + $s = true; +} + +/** + * @param-out int $var + */ +function variadicFoo(&...$var): void +{ + $var[0] = 2; + $var[1] = 2; +} + +/** + * @param-out string $s + * @param-out int $var + */ +function variadicFoo2(?string &$s, &...$var): void +{ + $s = ''; + $var[0] = 2; + $var[1] = 2; +} + +function foo1(?string $s): void { + assertType('string|null', $s); + addFoo($s); + assertType('string', $s); +} + +function foo2($mixed): void { + assertType('mixed', $mixed); + addFoo($mixed); + assertType('string', $mixed); +} + +/** + * @param FooBar $fooBar + * @return void + */ +function foo3($mixed, $fooBar): void { + assertType('mixed', $mixed); + $fooBar->genericClassFoo($mixed); + assertType('int', $mixed); +} + +function foo6(): void { + $b = false; + takesNullableBool($b); + + assertType('bool', $b); +} + +function foo7(): void { + variadicFoo( $a, $b); + assertType('int', $a); + assertType('int', $b); + + variadicFoo2($s, $a, $b); + assertType('string', $s); + assertType('int', $a); + assertType('int', $b); +} + +function foo8(string $s): void { + sodium_memzero($s); + assertType('null', $s); +} + +function foo9(?string $s): void { + $c = new OutFromStub(); + $c->stringOut($s); + assertType('string', $s); +} + +function foo10(?string $s): void { + $c = new ExtendsFooBar(); + $c->baseMethod($s); + assertType('string', $s); +} + +function foo11(?string $s): void { + $c = new ExtendsFooBar(); + $c->subMethod($s); + assertType('string', $s); +} + +function foo12(?string $s): void { + $c = new ExtendsFooBar(); + $c->overriddenMethod($s); + assertType('string', $s); +} + +function foo13(?string $s): void { + $c = new ExtendsFooBar(); + $c->overriddenButinheritedPhpDocMethod($s); + assertType('string', $s); +} + +/** + * @param array $a + * @param non-empty-array $nonEmptyArray + */ +function foo14(array $a, $nonEmptyArray): void { + \shuffle($a); + assertType('list', $a); + \shuffle($nonEmptyArray); + assertType('non-empty-list', $nonEmptyArray); +} + +function fooCompare (int $a, int $b): int { + return $a > $b ? 1 : -1; +} + +function foo15() { + $manifest = [1, 2, 3]; + uasort( + $manifest, + "fooCompare" + ); + assertType('array{1, 2, 3}', $manifest); +} + +function fooSpaceship (string $a, string $b): int { + return $a <=> $b; +} + +function foo16() { + $array = [1, 2]; + uksort( + $array, + "fooSpaceship" + ); + assertType('array{1, 2}', $array); +} + +function fooShuffle() { + $array = ["foo" => 123, "bar" => 456]; + shuffle($array); + assertType('non-empty-list<123|456>', $array); + + $emptyArray = []; + shuffle($emptyArray); + assertType('array{}', $emptyArray); +} + +function fooSort() { + $array = ["foo" => 123, "bar" => 456]; + sort($array); + assertType('non-empty-list<123|456>', $array); + assertType('true', array_is_list($array)); + + $emptyArray = []; + sort($emptyArray); + assertType('array{}', $emptyArray); +} + +function fooFscanf($r): void +{ + fscanf($r, "%d:%d:%d", $hours, $minutes, $seconds); + assertType('float|int|string|null', $hours); + assertType('float|int|string|null', $minutes); + assertType('float|int|string|null', $seconds); + + $n = fscanf($r, "%s %s", $p1, $p2); + assertType('float|int|string|null', $p1); + assertType('float|int|string|null', $p2); +} + +function fooScanf(): void +{ + sscanf("10:05:03", "%d:%d:%d", $hours, $minutes, $seconds); + assertType('float|int|string|null', $hours); + assertType('float|int|string|null', $minutes); + assertType('float|int|string|null', $seconds); + + $n = sscanf("42 psalm road", "%s %s", $p1, $p2); + assertType('int|null', $n); // could be 'int' + assertType('float|int|string|null', $p1); + assertType('float|int|string|null', $p2); +} + +function fooParams(ExtendsFooBar $subX, float $x1, float $y1) +{ + $subX->renamedParams($x1, $y1); + + assertType('float', $x1); + assertType('string', $y1); // overridden via reference of base-class, by param order (renamed params) +} + +function fooParams2(ExtendsFooBar $subX, float $x1, float $y1) { + $subX->paramOutOverridden($x1, $y1); + + assertType('float', $x1); + assertType('array', $y1); // overridden phpdoc-param-out-type in subclass +} + +function fooDateTime(\SplFileObject $splFileObject, ?string $wouldBlock) { + // php-src native method overridden via stub + $splFileObject->flock(1, $wouldBlock); + + assertType('string', $wouldBlock); +} + +function testParseStr() { + $str="first=value&arr[]=foo+bar&arr[]=baz"; + parse_str($str, $output); + + /* + echo $output['first'];//value + echo $output['arr'][0];//foo bar + echo $output['arr'][1];//baz + */ + + \PHPStan\Testing\assertType('array|lowercase-string>', $output); +} + +function fooSimilar() { + $similar = similar_text('foo', 'bar', $percent); + assertType('int', $similar); + assertType('float', $percent); +} + +function fooExec() { + exec("my cmd", $output, $exitCode); + + assertType('list', $output); + assertType('int', $exitCode); +} + +function fooSystem() { + system("my cmd", $exitCode); + + assertType('int', $exitCode); +} + +function fooPassthru() { + passthru("my cmd", $exitCode); + + assertType('int', $exitCode); +} + +class X { + /** + * @param-out array $ref + */ + public function __construct(string &$ref) { + $ref = []; + } +} + +class SubX extends X { + /** + * @param-out float $ref + */ + public function __construct(string $a, string &$ref) { + parent::__construct($ref); + } +} + +function fooConstruct(string $s) { + $x = new X($s); + assertType('array', $s); +} + +function fooSubConstruct(string $s) { + $x = new SubX('', $s); + assertType('float', $s); +} + +function fooFlock(int $f): void +{ + $fp=fopen('/tmp/lock.txt', 'r+'); + flock($fp, $f, $wouldBlock); + assertType('0|1', $wouldBlock); +} + +function fooFsockopen() { + $fp=fsockopen("udp://127.0.0.1",13, $errno, $errstr); + assertType('int', $errno); + assertType('string', $errstr); +} + +function fooHeadersSent() { + headers_sent($filename, $linenum); + assertType('int', $linenum); + assertType('string', $filename); +} + +function fooMbParseStr() { + mb_parse_str("foo=bar", $output); + assertType('array|lowercase-string>', $output); + + mb_parse_str('email=mail@example.org&city=town&x=1&y[g]=3&f=1.23', $output); + assertType('array|lowercase-string>', $output); +} + +function fooPreg() +{ + $string = 'April 15, 2003'; + $pattern = '/(\w+) (\d+), (\d+)/i'; + $replacement = '${1}1,$3'; + preg_replace($pattern, $replacement, $string, -1, $c); + assertType('int<0, max>', $c); + + preg_replace_callback($pattern, function ($matches) { + return strtolower($matches[0]); + }, $string, -1, $c); + assertType('int<0, max>', $c); + + preg_filter($pattern, $replacement, $string, -1, $c); + assertType('int<0, max>', $c); +} + +function fooReplace() { + $vowels = array("a", "e", "i", "o", "u", "A", "E", "I", "O", "U"); + str_replace($vowels, "", "World", $count); + assertType('int', $count); + + $vowels = array("a", "e", "i", "o", "u", "A", "E", "I", "O", "U"); + str_ireplace($vowels, "", "World", $count); + assertType('int', $count); +} + +function fooIsCallable($x, bool $b) +{ + is_callable($x, $b, $name); + assertType('callable-string', $name); +} + +function noParamOut(string &$s): void +{ + +} + +function noParamOutVariadic(string &...$s): void +{ + +} + +function ($s): void { + assertType('mixed', $s); + noParamOut($s); + assertType('string', $s); +}; + +function ($s, $t): void { + assertType('mixed', $s); + assertType('mixed', $t); + noParamOutVariadic($s, $t); + assertType('string', $s); + assertType('string', $t); +}; + +class NoParamOutClass +{ + + function doFoo(string &$s): void + { + + } + + function doFooVariadic(string &...$s): void + { + + } + +} + +function ($s): void { + assertType('mixed', $s); + $c = new NoParamOutClass(); + $c->doFoo($s); + assertType('string', $s); +}; + +function ($s, $t): void { + assertType('mixed', $s); + assertType('mixed', $t); + $c = new NoParamOutClass(); + $c->doFooVariadic($s, $t); + assertType('string', $s); + assertType('string', $t); +}; + +function fooMatch(string $input): void { + preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_PATTERN_ORDER); + assertType('array{list}', $matches); + + preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_SET_ORDER); + assertType('list', $matches); + + preg_match('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_UNMATCHED_AS_NULL); + assertType("array{0?: string}", $matches); +} + +function testMatch() { + preg_match('#.*#', 'foo', $matches); + assertType('array{0?: string}', $matches); +} diff --git a/tests/PHPStan/Analyser/data/param-out/ParamOutFunctionExtension.php b/tests/PHPStan/Analyser/data/param-out/ParamOutFunctionExtension.php new file mode 100644 index 0000000000..abeb8cb533 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out/ParamOutFunctionExtension.php @@ -0,0 +1,26 @@ +getName() === 'ParameterOutTests\callWithOut' && $parameter->getName() === 'outParam'; + } + + public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new StringType(); + } +} diff --git a/tests/PHPStan/Analyser/data/param-out/ParamOutMethodExtension.php b/tests/PHPStan/Analyser/data/param-out/ParamOutMethodExtension.php new file mode 100644 index 0000000000..087eb5ac46 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out/ParamOutMethodExtension.php @@ -0,0 +1,31 @@ +getDeclaringClass()->getName() === FooClass::class + && $methodReflection->getName() === 'callWithOut' + && $parameter->getName() === 'outParam' + ; + } + + public function getParameterOutTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new IntegerType(); + } +} diff --git a/tests/PHPStan/Analyser/data/param-out/ParamOutStaticMethodExtension.php b/tests/PHPStan/Analyser/data/param-out/ParamOutStaticMethodExtension.php new file mode 100644 index 0000000000..4c7b628bd7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out/ParamOutStaticMethodExtension.php @@ -0,0 +1,32 @@ +getDeclaringClass()->getName() === FooClass::class + && $methodReflection->getName() === 'staticCallWithOut' + && $parameter->getName() === 'outParam' + ; + } + + public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new BooleanType(); + } +} diff --git a/tests/PHPStan/Analyser/data/param-out/parameter-out-types.php b/tests/PHPStan/Analyser/data/param-out/parameter-out-types.php new file mode 100644 index 0000000000..57296b7d03 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out/parameter-out-types.php @@ -0,0 +1,39 @@ +callWithOut(12, $methodOut, $anotherOut); + assertType('int', $methodOut); + assertType('mixed', $anotherOut); +} + diff --git a/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php b/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php new file mode 100644 index 0000000000..b3876d7408 --- /dev/null +++ b/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php @@ -0,0 +1,199 @@ +getName() === 'ParameterClosureTypeExtensionArrowFunction\functionWithCallable'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + $args = $functionCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new IntegerType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new StringType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } +} + +class MethodParameterClosureTypeExtension implements \PHPStan\Type\MethodParameterClosureTypeExtension +{ + + public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && + $parameter->getName() === 'callback' && + $methodReflection->getName() === 'methodWithCallable'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + $args = $methodCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new IntegerType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new StringType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } +} + +class StaticMethodParameterClosureTypeExtension implements \PHPStan\Type\StaticMethodParameterClosureTypeExtension +{ + + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticMethodWithCallable'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + return new CallableType( + [ + new NativeParameterReflection('test', false, new FloatType(), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } +} + +class Foo +{ + + /** + * @param int $foo + * @param callable(Generic) $callback + * + * @return void + */ + public function methodWithCallable(int $foo, callable $callback) + { + + } + + /** + * @return void + */ + public static function staticMethodWithCallable(callable $callback) + { + + } + +} + +/** + * @template T + */ +class Generic +{ + private $value; + + /** + * @param T $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * @return T + */ + public function getValue() + { + return $this->value; + } +} + +/** + * @param int $foo + * @param callable(Generic) $callback + * + * @return void + */ +function functionWithCallable(int $foo, callable $callback) +{ + +} + +function test(Foo $foo): void +{ + + $foo->methodWithCallable(1, fn ($i) => assertType('int', $i->getValue())); + + (new Foo)->methodWithCallable(2, fn (Generic $i) => assertType('string', $i->getValue())); + + Foo::staticMethodWithCallable(fn ($i) => assertType('float', $i)); +} + +functionWithCallable(1, fn ($i) => assertType('int', $i->getValue())); + +functionWithCallable(2, fn (Generic $i) => assertType('string', $i->getValue())); diff --git a/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php b/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php new file mode 100644 index 0000000000..e081621ff1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php @@ -0,0 +1,220 @@ +getName() === 'ParameterClosureTypeExtension\functionWithClosure'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + $args = $functionCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new ClosureType( + [ + new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), new GenericObjectType(Generic::class, [new IntegerType()]), $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()), + ], + new VoidType() + ); + } + + return new ClosureType( + [ + new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), new GenericObjectType(Generic::class, [new StringType()]), $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()), + ], + new VoidType() + ); + } +} + +class MethodParameterClosureTypeExtension implements \PHPStan\Type\MethodParameterClosureTypeExtension +{ + + public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && + $parameter->getName() === 'callback' && + $methodReflection->getName() === 'methodWithClosure'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + $args = $methodCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new ClosureType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new IntegerType()]), PassedByReference::createNo(), false, null), + ], + new VoidType() + ); + } elseif ($integer === 5) { + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new IntegerType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + + return new ClosureType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new StringType()]), PassedByReference::createNo(), false, null), + ], + new VoidType() + ); + } +} + +class StaticMethodParameterClosureTypeExtension implements \PHPStan\Type\StaticMethodParameterClosureTypeExtension +{ + + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticMethodWithClosure'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + return new ClosureType( + [ + new NativeParameterReflection('test', false, new FloatType(), PassedByReference::createNo(), false, null), + ], + new VoidType() + ); + } +} + +class Foo +{ + + /** + * @param int $foo + * @param Closure(Generic): void $callback + * + * @return void + */ + public function methodWithClosure(int $foo, Closure $callback) + { + + } + + /** + * @param Closure(): void $callback + * + * @return void + */ + public static function staticMethodWithClosure(Closure $callback) + { + + } + +} + +/** + * @template T + */ +class Generic +{ + private $value; + + /** + * @param T $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * @return T + */ + public function getValue() + { + return $this->value; + } +} + +/** + * @param int $foo + * @param Closure(Generic): void $callback + * + * @return void + */ +function functionWithClosure(int $foo, Closure $callback) +{ + +} + +function test(Foo $foo): void +{ + $foo->methodWithClosure(1, function ($i) { + assertType('int', $i->getValue()); + }); + + (new Foo)->methodWithClosure(2, function (Generic $i) { + assertType('string', $i->getValue()); + }); + + Foo::staticMethodWithClosure(function ($i) { + assertType('float', $i); + }); +} + +functionWithClosure(1, function ($i) { + assertType('int', $i->getValue()); +}); + +functionWithClosure(2, function (Generic $i) { + assertType('string', $i->getValue()); +}); diff --git a/tests/PHPStan/Analyser/data/pathConstants-win.php b/tests/PHPStan/Analyser/data/pathConstants-win.php new file mode 100644 index 0000000000..a21b9fb49e --- /dev/null +++ b/tests/PHPStan/Analyser/data/pathConstants-win.php @@ -0,0 +1,6 @@ + $arrayWithStringKeys + * @param array{a?: 0, b: 1, c: 2} $constantArrayOptionalKeys1 + * @param array{a: 0, b?: 1, c: 2} $constantArrayOptionalKeys2 + * @param array{a: 0, b: 1, c?: 2} $constantArrayOptionalKeys3 */ public function doFoo( $mixed, int $integer, array $mixedArray, array $nonEmptyArray, - array $arrayWithStringKeys + array $arrayWithStringKeys, + array $constantArrayOptionalKeys1, + array $constantArrayOptionalKeys2, + array $constantArrayOptionalKeys3 ) { if (count($nonEmptyArray) === 0) { diff --git a/tests/PHPStan/Analyser/data/pow.php b/tests/PHPStan/Analyser/data/pow.php deleted file mode 100644 index 960b88fad3..0000000000 --- a/tests/PHPStan/Analyser/data/pow.php +++ /dev/null @@ -1,21 +0,0 @@ - $data + * @return array + */ + public function someMethod(array $data): array + { + foreach ($data[self::FIELD_NOTES][self::SUBFIELD_NOTE] ?? [] as $index => $noteData) { + $noteTitle = $noteData[self::FIELD_TITLE] ?? null; + $noteSource = $noteData[self::FIELD_SOURCE] ?? null; + $noteBody = $noteData[self::FIELD_BODY] ?? null; + + if ($noteBody === null || trim($noteBody) === '') { + $data[self::FIELD_NOTES] = self::EMPTY_NOTE_BODY; + } + } + + if (isset($data[self::FIELD_NOTES][self::SUBFIELD_NOTE])) {} + + return $data; + } + +} diff --git a/tests/PHPStan/Analyser/data/predefined-constants.php b/tests/PHPStan/Analyser/data/predefined-constants.php deleted file mode 100644 index d5e1cc9483..0000000000 --- a/tests/PHPStan/Analyser/data/predefined-constants.php +++ /dev/null @@ -1,76 +0,0 @@ -', 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 deleted file mode 100644 index 8eb5826df0..0000000000 --- a/tests/PHPStan/Analyser/data/preg_filter.php +++ /dev/null @@ -1,44 +0,0 @@ -', 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_split.php b/tests/PHPStan/Analyser/data/preg_split.php deleted file mode 100644 index bb89a67175..0000000000 --- a/tests/PHPStan/Analyser/data/preg_split.php +++ /dev/null @@ -1,30 +0,0 @@ -|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/prestashop-xml-loader.php b/tests/PHPStan/Analyser/data/prestashop-xml-loader.php new file mode 100644 index 0000000000..37c9e729e3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/prestashop-xml-loader.php @@ -0,0 +1,97 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShopBundleInfiniteRunBug; + +class XmlLoader +{ + + protected $data_path; + + public function getEntityInfo($entity, $exists) + { + $info = [ + 'config' => [ + 'id' => '', + 'primary' => '', + 'class' => '', + 'sql' => '', + 'ordersql' => '', + 'image' => '', + 'null' => '', + ], + 'fields' => [], + ]; + + if (!$exists) { + return $info; + } + + $xml = @simplexml_load_file($this->data_path . $entity . '.xml', 'SimplexmlElement'); + if (!$xml) { + return $info; + } + + if ($xml->fields['id']) { + $info['config']['id'] = (string) $xml->fields['id']; + } + + if ($xml->fields['primary']) { + $info['config']['primary'] = (string) $xml->fields['primary']; + } + + if ($xml->fields['class']) { + $info['config']['class'] = (string) $xml->fields['class']; + } + + if ($xml->fields['sql']) { + $info['config']['sql'] = (string) $xml->fields['sql']; + } + + if ($xml->fields['ordersql']) { + $info['config']['ordersql'] = (string) $xml->fields['ordersql']; + } + + if ($xml->fields['null']) { + $info['config']['null'] = (string) $xml->fields['null']; + } + + if ($xml->fields['image']) { + $info['config']['image'] = (string) $xml->fields['image']; + } + + foreach ($xml->fields->field as $field) { + $column = (string) $field['name']; + $info['fields'][$column] = []; + if (isset($field['relation'])) { + $info['fields'][$column]['relation'] = (string) $field['relation']; + } + } + + return $info; + } + +} diff --git a/tests/PHPStan/Analyser/data/process-called-method-infinite-loop.php b/tests/PHPStan/Analyser/data/process-called-method-infinite-loop.php new file mode 100644 index 0000000000..9b6cc0925d --- /dev/null +++ b/tests/PHPStan/Analyser/data/process-called-method-infinite-loop.php @@ -0,0 +1,41 @@ +value); + } + /** @param \Closure(T|null): T $callback */ + public function onResolve2(\Closure $callback) : void{ + $r = $callback($this->value); + assertType('TValue (class ProcessCalledMethodInfiniteLoop\\Promise, argument)', $r); + + $callback($this->value); + } +} +class HelloWorld +{ + /** + * @template TValue + * @param \Generator, TValue|null, void> $async + */ + public function next(\Generator $async) : void{ + $async->next(); + if(!$async->valid()) return; + $promise = $async->current(); + $promise->onResolve(function($value) use ($async) : void{ + $async->send($value); + $this->next($async); + }); + } +} diff --git a/tests/PHPStan/Analyser/data/properties-defined.php b/tests/PHPStan/Analyser/data/properties-defined.php index 2ca412e875..cdc4cbb255 100644 --- a/tests/PHPStan/Analyser/data/properties-defined.php +++ b/tests/PHPStan/Analyser/data/properties-defined.php @@ -2,6 +2,7 @@ namespace PropertiesNamespace; +use AllowDynamicProperties; use DOMDocument; use SomeNamespace\Sit as Dolor; @@ -9,6 +10,7 @@ * @property-read int $readOnlyProperty * @property-read int $overriddenReadOnlyProperty */ +#[AllowDynamicProperties] class Bar extends DOMDocument { diff --git a/tests/PHPStan/Analyser/data/property-native-types.php b/tests/PHPStan/Analyser/data/property-native-types.php index 2892179613..87d990b96f 100644 --- a/tests/PHPStan/Analyser/data/property-native-types.php +++ b/tests/PHPStan/Analyser/data/property-native-types.php @@ -1,4 +1,4 @@ -= 7.4 + $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/round-php8.php b/tests/PHPStan/Analyser/data/round-php8.php deleted file mode 100644 index 85428a2fd6..0000000000 --- a/tests/PHPStan/Analyser/data/round-php8.php +++ /dev/null @@ -1,61 +0,0 @@ - assertType('int', $nullable), + self::ALLOW_NULLABLE_INT => assertType('int|null', $nullable), + }; + } +} + + + diff --git a/tests/PHPStan/Analyser/data/skip-check-no-generic-classes.php b/tests/PHPStan/Analyser/data/skip-check-no-generic-classes.php new file mode 100644 index 0000000000..7062419000 --- /dev/null +++ b/tests/PHPStan/Analyser/data/skip-check-no-generic-classes.php @@ -0,0 +1,15 @@ +name instanceof Identifier && $bar->name instanceof Identifier) { - function () use ($call): void { - assertType('PhpParser\Node\Identifier', $call->name); - assertType('mixed', $bar->name); - }; - - assertType('PhpParser\Node\Identifier', $call->name); - } - } - - public function doBar(MethodCall $call, MethodCall $bar): void - { - if ($call->name instanceof Identifier && $bar->name instanceof Identifier) { - $a = 1; - function () use ($call, &$a): void { - assertType('PhpParser\Node\Identifier', $call->name); - assertType('mixed', $bar->name); - }; - - assertType('PhpParser\Node\Identifier', $call->name); - } - } - -} diff --git a/tests/PHPStan/Analyser/data/sscanf.php b/tests/PHPStan/Analyser/data/sscanf.php deleted file mode 100644 index 2b7105f939..0000000000 --- a/tests/PHPStan/Analyser/data/sscanf.php +++ /dev/null @@ -1,6 +0,0 @@ - $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('\'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/throws-tag-from-native-function-stub.php b/tests/PHPStan/Analyser/data/throws-tag-from-native-function-stub.php new file mode 100644 index 0000000000..c1cb5b40f2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/throws-tag-from-native-function-stub.php @@ -0,0 +1,21 @@ +doFoo(5)); +assertType('int', (new Bar)->notTypedFoo); diff --git a/tests/PHPStan/Analyser/data/trait-stubs.stub b/tests/PHPStan/Analyser/data/trait-stubs.stub new file mode 100644 index 0000000000..e3b314e362 --- /dev/null +++ b/tests/PHPStan/Analyser/data/trait-stubs.stub @@ -0,0 +1,17 @@ +pipeInto(User::class); +}; diff --git a/tests/PHPStan/Analyser/do-not-pollute-scope-with-block.neon b/tests/PHPStan/Analyser/do-not-pollute-scope-with-block.neon new file mode 100644 index 0000000000..4c33864671 --- /dev/null +++ b/tests/PHPStan/Analyser/do-not-pollute-scope-with-block.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithBlock: false diff --git a/tests/PHPStan/Analyser/do-not-remember-possibly-impure-function-values.neon b/tests/PHPStan/Analyser/do-not-remember-possibly-impure-function-values.neon new file mode 100644 index 0000000000..971a1184f4 --- /dev/null +++ b/tests/PHPStan/Analyser/do-not-remember-possibly-impure-function-values.neon @@ -0,0 +1,2 @@ +parameters: + rememberPossiblyImpureFunctionValues: false diff --git a/tests/PHPStan/Analyser/dynamic-return-type.neon b/tests/PHPStan/Analyser/dynamic-return-type.neon index b8bd815fb4..e80f2018a0 100644 --- a/tests/PHPStan/Analyser/dynamic-return-type.neon +++ b/tests/PHPStan/Analyser/dynamic-return-type.neon @@ -27,3 +27,15 @@ services: class: PHPStan\Tests\FooGetSelf tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\ConditionalGetSingle + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\Bug7344DynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\Bug7391BDynamicStaticMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension diff --git a/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon b/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon index 7e42b64ac0..905063b2f6 100644 --- a/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon +++ b/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon @@ -8,3 +8,14 @@ services: class: DynamicMethodThrowTypeExtension\StaticMethodThrowTypeExtension tags: - phpstan.dynamicStaticMethodThrowTypeExtension + + - + class: DynamicMethodThrowTypeExtensionNamedArgs\MethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + + - + class: DynamicMethodThrowTypeExtensionNamedArgs\StaticMethodThrowTypeExtension + tags: + - phpstan.dynamicStaticMethodThrowTypeExtension + diff --git a/tests/PHPStan/Analyser/expression-type-resolver-extension.neon b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon new file mode 100644 index 0000000000..de0f92640b --- /dev/null +++ b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon @@ -0,0 +1,7 @@ +# config for ExpressionTypeResolverExtensionTest +services: + - + class: ExpressionTypeResolverExtension\MethodCallReturnsBoolExpressionTypeResolverExtension + tags: + - phpstan.broker.expressionTypeResolverExtension + diff --git a/tests/PHPStan/Analyser/looseConstComparisonPhp7.neon b/tests/PHPStan/Analyser/looseConstComparisonPhp7.neon new file mode 100644 index 0000000000..b41c80f2a6 --- /dev/null +++ b/tests/PHPStan/Analyser/looseConstComparisonPhp7.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 70400 # PHP 7.4 diff --git a/tests/PHPStan/Analyser/looseConstComparisonPhp8.neon b/tests/PHPStan/Analyser/looseConstComparisonPhp8.neon new file mode 100644 index 0000000000..0ed5d49f80 --- /dev/null +++ b/tests/PHPStan/Analyser/looseConstComparisonPhp8.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80000 # PHP 8.0 diff --git a/tests/PHPStan/Analyser/nsrt/DateTimeCreateDynamicReturnTypes.php b/tests/PHPStan/Analyser/nsrt/DateTimeCreateDynamicReturnTypes.php new file mode 100644 index 0000000000..f50a903dbf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/DateTimeCreateDynamicReturnTypes.php @@ -0,0 +1,48 @@ +modify($modify)); + assertType('(DateTimeImmutable|false)', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'+2 day' $modify + */ + public function modifyWithValidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + + /** + * @param 'kewk'|'koko' $modify + */ + public function modifyWithInvalidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('false', $datetime->modify($modify)); + assertType('false', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'koko' $modify + */ + public function modifyWithBothConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('(DateTime|false)', $datetime->modify($modify)); + assertType('(DateTimeImmutable|false)', $dateTimeImmutable->modify($modify)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/DateTimeModifyReturnTypes83.php b/tests/PHPStan/Analyser/nsrt/DateTimeModifyReturnTypes83.php new file mode 100644 index 0000000000..d1d9af0558 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/DateTimeModifyReturnTypes83.php @@ -0,0 +1,42 @@ += 8.3 + +declare(strict_types = 1); + +namespace DateTimeModifyReturnTypes83; + +use DateTime; +use DateTimeImmutable; +use function PHPStan\Testing\assertType; + +class Foo +{ + public function modify(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'+2 day' $modify + */ + public function modifyWithValidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + + /** + * @param 'kewk'|'koko' $modify + */ + public function modifyWithInvalidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('*NEVER*', $datetime->modify($modify)); + assertType('*NEVER*', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'koko' $modify + */ + public function modifyWithBothConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/PDOStatement.php b/tests/PHPStan/Analyser/nsrt/PDOStatement.php new file mode 100644 index 0000000000..d9e930061e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/PDOStatement.php @@ -0,0 +1,22 @@ +fetchObject(Bar::class); + assertType('PDOStatement\Bar|false', $bar); + + $bar = $statement->fetchObject(); + assertType('stdClass|false', $bar); + } + +} + diff --git a/tests/PHPStan/Analyser/nsrt/abs.php b/tests/PHPStan/Analyser/nsrt/abs.php new file mode 100644 index 0000000000..506f436c02 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/abs.php @@ -0,0 +1,217 @@ += 8.0 + +declare(strict_types = 1); + +namespace Abs; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function singleIntegerRange(int $int): void + { + /** @var int $int */ + assertType('int<0, max>', abs($int)); + + /** @var positive-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var negative-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var non-negative-int $int */ + assertType('int<0, max>', abs($int)); + + /** @var non-positive-int $int */ + assertType('int<0, max>', abs($int)); + + /** @var int<0, max> $int */ + assertType('int<0, max>', abs($int)); + + /** @var int<0, 123> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int<-123, 0> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int<1, max> $int */ + assertType('int<1, max>', abs($int)); + + /** @var int<123, max> $int */ + assertType('int<123, max>', abs($int)); + + /** @var int<123, 456> $int */ + assertType('int<123, 456>', abs($int)); + + /** @var int $int */ + assertType('int<0, max>', abs($int)); + + /** @var int $int */ + assertType('int<1, max>', abs($int)); + + /** @var int $int */ + assertType('int<123, max>', abs($int)); + + /** @var int<-456, -123> $int */ + assertType('int<123, 456>', abs($int)); + + /** @var int<-123, 123> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int $int */ + assertType('int<0, max>', abs($int)); + } + + public function multipleIntegerRanges(int $int): void + { + /** @var non-zero-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var int|int<1, max> $int */ + assertType('int<1, max>', abs($int)); + + /** @var int<-20, -10>|int<5, 25> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-20, -5>|int<10, 25> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-25, -10>|int<5, 20> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-20, -10>|int<20, 30> $int */ + assertType('int<10, 30>', abs($int)); + } + + public function constantInteger(int $int): void + { + /** @var 0 $int */ + assertType('0', abs($int)); + + /** @var 1 $int */ + assertType('1', abs($int)); + + /** @var -1 $int */ + assertType('1', abs($int)); + + assertType('123', abs(123)); + + assertType('123', abs(-123)); + } + + public function mixedIntegerUnion(int $int): void + { + /** @var 123|int<456, max> $int */ + assertType('123|int<456, max>', abs($int)); + + /** @var int|-123 $int */ + assertType('123|int<456, max>', abs($int)); + + /** @var -123|int<124, 125> $int */ + assertType('int<123, 125>', abs($int)); + + /** @var int<124, 125>|-123 $int */ + assertType('int<123, 125>', abs($int)); + } + + public function constantFloat(float $float): void + { + /** @var 0.0 $float */ + assertType('0.0', abs($float)); + + /** @var 1.0 $float */ + assertType('1.0', abs($float)); + + /** @var -1.0 $float */ + assertType('1.0', abs($float)); + + assertType('123.4', abs(123.4)); + + assertType('123.4', abs(-123.4)); + } + + public function string(string $string): void + { + /** @var string $string */ + assertType('float|int<0, max>', abs($string)); + + /** @var numeric-string $string */ + assertType('float|int<0, max>', abs($string)); + + /** @var '-1' $string */ + assertType('1', abs($string)); + + /** @var '-1'|'-2.0'|'3.0'|'4' $string */ + assertType('1|2.0|3.0|4', abs($string)); + + /** @var literal-string $string */ + assertType('float|int<0, max>', abs($string)); + + assertType('123', abs('123')); + + assertType('123', abs('-123')); + + assertType('123.0', abs('123.0')); + + assertType('123.0', abs('-123.0')); + + assertType('float|int<0, max>', abs('foo')); + } + + public function mixedUnion(mixed $value): void + { + /** @var 1.0|int<2, 3> $value */ + assertType('1.0|int<2, 3>', abs($value)); + + /** @var -1.0|int<-3, -2> $value */ + assertType('1.0|int<2, 3>', abs($value)); + + /** @var 2.0|int<1, 3> $value */ + assertType('2.0|int<1, 3>', abs($value)); + + /** @var -2.0|int<-3, -1> $value */ + assertType('2.0|int<1, 3>', abs($value)); + + /** @var -1.0|int<2, 3>|numeric-string $value */ + assertType('float|int<0, max>', abs($value)); + } + + public function intersection(mixed $value): void + { + /** @var int&int<-10, 10> $value */ + assertType('int<0, 10>', abs($value)); + } + + public function invalidType(mixed $nonInt): void + { + /** @var string $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var string|positive-int $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var 'foo' $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var array $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var non-empty-list $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var object $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var \DateTime $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var null $nonInt */ + assertType('0', abs($nonInt)); + + assertType('float|int<0, max>', abs('foo')); + + assertType('0', abs(null)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/abstract-generic-trait-method-implicit-phpdoc-inheritance.php b/tests/PHPStan/Analyser/nsrt/abstract-generic-trait-method-implicit-phpdoc-inheritance.php new file mode 100644 index 0000000000..1010ae098b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/abstract-generic-trait-method-implicit-phpdoc-inheritance.php @@ -0,0 +1,32 @@ + */ + use Foo; + + public function doFoo() + { + return 1; + } + +} + +function (UseFoo $f): void { + assertType('int', $f->doFoo()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php b/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php new file mode 100644 index 0000000000..d21d17e739 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php @@ -0,0 +1,30 @@ +getFileName()); + assertType('int', $r->getStartLine()); + assertType('int', $r->getEndLine()); + assertType('string|false', $r->getDocComment()); + assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant|false', $r->getReflectionConstant($s)); + assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass|false', $r->getParentClass()); + assertType('non-empty-string|false', $r->getExtensionName()); + assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType|null', $r->getBackingType()); +}; + +function (ReflectionEnumBackedCase $r): void { + assertType('string|false', $r->getDocComment()); + assertType(ReflectionType::class . '|null', $r->getType()); +}; + +function (ReflectionEnumUnitCase $r): void { + assertType('string|false', $r->getDocComment()); + assertType(ReflectionType::class . '|null', $r->getType()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/allowed-subtypes-datetime.php b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-datetime.php new file mode 100644 index 0000000000..46c43086e0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-datetime.php @@ -0,0 +1,17 @@ += 8.1 + +namespace AllowedSubtypesEnum; + +use function PHPStan\Testing\assertType; + +enum Foo { + case A; + case B; + case C; +} + +function foo(Foo $foo): void { + assertType('AllowedSubtypesEnum\\Foo', $foo); + + if ($foo === Foo::B) { + return; + } + + assertType('AllowedSubtypesEnum\\Foo~AllowedSubtypesEnum\\Foo::B', $foo); + + if ($foo === Foo::C) { + return; + } + + assertType('AllowedSubtypesEnum\\Foo::A', $foo); +} diff --git a/tests/PHPStan/Analyser/nsrt/allowed-subtypes-throwable.php b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-throwable.php new file mode 100644 index 0000000000..4336e79d64 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-throwable.php @@ -0,0 +1,17 @@ + $arr1 + * @param array $arr2 + * @param array $arr3 + * @param array $arr4 + * @param array $arr5 + * @param array $arr6 + * @param array $arr7 + * @param array $arr8 + * @param array{foo: 1, bar?: 2} $arr9 + * @param array<'foo'|'bar', string> $arr10 + * @param list $list + * @param non-empty-array $nonEmpty + */ + public function sayHello( + array $arr1, + array $arr2, + array $arr3, + array $arr4, + array $arr5, + array $arr6, + array $arr7, + array $arr8, + array $arr9, + array $arr10, + array $list, + array $nonEmpty, + int $case + ): void { + assertType('array', array_change_key_case($arr1)); + assertType('array', array_change_key_case($arr1, CASE_LOWER)); + assertType('array', array_change_key_case($arr1, CASE_UPPER)); + assertType('array', array_change_key_case($arr1, $case)); + + assertType('array', array_change_key_case($arr2)); + assertType('array', array_change_key_case($arr2, CASE_LOWER)); + assertType('array', array_change_key_case($arr2, CASE_UPPER)); + assertType('array', array_change_key_case($arr2, $case)); + + assertType('array', array_change_key_case($arr3)); + assertType('array', array_change_key_case($arr3, CASE_LOWER)); + assertType('array', array_change_key_case($arr3, CASE_UPPER)); + assertType('array', array_change_key_case($arr3, $case)); + + assertType('array', array_change_key_case($arr4)); + assertType('array', array_change_key_case($arr4, CASE_LOWER)); + assertType('array', array_change_key_case($arr4, CASE_UPPER)); + assertType('array', array_change_key_case($arr4, $case)); + + assertType('array', array_change_key_case($arr5)); + assertType('array', array_change_key_case($arr5, CASE_LOWER)); + assertType('array', array_change_key_case($arr5, CASE_UPPER)); + assertType('array', array_change_key_case($arr5, $case)); + + assertType('array', array_change_key_case($arr6)); + assertType('array', array_change_key_case($arr6, CASE_LOWER)); + assertType('array', array_change_key_case($arr6, CASE_UPPER)); + assertType('array', array_change_key_case($arr6, $case)); + + assertType('array', array_change_key_case($arr7)); + assertType('array', array_change_key_case($arr7, CASE_LOWER)); + assertType('array', array_change_key_case($arr7, CASE_UPPER)); + assertType('array', array_change_key_case($arr7, $case)); + + assertType('array', array_change_key_case($arr8)); + assertType('array', array_change_key_case($arr8, CASE_LOWER)); + assertType('array', array_change_key_case($arr8, CASE_UPPER)); + assertType('array', array_change_key_case($arr8, $case)); + + assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9)); + assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9, CASE_LOWER)); + assertType('array{FOO: 1, BAR?: 2}', array_change_key_case($arr9, CASE_UPPER)); + assertType("non-empty-array<'BAR'|'bar'|'FOO'|'foo', 1|2>", array_change_key_case($arr9, $case)); + + assertType("array<'bar'|'foo', string>", array_change_key_case($arr10)); + assertType("array<'bar'|'foo', string>", array_change_key_case($arr10, CASE_LOWER)); + assertType("array<'BAR'|'FOO', string>", array_change_key_case($arr10, CASE_UPPER)); + assertType("array<'BAR'|'bar'|'FOO'|'foo', string>", array_change_key_case($arr10, $case)); + + assertType('list', array_change_key_case($list)); + assertType('list', array_change_key_case($list, CASE_LOWER)); + assertType('list', array_change_key_case($list, CASE_UPPER)); + assertType('list', array_change_key_case($list, $case)); + + assertType('non-empty-array', array_change_key_case($nonEmpty)); + assertType('non-empty-array', array_change_key_case($nonEmpty, CASE_LOWER)); + assertType('non-empty-array', array_change_key_case($nonEmpty, CASE_UPPER)); + assertType('non-empty-array', array_change_key_case($nonEmpty, $case)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-chunk-php8.php b/tests/PHPStan/Analyser/nsrt/array-chunk-php8.php new file mode 100644 index 0000000000..5c36290a8f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-chunk-php8.php @@ -0,0 +1,20 @@ += 8.0 + +declare(strict_types = 1); + +namespace ArrayChunkPhp8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** + * @param int<-5, -10> $negativeRange + * @param int<-5, 0> $negativeWithZero + */ + public function negativeLength(array $arr, $negativeRange, $negativeWithZero) { + assertType('*NEVER*', array_chunk($arr, $negativeRange)); + assertType('*NEVER*', array_chunk($arr, $negativeWithZero)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-chunk-php81.php b/tests/PHPStan/Analyser/nsrt/array-chunk-php81.php new file mode 100644 index 0000000000..d65050061d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-chunk-php81.php @@ -0,0 +1,32 @@ += 8.1 + +declare(strict_types = 1); + +namespace ArrayChunkPhp81; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** + * @param 1|2 $union + */ + public function enumTest($union) { + $arr = []; + $arr[] = Status::DRAFT; + $arr[] = Status::PUBLISHED; + if (rand(0,1)) { + $arr[] = Status::ARCHIVED; + } + + assertType('array{array{ArrayChunkPhp81\Status::DRAFT, ArrayChunkPhp81\Status::PUBLISHED}, array{0?: ArrayChunkPhp81\Status::ARCHIVED}}|array{array{ArrayChunkPhp81\Status::DRAFT}, array{ArrayChunkPhp81\Status::PUBLISHED}, array{0?: ArrayChunkPhp81\Status::ARCHIVED}}', array_chunk($arr, $union)); + } + +} + +enum Status +{ + case DRAFT; + case PUBLISHED; + case ARCHIVED; +} diff --git a/tests/PHPStan/Analyser/nsrt/array-chunk.php b/tests/PHPStan/Analyser/nsrt/array-chunk.php new file mode 100644 index 0000000000..cedb50ddb7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-chunk.php @@ -0,0 +1,99 @@ +>', array_chunk($arr, 2)); + assertType('list>', array_chunk($arr, 2, true)); + + /** @var array $arr */ + assertType('list>', array_chunk($arr, 2)); + assertType('list>', array_chunk($arr, 2, true)); + + /** @var non-empty-array $arr */ + assertType('non-empty-list>', array_chunk($arr, 1)); + assertType('non-empty-list>', array_chunk($arr, 1, true)); + } + + + public function constantArrays(array $arr): void + { + /** @var array{a: 0, 17: 1, b: 2} $arr */ + assertType('array{array{0, 1}, array{2}}', array_chunk($arr, 2)); + assertType('array{array{a: 0, 17: 1}, array{b: 2}}', array_chunk($arr, 2, true)); + assertType('array{array{0}, array{1}, array{2}}', array_chunk($arr, 1)); + assertType('array{array{a: 0}, array{17: 1}, array{b: 2}}', array_chunk($arr, 1, true)); + } + + public function constantArraysWithOptionalKeys(array $arr): void + { + /** @var array{a: 0, b?: 1, c: 2} $arr */ + assertType('array{array{a: 0, b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 2, true)); + assertType('array{array{a: 0, b?: 1, c: 2}}', array_chunk($arr, 3, true)); + assertType('array{array{a: 0}, array{b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 1, true)); + + /** @var array{a?: 0, b?: 1, c?: 2} $arr */ + assertType('array{array{a?: 0, b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 2, true)); + } + + /** + * @param int<2, 3> $positiveRange + * @param 2|3 $positiveUnion + */ + public function chunkUnionTypeLength(array $arr, $positiveRange, $positiveUnion) { + /** @var array{a: 0, b?: 1, c: 2} $arr */ + assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveRange)); + assertType('array{0: array{a: 0, b?: 1, c?: 2}, 1?: array{c?: 2}}', array_chunk($arr, $positiveRange, true)); + assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveUnion)); + assertType('array{0: array{a: 0, b?: 1, c?: 2}, 1?: array{c?: 2}}', array_chunk($arr, $positiveUnion, true)); + } + + /** + * @param positive-int $positiveInt + * @param int<50, max> $bigger50 + */ + public function lengthIntRanges(array $arr, int $positiveInt, int $bigger50) { + assertType('list', array_chunk($arr, $positiveInt)); + assertType('list', array_chunk($arr, $bigger50)); + } + + /** + * @param int<1, 4> $oneToFour + * @param int<1, 5> $tooBig + */ + function testLimits(array $arr, int $oneToFour, int $tooBig) { + /** @var array{a: 0, b?: 1, c: 2, d: 3} $arr */ + assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2|3, 3?: 3}, 1?: array{0?: 2|3, 1?: 3}}|array{array{0}, array{0?: 1|2, 1?: 2}, array{0?: 2|3, 1?: 3}, array{0?: 3}}', array_chunk($arr, $oneToFour)); + assertType('non-empty-list>', array_chunk($arr, $tooBig)); + } + + /** @param array $map */ + public function offsets(array $arr, array $map): void + { + if (array_key_exists('foo', $arr)) { + assertType('non-empty-list', array_chunk($arr, 2)); + assertType('non-empty-list', array_chunk($arr, 2, true)); + } + if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { + assertType('non-empty-list', array_chunk($arr, 2)); + assertType('non-empty-list', array_chunk($arr, 2, true)); + } + + if (array_key_exists('foo', $map)) { + assertType('non-empty-list>', array_chunk($map, 2)); + assertType('non-empty-list>', array_chunk($map, 2, true)); + } + if (array_key_exists('foo', $map) && $map['foo'] === 'bar') { + assertType('non-empty-list>', array_chunk($map, 2)); + assertType('non-empty-list>', array_chunk($map, 2, true)); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/array-column-php7.php b/tests/PHPStan/Analyser/nsrt/array-column-php7.php similarity index 96% rename from tests/PHPStan/Analyser/data/array-column-php7.php rename to tests/PHPStan/Analyser/nsrt/array-column-php7.php index 5b634d5146..5d8018f599 100644 --- a/tests/PHPStan/Analyser/data/array-column-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-column-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace ArrayColumn\Php8; diff --git a/tests/PHPStan/Analyser/nsrt/array-column-php82.php b/tests/PHPStan/Analyser/nsrt/array-column-php82.php new file mode 100644 index 0000000000..7f0a545edc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-column-php82.php @@ -0,0 +1,220 @@ += 8.2 + +namespace ArrayColumn82; + +use DOMElement; +use function PHPStan\Testing\assertType; + + +class ArrayColumnTest +{ + + /** @param array> $array */ + public function testArray1(array $array): void + { + assertType('list', 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('list', 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('list', 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("list<'foo'>", array_column($array, 'column')); + assertType("array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); + assertType("array<'bar'|int, array{column?: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray6(array $array): void + { + assertType('list', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2')); + } + + /** @param non-empty-array $array */ + public function testConstantArray7(array $array): void + { + assertType('non-empty-list', 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')); + } + + /** @param array{0?: array{column: 'foo', key: 'bar'}} $array */ + public function testConstantArray12(array $array): void + { + assertType("array{0?: 'foo'}", array_column($array, 'column')); + assertType("array{bar?: 'foo'}", 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("list<'foo'>", array_column($array, 'column')); + assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); + assertType("array{bar: array{column?: 'foo', key: 'bar'}}", array_column($array, null, 'key')); + } + + /** @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('list', array_column($array, 'column')); + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testImprecise5(array $array): void + { + assertType('list', array_column($array, 'nodeName')); + assertType('array', array_column($array, 'nodeName', 'tagName')); + assertType('array', array_column($array, null, 'tagName')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('array', array_column($array, 'nodeName', 'foo')); + assertType('array', array_column($array, null, 'foo')); + } + + /** @param non-empty-array $array */ + public function testObjects1(array $array): void + { + assertType('non-empty-list', array_column($array, 'nodeName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); + assertType('non-empty-array', array_column($array, null, 'tagName')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); + } + + /** @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('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); + } + +} + +final class Foo +{ + + /** @param array $a */ + public function doFoo(array $a): void + { + assertType('array{}', array_column($a, 'nodeName')); + assertType('array{}', array_column($a, 'nodeName', 'tagName')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-column.php b/tests/PHPStan/Analyser/nsrt/array-column.php new file mode 100644 index 0000000000..ee4ad00527 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-column.php @@ -0,0 +1,234 @@ +> $array */ + public function testArray1(array $array): void + { + assertType('list', 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('list', 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('list', 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("list<'foo'>", array_column($array, 'column')); + assertType("array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); + assertType("array<'bar'|int, array{column?: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray6(array $array): void + { + assertType('list', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2')); + } + + /** @param non-empty-array $array */ + public function testConstantArray7(array $array): void + { + assertType('non-empty-list', 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')); + } + + /** @param array{0?: array{column: 'foo', key: 'bar'}} $array */ + public function testConstantArray12(array $array): void + { + assertType("array{0?: 'foo'}", array_column($array, 'column')); + assertType("array{bar?: 'foo'}", array_column($array, 'column', 'key')); + } + + /** @param array{0?: array{column: 'foo1', key: 'bar1'}, 1?: array{column: 'foo2', key: 'bar2'}} $array */ + public function testConstantArray13(array $array): void + { + assertType("array{0?: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column')); + assertType("array{bar1?: 'foo1', bar2?: 'foo2'}", array_column($array, 'column', 'key')); + } + + /** @param array{0?: array{column: 'foo1', key: 'bar1'}, 1: array{column: 'foo2', key: 'bar2'}} $array */ + public function testConstantArray14(array $array): void + { + assertType("array{0: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column')); + assertType("array{bar1?: 'foo1', bar2: 'foo2'}", 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("list<'foo'>", array_column($array, 'column')); + assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); + assertType("array{bar: array{column?: 'foo', key: 'bar'}}", array_column($array, null, 'key')); + } + + /** @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('list', array_column($array, 'column')); + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testImprecise5(array $array): void + { + assertType('list', array_column($array, 'nodeName')); + assertType('array', array_column($array, 'nodeName', 'tagName')); + assertType('array', array_column($array, null, 'tagName')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('array', array_column($array, 'nodeName', 'foo')); + assertType('array', array_column($array, null, 'foo')); + } + + /** @param non-empty-array $array */ + public function testObjects1(array $array): void + { + assertType('non-empty-list', array_column($array, 'nodeName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); + assertType('non-empty-array', array_column($array, null, 'tagName')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); + } + + /** @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('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); + } + +} + +final class Foo +{ + + /** @param array $a */ + public function doFoo(array $a): void + { + assertType('list', array_column($a, 'nodeName')); + assertType('array', array_column($a, 'nodeName', 'tagName')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php7.php b/tests/PHPStan/Analyser/nsrt/array-combine-php7.php new file mode 100644 index 0000000000..7e589c5f6e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php7.php @@ -0,0 +1,85 @@ +|false", array_combine([new Bar, 'red', 'yellow'], $b)); + assertType("*NEVER*", array_combine([new Baz, 'red', 'yellow'], $b)); +} + +/** + * @param non-empty-array $a + * @param non-empty-array $b + */ +function withNonEmptyArray(array $a, array $b): void +{ + assertType("non-empty-array<'bar'|'baz'|'foo', 'apple'|'avocado'|'banana'>|false", array_combine($a, $b)); +} + +function withDifferentNumberOfElements(): void +{ + assertType('false', array_combine(['foo'], ['bar', 'baz'])); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php new file mode 100644 index 0000000000..1e02759b5e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -0,0 +1,93 @@ += 8.0 + +namespace ArrayCombinePHP8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** @phpstan-return 'foo' */ + public function __toString(): string + { + return 'foo'; + } +} + +class Bar +{ + public function __toString(): string + { + return 'bar'; + } +} + +class Baz {} + +function withBoolKey(): void +{ + $a = [true, 'red', 'yellow']; + $b = ['avocado', 'apple', 'banana']; + + assertType("array{1: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b)); + + $c = [false, 'red', 'yellow']; + $d = ['avocado', 'apple', 'banana']; + + assertType("array{: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($c, $d)); +} + +function withFloatKey(): void +{ + $a = [1.5, 'red', 'yellow']; + $b = ['avocado', 'apple', 'banana']; + + assertType("array{1.5: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b)); +} + +function withIntegerKey(): void +{ + $a = [1, 2, 3]; + $b = ['avocado', 'apple', 'banana']; + + assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b)); +} + +function withNumericStringKey(): void +{ + $a = ["1", "2", "3"]; + $b = ['avocado', 'apple', 'banana']; + + assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b)); +} + +function withObjectKey() : void +{ + $a = [new Foo, 'red', 'yellow']; + $b = ['avocado', 'apple', 'banana']; + + assertType("array{foo: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b)); + assertType("non-empty-array", array_combine([new Bar, 'red', 'yellow'], $b)); + assertType("*NEVER*", array_combine([new Baz, 'red', 'yellow'], $b)); +} + +/** + * @param non-empty-array $a + * @param non-empty-array $b + */ +function withNonEmptyArray(array $a, array $b): void +{ + assertType("non-empty-array<'bar'|'baz'|'foo', 'apple'|'avocado'|'banana'>", array_combine($a, $b)); +} + +function withDifferentNumberOfElements(): void +{ + assertType('*NEVER*', array_combine(['foo'], ['bar', 'baz'])); +} + +function bug11819(): void +{ + $keys = [1, 2, 3]; + $types = array_combine($keys, array_fill(0, \count($keys), false)); + $types[] = 'foo'; + assertType('array{1: false, 2: false, 3: false, 4: \'foo\'}', $types); +} diff --git a/tests/PHPStan/Analyser/data/array-destructuring-types.php b/tests/PHPStan/Analyser/nsrt/array-destructuring-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-destructuring-types.php rename to tests/PHPStan/Analyser/nsrt/array-destructuring-types.php diff --git a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php new file mode 100644 index 0000000000..c0a4e8dd7a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php @@ -0,0 +1,14 @@ +", array_fill_keys($mixed, 'b')); + } else { + assertType("null", array_fill_keys($mixed, 'b')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php new file mode 100644 index 0000000000..4710482534 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php @@ -0,0 +1,14 @@ += 8.0 + +namespace ArrayFillKeysPhp8; + +use function PHPStan\Testing\assertType; + +function mixedAndSubtractedArray($mixed): void +{ + if (is_array($mixed)) { + assertType("array", array_fill_keys($mixed, 'b')); + } else { + assertType("*NEVER*", array_fill_keys($mixed, 'b')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-fill-keys.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php new file mode 100644 index 0000000000..00aa87fc69 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php @@ -0,0 +1,106 @@ +", array_fill_keys([new Bar()], 'b')); + assertType("*ERROR*", array_fill_keys([new Baz()], 'b')); +} + +function withUnionKeys(): void +{ + $arr1 = ['foo', rand(0, 1) ? 'bar1' : 'bar2', 'baz']; + assertType("non-empty-array<'bar1'|'bar2'|'baz'|'foo', 'b'>", array_fill_keys($arr1, 'b')); + + $arr2 = ['foo']; + if (rand(0, 1)) { + $arr2[] = 'bar'; + } + $arr2[] = 'baz'; + assertType("non-empty-array<'bar'|'baz'|'foo', 'b'>", array_fill_keys($arr2, 'b')); +} + +function withOptionalKeys(): void +{ + $arr1 = ['foo', 'bar']; + if (rand(0, 1)) { + $arr1[] = 'baz'; + } + assertType("array{foo: 'b', bar: 'b', baz?: 'b'}", array_fill_keys($arr1, 'b')); + + /** @var array{0?: 'foo', 1: 'bar', }|array{0: 'baz', 1?: 'foobar'} $arr2 */ + $arr2 = []; + assertType("array{baz: 'b', foobar?: 'b'}|array{foo?: 'b', bar: 'b'}", array_fill_keys($arr2, 'b')); +} + +/** + * @param Bar[] $foo + * @param int[] $bar + * @param Foo[] $baz + * @param float[] $floats + * @param array $mixed + * @param list $list + * @param Baz[] $objectsWithoutToString + */ +function withNotConstantArray(array $foo, array $bar, array $baz, array $floats, array $mixed, array $list, array $objectsWithoutToString): void +{ + assertType("array", array_fill_keys($foo, null)); + assertType("array", array_fill_keys($bar, null)); + assertType("array<'foo', null>", array_fill_keys($baz, null)); + assertType("array", array_fill_keys($floats, null)); + assertType("array", array_fill_keys($mixed, null)); + assertType('array', array_fill_keys($list, null)); + assertType('*ERROR*', array_fill_keys($objectsWithoutToString, null)); + + if (array_key_exists(17, $mixed)) { + assertType('non-empty-array', array_fill_keys($mixed, null)); + } + + if (array_key_exists(17, $mixed) && $mixed[17] === 'foo') { + assertType('non-empty-array', array_fill_keys($mixed, null)); + } +} diff --git a/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php b/tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php similarity index 97% rename from tests/PHPStan/Analyser/data/array-filter-arrow-functions.php rename to tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php index 0c39643fc9..4ad761fc71 100644 --- a/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php +++ b/tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php @@ -1,6 +1,6 @@ -= 7.4 +|int<1, max>}|array{b?: non-falsy-string}', array_filter($a)); + + assertType('array{a: int}|array{b?: string}', array_filter($a, function ($v): bool { + return $v !== null; + })); + + $a = ['a' => 1, 'b' => null]; + assertType('array{a: 1}', array_filter($a, function ($v): bool { + return $v !== null; + })); + + assertType('array{a: 1}', array_filter($a)); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-filter-string-callables.php b/tests/PHPStan/Analyser/nsrt/array-filter-string-callables.php similarity index 89% rename from tests/PHPStan/Analyser/data/array-filter-string-callables.php rename to tests/PHPStan/Analyser/nsrt/array-filter-string-callables.php index e143e3bc91..f6e0b6c65e 100644 --- a/tests/PHPStan/Analyser/data/array-filter-string-callables.php +++ b/tests/PHPStan/Analyser/nsrt/array-filter-string-callables.php @@ -1,6 +1,6 @@ $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-falsy-string|true>', $filtered1); + + $filtered2 = array_filter($map2, null, ARRAY_FILTER_USE_KEY); + assertType('array|int<1, max>|non-falsy-string|true>', $filtered2); + + $filtered3 = array_filter($map3, null, ARRAY_FILTER_USE_BOTH); + assertType('array|int<1, max>|non-falsy-string|true>', $filtered3); +} + +function invalidCallableName(array $arr) { + assertType('*ERROR*', array_filter($arr, '')); + assertType('*ERROR*', array_filter($arr, '\\')); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-find-key.php b/tests/PHPStan/Analyser/nsrt/array-find-key.php new file mode 100644 index 0000000000..5caf828f53 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-find-key.php @@ -0,0 +1,62 @@ + $array + * @param callable(mixed, array-key=): mixed $callback + * @return ?array-key + */ + function array_find_key(array $array, callable $callback) + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean + return $key; + } + } + + return null; + } + } + +} + +namespace ArrayFindKey +{ + + use function PHPStan\Testing\assertType; + + /** + * @param array $array + * @phpstan-ignore missingType.callable + */ + function testMixed(array $array, callable $callback): void + { + assertType('int|string|null', array_find_key($array, $callback)); + assertType('int|string|null', array_find_key($array, 'is_int')); + } + + /** + * @param array{1, 'foo', \DateTime} $array + * @phpstan-ignore missingType.callable + */ + function testConstant(array $array, callable $callback): void + { + assertType("0|1|2|null", array_find_key($array, $callback)); + assertType("0|1|2|null", array_find_key($array, 'is_int')); + } + + function testCallback(): void + { + $subject = ['foo' => 1, 'bar' => null, 'buz' => '']; + $result = array_find_key($subject, function ($value, $key) { + assertType("array{value: 1|''|null, key: 'bar'|'buz'|'foo'}", compact('value', 'key')); + + return is_int($value); + }); + + assertType("'bar'|'buz'|'foo'|null", $result); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-find.php b/tests/PHPStan/Analyser/nsrt/array-find.php new file mode 100644 index 0000000000..f3b5b0b822 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-find.php @@ -0,0 +1,79 @@ + $array + * @param callable(mixed, array-key=): mixed $callback + * @return mixed + */ + function array_find(array $array, callable $callback) + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean + return $value; + } + } + + return null; + } + } + +} + +namespace ArrayFind +{ + + use function PHPStan\Testing\assertType; + + /** + * @param array $array + * @param non-empty-array $non_empty_array + * @phpstan-ignore missingType.callable + */ + function testMixed(array $array, array $non_empty_array, callable $callback): void + { + assertType('mixed', array_find($array, $callback)); + assertType('int|null', array_find($array, 'is_int')); + assertType('mixed', array_find($non_empty_array, $callback)); + assertType('int|null', array_find($non_empty_array, 'is_int')); + } + + /** + * @param array{1, 'foo', \DateTime} $array + * @phpstan-ignore missingType.callable + */ + function testConstant(array $array, callable $callback): void + { + assertType("1|'foo'|DateTime|null", array_find($array, $callback)); + assertType('1', array_find($array, 'is_int')); + } + + /** + * @param array $array + * @param non-empty-array $non_empty_array + * @phpstan-ignore missingType.callable + */ + function testInt(array $array, array $non_empty_array, callable $callback): void + { + assertType('int|null', array_find($array, $callback)); + assertType('int|null', array_find($array, 'is_int')); + assertType('int|null', array_find($non_empty_array, $callback)); + // should be 'int' + assertType('int|null', array_find($non_empty_array, 'is_int')); + } + + function testCallback(): void + { + $subject = ['foo' => 1, 'bar' => null, 'buz' => '']; + $result = array_find($subject, function ($value, $key) { + assertType("array{value: 1|''|null, key: 'bar'|'buz'|'foo'}", compact('value', 'key')); + + return is_int($value); + }); + + assertType("1|''|null", $result); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-flip-constant.php b/tests/PHPStan/Analyser/nsrt/array-flip-constant.php new file mode 100644 index 0000000000..1081520823 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-flip-constant.php @@ -0,0 +1,26 @@ +', array_flip($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('null', array_flip($mixed)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-flip-php8.php b/tests/PHPStan/Analyser/nsrt/array-flip-php8.php new file mode 100644 index 0000000000..294554e20c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php8.php @@ -0,0 +1,15 @@ += 8.0 + +namespace ArrayFlipPhp8; + +use function PHPStan\Testing\assertType; + +function mixedAndSubtractedArray($mixed) +{ + if (is_array($mixed)) { + assertType('array<(int|string)>', array_flip($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('*NEVER*', array_flip($mixed)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-flip.php b/tests/PHPStan/Analyser/nsrt/array-flip.php new file mode 100644 index 0000000000..9ec89f5c1d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-flip.php @@ -0,0 +1,95 @@ +', $flip); +} + +/** + * @param mixed[] $list + */ +function foo3($list) +{ + $flip = array_flip($list); + + assertType('array<(int|string)>', $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); +} + +/** + * @param non-empty-array<1|2|3, 4|5|6> $array + */ +function foo6($array) +{ + $flip = array_flip($array); + assertType('non-empty-array<4|5|6, 1|2|3>', $flip); +} + +/** + * @param list<1|2|3> $array + */ +function foo7($array) +{ + $flip = array_flip($array); + assertType('array<1|2|3, int<0, max>>', $flip); +} + +function foo8($mixed) +{ + assertType('mixed', $mixed); + $mixed = array_flip($mixed); + assertType('array', $mixed); +} + +/** @param array $array */ +function foo10(array $array) +{ + if (array_key_exists('foo', $array)) { + assertType('non-empty-array&hasOffset(\'foo\')', $array); + assertType('non-empty-array', array_flip($array)); + } + + if (array_key_exists('foo', $array) && is_int($array['foo'])) { + assertType("non-empty-array&hasOffsetValue('foo', int)", $array); + assertType('non-empty-array', array_flip($array)); + } + + if (array_key_exists('foo', $array) && $array['foo'] === 17) { + assertType("non-empty-array&hasOffsetValue('foo', 17)", $array); + assertType("non-empty-array&hasOffsetValue(17, 'foo')", array_flip($array)); + } + + if ( + array_key_exists('foo', $array) && $array['foo'] === 17 + && array_key_exists('bar', $array) && $array['bar'] === 17 + ) { + assertType("non-empty-array&hasOffsetValue('bar', 17)&hasOffsetValue('foo', 17)", $array); + assertType("*NEVER*", array_flip($array)); // this could be array&hasOffsetValue(17, 'bar') according to https://3v4l.org/1TAFk + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-intersect-key-constant.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key-constant.php new file mode 100644 index 0000000000..29763b0b3d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key-constant.php @@ -0,0 +1,48 @@ +, require-dev: array, stability: string|null, license: string|null, repository: array, autoload: string|null, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool, no-interaction: bool, profile: bool, no-plugins: bool, no-scripts: bool, working-dir: string|null, no-cache: bool} $options + * @return void + */ + public function doFoo(array $options): void + { + assertType('array{name: string|null, description: string|null, author: string|null, type: string|null, homepage: string|null, require: array, require-dev: array, stability: string|null, license: string|null, repository: array, autoload: string|null, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool, no-interaction: bool, profile: bool, no-plugins: bool, no-scripts: bool, working-dir: string|null, no-cache: bool}', $options); + + $allowlist = ['name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license', 'autoload']; + $options = array_filter(array_intersect_key($options, array_flip($allowlist))); + assertType('array{name?: non-falsy-string, description?: non-falsy-string, author?: non-falsy-string, type?: non-falsy-string, homepage?: non-falsy-string, require?: non-empty-array, require-dev?: non-empty-array, stability?: non-falsy-string, license?: non-falsy-string, autoload?: non-falsy-string}', $options); + } + + public function doBar(): void + { + assertType('array{a: 1}', array_intersect_key(['a' => 1])); + assertType('array{}', array_intersect_key(['a' => 1], [])); + + $a = ['a' => 1]; + if (rand(0, 1)) { + $a['b'] = 2; + } + + assertType('array{a: 1, b?: 2}', array_intersect_key(['a' => 1, 'b' => 2], $a)); + } + + public function doBaz(): void + { + $a = []; + if (rand(0, 1)) { + $a['a'] = 1; + } + $a['b'] = 2; + + assertType('array{a?: 1, b: 2}', array_intersect_key($a, ['a' => 1, 'b' => 2])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php new file mode 100644 index 0000000000..14dd7b5d0d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php @@ -0,0 +1,27 @@ + $otherArrs */ + assertType('array', array_intersect_key($mixed, $otherArrs)); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($mixed, $otherArrs)); + } else { + assertType('mixed~array', $mixed); + /** @var array $otherArrs */ + assertType('null', array_intersect_key($mixed, $otherArrs)); + /** @var array $otherArrs */ + assertType('null', array_intersect_key($mixed, $otherArrs)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php new file mode 100644 index 0000000000..9ffde9da83 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php @@ -0,0 +1,27 @@ += 8.0 + +namespace ArrayIntersectKeyPhp8; + +use function array_intersect_key; +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function mixedAndSubtractedArray($mixed, array $otherArrs): void + { + if (is_array($mixed)) { + /** @var array $otherArrs */ + assertType('array', array_intersect_key($mixed, $otherArrs)); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($mixed, $otherArrs)); + } else { + assertType('mixed~array', $mixed); + /** @var array $otherArrs */ + assertType('*NEVER*', array_intersect_key($mixed, $otherArrs)); + /** @var array $otherArrs */ + assertType('*NEVER*', array_intersect_key($mixed, $otherArrs)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-intersect-key.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key.php new file mode 100644 index 0000000000..3369063444 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key.php @@ -0,0 +1,91 @@ + $arr + * @param non-empty-array $arr2 + */ + public function nonEmpty(array $arr, array $arr2): void + { + assertType('non-empty-array', array_intersect_key($arr)); + assertType('array', array_intersect_key($arr, $arr)); + assertType('array', array_intersect_key($arr, $arr2)); + assertType('array', array_intersect_key($arr2, $arr)); + assertType('array{}', array_intersect_key($arr, [])); + assertType("array<'foo', string>", array_intersect_key($arr, ['foo' => 17])); + } + + + /** + * @param array $arr + * @param array $arr2 + */ + public function normalArrays(array $arr, array $arr2, array $otherArrs): void + { + assertType('array', array_intersect_key($arr)); + assertType('array', array_intersect_key($arr, $arr)); + assertType('array', array_intersect_key($arr, $arr2)); + assertType('array', array_intersect_key($arr2, $arr)); + assertType('array{}', array_intersect_key($arr, [])); + assertType("array<'foo', string>", array_intersect_key($arr, ['foo' => 17])); + + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr, $otherArrs)); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr, $otherArrs)); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr, $otherArrs)); + /** @var array<17, int> $otherArrs */ + assertType('array<17, string>', array_intersect_key($arr, $otherArrs)); + /** @var array $otherArrs */ + assertType('array<\'\', string>', array_intersect_key($arr, $otherArrs)); + + if (array_key_exists(17, $arr2)) { + assertType('non-empty-array<17, string>&hasOffset(17)', array_intersect_key($arr2, [17 => 'bar'])); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr2, $otherArrs)); + /** @var array $otherArrs */ + assertType('array{}', array_intersect_key($arr2, $otherArrs)); + } + + if (array_key_exists(17, $arr2) && $arr2[17] === 'foo') { + assertType("non-empty-array<17, string>&hasOffsetValue(17, 'foo')", array_intersect_key($arr2, [17 => 'bar'])); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr2, $otherArrs)); + /** @var array $otherArrs */ + assertType('array{}', array_intersect_key($arr2, $otherArrs)); + } + } + + /** + * @param list> $arrs + * @param list> $arrs2 + */ + public function arrayUnpacking(array $arrs, array $arrs2): void + { + assertType('array', array_intersect_key(...$arrs)); + assertType('array', array_intersect_key(...$arrs2)); + assertType('array', array_intersect_key(...$arrs, ...$arrs2)); + assertType('array', array_intersect_key(...$arrs2, ...$arrs)); + } + + /** @param list $arr */ + public function list(array $arr, array $otherArrs): void + { + assertType('list', array_intersect_key($arr, ['foo', 'bar'])); + /** @var array $otherArrs */ + assertType('array, string>', array_intersect_key($arr, $otherArrs)); + /** @var array $otherArrs */ + assertType('array{}', array_intersect_key($arr, $otherArrs)); + /** @var list $otherArrs */ + assertType('list', array_intersect_key($arr, $otherArrs)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-is-list-offset.php b/tests/PHPStan/Analyser/nsrt/array-is-list-offset.php new file mode 100644 index 0000000000..911490fac7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-is-list-offset.php @@ -0,0 +1,19 @@ + $key + */ + public function test(array $array, int $key) { + assertType('int<0, 1>', $key); + assertType('true', array_is_list($array)); + + $array[$key] = false; + assertType('true', array_is_list($array)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php b/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php new file mode 100644 index 0000000000..1b788158d6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php @@ -0,0 +1,61 @@ + $foo */ + +if (array_is_list($foo)) { + assertType('list', $foo); +} else { + assertType('array', $foo); +} + +$baz = []; + +if (array_is_list($baz)) { + assertType('array{}', $baz); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-is-list-unset.php b/tests/PHPStan/Analyser/nsrt/array-is-list-unset.php new file mode 100644 index 0000000000..b14b8c97df --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-is-list-unset.php @@ -0,0 +1,26 @@ += 8.0 + +namespace ArrayKeyExistsExtension; + +use function array_key_exists; +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param array $a + * @return void + */ + public function doFoo(array $a, string $key, int $anotherKey): void + { + assertType('false', array_key_exists(2, $a)); + assertType('bool', array_key_exists('foo', $a)); + assertType('false', array_key_exists('2', $a)); + + $a = ['foo' => 2, 3 => 'bar']; + assertType('true', array_key_exists('foo', $a)); + assertType('true', array_key_exists(3, $a)); + assertType('true', array_key_exists('3', $a)); + assertType('false', array_key_exists(4, $a)); + + if (array_key_exists($key, $a)) { + assertType("'3'|'foo'", $key); + } + if (array_key_exists($anotherKey, $a)) { + assertType('3', $anotherKey); + } + + $empty = []; + assertType('false', array_key_exists('foo', $empty)); + assertType('false', array_key_exists($key, $empty)); + } + + /** + * @param array $a + * @param array $b + * @param array $c + * @param array-key $key4 + * + * @return void + */ + public function doBar(array $a, array $b, array $c, int $key1, string $key2, int|string $key3, $key4, mixed $key5): void + { + if (array_key_exists($key1, $a)) { + assertType('int', $key1); + } + if (array_key_exists($key2, $a)) { + assertType('lowercase-string&numeric-string&uppercase-string', $key2); + } + if (array_key_exists($key3, $a)) { + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key3); + } + if (array_key_exists($key4, $a)) { + assertType('(int|(lowercase-string&numeric-string&uppercase-string))', $key4); + } + if (array_key_exists($key5, $a)) { + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key5); + } + + if (array_key_exists($key1, $b)) { + assertType('*NEVER*', $key1); + } + if (array_key_exists($key2, $b)) { + assertType('string', $key2); + } + if (array_key_exists($key3, $b)) { + assertType('string', $key3); + } + if (array_key_exists($key4, $b)) { + assertType('string', $key4); + } + if (array_key_exists($key5, $b)) { + assertType('string', $key5); + } + + if (array_key_exists($key1, $c)) { + assertType('int', $key1); + } + if (array_key_exists($key2, $c)) { + assertType('string', $key2); + } + if (array_key_exists($key3, $c)) { + assertType('(int|string)', $key3); + } + if (array_key_exists($key4, $c)) { + assertType('(int|string)', $key4); + } + if (array_key_exists($key5, $c)) { + assertType('(int|string)', $key5); + } + + if (array_key_exists($key1, [3 => 'foo', 4 => 'bar'])) { + assertType('3|4', $key1); + } + if (array_key_exists($key2, [3 => 'foo', 4 => 'bar'])) { + assertType("'3'|'4'", $key2); + } + if (array_key_exists($key3, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key3); + } + if (array_key_exists($key4, [3 => 'foo', 4 => 'bar'])) { + assertType("(3|4|'3'|'4')", $key4); + } + if (array_key_exists($key5, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key5); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/array-key.php b/tests/PHPStan/Analyser/nsrt/array-key.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-key.php rename to tests/PHPStan/Analyser/nsrt/array-key.php diff --git a/tests/PHPStan/Analyser/data/array-map-closure.php b/tests/PHPStan/Analyser/nsrt/array-map-closure.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-map-closure.php rename to tests/PHPStan/Analyser/nsrt/array-map-closure.php diff --git a/tests/PHPStan/Analyser/nsrt/array-map.php b/tests/PHPStan/Analyser/nsrt/array-map.php new file mode 100644 index 0000000000..75a68ab490 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-map.php @@ -0,0 +1,74 @@ + $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('list', $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-list', $mapped); +} + +/** @param array{foo?: 0, bar?: 1, baz?: 2} $array */ +function foo5(array $array): void { + $mapped = array_map( + static function(string $string): string { + return (string) $string; + }, + $array + ); + + assertType('array{foo?: string, bar?: string, baz?: string}', $mapped); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-merge.php b/tests/PHPStan/Analyser/nsrt/array-merge.php new file mode 100644 index 0000000000..6dd7a13005 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-merge.php @@ -0,0 +1,54 @@ + 17, 'a', 'bar' => 18, 'b']; + $bar = [99 => 'b', 'bar' => 19, 98 => 'c']; + $baz = array_merge($foo, $bar); + + assertType('array{foo: 17, 0: \'a\', bar: 19, 1: \'b\', 2: \'b\', 3: \'c\'}', $baz); +} + +/** + * @param string[][] $foo + * @param array> $bar1 + * @param non-empty-array> $bar2 + * @param non-empty-array> $bar3 + */ +function unpackingArrays(array $foo, array $bar1, array $bar2, array $bar3): void +{ + assertType('array', array_merge([], ...$foo)); + assertType('array', array_merge([], ...$bar1)); + assertType('non-empty-array', array_merge([], ...$bar2)); + assertType('array', array_merge([], ...$bar3)); +} + +function unpackingConstantArrays(): void +{ + assertType('array{}', array_merge([], ...[])); + assertType('array{17}', array_merge([], [17])); + assertType('array{17}', array_merge([], ...[[17]])); + assertType('array{foo: \'bar\', bar: \'baz2\', 0: 17}', array_merge(['foo' => 'bar', 'bar' => 'baz1'], ['bar' => 'baz2', 17])); + assertType('array{foo: \'bar\', bar: \'baz2\', 0: 17}', array_merge(['foo' => 'bar', 'bar' => 'baz1'], ...[['bar' => 'baz2', 17]])); +} + +/** + * @param list $a + * @param list $b + * @return void + */ +function listIsStillList(array $a, array $b): void +{ + assertType('list', array_merge($a, $b)); + + $c = []; + foreach ($a as $v) { + $c = array_merge($a, $c); + } + assertType('list', $c); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-merge2.php b/tests/PHPStan/Analyser/nsrt/array-merge2.php new file mode 100644 index 0000000000..f0d86e61b6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-merge2.php @@ -0,0 +1,62 @@ + 3])); + assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ...[['foo' => 3]])); + assertType("array{foo: '1', bar: '2'|'4', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge(rand(0, 1) ? $array1 : $array2, [])); + assertType("array{foo?: 3, bar?: 3}", array_merge([], ...[rand(0, 1) ? ['foo' => 3] : ['bar' => 3]])); + assertType("array{foo: '1', bar: '2'|'4', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge([], ...[rand(0, 1) ? $array1 : $array2])); + assertType("array{foo: 1, bar: 2, 0: 2, 1: 3}", array_merge(['foo' => 4, 'bar' => 5], ...[['foo' => 1, 'bar' => 2], [2, 3]])); + } + + /** + * @param int[] $array1 + * @param string[] $array2 + */ + public function arrayMergeSimple($array1, $array2): void + { + assertType("array", array_merge($array1, $array1)); + assertType("array", array_merge($array1, $array2)); + assertType("array", array_merge($array2, $array1)); + } + + /** + * @param array $array1 + * @param array $array2 + */ + public function arrayMergeUnionType($array1, $array2): void + { + assertType("list", array_merge($array1, $array1)); + assertType("list", array_merge($array1, $array2)); + assertType("list", array_merge($array2, $array1)); + } + + /** + * @param array $array1 + * @param array $array2 + */ + public function arrayMergeUnionTypeArrayShapes($array1, $array2): void + { + assertType("list", array_merge($array1, $array1)); + assertType("list", array_merge($array1, $array2)); + assertType("list", array_merge($array2, $array1)); + } +} diff --git a/tests/PHPStan/Analyser/data/array-next.php b/tests/PHPStan/Analyser/nsrt/array-next.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-next.php rename to tests/PHPStan/Analyser/nsrt/array-next.php diff --git a/tests/PHPStan/Analyser/nsrt/array-offset-unset.php b/tests/PHPStan/Analyser/nsrt/array-offset-unset.php new file mode 100644 index 0000000000..0c9395d6ce --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-offset-unset.php @@ -0,0 +1,16 @@ + $list + */ +function foo(array $list) { + assertType('array<0|1, mixed>', $list); + unset($list[0]); + assertType('array<1, mixed>', $list); + unset($list[1]); + assertType('array{}', $list); +} diff --git a/tests/PHPStan/Analyser/data/array-plus.php b/tests/PHPStan/Analyser/nsrt/array-plus.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-plus.php rename to tests/PHPStan/Analyser/nsrt/array-plus.php diff --git a/tests/PHPStan/Analyser/nsrt/array-pop.php b/tests/PHPStan/Analyser/nsrt/array-pop.php new file mode 100644 index 0000000000..37a986b0ce --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-pop.php @@ -0,0 +1,96 @@ + $arr */ + assertType('string', array_pop($arr)); + assertType('array', $arr); + } + + public function normalArrays(array $arr): void + { + /** @var array $arr */ + assertType('string|null', array_pop($arr)); + assertType('array', $arr); + } + + public function compoundTypes(array $arr): void + { + /** @var string[]|int[] $arr */ + assertType('int|string|null', array_pop($arr)); + assertType('array', $arr); + } + + public function constantArrays(array $arr): void + { + /** @var array{a: 0, b: 1, c: 2} $arr */ + assertType('2', array_pop($arr)); + assertType('array{a: 0, b: 1}', $arr); + + /** @var array{} $arr */ + assertType('null', array_pop($arr)); + assertType('array{}', $arr); + } + + public function constantArraysWithOptionalKeys(array $arr): void + { + /** @var array{a?: 0, b: 1, c: 2} $arr */ + assertType('2', array_pop($arr)); + assertType('array{a?: 0, b: 1}', $arr); + + /** @var array{a: 0, b?: 1, c: 2} $arr */ + assertType('2', array_pop($arr)); + assertType('array{a: 0, b?: 1}', $arr); + + /** @var array{a: 0, b: 1, c?: 2} $arr */ + assertType('1|2', array_pop($arr)); + assertType('array{a: 0, b?: 1}', $arr); + + /** @var array{a?: 0, b?: 1, c?: 2} $arr */ + assertType('0|1|2|null', array_pop($arr)); + assertType('array{a?: 0, b?: 1}', $arr); + } + + public function list(array $arr): void + { + /** @var list $arr */ + assertType('string|null', array_pop($arr)); + assertType('list', $arr); + } + + public function mixed($mixed): void + { + assertType('mixed', array_pop($mixed)); + assertType('array', $mixed); + } + + public function foo1($mixed): void + { + if(is_array($mixed)) { + assertType('mixed', array_pop($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('mixed', array_pop($mixed)); + assertType('*ERROR*', $mixed); + } + } + + /** @param non-empty-array $arr1 */ + public function nativeTypes(array $arr1, array $arr2): void + { + assertType('string', array_pop($arr1)); + assertType('array', $arr1); + + assertNativeType('mixed', array_pop($arr2)); + assertNativeType('array', $arr2); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-push.php b/tests/PHPStan/Analyser/nsrt/array-push.php new file mode 100644 index 0000000000..4e2e235530 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-push.php @@ -0,0 +1,74 @@ + $c + * @param array $d + * @param list $e + */ +function arrayPush(array $a, array $b, array $c, array $d, array $e, array $arr): void +{ + array_push($a, ...$b); + assertType('array', $a); + + /** @var non-empty-array $arr */ + array_push($arr, ...$b); + assertType('non-empty-array', $arr); + + array_push($b, ...[]); + assertType('array', $b); + + array_push($c, ...[19, 'baz', false]); + assertType('non-empty-array<\'baz\'|int|false>', $c); + + /** @var array $d1 */ + $d1 = []; + array_push($d, ...$d1); + assertType('array', $d); + + /** @var list $e1 */ + $e1 = []; + array_push($e, ...$e1); + assertType('list', $e); +} + +function arrayPushConstantArray(): void +{ + $a = ['foo' => 17, 'a', 'bar' => 18,]; + array_push($a, ...[19, 'baz', false]); + assertType('array{foo: 17, 0: \'a\', bar: 18, 1: 19, 2: \'baz\', 3: false}', $a); + + $b = ['foo' => 17, 'a', 'bar' => 18]; + array_push($b, 19, 'baz', false); + assertType('array{foo: 17, 0: \'a\', bar: 18, 1: 19, 2: \'baz\', 3: false}', $b); + + $c = ['foo' => 17, 'a', 'bar' => 18]; + array_push($c, ...[]); + assertType('array{foo: 17, 0: \'a\', bar: 18}', $c); + + $d = []; + array_push($d, ...[]); + assertType('array{}', $d); + + $e = []; + array_push($e, 19, 'baz', false); + assertType('array{19, \'baz\', false}', $e); + + $f = [17]; + /** @var array $f1 */ + $f1 = []; + array_push($f, ...$f1); + assertType('non-empty-list<17|bool|null>', $f); + + $g = [new stdClass()]; + array_push($g, ...[new stdClass(), new stdClass()]); + assertType('array{stdClass, stdClass, stdClass}', $g); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-replace.php b/tests/PHPStan/Analyser/nsrt/array-replace.php new file mode 100644 index 0000000000..436ce232fa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-replace.php @@ -0,0 +1,71 @@ +", array_replace($array1)); + assertType("non-empty-array<'bar'|'foo', '1'|'2'>", array_replace([], $array1)); + assertType("non-empty-array<'bar'|'foo', '1'|'2'|'4'>", array_replace($array1, $array2)); + } + + /** + * @param int[] $array1 + * @param string[] $array2 + */ + public function arrayReplaceSimple($array1, $array2): void + { + assertType("array", array_replace($array1, $array1)); + assertType("array", array_replace($array1, $array2)); + assertType("array", array_replace($array2, $array1)); + } + + /** + * @param int[] ...$arrays1 + */ + public function arrayReplaceVariadic(...$arrays1): void + { + assertType("array", array_replace(...$arrays1)); + } + + /** + * @param array $array1 + * @param array $array2 + */ + public function arrayReplaceUnionType($array1, $array2): void + { + assertType("array", array_replace($array1, $array1)); + assertType("array", array_replace($array1, $array2)); + assertType("array", array_replace($array2, $array1)); + } + + /** + * @param array $array1 + * @param array $array2 + */ + public function arrayReplaceUnionTypeArrayShapes($array1, $array2): void + { + assertType("array", array_replace($array1, $array1)); + assertType("array", array_replace($array1, $array2)); + assertType("array", array_replace($array2, $array1)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-reverse-php7.php b/tests/PHPStan/Analyser/nsrt/array-reverse-php7.php new file mode 100644 index 0000000000..4c9d1ab563 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-reverse-php7.php @@ -0,0 +1,16 @@ += 8.0 + +declare(strict_types = 1); + +namespace ArrayReversePhp8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function notArray(bool $bool): void + { + assertType('*NEVER*', array_reverse($bool)); + assertType('*NEVER*', array_reverse($bool, true)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-reverse.php b/tests/PHPStan/Analyser/nsrt/array-reverse.php new file mode 100644 index 0000000000..86a3bb72cf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-reverse.php @@ -0,0 +1,79 @@ += 8.0 + +declare(strict_types = 1); + +namespace ArrayReverse; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** + * @param mixed[] $a + * @param array $b + */ + public function normalArrays(array $a, array $b): void + { + assertType('array', array_reverse($a)); + assertType('array', array_reverse($a, true)); + + assertType('array', array_reverse($b)); + assertType('array', array_reverse($b, true)); + } + + /** + * @param array{a: 'foo', b: 'bar', c?: 'baz'} $a + * @param array{17: 'foo', 19: 'bar'}|array{foo: 17, bar: 19} $b + * @param array{0: 'A', 1?: 'B', 2?: 'C'} $c + */ + public function constantArrays(array $a, array $b, array $c): void + { + assertType('array{}', array_reverse([])); + assertType('array{}', array_reverse([], true)); + + assertType('array{1337, null, 42}', array_reverse([42, null, 1337])); + assertType('array{2: 1337, 1: null, 0: 42}', array_reverse([42, null, 1337], true)); + + assertType('array{test3: 1337, test2: null, test1: 42}', array_reverse(['test1' => 42, 'test2' => null, 'test3' => 1337])); + assertType('array{test3: 1337, test2: null, test1: 42}', array_reverse(['test1' => 42, 'test2' => null, 'test3' => 1337], true)); + + assertType('array{test3: 1337, test2: \'test 2\', 0: 42}', array_reverse([42, 'test2' => 'test 2', 'test3' => 1337])); + assertType('array{test3: 1337, test2: \'test 2\', 0: 42}', array_reverse([42, 'test2' => 'test 2', 'test3' => 1337], true)); + + assertType('array{bar: 17, 0: 1337, foo: null, 1: 42}', array_reverse([2 => 42, 'foo' => null, 3 => 1337, 'bar' => 17])); + assertType('array{bar: 17, 3: 1337, foo: null, 2: 42}', array_reverse([2 => 42, 'foo' => null, 3 => 1337, 'bar' => 17], true)); + + assertType('array{c?: \'baz\', b: \'bar\', a: \'foo\'}', array_reverse($a)); + assertType('array{c?: \'baz\', b: \'bar\', a: \'foo\'}', array_reverse($a, true)); + + assertType('array{\'bar\', \'foo\'}|array{bar: 19, foo: 17}', array_reverse($b)); + assertType('array{19: \'bar\', 17: \'foo\'}|array{bar: 19, foo: 17}', array_reverse($b, true)); + + assertType("array{0: 'A'|'B'|'C', 1?: 'A'|'B', 2?: 'A'}", array_reverse($c)); + assertType("array{2?: 'C', 1?: 'B', 0: 'A'}", array_reverse($c, true)); + } + + /** + * @param list $a + * @param non-empty-list $b + */ + public function list(array $a, array $b): void + { + assertType('list', array_reverse($a)); + assertType('array, string>', array_reverse($a, true)); + + assertType('non-empty-list', array_reverse($b)); + assertType('non-empty-array, string>', array_reverse($b, true)); + } + + public function mixed(mixed $mixed): void + { + assertType('array', array_reverse($mixed)); + assertType('array', array_reverse($mixed, true)); + + if (array_key_exists('foo', $mixed)) { + assertType('non-empty-array', array_reverse($mixed)); + assertType("non-empty-array&hasOffset('foo')", array_reverse($mixed, true)); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-search-php7.php b/tests/PHPStan/Analyser/nsrt/array-search-php7.php new file mode 100644 index 0000000000..5816daf659 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-search-php7.php @@ -0,0 +1,26 @@ +', $mixed); + assertType('null', array_search('foo', $mixed, true)); + assertType('null', array_search('foo', $mixed)); + assertType('null', array_search($string, $mixed, true)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-search-php8.php b/tests/PHPStan/Analyser/nsrt/array-search-php8.php new file mode 100644 index 0000000000..30b9527e10 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-search-php8.php @@ -0,0 +1,26 @@ += 8.0 + +declare(strict_types = 1); + +namespace ArraySearchPhp8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function mixedAndSubtractedArray($mixed, string $string): void + { + if (is_array($mixed)) { + assertType('int|string|false', array_search('foo', $mixed, true)); + assertType('int|string|false', array_search('foo', $mixed)); + assertType('int|string|false', array_search($string, $mixed, true)); + } else { + assertType('mixed~array', $mixed); + assertType('*NEVER*', array_search('foo', $mixed, true)); + assertType('*NEVER*', array_search('foo', $mixed)); + assertType('*NEVER*', array_search($string, $mixed, true)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-search-type-specifying.php b/tests/PHPStan/Analyser/nsrt/array-search-type-specifying.php new file mode 100644 index 0000000000..b500713f80 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-search-type-specifying.php @@ -0,0 +1,47 @@ + $arr */ + assertType('int|string|false', array_search('foo', $arr, true)); + assertType('int|string|false', array_search('foo', $arr)); + assertType('int|string|false', array_search($string, $arr, true)); + } + + public function normalArrays(array $arr, string $string): void + { + /** @var array $arr */ + assertType('int|false', array_search('foo', $arr, true)); + assertType('int|false', array_search('foo', $arr)); + assertType('int|false', array_search($string, $arr, true)); + + if (array_key_exists(17, $arr)) { + assertType('int|false', array_search('foo', $arr, true)); + assertType('int|false', array_search('foo', $arr)); + assertType('int|false', array_search($string, $arr, true)); + } + + if (array_key_exists(17, $arr) && $arr[17] === 'foo') { + assertType('17', array_search('foo', $arr, true)); + assertType('int|false', array_search('foo', $arr)); + assertType('int|false', array_search($string, $arr, true)); + } + } + + public function constantArrays(array $arr, string $string): void + { + /** @var array{'a', 'b', 'c'} $arr */ + assertType('1', array_search('b', $arr, true)); + assertType('0|1|2|false', array_search('b', $arr)); + assertType('0|1|2|false', array_search($string, $arr, true)); + + /** @var array{} $arr */ + assertType('false', array_search('b', $arr, true)); + assertType('false', array_search('b', $arr)); + assertType('false', array_search($string, $arr, true)); + } + + public function constantArraysWithOptionalKeys(array $arr, string $string): void + { + /** @var array{0: 'a', 1?: 'b', 2: 'c'} $arr */ + assertType('1|false', array_search('b', $arr, true)); + assertType('0|1|2|false', array_search('b', $arr)); + assertType('0|1|2|false', array_search($string, $arr, true)); + + /** @var array{0: 'a', 1?: 'b', 2: 'b'} $arr */ + assertType('1|2', array_search('b', $arr, true)); + assertType('0|1|2|false', array_search('b', $arr)); + assertType('0|1|2|false', array_search($string, $arr, true)); + } + + public function list(array $arr, string $string): void + { + /** @var list $arr */ + assertType('int<0, max>|false', array_search('foo', $arr, true)); + assertType('int<0, max>|false', array_search('foo', $arr)); + assertType('int<0, max>|false', array_search($string, $arr, true)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-shape-from-general-array-with-single-finite-key.php b/tests/PHPStan/Analyser/nsrt/array-shape-from-general-array-with-single-finite-key.php new file mode 100644 index 0000000000..50c027445c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-shape-from-general-array-with-single-finite-key.php @@ -0,0 +1,26 @@ + $a + */ + public function doFoo(array $a): void + { + assertType('array{1?: string}', $a); + } + + /** + * @param non-empty-array<1, string> $a + */ + public function doBar(array $a): void + { + assertType('array{1: string}', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php b/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php new file mode 100644 index 0000000000..10049a8317 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php @@ -0,0 +1,32 @@ + $arr */ + assertType('string', array_shift($arr)); + assertType('array', $arr); + } + + public function normalArrays(array $arr): void + { + /** @var array $arr */ + assertType('string|null', array_shift($arr)); + assertType('array', $arr); + } + + public function compoundTypes(array $arr): void + { + /** @var string[]|int[] $arr */ + assertType('int|string|null', array_shift($arr)); + assertType('array', $arr); + } + + public function constantArrays(array $arr): void + { + /** @var array{a: 0, b: 1, c: 2} $arr */ + assertType('0', array_shift($arr)); + assertType('array{b: 1, c: 2}', $arr); + + /** @var array{} $arr */ + assertType('null', array_shift($arr)); + assertType('array{}', $arr); + } + + public function constantArraysWithOptionalKeys(array $arr): void + { + /** @var array{a?: 0, b: 1, c: 2} $arr */ + assertType('0|1', array_shift($arr)); + assertType('array{b?: 1, c: 2}', $arr); + + /** @var array{a: 0, b?: 1, c: 2} $arr */ + assertType('0', array_shift($arr)); + assertType('array{b?: 1, c: 2}', $arr); + + /** @var array{a: 0, b: 1, c?: 2} $arr */ + assertType('0', array_shift($arr)); + assertType('array{b: 1, c?: 2}', $arr); + + /** @var array{a?: 0, b?: 1, c?: 2} $arr */ + assertType('0|1|2|null', array_shift($arr)); + assertType('array{b?: 1, c?: 2}', $arr); + } + + public function list(array $arr): void + { + /** @var list $arr */ + assertType('string|null', array_shift($arr)); + assertType('list', $arr); + } + + public function mixed($mixed): void + { + assertType('mixed', array_shift($mixed)); + assertType('array', $mixed); + } + + public function foo1($mixed): void + { + if(is_array($mixed)) { + assertType('mixed', array_shift($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('mixed', array_shift($mixed)); + assertType('*ERROR*', $mixed); + } + } + + /** @param non-empty-array $arr1 */ + public function nativeTypes(array $arr1, array $arr2): void + { + assertType('string', array_shift($arr1)); + assertType('array', $arr1); + + assertNativeType('mixed', array_shift($arr2)); + assertNativeType('array', $arr2); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array-slice.php b/tests/PHPStan/Analyser/nsrt/array-slice.php new file mode 100644 index 0000000000..caf08c8d65 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-slice.php @@ -0,0 +1,130 @@ +|non-empty-list $c + */ + public function nonEmpty(array $a, array $b, array $c): void + { + assertType('array', array_slice($a, 1)); + assertType('list', array_slice($b, 1)); + assertType('array', array_slice($c, 1)); + } + + /** + * @param mixed $arr + */ + public function fromMixed($arr): void + { + assertType('array', array_slice($arr, 1, 2)); + } + + public function normalArrays(array $arr): void + { + /** @var array $arr */ + assertType('list', array_slice($arr, 1, 2)); + assertType('array', array_slice($arr, 1, 2, true)); + + /** @var array $arr */ + assertType('array', array_slice($arr, 1, 2)); + assertType('array', array_slice($arr, 1, 2, true)); + + /** @var non-empty-array $arr */ + assertType('array{}', array_slice($arr, 0, 0)); + assertType('array{}', array_slice($arr, 0, 0, true)); + + /** @var non-empty-array $arr */ + assertType('array', array_slice($arr, 0, 1)); + assertType('array', array_slice($arr, 0, 1, true)); + + /** @var list $arr */ + assertType('list', array_slice($arr, 0, 1)); + assertType('list', array_slice($arr, 0, 1, true)); + + /** @var non-empty-list $arr */ + assertType('non-empty-list', array_slice($arr, 0, 1)); + assertType('non-empty-list', array_slice($arr, 0, 1, true)); + } + + public function constantArrays(array $arr): void + { + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + assertType('array{b: \'bar\', 0: \'baz\'}', array_slice($arr, 1, 2)); + assertType('array{b: \'bar\', 19: \'baz\'}', array_slice($arr, 1, 2, true)); + assertType('array<17|19|\'b\', \'bar\'|\'baz\'|\'foo\'>', array_slice($arr, rand(0, 1) ? 0 : 1, rand(0, 1) ? 0 : 1)); + + /** @var array{17: 'foo', 19: 'bar', 21: 'baz'}|array{foo: 17, bar: 19, baz: 21} $arr */ + assertType('array{\'bar\', \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr, 1, 2)); + assertType('array{19: \'bar\', 21: \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr, 1, 2, true)); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + assertType('array{}', array_slice($arr, -1, -1)); + assertType('array{}', array_slice($arr, -1, -1, true)); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + assertType('array{}', array_slice($arr, -1, -2)); + assertType('array{}', array_slice($arr, -1, -2, true)); + } + + public function constantArraysWithOptionalKeys(array $arr): void + { + /** @var array{a?: 0, b: 1, c: 2} $arr */ + assertType('array{a?: 0, b?: 1}', array_slice($arr, 0, 1)); + assertType('array{a?: 0, b: 1, c: 2}', array_slice($arr, 0)); + assertType('array{a?: 0, b: 1, c: 2}', array_slice($arr, -99)); + assertType('array{a?: 0, b: 1, c: 2}', array_slice($arr, 0, 99)); + assertType('array{a?: 0}', array_slice($arr, 0, -2)); + assertType('array{}', array_slice($arr, 0, -3)); + assertType('array{}', array_slice($arr, 0, -99)); + assertType('array{}', array_slice($arr, -99, -99)); + assertType('array{}', array_slice($arr, 99)); + + /** @var array{a?: 0, b?: 1, c: 2, d: 3, e: 4} $arr */ + assertType('array{c?: 2, d?: 3, e?: 4}', array_slice($arr, 2, 1)); + assertType('array{b?: 1, c?: 2, d: 3, e?: 4}', array_slice($arr, 1, 3)); + assertType('array{e: 4}', array_slice($arr, -1)); + assertType('array{d: 3}', array_slice($arr, -2, 1)); + + /** @var array{a: 0, b: 1, c: 2, d?: 3, e?: 4} $arr */ + assertType('array{c: 2}', array_slice($arr, 2, 1)); + assertType('array{c?: 2, d?: 3, e?: 4}', array_slice($arr, -1)); + assertType('array{b?: 1, c?: 2, d?: 3}', array_slice($arr, -2, 1)); + assertType('array{a: 0, b: 1, c?: 2, d?: 3}', array_slice($arr, 0, -1)); + assertType('array{a: 0, b?: 1, c?: 2}', array_slice($arr, 0, -2)); + + /** @var array{a: 0, b?: 1, c: 2, d?: 3, e: 4} $arr */ + assertType('array{b?: 1, c: 2, d?: 3, e?: 4}', array_slice($arr, 1, 2)); + assertType('array{a: 0, b?: 1, c?: 2}', array_slice($arr, 0, 2)); + assertType('array{a: 0}', array_slice($arr, 0, 1)); + assertType('array{b?: 1, c?: 2}', array_slice($arr, 1, 1)); + assertType('array{c?: 2, d?: 3, e?: 4}', array_slice($arr, 2, 1)); + assertType('array{a: 0, b?: 1, c: 2, d?: 3}', array_slice($arr, 0, -1)); + assertType('array{c?: 2, d?: 3, e: 4}', array_slice($arr, -2)); + + /** @var array{a: 0, b?: 1, c: 2} $arr */ + assertType('array{a: 0, b?: 1}', array_slice($arr, 0, -1)); + assertType('array{a: 0}', array_slice($arr, -3, 1)); + } + + + public function offsets(array $arr): void + { + if (array_key_exists(1, $arr)) { + assertType('non-empty-array', array_slice($arr, 1, null, false)); + assertType('non-empty-array&hasOffset(1)', array_slice($arr, 1, null, true)); + } + if (array_key_exists(1, $arr) && $arr[1] === 'foo') { + assertType('non-empty-array', array_slice($arr, 1, null, false)); + assertType("non-empty-array&hasOffsetValue(1, 'foo')", array_slice($arr, 1, null, true)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-sum.php b/tests/PHPStan/Analyser/nsrt/array-sum.php new file mode 100644 index 0000000000..3d53b450e3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-sum.php @@ -0,0 +1,266 @@ + $floatList + */ +function foo3($floatList) +{ + $sum = array_sum($floatList); + assertType('float', $sum); +} + +/** + * @param mixed[] $list + */ +function foo4($list) +{ + $sum = array_sum($list); + assertType('(float|int)', $sum); +} + +/** + * @param string[] $list + */ +function foo5($list) +{ + $sum = array_sum($list); + assertType('float|int', $sum); +} + +/** + * @param list<0> $list + */ +function foo6($list) +{ + assertType('0', array_sum($list)); +} +/** + * @param list<1> $list + */ +function foo7($list) +{ + assertType('int<0, max>', array_sum($list)); +} + +/** + * @param non-empty-list<1> $list + */ +function foo8($list) +{ + assertType('int<1, max>', array_sum($list)); +} + +/** + * @param list<-1> $list + */ +function foo9($list) +{ + assertType('int', array_sum($list)); +} + +/** + * @param list<1|2|3> $list + */ +function foo10($list) +{ + assertType('int<0, max>', array_sum($list)); +} + +/** + * @param non-empty-list<1|2|3> $list + */ +function foo11($list) +{ + assertType('int<1, max>', array_sum($list)); +} + +/** + * @param list<1|-1> $list + */ +function foo12($list) +{ + assertType('int', array_sum($list)); +} +/** + * @param non-empty-list<1|-1> $list + */ +function foo13($list) +{ + assertType('int', array_sum($list)); +} + +/** + * @param array{0} $list + */ +function foo14($list) +{ + assertType('0', array_sum($list)); +} +/** + * @param array{1} $list + */ +function foo15($list) +{ + assertType('1', array_sum($list)); +} + +/** + * @param array{1, 2, 3} $list + */ +function foo16($list) +{ + assertType('6', array_sum($list)); +} + +/** + * @param array{1, int} $list + */ +function foo17($list) +{ + assertType('int', array_sum($list)); +} + +/** + * @param array{1, float} $list + */ +function foo18($list) +{ + assertType('float', array_sum($list)); +} + +/** + * @param array{} $list + */ +function foo19($list) +{ + assertType('0', array_sum($list)); +} + + +/** + * @param list<1|float> $list + */ +function foo20($list) +{ + assertType('float|int<0, max>', array_sum($list)); +} + +/** + * @param array{1, int|float} $list + */ +function foo21($list) +{ + assertType('float|int', array_sum($list)); +} + +/** + * @param array{1, string} $list + */ +function foo22($list) +{ + assertType('float|int', array_sum($list)); +} + + +/** + * @param array{1, 3.2} $list + */ +function foo23($list) +{ + assertType('4.2', array_sum($list)); +} + +/** + * @param array{1, float|4} $list + */ +function foo24($list) +{ + assertType('5|float', array_sum($list)); +} + +/** + * @param array{1, 2|3.4} $list + */ +function foo25($list) +{ + assertType('3|4.4', array_sum($list)); +} + +/** + * @param array{1, 2.4|3.4} $list + */ +function foo26($list) +{ + assertType('3.4|4.4', array_sum($list)); +} + + +/** + * @param array{1}|array{2, 3} $list + */ +function foo27($list) +{ + assertType('1|5', array_sum($list)); +} + +/** + * @param array{1}|list<1>|array{float} $list + */ +function foo28($list) +{ + assertType('float|int<0, max>', array_sum($list)); +} + +/** + * @param non-empty-list|int<1, max>> $list + */ +function foo29($list) +{ + assertType('int', array_sum($list)); +} + +/** + * @param array{'133', 3} $list + */ +function foo30($list) +{ + assertType('136', array_sum($list)); +} + +/** + * @param array{0: 1, 1?: 2, 2?: 3} $list + */ +function foo31($list) +{ + assertType('1|3|4|6', array_sum($list)); +} + +/** + * @param mixed $list + */ +function foo32($list) +{ + assertType('(float|int)', array_sum($list)); +} diff --git a/tests/PHPStan/Analyser/data/array-typehint-without-null-in-phpdoc.php b/tests/PHPStan/Analyser/nsrt/array-typehint-without-null-in-phpdoc.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-typehint-without-null-in-phpdoc.php rename to tests/PHPStan/Analyser/nsrt/array-typehint-without-null-in-phpdoc.php diff --git a/tests/PHPStan/Analyser/nsrt/array-unpacking-string-keys.php b/tests/PHPStan/Analyser/nsrt/array-unpacking-string-keys.php new file mode 100644 index 0000000000..a03dbb75c9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-unpacking-string-keys.php @@ -0,0 +1,68 @@ += 8.1 + +namespace ArrayUnpackingWithStringKeys; + +use function PHPStan\Testing\assertType; + +$foo = ['a' => 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('array', $c); +} + +/** + * @param array $a + * @param array $b + */ +function bar(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('array', $c); +} + +/** + * @param array $a + * @param array $b + */ +function baz(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('array', $c); +} + +/** + * @param non-empty-array $a + * @param array $b + */ +function nonEmptyArray1(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('non-empty-array', $c); +} + +/** + * @param array $a + * @param non-empty-array $b + */ +function nonEmptyArray2(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('non-empty-array', $c); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-unshift.php b/tests/PHPStan/Analyser/nsrt/array-unshift.php new file mode 100644 index 0000000000..933aad522d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-unshift.php @@ -0,0 +1,74 @@ + $c + * @param array $d + * @param list $e + */ +function arrayUnshift(array $a, array $b, array $c, array $d, array $e, array $arr): void +{ + array_unshift($a, ...$b); + assertType('array', $a); + + /** @var non-empty-array $arr */ + array_unshift($arr, ...$b); + assertType('non-empty-array', $arr); + + array_unshift($b, ...[]); + assertType('array', $b); + + array_unshift($c, ...[19, 'baz', false]); + assertType('non-empty-array<\'baz\'|int|false>', $c); + + /** @var array $d1 */ + $d1 = []; + array_unshift($d, ...$d1); + assertType('array', $d); + + /** @var list $e1 */ + $e1 = []; + array_unshift($e, ...$e1); + assertType('list', $e); +} + +function arrayUnshiftConstantArray(): void +{ + $a = ['foo' => 17, 'a', 'bar' => 18,]; + array_unshift($a, ...[19, 'baz', false]); + assertType('array{0: 19, 1: \'baz\', 2: false, foo: 17, 3: \'a\', bar: 18}', $a); + + $b = ['foo' => 17, 'a', 'bar' => 18]; + array_unshift($b, 19, 'baz', false); + assertType('array{0: 19, 1: \'baz\', 2: false, foo: 17, 3: \'a\', bar: 18}', $b); + + $c = ['foo' => 17, 'a', 'bar' => 18]; + array_unshift($c, ...[]); + assertType('array{foo: 17, 0: \'a\', bar: 18}', $c); + + $d = []; + array_unshift($d, ...[]); + assertType('array{}', $d); + + $e = []; + array_unshift($e, 19, 'baz', false); + assertType('array{19, \'baz\', false}', $e); + + $f = [17]; + /** @var array $f1 */ + $f1 = []; + array_unshift($f, ...$f1); + assertType('non-empty-list<17|bool|null>', $f); + + $g = [new stdClass()]; + array_unshift($g, ...[new stdClass(), new stdClass()]); + assertType('array{stdClass, stdClass, stdClass}', $g); +} diff --git a/tests/PHPStan/Analyser/nsrt/array_diff_intersect_callbacks.php b/tests/PHPStan/Analyser/nsrt/array_diff_intersect_callbacks.php new file mode 100644 index 0000000000..5dc721664e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_diff_intersect_callbacks.php @@ -0,0 +1,127 @@ +', array_keys($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('null', array_keys($mixed)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array_keys.php b/tests/PHPStan/Analyser/nsrt/array_keys.php new file mode 100644 index 0000000000..6808bf36b3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_keys.php @@ -0,0 +1,27 @@ += 8.0 + +namespace ArrayKeys; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello($mixed): void + { + if(is_array($mixed)) { + assertType('list<(int|string)>', array_keys($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('*NEVER*', array_keys($mixed)); + } + } + + public function constantArrayType(): void + { + $numbers = array_filter( + [1 => 'a', 2 => 'b', 3 => 'c'], + static fn ($value) => mt_rand(0, 1) === 0, + ); + assertType("array{0?: 1|2|3, 1?: 2|3, 2?: 3}", array_keys($numbers)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php new file mode 100644 index 0000000000..2918ebeb89 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php @@ -0,0 +1,40 @@ + $i], ['bar' => $s]); + assertType('non-empty-list', $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-list', array_map(null, [1, 2, 3], [4, 5, 6])); + + assertType('non-empty-array', array_map(null, $array)); + assertType('non-empty-list', array_map(null, $array, $array)); + assertType('non-empty-list', array_map(null, $array, $array, $array)); + assertType('non-empty-list', array_map(null, $array, $other)); + + assertType('array{1}|array{true}', array_map(null, rand() ? [1] : [true])); + assertType('array{1}|array{true, false}', array_map(null, rand() ? [1] : [true, false])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array_splice.php b/tests/PHPStan/Analyser/nsrt/array_splice.php new file mode 100644 index 0000000000..7075c0fb8b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_splice.php @@ -0,0 +1,61 @@ + $arr + * @return void + */ +function insertViaArraySplice(array $arr): void +{ + $brr = $arr; + array_splice($brr, 0, 0, 1); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, [1]); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, ''); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, ['']); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, null); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, [null]); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, new Foo()); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, [new \stdClass()]); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, false); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, [false]); + assertType('array', $brr); +} diff --git a/tests/PHPStan/Analyser/nsrt/array_values-php7.php b/tests/PHPStan/Analyser/nsrt/array_values-php7.php new file mode 100644 index 0000000000..db378c95aa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_values-php7.php @@ -0,0 +1,20 @@ +', array_values($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('null', array_values($mixed)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array_values.php b/tests/PHPStan/Analyser/nsrt/array_values.php new file mode 100644 index 0000000000..18074963a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_values.php @@ -0,0 +1,48 @@ += 8.0 + +namespace ArrayValues; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function foo1($mixed): void + { + if(is_array($mixed)) { + assertType('list', array_values($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('*NEVER*', array_values($mixed)); + } + } + + /** + * @param list $list + */ + public function foo2($list): void + { + if(is_array($list)) { + assertType('list', array_values($list)); + } else { + assertType('*NEVER*', $list); + assertType('*NEVER*', array_values($list)); + } + } + + public function constantArrayType(): void + { + $numbers = array_filter( + [1 => 'a', 2 => 'b', 3 => 'c'], + static fn ($value) => mt_rand(0, 1) === 0, + ); + assertType("array{0?: 'a'|'b'|'c', 1?: 'b'|'c', 2?: 'c'}", array_values($numbers)); + } + + /** + * @param array> $a + */ + public function arrayMap(array $a): void + { + assertType('array>', array_map(array_values(...), $a)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php b/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php new file mode 100644 index 0000000000..a508035d19 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php @@ -0,0 +1,28 @@ + assertType('int', $context))($integer); + + (fn($context) => assertType('array{a: int}', $context))($array); + + (fn($context) => assertType('string|null', $context))($nullableString); + + (fn($a, $b, $c) => assertType('array{int, array{a: int}, string|null}', [$a, $b, $c]))($integer, $array, $nullableString); + + (fn($a, $b, $c = null) => assertType('array{int, array{a: int}, mixed}', [$a, $b, $c]))($integer, $array); + + ($callback = fn($context) => assertType('int', $context))($integer); + } + +} diff --git a/tests/PHPStan/Analyser/data/arrow-function-return-type.php b/tests/PHPStan/Analyser/nsrt/arrow-function-return-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/arrow-function-return-type.php rename to tests/PHPStan/Analyser/nsrt/arrow-function-return-type.php diff --git a/tests/PHPStan/Analyser/data/arrow-function-types.php b/tests/PHPStan/Analyser/nsrt/arrow-function-types.php similarity index 98% rename from tests/PHPStan/Analyser/data/arrow-function-types.php rename to tests/PHPStan/Analyser/nsrt/arrow-function-types.php index 77cbe12cf8..acb8f74ee4 100644 --- a/tests/PHPStan/Analyser/data/arrow-function-types.php +++ b/tests/PHPStan/Analyser/nsrt/arrow-function-types.php @@ -1,4 +1,4 @@ -= 7.4 +value = $value; + } + + /** + * @template K + * @param K $data + * @phpstan-assert T $data + */ + public function assert($data): void + { + if ($data !== $this->value) { + throw new Exception(); + } + } +} + +function () { + $a = new HelloWorld(123); + assertType('AssertClassType\\HelloWorld', $a); + + $b = $_GET['value']; + $a->assert($b); + + assertType('int', $b); +}; diff --git a/tests/PHPStan/Analyser/nsrt/assert-conditional.php b/tests/PHPStan/Analyser/nsrt/assert-conditional.php new file mode 100644 index 0000000000..4a8567a2db --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-conditional.php @@ -0,0 +1,37 @@ += 8.0 + +namespace AssertConditional; + +use function PHPStan\Testing\assertType; + +/** + * @phpstan-assert ($if is true ? true : false) $condition + */ +function assertIf(mixed $condition, bool $if) +{ +} + +function (mixed $value1, mixed $value2) { + assertIf($value1, true); + assertType('true', $value1); + + assertIf($value2, false); + assertType('false', $value2); +}; + +/** + * @template T of bool + * @param T $if + * @phpstan-assert (T is true ? true : false) $condition + */ +function assertIfTemplated(mixed $condition, bool $if) +{ +} + +function (mixed $value1, mixed $value2) { + assertIfTemplated($value1, true); + assertType('true', $value1); + + assertIfTemplated($value2, false); + assertType('false', $value2); +}; diff --git a/tests/PHPStan/Analyser/nsrt/assert-constructor.php b/tests/PHPStan/Analyser/nsrt/assert-constructor.php new file mode 100644 index 0000000000..2d133d8c6c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-constructor.php @@ -0,0 +1,22 @@ += 8.0 + +namespace AssertDocblock; + +use function PHPStan\Testing\assertType; + +/** + * @param mixed[] $arr + * @phpstan-assert string[] $arr + */ +function validateStringArray(array $arr) : void {} + +/** + * @param mixed[] $arr + * @phpstan-assert-if-true string[] $arr + */ +function validateStringArrayIfTrue(array $arr) : bool { + return true; +} + +/** + * @param mixed[] $arr + * @phpstan-assert-if-false string[] $arr + */ +function validateStringArrayIfFalse(array $arr) : bool { + return false; +} + +/** + * @param mixed[] $arr + * @phpstan-assert-if-true string[] $arr + * @phpstan-assert-if-false int[] $arr + */ +function validateStringOrIntArray(array $arr) : bool { + return false; +} + +/** + * @param mixed[] $arr + * @phpstan-assert-if-true =string[] $arr + * @phpstan-assert-if-false =int[] $arr + * @phpstan-assert-if-false =non-empty-array $arr + */ +function validateStringOrNonEmptyIntArray(array $arr) : bool { + return false; +} + +/** + * @param mixed $value + * @phpstan-assert !null $value + */ +function validateNotNull($value) : void {} + + +/** + * @param mixed[] $arr + */ +function takesArray(array $arr) : void { + assertType('array', $arr); + + validateStringArray($arr); + assertType('array', $arr); +} + +/** + * @param mixed[] $arr + */ +function takesArrayIfTrue(array $arr) : void { + assertType('array', $arr); + + if (validateStringArrayIfTrue($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} +/** + * @param mixed[] $arr + */ +function takesArrayIfTrue1(array $arr) : void { + assertType('array', $arr); + + if (!validateStringArrayIfTrue($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} + +/** + * @param mixed[] $arr + */ +function takesArrayIfFalse(array $arr) : void { + assertType('array', $arr); + + if (!validateStringArrayIfFalse($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} + +/** + * @param mixed[] $arr + */ +function takesArrayIfFalse1(array $arr) : void { + assertType('array', $arr); + + if (validateStringArrayIfFalse($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} + +/** + * @param mixed[] $arr + */ +function takesStringOrIntArray(array $arr) : void { + assertType('array', $arr); + + if (validateStringOrIntArray($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} + +/** + * @param mixed[] $arr + */ +function takesStringOrNonEmptyIntArray(array $arr) : void { + assertType('array', $arr); + + if (validateStringOrNonEmptyIntArray($arr)) { + assertType('array', $arr); + } else { + assertType('non-empty-array', $arr); + } +} + +/** + * @param mixed $value + */ +function takesNotNull($value) : void { + assertType('mixed', $value); + + validateNotNull($value); + assertType('mixed~null', $value); +} + + +/** + * @template T of object + * @param object $object + * @param class-string $class + * @phpstan-assert T $object + */ +function validateClassType(object $object, string $class): void {} + +class ClassToValidate {} + +function (object $object) { + validateClassType($object, ClassToValidate::class); + assertType('AssertDocblock\ClassToValidate', $object); +}; + + +class A { + /** + * @phpstan-assert-if-true int $x + */ + public function testInt(mixed $x): bool + { + return is_int($x); + } + + /** + * @phpstan-assert-if-true !int $x + */ + public function testNotInt(mixed $x): bool + { + return !is_int($x); + } +} + +class B extends A +{ + public function testInt(mixed $y): bool + { + return parent::testInt($y); + } +} + +function (A $a, $i) { + if ($a->testInt($i)) { + assertType('int', $i); + } else { + assertType('mixed~int', $i); + } + + if ($a->testNotInt($i)) { + assertType('mixed~int', $i); + } else { + assertType('int', $i); + } +}; + +function (B $b, $i) { + if ($b->testInt($i)) { + assertType('int', $i); + } else { + assertType('mixed~int', $i); + } +}; + +function (A $a, string $i) { + if ($a->testInt($i)) { + assertType('*NEVER*', $i); + } else { + assertType('string', $i); + } + + if ($a->testNotInt($i)) { + assertType('string', $i); + } else { + assertType('*NEVER*', $i); + } +}; + +function (A $a, int $i) { + if ($a->testInt($i)) { + assertType('int', $i); + } else { + assertType('*NEVER*', $i); + } + + if ($a->testNotInt($i)) { + assertType('*NEVER*', $i); + } else { + assertType('int', $i); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/assert-empty.php b/tests/PHPStan/Analyser/nsrt/assert-empty.php new file mode 100644 index 0000000000..a74e4c1e35 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-empty.php @@ -0,0 +1,29 @@ += 8.0 + +namespace AssertEmpty; + +use function PHPStan\Testing\assertType; + +/** + * @phpstan-assert empty $var + */ +function assertEmpty(mixed $var): void +{ +} + +/** + * @phpstan-assert !empty $var + */ +function assertNotEmpty(mixed $var): void +{ +} + +function ($var) { + assertEmpty($var); + assertType("0|0.0|''|'0'|array{}|false|null", $var); +}; + +function ($var) { + assertNotEmpty($var); + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $var); +}; diff --git a/tests/PHPStan/Analyser/nsrt/assert-inheritance.php b/tests/PHPStan/Analyser/nsrt/assert-inheritance.php new file mode 100644 index 0000000000..ffc9552321 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-inheritance.php @@ -0,0 +1,110 @@ += 8.0 + +namespace AssertInheritance; + +use function PHPStan\Testing\assertType; + +/** + * @template T + */ +interface WrapperInterface +{ + /** + * @phpstan-assert T $param + */ + public function assert(mixed $param): void; + + /** + * @phpstan-assert-if-true T $param + */ + public function supports(mixed $param): bool; + + /** + * @phpstan-assert-if-false T $param + */ + public function notSupports(mixed $param): bool; +} + +/** + * @implements WrapperInterface + */ +class IntWrapper implements WrapperInterface +{ + public function assert(mixed $param): void + { + } + + public function supports(mixed $param): bool + { + return is_int($param); + } + + public function notSupports(mixed $param): bool + { + return !is_int($param); + } +} + +/** + * @template T of object + * @implements WrapperInterface + */ +abstract class ObjectWrapper implements WrapperInterface +{ +} + +/** + * @extends ObjectWrapper<\DateTimeInterface> + */ +class DateTimeInterfaceWrapper extends ObjectWrapper +{ + public function assert(mixed $param): void + { + } + + public function supports(mixed $param): bool + { + return $param instanceof \DateTimeInterface; + } + + public function notSupports(mixed $param): bool + { + return !$param instanceof \DateTimeInterface; + } +} + +function (IntWrapper $test, $val) { + if ($test->supports($val)) { + assertType('int', $val); + } else { + assertType('mixed~int', $val); + } + + if ($test->notSupports($val)) { + assertType('mixed~int', $val); + } else { + assertType('int', $val); + } + + assertType('mixed', $val); + $test->assert($val); + assertType('int', $val); +}; + +function (DateTimeInterfaceWrapper $test, $val) { + if ($test->supports($val)) { + assertType('DateTimeInterface', $val); + } else { + assertType('mixed~DateTimeInterface', $val); + } + + if ($test->notSupports($val)) { + assertType('mixed~DateTimeInterface', $val); + } else { + assertType('DateTimeInterface', $val); + } + + assertType('mixed', $val); + $test->assert($val); + assertType('DateTimeInterface', $val); +}; diff --git a/tests/PHPStan/Analyser/nsrt/assert-intersected.php b/tests/PHPStan/Analyser/nsrt/assert-intersected.php new file mode 100644 index 0000000000..913f1e9034 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-intersected.php @@ -0,0 +1,30 @@ += 8.0 + +namespace AssertIntersected; + +use function PHPStan\Testing\assertType; + +interface AssertList +{ + /** + * @phpstan-assert list $value + */ + public function assert(mixed $value): void; +} + +interface AssertNonEmptyArray +{ + /** + * @phpstan-assert non-empty-array $value + */ + public function assert(mixed $value): void; +} + +/** + * @param AssertList&AssertNonEmptyArray $assert + */ +function intersection($assert, mixed $value): void +{ + $assert->assert($value); + assertType('non-empty-list', $value); +} diff --git a/tests/PHPStan/Analyser/nsrt/assert-invariant.php b/tests/PHPStan/Analyser/nsrt/assert-invariant.php new file mode 100644 index 0000000000..4efe160b18 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-invariant.php @@ -0,0 +1,29 @@ += 8.0 + +declare(strict_types = 1); + +namespace AssertInvariant; + +/** + * @phpstan-assert true $fact + */ +function invariant(bool $fact): void +{ +} + +function (mixed $m): void { + invariant(is_bool($m)); + \PHPStan\Testing\assertType('bool', $m); +}; + +/** + * @phpstan-assert !false $condition + */ +function assertNotFalse(mixed $condition): void +{ +} + +function (mixed $m): void { + assertNotFalse(is_bool($m)); + \PHPStan\Testing\assertType('bool', $m); +}; diff --git a/tests/PHPStan/Analyser/nsrt/assert-method.php b/tests/PHPStan/Analyser/nsrt/assert-method.php new file mode 100644 index 0000000000..41c1555852 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-method.php @@ -0,0 +1,26 @@ +getId() + * @phpstan-assert-if-false null $this->getId() + */ + public function hasId(): bool; +} + +function (Identity $identity) { + assertType('int|null', $identity->getId()); + + if ($identity->hasId()) { + assertType('int', $identity->getId()); + } else { + assertType('null', $identity->getId()); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/assert-methods.php b/tests/PHPStan/Analyser/nsrt/assert-methods.php new file mode 100644 index 0000000000..6c278c21ed --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-methods.php @@ -0,0 +1,99 @@ + $class + * @phpstan-assert iterable> $value + * + * @param iterable $value + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function doFoo($value, string $class): void + { + + } + + public function doBar($mixed) + { + self::doFoo($mixed, stdClass::class); + assertType('iterable|stdClass>', $mixed); + } + + /** + * @param array $objects + * @return void + */ + public function doBar2(array $objects) + { + self::doFoo($objects, stdClass::class); + assertType('array', $objects); + } + + /** + * @param array $strings + * @return void + */ + public function doBar3(array $strings) + { + self::doFoo($strings, stdClass::class); + assertType('array>', $strings); + } + + /** + * @template ExpectedType of object + * @param class-string $class + * @phpstan-assert iterable> $value + * + * @param iterable $value + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function doBaz($value, string $class): void + { + + } + + public function doLorem($mixed) + { + $this->doBaz($mixed, stdClass::class); + assertType('iterable|stdClass>', $mixed); + } + +} + +/** @template T */ +class Bar +{ + + /** + * @phpstan-assert T $arg + */ + public function doFoo($arg): void + { + + } + + /** + * @param Bar $bar + */ + public function doBar(Bar $bar, object $object): void + { + assertType('object', $object); + $bar->doFoo($object); + assertType(stdClass::class, $object); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/assert-property.php b/tests/PHPStan/Analyser/nsrt/assert-property.php new file mode 100644 index 0000000000..a8aaac79d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-property.php @@ -0,0 +1,30 @@ +id + * @phpstan-assert-if-false null $this->id + */ + public function hasId(): bool + { + return $this->id !== null; + } +} + +function (Identity $identity) { + assertType('int|null', $identity->id); + + if ($identity->hasId()) { + assertType('int', $identity->id); + } else { + assertType('null', $identity->id); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/assert-this.php b/tests/PHPStan/Analyser/nsrt/assert-this.php new file mode 100644 index 0000000000..4b29fa697c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-this.php @@ -0,0 +1,84 @@ + $this + * @phpstan-assert-if-false Err $this + */ + public function isOk(): bool; + + /** + * @return TOk|never + */ + public function unwrap(); +} + +/** + * @template TOk + * @template-implements Result + */ +class Ok implements Result { + public function __construct(private $value) { + } + + /** + * @return true + */ + public function isOk(): bool { + return true; + } + + /** + * @return TOk + */ + public function unwrap() { + return $this->value; + } +} + +/** + * @template TErr + * @template-implements Result + */ +class Err implements Result { + public function __construct(private $value) { + } + + /** + * @return false + */ + public function isOk(): bool { + return false; + } + + /** + * @return never + */ + public function unwrap() { + throw new RuntimeException('Tried to unwrap() an Err value'); + } +} + +function () { + /** @var Result $result */ + $result = new Ok(123); + assertType('AssertThis\\Result', $result); + + if ($result->isOk()) { + assertType('AssertThis\\Ok', $result); + assertType('int', $result->unwrap()); + } else { + assertType('AssertThis\\Err', $result); + assertType('never', $result->unwrap()); + } +}; \ No newline at end of file diff --git a/tests/PHPStan/Analyser/nsrt/assert-variable-certainty-on-array.php b/tests/PHPStan/Analyser/nsrt/assert-variable-certainty-on-array.php new file mode 100644 index 0000000000..813518812a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-variable-certainty-on-array.php @@ -0,0 +1,28 @@ +asymmetricPropertyRw); + assertType('int', $this->asymmetricPropertyXw); + assertType('int', $this->asymmetricPropertyRx); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/base64_decode.php b/tests/PHPStan/Analyser/nsrt/base64_decode.php new file mode 100644 index 0000000000..34de145d9a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/base64_decode.php @@ -0,0 +1,21 @@ += 8.0 + +// Verification for constant types: https://3v4l.org/96GSj + +/** @var mixed $mixed */ +$mixed = getMixed(); + +/** @var int $iUnknown */ +$iUnknown = getInt(); + +/** @var string $string */ +$string = getString(); + +$iNeg = -5; +$iPos = 5; +$nonNumeric = 'foo'; + + +// bcdiv ( string $dividend , string $divisor [, ?int $scale = null ] ) : string +// Returns the result of the division as a numeric-string. +\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', '0')); // DivisionByZeroError +\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', '0.0')); // DivisionByZeroError +\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', 0.0)); // DivisionByZeroError +\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', bcdiv('10', $mixed)); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iPos, $iPos)); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iUnknown)); +\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', $iPos, $nonNumeric)); // ValueError argument 3 +\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', $nonNumeric)); // ValueError argument 2 + +// bcmod ( string $dividend , string $divisor [, ?int $scale = null ] ) : string +// Returns the modulus as a numeric-string. +\PHPStan\Testing\assertType('*NEVER*', bcmod('10', '0')); // DivisionByZeroError +\PHPStan\Testing\assertType('*NEVER*', bcmod($iPos, '0')); // DivisionByZeroError +\PHPStan\Testing\assertType('*NEVER*', bcmod('10', $nonNumeric)); // ValueError argument 2 +\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', bcmod('10', $mixed)); + +// bcpowmod ( string $base , string $exponent , string $modulus [, ?int $scale = null ] ) : string +// Returns the result as a numeric-string, or FALSE if modulus is 0 or exponent is negative. +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '-2', '0')); // ValueError argument 2 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '-2', '1')); // ValueError argument 2 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '2', $nonNumeric)); // ValueError argument 3 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '-2', '-1')); // ValueError argument 2 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '-2', -1.3)); // ValueError argument 2 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', -$iPos, '-1')); // ValueError argument 2 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', -$iPos, '1')); // ValueError argument 2 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', $nonNumeric, $nonNumeric)); // ValueError argument 2 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod($iPos, $nonNumeric, $nonNumeric)); +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '2', '0')); // modulus is 0 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', 2.3, '0')); // modulus is 0 +\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '0', '0')); // modulus is 0 +\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', 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('*NEVER*', bcpowmod('10', $nonNumeric, '2')); // ValueError argument 2 +\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', $iUnknown, $iUnknown)); + +// bcsqrt ( string $operand [, ?int $scale = null ] ) : string +// Returns the square root as a numeric-string. +\PHPStan\Testing\assertType('*NEVER*', bcsqrt('10', $iNeg)); // ValueError argument 2 +\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', bcsqrt($iUnknown, $iUnknown)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt('10', $iPos)); +\PHPStan\Testing\assertType('*NEVER*', bcsqrt('-10', 0)); // ValueError argument 1 +\PHPStan\Testing\assertType('*NEVER*', bcsqrt($iNeg, null)); // ValueError argument 1 +\PHPStan\Testing\assertType('*NEVER*', bcsqrt('10', $nonNumeric)); // ValueError argument 2 +\PHPStan\Testing\assertType('numeric-string', bcsqrt('10')); +\PHPStan\Testing\assertType('numeric-string', bcsqrt($iUnknown)); +\PHPStan\Testing\assertType('*NEVER*', bcsqrt('-10')); // ValueError argument 1 + +\PHPStan\Testing\assertType('*NEVER*', bcsqrt($nonNumeric, -1)); // ValueError argument 1 +\PHPStan\Testing\assertType('numeric-string', bcsqrt('10', $mixed)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt($iPos)); diff --git a/tests/PHPStan/Analyser/nsrt/bcmath-number.php b/tests/PHPStan/Analyser/nsrt/bcmath-number.php new file mode 100644 index 0000000000..2bdd9611b4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bcmath-number.php @@ -0,0 +1,408 @@ += 8.4 + +declare(strict_types = 1); + +namespace BcMathNumber; + +use BcMath\Number; +use function PHPStan\Testing\assertType; + +class Foo +{ + public function bcVsBc(Number $a, Number $b): void + { + assertType('BcMath\Number', $a + $b); + assertType('BcMath\Number', $a - $b); + assertType('BcMath\Number', $a * $b); + assertType('BcMath\Number', $a / $b); + assertType('BcMath\Number', $a % $b); + assertType('non-falsy-string', $a . $b); + assertType('BcMath\Number', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsInt(Number $a, int $b): void + { + assertType('BcMath\Number', $a + $b); + assertType('BcMath\Number', $a - $b); + assertType('BcMath\Number', $a * $b); + assertType('BcMath\Number', $a / $b); + assertType('BcMath\Number', $a % $b); + assertType('non-falsy-string', $a . $b); + assertType('BcMath\Number', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsFloat(Number $a, float $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-falsy-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + /** @param numeric-string $b */ + public function bcVsNumericString(Number $a, string $b): void + { + assertType('BcMath\Number', $a + $b); + assertType('BcMath\Number', $a - $b); + assertType('BcMath\Number', $a * $b); + assertType('BcMath\Number', $a / $b); + assertType('BcMath\Number', $a % $b); + assertType('non-falsy-string', $a . $b); + assertType('BcMath\Number', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsNonNumericString(Number $a, string $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsBool(Number $a, bool $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string&numeric-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsNull(Number $a): void + { + $b = null; + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('0', $a * $b); // BUG: This throws type error, but getMulType assumes that since null (mostly) behaves like zero, it will be zero. + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string&numeric-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('false', $a && $b); + assertType('bool', $a || $b); + assertType('false', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsArray(Number $a, array $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsObject(Number $a, object $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + /** @param resource $b */ + public function bcVsResource(Number $a, $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + public function bcVsCallable(Number $a, callable $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + public function bcVsIterable(Number $a, iterable $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsStringable(Number $a, \Stringable $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + public function bcVsNever(Number $a): void + { + for ($b = 1; $b < count([]); $b++) { + assertType('*NEVER*', $a + $b); + assertType('*ERROR*', $a - $b); // Inconsistency: getPlusType handles never types right at the beginning, getMinusType doesn't. + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*NEVER*', $a % $b); + assertType('non-empty-string&numeric-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*NEVER*', $a << $b); + assertType('*NEVER*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('*NEVER*', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*NEVER*', $a & $b); + assertType('*NEVER*', $a ^ $b); + assertType('*NEVER*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/benevolent-union-math.php b/tests/PHPStan/Analyser/nsrt/benevolent-union-math.php new file mode 100644 index 0000000000..6b273c9672 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/benevolent-union-math.php @@ -0,0 +1,44 @@ +getBenevolent(); + assertType('array|null', $dbresponse); + + if ($dbresponse === null) {return;} + + assertType('array', $dbresponse); + assertType('(float|int|string|null)', $dbresponse['Value']); + assertType('int<0, max>', strlen($dbresponse['Value'])); + } + + /** + * @return array>|null + */ + private function getBenevolent(): ?array{ + return rand(10) > 1 ? null : []; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bitwise-not.php b/tests/PHPStan/Analyser/nsrt/bitwise-not.php new file mode 100644 index 0000000000..37c29f8f97 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bitwise-not.php @@ -0,0 +1,21 @@ +format('n'); + $monthDay2 = (int) $day2->format('n'); + + + if($monthDay1 === $month){ + return true; + } + + assertType('bool', $monthDay2 === $month); + + return $monthDay2 === $month; + } + + public function bar1(int $month): bool + { + $day1 = new \DateTime('2022-01-01'); + $day2 = new \DateTime('2022-05-01'); + + $monthDay1 = (int) $day1->format('n'); + $monthDay2 = (int) $day2->format('n'); + + + if($month === $monthDay1){ + return true; + } + + assertType('bool', $month === $monthDay2); + + return $monthDay2 === $month; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10037.php b/tests/PHPStan/Analyser/nsrt/bug-10037.php new file mode 100644 index 0000000000..56c49c331b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10037.php @@ -0,0 +1,98 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug10037; + +interface Identifier +{} + +interface Document +{} + +/** @template T of Identifier */ +interface Fetcher +{ + /** @phpstan-assert-if-true T $identifier */ + public function supports(Identifier $identifier): bool; + + /** @param T $identifier */ + public function fetch(Identifier $identifier): Document; +} + +/** @implements Fetcher */ +final readonly class PostFetcher implements Fetcher +{ + public function supports(Identifier $identifier): bool + { + return $identifier instanceof PostIdentifier; + } + + public function fetch(Identifier $identifier): Document + { + // SA knows $identifier is instance of PostIdentifier here + return $identifier->foo(); + } +} + +class PostIdentifier implements Identifier +{ + public function foo(): Document + { + return new class implements Document{}; + } +} + +function (Identifier $i): void { + $fetcher = new PostFetcher(); + \PHPStan\Testing\assertType('Bug10037\Identifier', $i); + if ($fetcher->supports($i)) { + \PHPStan\Testing\assertType('Bug10037\PostIdentifier', $i); + $fetcher->fetch($i); + } else { + $fetcher->fetch($i); + } +}; + +class Post +{ +} + +/** @template T */ +abstract class Voter +{ + + /** @phpstan-assert-if-true T $subject */ + abstract function supports(string $attribute, mixed $subject): bool; + + /** @param T $subject */ + abstract function voteOnAttribute(string $attribute, mixed $subject): bool; + +} + +/** @extends Voter */ +class PostVoter extends Voter +{ + + /** @phpstan-assert-if-true Post $subject */ + function supports(string $attribute, mixed $subject): bool + { + + } + + function voteOnAttribute(string $attribute, mixed $subject): bool + { + \PHPStan\Testing\assertType('Bug10037\Post', $subject); + } +} + +function ($subject): void { + $voter = new PostVoter(); + \PHPStan\Testing\assertType('mixed', $subject); + if ($voter->supports('aaa', $subject)) { + \PHPStan\Testing\assertType('Bug10037\Post', $subject); + $voter->voteOnAttribute('aaa', $subject); + } else { + $voter->voteOnAttribute('aaa', $subject); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10071.php b/tests/PHPStan/Analyser/nsrt/bug-10071.php new file mode 100644 index 0000000000..ef25a1d61d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10071.php @@ -0,0 +1,24 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug10071; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public ?bool $bar = null; +} + + +function okIfBar(?Foo $foo = null): void +{ + if ($foo?->bar !== false) { + assertType(Foo::class . '|null', $foo); + } else { + assertType(Foo::class, $foo); + } + + assertType(Foo::class . '|null', $foo); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10080.php b/tests/PHPStan/Analyser/nsrt/bug-10080.php new file mode 100644 index 0000000000..1875d50dfd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10080.php @@ -0,0 +1,76 @@ + assertType('null', $val), + default => assertType('int', $val), + }; +}; + +function (?int $val) { + match ($foo = $val) { + null => assertType('null', $val), + default => assertType('int', $val), + }; +}; + +function (?int $val) { + match ($foo = $val) { + null => assertType('null', $foo), + default => assertType('int', $foo), + }; +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10088.php b/tests/PHPStan/Analyser/nsrt/bug-10088.php new file mode 100644 index 0000000000..df9bd2b6c0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10088.php @@ -0,0 +1,135 @@ +assertInstanceOfStdClass($date ?? null); + assertVariableCertainty(TrinaryLogic::createYes(), $date); + } + + /** + * @param mixed $m + * @phpstan-assert stdClass $m + */ + private function assertInstanceOfStdClass($m): void + { + if (!$m instanceof stdClass) { + throw new \Exception(); + } + } + + /** + * @param mixed[] $period + */ + public function testCarbon2(array $period): void + { + foreach ($period as $date) { + break; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $date); + assert(($date ?? null) instanceof stdClass); + assertVariableCertainty(TrinaryLogic::createYes(), $date); + } + + function constantIfElse(int $x): void { + $link_mode = $x > 10 ? "remove" : "add"; + + assertType('int', $x); + if ($link_mode === "add") { + assertType('int', $x); + } else { + assertType('int<11, max>', $x); + } + assertType('int', $x); + } + + function constantIfElseShort(int $x): void { + $link_mode = $x > 10 ?: "remove"; + + assertType('int', $x); + if ($link_mode === "remove") { + assertType('int', $x); + } else { + assertType('int<11, max>', $x); + } + assertType('int', $x); + } + + function nonEmptyArray(array $arr): void { + $link_mode = $arr ? "truethy-arr" : "falsey-arr"; + assertType('array', $arr); + if ($link_mode === "truethy-arr") { + assertType('non-empty-array', $arr); + } else { + assertType('array{}', $arr); + } + assertType('array', $arr); + } + + /** + * @param array $arr + * @param 0|positive-int $intRange + */ + function nonEmptyArrayViaInt(array $arr, $intRange): void { + $link_mode = $arr ? $intRange : -10; + assertType('array', $arr); + if ($link_mode >= 0) { + assertType('non-empty-array', $arr); + } else { + assertType('array{}', $arr); + } + assertType('array', $arr); + } + + /** + * @param string[] $arr + * @param 0|positive-int $posInt + */ + function overlappingIfElseType($arr, int $x, int $posInt): void { + $link_mode = $arr ? $posInt : $x; + assert($link_mode >= 0); + + assertType('array', $arr); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10092.php b/tests/PHPStan/Analyser/nsrt/bug-10092.php new file mode 100644 index 0000000000..aa5bacc049 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10092.php @@ -0,0 +1,43 @@ + */ +function int() { } + +/** @return TypeInterface<0> */ +function zero() { } + +/** @return TypeInterface> */ +function positive_int() { } + +/** @return TypeInterface */ +function numeric_string() { } + + +/** + * @template T + * + * @param TypeInterface $first + * @param TypeInterface $second + * @param TypeInterface ...$rest + * + * @return TypeInterface + */ +function union( + TypeInterface $first, + TypeInterface $second, + TypeInterface ...$rest +) { + +} + +function (): void { + assertType('Bug10092\TypeInterface', union(int(), numeric_string())); + assertType('Bug10092\TypeInterface>', union(positive_int(), zero())); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10122.php b/tests/PHPStan/Analyser/nsrt/bug-10122.php new file mode 100644 index 0000000000..b945f83075 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10122.php @@ -0,0 +1,54 @@ += 8.0 + +namespace Bug10131; + +use function PHPStan\Testing\assertType; + +class A { +} + +class B { + public A|null $a = null; +} + +/** + * @phpstan-return array{0:A|null, 1:B|null} + */ +function foo(A|null $a, B|null $b): array +{ + $a ??= $b?->a ?? throw new \Exception(); + + assertType(A::class, $a); + assertType(B::class . '|null', $b); + + return [$a, $b]; +} diff --git a/tests/PHPStan/Analyser/data/bug-1014.php b/tests/PHPStan/Analyser/nsrt/bug-1014.php similarity index 93% rename from tests/PHPStan/Analyser/data/bug-1014.php rename to tests/PHPStan/Analyser/nsrt/bug-1014.php index 9d0f0567b0..d146c3341f 100644 --- a/tests/PHPStan/Analyser/data/bug-1014.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1014.php @@ -1,5 +1,7 @@ ", $files); + + return empty($files) ? [] : [1,2]; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10201.php b/tests/PHPStan/Analyser/nsrt/bug-10201.php new file mode 100644 index 0000000000..a5cae6e11e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10201.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug10201; + +use function PHPStan\Testing\assertType; + +enum Hello +{ + case Hi; +} + +function bla(null|string|Hello $hello): void { + if (!in_array($hello, [Hello::Hi, null], true) && !($hello instanceof Hello)) { + assertType('string', $hello); + } +} + +function bla2(null|string|Hello $hello): void { + if (!in_array($hello, [Hello::Hi, null], true)) { + assertType('string', $hello); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-1021.php b/tests/PHPStan/Analyser/nsrt/bug-1021.php new file mode 100644 index 0000000000..37e2f7244f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1021.php @@ -0,0 +1,32 @@ +', $x); + + if ($x) { + } +} + +function foo(array $x) { + if ($x) { + array_shift($x); + + assertType('array', $x); + + if ($x) { + echo ""; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10224.php b/tests/PHPStan/Analyser/nsrt/bug-10224.php new file mode 100644 index 0000000000..3158734620 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10224.php @@ -0,0 +1,33 @@ += 8.0 + +namespace Bug10254; + +use Closure; +use RuntimeException; +use function PHPStan\Testing\assertType; + +/** + * @template T + */ +class Option { + /** + * @param T $value + */ + private function __construct(private mixed $value) + { + } + + /** + * @template Tv + * @param Tv $value + * @return self + */ + public static function some($value): self + { + return new self($value); + } + + /** + * @template Tu + * + * @param (Closure(T): Tu) $closure + * + * @return Option + */ + public function map(Closure $closure): self + { + return new self($closure($this->unwrap())); + } + + /** + * @return T + */ + public function unwrap() + { + if ($this->value === null) { + throw new RuntimeException(); + } + + return $this->value; + } + + /** + * @template To + * @param self $other + * @return self + */ + public function zip(self $other) + { + return new self([ + $this->unwrap(), + $other->unwrap() + ]); + } +} + + +function (): void { + $value = Option::some(1) + ->zip(Option::some(2)); + + assertType('Bug10254\\Option', $value); + + $value1 = $value->map(function ($value) { + assertType('int', $value[0]); + assertType('int', $value[1]); + return $value[0] + $value[1]; + }); + + assertType('Bug10254\\Option', $value1); + + $value2 = $value->map(function ($value): int { + assertType('int', $value[0]); + assertType('int', $value[1]); + return $value[0] + $value[1]; + }); + + assertType('Bug10254\\Option', $value2); + +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10264.php b/tests/PHPStan/Analyser/nsrt/bug-10264.php new file mode 100644 index 0000000000..20b1361a25 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10264.php @@ -0,0 +1,80 @@ + $list */ + $list = []; + + assertType('list', $list); + + assert((count($list) <= 1) === true); + assertType('list', $list); + } + + function doFoo2() { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + assert((count($list, COUNT_NORMAL) <= 1) === true); + assertType('list', $list); + } + + /** @param list $c */ + public function sayHello(array $c): void + { + assertType('list', $c); + if (count($c) > 0) { + $c = array_map(fn() => new stdClass(), $c); + assertType('non-empty-list', $c); + } else { + assertType('array{}', $c); + } + + assertType('list', $c); + } + + function doBar() { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + assert((count($list, COUNT_RECURSIVE) <= 1) === true); + assertType('list', $list); + } + + function doIf():void { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + if( count($list, COUNT_RECURSIVE) >= 1) { + assertType('non-empty-list', $list); + } else { + assertType('array{}', $list); + } + } + + function countModeInt(int $i):void { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + if( count($list, $i) >= 1) { + assertType('non-empty-list', $list); + } else { + assertType('array{}', $list); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10283.php b/tests/PHPStan/Analyser/nsrt/bug-10283.php new file mode 100644 index 0000000000..e2cb63e31e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10283.php @@ -0,0 +1,25 @@ +x); + assertType('string', $test->y); + assertType('array', $test->z); + + assertType('int', $test->doFoo()); +} + +function testExtendedInterface(AnotherInterface $test): void +{ + assertType('int', $test->x); + assertType('string', $test->y); + assertType('array', $test->z); + + assertType('int', $test->doFoo()); +} + +interface AnotherInterface extends SampleInterface +{ +} + +class SomeSubClass extends SomeClass {} + +class ValidClass extends SomeClass implements SampleInterface {} + +class ValidSubClass extends SomeSubClass implements SampleInterface {} + +class InvalidClass implements SampleInterface {} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10302-trait-extends.php b/tests/PHPStan/Analyser/nsrt/bug-10302-trait-extends.php new file mode 100644 index 0000000000..6489de2dcc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10302-trait-extends.php @@ -0,0 +1,52 @@ +x); + assertType('string', $this->y); + assertType('*ERROR*', $this->z); + } +} + +/** + * @phpstan-require-extends SomeClass + */ +trait anotherTrait +{ +} + +class SomeClass { + public int $x = 1; + protected string $y = 'foo'; + private array $z = []; +} + +function test(SomeClass $test): void +{ + assertType('int', $test->x); + assertType('string', $test->y); + assertType('array', $test->z); +} + +class SomeSubClass extends SomeClass {} + +class ValidClass extends SomeClass { + use myTrait; +} + +class ValidSubClass extends SomeSubClass { + use myTrait; +} + +class InvalidClass { + use anotherTrait; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10302-trait-implements.php b/tests/PHPStan/Analyser/nsrt/bug-10302-trait-implements.php new file mode 100644 index 0000000000..f4b26cea11 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10302-trait-implements.php @@ -0,0 +1,54 @@ +foo(); + $this->doesNotExist(); + } +} + +interface SomeInterface +{ + public function foo(): string; +} + +class SomeClass implements SomeInterface { + use myTrait; + + public int $x; + protected string $y; + private array $z = []; + + public function foo(): string + { + return "hallo"; + } +} + +function test(SomeClass $test): void +{ + assertType('int', $test->x); + assertType('string', $test->y); + assertType('array', $test->z); + + assertType('string', $test->foo()); +} + +class ValidImplements implements SomeInterface { + public function foo(): string + { + return "hallo"; + } +} + +class InvalidClass { + use myTrait; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10317.php b/tests/PHPStan/Analyser/nsrt/bug-10317.php new file mode 100644 index 0000000000..1ccd87d41a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10317.php @@ -0,0 +1,14 @@ +optionalKey ?? null; + + assertType('int|null', $valueObject); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10338.php b/tests/PHPStan/Analyser/nsrt/bug-10338.php new file mode 100644 index 0000000000..cb9103eae0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10338.php @@ -0,0 +1,12 @@ + $options Array of options. Possible keys are: + * + * - `cache` - Can either be `true`, to enable caching using the config in View::$elementCache. Or an array + * If an array, the following keys can be used: + * + * - `config` - Used to store the cached element in a custom cache configuration. + * - `key` - Used to define the key used in the Cache::write(). It will be prefixed with `element_` + * + * - `callbacks` - Set to true to fire beforeRender and afterRender helper callbacks for this element. + * Defaults to false. + * - `ignoreMissing` - Used to allow missing elements. Set to true to not throw exceptions. + * - `plugin` - setting to false will force to use the application's element from plugin templates, when the + * plugin has element with same name. Defaults to true + * @return string Rendered Element + * @psalm-param array{cache?:array|true, callbacks?:bool, plugin?:string|false, ignoreMissing?:bool} $options + */ + public function element(string $name, array $data = [], array $options = []): string + { + assertType('array|true', $options['cache']); + $options += ['callbacks' => false, 'cache' => null, 'plugin' => null, 'ignoreMissing' => false]; + assertType('array|true|null', $options['cache']); + if (isset($options['cache'])) { + $options['cache'] = $this->_elementCache( + $name, + $data, + array_diff_key($options, ['callbacks' => false, 'plugin' => null, 'ignoreMissing' => null]) + ); + assertType('array{key: string, config: string}', $options['cache']); + } else { + assertType('null', $options['cache']); + } + assertType('array{key: string, config: string}|null', $options['cache']); + + $pluginCheck = $options['plugin'] !== false; + $file = $this->_getElementFileName($name, $pluginCheck); + if ($file && $options['cache']) { + assertType('array{key: string, config: string}', $options['cache']); + return $this->cache(function (): void { + echo ''; + }, $options['cache']); + } + + return $file; + } + + /** + * @param string $name + * @param bool $pluginCheck + */ + protected function _getElementFileName(string $name, bool $pluginCheck): string + { + return $name; + } + + /** + * @param callable $block The block of code that you want to cache the output of. + * @param array $options The options defining the cache key etc. + * @return string The rendered content. + * @throws \InvalidArgumentException When $options is lacking a 'key' option. + */ + public function cache(callable $block, array $options = []): string + { + $options += ['key' => '', 'config' => []]; + if (empty($options['key'])) { + throw new \InvalidArgumentException('Cannot cache content with an empty key'); + } + /** @var string $result */ + $result = $options['key']; + if ($result) { + return $result; + } + + $bufferLevel = ob_get_level(); + ob_start(); + + try { + $block(); + } catch (\Throwable $exception) { + while (ob_get_level() > $bufferLevel) { + ob_end_clean(); + } + + throw $exception; + } + + $result = (string)ob_get_clean(); + + return $result; + } + + /** + * Generate the cache configuration options for an element. + * + * @param string $name Element name + * @param array $data Data + * @param array $options Element options + * @return array Element Cache configuration. + * @psalm-return array{key:string, config:string} + */ + protected function _elementCache(string $name, array $data, array $options): array + { + [$plugin, $name] = explode(':', $name, 2); + + $pluginKey = null; + if ($plugin) { + $pluginKey = str_replace('/', '_', $plugin); + } + $elementKey = str_replace(['\\', '/'], '_', $name); + + $cache = $options['cache']; + unset($options['cache']); + $keys = array_merge( + [$pluginKey, $elementKey], + array_keys($options), + array_keys($data) + ); + $config = [ + 'config' => [], + 'key' => implode('_', array_keys($keys)), + ]; + if (is_array($cache)) { + $config = $cache + $config; + } + $config['key'] = 'element_' . $config['key']; + + /** @var array{config: string, key: string} */ + return $config; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10442.php b/tests/PHPStan/Analyser/nsrt/bug-10442.php new file mode 100644 index 0000000000..d8e2f7612c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10442.php @@ -0,0 +1,15 @@ += 8.1 + +namespace Bug10445; + +use function PHPStan\Testing\assertType; + +enum Error { + case A; + case B; + case C; +} + +/** + * @template T of array + */ +class Response +{ + /** + * @param ?T $data + * @param Error[] $errors + */ + public function __construct( + public ?array $data, + public array $errors = [], + ) { + } + + /** + * @return array{ + * result: ?T, + * errors?: string[], + * } + */ + public function format(): array + { + $output = [ + 'result' => $this->data, + ]; + assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output); + if (count($this->errors) > 0) { + $output['errors'] = array_map(fn ($e) => $e->name, $this->errors); + assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors: non-empty-array<'A'|'B'|'C'>}", $output); + } else { + assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output); + } + assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors?: non-empty-array<'A'|'B'|'C'>}", $output); + return $output; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10468.php b/tests/PHPStan/Analyser/nsrt/bug-10468.php new file mode 100644 index 0000000000..8a3e30e970 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10468.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug10473; + +use ArrayAccess; +use function PHPStan\Testing\assertType; + +/** + * @template TRow of array + */ +class Rows +{ + + /** + * @param list $rowsData + */ + public function __construct(private array $rowsData) + {} + + /** + * @return Row|NULL + */ + public function getByIndex(int $index): ?Row + { + return isset($this->rowsData[$index]) + ? new Row($this->rowsData[$index]) + : NULL; + } +} + +/** + * @template TRow of array + * @implements ArrayAccess, value-of> + */ +class Row implements ArrayAccess +{ + + /** + * @param TRow $data + */ + public function __construct(private array $data) + {} + + /** + * @param key-of $key + */ + public function offsetExists($key): bool + { + return isset($this->data[$key]); + } + + /** + * @template TKey of key-of + * @param TKey $key + * @return TRow[TKey] + */ + public function offsetGet($key): mixed + { + return $this->data[$key]; + } + + public function offsetSet($key, mixed $value): void + { + $this->data[$key] = $value; + } + + public function offsetUnset($key): void + { + unset($this->data[$key]); + } + + /** + * @return TRow + */ + public function toArray(): array + { + return $this->data; + } + +} + +class Foo +{ + + /** @param Rows}> $rows */ + public function doFoo(Rows $rows): void + { + assertType('Bug10473\Rows}>', $rows); + + $row = $rows->getByIndex(0); + + if ($row !== NULL) { + assertType('Bug10473\Row}>', $row); + $fooFromRow = $row['foo']; + + assertType('int<0, max>', $fooFromRow); + assertType('array{foo: int<0, max>}', $row->toArray()); + } + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10477.php b/tests/PHPStan/Analyser/nsrt/bug-10477.php new file mode 100644 index 0000000000..c97672bf98 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10477.php @@ -0,0 +1,28 @@ +foo); + assertType('$this(Bug10477\A)', $this); + (new B())->foo($this); + assertType('mixed', $this->foo); + assertType('$this(Bug10477\A)', $this); + if (isset($this->data['test'])) { + $this->foo = $this->data['test']; + } + } +} + +class B +{ + public function foo(mixed &$var): void {} +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10528.php b/tests/PHPStan/Analyser/nsrt/bug-10528.php new file mode 100644 index 0000000000..07fe77ade0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10528.php @@ -0,0 +1,17 @@ +', $pos); + + $sub = substr($string, 0, $pos); + assert($pos !== FALSE); + $sub = substr($string, 0, $pos); +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-10566.php b/tests/PHPStan/Analyser/nsrt/bug-10566.php new file mode 100644 index 0000000000..2fb46c5a7f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10566.php @@ -0,0 +1,171 @@ +running = true; + + while ($this->running) { + assertType('true', $this->running); + call_user_func(function () { + $this->stop(); + }); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + public function stop(): void + { + $this->running = false; + } + + public function run2(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(function () use ($s) { + $s->stop(); + }); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run3(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(function () { + $s = new self(); + $s->stop(); + }); + assertType('true', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run4(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + $cb = function () use ($s) { + $s = new self(); + $s->stop(); + }; + assertType('true', $s->running); + call_user_func($cb); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run5(): void + { + $this->running = true; + + while ($this->running) { + assertType('true', $this->running); + $cb = function () { + $this->stop(); + }; + assertType('true', $this->running); + call_user_func($cb); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + public function run6(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + (function () use ($s) { + $s = new self(); + $s->stop(); + })(); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run7(): void + { + $this->running = true; + + while ($this->running) { + assertType('true', $this->running); + (function () { + $this->stop(); + })(); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + function run8(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(static function () use ($s) { + $s->stop(); + }); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10627.php b/tests/PHPStan/Analyser/nsrt/bug-10627.php new file mode 100644 index 0000000000..17579ec52c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10627.php @@ -0,0 +1,95 @@ + $list + * @return void + */ + public function sayHello9(array $list): void + { + krsort($list); + assertType("array, string>", $list); + } + + public function sayHello10(): void + { + $list = ['a' => 'A', 'c' => 'C', 'b' => 'B']; + krsort($list); + assertType("array{a: 'A', c: 'C', b: 'B'}", $list); + assertType('false', array_is_list($list)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10650.php b/tests/PHPStan/Analyser/nsrt/bug-10650.php new file mode 100644 index 0000000000..97ce54a9af --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10650.php @@ -0,0 +1,33 @@ + $distPoints + */ + public function repro(array $distPoints): void + { + $ranges = []; + $pointPrev = null; + foreach ($distPoints as $distPoint) { + if ($pointPrev !== null) { + $ranges[] = 'x'; + } + $pointPrev = $distPoint; + } + + assertType('list<\'x\'>', $ranges); + + foreach (array_keys($ranges) as $key) { + if (mt_rand() === 0) { + unset($ranges[$key]); + } + } + + assertType('array, \'x\'>', $ranges); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10653.php b/tests/PHPStan/Analyser/nsrt/bug-10653.php new file mode 100644 index 0000000000..fc6642a229 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10653.php @@ -0,0 +1,13 @@ +mayFail(); + assertType('stdClass|false', $value); + $value = $a->throwOnFailure($value); + assertType(stdClass::class, $value); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10685.php b/tests/PHPStan/Analyser/nsrt/bug-10685.php new file mode 100644 index 0000000000..17f51f2b26 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10685.php @@ -0,0 +1,26 @@ += 8.1 + +namespace Bug10685; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @template A + * @param A $value + * @return A + */ + function identity(mixed $value): mixed + { + return $value; + } + + public function doFoo(): void + { + assertType('array{1|2|3, 1|2|3, 1|2|3}', array_map(fn($i) => $i, [1, 2, 3])); + assertType('array{1, 2, 3}', array_map($this->identity(...), [1, 2, 3])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10699.php b/tests/PHPStan/Analyser/nsrt/bug-10699.php new file mode 100644 index 0000000000..de06986e90 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10699.php @@ -0,0 +1,49 @@ + */ + const DATA = [ + 'af' => [ + 'code' => 'af', + 'english' => "Afrikaans", + 'local' => "Afrikaans", + 'rtl' => false, + 'country' => 'za', + 'variant' => false, + ], + 'am' => [ + 'code' => 'am', + 'english' => "Amharic", + 'local' => "አማርኛ", + 'rtl' => false, + 'country' => 'et', + 'variant' => false, + ], + 'ar' => [ + 'code' => 'ar', + 'english' => "Arabic", + 'local' => "العربية‏", + 'rtl' => true, + 'country' => 'sa', + 'variant' => false, + ], + 'az' => [ + 'code' => 'az', + 'english' => "Azerbaijani", + 'local' => "Azərbaycan dili", + 'rtl' => false, + 'country' => 'az', + 'variant' => false, + ], + 'ba' => [ + 'code' => 'ba', + 'english' => "Bashkir", + 'local' => "башҡорт теле", + 'rtl' => false, + 'country' => 'ru', + 'variant' => false, + ], + 'be' => [ + 'code' => 'be', + 'english' => "Belarusian", + 'local' => "Беларуская", + 'rtl' => false, + 'country' => 'by', + 'variant' => false, + ], + 'bg' => [ + 'code' => 'bg', + 'english' => "Bulgarian", + 'local' => "Български", + 'rtl' => false, + 'country' => 'bg', + 'variant' => false, + ], + 'bn' => [ + 'code' => 'bn', + 'english' => "Bengali", + 'local' => "বাংলা", + 'rtl' => false, + 'country' => 'bd', + 'variant' => false, + ], + 'br' => [ + 'code' => 'br', + 'english' => "Brazilian Portuguese", + 'local' => "Português Brasileiro", + 'rtl' => false, + 'country' => 'br', + 'variant' => false, + ], + 'bs' => [ + 'code' => 'bs', + 'english' => "Bosnian", + 'local' => "Bosanski", + 'rtl' => false, + 'country' => 'ba', + 'variant' => false, + ], + 'ca' => [ + 'code' => 'ca', + 'english' => "Catalan", + 'local' => "Català", + 'rtl' => false, + 'country' => 'es-ca', + 'variant' => false, + ], + 'co' => [ + 'code' => 'co', + 'english' => "Corsican", + 'local' => "Corsu", + 'rtl' => false, + 'country' => 'fr-co', + 'variant' => false, + ], + 'cs' => [ + 'code' => 'cs', + 'english' => "Czech", + 'local' => "Čeština", + 'rtl' => false, + 'country' => 'cz', + 'variant' => false, + ], + 'cy' => [ + 'code' => 'cy', + 'english' => "Welsh", + 'local' => "Cymraeg", + 'rtl' => false, + 'country' => 'gb-wls', + 'variant' => false, + ], + 'da' => [ + 'code' => 'da', + 'english' => "Danish", + 'local' => "Dansk", + 'rtl' => false, + 'country' => 'dk', + 'variant' => false, + ], + 'de' => [ + 'code' => 'de', + 'english' => "German", + 'local' => "Deutsch", + 'rtl' => false, + 'country' => 'de', + 'variant' => false, + ], + 'el' => [ + 'code' => 'el', + 'english' => "Greek", + 'local' => "Ελληνικά", + 'rtl' => false, + 'country' => 'gr', + 'variant' => false, + ], + 'en' => [ + 'code' => 'en', + 'english' => "English", + 'local' => "English", + 'rtl' => false, + 'country' => 'gb', + 'variant' => false, + ], + 'eo' => [ + 'code' => 'eo', + 'english' => "Esperanto", + 'local' => "Esperanto", + 'rtl' => false, + 'country' => 'eo', + 'variant' => false, + ], + 'es' => [ + 'code' => 'es', + 'english' => "Spanish", + 'local' => "Español", + 'rtl' => false, + 'country' => 'es', + 'variant' => false, + ], + 'et' => [ + 'code' => 'et', + 'english' => "Estonian", + 'local' => "Eesti", + 'rtl' => false, + 'country' => 'ee', + 'variant' => false, + ], + 'eu' => [ + 'code' => 'eu', + 'english' => "Basque", + 'local' => "Euskara", + 'rtl' => false, + 'country' => 'eus', + 'variant' => false, + ], + 'fa' => [ + 'code' => 'fa', + 'english' => "Persian", + 'local' => "فارسی", + 'rtl' => true, + 'country' => 'ir', + 'variant' => false, + ], + 'fi' => [ + 'code' => 'fi', + 'english' => "Finnish", + 'local' => "Suomi", + 'rtl' => false, + 'country' => 'fi', + 'variant' => false, + ], + 'fj' => [ + 'code' => 'fj', + 'english' => "Fijian", + 'local' => "Vosa Vakaviti", + 'rtl' => false, + 'country' => 'fj', + 'variant' => false, + ], + 'fl' => [ + 'code' => 'fl', + 'english' => "Filipino", + 'local' => "Filipino", + 'rtl' => false, + 'country' => 'ph', + 'variant' => false, + ], + 'fr' => [ + 'code' => 'fr', + 'english' => "French", + 'local' => "Français", + 'rtl' => false, + 'country' => 'fr', + 'variant' => false, + ], + 'fy' => [ + 'code' => 'fy', + 'english' => "Western Frisian", + 'local' => "frysk", + 'rtl' => false, + 'country' => 'nl', + 'variant' => false, + ], + 'ga' => [ + 'code' => 'ga', + 'english' => "Irish", + 'local' => "Gaeilge", + 'rtl' => false, + 'country' => 'ie', + 'variant' => false, + ], + 'gd' => [ + 'code' => 'gd', + 'english' => "Scottish Gaelic", + 'local' => "Gàidhlig", + 'rtl' => false, + 'country' => 'gb-sct', + 'variant' => false, + ], + 'gl' => [ + 'code' => 'gl', + 'english' => "Galician", + 'local' => "Galego", + 'rtl' => false, + 'country' => 'es-ga', + 'variant' => false, + ], + 'gu' => [ + 'code' => 'gu', + 'english' => "Gujarati", + 'local' => "ગુજરાતી", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'ha' => [ + 'code' => 'ha', + 'english' => "Hausa", + 'local' => "هَوُسَ", + 'rtl' => false, + 'country' => 'ne', + 'variant' => false, + ], + 'he' => [ + 'code' => 'he', + 'english' => "Hebrew", + 'local' => "עברית", + 'rtl' => true, + 'country' => 'il', + 'variant' => false, + ], + 'hi' => [ + 'code' => 'hi', + 'english' => "Hindi", + 'local' => "हिंदी", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'hr' => [ + 'code' => 'hr', + 'english' => "Croatian", + 'local' => "Hrvatski", + 'rtl' => false, + 'country' => 'hr', + 'variant' => false, + ], + 'ht' => [ + 'code' => 'ht', + 'english' => "Haitian Creole", + 'local' => "Kreyòl ayisyen", + 'rtl' => false, + 'country' => 'ht', + 'variant' => false, + ], + 'hu' => [ + 'code' => 'hu', + 'english' => "Hungarian", + 'local' => "Magyar", + 'rtl' => false, + 'country' => 'hu', + 'variant' => false, + ], + 'hw' => [ + 'code' => 'hw', + 'english' => "Hawaiian", + 'local' => "‘Ōlelo Hawai‘i", + 'rtl' => false, + 'country' => 'hw', + 'variant' => false, + ], + 'hy' => [ + 'code' => 'hy', + 'english' => "Armenian", + 'local' => "հայերեն", + 'rtl' => false, + 'country' => 'am', + 'variant' => false, + ], + 'id' => [ + 'code' => 'id', + 'english' => "Indonesian", + 'local' => "Bahasa Indonesia", + 'rtl' => false, + 'country' => 'id', + 'variant' => false, + ], + 'ig' => [ + 'code' => 'ig', + 'english' => "Igbo", + 'local' => "Igbo", + 'rtl' => false, + 'country' => 'ne', + 'variant' => false, + ], + 'is' => [ + 'code' => 'is', + 'english' => "Icelandic", + 'local' => "Íslenska", + 'rtl' => false, + 'country' => 'is', + 'variant' => false, + ], + 'it' => [ + 'code' => 'it', + 'english' => "Italian", + 'local' => "Italiano", + 'rtl' => false, + 'country' => 'it', + 'variant' => false, + ], + 'ja' => [ + 'code' => 'ja', + 'english' => "Japanese", + 'local' => "日本語", + 'rtl' => false, + 'country' => 'jp', + 'variant' => false, + ], + 'jv' => [ + 'code' => 'jv', + 'english' => "Javanese", + 'local' => "Wong Jawa", + 'rtl' => false, + 'country' => 'id', + 'variant' => false, + ], + 'ka' => [ + 'code' => 'ka', + 'english' => "Georgian", + 'local' => "ქართული", + 'rtl' => false, + 'country' => 'ge', + 'variant' => false, + ], + 'kk' => [ + 'code' => 'kk', + 'english' => "Kazakh", + 'local' => "Қазақша", + 'rtl' => false, + 'country' => 'kz', + 'variant' => false, + ], + 'km' => [ + 'code' => 'km', + 'english' => "Central Khmer", + 'local' => "ភាសាខ្មែរ", + 'rtl' => false, + 'country' => 'kh', + 'variant' => false, + ], + 'kn' => [ + 'code' => 'kn', + 'english' => "Kannada", + 'local' => "ಕನ್ನಡ", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'ko' => [ + 'code' => 'ko', + 'english' => "Korean", + 'local' => "한국어", + 'rtl' => false, + 'country' => 'kr', + 'variant' => false, + ], + 'ku' => [ + 'code' => 'ku', + 'english' => "Kurdish", + 'local' => "كوردی", + 'rtl' => true, + 'country' => 'iq', + 'variant' => false, + ], + 'ky' => [ + 'code' => 'ky', + 'english' => "Kyrgyz", + 'local' => "кыргызча", + 'rtl' => false, + 'country' => 'kg', + 'variant' => false, + ], + 'la' => [ + 'code' => 'la', + 'english' => "Latin", + 'local' => "Latine", + 'rtl' => false, + 'country' => 'it', + 'variant' => false, + ], + 'lb' => [ + 'code' => 'lb', + 'english' => "Luxembourgish", + 'local' => "Lëtzebuergesch", + 'rtl' => false, + 'country' => 'lu', + 'variant' => false, + ], + 'lo' => [ + 'code' => 'lo', + 'english' => "Lao", + 'local' => "ພາສາລາວ", + 'rtl' => false, + 'country' => 'la', + 'variant' => false, + ], + 'lt' => [ + 'code' => 'lt', + 'english' => "Lithuanian", + 'local' => "Lietuvių", + 'rtl' => false, + 'country' => 'lt', + 'variant' => false, + ], + 'lv' => [ + 'code' => 'lv', + 'english' => "Latvian", + 'local' => "Latviešu", + 'rtl' => false, + 'country' => 'lv', + 'variant' => false, + ], + 'lg' => [ + 'code' => 'lg', + 'english' => "Luganda", + 'local' => "Oluganda", + 'rtl' => false, + 'country' => 'ug', + 'variant' => false, + ], + 'mg' => [ + 'code' => 'mg', + 'english' => "Malagasy", + 'local' => "Malagasy", + 'rtl' => false, + 'country' => 'mg', + 'variant' => false, + ], + 'mi' => [ + 'code' => 'mi', + 'english' => "Māori", + 'local' => "te reo Māori", + 'rtl' => false, + 'country' => 'nz', + 'variant' => false, + ], + 'mk' => [ + 'code' => 'mk', + 'english' => "Macedonian", + 'local' => "Македонски", + 'rtl' => false, + 'country' => 'mk', + 'variant' => false, + ], + 'ml' => [ + 'code' => 'ml', + 'english' => "Malayalam", + 'local' => "മലയാളം", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'mn' => [ + 'code' => 'mn', + 'english' => "Mongolian", + 'local' => "Монгол", + 'rtl' => false, + 'country' => 'mn', + 'variant' => false, + ], + 'mr' => [ + 'code' => 'mr', + 'english' => "Marathi", + 'local' => "मराठी", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'ms' => [ + 'code' => 'ms', + 'english' => "Malay", + 'local' => "Bahasa Melayu", + 'rtl' => false, + 'country' => 'my', + 'variant' => false, + ], + 'mt' => [ + 'code' => 'mt', + 'english' => "Maltese", + 'local' => "Malti", + 'rtl' => false, + 'country' => 'mt', + 'variant' => false, + ], + 'my' => [ + 'code' => 'my', + 'english' => "Burmese", + 'local' => "မျန္မာစာ", + 'rtl' => false, + 'country' => 'mm', + 'variant' => false, + ], + 'ne' => [ + 'code' => 'ne', + 'english' => "Nepali", + 'local' => "नेपाली", + 'rtl' => false, + 'country' => 'np', + 'variant' => false, + ], + 'nl' => [ + 'code' => 'nl', + 'english' => "Dutch", + 'local' => "Nederlands", + 'rtl' => false, + 'country' => 'nl', + 'variant' => false, + ], + 'no' => [ + 'code' => 'no', + 'english' => "Norwegian", + 'local' => "Norsk", + 'rtl' => false, + 'country' => 'no', + 'variant' => false, + ], + 'ny' => [ + 'code' => 'ny', + 'english' => "Chichewa", + 'local' => "chiCheŵa", + 'rtl' => false, + 'country' => 'mw', + 'variant' => false, + ], + 'pa' => [ + 'code' => 'pa', + 'english' => "Punjabi", + 'local' => "ਪੰਜਾਬੀ", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'pl' => [ + 'code' => 'pl', + 'english' => "Polish", + 'local' => "Polski", + 'rtl' => false, + 'country' => 'pl', + 'variant' => false, + ], + 'ps' => [ + 'code' => 'ps', + 'english' => "Pashto", + 'local' => "پښتو", + 'rtl' => true, + 'country' => 'pk', + 'variant' => false, + ], + 'pt' => [ + 'code' => 'pt', + 'english' => "Portuguese", + 'local' => "Português", + 'rtl' => false, + 'country' => 'pt', + 'variant' => false, + ], + 'ro' => [ + 'code' => 'ro', + 'english' => "Romanian", + 'local' => "Română", + 'rtl' => false, + 'country' => 'ro', + 'variant' => false, + ], + 'ru' => [ + 'code' => 'ru', + 'english' => "Russian", + 'local' => "Русский", + 'rtl' => false, + 'country' => 'ru', + 'variant' => false, + ], + 'sd' => [ + 'code' => 'sd', + 'english' => "Sindhi", + 'local' => "سنڌي، سندھی, सिन्धी", + 'rtl' => false, + 'country' => 'pk', + 'variant' => false, + ], + 'si' => [ + 'code' => 'si', + 'english' => "Sinhalese", + 'local' => "සිංහල", + 'rtl' => false, + 'country' => 'lk', + 'variant' => false, + ], + 'sk' => [ + 'code' => 'sk', + 'english' => "Slovak", + 'local' => "Slovenčina", + 'rtl' => false, + 'country' => 'sk', + 'variant' => false, + ], + 'sl' => [ + 'code' => 'sl', + 'english' => "Slovenian", + 'local' => "Slovenščina", + 'rtl' => false, + 'country' => 'si', + 'variant' => false, + ], + 'sm' => [ + 'code' => 'sm', + 'english' => "Samoan", + 'local' => "gagana fa'a Samoa", + 'rtl' => false, + 'country' => 'ws', + 'variant' => false, + ], + 'sn' => [ + 'code' => 'sn', + 'english' => "Shona", + 'local' => "chiShona", + 'rtl' => false, + 'country' => 'zw', + 'variant' => false, + ], + 'so' => [ + 'code' => 'so', + 'english' => "Somali", + 'local' => "Soomaaliga", + 'rtl' => false, + 'country' => 'so', + 'variant' => false, + ], + 'sq' => [ + 'code' => 'sq', + 'english' => "Albanian", + 'local' => "Shqip", + 'rtl' => false, + 'country' => 'al', + 'variant' => false, + ], + 'sr' => [ + 'code' => 'sr', + 'english' => "Serbian (Cyrillic)", + 'local' => "Српски", + 'rtl' => false, + 'country' => 'rs', + 'variant' => false, + ], + 'st' => [ + 'code' => 'st', + 'english' => "Southern Sotho", + 'local' => "seSotho", + 'rtl' => false, + 'country' => 'ng', + 'variant' => false, + ], + 'su' => [ + 'code' => 'su', + 'english' => "Sundanese", + 'local' => "Sundanese", + 'rtl' => false, + 'country' => 'sd', + 'variant' => false, + ], + 'sv' => [ + 'code' => 'sv', + 'english' => "Swedish", + 'local' => "Svenska", + 'rtl' => false, + 'country' => 'se', + 'variant' => false, + ], + 'sw' => [ + 'code' => 'sw', + 'english' => "Swahili", + 'local' => "Kiswahili", + 'rtl' => false, + 'country' => 'ke', + 'variant' => false, + ], + 'ta' => [ + 'code' => 'ta', + 'english' => "Tamil", + 'local' => "தமிழ்", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'te' => [ + 'code' => 'te', + 'english' => "Telugu", + 'local' => "తెలుగు", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'tg' => [ + 'code' => 'tg', + 'english' => "Tajik", + 'local' => "Тоҷикӣ", + 'rtl' => false, + 'country' => 'tj', + 'variant' => false, + ], + 'th' => [ + 'code' => 'th', + 'english' => "Thai", + 'local' => "ภาษาไทย", + 'rtl' => false, + 'country' => 'th', + 'variant' => false, + ], + 'tl' => [ + 'code' => 'tl', + 'english' => "Tagalog", + 'local' => "Tagalog", + 'rtl' => false, + 'country' => 'ph', + 'variant' => false, + ], + 'to' => [ + 'code' => 'to', + 'english' => "Tongan", + 'local' => "faka-Tonga", + 'rtl' => false, + 'country' => 'to', + 'variant' => false, + ], + 'tr' => [ + 'code' => 'tr', + 'english' => "Turkish", + 'local' => "Türkçe", + 'rtl' => false, + 'country' => 'tr', + 'variant' => false, + ], + 'tt' => [ + 'code' => 'tt', + 'english' => "Tatar", + 'local' => "Tatar", + 'rtl' => false, + 'country' => 'tr', + 'variant' => false, + ], + 'tw' => [ + 'code' => 'tw', + 'english' => "Traditional Chinese", + 'local' => "中文 (繁體)", + 'rtl' => false, + 'country' => 'tw', + 'variant' => false, + ], + 'ty' => [ + 'code' => 'ty', + 'english' => "Tahitian", + 'local' => "te reo Tahiti, te reo Māʼohi", + 'rtl' => false, + 'country' => 'pf', + 'variant' => false, + ], + 'uk' => [ + 'code' => 'uk', + 'english' => "Ukrainian", + 'local' => "Українська", + 'rtl' => false, + 'country' => 'ua', + 'variant' => false, + ], + 'ur' => [ + 'code' => 'ur', + 'english' => "Urdu", + 'local' => "اردو", + 'rtl' => true, + 'country' => 'pk', + 'variant' => false, + ], + 'uz' => [ + 'code' => 'uz', + 'english' => "Uzbek", + 'local' => "O'zbek", + 'rtl' => false, + 'country' => 'uz', + 'variant' => false, + ], + 'vi' => [ + 'code' => 'vi', + 'english' => "Vietnamese", + 'local' => "Tiếng Việt", + 'rtl' => false, + 'country' => 'vn', + 'variant' => false, + ], + 'xh' => [ + 'code' => 'xh', + 'english' => "Xhosa", + 'local' => "isiXhosa", + 'rtl' => false, + 'country' => 'za', + 'variant' => false, + ], + 'yi' => [ + 'code' => 'yi', + 'english' => "Yiddish", + 'local' => "ייִדיש", + 'rtl' => false, + 'country' => 'il', + 'variant' => false, + ], + 'yo' => [ + 'code' => 'yo', + 'english' => "Yoruba", + 'local' => "Yorùbá", + 'rtl' => false, + 'country' => 'ng', + 'variant' => false, + ], + 'zh' => [ + 'code' => 'zh', + 'english' => "Simplified Chinese", + 'local' => "中文 (简体)", + 'rtl' => false, + 'country' => 'cn', + 'variant' => false, + ], + 'zu' => [ + 'code' => 'zu', + 'english' => "Zulu", + 'local' => "isiZulu", + 'rtl' => false, + 'country' => 'za', + 'variant' => false, + ], + 'hm' => [ + 'code' => 'hm', + 'english' => "Hmong", + 'local' => "Hmoob", + 'rtl' => false, + 'country' => 'hmn', + 'variant' => false, + ], + 'cb' => [ + 'code' => 'cb', + 'english' => "Cebuano", + 'local' => "Sugbuanon", + 'rtl' => false, + 'country' => 'ph', + 'variant' => false, + ], + 'or' => [ + 'code' => 'or', + 'english' => "Odia", + 'local' => "ଓଡ଼ିଆ", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'tk' => [ + 'code' => 'tk', + 'english' => "Turkmen", + 'local' => "Türkmen", + 'rtl' => false, + 'country' => 'tr', + 'variant' => false, + ], + 'ug' => [ + 'code' => 'ug', + 'english' => "Uyghur", + 'local' => "ئۇيغۇر", + 'rtl' => true, + 'country' => 'uig', + 'variant' => false, + ], + 'fc' => [ + 'code' => 'fc', + 'english' => "French (Canada)", + 'local' => "Français (Canada)", + 'rtl' => false, + 'country' => 'ca', + 'variant' => true, + ], + 'as' => [ + 'code' => 'as', + 'english' => "Assamese", + 'local' => "অসমীয়া", + 'rtl' => false, + 'country' => 'in', + 'variant' => false, + ], + 'sa' => [ + 'code' => 'sa', + 'english' => "Serbian (Latin)", + 'local' => "Srpski", + 'rtl' => false, + 'country' => 'rs', + 'variant' => false, + ], + 'om' => [ + 'code' => 'om', + 'english' => "Oromo", + 'local' => "Afaan Oromoo", + 'rtl' => false, + 'country' => 'et', + 'variant' => false, + ], + 'iu' => [ + 'code' => 'iu', + 'english' => "Inuktitut", + 'local' => "ᐃᓄᒃᑎᑐᑦ", + 'rtl' => false, + 'country' => 'ca', + 'variant' => false, + ], + 'ti' => [ + 'code' => 'ti', + 'english' => "Tigrinya", + 'local' => "ቲግሪንያ", + 'rtl' => false, + 'country' => 'er', + 'variant' => false, + ], + 'bm' => [ + 'code' => 'bm', + 'english' => "Bambara", + 'local' => "Bamanankan", + 'rtl' => false, + 'country' => 'ml', + 'variant' => false, + ], + 'bo' => [ + 'code' => 'bo', + 'english' => "Tibetan", + 'local' => "བོད་ཡིག", + 'rtl' => false, + 'country' => 'cn', + 'variant' => false, + ], + 'ak' => [ + 'code' => 'ak', + 'english' => "Akan", + 'local' => "Baoulé", + 'rtl' => false, + 'country' => 'gh', + 'variant' => false, + ], + 'rw' => [ + 'code' => 'rw', + 'english' => "Kinyarwanda", + 'local' => "Kinyarwanda", + 'rtl' => false, + 'country' => 'rw', + 'variant' => false, + ], + 'kb' => [ + 'code' => 'kb', + 'english' => "Kurdish (Sorani)", + 'local' => "سۆرانی", + 'rtl' => true, + 'country' => 'iq', + 'variant' => false, + ], + 'fo' => [ + 'code' => 'fo', + 'english' => "Faroese", + 'local' => "Føroyskt", + 'rtl' => false, + 'country' => 'fo', + 'variant' => false, + ] + ]; +} + +function test(string $code): void +{ + $country = Languages::DATA[$code]['country']; + + if ($country === 'fo' || $country === 'Faroese' || $country === 'Føroyskt') { + // foo + } else { + assertType('(bool|(literal-string&non-falsy-string))', $country); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-10721.php b/tests/PHPStan/Analyser/nsrt/bug-10721.php new file mode 100644 index 0000000000..c82d2298f2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10721.php @@ -0,0 +1,100 @@ + + */ + public function retrieve(?int $limit = 20): array + { + $list = [ + 'zib', + 'zib 2', + 'zeit im bild', + 'soko', + 'landkrimi', + 'tatort', + ]; + + assertType("array{'zib', 'zib 2', 'zeit im bild', 'soko', 'landkrimi', 'tatort'}", $list); + shuffle($list); + assertType("non-empty-list<'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>", $list); + + assertType("non-empty-list<'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>", array_slice($list, 0, max($limit, 1))); + return array_slice($list, 0, max($limit, 1)); + } + + public function listVariants(): void + { + $arr = [ + 2 => 'zib', + 4 => 'zib 2', + ]; + + assertType("array{2: 'zib', 4: 'zib 2'}", $arr); + shuffle($arr); + assertType("non-empty-list<'zib'|'zib 2'>", $arr); + + $list = [ + 'zib', + 'zib 2', + ]; + + assertType("array{'zib', 'zib 2'}", $list); + shuffle($list); + assertType("non-empty-list<'zib'|'zib 2'>", $list); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2)); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 1)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 1)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 1)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 1)); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 2)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 2)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 2)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 2)); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 3)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 3)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 3)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 3)); + + assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, -1, 3, true)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 3, true)); + assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, 1, 3, true)); // could be non-empty-array + assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, 2, 3, true)); + + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 3, false)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 3, false)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 3, false)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 3, false)); + } + + /** + * @param array $strings + * @param 0|1 $maybeZero + */ + public function arrayVariants(array $strings, $maybeZero): void + { + assertType("array", $strings); + assertType("list", array_slice($strings, 0)); + assertType("list", array_slice($strings, 1)); + assertType("list", array_slice($strings, $maybeZero)); + + if (count($strings) > 0) { + assertType("non-empty-array", $strings); + assertType("non-empty-list", array_slice($strings, 0)); + assertType("list", array_slice($strings, 1)); + assertType("list", array_slice($strings, $maybeZero)); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10834.php b/tests/PHPStan/Analyser/nsrt/bug-10834.php new file mode 100644 index 0000000000..8e9050ac4e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10834.php @@ -0,0 +1,23 @@ + $b + */ + public function doFoo($b): void + { + assertType('lowercase-string&non-falsy-string&uppercase-string', '@' . $b); + } + + /** + * @param int|false $b + */ + public function doFoo2($b): void + { + assertType('lowercase-string&non-falsy-string&uppercase-string', '@' . $b); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10893.php b/tests/PHPStan/Analyser/nsrt/bug-10893.php new file mode 100644 index 0000000000..a2b8396dd5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10893.php @@ -0,0 +1,28 @@ +|int<1, max>', (int)$str); + assertType('true', (int)$str !== 0); + + assertType('non-falsy-string&numeric-string', $value->format('u')); + assertType('int|int<1, max>', (int)$value->format('u')); + assertType('true', (int)$value->format('u') !== 0); + + assertType('non-falsy-string&numeric-string', $value->format('v')); + assertType('int|int<1, max>', (int)$value->format('v')); + assertType('true', (int)$value->format('v') !== 0); + + assertType('float', $value->format('u') * 1e-6); + assertType('float', $value->format('v') * 1e-3); + + return (int) $value->format('u') !== 0; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10952.php b/tests/PHPStan/Analyser/nsrt/bug-10952.php new file mode 100644 index 0000000000..d25c03b1fe --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10952.php @@ -0,0 +1,32 @@ + + */ + public function getArray(): array + { + return array_fill(0, random_int(0, 10), 'test'); + } + + public function test(): void + { + $array = $this->getArray(); + + if (count($array) > 1) { + assertType('non-empty-array', $array); + } else { + assertType('array', $array); + } + + match (true) { + count($array) > 1 => assertType('non-empty-array', $array), + default => assertType('array', $array), + }; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10952b.php b/tests/PHPStan/Analyser/nsrt/bug-10952b.php new file mode 100644 index 0000000000..02386aa4b7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10952b.php @@ -0,0 +1,50 @@ +getString(); + + if (1 < mb_strlen($string)) { + assertType('non-falsy-string', $string); + } else { + assertType("string", $string); + } + + if (mb_strlen($string) > 1) { + assertType('non-falsy-string', $string); + } else { + assertType("string", $string); + } + + if (2 < mb_strlen($string)) { + assertType('non-falsy-string', $string); + } else { + assertType("string", $string); + } + + match (true) { + mb_strlen($string) > 0 => assertType('non-empty-string', $string), + default => assertType("''", $string), + }; + + assertType('int<0, 1>', strlen($this->getBool())); + assertType('int<0, 1>', mb_strlen($this->getBool())); + } + + public function getBool(): bool + { + return rand(0, 1) === 1; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11035.php b/tests/PHPStan/Analyser/nsrt/bug-11035.php new file mode 100644 index 0000000000..dabb834965 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11035.php @@ -0,0 +1,41 @@ + $maybeOne + * @param int<2,10> $neverOne + */ +function lengthTypes(string $phone, int $maybeOne, int $neverOne): string +{ + if ( + 10 === strlen($phone) + ) { + assertType('non-falsy-string', $phone); + + assertType('non-empty-string', substr($phone, 0, 1)); + assertType('bool', '0' === substr($phone, 0, 1)); + + assertType('non-empty-string', substr($phone, 0, $maybeOne)); + assertType('bool', '0' === substr($phone, 0, $maybeOne)); + + assertType('non-falsy-string', substr($phone, 0, $neverOne)); + assertType('false', '0' === substr($phone, 0, $neverOne)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11064.php b/tests/PHPStan/Analyser/nsrt/bug-11064.php new file mode 100644 index 0000000000..8f0876667a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11064.php @@ -0,0 +1,30 @@ += 8.0 + +namespace Bug11188; + +use DateTime; +use function PHPStan\Testing\assertType; + +/** + * @template TDefault of string + * @template TExplicit of string + * + * @param TDefault $abstract + * @param array $parameters + * @param TExplicit|null $type + * @return ( + * $type is class-string ? new : + * $abstract is class-string ? new : mixed + * ) + */ +function instance(string $abstract, array $parameters = [], ?string $type = null): mixed +{ + return 'something'; +} + +function (): void { + assertType(DateTime::class, instance('cache', [], DateTime::class)); + assertType(DateTime::class, instance(DateTime::class)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11200-lt8.php b/tests/PHPStan/Analyser/nsrt/bug-11200-lt8.php new file mode 100644 index 0000000000..faae5146eb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11200-lt8.php @@ -0,0 +1,28 @@ +eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fgetss(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + } +} \ No newline at end of file diff --git a/tests/PHPStan/Analyser/nsrt/bug-11200.php b/tests/PHPStan/Analyser/nsrt/bug-11200.php new file mode 100644 index 0000000000..588298c7d6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11200.php @@ -0,0 +1,180 @@ +eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fflush(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fgetc() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fgetc(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fgetcsv() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fgetcsv(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fgets() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fgets(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fpassthru() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fpassthru(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fputcsv() : void + { + // places file pointer at the start of the file + $file = new \SplFileObject('php://memory', 'rw+'); + assertType('int|false', $file->ftell()); + if ($file->ftell() !== 0) + { + return; + } + assertType('0', $file->ftell()); + // This file is not empty. + // call method that has side effects + $file->fputcsv(['a']); + // the value of ftell may have changed + assertType('int|false', $file->ftell()); + } + + public function fread() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fread(1); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fscanf() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fscanf('%f'); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fseek() : void + { + // places file pointer at the start of the file + $file = new \SplFileObject('php://memory', 'rw+'); + assertType('int|false', $file->ftell()); + if ($file->ftell() !== 0) + { + return; + } + assertType('0', $file->ftell()); + // This file is not empty. + // call method that has side effects + $file->fseek(1,\SEEK_SET); + // the value of ftell may have changed + assertType('int|false', $file->ftell()); + } + + public function ftruncate() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->ftruncate(0); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fwrite() : void + { + // places file pointer at the start of the file + $file = new \SplFileObject('php://memory', 'rw+'); + assertType('int|false', $file->ftell()); + if ($file->ftell() !== 0) + { + return; + } + assertType('0', $file->ftell()); + // This file is not empty. + // call method that has side effects + $file->fwrite('a'); + // the value of ftell may have changed + assertType('int|false', $file->ftell()); + } + +} \ No newline at end of file diff --git a/tests/PHPStan/Analyser/nsrt/bug-11201.php b/tests/PHPStan/Analyser/nsrt/bug-11201.php new file mode 100644 index 0000000000..87625f777f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11201.php @@ -0,0 +1,56 @@ + */ +function returnsArray(){ + return []; +} + +/** @return non-empty-string */ +function returnsNonEmptyString(): string +{ + return 'a'; +} + +/** @return non-falsy-string */ +function returnsNonFalsyString(): string +{ + return '1'; +} + +/** @return string */ +function returnsJustString(): string +{ + return rand(0,1) === 1 ? 'foo' : ''; +} + +function returnsBool(): bool { + return true; +} + +$s = sprintf("%s", returnsNonEmptyString()); +assertType('non-empty-string', $s); + +$s = sprintf("%s", returnsNonFalsyString()); +assertType('non-falsy-string', $s); + +$s = sprintf("%s", returnsJustString()); +assertType('string', $s); + +$s = sprintf("%s", implode(', ', array_map('intval', returnsArray()))); +assertType('lowercase-string&uppercase-string', $s); + +$s = sprintf('%2$s', 1234, returnsNonFalsyString()); +assertType('non-falsy-string', $s); + +$s = sprintf('%20s', 'abc'); +assertType("' abc'", $s); + +$s = sprintf('%20s', true); +assertType("' 1'", $s); + +$s = sprintf('%20s', returnsBool()); +assertType("' '|' 1'", $s); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11233.php b/tests/PHPStan/Analyser/nsrt/bug-11233.php new file mode 100644 index 0000000000..e8191d37a5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11233.php @@ -0,0 +1,30 @@ += 8.1 + +namespace Bug11233; + +use function PHPStan\Testing\assertType; + +class EnumExtension +{ + /** + * @template T of \UnitEnum + * + * @param class-string $enum + */ + public static function getEnumCases(string $enum): void + { + assertType('list', $enum::cases()); + } + + /** + * @template T of \BackedEnum + * + * @param class-string $enum + * + * @return list + */ + public static function getEnumCases2(string $enum): void + { + assertType('list', $enum::cases()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11293.php b/tests/PHPStan/Analyser/nsrt/bug-11293.php new file mode 100644 index 0000000000..19a9a1eb5c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11293.php @@ -0,0 +1,62 @@ += 7.4 + +namespace Bug11293; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) > 0) { + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + } + } + + public function sayHello2(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) === 1) { + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + } + } + + public function sayHello3(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) >= 1) { + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + } + } + + public function sayHello4(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) <= 0) { + assertType('array{}', $matches); + + return; + } + + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + } + + public function sayHello5(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) < 1) { + assertType('array{}', $matches); + + return; + } + + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + } + + public function sayHello6(string $s): void + { + if (1 > preg_match('/data-(\d{6})\.json$/', $s, $matches)) { + assertType('array{}', $matches); + + return; + } + + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php new file mode 100644 index 0000000000..96b810431d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -0,0 +1,226 @@ +\d+)\.(?\d+)(?:\.(?\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + + assertType('array{0: non-falsy-string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch: numeric-string|null, 3: numeric-string|null}', $matches); + } +} + +function doUnmatchedAsNull(string $s): void { + if (preg_match('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType("array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches); + } + assertType("array{}|array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches); +} + +// see https://3v4l.org/VeDob +function unmatchedAsNullWithOptionalGroup(string $s): void { + if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + // with PREG_UNMATCHED_AS_NULL the offset 1 will always exist. It is correct that it's nullable because it's optional though + assertType("array{non-falsy-string, '£'|'€'|null}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{non-falsy-string, '£'|'€'|null}", $matches); +} + +function bug11331a(string $url):void { + // group a is actually optional as the entire (?:...) around it is optional + if (preg_match('{^ + (?: + (?.+) + )? + (?.+)}mix', $url, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{0: non-empty-string, a: non-empty-string|null, 1: non-empty-string|null, b: non-empty-string, 2: non-empty-string}', $matches); + } +} + +function bug11331b(string $url):void { + if (preg_match('{^ + (?: + (?.+) + )? + (?.+)?}mix', $url, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{0: string, a: non-empty-string|null, 1: non-empty-string|null, b: non-empty-string|null, 2: non-empty-string|null}', $matches); + } +} + +function bug11331c(string $url):void { + if (preg_match('{^ + (?: + (?:https?|git)://([^/]+)/ (?# group 1 here can be null if group 2 matches) + | (?# the alternation making it so that only either should match) + git@([^:]+):/? (?# group 2 here can be null if group 1 matches) + ) + ([^/]+) + / + ([^/]+?) + (?:\.git|/)? +$}x', $url, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{non-falsy-string, non-empty-string|null, non-empty-string|null, non-empty-string, non-empty-string}', $matches); + } +} + +class UnmatchedAsNullWithTopLevelAlternation { + function doFoo(string $s): void { + if (preg_match('/Price: (?:(£)|(€))\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType("array{non-falsy-string, '£'|null, '€'|null}", $matches); // could be tagged union + } + } + + function doBar(string $s): void { + if (preg_match('/Price: (?:(£)|(€))?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType("array{non-falsy-string, '£'|null, '€'|null}", $matches); // could be tagged union + } + } +} + +function (string $size): void { + if (preg_match('/ab(\d){2,4}xx([0-9])?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, numeric-string, numeric-string|null}', $matches); +}; + +function (string $size): void { + if (preg_match('/a(\dAB){2}b(\d){2,4}([1-5])([1-5a-z])e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-falsy-string, numeric-string, numeric-string, non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(ab(\d)){2,4}xx([0-9][a-c])?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-falsy-string, numeric-string, non-falsy-string|null}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\d+)e(\d?)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType("array{non-falsy-string, numeric-string, ''|numeric-string}", $matches); +}; + +function (string $size): void { + if (preg_match('/ab(?P\d+)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\d\d)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\d+\s)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-falsy-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\s)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\S)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\S?)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\S)?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-empty-string|null}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\d+\d?)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, numeric-string}', $matches); +}; + +function (string $s): void { + if (preg_match('/Price: ([2-5])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{non-falsy-string, numeric-string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([2-5A-Z])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{non-falsy-string, non-empty-string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { + assertType("array{non-falsy-string, non-falsy-string|null, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/(?\s*)(?.*)/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { + assertType('array{0: string, whitespace: string, 1: string, value: string, 2: string}', $matches); + } +}; + +function (string $s): void { + preg_match('/%a(\d*)/', $s, $matches, PREG_UNMATCHED_AS_NULL); + assertType("list{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} +}; + +function (string $s): void { + preg_match('/%a(\d*)?/', $s, $matches, PREG_UNMATCHED_AS_NULL); + assertType("list{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} +}; + +function (string $s): void { + if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType("array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); + } else { + assertType("array{}", $matches); + } + assertType("array{}|array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); +}; + +function (string $s): void { + if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE) === 1) { + assertType("array{array{non-empty-string|null, int<-1, max>}, array{numeric-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}", $matches); + } +}; + +function (string $s): void { + if (preg_match('~a|((u)x)|((v)y)~', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { + assertType("array{non-empty-string, 'ux'|null, 'u'|null, 'vy'|null, 'v'|null}", $matches); + } +}; + +function (string $s): void { + preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL); + assertType("list{0?: string, 1?: numeric-string|null, 2?: non-empty-string|null}", $matches); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11472.php b/tests/PHPStan/Analyser/nsrt/bug-11472.php new file mode 100644 index 0000000000..a6ee71f048 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11472.php @@ -0,0 +1,18 @@ += 8.1 + +namespace Bug11472; + +use function PHPStan\Testing\assertType; + +/** + * @phpstan-return ($maybeFoo is 'foo' ? true : false) + */ +function isFoo(mixed $maybeFoo): bool +{ + return $maybeFoo === 'foo'; +} + +function (): void { + assertType('true', isFoo('foo')); + assertType('true', isFoo(...)('foo')); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11518-types.php b/tests/PHPStan/Analyser/nsrt/bug-11518-types.php new file mode 100644 index 0000000000..19d5aeb15f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11518-types.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug11561; + +use function PHPStan\Testing\assertType; +use DateTime; + +/** @param array{date: DateTime} $c */ +function main(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + assertType('array{date: DateTime, id: 1}', $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); +} + + +/** @param array{date: DateTime} $c */ +function main2(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); +} + +/** @param array{date: DateTime} $c */ +function main3(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + if (rand(0,1)) { + $c['name'] = 'ruud'; + } + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); +} + +/** @param array{date: DateTime} $c */ +function main4(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + if (rand(0,1)) { + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'y'; + } + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); +} diff --git a/tests/PHPStan/Analyser/data/bug-1157.php b/tests/PHPStan/Analyser/nsrt/bug-1157.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1157.php rename to tests/PHPStan/Analyser/nsrt/bug-1157.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-11570.php b/tests/PHPStan/Analyser/nsrt/bug-11570.php new file mode 100644 index 0000000000..7a97062078 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11570.php @@ -0,0 +1,14 @@ + $var !== null); + assertType("array{one?: string, two?: string, three?: string}", $data); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11580.php b/tests/PHPStan/Analyser/nsrt/bug-11580.php new file mode 100644 index 0000000000..039a1895f5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11580.php @@ -0,0 +1,35 @@ + $criteria + * + * @return object[] The objects. + * @psalm-return list<\stdClass> + */ + function findBy(array $criteria): array + { + return [new \stdClass, new \stdCLass, new \stdClass, new \stdClass]; + } +} + +class Payload { + /** @var non-empty-list */ + public array $ids = ['one', 'two']; +} + +function doFoo() { + $payload = new Payload(); + + $fetcher = new Repository(); + $entries = $fetcher->findBy($payload->ids); + assertType('list', $entries); + assertType('int<0, max>', count($entries)); + assertType('int<1, max>', count($payload->ids)); + if (count($entries) !== count($payload->ids)) { + exit(); + } + + assertType('non-empty-list', $entries); + if (count($entries) > 3) { + throw new \RuntimeException(); + } + + assertType('non-empty-list', $entries); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11692.php b/tests/PHPStan/Analyser/nsrt/bug-11692.php new file mode 100644 index 0000000000..c1edbba1fd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11692.php @@ -0,0 +1,24 @@ +', range(1, 9, .01)); + assertType('array{1, 4, 7}', range(1, 9, 3)); + + assertType('non-empty-list', range(1, 9999, .01)); + assertType('non-empty-list>', range(1, 9999, 3)); + + assertType('list', range(1, 9999, $floatOrInt)); + assertType('list', range(1, 9999, $floatOrInt)); + + assertType('list', range(1, 3, $i)); + assertType('list', range(1, 3, $f)); + + assertType('list', range(1, 9999, $i)); + assertType('list', range(1, 9999, $f)); +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-11699.php b/tests/PHPStan/Analyser/nsrt/bug-11699.php new file mode 100644 index 0000000000..65eebc78a6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11699.php @@ -0,0 +1,51 @@ +[\~,\?\.])~', $string, $match); + if ($result === 1) { + assertType("','|'.'|'?'|'~'", $match['AB']); + } +} + +function doFoo1():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?[\~,\?.])~', $string, $match); // dot in character class does not need to be escaped + if ($result === 1) { + assertType("','|'.'|'?'|'~'", $match['AB']); + } +} + +function doFoo2():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?.)~', $string, $match); + if ($result === 1) { + assertType("non-empty-string", $match['AB']); + } +} + + +function doFoo3():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?\.)~', $string, $match); + if ($result === 1) { + assertType("'.'", $match['AB']); + } +} + +function doFoo4():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?[^\~,\?\.])~', $string, $match); + if ($result === 1) { + assertType("non-empty-string", $match['AB']); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11703.php b/tests/PHPStan/Analyser/nsrt/bug-11703.php new file mode 100644 index 0000000000..ae1a91127b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11703.php @@ -0,0 +1,99 @@ + 'Some message about the alert.', + 'duration' => $duration, + 'severity' => 100, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert2.', + 'duration' => $duration, + 'severity' => 99, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert3.', + 'duration' => $duration, + 'severity' => 75, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert4.', + 'duration' => $duration, + 'severity' => 60, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert5.', + 'duration' => null, + 'severity' => 25, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert6.', + 'duration' => $duration, + 'severity' => 24, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert7.', + 'duration' => $duration, + 'severity' => 24, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert8.', + 'duration' => $duration, + 'severity' => 24, + ]; + } + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert9.', + 'duration' => $duration, + 'severity' => 24, + ]; + } + + assertType('int<0, 9>', count($alerts)); + + if (mt_rand(0, 1)) { + $alerts[] = [ + 'message' => 'Some message about the alert10.', + 'duration' => $duration, + 'severity' => 23, + ]; + } + + assertType('int<0, max>', count($alerts)); + if (count($alerts) === 0) { + return null; + } + + return true; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11716.php b/tests/PHPStan/Analyser/nsrt/bug-11716.php new file mode 100644 index 0000000000..a2e86ffbec --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11716.php @@ -0,0 +1,236 @@ += 8.0 + +namespace Bug11716; + +use function PHPStan\Testing\assertType; + +class TypeExpression +{ + /** + * @return '&'|'|' + */ + public function parse(string $glue): string + { + $seenGlues = ['|' => false, '&' => false]; + + assertType("array{|: false, &: false}", $seenGlues); + + if ($glue !== '') { + assertType('non-empty-string', $glue); + + \assert(isset($seenGlues[$glue])); + $seenGlues[$glue] = true; + + assertType("'&'|'|'", $glue); + assertType('array{|: bool, &: bool}', $seenGlues); + } else { + assertType("''", $glue); + } + + assertType("''|'&'|'|'", $glue); + assertType("array{|: bool, &: bool}", $seenGlues); + + return array_key_first($seenGlues); + } +} + +/** + * @param array $intKeyedArr + * @param array $stringKeyedArr + */ +function narrowKey($mixed, string $s, int $i, array $generalArr, array $intKeyedArr, array $stringKeyedArr): void { + if (isset($generalArr[$mixed])) { + assertType('mixed~(array|object|resource)', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($generalArr[$i])) { + assertType('int', $i); + } else { + assertType('int', $i); + } + assertType('int', $i); + + if (isset($generalArr[$s])) { + assertType('string', $s); + } else { + assertType('string', $s); + } + assertType('string', $s); + + if (isset($intKeyedArr[$mixed])) { + assertType('mixed~(array|object|resource)', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($intKeyedArr[$i])) { + assertType('int', $i); + } else { + assertType('int', $i); + } + assertType('int', $i); + + if (isset($intKeyedArr[$s])) { + assertType("lowercase-string&numeric-string&uppercase-string", $s); + } else { + assertType('string', $s); + } + assertType('string', $s); + + if (isset($stringKeyedArr[$mixed])) { + assertType('mixed~(array|object|resource)', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($stringKeyedArr[$i])) { + assertType('int', $i); + } else { + assertType('int', $i); + } + assertType('int', $i); + + if (isset($stringKeyedArr[$s])) { + assertType('string', $s); + } else { + assertType('string', $s); + } + assertType('string', $s); +} + +/** + * @param array> $arr + */ +function multiDim($mixed, $mixed2, array $arr) { + if (isset($arr[$mixed])) { + assertType('mixed~(array|object|resource)', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($arr[$mixed]) && isset($arr[$mixed][$mixed2])) { + assertType('mixed~(array|object|resource)', $mixed); + assertType('mixed~(array|object|resource)', $mixed2); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($arr[$mixed][$mixed2])) { + assertType('mixed~(array|object|resource)', $mixed); + assertType('mixed~(array|object|resource)', $mixed2); + } else { + assertType('mixed', $mixed); + assertType('mixed', $mixed2); + } + assertType('mixed', $mixed); + assertType('mixed', $mixed2); +} + +/** + * @param array $arr + */ +function emptyArrr($mixed, array $arr) +{ + if (count($arr) !== 0) { + return; + } + + assertType('array{}', $arr); + if (isset($arr[$mixed])) { + assertType('mixed', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); +} + +function emptyString($mixed) +{ + // see https://3v4l.org/XHZdr + $arr = ['' => 1, 'a' => 2]; + if (isset($arr[$mixed])) { + assertType("''|'a'|null", $mixed); + } else { + assertType('mixed', $mixed); // could be mixed~(''|'a'|null) + } + assertType('mixed', $mixed); +} + +function numericString($mixed, int $i, string $s) +{ + $arr = ['1' => 1, '2' => 2]; + if (isset($arr[$mixed])) { + assertType("1|2|'1'|'2'|float|true", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + $arr = ['0' => 1, '2' => 2]; + if (isset($arr[$mixed])) { + assertType("0|2|'0'|'2'|float|false", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + $arr = ['1' => 1, '2' => 2]; + if (isset($arr[$i])) { + assertType("1|2", $i); + } else { + assertType('int', $i); + } + assertType('int', $i); + + $arr = ['1' => 1, '2' => 2, 3 => 3]; + if (isset($arr[$s])) { + assertType("'1'|'2'|'3'", $s); + } else { + assertType('string', $s); + } + assertType('string', $s); + + $arr = ['1' => 1, '2' => 2, 3 => 3]; + if (isset($arr[substr($s, 10)])) { + assertType("string", $s); + assertType("'1'|'2'|'3'", substr($s, 10)); + } else { + assertType('string', $s); + } + assertType('string', $s); +} + +function intKeys($mixed) +{ + $arr = [1 => 1, 2 => 2]; + if (isset($arr[$mixed])) { + assertType("1|2|'1'|'2'|float|true", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + $arr = [0 => 0, 1 => 1, 2 => 2]; + if (isset($arr[$mixed])) { + assertType("0|1|2|'0'|'1'|'2'|bool|float", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); +} + +function arrayAccess(\ArrayAccess $arr, $mixed) { + if (isset($arr[$mixed])) { + assertType("mixed", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11854.php b/tests/PHPStan/Analyser/nsrt/bug-11854.php new file mode 100644 index 0000000000..48a49258cc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11854.php @@ -0,0 +1,18 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug11861; + +use function PHPStan\Testing\assertType; + +/** + * @template T + * @template K of array-key + * @template R + * + * @param array $source + * @param callable(T, K): R $mappingFunction + * @return array + */ +function mapArray(array $source, callable $mappingFunction): array +{ + $result = []; + + foreach ($source as $key => $value) { + $result[$key] = $mappingFunction($value, $key); + } + + return $result; +} + +/** + * @template K + * @template T + * + * @param array $source + * @return array + */ +function filterArrayNotNull(array $source): array +{ + return array_filter( + $source, + fn($item) => $item !== null, + ARRAY_FILTER_USE_BOTH + ); +} + +/** @var list> $a */ +$a = []; + +$mappedA = mapArray( + $a, + static fn(array $entry) => filterArrayNotNull($entry) +); + +$mappedAWithFirstClassSyntax = mapArray( + $a, + filterArrayNotNull(...) +); + +assertType('array, array>', $mappedA); +assertType('array, array>', $mappedAWithFirstClassSyntax); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11899.php b/tests/PHPStan/Analyser/nsrt/bug-11899.php new file mode 100644 index 0000000000..c56b47dfcb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11899.php @@ -0,0 +1,34 @@ +test); +} + +/** + * @param UserTest $ut + */ +function acceptUserTest2(UserTest $ut) : void { + assertType('Bug11899\\UserTest', $ut); + assertType('Bug11899\\InvertedQuestions|null', $ut->test); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12065.php b/tests/PHPStan/Analyser/nsrt/bug-12065.php new file mode 100644 index 0000000000..e0f9353eec --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12065.php @@ -0,0 +1,43 @@ + $key + * @param bool $preserveKeys + * + * @return void + */ + public function bar2( + string $key, + bool $preserveKeys, + ): void { + $format = $preserveKeys ? '%s' : '%d'; + + $_key = sprintf($format, $key); + assertType("string", $_key); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12077.php b/tests/PHPStan/Analyser/nsrt/bug-12077.php new file mode 100644 index 0000000000..07163ecaa1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12077.php @@ -0,0 +1,11 @@ += 8.3 + +namespace Bug12077; + +use ReflectionMethod; +use function PHPStan\Testing\assertType; + +function (): void { + $methodInfo = ReflectionMethod::createFromMethodName("Exception::getMessage"); + assertType(ReflectionMethod::class, $methodInfo); +}; diff --git a/tests/PHPStan/Analyser/data/bug-1209.php b/tests/PHPStan/Analyser/nsrt/bug-1209.php similarity index 92% rename from tests/PHPStan/Analyser/data/bug-1209.php rename to tests/PHPStan/Analyser/nsrt/bug-1209.php index fff8d13dff..4b4ce770d1 100644 --- a/tests/PHPStan/Analyser/data/bug-1209.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1209.php @@ -13,7 +13,7 @@ public function sayHello($value): void { $isArray = is_array($value); if($isArray){ - assertType('array', $value); + assertType('array', $value); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12107.php b/tests/PHPStan/Analyser/nsrt/bug-12107.php new file mode 100644 index 0000000000..1a2c839c05 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12107.php @@ -0,0 +1,43 @@ + $e2 */ + public function sayHello2(Throwable $e1, string $e2): void + { + if ($e1 instanceof $e2) { + return; + } + + + assertType('Throwable', $e1); + assertType('bool', $e1 instanceof $e2); // could be false + } + + public function sayHello3(Throwable $e1): void + { + if ($e1 instanceof LogicException) { + return; + } + + assertType('Throwable~LogicException', $e1); + assertType('false', $e1 instanceof LogicException); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12126.php b/tests/PHPStan/Analyser/nsrt/bug-12126.php new file mode 100644 index 0000000000..c494d8d60d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12126.php @@ -0,0 +1,36 @@ += 7.4 + +namespace Bug12126; + +use function PHPStan\Testing\assertType; + + +class HelloWorld +{ + public function sayHello(): void + { + $options = ['footest', 'testfoo']; + $key = array_rand($options, 1); + + $regex = '/foo(?Ptest)|test(?Pfoo)/J'; + if (!preg_match_all($regex, $options[$key], $matches, PREG_SET_ORDER)) { + return; + } + + assertType('list>', $matches); + // could be assertType("list", $matches); + if (!preg_match_all($regex, $options[$key], $matches, PREG_PATTERN_ORDER)) { + return; + } + + assertType('array>', $matches); + // could be assertType("array{0: list, test: list<'foo'|'test'>, 1: list<'test'|''>, 2: list<''|'foo'>}", $matches); + + if (!preg_match($regex, $options[$key], $matches)) { + return; + } + + assertType('array', $matches); + // could be assertType("array{0: list, test: 'foo', 1: '', 2: 'foo'}|array{0: list, test: 'test', 1: 'test', 2: ''}", $matches); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-1216.php b/tests/PHPStan/Analyser/nsrt/bug-1216.php similarity index 92% rename from tests/PHPStan/Analyser/data/bug-1216.php rename to tests/PHPStan/Analyser/nsrt/bug-1216.php index 1ccb7d093d..7c0beae95d 100644 --- a/tests/PHPStan/Analyser/data/bug-1216.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1216.php @@ -2,6 +2,7 @@ namespace Bug1216; +use AllowDynamicProperties; use function PHPStan\Testing\assertType; abstract class Foo @@ -27,6 +28,7 @@ trait Bar * @property string $bar * @property string $untypedBar */ +#[AllowDynamicProperties] class Baz extends Foo { diff --git a/tests/PHPStan/Analyser/nsrt/bug-12173.php b/tests/PHPStan/Analyser/nsrt/bug-12173.php new file mode 100644 index 0000000000..e92ce7da4e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12173.php @@ -0,0 +1,19 @@ += 7.4 + +namespace Bug12173; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function parse(string $string): void + { + $regex = '#.*(?(apple|orange)).*#'; + + if (preg_match($regex, $string, $matches) !== 1) { + throw new \Exception('Invalid input'); + } + + assertType("'apple'|'orange'", $matches['fruit']);; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12182.php b/tests/PHPStan/Analyser/nsrt/bug-12182.php new file mode 100644 index 0000000000..5566a2a2da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12182.php @@ -0,0 +1,19 @@ += 8.0 + +namespace Bug12182; + +use ArrayObject; +use function PHPStan\Testing\assertType; + +/** + * @extends ArrayObject + */ +class HelloWorld extends ArrayObject +{ + public function __construct(private int $a = 42) { + } +} + +function (HelloWorld $hw): void { + assertType('array', (array) $hw); +}; diff --git a/tests/PHPStan/Analyser/data/bug-1219.php b/tests/PHPStan/Analyser/nsrt/bug-1219.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1219.php rename to tests/PHPStan/Analyser/nsrt/bug-1219.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12210.php b/tests/PHPStan/Analyser/nsrt/bug-12210.php new file mode 100644 index 0000000000..13cf62ed26 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12210.php @@ -0,0 +1,27 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12210; + +use function PHPStan\Testing\assertType; + +function bug12210a(string $text): void { + assert(preg_match('(((sum|min|max)))', $text, $match) === 1); + assertType("array{non-empty-string, 'max'|'min'|'sum', 'max'|'min'|'sum'}", $match); +} + +function bug12210b(string $text): void { + assert(preg_match('(((sum|min|ma.)))', $text, $match) === 1); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210c(string $text): void { + assert(preg_match('(((su.|min|max)))', $text, $match) === 1); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210d(string $text): void { + assert(preg_match('(((sum|mi.|max)))', $text, $match) === 1); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12211.php b/tests/PHPStan/Analyser/nsrt/bug-12211.php new file mode 100644 index 0000000000..33131edfe8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12211.php @@ -0,0 +1,16 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12211; + +use function PHPStan\Testing\assertType; + +const REGEX = '((m.x))'; + +function foo(string $text): void { + assert(preg_match(REGEX, $text, $match) === 1); + assertType('array{non-falsy-string, non-falsy-string}', $match); +} + + diff --git a/tests/PHPStan/Analyser/nsrt/bug-12242.php b/tests/PHPStan/Analyser/nsrt/bug-12242.php new file mode 100644 index 0000000000..d9335610d3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12242.php @@ -0,0 +1,43 @@ += 7.4 + +namespace Bug12242; + +use function PHPStan\Testing\assertType; + +function foo(string $str): void +{ + $regexp = '/ + # ( + ([\d,]*) + # ) + /x'; + if (preg_match($regexp, $str, $match)) { + assertType('array{string, string}', $match); + } +} + +function bar(string $str): void +{ + $regexp = '/^ + (\w+) # column type [1] + [\(] # ( + ?([\d,]*) # size or size, precision [2] + [\)] # ) + ?\s* # whitespace + (\w*) # extra description (UNSIGNED, CHARACTER SET, ...) [3] + $/x'; + if (preg_match($regexp, $str, $matches)) { + assertType('array{non-falsy-string, non-empty-string, string, string}', $matches); + } +} + +function foobar(string $str): void +{ + $regexp = '/ + # ( + ([\d,]*)# a comment immediately behind with a closing parenthesis ) + /x'; + if (preg_match($regexp, $str, $match)) { + assertType('array{string, string}', $match); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12274.php b/tests/PHPStan/Analyser/nsrt/bug-12274.php new file mode 100644 index 0000000000..437dc09ae3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12274.php @@ -0,0 +1,109 @@ + $items + * + * @return non-empty-list + */ +function getItems(array $items): array +{ + foreach ($items as $index => $item) { + $items[$index] = 1; + } + + assertType('non-empty-list', $items); + return $items; +} + +/** + * @param non-empty-list $items + * + * @return non-empty-list + */ +function getItemsByModifiedIndex(array $items): array +{ + foreach ($items as $index => $item) { + $index++; + + $items[$index] = 1; + } + + assertType('non-empty-array, int>', $items); + return $items; +} + +/** @param list $list */ +function testKeepListAfterIssetIndex(array $list, int $i): void +{ + if (isset($list[$i])) { + assertType('list', $list); + $list[$i] = 21; + assertType('non-empty-list', $list); + $list[$i+1] = 21; + assertType('non-empty-list', $list); + } + assertType('list', $list); +} + +/** @param list> $nestedList */ +function testKeepNestedListAfterIssetIndex(array $nestedList, int $i, int $j): void +{ + if (isset($nestedList[$i][$j])) { + assertType('list>', $nestedList); + assertType('list', $nestedList[$i]); + $nestedList[$i][$j] = 21; + assertType('non-empty-list>', $nestedList); + assertType('non-empty-list', $nestedList[$i]); + } + assertType('list>', $nestedList); +} + +/** @param list $list */ +function testKeepListAfterIssetIndexPlusOne(array $list, int $i): void +{ + if (isset($list[$i])) { + assertType('list', $list); + $list[$i+1] = 21; + assertType('non-empty-list', $list); + } + assertType('list', $list); +} + +/** @param list $list */ +function testKeepListAfterIssetIndexOnePlus(array $list, int $i): void +{ + if (isset($list[$i])) { + assertType('list', $list); + $list[1+$i] = 21; + assertType('non-empty-list', $list); + } + assertType('list', $list); +} + +/** @param list $list */ +function testShouldLooseListbyAst(array $list, int $i): void +{ + if (isset($list[$i])) { + $i++; + + assertType('list', $list); + $list[1+$i] = 21; + assertType('non-empty-array', $list); + } + assertType('array', $list); +} + +/** @param list $list */ +function testShouldLooseListbyAst2(array $list, int $i): void +{ + if (isset($list[$i])) { + assertType('list', $list); + $list[2+$i] = 21; + assertType('non-empty-array', $list); + } + assertType('array', $list); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12297.php b/tests/PHPStan/Analyser/nsrt/bug-12297.php new file mode 100644 index 0000000000..4a956e42a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12297.php @@ -0,0 +1,19 @@ +', $value); + return $value; + } + + assertType('mixed~array', $value); + + if (is_iterable($value)) { + assertType('Traversable', $value); + return iterator_to_array($value); + } + + assertType('mixed~array', $value); + + throw new \LogicException(); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12386.php b/tests/PHPStan/Analyser/nsrt/bug-12386.php new file mode 100644 index 0000000000..59d02b403f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12386.php @@ -0,0 +1,68 @@ +', $landMapper->fetchAllActivePrependDefault(12)); +} + +/** + * @template T of Clx_Model_Abstract + */ +abstract class Clx_Model_Mapper_Abstract +{ + public function __construct() + { + } +} + +/** + * @template T of Application_Model_Land + * + * @extends Clx_Model_Mapper_Abstract + */ +class ClxProductNet_Model_Mapper_Land extends Clx_Model_Mapper_Abstract +{ + /** + * @param int $defaultLandid + * + * @return Clx_Model_Iterator + */ + public function fetchAllActivePrependDefault($defaultLandid): Clx_Model_Iterator + {} +} + +/** + * @template T of Application_Model_Land + * + * @extends ClxProductNet_Model_Mapper_Land + */ +final class Application_Model_Mapper_Land extends ClxProductNet_Model_Mapper_Land +{ +} + +/** + * @template T of Clx_Model_Abstract + * + * @implements \Iterator + */ +abstract class Clx_Model_Iterator implements \Countable, \Iterator +{} + +abstract class Clx_Model_Abstract implements \Stringable +{} + +abstract class ClxProductNet_Model_Land extends Clx_Model_Abstract +{} + +final class Application_Model_Land extends ClxProductNet_Model_Land +{ + public function __toString() + { + return 'foo'; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php new file mode 100644 index 0000000000..b73906fdfd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php @@ -0,0 +1,23 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug12393Php84; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + + +class StringableFoo { + private string $foo; + + // https://3v4l.org/2SPPj#v8.4.6 + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393.php b/tests/PHPStan/Analyser/nsrt/bug-12393.php new file mode 100644 index 0000000000..4edd2300c1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393.php @@ -0,0 +1,184 @@ +name = $plugin["name"]; + assertType('string', $this->name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + $this->untypedName = $plugin["name"]; + assertType('mixed', $this->untypedName); + } + + public function doBar(int $i){ + $this->float = $i; + assertType('float', $this->float); + } + + public function doBaz(int $i){ + $this->untypedFloat = $i; + assertType('int', $this->untypedFloat); + } + + public function doLorem(): void + { + $this->a = ['a' => 1]; + assertType('array{a: 1}', $this->a); + } + + public function doFloatTricky(){ + $this->float = 1; + assertType('1.0', $this->float); + } +} + +class HelloWorldStatic +{ + private static string $name; + + /** @var string */ + private static $untypedName; + + private static float $float; + + /** @var float */ + private static $untypedFloat; + + private static array $a; + + /** + * @param mixed[] $plugin + */ + public function __construct(array $plugin){ + self::$name = $plugin["name"]; + assertType('string', self::$name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + self::$untypedName = $plugin["name"]; + assertType('mixed', self::$untypedName); + } + + public function doBar(int $i){ + self::$float = $i; + assertType('float', self::$float); + } + + public function doBaz(int $i){ + self::$untypedFloat = $i; + assertType('int', self::$untypedFloat); + } + + public function doLorem(): void + { + self::$a = ['a' => 1]; + assertType('array{a: 1}', self::$a); + } +} + +class EntryPointLookup +{ + + /** @var array|null */ + private ?array $entriesData = null; + + /** + * @return array + */ + public function doFoo(): void + { + if ($this->entriesData !== null) { + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + + $data = $this->getMixed(); + if ($data !== null) { + $this->entriesData = $data; + assertType('array', $this->entriesData); + assertNativeType('array', $this->entriesData); + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + } + + /** + * @return mixed + */ + public function getMixed() + { + + } + +} + +// https://3v4l.org/LK6Rh +class CallableString { + private string $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; // PHPStorm wrongly reports an error on this line + assertType('callable-string|non-empty-string', $this->foo); + } +} + +// https://3v4l.org/WJ8NW +class CallableArray { + private array $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; + assertType('array', $this->foo); // could be non-empty-array + } +} + +class StringableFoo { + private string $foo; + + // https://3v4l.org/DQSgA#v8.4.6 + public function doFoo(StringableFoo $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function doFoo2(NotStringable $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php new file mode 100644 index 0000000000..ae1946cdb2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php @@ -0,0 +1,22 @@ += 8.4 + +declare(strict_types = 0); + +namespace Bug12393bPhp84; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class StringableFoo { + private string $foo; + + // https://3v4l.org/nelJF#v8.4.6 + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('non-empty-string&numeric-string', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php new file mode 100644 index 0000000000..7ec8f3012b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -0,0 +1,709 @@ += 8.0 + +declare(strict_types = 0); + +namespace Bug12393b; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + private string $name; + + /** @var string */ + private $untypedName; + + private float $float; + + /** @var float */ + private $untypedFloat; + + private array $a; + + /** + * @param mixed[] $plugin + */ + public function __construct(array $plugin){ + $this->name = $plugin["name"]; + assertType('string', $this->name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + $this->untypedName = $plugin["name"]; + assertType('mixed', $this->untypedName); + } + + public function doBar(int $i){ + $this->float = $i; + assertType('float', $this->float); + } + + public function doBaz(int $i){ + $this->untypedFloat = $i; + assertType('int', $this->untypedFloat); + } + + public function doLorem(): void + { + $this->a = ['a' => 1]; + assertType('array{a: 1}', $this->a); + } + + public function doFloatTricky(){ + $this->float = 1; + assertType('1.0', $this->float); + } +} + +class HelloWorldStatic +{ + private static string $name; + + /** @var string */ + private static $untypedName; + + private static float $float; + + /** @var float */ + private static $untypedFloat; + + private static array $a; + + /** + * @param mixed[] $plugin + */ + public function __construct(array $plugin){ + self::$name = $plugin["name"]; + assertType('string', self::$name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + self::$untypedName = $plugin["name"]; + assertType('mixed', self::$untypedName); + } + + public function doBar(int $i){ + self::$float = $i; + assertType('float', self::$float); + } + + public function doBaz(int $i){ + self::$untypedFloat = $i; + assertType('int', self::$untypedFloat); + } + + public function doLorem(): void + { + self::$a = ['a' => 1]; + assertType('array{a: 1}', self::$a); + } +} + +class EntryPointLookup +{ + + /** @var array|null */ + private ?array $entriesData = null; + + /** + * @return array + */ + public function doFoo(): void + { + if ($this->entriesData !== null) { + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + + $data = $this->getMixed(); + if ($data !== null) { + $this->entriesData = $data; + assertType('array', $this->entriesData); + assertNativeType('array', $this->entriesData); + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + } + + /** + * @return mixed + */ + public function getMixed() + { + + } + +} + +class FooStringInt +{ + + public int $foo; + + public function doFoo(string $s): void + { + $this->foo = $s; + assertType('int', $this->foo); + } + + public function doBar(): void + { + $this->foo = 'foo'; + assertType('*NEVER*', $this->foo); + $this->foo = '123'; + assertType('123', $this->foo); + } + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param numeric-string $numeric + * @param literal-string $literal + * @param lowercase-string $lower + * @param uppercase-string $upper + */ + function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { + $this->foo = $nonEmpty; + assertType('int', $this->foo); + $this->foo = $nonFalsy; + assertType('int|int<1, max>', $this->foo); + $this->foo = $numeric; + assertType('int', $this->foo); + $this->foo = $literal; + assertType('int', $this->foo); + $this->foo = $lower; + assertType('int', $this->foo); + $this->foo = $upper; + assertType('int', $this->foo); + } +} + +class FooStringFloat +{ + + public float $foo; + + public function doFoo(string $s): void + { + $this->foo = $s; + assertType('float', $this->foo); + } + + public function doBar(): void + { + $this->foo = 'foo'; + assertType('*NEVER*', $this->foo); + $this->foo = '123'; + assertType('123.0', $this->foo); + } + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param numeric-string $numeric + * @param literal-string $literal + * @param lowercase-string $lower + * @param uppercase-string $upper + */ + function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { + $this->foo = $nonEmpty; + assertType('float', $this->foo); + $this->foo = $nonFalsy; + assertType('float', $this->foo); + $this->foo = $numeric; + assertType('float', $this->foo); + $this->foo = $literal; + assertType('float', $this->foo); + $this->foo = $lower; + assertType('float', $this->foo); + $this->foo = $upper; + assertType('float', $this->foo); + } +} + +class FooStringBool +{ + + public bool $foo; + + public function doFoo(string $s): void + { + $this->foo = $s; + assertType('bool', $this->foo); + } + + public function doBar(): void + { + $this->foo = '0'; + assertType('false', $this->foo); + $this->foo = 'foo'; + assertType('true', $this->foo); + $this->foo = '123'; + assertType('true', $this->foo); + } + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param numeric-string $numeric + * @param literal-string $literal + * @param lowercase-string $lower + * @param uppercase-string $upper + */ + function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { + $this->foo = $nonEmpty; + assertType('bool', $this->foo); + $this->foo = $nonFalsy; + assertType('true', $this->foo); + $this->foo = $numeric; + assertType('bool', $this->foo); + $this->foo = $literal; + assertType('bool', $this->foo); + $this->foo = $lower; + assertType('bool', $this->foo); + $this->foo = $upper; + assertType('bool', $this->foo); + } +} + +class FooBoolInt +{ + + public int $foo; + + public function doFoo(bool $b): void + { + $this->foo = $b; + assertType('0|1', $this->foo); + } + + public function doBar(): void + { + $this->foo = true; + assertType('1', $this->foo); + $this->foo = false; + assertType('0', $this->foo); + } +} + +class FooVoidInt { + private ?int $foo; + private int $fooNonNull; + + public function doFoo(): void { + $this->foo = $this->returnVoid(); + assertType('null', $this->foo); + + $this->fooNonNull = $this->returnVoid(); + assertType('int|null', $this->foo); // should be *NEVER* + } + + public function returnVoid(): void { + return; + } +} + + +class FooBoolString +{ + + public string $foo; + + public function doFoo(bool $b): void + { + $this->foo = $b; + assertType("''|'1'", $this->foo); + } + + public function doBar(): void + { + $this->foo = true; + assertType("'1'", $this->foo); + $this->foo = false; + assertType("''", $this->foo); + } +} + +class FooIntString +{ + + public string $foo; + + public function doFoo(int $b): void + { + $this->foo = $b; + assertType('lowercase-string&numeric-string&uppercase-string', $this->foo); + } + + public function doBar(): void + { + $this->foo = -1; + assertType("'-1'", $this->foo); + $this->foo = 1; + assertType("'1'", $this->foo); + $this->foo = 0; + assertType("'0'", $this->foo); + } +} + +class FooIntBool +{ + + public bool $foo; + + public function doFoo(int $b): void + { + $this->foo = $b; + assertType('bool', $this->foo); + + if ($b !== 0) { + $this->foo = $b; + assertType('true', $this->foo); + } + if ($b !== 1) { + $this->foo = $b; + assertType('bool', $this->foo); + } + } + + public function doBar(): void + { + $this->foo = -1; + assertType("true", $this->foo); + $this->foo = 1; + assertType("true", $this->foo); + $this->foo = 0; + assertType("false", $this->foo); + } +} + +class FooIntRangeString +{ + + public string $foo; + + /** + * @param int<5, 10> $b + */ + public function doFoo(int $b): void + { + $this->foo = $b; + assertType("'10'|'5'|'6'|'7'|'8'|'9'", $this->foo); + } + + public function doBar(): void + { + $i = rand(5, 10); + $this->foo = $i; + assertType("'10'|'5'|'6'|'7'|'8'|'9'", $this->foo); + } +} + +class FooNullableIntString +{ + + public string $foo; + + public function doFoo(?int $b): void + { + $this->foo = $b; + assertType('lowercase-string&numeric-string&uppercase-string', $this->foo); + } + + public function doBar(): void + { + $this->foo = null; + assertType('*NEVER*', $this->foo); // null cannot be coerced to string, see https://3v4l.org/5k1Dl + } +} + +class FooFloatString +{ + + public string $foo; + + public function doFoo(float $b): void + { + $this->foo = $b; + assertType('numeric-string&uppercase-string', $this->foo); + } + + public function doBar(): void + { + $this->foo = 1.0; + assertType("'1'", $this->foo); + } +} + +class FooStringToUnion +{ + + public int|float $foo; + + public function doFoo(string $b): void + { + $this->foo = $b; + assertType('float|int', $this->foo); + } + + public function doBar(): void + { + $this->foo = "1.0"; + assertType('1|1.0', $this->foo); + } +} + +class FooNumericToString +{ + + public string $foo; + + public function doFoo(float|int $b): void + { + $this->foo = $b; + assertType('numeric-string&uppercase-string', $this->foo); + } + +} + +class FooMixedToInt +{ + + public int $foo; + + public function doFoo(mixed $b): void + { + $this->foo = $b; + assertType('int', $this->foo); + } + +} + + +class FooArrayToInt +{ + public int $foo; + + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-list $list + */ + public function doBaz(array $list): void + { + $this->foo = $list; + assertType('*NEVER*', $this->foo); + } +} + +class FooArrayToFloat +{ + public float $foo; + + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-list $list + */ + public function doBaz(array $list): void + { + $this->foo = $list; + assertType('*NEVER*', $this->foo); + } +} + +class FooArrayToString +{ + public string $foo; + + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-list $list + */ + public function doBaz(array $list): void + { + $this->foo = $list; + assertType('*NEVER*', $this->foo); + } +} + +class FooArray +{ + public array $foo; + + /** + * @param non-empty-array $arr + */ + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('non-empty-array', $this->foo); + + if (array_key_exists('foo', $arr)) { + $this->foo = $arr; + assertType("non-empty-array&hasOffset('foo')", $this->foo); + } + + if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { + $this->foo = $arr; + assertType("non-empty-array&hasOffsetValue('foo', 'bar')", $this->foo); + } + } +} + +class FooTypedArray +{ + /** + * @var array + */ + public array $foo; + + /** + * @param array $arr + */ + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('array', $this->foo); + } + + /** + * @param array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('array', $this->foo); + } +} + +class FooList +{ + public array $foo; + + /** + * @param non-empty-list $list + */ + public function doFoo(array $list): void + { + $this->foo = $list; + assertType('non-empty-list', $this->foo); + + if (array_key_exists(3, $list)) { + $this->foo = $list; + assertType("non-empty-list&hasOffset(3)", $this->foo); + } + + if (array_key_exists(3, $list) && is_string($list[3])) { + $this->foo = $list; + assertType("non-empty-list&hasOffsetValue(3, string)", $this->foo); + } + } + +} + +// https://3v4l.org/LJiRB +class CallableString { + private string $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; + assertType('callable-string|non-empty-string', $this->foo); + } +} + +// https://3v4l.org/VvUsp +class CallableArray { + private array $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; + assertType('array', $this->foo); // could be non-empty-array + } +} + +class StringableFoo { + private string $foo; + + public function doFoo(StringableFoo $foo): void { + $this->foo = $foo; + assertType('string', $this->foo); + } + + public function doFoo2(NotStringable $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} + +final class NotStringable {} + +class ObjectWithToStringMethod { + private string $foo; + + public function doFoo(object $foo): void { + if (method_exists($foo, '__toString')) { + $this->foo = $foo; + assertType('string', $this->foo); + } + } + public function __toString(): string { + return 'Foo'; + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-12398.php b/tests/PHPStan/Analyser/nsrt/bug-12398.php new file mode 100644 index 0000000000..b89a699dd3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12398.php @@ -0,0 +1,26 @@ +$a); + } + +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-12473-types.php b/tests/PHPStan/Analyser/nsrt/bug-12473-types.php new file mode 100644 index 0000000000..d1f7a0a9cc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12473-types.php @@ -0,0 +1,48 @@ += 8.4 + +namespace Bug12473Types; + +use ReflectionClass; +use function PHPStan\Testing\assertType; + +class Picture +{ +} + +class PictureUser extends Picture +{ +} + +class PictureProduct extends Picture +{ +} + +/** + * @param class-string $a + */ +function doFoo(string $a): void +{ + $r = new ReflectionClass($a); + assertType('ReflectionClass', $r); + if ($r->isSubclassOf(Picture::class)) { + assertType('ReflectionClass', $r); + } else { + assertType('ReflectionClass', $r); + } + assertType('ReflectionClass|ReflectionClass', $r); +} + +/** + * @param class-string $a + */ +function doFoo2(string $a): void +{ + $r = new ReflectionClass($a); + assertType('ReflectionClass', $r); + if ($r->isSubclassOf(Picture::class)) { + assertType('ReflectionClass', $r); + } else { + assertType('*NEVER*', $r); + } + assertType('ReflectionClass', $r); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12575.php b/tests/PHPStan/Analyser/nsrt/bug-12575.php new file mode 100644 index 0000000000..f5199523d4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12575.php @@ -0,0 +1,85 @@ + $class + * @return $this + * @phpstan-self-out static + */ + public function add(string $class) + { + return $this; + } + +} + +/** + * @template T of object + * @extends Foo + */ +class Bar extends Foo +{ + + public function doFoo(): void + { + assertType('$this(Bug12575\Bar)&static(Bug12575\Bar)', $this->add(A::class)); + assertType('$this(Bug12575\Bar)&static(Bug12575\Bar)', $this); + assertType('T of object (class Bug12575\Bar, argument)', $this->getT()); + } + + public function doBar(): void + { + $this->add(B::class); + assertType('$this(Bug12575\Bar)&static(Bug12575\Bar)', $this); + assertType('T of object (class Bug12575\Bar, argument)', $this->getT()); + } + + /** + * @return T + */ + public function getT() + { + + } + +} + +interface A +{ + +} + +interface B +{ + +} + +/** + * @param Bar $bar + * @return void + */ +function doFoo(Bar $bar): void { + assertType('Bug12575\\Bar', $bar->add(B::class)); + assertType('Bug12575\\Bar', $bar); + assertType('Bug12575\A&Bug12575\B', $bar->getT()); +}; + +/** + * @param Bar $bar + * @return void + */ +function doBar(Bar $bar): void { + $bar->add(B::class); + assertType('Bug12575\\Bar', $bar); + assertType('Bug12575\A&Bug12575\B', $bar->getT()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12660.php b/tests/PHPStan/Analyser/nsrt/bug-12660.php new file mode 100644 index 0000000000..44c79a0444 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12660.php @@ -0,0 +1,21 @@ + + */ + public function map($callable): self + { + return new self($callable($this->value)); + } + + /** + * @template S + * @param Closure(T): S $callable + * @return self + */ + public function mapClosure($callable): self + { + return new self($callable($this->value)); + } +} + +/** + * @param Option> $ints + */ +function doFoo(Option $ints): void { + assertType('Bug12691\\Option>', $ints->map(array_values(...))); + assertType('Bug12691\\Option>', $ints->map('array_values')); + assertType('Bug12691\\Option>', $ints->map(static fn ($value) => array_values($value))); +}; + +/** + * @param Option> $ints + */ +function doFooClosure(Option $ints): void { + assertType('Bug12691\\Option>', $ints->mapClosure(array_values(...))); + assertType('Bug12691\\Option>', $ints->mapClosure(static fn ($value) => array_values($value))); +}; + +/** + * @template T + * @param array $a + * @return ($a is non-empty-array ? non-empty-list : list) + */ +function myArrayValues(array $a): array { + +} + +/** + * @param Option> $ints + */ +function doBar(Option $ints): void { + assertType('Bug12691\\Option>', $ints->mapClosure(myArrayValues(...))); + assertType('Bug12691\\Option>', $ints->mapClosure(static fn ($value) => myArrayValues($value))); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12731.php b/tests/PHPStan/Analyser/nsrt/bug-12731.php new file mode 100644 index 0000000000..79c8160461 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12731.php @@ -0,0 +1,24 @@ +', max(4, pure_int())); + +$_ = impure_int(); +assertType('int<4, max>', max(4, $_)); + +assertType('int<4, max>', max(4, impure_int())); +assertType('int<4, max>', max(impure_int(), 4)); +assertType('int', impure_int()); diff --git a/tests/PHPStan/Analyser/data/bug-1283.php b/tests/PHPStan/Analyser/nsrt/bug-1283.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1283.php rename to tests/PHPStan/Analyser/nsrt/bug-1283.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12866.php b/tests/PHPStan/Analyser/nsrt/bug-12866.php new file mode 100644 index 0000000000..4360d8bdd1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12866.php @@ -0,0 +1,65 @@ += 8.0 + +namespace Bug12866; + +use function PHPStan\Testing\assertType; + +interface I +{ + /** + * @phpstan-assert-if-true A $this + */ + public function isA(): bool; +} + +class A implements I +{ + public function isA(): bool + { + return true; + } +} + +class B implements I +{ + public function isA(): bool + { + return false; + } +} + +function takesI(I $i): void +{ + if (!$i->isA()) { + return; + } + + assertType('Bug12866\\A', $i); +} + +function takesIStrictComparison(I $i): void +{ + if ($i->isA() !== true) { + return; + } + + assertType('Bug12866\\A', $i); +} + +function takesNullableI(?I $i): void +{ + if (!$i?->isA()) { + return; + } + + assertType('Bug12866\\A', $i); +} + +function takesNullableIStrictComparison(?I $i): void +{ + if ($i?->isA() !== true) { + return; + } + + assertType('Bug12866\\A', $i); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12891.php b/tests/PHPStan/Analyser/nsrt/bug-12891.php new file mode 100644 index 0000000000..a932a97491 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12891.php @@ -0,0 +1,22 @@ + */ + private iterable $builders; + + /** + * @param iterable $builders + */ + public function __construct(iterable $builders) { + $this->builders = $builders; + assertType('iterable<(int|string), string>', $builders); + assertType('iterable<(int|string), string>', $this->builders); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php new file mode 100644 index 0000000000..33f8a11e26 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php @@ -0,0 +1,90 @@ += 8.1 + +declare(strict_types = 0); + +namespace Bug12902NonStrict; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class NarrowsNativeConstantValue +{ + private readonly int|float $i; + + public function __construct() + { + $this->i = 1; + } + + public function doFoo(): void + { + assertType('1', $this->i); + assertNativeType('1', $this->i); + } +} + +class NarrowsNativeReadonlyUnion { + private readonly int|float $i; + + public function __construct() + { + $this->i = getInt(); + assertType('int', $this->i); + assertNativeType('int', $this->i); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +class NarrowsNativeUnion { + private int|float $i; + + public function __construct() + { + $this->i = getInt(); + assertType('int', $this->i); + assertNativeType('int', $this->i); + + $this->impureCall(); + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } + + public function doFoo(): void { + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } + + /** @phpstan-impure */ + public function impureCall(): void {} +} + +class NarrowsStaticNativeUnion { + private static int|float $i; + + public function __construct() + { + self::$i = getInt(); + assertType('int', self::$i); + assertNativeType('int', self::$i); + + $this->impureCall(); + assertType('float|int', self::$i); + assertNativeType('float|int', self::$i); + } + + public function doFoo(): void { + assertType('float|int', self::$i); + assertNativeType('float|int', self::$i); + } + + /** @phpstan-impure */ + public function impureCall(): void {} +} + +function getInt(): int { + return 1; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php new file mode 100644 index 0000000000..2330c0c130 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -0,0 +1,117 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug12902; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class NarrowsNativeConstantValue +{ + private readonly int|float $i; + + public function __construct() + { + $this->i = 1; + } + + public function doFoo(): void + { + assertType('1', $this->i); + assertNativeType('1', $this->i); + } +} + +class NarrowsNativeReadonlyUnion { + private readonly int|float $i; + + public function __construct() + { + $this->i = getInt(); + assertType('int', $this->i); + assertNativeType('int', $this->i); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +class NarrowsNativeUnion { + private int|float $i; + + public function __construct() + { + $this->i = getInt(); + assertType('int', $this->i); + assertNativeType('int', $this->i); + + $this->impureCall(); + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } + + public function doFoo(): void { + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } + + /** @phpstan-impure */ + public function impureCall(): void {} +} + +class NarrowsStaticNativeUnion { + private static int|float $i; + + public function __construct() + { + self::$i = getInt(); + assertType('int', self::$i); + assertNativeType('int', self::$i); + + $this->impureCall(); + assertType('float|int', self::$i); + assertNativeType('float|int', self::$i); + } + + public function doFoo(): void { + assertType('float|int', self::$i); + assertNativeType('float|int', self::$i); + } + + /** @phpstan-impure */ + public function impureCall(): void {} +} + +class BaseClass +{ + static protected int|float $i; +} + +class UsesBaseClass extends BaseClass +{ + public function __construct() + { + parent::$i = getInt(); + assertType('int', parent::$i); + assertNativeType('int', parent::$i); + + $this->impureCall(); + assertType('float|int', parent::$i); + assertNativeType('float|int', parent::$i); + } + + public function doFoo(): void { + assertType('float|int', parent::$i); + assertNativeType('float|int', parent::$i); + } + + /** @phpstan-impure */ + public function impureCall(): void {} +} + +function getInt(): int { + return 1; +} diff --git a/tests/PHPStan/Analyser/data/bug-1511.php b/tests/PHPStan/Analyser/nsrt/bug-1511.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1511.php rename to tests/PHPStan/Analyser/nsrt/bug-1511.php diff --git a/tests/PHPStan/Analyser/data/bug-1516.php b/tests/PHPStan/Analyser/nsrt/bug-1516.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1516.php rename to tests/PHPStan/Analyser/nsrt/bug-1516.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-1519.php b/tests/PHPStan/Analyser/nsrt/bug-1519.php new file mode 100644 index 0000000000..7d01391c63 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1519.php @@ -0,0 +1,22 @@ + + */ + static function (): Generator { + yield true => true; + yield false => false; + }; + +$iterator = new CachingIterator($generator(), CachingIterator::FULL_CACHE); +$cache = $iterator->getCache(); +assertType('array', $cache); diff --git a/tests/PHPStan/Analyser/data/bug-1597.php b/tests/PHPStan/Analyser/nsrt/bug-1597.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1597.php rename to tests/PHPStan/Analyser/nsrt/bug-1597.php diff --git a/tests/PHPStan/Analyser/data/bug-1657.php b/tests/PHPStan/Analyser/nsrt/bug-1657.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1657.php rename to tests/PHPStan/Analyser/nsrt/bug-1657.php diff --git a/tests/PHPStan/Analyser/data/bug-1670.php b/tests/PHPStan/Analyser/nsrt/bug-1670.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1670.php rename to tests/PHPStan/Analyser/nsrt/bug-1670.php diff --git a/tests/PHPStan/Analyser/data/bug-1801.php b/tests/PHPStan/Analyser/nsrt/bug-1801.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1801.php rename to tests/PHPStan/Analyser/nsrt/bug-1801.php diff --git a/tests/PHPStan/Analyser/data/bug-1861.php b/tests/PHPStan/Analyser/nsrt/bug-1861.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1861.php rename to tests/PHPStan/Analyser/nsrt/bug-1861.php diff --git a/tests/PHPStan/Analyser/data/bug-1865.php b/tests/PHPStan/Analyser/nsrt/bug-1865.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1865.php rename to tests/PHPStan/Analyser/nsrt/bug-1865.php diff --git a/tests/PHPStan/Analyser/data/bug-1870.php b/tests/PHPStan/Analyser/nsrt/bug-1870.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1870.php rename to tests/PHPStan/Analyser/nsrt/bug-1870.php diff --git a/tests/PHPStan/Analyser/data/bug-1897.php b/tests/PHPStan/Analyser/nsrt/bug-1897.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1897.php rename to tests/PHPStan/Analyser/nsrt/bug-1897.php diff --git a/tests/PHPStan/Analyser/data/bug-1924.php b/tests/PHPStan/Analyser/nsrt/bug-1924.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1924.php rename to tests/PHPStan/Analyser/nsrt/bug-1924.php diff --git a/tests/PHPStan/Analyser/data/bug-1945.php b/tests/PHPStan/Analyser/nsrt/bug-1945.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-1945.php rename to tests/PHPStan/Analyser/nsrt/bug-1945.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2001.php b/tests/PHPStan/Analyser/nsrt/bug-2001.php new file mode 100644 index 0000000000..69d429d8bd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2001.php @@ -0,0 +1,51 @@ +, 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<0, 65535>, 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<0, 65535>, 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<0, 65535>, 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<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment: string}', $parsedUrl); + $redirectUrl .= '#' . $parsedUrl['query']; + } + + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl); + + return $redirectUrl; + } + + public function doFoo(int $i) + { + $a = ['a' => $i]; + if (rand(0, 1)) { + $a['b'] = $i; + } + + if (rand(0,1)) { + $a = ['d' => $i]; + } + + assertType('array{a: int, b?: int}|array{d: int}', $a); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2003.php b/tests/PHPStan/Analyser/nsrt/bug-2003.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2003.php rename to tests/PHPStan/Analyser/nsrt/bug-2003.php diff --git a/tests/PHPStan/Analyser/data/bug-2112.php b/tests/PHPStan/Analyser/nsrt/bug-2112.php similarity index 78% rename from tests/PHPStan/Analyser/data/bug-2112.php rename to tests/PHPStan/Analyser/nsrt/bug-2112.php index 756c061b5e..65634c415b 100644 --- a/tests/PHPStan/Analyser/data/bug-2112.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2112.php @@ -19,7 +19,7 @@ public function doBar(): void $foos[0] = null; assertType('null', $foos[0]); - assertType('non-empty-array', $foos); + assertType('non-empty-array&hasOffsetValue(0, null)', $foos); } /** @return self[] */ @@ -35,7 +35,7 @@ public function doBars(): void $foos[0] = null; assertType('null', $foos[0]); - assertType('non-empty-array', $foos); + assertType('non-empty-array&hasOffsetValue(0, null)', $foos); } } diff --git a/tests/PHPStan/Analyser/data/bug-2142.php b/tests/PHPStan/Analyser/nsrt/bug-2142.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2142.php rename to tests/PHPStan/Analyser/nsrt/bug-2142.php diff --git a/tests/PHPStan/Analyser/data/bug-2231.php b/tests/PHPStan/Analyser/nsrt/bug-2231.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2231.php rename to tests/PHPStan/Analyser/nsrt/bug-2231.php diff --git a/tests/PHPStan/Analyser/data/bug-2232.php b/tests/PHPStan/Analyser/nsrt/bug-2232.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2232.php rename to tests/PHPStan/Analyser/nsrt/bug-2232.php diff --git a/tests/PHPStan/Analyser/data/bug-2288.php b/tests/PHPStan/Analyser/nsrt/bug-2288.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2288.php rename to tests/PHPStan/Analyser/nsrt/bug-2288.php diff --git a/tests/PHPStan/Analyser/data/bug-2378.php b/tests/PHPStan/Analyser/nsrt/bug-2378.php similarity index 86% rename from tests/PHPStan/Analyser/data/bug-2378.php rename to tests/PHPStan/Analyser/nsrt/bug-2378.php index d5984a6364..a05de0f302 100644 --- a/tests/PHPStan/Analyser/data/bug-2378.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2378.php @@ -17,7 +17,7 @@ public function doFoo( assertType('array{\'a\', \'b\', \'c\', \'d\'}', range('a', 'd')); assertType('array{\'a\', \'c\', \'e\', \'g\', \'i\'}', range('a', 'i', 2)); - assertType('array', range($s, $s)); + assertType('list', range($s, $s)); } } diff --git a/tests/PHPStan/Analyser/data/bug-2413.php b/tests/PHPStan/Analyser/nsrt/bug-2413.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2413.php rename to tests/PHPStan/Analyser/nsrt/bug-2413.php diff --git a/tests/PHPStan/Analyser/data/bug-2420.php b/tests/PHPStan/Analyser/nsrt/bug-2420.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2420.php rename to tests/PHPStan/Analyser/nsrt/bug-2420.php diff --git a/tests/PHPStan/Analyser/data/bug-2443.php b/tests/PHPStan/Analyser/nsrt/bug-2443.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2443.php rename to tests/PHPStan/Analyser/nsrt/bug-2443.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2471.php b/tests/PHPStan/Analyser/nsrt/bug-2471.php new file mode 100644 index 0000000000..6b405cfb66 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2471.php @@ -0,0 +1,28 @@ +doFoo(); + + $x = array_fill_keys($y, null); + + assertType('array', $x); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2539.php b/tests/PHPStan/Analyser/nsrt/bug-2539.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2539.php rename to tests/PHPStan/Analyser/nsrt/bug-2539.php diff --git a/tests/PHPStan/Analyser/data/bug-2549.php b/tests/PHPStan/Analyser/nsrt/bug-2549.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2549.php rename to tests/PHPStan/Analyser/nsrt/bug-2549.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2580.php b/tests/PHPStan/Analyser/nsrt/bug-2580.php new file mode 100644 index 0000000000..98d5a8160c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2580.php @@ -0,0 +1,16 @@ + $typeName + * @param mixed $value + */ +function cast($value, string $typeName): void { + if (is_object($value) && get_class($value) === $typeName) { + assertType('T of object (function Bug2580\cast(), argument)', $value); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php b/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php new file mode 100644 index 0000000000..bf13358857 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php @@ -0,0 +1,26 @@ + 7.4 + +namespace Bug2600PhpVersionScope; + +use function PHPStan\Testing\assertType; + +if (PHP_VERSION_ID >= 80000) { + class Foo8 { + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('array', $x); + } + } +} else { + class Foo9 { + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('list', $x); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-2600-php8.php b/tests/PHPStan/Analyser/nsrt/bug-2600-php8.php similarity index 89% rename from tests/PHPStan/Analyser/data/bug-2600-php8.php rename to tests/PHPStan/Analyser/nsrt/bug-2600-php8.php index 5df2b73736..4dd75b4250 100644 --- a/tests/PHPStan/Analyser/data/bug-2600-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2600-php8.php @@ -1,6 +1,6 @@ -= 8.0 -namespace Bug2600; +namespace Bug2600Php8; use function PHPStan\Testing\assertType; @@ -12,7 +12,7 @@ class Foo public function doFoo($x = null) { $args = func_get_args(); assertType('mixed', $x); - assertType('array', $args); + assertType('list', $args); } /** @@ -39,7 +39,7 @@ public function doLorem(...$x) { public function doIpsum($x = null) { $args = func_get_args(); assertType('mixed', $x); - assertType('array', $args); + assertType('list', $args); } } @@ -51,7 +51,7 @@ class Bar public function doFoo($x = null) { $args = func_get_args(); assertType('string|null', $x); - assertType('array', $args); + assertType('list', $args); } /** diff --git a/tests/PHPStan/Analyser/data/bug-2600.php b/tests/PHPStan/Analyser/nsrt/bug-2600.php similarity index 76% rename from tests/PHPStan/Analyser/data/bug-2600.php rename to tests/PHPStan/Analyser/nsrt/bug-2600.php index 07ca71fd77..9ab5e49598 100644 --- a/tests/PHPStan/Analyser/data/bug-2600.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2600.php @@ -1,4 +1,4 @@ -', $args); } /** @@ -26,20 +26,20 @@ public function doBar($x = null) { * @param mixed $x */ public function doBaz(...$x) { - assertType('array', $x); + assertType('list', $x); } /** * @param mixed ...$x */ public function doLorem(...$x) { - assertType('array', $x); + assertType('list', $x); } public function doIpsum($x = null) { $args = func_get_args(); assertType('mixed', $x); - assertType('array', $args); + assertType('list', $args); } } @@ -51,7 +51,7 @@ class Bar public function doFoo($x = null) { $args = func_get_args(); assertType('string|null', $x); - assertType('array', $args); + assertType('list', $args); } /** @@ -65,24 +65,24 @@ public function doBar($x = null) { * @param string $x */ public function doBaz(...$x) { - assertType('array', $x); + assertType('list', $x); } /** * @param string ...$x */ public function doLorem(...$x) { - assertType('array', $x); + assertType('list', $x); } } function foo($x, string ...$y): void { assertType('mixed', $x); - assertType('array', $y); + assertType('list', $y); } function ($x, string ...$y): void { assertType('mixed', $x); - assertType('array', $y); + assertType('list', $y); }; diff --git a/tests/PHPStan/Analyser/data/bug-2611.php b/tests/PHPStan/Analyser/nsrt/bug-2611.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2611.php rename to tests/PHPStan/Analyser/nsrt/bug-2611.php diff --git a/tests/PHPStan/Analyser/data/bug-2612.php b/tests/PHPStan/Analyser/nsrt/bug-2612.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2612.php rename to tests/PHPStan/Analyser/nsrt/bug-2612.php diff --git a/tests/PHPStan/Analyser/data/bug-2640.php b/tests/PHPStan/Analyser/nsrt/bug-2640.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2640.php rename to tests/PHPStan/Analyser/nsrt/bug-2640.php diff --git a/tests/PHPStan/Analyser/data/bug-2648.php b/tests/PHPStan/Analyser/nsrt/bug-2648.php similarity index 94% rename from tests/PHPStan/Analyser/data/bug-2648.php rename to tests/PHPStan/Analyser/nsrt/bug-2648.php index 11087dfbaa..9acaa05026 100644 --- a/tests/PHPStan/Analyser/data/bug-2648.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2648.php @@ -15,7 +15,7 @@ public function doFoo(array $list): void if (count($list) > 1) { assertType('int<2, max>', count($list)); unset($list['fooo']); - assertType('array', $list); + assertType("array", $list); assertType('int<0, max>', count($list)); } } diff --git a/tests/PHPStan/Analyser/data/bug-2676.php b/tests/PHPStan/Analyser/nsrt/bug-2676.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2676.php rename to tests/PHPStan/Analyser/nsrt/bug-2676.php diff --git a/tests/PHPStan/Analyser/data/bug-2677.php b/tests/PHPStan/Analyser/nsrt/bug-2677.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2677.php rename to tests/PHPStan/Analyser/nsrt/bug-2677.php diff --git a/tests/PHPStan/Analyser/data/bug-2718.php b/tests/PHPStan/Analyser/nsrt/bug-2718.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2718.php rename to tests/PHPStan/Analyser/nsrt/bug-2718.php diff --git a/tests/PHPStan/Analyser/data/bug-2733.php b/tests/PHPStan/Analyser/nsrt/bug-2733.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2733.php rename to tests/PHPStan/Analyser/nsrt/bug-2733.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2735.php b/tests/PHPStan/Analyser/nsrt/bug-2735.php new file mode 100644 index 0000000000..a486eda5c0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2735.php @@ -0,0 +1,134 @@ + */ + protected $arr = []; + + /** + * @param array $arr + */ + public function __construct(array $arr) { + $this->arr = $arr; + } + + /** + * @return T + */ + public function last() + { + if (!$this->arr) { + throw new \Exception('bad'); + } + return end($this->arr); + } +} + +/** + * @template T + * @extends Collection + */ +class CollectionChild extends Collection { +} + +$dogs = new CollectionChild([new Dog(), new Dog()]); +assertType('Bug2735\\CollectionChild', $dogs); + +/** + * @template X + * @template Y + */ +class ParentWithConstructor +{ + + /** + * @param X $x + * @param Y $y + */ + public function __construct($x, $y) + { + } + +} + +/** + * @template T + * @extends ParentWithConstructor + */ +class ChildOne extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildOne(1, new Dog()); + assertType('Bug2735\\ChildOne', $a); +}; + +/** + * @template T + * @extends ParentWithConstructor + */ +class ChildTwo extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildTwo(new Cat(), 2); + assertType('Bug2735\\ChildTwo', $a); +}; + +/** + * @template T + * @extends ParentWithConstructor + */ +class ChildThree extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildThree(new Cat(), new Dog()); + assertType('Bug2735\\ChildThree', $a); +}; + +/** + * @template T + * @template U + * @extends ParentWithConstructor + */ +class ChildFour extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildFour(new Cat(), new Dog()); + assertType('Bug2735\\ChildFour', $a); +}; + +/** + * @template T + * @template U + * @extends ParentWithConstructor + */ +class ChildFive extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildFive(new Cat(), new Dog()); + assertType('Bug2735\\ChildFive', $a); +}; diff --git a/tests/PHPStan/Analyser/data/bug-2740.php b/tests/PHPStan/Analyser/nsrt/bug-2740.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2740.php rename to tests/PHPStan/Analyser/nsrt/bug-2740.php diff --git a/tests/PHPStan/Analyser/data/bug-2750.php b/tests/PHPStan/Analyser/nsrt/bug-2750.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2750.php rename to tests/PHPStan/Analyser/nsrt/bug-2750.php diff --git a/tests/PHPStan/Analyser/data/bug-2760.php b/tests/PHPStan/Analyser/nsrt/bug-2760.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2760.php rename to tests/PHPStan/Analyser/nsrt/bug-2760.php diff --git a/tests/PHPStan/Analyser/data/bug-2806.php b/tests/PHPStan/Analyser/nsrt/bug-2806.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2806.php rename to tests/PHPStan/Analyser/nsrt/bug-2806.php diff --git a/tests/PHPStan/Analyser/data/bug-2816-2.php b/tests/PHPStan/Analyser/nsrt/bug-2816-2.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2816-2.php rename to tests/PHPStan/Analyser/nsrt/bug-2816-2.php diff --git a/tests/PHPStan/Analyser/data/bug-2816.php b/tests/PHPStan/Analyser/nsrt/bug-2816.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2816.php rename to tests/PHPStan/Analyser/nsrt/bug-2816.php diff --git a/tests/PHPStan/Analyser/data/bug-2822.php b/tests/PHPStan/Analyser/nsrt/bug-2822.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2822.php rename to tests/PHPStan/Analyser/nsrt/bug-2822.php diff --git a/tests/PHPStan/Analyser/data/bug-2835.php b/tests/PHPStan/Analyser/nsrt/bug-2835.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2835.php rename to tests/PHPStan/Analyser/nsrt/bug-2835.php diff --git a/tests/PHPStan/Analyser/data/bug-2850.php b/tests/PHPStan/Analyser/nsrt/bug-2850.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2850.php rename to tests/PHPStan/Analyser/nsrt/bug-2850.php diff --git a/tests/PHPStan/Analyser/data/bug-2863.php b/tests/PHPStan/Analyser/nsrt/bug-2863.php similarity index 84% rename from tests/PHPStan/Analyser/data/bug-2863.php rename to tests/PHPStan/Analyser/nsrt/bug-2863.php index 5f70c4795a..1e81b90d1d 100644 --- a/tests/PHPStan/Analyser/data/bug-2863.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2863.php @@ -5,7 +5,7 @@ use function PHPStan\Testing\assertType; $result = json_decode('{"a":5}'); -assertType('int', json_last_error()); +assertType('0|1|2|3|4|5|6|7|8|9|10', json_last_error()); assertType('string', json_last_error_msg()); if (json_last_error() !== JSON_ERROR_NONE || json_last_error_msg() !== 'No error') { @@ -17,7 +17,7 @@ // $result2 = json_decode(''); -assertType('int', json_last_error()); +assertType('0|1|2|3|4|5|6|7|8|9|10', json_last_error()); assertType('string', json_last_error_msg()); if (json_last_error() !== JSON_ERROR_NONE || json_last_error_msg() !== 'No error') { @@ -29,7 +29,7 @@ // $result3 = json_encode([]); -assertType('int', json_last_error()); +assertType('0|1|2|3|4|5|6|7|8|9|10', json_last_error()); assertType('string', json_last_error_msg()); if (json_last_error() !== JSON_ERROR_NONE || json_last_error_msg() !== 'No error') { diff --git a/tests/PHPStan/Analyser/data/bug-2869.php b/tests/PHPStan/Analyser/nsrt/bug-2869.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2869.php rename to tests/PHPStan/Analyser/nsrt/bug-2869.php diff --git a/tests/PHPStan/Analyser/data/bug-2899.php b/tests/PHPStan/Analyser/nsrt/bug-2899.php similarity index 83% rename from tests/PHPStan/Analyser/data/bug-2899.php rename to tests/PHPStan/Analyser/nsrt/bug-2899.php index f027ff8ae4..557f95cc96 100644 --- a/tests/PHPStan/Analyser/data/bug-2899.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2899.php @@ -10,7 +10,7 @@ class Foo public function doFoo(string $s, $mixed) { assertType('numeric-string', date('Y')); - assertType('non-empty-string', date('Y.m.d')); + assertType('non-falsy-string', date('Y.m.d')); assertType('string', date($s)); assertType('string', date($mixed)); } diff --git a/tests/PHPStan/Analyser/data/bug-2906.php b/tests/PHPStan/Analyser/nsrt/bug-2906.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2906.php rename to tests/PHPStan/Analyser/nsrt/bug-2906.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2911.php b/tests/PHPStan/Analyser/nsrt/bug-2911.php new file mode 100644 index 0000000000..3cfbd308f1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2911.php @@ -0,0 +1,139 @@ + + */ + public function getMutatorSettings(): array + { + return []; + } +} + +final class ArrayItemRemoval +{ + private const DEFAULT_SETTINGS = [ + 'remove' => 'first', + 'limit' => PHP_INT_MAX, + ]; + + /** + * @var string first|last|all + */ + private $remove; + + /** + * @var int + */ + private $limit; + + public function __construct(MutatorConfig $config) + { + $settings = $this->getResultSettings($config->getMutatorSettings()); + + $this->remove = $settings['remove']; + $this->limit = $settings['limit']; + } + + /** + * @param array $settings + * + * @return array{remove: string, limit: int} + */ + private function getResultSettings(array $settings): array + { + $settings = array_merge(self::DEFAULT_SETTINGS, $settings); + assertType('non-empty-array', $settings); + + if (!is_string($settings['remove'])) { + throw $this->configException($settings, 'remove'); + } + + assertType("non-empty-array&hasOffsetValue('remove', string)", $settings); + + $settings['remove'] = strtolower($settings['remove']); + + assertType("non-empty-array&hasOffsetValue('remove', lowercase-string)", $settings); + + if (!in_array($settings['remove'], ['first', 'last', 'all'], true)) { + throw $this->configException($settings, 'remove'); + } + + assertType("non-empty-array&hasOffsetValue('remove', 'all'|'first'|'last')", $settings); + + if (!is_numeric($settings['limit']) || $settings['limit'] < 1) { + throw $this->configException($settings, 'limit'); + } + assertType("non-empty-array&hasOffsetValue('limit', float|int<1, max>|numeric-string)&hasOffsetValue('remove', 'all'|'first'|'last')", $settings); + + $settings['limit'] = (int) $settings['limit']; + + assertType("non-empty-array&hasOffsetValue('limit', int)&hasOffsetValue('remove', 'all'|'first'|'last')", $settings); + + return $settings; + } + + /** + * @param array $settings + */ + private function configException(array $settings, string $property): Exception + { + $value = $settings[$property]; + + return new Exception(sprintf( + 'Invalid configuration of ArrayItemRemoval mutator. Setting `%s` is invalid (%s)', + $property, + is_scalar($value) ? $value : '<' . strtoupper(gettype($value)) . '>' + )); + } +} + +final class ArrayItemRemoval2 +{ + private const DEFAULT_SETTINGS = [ + 'remove' => 'first', + 'limit' => PHP_INT_MAX, + ]; + + /** + * @param array $settings + * + * @return array{remove: string, limit: int} + */ + private function getResultSettings(array $settings): array + { + $settings = array_merge(self::DEFAULT_SETTINGS, $settings); + + assertType('non-empty-array', $settings); + + if (!is_string($settings['remove'])) { + throw new Exception(); + } + + assertType("non-empty-array&hasOffsetValue('remove', string)", $settings); + + if (!is_int($settings['limit'])) { + throw new Exception(); + } + + assertType("non-empty-array&hasOffsetValue('limit', int)&hasOffsetValue('remove', string)", $settings); + + return $settings; + } + + + /** + * @param array $array + */ + function foo(array $array): void { + $array['bar'] = 'string'; + + assertType("non-empty-array&hasOffsetValue('bar', 'string')", $array); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2927.php b/tests/PHPStan/Analyser/nsrt/bug-2927.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2927.php rename to tests/PHPStan/Analyser/nsrt/bug-2927.php diff --git a/tests/PHPStan/Analyser/data/bug-2945.php b/tests/PHPStan/Analyser/nsrt/bug-2945.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2945.php rename to tests/PHPStan/Analyser/nsrt/bug-2945.php diff --git a/tests/PHPStan/Analyser/data/bug-2969.php b/tests/PHPStan/Analyser/nsrt/bug-2969.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2969.php rename to tests/PHPStan/Analyser/nsrt/bug-2969.php diff --git a/tests/PHPStan/Analyser/data/bug-2977.php b/tests/PHPStan/Analyser/nsrt/bug-2977.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2977.php rename to tests/PHPStan/Analyser/nsrt/bug-2977.php diff --git a/tests/PHPStan/Analyser/data/bug-2980.php b/tests/PHPStan/Analyser/nsrt/bug-2980.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2980.php rename to tests/PHPStan/Analyser/nsrt/bug-2980.php diff --git a/tests/PHPStan/Analyser/data/bug-2997.php b/tests/PHPStan/Analyser/nsrt/bug-2997.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-2997.php rename to tests/PHPStan/Analyser/nsrt/bug-2997.php diff --git a/tests/PHPStan/Analyser/data/bug-3004.php b/tests/PHPStan/Analyser/nsrt/bug-3004.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3004.php rename to tests/PHPStan/Analyser/nsrt/bug-3004.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-3009.php b/tests/PHPStan/Analyser/nsrt/bug-3009.php new file mode 100644 index 0000000000..969efdc372 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3009.php @@ -0,0 +1,30 @@ +, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $redirectUrlParts); + return null; + } + + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts); + + if (true === array_key_exists('query', $redirectUrlParts)) { + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $redirectUrlParts); + $redirectServer['QUERY_STRING'] = $redirectUrlParts['query']; + } + + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts); + + return 'foo'; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3013.php b/tests/PHPStan/Analyser/nsrt/bug-3013.php new file mode 100644 index 0000000000..7039b43910 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3013.php @@ -0,0 +1,59 @@ +', $foo); + + $bar = $this->intOrNull(); + assertType('int|null', $bar); + + if (in_array($bar, $foo, true)) { + assertType('non-empty-array', $foo); + assertType('int', $bar); + return; + } + assertType('array', $foo); + assertType('int|null', $bar); + + if (in_array($bar, $foo, true) === true) { + assertType('int', $bar); + return; + } + assertType('array', $foo); + assertType('int|null', $bar); + } + + + public function intOrNull(): ?int + { + return rand() === 2 ? null : rand(); + } + + /** + * @param array{0: 1, 1?: 2} $foo + */ + public function testArrayKeyExists($foo): void + { + assertType("array{0: 1, 1?: 2}", $foo); + + $bar = 1; + assertType("1", $bar); + + if (array_key_exists($bar, $foo) === true) { + assertType("array{1, 2}", $foo); + assertType("1", $bar); + return; + } + + assertType("array{1}", $foo); + assertType("1", $bar); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3019.php b/tests/PHPStan/Analyser/nsrt/bug-3019.php new file mode 100644 index 0000000000..1ea4949c3f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3019.php @@ -0,0 +1,35 @@ +sayHi()); - assertType('int', $hw->sayHello()); + assertType('string', $hw->sayHello()); }; interface DecoratorInterface diff --git a/tests/PHPStan/Analyser/data/bug-3158.php b/tests/PHPStan/Analyser/nsrt/bug-3158.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3158.php rename to tests/PHPStan/Analyser/nsrt/bug-3158.php diff --git a/tests/PHPStan/Analyser/data/bug-3190.php b/tests/PHPStan/Analyser/nsrt/bug-3190.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3190.php rename to tests/PHPStan/Analyser/nsrt/bug-3190.php diff --git a/tests/PHPStan/Analyser/data/bug-3226.php b/tests/PHPStan/Analyser/nsrt/bug-3226.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3226.php rename to tests/PHPStan/Analyser/nsrt/bug-3226.php diff --git a/tests/PHPStan/Analyser/data/bug-3266.php b/tests/PHPStan/Analyser/nsrt/bug-3266.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3266.php rename to tests/PHPStan/Analyser/nsrt/bug-3266.php diff --git a/tests/PHPStan/Analyser/data/bug-3269.php b/tests/PHPStan/Analyser/nsrt/bug-3269.php similarity index 80% rename from tests/PHPStan/Analyser/data/bug-3269.php rename to tests/PHPStan/Analyser/nsrt/bug-3269.php index f7f5a2bce6..4a0d7fef71 100644 --- a/tests/PHPStan/Analyser/data/bug-3269.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3269.php @@ -20,10 +20,10 @@ public static function bar(array $intervalGroups): void } } - assertType('array', $borders); + assertType("list", $borders); foreach ($borders as $border) { - assertType('array{version: string, operator: string, side: \'end\'|\'start\'}', $border); + 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/nsrt/bug-3276.php similarity index 95% rename from tests/PHPStan/Analyser/data/bug-3276.php rename to tests/PHPStan/Analyser/nsrt/bug-3276.php index ece368c9b1..53faad441b 100644 --- a/tests/PHPStan/Analyser/data/bug-3276.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3276.php @@ -1,4 +1,4 @@ -= 7.4 + 'een', 'two' => 'twee', 'three' => 'drie']; + usort($arr, 'strcmp'); + assertType("non-empty-list<'drie'|'een'|'twee'>", $arr); +} diff --git a/tests/PHPStan/Analyser/data/bug-3321.php b/tests/PHPStan/Analyser/nsrt/bug-3321.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3321.php rename to tests/PHPStan/Analyser/nsrt/bug-3321.php diff --git a/tests/PHPStan/Analyser/data/bug-3331.php b/tests/PHPStan/Analyser/nsrt/bug-3331.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3331.php rename to tests/PHPStan/Analyser/nsrt/bug-3331.php diff --git a/tests/PHPStan/Analyser/data/bug-3336.php b/tests/PHPStan/Analyser/nsrt/bug-3336.php similarity index 95% rename from tests/PHPStan/Analyser/data/bug-3336.php rename to tests/PHPStan/Analyser/nsrt/bug-3336.php index a0f7127237..a6712e6f69 100644 --- a/tests/PHPStan/Analyser/data/bug-3336.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3336.php @@ -1,4 +1,4 @@ -combine($a, $b); - assertType('array|false', $c); + assertType("array<'a'|'b'|'c', 1|2|3>|false", $c); assertType('array{a: 1, b: 2, c: 3}', array_combine($a, $b)); } diff --git a/tests/PHPStan/Analyser/data/bug-3379.php b/tests/PHPStan/Analyser/nsrt/bug-3379.php similarity index 79% rename from tests/PHPStan/Analyser/data/bug-3379.php rename to tests/PHPStan/Analyser/nsrt/bug-3379.php index 53d82890e3..4500775f5a 100644 --- a/tests/PHPStan/Analyser/data/bug-3379.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3379.php @@ -13,5 +13,5 @@ class Foo function () { echo Foo::URL; - assertType('mixed', Foo::URL); + assertType('non-falsy-string', Foo::URL); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3382.php b/tests/PHPStan/Analyser/nsrt/bug-3382.php new file mode 100644 index 0000000000..65899b7d27 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3382.php @@ -0,0 +1,9 @@ +', $var); } } } diff --git a/tests/PHPStan/Analyser/data/bug-3548.php b/tests/PHPStan/Analyser/nsrt/bug-3548.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3548.php rename to tests/PHPStan/Analyser/nsrt/bug-3548.php diff --git a/tests/PHPStan/Analyser/data/bug-3558.php b/tests/PHPStan/Analyser/nsrt/bug-3558.php similarity index 83% rename from tests/PHPStan/Analyser/data/bug-3558.php rename to tests/PHPStan/Analyser/nsrt/bug-3558.php index b473c961bb..2f71cf07d2 100644 --- a/tests/PHPStan/Analyser/data/bug-3558.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3558.php @@ -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{1, array{1, 2}, array{1, 2}, array{1, 2}}', $idGroups); } }; diff --git a/tests/PHPStan/Analyser/data/bug-3617.php b/tests/PHPStan/Analyser/nsrt/bug-3617.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3617.php rename to tests/PHPStan/Analyser/nsrt/bug-3617.php diff --git a/tests/PHPStan/Analyser/data/bug-3677.php b/tests/PHPStan/Analyser/nsrt/bug-3677.php similarity index 86% rename from tests/PHPStan/Analyser/data/bug-3677.php rename to tests/PHPStan/Analyser/nsrt/bug-3677.php index 0280d14773..e01f685ec3 100644 --- a/tests/PHPStan/Analyser/data/bug-3677.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3677.php @@ -61,8 +61,7 @@ public function sayGoodbye(): void { [$first, $second] = $this->getValue(); if ($first || $second) { - // this assert passes but the ternary breaks the next assert - // assertType(Field::class, $first ?: $second); + assertType(Field::class, $first ?: $second); assertType(Field::class, $first ?? $second); } } @@ -71,8 +70,7 @@ public function sayGoodbye2(): void { [$first, $second] = $this->getValue(); if ($first || $second) { - // this assert passes but the ternary breaks the next assert - // assertType(Field::class, $first ? $first : $second); + assertType(Field::class, $first ? $first : $second); assertType(Field::class, $first ?? $second); } } diff --git a/tests/PHPStan/Analyser/data/bug-3710.php b/tests/PHPStan/Analyser/nsrt/bug-3710.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3710.php rename to tests/PHPStan/Analyser/nsrt/bug-3710.php diff --git a/tests/PHPStan/Analyser/data/bug-3760.php b/tests/PHPStan/Analyser/nsrt/bug-3760.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3760.php rename to tests/PHPStan/Analyser/nsrt/bug-3760.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-3789.php b/tests/PHPStan/Analyser/nsrt/bug-3789.php new file mode 100644 index 0000000000..133ce49b50 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3789.php @@ -0,0 +1,23 @@ +} $params + * @return ($params is array{wrapperClass:mixed} ? T : Connection) + */ + public static function getConnection(array $params): Connection { + return new Connection(); + } + + public static function test(): void { + assertType('Bug3853\Connection', DriverManager::getConnection([])); + assertType('Bug3853\SubConnection', DriverManager::getConnection(['wrapperClass' => SubConnection::class])); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3858.php b/tests/PHPStan/Analyser/nsrt/bug-3858.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3858.php rename to tests/PHPStan/Analyser/nsrt/bug-3858.php diff --git a/tests/PHPStan/Analyser/data/bug-3866.php b/tests/PHPStan/Analyser/nsrt/bug-3866.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3866.php rename to tests/PHPStan/Analyser/nsrt/bug-3866.php diff --git a/tests/PHPStan/Analyser/data/bug-3875.php b/tests/PHPStan/Analyser/nsrt/bug-3875.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3875.php rename to tests/PHPStan/Analyser/nsrt/bug-3875.php diff --git a/tests/PHPStan/Analyser/data/bug-3880.php b/tests/PHPStan/Analyser/nsrt/bug-3880.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3880.php rename to tests/PHPStan/Analyser/nsrt/bug-3880.php diff --git a/tests/PHPStan/Analyser/data/bug-3915.php b/tests/PHPStan/Analyser/nsrt/bug-3915.php similarity index 83% rename from tests/PHPStan/Analyser/data/bug-3915.php rename to tests/PHPStan/Analyser/nsrt/bug-3915.php index 2ba8131fbf..39a0addeee 100644 --- a/tests/PHPStan/Analyser/data/bug-3915.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3915.php @@ -13,7 +13,7 @@ public function sayHello(): void foreach ([1] as $row) { $lengths[] = self::getInt(); } - assertType('non-empty-array', $lengths); + assertType('non-empty-list', $lengths); } public static function getInt(): int diff --git a/tests/PHPStan/Analyser/data/bug-3922.php b/tests/PHPStan/Analyser/nsrt/bug-3922.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3922.php rename to tests/PHPStan/Analyser/nsrt/bug-3922.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php b/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php new file mode 100644 index 0000000000..9eaeff4a72 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php @@ -0,0 +1,21 @@ += 8.0 + +namespace Bug3961Php8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(string $v, string $d, $m): void + { + assertType('non-empty-list', explode('.', $v)); + assertType('*NEVER*', explode('', $v)); + assertType('list', explode('.', $v, -2)); + assertType('non-empty-list', explode('.', $v, 0)); + assertType('non-empty-list', explode('.', $v, 1)); + assertType('non-empty-list', explode($d, $v)); + assertType('non-empty-list', explode($m, $v)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3961.php b/tests/PHPStan/Analyser/nsrt/bug-3961.php new file mode 100644 index 0000000000..b4725ec070 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3961.php @@ -0,0 +1,21 @@ +', explode('.', $v)); + assertType('false', explode('', $v)); + assertType('list', explode('.', $v, -2)); + assertType('non-empty-list', explode('.', $v, 0)); + assertType('non-empty-list', explode('.', $v, 1)); + assertType('non-empty-list|false', explode($d, $v)); + assertType('(non-empty-list|false)', explode($m, $v)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3981.php b/tests/PHPStan/Analyser/nsrt/bug-3981.php new file mode 100644 index 0000000000..c962983a3e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3981.php @@ -0,0 +1,54 @@ +getType() which is `stdClass|array|null` here + assertNativeType('array{}|null', $config); + assertType('array{}|null', $config); + $config = new \stdClass(); + } elseif (! (is_array($config) || $config instanceof \stdClass)) { + assertNativeType('mixed~(0|0.0|\'\'|\'0\'|array{}|stdClass|false|null)', $config); + assertType('*NEVER*', $config); + } + + return new \stdClass($config); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3993.php b/tests/PHPStan/Analyser/nsrt/bug-3993.php similarity index 76% rename from tests/PHPStan/Analyser/data/bug-3993.php rename to tests/PHPStan/Analyser/nsrt/bug-3993.php index 4c106dbab8..a1a24e380d 100644 --- a/tests/PHPStan/Analyser/data/bug-3993.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3993.php @@ -13,11 +13,11 @@ public function doFoo($arguments) return; } - assertType('mixed~null', $arguments); + assertType('mixed~(array{}|null)', $arguments); array_shift($arguments); - assertType('mixed~null', $arguments); + assertType('array', $arguments); assertType('int<0, max>', count($arguments)); } diff --git a/tests/PHPStan/Analyser/data/bug-3997.php b/tests/PHPStan/Analyser/nsrt/bug-3997.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3997.php rename to tests/PHPStan/Analyser/nsrt/bug-3997.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4016.php b/tests/PHPStan/Analyser/nsrt/bug-4016.php new file mode 100644 index 0000000000..c6d67ad772 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4016.php @@ -0,0 +1,36 @@ + $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + $a[] = 2; + assertType('non-empty-array', $a); + + unset($a[0]); + assertType('array|int<1, max>, int>', $a); + } + + /** + * @param array $a + */ + public function doBar(array $a): void + { + assertType('array', $a); + $a[1] = 2; + assertType('non-empty-array&hasOffsetValue(1, 2)', $a); + + unset($a[1]); + assertType('array|int<2, max>, int>', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4091.php b/tests/PHPStan/Analyser/nsrt/bug-4091.php new file mode 100644 index 0000000000..ba691e03b1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4091.php @@ -0,0 +1,10 @@ + 3) { + echo 'Fizz'; + assertType('int<0, 10>', mt_rand(0,10)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4099.php b/tests/PHPStan/Analyser/nsrt/bug-4099.php new file mode 100644 index 0000000000..5e5eb30ca2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4099.php @@ -0,0 +1,41 @@ + + */ +class GenericList implements IteratorAggregate +{ + /** @var array */ + protected $items = []; + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->items); + } + + /** + * @return ?T + */ + public function broken(int $key) + { + $item = $this->items[$key] ?? null; + assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); + if ($item) { + assertType("T of mixed~(0|0.0|''|'0'|array{}|false|null) (class Bug4117Types\GenericList, argument)", $item); + } else { + assertType("(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0&T of mixed~null (class Bug4117Types\GenericList, argument))|(list{}&T of mixed~null (class Bug4117Types\GenericList, argument))|(''&T of mixed~null (class Bug4117Types\GenericList, argument))|('0'&T of mixed~null (class Bug4117Types\GenericList, argument))|null", $item); + } + + assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); + + return $item; + } + + /** + * @return ?T + */ + public function works(int $key) + { + $item = $this->items[$key] ?? null; + assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); + + return $item; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4177.php b/tests/PHPStan/Analyser/nsrt/bug-4177.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4177.php rename to tests/PHPStan/Analyser/nsrt/bug-4177.php diff --git a/tests/PHPStan/Analyser/data/bug-4188.php b/tests/PHPStan/Analyser/nsrt/bug-4188.php similarity index 81% rename from tests/PHPStan/Analyser/data/bug-4188.php rename to tests/PHPStan/Analyser/nsrt/bug-4188.php index 07a44f9458..a34cca26a4 100644 --- a/tests/PHPStan/Analyser/data/bug-4188.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4188.php @@ -1,6 +1,6 @@ -= 7.4 +', $filtered); + assertType('array', $filtered); $this->onlyB($filtered); } @@ -30,7 +30,7 @@ public function setShort(array $data): void $data, fn($param): bool => $param instanceof B, ); - assertType('array', $filtered); + assertType('array', $filtered); $this->onlyB($filtered); } diff --git a/tests/PHPStan/Analyser/data/bug-4190.php b/tests/PHPStan/Analyser/nsrt/bug-4190.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4190.php rename to tests/PHPStan/Analyser/nsrt/bug-4190.php diff --git a/tests/PHPStan/Analyser/data/bug-4205.php b/tests/PHPStan/Analyser/nsrt/bug-4205.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4205.php rename to tests/PHPStan/Analyser/nsrt/bug-4205.php diff --git a/tests/PHPStan/Analyser/data/bug-4206.php b/tests/PHPStan/Analyser/nsrt/bug-4206.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4206.php rename to tests/PHPStan/Analyser/nsrt/bug-4206.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4207.php b/tests/PHPStan/Analyser/nsrt/bug-4207.php new file mode 100644 index 0000000000..756eef4c9f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4207.php @@ -0,0 +1,10 @@ +>', range(1, 10000)); + assertType('non-empty-list>', range(10000, 1)); +}; diff --git a/tests/PHPStan/Analyser/data/bug-4209-2.php b/tests/PHPStan/Analyser/nsrt/bug-4209-2.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4209-2.php rename to tests/PHPStan/Analyser/nsrt/bug-4209-2.php diff --git a/tests/PHPStan/Analyser/data/bug-4209.php b/tests/PHPStan/Analyser/nsrt/bug-4209.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4209.php rename to tests/PHPStan/Analyser/nsrt/bug-4209.php diff --git a/tests/PHPStan/Analyser/data/bug-4213.php b/tests/PHPStan/Analyser/nsrt/bug-4213.php similarity index 97% rename from tests/PHPStan/Analyser/data/bug-4213.php rename to tests/PHPStan/Analyser/nsrt/bug-4213.php index 538bacbb79..a8f06cb1c5 100644 --- a/tests/PHPStan/Analyser/data/bug-4213.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4213.php @@ -1,4 +1,4 @@ -= 8.1 namespace Bug4213; diff --git a/tests/PHPStan/Analyser/data/bug-4215.php b/tests/PHPStan/Analyser/nsrt/bug-4215.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4215.php rename to tests/PHPStan/Analyser/nsrt/bug-4215.php diff --git a/tests/PHPStan/Analyser/data/bug-4231.php b/tests/PHPStan/Analyser/nsrt/bug-4231.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4231.php rename to tests/PHPStan/Analyser/nsrt/bug-4231.php diff --git a/tests/PHPStan/Analyser/data/bug-4247.php b/tests/PHPStan/Analyser/nsrt/bug-4247.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4247.php rename to tests/PHPStan/Analyser/nsrt/bug-4247.php diff --git a/tests/PHPStan/Analyser/data/bug-4267.php b/tests/PHPStan/Analyser/nsrt/bug-4267.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4267.php rename to tests/PHPStan/Analyser/nsrt/bug-4267.php diff --git a/tests/PHPStan/Analyser/data/bug-4287.php b/tests/PHPStan/Analyser/nsrt/bug-4287.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4287.php rename to tests/PHPStan/Analyser/nsrt/bug-4287.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4302b.php b/tests/PHPStan/Analyser/nsrt/bug-4302b.php new file mode 100644 index 0000000000..ff24b6a689 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4302b.php @@ -0,0 +1,20 @@ += 7.4 +', array_keys($meters)); + assertType('non-empty-list', array_values($meters)); +}; diff --git a/tests/PHPStan/Analyser/data/bug-4415.php b/tests/PHPStan/Analyser/nsrt/bug-4415.php similarity index 75% rename from tests/PHPStan/Analyser/data/bug-4415.php rename to tests/PHPStan/Analyser/nsrt/bug-4415.php index 338401f86a..8dce55c08e 100644 --- a/tests/PHPStan/Analyser/data/bug-4415.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4415.php @@ -19,7 +19,7 @@ public function getIterator(): \Iterator function (Foo $foo): void { foreach ($foo as $k => $v) { - assertType('int', $k); - assertType('string', $v); + assertType('mixed', $k); // should be int + assertType('mixed', $v); // should be string } }; diff --git a/tests/PHPStan/Analyser/data/bug-4423.php b/tests/PHPStan/Analyser/nsrt/bug-4423.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4423.php rename to tests/PHPStan/Analyser/nsrt/bug-4423.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4434.php b/tests/PHPStan/Analyser/nsrt/bug-4434.php new file mode 100644 index 0000000000..7b48df20fd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4434.php @@ -0,0 +1,42 @@ +', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); + if (PHP_MAJOR_VERSION === 7) { + assertType('7', PHP_MAJOR_VERSION); + assertType('7', \PHP_MAJOR_VERSION); + } else { + assertType('8|int<5, 6>', PHP_MAJOR_VERSION); + assertType('8|int<5, 6>', \PHP_MAJOR_VERSION); + } + } + } +} + +class HelloWorld2 +{ + public function testSendEmailToLog(): void + { + foreach ([1] as $emailFile) { + assertType('int<5, 8>', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); + if (PHP_MAJOR_VERSION === 100) { + assertType('*NEVER*', PHP_MAJOR_VERSION); + assertType('*NEVER*', \PHP_MAJOR_VERSION); + } else { + assertType('int<5, 8>', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); + } + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4436.php b/tests/PHPStan/Analyser/nsrt/bug-4436.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4436.php rename to tests/PHPStan/Analyser/nsrt/bug-4436.php diff --git a/tests/PHPStan/Analyser/data/bug-4498.php b/tests/PHPStan/Analyser/nsrt/bug-4498.php similarity index 84% rename from tests/PHPStan/Analyser/data/bug-4498.php rename to tests/PHPStan/Analyser/nsrt/bug-4498.php index 19e878c763..ad07baa3db 100644 --- a/tests/PHPStan/Analyser/data/bug-4498.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4498.php @@ -38,7 +38,7 @@ public function fcn(iterable $iterable): iterable public function bar(iterable $iterable): iterable { if (is_array($iterable)) { - assertType('array', $iterable); + assertType('array<((int&TKey (method Bug4498\Foo::bar(), argument))|(string&TKey (method Bug4498\Foo::bar(), argument))), TValue (method Bug4498\Foo::bar(), argument)>', $iterable); return $iterable; } diff --git a/tests/PHPStan/Analyser/data/bug-4499.php b/tests/PHPStan/Analyser/nsrt/bug-4499.php similarity index 82% rename from tests/PHPStan/Analyser/data/bug-4499.php rename to tests/PHPStan/Analyser/nsrt/bug-4499.php index 046da4efa1..fe65958259 100644 --- a/tests/PHPStan/Analyser/data/bug-4499.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4499.php @@ -11,7 +11,7 @@ class Foo function thing(array $things) : void{ switch(count($things)){ case 1: - assertType('non-empty-array', $things); + assertType('array{int}', $things); assertType('int', array_shift($things)); } } diff --git a/tests/PHPStan/Analyser/data/bug-4500.php b/tests/PHPStan/Analyser/nsrt/bug-4500.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4500.php rename to tests/PHPStan/Analyser/nsrt/bug-4500.php diff --git a/tests/PHPStan/Analyser/data/bug-4504.php b/tests/PHPStan/Analyser/nsrt/bug-4504.php similarity index 78% rename from tests/PHPStan/Analyser/data/bug-4504.php rename to tests/PHPStan/Analyser/nsrt/bug-4504.php index f70d3c9567..ceab5de4e2 100644 --- a/tests/PHPStan/Analyser/data/bug-4504.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4504.php @@ -14,7 +14,7 @@ public function sayHello($models): void assertType('Bug4504TypeInference\A', $v); } - assertType('array{}|Iterator', $models); + assertType('Iterator', $models); } } diff --git a/tests/PHPStan/Analyser/data/bug-4538.php b/tests/PHPStan/Analyser/nsrt/bug-4538.php similarity index 81% rename from tests/PHPStan/Analyser/data/bug-4538.php rename to tests/PHPStan/Analyser/nsrt/bug-4538.php index ebdbec8e48..20be659998 100644 --- a/tests/PHPStan/Analyser/data/bug-4538.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4538.php @@ -12,6 +12,6 @@ class Foo public function bar(string $index): void { assertType('string|false', getenv($index)); - assertType('array', getenv()); + assertType('array', getenv()); } } diff --git a/tests/PHPStan/Analyser/data/bug-4545.php b/tests/PHPStan/Analyser/nsrt/bug-4545.php similarity index 90% rename from tests/PHPStan/Analyser/data/bug-4545.php rename to tests/PHPStan/Analyser/nsrt/bug-4545.php index a7162e9f79..e7f48619cd 100644 --- a/tests/PHPStan/Analyser/data/bug-4545.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4545.php @@ -33,7 +33,7 @@ function compareMaps(Map $firstMap, Map $secondMap, Closure $comparator): Set foreach ($intersect as $key) { assertType('TValue1 (method Bug4545\Foo::compareMaps(), argument)', $firstMap->get($key)); assertType('TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key)); - assertType('int|TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key, 1)); + assertType('1|TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key, 1)); } return $keys; diff --git a/tests/PHPStan/Analyser/data/bug-4557.php b/tests/PHPStan/Analyser/nsrt/bug-4557.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4557.php rename to tests/PHPStan/Analyser/nsrt/bug-4557.php diff --git a/tests/PHPStan/Analyser/data/bug-4558.php b/tests/PHPStan/Analyser/nsrt/bug-4558.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4558.php rename to tests/PHPStan/Analyser/nsrt/bug-4558.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4565.php b/tests/PHPStan/Analyser/nsrt/bug-4565.php new file mode 100644 index 0000000000..48ab02dd92 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4565.php @@ -0,0 +1,19 @@ + ''] + $variables['attributes']; + assertType('non-empty-array', $attributes); + if (!empty($variables['button'])) { + assertType('non-empty-array', $attributes); + $attributes['type'] = 'button'; + assertType("non-empty-array&hasOffsetValue('type', 'button')", $attributes); + unset($attributes['href']); + assertType("non-empty-array&hasOffsetValue('type', 'button')", $attributes); + } + assertType('non-empty-array', $attributes); + return $attributes; +} diff --git a/tests/PHPStan/Analyser/data/bug-4573.php b/tests/PHPStan/Analyser/nsrt/bug-4573.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4573.php rename to tests/PHPStan/Analyser/nsrt/bug-4573.php diff --git a/tests/PHPStan/Analyser/data/bug-4577.php b/tests/PHPStan/Analyser/nsrt/bug-4577.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4577.php rename to tests/PHPStan/Analyser/nsrt/bug-4577.php diff --git a/tests/PHPStan/Analyser/data/bug-4579.php b/tests/PHPStan/Analyser/nsrt/bug-4579.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4579.php rename to tests/PHPStan/Analyser/nsrt/bug-4579.php diff --git a/tests/PHPStan/Analyser/data/bug-4586.php b/tests/PHPStan/Analyser/nsrt/bug-4586.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4586.php rename to tests/PHPStan/Analyser/nsrt/bug-4586.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4587.php b/tests/PHPStan/Analyser/nsrt/bug-4587.php new file mode 100644 index 0000000000..061c953a28 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4587.php @@ -0,0 +1,37 @@ + $results */ + $results = []; + + $type = array_map(static function (array $result): array { + assertType('array{a: int}', $result); + return $result; + }, $results); + + assertType('list', $type); + } + + public function b(): void + { + /** @var list $results */ + $results = []; + + $type = array_map(static function (array $result): array { + assertType('array{a: int}', $result); + $result['a'] = (string) $result['a']; + assertType('array{a: lowercase-string&numeric-string&uppercase-string}', $result); + + return $result; + }, $results); + + assertType('list', $type); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4588.php b/tests/PHPStan/Analyser/nsrt/bug-4588.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4588.php rename to tests/PHPStan/Analyser/nsrt/bug-4588.php diff --git a/tests/PHPStan/Analyser/data/bug-4592.php b/tests/PHPStan/Analyser/nsrt/bug-4592.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4592.php rename to tests/PHPStan/Analyser/nsrt/bug-4592.php diff --git a/tests/PHPStan/Analyser/data/bug-4602.php b/tests/PHPStan/Analyser/nsrt/bug-4602.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4602.php rename to tests/PHPStan/Analyser/nsrt/bug-4602.php diff --git a/tests/PHPStan/Analyser/data/bug-4606.php b/tests/PHPStan/Analyser/nsrt/bug-4606.php similarity index 84% rename from tests/PHPStan/Analyser/data/bug-4606.php rename to tests/PHPStan/Analyser/nsrt/bug-4606.php index 1cf9cf4a32..aa06628417 100644 --- a/tests/PHPStan/Analyser/data/bug-4606.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4606.php @@ -11,7 +11,7 @@ */ assertType(Foo::class, $this); -assertType('array', $assigned); +assertType('list', $assigned); /** diff --git a/tests/PHPStan/Analyser/data/bug-4642.php b/tests/PHPStan/Analyser/nsrt/bug-4642.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4642.php rename to tests/PHPStan/Analyser/nsrt/bug-4642.php diff --git a/tests/PHPStan/Analyser/data/bug-4650.php b/tests/PHPStan/Analyser/nsrt/bug-4650.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4650.php rename to tests/PHPStan/Analyser/nsrt/bug-4650.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4657.php b/tests/PHPStan/Analyser/nsrt/bug-4657.php new file mode 100644 index 0000000000..db175d8a6d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4657.php @@ -0,0 +1,58 @@ +', $count); + + $a = []; + if (isset($array['a'])) $a[] = $array['a']; + if (isset($array['b'])) $a[] = $array['b']; + if (isset($array['c'])) $a[] = $array['c']; + if (isset($array['d'])) $a[] = $array['d']; + if (isset($array['e'])) $a[] = $array['e']; + if (count($a) >= $count) { + assertType('int<1, 5>', count($a)); + assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + } else { + assertType('0', count($a)); + assertType('array{}', $a); + } +}; + +function(array $array, int $count): void { + if ($count < 1) { + return; + } + + assertType('int<1, max>', $count); + + $a = []; + if (isset($array['a'])) $a[] = $array['a']; + if (isset($array['b'])) $a[] = $array['b']; + if (isset($array['c'])) $a[] = $array['c']; + if (isset($array['d'])) $a[] = $array['d']; + if (isset($array['e'])) $a[] = $array['e']; + if (count($a) > $count) { + assertType('int<2, 5>', count($a)); + assertType('list{0: mixed~null, 1: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + } else { + assertType('int<0, 5>', count($a)); // Could be int<0, 1> + assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); // Could be array{}|array{0: mixed~null} + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-4707.php b/tests/PHPStan/Analyser/nsrt/bug-4707.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4707.php rename to tests/PHPStan/Analyser/nsrt/bug-4707.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4708.php b/tests/PHPStan/Analyser/nsrt/bug-4708.php new file mode 100644 index 0000000000..b6f2302722 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4708.php @@ -0,0 +1,96 @@ + FALSE, + 'dberror' => 'xyz']; + } + else + { + assertType('array', $result); + if (!isset($result['bsw'])) + { + assertType('array', $result); + $result['bsw'] = 1; + assertType("non-empty-array<1|string>&hasOffsetValue('bsw', 1)", $result); + } + else + { + assertType("non-empty-array&hasOffsetValue('bsw', string)", $result); + $result['bsw'] = (int) $result['bsw']; + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + } + + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + + if (!isset($result['bew'])) + { + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + $result['bew'] = 5; + assertType("non-empty-array&hasOffsetValue('bew', 5)&hasOffsetValue('bsw', int)", $result); + } + else + { + assertType("non-empty-array&hasOffsetValue('bew', int|string)&hasOffsetValue('bsw', int)", $result); + $result['bew'] = (int) $result['bew']; + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + } + + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + + foreach (['utc', 'ssi'] as $field) + { + if (array_key_exists($field, $result)) + { + $result[$field] = (int) $result[$field]; + } + } + + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + } + + assertType('non-empty-array', $result); + + return $result; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4711.php b/tests/PHPStan/Analyser/nsrt/bug-4711.php new file mode 100644 index 0000000000..9050c74c15 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4711.php @@ -0,0 +1,19 @@ +', explode($string, '')); + assertType('non-empty-list', explode($string[0], '')); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-4714.php b/tests/PHPStan/Analyser/nsrt/bug-4714.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4714.php rename to tests/PHPStan/Analyser/nsrt/bug-4714.php diff --git a/tests/PHPStan/Analyser/data/bug-4725.php b/tests/PHPStan/Analyser/nsrt/bug-4725.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4725.php rename to tests/PHPStan/Analyser/nsrt/bug-4725.php diff --git a/tests/PHPStan/Analyser/data/bug-4733.php b/tests/PHPStan/Analyser/nsrt/bug-4733.php similarity index 80% rename from tests/PHPStan/Analyser/data/bug-4733.php rename to tests/PHPStan/Analyser/nsrt/bug-4733.php index 39961cc464..dec6f9bd3b 100644 --- a/tests/PHPStan/Analyser/data/bug-4733.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4733.php @@ -23,6 +23,23 @@ public function getDescription(?\DateTimeImmutable $start, ?string $someObject): assertType('string', $someObject); } + public function getDescriptionn(?\DateTimeImmutable $start, ?string $someObject): void + { + if ($start !== null && $someObject !== null) { + return; + } + + // $start === null || $someObject === null + + if ($start === null) { + return; + } + + // $start !== null therefore $someObject === null + + assertType('null', $someObject); + } + public function getDescription2(?\DateTimeImmutable $start, ?string $someObject): void { if ($start !== null || $someObject !== null) { diff --git a/tests/PHPStan/Analyser/data/bug-4741.php b/tests/PHPStan/Analyser/nsrt/bug-4741.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4741.php rename to tests/PHPStan/Analyser/nsrt/bug-4741.php diff --git a/tests/PHPStan/Analyser/data/bug-4743.php b/tests/PHPStan/Analyser/nsrt/bug-4743.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4743.php rename to tests/PHPStan/Analyser/nsrt/bug-4743.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4754.php b/tests/PHPStan/Analyser/nsrt/bug-4754.php new file mode 100644 index 0000000000..ffa98ee0f4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4754.php @@ -0,0 +1,42 @@ +, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedComponentNotSpecified); + assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|int<0, 65535>|string|false|null', $parsedNotConstant); + assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedAllConstant); + assertType('string|false|null', $parsedSchemeConstant); + assertType('string|false|null', $parsedHostConstant); + assertType('int<0, 65535>|false|null', $parsedPortConstant); + assertType('string|false|null', $parsedUserConstant); + assertType('string|false|null', $parsedPassConstant); + assertType('string|false|null', $parsedPathConstant); + assertType('string|false|null', $parsedQueryConstant); + assertType('string|false|null', $parsedFragmentConstant); +} diff --git a/tests/PHPStan/Analyser/data/bug-4757.php b/tests/PHPStan/Analyser/nsrt/bug-4757.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4757.php rename to tests/PHPStan/Analyser/nsrt/bug-4757.php diff --git a/tests/PHPStan/Analyser/data/bug-4761.php b/tests/PHPStan/Analyser/nsrt/bug-4761.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4761.php rename to tests/PHPStan/Analyser/nsrt/bug-4761.php diff --git a/tests/PHPStan/Analyser/data/bug-4803.php b/tests/PHPStan/Analyser/nsrt/bug-4803.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4803.php rename to tests/PHPStan/Analyser/nsrt/bug-4803.php diff --git a/tests/PHPStan/Analyser/data/bug-4814.php b/tests/PHPStan/Analyser/nsrt/bug-4814.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4814.php rename to tests/PHPStan/Analyser/nsrt/bug-4814.php diff --git a/tests/PHPStan/Analyser/data/bug-4816.php b/tests/PHPStan/Analyser/nsrt/bug-4816.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4816.php rename to tests/PHPStan/Analyser/nsrt/bug-4816.php diff --git a/tests/PHPStan/Analyser/data/bug-4820.php b/tests/PHPStan/Analyser/nsrt/bug-4820.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4820.php rename to tests/PHPStan/Analyser/nsrt/bug-4820.php diff --git a/tests/PHPStan/Analyser/data/bug-4821.php b/tests/PHPStan/Analyser/nsrt/bug-4821.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4821.php rename to tests/PHPStan/Analyser/nsrt/bug-4821.php diff --git a/tests/PHPStan/Analyser/data/bug-4822.php b/tests/PHPStan/Analyser/nsrt/bug-4822.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4822.php rename to tests/PHPStan/Analyser/nsrt/bug-4822.php diff --git a/tests/PHPStan/Analyser/data/bug-4838.php b/tests/PHPStan/Analyser/nsrt/bug-4838.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4838.php rename to tests/PHPStan/Analyser/nsrt/bug-4838.php diff --git a/tests/PHPStan/Analyser/data/bug-4843.php b/tests/PHPStan/Analyser/nsrt/bug-4843.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4843.php rename to tests/PHPStan/Analyser/nsrt/bug-4843.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4875.php b/tests/PHPStan/Analyser/nsrt/bug-4875.php new file mode 100644 index 0000000000..ee0e89a44d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4875.php @@ -0,0 +1,37 @@ + $interface + * @return T&Mock + */ + function mockIt(string $interface): object + { + return eval("new class implements $interface, Mock {}"); + } + + function doFoo() + { + $mock = $this->mockIt(Blah::class); + + assertType('Bug4875\Blah&Bug4875\Mock', $mock); + assertType('class-string&literal-string', $mock::class); + assertType('class-string', get_class($mock)); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-4879.php b/tests/PHPStan/Analyser/nsrt/bug-4879.php similarity index 90% rename from tests/PHPStan/Analyser/data/bug-4879.php rename to tests/PHPStan/Analyser/nsrt/bug-4879.php index 1c6c9536c4..26d74b1c73 100644 --- a/tests/PHPStan/Analyser/data/bug-4879.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4879.php @@ -33,7 +33,7 @@ public function sayHello2(bool $bool1): void $this->test(); } catch (\Exception $ex) { - assertVariableCertainty(TrinaryLogic::createNo(), $var); + assertVariableCertainty(TrinaryLogic::createMaybe(), $var); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4885.php b/tests/PHPStan/Analyser/nsrt/bug-4885.php new file mode 100644 index 0000000000..c047e0e41e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4885.php @@ -0,0 +1,22 @@ += 8.0 + +namespace Bug4885Types; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** @param array{word?: string} $data */ + public function sayHello(array $data): void + { + echo ($data['word'] ?? throw new \RuntimeException('bye')) . ', World!'; + assertType('array{word: string}', $data); + } + + /** @param array{word?: string|null} $data */ + public function sayHi(array $data): void + { + echo ($data['word'] ?? throw new \RuntimeException('bye')) . ', World!'; + assertType('array{word: string}', $data); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4887.php b/tests/PHPStan/Analyser/nsrt/bug-4887.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4887.php rename to tests/PHPStan/Analyser/nsrt/bug-4887.php diff --git a/tests/PHPStan/Analyser/data/bug-4896.php b/tests/PHPStan/Analyser/nsrt/bug-4896.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4896.php rename to tests/PHPStan/Analyser/nsrt/bug-4896.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4902-php8.php b/tests/PHPStan/Analyser/nsrt/bug-4902-php8.php new file mode 100644 index 0000000000..760070dd83 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4902-php8.php @@ -0,0 +1,53 @@ += 8.0 + +namespace Bug4902Php8; + +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/nsrt/bug-4903.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4903.php rename to tests/PHPStan/Analyser/nsrt/bug-4903.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4907.php b/tests/PHPStan/Analyser/nsrt/bug-4907.php new file mode 100644 index 0000000000..242aa29cc7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4907.php @@ -0,0 +1,15 @@ + $foo) { + // ... + } + + assertType('5|6|7', $foo); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4950.php b/tests/PHPStan/Analyser/nsrt/bug-4950.php new file mode 100644 index 0000000000..9f793675d6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4950.php @@ -0,0 +1,35 @@ +', $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); + assertType('list<0|1|2|3|4>', $batch); } } @@ -38,7 +38,7 @@ public function doBar2() 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); + assertType('array{0, 1}', $batch); } /** diff --git a/tests/PHPStan/Analyser/data/bug-505.php b/tests/PHPStan/Analyser/nsrt/bug-505.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-505.php rename to tests/PHPStan/Analyser/nsrt/bug-505.php diff --git a/tests/PHPStan/Analyser/data/bug-5072.php b/tests/PHPStan/Analyser/nsrt/bug-5072.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5072.php rename to tests/PHPStan/Analyser/nsrt/bug-5072.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5077.php b/tests/PHPStan/Analyser/nsrt/bug-5077.php new file mode 100644 index 0000000000..f20bf085b9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5077.php @@ -0,0 +1,33 @@ +> $array + */ +function test(array &$array): void +{ + $array[] = ['test' => rand(), 'p' => 'test']; +} + +function (): void { + $array = []; + $array['key'] = []; + + assertType('array{key: array{}}', $array); + assertType('array{}', $array['key']); + + test($array['key']); + assertType('array{key: array>}', $array); + assertType('array>', $array['key']); + + test($array['key']); + assertType('array{key: array>}', $array); + assertType('array>', $array['key']); + + test($array['key']); + assertType('array{key: array>}', $array); + assertType('array>', $array['key']); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5086.php b/tests/PHPStan/Analyser/nsrt/bug-5086.php new file mode 100644 index 0000000000..6018447ba7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5086.php @@ -0,0 +1,26 @@ +doFoo())) { + return; + } + + assertType(stdClass::class, $obj); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5129.php b/tests/PHPStan/Analyser/nsrt/bug-5129.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5129.php rename to tests/PHPStan/Analyser/nsrt/bug-5129.php diff --git a/tests/PHPStan/Analyser/data/bug-5140.php b/tests/PHPStan/Analyser/nsrt/bug-5140.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5140.php rename to tests/PHPStan/Analyser/nsrt/bug-5140.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5168-php7.php b/tests/PHPStan/Analyser/nsrt/bug-5168-php7.php new file mode 100644 index 0000000000..9e981de789 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5168-php7.php @@ -0,0 +1,12 @@ += 8.0 + +namespace Bug5168Php8; + +use function PHPStan\Testing\assertType; + +function (float $f): void { + define('LARAVEL_START', microtime(true)); + + $comment = 'Calculated in ' . microtime(true) - $f; + assertType('non-falsy-string', $comment); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5172.php b/tests/PHPStan/Analyser/nsrt/bug-5172.php new file mode 100644 index 0000000000..63519d8ca7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5172.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug5172; + +use function PHPStan\Testing\assertType; + +class Period +{ + public mixed $from; + public mixed $to; + + public function year(): ?int + { + assertType('mixed', $this->from); + assertType('mixed', $this->to); + + // let's say $this->from === null && $model->to === null + + if ($this->from?->year !== $this->to?->year) { + return null; + } + + assertType('mixed', $this->from); + assertType('mixed', $this->to); + + return $this->from?->year; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5219.php b/tests/PHPStan/Analyser/nsrt/bug-5219.php similarity index 79% rename from tests/PHPStan/Analyser/data/bug-5219.php rename to tests/PHPStan/Analyser/nsrt/bug-5219.php index 91ce62bb02..39d1540111 100644 --- a/tests/PHPStan/Analyser/data/bug-5219.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5219.php @@ -11,8 +11,8 @@ protected function foo(string $message, string $x): void { $header = sprintf('%s-%s', '', implode('-', [$x])); - assertType('non-empty-string', $header); - assertType('non-empty-array', [$header => $message]); + assertType('non-falsy-string', $header); + assertType('non-empty-array', [$header => $message]); } protected function bar(string $message): void diff --git a/tests/PHPStan/Analyser/nsrt/bug-5223.php b/tests/PHPStan/Analyser/nsrt/bug-5223.php new file mode 100644 index 0000000000..0e19768606 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5223.php @@ -0,0 +1,56 @@ +, tagNames: array}", $filters); + + unset($filters['page']); + assertType("array{categoryKeys: array, tagNames: array}", $filters); + + unset($filters['limit']); + assertType("array{categoryKeys: array, tagNames: array}", $filters); + + assertType('*ERROR*', $filters['something']); + var_dump($filters['something']); + + $this->test($filters); + } + + /** + * @param array{ + * categoryKeys: string[], + * tagNames: string[], + * } $filters + */ + public function withoutUnset(array $filters): void + { + assertType("array{categoryKeys: array, tagNames: array}", $filters); + assertType('*ERROR*', $filters['something']); + var_dump($filters['something']); + + $this->test($filters); + } + + /** + * @param array{ + * categoryKeys: string[], + * tagNames: string[], + * } $filters + */ + private function test(array $filters): void + { + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5259.php b/tests/PHPStan/Analyser/nsrt/bug-5259.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5259.php rename to tests/PHPStan/Analyser/nsrt/bug-5259.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5262.php b/tests/PHPStan/Analyser/nsrt/bug-5262.php new file mode 100644 index 0000000000..229d9fbeb2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5262.php @@ -0,0 +1,36 @@ + $testclass + * @return T + */ +function test(bool $optional = false, string $testclass = TestBase::class): TestBase +{ + return new $testclass(); +} + +class TestBase +{ +} + +class TestChild extends TestBase +{ + public function hello(): string + { + return 'world'; + } +} + +function runTest(): void +{ + assertType('Bug5262\TestChild', test(false, TestChild::class)); + assertType('Bug5262\TestChild', test(false, testclass: TestChild::class)); + assertType('Bug5262\TestChild', test(optional: false, testclass: TestChild::class)); + assertType('Bug5262\TestChild', test(testclass: TestChild::class, optional: false)); + assertType('Bug5262\TestChild', test(testclass: TestChild::class)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php b/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php new file mode 100644 index 0000000000..d1cb994c92 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php @@ -0,0 +1,61 @@ += 8.1 + +declare(strict_types=1); + +namespace Bug5287Php81; + +use function PHPStan\Testing\assertType; + +/** + * @param list $arr + */ +function foo(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('list', $arrSpread); +} + +/** + * @param list $arr + */ +function foo2(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('list', $arrSpread); +} + +/** + * @param non-empty-list $arr + */ +function foo3(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-list', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo4(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo5(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/nsrt/bug-5287.php b/tests/PHPStan/Analyser/nsrt/bug-5287.php new file mode 100644 index 0000000000..83bbd544d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5287.php @@ -0,0 +1,61 @@ + $arr + */ +function foo(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('list', $arrSpread); +} + +/** + * @param list $arr + */ +function foo2(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('list', $arrSpread); +} + +/** + * @param non-empty-list $arr + */ +function foo3(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-list', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo4(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-list', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo5(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-list', $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/nsrt/bug-5293.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5293.php rename to tests/PHPStan/Analyser/nsrt/bug-5293.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5304.php b/tests/PHPStan/Analyser/nsrt/bug-5304.php new file mode 100644 index 0000000000..2346517d4d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5304.php @@ -0,0 +1,20 @@ + 0) { + $x += 1; + } + assertType('0.0|1.0', $x); + if($x > 0) { + assertType('1.0', $x); + return 5 / $x; + } + assertType('0.0|1.0', $x); // could be '0.0' when we support float-ranges + + return 1.0; +} + +function greaterEqual(float $y): float { + $x = 0.0; + if($y > 0) { + $x += 1; + } + assertType('0.0|1.0', $x); + if($x >= 0) { + assertType('0.0|1.0', $x); + return 5 / $x; + } + assertType('0.0|1.0', $x); // could be '*NEVER*' when we support float-ranges + + return 1.0; +} + +function smaller(float $y): float { + $x = 0.0; + if($y > 0) { + $x -= 1; + } + assertType('-1.0|0.0', $x); + if($x < 0) { + assertType('-1.0', $x); + return 5 / $x; + } + assertType('-1.0|0.0', $x); // could be '0.0' when we support float-ranges + + return 1.0; +} + +function smallerEqual(float $y): float { + $x = 0.0; + if($y > 0) { + $x -= 1; + } + assertType('-1.0|0.0', $x); + if($x <= 0) { + assertType('-1.0|0.0', $x); + return 5 / $x; + } + assertType('*NEVER*', $x); + + return 1.0; +} diff --git a/tests/PHPStan/Analyser/data/bug-5316.php b/tests/PHPStan/Analyser/nsrt/bug-5316.php similarity index 89% rename from tests/PHPStan/Analyser/data/bug-5316.php rename to tests/PHPStan/Analyser/nsrt/bug-5316.php index b12f0be55b..13dbf2a179 100644 --- a/tests/PHPStan/Analyser/data/bug-5316.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5316.php @@ -20,6 +20,6 @@ function (): void { foreach ($array as $name => $elements) { assertType('bool', count($elements) > 0); - assertType('array', $elements); + assertType('list<1|2|3>', $elements); } }; diff --git a/tests/PHPStan/Analyser/data/bug-5322.php b/tests/PHPStan/Analyser/nsrt/bug-5322.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5322.php rename to tests/PHPStan/Analyser/nsrt/bug-5322.php diff --git a/tests/PHPStan/Analyser/data/bug-5328.php b/tests/PHPStan/Analyser/nsrt/bug-5328.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5328.php rename to tests/PHPStan/Analyser/nsrt/bug-5328.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5336.php b/tests/PHPStan/Analyser/nsrt/bug-5336.php new file mode 100644 index 0000000000..c8373236e1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5336.php @@ -0,0 +1,47 @@ +query = $query; + } +} + +abstract class Test +{ + /** + * @template T of object + * @param class-string $originalClassName + * @return T&Stub + */ + abstract public function createStub(string $originalClassName): Stub; + + public function sayHello(): void + { + $query = $this->createStub(ProxyQueryInterface::class); + assertType('Bug5336\Pager', new Pager($query)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5351.php b/tests/PHPStan/Analyser/nsrt/bug-5351.php new file mode 100644 index 0000000000..a143b8f284 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5351.php @@ -0,0 +1,29 @@ += 8.0 + +namespace Bug5351; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(?string $html): void + { + $html ?? throw new \Exception(); + assertType('string', $html); + } + + /** + * @return never + */ + public function neverReturn() { + throw new \Exception(); + } + + public function doBar(?string $html): void + { + $html ?? $this->neverReturn(); + assertType('string', $html); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5458.php b/tests/PHPStan/Analyser/nsrt/bug-5458.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5458.php rename to tests/PHPStan/Analyser/nsrt/bug-5458.php diff --git a/tests/PHPStan/Analyser/data/bug-5501.php b/tests/PHPStan/Analyser/nsrt/bug-5501.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5501.php rename to tests/PHPStan/Analyser/nsrt/bug-5501.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5508.php b/tests/PHPStan/Analyser/nsrt/bug-5508.php new file mode 100644 index 0000000000..89af5b4b98 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5508.php @@ -0,0 +1,57 @@ + + */ + 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; + } +} + +function (): void { + $result = (new Collection(['book', 'cars']))->map(function($category) { + return $category; + })->all(); + + assertType('array', $result); +}; diff --git a/tests/PHPStan/Analyser/data/bug-5529.php b/tests/PHPStan/Analyser/nsrt/bug-5529.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5529.php rename to tests/PHPStan/Analyser/nsrt/bug-5529.php diff --git a/tests/PHPStan/Analyser/data/bug-5530.php b/tests/PHPStan/Analyser/nsrt/bug-5530.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5530.php rename to tests/PHPStan/Analyser/nsrt/bug-5530.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5552.php b/tests/PHPStan/Analyser/nsrt/bug-5552.php new file mode 100644 index 0000000000..4b05d1b053 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5552.php @@ -0,0 +1,55 @@ +', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (A::class === get_parent_class($mixed)) { + assertType('Bug5552\A|class-string', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (get_parent_class($mixed) === 'Bug5552\A') { + assertType('Bug5552\A|class-string', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if ('Bug5552\A' === get_parent_class($mixed)) { + assertType('Bug5552\A|class-string', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (get_parent_class($o) === A::class) { + assertType('Bug5552\A', $o); + } + if (A::class === get_parent_class($o)) { + assertType('Bug5552\A', $o); + } + + if (get_parent_class($s) === A::class) { + assertType('class-string', $s); + } + if (A::class === get_parent_class($s)) { + assertType('class-string', $s); + } + } +} + +class A {} diff --git a/tests/PHPStan/Analyser/data/bug-5584.php b/tests/PHPStan/Analyser/nsrt/bug-5584.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5584.php rename to tests/PHPStan/Analyser/nsrt/bug-5584.php diff --git a/tests/PHPStan/Analyser/data/bug-560.php b/tests/PHPStan/Analyser/nsrt/bug-560.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-560.php rename to tests/PHPStan/Analyser/nsrt/bug-560.php diff --git a/tests/PHPStan/Analyser/data/bug-5615.php b/tests/PHPStan/Analyser/nsrt/bug-5615.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5615.php rename to tests/PHPStan/Analyser/nsrt/bug-5615.php diff --git a/tests/PHPStan/Analyser/data/bug-5628.php b/tests/PHPStan/Analyser/nsrt/bug-5628.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5628.php rename to tests/PHPStan/Analyser/nsrt/bug-5628.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5668.php b/tests/PHPStan/Analyser/nsrt/bug-5668.php new file mode 100644 index 0000000000..65f66601bd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5668.php @@ -0,0 +1,53 @@ + $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, string $s): void + { + assertType('bool', in_array('test', $in, true)); + assertType('bool', in_array(rand() ? 'test' : 'bar', $in, true)); + assertType('bool', in_array($s, $in, true)); + } + + + /** + * @param non-empty-array $in + */ + function has4(array $in): void + { + assertType('true', in_array('test', $in, true)); + } + + /** + * @param non-empty-array $in + */ + function has5(array $in): void + { + assertType('bool', in_array('test', $in, true)); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5675.php b/tests/PHPStan/Analyser/nsrt/bug-5675.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5675.php rename to tests/PHPStan/Analyser/nsrt/bug-5675.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5698-php7.php b/tests/PHPStan/Analyser/nsrt/bug-5698-php7.php new file mode 100644 index 0000000000..76ac881bc6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5698-php7.php @@ -0,0 +1,16 @@ +', $foo); + assertNativeType('list', $foo); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5698-php8.php b/tests/PHPStan/Analyser/nsrt/bug-5698-php8.php similarity index 92% rename from tests/PHPStan/Analyser/data/bug-5698-php8.php rename to tests/PHPStan/Analyser/nsrt/bug-5698-php8.php index 91a541351d..fb54d36cff 100644 --- a/tests/PHPStan/Analyser/data/bug-5698-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5698-php8.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug5698; diff --git a/tests/PHPStan/Analyser/data/bug-5759.php b/tests/PHPStan/Analyser/nsrt/bug-5759.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5759.php rename to tests/PHPStan/Analyser/nsrt/bug-5759.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5782b-php7.php b/tests/PHPStan/Analyser/nsrt/bug-5782b-php7.php new file mode 100644 index 0000000000..46677a9005 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5782b-php7.php @@ -0,0 +1,27 @@ += 8.0 + +namespace Bug5782bPhp8; + +use function PHPStan\Testing\assertType; + +class X +{ + public function classMethod(): void + { + } + + static public function staticMethod(): void + { + } +} + +function doFoo(): void { + assertType('true', is_callable(['Bug5782bPhp8\X', 'staticMethod'])); + assertType('false', is_callable(['Bug5782bPhp8\X', 'classMethod'])); // should be true on php7, false on php8 + + assertType('true', is_callable('Bug5782bPhp8\X::staticMethod')); + assertType('false', is_callable('Bug5782bPhp8\X::classMethod')); // should be true on php7, false on php8 + + assertType('true', is_callable([new X(), 'staticMethod'])); + assertType('true', is_callable([new X(), 'classMethod'])); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5783.php b/tests/PHPStan/Analyser/nsrt/bug-5783.php new file mode 100644 index 0000000000..d022601b3a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5783.php @@ -0,0 +1,18 @@ + $iterable + * @param-out iterable $iterable + */ + public static function act(iterable &$iterable): void + { + } +} + +function doFoo() { + /** @var HelloWorld[] $a */ + $a = []; + + assertType('array', $a); + IterableHelper::act($a); + assertType('iterable', $a); + +} + + diff --git a/tests/PHPStan/Analyser/data/bug-5817.php b/tests/PHPStan/Analyser/nsrt/bug-5817.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5817.php rename to tests/PHPStan/Analyser/nsrt/bug-5817.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5843.php b/tests/PHPStan/Analyser/nsrt/bug-5843.php new file mode 100644 index 0000000000..9b244969dd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/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&literal-string', $object::class); + switch ($object::class) { + case \DateTime::class: + assertType(\DateTime::class, $object); + break; + case \Throwable::class: + assertType('Throwable', $object); + break; + } + } + +} + +class Bar +{ + + function doFoo(object $object): void + { + match ($object::class) { + \DateTime::class => assertType(\DateTime::class, $object), + \Throwable::class => assertType('Throwable', $object), + }; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5845.php b/tests/PHPStan/Analyser/nsrt/bug-5845.php new file mode 100644 index 0000000000..a5ceeaa53f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5845.php @@ -0,0 +1,17 @@ + 1, + 'b' => 'bee', + ]; + $data = array_merge($arr, $arr); + $data2 = array_merge($arr); + + assertType('array{a: 1, b: \'bee\'}', $arr); + assertType('array{a: 1, b: \'bee\'}', $data); + assertType('array{a: 1, b: \'bee\'}', $data2); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5896.php b/tests/PHPStan/Analyser/nsrt/bug-5896.php new file mode 100644 index 0000000000..7ba0cfc139 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5896.php @@ -0,0 +1,26 @@ +load(); + assertType('array{default?: int}', $y); + if ($x !== $y) { + assertType('array{default?: int}', $y); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5920.php b/tests/PHPStan/Analyser/nsrt/bug-5920.php new file mode 100644 index 0000000000..e80423418f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5920.php @@ -0,0 +1,21 @@ +open(''); + assertType('bool', $reader->read()); + while ($reader->read()) {} + assertType('bool', $reader->read()); + $reader->close(); + $reader->open(''); + assertType('bool', $reader->read()); + while ($reader->read()) {} + assertType('bool', $reader->read()); + $reader->close(); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5961.php b/tests/PHPStan/Analyser/nsrt/bug-5961.php new file mode 100644 index 0000000000..f38c663014 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5961.php @@ -0,0 +1,11 @@ +|null */ + private ?Generator $nullableGenerator; + + /** @var Generator */ + private Generator $regularGenerator; + + public function iterate() : void{ + foreach($this->nullableGenerator as $object){ + assertType(Block::class, $object); + } + + foreach($this->regularGenerator as $object){ + assertType(Block::class, $object); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6001.php b/tests/PHPStan/Analyser/nsrt/bug-6001.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6001.php rename to tests/PHPStan/Analyser/nsrt/bug-6001.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6006.php b/tests/PHPStan/Analyser/nsrt/bug-6006.php new file mode 100644 index 0000000000..e9ad4e2464 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6006.php @@ -0,0 +1,19 @@ + $data */ + $data = [ + 'name' => 'John', + 'dob' => null, + ]; + + $data = array_filter($data, fn(?string $input): bool => (bool)$input); + + assertType('array', $data); +} + + diff --git a/tests/PHPStan/Analyser/data/bug-6070.php b/tests/PHPStan/Analyser/nsrt/bug-6070.php similarity index 81% rename from tests/PHPStan/Analyser/data/bug-6070.php rename to tests/PHPStan/Analyser/nsrt/bug-6070.php index ac1355eee0..aa0d318458 100644 --- a/tests/PHPStan/Analyser/data/bug-6070.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6070.php @@ -16,7 +16,7 @@ public function getNonEmptyArray(): array { $nonEmptyArray[] = 1; } - assertType('non-empty-array>', $nonEmptyArray); + assertType('non-empty-list>', $nonEmptyArray); return $nonEmptyArray; } diff --git a/tests/PHPStan/Analyser/data/bug-6108.php b/tests/PHPStan/Analyser/nsrt/bug-6108.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6108.php rename to tests/PHPStan/Analyser/nsrt/bug-6108.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6138.php b/tests/PHPStan/Analyser/nsrt/bug-6138.php new file mode 100644 index 0000000000..f26da16055 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6138.php @@ -0,0 +1,29 @@ + 1, + 'b' => 2, + 'c' => 3, +]; +$unordered = [ + 0 => 1, + 3 => 2, + 42 => 3, +]; + +shuffle( $indexed ); +shuffle( $associative ); +shuffle( $unordered ); + +assertType( 'non-empty-list<0|1|2>', array_keys( $indexed ) ); +assertType( 'non-empty-list<0|1|2>', array_keys( $associative ) ); +assertType( 'non-empty-list<0|1|2>', array_keys( $unordered ) ); diff --git a/tests/PHPStan/Analyser/nsrt/bug-6170.php b/tests/PHPStan/Analyser/nsrt/bug-6170.php new file mode 100644 index 0000000000..f632e87a49 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6170.php @@ -0,0 +1,28 @@ + $array + **/ + public static function sayHello(array $array): bool + { + assertType('array', $array); + if (rand(0,1)) { + unset($array['bar']['baz']); + } + + assertType('array', $array); + + foreach ($array as $key => $value) { + assertType('array', $value); + assertType('int<0, max>', count($value)); + } + + return false; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6174.php b/tests/PHPStan/Analyser/nsrt/bug-6174.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6174.php rename to tests/PHPStan/Analyser/nsrt/bug-6174.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6196.php b/tests/PHPStan/Analyser/nsrt/bug-6196.php new file mode 100644 index 0000000000..183476932d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6196.php @@ -0,0 +1,27 @@ + zlib_decode("aaaaaaa"))); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-6228.php b/tests/PHPStan/Analyser/nsrt/bug-6228.php new file mode 100644 index 0000000000..aee5112add --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6228.php @@ -0,0 +1,16 @@ +|\DOMNode|\DOMNode[]|string|null $node + */ + public function __construct($node) + { + assertType('array|DOMNode|DOMNodeList|string|null', $node); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6251.php b/tests/PHPStan/Analyser/nsrt/bug-6251.php new file mode 100644 index 0000000000..6909623930 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6251.php @@ -0,0 +1,65 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug6251; + +use function PHPStan\Testing\assertType; + +class Foo +{ + function foo() + { + $var = 1; + if (rand(0, 1)) { + match(1) { + 1 => throw new \Exception(), + }; + } else { + $var = 2; + } + assertType('2', $var); + } + + function bar($a): void + { + $var = 1; + if (rand(0, 1)) { + match($a) { + 'a' => throw new \Error(), + default => throw new \Exception(), + }; + } else { + $var = 2; + } + assertType('2', $var); + } + + function baz($a): void + { + $var = 1; + if (rand(0, 1)) { + match($a) { + 'a' => throw new \Error(), + // throws UnhandledMatchError if not handled + }; + } else { + $var = 2; + } + assertType('2', $var); + } + + function buz($a): void + { + $var = 1; + if (rand(0, 1)) { + match($a) { + 'a' => throw new \Exception(), + default => var_dump($a), + }; + } else { + $var = 2; + } + assertType('1|2', $var); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6293.php b/tests/PHPStan/Analyser/nsrt/bug-6293.php similarity index 94% rename from tests/PHPStan/Analyser/data/bug-6293.php rename to tests/PHPStan/Analyser/nsrt/bug-6293.php index 0a5c8548be..993f7b470e 100644 --- a/tests/PHPStan/Analyser/data/bug-6293.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6293.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug6239; diff --git a/tests/PHPStan/Analyser/nsrt/bug-6294.php b/tests/PHPStan/Analyser/nsrt/bug-6294.php new file mode 100644 index 0000000000..8a363c1bae --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6294.php @@ -0,0 +1,39 @@ + $classString + * @phpstan-return HelloWorld1|null + */ + public function sayHello(object $object, $classString): ?object + { + if ($classString === get_class($object)) { + assertType(HelloWorld1::class, $object); + + return $object; + } + + return null; + } + + /** + * @phpstan-param HelloWorld1 $object + * @phpstan-return HelloWorld1|null + */ + public function sayHello2(object $object, object $object2): ?object + { + if (get_class($object2) === get_class($object)) { + assertType(HelloWorld1::class, $object); + + return $object; + } + + return null; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6305.php b/tests/PHPStan/Analyser/nsrt/bug-6305.php similarity index 79% rename from tests/PHPStan/Analyser/data/bug-6305.php rename to tests/PHPStan/Analyser/nsrt/bug-6305.php index 5cf5d353b4..89bfea9c62 100644 --- a/tests/PHPStan/Analyser/data/bug-6305.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6305.php @@ -1,6 +1,6 @@ |null', $a); } if ([] !== $a && is_array($a) || null === $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if (null === $a || is_array($a) && [] !== $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if (null === $a || [] !== $a && is_array($a)) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } } @@ -128,11 +128,11 @@ function nonEmptyArray1($a): void function nonEmptyArray2($a): void { if (is_array($a) && count($a) > 0 || null === $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if (null === $a || is_array($a) && count($a) > 0) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } } @@ -155,7 +155,7 @@ function inverse($a, $b, $c): void if (null !== $c && (!is_array($c) || count($c) <= 0)) { } else { - assertType('non-empty-array|null', $c); + assertType('non-empty-array|null', $c); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6383.php b/tests/PHPStan/Analyser/nsrt/bug-6383.php new file mode 100644 index 0000000000..c2c06faf56 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6383.php @@ -0,0 +1,36 @@ + 'a', + 'checked' => false, + 'only_in_country' => ['DE'], + ], + [ + 'value' => 'b', + 'checked' => false, + 'only_in_country' => ['BE', 'CH', 'DE', 'DK', 'FR', 'NL', 'SE'], + ], + [ + 'value' => 'c', + 'checked' => false, + ], + ]; + + foreach ($options as $key => $option) { + if (isset($option['only_in_country'])) { + assertType("array{value: 'a', checked: false, only_in_country: array{'DE'}}|array{value: 'b', checked: false, only_in_country: array{'BE', 'CH', 'DE', 'DK', 'FR', 'NL', 'SE'}}", $option); + continue; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-6399.php b/tests/PHPStan/Analyser/nsrt/bug-6399.php similarity index 93% rename from tests/PHPStan/Analyser/data/bug-6399.php rename to tests/PHPStan/Analyser/nsrt/bug-6399.php index bc3eb70c59..50de3ae3f1 100644 --- a/tests/PHPStan/Analyser/data/bug-6399.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6399.php @@ -15,7 +15,7 @@ final public function __destruct(){ if(self::$threadLocalStorage !== null){ assertType('ArrayObject>', self::$threadLocalStorage); if (isset(self::$threadLocalStorage[$h = spl_object_id($this)])) { - assertType('ArrayObject>&hasOffset(int)', self::$threadLocalStorage); + assertType('ArrayObject>', self::$threadLocalStorage); unset(self::$threadLocalStorage[$h]); assertType('ArrayObject>', self::$threadLocalStorage); if(self::$threadLocalStorage->count() === 0){ @@ -54,7 +54,7 @@ public function doBar(array $a): void { assertType('non-empty-array', $a); unset($a[1]); - assertType('array', $a); + assertType('array', $a); } } diff --git a/tests/PHPStan/Analyser/data/bug-6404.php b/tests/PHPStan/Analyser/nsrt/bug-6404.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6404.php rename to tests/PHPStan/Analyser/nsrt/bug-6404.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6433.php b/tests/PHPStan/Analyser/nsrt/bug-6433.php new file mode 100644 index 0000000000..e7346a61f2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/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/nsrt/bug-6462.php b/tests/PHPStan/Analyser/nsrt/bug-6462.php new file mode 100644 index 0000000000..51854608d7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6462.php @@ -0,0 +1,55 @@ +getThis()); + assertType('Bug6462\Child', $child->getThis()); + + if ($base instanceof \Traversable) { + assertType('Bug6462\Base&Traversable', $base->getThis()); + } + + if ($child instanceof \Traversable) { + assertType('Bug6462\Child&Traversable', $child->getThis()); + } + + if ($fixedChild instanceof \Traversable) { + assertType('Bug6462\FixedChild&Traversable', $fixedChild->getThis()); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-6488.php b/tests/PHPStan/Analyser/nsrt/bug-6488.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6488.php rename to tests/PHPStan/Analyser/nsrt/bug-6488.php diff --git a/tests/PHPStan/Analyser/data/bug-6497.php b/tests/PHPStan/Analyser/nsrt/bug-6497.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6497.php rename to tests/PHPStan/Analyser/nsrt/bug-6497.php diff --git a/tests/PHPStan/Analyser/data/bug-6500.php b/tests/PHPStan/Analyser/nsrt/bug-6500.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6500.php rename to tests/PHPStan/Analyser/nsrt/bug-6500.php diff --git a/tests/PHPStan/Analyser/data/bug-6505.php b/tests/PHPStan/Analyser/nsrt/bug-6505.php similarity index 95% rename from tests/PHPStan/Analyser/data/bug-6505.php rename to tests/PHPStan/Analyser/nsrt/bug-6505.php index 0771caea9f..c5d41849d5 100644 --- a/tests/PHPStan/Analyser/data/bug-6505.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6505.php @@ -133,7 +133,7 @@ class Example public function getFactories(): void { - assertType('Bug6505\Collection>', new Collection($this->factories)); + assertType('Bug6505\Collection>', new Collection($this->factories)); } } diff --git a/tests/PHPStan/Analyser/data/bug-651.php b/tests/PHPStan/Analyser/nsrt/bug-651.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-651.php rename to tests/PHPStan/Analyser/nsrt/bug-651.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6556.php b/tests/PHPStan/Analyser/nsrt/bug-6556.php new file mode 100644 index 0000000000..d05d2fdad5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6556.php @@ -0,0 +1,42 @@ +' . $testArg[$option] . '

'; + } + } + + assertType('array{test1?: string, test2?: string, test3?: array{title: string, details: string}}', $testArg); + + if (\array_key_exists('test3', $testArg)) { + $result .= '

'; + $result .= '' . $testArg['test3']['title'] . '
'; + $result .= $testArg['test3']['details']; + $result .= '

'; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6566-types.php b/tests/PHPStan/Analyser/nsrt/bug-6566-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6566-types.php rename to tests/PHPStan/Analyser/nsrt/bug-6566-types.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6576.php b/tests/PHPStan/Analyser/nsrt/bug-6576.php new file mode 100644 index 0000000000..9122438603 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6576.php @@ -0,0 +1,25 @@ + $arr + */ +function alreadyWorks(array $arr): void { + foreach ($arr as $key => $value) { + assertType('int|string', $key); + } +} + +/** + * @template ArrType of array + * + * @param ArrType $arr + */ +function shouldWork(array $arr): void { + foreach ($arr as $key => $value) { + assertType('int|string', $key); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6584.php b/tests/PHPStan/Analyser/nsrt/bug-6584.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6584.php rename to tests/PHPStan/Analyser/nsrt/bug-6584.php diff --git a/tests/PHPStan/Analyser/data/bug-6591.php b/tests/PHPStan/Analyser/nsrt/bug-6591.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6591.php rename to tests/PHPStan/Analyser/nsrt/bug-6591.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6609-83.php b/tests/PHPStan/Analyser/nsrt/bug-6609-83.php new file mode 100644 index 0000000000..4a5f5bb781 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6609-83.php @@ -0,0 +1,58 @@ += 8.3 + +namespace Bug6609Php83; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * This method should return the same type as a parameter passed. + * + * @template T of \DateTime|\DateTimeImmutable + * + * @param T $date + * + * @return T + */ + function modify(\DateTimeInterface $date) { + $date = $date->modify('+1 day'); + assertType('T of DateTime|DateTimeImmutable (method Bug6609Php83\Foo::modify(), argument)', $date); + + return $date; + } + + /** + * This method should return the same type as a parameter passed. + * + * @template T of \DateTime|\DateTimeImmutable + * + * @param T $date + * + * @return T + */ + function modify2(\DateTimeInterface $date) { + $date = $date->modify('invalidd'); + assertType('*NEVER*', $date); + + return $date; + } + + /** + * This method should return the same type as a parameter passed. + * + * @template T of \DateTime|\DateTimeImmutable + * + * @param T $date + * + * @return T + */ + function modify3(\DateTimeInterface $date, string $s) { + $date = $date->modify($s); + assertType('T of DateTime|DateTimeImmutable (method Bug6609Php83\Foo::modify3(), argument)', $date); + + return $date; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6609.php b/tests/PHPStan/Analyser/nsrt/bug-6609.php new file mode 100644 index 0000000000..571f97d988 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6609.php @@ -0,0 +1,58 @@ +modify('+1 day'); + assertType('T of DateTime|DateTimeImmutable (method Bug6609\Foo::modify(), argument)', $date); + + return $date; + } + + /** + * This method should return the same type as a parameter passed. + * + * @template T of \DateTime|\DateTimeImmutable + * + * @param T $date + * + * @return T + */ + function modify2(\DateTimeInterface $date) { + $date = $date->modify('invalidd'); + assertType('false', $date); + + return $date; + } + + /** + * This method should return the same type as a parameter passed. + * + * @template T of \DateTime|\DateTimeImmutable + * + * @param T $date + * + * @return T + */ + function modify3(\DateTimeInterface $date, string $s) { + $date = $date->modify($s); + assertType('((T of DateTime|DateTimeImmutable (method Bug6609\Foo::modify3(), argument))|false)', $date); + + return $date; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6613.php b/tests/PHPStan/Analyser/nsrt/bug-6613.php new file mode 100644 index 0000000000..20abbe4b24 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6613.php @@ -0,0 +1,15 @@ +format('u')); + + assertType("'000'", date('v')); + assertType('non-falsy-string&numeric-string', date_format($dt, 'v')); + assertType('non-falsy-string&numeric-string', $dt->format('v')); +}; diff --git a/tests/PHPStan/Analyser/data/bug-6624.php b/tests/PHPStan/Analyser/nsrt/bug-6624.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6624.php rename to tests/PHPStan/Analyser/nsrt/bug-6624.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6633.php b/tests/PHPStan/Analyser/nsrt/bug-6633.php new file mode 100644 index 0000000000..3689f53ee9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6633.php @@ -0,0 +1,75 @@ +name . $this->version; + } +} + +class ServiceRedis +{ + public function __construct( + private string $name, + private string $version, + private bool $persistent, + ) {} + + public function touchAll() : string{ + return $this->persistent ? $this->name : $this->version; + } +} + +function test(?string $type = NULL) : void { + $types = [ + 'solr' => [ + 'label' => 'SOLR Search', + 'data_class' => CreateServiceSolrData::class, + 'to_entity' => function (CreateServiceSolrData $data) { + assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation"); + return new ServiceSolr($data->name, $data->version); + }, + ], + 'redis' => [ + 'label' => 'Redis', + 'data_class' => CreateServiceRedisData::class, + 'to_entity' => function (CreateServiceRedisData $data) { + assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation"); + return new ServiceRedis($data->name, $data->version, $data->persistent); + }, + ], + ]; + + if ($type === NULL || !isset($types[$type])) { + throw new \RuntimeException("404 or choice form here"); + } + + $data = new $types[$type]['data_class'](); + + $service = $types[$type]['to_entity']($data); + + assertType('Bug6633\ServiceRedis|Bug6633\ServiceSolr', $service); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6654.php b/tests/PHPStan/Analyser/nsrt/bug-6654.php new file mode 100644 index 0000000000..99508b2d6b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6654.php @@ -0,0 +1,19 @@ += 7.3 + +namespace Bug6654; + +use function PHPStan\Testing\assertType; + +class Foo { + function doFoo() { + $data = ''; + $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR; + assertType('non-empty-string',json_encode($data, $flags)); + + if (rand(0, 1)) { + $flags |= JSON_FORCE_OBJECT; + } + + assertType('non-empty-string', json_encode($data, $flags)); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6672.php b/tests/PHPStan/Analyser/nsrt/bug-6672.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6672.php rename to tests/PHPStan/Analyser/nsrt/bug-6672.php diff --git a/tests/PHPStan/Analyser/data/bug-6682.php b/tests/PHPStan/Analyser/nsrt/bug-6682.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6682.php rename to tests/PHPStan/Analyser/nsrt/bug-6682.php diff --git a/tests/PHPStan/Analyser/data/bug-6687.php b/tests/PHPStan/Analyser/nsrt/bug-6687.php similarity index 92% rename from tests/PHPStan/Analyser/data/bug-6687.php rename to tests/PHPStan/Analyser/nsrt/bug-6687.php index d3de13e67c..77ee0f940a 100644 --- a/tests/PHPStan/Analyser/data/bug-6687.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6687.php @@ -28,7 +28,7 @@ function bar(string $a): void function baz(string $a): void { if ($a === BAZ || is_subclass_of($a, BAZ)) { - assertType('class-string', $a); + assertType("'BAZ'|class-string", $a); } } diff --git a/tests/PHPStan/Analyser/data/bug-6695.php b/tests/PHPStan/Analyser/nsrt/bug-6695.php similarity index 88% rename from tests/PHPStan/Analyser/data/bug-6695.php rename to tests/PHPStan/Analyser/nsrt/bug-6695.php index 094c142ce7..396548a4aa 100644 --- a/tests/PHPStan/Analyser/data/bug-6695.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6695.php @@ -11,7 +11,7 @@ enum Foo: int public function toCollection(): void { - assertType('Bug6695\Collection', $this->collect(self::cases())); + assertType('Bug6695\Collection', $this->collect(self::cases())); } /** diff --git a/tests/PHPStan/Analyser/data/bug-6696.php b/tests/PHPStan/Analyser/nsrt/bug-6696.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6696.php rename to tests/PHPStan/Analyser/nsrt/bug-6696.php diff --git a/tests/PHPStan/Analyser/data/bug-6698.php b/tests/PHPStan/Analyser/nsrt/bug-6698.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6698.php rename to tests/PHPStan/Analyser/nsrt/bug-6698.php diff --git a/tests/PHPStan/Analyser/data/bug-6699.php b/tests/PHPStan/Analyser/nsrt/bug-6699.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6699.php rename to tests/PHPStan/Analyser/nsrt/bug-6699.php diff --git a/tests/PHPStan/Analyser/data/bug-6704.php b/tests/PHPStan/Analyser/nsrt/bug-6704.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6704.php rename to tests/PHPStan/Analyser/nsrt/bug-6704.php diff --git a/tests/PHPStan/Analyser/data/bug-6715.php b/tests/PHPStan/Analyser/nsrt/bug-6715.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6715.php rename to tests/PHPStan/Analyser/nsrt/bug-6715.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6728.php b/tests/PHPStan/Analyser/nsrt/bug-6728.php new file mode 100644 index 0000000000..8a47d6f01d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6728.php @@ -0,0 +1,32 @@ + false, 'errorReason' => 'Test']; + } + + return ['success' => true, 'id' => 1]; + } + + public function test(): void + { + $retArr = $this->sayHello(); + assertType('array{success: false, errorReason: string}|array{success: true, id: int}', $retArr); + if ($retArr['success'] === true) { + assertType('array{success: true, id: int}', $retArr); + assertType('true', isset($retArr['id'])); + assertType('int', $retArr['id']); + } else { + assertType('array{success: false, errorReason: string}', $retArr); + assertType('string', $retArr['errorReason']); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6748.php b/tests/PHPStan/Analyser/nsrt/bug-6748.php new file mode 100644 index 0000000000..2001a076f7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6748.php @@ -0,0 +1,22 @@ + $list */ + public function iterateNodes ($list): void + { + foreach($list as $item) { + assertType('DOMNode', $item); + } + } + + /** @param \DOMXPath $path */ + public function xPathQuery ($path) + { + assertType('DOMNodeList|false', $path->query('')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6790.php b/tests/PHPStan/Analyser/nsrt/bug-6790.php new file mode 100644 index 0000000000..b9ea9ee82b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6790.php @@ -0,0 +1,68 @@ += 8.0 + +declare(strict_types = 1); + +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/nsrt/bug-6845.php b/tests/PHPStan/Analyser/nsrt/bug-6845.php new file mode 100644 index 0000000000..767a1e8c8a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6845.php @@ -0,0 +1,46 @@ + $class + * @return T + */ + public function LogAction(string $class) : BaseActionLog + { + return new $class(); + } +} + +interface CoreActionLog +{ + public function SetAdmin(bool $admin) : void; +} + +class ActionLog extends BaseActionLog implements CoreActionLog +{ + public function SetAdmin(bool $admin) : void { } +} + +class CoreApp +{ + /** @return class-string */ + public static function getLogClass() : string + { + return ActionLog::class; + } + + public function Run() : void + { + $requestlog = new RequestLog(); + $actionlog = $requestlog->LogAction(self::getLogClass()); + assertType('Bug6845\BaseActionLog&Bug6845\CoreActionLog', $actionlog); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6859.php b/tests/PHPStan/Analyser/nsrt/bug-6859.php new file mode 100644 index 0000000000..56cd257c5e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6859.php @@ -0,0 +1,36 @@ +', array_keys($body)); + + $someKeys = array_filter( + array_keys($body), + fn ($key) => preg_match("/^somePattern[0-9]+$/", $key) + ); + + assertType('array, (int|string)>', $someKeys); + + if (count($someKeys) > 0) { + return 1; + } + return 0; + } + } + + public function values($body) + { + if (array_key_exists("someParam", $body)) { + assertType('non-empty-list', array_values($body)); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6864.php b/tests/PHPStan/Analyser/nsrt/bug-6864.php new file mode 100644 index 0000000000..d606d302fa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6864.php @@ -0,0 +1,42 @@ += 8.1 + +namespace Bug6864; + +use function PHPStan\Testing\assertType; + +class Model { + +} + +enum Foo { + case Value; +} + +/** + * @template TModel of Model + */ +class ModelHelper { + /** + * @var TModel + */ + private Model $model; + + /** + * @var TModel|null + */ + private ?Model $nullableModel; + + /** + * @param TModel $model + */ + public function __construct(Model $model) { + $this->model = $model; + } + + public function bug(): void { + assertType('class-string&literal-string', $this->model::class); + assertType('(class-string&literal-string)|null', $this->nullableModel::class); + } +} + +assertType('class-string&literal-string', Foo::Value::class); diff --git a/tests/PHPStan/Analyser/nsrt/bug-6870.php b/tests/PHPStan/Analyser/nsrt/bug-6870.php new file mode 100644 index 0000000000..73d6a6dc04 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6870.php @@ -0,0 +1,52 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug6870; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function foo(?string $data): void + { + $data === null ? throw new \Exception() : $data; + assertType('string', $data); + } + + public function buz(?string $data): void + { + $data !== null ? $data : throw new \Exception(); + assertType('string', $data); + } + + public function bar(?string $data): void + { + $data || throw new \Exception(); + assertType('non-falsy-string', $data); + } + + public function bar2(?string $data): void + { + $data or throw new \Exception(); + assertType('non-falsy-string', $data); + } + + public function baz(?string $data): void + { + !$data && throw new \Exception(); + assertType('non-falsy-string', $data); + } + + public function baz2(?string $data): void + { + !$data and throw new \Exception(); + assertType('non-falsy-string', $data); + } + + public function boo(?string $data): void + { + $data ?? throw new \Exception(); + assertType('string', $data); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6889.php b/tests/PHPStan/Analyser/nsrt/bug-6889.php new file mode 100644 index 0000000000..11ecc43b98 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6889.php @@ -0,0 +1,24 @@ +reflection = $reflection; + } + + /** + * @return class-string + */ + public function getClassName(): string { + assertType('class-string', $this->reflection->class); + return $this->reflection->class; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6891.php b/tests/PHPStan/Analyser/nsrt/bug-6891.php new file mode 100644 index 0000000000..8cd7977a59 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6891.php @@ -0,0 +1,42 @@ +|bool $y + * @return integer + */ +function foo($y) +{ + switch (gettype($y)) { + case "integer": + assertType('int', $y); + break; + case "string": + assertType('string', $y); + break; + case "boolean": + assertType('bool', $y); + break; + case "array": + assertType('array', $y); + break; + default: + assertType('*NEVER*', $y); + } + assertType('array|bool|int|string', $y); + return 0; +} + +/** + * @param object|float|null|resource $y + * @return integer + */ +function bar($y) +{ + switch (gettype($y)) { + case "object": + assertType('object', $y); + break; + case "double": + assertType('float', $y); + break; + case "NULL": + assertType('null', $y); + break; + case "resource": + assertType('resource', $y); + break; + default: + assertType('*NEVER*', $y); + } + assertType('float|object|resource|null', $y); + return 0; +} + +/** + * @param int|string|bool $x + * @param int|string|bool $y + */ +function foobarIdentical($x, $y) +{ + if (gettype($x) === 'integer') { + assertType('int', $x); + return; + } + assertType('bool|string', $x); + + if ('boolean' === gettype($x)) { + assertType('bool', $x); + return; + } + + if (gettype($y) === 'string' || gettype($y) === 'integer') { + assertType('int|string', $y); + } +} + +/** + * @param int|string|bool $x + */ +function foobarEqual($x) +{ + if (gettype($x) == 'integer') { + assertType('int', $x); + return; + } + + if ('boolean' == gettype($x)) { + assertType('bool', $x); + return; + } + + assertType('string', $x); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6904.php b/tests/PHPStan/Analyser/nsrt/bug-6904.php new file mode 100644 index 0000000000..748e4954c3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6904.php @@ -0,0 +1,48 @@ +&Selectable + */ + public Collection&Selectable $items; + + /** + * @param Selectable $selectable + * @return TValue + * + * @template TValue + */ + private function matchOne(Selectable $selectable) + { + return $selectable->first(); + } + + public function run(): void + { + assertType('stdClass', $this->matchOne($this->items)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6917.php b/tests/PHPStan/Analyser/nsrt/bug-6917.php new file mode 100644 index 0000000000..8643b72426 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6917.php @@ -0,0 +1,48 @@ + $admin + * @phpstan-return T + */ + public function setAdmin(AdminInterface $admin): object; +} + +class Hello implements HelloInterface +{ + /** @inheritdoc */ + public function setAdmin(AdminInterface $admin): object + { + return $admin->getObject(); + } +} + +class MockObject {} + +class Foo +{ + /** + * @var MockObject&AdminInterface + */ + public $admin; + + public function test(): void + { + $hello = new Hello(); + assertType('stdClass', $hello->setAdmin($this->admin)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6927.php b/tests/PHPStan/Analyser/nsrt/bug-6927.php new file mode 100644 index 0000000000..5e5cf194f9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6927.php @@ -0,0 +1,64 @@ + $params1 + * @param array $params2 + */ + function foo1(array $params1, array $params2): void + { + $params2 = array_merge($params1, $params2); + + assertType('array', $params2); + } + + /** + * @param array $params1 + * @param array $params2 + */ + function foo2(array $params1, array $params2): void + { + $params2 = array_merge($params1, $params2); + + assertType('array', $params2); + } + + /** + * @param array $params1 + * @param array $params2 + */ + function foo3(array $params1, array $params2): void + { + $params2 = array_merge($params1, $params2); + + assertType('array', $params2); + } + + /** + * @param array $params1 + * @param array $params2 + */ + function foo4(array $params1, array $params2): void + { + $params2 = array_merge($params1, $params2); + + assertType('array', $params2); + } + + /** + * @param array{return: int, stdout: string, stderr: string} $params1 + * @param array{return: int, stdout?: string, stderr?: string} $params2 + */ + function foo5(array $params1, array $params2): void + { + $params3 = array_merge($params1, $params2); + + assertType('array{return: int, stdout: string, stderr: string}', $params3); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6936-limit.php b/tests/PHPStan/Analyser/nsrt/bug-6936-limit.php new file mode 100644 index 0000000000..78f946a89e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6936-limit.php @@ -0,0 +1,51 @@ += 8.0 + +namespace Bug6993; + +use function PHPStan\Testing\assertType; + +/** + * @template T + * + * Generic specification interface + */ +interface SpecificationInterface { + /** + * @param T $specificable + */ + public function isSatisfiedBy($specificable): bool; +} + +/** + * @template-extends SpecificationInterface + */ +interface FooSpecificationInterface extends SpecificationInterface +{ +} + +/** + * Class-conctrete specification + */ +class TestSpecification implements FooSpecificationInterface +{ + public function isSatisfiedBy($specificable): bool + { + return true; + } +} + +/** + * @template TSpecifications of SpecificationInterface + * @template TValue + * @template-implements SpecificationInterface + */ +class AndSpecificationValidator implements SpecificationInterface +{ + /** + * @param array $specifications + */ + public function __construct(private array $specifications) + { + } + + public function isSatisfiedBy($specificable): bool + { + foreach ($this->specifications as $specification) { + if (!$specification->isSatisfiedBy($specificable)) { + return false; + } + } + + return true; + } +} + +/** + * Admitted value for FooSpecificationInterface instances + */ +class Foo +{ +} + +/** + * Value not admitted for FooSpecificationInterface instances + */ +class Bar +{ +} + +function (): void { + $and = (new AndSpecificationValidator([new TestSpecification()])); + assertType('Bug6993\AndSpecificationValidator', $and); + $and->isSatisfiedBy(new Foo()); + $and->isSatisfiedBy(new Bar()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7000.php b/tests/PHPStan/Analyser/nsrt/bug-7000.php new file mode 100644 index 0000000000..a2e536a6da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7000.php @@ -0,0 +1,20 @@ +, require-dev?: array} $composer */ + $composer = array(); + foreach (array('require', 'require-dev') as $linkType) { + if (isset($composer[$linkType])) { + assertType('array{require?: array, require-dev?: array}', $composer); + foreach ($composer[$linkType] as $x) {} + } + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7031.php b/tests/PHPStan/Analyser/nsrt/bug-7031.php new file mode 100644 index 0000000000..a325a67d1f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7031.php @@ -0,0 +1,12 @@ +', static fn(int $value): iterable => yield new SomeKey); + assertType('Closure(int): Generator', static function (int $value): iterable { yield new SomeKey; }); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7056.php b/tests/PHPStan/Analyser/nsrt/bug-7056.php new file mode 100644 index 0000000000..4398cc4bf1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7056.php @@ -0,0 +1,33 @@ +', $ref->getName()); + + $property = $ref->getProperty('foo'); + assertType('non-empty-string', $property->getName()); + + $method = $ref->getMethod('a'); + assertType('non-empty-string', $method->getName()); + + $m = new \ReflectionMethod($this, 'a'); + assertType('non-empty-string', $m->getName()); + + $params = $m->getParameters(); + assertType('non-empty-string', $params[0]->getName()); + + $rf = new \ReflectionFunction('Bug7056\fooo'); + assertType('non-empty-string', $rf->getName()); + } +} + +function fooo() {} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7068.php b/tests/PHPStan/Analyser/nsrt/bug-7068.php new file mode 100644 index 0000000000..97c0bda6d9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7068.php @@ -0,0 +1,25 @@ + ...$arrays + * @return array + */ + function merge(array ...$arrays): array { + return array_merge(...$arrays); + } + + public function doFoo(): void + { + assertType('array<1|2|3|4|5>', $this->merge([1, 2], [3, 4], [5])); + assertType('array<1|2|\'bar\'|\'foo\'>', $this->merge([1, 2], ['foo', 'bar'])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7078.php b/tests/PHPStan/Analyser/nsrt/bug-7078.php new file mode 100644 index 0000000000..5287dcc7cf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7078.php @@ -0,0 +1,37 @@ += 8.0 + +namespace Bug7078; + +use function PHPStan\Testing\assertType; + +/** + * @template-covariant T + */ +final class TypeDefault +{ + /** @param T $defaultValue */ + public function __construct(private mixed $defaultValue) + { + } + + /** @return T */ + public function parse(): mixed + { + return $this->defaultValue; + } +} + +interface Param { + /** + * @param TypeDefault $type + * + * @template T + * @return T + */ + public function get(TypeDefault ...$type); +} + +function (Param $p) { + $result = $p->get(new TypeDefault(1), new TypeDefault('a')); + assertType('1|\'a\'', $result); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7096.php b/tests/PHPStan/Analyser/nsrt/bug-7096.php new file mode 100644 index 0000000000..7659a2e04a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7096.php @@ -0,0 +1,32 @@ += 8.0 + +namespace Bug7096; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param class-string<\BackedEnum> $enumClass + */ + function enumFromString(string $enumClass, string|int $value): void + { + assertType(\BackedEnum::class, $enumClass::from($value)); + assertType(\BackedEnum::class . '|null', $enumClass::tryFrom($value)); + } + + function customStaticMethod(): static + { + return new static(); + } + + /** + * @param class-string $class + */ + function test(string $class): void + { + assertType(self::class, $class::customStaticMethod()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7106.php b/tests/PHPStan/Analyser/nsrt/bug-7106.php new file mode 100644 index 0000000000..50e6c0e86f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7106.php @@ -0,0 +1,22 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug7106; + +use function PHPStan\Testing\assertType; +use function openssl_error_string; + +Class Example +{ + public function openSslError(string $signature): string + { + assertType('string|false', openssl_error_string()); + + if (false === \openssl_error_string()) { + assertType('false', openssl_error_string()); + openssl_sign('1', $signature, ''); + assertType('string|false', openssl_error_string()); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7115.php b/tests/PHPStan/Analyser/nsrt/bug-7115.php new file mode 100644 index 0000000000..40263db3fa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7115.php @@ -0,0 +1,36 @@ + + */ + public function getThings(): array { return []; } + + public function doFoo(): void + { + $a = $this->getThings(); + $b = []; + $c = []; + $d = []; + + array_push($b, ...$a); + + foreach ($a as $thing) { + $c[] = $thing; + array_push($d, $thing); + } + + assertType('list', $b); + assertType('list', $c); + assertType('list', $d); + } + +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7141.php b/tests/PHPStan/Analyser/nsrt/bug-7141.php new file mode 100644 index 0000000000..2cf34a5733 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7141.php @@ -0,0 +1,23 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug7141; + +use stdClass; +use function PHPStan\Testing\assertType; + +interface Container +{ + /** + * @template T + * @return ($id is class-string ? T : mixed) + */ + public function get(string $id): mixed; +} + + +function(Container $c) { + assertType('mixed', $c->get('test')); + assertType('stdClass', $c->get(stdClass::class)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php b/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php new file mode 100644 index 0000000000..c60d776d38 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php @@ -0,0 +1,49 @@ + array( + 'method' => CURLOPT_CUSTOMREQUEST, + 'content' => CURLOPT_POSTFIELDS, + 'header' => CURLOPT_HTTPHEADER, + 'timeout' => CURLOPT_TIMEOUT, + ), + 'ssl' => array( + 'cafile' => CURLOPT_CAINFO, + 'capath' => CURLOPT_CAPATH, + 'verify_peer' => CURLOPT_SSL_VERIFYPEER, + 'verify_peer_name' => CURLOPT_SSL_VERIFYHOST, + 'local_cert' => CURLOPT_SSLCERT, + 'local_pk' => CURLOPT_SSLKEY, + 'passphrase' => CURLOPT_SSLKEYPASSWD, + ), + ); + + /** + * @param array{http: array{header: string[], proxy?: string, request_fulluri: bool}, ssl?: mixed[]} $options + */ + public function test3(array $options): void + { + $curlHandle = curl_init(); + foreach (self::$options as $type => $curlOptions) { + foreach ($curlOptions as $name => $curlOption) { + \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); + if (isset($options[$type][$name])) { + \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); + if ($type === 'ssl' && $name === 'verify_peer_name') { + \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); + curl_setopt($curlHandle, $curlOption, $options[$type][$name] === true ? 2 : $options[$type][$name]); + } else { + \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); + curl_setopt($curlHandle, $curlOption, $options[$type][$name]); + } + } + } + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7144.php b/tests/PHPStan/Analyser/nsrt/bug-7144.php new file mode 100644 index 0000000000..4c4e301394 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7144.php @@ -0,0 +1,51 @@ +, bar?: array} $arr + */ + public function test1(array $arr): void + { + foreach (['foo', 'bar'] as $key) { + \PHPStan\Testing\assertType('array{foo?: array, bar?: array}', $arr); + foreach ($arr[$key] as $x) {} + } + } + + /** + * @param array{foo?: array, bar?: array} $arr + */ + public function test2(array $arr): void + { + foreach (['foo', 'bar', 'baz'] as $key) { + \PHPStan\Testing\assertType('array{foo?: array, bar?: array}', $arr); + } + } + + /** + * @param array{foo?: array, bar?: array} $arr + */ + public function test3(array $arr): void + { + foreach (['foo', 'bar', 'baz'] as $key) { + \PHPStan\Testing\assertType('array{foo?: array, bar?: array}', $arr); + foreach ($arr[$key] as $x) {} + } + } + + /** + * @param 'foo'|'bar'|'baz' $key + * @param array{foo: array, bar: array} $arr + */ + public function test4(string $key, array $arr): void + { + if ($arr[$key] === []) { + return; + } + \PHPStan\Testing\assertType('array{foo: array, bar: array}', $arr); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7153.php b/tests/PHPStan/Analyser/nsrt/bug-7153.php new file mode 100644 index 0000000000..902764f977 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7153.php @@ -0,0 +1,36 @@ + 0 ? 'bleh' : null; +} + +function blih(string $blah, string $bleh): void +{ + echo 'test'; +} + +function () { + $data = [blah(), bleh()]; + + assertType('array{string, string|null}', $data); + + if (in_array(null, $data, true)) { + assertType('array{string, string|null}', $data); + throw new Exception(); + } + + assertType('array{string, string}', $data); + + blih($data[0], $data[1]); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7162.php b/tests/PHPStan/Analyser/nsrt/bug-7162.php new file mode 100644 index 0000000000..9b1fb4f54b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7162.php @@ -0,0 +1,37 @@ += 8.1 + +declare(strict_types=1); + +namespace Bug7162; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + + /** + * @param class-string<\BackedEnum> $enumClassString + */ + public static function casesWithLabel(string $enumClassString): void + { + foreach ($enumClassString::cases() as $unitEnum) { + assertType('BackedEnum', $unitEnum); + } + } +} + +enum Test{ + case ONE; +} + +/** + * @phpstan-template TEnum of \UnitEnum + * @phpstan-param TEnum $case + */ +function dumpCases(\UnitEnum $case) : void{ + assertType('list', $case::cases()); +} + +function dumpCases2(Test $case) : void{ + assertType('array{Bug7162\\Test::ONE}', $case::cases()); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7167.php b/tests/PHPStan/Analyser/nsrt/bug-7167.php new file mode 100644 index 0000000000..b62b834988 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7167.php @@ -0,0 +1,14 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug7167; + +use function PHPStan\Testing\assertType; + +enum Foo { + case Value; +} + +assertType('class-string', get_class(Foo::Value)); + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7176.php b/tests/PHPStan/Analyser/nsrt/bug-7176.php new file mode 100644 index 0000000000..29a5f83184 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7176.php @@ -0,0 +1,34 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug7176Types; + +use function PHPStan\Testing\assertType; + +enum Suit +{ + case Hearts; + case Diamonds; + case Clubs; + case Spades; +} + +function test(Suit $x): string { + if ($x === Suit::Clubs) { + assertType('Bug7176Types\Suit::Clubs', $x); + return 'WORKS'; + } + assertType('Bug7176Types\Suit~Bug7176Types\Suit::Clubs', $x); + + if (in_array($x, [Suit::Spades], true)) { + assertType('Bug7176Types\Suit::Spades', $x); + return 'DOES NOT WORK'; + } + assertType('Bug7176Types\Suit~(Bug7176Types\Suit::Clubs|Bug7176Types\Suit::Spades)', $x); + + return match ($x) { + Suit::Hearts => 'a', + Suit::Diamonds => 'b', + }; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7210.php b/tests/PHPStan/Analyser/nsrt/bug-7210.php new file mode 100644 index 0000000000..9509b3e688 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7210.php @@ -0,0 +1,34 @@ + new \DateTime()]; + + if (\array_key_exists('team_name', $roleUpdates)) { + $fieldUpdates['team_name'] = $roleUpdates['team_name']; + } + + if (isset($roleUpdates['name'])) { + $fieldUpdates['name'] = $roleUpdates['name']; + } + + saveUpdates($roleUpdates['id'], $fieldUpdates); +} + +/** + * @param array{id: string, name?: string, team_name?: string|null} $roleUpdates + */ +function processUpdates2(array $roleUpdates): void { + assertType('array{id: string, name?: string, team_name?: string|null}', $roleUpdates); + if (!isset($roleUpdates['team_name'])) { + + } + + assertType('array{id: string, name?: string, team_name?: string|null}', $roleUpdates); + + $fieldUpdates = ['updated_at' => new \DateTime()]; + + if (\array_key_exists('team_name', $roleUpdates)) { + $fieldUpdates['team_name'] = $roleUpdates['team_name']; + } + + if (isset($roleUpdates['name'])) { + $fieldUpdates['name'] = $roleUpdates['name']; + } + + saveUpdates($roleUpdates['id'], $fieldUpdates); +} + +/** + * @param array $updatedFields + */ +function saveUpdates(string $id, array $updatedFields): void { + // ... +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7239-php8.php b/tests/PHPStan/Analyser/nsrt/bug-7239-php8.php new file mode 100644 index 0000000000..24564d4233 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7239-php8.php @@ -0,0 +1,38 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug7239php8; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + /** + * @param string[] $strings + */ + public function sayHello(array $arr, $strings): void + { + assertType('*ERROR*', max([])); + assertType('*ERROR*', min([])); + + if (count($arr) > 0) { + assertType('mixed', max($arr)); + assertType('mixed', min($arr)); + } else { + assertType('*ERROR*', max($arr)); + assertType('*ERROR*', min($arr)); + } + + assertType('array', max([], $arr)); + assertType('array', min([], $arr)); + + if (count($strings) > 0) { + assertType('string', max($strings)); + assertType('string', min($strings)); + } else { + assertType('*ERROR*', max($strings)); + assertType('*ERROR*', min($strings)); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7239.php b/tests/PHPStan/Analyser/nsrt/bug-7239.php new file mode 100644 index 0000000000..62bf97a119 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7239.php @@ -0,0 +1,38 @@ + 0) { + assertType('mixed', max($arr)); + assertType('mixed', min($arr)); + } else { + assertType('false', max($arr)); + assertType('false', min($arr)); + } + + assertType('array', max([], $arr)); + assertType('array', min([], $arr)); + + if (count($strings) > 0) { + assertType('string', max($strings)); + assertType('string', min($strings)); + } else { + assertType('false', max($strings)); + assertType('false', min($strings)); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7244.php b/tests/PHPStan/Analyser/nsrt/bug-7244.php new file mode 100644 index 0000000000..2a7ae03b51 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7244.php @@ -0,0 +1,26 @@ + $arguments + */ + public function getFormat(array $arguments): string { + $value = \is_string($arguments['format'] ?? null) ? $arguments['format'] : 'Y-m-d'; + assertType('string', $value); + return $value; + } + + /** + * @param array $arguments + */ + public function getFormatWithoutFallback(array $arguments): string { + $value = \is_string($arguments['format']) ? $arguments['format'] : 'Y-m-d'; + assertType('string', $value); + return $value; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7281.php b/tests/PHPStan/Analyser/nsrt/bug-7281.php new file mode 100644 index 0000000000..83b9014e45 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7281.php @@ -0,0 +1,101 @@ + $array + * @param (callable(T, K): U) $fn + * + * @return array + */ +function map(array $array, callable $fn): array +{ + /** @phpstan-ignore-next-line */ + return array_map($fn, $array); +} + +function (): void { + /** + * @var array> $timelines + */ + $timelines = []; + + assertType('array>', map( + $timelines, + static function (Timeline $timeline): Timeline { + return $timeline; + }, + )); + assertType('array>', map( + $timelines, + static function ($timeline) { + return $timeline; + }, + )); + + assertType('array>', map( + $timelines, + static fn (Timeline $timeline): Timeline => $timeline, + )); + assertType('array>', map( + $timelines, + static fn ($timeline) => $timeline, + )); + + assertType('array>', array_map( + static function (Timeline $timeline): Timeline { + return $timeline; + }, + $timelines, + )); + assertType('array>', array_map( + static function ($timeline) { + return $timeline; + }, + $timelines, + )); + + assertType('array>', array_map( + static fn (Timeline $timeline): Timeline => $timeline, + $timelines, + )); + assertType('array>', array_map( + static fn ($timeline) => $timeline, + $timelines, + )); + + assertType('array>', array_map( + static function (Timeline $timeline) { + return $timeline; + }, + $timelines, + )); + assertType('array>', array_map( + static function ($timeline): Timeline { + return $timeline; + }, + $timelines, + )); + + assertType('array>', array_map( + static fn (Timeline $timeline) => $timeline, + $timelines, + )); + assertType('array>', array_map( + static fn ($timeline): Timeline => $timeline, + $timelines, + )); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7291.php b/tests/PHPStan/Analyser/nsrt/bug-7291.php new file mode 100644 index 0000000000..cae3e945b3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7291.php @@ -0,0 +1,25 @@ +foo; + + assertType('stdClass|null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7301.php b/tests/PHPStan/Analyser/nsrt/bug-7301.php new file mode 100644 index 0000000000..334c6d989d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7301.php @@ -0,0 +1,29 @@ + + */ + $arg = function () { + return ['key' => 'value']; + }; + + $result = templated($arg); + + assertType('array', $result); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7341-php-84.php b/tests/PHPStan/Analyser/nsrt/bug-7341-php-84.php new file mode 100644 index 0000000000..a756e468d5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7341-php-84.php @@ -0,0 +1,29 @@ += 8.4 + +namespace Bug7341Php84; + +use function PHPStan\Testing\assertType; + +final class CsvWriterTerminate extends \php_user_filter +{ + /** + * @param resource $in + * @param resource $out + * @param int $consumed + * @param bool $closing + */ + public function filter($in, $out, &$consumed, $closing): int + { + while ($bucket = stream_bucket_make_writeable($in)) { + assertType('StreamBucket', $bucket); + + if (isset($this->params['terminate'])) { + $bucket->data = preg_replace('/([^\r])\n/', '$1'.$this->params['terminate'], $bucket->data); + } + $consumed += $bucket->datalen; + stream_bucket_append($out, $bucket); + } + + return \PSFS_PASS_ON; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7341.php b/tests/PHPStan/Analyser/nsrt/bug-7341.php new file mode 100644 index 0000000000..45b3efe97b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7341.php @@ -0,0 +1,29 @@ +params['terminate'])) { + $bucket->data = preg_replace('/([^\r])\n/', '$1'.$this->params['terminate'], $bucket->data); + } + $consumed += $bucket->datalen; + stream_bucket_append($out, $bucket); + } + + return \PSFS_PASS_ON; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7353.php b/tests/PHPStan/Analyser/nsrt/bug-7353.php new file mode 100644 index 0000000000..8fa84b32c0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7353.php @@ -0,0 +1,14 @@ + $data */ + public function sayHello(array $data): void + { + assertType('array', $data); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7374.php b/tests/PHPStan/Analyser/nsrt/bug-7374.php new file mode 100644 index 0000000000..af1183358a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7374.php @@ -0,0 +1,18 @@ +&literal-string */ + public static function getClass(): string { + return self::class; + } + + public function build(): void { + $class = self::getClass(); + assertType(self::class, new $class()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php new file mode 100644 index 0000000000..1b283a7990 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7387.php @@ -0,0 +1,117 @@ + $intRange + */ + public function inputTypes(int $i, float $f, string $s, int $intRange) { + // https://3v4l.org/iXaDX + assertType('numeric-string', sprintf('%.14F', $i)); + assertType('numeric-string', sprintf('%.14F', $f)); + assertType('numeric-string', sprintf('%.14F', $s)); + + assertType('numeric-string', sprintf('%1.14F', $i)); + assertType('numeric-string', sprintf('%2.14F', $f)); + assertType('numeric-string', sprintf('%3.14F', $s)); + + assertType('numeric-string', sprintf('%14F', $i)); + assertType('numeric-string', sprintf('%14F', $f)); + assertType('numeric-string', sprintf('%14F', $s)); + + assertType("'-1'|'0'|'1'|'2'|'3'|'4'|'5'", sprintf('%s', $intRange)); + assertType("' 0'|' 1'|' 2'|' 3'|' 4'|' 5'|'-1'", sprintf('%2s', $intRange)); + } + + public function specifiers(int $i) { + // https://3v4l.org/fmVIg + assertType('lowercase-string&numeric-string&uppercase-string', sprintf('%14s', $i)); + + assertType('lowercase-string&numeric-string', sprintf('%d', $i)); + + assertType('lowercase-string&numeric-string', sprintf('%14b', $i)); + assertType('lowercase-string&non-falsy-string', sprintf('%14c', $i)); // binary string + assertType('lowercase-string&numeric-string', sprintf('%14d', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14e', $i)); + assertType('numeric-string', sprintf('%14E', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14f', $i)); + assertType('numeric-string', sprintf('%14F', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14g', $i)); + assertType('numeric-string', sprintf('%14G', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14h', $i)); + assertType('numeric-string', sprintf('%14H', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14o', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14u', $i)); + assertType('lowercase-string&numeric-string', sprintf('%14x', $i)); + assertType('numeric-string', sprintf('%14X', $i)); + + } + + /** + * @param positive-int $posInt + * @param negative-int $negInt + * @param int<1, 5> $nonZeroIntRange + * @param int<-1, 5> $intRange + */ + public function positionalArgs($mixed, int $i, float $f, string $s, int $posInt, int $negInt, int $nonZeroIntRange, int $intRange) { + // https://3v4l.org/vVL0c + assertType('lowercase-string&numeric-string&uppercase-string', sprintf('%2$6s', $mixed, $i)); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', sprintf('%2$6s', $mixed, $posInt)); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', sprintf('%2$6s', $mixed, $negInt)); + assertType("' 1'|' 2'|' 3'|' 4'|' 5'", sprintf('%2$6s', $mixed, $nonZeroIntRange)); + + // https://3v4l.org/1ECIq + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, 1)); + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, $i)); + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, $posInt)); + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, $negInt)); + assertType("' 0'|' 1'|' 2'|' 3'|' 4'|' 5'|' -1'", sprintf('%2$6s', $mixed, $intRange)); + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, $nonZeroIntRange)); + + assertType("' 1'", sprintf('%2$6s', $mixed, 1)); + assertType("' 1'", sprintf('%2$6s', $mixed, '1')); + assertType("' abc'", sprintf('%2$6s', $mixed, 'abc')); + assertType("' 0'|' 1'|' 2'|' 3'|' 4'|' 5'|' -1'", sprintf('%2$6s', $mixed, $intRange)); + assertType("'1'", sprintf('%2$s', $mixed, 1)); + assertType("'1'", sprintf('%2$s', $mixed, '1')); + assertType("'abc'", sprintf('%2$s', $mixed, 'abc')); + assertType("'-1'|'0'|'1'|'2'|'3'|'4'|'5'", sprintf('%2$s', $mixed, $intRange)); + + assertType('numeric-string', sprintf('%2$.14F', $mixed, $i)); + assertType('numeric-string', sprintf('%2$.14F', $mixed, $f)); + assertType('numeric-string', sprintf('%2$.14F', $mixed, $s)); + + assertType('numeric-string', sprintf('%2$1.14F', $mixed, $i)); + assertType('numeric-string', sprintf('%2$2.14F', $mixed, $f)); + assertType('numeric-string', sprintf('%2$3.14F', $mixed, $s)); + + assertType('numeric-string', sprintf('%2$14F', $mixed, $i)); + assertType('numeric-string', sprintf('%2$14F', $mixed, $f)); + assertType('numeric-string', sprintf('%2$14F', $mixed, $s)); + + assertType('string', sprintf('%10$14F', $mixed, $s)); + } + + public function invalidPositionalArgFormat($mixed, string $s) { + assertType('string', sprintf('%0$14F', $mixed, $s)); + } + + public function escapedPercent(int $i) { + // https://3v4l.org/2m50L + assertType('lowercase-string&non-falsy-string', sprintf("%%d", $i)); + } + + public function vsprintf(array $array) + { + assertType('lowercase-string&numeric-string', vsprintf("%4d", explode('-', '1988-8-1'))); + assertType('numeric-string', vsprintf("%4d", $array)); + assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123'])); + assertType('\'123\'', vsprintf("%s", ['123'])); + // too many arguments.. php silently allows it + assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123', '456'])); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7391.php b/tests/PHPStan/Analyser/nsrt/bug-7391.php new file mode 100644 index 0000000000..7ad5eb7600 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7391.php @@ -0,0 +1,18 @@ + $class + */ +function bar($class): string +{ + assertType('class-string', ltrim($class, '\\')); +} + +/** + * @param class-string $class + * @return class-string + */ +function foo($class): string +{ + assertType('class-string', ltrim($class, '\\')); + assertType("'Bug7483\\\\A'", ltrim(A::class, '\\')); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7490.php b/tests/PHPStan/Analyser/nsrt/bug-7490.php new file mode 100644 index 0000000000..db94bd831f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7490.php @@ -0,0 +1,10 @@ +> -1); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7492.php b/tests/PHPStan/Analyser/nsrt/bug-7492.php new file mode 100644 index 0000000000..67fb2909d9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7492.php @@ -0,0 +1,14 @@ + '', 'login' => '', 'password' => '', 'name' => '']; + assertType('non-empty-array', $x); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7501.php b/tests/PHPStan/Analyser/nsrt/bug-7501.php new file mode 100644 index 0000000000..01b59c0901 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7501.php @@ -0,0 +1,19 @@ +> + */ +class FooFilterIterator extends FilterIterator +{ + /** + * @param Iterator $iterator + */ + public function __construct(Iterator $iterator) + { + parent::__construct($iterator); + } + + public function accept(): bool + { + return true; + } +} + +function doFoo() { + $generator = static function (): Generator { + yield true => true; + yield false => false; + yield new stdClass => new StdClass; + yield [] => []; + }; + + $iterator = new FooFilterIterator($generator()); + + assertType('array{}|bool|stdClass', $iterator->key()); + assertType('array{}|bool|stdClass', $iterator->current()); + + $generator = static function (): Generator { + yield true => true; + yield false => false; + }; + + $iterator = new FooFilterIterator($generator()); + + assertType('bool', $iterator->key()); + assertType('bool', $iterator->current()); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7547.php b/tests/PHPStan/Analyser/nsrt/bug-7547.php new file mode 100644 index 0000000000..c2a7a3ad80 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7547.php @@ -0,0 +1,17 @@ +_load(); + assertType('static(Bug7550\Foo)', $res); + if ($res !== $this) { + throw new \Exception('y'); + } + + assertType('$this(Bug7550\Foo)', $this); + assertType('$this(Bug7550\Foo)', $res); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7563.php b/tests/PHPStan/Analyser/nsrt/bug-7563.php new file mode 100644 index 0000000000..da259a876f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7563.php @@ -0,0 +1,35 @@ +', mb_str_split($v, 1)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7607.php b/tests/PHPStan/Analyser/nsrt/bug-7607.php new file mode 100644 index 0000000000..e8d6aa5911 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7607.php @@ -0,0 +1,61 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug7607; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + /** + * Determine if the given value is "blank". + * + * @param mixed $value + * @return bool + * + * @phpstan-assert-if-false !(null|''|array{}) $value + */ + public function blank($value) + { + if (is_null($value)) { + return true; + } + + if (is_string($value)) { + return trim($value) === ''; + } + + if (is_numeric($value) || is_bool($value)) { + return false; + } + + if ($value instanceof Countable) { + return count($value) === 0; + } + + return empty($value); + } + + public function getValue(): string|null + { + return 'value'; + } + + public function getUrlForCurrentRequest(): string|null + { + return rand(0,1) === 1 ? 'string' : null; + } + + public function isUrl(string|null $url): bool + { + if ($this->blank($url = $url ?? $this->getUrlForCurrentRequest())) { + return false; + } + + assertType('non-empty-string', $url); + $parsed = parse_url(http://github.com/$url); + + return is_array($parsed); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7621-1.php b/tests/PHPStan/Analyser/nsrt/bug-7621-1.php new file mode 100644 index 0000000000..e40ff83e62 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7621-1.php @@ -0,0 +1,149 @@ + [ + self::GROUP_PUBLIC_CONSTANTS, + self::GROUP_PROTECTED_CONSTANTS, + self::GROUP_PRIVATE_CONSTANTS, + ], + self::GROUP_SHORTCUT_STATIC_PROPERTIES => [ + self::GROUP_PUBLIC_STATIC_PROPERTIES, + self::GROUP_PROTECTED_STATIC_PROPERTIES, + self::GROUP_PRIVATE_STATIC_PROPERTIES, + ], + self::GROUP_SHORTCUT_PROPERTIES => [ + self::GROUP_SHORTCUT_STATIC_PROPERTIES, + self::GROUP_PUBLIC_PROPERTIES, + self::GROUP_PROTECTED_PROPERTIES, + self::GROUP_PRIVATE_PROPERTIES, + ], + self::GROUP_SHORTCUT_PUBLIC_METHODS => [ + self::GROUP_PUBLIC_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PUBLIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_METHODS, + self::GROUP_PUBLIC_METHODS, + ], + self::GROUP_SHORTCUT_PROTECTED_METHODS => [ + self::GROUP_PROTECTED_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_METHODS, + self::GROUP_PROTECTED_METHODS, + ], + self::GROUP_SHORTCUT_PRIVATE_METHODS => [ + self::GROUP_PRIVATE_STATIC_METHODS, + self::GROUP_PRIVATE_METHODS, + ], + self::GROUP_SHORTCUT_FINAL_METHODS => [ + self::GROUP_PUBLIC_FINAL_METHODS, + self::GROUP_PROTECTED_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + ], + self::GROUP_SHORTCUT_ABSTRACT_METHODS => [ + self::GROUP_PUBLIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + ], + self::GROUP_SHORTCUT_STATIC_METHODS => [ + self::GROUP_STATIC_CONSTRUCTORS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_METHODS, + self::GROUP_PROTECTED_STATIC_METHODS, + self::GROUP_PRIVATE_STATIC_METHODS, + ], + self::GROUP_SHORTCUT_METHODS => [ + self::GROUP_SHORTCUT_FINAL_METHODS, + self::GROUP_SHORTCUT_ABSTRACT_METHODS, + self::GROUP_SHORTCUT_STATIC_METHODS, + self::GROUP_CONSTRUCTOR, + self::GROUP_DESTRUCTOR, + self::GROUP_PUBLIC_METHODS, + self::GROUP_PROTECTED_METHODS, + self::GROUP_PRIVATE_METHODS, + self::GROUP_MAGIC_METHODS, + ], + ]; + + /** + * @param array $supportedGroups + * @return array + */ + public function unpackShortcut(string $shortcut, array $supportedGroups): array + { + $groups = []; + + foreach (self::SHORTCUTS[$shortcut] as $groupOrShortcut) { + if (in_array($groupOrShortcut, $supportedGroups, true)) { + $groups[] = $groupOrShortcut; + assertType("array{'public final methods', 'protected final methods', 'public static final methods', 'protected static final methods'}", self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]); + } elseif ( + !array_key_exists($groupOrShortcut, self::SHORTCUTS) + ) { + // Nothing + assertType("array{constants: array{'public constants', 'protected constants', 'private constants'}, static properties: array{'public static properties', 'protected static properties', 'private static properties'}, properties: array{'static properties', 'public properties', 'protected properties', 'private properties'}, all public methods: array{'public final methods', 'public static final methods', 'public abstract methods', 'public static abstract methods', 'public static methods', 'public methods'}, all protected methods: array{'protected final methods', 'protected static final methods', 'protected abstract methods', 'protected static abstract methods', 'protected static methods', 'protected methods'}, all private methods: array{'private static methods', 'private methods'}, final methods: array{'public final methods', 'protected final methods', 'public static final methods', 'protected static final methods'}, abstract methods: array{'public abstract methods', 'protected abstract methods', 'public static abstract methods', 'protected static abstract methods'}, static methods: array{'static constructors', 'public static final methods', 'protected static final methods', 'public static abstract methods', 'protected static abstract methods', 'public static methods', 'protected static methods', 'private static methods'}, methods: array{'final methods', 'abstract methods', 'static methods', 'constructor', 'destructor', 'public methods', 'protected methods', 'private methods', 'magic methods'}}", self::SHORTCUTS); + assertType("array{'public final methods', 'protected final methods', 'public static final methods', 'protected static final methods'}", self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]); + } else { + $groups = array_merge($groups, $this->unpackShortcut($groupOrShortcut, $supportedGroups)); + assertType("array{'public final methods', 'protected final methods', 'public static final methods', 'protected static final methods'}", self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]); + } + } + + return $groups; + } + +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7621-2.php b/tests/PHPStan/Analyser/nsrt/bug-7621-2.php new file mode 100644 index 0000000000..3a7926aa33 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7621-2.php @@ -0,0 +1,23 @@ + ['foo', 'bar'] ]; + + public function foo(): void + { + assertType("array{'foo', 'bar'}", self::FOO['foo']); + $keys = [0, 1, 2]; + foreach ($keys as $key) { + if (array_key_exists($key, self::FOO['foo'])) { + assertType("array{'foo', 'bar'}", self::FOO['foo']); + } else { + assertType("array{'foo', 'bar'}", self::FOO['foo']); + } + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7621-3.php b/tests/PHPStan/Analyser/nsrt/bug-7621-3.php new file mode 100644 index 0000000000..d6811657ee --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7621-3.php @@ -0,0 +1,20 @@ + ['foo', 'bar'] ]; + + + /** @param 'foo'|'bar' $key */ + public function foo(string $key): void + { + if (!array_key_exists($key, self::FOO)) { + assertType("array{foo: array{'foo', 'bar'}}", self::FOO); + assertType("array{'foo', 'bar'}", self::FOO['foo']); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7639.php b/tests/PHPStan/Analyser/nsrt/bug-7639.php new file mode 100644 index 0000000000..68c575a62d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7639.php @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug7663; + +use function PHPStan\Testing\assertType; + +class HelloWorld8 +{ + /** + * @param 'de_DE'|'pretty-long' $str + */ + public function sayHello($str): void + { + assertType("''", substr('de_DE', 5, -5)); + assertType("'y'", substr('pretty-long', 5, -5)); + assertType("''|'y'", substr($str, 5, -5)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7663.php b/tests/PHPStan/Analyser/nsrt/bug-7663.php new file mode 100644 index 0000000000..3dda66aeca --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7663.php @@ -0,0 +1,21 @@ += 8.0 + +namespace bug7685; + +use function PHPStan\Testing\assertType; + +interface Reader { + public function getFilePath(): string|false; +} + +function bug7685(Reader $reader): void { + $filePath = $reader->getFilePath(); + if (false !== (bool) $filePath) { + assertType('non-falsy-string', $filePath); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7688.php b/tests/PHPStan/Analyser/nsrt/bug-7688.php new file mode 100644 index 0000000000..cc0f7818a8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7688.php @@ -0,0 +1,50 @@ + + */ +function baz($value) +{ + if (is_int($value)) { + assertType('int', $value); + return $value < 1 ? 1 : $value; + } + + return $value; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7689.php b/tests/PHPStan/Analyser/nsrt/bug-7689.php new file mode 100644 index 0000000000..d7234af1d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7689.php @@ -0,0 +1,50 @@ +isEmptyElement) { + $reader->next(); + + return []; + } + + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + + $values = []; + + do { + if (Reader::ELEMENT === $reader->nodeType) { + + } else { + assertType('bool', $reader->read()); + if (!$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + assertType('bool', $reader->read()); + $reader->read(); + + return $values; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7698.php b/tests/PHPStan/Analyser/nsrt/bug-7698.php new file mode 100644 index 0000000000..b88ad68617 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7698.php @@ -0,0 +1,41 @@ +value::class; + assertType("'Bug7698\\\\A'|'Bug7698\\\\B'", $class); + + if ($class === A::class) { + return; + } + + assertType("'Bug7698\\\\B'", $class); + + if ($class === B::class) { + return; + } + + assertType('*NEVER*', $class); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7764.php b/tests/PHPStan/Analyser/nsrt/bug-7764.php new file mode 100644 index 0000000000..2583a46d6f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7764.php @@ -0,0 +1,17 @@ + 1) { + echo 'Success', "\n"; + } + print_r($split); +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7776.php b/tests/PHPStan/Analyser/nsrt/bug-7776.php new file mode 100644 index 0000000000..e01fa5f841 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7776.php @@ -0,0 +1,27 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug7776; + +use function PHPStan\Testing\assertType; + +/** + * @param array{page?: int, search?: string} $settings + */ +function test(array $settings = []): bool { + $copy = [...$settings]; + assertType('array{page?: int, search?: string}', $copy); + assertType('array{page?: int, search?: string}', $settings); + return isset($copy['search']); +} + +/** + * @param array{page?: int, search?: string} $settings + */ +function test2(array $settings = []): bool { + $copy = ['page' => 1, ...$settings]; + assertType('array{page: int, search?: string}', $copy); + assertType('array{page?: int, search?: string}', $settings); + return isset($copy['search']); +} diff --git a/tests/PHPStan/Analyser/data/bug-778.php b/tests/PHPStan/Analyser/nsrt/bug-778.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-778.php rename to tests/PHPStan/Analyser/nsrt/bug-778.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-7788.php b/tests/PHPStan/Analyser/nsrt/bug-7788.php new file mode 100644 index 0000000000..fa5c6a73af --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7788.php @@ -0,0 +1,34 @@ += 8.0 + +namespace Bug7788; + +use function PHPStan\Testing\assertType; + +/** + * @template T of array + */ +final class Props +{ + /** + * @param T $props + */ + public function __construct(private array $props = []) + { + } + + /** + * @template K of key-of + * @template TDefault + * @param K $propKey + * @param TDefault $default + * @return T[K]|TDefault + */ + public function getProp(string $propKey, mixed $default = null): mixed + { + return $this->props[$propKey] ?? $default; + } +} + +function () { + assertType('int', (new Props(['title' => 'test', 'value' => 30]))->getProp('value', 0)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7805.php b/tests/PHPStan/Analyser/nsrt/bug-7805.php new file mode 100644 index 0000000000..ec9464ebd3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7805.php @@ -0,0 +1,33 @@ +", $params); + $params = $params === [] ? ['list'] : $params; + assertType("array{'list'}", $params); + assertNativeType("array{'list'}", $params); + array_unshift($params, 'help'); + assertType("array{'help', 'list'}", $params); + assertNativeType("array{'help', 'list'}", $params); + } + assertType("array{}|array{'help', 'list'}", $params); + assertNativeType('array', $params); + + return $params; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7809.php b/tests/PHPStan/Analyser/nsrt/bug-7809.php new file mode 100644 index 0000000000..2769152110 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7809.php @@ -0,0 +1,18 @@ + : non-falsy-string + * ) $v + * @return void + */ + function foo( $k, $v ) { + if ( $k === 'a' ) { + assertType('int<0, 1>', $v); + } else { + assertType('non-falsy-string', $v); + } + } +} + +class HelloWorld2 +{ + /** + * @param string|array $name + * @param ($name is array ? null : int) $value + */ + public function setConfig($name, $value): void + { + if (is_array($name)) { + assertType('null', $value); + } else { + assertType('int', $value); + } + } + + /** + * @param string|array $name + * @param int $value + */ + public function setConfigMimicConditionalParamType($name, $value): void + { + if (is_array($name)) { + $value = null; + } + + if (is_array($name)) { + assertType('null', $value); + } else { + assertType('int', $value); + } + } +} + +/** + * @param ($isArray is false ? string : array) $data + * + * @return ($isArray is false ? string : array) + */ +function to_utf8($data, bool $isArray = false) +{ + if ($isArray) { + assertType('array', $data); + if (is_array($data)) { // always true + foreach ($data as $k => $value) { + $data[$k] = to_utf8($value, is_array($value)); + } + } else { + assertType('*NEVER*', $data); + $data = []; // dead code + } + } else { + assertType('string', $data); + $data = @iconv('UTF-8', 'UTF-8//IGNORE', $data); + } + + return $data; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7921.php b/tests/PHPStan/Analyser/nsrt/bug-7921.php new file mode 100644 index 0000000000..0a95bc79f3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7921.php @@ -0,0 +1,41 @@ + $arr */ + public function sayHello(array $arr): void + { + $pre_computed_arr = [ + 'a' => null, + 'b' => null, + 'c' => null, + ]; + + foreach ($arr as $arr_val) { + $pre_computed_arr['a'] = $arr_val['a']; + $pre_computed_arr['b'] = $arr_val['b']; + $pre_computed_arr['c'] = $arr_val['c']; + } + + assertType('string|null', $pre_computed_arr['a']); + assertType('string|null', $pre_computed_arr['b']); + assertType('string|null', $pre_computed_arr['c']); + + if ($pre_computed_arr['a'] === null) { + assertType('null', $pre_computed_arr['a']); + assertType('null', $pre_computed_arr['b']); + assertType('null', $pre_computed_arr['c']); + return; + } + + assertType('string', $pre_computed_arr['a']); + assertType('string', $pre_computed_arr['b']); + assertType('string|null', $pre_computed_arr['c']); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7928.php b/tests/PHPStan/Analyser/nsrt/bug-7928.php new file mode 100644 index 0000000000..df5cee1214 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7928.php @@ -0,0 +1,29 @@ += 8.0 + +namespace Bug7944; + +use function PHPStan\Testing\assertType; + +/** + * @template TValue + */ +final class Value +{ + /** @var TValue */ + public readonly mixed $value; + + /** + * @param TValue $value + */ + public function __construct(mixed $value) + { + $this->value = $value; + } +} + +/** + * @param non-empty-string $p + */ +function test($p): void { + $value = new Value($p); + assertType('Bug7944\\Value', $value); +}; + diff --git a/tests/PHPStan/Analyser/nsrt/bug-7949.php b/tests/PHPStan/Analyser/nsrt/bug-7949.php new file mode 100644 index 0000000000..2c1b056fea --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7949.php @@ -0,0 +1,19 @@ + 0 ? $price : '0'; + assertType('non-empty-string', $price); + + $this->foo($price); + } + + public function foo(string $test): void { + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7963-three.php b/tests/PHPStan/Analyser/nsrt/bug-7963-three.php new file mode 100644 index 0000000000..75c7d9a05d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7963-three.php @@ -0,0 +1,29 @@ + $objectClass + * @return TypeObject + */ + public function getObject(string $objectClass): AbstractA|AbstractB + { + if (is_subclass_of($objectClass, AbstractA::class)) { + assertType('class-string', $objectClass); + $object = $this->getObjectA($objectClass); + assertType('TypeObject of Bug7987\AbstractA (method Bug7987\Factory::getObject(), argument)', $object); + } elseif (is_subclass_of($objectClass, AbstractB::class)) { + assertType('class-string', $objectClass); + $object = $this->getObjectB($objectClass); + assertType('TypeObject of Bug7987\AbstractB (method Bug7987\Factory::getObject(), argument)', $object); + } else { + throw new \Exception("unable to instantiate $objectClass"); + } + assertType('TypeObject of Bug7987\AbstractA (method Bug7987\Factory::getObject(), argument)|TypeObject of Bug7987\AbstractB (method Bug7987\Factory::getObject(), argument)', $object); + return $object; + } + + /** + * @template TypeObject of AbstractA + * @param class-string $objectClass + * @return TypeObject + */ + private function getObjectA(string $objectClass): AbstractA + { + return new $objectClass(); + } + + /** + * @template TypeObject of AbstractB + * @param class-string $objectClass + * @return TypeObject + */ + private function getObjectB(string $objectClass): AbstractB + { + return new $objectClass(); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7993.php b/tests/PHPStan/Analyser/nsrt/bug-7993.php new file mode 100644 index 0000000000..03093f189b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7993.php @@ -0,0 +1,9 @@ + 0 === $value % 2)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7996.php b/tests/PHPStan/Analyser/nsrt/bug-7996.php new file mode 100644 index 0000000000..e336dee7f9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7996.php @@ -0,0 +1,31 @@ + $inputArray + * @return non-empty-array<\stdclass> + */ + public function filter(array $inputArray): array + { + $currentItem = reset($inputArray); + $outputArray = [$currentItem]; // $outputArray is now non-empty-array + assertType('array{stdclass}', $outputArray); + + while ($nextItem = next($inputArray)) { + if (rand(1, 2) === 1) { + assertType('non-empty-list', $outputArray); + // The fact that this is into an if, reverts type of $outputArray to array + $outputArray[] = $nextItem; + } + assertType('non-empty-list', $outputArray); + } + + assertType('non-empty-list', $outputArray); + return $outputArray; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8008.php b/tests/PHPStan/Analyser/nsrt/bug-8008.php new file mode 100644 index 0000000000..d3e8ae63d4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8008.php @@ -0,0 +1,61 @@ + $items + */ + public function __construct( + public array $items, + ) { + } + + /** + * @return array + */ + public function all() { + return $this->items; + } +} + +/** + * @template TValue of object + * + * @mixin Collection + */ +class Paginator +{ + /** + * @var Collection + */ + public Collection $collection; + + /** + * @param array $items + */ + public function __construct(public array $items) + { + $this->collection = new Collection($items); + } +} + +class MyObject {} + + +function (): void { + $paginator = new Paginator([new MyObject()]); + + assertType('Bug8008\Paginator', $paginator); + assertType('array', $paginator->items); + assertType('Bug8008\Collection', $paginator->collection); + assertType('array', $paginator->collection->items); + + assertType('array', $paginator->all()); +}; diff --git a/tests/PHPStan/Analyser/data/bug-801.php b/tests/PHPStan/Analyser/nsrt/bug-801.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-801.php rename to tests/PHPStan/Analyser/nsrt/bug-801.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-8015.php b/tests/PHPStan/Analyser/nsrt/bug-8015.php new file mode 100644 index 0000000000..99fa39d27b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8015.php @@ -0,0 +1,30 @@ + $items + * @return array + */ +function extractParameters(array $items): array +{ + $config = []; + foreach ($items as $itemName => $item) { + if (is_array($item)) { + $config['things'] = []; + assertType('array{}', $config['things']); + foreach ($item as $thing) { + assertType('list', $config['things']); + $config['things'][] = (string) $thing; + } + assertType('list', $config['things']); + } else { + $config[$itemName] = (string) $item; + } + } + assertType('list|string', $config['things']); + + return $config; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8017.php b/tests/PHPStan/Analyser/nsrt/bug-8017.php new file mode 100644 index 0000000000..dfa7284786 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8017.php @@ -0,0 +1,16 @@ + 0) { + assertType('array{dirname: string, basename: string, extension?: string, filename: string}', pathinfo($fileName)); + } + + return $pathinfo['dirname'] ?? ''; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8084.php b/tests/PHPStan/Analyser/nsrt/bug-8084.php new file mode 100644 index 0000000000..fe5869e8e2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8084.php @@ -0,0 +1,20 @@ + $data + **/ + public function sayHello(array $data): bool + { + \PHPStan\dumpType($data); + assertType('array', $data); + + $data['uses'] = ['']; + + assertType("non-empty-array&hasOffsetValue('uses', array{''})", $data); + + $data['uses'][] = ''; + + assertType("non-empty-array&hasOffsetValue('uses', array{'', ''})", $data); + + return count($data['foo']) > 0; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8092.php b/tests/PHPStan/Analyser/nsrt/bug-8092.php new file mode 100644 index 0000000000..52bd9abe1e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8092.php @@ -0,0 +1,39 @@ + */ +class TypeWithSpecific implements TypeWithGeneric +{ + public function get(): Specific + { + return new Specific(); + } +} + +class HelloWorld +{ + /** @param TypeWithGeneric $type */ + public function test(TypeWithGeneric $type): void + { + match (get_class($type)) { + TypeWithSpecific::class => assertType(TypeWithSpecific::class, $type), + default => false, + }; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8127.php b/tests/PHPStan/Analyser/nsrt/bug-8127.php new file mode 100644 index 0000000000..6e38769e6f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8127.php @@ -0,0 +1,52 @@ + + */ +final class SinkCollector implements Collector +{ + public function getNodeType(): string + { + return CallLike::class; + } + + public function processNode(\PhpParser\Node $node, Scope $scope) + {} +} + +class TaintType +{ + public const TYPE_INPUT = 'input'; + public const TYPE_SQL = 'sql'; + public const TYPE_HTML = 'html'; + + public const TYPES = [self::TYPE_INPUT, self::TYPE_SQL, self::TYPE_HTML]; +} + +/** + * @implements Rule + */ +final class TaintRule implements Rule +{ + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(\PhpParser\Node $node, Scope $scope): array + { + $sinkCollectorData = $node->get(SinkCollector::class); + assertType("array>", $sinkCollectorData); + + return []; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-82.php b/tests/PHPStan/Analyser/nsrt/bug-82.php new file mode 100644 index 0000000000..754d2244d8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-82.php @@ -0,0 +1,23 @@ +, + * } $array + */ + public function sayHello(array $array, string $string): void + { + assertType('array{notImportant: bool, attributesRequiredLogistic?: array}', $array); + unset($array[$string]); + assertType('array{notImportant?: bool, attributesRequiredLogistic?: array}', $array); + } + + public function edgeCase(): void + { + $arr = [1,2,3]; + unset($arr['1']); + assertType('array{0: 1, 2: 3}', $arr); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8242.php b/tests/PHPStan/Analyser/nsrt/bug-8242.php new file mode 100644 index 0000000000..3e516a7199 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8242.php @@ -0,0 +1,60 @@ += 8.0 + +namespace Bug8249; + +use function PHPStan\Testing\assertType; + +function foo(): mixed +{ + return null; +} + +function () { + $x = foo(); + + if (is_int($x)) { + assertType('int', $x); + assertType('true', is_int($x)); + } else { + assertType('mixed~int', $x); + assertType('false', is_int($x)); + } +}; + +function () { + $x = ['x' => foo()]; + + if (is_int($x['x'])) { + assertType('array{x: int}', $x); + assertType('int', $x['x']); + assertType('true', is_int($x['x'])); + } else { + assertType('array{x: mixed~int}', $x); + assertType('mixed~int', $x['x']); + assertType('false', is_int($x['x'])); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-8272.php b/tests/PHPStan/Analyser/nsrt/bug-8272.php new file mode 100644 index 0000000000..e26fba40f4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8272.php @@ -0,0 +1,10 @@ +', mt_rand(1, 5)); + assertType('int<0, max>', mt_rand()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-8361.php b/tests/PHPStan/Analyser/nsrt/bug-8361.php new file mode 100644 index 0000000000..cf0bb34b9b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8361.php @@ -0,0 +1,32 @@ +format(DateTimeInterface::ATOM); + assertType('true', $from || $to); + assertType('DateTimeInterface', $from ?? $to); + } + } + + public function sayHello2(?DateTimeInterface $from = null, ?DateTimeInterface $to = null): void + { + if ($from || $to) { + $operator = $from ? 'notBefore' : 'notAfter'; + $date = ($from ?? $to)->format(DateTimeInterface::ATOM); + $date = ($from ?? $to)->format(DateTimeInterface::ATOM); + assertType('true', $from || $to); + assertType('DateTimeInterface', $from ?? $to); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8366.php b/tests/PHPStan/Analyser/nsrt/bug-8366.php new file mode 100644 index 0000000000..fd6c65e972 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8366.php @@ -0,0 +1,36 @@ + $untilDate) { + assertType('DateTimeImmutable', $untilDate); + assertType('null', $count); + throw new \InvalidArgumentException('End date must not be greater than until date.'); + } + + if ($count !== null && $count < 1) { + assertType('null', $untilDate); + assertType('int', $count); + throw new \InvalidArgumentException('Count must be positive.'); + } + + assertType('DateTimeImmutable|null', $untilDate); + assertType('int<1, max>|null', $count); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8373.php b/tests/PHPStan/Analyser/nsrt/bug-8373.php new file mode 100644 index 0000000000..54471fb19f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8373.php @@ -0,0 +1,19 @@ +foo($a); + assertType('int', $a); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8442.php b/tests/PHPStan/Analyser/nsrt/bug-8442.php new file mode 100644 index 0000000000..96005d7d85 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8442.php @@ -0,0 +1,41 @@ + + * + * @phpstan-param list|null $mirrors + */ + protected function getUrls(?string $url, ?array $mirrors, ?string $ref, ?string $type, string $urlType): array + { + if (!$url) { + return []; + } + + if ($urlType === 'dist' && false !== strpos($url, '%')) { + assertType('string|null', $type); + $url = 'test'; + } + assertType('non-falsy-string', $url); + + $urls = [$url]; + if ($mirrors) { + foreach ($mirrors as $mirror) { + if ($urlType === 'dist') { + assertType('string|null', $type); + } elseif ($urlType === 'source' && $type === 'git') { + assertType("'git'", $type); + } elseif ($urlType === 'source' && $type === 'hg') { + assertType("'hg'", $type); + } else { + continue; + } + } + } + + return $urls; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8486.php b/tests/PHPStan/Analyser/nsrt/bug-8486.php new file mode 100644 index 0000000000..e15c8a6544 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8486.php @@ -0,0 +1,88 @@ += 8.1 + +namespace Bug8486; + +use function PHPStan\Testing\assertType; + +enum Operator: string +{ + case Foo = 'foo'; + case Bar = 'bar'; + case None = ''; + + public function explode(): void + { + $character = match ($this) { + self::None => 'baz', + default => $this->value, + }; + + assertType("'bar'|'baz'|'foo'", $character); + } + + public function typeInference(): void + { + match ($this) { + self::None => 'baz', + default => assertType('($this(Bug8486\Operator)&Bug8486\Operator::Foo)|($this(Bug8486\Operator)&Bug8486\Operator::Bar)', $this), + }; + } + + public function typeInference2(): void + { + if ($this === self::None) { + return; + } + + assertType("'Bar'|'Foo'", $this->name); + assertType("'bar'|'foo'", $this->value); + } +} + +class Foo +{ + + public function doFoo(Operator $operator) + { + $character = match ($operator) { + Operator::None => 'baz', + default => $operator->value, + }; + + assertType("'bar'|'baz'|'foo'", $character); + } + + public function typeInference(Operator $operator): void + { + match ($operator) { + Operator::None => 'baz', + default => assertType('Bug8486\Operator::Bar|Bug8486\Operator::Foo', $operator), + }; + } + + public function typeInference2(Operator $operator): void + { + if ($operator === Operator::None) { + return; + } + + assertType("'Bar'|'Foo'", $operator->name); + assertType("'bar'|'foo'", $operator->value); + } + + public function typeInference3(Operator $operator): void + { + if ($operator === Operator::None) { + return; + } + + if ($operator === Operator::Foo) { + return; + } + + assertType("Bug8486\Operator::Bar", $operator); + assertType("'Bar'", $operator->name); + assertType("'bar'", $operator->value); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8517.php b/tests/PHPStan/Analyser/nsrt/bug-8517.php new file mode 100644 index 0000000000..ab6570b796 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8517.php @@ -0,0 +1,15 @@ +attributes->get('_route_params', []); + assertType('stdClass|null', $request); + $routeParams = $request?->attributes->get('_route_params', []) ?? []; + $param = $request?->attributes->get('_param') ?? $routeParams['_param']; + assertType('stdClass|null', $request); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8520.php b/tests/PHPStan/Analyser/nsrt/bug-8520.php new file mode 100644 index 0000000000..d5d6c605fd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8520.php @@ -0,0 +1,15 @@ +', $i); + $tryMax = true; + while ($tryMax) { + $tryMax = false; + } +} + +assertType('int<7, max>', $i); diff --git a/tests/PHPStan/Analyser/nsrt/bug-8543.php b/tests/PHPStan/Analyser/nsrt/bug-8543.php new file mode 100644 index 0000000000..01d7b93c45 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8543.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug8543; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public readonly int $i; + + public int $j; + + public function invalidate(): void + { + } +} + +function (HelloWorld $hw): void { + $hw->i = 1; + $hw->j = 2; + assertType('1', $hw->i); + assertType('2', $hw->j); + + $hw->invalidate(); + assertType('1', $hw->i); + assertType('int', $hw->j); + + $hw = new HelloWorld(); + assertType('int', $hw->i); + assertType('int', $hw->j); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-8559.php b/tests/PHPStan/Analyser/nsrt/bug-8559.php new file mode 100644 index 0000000000..ee68b2fff0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8559.php @@ -0,0 +1,40 @@ + 1, 'b' => 2]; + + /** + * @phpstan-assert key-of $key + * @return value-of + */ + public static function get(string $key): int + { + assert(isset(self::KEYS[$key])); + assertType("'a'|'b'", $key); + return self::KEYS[$key]; + } + + /** + * @phpstan-assert key-of $key + * @return value-of + */ + public static function get2(string $key): int + { + assert(in_array($key, array_keys(self::KEYS), true)); + assertType("'a'|'b'", $key); + return self::KEYS[$key]; + } +} + +$key = 'x'; +$v = X::get($key); +assertType("*NEVER*", $key); + +$key = 'a'; +$v = X::get($key); +assertType("'a'", $key); diff --git a/tests/PHPStan/Analyser/nsrt/bug-8568.php b/tests/PHPStan/Analyser/nsrt/bug-8568.php new file mode 100644 index 0000000000..9236447acf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8568.php @@ -0,0 +1,26 @@ +get()); + } + + public function get(): ?int + { + return rand() ? 5 : null; + } + + /** + * @param numeric-string $numericS + */ + public function intersections($numericS): void { + assertType('non-falsy-string', 'a'. $numericS); + assertType('numeric-string', (string) $numericS); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8609.php b/tests/PHPStan/Analyser/nsrt/bug-8609.php new file mode 100644 index 0000000000..bac619be6a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8609.php @@ -0,0 +1,38 @@ + $f + * @param Foo $g + * @param Foo $h + * @param Foo $i + */ + public function doFoo(Foo $f, Foo $g, Foo $h, Foo $i): void + { + assertType('\'foo\'', $f->doFoo()); + assertType('\'bar\'', $g->doFoo()); + assertType('\'bar\'|\'foo\'', $h->doFoo()); + assertType('\'bar\'|\'foo\'', $i->doFoo()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8621.php b/tests/PHPStan/Analyser/nsrt/bug-8621.php new file mode 100644 index 0000000000..9bec8c9138 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8621.php @@ -0,0 +1,27 @@ + $data + */ + public function rows (array $data): void + { + $even = true; + + echo ""; + foreach ($data as $datum) + { + $even = !$even; + assertType('bool', $even); + + echo ""; + echo ""; + } + echo "
{$datum}
"; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8625.php b/tests/PHPStan/Analyser/nsrt/bug-8625.php new file mode 100644 index 0000000000..0558b314b9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8625.php @@ -0,0 +1,23 @@ +abc(); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8775.php b/tests/PHPStan/Analyser/nsrt/bug-8775.php new file mode 100644 index 0000000000..3a1678e919 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8775.php @@ -0,0 +1,279 @@ += 8.0 + +namespace Bug8803; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello(): void + { + $from = new \DateTimeImmutable('2023-01-30'); + for ($offset = 1; $offset <= 14; $offset++) { + $value = $from->format('N') + $offset; + if ($value > 7) { + } + + $value2 = $offset + $from->format('N'); + $value3 = '1e3' + $offset; + $value4 = $offset + '1e3'; + + assertType("'1'|'2'|'3'|'4'|'5'|'6'|'7'", $from->format('N')); + assertType('int<1, 14>', $offset); + assertType('int<2, 21>', $value); + assertType('int<2, 21>', $value2); + assertType('float', $value3); + assertType('float', $value4); + } + } + + public function testWithMixed(mixed $a, mixed $b): void + { + assertType('(array|float|int)', $a + $b); + assertType('(float|int)', 3 + $b); + assertType('(float|int)', $a + 3); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8827.php b/tests/PHPStan/Analyser/nsrt/bug-8827.php new file mode 100644 index 0000000000..fae38f26b2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8827.php @@ -0,0 +1,27 @@ +', $efferent); // Expected: int<0, $nbElements> | Actual: 0|1 + assertType('int<0, max>', $afferent); // Expected: int<0, $nbElements> | Actual: 0|1 + + $instability = ($efferent + $afferent > 0) ? $efferent / ($afferent + $efferent) : 0; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8917.php b/tests/PHPStan/Analyser/nsrt/bug-8917.php new file mode 100644 index 0000000000..2cc2106202 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8917.php @@ -0,0 +1,22 @@ + 1]], 'a'); + + assertType('array{1}', $array); + assertType('1', count($array)); + assertType('true', array_key_exists(0, $array)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8924.php b/tests/PHPStan/Analyser/nsrt/bug-8924.php new file mode 100644 index 0000000000..ccb3ccdf45 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8924.php @@ -0,0 +1,32 @@ + $array + */ +function foo(array $array): void { + foreach ($array as $element) { + assertType('int', $element); + $array = null; + } +} + +function makeValidNumbers(): array +{ + $validNumbers = [1, 2]; + foreach ($validNumbers as $k => $v) { + assertType("non-empty-list<-2|-1|1|2|' 1'|' 2'>", $validNumbers); + assertType('0|1', $k); + assertType('1|2', $v); + $validNumbers[] = -$v; + $validNumbers[] = ' ' . (string)$v; + assertType("non-empty-list<-2|-1|1|2|' 1'|' 2'>", $validNumbers); + } + + assertType("non-empty-list<-2|-1|1|2|' 1'|' 2'>", $validNumbers); + + return $validNumbers; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8956.php b/tests/PHPStan/Analyser/nsrt/bug-8956.php new file mode 100644 index 0000000000..15ba7b8dfd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8956.php @@ -0,0 +1,29 @@ +', array_chunk(range(0, 10), 60)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9000.php b/tests/PHPStan/Analyser/nsrt/bug-9000.php new file mode 100644 index 0000000000..281a6156be --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9000.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug9000; + +use function PHPStan\Testing\assertType; + +enum A:string { + case A = "A"; + case B = "B"; + case C = "C"; +} + +const A_ARRAY = [ + 'A' => A::A, + 'B' => A::B, +]; + +/** + * @param string $key + * @return value-of + */ +function testA(string $key): A +{ + return A_ARRAY[$key]; +} + +function (): void { + $test = testA('A'); + assertType('Bug9000\A::A|Bug9000\A::B', $test); + assertType("'A'|'B'", $test->value); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9062.php b/tests/PHPStan/Analyser/nsrt/bug-9062.php new file mode 100644 index 0000000000..7280c8634c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9062.php @@ -0,0 +1,42 @@ += 8.0 + +namespace Bug9062; + +use function PHPStan\Testing\assertType; + +/** + * @property-read int|null $port + * @property-write int|string|null $port + */ +class Foo { + private ?int $port; + + public function __set(string $name, mixed $value): void { + if ($name === 'port') { + if ($value === null || is_int($value)) { + $this->port = $value; + } elseif (is_string($value) && strspn($value, '0123456789') === strlen($value)) { + $this->port = (int) $value; + } else { + throw new \Exception("Property {$name} can only be a null, an int or a string containing the latter."); + } + } else { + throw new \Exception("Unknown property {$name}."); + } + } + + public function __get(string $name): mixed { + if ($name === 'port') { + return $this->port; + } else { + throw new \Exception("Unknown property {$name}."); + } + } +} + +function (): void { + $foo = new Foo; + $foo->port = "66"; + + assertType('int|null', $foo->port); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9084.php b/tests/PHPStan/Analyser/nsrt/bug-9084.php new file mode 100644 index 0000000000..b44c1f9010 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9084.php @@ -0,0 +1,76 @@ += 8.1 + +namespace Bug9084; + +use function PHPStan\Testing\assertType; + +enum UnitType +{ + case Mass; + + case Length; +} + +/** + * @template TUnitType of UnitType::* + */ +interface UnitInterface +{ + public function getValue(): float; +} + +/** + * @implements UnitInterface + */ +enum MassUnit: int implements UnitInterface +{ + case KiloGram = 1000000; + + case Gram = 1000; + + case MilliGram = 1; + + public function getValue(): float + { + return $this->value; + } +} + +/** + * @template TUnit of UnitType::* + */ +class Value +{ + public function __construct( + public readonly float $value, + /** @var UnitInterface */ + public readonly UnitInterface $unit + ) { + } + + /** + * @param UnitInterface $unit + * @return Value + */ + public function convert(UnitInterface $unit): Value + { + return new Value($this->value / $unit->getValue(), $unit); + } +} + +/** + * @template S + * @param S $value + * @return S + */ +function duplicate($value) +{ + return clone $value; +} + +function (): void { + $a = new Value(10, MassUnit::KiloGram); + assertType('Bug9084\Value', $a); + $b = duplicate($a); + assertType('Bug9084\Value', $b); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9086.php b/tests/PHPStan/Analyser/nsrt/bug-9086.php new file mode 100644 index 0000000000..e099f4eec1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9086.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug9086; + +use ArrayObject; +use function PHPStan\Testing\assertType; + +/** + * @template A + * @template B + * + * @param A $items + * @param callable(A): B $ab + * @return B + */ +function pipe(mixed $items, callable $ab): mixed +{ + return $ab($items); +} + +/** + * @return ArrayObject + */ +function getObject(): ArrayObject +{ + return new ArrayObject; +} + +function (): void { + $result = pipe(getObject(), function(ArrayObject $i) { + assertType('ArrayObject', $i); + return $i; + }); + + assertType('ArrayObject', $result); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9105.php b/tests/PHPStan/Analyser/nsrt/bug-9105.php new file mode 100644 index 0000000000..296baba23d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9105.php @@ -0,0 +1,24 @@ += 8.0 + +namespace Bug9105; + +use function PHPStan\Testing\assertType; + +class H +{ + public int|null $a = null; + public self|null $b = null; + + public function h(): void + { + assertType('Bug9105\H|null', $this->b); + if ($this->b?->a < 5) { + echo '<5', PHP_EOL; + } + assertType('Bug9105\H|null', $this->b); + if ($this->b?->a > 0) { + echo '>0', PHP_EOL; + } + assertType('Bug9105\H|null', $this->b); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9123.php b/tests/PHPStan/Analyser/nsrt/bug-9123.php new file mode 100644 index 0000000000..d1d307fff1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9123.php @@ -0,0 +1,53 @@ + */ +final class Implementation implements EventListener +{ + public function canBeListen(Event $event): bool + { + return $event instanceof MyEvent; + } + + public function listen(Event $event): void + { + if (! $this->canBeListen($event)) { + return; + } + + \PHPStan\Testing\assertType('Bug9123\MyEvent', $event); + } +} + +/** @implements EventListener */ +final class Implementation2 implements EventListener +{ + /** @phpstan-assert-if-true MyEvent $event */ + public function canBeListen(Event $event): bool + { + return $event instanceof MyEvent; + } + + public function listen(Event $event): void + { + if (! $this->canBeListen($event)) { + return; + } + + \PHPStan\Testing\assertType('Bug9123\MyEvent', $event); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9131.php b/tests/PHPStan/Analyser/nsrt/bug-9131.php new file mode 100644 index 0000000000..97302bf1d3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9131.php @@ -0,0 +1,37 @@ + $b + * @param array, string> $c + * @param array|string, string> $d + * @return void + */ + public function doFoo( + array $a, + array $b, + array $c, + array $d + ): void + { + $a[] = 'foo'; + assertType('non-empty-array', $a); + + $b[] = 'foo'; + assertType('non-empty-array', $b); + + $c[] = 'foo'; + assertType('non-empty-array, string>', $c); + + $d[] = 'foo'; + assertType('non-empty-array|string, string>', $d); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9208.php b/tests/PHPStan/Analyser/nsrt/bug-9208.php new file mode 100644 index 0000000000..fa9ec21a30 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9208.php @@ -0,0 +1,22 @@ + $id_or_ids + * @return non-empty-list + */ +function f(int|array $id_or_ids): array +{ + if (is_array($id_or_ids)) { + assertType('non-empty-list', (array)$id_or_ids); + } else { + assertType('array{int}', (array)$id_or_ids); + } + + $ids = (array)$id_or_ids; + assertType('non-empty-list', $ids); + return $ids; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9224b.php b/tests/PHPStan/Analyser/nsrt/bug-9224b.php new file mode 100644 index 0000000000..863d17cb85 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9224b.php @@ -0,0 +1,17 @@ += 8.1 + +namespace Bug9224b; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** @param array $arr */ + public function sayHello(array $arr): void + { + assertType('array>', array_map('abs', $arr)); + assertType('array>', array_map(abs(...), $arr)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9274.php b/tests/PHPStan/Analyser/nsrt/bug-9274.php new file mode 100644 index 0000000000..c01521ff1d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9274.php @@ -0,0 +1,35 @@ + */ +class A extends SplDoublyLinkedList {} +/** @extends SplQueue */ +class B extends SplQueue {} + +function testSplDoublyLinkedList(): void +{ + $dll = new A(); + $p1 = $dll[0]; + + assertType('Bug9274\Point', $p1); + assertType('int', $p1->x); +} + +function testSplQueue(): void +{ + $queue = new B(); + $p2 = $queue[0]; + + assertType('Bug9274\Point', $p2); + assertType('int', $p2->x); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9293.php b/tests/PHPStan/Analyser/nsrt/bug-9293.php new file mode 100644 index 0000000000..a095e58011 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9293.php @@ -0,0 +1,33 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug9293; + +use function PHPStan\Testing\assertType; + +class B +{ + public function int(): int + { + return 0; + } + + public function mixed(): mixed + { + return new self(); + } +} + +/** + * @var null|B $b + */ +$b = null; + +assertType('Bug9293\B|null', $b); + +$b?->mixed()->int() ?? 0; + +assertType('Bug9293\B|null', $b); + +$b?->int() ?? 0; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9341.php b/tests/PHPStan/Analyser/nsrt/bug-9341.php new file mode 100644 index 0000000000..3265c4a7b0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9341.php @@ -0,0 +1,34 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug9341; + +use function PHPStan\Testing\assertType; + +interface MyInterface {} + +trait MyTrait +{ + public static function parse(): mixed + { + $class = get_called_class(); + assertType('class-string', $class); + if (!is_a($class, MyInterface::class, true)) { + return false; + } + assertType('class-string', $class); + $fileObject = new $class(); + assertType('Bug9341\MyInterface&static(Bug9341\MyAbstractBase)', $fileObject); + return $fileObject; + } +} + +abstract class MyAbstractBase { + use MyTrait; +} + +class MyClass extends MyAbstractBase implements MyInterface +{ + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9394.php b/tests/PHPStan/Analyser/nsrt/bug-9394.php new file mode 100644 index 0000000000..834a19656c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9394.php @@ -0,0 +1,18 @@ +is_pre_order === false) { + return; + } + + assertType(Order::class . '|null', $order); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9397.php b/tests/PHPStan/Analyser/nsrt/bug-9397.php new file mode 100644 index 0000000000..f197e3b438 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9397.php @@ -0,0 +1,101 @@ + + * If the above type has 63 or more properties, the bug occurs + */ + private static function callable(): array { + return []; + } + + public function callsite(): void { + $result = self::callable(); + foreach ($result as $id => $p) { + assertType(Money::class, $p['foo1']); + assertType(Money::class . '|null', $p['foo2']); + assertType('string', $p['foo3']); + + $baseDeposit = $p['foo2'] ?? Money::zero(); + assertType(Money::class, $p['foo1']); + assertType(Money::class . '|null', $p['foo2']); + assertType('string', $p['foo3']); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9404.php b/tests/PHPStan/Analyser/nsrt/bug-9404.php new file mode 100644 index 0000000000..e03c4cd386 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9404.php @@ -0,0 +1,12 @@ += 8.0 + +namespace Bug9472; + +use Closure; +use function PHPStan\Testing\assertType; + +/** + * @template Tk + * @template Tv + * @template T + * @param array $iterable + * @param (Closure(Tv): T) $function + * + * @return list + */ +function map(array $iterable, Closure $function): array +{ + $result = []; + foreach ($iterable as $value) { + $result[] = $function($value); + } + + return $result; +} + +function (): void { + /** @var list */ + $nonEmptyStrings = []; + + map($nonEmptyStrings, static function (string $variable) { + assertType('non-empty-string', $variable); + return $variable; + }); +}; + +/** + * @template Type + * @param Type $x + * @return Type + */ +function identity($x) { + return $x; +} + +function (): void { + $x = rand() > 5 ? 'a' : 'b'; + assertType('\'a\'|\'b\'', $x); + $y = identity($x); + assertType('\'a\'|\'b\'', $y); +}; + +/** + * @template ParseResultType + * @param callable():ParseResultType $parseFunction + * @return ParseResultType|null + */ +function tryParse(callable $parseFunction) { + try { + return $parseFunction(); + } catch (\Exception $e) { + return null; + } +} + +/** @return array{type: 'typeA'|'typeB'} */ +function parseData(mixed $data): array { + return ['type' => 'typeA']; +} + +function (): void { + $data = tryParse(fn() => parseData('whatever')); + assertType('array{type: \'typeA\'|\'typeB\'}|null', $data); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php b/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php new file mode 100644 index 0000000000..13a26b9582 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php @@ -0,0 +1,105 @@ += 8.1 + +namespace Bug9662Enums; + +use function PHPStan\Testing\assertType; + +enum Suit +{ + case Hearts; + case Diamonds; + case Clubs; + case Spades; +} + +/** + * @param array $suite + */ +function doEnum(array $suite, array $arr) { + if (in_array('NotAnEnumCase', $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + if (in_array(Suit::Hearts, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + + if (in_array(Suit::Hearts, $arr) === false) { + assertType('array', $arr); + } else { + assertType("non-empty-array", $arr); + } + assertType('array', $arr); + + + if (in_array('NotAnEnumCase', $arr) === false) { + assertType('array', $arr); + } else { + assertType("non-empty-array", $arr); + } + assertType('array', $arr); +} + +enum StringBackedSuit: string +{ + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} + +/** + * @param array $suite + */ +function doBackedEnum(array $suite, array $arr, string $s, int $i, $mixed) { + if (in_array('NotAnEnumCase', $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + if (in_array(StringBackedSuit::Hearts, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + + if (in_array($s, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + if (in_array($i, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + if (in_array($mixed, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + + if (in_array(StringBackedSuit::Hearts, $arr) === false) { + assertType('array', $arr); + } else { + assertType("non-empty-array", $arr); + } + assertType('array', $arr); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9662.php b/tests/PHPStan/Analyser/nsrt/bug-9662.php new file mode 100644 index 0000000000..d88555a863 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9662.php @@ -0,0 +1,189 @@ + $a + * @param array $strings + * @return void + */ +function doFoo(string $s, $a, $strings, $mixed) { + if (in_array('foo', $a, true)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array('foo', $a, false)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array('foo', $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array('0', $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array('1', $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array(true, $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array(false, $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array($s, $a, true)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array($s, $a, false)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array($s, $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array($mixed, $strings, true)) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($mixed, $strings, false)) { + assertType('array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($mixed, $strings)) { + assertType('array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, true)) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, false)) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings)) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, true) === true) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, false) === true) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings) === true) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, true) === false) { + assertType('array', $strings); + } else { + assertType("non-empty-array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, false) === false) { + assertType('array', $strings); + } else { + assertType("non-empty-array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings) === false) { + assertType('array', $strings); + } else { + assertType("non-empty-array", $strings); + } + assertType('array', $strings); +} + +/** + * Add new delivery prices. + * + * @param array $price_list Prices list in multiple arrays (changed to array since 1.5.0) + * @param bool $delete + */ +function addDeliveryPrice($price_list, $delete = false): void +{ + if (!$price_list) { + return; + } + + $keys = array_keys($price_list[0]); + if (!in_array('id_shop', $keys)) { + $keys[] = 'id_shop'; + } + if (!in_array('id_shop_group', $keys)) { + $keys[] = 'id_shop_group'; + } + + var_dump($keys); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9704.php b/tests/PHPStan/Analyser/nsrt/bug-9704.php new file mode 100644 index 0000000000..1d435746a5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9704.php @@ -0,0 +1,54 @@ + + */ + private const TYPES = [ + 'foo' => DateTime::class, + 'bar' => DateTimeImmutable::class, + ]; + + /** + * @template M of self::TYPES + * @template T of key-of + * @param T $type + * + * @return new + */ + public static function get(string $type) : object + { + $class = self::TYPES[$type]; + + return new $class('now'); + } + + /** + * @template T of key-of + * @param T $type + * + * @return new + */ + public static function get2(string $type) : object + { + $class = self::TYPES[$type]; + + return new $class('now'); + } +} + +assertType(DateTime::class, Foo::get('foo')); +assertType(DateTimeImmutable::class, Foo::get('bar')); + +assertType(DateTime::class, Foo::get2('foo')); +assertType(DateTimeImmutable::class, Foo::get2('bar')); + + diff --git a/tests/PHPStan/Analyser/nsrt/bug-9714.php b/tests/PHPStan/Analyser/nsrt/bug-9714.php new file mode 100644 index 0000000000..3dbb7d1b87 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9714.php @@ -0,0 +1,16 @@ +xpath('//data'); + assertType('array', $elements); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-9721.php b/tests/PHPStan/Analyser/nsrt/bug-9721.php new file mode 100644 index 0000000000..3be9804bb5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9721.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug9721; + +use function PHPStan\Testing\assertType; + +class Example { + public function mergeWith(): self + { + return $this; + } +} + +function () { + $mergedExample = null; + $loop = 2; + + do { + + $example = new Example(); + $mergedExample = $mergedExample?->mergeWith() ?? $example; + + assertType(Example::class, $mergedExample); + + $loop--; + } while ($loop); + +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9734.php b/tests/PHPStan/Analyser/nsrt/bug-9734.php new file mode 100644 index 0000000000..1628ad6859 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9734.php @@ -0,0 +1,47 @@ + $a + * @return void + */ + public function doFoo(array $a): void + { + if (array_is_list($a)) { + assertType('list', $a); + } else { + assertType('array', $a); // could be non-empty-array + } + } + + public function doFoo2(): void + { + $a = []; + if (array_is_list($a)) { + assertType('array{}', $a); + } else { + assertType('*NEVER*', $a); + } + } + + /** + * @param non-empty-array $a + * @return void + */ + public function doFoo3(array $a): void + { + if (array_is_list($a)) { + assertType('non-empty-list', $a); + } else { + assertType('non-empty-array', $a); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9753.php b/tests/PHPStan/Analyser/nsrt/bug-9753.php new file mode 100644 index 0000000000..0d521cd960 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9753.php @@ -0,0 +1,24 @@ +|null', $items); + if (isset($items)) { + if (count($items) > 2) { + $items = null; + } else { + $items[] = $entry; + } + } + assertType('non-empty-list<1|2|3|4|5>|null', $items); + } + + assertType('non-empty-list<1|2|3|4|5>|null', $items); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9764.php b/tests/PHPStan/Analyser/nsrt/bug-9764.php new file mode 100644 index 0000000000..f24b810fe8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9764.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug9764; + +use function PHPStan\Testing\assertType; + +/** + * @template T + * @param callable(): T $fnc + * @return T + */ +function result(callable $fnc): mixed +{ + return $fnc(); +} + +function (): void { + /** @var array $a */ + $a = []; + $c = static fn (): array => $a; + assertType('Closure(): array', $c); + + $r = result($c); + assertType('array', $r); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9778.php b/tests/PHPStan/Analyser/nsrt/bug-9778.php new file mode 100644 index 0000000000..240fb5bbc7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9778.php @@ -0,0 +1,29 @@ + $articles, + 'farben' => null, + 'artikel_ids' => [], + ]; + + // collect article ids + foreach ($result['artikel'] as $article) { + $result['artikel_ids'][] = 1; + } + + assertType('array{artikel: Iterator, farben: null, artikel_ids: list<1>}', $result); + assertType('list<1>', $result['artikel_ids']); + + if ($result['artikel_ids'] !== []) { + $result['farben'] = new stdClass(); + } + + // $result['farben'] might be also null + assertType('stdClass|null', $result['farben']); + if ($result['farben'] instanceof stdClass) { + echo '123'; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9867.php b/tests/PHPStan/Analyser/nsrt/bug-9867.php new file mode 100644 index 0000000000..6ab9515b87 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9867.php @@ -0,0 +1,77 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug9867; + +use function PHPStan\Testing\assertType; + +/** @extends \SplMinHeap<\DateTime> */ +class MyMinHeap extends \SplMinHeap +{ + public function test(): void + { + assertType('DateTime', parent::current()); + assertType('DateTime', parent::extract()); + assertType('DateTime', parent::top()); + } + + public function insert(mixed $value): bool + { + assertType('DateTime', $value); + + return parent::insert($value); + } + + protected function compare(mixed $value1, mixed $value2) + { + assertType('DateTime', $value1); + assertType('DateTime', $value2); + + return parent::compare($value1, $value2); + } +} + +/** @extends \SplMaxHeap<\DateTime> */ +class MyMaxHeap extends \SplMaxHeap +{ + public function test(): void + { + assertType('DateTime', parent::current()); + assertType('DateTime', parent::extract()); + assertType('DateTime', parent::top()); + } + + public function insert(mixed $value): bool + { + assertType('DateTime', $value); + + return parent::insert($value); + } + + protected function compare(mixed $value1, mixed $value2) + { + assertType('DateTime', $value1); + assertType('DateTime', $value2); + + return parent::compare($value1, $value2); + } +} + +/** @extends \SplHeap<\DateTime> */ +abstract class MyHeap extends \SplHeap +{ + public function test(): void + { + assertType('DateTime', parent::current()); + assertType('DateTime', parent::extract()); + assertType('DateTime', parent::top()); + } + + public function insert(mixed $value): bool + { + assertType('DateTime', $value); + + return parent::insert($value); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-987.php b/tests/PHPStan/Analyser/nsrt/bug-987.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-987.php rename to tests/PHPStan/Analyser/nsrt/bug-987.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-9881.php b/tests/PHPStan/Analyser/nsrt/bug-9881.php new file mode 100644 index 0000000000..129ca9c87f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9881.php @@ -0,0 +1,27 @@ += 8.1 + +namespace Bug9881; + +use BackedEnum; +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + /** + * @template B of BackedEnum + * @param B[] $enums + * @return value-of[] + */ + public static function arrayEnumToStrings(array $enums): array + { + return array_map(static fn (BackedEnum $code): string|int => $code->value, $enums); + } +} + +enum Test: string { + case DA = 'da'; +} + +function (Test ...$da): void { + assertType('array<\'da\'>', HelloWorld::arrayEnumToStrings($da)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9939.php b/tests/PHPStan/Analyser/nsrt/bug-9939.php new file mode 100644 index 0000000000..5f828bebd2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9939.php @@ -0,0 +1,65 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug9939; + +use function PHPStan\Testing\assertType; + +enum Combinator +{ + case NEXT_SIBLING; + case CHILD; + case FOLLOWING_SIBLING; + + public function getText(): string + { + return match ($this) { + self::NEXT_SIBLING => '+', + self::CHILD => '>', + self::FOLLOWING_SIBLING => '~', + }; + } +} + +/** + * @template T of string|\Stringable|array|Combinator|null + */ +class CssValue +{ + /** + * @param T $value + */ + public function __construct(private readonly mixed $value) + { + } + + /** + * @return T + */ + public function getValue(): mixed + { + return $this->value; + } + + public function __toString(): string + { + assertType('T of array|Bug9939\Combinator|string|Stringable|null (class Bug9939\CssValue, argument)', $this->value); + + if ($this->value instanceof Combinator) { + assertType('T of Bug9939\Combinator (class Bug9939\CssValue, argument)', $this->value); + return $this->value->getText(); + } + + assertType('T of array|string|Stringable|null (class Bug9939\CssValue, argument)', $this->value); + + if (\is_array($this->value)) { + assertType('T of array (class Bug9939\CssValue, argument)', $this->value); + return implode($this->value); + } + + assertType('T of string|Stringable|null (class Bug9939\CssValue, argument)', $this->value); + + return (string) $this->value; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9963.php b/tests/PHPStan/Analyser/nsrt/bug-9963.php new file mode 100644 index 0000000000..e5d8444afd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9963.php @@ -0,0 +1,23 @@ +|Bug9963\HelloWorld|false', $h->find($something)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9985.php b/tests/PHPStan/Analyser/nsrt/bug-9985.php new file mode 100644 index 0000000000..09a7ad92ea --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9985.php @@ -0,0 +1,25 @@ += 1) { + $warnings['a'] = true; + } + + if (rand(0, 100) >= 2) { + $warnings['b'] = true; + } elseif (rand(0, 100) >= 3) { + $warnings['c'] = true; + } + + assertType('array{}|array{a?: true, b: true}|array{a?: true, c?: true}', $warnings); + + if (!empty($warnings)) { + assertType('array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', $warnings); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9995.php b/tests/PHPStan/Analyser/nsrt/bug-9995.php new file mode 100644 index 0000000000..c4fe4d6ada --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9995.php @@ -0,0 +1,15 @@ +format('c'); +} diff --git a/tests/PHPStan/Analyser/data/bug-empty-array.php b/tests/PHPStan/Analyser/nsrt/bug-empty-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-empty-array.php rename to tests/PHPStan/Analyser/nsrt/bug-empty-array.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php b/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php new file mode 100644 index 0000000000..14d4ecf708 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php @@ -0,0 +1,30 @@ += 8.0 + +declare(strict_types=1); + +namespace BugNullsafePropStaticAccess; + +class A +{ + public function __construct(public readonly B $b) + {} +} + +class B +{ + public static int $value = 0; + + public static function get(): string + { + return 'B'; + } +} + +function foo(?A $a): void +{ + \PHPStan\Testing\assertType('string|null', $a?->b::get()); + \PHPStan\Testing\assertType('string|null', $a?->b->get()); + + \PHPStan\Testing\assertType('int|null', $a?->b::$value); + \PHPStan\Testing\assertType('int|null', $a?->b->value); +} diff --git a/tests/PHPStan/Analyser/data/bug-pr-339.php b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php similarity index 87% rename from tests/PHPStan/Analyser/data/bug-pr-339.php rename to tests/PHPStan/Analyser/nsrt/bug-pr-339.php index 768efbc147..4d841ad783 100644 --- a/tests/PHPStan/Analyser/data/bug-pr-339.php +++ b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php @@ -17,14 +17,14 @@ 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 { diff --git a/tests/PHPStan/Analyser/nsrt/bug11384.php b/tests/PHPStan/Analyser/nsrt/bug11384.php new file mode 100644 index 0000000000..709f298635 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug11384.php @@ -0,0 +1,20 @@ + 0) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) > 1) { + assertType("array{'ab', 'xy'}", $x); + } else { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) >= 1) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + public function arraySmallerThan(): void + { + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + if (count($x) < 1) { + assertType("array{}", $x); + } else { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) <= 1) { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{'ab', 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + public function intUnionCount(): void + { + $count = 1; + if (rand(0, 1)) { + $count++; + } + + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + assertType('1|2', $count); + + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + if (count($x) >= $count) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + /** + * @param int<1,2> $count + */ + public function intRangeCount($count): void + { + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + if (count($x) >= $count) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } +} diff --git a/tests/PHPStan/Analyser/data/bug2574.php b/tests/PHPStan/Analyser/nsrt/bug2574.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug2574.php rename to tests/PHPStan/Analyser/nsrt/bug2574.php diff --git a/tests/PHPStan/Analyser/data/bug2577.php b/tests/PHPStan/Analyser/nsrt/bug2577.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug2577.php rename to tests/PHPStan/Analyser/nsrt/bug2577.php diff --git a/tests/PHPStan/Analyser/nsrt/bug7856.php b/tests/PHPStan/Analyser/nsrt/bug7856.php new file mode 100644 index 0000000000..8da8b7343e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug7856.php @@ -0,0 +1,16 @@ +", $intervals); + $periodEnd = $periodEnd->modify(array_shift($intervals)); + } while (count($intervals) > 0 && $periodEnd->format('U') < $endDate); +} diff --git a/tests/PHPStan/Analyser/nsrt/call-user-func-php7.php b/tests/PHPStan/Analyser/nsrt/call-user-func-php7.php new file mode 100644 index 0000000000..756185972c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/call-user-func-php7.php @@ -0,0 +1,26 @@ +', call_user_func('CallUserFuncPhp7\generic', $params)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/call-user-func-php8.php b/tests/PHPStan/Analyser/nsrt/call-user-func-php8.php new file mode 100644 index 0000000000..3550b21d56 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/call-user-func-php8.php @@ -0,0 +1,55 @@ += 8.0 + +namespace CallUserFuncPhp8; + +use function PHPStan\Testing\assertType; + +/** + * @template T + * @param T $t + * @return T + */ +function generic($t) { + return $t; +} + +/** + * @template T + * @param T $t + * @return T + */ +function generic3($t = '', int $b = 100, string $c = '') { + return $t; +} + + +function fun3($a = '', $b = '', $c = ''): int { + return 1; +} + +class Foo { + + /** + * @param string $params,... + */ + function doVariadics(...$params) { + // because of named arguments support in php8 we have a different return type as in php7 + // see https://phpstan.org/r/58c30346-9568-47ca-82e5-53b2fffda7d0 + assertType('array', call_user_func('CallUserFuncPhp8\generic', $params)); + } + + function doNamed() { + assertType('1', call_user_func('CallUserFuncPhp8\generic', t: 1)); + assertType('array{1, 2, 3}', call_user_func('CallUserFuncPhp8\generic', t: [1, 2, 3])); + + assertType('array{1, 2, 3}', call_user_func('CallUserFuncPhp8\generic3', t: [1, 2, 3])); + assertType('\'\'', call_user_func('CallUserFuncPhp8\generic3', b: 150)); + assertType('\'\'', call_user_func('CallUserFuncPhp8\generic3', c: 'lala')); + assertType('\'\'', call_user_func(c: 'lala', callback: 'CallUserFuncPhp8\generic3')); + + assertType('int', call_user_func('CallUserFuncPhp8\fun3', a: [1, 2, 3])); + assertType('int', call_user_func('CallUserFuncPhp8\fun3', b: [1, 2, 3])); + assertType('int', call_user_func('CallUserFuncPhp8\fun3', c: [1, 2, 3])); + assertType('int', call_user_func('CallUserFuncPhp8\fun3', a: [1, 2, 3], c: 'c')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/call-user-func.php b/tests/PHPStan/Analyser/nsrt/call-user-func.php new file mode 100644 index 0000000000..b54952ce4c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/call-user-func.php @@ -0,0 +1,63 @@ +', call_user_func('CallUserFunc\generic', $strings)); + + assertType('int', call_user_func('CallUserFunc\fun')); + assertType('int', call_user_func('CallUserFunc\fun3', 1 ,2 ,3)); + assertType('string', call_user_func(['CallUserFunc\c', 'm'])); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/callable-in-union.php b/tests/PHPStan/Analyser/nsrt/callable-in-union.php new file mode 100644 index 0000000000..724ce9cafa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/callable-in-union.php @@ -0,0 +1,32 @@ +|(callable(array): array) $_ */ +function acceptArrayOrCallable($_) +{ +} + +acceptArrayOrCallable(fn ($parameter) => assertType('array', $parameter)); + +acceptArrayOrCallable(function ($parameter) { + assertType('array', $parameter); + return $parameter; +}); + +/** + * @param (callable(string): void)|callable(int): void $a + * @return void + */ +function acceptCallableOrCallableLikeArray($a): void +{ + +} + +acceptCallableOrCallableLikeArray(function ($p) { + assertType('int|string', $p); +}); + +acceptCallableOrCallableLikeArray(fn ($p) => assertType('int|string', $p)); diff --git a/tests/PHPStan/Analyser/nsrt/callable-object.php b/tests/PHPStan/Analyser/nsrt/callable-object.php new file mode 100644 index 0000000000..f9f3dee69c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/callable-object.php @@ -0,0 +1,38 @@ +|numeric-string|true', $mixed); + } else { + assertType('mixed~(int<0, max>|numeric-string|true)', $mixed); + } + assertType('mixed', $mixed); + + if (ctype_digit((int) $mixed)) { + assertType('mixed', $mixed); // could be *NEVER* + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (ctype_digit((string) $int)) { + assertType('int', $int); + } else { + assertType('int', $int); + } + assertType('int', $int); + + if (ctype_digit((int) $int)) { + assertType('int', $int); // could be *NEVER* + } else { + assertType('int', $int); + } + assertType('int', $int); + + if (ctype_digit((string) $string)) { + assertType('numeric-string', $string); + } else { + assertType('string', $string); + } + assertType('string', $string); + + if (ctype_digit((int) $string)) { + assertType('string', $string); // could be *NEVER* + } else { + assertType('string', $string); + } + assertType('string', $string); + + if (ctype_digit((string) $numericString)) { + assertType('numeric-string', $numericString); + } else { + assertType('*NEVER*', $numericString); + } + assertType('numeric-string', $numericString); + + if (ctype_digit((string) $bool)) { + assertType('true', $bool); + } else { + assertType('false', $bool); + } + assertType('bool', $bool); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/case-insensitive-parent.php b/tests/PHPStan/Analyser/nsrt/case-insensitive-parent.php new file mode 100644 index 0000000000..a1eb8830a3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/case-insensitive-parent.php @@ -0,0 +1,35 @@ + $positive + * @param int $negative + */ +function integerRangeToString($positive, $negative) +{ + assertType('lowercase-string&numeric-string&uppercase-string', (string) $positive); + assertType('lowercase-string&numeric-string&uppercase-string', (string) $negative); + + if ($positive !== 0) { + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string) $positive); + } + if ($negative !== 0) { + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string) $negative); + } +} diff --git a/tests/PHPStan/Analyser/data/catch-without-variable.php b/tests/PHPStan/Analyser/nsrt/catch-without-variable.php similarity index 100% rename from tests/PHPStan/Analyser/data/catch-without-variable.php rename to tests/PHPStan/Analyser/nsrt/catch-without-variable.php diff --git a/tests/PHPStan/Analyser/nsrt/class-constant-native-type.php b/tests/PHPStan/Analyser/nsrt/class-constant-native-type.php new file mode 100644 index 0000000000..f8db2259e0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-constant-native-type.php @@ -0,0 +1,63 @@ += 8.3 + +namespace ClassConstantNativeType; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public const int FOO = 1; + + public function doFoo(): void + { + assertType('1', self::FOO); + assertType('int', static::FOO); + assertType('int', $this::FOO); + } + +} + +final class FinalFoo +{ + + public const int FOO = 1; + + public function doFoo(): void + { + assertType('1', self::FOO); + assertType('1', static::FOO); + assertType('1', $this::FOO); + } + +} + +class FooWithPhpDoc +{ + + /** @var positive-int */ + public const int FOO = 1; + + public function doFoo(): void + { + assertType('1', self::FOO); + assertType('int<1, max>', static::FOO); + assertType('int<1, max>', $this::FOO); + } + +} + +final class FinalFooWithPhpDoc +{ + + /** @var positive-int */ + public const int FOO = 1; + + public function doFoo(): void + { + assertType('1', self::FOO); + assertType('1', static::FOO); + assertType('1', $this::FOO); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/class-constant-on-expr.php b/tests/PHPStan/Analyser/nsrt/class-constant-on-expr.php new file mode 100644 index 0000000000..4a96232ce4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-constant-on-expr.php @@ -0,0 +1,24 @@ +&literal-string', $std::class); + assertType('*ERROR*', $string::class); + assertType('(class-string&literal-string)|null', $stdOrNull::class); + assertType('*ERROR*', $stringOrNull::class); + assertType("'Foo'", 'Foo'::class); + } + +} diff --git a/tests/PHPStan/Analyser/data/class-constant-types.php b/tests/PHPStan/Analyser/nsrt/class-constant-types.php similarity index 91% rename from tests/PHPStan/Analyser/data/class-constant-types.php rename to tests/PHPStan/Analyser/nsrt/class-constant-types.php index 9d60af25c5..8f19f7659e 100644 --- a/tests/PHPStan/Analyser/data/class-constant-types.php +++ b/tests/PHPStan/Analyser/nsrt/class-constant-types.php @@ -15,6 +15,9 @@ class Foo /** @var string */ private const PRIVATE_TYPE = 'foo'; + /** @final */ + const FINAL_TYPE = 'zoo'; + public function doFoo() { assertType('1', self::NO_TYPE); @@ -28,6 +31,10 @@ public function doFoo() assertType('\'foo\'', self::PRIVATE_TYPE); assertType('string', static::PRIVATE_TYPE); assertType('string', $this::PRIVATE_TYPE); + + assertType('\'zoo\'', self::FINAL_TYPE); + assertType('\'zoo\'', static::FINAL_TYPE); + assertType('\'zoo\'', $this::FINAL_TYPE); } } diff --git a/tests/PHPStan/Analyser/nsrt/class-implements.php b/tests/PHPStan/Analyser/nsrt/class-implements.php new file mode 100644 index 0000000000..316c8e8ed4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-implements.php @@ -0,0 +1,128 @@ += 8.0 + +namespace ClassImplements; + +use function PHPStan\Testing\assertType; + +class ClassImplements +{ + /** + * @param object|class-string $objectOrClassString + * @param class-string $classString + */ + public function test( + object $object, + object|string $objectOrClassString, + object|string $objectOrString, + string $classString, + string $string, + bool $bool, + mixed $mixed, + ): void { + assertType('array', class_implements($object)); + assertType('(array|false)', class_implements($objectOrClassString)); + assertType('array|false', class_implements($objectOrString)); + assertType('(array|false)', class_implements($classString)); + assertType('array|false', class_implements($string)); + assertType('false', class_implements('thisIsNotAClass')); + + assertType('array', class_implements($object, true)); + assertType('(array|false)', class_implements($objectOrClassString, true)); + assertType('array|false', class_implements($objectOrString, true)); + assertType('(array|false)', class_implements($classString, true)); + assertType('array|false', class_implements($string, true)); + assertType('false', class_implements('thisIsNotAClass', true)); + + assertType('array', class_implements($object, false)); + assertType('array|false', class_implements($objectOrClassString, false)); + assertType('array|false', class_implements($objectOrString, false)); + assertType('array|false', class_implements($classString, false)); + assertType('array|false', class_implements($string, false)); + assertType('false', class_implements('thisIsNotAClass', false)); + + assertType('array', class_implements($object, $bool)); + assertType('array|false', class_implements($objectOrClassString, $bool)); + assertType('array|false', class_implements($objectOrString, $bool)); + assertType('array|false', class_implements($classString, $bool)); + assertType('array|false', class_implements($string, $bool)); + assertType('false', class_implements('thisIsNotAClass', $bool)); + + assertType('array', class_implements($object, $mixed)); + assertType('array|false', class_implements($objectOrClassString, $mixed)); + assertType('array|false', class_implements($objectOrString, $mixed)); + assertType('array|false', class_implements($classString, $mixed)); + assertType('array|false', class_implements($string, $mixed)); + assertType('false', class_implements('thisIsNotAClass', $mixed)); + + assertType('array', class_uses($object)); + assertType('(array|false)', class_uses($objectOrClassString)); + assertType('array|false', class_uses($objectOrString)); + assertType('(array|false)', class_uses($classString)); + assertType('array|false', class_uses($string)); + assertType('false', class_uses('thisIsNotAClass')); + + assertType('array', class_uses($object, true)); + assertType('(array|false)', class_uses($objectOrClassString, true)); + assertType('array|false', class_uses($objectOrString, true)); + assertType('(array|false)', class_uses($classString, true)); + assertType('array|false', class_uses($string, true)); + assertType('false', class_uses('thisIsNotAClass', true)); + + assertType('array', class_uses($object, false)); + assertType('array|false', class_uses($objectOrClassString, false)); + assertType('array|false', class_uses($objectOrString, false)); + assertType('array|false', class_uses($classString, false)); + assertType('array|false', class_uses($string, false)); + assertType('false', class_uses('thisIsNotAClass', false)); + + assertType('array', class_uses($object, $bool)); + assertType('array|false', class_uses($objectOrClassString, $bool)); + assertType('array|false', class_uses($objectOrString, $bool)); + assertType('array|false', class_uses($classString, $bool)); + assertType('array|false', class_uses($string, $bool)); + assertType('false', class_uses('thisIsNotAClass', $bool)); + + assertType('array', class_uses($object, $mixed)); + assertType('array|false', class_uses($objectOrClassString, $mixed)); + assertType('array|false', class_uses($objectOrString, $mixed)); + assertType('array|false', class_uses($classString, $mixed)); + assertType('array|false', class_uses($string, $mixed)); + assertType('false', class_uses('thisIsNotAClass', $mixed)); + + assertType('array', class_parents($object)); + assertType('(array|false)', class_parents($objectOrClassString)); + assertType('array|false', class_parents($objectOrString)); + assertType('(array|false)', class_parents($classString)); + assertType('array|false', class_parents($string)); + assertType('false', class_parents('thisIsNotAClass')); + + assertType('array', class_parents($object, true)); + assertType('(array|false)', class_parents($objectOrClassString, true)); + assertType('array|false', class_parents($objectOrString, true)); + assertType('(array|false)', class_parents($classString, true)); + assertType('array|false', class_parents($string, true)); + assertType('false', class_parents('thisIsNotAClass', true)); + + assertType('array', class_parents($object, false)); + assertType('array|false', class_parents($objectOrClassString, false)); + assertType('array|false', class_parents($objectOrString, false)); + assertType('array|false', class_parents($classString, false)); + assertType('array|false', class_parents($string, false)); + assertType('false', class_parents('thisIsNotAClass', false)); + + assertType('array', class_parents($object, $bool)); + assertType('array|false', class_parents($objectOrClassString, $bool)); + assertType('array|false', class_parents($objectOrString, $bool)); + assertType('array|false', class_parents($classString, $bool)); + assertType('array|false', class_parents($string, $bool)); + assertType('false', class_parents('thisIsNotAClass', $bool)); + + assertType('array', class_parents($object, $mixed)); + assertType('array|false', class_parents($objectOrClassString, $mixed)); + assertType('array|false', class_parents($objectOrString, $mixed)); + assertType('array|false', class_parents($classString, $mixed)); + assertType('array|false', class_parents($string, $mixed)); + assertType('false', class_parents('thisIsNotAClass', $mixed)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/class-name-usage-location.php b/tests/PHPStan/Analyser/nsrt/class-name-usage-location.php new file mode 100644 index 0000000000..b892739f19 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-name-usage-location.php @@ -0,0 +1,14 @@ +createIdentifier('test')); + + if ($location->value === ClassNameUsageLocation::INSTANTIATION || $location->value === ClassNameUsageLocation::PROPERTY_TYPE) { + assertType("'new.test'|'property.test'", $location->createIdentifier('test')); + } +}; diff --git a/tests/PHPStan/Analyser/data/class-reflection-interfaces.php b/tests/PHPStan/Analyser/nsrt/class-reflection-interfaces.php similarity index 100% rename from tests/PHPStan/Analyser/data/class-reflection-interfaces.php rename to tests/PHPStan/Analyser/nsrt/class-reflection-interfaces.php diff --git a/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php b/tests/PHPStan/Analyser/nsrt/classPhpDocs-phpstanPropertyPrefix.php similarity index 83% rename from tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php rename to tests/PHPStan/Analyser/nsrt/classPhpDocs-phpstanPropertyPrefix.php index f08edc152b..4e2429e6dc 100644 --- a/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php +++ b/tests/PHPStan/Analyser/nsrt/classPhpDocs-phpstanPropertyPrefix.php @@ -2,6 +2,7 @@ namespace ClassPhpDocsNamespace; +use AllowDynamicProperties; use function PHPStan\Testing\assertType; /** @@ -16,6 +17,7 @@ * @property-write string $baz * @phpstan-property-write int $baz */ +#[AllowDynamicProperties] class PhpstanProperties { public function doFoo() @@ -23,6 +25,6 @@ public function doFoo() assertType('string', $this->base); assertType('int', $this->foo); assertType('int', $this->bar); - assertType('int', $this->baz); + assertType('*NEVER*', $this->baz); } } diff --git a/tests/PHPStan/Analyser/data/classPhpDocs.php b/tests/PHPStan/Analyser/nsrt/classPhpDocs.php similarity index 95% rename from tests/PHPStan/Analyser/data/classPhpDocs.php rename to tests/PHPStan/Analyser/nsrt/classPhpDocs.php index 0d447a3d49..f0024022ce 100644 --- a/tests/PHPStan/Analyser/data/classPhpDocs.php +++ b/tests/PHPStan/Analyser/nsrt/classPhpDocs.php @@ -9,6 +9,7 @@ * @method array arrayOfStrings() * @psalm-method array arrayOfStrings() * @phpstan-method array arrayOfInts() + * @phan-method array arrayOfStrings() * @method array arrayOfInts() * @method mixed overrodeMethod() * @method static mixed overrodeStaticMethod() diff --git a/tests/PHPStan/Analyser/data/clear-stat-cache.php b/tests/PHPStan/Analyser/nsrt/clear-stat-cache.php similarity index 100% rename from tests/PHPStan/Analyser/data/clear-stat-cache.php rename to tests/PHPStan/Analyser/nsrt/clear-stat-cache.php diff --git a/tests/PHPStan/Analyser/nsrt/cli-globals.php b/tests/PHPStan/Analyser/nsrt/cli-globals.php new file mode 100644 index 0000000000..9dc930c395 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/cli-globals.php @@ -0,0 +1,33 @@ +', $argc); +assertType('non-empty-list', $argv); + +function f() { + assertType('*ERROR*', $argc); + assertType('*ERROR*', $argv); +} + +function g($argc, $argv) { + assertType('mixed', $argc); + assertType('mixed', $argv); +} + +function h() { + global $argc, $argv; + assertType('mixed', $argc); // should be int<1, max> + assertType('mixed', $argv); // should be non-empty-array +} + +function i() { + // user created local variable + $argc = 'hallo'; + $argv = 'welt'; + + assertType("'hallo'", $argc); + assertType("'welt'", $argv); +} diff --git a/tests/PHPStan/Analyser/nsrt/closure-argument-type.php b/tests/PHPStan/Analyser/nsrt/closure-argument-type.php new file mode 100644 index 0000000000..b24570b298 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-argument-type.php @@ -0,0 +1,44 @@ + $items + * @param callable(T): U $cb + * @return array + */ + public function doFoo(array $items, callable $cb) + { + + } + + public function doBar() + { + $a = [1, 2, 3]; + $b = $this->doFoo($a, function ($item) { + assertType('1|2|3', $item); + return $item; + }); + assertType('array<1|2|3>', $b); + } + + public function doBaz() + { + $a = [1, 2, 3]; + $b = $this->doFoo($a, fn ($item) => $item); + assertType('array<1|2|3>', $b); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/closure-retain-expression-types.php b/tests/PHPStan/Analyser/nsrt/closure-retain-expression-types.php new file mode 100644 index 0000000000..0db3aa730e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-retain-expression-types.php @@ -0,0 +1,23 @@ +call($newThis, new class {}); assertType('true', $returnType); + +$staticallyBoundClosureCaseInsensitive = \closure::bind($closure, $newThis); +assertType('Closure(object): true', $staticallyBoundClosureCaseInsensitive); diff --git a/tests/PHPStan/Analyser/data/closure-return-type.php b/tests/PHPStan/Analyser/nsrt/closure-return-type.php similarity index 93% rename from tests/PHPStan/Analyser/data/closure-return-type.php rename to tests/PHPStan/Analyser/nsrt/closure-return-type.php index f71b056a55..386fec990c 100644 --- a/tests/PHPStan/Analyser/data/closure-return-type.php +++ b/tests/PHPStan/Analyser/nsrt/closure-return-type.php @@ -111,12 +111,12 @@ public function doBaz(): void $f = function() { $this->returnNever(); }; - assertType('*NEVER*', $f()); + assertType('never', $f()); $f = function(): void { $this->returnNever(); }; - assertType('*NEVER*', $f()); + assertType('never', $f()); $f = function() { if (rand(0, 1)) { @@ -134,7 +134,7 @@ public function doBaz(): void $this->returnNever(); }; - assertType('*NEVER*', $f([])); + assertType('never', $f([])); $f = function(array $a) { foreach ($a as $v) { @@ -148,12 +148,12 @@ public function doBaz(): void $this->returnNever(); } }; - assertType('*NEVER*', $f()); + assertType('never', $f()); $f = function (): \stdClass { throw new \Exception(); }; - assertType('*NEVER*', $f()); + assertType('never', $f()); } } diff --git a/tests/PHPStan/Analyser/nsrt/closure-types.php b/tests/PHPStan/Analyser/nsrt/closure-types.php new file mode 100644 index 0000000000..64a168e99a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-types.php @@ -0,0 +1,70 @@ + */ + private $arrayShapes; + + public function doFoo(): void + { + $a = array_map(function (array $a): array { + assertType('array{foo: string, bar: int}', $a); + + return $a; + }, $this->arrayShapes); + assertType('array', $a); + + $b = array_map(function ($b) { + assertType('array{foo: string, bar: int}', $b); + + return $b['foo']; + }, $this->arrayShapes); + assertType('array', $b); + } + + 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); + + return 1; + }); + } + + 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); + + return 1; + }); + } + + public function closureNewThisIntersection(stdClass $foo) { + if (!$foo instanceof DateTimeInterface) { + return; + } + + (function () { + assertType('DateTimeInterface&stdClass', $this); + })->call($foo); + } + + public function arrowFunctionNewThisIntersection(stdClass $foo) { + if (!$foo instanceof DateTimeInterface) { + return; + } + + (fn () => assertType('DateTimeInterface&stdClass', $this))->call($foo); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/collected-data.php b/tests/PHPStan/Analyser/nsrt/collected-data.php new file mode 100644 index 0000000000..10c6d5fd8c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/collected-data.php @@ -0,0 +1,36 @@ + + */ +class TestCollector implements Collector +{ + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope) + { + return 1; + } + +} + +class Foo +{ + + public function doFoo(CollectedDataNode $node): void + { + assertType('array>', $node->get(TestCollector::class)); + } + +} diff --git a/tests/PHPStan/Analyser/data/compact.php b/tests/PHPStan/Analyser/nsrt/compact.php similarity index 100% rename from tests/PHPStan/Analyser/data/compact.php rename to tests/PHPStan/Analyser/nsrt/compact.php diff --git a/tests/PHPStan/Analyser/data/comparison-operators.php b/tests/PHPStan/Analyser/nsrt/comparison-operators.php similarity index 100% rename from tests/PHPStan/Analyser/data/comparison-operators.php rename to tests/PHPStan/Analyser/nsrt/comparison-operators.php diff --git a/tests/PHPStan/Analyser/data/complex-generics-example.php b/tests/PHPStan/Analyser/nsrt/complex-generics-example.php similarity index 100% rename from tests/PHPStan/Analyser/data/complex-generics-example.php rename to tests/PHPStan/Analyser/nsrt/complex-generics-example.php diff --git a/tests/PHPStan/Analyser/nsrt/composer-array-bug.php b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php new file mode 100644 index 0000000000..354577f098 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php @@ -0,0 +1,61 @@ +config['authors'])) { + foreach ($this->config['authors'] as $key => $author) { + if (!is_array($author)) { + $this->errors[] = 'authors.'.$key.' : should be an array, '.gettype($author).' given'; + assertType("mixed", $this->config['authors']); + unset($this->config['authors'][$key]); + assertType("mixed", $this->config['authors']); + continue; + } + assertType("mixed", $this->config['authors']); + foreach (['homepage', 'email', 'name', 'role'] as $authorData) { + if (isset($author[$authorData]) && !is_string($author[$authorData])) { + $this->errors[] = 'authors.'.$key.'.'.$authorData.' : invalid value, must be a string'; + unset($this->config['authors'][$key][$authorData]); + } + } + if (isset($author['homepage'])) { + assertType("mixed", $this->config['authors']); + unset($this->config['authors'][$key]['homepage']); + assertType("mixed", $this->config['authors']); + } + if (isset($author['email']) && !filter_var($author['email'], FILTER_VALIDATE_EMAIL)) { + unset($this->config['authors'][$key]['email']); + } + if (empty($this->config['authors'][$key])) { + unset($this->config['authors'][$key]); + } + } + + assertType("non-empty-array&hasOffsetValue('authors', mixed)", $this->config); + assertType("mixed", $this->config['authors']); + + if (empty($this->config['authors'])) { + unset($this->config['authors']); + assertType("array", $this->config); + } else { + assertType("non-empty-array&hasOffsetValue('authors', mixed~(0|0.0|''|'0'|array{}|false|null))", $this->config); + } + + assertType('array', $this->config); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php b/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php new file mode 100644 index 0000000000..0cec47c79a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php @@ -0,0 +1,46 @@ +config['authors'])) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $this->config['authors']); + foreach ($this->config['authors'] as $key => $author) { + assertType("mixed", $this->config['authors']); + if (!is_array($author)) { + unset($this->config['authors'][$key]); + assertType("mixed", $this->config['authors']); + continue; + } + foreach (['homepage', 'email', 'name', 'role'] as $authorData) { + if (isset($author[$authorData]) && !is_string($author[$authorData])) { + unset($this->config['authors'][$key][$authorData]); + } + } + if (isset($author['homepage'])) { + unset($this->config['authors'][$key]['homepage']); + } + if (isset($author['email']) && !filter_var($author['email'], FILTER_VALIDATE_EMAIL)) { + unset($this->config['authors'][$key]['email']); + } + if (empty($this->config['authors'][$key])) { + assertType("mixed", $this->config['authors']); + unset($this->config['authors'][$key]); + assertType("mixed", $this->config['authors']); + } + assertType("mixed", $this->config['authors']); + } + assertType("mixed", $this->config['authors']); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/composer-treatPhpDocTypesAsCertainBug.php b/tests/PHPStan/Analyser/nsrt/composer-treatPhpDocTypesAsCertainBug.php new file mode 100644 index 0000000000..4754ea1db4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/composer-treatPhpDocTypesAsCertainBug.php @@ -0,0 +1,42 @@ + $files + */ + function setupDummyRepo(array $files): void + { + assertType('array', $files); + assertNativeType('array', $files); + foreach ($files as $path => $content) { + assertType('non-empty-array', $files); + assertNativeType('non-empty-array', $files); + assertType('string', $path); + assertNativeType('(int|string)', $path); + assertType('string|null', $content); + assertNativeType('mixed', $content); + assertType('string|null', $files[$path]); + assertNativeType('mixed', $files[$path]); + if ($files[$path] === null) { + assertType('null', $files[$path]); + assertNativeType('null', $files[$path]); + $files[$path] = 'content'; + assertType('\'content\'', $files[$path]); + assertNativeType('\'content\'', $files[$path]); + } + + assertType('string', $files[$path]); + assertNativeType('mixed~null', $files[$path]); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/conditional-non-empty-array.php b/tests/PHPStan/Analyser/nsrt/conditional-non-empty-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/conditional-non-empty-array.php rename to tests/PHPStan/Analyser/nsrt/conditional-non-empty-array.php diff --git a/tests/PHPStan/Analyser/nsrt/conditional-types-constant.php b/tests/PHPStan/Analyser/nsrt/conditional-types-constant.php new file mode 100644 index 0000000000..2d19a65439 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-types-constant.php @@ -0,0 +1,24 @@ +returnsTrueForPREG_SPLIT_NO_EMPTY(PREG_SPLIT_NO_EMPTY)); + assertType('true', $this->returnsTrueForPREG_SPLIT_NO_EMPTY(1)); + assertType('false', $this->returnsTrueForPREG_SPLIT_NO_EMPTY(PREG_SPLIT_OFFSET_CAPTURE)); + assertType('false', $this->returnsTrueForPREG_SPLIT_NO_EMPTY(4)); + assertType('bool', $this->returnsTrueForPREG_SPLIT_NO_EMPTY($_GET['flag'])); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php b/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php new file mode 100644 index 0000000000..596e97634a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php @@ -0,0 +1,93 @@ += 8.0 + +namespace ConditionalTypesInference; + + +use function PHPStan\Testing\assertType; + +/** + * @return ($value is int ? true : false) + */ +function testIsInt(mixed $value): bool +{ + return is_int($value); +} + +/** + * @return ($value is not int ? true : false) + */ +function testIsNotInt(mixed $value): bool +{ + return !is_int($value); +} + +/** + * @return ($value is int ? void : never) + */ +function assertIsInt(mixed $value): void { + assert(is_int($value)); +} + +function (mixed $value) { + if (testIsInt($value)) { + assertType('int', $value); + } else { + assertType('mixed~int', $value); + } + + if (testIsNotInt($value)) { + assertType('mixed~int', $value); + } else { + assertType('int', $value); + } + + assertIsInt($value); + assertType('int', $value); +}; + +function (string $value) { + if (testIsInt($value)) { + assertType('*NEVER*', $value); + } else { + assertType('string', $value); + } + + if (testIsNotInt($value)) { + assertType('string', $value); + } else { + assertType('*NEVER*', $value); + } + + assertIsInt($value); + assertType('*NEVER*', $value); +}; + +function (int $value) { + if (testIsInt($value)) { + assertType('int', $value); + } else { + assertType('*NEVER*', $value); + } + + if (testIsNotInt($value)) { + assertType('*NEVER*', $value); + } else { + assertType('int', $value); + } + + assertIsInt($value); + assertType('int', $value); +}; + +/** + * @return ($condition is true ? void : never) + */ +function invariant(bool $condition, string $message): void +{ + assert($condition, $message); +} + +function (mixed $value) { + invariant(is_array($value), 'must be array'); + assertType('array', $value); +}; diff --git a/tests/PHPStan/Analyser/nsrt/conditional-types.php b/tests/PHPStan/Analyser/nsrt/conditional-types.php new file mode 100644 index 0000000000..0cdc741503 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-types.php @@ -0,0 +1,252 @@ + + * + * @param TArray $array + * + * @return (TArray is non-empty-array ? non-empty-list : list) + */ + abstract public function arrayKeys(array $array); + + /** + * @param array $array + * @param non-empty-array $nonEmptyArray + * + * @param array $intArray + * @param non-empty-array $nonEmptyIntArray + * + * @param array{} $emptyArray + */ + public function testArrayKeys(array $array, array $nonEmptyArray, array $intArray, array $nonEmptyIntArray, array $emptyArray): void + { + assertType('list<(int|string)>', $this->arrayKeys($array)); + assertType('list', $this->arrayKeys($intArray)); + + assertType('non-empty-list<(int|string)>', $this->arrayKeys($nonEmptyArray)); + assertType('non-empty-list', $this->arrayKeys($nonEmptyIntArray)); + + assertType('list<*NEVER*>', $this->arrayKeys($emptyArray)); + } + + /** + * @return ($array is non-empty-array ? true : false) + */ + abstract public function accessory(array $array): bool; + + /** + * @param array $array + * @param non-empty-array $nonEmptyArray + * @param array{} $emptyArray + */ + public function testAccessory(array $array, array $nonEmptyArray, array $emptyArray): void + { + assertType('bool', $this->accessory($array)); + assertType('true', $this->accessory($nonEmptyArray)); + assertType('false', $this->accessory($emptyArray)); + assertType('bool', $this->accessory($_GET['array'])); + } + + /** + * @return ($as_float is true ? float : string) + */ + abstract public function microtime(bool $as_float); + + public function testMicrotime(): void + { + assertType('float', $this->microtime(true)); + assertType('string', $this->microtime(false)); + + assertType('float|string', $this->microtime($_GET['as_float'])); + } + + /** + * @return ($version is 8 ? true : ($version is 10 ? true : false)) + */ + abstract public function versionIsEightOrTen(int $version); + + public function testVersionIsEightOrTen(): void + { + assertType('false', $this->versionIsEightOrTen(6)); + assertType('false', $this->versionIsEightOrTen(7)); + assertType('true', $this->versionIsEightOrTen(8)); + assertType('false', $this->versionIsEightOrTen(9)); + assertType('true', $this->versionIsEightOrTen(10)); + assertType('false', $this->versionIsEightOrTen(11)); + assertType('false', $this->versionIsEightOrTen(12)); + + assertType('bool', $this->versionIsEightOrTen($_GET['version'])); + } + + /** + * @return ($parameter is true ? int : string) + */ + abstract public function missingParameter(); + + public function testMissingParameter(): void + { + assertType('int|string', $this->missingParameter()); + } + + /** + * @return (5 is int ? true : false) + */ + abstract public function deterministicReturnValue(); + + public function testDeterministicReturnValue(): void + { + assertType('true', $this->deterministicReturnValue()); + } + + /** + * @param (true is true ? string : bool) $foo + * @param (5 is int<4, 6> ? string : bool) $bar + * @param (5 is not int<0, 4> ? (4 is bool ? float : string) : bool) $baz + */ + public function testDeterministicParameter($foo, $bar, $baz): void + { + assertType('string', $foo); + assertType('string', $bar); + assertType('string', $baz); + } + + /** + * @template TInt of int + * @param TInt $foo + * @param (TInt is 5 ? int<0, 10> : int<10, 100>) $bar + */ + public function testConditionalInParameter(int $foo, int $bar): void + { + assertType('TInt of int (method ConditionalTypes\Test::testConditionalInParameter(), argument)', $foo); + assertType('int<0, 100>', $bar); + } + + /** + * @return ($input is null ? null : string) + */ + abstract public function retainNullable(?bool $input): ?string; + + public function testRetainNullable(?bool $input): void + { + assertType('string|null', $this->retainNullable($input)); + + if ($input === null) { + assertType('null', $this->retainNullable($input)); + } else { + assertType('string', $this->retainNullable($input)); + } + } + + /** + * @return ($option is 1 ? never : void) + */ + abstract public function maybeNever(int $option): void; + + public function testMaybeNever(): void + { + assertType('null', $this->maybeNever(0)); + assertType('never', $this->maybeNever(1)); + assertType('null', $this->maybeNever(2)); + } + + /** + * @return ($if is true ? mixed : null)|false + */ + abstract public function lateConditional1(bool $if); + + /** + * @return ($if is true ? mixed : null)|($if is true ? null : mixed)|false + */ + abstract public function lateConditional2(bool $if); + + public function testLateConditional(): void + { + assertType('mixed', $this->lateConditional1(true)); + assertType('false|null', $this->lateConditional1(false)); + + assertType('mixed', $this->lateConditional2(true)); + assertType('mixed', $this->lateConditional2(false)); + } +} + +class ParentClassToInherit +{ + + /** + * @param mixed $p + * @return ($p is int ? int : string) + */ + public function doFoo($p) + { + + } + +} + +class ChildClass extends ParentClassToInherit +{ + + public function doFoo($p) + { + + } + +} + +function (ChildClass $c): void { + assertType('int', $c->doFoo(1)); + assertType('string', $c->doFoo('foo')); +}; + +class ChildClass2 extends ParentClassToInherit +{ + + public function doFoo($x) + { + + } + +} + +function (ChildClass2 $c): void { + assertType('int', $c->doFoo(1)); + assertType('string', $c->doFoo('foo')); +}; + +/** + * @template T of object + */ +class ConditionalTypeFromClassScopeGenerics +{ + + /** + * @return (T is \Exception ? string : int) + */ + public function doFoo() + { + + } + +} + +class TestConditionalTypeFromClassScopeGenerics +{ + + /** + * @param ConditionalTypeFromClassScopeGenerics<\Exception> $a + * @param ConditionalTypeFromClassScopeGenerics<\stdClass> $b + */ + public function doFoo(ConditionalTypeFromClassScopeGenerics $a, ConditionalTypeFromClassScopeGenerics $b) + { + assertType('string', $a->doFoo()); + assertType('int', $b->doFoo()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/conditional-vars.php b/tests/PHPStan/Analyser/nsrt/conditional-vars.php new file mode 100644 index 0000000000..568c6a8b7f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-vars.php @@ -0,0 +1,38 @@ + $innerHits */ + public function conditionalVarInTernary(array $innerHits): void + { + if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) { + assertType('non-empty-array', $innerHits); + $x = array_key_exists('nearest_premise', $innerHits) + ? assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits) + : assertType('non-empty-array', $innerHits); + + assertType('non-empty-array', $innerHits); + } + assertType('array', $innerHits); + } + + /** @param array $innerHits */ + public function conditionalVarInIf(array $innerHits): void + { + if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) { + assertType('non-empty-array', $innerHits); + if (array_key_exists('nearest_premise', $innerHits)) { + assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits); + } else { + assertType('non-empty-array', $innerHits); + } + + assertType('non-empty-array', $innerHits); + } + assertType('array', $innerHits); + } +} diff --git a/tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php b/tests/PHPStan/Analyser/nsrt/const-expr-phpdoc-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php rename to tests/PHPStan/Analyser/nsrt/const-expr-phpdoc-type.php diff --git a/tests/PHPStan/Analyser/data/const-in-functions-namespaced.php b/tests/PHPStan/Analyser/nsrt/const-in-functions-namespaced.php similarity index 100% rename from tests/PHPStan/Analyser/data/const-in-functions-namespaced.php rename to tests/PHPStan/Analyser/nsrt/const-in-functions-namespaced.php diff --git a/tests/PHPStan/Analyser/data/const-in-functions.php b/tests/PHPStan/Analyser/nsrt/const-in-functions.php similarity index 100% rename from tests/PHPStan/Analyser/data/const-in-functions.php rename to tests/PHPStan/Analyser/nsrt/const-in-functions.php diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-intersect.php b/tests/PHPStan/Analyser/nsrt/constant-array-intersect.php new file mode 100644 index 0000000000..c1633bb14a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/constant-array-intersect.php @@ -0,0 +1,17 @@ + $array1 + * @param array&array{key: string|null} $array2 + */ +function test( + array $array1, + array $array2, +): void { + assertType('array{key: string}', $array1); + assertType('array{key: string}', $array2); +} diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php new file mode 100644 index 0000000000..fe3512a45b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php @@ -0,0 +1,114 @@ +|int $nextAutoIndexes + * @return void + */ + public function doFoo($nextAutoIndexes) + { + assertType('int|non-empty-list', $nextAutoIndexes); + if (is_int($nextAutoIndexes)) { + assertType('int', $nextAutoIndexes); + } else { + assertType('non-empty-list', $nextAutoIndexes); + } + assertType('int|non-empty-list', $nextAutoIndexes); + } + + /** + * @param non-empty-list|int $nextAutoIndexes + * @return void + */ + public function doBar($nextAutoIndexes) + { + assertType('int|non-empty-list', $nextAutoIndexes); + if (is_int($nextAutoIndexes)) { + $nextAutoIndexes = [$nextAutoIndexes]; + assertType('array{int}', $nextAutoIndexes); + } else { + assertType('non-empty-list', $nextAutoIndexes); + } + assertType('non-empty-list', $nextAutoIndexes); + } + +} + +class Baz +{ + + public function doFoo() + { + $conditionalArray = [1, 1, 1]; + if (doFoo()) { + $conditionalArray[] = 2; + $conditionalArray[] = 3; + } + + assertType('array{1, 1, 1, 2, 3}|array{1, 1, 1}', $conditionalArray); + + $unshiftedConditionalArray = $conditionalArray; + array_unshift($unshiftedConditionalArray, 'lorem', new \stdClass()); + assertType("array{'lorem', stdClass, 1, 1, 1, 2, 3}|array{'lorem', stdClass, 1, 1, 1}", $unshiftedConditionalArray); + + assertType('array{1, 1, 1, 1, 1, 2, 3}|array{1, 1, 1, 1, 1}|array{1, 1, 1, 2, 3, 2, 3}|array{1, 1, 1, 2, 3}', $conditionalArray + $unshiftedConditionalArray); + assertType("array{'lorem', stdClass, 1, 1, 1, 2, 3}|array{'lorem', stdClass, 1, 1, 1}", $unshiftedConditionalArray + $conditionalArray); + + $conditionalArray[] = 4; + assertType('array{1, 1, 1, 2, 3, 4}|array{1, 1, 1, 4}', $conditionalArray); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-type-identical.php b/tests/PHPStan/Analyser/nsrt/constant-array-type-identical.php new file mode 100644 index 0000000000..69bfb7a1a3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/constant-array-type-identical.php @@ -0,0 +1,80 @@ + 1]; + } else { + $array = ['b' => 1]; + } + + assertType('array{a: 1}|array{b: 1}', $array); + + array_unshift($array, 2); + + assertType('array{0: 2, a: 1}|array{0: 2, b: 1}', $array); +}; diff --git a/tests/PHPStan/Analyser/nsrt/constant-phpdoc-type.php b/tests/PHPStan/Analyser/nsrt/constant-phpdoc-type.php new file mode 100644 index 0000000000..cc739c1d5e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/constant-phpdoc-type.php @@ -0,0 +1,55 @@ += 8.1 + +namespace Constant; + +use function PHPStan\Testing\assertType; + +define('FOO', 'foo'); +const BAR = 'bar'; + +class Baz +{ + const BAZ = 'baz'; +} + +enum Suit +{ + case Hearts; +} + +function doFoo(string $constantName): void +{ + assertType('mixed', constant($constantName)); +} + +assertType("'foo'", FOO); +assertType("'foo'", constant('FOO')); +assertType("*ERROR*", constant('\Constant\FOO')); + +assertType("'bar'", BAR); +assertType("*ERROR*", constant('BAR')); +assertType("'bar'", constant('\Constant\BAR')); + +assertType("'bar'|'foo'", constant(rand(0, 1) ? 'FOO' : '\Constant\BAR')); + +assertType("'baz'", constant('\Constant\Baz::BAZ')); + +assertType('Constant\Suit::Hearts', Suit::Hearts); +assertType('Constant\Suit::Hearts', constant('\Constant\Suit::Hearts')); + +assertType('*ERROR*', constant('UNDEFINED')); +assertType('*ERROR*', constant('::aa')); diff --git a/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php b/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php new file mode 100644 index 0000000000..76e0bea581 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php @@ -0,0 +1,22 @@ +|false', count_chars(self::ABC)); + assertType('array|false', count_chars(self::ABC, 0)); + assertType('array|false', count_chars(self::ABC, 1)); + assertType('array|false', count_chars(self::ABC, 2)); + + assertType('string|false', count_chars(self::ABC, 3)); + assertType('string|false', count_chars(self::ABC, 4)); + + assertType('string|false', count_chars(self::ABC, -1)); + assertType('string|false', count_chars(self::ABC, 5)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php b/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php new file mode 100644 index 0000000000..6e0af08f03 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php @@ -0,0 +1,22 @@ += 8.0 + +namespace CountChars; + +use function PHPStan\Testing\assertType; + +class Y { + const ABC = 'abcdef'; + + function doFoo(): void { + assertType('array', count_chars(self::ABC)); + assertType('array', count_chars(self::ABC, 0)); + assertType('array', count_chars(self::ABC, 1)); + assertType('array', count_chars(self::ABC, 2)); + + assertType('string', count_chars(self::ABC, 3)); + assertType('string', count_chars(self::ABC, 4)); + + assertType('string', count_chars(self::ABC, -1)); + assertType('string', count_chars(self::ABC, 5)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/count-const-array-2.php b/tests/PHPStan/Analyser/nsrt/count-const-array-2.php new file mode 100644 index 0000000000..f83d7d8b5f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-const-array-2.php @@ -0,0 +1,35 @@ + $limit + * @return list<\stdClass> + */ + public function searchRecommendedMinPrices(int $limit): array + { + $bestMinPrice = new \stdClass(); + $limit--; + if ($limit === 0) { + return [$bestMinPrice]; + } + + $otherMinPrices = [new \stdClass()]; + while (count($otherMinPrices) < $limit) { + $otherMinPrice = new \stdClass(); + if (rand(0, 1)) { + $otherMinPrice = null; + } + if ($otherMinPrice === null) { + break; + } + array_unshift($otherMinPrices, $otherMinPrice); + } + assertType('non-empty-list', $otherMinPrices); + return [$bestMinPrice, ...$otherMinPrices]; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/count-const-array.php b/tests/PHPStan/Analyser/nsrt/count-const-array.php new file mode 100644 index 0000000000..dfcb626150 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-const-array.php @@ -0,0 +1,78 @@ + [ + '17:00', + 'evening', + ], + '2019-01-05' => [ + '07:00', + 'morning', + ], + '2019-01-06' => [ + '12:00', + 'afternoon', + ], + '2019-01-07' => [ + '10:00', + '11:00', + '12:00', + '13:00', + '14:00', + '15:00', + '16:00', + '17:00', + 'morning', + 'afternoon', + 'evening', + ], + '2019-01-08' => [ + '07:00', + '08:00', + '13:00', + '19:00', + 'morning', + 'afternoon', + 'evening', + ], + 'anyDay' => [ + '07:00', + '08:00', + '10:00', + '11:00', + '12:00', + '13:00', + '14:00', + '15:00', + '16:00', + '17:00', + '19:00', + 'morning', + 'afternoon', + 'evening', + ], + ]; + $actualEnabledDays = $this->getEnabledDays(); + assert(count($expectedDaysResult) === count($actualEnabledDays)); + assertType("array{2019-01-04: array{'17:00', 'evening'}, 2019-01-05: array{'07:00', 'morning'}, 2019-01-06: array{'12:00', 'afternoon'}, 2019-01-07: array{'10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', 'morning', 'afternoon', 'evening'}, 2019-01-08: array{'07:00', '08:00', '13:00', '19:00', 'morning', 'afternoon', 'evening'}, anyDay: array{'07:00', '08:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '19:00', 'morning', 'afternoon', 'evening'}}", $expectedDaysResult); + } + + /** + * @return array> + */ + private function getEnabledDays(): array + { + return []; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/count-maybe.php b/tests/PHPStan/Analyser/nsrt/count-maybe.php new file mode 100644 index 0000000000..4be30d9f49 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-maybe.php @@ -0,0 +1,192 @@ + 0) { + assertType('float', $notCountable); + } else { + assertType('float', $notCountable); + } + assertType('float', $notCountable); +} + +/** + * @param array|int $maybeMode + */ +function doBar2(float $notCountable, $maybeMode): void +{ + if (count($notCountable, $maybeMode) > 0) { + assertType('float', $notCountable); + } else { + assertType('float', $notCountable); + } + assertType('float', $notCountable); +} + +function doBar3(float $notCountable, float $invalidMode): void +{ + if (count($notCountable, $invalidMode) > 0) { + assertType('float', $notCountable); + } else { + assertType('float', $notCountable); + } + assertType('float', $notCountable); +} + +/** + * @param float|int[] $maybeCountable + */ +function doFoo1($maybeCountable, int $mode): void +{ + if (count($maybeCountable, $mode) > 0) { + assertType('non-empty-array', $maybeCountable); + } else { + assertType('array|float', $maybeCountable); + } + assertType('array|float', $maybeCountable); +} + +/** + * @param float|int[] $maybeCountable + * @param array|int $maybeMode + */ +function doFoo2($maybeCountable, $maybeMode): void +{ + if (count($maybeCountable, $maybeMode) > 0) { + assertType('non-empty-array', $maybeCountable); + } else { + assertType('array|float', $maybeCountable); + } + assertType('array|float', $maybeCountable); +} + +/** + * @param float|int[] $maybeCountable + */ +function doFoo3($maybeCountable, float $invalidMode): void +{ + if (count($maybeCountable, $invalidMode) > 0) { + assertType('non-empty-array', $maybeCountable); + } else { + assertType('array|float', $maybeCountable); + } + assertType('array|float', $maybeCountable); +} + +/** + * @param float|list $maybeCountable + */ +function doFoo4($maybeCountable, int $mode): void +{ + if (count($maybeCountable, $mode) > 0) { + assertType('non-empty-list', $maybeCountable); + } else { + assertType('float|list', $maybeCountable); + } + assertType('float|list', $maybeCountable); +} + +/** + * @param float|list $maybeCountable + * @param array|int $maybeMode + */ +function doFoo5($maybeCountable, $maybeMode): void +{ + if (count($maybeCountable, $maybeMode) > 0) { + assertType('non-empty-list', $maybeCountable); + } else { + assertType('float|list', $maybeCountable); + } + assertType('float|list', $maybeCountable); +} + +/** + * @param float|list $maybeCountable + */ +function doFoo6($maybeCountable, float $invalidMode): void +{ + if (count($maybeCountable, $invalidMode) > 0) { + assertType('non-empty-list', $maybeCountable); + } else { + assertType('float|list', $maybeCountable); + } + assertType('float|list', $maybeCountable); +} + +/** + * @param float|list|Countable $maybeCountable + */ +function doFoo7($maybeCountable, int $mode): void +{ + if (count($maybeCountable, $mode) > 0) { + assertType('Countable|non-empty-list', $maybeCountable); + } else { + assertType('Countable|float|list', $maybeCountable); + } + assertType('Countable|float|list', $maybeCountable); +} + +/** + * @param float|list|Countable $maybeCountable + * @param array|int $maybeMode + */ +function doFoo8($maybeCountable, $maybeMode): void +{ + if (count($maybeCountable, $maybeMode) > 0) { + assertType('Countable|non-empty-list', $maybeCountable); + } else { + assertType('Countable|float|list', $maybeCountable); + } + assertType('Countable|float|list', $maybeCountable); +} + +/** + * @param float|list|Countable $maybeCountable + */ +function doFoo9($maybeCountable, float $invalidMode): void +{ + if (count($maybeCountable, $invalidMode) > 0) { + assertType('Countable|non-empty-list', $maybeCountable); + } else { + assertType('Countable|float|list', $maybeCountable); + } + assertType('Countable|float|list', $maybeCountable); +} + +function doFooBar1(array $countable, int $mode): void +{ + if (count($countable, $mode) > 0) { + assertType('non-empty-array', $countable); + } else { + assertType('array{}', $countable); + } + assertType('array', $countable); +} + +/** + * @param array|int $maybeMode + */ +function doFooBar2(array $countable, $maybeMode): void +{ + if (count($countable, $maybeMode) > 0) { + assertType('non-empty-array', $countable); + } else { + assertType('array{}', $countable); + } + assertType('array', $countable); +} + +function doFooBar3(array $countable, float $invalidMode): void +{ + if (count($countable, $invalidMode) > 0) { + assertType('non-empty-array', $countable); + } else { + assertType('array{}', $countable); + } + assertType('array', $countable); +} diff --git a/tests/PHPStan/Analyser/nsrt/count-type.php b/tests/PHPStan/Analyser/nsrt/count-type.php new file mode 100644 index 0000000000..1deb2e8695 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-type.php @@ -0,0 +1,121 @@ +', count($nonEmpty)); + assertType('int<1, max>', sizeof($nonEmpty)); + } + + /** + * @param int<3,5> $range + * @param int<0,5> $maybeZero + * @param int<-10,-5> $negative + */ + public function doFooBar( + array $arr, + int $range, + int $maybeZero, + int $negative + ) + { + if (count($arr) == $range) { + assertType('non-empty-array', $arr); + } else { + assertType('array', $arr); + } + if (count($arr) === $range) { + assertType('non-empty-array', $arr); + } else { + assertType('array', $arr); + } + + if (count($arr) == $maybeZero) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } + if (count($arr) === $maybeZero) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } + + if (count($arr) == $negative) { + assertType('*NEVER*', $arr); + } else { + assertType('array', $arr); + } + if (count($arr) === $negative) { + assertType('*NEVER*', $arr); + } else { + assertType('array', $arr); + } + } + + /** @param array{0: string, 1?: string} $arr */ + public function doBar(array $arr): void + { + if (count($arr) <= 1) { + assertType('1', count($arr)); + return; + } + + assertType('2', count($arr)); + assertType('array{string, string}', $arr); + } + + /** @param array{0: string, 1?: string} $arr */ + public function doBaz(array $arr): void + { + if (count($arr) > 1) { + assertType('2', count($arr)); + assertType('array{string, string}', $arr); + } + + assertType('1|2', count($arr)); + } + + public function constantArrayWhichCanBecomeList(string $h): void + { + preg_match('#^([a-z0-9-]+)\..+$#', $h, $matches); + if (count($matches) !== 2) { + return; + } + + assertType('array{string, non-empty-string}', $matches); + } + +} + +/** + * @param \ArrayObject $obj + */ +function(\ArrayObject $obj): void { + if (count($obj) === 0) { + assertType('ArrayObject', $obj); + return; + } + + assertType('ArrayObject', $obj); +}; + +function($mixed): void { + if (count($mixed) === 0) { + assertType('array{}|Countable', $mixed); + return; + } + + assertType('mixed~array{}', $mixed); +}; diff --git a/tests/PHPStan/Analyser/nsrt/countable.php b/tests/PHPStan/Analyser/nsrt/countable.php new file mode 100644 index 0000000000..740430a4c5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/countable.php @@ -0,0 +1,23 @@ +', $foo->count()); + } +} + +class NonCountable {} + +function doFoo() { + assertType('int<0, max>', count(new Foo())); + assertType('*ERROR*', count(new NonCountable())); +} diff --git a/tests/PHPStan/Analyser/nsrt/ctype-digit.php b/tests/PHPStan/Analyser/nsrt/ctype-digit.php new file mode 100644 index 0000000000..ed4704daa7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/ctype-digit.php @@ -0,0 +1,35 @@ += 8.0 + +declare(strict_types=1); + +namespace CtypeDigit; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function foo(mixed $foo): void + { + ctype_digit($foo); + assertType('mixed', $foo); + + if (is_string($foo) && ctype_digit($foo)) { + assertType('numeric-string', $foo); + } else { + assertType('mixed', $foo); + } + + if (is_int($foo) && ctype_digit($foo)) { + assertType('int<48, 57>|int<256, max>', $foo); + } else { + assertType('mixed~(int<48, 57>|int<256, max>)', $foo); + } + + if (ctype_digit($foo)) { + assertType('int<48, 57>|int<256, max>|numeric-string', $foo); + return; + } + + assertType('mixed~(int<48, 57>|int<256, max>)', $foo); // not all numeric strings are covered by ctype_digit + } +} diff --git a/tests/PHPStan/Analyser/nsrt/curl_getinfo.php b/tests/PHPStan/Analyser/nsrt/curl_getinfo.php new file mode 100644 index 0000000000..453b835778 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/curl_getinfo.php @@ -0,0 +1,66 @@ +>, primary_port: int, local_ip: string, local_port: int, http_version: int, protocol: int, ssl_verifyresult: int, scheme: string}|false)', curl_getinfo($handle, PHP_INT_MAX)); + assertType('false', curl_getinfo($handle, PHP_EOL)); + assertType('(array{url: string, content_type: string|null, http_code: int, header_size: int, request_size: int, filetime: int, ssl_verify_result: int, redirect_count: int, total_time: float, namelookup_time: float, connect_time: float, pretransfer_time: float, size_upload: float, size_download: float, speed_download: float, speed_upload: float, download_content_length: float, upload_content_length: float, starttransfer_time: float, redirect_time: float, redirect_url: string, primary_ip: string, certinfo: array>, primary_port: int, local_ip: string, local_port: int, http_version: int, protocol: int, ssl_verifyresult: int, scheme: string}|false)', curl_getinfo($handle)); + assertType('(array{url: string, content_type: string|null, http_code: int, header_size: int, request_size: int, filetime: int, ssl_verify_result: int, redirect_count: int, total_time: float, namelookup_time: float, connect_time: float, pretransfer_time: float, size_upload: float, size_download: float, speed_download: float, speed_upload: float, download_content_length: float, upload_content_length: float, starttransfer_time: float, redirect_time: float, redirect_url: string, primary_ip: string, certinfo: array>, primary_port: int, local_ip: string, local_port: int, http_version: int, protocol: int, ssl_verifyresult: int, scheme: string}|false)', curl_getinfo($handle, null)); + assertType('string', curl_getinfo($handle, CURLINFO_EFFECTIVE_URL)); + assertType('string', curl_getinfo($handle, 1048577)); // CURLINFO_EFFECTIVE_URL int value without using constant + assertType('false', curl_getinfo($handle, 12345678)); // Non constant non CURLINFO_* int value + assertType('int', curl_getinfo($handle, CURLINFO_FILETIME)); + assertType('float', curl_getinfo($handle, CURLINFO_TOTAL_TIME)); + assertType('float', curl_getinfo($handle, CURLINFO_NAMELOOKUP_TIME)); + assertType('float', curl_getinfo($handle, CURLINFO_CONNECT_TIME)); + assertType('float', curl_getinfo($handle, CURLINFO_PRETRANSFER_TIME)); + assertType('float', curl_getinfo($handle, CURLINFO_STARTTRANSFER_TIME)); + assertType('int', curl_getinfo($handle, CURLINFO_REDIRECT_COUNT)); + assertType('float', curl_getinfo($handle, CURLINFO_REDIRECT_TIME)); + assertType('string', curl_getinfo($handle, CURLINFO_REDIRECT_URL)); + assertType('string', curl_getinfo($handle, CURLINFO_PRIMARY_IP)); + assertType('int', curl_getinfo($handle, CURLINFO_PRIMARY_PORT)); + assertType('string', curl_getinfo($handle, CURLINFO_LOCAL_IP)); + assertType('int', curl_getinfo($handle, CURLINFO_LOCAL_PORT)); + assertType('int', curl_getinfo($handle, CURLINFO_SIZE_UPLOAD)); + assertType('int', curl_getinfo($handle, CURLINFO_SIZE_DOWNLOAD)); + assertType('int', curl_getinfo($handle, CURLINFO_SPEED_DOWNLOAD)); + assertType('int', curl_getinfo($handle, CURLINFO_SPEED_UPLOAD)); + assertType('int', curl_getinfo($handle, CURLINFO_HEADER_SIZE)); + assertType('string|false', curl_getinfo($handle, CURLINFO_HEADER_OUT)); + assertType('int', curl_getinfo($handle, CURLINFO_REQUEST_SIZE)); + assertType('int', curl_getinfo($handle, CURLINFO_SSL_VERIFYRESULT)); + assertType('float', curl_getinfo($handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD)); + assertType('float', curl_getinfo($handle, CURLINFO_CONTENT_LENGTH_UPLOAD)); + assertType('string|false', curl_getinfo($handle, CURLINFO_CONTENT_TYPE)); + assertType('string|false', curl_getinfo($handle, CURLINFO_PRIVATE)); + assertType('int', curl_getinfo($handle, CURLINFO_RESPONSE_CODE)); + assertType('int', curl_getinfo($handle, CURLINFO_HTTP_CONNECTCODE)); + assertType('int', curl_getinfo($handle, CURLINFO_HTTPAUTH_AVAIL)); + assertType('int', curl_getinfo($handle, CURLINFO_PROXYAUTH_AVAIL)); + assertType('int', curl_getinfo($handle, CURLINFO_OS_ERRNO)); + assertType('int', curl_getinfo($handle, CURLINFO_NUM_CONNECTS)); + assertType('array', curl_getinfo($handle, CURLINFO_SSL_ENGINES)); + assertType('array', curl_getinfo($handle, CURLINFO_COOKIELIST)); + assertType('string|false', curl_getinfo($handle, CURLINFO_FTP_ENTRY_PATH)); + assertType('float', curl_getinfo($handle, CURLINFO_APPCONNECT_TIME)); + assertType('array>', curl_getinfo($handle, CURLINFO_CERTINFO)); + assertType('int', curl_getinfo($handle, CURLINFO_CONDITION_UNMET)); + assertType('int', curl_getinfo($handle, CURLINFO_RTSP_CLIENT_CSEQ)); + assertType('int', curl_getinfo($handle, CURLINFO_RTSP_CSEQ_RECV)); + assertType('int', curl_getinfo($handle, CURLINFO_RTSP_SERVER_CSEQ)); + assertType('int', curl_getinfo($handle, CURLINFO_RTSP_SESSION_ID)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/curl_getinfo_7.3.php b/tests/PHPStan/Analyser/nsrt/curl_getinfo_7.3.php new file mode 100644 index 0000000000..a42f17ab32 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/curl_getinfo_7.3.php @@ -0,0 +1,31 @@ += 7.3 + +namespace CurlGetinfo73; + +use CurlHandle; +use function PHPStan\Testing\assertType; + +class Foo { + public function bar() + { + $handle = new CurlHandle(); + assertType('int', curl_getinfo($handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T)); + assertType('int', curl_getinfo($handle, CURLINFO_CONTENT_LENGTH_UPLOAD_T)); + assertType('int', curl_getinfo($handle, CURLINFO_HTTP_VERSION)); + assertType('string', curl_getinfo($handle, CURLINFO_PROTOCOL)); + assertType('int', curl_getinfo($handle, CURLINFO_PROXY_SSL_VERIFYRESULT)); + assertType('string', curl_getinfo($handle, CURLINFO_SCHEME)); + assertType('int', curl_getinfo($handle, CURLINFO_SIZE_DOWNLOAD_T)); + assertType('int', curl_getinfo($handle, CURLINFO_SIZE_UPLOAD_T)); + assertType('int', curl_getinfo($handle, CURLINFO_SPEED_DOWNLOAD_T)); + assertType('int', curl_getinfo($handle, CURLINFO_SPEED_UPLOAD_T)); + assertType('int', curl_getinfo($handle, CURLINFO_APPCONNECT_TIME_T)); + assertType('int', curl_getinfo($handle, CURLINFO_CONNECT_TIME_T)); + assertType('int', curl_getinfo($handle, CURLINFO_FILETIME_T)); + assertType('int', curl_getinfo($handle, CURLINFO_NAMELOOKUP_TIME_T)); + assertType('int', curl_getinfo($handle, CURLINFO_PRETRANSFER_TIME_T)); + assertType('int', curl_getinfo($handle, CURLINFO_REDIRECT_TIME_T)); + assertType('int', curl_getinfo($handle, CURLINFO_STARTTRANSFER_TIME_T)); + assertType('int', curl_getinfo($handle, CURLINFO_TOTAL_TIME_T)); + } +} diff --git a/tests/PHPStan/Analyser/data/date-format.php b/tests/PHPStan/Analyser/nsrt/date-format.php similarity index 77% rename from tests/PHPStan/Analyser/data/date-format.php rename to tests/PHPStan/Analyser/nsrt/date-format.php index 92253edb14..e8a6878521 100644 --- a/tests/PHPStan/Analyser/data/date-format.php +++ b/tests/PHPStan/Analyser/nsrt/date-format.php @@ -7,7 +7,7 @@ function (string $s): void { assertType('\'\'', date('')); assertType('string', date($s)); - assertType('non-empty-string', date('D')); + assertType('non-falsy-string', date('D')); assertType('numeric-string', date('Y')); assertType('numeric-string', date('Ghi')); }; @@ -15,7 +15,7 @@ function (string $s): void { function (\DateTime $dt, string $s): void { assertType('\'\'', date_format($dt, '')); assertType('string', date_format($dt, $s)); - assertType('non-empty-string', date_format($dt, 'D')); + assertType('non-falsy-string', date_format($dt, 'D')); assertType('numeric-string', date_format($dt, 'Y')); assertType('numeric-string', date_format($dt, 'Ghi')); }; @@ -23,7 +23,7 @@ function (\DateTime $dt, string $s): void { function (\DateTimeInterface $dt, string $s): void { assertType('\'\'', $dt->format('')); assertType('string', $dt->format($s)); - assertType('non-empty-string', $dt->format('D')); + assertType('non-falsy-string', $dt->format('D')); assertType('numeric-string', $dt->format('Y')); assertType('numeric-string', $dt->format('Ghi')); }; @@ -31,7 +31,7 @@ function (\DateTimeInterface $dt, string $s): void { function (\DateTime $dt, string $s): void { assertType('\'\'', $dt->format('')); assertType('string', $dt->format($s)); - assertType('non-empty-string', $dt->format('D')); + assertType('non-falsy-string', $dt->format('D')); assertType('numeric-string', $dt->format('Y')); assertType('numeric-string', $dt->format('Ghi')); }; @@ -39,7 +39,11 @@ function (\DateTime $dt, string $s): void { function (\DateTimeImmutable $dt, string $s): void { assertType('\'\'', $dt->format('')); assertType('string', $dt->format($s)); - assertType('non-empty-string', $dt->format('D')); + assertType('non-falsy-string', $dt->format('D')); assertType('numeric-string', $dt->format('Y')); assertType('numeric-string', $dt->format('Ghi')); }; + +function (?\DateTimeImmutable $d): void { + assertType('DateTimeImmutable|null', $d->modify('+1 day')); +}; diff --git a/tests/PHPStan/Analyser/data/date-period-return-types.php b/tests/PHPStan/Analyser/nsrt/date-period-return-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/date-period-return-types.php rename to tests/PHPStan/Analyser/nsrt/date-period-return-types.php diff --git a/tests/PHPStan/Analyser/data/date.php b/tests/PHPStan/Analyser/nsrt/date.php similarity index 100% rename from tests/PHPStan/Analyser/data/date.php rename to tests/PHPStan/Analyser/nsrt/date.php diff --git a/tests/PHPStan/Analyser/nsrt/dependent-expression-certainty.php b/tests/PHPStan/Analyser/nsrt/dependent-expression-certainty.php new file mode 100644 index 0000000000..302d82b609 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/dependent-expression-certainty.php @@ -0,0 +1,233 @@ +', $itemsCounter); } - assertType('Generator&iterable', $associationData); + assertType('Generator', $associationData); assertType('int<0, max>', $itemsCounter); } diff --git a/tests/PHPStan/Analyser/nsrt/discussion-10285-php8.php b/tests/PHPStan/Analyser/nsrt/discussion-10285-php8.php new file mode 100644 index 0000000000..d59a186bf0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-10285-php8.php @@ -0,0 +1,21 @@ += 8.0 + +namespace Discussion10285Php8; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello(): void + { + $socket = socket_create(AF_INET, SOCK_STREAM, 0); + if($socket === false) return; + $read = [$socket]; + $write = []; + $except = null; + socket_select($read, $write, $except, 0, 1); + assertType('array', $read); + assertType('array', $write); + assertType('null', $except); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-10285.php b/tests/PHPStan/Analyser/nsrt/discussion-10285.php new file mode 100644 index 0000000000..4b8da269ad --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-10285.php @@ -0,0 +1,21 @@ +', $read); + assertType('array', $write); + assertType('null', $except); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-8447.php b/tests/PHPStan/Analyser/nsrt/discussion-8447.php new file mode 100644 index 0000000000..21bd7d4dda --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-8447.php @@ -0,0 +1,45 @@ + + * @param TLead $lead + * @return TQuote + */ + public function store(Lead $lead): Quote + { + assertType('TQuote of Discussion8447\Quote (class Discussion8447\Controller, argument)', $lead->quoteRepository()->create()); + return $lead->quoteRepository()->create(); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-9134.php b/tests/PHPStan/Analyser/nsrt/discussion-9134.php new file mode 100644 index 0000000000..330b51cbaa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-9134.php @@ -0,0 +1,12 @@ +|false', $res); +if (is_array($res) === false) { + throw new \RuntimeException(); +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-9972.php b/tests/PHPStan/Analyser/nsrt/discussion-9972.php new file mode 100644 index 0000000000..c0e4eabf23 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-9972.php @@ -0,0 +1,26 @@ +helper($myBool); + + if ($myBool) { + assertVariableCertainty(TrinaryLogic::createYes(), $myObject); + } + } + + protected function helper(bool $input): void + { + } +} diff --git a/tests/PHPStan/Analyser/nsrt/div-by-zero.php b/tests/PHPStan/Analyser/nsrt/div-by-zero.php new file mode 100644 index 0000000000..ad027888bc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/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<1, 5>', 5 / $range1); + assertType('float|int<-5, -1>', 5 / $range2); + assertType('float|int', $range1 / $range2); + assertType('(float|int)', 5 / $int); + + assertType('*ERROR*', 5 / 0); + assertType('*ERROR*', 5 / '0'); + assertType('*ERROR*', 5 / 0.0); + assertType('*ERROR*', 5 / false); + assertType('*ERROR*', 5 / null); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/dnf.php b/tests/PHPStan/Analyser/nsrt/dnf.php new file mode 100644 index 0000000000..dbe8d11eef --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/dnf.php @@ -0,0 +1,25 @@ += 8.2 + +namespace Dnf; + +use function PHPStan\Testing\assertType; + +interface A {} +interface B {} +interface D {} + +class Foo +{ + + public function doFoo((A&B)|D $a): void + { + assertType('(Dnf\A&Dnf\B)|Dnf\D', $a); + assertType('(Dnf\A&Dnf\B)|Dnf\D', $this->doBar()); + } + + public function doBar(): (A&B)|D + { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/do-not-remember-impure-functions.php b/tests/PHPStan/Analyser/nsrt/do-not-remember-impure-functions.php new file mode 100644 index 0000000000..6900709f99 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/do-not-remember-impure-functions.php @@ -0,0 +1,123 @@ +', rand(0, 1)); + } + }; + + function (): void { + if (rand(0, 1) === 0) { + assertType('int<0, 1>', rand(0, 1)); + } + }; + function (): void { + assertType('1|\'foo\'', rand(0, 1) ?: 'foo'); + assertType('\'foo\'|int<0, 1>', rand(0, 1) ? rand(0, 1) : 'foo'); + }; + } + + public function doBar(): bool + { + + } + + /** @phpstan-pure */ + public function doBaz(): bool + { + + } + + /** @phpstan-impure */ + public function doLorem(): bool + { + + } + + public function doIpsum() + { + if ($this->doBar() === true) { + assertType('true', $this->doBar()); + } + + if ($this->doBaz() === true) { + assertType('true', $this->doBaz()); + } + + if ($this->doLorem() === true) { + assertType('bool', $this->doLorem()); + } + } + + public function doDolor() + { + if ($this->doBar()) { + assertType('true', $this->doBar()); + } + + if ($this->doBaz()) { + assertType('true', $this->doBaz()); + } + + if ($this->doLorem()) { + assertType('bool', $this->doLorem()); + } + } + +} + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + /** + * @return int + */ + public function pure(): int + { + echo 'test'; + return 1; + } + + /** + * @return int + */ + public function impure(): int + { + return 1; + } + +} + +function (ExtendingClass $e): void { + assert($e->pure() === 1); + assertType('1', $e->pure()); + $e->impure(); + assertType('int', $e->pure()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/ds-copy.php b/tests/PHPStan/Analyser/nsrt/ds-copy.php new file mode 100644 index 0000000000..8bf29a71ba --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/ds-copy.php @@ -0,0 +1,65 @@ + $col + * @param Sequence $seq + * @param Vector $vec + * @param Deque $deque + * @param Map $map + * @param Queue $queue + * @param Stack $stack + * @param PriorityQueue $pq + * @param Set $set + */ + public function __construct( + private readonly Collection $col, + private readonly Sequence $seq, + private readonly Vector $vec, + private readonly Deque $deque, + private readonly Map $map, + private readonly Queue $queue, + private readonly Stack $stack, + private readonly PriorityQueue $pq, + private readonly Set $set, + ) { + } + + public function copy(): void + { + $col = $this->col->copy(); + $seq = $this->seq->copy(); + $vec = $this->vec->copy(); + $deque = $this->deque->copy(); + $map = $this->map->copy(); + $queue = $this->queue->copy(); + $stack = $this->stack->copy(); + $pq = $this->pq->copy(); + $set = $this->set->copy(); + + assertType('Ds\Collection', $col); + assertType('Ds\Sequence', $seq); + assertType('Ds\Vector', $vec); + assertType('Ds\Deque', $deque); + assertType('Ds\Map', $map); + assertType('Ds\Queue', $queue); + assertType('Ds\Stack', $stack); + assertType('Ds\PriorityQueue', $pq); + assertType('Ds\Set', $set); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php new file mode 100644 index 0000000000..3555613fe0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php @@ -0,0 +1,39 @@ + $a + * @param 'b'|'bb' $b + */ + public function integerRange(int $a, string $b): void + { + assertType("'0 b'|'0 bb'|'1 b'|'1 bb'|'2 b'|'2 bb'|'3 b'|'3 bb'", sprintf('%d %s', $a, $b)); + } + + /** + * @param int<0,64> $a + * @param 'b'|'bb' $b + */ + public function tooBigRange(int $a, string $b): void + { + assertType("lowercase-string&non-falsy-string", sprintf('%d %s', $a, $b)); + } + +} diff --git a/tests/PHPStan/Analyser/data/early-termination-phpdoc.php b/tests/PHPStan/Analyser/nsrt/early-termination-phpdoc.php similarity index 100% rename from tests/PHPStan/Analyser/data/early-termination-phpdoc.php rename to tests/PHPStan/Analyser/nsrt/early-termination-phpdoc.php diff --git a/tests/PHPStan/Analyser/data/empty-array-shape.php b/tests/PHPStan/Analyser/nsrt/empty-array-shape.php similarity index 100% rename from tests/PHPStan/Analyser/data/empty-array-shape.php rename to tests/PHPStan/Analyser/nsrt/empty-array-shape.php diff --git a/tests/PHPStan/Analyser/nsrt/emptyiterator.php b/tests/PHPStan/Analyser/nsrt/emptyiterator.php new file mode 100644 index 0000000000..fd338eda7f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/emptyiterator.php @@ -0,0 +1,18 @@ +key()); + assertType('never', $it->current()); + assertType('null', $it->next()); + assertType('false', $it->valid()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-from.php b/tests/PHPStan/Analyser/nsrt/enum-from.php new file mode 100644 index 0000000000..9f65726914 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-from.php @@ -0,0 +1,94 @@ += 8.1 + +declare(strict_types=1); + +namespace EnumFrom; + +use function PHPStan\Testing\assertType; + +enum FooIntegerEnum: int +{ + + case BAR = 1; + case BAZ = 2; + +} + +enum FooIntegerEnumSubset: int +{ + + case BAR = 1; + +} + +enum FooStringEnum: string +{ + + case BAR = 'bar'; + case BAZ = 'baz'; + +} + +enum FooNumericStringEnum: string +{ + + case ONE = '1'; + +} + +class Foo +{ + + public function doFoo(): void + { + assertType('1', FooIntegerEnum::BAR->value); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::BAR); + + assertType('null', FooIntegerEnum::tryFrom(0)); + assertType(FooIntegerEnum::class, FooIntegerEnum::from(0)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::tryFrom(0 + 1)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::from(1 * FooIntegerEnum::BAR->value)); + + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::tryFrom(2)); + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::tryFrom(FooIntegerEnum::BAZ->value)); + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::from(FooIntegerEnum::BAZ->value)); + + assertType("'bar'", FooStringEnum::BAR->value); + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::BAR); + + assertType('null', FooStringEnum::tryFrom('barz')); + assertType(FooStringEnum::class, FooStringEnum::from('barz')); + + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::tryFrom('ba' . 'r')); + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::from(sprintf('%s%s', 'ba', 'r'))); + + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::tryFrom('baz')); + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::tryFrom(FooStringEnum::BAZ->value)); + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::from(FooStringEnum::BAZ->value)); + + assertType('null', FooIntegerEnum::tryFrom('1')); + assertType('null', FooIntegerEnum::tryFrom(1.0)); + assertType('null', FooIntegerEnum::tryFrom(1.0001)); + assertType('null', FooIntegerEnum::tryFrom(true)); + assertType('null', FooNumericStringEnum::tryFrom(1)); + } + + public function supersetToSubset(FooIntegerEnum $foo): void + { + assertType('EnumFrom\FooIntegerEnumSubset::BAR|null', FooIntegerEnumSubset::tryFrom($foo->value)); + assertType('EnumFrom\FooIntegerEnumSubset::BAR', FooIntegerEnumSubset::from($foo->value)); + } + + public function subsetToSuperset(FooIntegerEnumSubset $foo): void + { + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::tryFrom($foo->value)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::from($foo->value)); + } + + public function doCaseInsensitive(): void + { + assertType('1', FooInTeGerEnum::BAR->value); + assertType('null', FooInTeGerEnum::tryFrom(0)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-in-array.php b/tests/PHPStan/Analyser/nsrt/enum-in-array.php new file mode 100644 index 0000000000..ca34261bc8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-in-array.php @@ -0,0 +1,187 @@ += 8.1 + +use function PHPStan\Testing\assertType; + +enum MyEnum: string +{ + + case A = 'a'; + case B = 'b'; + case C = 'c'; + + const SET_AB = [self::A, self::B]; + const SET_C = [self::C]; + const SET_ABC = [self::A, self::B, self::C]; + + public function test1(): void + { + foreach (self::cases() as $enum) { + if (in_array($enum, MyEnum::SET_AB, true)) { + assertType('MyEnum::A|MyEnum::B', $enum); + } elseif (in_array($enum, MyEnum::SET_C, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('*NEVER*', $enum); + } + } + } + + public function test2(): void + { + foreach (self::cases() as $enum) { + if (in_array($enum, MyEnum::SET_ABC, true)) { + assertType('MyEnum::A|MyEnum::B|MyEnum::C', $enum); + } else { + assertType('*NEVER*', $enum); + } + } + } + + public function test3(): void + { + foreach (self::cases() as $enum) { + if (in_array($enum, MyEnum::SET_C, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('MyEnum::A|MyEnum::B', $enum); + } + } + } + + public function test4(): void + { + foreach ([MyEnum::C] as $enum) { + if (in_array($enum, MyEnum::SET_C, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('*NEVER*', $enum); + } + } + } + + public function testNegative1(): void + { + foreach (self::cases() as $enum) { + if (!in_array($enum, MyEnum::SET_AB, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('MyEnum::A|MyEnum::B', $enum); + } + } + } + + public function testNegative2(): void + { + foreach (self::cases() as $enum) { + if (!in_array($enum, MyEnum::SET_AB, true)) { + assertType('MyEnum::C', $enum); + } elseif (!in_array($enum, MyEnum::SET_AB, true)) { + assertType('*NEVER*', $enum); + } + } + } + + public function testNegative3(): void + { + foreach ([MyEnum::C] as $enum) { + if (!in_array($enum, MyEnum::SET_C, true)) { + assertType('*NEVER*', $enum); + } + } + } + + /** + * @param array $array + */ + public function testNegative4(MyEnum $enum, array $array): void + { + if (!in_array($enum, $array, true)) { + assertType('MyEnum', $enum); + assertType('array', $array); + } else { + assertType('MyEnum', $enum); + assertType('non-empty-array', $array); + } + } + +} + +class InArrayEnum +{ + + /** @param list $list */ + public function testPositive(MyEnum $enum, array $list): void + { + if (in_array($enum, $list, true)) { + return; + } + + assertType(MyEnum::class, $enum); + assertType('list', $list); + } + + /** @param list $list */ + public function testNegative(MyEnum $enum, array $list): void + { + if (!in_array($enum, $list, true)) { + return; + } + + assertType(MyEnum::class, $enum); + assertType('non-empty-list', $list); + } + +} + + +class InArrayOtherFiniteType { + + const SET_AB = ['a', 'b']; + const SET_C = ['c']; + const SET_ABC = ['a', 'b', 'c']; + + public function test1(): void + { + foreach (['a', 'b', 'c'] as $item) { + if (in_array($item, self::SET_AB, true)) { + assertType("'a'|'b'", $item); + } elseif (in_array($item, self::SET_C, true)) { + assertType("'c'", $item); + } else { + assertType('*NEVER*', $item); + } + } + } + + public function test2(): void + { + foreach (['a', 'b', 'c'] as $item) { + if (in_array($item, self::SET_ABC, true)) { + assertType("'a'|'b'|'c'", $item); + } else { + assertType('*NEVER*', $item); + } + } + } + + public function test3(): void + { + foreach (['a', 'b', 'c'] as $item) { + if (in_array($item, self::SET_C, true)) { + assertType("'c'", $item); + } else { + assertType("'a'|'b'", $item); + } + } + } + public function test4(): void + { + foreach (['c'] as $item) { + if (in_array($item, self::SET_C, true)) { + assertType("'c'", $item); + } else { + assertType('*NEVER*', $item); + } + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-reflection-php82.php b/tests/PHPStan/Analyser/nsrt/enum-reflection-php82.php new file mode 100644 index 0000000000..7584e5b4cf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-reflection-php82.php @@ -0,0 +1,23 @@ += 8.2 + +namespace EnumReflection82; + +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionEnumUnitCase; +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + + case FOO = 1; + case BAR = 2; +} + +function testNarrowGetBackingTypeAfterIsBacked() { + $r = new ReflectionEnum(Foo::class); + assertType('ReflectionNamedType|null', $r->getBackingType()); + if ($r->isBacked()) { + assertType('ReflectionNamedType', $r->getBackingType()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-reflection.php b/tests/PHPStan/Analyser/nsrt/enum-reflection.php new file mode 100644 index 0000000000..85e98f4f8d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-reflection.php @@ -0,0 +1,43 @@ += 8.1 + +namespace EnumReflection; + +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionEnumUnitCase; +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + + case FOO = 1; + case BAR = 2; + + public function doFoo(): void + { + $r = new ReflectionEnum(self::class); + foreach ($r->getCases() as $case) { + assertType(ReflectionEnumBackedCase::class, $case); + } + + assertType(ReflectionEnumBackedCase::class, $r->getCase('FOO')); + } + +} + +enum Bar +{ + + case FOO; + case BAR; + + public function doFoo(): void + { + $r = new ReflectionEnum(self::class); + foreach ($r->getCases() as $case) { + assertType(ReflectionEnumUnitCase::class, $case); + } + assertType(ReflectionEnumUnitCase::class, $r->getCase('FOO')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php b/tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php new file mode 100644 index 0000000000..4a9a22e4c5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php @@ -0,0 +1,43 @@ += 8.1 + +declare(strict_types = 1); + +namespace EnumVsInArray; + +use function PHPStan\Testing\assertType; + +enum FooEnum +{ + case A; + case B; + case C; + case D; + case E; + case F; + case G; + case H; + case I; + case J; +} + +function foo(FooEnum $e): int +{ + if (in_array($e, [FooEnum::A, FooEnum::B, FooEnum::C], true)) { + throw new \Exception('a'); + } + + assertType('EnumVsInArray\FooEnum~(EnumVsInArray\FooEnum::A|EnumVsInArray\FooEnum::B|EnumVsInArray\FooEnum::C)', $e); + + if (rand(0, 10) === 1) { + if (!in_array($e, [FooEnum::D, FooEnum::E], true)) { + throw new \Exception('d'); + } + } + + assertType('EnumVsInArray\FooEnum~(EnumVsInArray\FooEnum::A|EnumVsInArray\FooEnum::B|EnumVsInArray\FooEnum::C)', $e); + + return match ($e) { + FooEnum::D, FooEnum::E, FooEnum::F, FooEnum::G, FooEnum::H, FooEnum::I => 2, + FooEnum::J => 3, + }; +} diff --git a/tests/PHPStan/Analyser/nsrt/enum_exists.php b/tests/PHPStan/Analyser/nsrt/enum_exists.php new file mode 100644 index 0000000000..37809016ad --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum_exists.php @@ -0,0 +1,28 @@ += 8.0 + +namespace EnumExists; + +use function PHPStan\Testing\assertType; + +function getEnumValue(string $enumFqcn, string $name): mixed { + if (enum_exists($enumFqcn)) { + assertType('class-string', $enumFqcn); + return (new \ReflectionEnum($enumFqcn))->getCase($name)->getValue(); + } + assertType('string', $enumFqcn); + + return null; +} + +/** + * @param class-string $enumFqcn + */ +function getEnumValueFromClassString(string $enumFqcn, string $name): mixed { + if (enum_exists($enumFqcn)) { + assertType('class-string', $enumFqcn); + return (new \ReflectionEnum($enumFqcn))->getCase($name)->getValue(); + } + assertType('class-string', $enumFqcn); + + return null; +} diff --git a/tests/PHPStan/Analyser/data/enums-import-alias.php b/tests/PHPStan/Analyser/nsrt/enums-import-alias.php similarity index 100% rename from tests/PHPStan/Analyser/data/enums-import-alias.php rename to tests/PHPStan/Analyser/nsrt/enums-import-alias.php diff --git a/tests/PHPStan/Analyser/nsrt/enums.php b/tests/PHPStan/Analyser/nsrt/enums.php new file mode 100644 index 0000000000..37490c1847 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enums.php @@ -0,0 +1,350 @@ += 8.1 + +namespace EnumTypeAssertions; + +use function in_array; +use function PHPStan\Testing\assertType; + +enum Foo +{ + + case ONE; + case TWO; + + public function doFoo(): void + { + if ($this === self::ONE) { + assertType('$this(EnumTypeAssertions\Foo)&' . self::class . '::ONE', $this); + return; + } else { + assertType('$this(EnumTypeAssertions\Foo)&' . self::class . '::TWO', $this); + } + + assertType('$this(EnumTypeAssertions\Foo)&' . 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("'EnumTypeAssertions\\\\Foo'", $foo::class); + assertType(Foo::class . '::ONE', Foo::ONE); + assertType('class-string<' . Foo::class . '>&literal-string', Foo::ONE::class); + assertType(Bar::class . '::ONE', Bar::ONE); + assertType('class-string<' . Bar::class . '>&literal-string', 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()); + } + +} + +class InArrayEnum +{ + + /** @var list */ + private $list; + + public function doFoo(Foo $foo): void + { + if (in_array($foo, $this->list, true)) { + return; + } + + assertType(Foo::class, $foo); + } + +} + +class LooseComparisonWithEnums +{ + public function testEquality(Foo $foo, Bar $bar, Baz $baz, string $s, int $i, bool $b): void + { + assertType('true', $foo == $foo); + assertType('false', $foo == $bar); + assertType('false', $bar == $s); + assertType('false', $s == $bar); + assertType('false', $baz == $i); + assertType('false', $i == $baz); + + assertType('true', true == $foo); + assertType('true', $foo == true); + assertType('false', false == $baz); + assertType('false', $baz == false); + assertType('false', null == $baz); + assertType('false', $baz == null); + + assertType('true', Foo::ONE == true); + assertType('true', true == Foo::ONE); + assertType('false', Foo::ONE == false); + assertType('false', false == Foo::ONE); + assertType('false', null == Foo::ONE); + assertType('false', Foo::ONE == null); + assertType('true', $foo == Foo::ONE || Foo::TWO == $foo); + + assertType('bool', (rand() ? $bar : null) == $s); + assertType('bool', $s == (rand() ? $bar : null)); + assertType('bool', (rand() ? $baz : null) == $i); + assertType('bool', $i == (rand() ? $baz : null)); + assertType('bool', $foo == $b); + assertType('bool', $b == $foo); + } + + public function testNonEquality(Foo $foo, Bar $bar, Baz $baz, string $s, int $i, bool $b): void + { + assertType('false', $foo != $foo); + assertType('true', $foo != $bar); + assertType('true', $bar != $s); + assertType('true', $s != $bar); + assertType('true', $baz != $i); + assertType('true', $i != $baz); + + assertType('false', true != $foo); + assertType('false', $foo != true); + assertType('true', false != $baz); + assertType('true', $baz != false); + assertType('true', null != $baz); + assertType('true', $baz != null); + + assertType('false', Foo::ONE != true); + assertType('false', true != Foo::ONE); + assertType('true', Foo::ONE != false); + assertType('true', false != Foo::ONE); + assertType('true', null != Foo::ONE); + assertType('true', Foo::ONE != null); + + assertType('bool', (rand() ? $bar : null) != $s); + assertType('bool', $s != (rand() ? $bar : null)); + assertType('bool', (rand() ? $baz : null) != $i); + assertType('bool', $i != (rand() ? $baz : null)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/equal-narrow.php b/tests/PHPStan/Analyser/nsrt/equal-narrow.php new file mode 100644 index 0000000000..774377f400 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/equal-narrow.php @@ -0,0 +1,180 @@ +|int<1, max>|non-empty-string", $y); + } + + if ($z == null) { + assertType("0|0.0|''|array{}|false|null", $z); + } else { + assertType("mixed~(0|0.0|''|array{}|false|null)", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doFalse($x, $y, $z): void +{ + if ($x == false) { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } else { + assertType("1|'x'|object|true", $x); + } + if (false != $x) { + assertType("1|'x'|object|true", $x); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } + + if (!$x) { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } else { + assertType("1|'x'|object|true", $x); + } + + if ($y == false) { + assertType("0|''|'0'|null", $y); + } else { + assertType("int|int<1, max>|non-falsy-string", $y); + } + + if ($z == false) { + assertType("0|0.0|''|'0'|array{}|false|null", $z); + } else { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doTrue($x, $y, $z): void +{ + if ($x == true) { + assertType("1|'x'|object|true", $x); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } + if (true != $x) { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } else { + assertType("1|'x'|object|true", $x); + } + + if ($x) { + assertType("1|'x'|object|true", $x); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } + + if ($y == true) { + assertType("int|int<1, max>|non-falsy-string", $y); + } else { + assertType("0|''|'0'|null", $y); + } + + if ($z == true) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $z); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doZero($x, $y, $z): void +{ + // PHP 7.x/8.x compatibility: Keep zero in both cases + if ($x == 0) { + assertType("0|0.0|''|'0'|'x'|false|null", $x); + } else { + assertType("1|''|'x'|array{}|object|true", $x); + } + if (0 != $x) { + assertType("1|''|'x'|array{}|object|true", $x); + } else { + assertType("0|0.0|''|'0'|'x'|false|null", $x); + } + + if ($y == 0) { + assertType("0|string|null", $y); + } else { + assertType("int|int<1, max>|string", $y); + } + + if ($z == 0) { + assertType("0|0.0|string|false|null", $z); + } else { + assertType("mixed~(0|0.0|'0'|false|null)", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doEmptyString($x, $y, $z): void +{ + // PHP 7.x/8.x compatibility: Keep zero in both cases + if ($x == '') { + assertType("0|0.0|''|false|null", $x); + } else { + assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); + } + if ('' != $x) { + assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); + } else { + assertType("0|0.0|''|false|null", $x); + } + + if ($y == '') { + assertType("0|''|null", $y); + } else { + assertType("int|non-empty-string", $y); + } + + if ($z == '') { + assertType("0|0.0|''|false|null", $z); + } else { + assertType("mixed~(''|false|null)", $z); + } +} diff --git a/tests/PHPStan/Analyser/data/equal.php b/tests/PHPStan/Analyser/nsrt/equal.php similarity index 97% rename from tests/PHPStan/Analyser/data/equal.php rename to tests/PHPStan/Analyser/nsrt/equal.php index aad0f3ef5b..e91a274257 100644 --- a/tests/PHPStan/Analyser/data/equal.php +++ b/tests/PHPStan/Analyser/nsrt/equal.php @@ -135,7 +135,7 @@ public static function createStdClass(): \stdClass class Baz { - public function doFoo(string $a, int $b, float $c): void + public function doFoo(string $a, float $c): void { $nullableA = $a; if (rand(0, 1)) { @@ -152,7 +152,6 @@ public function doFoo(string $a, int $b, float $c): void assertType('false', 'a' != 'a'); assertType('true', 'a' != 'b'); - assertType('bool', $b == 'a'); assertType('bool', $a == 1); assertType('true', 1 == 1); assertType('false', 1 == 0); diff --git a/tests/PHPStan/Analyser/data/eval-implicit-throw.php b/tests/PHPStan/Analyser/nsrt/eval-implicit-throw.php similarity index 100% rename from tests/PHPStan/Analyser/data/eval-implicit-throw.php rename to tests/PHPStan/Analyser/nsrt/eval-implicit-throw.php diff --git a/tests/PHPStan/Analyser/nsrt/explicit-throws.php b/tests/PHPStan/Analyser/nsrt/explicit-throws.php new file mode 100644 index 0000000000..e37d6be94a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/explicit-throws.php @@ -0,0 +1,53 @@ +throwInvalidArgument(); + } catch (\InvalidArgumentException $e) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + } + + public function doBaz(): void + { + try { + doFoo(); + $a = 1; + $this->throwInvalidArgument(); + throw new \InvalidArgumentException(); + } catch (\InvalidArgumentException $e) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + } + + /** + * @throws \InvalidArgumentException + */ + private function throwInvalidArgument(): void + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/ext-ds.php b/tests/PHPStan/Analyser/nsrt/ext-ds.php similarity index 100% rename from tests/PHPStan/Analyser/data/ext-ds.php rename to tests/PHPStan/Analyser/nsrt/ext-ds.php diff --git a/tests/PHPStan/Analyser/nsrt/extra-extra-int-types.php b/tests/PHPStan/Analyser/nsrt/extra-extra-int-types.php new file mode 100644 index 0000000000..98c40a4326 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/extra-extra-int-types.php @@ -0,0 +1,23 @@ +', $nonPositiveInt); + assertType('int<0, max>', $nonNegativeInt); + } + +} diff --git a/tests/PHPStan/Analyser/data/extra-int-types.php b/tests/PHPStan/Analyser/nsrt/extra-int-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/extra-int-types.php rename to tests/PHPStan/Analyser/nsrt/extra-int-types.php diff --git a/tests/PHPStan/Analyser/nsrt/extract.php b/tests/PHPStan/Analyser/nsrt/extract.php new file mode 100644 index 0000000000..dff42bc482 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/extract.php @@ -0,0 +1,86 @@ + 42]); + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertType('42', $foo); +} + + +function doTyped5(): void +{ + $foo = ['foo' => 42]; + extract($foo); + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertType('42', $foo); +} diff --git a/tests/PHPStan/Analyser/nsrt/falsey-coalesce.php b/tests/PHPStan/Analyser/nsrt/falsey-coalesce.php new file mode 100644 index 0000000000..60cbd4971a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsey-coalesce.php @@ -0,0 +1,127 @@ +get('position'); + assertVariableCertainty(TrinaryLogic::createYes(), $location); + + $location ?? ''; + assertVariableCertainty(TrinaryLogic::createYes(), $location); + } + +} + +function maybeTrueVarAssign():void { + if (rand(0,1)) { + $a = true; + } + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1|true', $x); + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('true', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function nullableVarAssign():void { + if (rand(0,1)) { + $a = true; + } else { + $a = null; + } + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1|true', $x); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType('true|null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function maybeNullableVarAssign():void { + if (rand(0,1)) { + $a = null; + } + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1', $x); + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function notExistsAssign():void { + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1', $x); + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType('*ERROR*', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function nullableVarExpr():void { + if (rand(0,1)) { + $a = true; + } else { + $a = null; + } + $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType('true|null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function maybeNullableVarExpr():void { + if (rand(0,1)) { + $a = null; + } + $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function notExistsExpr():void { + $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType('*ERROR*', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} diff --git a/tests/PHPStan/Analyser/nsrt/falsey-empty-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-empty-certainty.php new file mode 100644 index 0000000000..ba24b22730 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsey-empty-certainty.php @@ -0,0 +1,98 @@ + null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (empty($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyEmptyUncertainPropertyFetch(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + if (rand() % 3) { + $a->x = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (empty($a->x)) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function justEmpty(): void +{ + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + if (!empty($foo)) { + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } + assertVariableCertainty(TrinaryLogic::createNo(), $foo); +} + +function maybeEmpty(): void +{ + if (rand() % 2) { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + if (!empty($foo)) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); +} + +function maybeEmptyUnset(): void +{ + if (rand() % 2) { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + if (!empty($foo)) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + unset($foo); + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); +} + + +function parseVariableSymbolFromXmlNode(SimpleXMLElement $transactionXmlElement): string +{ + if ( + !empty($transactionXmlElement->invoice_number) + && preg_match('~^\d+/\d+\-0*(\d+)$~', (string) $transactionXmlElement->invoice_number, $matches) === 1 + ) { + assertVariableCertainty(TrinaryLogic::createYes(), $matches); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php new file mode 100644 index 0000000000..484d0363e3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php @@ -0,0 +1,282 @@ += 8.0 + +namespace FalseyIssetCertainty; + +use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; +use PHPStan\TrinaryLogic; + +function getFoo():mixed { + return 1; +} + +function falseyIssetArrayDimFetchOnProperty(): void +{ + $a = new \stdClass(); + $a->bar = null; + if (rand() % 3) { + $a->bar = 'hello'; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a->bar)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyIssetUncertainArrayDimFetchOnProperty(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + $a->bar = null; + $a = ['bar' => null]; + if (rand() % 3) { + $a->bar = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a->bar)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetUncertainPropertyFetch(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + if (rand() % 3) { + $a->x = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a->x)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetArrayDimFetch(): void +{ + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyIssetUncertainArrayDimFetch(): void +{ + if (rand() % 2) { + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function nullableVariable(): void +{ + $a = 'bar'; + if (rand() % 2) { + $a = null; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariable(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariableUnset(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + unset($a); + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createNo(), $a); +} + +function falseyIssetNullableVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + if (rand() % 3) { + $a = null; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyMixedIssetVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseySubtractedMixedIssetVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + if ($a === null) { + return; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetWithAssignment(): void +{ + if (rand() % 2) { + $x = ['x' => 1]; + } + + if (isset($x[$z = getFoo()])) { + assertVariableCertainty(TrinaryLogic::createYes(), $z); + assertVariableCertainty(TrinaryLogic::createYes(), $x); + + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $z); + assertVariableCertainty(TrinaryLogic::createMaybe(), $x); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $z); + assertVariableCertainty(TrinaryLogic::createMaybe(), $x); +} + +function justIsset(): void +{ + if (isset($foo)) { + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } +} + +function maybeIsset(): void +{ + if (rand() % 2) { + $foo = 1; + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + if (isset($foo)) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertType('1', $foo); + } +} + +function isStringNarrowsMaybeCertainty(int $i, string $s): void +{ + if (rand(0, 1)) { + $a = rand(0,1) ? $i : $s; + } + + if (is_string($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + echo $a; + } +} + +function parseVariableSymbolFromXmlNode(SimpleXMLElement $transactionXmlElement): string +{ + if ( + isset($transactionXmlElement->invoice_number) + && preg_match('~^\d+/\d+\-0*(\d+)$~', (string) $transactionXmlElement->invoice_number, $matches) === 1 + ) { + assertVariableCertainty(TrinaryLogic::createYes(), $matches); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php new file mode 100644 index 0000000000..cc831b87a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php @@ -0,0 +1,226 @@ += 8.0 + +namespace FalseyTernaryCertainty; + +use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; +use PHPStan\TrinaryLogic; + +function getFoo():mixed { + return 1; +} + +function falseyTernaryArrayDimFetchOnProperty(): void +{ + $a = new \stdClass(); + $a->bar = null; + if (rand() % 3) { + $a->bar = 'hello'; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a->bar)? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createYes(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyTernaryUncertainArrayDimFetchOnProperty(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + $a->bar = null; + $a = ['bar' => null]; + if (rand() % 3) { + $a->bar = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a->bar) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyTernaryUncertainPropertyFetch(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + if (rand() % 3) { + $a->x = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a->x) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyTernaryArrayDimFetch(): void +{ + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a['bar']) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createYes(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyTernaryUncertainArrayDimFetch(): void +{ + if (rand() % 2) { + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a['bar']) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyTernaryVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function nullableVariable(): void +{ + $a = 'bar'; + if (rand() % 2) { + $a = null; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createYes(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariable(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariableShort(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a) ?: + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyTernaryNullableVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + if (rand() % 3) { + $a = null; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyMixedTernaryVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseySubtractedMixedTernaryVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + if ($a === null) { + return; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function parseVariableSymbolFromXmlNode(SimpleXMLElement $transactionXmlElement): string +{ + return isset($transactionXmlElement->invoice_number) + && preg_match('~^\d+/\d+\-0*(\d+)$~', (string) $transactionXmlElement->invoice_number, $matches) === 1 + ? assertVariableCertainty(TrinaryLogic::createYes(), $matches) + : ''; +} diff --git a/tests/PHPStan/Analyser/nsrt/falsy-isset.php b/tests/PHPStan/Analyser/nsrt/falsy-isset.php new file mode 100644 index 0000000000..eb11c5254d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/falsy-isset.php @@ -0,0 +1,99 @@ += 8.0 + +namespace FalsyIsset; + +use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; +use PHPStan\TrinaryLogic; + +function doFoo():mixed { + return 1; +} + +function maybeMixedVariable(): void +{ + if (rand(0,1)) { + $a = doFoo(); + } + + if (isset($a)) { + assertType("mixed~null", $a); + } else { + assertType("null", $a); + } +} + +function maybeNullableVariable(): void +{ + if (rand(0,1)) { + $a = 'hello'; + + if (rand(0,2)) { + $a = null; + } + } + + if (isset($a)) { + assertType("'hello'", $a); + } else { + assertType("null", $a); + } +} + +function subtractedMixedIsset(mixed $m): void +{ + if ($m === null) { + return; + } + + assertType("mixed~null", $m); + if (isset($m)) { + assertType("mixed~null", $m); + } else { + assertType("*ERROR*", $m); + } +} + +function mixedIsset(mixed $m): void +{ + if (isset($m)) { + assertType("mixed~null", $m); + } else { + assertType("null", $m); + } +} + +function stdclassIsset(?\stdClass $m): void +{ + if (isset($m)) { + assertType("stdClass", $m); + } else { + assertType("null", $m); + } +} + +function nullableVariable(?string $a): void +{ + if (isset($a)) { + assertType("string", $a); + } else { + assertType("null", $a); + } +} + +function nullableUnionVariable(null|string|int $a): void +{ + if (isset($a)) { + assertType("int|string", $a); + } else { + assertType("null", $a); + } +} + +function render(?int $noteListLimit, int $count): void +{ + $showAllLink = $noteListLimit !== null && $count > $noteListLimit; + if ($showAllLink) { + assertType('int', $noteListLimit); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php b/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php new file mode 100644 index 0000000000..8a38465040 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php @@ -0,0 +1,12 @@ +|false|null', fgetcsv($resource)); // nullable when invalid argument is given (https://3v4l.org/4WmR5#v7.4.30) +} diff --git a/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php b/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php new file mode 100644 index 0000000000..fccf29931c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php @@ -0,0 +1,12 @@ += 8.0 + +declare(strict_types = 1); + +namespace TestFGetCsvPhp8; + +use function PHPStan\Testing\assertType; + +function test($resource): void +{ + assertType('list|false', fgetcsv($resource)); +} diff --git a/tests/PHPStan/Analyser/data/filesystem-functions.php b/tests/PHPStan/Analyser/nsrt/filesystem-functions.php similarity index 94% rename from tests/PHPStan/Analyser/data/filesystem-functions.php rename to tests/PHPStan/Analyser/nsrt/filesystem-functions.php index 988fbc8dc3..fc7614c63a 100644 --- a/tests/PHPStan/Analyser/data/filesystem-functions.php +++ b/tests/PHPStan/Analyser/nsrt/filesystem-functions.php @@ -59,7 +59,7 @@ public function test3($fh): void public function test4(string $path): void { if (file_get_contents($path) === 'data') { - assertType('\'data\'', file_get_contents($path)); + assertType('string|false', file_get_contents($path)); file_put_contents($path, 'other'); assertType('string|false', file_get_contents($path)); } diff --git a/tests/PHPStan/Analyser/nsrt/filter-input-array.php b/tests/PHPStan/Analyser/nsrt/filter-input-array.php new file mode 100644 index 0000000000..d773c237f8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/filter-input-array.php @@ -0,0 +1,81 @@ += 8.0 + +declare(strict_types=1); + +namespace FilterVarArray; + +use function PHPStan\Testing\assertType; + +class FilterInput +{ + function superGlobalVariables(): void + { + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1], + ]; + + // filter array with add_empty=default + assertType('array{int: int|false|null, positive_int: int<1, max>|false|null}', filter_input_array(INPUT_GET, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ])); + + // filter array with add_empty=true + assertType('array{int: int|false|null, positive_int: int<1, max>|false|null}', filter_input_array(INPUT_GET, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], true)); + + // filter array with add_empty=false + assertType('array{int?: int|false, positive_int?: int<1, max>|false}', filter_input_array(INPUT_GET, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], false)); + + // filter flag with add_empty=default + assertType('array', filter_input_array(INPUT_GET, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array', filter_input_array(INPUT_GET, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array', filter_input_array(INPUT_GET, FILTER_VALIDATE_INT, false)); + } + + /** + * @param array $arrayFilter + * @param FILTER_VALIDATE_* $intFilter + */ + function dynamicFilter(array $input, array $arrayFilter, int $intFilter): void + { + // filter array with add_empty=default + assertType('array', filter_input_array(INPUT_GET, $arrayFilter)); + // filter array with add_empty=true + assertType('array', filter_input_array(INPUT_GET, $arrayFilter, true)); + // filter array with add_empty=false + assertType('array', filter_input_array(INPUT_GET, $arrayFilter, false)); + + // filter flag with add_empty=default + assertType('array', filter_input_array(INPUT_GET, $intFilter)); + // filter flag with add_empty=true + assertType('array', filter_input_array(INPUT_GET, $intFilter, true)); + // filter flag with add_empty=false + assertType('array', filter_input_array(INPUT_GET, $intFilter, false)); + } + + /** + * @param INPUT_GET|INPUT_POST $union + */ + public function dynamicInputType($union, mixed $mixed): void + { + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1], + ]; + + assertType('array{foo: int<1, max>|false|null}', filter_input_array($union, ['foo' => $filter])); + assertType('array|false|null', filter_input_array($mixed, ['foo' => $filter])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/filter-input-php7.php b/tests/PHPStan/Analyser/nsrt/filter-input-php7.php new file mode 100644 index 0000000000..8a87e04edc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/filter-input-php7.php @@ -0,0 +1,18 @@ += 8.0 + +declare(strict_types=1); + +namespace FilterInputPhp8; + +use function PHPStan\Testing\assertType; + +class FilterInputPhp8 +{ + + public function invalidTypesOrVarNames($mixed): void + { + assertType('*NEVER*', filter_input(-1, 'foo', FILTER_VALIDATE_INT)); + assertType('*NEVER*', filter_input(-1, 'foo', FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/filter-input.php b/tests/PHPStan/Analyser/nsrt/filter-input.php new file mode 100644 index 0000000000..466862d85b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/filter-input.php @@ -0,0 +1,42 @@ + FILTER_NULL_ON_FAILURE])); + assertType("'invalid'|int|null", filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 'invalid']])); + assertType('array|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array|false', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('0|int<17, 19>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-array.php b/tests/PHPStan/Analyser/nsrt/filter-var-array.php new file mode 100644 index 0000000000..1151d370c1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/filter-var-array.php @@ -0,0 +1,340 @@ += 8.0 + +namespace FilterVarArray; + +use function PHPStan\Testing\assertType; + +function constantValues(): void +{ + $input = [ + 'valid' => '1', + 'invalid' => 'a', + ]; + + // filter array with add_empty=default + assertType('array{valid: 1, invalid: false, missing: null}', filter_var_array($input, [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ])); + + // filter array with add_empty=true + assertType('array{valid: 1, invalid: false, missing: null}', filter_var_array($input, [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], true)); + + // filter array with add_empty=false + assertType('array{valid: 1, invalid: false}', filter_var_array($input, [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], false)); + + // filter flag with add_empty=default + assertType('array{valid: 1, invalid: false}', filter_var_array($input, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array{valid: 1, invalid: false}', filter_var_array($input, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array{valid: 1, invalid: false}', filter_var_array($input, FILTER_VALIDATE_INT, false)); + + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1, 'max_range' => 10], + ]; + + // filter array with add_empty=default + assertType('array{valid: 1, invalid: false, missing: null}', filter_var_array($input, [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ])); + + // filter array with add_empty=default + assertType('array{valid: 1, invalid: false, missing: null}', filter_var_array($input, [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ], true)); + + // filter array with add_empty=default + assertType('array{valid: 1, invalid: false}', filter_var_array($input, [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ], false)); +} + +function mixedInput(mixed $input): void +{ + // filter array with add_empty=default + assertType('array{id: int|false|null}', filter_var_array($input, [ + 'id' => FILTER_VALIDATE_INT, + ])); + + // filter array with add_empty=true + assertType('array{id: int|false|null}', filter_var_array($input, [ + 'id' => FILTER_VALIDATE_INT, + ], true)); + + // filter array with add_empty=false + assertType('array{id?: int|false}', filter_var_array($input, [ + 'id' => FILTER_VALIDATE_INT, + ], false)); + + // filter flag with add_empty=default + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT, false)); + + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1, 'max_range' => 10], + ]; + + // filter array with add_empty=default + assertType('array{id: int<1, 10>|false|null}', filter_var_array($input, [ + 'id' => $filter, + ])); + + // filter array with add_empty=default + assertType('array{id: int<1, 10>|false|null}', filter_var_array($input, [ + 'id' => $filter, + ], true)); + + // filter array with add_empty=default + assertType('array{id?: int<1, 10>|false}', filter_var_array($input, [ + 'id' => $filter, + ], false)); +} + +function emptyArrayInput(): void +{ + // filter array with add_empty=default + assertType('array{valid: null, invalid: null, missing: null}', filter_var_array([], [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ])); + + // filter array with add_empty=true + assertType('array{valid: null, invalid: null, missing: null}', filter_var_array([], [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], true)); + + // filter array with add_empty=false + assertType('array{}', filter_var_array([], [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], false)); + + // filter flag with add_empty=default + assertType('array{}', filter_var_array([], FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array{}', filter_var_array([], FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array{}', filter_var_array([], FILTER_VALIDATE_INT, false)); + + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1, 'max_range' => 10], + ]; + + // filter array with add_empty=default + assertType('array{valid: null, invalid: null, missing: null}', filter_var_array([], [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ])); + + // filter array with add_empty=default + assertType('array{valid: null, invalid: null, missing: null}', filter_var_array([], [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ], true)); + + // filter array with add_empty=default + assertType('array{}', filter_var_array([], [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ], false)); +} + +function superGlobalVariables(): void +{ + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1], + ]; + + // filter array with add_empty=default + assertType('array{int: int|false|null, positive_int: int<1, max>|false|null}', filter_var_array($_POST, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ])); + + // filter array with add_empty=true + assertType('array{int: int|false|null, positive_int: int<1, max>|false|null}', filter_var_array($_POST, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], true)); + + // filter array with add_empty=false + assertType('array{int?: int|false, positive_int?: int<1, max>|false}', filter_var_array($_POST, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], false)); + + // filter flag with add_empty=default + assertType('array', filter_var_array($_POST, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array', filter_var_array($_POST, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array', filter_var_array($_POST, FILTER_VALIDATE_INT, false)); +} + +/** + * @param list $input + */ +function typedList($input): void +{ + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1], + ]; + + // filter array with add_empty=default + assertType('array{int: int|null, positive_int: int<1, max>|false|null}', filter_var_array($input, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ])); + + // filter array with add_empty=true + assertType('array{int: int|null, positive_int: int<1, max>|false|null}', filter_var_array($input, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], true)); + + // filter array with add_empty=false + assertType('array{int?: int, positive_int?: int<1, max>|false}', filter_var_array($input, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], false)); + + // filter flag with add_empty=default + assertType('list', filter_var_array($input, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('list', filter_var_array($input, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('list', filter_var_array($input, FILTER_VALIDATE_INT, false)); +} + +/** + * @param array{exists: int, optional?: int, extra: int} $input + */ +function dynamicVariables(array $input): void +{ + // filter array with add_empty=default + assertType('array{exists: int, optional: int|null, missing: null}', filter_var_array($input, [ + 'exists' => FILTER_VALIDATE_INT, + 'optional' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ])); + + // filter array with add_empty=true + assertType('array{exists: int, optional: int|null, missing: null}', filter_var_array($input, [ + 'exists' => FILTER_VALIDATE_INT, + 'optional' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], true)); + + // filter array with add_empty=false + assertType('array{exists: int, optional?: int}', filter_var_array($input, [ + 'exists' => FILTER_VALIDATE_INT, + 'optional' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], false)); + + // filter flag with add_empty=default + assertType('array{exists: int, optional?: int, extra: int}', filter_var_array($input, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array{exists: int, optional?: int, extra: int}', filter_var_array($input, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array{exists: int, optional?: int, extra: int}', filter_var_array($input, FILTER_VALIDATE_INT, false)); + + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1, 'max_range' => 10], + ]; + + // filter array with add_empty=default + assertType('array{exists: int<1, 10>|false, optional: int<1, 10>|false|null, missing: null}', filter_var_array($input, [ + 'exists' => $filter, + 'optional' => $filter, + 'missing' => $filter, + ])); + + // filter array with add_empty=default + assertType('array{exists: int<1, 10>|false, optional: int<1, 10>|false|null, missing: null}', filter_var_array($input, [ + 'exists' => $filter, + 'optional' => $filter, + 'missing' => $filter, + ], true)); + + // filter array with add_empty=default + assertType('array{exists: int<1, 10>|false, optional?: int<1, 10>|false}', filter_var_array($input, [ + 'exists' => $filter, + 'optional' => $filter, + 'missing' => $filter, + ], false)); +} + +/** + * @param array{exists: int, optional?: int, extra: int} $input + * @param array $arrayFilter + * @param FILTER_VALIDATE_* $intFilter + */ +function dynamicFilter(array $input, array $arrayFilter, int $intFilter): void +{ + // filter array with add_empty=default + assertType('array|false|null', filter_var_array($input, $arrayFilter)); + // filter array with add_empty=true + assertType('array|false|null', filter_var_array($input, $arrayFilter, true)); + // filter array with add_empty=false + assertType('array|false|null', filter_var_array($input, $arrayFilter, false)); + + // filter flag with add_empty=default + assertType('array|false|null', filter_var_array($input, $intFilter)); + // filter flag with add_empty=true + assertType('array|false|null', filter_var_array($input, $intFilter, true)); + // filter flag with add_empty=false + assertType('array|false|null', filter_var_array($input, $intFilter, false)); + + // filter array with add_empty=default + assertType('array|false|null', filter_var_array([], $arrayFilter)); + // filter array with add_empty=true + assertType('array|false|null', filter_var_array([], $arrayFilter, true)); + // filter array with add_empty=false + assertType('array|false|null', filter_var_array([], $arrayFilter, false)); + + // filter flag with add_empty=default + assertType('array|false|null', filter_var_array([], $intFilter)); + // filter flag with add_empty=true + assertType('array|false|null', filter_var_array([], $intFilter, true)); + // filter flag with add_empty=false + assertType('array|false|null', filter_var_array([], $intFilter, false)); +} diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-dynamic-return-type-extension-regression.php b/tests/PHPStan/Analyser/nsrt/filter-var-dynamic-return-type-extension-regression.php new file mode 100644 index 0000000000..172fa40b4f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/filter-var-dynamic-return-type-extension-regression.php @@ -0,0 +1,60 @@ +determineExactType(); + $type = $exactType ?? new MixedType(); + $otherTypes = $this->getOtherTypes(); + + assertType('array{default: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); + if (isset($otherTypes['range'])) { + assertType('array{default: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); + if ($type instanceof ConstantScalarType) { + if ($otherTypes['range']->isSuperTypeOf($type)->no()) { + $type = $otherTypes['default']; + } + assertType('array{default: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); + unset($otherTypes['default']); + assertType('array{range: PHPStan\Type\Type}', $otherTypes); + } else { + $type = $otherTypes['range']; + assertType('array{default: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); + } + assertType('array{default?: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); + } + assertType('non-empty-array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); + if ($exactType !== null) { + assertType('non-empty-array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); + unset($otherTypes['default']); + assertType('array{range?: PHPStan\Type\Type}', $otherTypes); + } + assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); + if (isset($otherTypes['default']) && $otherTypes['default']->isSuperTypeOf($type)->no()) { + $type = TypeCombinator::union($type, $otherTypes['default']); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php new file mode 100644 index 0000000000..dc6620b0ca --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php @@ -0,0 +1,133 @@ + ['min_range' => 1]]); + assertType('int<1, max>|false', $return); + + $return = filter_var($str, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1], 'flags' => FILTER_NULL_ON_FAILURE]); + assertType('int<1, max>|null', $return); + + $return = filter_var($str, FILTER_VALIDATE_INT, ['options' => ['max_range' => 0]]); + assertType('int|false', $return); + + $return = filter_var($str, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]); + assertType('int<1, 9>|false', $return); + + $return = filter_var(100, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]); + assertType('false', $return); + + $return = filter_var(100, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 1]]); + assertType('false', $return); + + $return = filter_var(1, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]); + assertType('1', $return); + + $return = filter_var(1, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 1]]); + assertType('1', $return); + + $return = filter_var(9, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]); + assertType('9', $return); + + $return = filter_var(1.0, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]); + assertType('1', $return); + + $return = filter_var(11.0, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]); + assertType('false', $return); + + $return = filter_var($str, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => $positive_int]]); + assertType('int<1, max>|false', $return); + + $return = filter_var($str, FILTER_VALIDATE_INT, ['options' => ['min_range' => $negative_int, 'max_range' => 0]]); + assertType('int|false', $return); + + $return = filter_var($str, FILTER_VALIDATE_INT, ['options' => ['min_range' => $int, 'max_range' => $int]]); + assertType('int|false', $return); + + $str2 = ''; + $return = filter_var($str2, FILTER_DEFAULT); + assertType("''", $return); + + $return = filter_var($str2, FILTER_VALIDATE_URL); + assertType('non-falsy-string|false', $return); + + $return = filter_var('foo', FILTER_VALIDATE_INT); + assertType('false', $return); + + $return = filter_var('foo', FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); + assertType('null', $return); + + $return = filter_var('1', FILTER_VALIDATE_INT); + assertType('1', $return); + + $return = filter_var('0', FILTER_VALIDATE_INT); + assertType('0', $return); + + $return = filter_var('-1', FILTER_VALIDATE_INT); + assertType('-1', $return); + + $return = filter_var('0o10', FILTER_VALIDATE_INT); + assertType('false', $return); + + $return = filter_var('0o10', FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_OCTAL); + assertType('8', $return); + + $return = filter_var('0x10', FILTER_VALIDATE_INT); + assertType('false', $return); + + $return = filter_var('0x10', FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX); + assertType('16', $return); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/filter-var.php b/tests/PHPStan/Analyser/nsrt/filter-var.php new file mode 100644 index 0000000000..abe5331fc6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/filter-var.php @@ -0,0 +1,172 @@ + $stringMixedMap + */ + public function doFoo($mixed, array $stringMixedMap): void + { + assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT)); + assertType('int|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_NULL_ON_FAILURE])); + + assertType('17', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_SCALAR])); + assertType('false', filter_var([17], FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_SCALAR])); + + assertType('false', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('null', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('false', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('null', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('array|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('array|null', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + + assertType('array<17>', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array<17>', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + + assertType('false', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('null', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('false', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('null', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('array|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('array|null', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + + assertType('0|int<17, 19>', filter_var($mixed, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]])); + + assertType('array', filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE)); + } + + /** + * @param int<17, 19> $range1 + * @param int<1, 5> $range2 + * @param int<18, 19> $range3 + */ + public function intRanges(int $int, int $min, int $max, int $range1, int $range2, int $range3): void + { + assertType('int<17, 19>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 19, 'max_range' => 17]])); + assertType('0|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => null, 'max_range' => null]])); + assertType('int<17, 19>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => '17', 'max_range' => '19']])); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min, 'max_range' => 19]])); + assertType('int<17, max>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => $max]])); + assertType('int<17, 19>', filter_var($range1, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('false', filter_var(9, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('18', filter_var(18, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('18', filter_var(18, FILTER_VALIDATE_INT, ['options' => ['min_range' => '17', 'max_range' => '19']])); + assertType('false', filter_var(-18, FILTER_VALIDATE_INT, ['options' => ['min_range' => null, 'max_range' => 19]])); + assertType('false', filter_var(18, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => null]])); + assertType('false', filter_var($range2, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('int<18, 19>', filter_var($range3, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min, 'max_range' => $max]])); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min]])); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['max_range' => $max]])); + } + + /** @param resource $resource */ + public function invalidInput(array $arr, object $object, $resource): void + { + assertType('false', filter_var($arr)); + assertType('false', filter_var($object)); + assertType('false', filter_var($resource)); + assertType('null', filter_var(new stdClass(), FILTER_DEFAULT, FILTER_NULL_ON_FAILURE)); + assertType("'invalid'", filter_var(new stdClass(), FILTER_DEFAULT, ['options' => ['default' => 'invalid']])); + } + + public function intToInt(int $int, array $options): void + { + assertType('int', filter_var($int, FILTER_VALIDATE_INT)); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, $options)); + assertType('int<0, max>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])); + } + + /** + * @param int<0, 9> $intRange + * @param non-empty-string $nonEmptyString + */ + public function scalars(bool $bool, float $float, int $int, string $string, int $intRange, string $nonEmptyString): void + { + assertType('bool', filter_var($bool, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('true', filter_var(true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('false', filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var($float, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var(17.0, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var(17.1, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var(1e-50, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var($int, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var($intRange, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var(17, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var($string, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var($nonEmptyString, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var('17', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var('17.0', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var('17.1', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('null', filter_var(null, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + + assertType('float|false', filter_var($bool, FILTER_VALIDATE_FLOAT)); + assertType('1.0', filter_var(true, FILTER_VALIDATE_FLOAT)); + assertType('false', filter_var(false, FILTER_VALIDATE_FLOAT)); + assertType('float', filter_var($float, FILTER_VALIDATE_FLOAT)); + assertType('17.0', filter_var(17.0, FILTER_VALIDATE_FLOAT)); + assertType('17.1', filter_var(17.1, FILTER_VALIDATE_FLOAT)); + assertType('1.0E-50', filter_var(1e-50, FILTER_VALIDATE_FLOAT)); + assertType('float', filter_var($int, FILTER_VALIDATE_FLOAT)); + assertType('float', filter_var($intRange, FILTER_VALIDATE_FLOAT)); + assertType('17.0', filter_var(17, FILTER_VALIDATE_FLOAT)); + assertType('float|false', filter_var($string, FILTER_VALIDATE_FLOAT)); + assertType('float|false', filter_var($nonEmptyString, FILTER_VALIDATE_FLOAT)); + assertType('float|false', filter_var('17', FILTER_VALIDATE_FLOAT)); // could be 17.0 + assertType('float|false', filter_var('17.0', FILTER_VALIDATE_FLOAT)); // could be 17.0 + assertType('float|false', filter_var('17.1', FILTER_VALIDATE_FLOAT)); // could be 17.1 + assertType('false', filter_var(null, FILTER_VALIDATE_FLOAT)); + + assertType('int|false', filter_var($bool, FILTER_VALIDATE_INT)); + assertType('1', filter_var(true, FILTER_VALIDATE_INT)); + assertType('false', filter_var(false, FILTER_VALIDATE_INT)); + assertType('int|false', filter_var($float, FILTER_VALIDATE_INT)); + assertType('17', filter_var(17.0, FILTER_VALIDATE_INT)); + assertType('false', filter_var(17.1, FILTER_VALIDATE_INT)); + assertType('false', filter_var(1e-50, FILTER_VALIDATE_INT)); + assertType('int', filter_var($int, FILTER_VALIDATE_INT)); + assertType('int<0, 9>', filter_var($intRange, FILTER_VALIDATE_INT)); + assertType('17', filter_var(17, FILTER_VALIDATE_INT)); + assertType('int|false', filter_var($string, FILTER_VALIDATE_INT)); + assertType('int|false', filter_var($nonEmptyString, FILTER_VALIDATE_INT)); + assertType('17', filter_var('17', FILTER_VALIDATE_INT)); + assertType('false', filter_var('17.0', FILTER_VALIDATE_INT)); + assertType('false', filter_var('17.1', FILTER_VALIDATE_INT)); + assertType('false', filter_var(null, FILTER_VALIDATE_INT)); + + assertType("''|'1'", filter_var($bool)); + assertType("'1'", filter_var(true)); + assertType("''", filter_var(false)); + assertType('numeric-string&uppercase-string', filter_var($float)); + assertType("'17'", filter_var(17.0)); + assertType("'17.1'", filter_var(17.1)); + assertType("'1.0E-50'", filter_var(1e-50)); + assertType('lowercase-string&numeric-string&uppercase-string', filter_var($int)); + assertType("'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'", filter_var($intRange)); + assertType("'17'", filter_var(17)); + assertType('string', filter_var($string)); + assertType('non-empty-string', filter_var($nonEmptyString)); + assertType("'17'", filter_var('17')); + assertType("'17.0'", filter_var('17.0')); + assertType("'17.1'", filter_var('17.1')); + assertType("''", filter_var(null)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/finally-scope.php b/tests/PHPStan/Analyser/nsrt/finally-scope.php new file mode 100644 index 0000000000..d7592eb4d1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/finally-scope.php @@ -0,0 +1,137 @@ +mightThrowException(); + $s = 2; + } catch (\Throwable $e) { // always catches + assertType('1', $s); + $s = 'str'; + } finally { + assertType("2|'str'", $s); + } + } + + public function doBar() + { + try { + $s = 1; + $this->mightThrowException(); + } catch (\InvalidArgumentException $e) { // might catch + assertType('1', $s); + $s = "bar"; + } catch (\Throwable $e) { // always catches what isn't InvalidArgumentException + assertType('1', $s); + $s = 'str'; + } finally { + assertType("1|'bar'|'str'", $s); + } + } + + public function doBar2() + { + try { + $s = 1; + $this->throwsDomainException(); + } catch (\DomainException $e) { // always catches + assertType('1', $s); + $s = "bar"; + } catch (\Throwable $e) { // dead catch + assertType('1', $s); + $s = 'str'; + } finally { + assertType("1|'bar'|'str'", $s); // could be 1|'bar' + } + } + + public function doBar3() + { + try { + $s = 1; + $this->throwsDomainException(); + } catch (\LogicException $e) { // always catches + assertType('1', $s); + $s = "bar"; + } catch (\Throwable $e) { // dead catch + assertType('1', $s); + $s = 'str'; + } finally { + assertType("1|'bar'|'str'", $s); // could be 1|'bar' + } + } + + public function doBar4() + { + try { + $s = 1; + $this->throwsLogicException(); + } catch (\DomainException $e) { // might catch + assertType('1', $s); + $s = "bar"; + } catch (\Throwable $e) { // always catches what isn't DomainException + assertType('1', $s); + $s = 'str'; + } finally { + assertType("1|'bar'|'str'", $s); + } + } + + public function doBar5() + { + try { + $s = 1; + $this->throwsLogicException(); + } catch (\DomainException $e) { // might catch + assertType('1', $s); + $s = "bar"; + } catch (\LogicException $e) { // always catches what isn't DomainException + assertType('1', $s); + $s = "str"; + } catch (\Throwable $e) { // dead catch + assertType('1', $s); + $s = 'foo'; + } finally { + assertType("1|'bar'|'foo'|'str'", $s); // could be 1|'bar'|'str' + } + } + + public function doBar6() + { + try { + $s = 1; + $this->throwsLogicException(); + } catch (\RuntimeException $e) { // dead catch + assertType('1', $s); + $s = "bar"; + } finally { + assertType('1', $s); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/finite-types.php b/tests/PHPStan/Analyser/nsrt/finite-types.php new file mode 100644 index 0000000000..c3e7e719a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/finite-types.php @@ -0,0 +1,33 @@ +doFoo(...); - assertType('int', $f(1)); - assertType('string', $f('foo')); + assertType('1', $f(1)); + assertType('\'foo\'', $f('foo')); $g = \Closure::fromCallable([$this, 'doFoo']); - assertType('int', $g(1)); - assertType('string', $g('foo')); + assertType('1', $g(1)); + assertType('\'foo\'', $g('foo')); } public function doBaz() diff --git a/tests/PHPStan/Analyser/nsrt/fizz-buzz.php b/tests/PHPStan/Analyser/nsrt/fizz-buzz.php new file mode 100644 index 0000000000..0ffb249f43 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fizz-buzz.php @@ -0,0 +1,239 @@ + match($n % 5) { + 0 => new FizzBuzz, + default => new Fizz, + }, + default => match($n % 5) { + 0 => new Buzz, + default => $n, + }, + }; +} + +assertType('array{FizzBuzz\n1, FizzBuzz\n2, FizzBuzz\Fizz, FizzBuzz\n4, FizzBuzz\Buzz, FizzBuzz\Fizz, FizzBuzz\n7, FizzBuzz\n8, FizzBuzz\Fizz, FizzBuzz\Buzz, FizzBuzz\n11, FizzBuzz\Fizz, FizzBuzz\n13, FizzBuzz\n14, FizzBuzz\FizzBuzz, FizzBuzz\n16, FizzBuzz\n17, FizzBuzz\Fizz, FizzBuzz\n19, FizzBuzz\Buzz, FizzBuzz\Fizz, FizzBuzz\n22, FizzBuzz\n23, FizzBuzz\Fizz, FizzBuzz\Buzz, FizzBuzz\n26, FizzBuzz\Fizz, FizzBuzz\n28, FizzBuzz\n29, FizzBuzz\FizzBuzz, FizzBuzz\n31, FizzBuzz\n32, FizzBuzz\Fizz, FizzBuzz\n34, FizzBuzz\Buzz, FizzBuzz\Fizz, FizzBuzz\n37, FizzBuzz\n38, FizzBuzz\Fizz, FizzBuzz\Buzz, FizzBuzz\n41, FizzBuzz\Fizz, FizzBuzz\n43, FizzBuzz\n44, FizzBuzz\FizzBuzz, FizzBuzz\n46, FizzBuzz\n47, FizzBuzz\Fizz, FizzBuzz\n49, FizzBuzz\Buzz, FizzBuzz\Fizz, FizzBuzz\n52, FizzBuzz\n53, FizzBuzz\Fizz, FizzBuzz\Buzz, FizzBuzz\n56, FizzBuzz\Fizz, FizzBuzz\n58, FizzBuzz\n59, FizzBuzz\FizzBuzz, FizzBuzz\n61, FizzBuzz\n62, FizzBuzz\Fizz, FizzBuzz\n64, FizzBuzz\Buzz, FizzBuzz\Fizz, FizzBuzz\n67, FizzBuzz\n68, FizzBuzz\Fizz, FizzBuzz\Buzz, FizzBuzz\n71, FizzBuzz\Fizz, FizzBuzz\n73, FizzBuzz\n74, FizzBuzz\FizzBuzz, FizzBuzz\n76, FizzBuzz\n77, FizzBuzz\Fizz, FizzBuzz\n79, FizzBuzz\Buzz, FizzBuzz\Fizz, FizzBuzz\n82, FizzBuzz\n83, FizzBuzz\Fizz, FizzBuzz\Buzz, FizzBuzz\n86, FizzBuzz\Fizz, FizzBuzz\n88, FizzBuzz\n89, FizzBuzz\FizzBuzz, FizzBuzz\n91, FizzBuzz\n92, FizzBuzz\Fizz, FizzBuzz\n94, FizzBuzz\Buzz, FizzBuzz\Fizz, FizzBuzz\n97, FizzBuzz\n98, FizzBuzz\Fizz, FizzBuzz\Buzz}', [ + fizzbuzz(new n1), + fizzbuzz(new n2), + fizzbuzz(new n3), + fizzbuzz(new n4), + fizzbuzz(new n5), + fizzbuzz(new n6), + fizzbuzz(new n7), + fizzbuzz(new n8), + fizzbuzz(new n9), + fizzbuzz(new n10), + fizzbuzz(new n11), + fizzbuzz(new n12), + fizzbuzz(new n13), + fizzbuzz(new n14), + fizzbuzz(new n15), + fizzbuzz(new n16), + fizzbuzz(new n17), + fizzbuzz(new n18), + fizzbuzz(new n19), + fizzbuzz(new n20), + fizzbuzz(new n21), + fizzbuzz(new n22), + fizzbuzz(new n23), + fizzbuzz(new n24), + fizzbuzz(new n25), + fizzbuzz(new n26), + fizzbuzz(new n27), + fizzbuzz(new n28), + fizzbuzz(new n29), + fizzbuzz(new n30), + fizzbuzz(new n31), + fizzbuzz(new n32), + fizzbuzz(new n33), + fizzbuzz(new n34), + fizzbuzz(new n35), + fizzbuzz(new n36), + fizzbuzz(new n37), + fizzbuzz(new n38), + fizzbuzz(new n39), + fizzbuzz(new n40), + fizzbuzz(new n41), + fizzbuzz(new n42), + fizzbuzz(new n43), + fizzbuzz(new n44), + fizzbuzz(new n45), + fizzbuzz(new n46), + fizzbuzz(new n47), + fizzbuzz(new n48), + fizzbuzz(new n49), + fizzbuzz(new n50), + fizzbuzz(new n51), + fizzbuzz(new n52), + fizzbuzz(new n53), + fizzbuzz(new n54), + fizzbuzz(new n55), + fizzbuzz(new n56), + fizzbuzz(new n57), + fizzbuzz(new n58), + fizzbuzz(new n59), + fizzbuzz(new n60), + fizzbuzz(new n61), + fizzbuzz(new n62), + fizzbuzz(new n63), + fizzbuzz(new n64), + fizzbuzz(new n65), + fizzbuzz(new n66), + fizzbuzz(new n67), + fizzbuzz(new n68), + fizzbuzz(new n69), + fizzbuzz(new n70), + fizzbuzz(new n71), + fizzbuzz(new n72), + fizzbuzz(new n73), + fizzbuzz(new n74), + fizzbuzz(new n75), + fizzbuzz(new n76), + fizzbuzz(new n77), + fizzbuzz(new n78), + fizzbuzz(new n79), + fizzbuzz(new n80), + fizzbuzz(new n81), + fizzbuzz(new n82), + fizzbuzz(new n83), + fizzbuzz(new n84), + fizzbuzz(new n85), + fizzbuzz(new n86), + fizzbuzz(new n87), + fizzbuzz(new n88), + fizzbuzz(new n89), + fizzbuzz(new n90), + fizzbuzz(new n91), + fizzbuzz(new n92), + fizzbuzz(new n93), + fizzbuzz(new n94), + fizzbuzz(new n95), + fizzbuzz(new n96), + fizzbuzz(new n97), + fizzbuzz(new n98), + fizzbuzz(new n99), + fizzbuzz(new n100) +]); + +final class n1 extends Num {} +final class n2 extends Num {} +final class n3 extends Num {} +final class n4 extends Num {} +final class n5 extends Num {} +final class n6 extends Num {} +final class n7 extends Num {} +final class n8 extends Num {} +final class n9 extends Num {} +final class n10 extends Num {} +final class n11 extends Num {} +final class n12 extends Num {} +final class n13 extends Num {} +final class n14 extends Num {} +final class n15 extends Num {} +final class n16 extends Num {} +final class n17 extends Num {} +final class n18 extends Num {} +final class n19 extends Num {} +final class n20 extends Num {} +final class n21 extends Num {} +final class n22 extends Num {} +final class n23 extends Num {} +final class n24 extends Num {} +final class n25 extends Num {} +final class n26 extends Num {} +final class n27 extends Num {} +final class n28 extends Num {} +final class n29 extends Num {} +final class n30 extends Num {} +final class n31 extends Num {} +final class n32 extends Num {} +final class n33 extends Num {} +final class n34 extends Num {} +final class n35 extends Num {} +final class n36 extends Num {} +final class n37 extends Num {} +final class n38 extends Num {} +final class n39 extends Num {} +final class n40 extends Num {} +final class n41 extends Num {} +final class n42 extends Num {} +final class n43 extends Num {} +final class n44 extends Num {} +final class n45 extends Num {} +final class n46 extends Num {} +final class n47 extends Num {} +final class n48 extends Num {} +final class n49 extends Num {} +final class n50 extends Num {} +final class n51 extends Num {} +final class n52 extends Num {} +final class n53 extends Num {} +final class n54 extends Num {} +final class n55 extends Num {} +final class n56 extends Num {} +final class n57 extends Num {} +final class n58 extends Num {} +final class n59 extends Num {} +final class n60 extends Num {} +final class n61 extends Num {} +final class n62 extends Num {} +final class n63 extends Num {} +final class n64 extends Num {} +final class n65 extends Num {} +final class n66 extends Num {} +final class n67 extends Num {} +final class n68 extends Num {} +final class n69 extends Num {} +final class n70 extends Num {} +final class n71 extends Num {} +final class n72 extends Num {} +final class n73 extends Num {} +final class n74 extends Num {} +final class n75 extends Num {} +final class n76 extends Num {} +final class n77 extends Num {} +final class n78 extends Num {} +final class n79 extends Num {} +final class n80 extends Num {} +final class n81 extends Num {} +final class n82 extends Num {} +final class n83 extends Num {} +final class n84 extends Num {} +final class n85 extends Num {} +final class n86 extends Num {} +final class n87 extends Num {} +final class n88 extends Num {} +final class n89 extends Num {} +final class n90 extends Num {} +final class n91 extends Num {} +final class n92 extends Num {} +final class n93 extends Num {} +final class n94 extends Num {} +final class n95 extends Num {} +final class n96 extends Num {} +final class n97 extends Num {} +final class n98 extends Num {} +final class n99 extends Num {} +final class n100 extends Num {} diff --git a/tests/PHPStan/Analyser/data/for-loop-i-type.php b/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php similarity index 84% rename from tests/PHPStan/Analyser/data/for-loop-i-type.php rename to tests/PHPStan/Analyser/nsrt/for-loop-i-type.php index 0e602ba293..1317b3695c 100644 --- a/tests/PHPStan/Analyser/data/for-loop-i-type.php +++ b/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php @@ -39,7 +39,7 @@ public function doCount2() { $foo = null; for($i = 1; $i < count([]); $i++) { $foo = new \stdClass(); - assertType('string', $i); // should be *NEVER* + assertType('*NEVER*', $i); } assertType('1', $i); @@ -94,4 +94,12 @@ public static function groupCapacities(array $startTimes): array return $capacities; } + + public function lastConditionResult(): void + { + for ($i = 0, $j = 5; $i < 10, $j > 0; $i++, $j--) { + assertType('int<0, max>', $i); // int<0,4> would be more precise, see https://github.com/phpstan/phpstan/issues/11872 + assertType('int<1, 5>', $j); + } + } } diff --git a/tests/PHPStan/Analyser/data/foreach-dependent-key-value.php b/tests/PHPStan/Analyser/nsrt/foreach-dependent-key-value.php similarity index 100% rename from tests/PHPStan/Analyser/data/foreach-dependent-key-value.php rename to tests/PHPStan/Analyser/nsrt/foreach-dependent-key-value.php diff --git a/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php new file mode 100644 index 0000000000..a1d7279252 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php @@ -0,0 +1,35 @@ +|false $a + */ + public function doFoo($a): void + { + foreach ($a as $k => $v) { + assertType('string', $k); + assertType('int', $v); + } + } + +} + +class Bar +{ + + public function sayHello(\stdClass $s): void + { + $a = null; + foreach ($s as $k => $v) { + $a .= 'test'; + } + assertType('(literal-string&lowercase-string&non-falsy-string)|null', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/fpm-get-status.php b/tests/PHPStan/Analyser/nsrt/fpm-get-status.php new file mode 100644 index 0000000000..8a276ec7ee --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fpm-get-status.php @@ -0,0 +1,16 @@ += 7.3 + +namespace FpmGetStatus; + +use function fpm_get_status; +use function PHPStan\Testing\assertType; + +$status = fpm_get_status(); + +assertType('array{pool: string, process-manager: \'dynamic\'|\'ondemand\'|\'static\', start-time: int<0, max>, start-since: int<0, max>, accepted-conn: int<0, max>, listen-queue: int<0, max>, max-listen-queue: int<0, max>, listen-queue-len: int<0, max>, idle-processes: int<0, max>, active-processes: int<1, max>, total-processes: int<1, max>, max-active-processes: int<1, max>, max-children-reached: 0|1, slow-requests: int<0, max>, procs: array, state: \'Idle\'|\'Running\', start-time: int<0, max>, start-since: int<0, max>, requests: int<0, max>, request-duration: int<0, max>, request-method: string, request-uri: string, query-string: string, request-length: int<0, max>, user: string, script: string, last-request-cpu: float, last-request-memory: int<0, max>}>}|false', $status); + +if ($status !== false && isset($status['procs'][0])) { + assertType('array{pid: int<2, max>, state: \'Idle\'|\'Running\', start-time: int<0, max>, start-since: int<0, max>, requests: int<0, max>, request-duration: int<0, max>, request-method: string, request-uri: string, query-string: string, request-length: int<0, max>, user: string, script: string, last-request-cpu: float, last-request-memory: int<0, max>}', $status['procs'][0]); + + assertType('int<2, max>', $status['procs'][0]['pid']); +} diff --git a/tests/PHPStan/Analyser/nsrt/generic-callables.php b/tests/PHPStan/Analyser/nsrt/generic-callables.php new file mode 100644 index 0000000000..9fde822894 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-callables.php @@ -0,0 +1,80 @@ += 8.0 + +namespace GenericCallables; + +use Closure; + +use function PHPStan\Testing\assertType; + +/** + * @template TFuncRet of mixed + * @param TFuncRet $mixed + * + * @return Closure(): TFuncRet + */ +function testFuncClosure(mixed $mixed): Closure +{ +} + +/** + * @template TFuncRet of mixed + * @param TFuncRet $mixed + * + * @return Closure(TClosureRet $val): (TClosureRet|TFuncRet) + */ +function testFuncClosureMixed(mixed $mixed) +{ +} + +/** + * @template TFuncRet of mixed + * @param TFuncRet $mixed + * + * @return callable(): TFuncRet + */ +function testFuncCallable(mixed $mixed): callable +{ +} + +/** + * @param Closure(TRet $val): TRet $callable + * @param non-empty-list(TRet $val): TRet> $callables + */ +function testClosure(Closure $callable, int $int, string $str, array $callables): void +{ + assertType('Closure(TRet): TRet', $callable); + assertType('int', $callable($int)); + assertType('string', $callable($str)); + assertType('string', $callables[0]($str)); + assertType('Closure(): 1', testFuncClosure(1)); +} + +function testClosureMixed(int $int, string $str): void +{ + $closure = testFuncClosureMixed($int); + assertType('Closure(TClosureRet): (int|TClosureRet)', $closure); + assertType('int|string', $closure($str)); +} + +/** + * @param callable(TRet $val): TRet $callable + */ +function testCallable(callable $callable, int $int, string $str): void +{ + assertType('callable(TRet): TRet', $callable); + assertType('int', $callable($int)); + assertType('string', $callable($str)); + assertType('callable(): 1', testFuncCallable(1)); +} + +/** + * @param Closure(TRetFirst $valone): (Closure(TRetSecond $valtwo): (TRetFirst|TRetSecond)) $closure + */ +function testNestedClosures(Closure $closure, string $str, int $int): void +{ + assertType('Closure(TRetFirst): (Closure(TRetSecond $valtwo): (TRetFirst|TRetSecond))', $closure); + $closure1 = $closure($str); + assertType('Closure(TRetSecond): (string|TRetSecond)', $closure1); + $result = $closure1($int); + assertType('int|string', $result); +} diff --git a/tests/PHPStan/Analyser/data/generic-class-string.php b/tests/PHPStan/Analyser/nsrt/generic-class-string.php similarity index 86% rename from tests/PHPStan/Analyser/data/generic-class-string.php rename to tests/PHPStan/Analyser/nsrt/generic-class-string.php index d808a0fcf8..ed52a6fa21 100644 --- a/tests/PHPStan/Analyser/data/generic-class-string.php +++ b/tests/PHPStan/Analyser/nsrt/generic-class-string.php @@ -21,21 +21,20 @@ function testMixed($a) { assertType('class-string|DateTimeInterface', $a); assertType('DateTimeInterface', new $a()); } else { - assertType('mixed~class-string|DateTimeInterface', $a); + assertType('mixed', $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); + assertType('mixed', $a); } if (is_subclass_of($a, C::class)) { assertType('int', $a::f()); } else { - assertType('mixed~class-string|PHPStan\Generics\GenericClassStringType\C', $a); + assertType('mixed', $a); } } @@ -48,7 +47,7 @@ function testObject($a) { if (is_subclass_of($a, 'DateTimeInterface')) { assertType('DateTimeInterface', $a); } else { - assertType('object~DateTimeInterface', $a); + assertType('object', $a); } } @@ -82,13 +81,13 @@ function testStringObject($a) { assertType('class-string|DateTimeInterface', $a); assertType('DateTimeInterface', new $a()); } else { - assertType('object~DateTimeInterface|string', $a); + assertType('object|string', $a); } if (is_subclass_of($a, C::class)) { assertType('int', $a::f()); } else { - assertType('object~PHPStan\Generics\GenericClassStringType\C|string', $a); + assertType('object|string', $a); } } diff --git a/tests/PHPStan/Analyser/nsrt/generic-enum-class-string.php b/tests/PHPStan/Analyser/nsrt/generic-enum-class-string.php new file mode 100644 index 0000000000..5c4262bb3c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-enum-class-string.php @@ -0,0 +1,14 @@ += 8.1 + +namespace PHPStan\Generics\GenericEnumClassStringType; + +use function PHPStan\Testing\assertType; + +function testEnumExists(string $str) +{ + assertType('string', $str); + if (enum_exists($str)) { + assertType('class-string', $str); + } +} + diff --git a/tests/PHPStan/Analyser/data/generic-generalization.php b/tests/PHPStan/Analyser/nsrt/generic-generalization.php similarity index 80% rename from tests/PHPStan/Analyser/data/generic-generalization.php rename to tests/PHPStan/Analyser/nsrt/generic-generalization.php index 34280cdd25..dcbde64d5b 100644 --- a/tests/PHPStan/Analyser/data/generic-generalization.php +++ b/tests/PHPStan/Analyser/nsrt/generic-generalization.php @@ -32,17 +32,17 @@ function testUnbounded( string $numericString, string $nonEmptyString ): void { - assertType('string', unbounded('hello')); - assertType('string', unbounded('stdClass')); + assertType('\'hello\'', unbounded('hello')); + assertType('\'stdClass\'', unbounded('stdClass')); assertType('class-string', unbounded($classString)); assertType('class-string', unbounded($genericClassString)); - assertType('string', unbounded(rand(0,1) === 1 ? 'hello' : $classString)); + assertType("'hello'|class-string", unbounded(rand(0,1) === 1 ? 'hello' : $classString)); - assertType('array{foo: int}', unbounded($arrayShape)); + assertType('array{foo: 42}', unbounded($arrayShape)); - assertType('string', unbounded($numericString)); - assertType('string', unbounded($nonEmptyString)); + assertType('numeric-string', unbounded($numericString)); + assertType('non-empty-string', unbounded($nonEmptyString)); } /** diff --git a/tests/PHPStan/Analyser/nsrt/generic-method-tags.php b/tests/PHPStan/Analyser/nsrt/generic-method-tags.php new file mode 100644 index 0000000000..0aab6ea591 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-method-tags.php @@ -0,0 +1,25 @@ += 8.0 + +namespace GenericMethodTags; + +use function PHPStan\Testing\assertType; + +/** + * @method TVal doThing(TVal $param) + * @method TVal doAnotherThing(int $param) + */ +class Test +{ + public function __call(): mixed + { + } +} + +function test(int $int, string $string): void +{ + $test = new Test(); + + assertType('int', $test->doThing($int)); + assertType('string', $test->doThing($string)); + assertType(TVal::class, $test->doAnotherThing($int)); +} diff --git a/tests/PHPStan/Analyser/data/generic-object-lower-bound.php b/tests/PHPStan/Analyser/nsrt/generic-object-lower-bound.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-object-lower-bound.php rename to tests/PHPStan/Analyser/nsrt/generic-object-lower-bound.php diff --git a/tests/PHPStan/Analyser/data/generic-offset-get.php b/tests/PHPStan/Analyser/nsrt/generic-offset-get.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-offset-get.php rename to tests/PHPStan/Analyser/nsrt/generic-offset-get.php diff --git a/tests/PHPStan/Analyser/data/generic-parent.php b/tests/PHPStan/Analyser/nsrt/generic-parent.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-parent.php rename to tests/PHPStan/Analyser/nsrt/generic-parent.php diff --git a/tests/PHPStan/Analyser/nsrt/generic-static.php b/tests/PHPStan/Analyser/nsrt/generic-static.php new file mode 100644 index 0000000000..c7163151f7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-static.php @@ -0,0 +1,173 @@ + + */ + public function map(callable $cb); + + /** @return static */ + public function flip(); + + /** @return static */ + public function fluent(); + + /** @return static> */ + public function nested(); + +} + +/** + * @template T + * @template U + * @implements Foo + */ +class FooImpl implements Foo +{ + + public function map(callable $cb) + { + + } + + public function flip() + { + + } + + public function fluent() + { + + } + + public function doFoo(): void + { + assertType('static(GenericStatic\FooImpl)', $this->map(function () { + return 1; + })); + + assertType('static(GenericStatic\FooImpl)', $this->flip()); + assertType('static(GenericStatic\FooImpl)', $this->fluent()); + assertType('static(GenericStatic\FooImpl)>)', $this->nested()); + } + + /** + * @param FooImpl $s + */ + public function doBar(self $s): void + { + assertType('GenericStatic\\FooImpl', $s->map(function () { + return 1; + })); + + assertType('GenericStatic\\FooImpl', $s->flip()); + assertType('GenericStatic\\FooImpl', $s->fluent()); + assertType('GenericStatic\FooImpl>', $s->nested()); + } + +} + +/** + * @template T + * @template U + * @implements Foo + */ +abstract class Inconsistent implements Foo +{ + + public function fluent() + { + + } + + /** + * @param Inconsistent $s + */ + public function test(self $s): void + { + assertType('static(GenericStatic\Inconsistent)', $this->fluent()); + assertType('GenericStatic\\Inconsistent', $s->fluent()); + } + +} + +/** + * @template T + * @implements Foo + */ +abstract class Inconsistent2 implements Foo +{ + + public function fluent() + { + + } + + /** + * @param Inconsistent2 $s + */ + public function test(self $s): void + { + assertType('static(GenericStatic\Inconsistent2)', $this->fluent()); + assertType('GenericStatic\\Inconsistent2', $s->fluent()); + } + +} + +/** + * @template T + * @implements Foo + */ +abstract class Inconsistent3 implements Foo +{ + + public function fluent() + { + + } + + /** + * @param Inconsistent3 $s + */ + public function test(self $s): void + { + assertType('static(GenericStatic\Inconsistent3)', $this->fluent()); + assertType('GenericStatic\\Inconsistent3', $s->fluent()); + } + +} + +/** + * @template T + * @template K + */ +class A { + /** @return static */ + public function doFoo() {} + +} + +/** @extends A */ +class B extends A { + public function doBar(): void + { + $f = $this->doFoo(); + assertType('static(GenericStatic\B)', $f); + } +} + +function (): void { + assertType(B::class, (new B)->doFoo()); +}; diff --git a/tests/PHPStan/Analyser/data/generic-traits.php b/tests/PHPStan/Analyser/nsrt/generic-traits.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-traits.php rename to tests/PHPStan/Analyser/nsrt/generic-traits.php diff --git a/tests/PHPStan/Analyser/nsrt/generic-unions.php b/tests/PHPStan/Analyser/nsrt/generic-unions.php new file mode 100644 index 0000000000..da3d1bac83 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-unions.php @@ -0,0 +1,135 @@ +doFoo($nullableString)); + assertType('int|string', $this->doFoo($stringOrInt)); + + assertType('string|null', $this->doBar($nullableString)); + + assertType('1', $this->doBaz(1)); + assertType('\'foo\'', $this->doBaz('foo')); + assertType('1.2', $this->doBaz(1.2)); + assertType('string', $this->doBaz($stringOrInt)); + } + +} + +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('3|null', getWithDefault(3)); +assertType('3|null', getWithDefaultCallable(3)); +assertType('3|\'foo\'', getWithDefault(3, 'foo')); +assertType('3|\'foo\'', getWithDefaultCallable(3, 'foo')); +assertType('3|\'foo\'', getWithDefault(3, function () { + return 'foo'; +})); +assertType('3|\'foo\'', getWithDefaultCallable(3, function () { + return 'foo'; +})); +assertType('3|GenericUnions\Foo', getWithDefault(3, function () { + return new Foo; +})); +assertType('3|GenericUnions\Foo', getWithDefaultCallable(3, function () { + return new Foo; +})); +assertType('3|GenericUnions\Foo', getWithDefault(3, new Foo)); +assertType('3|GenericUnions\Foo', getWithDefaultCallable(3, new Foo)); +assertType('3|string', getWithDefaultCallable(3, new InvokableClass)); diff --git a/tests/PHPStan/Analyser/data/generics-default.php b/tests/PHPStan/Analyser/nsrt/generics-default.php similarity index 100% rename from tests/PHPStan/Analyser/data/generics-default.php rename to tests/PHPStan/Analyser/nsrt/generics-default.php diff --git a/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php b/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php new file mode 100644 index 0000000000..d00b8b699a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php @@ -0,0 +1,148 @@ + + */ +function test2($param): Foo +{ + +} + +/** @template T */ +class Foo +{ + + /** @param T $p */ + public function __construct($p) + { + + } + +} + +function (): void { + assertType('array<1>', test(1)); + assertType('GenericsDoNotGeneralize\Foo', test2(1)); + assertType('GenericsDoNotGeneralize\Foo', new Foo(1)); +}; + +class Test +{ + public const CONST_A = 1; + public const CONST_B = 2; + + /** + * @return self::CONST_* + */ + public static function foo(): int + { + return self::CONST_A; + } +} + +/** + * Produces a new array of elements by mapping each element in collection through a transformation function (callback). + * Callback arguments will be element, index, collection + * + * @template K of array-key + * @template V + * @template V2 + * + * @param iterable $collection + * @param callable(V,K,iterable):V2 $callback + * + * @return ($collection is list ? list : array) + * + * @no-named-arguments + */ +function map($collection, callable $callback) +{ + $aggregation = []; + + foreach ($collection as $index => $element) { + $aggregation[$index] = $callback($element, $index, $collection); + } + + return $aggregation; +} + +function (): void { + $foo = Test::foo(); + + assertType('1|2', $foo); + + $bar = map([new Test()], static fn(Test $test) => $test::foo()); + + assertType('list<1|2>', $bar); +}; + +function (): void { + /** @var list $a */ + $a = doFoo(); + + assertType('ArrayIterator', new ArrayIterator($a)); +}; + +/** + * @template K of array-key + * @template V + * @param array $a + * @return ArrayIterator + */ +function createArrayIterator(array $a): ArrayIterator +{ + +} + +function (): void { + /** @var list $a */ + $a = doFoo(); + + assertType('ArrayIterator', createArrayIterator($a)); +}; + +/** @template T */ +class FooInvariant +{ + + /** @param T $p */ + public function __construct($p) + { + + } + +} + +/** @template-covariant T */ +class FooCovariant +{ + + /** @param T $p */ + public function __construct($p) + { + + } + +} + +function (): void { + assertType('GenericsDoNotGeneralize\\FooInvariant', new FooInvariant(1)); + assertType('GenericsDoNotGeneralize\\FooCovariant<1>', new FooCovariant(1)); +}; diff --git a/tests/PHPStan/Analyser/data/generics-empty-array.php b/tests/PHPStan/Analyser/nsrt/generics-empty-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/generics-empty-array.php rename to tests/PHPStan/Analyser/nsrt/generics-empty-array.php diff --git a/tests/PHPStan/Analyser/data/generics-reduce-types-first.php b/tests/PHPStan/Analyser/nsrt/generics-reduce-types-first.php similarity index 100% rename from tests/PHPStan/Analyser/data/generics-reduce-types-first.php rename to tests/PHPStan/Analyser/nsrt/generics-reduce-types-first.php diff --git a/tests/PHPStan/Analyser/data/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php similarity index 96% rename from tests/PHPStan/Analyser/data/generics.php rename to tests/PHPStan/Analyser/nsrt/generics.php index 45e522e954..1968fab471 100644 --- a/tests/PHPStan/Analyser/data/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -97,7 +97,7 @@ function testD($int, $float, $intFloat) assertType('DateTime|int', d($int, new \DateTime())); assertType('DateTime|float|int', d($intFloat, new \DateTime())); assertType('array{}|DateTime', d([], new \DateTime())); - assertType('array{blabla: string}|DateTime', d(['blabla' => 'barrrr'], new \DateTime())); + assertType('array{blabla: \'barrrr\'}|DateTime', d(['blabla' => 'barrrr'], new \DateTime())); } /** @@ -131,7 +131,7 @@ function f($a, $b) { $result = []; assertType('array
', $a); - assertType('callable(A (function PHPStan\Generics\FunctionsAssertType\f(), argument)): B (function PHPStan\Generics\FunctionsAssertType\f(), argument)', $b); + assertType('callable(A): B', $b); foreach ($a as $k => $v) { assertType('A (function PHPStan\Generics\FunctionsAssertType\f(), argument)', $v); $newV = $b($v); @@ -147,10 +147,10 @@ function f($a, $b) */ function testF($arrayOfInt, $callableOrNull) { - assertType('Closure(int): numeric-string', function (int $a): string { + assertType('Closure(int): (lowercase-string&numeric-string&uppercase-string)', function (int $a): string { return (string)$a; }); - assertType('array', f($arrayOfInt, function (int $a): string { + assertType('array', f($arrayOfInt, function (int $a): string { return (string)$a; })); assertType('Closure(mixed): string', function ($a): string { @@ -159,12 +159,12 @@ function testF($arrayOfInt, $callableOrNull) assertType('array', f($arrayOfInt, function ($a): string { return (string)$a; })); - assertType('array', f($arrayOfInt, function ($a) { + assertType('array', f($arrayOfInt, function ($a) { return $a; })); assertType('array', f($arrayOfInt, $callableOrNull)); - assertType('array', f($arrayOfInt, null)); - assertType('array', f($arrayOfInt, '')); + assertType('array', f($arrayOfInt, null)); + assertType('array', f($arrayOfInt, '')); } /** @@ -224,7 +224,7 @@ function testArrayMap(array $listOfIntegers) return (string) $int; }, $listOfIntegers); - assertType('array', $strings); + assertType('array', $strings); } /** @@ -741,7 +741,7 @@ function testClasses() assertType('DateTime', $ab->getB(new \DateTime())); $noConstructor = new NoConstructor(1); - assertType('PHPStan\Generics\FunctionsAssertType\NoConstructor', $noConstructor); + assertType('PHPStan\Generics\FunctionsAssertType\NoConstructor', $noConstructor); assertType('stdClass', acceptsClassString(\stdClass::class)); assertType('class-string', returnsClassString(new \stdClass())); @@ -763,7 +763,7 @@ function testClasses() $factory = new Factory(new \DateTime(), new A(1)); assertType( - 'array{DateTime, PHPStan\\Generics\\FunctionsAssertType\\A, string, PHPStan\\Generics\\FunctionsAssertType\\A}', + 'array{DateTime, PHPStan\\Generics\\FunctionsAssertType\\A, \'\', PHPStan\\Generics\\FunctionsAssertType\\A}', $factory->create(new \DateTime(), '', new A(new \DateTime())) ); } @@ -1176,6 +1176,7 @@ class PrefixedTemplateWins2 * @template T of Foo * @phpstan-template T of Bar * @psalm-template T of Baz + * @phan-template T of Quux */ class PrefixedTemplateWins3 { @@ -1209,12 +1210,25 @@ class PrefixedTemplateWins5 } +/** + * @phan-template T of Foo + * @phpstan-template T of Bar + */ +class PrefixedTemplateWins6 +{ + + /** @var T */ + public $name; + +} + function testPrefixed( PrefixedTemplateWins $a, PrefixedTemplateWins2 $b, PrefixedTemplateWins3 $c, PrefixedTemplateWins4 $d, - PrefixedTemplateWins5 $e + PrefixedTemplateWins5 $e, + PrefixedTemplateWins6 $f ) { assertType('PHPStan\Generics\FunctionsAssertType\Bar', $a->name); @@ -1222,6 +1236,7 @@ function testPrefixed( assertType('PHPStan\Generics\FunctionsAssertType\Bar', $c->name); assertType('PHPStan\Generics\FunctionsAssertType\Bar', $d->name); assertType('PHPStan\Generics\FunctionsAssertType\Bar', $e->name); + assertType('PHPStan\Generics\FunctionsAssertType\Bar', $f->name); }; /** @@ -1405,7 +1420,7 @@ function (\Throwable $e): void { function (): void { $array = ['a' => 1, 'b' => 2]; - assertType('array{a: int, b: int}', a($array)); + assertType('array{a: 1, b: 2}', a($array)); }; @@ -1545,7 +1560,7 @@ function (): void { assertType('array{\'a\', \'b\', \'c\'}', arrayBound2(range('a', 'c'))); assertType('array', arrayBound2([1, 2, 3])); assertType('array{true, false, true}', arrayBound3([true, false, true])); - assertType('array{array{a: \'a\'}, array{b: \'b\'}, array{c: \'c\'}}', arrayBound4([['a' => 'a'], ['b' => 'b'], ['c' => 'c']])); + assertType("array{array{a: 'a'}, array{b: 'b'}, array{c: 'c'}}", arrayBound4([['a' => 'a'], ['b' => 'b'], ['c' => 'c']])); assertType('array', arrayBound5(range('a', 'c'))); }; diff --git a/tests/PHPStan/Analyser/nsrt/get-class-static-class.php b/tests/PHPStan/Analyser/nsrt/get-class-static-class.php new file mode 100644 index 0000000000..e6e378c808 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/get-class-static-class.php @@ -0,0 +1,28 @@ +', get_defined_vars()); // any variable can exist + +function doFoo(int $param) { + $local = "foo"; + assertType('array{param: int, local: \'foo\'}', get_defined_vars()); + assertType('array{\'param\', \'local\'}', array_keys(get_defined_vars())); +} + +function doBar(int $param) { + global $global; + $local = "foo"; + assertType('array{param: int, global: mixed, local: \'foo\'}', get_defined_vars()); + assertType('array{\'param\', \'global\', \'local\'}', array_keys(get_defined_vars())); +} + +function doConditional(int $param) { + $local = "foo"; + if(true) { + $conditional = "bar"; + assertType('array{param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars()); + } else { + $other = "baz"; + assertType('array{param: int, local: \'foo\', other: \'baz\'}', get_defined_vars()); + } + assertType('array{param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars()); +} + +function doRandom(int $param) { + $local = "foo"; + if(rand(0, 1)) { + $random1 = "bar"; + assertType('array{param: int, local: \'foo\', random1: \'bar\'}', get_defined_vars()); + } else { + $random2 = "baz"; + assertType('array{param: int, local: \'foo\', random2: \'baz\'}', get_defined_vars()); + } + assertType('array{param: int, local: \'foo\', random2?: \'baz\', random1?: \'bar\'}', get_defined_vars()); +} diff --git a/tests/PHPStan/Analyser/nsrt/get-native-type.php b/tests/PHPStan/Analyser/nsrt/get-native-type.php new file mode 100644 index 0000000000..a8d6366108 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/get-native-type.php @@ -0,0 +1,44 @@ +doBar()); + assertNativeType('string', $this->doBar()); + + assertType('string', $this->doBaz()); + assertNativeType('mixed', $this->doBaz()); + + assertType('non-empty-string', $this->doLorem()); + assertNativeType('string', $this->doLorem()); + } + + public function doBar(): string + { + + } + + /** + * @return string + */ + public function doBaz() + { + + } + + /** + * @return non-empty-string + */ + public function doLorem(): string + { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/getopt.php b/tests/PHPStan/Analyser/nsrt/getopt.php new file mode 100644 index 0000000000..aae0e128e5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/getopt.php @@ -0,0 +1,10 @@ +|string|false>|false)', $opts); +assertType('int<1, max>', $restIndex); diff --git a/tests/PHPStan/Analyser/nsrt/gettype.php b/tests/PHPStan/Analyser/nsrt/gettype.php new file mode 100644 index 0000000000..a5b0d751ec --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/gettype.php @@ -0,0 +1,54 @@ +', $GLOBALS); +\PHPStan\Testing\assertType('array', $_SERVER); +\PHPStan\Testing\assertType('array', $_GET); +\PHPStan\Testing\assertType('array', $_POST); +\PHPStan\Testing\assertType('array', $_FILES); +\PHPStan\Testing\assertType('array', $_COOKIE); +\PHPStan\Testing\assertType('array', $_SESSION); +\PHPStan\Testing\assertType('array', $_REQUEST); +\PHPStan\Testing\assertType('array', $_ENV); diff --git a/tests/PHPStan/Analyser/data/graphics-draw-return-types.php b/tests/PHPStan/Analyser/nsrt/graphics-draw-return-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/graphics-draw-return-types.php rename to tests/PHPStan/Analyser/nsrt/graphics-draw-return-types.php diff --git a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php new file mode 100644 index 0000000000..eacfb06af6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php @@ -0,0 +1,163 @@ +>', $fileErrorsCounts); + assertType('int<1, max>', $fileErrorsCounts[$errorMessage]); + $fileErrorsCounts[$errorMessage] = 1; + assertType('non-empty-array>', $fileErrorsCounts); + assertType('1', $fileErrorsCounts[$errorMessage]); + continue; + } + + assertType('array>', $fileErrorsCounts); + assertType('int<1, max>', $fileErrorsCounts[$errorMessage]); + + $fileErrorsCounts[$errorMessage]++; + + assertType('non-empty-array>', $fileErrorsCounts); + assertType('int<2, max>', $fileErrorsCounts[$errorMessage]); + } + + assertType('array>', $fileErrorsCounts); + } + + /** + * @param mixed[] $result + * @return void + */ + public function doBar(array $result): void + { + assertType('array', $result); + assert($result['totals']['file_errors'] === 3); + assertType("array", $result); + assertType("mixed", $result['totals']); + assertType('3', $result['totals']['file_errors']); + assertType('mixed', $result['totals']['errors']); + assert($result['totals']['errors'] === 0); + assertType("array", $result); + assertType("mixed", $result['totals']); + assertType('3', $result['totals']['file_errors']); + assertType('0', $result['totals']['errors']); + } + + /** + * @param array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null} $range + * @return void + */ + public function testIsset($range): void + { + assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + if (isset($range['min']) || isset($range['max'])) { + assertType("non-empty-array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + } else { + assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + } + + assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + } + +} + +class TryMixed +{ + + public function doFoo($mixed) + { + if (isset($mixed[0])) { + assertType("mixed~null", $mixed[0]); + assertType("mixed~null", $mixed); + } else { + assertType("mixed", $mixed); + } + + assertType("mixed", $mixed); + } + + public function doFoo2($mixed) + { + if (isset($mixed['foo'])) { + assertType("mixed~null", $mixed['foo']); + assertType("mixed~null", $mixed); + } else { + assertType("mixed", $mixed); + } + + assertType("mixed", $mixed); + } + + public function doBar(\SimpleXMLElement $xml) + { + if (isset($xml['foo'])) { + assertType('SimpleXMLElement', $xml['foo']); + assertType("SimpleXMLElement&hasOffset('foo')", $xml); + } + } + +} + + +class AssignVsNarrow +{ + + /** + * @param array{a: string} $a + * @return void + */ + public function doFoo(array $a) + { + if (is_int($a['a'])) { + assertType('*NEVER*', $a); + } + } + + /** + * @param array{a: string} $a + * @return void + */ + public function doBar(array $a, int $i) + { + $a['a'] = $i; + assertType('array{a: int}', $a); + } + + /** + * @param array $a + * @return void + */ + public function doFoo2(array $a) + { + if (is_int($a['a'])) { + assertType("non-empty-array&hasOffsetValue('a', *NEVER*)", $a); + } + } + + /** + * @param array $a + * @return void + */ + public function doBar2(array $a, int $i, string $s) + { + $a['a'] = $i; + assertType('non-empty-array&hasOffsetValue(\'a\', int)', $a); + $a['a'] = $s; + assertType('non-empty-array&hasOffsetValue(\'a\', string)', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions-74.php b/tests/PHPStan/Analyser/nsrt/hash-functions-74.php new file mode 100644 index 0000000000..2ffcb920f8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/hash-functions-74.php @@ -0,0 +1,56 @@ += 8.0 + +namespace HashFunctions; + +use function PHPStan\Testing\assertType; + +class HashFunctionTests80 +{ + + public function hash_hmac(string $string): void + { + assertType('*NEVER*', hash_hmac('crc32', 'data', 'key')); + assertType('*NEVER*', hash_hmac('invalid', 'data', 'key')); + assertType('lowercase-string&non-falsy-string', hash_hmac($string, 'data', 'key')); + assertType('non-falsy-string', hash_hmac($string, 'data', 'key', true)); + } + + public function hash_hmac_file(): void + { + assertType('*NEVER*', hash_hmac_file('crc32', 'filename', 'key')); + assertType('*NEVER*', hash_hmac_file('invalid', 'filename', 'key')); + } + + public function hash(string $string): void + { + assertType('*NEVER*', hash('invalid', 'data', false)); + assertType('lowercase-string&non-falsy-string', hash($string, 'data')); + } + + public function hash_file(): void + { + assertType('*NEVER*', hash_file('invalid', 'filename', false)); + } + + public function hash_hkdf(string $string): void + { + assertType('*NEVER*', hash_hkdf('crc32', 'key')); + assertType('*NEVER*', hash_hkdf('invalid', 'key')); + assertType('non-falsy-string', hash_hkdf($string, 'key')); + } + + public function hash_pbkdf2(string $string): void + { + assertType('*NEVER*', hash_pbkdf2('crc32', 'password', 'salt', 1000)); + assertType('*NEVER*', hash_pbkdf2('invalid', 'password', 'salt', 1000)); + assertType('lowercase-string&non-falsy-string', hash_pbkdf2($string, 'password', 'salt', 1000)); + } + + public function caseSensitive() + { + assertType('*NEVER*', hash_hkdf('CRC32', 'key')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions.php b/tests/PHPStan/Analyser/nsrt/hash-functions.php new file mode 100644 index 0000000000..72f977baae --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/hash-functions.php @@ -0,0 +1,84 @@ +', $http_response_header); +assertNativeType('array', $http_response_header); diff --git a/tests/PHPStan/Analyser/nsrt/ibm_db2.php b/tests/PHPStan/Analyser/nsrt/ibm_db2.php new file mode 100644 index 0000000000..ca132f98eb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/ibm_db2.php @@ -0,0 +1,20 @@ + 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 + { + + } + + public function doBar(array $arr1, array $arr2): void + { + /** + * @var array{foo: bool, bar: int} $arr1 + * @var array{foo: bool, bar: int} $arr2 + */ + if ($arr1 === $arr2) { + assertType('array{foo: bool, bar: int}', $arr1); + assertType('array{foo: bool, bar: int}', $arr2); + } else { + assertType('array{foo: bool, bar: int}', $arr1); + assertType('array{foo: bool, bar: int}', $arr2); + } + + /** + * @var array{foo: true, bar: 17} $arr1 + * @var array{foo: bool, bar: int} $arr2 + */ + if ($arr1 === $arr2) { + assertType('array{foo: true, bar: 17}', $arr1); + assertType('array{foo: true, bar: 17}', $arr2); + } else { + assertType('array{foo: true, bar: 17}', $arr1); + assertType('array{foo: bool, bar: int}', $arr2); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/image-size.php b/tests/PHPStan/Analyser/nsrt/image-size.php new file mode 100644 index 0000000000..a68a0706b3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/image-size.php @@ -0,0 +1,43 @@ +', $width); + assertType('int<0, max>', $height); + assertType('int', $type); + assertType('string', $attr); + assertType('string', $imagesize['mime']); + assertType('int', $imagesize['channels']); + assertType('int', $imagesize['bits']); +} + +function imagesizeFoo(string $s): void +{ + $imagesize = getimagesizefromstring($s); + if ($imagesize === false) { + return; + } + list($width, $height, $type, $attr) = $imagesize; + + assertType('int<0, max>', $width); + assertType('int<0, max>', $height); + assertType('int', $type); + assertType('string', $attr); + assertType('string', $imagesize['mime']); + assertType('int', $imagesize['channels']); + assertType('int', $imagesize['bits']); +} + + diff --git a/tests/PHPStan/Analyser/nsrt/imagick-pixel.php b/tests/PHPStan/Analyser/nsrt/imagick-pixel.php new file mode 100644 index 0000000000..8904482f3e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/imagick-pixel.php @@ -0,0 +1,15 @@ +, g: int<0, 255>, b: int<0, 255>, a: int<0, 1>}', $imagickPixel->getColor()); + assertType('array{r: int<0, 255>, g: int<0, 255>, b: int<0, 255>, a: int<0, 1>}', $imagickPixel->getColor(0)); + assertType('array{r: float, g: float, b: float, a: float}', $imagickPixel->getColor(1)); + assertType('array{r: int<0, 255>, g: int<0, 255>, b: int<0, 255>, a: int<0, 255>}', $imagickPixel->getColor(2)); + assertType('array{}', $imagickPixel->getColor(3)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php new file mode 100644 index 0000000000..8e97e19f72 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/implode.php @@ -0,0 +1,71 @@ + $arr + */ + public function ints(array $arr, int $i) + { + assertType("lowercase-string&uppercase-string", implode($arr)); + assertType("lowercase-string&non-empty-string&uppercase-string", implode([$i, $i])); + if ($i !== 0) { + assertType("lowercase-string&non-falsy-string&uppercase-string", implode([$i, $i])); + } + } + + const X = 'x'; + const ONE = 1; + + public function constants() { + assertType("'12345'", implode(['12', '345'])); + + assertType("'12345'", implode('', ['12', '345'])); + assertType("'12345'", join('', ['12', '345'])); + + assertType("'12,345'", implode(',', ['12', '345'])); + assertType("'12,345'", join(',', ['12', '345'])); + + assertType("'x,345'", join(',', [self::X, '345'])); + assertType("'1,345'", join(',', [self::ONE, '345'])); + } + + /** @param array{0: 1|2, 1: 'a'|'b'} $constArr */ + public function constArrays($constArr) { + assertType("'1a'|'1b'|'2a'|'2b'", implode('', $constArr)); + } + + /** @param array{0: 1|2|3, 1: 'a'|'b'|'c'} $constArr */ + public function constArrays2($constArr) { + assertType("'1a'|'1b'|'1c'|'2a'|'2b'|'2c'|'3a'|'3b'|'3c'", implode('', $constArr)); + } + + /** @param array{0: 1, 1: 'a'|'b', 2: 'x'|'y'} $constArr */ + public function constArrays3($constArr) { + assertType("'1ax'|'1ay'|'1bx'|'1by'", implode('', $constArr)); + } + + /** @param array{0: 1, 1: 'a'|'b', 2?: 'x'|'y'} $constArr */ + public function constArrays4($constArr) { + assertType("'1a'|'1ax'|'1ay'|'1b'|'1bx'|'1by'", implode('', $constArr)); + } + + /** @param array{10: 1|2|3, xy: 'a'|'b'|'c'} $constArr */ + public function constArrays5($constArr) { + assertType("'1a'|'1b'|'1c'|'2a'|'2b'|'2c'|'3a'|'3b'|'3c'", implode('', $constArr)); + } + + /** @param array{0: 1, 1: 'a'|'b', 3?: 'c'|'d', 4?: 'e'|'f', 5?: 'g'|'h', 6?: 'x'|'y'} $constArr */ + public function constArrays6($constArr) { + assertType("lowercase-string&non-falsy-string", implode('', $constArr)); + } + + /** @param array{10: 1|2|bool, xy: 'a'|'b'|'c'} $constArr */ + public function constArrays7($constArr) { + assertType("'1a'|'1b'|'1c'|'2a'|'2b'|'2c'|'a'|'b'|'c'", implode('', $constArr)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/impure-connection-fns.php b/tests/PHPStan/Analyser/nsrt/impure-connection-fns.php new file mode 100644 index 0000000000..314b3e7bfb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/impure-connection-fns.php @@ -0,0 +1,15 @@ +', connection_status()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/impure-constructor.php b/tests/PHPStan/Analyser/nsrt/impure-constructor.php new file mode 100644 index 0000000000..d992b822ba --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/impure-constructor.php @@ -0,0 +1,130 @@ +active = false; + } + + public function getActive(): bool + { + return $this->active; + } + + public function activate(): void + { + $this->active = true; + } + +} + + +class ClassWithImpureConstructorNotMarked +{ + + public function __construct(Foo $foo) + { + $foo->activate(); + } + +} + +class ClassWithImpureConstructorMarked +{ + + /** + * @phpstan-impure + */ + public function __construct(Foo $foo) + { + $foo->activate(); + } + +} + +class ClassWithPureConstructorMarked +{ + + /** + * @phpstan-pure + */ + public function __construct(Foo $foo) + { + } + +} + +class ClassWithImpureConstructorNotMarkedWithoutParameters +{ + + /** @var string */ + private $lorem; + + public function __construct() + { + $this->lorem = 'lorem'; + } + +} + +class Test +{ + + public function testClassWithImpureConstructorNotMarked() + { + $foo = new Foo(); + assertType('bool', $foo->getActive()); + + assert(!$foo->getActive()); + assertType('false', $foo->getActive()); + + new ClassWithImpureConstructorNotMarked($foo); + assertType('false', $foo->getActive()); + } + + public function testClassWithImpureConstructorMarked() + { + $foo = new Foo(); + assertType('bool', $foo->getActive()); + + assert(!$foo->getActive()); + assertType('false', $foo->getActive()); + + new ClassWithImpureConstructorMarked($foo); + assertType('bool', $foo->getActive()); + } + + public function testClassWithPureConstructorMarked() + { + $foo = new Foo(); + assertType('bool', $foo->getActive()); + + assert(!$foo->getActive()); + assertType('false', $foo->getActive()); + + new ClassWithPureConstructorMarked($foo); + assertType('false', $foo->getActive()); + } + + public function testClassWithImpureConstructorNotMarkedWithoutParameters() + { + $foo = new Foo(); + assertType('bool', $foo->getActive()); + + assert(!$foo->getActive()); + assertType('false', $foo->getActive()); + + new ClassWithImpureConstructorNotMarkedWithoutParameters(); + assertType('false', $foo->getActive()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/impure-error-log.php b/tests/PHPStan/Analyser/nsrt/impure-error-log.php new file mode 100644 index 0000000000..082112b83b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/impure-error-log.php @@ -0,0 +1,14 @@ +fooProp = rand(0, 1); + } + + /** + * @return $this + */ + public function returnsThis($arg) + { + $this->fooProp = rand(0, 1); + } + + /** + * @return $this + * @phpstan-impure + */ + public function returnsThisImpure($arg) + { + $this->fooProp = rand(0, 1); + } + + public function ordinaryMethod(): int + { + return 1; + } + + /** + * @phpstan-impure + * @return int + */ + public function impureMethod(): int + { + $this->fooProp = rand(0, 1); + + return $this->fooProp; + } + + /** + * @impure + * @return int + */ + public function impureMethod2(): int + { + $this->fooProp = rand(0, 1); + + return $this->fooProp; + } + + public function doFoo(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->voidMethod(); + assertType('int', $this->fooProp); + } + + public function doFluent(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->returnsThis(new stdClass()); + assertType('int', $this->fooProp); + } + + public function doFluent2(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->phpDocReturnThis(); + assertType('int', $this->fooProp); + } + + public function doBar(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->ordinaryMethod(); + assertType('1', $this->fooProp); + } + + public function doBaz(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->impureMethod(); + assertType('int', $this->fooProp); + } + + public function doLorem(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->impureMethod2(); + assertType('int', $this->fooProp); + } + +} + +class Person +{ + + public function getName(): ?string + { + } + +} + +class Bar +{ + + public function doFoo(): void + { + $f = new Foo(); + + $p = new Person(); + assert($p->getName() !== null); + assertType('string', $p->getName()); + $f->returnsThis($p); + assertType('string', $p->getName()); + } + + public function doFoo2(): void + { + $f = new Foo(); + + $p = new Person(); + assert($p->getName() !== null); + assertType('string', $p->getName()); + $f->returnsThisImpure($p); + assertType('string|null', $p->getName()); + } + +} + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + /** + * @return int + */ + public function pure(): int + { + echo 'test'; + return 1; + } + + /** + * @return int + */ + public function impure(): int + { + return 1; + } + +} + +function (ExtendingClass $e): void { + assert($e->pure() === 1); + assertType('1', $e->pure()); + $e->impure(); + assertType('int', $e->pure()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/in-array-enum.php b/tests/PHPStan/Analyser/nsrt/in-array-enum.php new file mode 100644 index 0000000000..66ae579980 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in-array-enum.php @@ -0,0 +1,77 @@ += 8.1 + +declare(strict_types=1); + +namespace InArrayEnum; + +use function PHPStan\Testing\assertType; + +enum FooUnitEnum +{ + case A; + case B; +} + +class Foo +{ + + /** + * @param array $strings + * @param array $ints + */ + public function nonConstantValues(FooUnitEnum $a, array $strings, array $ints): void + { + assertType('false', in_array($a, $strings, true)); + assertType('false', in_array($a, $strings, false)); + assertType('false', in_array($a, $strings)); + + assertType('bool', in_array($a->name, $strings, true)); + assertType('bool', in_array($a->name, $strings, false)); + assertType('bool', in_array($a->name, $strings)); + + assertType('false', in_array($a->name, $ints, true)); + assertType('bool', in_array($a->name, $ints, false)); + assertType('bool', in_array($a->name, $ints)); + } + + public function looseCheckEnumSpecifyNeedle(mixed $v): void + { + if (in_array($v, FooUnitEnum::cases())) { + assertType('InArrayEnum\FooUnitEnum::A|InArrayEnum\FooUnitEnum::B', $v); + + if (in_array($v, ['A', null, FooUnitEnum::B])) { + assertType('InArrayEnum\FooUnitEnum::B', $v); + } + } + + } + + /** @param array $haystack */ + public function looseCheckEnumSpecifyHaystack(array $haystack): void + { + if (! in_array(FooUnitEnum::A, $haystack)) { + assertType('array', $haystack); + } + + if (! in_array(rand() ? FooUnitEnum::A : FooUnitEnum::B, $haystack, true)) { + assertType('array', $haystack); + } + + if (! in_array(rand() ? 5 : 6, $haystack, true)) { + assertType('array', $haystack); + } + + if (! in_array(rand() ? 5 : rand(), $haystack, true)) { + assertType('array', $haystack); + } + } + + /** @param array $haystack */ + public function skipUnsafeLooseComparison(?FooUnitEnum $v, array $haystack): void + { + if (in_array($v, $haystack, false)) { + assertType('InArrayEnum\FooUnitEnum|null', $v); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/in-array-haystack-subtract.php b/tests/PHPStan/Analyser/nsrt/in-array-haystack-subtract.php new file mode 100644 index 0000000000..ee1757521a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in-array-haystack-subtract.php @@ -0,0 +1,18 @@ + $haystack */ + public function specifyHaystack(array $haystack): void + { + if (! in_array(rand() ? 5 : 6, $haystack, true)) { + assertType('array', $haystack); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/in-array-non-empty.php b/tests/PHPStan/Analyser/nsrt/in-array-non-empty.php new file mode 100644 index 0000000000..999dbc8ff3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in-array-non-empty.php @@ -0,0 +1,28 @@ + $array + */ + public function sayHello(array $array): void + { + if(in_array("thing", $array, true)){ + assertType('non-empty-list', $array); + } + } + + /** @param array $haystack */ + public function nonConstantNeedle(int $needle, array $haystack): void + { + if (in_array($needle, $haystack, true)) { + assertType('non-empty-array', $haystack); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/in-array.php b/tests/PHPStan/Analyser/nsrt/in-array.php new file mode 100644 index 0000000000..7b0cf403da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in-array.php @@ -0,0 +1,61 @@ + $strings */ + public function doBar(int $i, array $strings): void + { + assertType('bool', in_array($i, $strings)); + assertType('bool', in_array($i, $strings, false)); + assertType('false', in_array($i, $strings, true)); + assertType('false', in_array(1, $strings, true)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/in_array_loose.php b/tests/PHPStan/Analyser/nsrt/in_array_loose.php new file mode 100644 index 0000000000..78d2899b8c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in_array_loose.php @@ -0,0 +1,48 @@ += 8.0 + +namespace InArrayLoose; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function looseComparison( + string $string, + int $int, + float $float, + bool $bool, + string|int $stringOrInt, + string|null $stringOrNull, + ): void { + if (in_array($string, ['1', 'a'])) { + assertType("'1'|'a'", $string); + } + if (in_array($string, [1, 'a'])) { + assertType("string", $string); // could be '1'|'a' + } + if (in_array($int, [1, 2])) { + assertType('1|2', $int); + } + if (in_array($int, ['1', 2])) { + assertType('int', $int); // could be 1|2 + } + if (in_array($bool, [true])) { + assertType('true', $bool); + } + if (in_array($bool, [true, null])) { + assertType('bool', $bool); + } + if (in_array($float, [1.0, 2.0])) { + assertType('1.0|2.0', $float); + } + if (in_array($float, ['1', 2.0])) { + assertType('float', $float); // could be 1.0|2.0 + } + if (in_array($stringOrInt, ['1', '2'])) { + assertType('int|string', $stringOrInt); // could be '1'|'2'|1|2 + } + if (in_array($stringOrNull, ['1', 'a'])) { + assertType('string|null', $stringOrNull); // could be '1'|'a' + } + } +} diff --git a/tests/PHPStan/Analyser/data/inc-dec-in-conditions.php b/tests/PHPStan/Analyser/nsrt/inc-dec-in-conditions.php similarity index 100% rename from tests/PHPStan/Analyser/data/inc-dec-in-conditions.php rename to tests/PHPStan/Analyser/nsrt/inc-dec-in-conditions.php diff --git a/tests/PHPStan/Analyser/nsrt/inherit-abstract-trait-method-phpdoc.php b/tests/PHPStan/Analyser/nsrt/inherit-abstract-trait-method-phpdoc.php new file mode 100644 index 0000000000..8bddb79eca --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/inherit-abstract-trait-method-phpdoc.php @@ -0,0 +1,41 @@ +doFoo()); + assertType('mixed', $foo->doBar()); +}; diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-param.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-param.php similarity index 100% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-param.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-param.php diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-return.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-return.php similarity index 100% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-return.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-return.php diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-template.php similarity index 89% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-template.php index e340baba85..087d2893af 100644 --- a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php +++ b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-template.php @@ -32,7 +32,7 @@ public function doFoo($a, $b) public function doBar() { - assertType('array|int', $this->doFoo(1, 'hahaha')); + assertType('1|array<\'hahaha\'>', $this->doFoo(1, 'hahaha')); } } @@ -75,7 +75,7 @@ public function doFoo($a, $b) public function doBar() { - assertType('array|int', $this->doFoo(1, 'hahaha')); + assertType('1|array', $this->doFoo(1, 'hahaha')); } } @@ -92,7 +92,7 @@ public function doFoo($a, $b) public function doBar() { - assertType('array|int', $this->doFoo(1, 'hahaha')); + assertType('1|array<\'hahaha\'>', $this->doFoo(1, 'hahaha')); } } diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-var.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-var.php similarity index 100% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-var.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-var.php diff --git a/tests/PHPStan/Analyser/data/inheritdoc-constructors.php b/tests/PHPStan/Analyser/nsrt/inheritdoc-constructors.php similarity index 100% rename from tests/PHPStan/Analyser/data/inheritdoc-constructors.php rename to tests/PHPStan/Analyser/nsrt/inheritdoc-constructors.php diff --git a/tests/PHPStan/Analyser/data/inheritdoc-parameter-remapping.php b/tests/PHPStan/Analyser/nsrt/inheritdoc-parameter-remapping.php similarity index 100% rename from tests/PHPStan/Analyser/data/inheritdoc-parameter-remapping.php rename to tests/PHPStan/Analyser/nsrt/inheritdoc-parameter-remapping.php diff --git a/tests/PHPStan/Analyser/nsrt/ini-get.php b/tests/PHPStan/Analyser/nsrt/ini-get.php new file mode 100644 index 0000000000..6751b3cc16 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/ini-get.php @@ -0,0 +1,29 @@ + $one + * @param int-mask $two + * @param int-mask<1, 2, 8> $three + * @param int-mask<1, 4, 16, 64, 256, 1024> $four + * @param int-mask-of $five + */ + public static function test(int $one, int $two, int $three, int $four, int $five): void + { + assertType('int<0, 3>', $one); + assertType('int<0, 3>', $two); + assertType('0|1|2|3|8|9|10|11', $three); + assertType('0|1|4|5|16|17|20|21|64|65|68|69|80|81|84|85|256|257|260|261|272|273|276|277|320|321|324|325|336|337|340|341|1024|1025|1028|1029|1040|1041|1044|1045|1088|1089|1092|1093|1104|1105|1108|1109|1280|1281|1284|1285|1296|1297|1300|1301|1344|1345|1348|1349|1360|1361|1364|1365', $four); + assertType('0|1|4|5', $five); + } + + /** + * @param int-mask-of $one + * @param int-mask<0, 1, false> $two + */ + public static function invalid(int $one, int $two, int $three): void + { + assertType('int', $one); // not all constant integers + assertType('int', $two); // not all constant integers + } +} diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-types.php b/tests/PHPStan/Analyser/nsrt/integer-range-types.php new file mode 100644 index 0000000000..876a791352 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/integer-range-types.php @@ -0,0 +1,489 @@ +', $i); + + $i++; + assertType('int', $i); + } else { + assertType('int<3, max>', $i); + } + + if ($i < 3) { + assertType('int', $i); + + $i--; + assertType('int', $i); + } + + assertType('int|int<3, max>', $i); + + if ($i < 3 && $i > 5) { + assertType('*NEVER*', $i); + } else { + assertType('int|int<3, max>', $i); + } + + if ($i > 3 && $i < 5) { + assertType('4', $i); + } else { + assertType('3|int|int<5, max>', $i); + } + + if ($i >= 3 && $i <= 5) { + assertType('int<3, 5>', $i); + + if ($i === 2) { + assertType('*NEVER*', $i); + } else { + assertType('int<3, 5>', $i); + } + + if ($i !== 3) { + assertType('int<4, 5>', $i); + } else { + assertType('3', $i); + } + } +}; + + +function () { + for ($i = 0; $i < 5; $i++) { + assertType('int<0, 4>', $i); + } + + $i = 0; + while ($i < 5) { + assertType('int<0, 4>', $i); + $i++; + } + + $i = 0; + while ($i++ < 5) { + assertType('int<1, 5>', $i); + } + + $i = 0; + while (++$i < 5) { + assertType('int<1, 4>', $i); + } + + $i = 5; + while ($i-- > 0) { + assertType('int<0, 4>', $i); + } + + $i = 5; + while (--$i > 0) { + assertType('int<1, 4>', $i); + } +}; + + +function (int $j) { + $i = 1; + + assertType('true', $i > 0); + assertType('true', $i >= 1); + assertType('true', $i <= 1); + assertType('true', $i < 2); + + assertType('false', $i < 1); + assertType('false', $i <= 0); + assertType('false', $i >= 2); + assertType('false', $i > 1); + + assertType('true', 0 < $i); + assertType('true', 1 <= $i); + assertType('true', 1 >= $i); + assertType('true', 2 > $i); + + assertType('bool', $j > 0); + assertType('bool', $j >= 0); + assertType('bool', $j <= 0); + assertType('bool', $j < 0); + + if ($j < 5) { + assertType('bool', $j > 0); + assertType('false', $j > 4); + assertType('bool', 0 < $j); + assertType('false', 4 < $j); + + assertType('bool', $j >= 0); + assertType('false', $j >= 5); + assertType('bool', 0 <= $j); + assertType('false', 5 <= $j); + + assertType('true', $j <= 4); + assertType('bool', $j <= 3); + assertType('true', 4 >= $j); + assertType('bool', 3 >= $j); + + assertType('true', $j < 5); + assertType('bool', $j < 4); + assertType('true', 5 > $j); + assertType('bool', 4 > $j); + } +}; + +function (int $a, int $b, int $c): void { + + if ($a <= 11) { + return; + } + + assertType('int<12, max>', $a); + + if ($b <= 12) { + return; + } + + assertType('int<13, max>', $b); + + if ($c <= 13) { + return; + } + + assertType('int<14, max>', $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<1, 10>', $r1 / $j); + assertType('int', $rMin * $j); + assertType('int<5, max>', $rMax * $j); + + assertType('int<2, 13>', $j + $r1); + assertType('int<-9, 2>', $j - $r1); + assertType('int<1, 30>', $j * $r1); + assertType('float|int<1, 3>', $j / $r1); + assertType('int', $j * $rMin); + assertType('int<5, max>', $j * $rMax); + + assertType('int<-19, -10>|int<2, 13>', $r1 + $z); + assertType('int<-2, 9>|int<21, 30>', $r1 - $z); + assertType('int<-200, -20>|int<1, 30>', $r1 * $z); + assertType('float|int<1, 10>', $r1 / $z); + assertType('int', $rMin * $z); + assertType('int|int<5, max>', $rMax * $z); + + assertType('int<2, max>', $pi + 1); + assertType('int<-1, max>', $pi - 2); + assertType('int<2, max>', $pi * 2); + assertType('float|int<1, max>', $pi / 2); + assertType('int<2, max>', 1 + $pi); + assertType('int', 2 - $pi); + assertType('int<2, max>', 2 * $pi); + assertType('float|int<1, 2>', 2 / $pi); + + assertType('int<5, 14>', $r1 + 4); + assertType('int<-3, 6>', $r1 - 4); + assertType('int<4, 40>', $r1 * 4); + assertType('float|int<1, 2>', $r1 / 4); + assertType('int<9, max>', $rMax + 4); + assertType('int<1, max>', $rMax - 4); + assertType('int<20, max>', $rMax * 4); + assertType('float|int<2, max>', $rMax / 4); + + assertType('int<6, 20>', $r1 + $r2); + assertType('int<-9, 5>', $r1 - $r2); + assertType('int<5, 100>', $r1 * $r2); + assertType('float|int<1, 2>', $r1 / $r2); + + assertType('int<-99, 19>', $r1 - $r3); + + assertType('int', $r1 + $rMin); + assertType('int<-4, max>', $r1 - $rMin); + assertType('int', $r1 * $rMin); + assertType('float|int<-10, -1>|int<1, 10>', $r1 / $rMin); + assertType('int', $rMin + $r1); + assertType('int', $rMin - $r1); + assertType('int', $rMin * $r1); + assertType('float|int', $rMin / $r1); + + assertType('int<6, max>', $r1 + $rMax); + assertType('int', $r1 - $rMax); + assertType('int<5, max>', $r1 * $rMax); + assertType('float|int<1, 2>', $r1 / $rMax); + assertType('int<6, max>', $rMax + $r1); + assertType('int<-5, max>', $rMax - $r1); + assertType('int<5, max>', $rMax * $r1); + assertType('float|int<1, max>', $rMax / $r1); + + assertType('5|10|15|20|30', $x / $y); + + assertType('float|int<1, max>', $rMax / $rMax); + assertType('(float|int)', $rMin / $rMin); + } + + /** + * @param int<0, max> $a + * @param int<0, max> $b + * @param int<16, 32> $c + * @param int<2, 4> $d + */ + function divisionLoosesInformation(int $a, int $b, int $c, int $d): void { + assertType('float|int<0, max>', $a / $b); + assertType('float|int<8, 16>', $c / 2); + assertType('float|int<4, 16>', $c / $d); + } + + /** + * @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('-1|1|float', -1 / $rMin); + assertType('float', -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); + } + + /** + * @param int<-1, 2> $p + * @param int<-1, 2> $u + */ + public function sayHello($p, $u): void + { + assertType('int<-2, 4>', $p + $u); + assertType('int<-3, 3>', $p - $u); + assertType('int<-2, 4>', $p * $u); + assertType('float|int<-2, 2>', $p / $u); + } + + /** + * @param int<-5, 5> $a + * @param int<5, max> $b + * @param int $c + * @param 1|int<5, 10>|25|int<30, 40> $d + * @param 1|3.0|"5" $e + * @param 1|"ciao" $f + */ + public function shiftLeft($a, $b, $c, $d, $e, $f): void + { + assertType('int<-5, 5>', $a << 0); + assertType('int<5, max>', $b << 0); + assertType('int', $c << 0); + assertType('1|25|int<5, 10>|int<30, 40>', $d << 0); + assertType('1|3|5', $e << 0); + assertType('*ERROR*', $f << 0); + + assertType('int<-10, 10>', $a << 1); + assertType('int<10, max>', $b << 1); + assertType('int', $c << 1); + assertType('2|50|int<10, 20>|int<60, 80>', $d << 1); + assertType('2|6|10', $e << 1); + assertType('*ERROR*', $f << 1); + + assertType('*ERROR*', $a << -1); + + assertType('int', $a << $b); + + assertType('0', null << 1); + assertType('0', false << 1); + assertType('2', true << 1); + assertType('10', "10" << 0); + assertType('*ERROR*', "ciao" << 0); + assertType('30', 15.9 << 1); + assertType('*ERROR*', array(5) << 1); + + assertType('8', 4.1 << 1.9); + + /** @var float */ + $float = 4.1; + assertType('int', $float << 1.9); + } + + /** + * @param int<-5, 5> $a + * @param int<5, max> $b + * @param int $c + * @param 1|int<5, 10>|25|int<30, 40> $d + * @param 1|3.0|"5" $e + * @param 1|"ciao" $f + */ + public function shiftRight($a, $b, $c, $d, $e, $f): void + { + assertType('int<-5, 5>', $a >> 0); + assertType('int<5, max>', $b >> 0); + assertType('int', $c >> 0); + assertType('1|25|int<5, 10>|int<30, 40>', $d >> 0); + assertType('1|3|5', $e >> 0); + assertType('*ERROR*', $f >> 0); + + assertType('int<-3, 2>', $a >> 1); + assertType('int<2, max>', $b >> 1); + assertType('int', $c >> 1); + assertType('0|12|int<2, 5>|int<15, 20>', $d >> 1); + assertType('0|1|2', $e >> 1); + assertType('*ERROR*', $f >> 1); + + assertType('*ERROR*', $a >> -1); + + assertType('int', $a >> $b); + + assertType('0', null >> 1); + assertType('0', false >> 1); + assertType('0', true >> 1); + assertType('10', "10" >> 0); + assertType('*ERROR*', "ciao" >> 0); + assertType('7', 15.9 >> 1); + assertType('*ERROR*', array(5) >> 1); + + assertType('2', 4.1 >> 1.9); + + /** @var float */ + $float = 4.1; + assertType('int', $float >> 1.9); + } + + /** + * @param int<0, max> $positive + * @param int $negative + */ + public function zeroIssues($positive, $negative) + { + assertType('0', 0 * $positive); + assertType('int<0, max>', $positive * $positive); + assertType('0', 0 * $negative); + assertType('int<0, max>', $negative * $negative); + assertType('int', $negative * $positive); + } + +} + +function subtract($m) { + if ($m != 0) { + assertType("mixed~(0|0.0|'0'|false|null)", $m); // could be "mixed~(0|0.0|''|'0'|false|null)" + assertType('int', (int) $m); + } + if ($m !== 0) { + assertType("mixed~0", $m); + assertType('int', (int) $m); // mixed could still contain falsey values, which cast to 0 + } + if (!is_int($m)) { + assertType("mixed~int", $m); + assertType('int', (int) $m); // mixed could still contain falsey values, which cast to 0 + } + + if ($m != true) { + assertType("0|0.0|''|'0'|array{}|false|null", $m); + assertType('0', (int) $m); + } + if ($m !== true) { + assertType("mixed~true", $m); + assertType('int', (int) $m); + } + + if ($m != false) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + assertType('int', (int) $m); + } + if ($m !== false) { + assertType("mixed~false", $m); + assertType('int', (int) $m); // mixed could still contain falsey values, which cast to 0 + } + if (!is_string($m) && !is_float($m)) { + assertType("mixed~(float|string)", $m); + assertType('int', (int) $m); + if ($m != false) { + assertType("mixed~(0|array{}|float|string|false|null)", $m); + assertType('int|int<1, max>', (int) $m); + } + } +} diff --git a/tests/PHPStan/Analyser/data/intersection-static.php b/tests/PHPStan/Analyser/nsrt/intersection-static.php similarity index 100% rename from tests/PHPStan/Analyser/data/intersection-static.php rename to tests/PHPStan/Analyser/nsrt/intersection-static.php diff --git a/tests/PHPStan/Analyser/nsrt/invalid-type-aliases.php b/tests/PHPStan/Analyser/nsrt/invalid-type-aliases.php new file mode 100644 index 0000000000..5b91643af1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/invalid-type-aliases.php @@ -0,0 +1,22 @@ +returnsAlias()); + } + + /** @psalm-return MyObject */ + public function returnsAlias() + { + + } +} diff --git a/tests/PHPStan/Analyser/data/invalidate-object-argument-function.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-function.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-object-argument-function.php rename to tests/PHPStan/Analyser/nsrt/invalidate-object-argument-function.php diff --git a/tests/PHPStan/Analyser/data/invalidate-object-argument-static.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-static.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-object-argument-static.php rename to tests/PHPStan/Analyser/nsrt/invalidate-object-argument-static.php diff --git a/tests/PHPStan/Analyser/data/invalidate-object-argument.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-object-argument.php rename to tests/PHPStan/Analyser/nsrt/invalidate-object-argument.php diff --git a/tests/PHPStan/Analyser/data/invalidate-readonly-properties.php b/tests/PHPStan/Analyser/nsrt/invalidate-readonly-properties.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-readonly-properties.php rename to tests/PHPStan/Analyser/nsrt/invalidate-readonly-properties.php diff --git a/tests/PHPStan/Analyser/data/is-a.php b/tests/PHPStan/Analyser/nsrt/is-a.php similarity index 100% rename from tests/PHPStan/Analyser/data/is-a.php rename to tests/PHPStan/Analyser/nsrt/is-a.php diff --git a/tests/PHPStan/Analyser/data/is-numeric.php b/tests/PHPStan/Analyser/nsrt/is-numeric.php similarity index 100% rename from tests/PHPStan/Analyser/data/is-numeric.php rename to tests/PHPStan/Analyser/nsrt/is-numeric.php diff --git a/tests/PHPStan/Analyser/data/is-subclass-of.php b/tests/PHPStan/Analyser/nsrt/is-subclass-of.php similarity index 100% rename from tests/PHPStan/Analyser/data/is-subclass-of.php rename to tests/PHPStan/Analyser/nsrt/is-subclass-of.php diff --git a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-post-81.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-post-81.php new file mode 100644 index 0000000000..8cc3cc8547 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-post-81.php @@ -0,0 +1,9 @@ += 8.1 + +namespace IssetCoalesceEmptyTypePost81; + +use function PHPStan\Testing\assertType; + +function baz(\ReflectionClass $ref): void { + assertType('class-string|false', $ref->name ?? false); +} diff --git a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-pre-81.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-pre-81.php new file mode 100644 index 0000000000..a03c000a3b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-pre-81.php @@ -0,0 +1,9 @@ +', $ref->name ?? false); +} diff --git a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-root.php similarity index 90% rename from tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php rename to tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-root.php index 29671650c0..2d027da378 100644 --- a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-root.php @@ -13,7 +13,7 @@ assertType('bool', isset($sometimesDefinedVariable)); assertType('bool', isset($neverDefinedVariable)); -assertType('mixed', $foo ?? false); +assertType('mixed~null', $foo ?? false); $bar = 'abc'; assertType('\'abc\'', $bar ?? false); diff --git a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php similarity index 99% rename from tests/PHPStan/Analyser/data/isset-coalesce-empty-type.php rename to tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php index ef291855ed..ad81a1e6ac 100644 --- a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type.php +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php @@ -1,4 +1,4 @@ -= 7.4 +', rand() ?? false); - assertType('0|string', preg_replace('', '', '') ?? 0); + assertType('0|(lowercase-string&uppercase-string)', preg_replace('', '', '') ?? 0); $foo = new FooCoalesce(); diff --git a/tests/PHPStan/Analyser/data/iterator-iterator.php b/tests/PHPStan/Analyser/nsrt/iterator-iterator.php similarity index 100% rename from tests/PHPStan/Analyser/data/iterator-iterator.php rename to tests/PHPStan/Analyser/nsrt/iterator-iterator.php diff --git a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php new file mode 100644 index 0000000000..038b36f7fd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php @@ -0,0 +1,67 @@ + $foo + */ + public function testDefaultBehavior(Traversable $foo) + { + 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('list', iterator_to_array($foo, false)); + } + + public function testBehaviorOnGenerators(): void + { + $generator1 = static function (): iterable { + yield 0 => 1; + yield true => 2; + yield 2 => 3; + yield null => 4; + }; + $generator2 = static function (): iterable { + yield 0 => 1; + yield 'a' => 2; + yield null => 3; + yield true => 4; + }; + + assertType('array<0|1|2|\'\', 1|2|3|4>', iterator_to_array($generator1())); + assertType('list<1|2|3|4>', iterator_to_array($generator1(), false)); + assertType('array<0|1|\'\'|\'a\', 1|2|3|4>', iterator_to_array($generator2())); + assertType('list<1|2|3|4>', iterator_to_array($generator2(), false)); + } + + public function testOnGeneratorsWithIllegalKeysForArray(): void + { + $illegalGenerator = static function (): iterable { + yield 'a' => 'b'; + yield new stdClass => 'c'; + }; + + assertType('*NEVER*', iterator_to_array($illegalGenerator())); + assertType('list<\'b\'|\'c\'>', iterator_to_array($illegalGenerator(), false)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/json-decode/invalid_type.php b/tests/PHPStan/Analyser/nsrt/json-decode/invalid_type.php new file mode 100644 index 0000000000..4919a83bf9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/json-decode/invalid_type.php @@ -0,0 +1,17 @@ += 8.3 + +namespace JsonValidate; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(string $s): void + { + if (json_validate($s)) { + assertType('non-empty-string', $s); + } else { + assertType('string', $s); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/key-exists.php b/tests/PHPStan/Analyser/nsrt/key-exists.php new file mode 100644 index 0000000000..67c42f6c14 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/key-exists.php @@ -0,0 +1,112 @@ += 8.0 + +namespace KeyExists; + +use function key_exists; +use function PHPStan\Testing\assertType; + +class KeyExists +{ + /** + * @param array $a + * @return void + */ + public function doFoo(array $a, string $key, int $anotherKey): void + { + assertType('false', key_exists(2, $a)); + assertType('bool', key_exists('foo', $a)); + assertType('false', key_exists('2', $a)); + + $a = ['foo' => 2, 3 => 'bar']; + assertType('true', key_exists('foo', $a)); + assertType('true', key_exists(3, $a)); + assertType('true', key_exists('3', $a)); + assertType('false', key_exists(4, $a)); + + if (key_exists($key, $a)) { + assertType("'3'|'foo'", $key); + } + if (key_exists($anotherKey, $a)) { + assertType('3', $anotherKey); + } + + $empty = []; + assertType('false', key_exists('foo', $empty)); + assertType('false', key_exists($key, $empty)); + } + + /** + * @param array $a + * @param array $b + * @param array $c + * @param array-key $key4 + * + * @return void + */ + public function doBar(array $a, array $b, array $c, int $key1, string $key2, int|string $key3, $key4, mixed $key5): void + { + if (key_exists($key1, $a)) { + assertType('int', $key1); + } + if (key_exists($key2, $a)) { + assertType('lowercase-string&numeric-string&uppercase-string', $key2); + } + if (key_exists($key3, $a)) { + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key3); + } + if (key_exists($key4, $a)) { + assertType('(int|(lowercase-string&numeric-string&uppercase-string))', $key4); + } + if (key_exists($key5, $a)) { + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key5); + } + + if (key_exists($key1, $b)) { + assertType('*NEVER*', $key1); + } + if (key_exists($key2, $b)) { + assertType('string', $key2); + } + if (key_exists($key3, $b)) { + assertType('string', $key3); + } + if (key_exists($key4, $b)) { + assertType('string', $key4); + } + if (key_exists($key5, $b)) { + assertType('string', $key5); + } + + if (key_exists($key1, $c)) { + assertType('int', $key1); + } + if (key_exists($key2, $c)) { + assertType('string', $key2); + } + if (key_exists($key3, $c)) { + assertType('(int|string)', $key3); + } + if (key_exists($key4, $c)) { + assertType('(int|string)', $key4); + } + if (key_exists($key5, $c)) { + assertType('(int|string)', $key5); + } + + if (key_exists($key1, [3 => 'foo', 4 => 'bar'])) { + assertType('3|4', $key1); + } + if (key_exists($key2, [3 => 'foo', 4 => 'bar'])) { + assertType("'3'|'4'", $key2); + } + if (key_exists($key3, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key3); + } + if (key_exists($key4, [3 => 'foo', 4 => 'bar'])) { + assertType("(3|4|'3'|'4')", $key4); + } + if (key_exists($key5, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key5); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/key-of-generic.php b/tests/PHPStan/Analyser/nsrt/key-of-generic.php new file mode 100644 index 0000000000..71b05a7d3e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/key-of-generic.php @@ -0,0 +1,40 @@ + + */ +interface Result +{ + /** + * @return key-of|null + */ + public function getKey(); +} + +/** + * @param Result $result + * @param Result $listResult + * @param Result> $mixedResult + * @param Result> $stringKeyResult + * @param Result> $intKeyResult + * @param Result $emptyResult + */ +function test( + Result $result, + Result $listResult, + Result $mixedResult, + Result $stringKeyResult, + Result $intKeyResult, + Result $emptyResult, +): void { + assertType("'j'|'k'|null", $result->getKey()); + assertType('0|1|null', $listResult->getKey()); + assertType('int|string|null', $mixedResult->getKey()); + assertType('string|null', $stringKeyResult->getKey()); + assertType('int|null', $intKeyResult->getKey()); + assertType('null', $emptyResult->getKey()); +} diff --git a/tests/PHPStan/Analyser/data/key-of.php b/tests/PHPStan/Analyser/nsrt/key-of.php similarity index 100% rename from tests/PHPStan/Analyser/data/key-of.php rename to tests/PHPStan/Analyser/nsrt/key-of.php diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php new file mode 100644 index 0000000000..b42f0ec8bd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -0,0 +1,458 @@ + $items + */ +function foo(array $items) { + assertType('list', $items); + if (count($items) === 3) { + assertType('array{int, int, int}', $items); + array_shift($items); + assertType('array{int, int}', $items); + } elseif (count($items) === 0) { + assertType('array{}', $items); + } elseif (count($items) === 5) { + assertType('array{int, int, int, int, int}', $items); + } else { + assertType('non-empty-list', $items); + } + assertType('list', $items); +} + +/** + * @param list $items + */ +function modeCount(array $items, int $mode) { + assertType('list', $items); + if (count($items, $mode) === 3) { + assertType('array{int, int, int}', $items); + array_shift($items); + assertType('array{int, int}', $items); + } elseif (count($items, $mode) === 0) { + assertType('array{}', $items); + } elseif (count($items, $mode) === 5) { + assertType('array{int, int, int, int, int}', $items); + } else { + assertType('non-empty-list', $items); + } + assertType('list', $items); +} + +/** + * @param list $items + */ +function modeCountOnMaybeArray(array $items, int $mode) { + assertType('list|int>', $items); + if (count($items, $mode) === 3) { + assertType('non-empty-list|int>', $items); + array_shift($items); + assertType('list|int>', $items); + } elseif (count($items, $mode) === 0) { + assertType('array{}', $items); + } elseif (count($items, $mode) === 5) { + assertType('non-empty-list|int>', $items); + } else { + assertType('non-empty-list|int>', $items); + } + assertType('list|int>', $items); +} + + +/** + * @param list $items + */ +function normalCount(array $items) { + assertType('list', $items); + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{int, int, int}', $items); + array_shift($items); + assertType('array{int, int}', $items); + } elseif (count($items, COUNT_NORMAL) === 0) { + assertType('array{}', $items); + } elseif (count($items, COUNT_NORMAL) === 5) { + assertType('array{int, int, int, int, int}', $items); + } else { + assertType('non-empty-list', $items); + } + assertType('list', $items); +} + +/** + * @param list $items + */ +function recursiveCountOnMaybeArray(array $items):void { + assertType('list|int>', $items); + if (count($items, COUNT_RECURSIVE) === 3) { + assertType('non-empty-list|int>', $items); + array_shift($items); + assertType('list|int>', $items); + } elseif (count($items, COUNT_RECURSIVE) === 0) { + assertType('array{}', $items); + } elseif (count($items, COUNT_RECURSIVE) === 5) { + assertType('non-empty-list|int>', $items); + } else { + assertType('non-empty-list|int>', $items); + } + assertType('list|int>', $items); +} + +/** + * @param list $items + */ +function normalCountOnMaybeArray(array $items):void { + assertType('list|int>', $items); + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{array|int, array|int, array|int}', $items); + array_shift($items); + assertType('array{array|int, array|int}', $items); + } elseif (count($items, COUNT_NORMAL) === 0) { + assertType('array{}', $items); + } elseif (count($items, COUNT_NORMAL) === 5) { + assertType('array{array|int, array|int, array|int, array|int, array|int}', $items); + } else { + assertType('non-empty-list|int>', $items); + } + assertType('list|int>', $items); +} + +class A {} + +/** + * @param list $items + */ +function cannotCountRecursive($items, int $mode) +{ + if (count($items) === 3) { + assertType('array{ListCount\A, ListCount\A, ListCount\A}', $items); + } + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{ListCount\A, ListCount\A, ListCount\A}', $items); + } + if (count($items, COUNT_RECURSIVE) === 3) { + assertType('array{ListCount\A, ListCount\A, ListCount\A}', $items); + } + if (count($items, $mode) === 3) { + assertType('array{ListCount\A, ListCount\A, ListCount\A}', $items); + } +} + +/** + * @param list> $items + */ +function cannotCountRecursiveNestedArray($items, int $mode) +{ + if (count($items) === 3) { + assertType('array{array, array, array}', $items); + } + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{array, array, array}', $items); + } + if (count($items, COUNT_RECURSIVE) === 3) { + assertType('non-empty-list>', $items); + } + if (count($items, $mode) === 3) { + assertType('non-empty-list>', $items); + } +} + +class CountableFoo implements \Countable +{ + public function count(): int + { + return 3; + } +} + +/** + * @param list $items + */ +function cannotCountRecursiveCountable($items, int $mode) +{ + if (count($items) === 3) { + assertType('array{ListCount\CountableFoo, ListCount\CountableFoo, ListCount\CountableFoo}', $items); + } + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{ListCount\CountableFoo, ListCount\CountableFoo, ListCount\CountableFoo}', $items); + } + if (count($items, COUNT_RECURSIVE) === 3) { + assertType('array{ListCount\CountableFoo, ListCount\CountableFoo, ListCount\CountableFoo}', $items); + } + if (count($items, $mode) === 3) { + assertType('array{ListCount\CountableFoo, ListCount\CountableFoo, ListCount\CountableFoo}', $items); + } +} + +function countCountable(CountableFoo $x, int $mode) +{ + if (count($x) === 3) { + assertType('ListCount\CountableFoo', $x); + } else { + assertType('ListCount\CountableFoo', $x); + } + assertType('ListCount\CountableFoo', $x); + + if (count($x, COUNT_NORMAL) === 3) { + assertType('ListCount\CountableFoo', $x); + } else { + assertType('ListCount\CountableFoo', $x); + } + assertType('ListCount\CountableFoo', $x); + + if (count($x, COUNT_RECURSIVE) === 3) { + assertType('ListCount\CountableFoo', $x); + } else { + assertType('ListCount\CountableFoo', $x); + } + assertType('ListCount\CountableFoo', $x); + + if (count($x, $mode) === 3) { + assertType('ListCount\CountableFoo', $x); + } else { + assertType('ListCount\CountableFoo', $x); + } + assertType('ListCount\CountableFoo', $x); +} + +class CountWithOptionalKeys +{ + /** + * @param array{0: mixed, 1?: string|null} $row + */ + protected function testOptionalKeys($row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 1) { + assertType('array{mixed}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{mixed, string|null}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + } + + /** + * @param array{mixed}|array{0: mixed, 1?: string|null} $row + */ + protected function testOptionalKeysInUnion($row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 1) { + assertType('array{mixed}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{mixed, string|null}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + } + + /** + * @param array{string}|array{0: int, 1?: string|null} $row + */ + protected function testOptionalKeysInListsOfTaggedUnion($row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 1?: string|null}|array{string}', $row); + } + + if (count($row) === 1) { + assertType('array{0: int, 1?: string|null}|array{string}', $row); + } else { + assertType('array{0: int, 1?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{int, string|null}', $row); + } else { + assertType('array{0: int, 1?: string|null}|array{string}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 1?: string|null}|array{string}', $row); + } + } + + /** + * @param array{string}|array{0: int, 3?: string|null} $row + */ + protected function testOptionalKeysInUnionArray($row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } + + if (count($row) === 1) { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } else { + assertType('array{0: int, 3?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{0: int, 3?: string|null}', $row); + } else { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } + } + + /** + * @param array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null} $row + * @param list $listRow + * @param int<2, 3> $twoOrThree + * @param int<2, max> $twoOrMore + * @param int $maxThree + * @param int<10, 11> $tenOrEleven + * @param int<3, 32> $threeOrMoreInRangeLimit + * @param int<3, 512> $threeOrMoreOverRangeLimit + */ + protected function testOptionalKeysInUnionListWithIntRange($row, $listRow, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven, $threeOrMoreInRangeLimit, $threeOrMoreOverRangeLimit): void + { + if (count($row) >= $twoOrThree) { + assertType('array{0: int, 1: string|null, 2?: int|null}', $row); + } else { + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } + + if (count($row) >= $tenOrEleven) { + assertType('*NEVER*', $row); + } else { + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } + + if (count($row) >= $twoOrMore) { + assertType('list{0: int, 1: string|null, 2?: int|null, 3?: float|null}', $row); + } else { + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } + + if (count($row) >= $maxThree) { + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } else { + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } + + if (count($row) >= $threeOrMoreInRangeLimit) { + assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } else { + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } + + if (count($listRow) >= $threeOrMoreInRangeLimit) { + assertType('list{0: string, 1: string, 2: string, 3?: string, 4?: string, 5?: string, 6?: string, 7?: string, 8?: string, 9?: string, 10?: string, 11?: string, 12?: string, 13?: string, 14?: string, 15?: string, 16?: string, 17?: string, 18?: string, 19?: string, 20?: string, 21?: string, 22?: string, 23?: string, 24?: string, 25?: string, 26?: string, 27?: string, 28?: string, 29?: string, 30?: string, 31?: string}', $listRow); + } else { + assertType('list', $listRow); + } + + if (count($row) >= $threeOrMoreOverRangeLimit) { + assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } else { + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } + + if (count($listRow) >= $threeOrMoreOverRangeLimit) { + assertType('non-empty-list', $listRow); + } else { + assertType('list', $listRow); + } + } + + /** + * @param array{string}|array{0: int, 1?: string|null, 2?: int|null, 3?: float|null} $row + * @param int<2, 3> $twoOrThree + */ + protected function testOptionalKeysInUnionArrayWithIntRange($row, $twoOrThree): void + { + if (count($row) >= $twoOrThree) { + assertType('array{0: int, 1: string|null, 2?: int|null}', $row); + } else { + assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}|array{string}', $row); + } + } +} + +class FooBug +{ + public int $totalExpectedRows = 0; + + /** @var list<\stdClass> */ + public array $importedDaySummaryRows = []; + + public function sayHello(): void + { + assertType('int', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + if ($this->totalExpectedRows !== count($this->importedDaySummaryRows)) { + assertType('int', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + } + assertType('int', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + } +} + +class FooBugPositiveInt +{ + /** + * @var positive-int + */ + public int $totalExpectedRows = 1; + + /** @var list<\stdClass> */ + public array $importedDaySummaryRows = []; + + public function sayHello(): void + { + assertType('int<1, max>', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + if ($this->totalExpectedRows !== count($this->importedDaySummaryRows)) { + assertType('int<1, max>', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + } + assertType('int<1, max>', $this->totalExpectedRows); + assertType('list', $this->importedDaySummaryRows); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/list-shapes.php b/tests/PHPStan/Analyser/nsrt/list-shapes.php new file mode 100644 index 0000000000..62313ca8e7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/list-shapes.php @@ -0,0 +1,26 @@ + $list */ + public function directAssertionObjectParamHint($list): void + { + assertType('list', $list); + } + + public function withoutGenerics(): void + { + /** @var list $list */ + $list = []; + $list[] = '1'; + $list[] = true; + $list[] = new \stdClass(); + assertType('non-empty-list', $list); + } + + + public function withMixedType(): void + { + /** @var list $list */ + $list = []; + $list[] = '1'; + $list[] = true; + $list[] = new \stdClass(); + assertType('non-empty-list', $list); + } + + public function withObjectType(): void + { + /** @var list<\DateTime> $list */ + $list = []; + $list[] = new \DateTime(); + assertType('non-empty-list', $list); + } + + /** @return list */ + public function withScalarGoodContent(): void + { + /** @var list $list */ + $list = []; + $list[] = '1'; + $list[] = true; + assertType('non-empty-list', $list); + } + + public function withNumericKey(): void + { + /** @var list $list */ + $list = []; + $list[] = '1'; + $list['1'] = true; + assertType('non-empty-array, mixed>&hasOffsetValue(1, true)', $list); + } + + public function withFullListFunctionality(): void + { + // These won't output errors for now but should when list type will be fully implemented + /** @var list $list */ + $list = []; + assertType('list', $list); + $list[] = '1'; + assertType('non-empty-list', $list); + $list[] = '2'; + assertType('non-empty-list', $list); + unset($list[0]);//break list behaviour + assertType('array, mixed>', $list); + + /** @var list $list2 */ + $list2 = []; + assertType('list', $list2); + $list2[2] = '1';//Most likely to create a gap in indexes + assertType('non-empty-array, mixed>&hasOffsetValue(2, \'1\')', $list2); + } + + /** @param list $list */ + public function testUnset(array $list): void + { + assertType('list', $list); + unset($list[2]); + assertType('array|int<3, max>, int>', $list); + } + + /** @param list $list */ + public function testSetOffsetExplicitlyWithoutGap(array $list): void + { + assertType('list', $list); + $list[0] = 17; + assertType('non-empty-list&hasOffsetValue(0, 17)', $list); + $list[1] = 19; + assertType('non-empty-list&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)', $list); + $list[0] = 21; + assertType('non-empty-list&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)', $list); + } + + /** @param list $list */ + public function testSetOffsetExplicitlyWithGap(array $list): void + { + assertType('list', $list); + $list[0] = 17; + assertType('non-empty-list&hasOffsetValue(0, 17)', $list); + $list[2] = 21; + assertType('non-empty-array, int>&hasOffsetValue(0, 17)&hasOffsetValue(2, 21)', $list); + } + + /** @param list $list */ + function testAppendImmediatelyAfterLastElement(array $list): void + { + assertType('list', $list); + $list[0] = 17; + assertType('non-empty-list&hasOffsetValue(0, 17)', $list); + $list[1] = 19; + assertType('non-empty-list&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)', $list); + $list[2] = 21; + assertType('non-empty-list&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)&hasOffsetValue(2, 21)', $list); + $list[3] = 21; + assertType('non-empty-list&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)&hasOffsetValue(2, 21)&hasOffsetValue(3, 21)', $list); + + // hole in the list -> turns it into a array + + $list[5] = 21; + assertType('non-empty-array, int>&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)&hasOffsetValue(2, 21)&hasOffsetValue(3, 21)&hasOffsetValue(5, 21)', $list); + } + + + /** @param list $list */ + function testKeepListAfterLast(array $list): void + { + if (isset($list[5])) { + assertType('non-empty-list&hasOffsetValue(5, int)', $list); + $list[6] = 21; + assertType('non-empty-list&hasOffsetValue(5, int)&hasOffsetValue(6, 21)', $list); + } + assertType('list', $list); + } + + /** @param list $list */ + function testKeepListAfterLastArrayKey(array $list): void + { + if (array_key_exists(5, $list) && is_int($list[5])) { + assertType('non-empty-list&hasOffsetValue(5, int)', $list); + $list[6] = 21; + assertType('non-empty-list&hasOffsetValue(5, int)&hasOffsetValue(6, 21)', $list); + } + assertType('list', $list); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/literal-string.php b/tests/PHPStan/Analyser/nsrt/literal-string.php new file mode 100644 index 0000000000..c30fbdac80 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/literal-string.php @@ -0,0 +1,92 @@ += 8.0 + +declare(strict_types=1); + +namespace LooseSemanticsPhp8; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + /** + * @param 0 $zero + * @param 'php' $phpStr + * @param '' $emptyStr + */ + public function sayZero( + $zero, + $phpStr, + $emptyStr + ): void + { + assertType('false', $zero == $phpStr); // PHP8+ only + assertType('false', $zero == $emptyStr); // PHP8+ only + } + + /** + * @param 0 $zero + * @param 'php' $phpStr + */ + public function sayPhpStr( + $zero, + $phpStr, + ): void + { + assertType('false', $phpStr == $zero); // PHP8+ only + } + + /** + * @param 0 $zero + * @param '' $emptyStr + */ + public function sayEmptyStr( + $zero, + $emptyStr + ): void + { + assertType('false', $emptyStr == $zero); // PHP8+ only + } + + /** + * @param 'php' $phpStr + * @param '' $emptyStr + * @param int<10, 20> $intRange + */ + public function sayInt( + $emptyStr, + $phpStr, + int $int, + int $intRange + ): void + { + assertType('false', $int == $emptyStr); + assertType('false', $int == $phpStr); + assertType('false', $int == 'a'); + + assertType('false', $intRange == $emptyStr); + assertType('false', $intRange == $phpStr); + assertType('false', $intRange == 'a'); + } + + /** + * @param "abc"|"def" $constNonFalsy + */ + public function sayConstUnion( + $constNonFalsy, + ): void + { + assertType('false', $constNonFalsy == 0); + assertType('false', "" == 0); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php new file mode 100644 index 0000000000..c385548cf5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php @@ -0,0 +1,963 @@ + $positiveIntRange + * @param int<-20, -10> $negativeIntRange + * @param int<-10, 10> $minusTenToTen + */ + public function sayInt( + $true, + $false, + $one, + $zero, + $minusOne, + $oneStr, + $zeroStr, + $minusOneStr, + $plusOneStr, + $null, + $emptyArr, + array $array, + int $int, + int $intRange, + string $emptyStr, + string $phpStr, + int $positiveIntRange, + int $negativeIntRange, + int $minusTenToTen, + ): void + { + assertType('bool', $int == $true); + assertType('bool', $int == $false); + assertType('bool', $int == $one); + assertType('bool', $int == $zero); + assertType('bool', $int == $minusOne); + assertType('bool', $int == $oneStr); + assertType('bool', $int == $zeroStr); + assertType('bool', $int == $minusOneStr); + assertType('bool', $int == $plusOneStr); + assertType('bool', $int == $null); + assertType('false', $int == $emptyArr); + assertType('false', $int == $array); + + assertType('true', $positiveIntRange == $true); + assertType('false', $positiveIntRange == $false); + assertType('false', $positiveIntRange == $one); + assertType('false', $positiveIntRange == $zero); + assertType('false', $positiveIntRange == $minusOne); + assertType('false', $positiveIntRange == $oneStr); + assertType('false', $positiveIntRange == $zeroStr); + assertType('false', $positiveIntRange == $minusOneStr); + assertType('false', $positiveIntRange == $plusOneStr); + assertType('false', $positiveIntRange == $null); + assertType('false', $positiveIntRange == $emptyArr); + assertType('false', $positiveIntRange == $array); + + assertType('true', $negativeIntRange == $true); + assertType('false', $negativeIntRange == $false); + assertType('false', $negativeIntRange == $one); + assertType('false', $negativeIntRange == $zero); + assertType('false', $negativeIntRange == $minusOne); + assertType('false', $negativeIntRange == $oneStr); + assertType('false', $negativeIntRange == $zeroStr); + assertType('false', $negativeIntRange == $minusOneStr); + assertType('false', $negativeIntRange == $plusOneStr); + assertType('false', $negativeIntRange == $null); + assertType('false', $negativeIntRange == $emptyArr); + assertType('false', $negativeIntRange == $array); + + // see https://3v4l.org/VudDK + assertType('bool', $minusTenToTen == $true); + assertType('bool', $minusTenToTen == $false); + assertType('bool', $minusTenToTen == $one); + assertType('bool', $minusTenToTen == $zero); + assertType('bool', $minusTenToTen == $minusOne); + assertType('bool', $minusTenToTen == $oneStr); + assertType('bool', $minusTenToTen == $zeroStr); + assertType('bool', $minusTenToTen == $minusOneStr); + assertType('bool', $minusTenToTen == $plusOneStr); + assertType('bool', $minusTenToTen == $null); + assertType('false', $minusTenToTen == $emptyArr); + assertType('false', $minusTenToTen == $array); + + // see https://3v4l.org/oJl3K + assertType('false', $minusTenToTen < $null); + assertType('bool', $minusTenToTen > $null); + assertType('bool', $minusTenToTen <= $null); + assertType('true', $minusTenToTen >= $null); + + // see https://3v4l.org/oRSgU + assertType('bool', $null < $minusTenToTen); + assertType('false', $null > $minusTenToTen); + assertType('true', $null <= $minusTenToTen); + assertType('bool', $null >= $minusTenToTen); + + assertType('false', 5 == $emptyArr); + assertType('false', $emptyArr == 5); + assertType('false', 5 == $array); + assertType('false', $array == 5); + assertType('false', [] == 5); + assertType('false', 5 == []); + + assertType('false', 5 == $emptyStr); + assertType('false', 5 == $phpStr); + assertType('false', 5 == 'a'); + + assertType('false', $emptyStr == 5); + assertType('false', $phpStr == 5); + assertType('false', 'a' == 5); + } + + /** + * @param true|1|"1" $looseOne + * @param false|0|"0" $looseZero + * @param false|1 $constMix + * @param "abc"|"def" $constNonFalsy + * @param array{abc: string, num?: int, nullable: ?string} $arrShape + * @param array{} $emptyArr + */ + public function sayConstUnion( + $looseOne, + $looseZero, + $constMix, + $constNonFalsy, + array $arrShape, + array $emptyArr + ): void + { + assertType('true', $looseOne == 1); + assertType('false', $looseOne == 0); + assertType('true', $looseOne == true); + assertType('false', $looseOne == false); + assertType('true', $looseOne == "1"); + assertType('false', $looseOne == "0"); + assertType('false', $looseOne == []); + + assertType('false', $looseZero == 1); + assertType('true', $looseZero == 0); + assertType('false', $looseZero == true); + assertType('true', $looseZero == false); + assertType('false', $looseZero == "1"); + assertType('true', $looseZero == "0"); + assertType('bool', $looseZero == []); + + assertType('bool', $constMix == 0); + assertType('bool', $constMix == 1); + assertType('bool', $constMix == true); + assertType('bool', $constMix == false); + assertType('bool', $constMix == "1"); + assertType('bool', $constMix == "0"); + assertType('bool', $constMix == []); + + assertType('true', $looseOne == $looseOne); + assertType('true', $looseZero == $looseZero); + assertType('false', $looseOne == $looseZero); + assertType('false', $looseZero == $looseOne); + assertType('bool', $looseOne == $constMix); + assertType('bool', $constMix == $looseOne); + assertType('bool', $looseZero == $constMix); + assertType('bool', $constMix == $looseZero); + + assertType('false', $constNonFalsy == 1); + assertType('false', $constNonFalsy == null); + assertType('true', $constNonFalsy == true); + assertType('false', $constNonFalsy == false); + assertType('false', $constNonFalsy == "1"); + assertType('false', $constNonFalsy == "0"); + assertType('false', $constNonFalsy == []); + + assertType('false', $emptyArr == $looseOne); + assertType('bool', $emptyArr == $constMix); + assertType('bool', $emptyArr == $looseZero); + + assertType('bool', $arrShape == $looseOne); + assertType('bool', $arrShape == $constMix); + assertType('bool', $arrShape == $looseZero); + } + + /** + * @param uppercase-string $upper + * @param lowercase-string $lower + * @param array{} $emptyArr + * @param non-empty-array $nonEmptyArr + * @param array{abc: string, num?: int, nullable: ?string} $arrShape + * @param int<10, 20> $intRange + */ + public function sayIntersection( + string $upper, + string $lower, + string $s, + array $emptyArr, + array $nonEmptyArr, + array $arr, + array $arrShape, + int $i, + int $intRange, + ): void + { + // https://3v4l.org/q8OP2 + assertType('true', '1e2' == '1E2'); + assertType('false', '1e2' === '1E2'); + + assertType('bool', '' == $upper); + assertType('bool', '0' == $upper); + assertType('false', 'a' == $upper); + assertType('false', 'abc' == $upper); + assertType('false', 'aBc' == $upper); + assertType('bool', '1e2' == $upper); + assertType('bool', strtoupper($s) == $upper); + assertType('bool', strtolower($s) == $upper); + assertType('bool', $upper == $lower); + + assertType('bool', '0' == $lower); + assertType('false', 'A' == $lower); + assertType('false', 'ABC' == $lower); + assertType('false', 'AbC' == $lower); + assertType('bool', '1E2' == $lower); + assertType('bool', strtoupper($s) == $lower); + assertType('bool', strtolower($s) == $lower); + assertType('bool', $lower == $upper); + + assertType('false', $arr == $i); + assertType('false', $nonEmptyArr == $i); + assertType('false', $arr == $intRange); + assertType('false', $nonEmptyArr == $intRange); + assertType('false', $emptyArr == $nonEmptyArr); + assertType('false', $nonEmptyArr == $emptyArr); + assertType('bool', $arr == $nonEmptyArr); + assertType('bool', $nonEmptyArr == $arr); + + assertType('false', 5 == $arr); + assertType('false', $arr == 5); + assertType('false', 5 == $emptyArr); + assertType('false', $emptyArr == 5); + assertType('false', 5 == $nonEmptyArr); + assertType('false', $nonEmptyArr == 5); + assertType('false', 5 == $arrShape); + assertType('false', $arrShape == 5); + if (count($arr) > 0) { + assertType('false', 5 == $arr); + assertType('false', $arr == 5); + } + + assertType('bool', '' == $lower); + if ($lower != '') { + assertType('false', '' == $lower); + } + if ($upper != '') { + assertType('false', '' == $upper); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php new file mode 100644 index 0000000000..3aff9f3389 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php @@ -0,0 +1,22 @@ + $commonStrings + * @param array $lowercaseStrings + */ + public function doFoo(string $s, string $ls, array $commonStrings, array $lowercaseStrings): void + { + assertType('string', implode($s, $commonStrings)); + assertType('string', implode($s, $lowercaseStrings)); + assertType('string', implode($ls, $commonStrings)); + assertType('lowercase-string', implode($ls, $lowercaseStrings)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-pad.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-pad.php new file mode 100644 index 0000000000..79633d7538 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-pad.php @@ -0,0 +1,23 @@ +, user?: lowercase-string, pass?: lowercase-string, path?: lowercase-string, query?: lowercase-string, fragment?: lowercase-string}|false', parse_url(http://github.com/$lowercase)); + assertType('lowercase-string|false|null', parse_url(http://github.com/$lowercase,%20PHP_URL_SCHEME)); + assertType('lowercase-string|false|null', parse_url(http://github.com/$lowercase,%20PHP_URL_HOST)); + assertType('int<0, 65535>|false|null', parse_url(http://github.com/$lowercase,%20PHP_URL_PORT)); + assertType('lowercase-string|false|null', parse_url(http://github.com/$lowercase,%20PHP_URL_USER)); + assertType('lowercase-string|false|null', parse_url(http://github.com/$lowercase,%20PHP_URL_PASS)); + assertType('lowercase-string|false|null', parse_url(http://github.com/$lowercase,%20PHP_URL_PATH)); + assertType('lowercase-string|false|null', parse_url(http://github.com/$lowercase,%20PHP_URL_QUERY)); + assertType('lowercase-string|false|null', parse_url(http://github.com/$lowercase,%20PHP_URL_FRAGMENT)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-parse.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-parse.php new file mode 100644 index 0000000000..ba95e975da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-parse.php @@ -0,0 +1,36 @@ += 8.0 + +namespace LowercaseStringSubstr; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param lowercase-string $lowercase + */ + public function doSubstr(string $lowercase): void + { + assertType('lowercase-string', substr($lowercase, 5)); + assertType('lowercase-string', substr($lowercase, -5)); + assertType('lowercase-string', substr($lowercase, 0, 5)); + } + + /** + * @param lowercase-string $lowercase + */ + public function doMbSubstr(string $lowercase): void + { + assertType('lowercase-string', mb_substr($lowercase, 5)); + assertType('lowercase-string', mb_substr($lowercase, -5)); + assertType('lowercase-string', mb_substr($lowercase, 0, 5)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-trim.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-trim.php new file mode 100644 index 0000000000..e5632e293d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-trim.php @@ -0,0 +1,29 @@ += 8.0 + +namespace MatchExpr; + +use function get_class; +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param 1|2|3|4 $i + */ + public function doFoo(int $i): void + { + assertType('*NEVER*', match ($i) { + 0 => $i, + }); + assertType('1|2|3|4', $i); + assertType('1', match ($i) { + 1 => $i, + }); + assertType('1|2|3|4', $i); + assertType('1|2', match ($i) { + 1, 2 => $i, + }); + assertType('1|2|3|4', $i); + assertType('1|2|3', match ($i) { + 1, 2, 3 => $i, + }); + assertType('1|2|3|4', $i); + assertType('2|3', match ($i) { + 1 => exit(), + 2, 3 => $i, + }); + assertType('1|2|3|4', $i); + } + + /** + * @param 1|2|3|4 $i + */ + public function doBar(int $i): void + { + match ($i) { + 0 => assertType('*NEVER*', $i), + default => assertType('1|2|3|4', $i), + }; + assertType('1|2|3|4', $i); + match ($i) { + 1 => assertType('1', $i), + default => assertType('2|3|4', $i), + }; + assertType('1|2|3|4', $i); + match ($i) { + 1, 2 => assertType('1|2', $i), + default => assertType('3|4', $i), + }; + assertType('1|2|3|4', $i); + match ($i) { + 1, 2, 3 => assertType('1|2|3', $i), + default => assertType('4', $i), + }; + assertType('1|2|3|4', $i); + + match ($i) { + assertType('1|2|3|4', $i), 1, assertType('2|3|4', $i) => null, + assertType('2|3|4', $i) => null, + default => assertType('2|3|4', $i), + }; + } + + public function doGettype(int|float|bool|string|object|array $value): void + { + match (gettype($value)) { + 'integer' => assertType('int', $value), + 'string' => assertType('string', $value), + }; + } + + public function doGettypeUnion(int|float|bool|string|object|array $value): void + { + $intOrString = 'integer'; + if (rand(0, 1)) { + $intOrString = 'string'; + } + match (gettype($value)) { + $intOrString => assertType('int|string', $value), + }; + } + +} + +final class FinalFoo +{ + +} + +final class FinalBar +{ + +} + +class TestGetClass +{ + + public function doMatch(FinalFoo|FinalBar $class): void + { + match (get_class($class)) { + FinalFoo::class => assertType(FinalFoo::class, $class), + FinalBar::class => assertType(FinalBar::class, $class), + }; + + match (get_debug_type($class)) { + FinalFoo::class => assertType(FinalFoo::class, $class), + FinalBar::class => assertType(FinalBar::class, $class), + }; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/match-expression-inference.php b/tests/PHPStan/Analyser/nsrt/match-expression-inference.php new file mode 100644 index 0000000000..3cf020e98c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/match-expression-inference.php @@ -0,0 +1,19 @@ + 'one', + 2 => 'two', + }; + + assertType("'one'", $foo); + + return $foo; + } +} diff --git a/tests/PHPStan/Analyser/data/math.php b/tests/PHPStan/Analyser/nsrt/math.php similarity index 77% rename from tests/PHPStan/Analyser/data/math.php rename to tests/PHPStan/Analyser/nsrt/math.php index 42ac905c67..9d12809783 100644 --- a/tests/PHPStan/Analyser/data/math.php +++ b/tests/PHPStan/Analyser/nsrt/math.php @@ -139,4 +139,40 @@ public function multiplyZero(int $i, float $f, $range): void } + public function never(): void + { + for ($i = 1; $i < count([]); $i++) { + assertType('*NEVER*', $i); + assertType('*NEVER*', --$i); + assertType('*NEVER*', $i--); + assertType('*NEVER*', ++$i); + assertType('*NEVER*', $i++); + + assertType('*NEVER*', $i + 2); + assertType('*NEVER*', 2 + $i); + assertType('*NEVER*', $i - 2); + assertType('*NEVER*', 2 - $i); + assertType('*NEVER*', $i * 2); + assertType('*NEVER*', 2 * $i); + assertType('*NEVER*', $i ** 2); + assertType('*NEVER*', 2 ** $i); + assertType('*NEVER*', $i / 2); + assertType('*NEVER*', 2 / $i); + assertType('*NEVER*', $i % 2); + + assertType('*NEVER*', $i | 2); + assertType('*NEVER*', 2 | $i); + assertType('*NEVER*', $i & 2); + assertType('*NEVER*', 2 & $i); + assertType('*NEVER*', $i ^ 2); + assertType('*NEVER*', 2 ^ $i); + assertType('*NEVER*', $i << 2); + assertType('*NEVER*', 2 << $i); + assertType('*NEVER*', $i >> 2); + assertType('*NEVER*', 2 >> $i); + assertType('*NEVER*', $i <=> 2); + assertType('*NEVER*', 2 <=> $i); + } + } + } diff --git a/tests/PHPStan/Analyser/nsrt/mb-strlen-php83.php b/tests/PHPStan/Analyser/nsrt/mb-strlen-php83.php new file mode 100644 index 0000000000..5bd069c873 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mb-strlen-php83.php @@ -0,0 +1,57 @@ += 8.3 + +declare(strict_types=1); + +namespace MbStrlenPhp83; + +use function PHPStan\Testing\assertType; + +class MbStrlenPhp83 +{ + + /** + * @param non-empty-string $nonEmpty + * @param 'utf-8'|'8bit' $utf8And8bit + * @param 'utf-8'|'foo' $utf8AndInvalidEncoding + * @param '1'|'2'|'5'|'10' $constUnion + * @param 1|2|5|10|123|'1234'|false $constUnionMixed + * @param int|float $intFloat + * @param non-empty-string|int|float $nonEmptyStringIntFloat + * @param ""|false|null $emptyStringFalseNull + * @param ""|bool|null $emptyStringBoolNull + * @param "pass"|"none" $encodingsValidOnlyUntilPhp72 + */ + public function doFoo(int $i, string $s, bool $bool, float $float, $intFloat, $nonEmpty, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull, $constUnion, $constUnionMixed, $utf8And8bit, $utf8AndInvalidEncoding, string $unknownEncoding, $encodingsValidOnlyUntilPhp72) + { + assertType('0', mb_strlen('')); + assertType('5', mb_strlen('hallo')); + assertType('int<0, 1>', mb_strlen($bool)); + assertType('int<1, max>', mb_strlen($i)); + assertType('int<0, max>', mb_strlen($s)); + assertType('int<1, max>', mb_strlen($nonEmpty)); + assertType('int<1, 2>', mb_strlen($constUnion)); + assertType('int<0, 4>', mb_strlen($constUnionMixed)); + assertType('3', mb_strlen(123)); + assertType('1', mb_strlen(true)); + assertType('0', mb_strlen(false)); + assertType('0', mb_strlen(null)); + assertType('1', mb_strlen(1.0)); + assertType('4', mb_strlen(1.23)); + assertType('int<1, max>', mb_strlen($float)); + assertType('int<1, max>', mb_strlen($intFloat)); + assertType('int<1, max>', mb_strlen($nonEmptyStringIntFloat)); + assertType('0', mb_strlen($emptyStringFalseNull)); + assertType('int<0, 1>', mb_strlen($emptyStringBoolNull)); + assertType('8', mb_strlen('паляниця', 'utf-8')); + assertType('11', mb_strlen('alias test🤔', 'utf8')); + assertType('*NEVER*', mb_strlen('', 'invalid encoding')); + assertType('int<5, 6>', mb_strlen('école', $utf8And8bit)); + assertType('5', mb_strlen('école', $utf8AndInvalidEncoding)); + assertType('1|3|5|6', mb_strlen('école', $unknownEncoding)); + assertType('2|4|5|8', mb_strlen('מזגן', $unknownEncoding)); + assertType('6|8|12|13|15|16|24', mb_strlen('いい天気ですね〜', $unknownEncoding)); + assertType('3', mb_strlen(123, $utf8AndInvalidEncoding)); + assertType('*NEVER*', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); + } + +} diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character-php8.php b/tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php similarity index 98% rename from tests/PHPStan/Analyser/data/mb_substitute_character-php8.php rename to tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php index b53353bd3a..2933d4ccab 100644 --- a/tests/PHPStan/Analyser/data/mb_substitute_character-php8.php +++ b/tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php @@ -1,4 +1,4 @@ -= 8.0 \PHPStan\Testing\assertType('\'entity\'|\'long\'|\'none\'|int<0, 55295>|int<57344, 1114111>', mb_substitute_character()); \PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('')); diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character.php b/tests/PHPStan/Analyser/nsrt/mb_substitute_character.php similarity index 98% rename from tests/PHPStan/Analyser/data/mb_substitute_character.php rename to tests/PHPStan/Analyser/nsrt/mb_substitute_character.php index 9dab962ec5..41a921c9f7 100644 --- a/tests/PHPStan/Analyser/data/mb_substitute_character.php +++ b/tests/PHPStan/Analyser/nsrt/mb_substitute_character.php @@ -1,4 +1,4 @@ -|int<57344, 1114111>', mb_substitute_character()); \PHPStan\Testing\assertType('true', mb_substitute_character('')); diff --git a/tests/PHPStan/Analyser/nsrt/memcache-get.php b/tests/PHPStan/Analyser/nsrt/memcache-get.php new file mode 100644 index 0000000000..533e483682 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/memcache-get.php @@ -0,0 +1,14 @@ +get("key1")); + assertType('array|false', $memcache->get(array("key1", "key2", "key3"))); +}; diff --git a/tests/PHPStan/Analyser/nsrt/minmax-arrays.php b/tests/PHPStan/Analyser/nsrt/minmax-arrays.php new file mode 100644 index 0000000000..bd94a81fb3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/minmax-arrays.php @@ -0,0 +1,176 @@ + 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } + if (count($ints) >= 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } + if (count($ints) >= 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int|false', min($ints)); + assertType('int|false', max($ints)); + } + if (count($ints) <= 0) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 1) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 2) { + assertType('int|false', min($ints)); + assertType('int|false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function dummy3(array $ints): void +{ + assertType('int|false', min($ints)); + assertType('int|false', max($ints)); +} + + +function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void +{ + assertType('array{0: DateTimeInterface, 1?: DateTimeInterface}', array_filter([$dateA, $dateB])); + assertType('DateTimeInterface', min(array_filter([$dateA, $dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateA, $dateB]))); + assertType('array{0?: DateTimeInterface}', array_filter([$dateB])); + assertType('DateTimeInterface|false', min(array_filter([$dateB]))); + assertType('DateTimeInterface|false', max(array_filter([$dateB]))); +} + +class HelloWorld +{ + public function unionType(): void + { + /** + * @var array<0|1|2|3|4|5|6|7|8|9> + */ + $numbers = getFoo(); + + assertType('0|1|2|3|4|5|6|7|8|9|false', min($numbers)); + assertType('0', min([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + + assertType('0|1|2|3|4|5|6|7|8|9|false', max($numbers)); + assertType('9', max([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + } +} + +/** + * @param int[] $ints + */ +function countMode(array $ints, int $mode): void +{ + if (count($ints, $mode) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countNormal(array $ints): void +{ + if (count($ints, COUNT_NORMAL) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countRecursive(array $ints): void +{ + if (count($ints, COUNT_RECURSIVE) <= 0) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints, COUNT_RECURSIVE) < 1) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/minmax-php8.php b/tests/PHPStan/Analyser/nsrt/minmax-php8.php new file mode 100644 index 0000000000..2c75f363c9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/minmax-php8.php @@ -0,0 +1,177 @@ += 8.0 + +namespace MinMaxArraysPhp8; + +use function PHPStan\Testing\assertType; + +function dummy(): void +{ + assertType('1', min([1])); + assertType('*ERROR*', min([])); + assertType('1', max([1])); + assertType('*ERROR*', max([])); +} + +/** + * @param int[] $ints + */ +function dummy2(array $ints): void +{ + if (count($ints) === 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) === 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) !== 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) !== 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) >= 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) >= 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) <= 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 1) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function dummy3(array $ints): void +{ + assertType('int', min($ints)); + assertType('int', max($ints)); +} + + +function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void +{ + assertType('array{0: DateTimeInterface, 1?: DateTimeInterface}', array_filter([$dateA, $dateB])); + assertType('DateTimeInterface', min(array_filter([$dateA, $dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateA, $dateB]))); + assertType('array{0?: DateTimeInterface}', array_filter([$dateB])); + assertType('DateTimeInterface', min(array_filter([$dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateB]))); +} + + +class HelloWorld +{ + public function unionType(): void + { + /** + * @var array<0|1|2|3|4|5|6|7|8|9> + */ + $numbers = getFoo(); + + assertType('0|1|2|3|4|5|6|7|8|9', min($numbers)); + assertType('0', min([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + + assertType('0|1|2|3|4|5|6|7|8|9', max($numbers)); + assertType('9', max([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + } +} + +/** + * @param int[] $ints + */ +function countMode(array $ints, int $mode): void +{ + if (count($ints, $mode) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countNormal(array $ints): void +{ + if (count($ints, COUNT_NORMAL) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countRecursive(array $ints): void +{ + if (count($ints, COUNT_RECURSIVE) < 1) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints, COUNT_RECURSIVE) < 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/minmax.php b/tests/PHPStan/Analyser/nsrt/minmax.php new file mode 100644 index 0000000000..d4cbb77c44 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/minmax.php @@ -0,0 +1,66 @@ +|int<1, max>, 1?: int|int<1, max>}', array_filter([$i, $j])); + assertType('array{1: true}', array_filter([false, true])); +} + +function dummy6(string $s, string $t): void { + assertType('array{0?: non-falsy-string, 1?: non-falsy-string}', array_filter([$s, $t])); +} + +class HelloWorld +{ + public function setRange(int $range): void + { + if ($range < 0) { + return; + } + assertType('int<0, 100>', min($range, 100)); + assertType('int<0, 100>', min(100, $range)); + } + + public function setRange2(int $range): void + { + if ($range > 100) { + return; + } + assertType('int<0, 100>', max($range, 0)); + assertType('int<0, 100>', max(0, $range)); + } + + public function boundRange(): void + { + /** + * @var int<1, 6> $range + */ + $range = getFoo(); + + assertType('int<1, 4>', min($range, 4)); + assertType('int<4, 6>', max(4, $range)); + } + + public function unionType(): void + { + /** + * @var array{0, 1, 2}|array{4, 5, 6} $numbers2 + */ + $numbers2 = getFoo(); + + assertType('0|4', min($numbers2)); + assertType('2|6', max($numbers2)); + } +} diff --git a/tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php b/tests/PHPStan/Analyser/nsrt/missing-closure-native-return-typehint.php similarity index 100% rename from tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php rename to tests/PHPStan/Analyser/nsrt/missing-closure-native-return-typehint.php diff --git a/tests/PHPStan/Analyser/nsrt/mixed-subtract.php b/tests/PHPStan/Analyser/nsrt/mixed-subtract.php new file mode 100644 index 0000000000..c544802655 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mixed-subtract.php @@ -0,0 +1,61 @@ += 8.0 + +namespace SubtractMixed; + +use function PHPStan\Testing\assertType; + +/** + * @param int|0.0|''|'0'|array{}|false|null $moreThenFalsy + */ +function subtract(mixed $m, $moreThenFalsy) { + if ($m !== true) { + assertType("mixed~true", $m); + assertType('bool', (bool) $m); // mixed could still contain something truthy + } + if ($m !== false) { + assertType("mixed~false", $m); + assertType('bool', (bool) $m); // mixed could still contain something falsy + } + if (!is_bool($m)) { + assertType('mixed~bool', $m); + assertType('bool', (bool) $m); + } + if (!is_array($m)) { + assertType('mixed~array', $m); + assertType('bool', (bool) $m); + } + + if ($m) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + assertType('true', (bool) $m); + } + if (!$m) { + assertType("0|0.0|''|'0'|array{}|false|null", $m); + assertType('false', (bool) $m); + } + if (!$m) { + if (!is_int($m)) { + assertType("0.0|''|'0'|array{}|false|null", $m); + assertType('false', (bool)$m); + } + if (!is_bool($m)) { + assertType("0|0.0|''|'0'|array{}|null", $m); + assertType('false', (bool)$m); + } + } + + if (!$m || is_int($m)) { + assertType("0.0|''|'0'|array{}|int|false|null", $m); + assertType('bool', (bool) $m); + } + + if ($m !== $moreThenFalsy) { + assertType('mixed', $m); + assertType('bool', (bool) $m); // could be true + } + + if ($m != 0 && !is_array($m) && $m != null && !is_object($m)) { // subtract more types then falsy + assertType("mixed~(0|0.0|''|'0'|array|object|false|null)", $m); + assertType('true', (bool) $m); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/mixed-to-number.php b/tests/PHPStan/Analyser/nsrt/mixed-to-number.php new file mode 100644 index 0000000000..fee2d7bed4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mixed-to-number.php @@ -0,0 +1,47 @@ +', $mixed); + assertType('(float|int)', $mixed + $mixed); + } +} + +function addingAlphabet() { + $a = 'a'; + $a++; + assertType("'b'", $a); +} diff --git a/tests/PHPStan/Analyser/data/mixed-typehint.php b/tests/PHPStan/Analyser/nsrt/mixed-typehint.php similarity index 95% rename from tests/PHPStan/Analyser/data/mixed-typehint.php rename to tests/PHPStan/Analyser/nsrt/mixed-typehint.php index 8d7ce4ad16..5b3c17cbb1 100644 --- a/tests/PHPStan/Analyser/data/mixed-typehint.php +++ b/tests/PHPStan/Analyser/nsrt/mixed-typehint.php @@ -1,4 +1,4 @@ -= 8.0 namespace MixedTypehint; diff --git a/tests/PHPStan/Analyser/data/model-mixin.php b/tests/PHPStan/Analyser/nsrt/model-mixin.php similarity index 100% rename from tests/PHPStan/Analyser/data/model-mixin.php rename to tests/PHPStan/Analyser/nsrt/model-mixin.php diff --git a/tests/PHPStan/Analyser/data/modulo-operator.php b/tests/PHPStan/Analyser/nsrt/modulo-operator.php similarity index 100% rename from tests/PHPStan/Analyser/data/modulo-operator.php rename to tests/PHPStan/Analyser/nsrt/modulo-operator.php diff --git a/tests/PHPStan/Analyser/nsrt/more-type-strings-php8.php b/tests/PHPStan/Analyser/nsrt/more-type-strings-php8.php new file mode 100644 index 0000000000..d81c6e9907 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/more-type-strings-php8.php @@ -0,0 +1,48 @@ += 8.1 + +namespace MoreTypeStringsPhp8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param interface-string $interfaceString + * @param trait-string $traitString + * @param interface-string $genericInterfaceString + * @param trait-string $genericTraitString + * @param enum-string $genericEnumString + * @param enum-string $genericInterfaceEnumString + */ + public function doFoo( + string $interfaceString, + string $traitString, + string $genericInterfaceString, + string $genericTraitString, + string $genericEnumString, + string $genericInterfaceEnumString, + ): void + { + assertType('class-string', $interfaceString); + assertType('class-string', $traitString); + assertType('class-string', $genericInterfaceString); + assertType('string', $genericTraitString); + assertType('class-string', $genericEnumString); + assertType('class-string', $genericInterfaceEnumString); + } + +} + +enum Bar +{ + + case A; + case B; + +} + +interface BuzInterface +{ + +} diff --git a/tests/PHPStan/Analyser/data/more-type-strings.php b/tests/PHPStan/Analyser/nsrt/more-type-strings.php similarity index 100% rename from tests/PHPStan/Analyser/data/more-type-strings.php rename to tests/PHPStan/Analyser/nsrt/more-type-strings.php diff --git a/tests/PHPStan/Analyser/nsrt/more-types.php b/tests/PHPStan/Analyser/nsrt/more-types.php new file mode 100644 index 0000000000..c8ae927b2d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/more-types.php @@ -0,0 +1,56 @@ +', $enumString); + assertType('literal-string&non-empty-string', $nonEmptyLiteralString); + assertType('float|int|int<1, max>|non-falsy-string|true', $nonEmptyScalar); + assertType("0|0.0|''|'0'|false", $emptyScalar); + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $nonEmptyMixed); + assertType('lowercase-string', $lowercaseString); + assertType('lowercase-string&non-empty-string', $nonEmptyLowercaseString); + assertType('uppercase-string', $uppercaseString); + assertType('non-empty-string&uppercase-string', $nonEmptyUppercaseString); + } + +} diff --git a/tests/PHPStan/Analyser/data/multi-assign.php b/tests/PHPStan/Analyser/nsrt/multi-assign.php similarity index 100% rename from tests/PHPStan/Analyser/data/multi-assign.php rename to tests/PHPStan/Analyser/nsrt/multi-assign.php diff --git a/tests/PHPStan/Analyser/nsrt/mysqli-affected-rows.php b/tests/PHPStan/Analyser/nsrt/mysqli-affected-rows.php new file mode 100644 index 0000000000..6f227da9d6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mysqli-affected-rows.php @@ -0,0 +1,15 @@ +query('UPDATE x SET y = 0;'); + assertType('int<-1, max>|numeric-string', $mysqli->affected_rows); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/mysqli-result-num-rows.php b/tests/PHPStan/Analyser/nsrt/mysqli-result-num-rows.php new file mode 100644 index 0000000000..14aa92bd6b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mysqli-result-num-rows.php @@ -0,0 +1,15 @@ +query('SELECT x FROM z;'); + assertType('int<0, max>|numeric-string', $mysqliResult->num_rows); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/mysqli-stmt-affected-rows-and-num-rows.php b/tests/PHPStan/Analyser/nsrt/mysqli-stmt-affected-rows-and-num-rows.php new file mode 100644 index 0000000000..1ab625db4f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mysqli-stmt-affected-rows-and-num-rows.php @@ -0,0 +1,20 @@ +prepare('SELECT x FROM z;'); + $stmt->execute(); + assertType('int<0, max>|numeric-string', $stmt->num_rows); + + $stmt = $mysqli->prepare('DELETE FROM z;'); + $stmt->execute(); + assertType('int<-1, max>|numeric-string', $stmt->affected_rows); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/mysqli_fetch_object.php b/tests/PHPStan/Analyser/nsrt/mysqli_fetch_object.php new file mode 100644 index 0000000000..ceb0c6c78d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mysqli_fetch_object.php @@ -0,0 +1,16 @@ +fetch_object()); + assertType('MysqliFetchObject\MyClass|false|null', $result->fetch_object(MyClass::class)); + + assertType('stdClass|false|null', mysqli_fetch_object($result)); + assertType('MysqliFetchObject\MyClass|false|null', mysqli_fetch_object($result, MyClass::class)); +} + +class MyClass {} + diff --git a/tests/PHPStan/Analyser/nsrt/named-arguments.php b/tests/PHPStan/Analyser/nsrt/named-arguments.php new file mode 100644 index 0000000000..6c30e37f32 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/named-arguments.php @@ -0,0 +1,14 @@ += 8.0 + +namespace NamedArguments; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function array_search() { + $haystack = ['a', 'b', 'c']; + $needle = 'c'; + assertType('2', array_search(strict: true, needle: $needle, haystack: $haystack)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/narrow-cast.php b/tests/PHPStan/Analyser/nsrt/narrow-cast.php new file mode 100644 index 0000000000..5687c0b23a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/narrow-cast.php @@ -0,0 +1,98 @@ + $arr */ +function doFoo(string $x, array $arr): void { + if ((bool) strlen($x)) { + assertType('string', $x); // could be non-empty-string + } else { + assertType('string', $x); + } + assertType('string', $x); + + if ((bool) array_search($x, $arr, true)) { + assertType('non-empty-array', $arr); + } else { + assertType('array', $arr); + } + assertType('string', $x); + + if ((bool) preg_match('~.*~', $x, $matches)) { + assertType('array{string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string}', $matches); +} + +/** @param int<-5, 5> $x */ +function castString($x, string $s, bool $b) { + if ((string) $x) { + assertType('int<-5, 5>', $x); + } else { + assertType('0', $x); + } + + if ((string) $b) { + assertType('true', $b); + } else { + assertType('false', $b); + } + + if ((string) strrchr($s, 'xy')) { + assertType('string', $s); // could be non-empty-string + } else { + assertType('string', $s); + } +} + +/** @param int<-5, 5> $x */ +function castInt($x, string $s, bool $b) { + if ((int) $x) { + assertType('int<-5, -1>|int<1, 5>', $x); + } else { + assertType('0', $x); + } + + if ((int) $b) { + assertType('true', $b); + } else { + assertType('false', $b); + } + + if ((int) $s) { + assertType('string', $s); + } else { + assertType('string', $s); + } + + if ((int) strpos($s, 'xy')) { + assertType('string', $s); + } else { + assertType('string', $s); + } +} + +/** @param int<-5, 5> $x */ +function castFloat($x, string $s, bool $b) { + if ((float) $x) { + assertType('int<-5, 5>', $x); + } else { + assertType('int<-5, 5>', $x); + } + + if ((float) $b) { + assertType('true', $b); + } else { + assertType('false', $b); + } + + if ((float) $s) { + assertType('string', $s); + } else { + assertType("string", $s); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php b/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php new file mode 100644 index 0000000000..8ecf3438e7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php @@ -0,0 +1,111 @@ +, numeric-string} $arr */ + public function nestedArrays(array $arr): void + { + // don't narrow when $arr contains recursive arrays + if (count($arr, COUNT_RECURSIVE) === 3) { + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } else { + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + + if (count($arr, COUNT_NORMAL) === 3) { + assertType("array{string, '', non-empty-string}", $arr); + } else { + assertType("array{array, numeric-string}", $arr); + } + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } + + /** @param array{string, '', non-empty-string}|array $arr */ + public function mixedArrays(array $arr): void + { + if (count($arr, COUNT_NORMAL) === 3) { + assertType("non-empty-array", $arr); // could be array{string, '', non-empty-string}|non-empty-array + } else { + assertType("array", $arr); // could be array{string, '', non-empty-string}|array + } + assertType("array", $arr); // could be array{string, '', non-empty-string}|array + } + + public function arrayIntRangeSize(): void + { + $x = []; + if (rand(0,1)) { + $x[] = 'ab'; + } + if (rand(0,1)) { + $x[] = 'xy'; + } + + if (count($x) === 1) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/native-expressions.php b/tests/PHPStan/Analyser/nsrt/native-expressions.php new file mode 100644 index 0000000000..abe041686a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/native-expressions.php @@ -0,0 +1,56 @@ +|non-empty-string', $a); + assertNativeType('int|string', $a); + if (is_string($a)) { + assertType('non-empty-string', $a); + assertNativeType('string', $a); + } +} + +class Foo{ + public function __construct( + /** @var non-empty-array */ + private array $array + ){ + assertType('non-empty-array', $this->array); + assertNativeType('array', $this->array); + if(count($array) === 0){ + throw new \InvalidArgumentException(); + } + } + + /** + * @param array{a: 'b'} $a + * @return void + */ + public function doUnset(array $a){ + assertType("array{a: 'b'}", $a); + assertNativeType('array', $a); + unset($a['a']); + assertType("array{}", $a); + assertNativeType("array", $a); + } +} + diff --git a/tests/PHPStan/Analyser/data/native-intersection.php b/tests/PHPStan/Analyser/nsrt/native-intersection.php similarity index 100% rename from tests/PHPStan/Analyser/data/native-intersection.php rename to tests/PHPStan/Analyser/nsrt/native-intersection.php diff --git a/tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php b/tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php new file mode 100644 index 0000000000..5d0b8cdeab --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php @@ -0,0 +1,11 @@ +', new \ArrayObject()); + assertType('ArrayObject<*NEVER*, *NEVER*>', new \ArrayObject([])); + assertType('ArrayObject', new \ArrayObject(['key' => 1])); +}; diff --git a/tests/PHPStan/Analyser/nsrt/native-types-first-class-callables.php b/tests/PHPStan/Analyser/nsrt/native-types-first-class-callables.php new file mode 100644 index 0000000000..35b693f916 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/native-types-first-class-callables.php @@ -0,0 +1,87 @@ += 8.1 + +namespace NativeTypesFirstClassCallables; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** @return non-empty-string */ + public function doFoo(): string + { + + } + + /** @return non-empty-string */ + public static function doBar(): string + { + + } + +} + +/** @return non-empty-string */ +function doFooFunction(): string +{ + +} + +class Test +{ + + public function doFoo(): void + { + $foo = new Foo(); + $f = $foo->doFoo(...); + assertType('non-empty-string', $f()); + assertNativeType('string', $f()); + assertType('non-empty-string', ($foo->doFoo(...))()); + assertNativeType('string', ($foo->doFoo(...))()); + + $g = Foo::doBar(...); + assertType('non-empty-string', $g()); + assertNativeType('string', $g()); + + $h = doFooFunction(...); + assertType('non-empty-string', $h()); + assertNativeType('string', $h()); + + $i = $h(...); + assertType('non-empty-string', $i()); + assertNativeType('string', $i()); + + $j = [Foo::class, 'doBar'](...); + assertType('non-empty-string', $j()); + assertNativeType('string', $j()); + } + +} + +class Nullsafe +{ + + /** @var int */ + private $untyped; + + private int $typed; + + /** @return non-empty-string */ + public function doFoo(): string + { + + } + + public function doBar(?self $self): void + { + assertType('non-empty-string|null', $self?->doFoo()); + assertNativeType('string|null', $self?->doFoo()); + + assertType('int|null', $self?->untyped); + assertNativeType('mixed', $self?->untyped); + assertType('int|null', $self?->typed); + assertNativeType('int|null', $self?->typed); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/native-types-ftp-connect-resource.php b/tests/PHPStan/Analyser/nsrt/native-types-ftp-connect-resource.php new file mode 100644 index 0000000000..a932d93286 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/native-types-ftp-connect-resource.php @@ -0,0 +1,18 @@ += 8.1 + +namespace NativeTypesFtpConnect; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(): void + { + $f = ftp_connect('example.com'); + assertType('FTP\Connection|false', $f); + assertNativeType('FTP\Connection|false', $f); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/native-types.php b/tests/PHPStan/Analyser/nsrt/native-types.php new file mode 100644 index 0000000000..6f17a60f5d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/native-types.php @@ -0,0 +1,396 @@ + $array + */ + public function doForeach(array $array): void + { + assertType('array', $array); + assertNativeType('array', $array); + + foreach ($array as $key => $value) { + assertType('non-empty-array', $array); + assertNativeType('non-empty-array', $array); + + assertType('string', $key); + assertNativeType('(int|string)', $key); + + assertType('int', $value); + assertNativeType('mixed', $value); + } + } + + /** + * @param self $foo + */ + public function doCatch($foo): void + { + assertType(Foo::class, $foo); + assertNativeType('mixed', $foo); + + try { + throw new \Exception(); + } catch (\InvalidArgumentException $foo) { + assertType(\InvalidArgumentException::class, $foo); + assertNativeType(\InvalidArgumentException::class, $foo); + } catch (\Exception $e) { + assertType('Exception~InvalidArgumentException', $e); + assertNativeType('Exception~InvalidArgumentException', $e); + + assertType(Foo::class, $foo); + assertNativeType('mixed', $foo); + } + } + + /** + * @param array $array + */ + public function doForeachArrayDestructuring(array $array) + { + assertType('array', $array); + assertNativeType('array', $array); + foreach ($array as $key => [$i, $s]) { + assertType('non-empty-array', $array); + assertNativeType('non-empty-array', $array); + + assertType('string', $key); + assertNativeType('(int|string)', $key); + + assertType('int', $i); + // assertNativeType('mixed', $i); + + assertType('string', $s); + // assertNativeType('mixed', $s); + } + } + + /** + * @param \DateTimeImmutable $date + */ + public function doIfElse(\DateTimeInterface $date): void + { + if ($date instanceof \DateTimeInterface) { + assertType(\DateTimeImmutable::class, $date); + assertNativeType(\DateTimeInterface::class, $date); + } else { + assertType('*NEVER*', $date); + assertNativeType('*NEVER*', $date); + } + + assertType(\DateTimeImmutable::class, $date); + assertNativeType(\DateTimeInterface::class, $date); + + if ($date instanceof \DateTimeImmutable) { + assertType(\DateTimeImmutable::class, $date); + assertNativeType(\DateTimeImmutable::class, $date); + } else { + assertType('*NEVER*', $date); + assertNativeType('DateTime', $date); + } + + assertType(\DateTimeImmutable::class, $date); + assertNativeType(\DateTimeImmutable::class, $date); // could be DateTimeInterface + + if ($date instanceof \DateTime) { + + } + } + + public function declareStrictTypes(array $array): void + { + /** @var array $array */ + assertType('array', $array); + assertNativeType('array', $array); + + declare(strict_types=1); + assertType('array', $array); + assertNativeType('array', $array); + } + + public function arrowFunction(array $array): void + { + /** @var array $array */ + assertType('array', $array); + assertNativeType('array', $array); + + (fn () => assertNativeType('array', $array))(); + } + + public function closuresUsingCallMethod(array $array, object $object): void + { + /** @var \stdClass $object */ + assertType('$this(NativeTypes\Foo)', $this); + assertNativeType('$this(NativeTypes\Foo)', $this); + + /** @var array $array */ + assertType('array', $array); + assertNativeType('array', $array); + + (function () use ($array) { + assertType('stdClass', $this); + assertNativeType('object', $this); + + assertType('array', $array); + assertNativeType('array', $array); + })->call($object); + + assertType('$this(NativeTypes\Foo)', $this); + assertNativeType('$this(NativeTypes\Foo)', $this); + } + + public function closureBind(array $array, object $object): void + { + /** @var \stdClass $object */ + assertType('$this(NativeTypes\Foo)', $this); + assertNativeType('$this(NativeTypes\Foo)', $this); + + /** @var array $array */ + assertType('array', $array); + assertNativeType('array', $array); + + \Closure::bind(function () use ($array) { + assertType('stdClass', $this); + assertNativeType('object', $this); + + assertType('array', $array); + assertNativeType('array', $array); + }, $object); + + assertType('$this(NativeTypes\Foo)', $this); + assertNativeType('$this(NativeTypes\Foo)', $this); + } + +} + +/** + * @param Foo $foo + * @param \DateTimeImmutable $dateTime + * @param \DateTimeImmutable $dateTimeMutable + * @param string $nullableString + * @param string|null $nonNullableString + */ +function fooFunction( + $foo, + \DateTimeInterface $dateTime, + \DateTime $dateTimeMutable, + ?string $nullableString, + string $nonNullableString +): void +{ + assertType(Foo::class, $foo); + assertNativeType('mixed', $foo); + + assertType(\DateTimeImmutable::class, $dateTime); + assertNativeType(\DateTimeInterface::class, $dateTime); + + assertType(\DateTime::class, $dateTimeMutable); + assertNativeType(\DateTime::class, $dateTimeMutable); + + assertType('string|null', $nullableString); + assertNativeType('string|null', $nullableString); + + assertType('string', $nonNullableString); + assertNativeType('string', $nonNullableString); +} + +function phpDocDoesNotInfluenceExistingNativeType(): void +{ + $array = []; + + assertType('array{}', $array); + assertNativeType('array{}', $array); + + /** @var array $array */ + assertType('array', $array); + assertNativeType('array{}', $array); +} + +class NativeStaticCall +{ + + public function doFoo() + { + assertType('non-empty-string', self::doBar()); + assertNativeType('string', self::doBar()); + + $s = new self(); + assertType('non-empty-string', $s::doBar()); + assertNativeType('string', $s::doBar()); + } + + /** @return non-empty-string */ + public static function doBar(): string + { + + } + +} + +class TypedProperties +{ + + /** @var int */ + private $untyped; + + private int $typed; + + /** @var int */ + private static $untypedStatic; + + private static int $typedStatic; + + public function doFoo(): void + { + assertType('int', $this->untyped); + assertNativeType('mixed', $this->untyped); + assertType('int', $this->typed); + assertNativeType('int', $this->typed); + assertType('int', self::$untypedStatic); + assertNativeType('mixed', self::$untypedStatic); + assertType('int', self::$typedStatic); + assertNativeType('int', self::$typedStatic); + } + +} + +/** @return non-empty-string */ +function funcWithANativeReturnType(): string +{ + +} + +class TestFuncWithANativeReturnType +{ + + public function doFoo(): void + { + assertType('non-empty-string', funcWithANativeReturnType()); + assertNativeType('string', funcWithANativeReturnType()); + + $f = function (): string { + return funcWithANativeReturnType(); + }; + + assertType('non-empty-string', $f()); + assertNativeType('string', $f()); + + assertType('non-empty-string', (function (): string { + return funcWithANativeReturnType(); + })()); + assertNativeType('string', (function (): string { + return funcWithANativeReturnType(); + })()); + + $g = fn () => funcWithANativeReturnType(); + + assertType('non-empty-string', $g()); + assertNativeType('string', $g()); + + assertType('non-empty-string', (fn () => funcWithANativeReturnType())()); + assertNativeType('string', (fn () => funcWithANativeReturnType())()); + } + +} + +class TestPhp8Stubs +{ + + public function doFoo(): void + { + $a = array_replace([1, 2, 3], [4, 5, 6]); + assertType('non-empty-array<0|1|2, 1|2|3|4|5|6>', $a); + assertNativeType('array', $a); + } + +} + +class PositiveInt +{ + + /** + * @param positive-int $i + * @return void + */ + public function doFoo(int $i): void + { + assertType('true', $i > 0); + assertType('false', $i <= 0); + assertNativeType('bool', $i > 0); + assertNativeType('bool', $i <= 0); + } + +} diff --git a/tests/PHPStan/Analyser/data/nested-generic-incomplete-constructor.php b/tests/PHPStan/Analyser/nsrt/nested-generic-incomplete-constructor.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-incomplete-constructor.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-incomplete-constructor.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-types-unwrapping-covariant.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping-covariant.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-types-unwrapping-covariant.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping-covariant.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-types-unwrapping.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-types-unwrapping.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-types.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-types.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-types.php diff --git a/tests/PHPStan/Analyser/data/never.php b/tests/PHPStan/Analyser/nsrt/never.php similarity index 87% rename from tests/PHPStan/Analyser/data/never.php rename to tests/PHPStan/Analyser/nsrt/never.php index d09728b2f3..d57a16e5cb 100644 --- a/tests/PHPStan/Analyser/data/never.php +++ b/tests/PHPStan/Analyser/nsrt/never.php @@ -14,7 +14,7 @@ public function doFoo(): never public function doBar() { - assertType('*NEVER*', $this->doFoo()); + assertType('never', $this->doFoo()); } public function doBaz(?int $i) diff --git a/tests/PHPStan/Analyser/data/new-in-initializers.php b/tests/PHPStan/Analyser/nsrt/new-in-initializers.php similarity index 100% rename from tests/PHPStan/Analyser/data/new-in-initializers.php rename to tests/PHPStan/Analyser/nsrt/new-in-initializers.php diff --git a/tests/PHPStan/Analyser/nsrt/no-named-arguments.php b/tests/PHPStan/Analyser/nsrt/no-named-arguments.php new file mode 100644 index 0000000000..d16a28a5dd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/no-named-arguments.php @@ -0,0 +1,55 @@ +', $args); + assertNativeType('list', $args); +} + +class Baz extends Foo implements Bar +{ + /** + * @no-named-arguments + */ + public function noNamedArgumentsInMethod(float ...$args) + { + assertType('list', $args); + assertNativeType('list', $args); + } + + public function noNamedArgumentsInParent(float ...$args) + { + assertType('list', $args); + assertNativeType('list', $args); + } + + public function noNamedArgumentsInInterface(float ...$args) + { + assertType('list', $args); + assertNativeType('list', $args); + } +} + +abstract class Foo +{ + /** + * @no-named-arguments + */ + abstract public function noNamedArgumentsInParent(float ...$args); +} + +interface Bar +{ + /** + * @no-named-arguments + */ + public function noNamedArgumentsInInterface(); +} diff --git a/tests/PHPStan/Analyser/data/non-empty-array-key-type.php b/tests/PHPStan/Analyser/nsrt/non-empty-array-key-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/non-empty-array-key-type.php rename to tests/PHPStan/Analyser/nsrt/non-empty-array-key-type.php diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-array.php b/tests/PHPStan/Analyser/nsrt/non-empty-array.php new file mode 100644 index 0000000000..d28aad3556 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/non-empty-array.php @@ -0,0 +1,60 @@ + $arrayOfStrings + * @param non-empty-list<\stdClass> $listOfStd + * @param non-empty-list<\stdClass> $listOfStd2 + * @param non-empty-list $invalidList + */ + public function doFoo( + array $array, + array $list, + array $arrayOfStrings, + array $listOfStd, + $listOfStd2, + array $invalidList, + $invalidList2 + ): void + { + assertType('non-empty-array', $array); + assertType('non-empty-list', $list); + assertType('non-empty-array', $arrayOfStrings); + assertType('non-empty-list', $listOfStd); + assertType('non-empty-list', $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_replace($array)); + assertType('non-empty-array', array_replace([], $array)); + assertType('non-empty-array', array_replace($array, [])); + assertType('non-empty-array', array_replace($array, $array)); + + assertType('non-empty-array<(int|string)>', 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/nsrt/non-empty-string-replace-functions.php similarity index 80% rename from tests/PHPStan/Analyser/data/non-empty-string-replace-functions.php rename to tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php index f9a763598f..a774b4eba4 100644 --- a/tests/PHPStan/Analyser/data/non-empty-string-replace-functions.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php @@ -23,4 +23,12 @@ public function replace(string $search, string $replacement, string $subject){ assertType('non-empty-string', substr_replace($subject, $replacement, 1)); assertType('non-empty-string', substr_replace($subject, $replacement, -1)); } -} \ No newline at end of file + + function foo(float $f) { + $s = (string) $f; + assertType('numeric-string&uppercase-string', $s); + + $price = str_replace(',', '.', $s); + assertType('non-empty-string&uppercase-string', $price); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php new file mode 100644 index 0000000000..19482b7fd0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php @@ -0,0 +1,198 @@ += 8.0 + +namespace NonEmptyStringSubstr; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @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)); + } + + /** + * @param non-empty-string $nonEmpty + * @param positive-int $positiveInt + * @param 1|2|3 $postiveRange + * @param -1|-2|-3 $negativeRange + */ + public function doMbSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $negativeRange): void + { + assertType('string', mb_substr($s, 5)); + + assertType('string', mb_substr($s, -5)); + assertType('non-empty-string', mb_substr($nonEmpty, -5)); + assertType('non-empty-string', mb_substr($nonEmpty, $negativeRange)); + + assertType('string', mb_substr($s, 0, 5)); + assertType('non-empty-string', mb_substr($nonEmpty, 0, 5)); + assertType('non-empty-string', mb_substr($nonEmpty, 0, $postiveRange)); + + assertType('string', mb_substr($nonEmpty, 0, -5)); + + assertType('string', mb_substr($s, 0, $positiveInt)); + assertType('non-empty-string', mb_substr($nonEmpty, 0, $positiveInt)); + + assertType('lowercase-string&non-empty-string', mb_substr("déjà_vu", 0, $positiveInt)); + assertType("'déjà_vu'", mb_substr("déjà_vu", 0)); + assertType("'déj'", mb_substr("déjà_vu", 0, 3)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php new file mode 100644 index 0000000000..c8031310ae --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -0,0 +1,464 @@ + 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); + } + + /** + * @param literal-string $s + */ + public function doBar6($s): void + { + if (1 === strlen($s)) { + assertType('literal-string&non-empty-string', $s); + return; + } + assertType('literal-string', $s); + } + + /** + * @param literal-string $s + */ + public function doBar7($s): void + { + if (0 < strlen($s)) { + assertType('literal-string&non-empty-string', $s); + return; + } + assertType("''", $s); + } + + public function doFoo3(string $s): void + { + if ($s) { + assertType('non-falsy-string', $s); + } else { + assertType('\'\'|\'0\'', $s); + } + } + + /** + * @param non-empty-string $s + */ + public function doFoo4(string $s): void + { + assertType('non-empty-list', 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-falsy-string', $s); + } + + public function doEmpty2(string $s): void + { + if (!empty($s)) { + assertType('non-falsy-string', $s); + } + } + +} + +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-falsy-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-falsy-string', implode(' ', $nonEmptyArrayWithNonEmptyStrings)); + assertType('non-empty-string', implode($nonEmptyArrayWithNonEmptyStrings)); + } + + public function sayHello(int $i): void + { + // coming from issue #5291 + $s = array(1, $i); + + assertType('lowercase-string&non-falsy-string', implode("a", $s)); + assertType('non-falsy-string&uppercase-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('lowercase-string&non-falsy-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-falsy-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&hasOffsetValue(\'\', 2)', $a); + } + +} + +class MoreNonEmptyStringFunctions +{ + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param '1'|'2'|'5'|'10' $constUnion + */ + public function doFoo(string $s, string $nonEmpty, string $nonFalsy, 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('uppercase-string', strtoupper($s)); + assertType('non-empty-string&uppercase-string', strtoupper($nonEmpty)); + assertType('lowercase-string', strtolower($s)); + assertType('lowercase-string&non-empty-string', strtolower($nonEmpty)); + assertType('uppercase-string', mb_strtoupper($s)); + assertType('non-empty-string&uppercase-string', mb_strtoupper($nonEmpty)); + assertType('lowercase-string', mb_strtolower($s)); + assertType('lowercase-string&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('string', htmlspecialchars($s, ENT_SUBSTITUTE)); + assertType('string', htmlspecialchars($s, 0)); + assertType('non-empty-string', htmlspecialchars($nonEmpty)); + assertType('non-empty-string', htmlspecialchars($nonEmpty, ENT_SUBSTITUTE)); + assertType('string', htmlspecialchars($nonEmpty, 0)); + assertType('string', htmlentities($s)); + assertType('string', htmlentities($s, ENT_SUBSTITUTE)); + assertType('string', htmlentities($s, 0)); + assertType('non-empty-string', htmlentities($nonEmpty)); + assertType('non-empty-string', htmlentities($nonEmpty, ENT_SUBSTITUTE)); + assertType('string', htmlentities($nonEmpty, 0)); + + 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('string', sprintf($nonEmpty)); + assertType('string', sprintf($s, $nonEmpty)); + assertType('string', sprintf($nonEmpty, $s)); + assertType('string', sprintf($s, $nonFalsy)); + assertType('string', sprintf($nonFalsy, $s)); + assertType('non-empty-string', sprintf($nonEmpty, $nonEmpty)); + assertType('non-empty-string', sprintf($nonEmpty, $nonEmpty, $nonEmpty)); + assertType('non-empty-string', sprintf($nonEmpty, $nonFalsy, $nonFalsy)); + assertType('non-empty-string', sprintf($nonFalsy, $nonEmpty)); + assertType('non-empty-string', sprintf($nonFalsy, $nonEmpty, $nonEmpty)); + assertType('non-empty-string', sprintf($nonFalsy, $nonFalsy, $nonEmpty)); + assertType('non-empty-string', sprintf($nonFalsy, $nonFalsy, $nonFalsy)); + assertType('string', vsprintf($s, [])); + assertType('string', vsprintf($nonEmpty, [])); + assertType('non-empty-string', vsprintf($nonEmpty, [$nonEmpty])); + assertType('non-empty-string', vsprintf($nonEmpty, [$nonEmpty, $nonEmpty])); + assertType('non-empty-string', vsprintf($nonEmpty, [$nonFalsy, $nonFalsy])); + assertType('non-empty-string', vsprintf($nonFalsy, [$nonEmpty])); + assertType('non-empty-string', vsprintf($nonFalsy, [$nonEmpty, $nonEmpty])); + assertType('non-empty-string', vsprintf($nonFalsy, [$nonFalsy, $nonEmpty])); + assertType('non-empty-string', vsprintf($nonFalsy, [$nonFalsy, $nonFalsy])); + + assertType('non-empty-string', sprintf("%s0%s", $s, $s)); + assertType('non-empty-string', sprintf("%s0%s%s%s%s", $s, $s, $s, $s, $s)); + assertType('non-empty-string', sprintf("%s0%s%s%s%s%s", $s, $s, $s, $s, $s, $s)); + + 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)); + } + + function multiplesPrintfFormats(string $s) { + $maybeNonEmpty = '%s'; + $maybeNonFalsy = '%s'; + $nonEmpty = '%s0'; + $nonFalsy = '%sAA'; + + if (rand(0,1)) { + $maybeNonEmpty = '%s0'; + $maybeNonFalsy = '%sAA'; + $nonEmpty = '0%s'; + $nonFalsy = 'AA%s'; + } + + assertType('string', sprintf($maybeNonEmpty, $s)); + assertType('string', sprintf($maybeNonFalsy, $s)); + assertType('non-empty-string', sprintf($nonEmpty, $s)); + assertType('non-falsy-string', sprintf($nonFalsy, $s)); + } + + function subtract($m) { + if ($m) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + assertType('non-falsy-string', (string) $m); + } + if ($m != '') { + assertType("mixed~(''|false|null)", $m); + assertType('non-empty-string', (string) $m); + } + if ($m !== '') { + assertType("mixed~''", $m); + assertType('string', (string) $m); + } + if (!is_string($m)) { + assertType("mixed~string", $m); + assertType('string', (string) $m); + } + + if ($m !== true) { + assertType("mixed~true", $m); + assertType('string', (string) $m); + } + if ($m !== false) { + assertType("mixed~false", $m); + assertType('string', (string) $m); + } + if ($m !== false && $m !== '' && $m !== null) { + assertType("mixed~(''|false|null)", $m); + assertType('non-empty-string', (string) $m); + } + if (!is_bool($m) && $m !== '' && $m !== null) { + assertType("mixed~(''|bool|null)", $m); + assertType('non-empty-string', (string) $m); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php new file mode 100644 index 0000000000..5e7e05f229 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -0,0 +1,168 @@ +|int<1, max>', (int) $nonFalseyString); + // truthy-string is an alias for non-falsy-string + assertType('non-falsy-string', $truthyString); + } + + function removeZero(string $s) { + if ($s === '0') { + return; + } + + assertType('string', $s); + } + + /** + * @param non-empty-string $nonEmpty + */ + public function doBar5(string $s, $nonEmpty): void + { + if (2 <= strlen($s)) { + assertType('non-falsy-string', $s); + } + assertType('string', $s); + + if (3 === strlen($s)) { + assertType('non-falsy-string', $s); + } + assertType('string', $s); + + if (2 <= strlen($nonEmpty)) { + assertType('non-falsy-string', $nonEmpty); + } + } + + /** + * @param numeric-string $numericS + * @param non-falsy-string $nonFalsey + * @param non-empty-string $nonEmpty + * @param literal-string $literalString + */ + function concat(string $s, string $nonFalsey, $numericS, $nonEmpty, $literalString): void + { + assertType('non-falsy-string', $nonFalsey . ''); + assertType('non-falsy-string', $nonFalsey . $s); + + assertType('non-falsy-string', $nonFalsey . $nonEmpty); + assertType('non-falsy-string', $nonEmpty . $nonFalsey); + + assertType('non-falsy-string', $nonEmpty . $nonEmpty); + + assertType('non-falsy-string', $nonFalsey . $literalString); + assertType('non-falsy-string', $literalString . $nonFalsey); + + assertType('non-falsy-string', $nonFalsey . $numericS); + assertType('non-falsy-string', $numericS . $nonFalsey); + + assertType('non-falsy-string', $nonEmpty . $numericS); + assertType('non-falsy-string', $numericS . $nonEmpty); + } + + /** + * @param non-falsy-string $nonFalsey + * @param non-empty-array $arrayOfNonFalsey + * @param non-empty-array $nonEmptyArray + */ + function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArray, array $arr) + { + assertType('string', implode($nonFalsey, [])); + assertType('non-falsy-string', implode($nonFalsey, $nonEmptyArray)); + assertType('non-falsy-string', implode($nonFalsey, $arrayOfNonFalsey)); + assertType('non-falsy-string', implode($s, $arrayOfNonFalsey)); + + assertType('non-falsy-string', addslashes($nonFalsey)); + assertType('non-falsy-string', addcslashes($nonFalsey)); + + assertType('non-falsy-string', escapeshellarg($nonFalsey)); + assertType('non-falsy-string', escapeshellcmd($nonFalsey)); + + assertType('non-falsy-string&uppercase-string', strtoupper($s ?: 1)); + assertType('non-falsy-string&uppercase-string', strtoupper($nonFalsey)); + assertType('lowercase-string&non-falsy-string', strtolower($nonFalsey)); + assertType('non-falsy-string&uppercase-string', mb_strtoupper($nonFalsey)); + assertType('lowercase-string&non-falsy-string', mb_strtolower($nonFalsey)); + assertType('non-falsy-string', lcfirst($nonFalsey)); + assertType('non-falsy-string', ucfirst($nonFalsey)); + assertType('non-falsy-string', ucwords($nonFalsey)); + assertType('non-falsy-string', htmlspecialchars($nonFalsey)); + assertType('non-falsy-string', htmlspecialchars($nonFalsey, ENT_SUBSTITUTE)); + assertType('string', htmlspecialchars($nonFalsey, 0)); + assertType('non-falsy-string', htmlentities($nonFalsey)); + assertType('non-falsy-string', htmlentities($nonFalsey, ENT_SUBSTITUTE)); + assertType('string', htmlentities($nonFalsey, 0)); + + assertType('non-falsy-string', urlencode($nonFalsey)); + assertType('non-falsy-string', urldecode($nonFalsey)); + assertType('non-falsy-string', rawurlencode($nonFalsey)); + assertType('non-falsy-string', rawurldecode($nonFalsey)); + + assertType('non-falsy-string', preg_quote($nonFalsey)); + + assertType('string', sprintf($nonFalsey)); + assertType("'foo'", sprintf('foo')); + assertType("string", sprintf(...$arr)); + assertType("string", sprintf('%s', ...$arr)); + + // empty array only works as long as no placeholder in the pattern + assertType('string', vsprintf($nonFalsey, [])); + assertType('string', vsprintf($nonFalsey, [])); + assertType("string", vsprintf('foo', [])); + + assertType("string", vsprintf('%s', ...$arr)); + assertType("string", vsprintf(...$arr)); + assertType('non-falsy-string', vsprintf('%sAA%s', [$s, $s])); + assertType('non-falsy-string', vsprintf('%d%d', [$s, $s])); // could be non-falsy-string&numeric-string + + assertType('non-falsy-string', sprintf("%sAA%s", $s, $s)); + assertType('non-falsy-string', sprintf("%d%d", $s, $s)); // could be non-falsy-string&numeric-string + assertType('non-falsy-string', sprintf("%sAA%s%s%s%s", $s, $s, $s, $s, $s)); + assertType('non-falsy-string', sprintf("%sAA%s%s%s%s%s", $s, $s, $s, $s, $s, $s)); + + assertType('int<1, max>', strlen($nonFalsey)); + + assertType('non-falsy-string', str_pad($nonFalsey, 0)); + assertType('non-falsy-string', str_repeat($nonFalsey, 1)); + + } + + /** + * @param non-falsy-string $nonFalsey + * @param positive-int $positiveInt + * @param 1|2|3 $postiveRange + * @param -1|-2|-3 $negativeRange + */ + public function doSubstr($nonFalsey, $positiveInt, $postiveRange, $negativeRange): void + { + assertType('non-falsy-string', substr($nonFalsey, -5)); + assertType('non-falsy-string', substr($nonFalsey, $negativeRange)); + + assertType('non-falsy-string', substr($nonFalsey, 0, 5)); + assertType('non-empty-string', substr($nonFalsey, 0, $postiveRange)); + + assertType('non-empty-string', substr($nonFalsey, 0, $positiveInt)); + } + + function numericIntoFalsy(string $s): void + { + if (is_numeric($s)) { + assertType('numeric-string', $s); + + if ('0' !== $s) { + assertType('non-falsy-string&numeric-string', $s); + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/nullable-closure-parameter.php b/tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php similarity index 95% rename from tests/PHPStan/Analyser/data/nullable-closure-parameter.php rename to tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php index 86eb0c1e87..0758feb78c 100644 --- a/tests/PHPStan/Analyser/data/nullable-closure-parameter.php +++ b/tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php @@ -1,4 +1,4 @@ -= 7.4 +getTimestamp() > 0 ? $date->format('j') : ''; + echo $date?->getTimestamp() > 0 ? $date->format('j') : ''; + assertType('DateTimeImmutable|null', $date); + } + + public function bbb(?\DateTimeImmutable $date): void + { + echo 0 < $date?->getTimestamp() ? $date->format('j') : ''; + echo 0 < $date?->getTimestamp() ? $date->format('j') : ''; + assertType('DateTimeImmutable|null', $date); + } + + /** @param mixed $date */ + public function ccc($date): void + { + if ($date?->getTimestamp() > 0) { + assertType('mixed~null', $date); + } + + echo $date?->getTimestamp() > 0 ? $date->format('j') : ''; + echo $date?->getTimestamp() > 0 ? $date->format('j') : ''; + assertType('mixed', $date); + } +} diff --git a/tests/PHPStan/Analyser/data/nullsafe.php b/tests/PHPStan/Analyser/nsrt/nullsafe.php similarity index 95% rename from tests/PHPStan/Analyser/data/nullsafe.php rename to tests/PHPStan/Analyser/nsrt/nullsafe.php index fcb27c2ebd..ed4b00481a 100644 --- a/tests/PHPStan/Analyser/data/nullsafe.php +++ b/tests/PHPStan/Analyser/nsrt/nullsafe.php @@ -99,4 +99,11 @@ public function doDolor(?self $self) assertType('Nullsafe\Foo|null', $self?->nullableSelf); } + public function doNull(): void + { + $null = null; + assertType('null', $null?->foo); + assertType('null', $null?->doFoo()); + } + } diff --git a/tests/PHPStan/Analyser/data/number_format.php b/tests/PHPStan/Analyser/nsrt/number_format.php similarity index 100% rename from tests/PHPStan/Analyser/data/number_format.php rename to tests/PHPStan/Analyser/nsrt/number_format.php diff --git a/tests/PHPStan/Analyser/nsrt/object-shape.php b/tests/PHPStan/Analyser/nsrt/object-shape.php new file mode 100644 index 0000000000..89d0f80654 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/object-shape.php @@ -0,0 +1,204 @@ +foo); + assertType('int', $o->bar); + assertType('*ERROR*', $o->baz); + } + + /** + * @param object{foo: self, bar: int, baz?: string} $o + */ + public function doFoo2(object $o): void + { + assertType('object{foo: ObjectShape\Foo, bar: int, baz?: string}', $o); + } + + public function doBaz(): void + { + assertType('object{}&stdClass', (object) []); + + $a = ['bar' => 2]; + if (rand(0, 1)) { + $a['foo'] = 1; + } + + assertType('object{bar: 2, foo?: 1}&stdClass', (object) $a); + } + + /** + * @template T + * @param object{foo: int, bar: T} $o + * @return T + */ + public function generics(object $o) + { + + } + + public function testGenerics() + { + $o = (object) ['foo' => 1, 'bar' => new \Exception()]; + assertType('object{foo: 1, bar: Exception}&stdClass', $o); + assertType('1', $o->foo); + assertType('Exception', $o->bar); + + assertType('Exception', $this->generics($o)); + } + + /** + * @return object{foo: static} + */ + public function returnObjectShapeWithStatic(): object + { + + } + + public function testObjectShapeWithStatic() + { + assertType('object{foo: static(ObjectShape\Foo)}', $this->returnObjectShapeWithStatic()); + } + +} + +class FooChild extends Foo +{ + +} + +class Bar +{ + + public function doFoo(Foo $foo) + { + assertType('object{foo: ObjectShape\Foo}', $foo->returnObjectShapeWithStatic()); + } + + public function doFoo2(FooChild $foo) + { + assertType('object{foo: ObjectShape\FooChild}', $foo->returnObjectShapeWithStatic()); + } + +} + +class OptionalProperty +{ + + /** + * @param object{foo: string, bar?: int} $o + * @return void + */ + public function doFoo(object $o): void + { + assertType('object{foo: string, bar?: int}', $o); + if (isset($o->foo)) { + assertType('object{foo: string, bar?: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + if (isset($o->bar)) { + assertType('object{foo: string, bar: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + } + + /** + * @param object{foo: string, bar?: int} $o + * @return void + */ + public function doBar(object $o): void + { + assertType('object{foo: string, bar?: int}', $o); + if (property_exists($o, 'foo')) { + assertType('object{foo: string, bar?: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + if (property_exists($o, 'bar')) { + assertType('object{foo: string, bar: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + } + +} + +class MethodExistsCheck +{ + + /** + * @param object{foo: string, bar?: int} $o + */ + public function doFoo(object $o): void + { + if (method_exists($o, 'doFoo')) { + assertType('object{foo: string, bar?: int}&hasMethod(doFoo)', $o); + } else { + assertType('object{foo: string, bar?: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + } + +} + +class ObjectWithProperty +{ + + public function doFoo(object $o): void + { + if (property_exists($o, 'foo')) { + assertType('object&hasProperty(foo)', $o); + } else { + assertType('object', $o); + } + assertType('object', $o); + + if (isset($o->foo)) { + assertType('object&hasProperty(foo)', $o); + } else { + assertType('object', $o); + } + assertType('object', $o); + } + +} + +class TestTemplate +{ + + /** + * @template T of object{foo: int} + * @param T $o + * @return T + */ + public function doBar(object $o): object + { + return $o; + } + + /** + * @param object{foo: positive-int} $o + * @return void + */ + public function doFoo(object $o): void + { + assertType('object{foo: int<1, max>}', $this->doBar($o)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/offset-access.php b/tests/PHPStan/Analyser/nsrt/offset-access.php new file mode 100644 index 0000000000..593dd799ab --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/offset-access.php @@ -0,0 +1,44 @@ += 8.0 + +namespace OffsetAccess; + +use function PHPStan\Testing\assertType; + +/** + * @template T of array + * @template K of key-of + * @param T $array + * @param K $offset + * @return T[K] + */ +function takeOffset(array $array, int $offset): mixed +{ + return $array[$offset]; +} + +function () { + assertType('2', takeOffset([1, 2], 1)); +}; + +/** + * @template T of array + * @param T $array + * @return T[($maybeZero is 0 ? 0 : key-of)] + */ +function takeConditionalOffset(array $array, int $maybeZero): int +{ + return $array[0]; +} + +function () { + assertType('1', takeConditionalOffset([1, 2, 3], 0)); + assertType('1|2|3', takeConditionalOffset([1, 2, 3], 1)); + assertType('1|2|3', takeConditionalOffset([1, 2, 3], 2)); +}; + +/** + * @return int[mixed] + */ +function impossibleOffset(int $value): mixed { + return $value; +} diff --git a/tests/PHPStan/Analyser/data/offset-value-after-assign.php b/tests/PHPStan/Analyser/nsrt/offset-value-after-assign.php similarity index 100% rename from tests/PHPStan/Analyser/data/offset-value-after-assign.php rename to tests/PHPStan/Analyser/nsrt/offset-value-after-assign.php diff --git a/tests/PHPStan/Analyser/nsrt/openssl-encrypt.php b/tests/PHPStan/Analyser/nsrt/openssl-encrypt.php new file mode 100644 index 0000000000..91e178514c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/openssl-encrypt.php @@ -0,0 +1,73 @@ + $class + * @param-closure-this T $cb + */ + public function paramClosureGenerics(string $class, callable $cb): void + { + + } + + public function voidMethod(): void + { + + } + + public function doFoo(): void + { + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(function () { + assertType(Some::class, $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + + $this->paramClosureSelf(function () use (&$a) { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(1, function () { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(2, function () { + assertType(Some::class, $this); + }); + $this->paramClosureGenerics(\stdClass::class, function () { + assertType(\stdClass::class, $this); + }); + } + + public function doFoo2(): void + { + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(fn () => assertType(Some::class, $this)); + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(static fn () => assertType('*ERROR*', $this)); + assertType(sprintf('$this(%s)', self::class), $this); + } + + public function doFoo3(): void + { + $a = 1; + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(function () use (&$a) { + assertType(Some::class, $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(static function () use (&$a) { + assertType('*ERROR*', $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + } + + public function interplayWithProcessImmediatelyCalledCallable(): void + { + assert($this->prop !== null); + assertType('string', $this->prop); + $this->paramClosureClassImmediatelyCalled(function () { + // $this is Some, not Foo + $this->voidMethod(); + }); + + // keep the narrowed type + assertType('string', $this->prop); + } + + public function interplayWithProcessImmediatelyCalledCallable2(): void + { + $s = new self(); + assert($s->prop !== null); + assertType('string', $s->prop); + $this->paramClosureClassImmediatelyCalled(function () use ($s) { + // $this is Some, not Foo + $this->voidMethod(); + + // but still invalidate $s + $s->voidMethod(); + }); + assertType('string|null', $s->prop); + } + +} + +function (Foo $f): void { + assertType('*ERROR*', $this); + $f->paramClosureClass(function () { + assertType(Some::class, $this); + }); + assertType('*ERROR*', $this); + $f->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + assertType('*ERROR*', $this); +}; + +function (Foo $f): void { + $a = 1; + assertType('*ERROR*', $this); + $f->paramClosureClass(function () use (&$a) { + assertType(Some::class, $this); + }); + assertType('*ERROR*', $this); + $f->paramClosureClass(static function () use (&$a) { + assertType('*ERROR*', $this); + }); + assertType('*ERROR*', $this); +}; + +function (Foo $f): void { + assertType('*ERROR*', $this); + $f->paramClosureClass(fn () => assertType(Some::class, $this)); + assertType('*ERROR*', $this); + $f->paramClosureClass(static fn () => assertType('*ERROR*', $this)); + assertType('*ERROR*', $this); +}; + +class Bar extends Foo +{ + + public function testClosureStatic(): void + { + assertType('$this(ParamClosureThis\Bar)', $this); + $this->paramClosureStatic(function () { + assertType('static(ParamClosureThis\Bar)', $this); + }); + assertType('$this(ParamClosureThis\Bar)', $this); + } + +} + +function (Bar $b): void { + $b->paramClosureStatic(function () { + assertType(Bar::class, $this); + }); +}; + +class ImplicitInheritance extends Foo +{ + + public function paramClosureClass(callable $cb) + { + + } + + public function paramClosureSelf(callable $cb) + { + + } + + public function paramClosureStatic(callable $cb) + { + + } + + public function paramClosureConditional(int $j, callable $ca) + { + // renamed parameter names + } + + public function doFoo(): void + { + $this->paramClosureClass(function () { + assertType(Some::class, $this); + }); + $this->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + $this->paramClosureSelf(function () use (&$a) { + assertType(Foo::class, $this); + }); + $this->paramClosureStatic(function () use (&$a) { + assertType('static(ParamClosureThis\ImplicitInheritance)', $this); + }); + $this->paramClosureConditional(1, function () { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(2, function () { + assertType(Some::class, $this); + }); + $this->paramClosureGenerics(\stdClass::class, function () { + assertType(\stdClass::class, $this); + }); + } + +} + +class ImplicitInheritanceMoreComplicated extends Foo +{ + + /** + * @param callable $cb + */ + public function paramClosureClass(callable $cb) + { + + } + + /** + * @param callable $cb + */ + public function paramClosureSelf(callable $cb) + { + + } + + /** + * @param callable $cb + */ + public function paramClosureStatic(callable $cb) + { + + } + + /** + * @param callable $ca + */ + public function paramClosureConditional(int $j, callable $ca) + { + // renamed parameter names + } + + public function doFoo(): void + { + $this->paramClosureClass(function () { + assertType(Some::class, $this); + }); + $this->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + $this->paramClosureSelf(function () use (&$a) { + assertType(Foo::class, $this); + }); + $this->paramClosureStatic(function () use (&$a) { + assertType('static(ParamClosureThis\ImplicitInheritanceMoreComplicated)', $this); + }); + $this->paramClosureConditional(1, function () { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(2, function () { + assertType(Some::class, $this); + }); + $this->paramClosureGenerics(\stdClass::class, function () { + assertType(\stdClass::class, $this); + }); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/param-out-default.php b/tests/PHPStan/Analyser/nsrt/param-out-default.php new file mode 100644 index 0000000000..eed6b94c6f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/param-out-default.php @@ -0,0 +1,36 @@ + : array) $out + */ + public function doFoo(&$out, $flags = 1): void + { + + } + + public function doBar(): void + { + $this->doFoo($a); + assertType('array', $a); + + $this->doFoo($b, 1); + assertType('array', $b); + + $this->doFoo($c, 2); + assertType('array', $c); + } + + public function sayHello(string $row): void + { + preg_match_all('#// error:(.+)#', $row, $matches); + assertType('array{list, list}', $matches); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/pathinfo-php8.php b/tests/PHPStan/Analyser/nsrt/pathinfo-php8.php new file mode 100644 index 0000000000..632c2c2b96 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pathinfo-php8.php @@ -0,0 +1,9 @@ +loadHTML($actual); + } else { + $loaded = $document->loadXML($actual); + } + + foreach (libxml_get_errors() as $error) { + $message .= "\n" . $error->message; + } + + if ($loaded === false || ($strict && $message !== '')) { + assertType('string', $message); + assertNativeType('string', $message); + if ($filename !== '') { + assertType('string', $message); + assertNativeType('string', $message); + throw new Exception( + sprintf( + 'Could not load "%s".%s', + $filename, + $message !== '' ? "\n" . $message : '' + ) + ); + } + + assertType('string', $message); + assertNativeType('string', $message); + + if ($message === '') { + $message = 'Could not load XML for unknown reason'; + } + + assertType('non-empty-string', $message); + assertNativeType('non-empty-string', $message); + + throw new Exception($message); + } + + return $document; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/pow.php b/tests/PHPStan/Analyser/nsrt/pow.php new file mode 100644 index 0000000000..3ca27690db --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pow.php @@ -0,0 +1,199 @@ +', $range); + + assertType('int<1, 9>', pow($range, 2)); + assertType('int<1, 9>', $range ** 2); + + assertType('int<2, 8>', pow(2, $range)); + assertType('int<2, 8>', 2 ** $range); +}; + +function (): void { + $range = rand(2, 3); + $x = 2; + if (rand(0, 1)) { + $x = 3; + } else if (rand(0, 10)) { + $x = 4; + } + + assertType('int<4, 81>', pow($range, $x)); + assertType('int<4, 81>', $range ** $x); + + assertType('int<4, 64>', pow($x, $range)); + assertType('int<4, 64>', $x ** $range); + + assertType('int<4, 27>', pow($range, $range)); + assertType('int<4, 27>', $range ** $range); +}; + +/** + * @param positive-int $positiveInt + * @param int $range2 + * @param int<-6, -4>|int<-2, -1> $unionRange1 + * @param int<4, 6>|int<1, 2> $unionRange2 + */ +function foo($positiveInt, $range2, $unionRange1, $unionRange2): void { + $range = rand(2, 3); + + assertType('int<2, max>', pow($range, $positiveInt)); + assertType('int<2, max>', $range ** $positiveInt); + + assertType('int', pow($range, $range2)); + assertType('int', $range ** $range2); + + assertType('(float|int)', pow($range, PHP_INT_MAX)); + assertType('(float|int)', $range ** PHP_INT_MAX); + + assertType('(float|int)', pow($range2, $positiveInt)); + assertType('(float|int)', $range2 ** $positiveInt); + + assertType('(float|int)', pow($positiveInt, $range2)); + assertType('(float|int)', $positiveInt ** $range2); + + assertType('int<-6, 16>|int<1296, 4096>', pow($unionRange1, $unionRange2)); + assertType('int<-6, 16>|int<1296, 4096>', $unionRange1 ** $unionRange2); + + assertType('int<2, 4>|int<16, 64>', pow(2, $unionRange2)); + assertType('int<2, 4>|int<16, 64>', 2 ** $unionRange2); + + assertType('int<2, 4>|int<16, 64>', pow("2", $unionRange2)); + assertType('int<2, 4>|int<16, 64>', "2" ** $unionRange2); + + assertType('1', pow(true, $unionRange2)); + assertType('1', true ** $unionRange2); + + assertType('0|1', pow(null, $unionRange2)); + assertType('0|1', null ** $unionRange2); +} + +/** + * @param numeric-string $numericS + */ +function doFoo(int $intA, int $intB, string $s, bool $bool, $numericS, float $float, array $arr): void { + assertType('(float|int)', pow($intA, $intB)); + assertType('(float|int)', $intA ** $intB); + + assertType('(float|int)', pow($intA, $numericS)); + assertType('(float|int)', $intA ** $numericS); + assertType('(float|int)', $numericS ** $numericS); + assertType('(float|int)', pow($intA, "123")); + assertType('(float|int)', $intA ** "123"); + assertType('int', pow($intA, 1)); + assertType('int', $intA ** '1'); + + assertType('(float|int)', pow($intA, $s)); + assertType('(float|int)', $intA ** $s); + + assertType('(float|int)', pow($intA, $bool)); // could be int + assertType('(float|int)', $intA ** $bool); // could be int + assertType('int', pow($intA, true)); + assertType('int', $intA ** true); + + assertType('*ERROR*', pow($bool, $arr)); + assertType('*ERROR*', pow($bool, [])); + + assertType('0|1', pow(null, "123")); + assertType('0|1', pow(null, $intA)); + assertType('1', "123" ** null); + assertType('1', $intA ** null); + assertType('1.0', $float ** null); + + assertType('*ERROR*', "123" ** $arr); + assertType('*ERROR*', "123" ** []); + + assertType('625', pow('5', '4')); + assertType('625', '5' ** '4'); + + assertType('(float|int)', pow($intA, $bool)); // could be float + assertType('(float|int)', $intA ** $bool); // could be float + assertType('*ERROR*', $intA ** $arr); + assertType('*ERROR*', $intA ** []); + + assertType('1', pow($intA, 0)); + assertType('1', $intA ** '0'); + assertType('1', $intA ** false); + assertType('int', $intA ** true); + + assertType('1.0', pow($float, 0)); + assertType('1.0', $float ** '0'); + assertType('1.0', $float ** false); + assertType('float', pow($float, 1)); + assertType('float', $float ** '1'); + assertType('*ERROR*', $float ** $arr); + assertType('*ERROR*', $float ** []); + + assertType('1.0', pow(1.1, 0)); + assertType('1.0', 1.1 ** '0'); + assertType('1.0', 1.1 ** false); + assertType('*ERROR*', 1.1 ** $arr); + assertType('*ERROR*', 1.1 ** []); + + assertType('NAN', pow(-1,5.5)); + + assertType('1', pow($s, 0)); + assertType('1', $s ** '0'); + assertType('1', $s ** false); + assertType('(float|int)', pow($s, 1)); + assertType('(float|int)', $s ** '1'); + assertType('*ERROR*', $s ** $arr); + assertType('*ERROR*', $s ** []); + + assertType('1', pow($bool, 0)); + assertType('1', $bool ** '0'); + assertType('1', $bool ** false); + assertType('(float|int)', pow($bool, 1)); + assertType('(float|int)', $bool ** '1'); + assertType('*ERROR*', $bool ** $arr); + assertType('*ERROR*', $bool ** []); +}; + +function invalidConstantOperands(): void { + assertType('*ERROR*', 'a' ** 1); + assertType('*ERROR*', 1 ** 'a'); + + assertType('*ERROR*', [] ** 1); + assertType('*ERROR*', 1 ** []); + + assertType('*ERROR*', (new \stdClass()) ** 1); + assertType('*ERROR*', 1 ** (new \stdClass())); +} + +function validConstantOperands(): void { + assertType('1', '1' ** 1); + assertType('1', 1 ** '1'); + assertType('1', '1' ** '1'); + + assertType('1', true ** 1); + assertType('1', 1 ** false); +} diff --git a/tests/PHPStan/Analyser/nsrt/pr-1244-php-84.php b/tests/PHPStan/Analyser/nsrt/pr-1244-php-84.php new file mode 100644 index 0000000000..17bf3d44c1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pr-1244-php-84.php @@ -0,0 +1,35 @@ += 8.4 + +namespace Pr1244Php84; + +use function PHPStan\Testing\assertType; + +function foo() { + /** @var string $string */ + $string = doFoo(); + + assertType('null', var_export()); + assertType('null', var_export($string)); + assertType('null', var_export($string, false)); + assertType('string', var_export($string, true)); + + assertType('true', highlight_string()); + assertType('true', highlight_string($string)); + assertType('true', highlight_string($string, false)); + assertType('string', highlight_string($string, true)); + + assertType('bool', highlight_file()); + assertType('bool', highlight_file($string)); + assertType('bool', highlight_file($string, false)); + assertType('string', highlight_file($string, true)); + + assertType('bool', show_source()); + assertType('bool', show_source($string)); + assertType('bool', show_source($string, false)); + assertType('string', show_source($string, true)); + + assertType('true', print_r()); + assertType('true', print_r($string)); + assertType('true', print_r($string, false)); + assertType('string', print_r($string, true)); +} diff --git a/tests/PHPStan/Analyser/nsrt/pr-1244.php b/tests/PHPStan/Analyser/nsrt/pr-1244.php new file mode 100644 index 0000000000..a8c0fc19ae --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pr-1244.php @@ -0,0 +1,35 @@ +', PHP_MAJOR_VERSION); +assertType('int<0, max>', PHP_MINOR_VERSION); +assertType('int<0, max>', PHP_RELEASE_VERSION); +assertType('int<50207, 80499>', 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-falsy-string', PHP_OS); +assertType('\'apache\'|\'apache2handler\'|\'cgi\'|\'cli\'|\'cli-server\'|\'embed\'|\'fpm-fcgi\'|\'litespeed\'|\'phpdbg\'|non-falsy-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-falsy-string', PHP_EXTENSION_DIR); +assertType('non-falsy-string', PHP_PREFIX); +assertType('non-falsy-string', PHP_BINDIR); +assertType('non-falsy-string', PHP_BINARY); +assertType('non-falsy-string', PHP_MANDIR); +assertType('non-falsy-string', PHP_LIBDIR); +assertType('non-falsy-string', PHP_DATADIR); +assertType('non-falsy-string', PHP_SYSCONFDIR); +assertType('non-falsy-string', PHP_LOCALSTATEDIR); +assertType('non-falsy-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('int', E_ALL); +assertType('2048', E_STRICT); +assertType('int<1, 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-falsy-string', ICONV_IMPL); + +// libxml, https://www.php.net/manual/en/libxml.constants.php +assertType('int<1, max>', LIBXML_VERSION); +assertType('non-falsy-string', LIBXML_DOTTED_VERSION); + +// openssl, https://www.php.net/manual/en/openssl.constants.php +assertType('int<1, max>', OPENSSL_VERSION_NUMBER); + +// pcre, https://www.php.net/manual/en/pcre.constants.php +assertType('non-falsy-string', PCRE_VERSION); + +// other +assertType('bool', ZEND_DEBUG_BUILD); +assertType('bool', ZEND_THREAD_SAFE); diff --git a/tests/PHPStan/Analyser/nsrt/preg_filter.php b/tests/PHPStan/Analyser/nsrt/preg_filter.php new file mode 100644 index 0000000000..aedf0bca2a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/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('string|null', preg_filter('/\d/', '$0', $subject)); + + $subject = 123.123; + assertType('string|null', preg_filter('/\d/', '$0', $subject)); + } + + public function dooFoo3(string $pattern, string $replace) { + assertType('list|string|null', preg_filter($pattern, $replace)); + assertType('list|string|null', preg_filter($pattern)); + assertType('list|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/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php new file mode 100644 index 0000000000..7ed783a8e9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -0,0 +1,186 @@ += 7.2 + +namespace PregMatchAllShapes; + +use function PHPStan\Testing\assertType; + +function (string $size): void { + preg_match_all('/ab(\d+)?/', $size, $matches, PREG_UNMATCHED_AS_NULL); + assertType('array{list, list}', $matches); +}; + +function (string $size): void { + preg_match_all('/ab(?P\d+)?/', $size, $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + preg_match_all('/ab(\d+)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER); + assertType('array{list, list}', $matches); +}; + +function (string $size): void { + preg_match_all('/ab(?P\d+)?/', $size, $matches, PREG_PATTERN_ORDER); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches)) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{}", $matches); + } + assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches) > 0) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches) != false) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{}", $matches); + } + assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches) == true) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{}", $matches); + } + assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches) === 1) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + preg_match_all('/a(b)(\d+)?/', $size, $matches, PREG_SET_ORDER); + assertType("list", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches)) { + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER)) { + assertType("list", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER)) { + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER)) { + assertType("list", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER)) { + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { + assertType("list}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { + assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { + assertType("list}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { + assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); + } +}; + +class Bug11457 +{ + public function sayHello(string $content): void + { + if (preg_match_all("~text=~mU", $content, $matches, PREG_OFFSET_CAPTURE) === 0) { + return; + } + + assertType('array{list}>}', $matches); + } + + public function sayFoo(string $content): void + { + if (preg_match_all("~text=~mU", $content, $matches, PREG_SET_ORDER) === 0) { + return; + } + + assertType('list', $matches); + } + + public function sayBar(string $content): void + { + if (preg_match_all("~text=~mU", $content, $matches, PREG_PATTERN_ORDER) === 0) { + return; + } + + assertType('array{list}', $matches); + } + + function doFoobar(string $s): void { + if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE)) { + assertType("array{list}>, list}>, list}>, list}>}", $matches); + } + } + + function doFoobarNull(string $s): void { + if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) { + assertType("array{list}>, list}>, list}>, list}>}", $matches); + } + } +} + +function bug11661(): void { + preg_match_all('/(ERR)?(.+)/', 'abc', $results, PREG_SET_ORDER); + assertType("list", $results); + + preg_match_all('/(ERR)?.+/', 'abc', $results, PREG_SET_ORDER); + assertType("list", $results); + +} diff --git a/tests/PHPStan/Analyser/data/preg_match_php7.php b/tests/PHPStan/Analyser/nsrt/preg_match_php7.php similarity index 81% rename from tests/PHPStan/Analyser/data/preg_match_php7.php rename to tests/PHPStan/Analyser/nsrt/preg_match_php7.php index 2e435cbf87..0d4887ffea 100644 --- a/tests/PHPStan/Analyser/data/preg_match_php7.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_php7.php @@ -1,6 +1,6 @@ -= 8.0 -namespace PregMatch; +namespace PregMatchPhp8; use function PHPStan\Testing\assertType; diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php new file mode 100644 index 0000000000..545fd191f1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -0,0 +1,1075 @@ += 7.2 + +namespace PregMatchShapes; + +use function PHPStan\Testing\assertType; +use InvalidArgumentException; + +function doMatch(string $s): void { + if (preg_match('/Price: /i', $s, $matches)) { + assertType('array{non-falsy-string}', $matches); + } + assertType('array{}|array{non-falsy-string}', $matches); + + if (preg_match('/Price: (£|€)\d+/', $s, $matches)) { + assertType("array{non-falsy-string, '£'|'€'}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{non-falsy-string, '£'|'€'}", $matches); + + if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) { + assertType('array{non-falsy-string, non-empty-string, numeric-string}', $matches); + } + assertType('array{}|array{non-falsy-string, non-empty-string, numeric-string}', $matches); + + if (preg_match(' /Price: (£|€)\d+/ i u', $s, $matches)) { + assertType('array{non-falsy-string, non-empty-string}', $matches); + } + assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); + + if (preg_match('(Price: (£|€))i', $s, $matches)) { + assertType('array{non-falsy-string, non-empty-string}', $matches); + } + assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); + + if (preg_match('_foo(.)\_i_i', $s, $matches)) { + assertType('array{non-falsy-string, non-empty-string}', $matches); + } + assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); + + if (preg_match('/(a)(b)*(c)(d)*/', $s, $matches)) { + assertType("array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); + } + assertType("array{}|array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); + + if (preg_match('/(a)(?b)*(c)(d)*/', $s, $matches)) { + assertType("array{0: non-falsy-string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); + } + assertType("array{}|array{0: non-falsy-string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); + + if (preg_match('/(a)(b)*(c)(?d)*/', $s, $matches)) { + assertType("array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $matches); + } + assertType("array{}|array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $matches); + + if (preg_match('/(a|b)|(?:c)/', $s, $matches)) { + assertType("array{0: non-empty-string, 1?: 'a'|'b'}", $matches); + } + assertType("array{}|array{0: non-empty-string, 1?: 'a'|'b'}", $matches); + + if (preg_match('/(foo)(bar)(baz)+/', $s, $matches)) { + assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + } + assertType("array{}|array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + + if (preg_match('/(foo)(bar)(baz)*/', $s, $matches)) { + assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + } + assertType("array{}|array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + + if (preg_match('/(foo)(bar)(baz)?/', $s, $matches)) { + assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); + } + assertType("array{}|array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); + + if (preg_match('/(foo)(bar)(baz){0,3}/', $s, $matches)) { + assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + } + assertType("array{}|array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + + if (preg_match('/(foo)(bar)(baz){2,3}/', $s, $matches)) { + assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + } + assertType("array{}|array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + + if (preg_match('/(foo)(bar)(baz){2}/', $s, $matches)) { + assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + } + assertType("array{}|array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); +} + +function doNonCapturingGroup(string $s): void { + if (preg_match('/Price: (?:£|€)(\d+)/', $s, $matches)) { + assertType('array{non-falsy-string, numeric-string}', $matches); + } + assertType('array{}|array{non-falsy-string, numeric-string}', $matches); +} + +function doNamedSubpattern(string $s): void { + if (preg_match('/\w-(?P\d+)-(\w)/', $s, $matches)) { + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); + } + assertType('array{}|array{0: non-falsy-string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); + + if (preg_match('/^(?\S+::\S+)/', $s, $matches)) { + assertType('array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string}', $matches); + } + assertType('array{}|array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string}', $matches); + + if (preg_match('/^(?\S+::\S+)(?:(? with data set (?:#\d+|"[^"]+"))\s\()?/', $s, $matches)) { + assertType('array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string, dataname?: non-falsy-string, 2?: non-falsy-string}', $matches); + } + assertType('array{}|array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string, dataname?: non-falsy-string, 2?: non-falsy-string}', $matches); +} + +function doOffsetCapture(string $s): void { + if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE)) { + assertType("array{array{non-falsy-string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); + } + assertType("array{}|array{array{non-falsy-string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); +} + +function doUnknownFlags(string $s, int $flags): void { + if (preg_match('/(foo)(bar)(baz)/xyz', $s, $matches, $flags)) { + assertType('array}|string|null>', $matches); + } + assertType('array}|string|null>', $matches); +} + +function doMultipleAlternativeCaptureGroupsWithSameNameWithModifier(string $s): void { + if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { + assertType("array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + } + assertType("array{}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); +} + +function doMultipleConsecutiveCaptureGroupsWithSameNameWithModifier(string $s): void { + if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { + assertType("array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + } + assertType("array{}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); +} + +// https://github.com/hoaproject/Regex/issues/31 +function hoaBug31(string $s): void { + if (preg_match('/([\w-])/', $s, $matches)) { + assertType('array{non-empty-string, non-empty-string}', $matches); + } + assertType('array{}|array{non-empty-string, non-empty-string}', $matches); + + if (preg_match('/\w-(\d+)-(\w)/', $s, $matches)) { + assertType('array{non-falsy-string, numeric-string, non-empty-string}', $matches); + } + assertType('array{}|array{non-falsy-string, numeric-string, non-empty-string}', $matches); +} + +// https://github.com/phpstan/phpstan/issues/10855#issuecomment-2044323638 +function testHoaUnsupportedRegexSyntax(string $s): void { + if (preg_match('#\QPHPDoc type array of property App\Log::$fillable is not covariant with PHPDoc type array of overridden property Illuminate\Database\E\\\\\QEloquent\Model::$fillable.\E#', $s, $matches)) { + assertType('array{non-falsy-string}', $matches); + } + assertType('array{}|array{non-falsy-string}', $matches); +} + +function testPregMatchSimpleCondition(string $value): void { + if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { + assertType('array{non-falsy-string, string}', $matches); + } +} + + +function testPregMatchIdenticalToOne(string $value): void { + if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) === 1) { + assertType('array{non-falsy-string, string}', $matches); + } +} + +function testPregMatchIdenticalToOneFalseyContext(string $value): void { + if (!(preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) !== 1)) { + assertType('array{non-falsy-string, string}', $matches); + } +} + +function testPregMatchIdenticalToOneInverted(string $value): void { + if (1 === preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { + assertType('array{non-falsy-string, string}', $matches); + } +} + +function testPregMatchIdenticalToOneFalseyContextInverted(string $value): void { + if (!(1 !== preg_match('/%env\((.*)\:.*\)%/U', $value, $matches))) { + assertType('array{non-falsy-string, string}', $matches); + } +} + +function testPregMatchEqualToOne(string $value): void { + if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) == 1) { + assertType('array{non-falsy-string, string}', $matches); + } +} + +function testPregMatchEqualToOneFalseyContext(string $value): void { + if (!(preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) != 1)) { + assertType('array{non-falsy-string, string}', $matches); + } +} + +function testPregMatchEqualToOneInverted(string $value): void { + if (1 == preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { + assertType('array{non-falsy-string, string}', $matches); + } +} + +function testPregMatchEqualToOneFalseyContextInverted(string $value): void { + if (!(1 != preg_match('/%env\((.*)\:.*\)%/U', $value, $matches))) { + assertType('array{non-falsy-string, string}', $matches); + } +} + +function testUnionPattern(string $s): void +{ + if (rand(0,1)) { + $pattern = '/Price: (\d+)/i'; + } else { + $pattern = '/Price: (\d+)(\d+)(\d+)/'; + } + if (preg_match($pattern, $s, $matches)) { + assertType('array{non-falsy-string, numeric-string, numeric-string, numeric-string}|array{non-falsy-string, numeric-string}', $matches); + } + assertType('array{}|array{non-falsy-string, numeric-string, numeric-string, numeric-string}|array{non-falsy-string, numeric-string}', $matches); +} + +function doFoo(string $row): void +{ + if (preg_match('~^(a(b))$~', $row, $matches) === 1) { + assertType("array{non-falsy-string, 'ab', 'b'}", $matches); + } + if (preg_match('~^(a(b)?)$~', $row, $matches) === 1) { + assertType("array{0: non-falsy-string, 1: non-falsy-string, 2?: 'b'}", $matches); + } + if (preg_match('~^(a(b)?)?$~', $row, $matches) === 1) { + assertType("array{0: string, 1?: non-falsy-string, 2?: 'b'}", $matches); + } +} + +function doFoo2(string $row): void +{ + if (preg_match('~^((?\\d{1,6})-)?(?\\d{1,10})/(?\\d{4})$~', $row, $matches) !== 1) { + return; + } + + assertType("array{0: non-falsy-string, 1: string, branchCode: ''|numeric-string, 2: ''|numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: non-falsy-string&numeric-string, 4: non-falsy-string&numeric-string}", $matches); +} + +function doFoo3(string $row): void +{ + if (preg_match('~^(02,([\d.]{10}),(\d+),(\d+),(\d+),)(\d+)$~', $row, $matches) !== 1) { + return; + } + + assertType('array{non-falsy-string, non-falsy-string, non-falsy-string, numeric-string, numeric-string, numeric-string, numeric-string}', $matches); +} + +function (string $size): void { + if (preg_match('~^a\.b(c(\d+)(\d+)(\s+))?d~', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-falsy-string, numeric-string, numeric-string, non-empty-string}|array{non-falsy-string}', $matches); +}; + +function (string $size): void { + if (preg_match('~^a\.b(c(\d+))?d~', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-falsy-string, numeric-string}|array{non-falsy-string}', $matches); +}; + +function (string $size): void { + if (preg_match('~^a\.b(c(\d+)?)d~', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{0: non-falsy-string, 1: non-falsy-string, 2?: numeric-string}', $matches); +}; + +function (string $size): void { + if (preg_match('~^a\.b(c(\d+)?)?d~', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{0: non-falsy-string, 1?: non-falsy-string, 2?: numeric-string}', $matches); +}; + +function (string $size): void { + if (preg_match('~^a\.b(c(\d+))d~', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); +}; + +function (string $size): void { + if (preg_match('~^a\.(b)?(c)?d~', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType("list{0: non-falsy-string, 1?: ''|'b', 2?: 'c'}", $matches); +}; + +function (string $size): void { + if (preg_match('~^(?:(\\d+)x(\\d+)|(\\d+)|x(\\d+))$~', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType("array{non-empty-string, '', '', '', numeric-string}|array{non-empty-string, '', '', numeric-string}|array{non-empty-string, numeric-string, numeric-string}", $matches); +}; + +function (string $size): void { + if (preg_match('~^(?:(\\d+)x(\\d+)|(\\d+)|x(\\d+))?$~', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType("array{string, '', '', '', numeric-string}|array{string, '', '', numeric-string}|array{string, numeric-string, numeric-string}|array{string}", $matches); +}; + +function (string $size): void { + if (preg_match('~\{(?:(include)\\s+(?:[$]?\\w+(?£|€)\d+/', $s, $matches)) { + assertType("array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); +} + +function bug11323b(string $s): void +{ + if (preg_match('/Price: (?£|€)\d+/', $s, $matches)) { + assertType("array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); +} + +function unmatchedAsNullWithMandatoryGroup(string $s): void { + if (preg_match('/Price: (?£|€)\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType("array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); +} + +function (string $s): void { + if (preg_match('{' . preg_quote('xxx') . '(z)}', $s, $matches)) { + assertType("array{non-falsy-string, 'z'}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{non-falsy-string, 'z'}", $matches); +}; + +function (string $s): void { + if (preg_match('{' . preg_quote($s) . '(z)}', $s, $matches)) { + assertType("array{non-falsy-string, 'z'}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{non-falsy-string, 'z'}", $matches); +}; + +function (string $s): void { + if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) { + assertType('array{non-empty-string, numeric-string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{non-empty-string, numeric-string}', $matches); +}; + +function (string $s): void { + if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)(def)?}', $s, $matches)) { + assertType("array{0: non-falsy-string, 1: 'z', 2?: 'def'}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{0: non-falsy-string, 1: 'z', 2?: 'def'}", $matches); +}; + +function (string $s, $mixed): void { + if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)'. $mixed .'(def)?}', $s, $matches)) { + assertType('array', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array', $matches); +}; + +function (string $s): void { + if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches) === 1) { + assertType("array{non-falsy-string, string, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('~^((\\d{1,6})-)$~', $s, $matches) === 1) { + assertType("array{non-falsy-string, non-falsy-string, numeric-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('~^((\\d{1,6}).)$~', $s, $matches) === 1) { + assertType("array{non-falsy-string, non-falsy-string, numeric-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('~^([157])$~', $s, $matches) === 1) { + assertType("array{non-falsy-string, '1'|'5'|'7'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('~^([157XY])$~', $s, $matches) === 1) { + assertType("array{non-falsy-string, '1'|'5'|'7'|'X'|'Y'}", $matches); + } +}; + +function bug11323(string $s): void { + if (preg_match('/([*|+?{}()]+)([^*|+[:digit:]?{}()]+)/', $s, $matches)) { + assertType('array{non-falsy-string, non-empty-string, non-empty-string}', $matches); + } + if (preg_match('/\p{L}[[\]]+([-*|+?{}(?:)]+)([^*|+[:digit:]?{a-z}(\p{L})\a-]+)/', $s, $matches)) { + assertType('array{non-falsy-string, non-empty-string, non-empty-string}', $matches); + } + if (preg_match('{([-\p{L}[\]*|\x03\a\b+?{}(?:)-]+[^[:digit:]?{}a-z0-9#-k]+)(a-z)}', $s, $matches)) { + assertType("array{non-falsy-string, non-falsy-string, 'a-z'}", $matches); + } + if (preg_match('{(\d+)(?i)insensitive((?xs-i)case SENSITIVE here.+and dot matches new lines)}', $s, $matches)) { + assertType('array{non-falsy-string, numeric-string, non-falsy-string}', $matches); + } + if (preg_match('{(\d+)(?i)insensitive((?x-i)case SENSITIVE here(?i:insensitive non-capturing group))}', $s, $matches)) { + assertType('array{non-falsy-string, numeric-string, non-falsy-string}', $matches); + } + if (preg_match('{([]] [^]])}', $s, $matches)) { + assertType('array{non-falsy-string, non-falsy-string}', $matches); + } + if (preg_match('{([[:digit:]])}', $s, $matches)) { + assertType('array{non-empty-string, numeric-string}', $matches); + } + if (preg_match('{([\d])(\d)}', $s, $matches)) { + assertType('array{non-falsy-string, numeric-string, numeric-string}', $matches); + } + if (preg_match('{([0-9])}', $s, $matches)) { + assertType('array{non-empty-string, numeric-string}', $matches); + } + if (preg_match('{(\p{L})(\p{P})(\p{Po})}', $s, $matches)) { + assertType('array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $matches); + } + if (preg_match('{(a)??(b)*+(c++)(d)+?}', $s, $matches)) { + assertType("array{non-falsy-string, ''|'a', string, non-empty-string, non-empty-string}", $matches); + } + if (preg_match('{(.\d)}', $s, $matches)) { + assertType('array{non-falsy-string, non-falsy-string}', $matches); + } + if (preg_match('{(\d.)}', $s, $matches)) { + assertType('array{non-falsy-string, non-falsy-string}', $matches); + } + if (preg_match('{(\d\d)}', $s, $matches)) { + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + } + if (preg_match('{(.(\d))}', $s, $matches)) { + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); + } + if (preg_match('{((\d).)}', $s, $matches)) { + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); + } + if (preg_match('{(\d([1-4])\d)}', $s, $matches)) { + assertType('array{non-falsy-string, non-falsy-string&numeric-string, numeric-string}', $matches); + } + if (preg_match('{(x?([1-4])\d)}', $s, $matches)) { + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); + } + if (preg_match('{([^1-4])}', $s, $matches)) { + assertType('array{non-empty-string, non-empty-string}', $matches); + } + if (preg_match("{([\r\n]+)(\n)([\n])}", $s, $matches)) { + assertType('array{non-falsy-string, non-empty-string, "\n", "\n"}', $matches); + } + if (preg_match('/foo(*:first)|bar(*:second)([x])/', $s, $matches)) { + assertType("array{0: non-empty-string, 1?: 'x', MARK?: 'first'|'second'}", $matches); + } +} + +function (string $s): void { + preg_match('/%a(\d*)/', $s, $matches); + assertType("list{0?: string, 1?: ''|numeric-string}", $matches); +}; + +class Bug11376 +{ + public function test(string $str): void + { + preg_match('~^(?:(\w+)::)?(\w+)$~', $str, $matches); + assertType('list{0?: string, 1?: string, 2?: non-empty-string}', $matches); + } + + public function test2(string $str): void + { + if (preg_match('~^(?:(\w+)::)?(\w+)$~', $str, $matches) === 1) { + assertType('array{non-empty-string, string, non-empty-string}', $matches); + } + } +} + +function (string $s): void { + if (rand(0,1)) { + $p = '/Price: (£)(abc)/'; + } else { + $p = '/Price: (\d)(b)/'; + } + + if (preg_match($p, $s, $matches)) { + assertType("array{non-falsy-string, '£', 'abc'}|array{non-falsy-string, numeric-string, 'b'}", $matches); + } +}; + +function (string $s): void { + if (rand(0,1)) { + $p = '/Price: (£)/'; + } else { + $p = '/Price: (£|(\d)|(x))/'; + } + + if (preg_match($p, $s, $matches)) { + assertType("list{0: non-falsy-string, 1: 'x'|'£'|numeric-string, 2?: ''|numeric-string, 3?: 'x'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([a-z])/i', $s, $matches)) { + assertType("array{non-falsy-string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([0-9])/i', $s, $matches)) { + assertType("array{non-falsy-string, numeric-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([xXa])/i', $s, $matches)) { + assertType("array{non-falsy-string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([xXa])/', $s, $matches)) { + assertType("array{non-falsy-string, 'a'|'X'|'x'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (ba[rz])/', $s, $matches)) { + assertType("array{non-falsy-string, 'bar'|'baz'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (b[ao][mn])/', $s, $matches)) { + assertType("array{non-falsy-string, 'bam'|'ban'|'bom'|'bon'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (\s{3}|0)/', $s, $matches)) { + assertType("array{non-falsy-string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (a|bc?)/', $s, $matches)) { + assertType("array{non-falsy-string, non-falsy-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (?a|bc?)/', $s, $matches)) { + assertType("array{0: non-falsy-string, named: non-falsy-string, 1: non-falsy-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (a|0c?)/', $s, $matches)) { + assertType("array{non-falsy-string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (a|\d)/', $s, $matches)) { + assertType("array{non-falsy-string, 'a'|numeric-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (?a|\d)/', $s, $matches)) { + assertType("array{0: non-falsy-string, named: 'a'|numeric-string, 1: 'a'|numeric-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (a|0)/', $s, $matches)) { + assertType("array{non-falsy-string, '0'|'a'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (aa|0)/', $s, $matches)) { + assertType("array{non-falsy-string, '0'|'aa'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/( \d+ )/x', $s, $matches)) { + assertType('array{non-empty-string, numeric-string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/( .? )/x', $s, $matches)) { + assertType('array{string, string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/( .* )/x', $s, $matches)) { + assertType('array{string, string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/( .+ )/x', $s, $matches)) { + assertType('array{non-empty-string, non-empty-string}', $matches); + } +}; + +function (string $value): void +{ + if (preg_match('/^(x)*$/', $value, $matches, PREG_OFFSET_CAPTURE)) { + assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}}", $matches); + } +}; + +function (string $value): void { + if (preg_match('/^(?:(x)|(y))*$/', $value, $matches, PREG_OFFSET_CAPTURE)) { + assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}}|array{array{string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); + } +}; + +class Bug11479 +{ + static public function sayHello(string $source): void + { + $pattern = "~^(?P\d)?\-?(?P\d)?$~"; + + preg_match($pattern, $source, $matches); + + // for $source = "-1" in $matches is + // array ( + // 0 => '-1', + // 'dateFrom' => '', + // 1 => '', + // 'dateTo' => '1', + // 2 => '1', + //) + + assertType("array{0?: string, dateFrom?: ''|numeric-string, 1?: ''|numeric-string, dateTo?: numeric-string, 2?: numeric-string}", $matches); + } +} + +function (string $s): void { + if (preg_match('~a|(\d)|(\s)~', $s, $matches) === 1) { + assertType("array{0: non-empty-string, 1?: numeric-string}|array{non-empty-string, '', non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('~a|((u)x)|((v)y)~', $s, $matches) === 1) { + assertType("array{non-empty-string, '', '', 'vy', 'v'}|array{non-empty-string, 'ux', 'u'}|array{non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_OFFSET_CAPTURE) === 1) { + assertType("array{0: array{non-empty-string, int<-1, max>}, 1?: array{numeric-string, int<-1, max>}}|array{array{non-empty-string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); + } +}; + +function (string $s): void { + preg_match('~a|(\d)|(\s)~', $s, $matches); + assertType("list{0?: string, 1?: '', 2?: non-empty-string}|list{0?: string, 1?: numeric-string}", $matches); +}; + +function bug11490 (string $expression): void { + $matches = []; + + if (preg_match('/([-+])?([\d]+)%/', $expression, $matches) === 1) { + assertType("array{non-falsy-string, ''|'+'|'-', numeric-string}", $matches); + } +} + +function bug11490b (string $expression): void { + $matches = []; + + if (preg_match('/([\\[+])?([\d]+)%/', $expression, $matches) === 1) { + assertType("array{non-falsy-string, ''|'+'|'[', numeric-string}", $matches); + } +} + +function bug11622 (string $expression): void { + $matches = []; + + if (preg_match('/^abc(def|$)/', $expression, $matches) === 1) { + assertType("array{non-falsy-string, string}", $matches); + } +} + +function bug11604 (string $string): void { + if (! preg_match('/(XX)|(YY)?ZZ/', $string, $matches)) { + return; + } + + assertType("list{0: non-empty-string, 1?: ''|'XX', 2?: 'YY'}", $matches); + // could be array{string, '', 'YY'}|array{string, 'XX'}|array{string} +} + +function bug11604b (string $string): void { + if (preg_match('/(XX)|(YY)?(ZZ)/', $string, $matches)) { + assertType("list{0: non-empty-string, 1?: ''|'XX', 2?: ''|'YY', 3?: 'ZZ'}", $matches); + } +} + +function testLtrimDelimiter (string $string): void { + if (preg_match(' /(x)/', $string, $matches)) { + assertType("array{non-empty-string, 'x'}", $matches); + } + + if (preg_match(' /(x)/', $string, $matches)) { + assertType("array{non-empty-string, 'x'}", $matches); + } +} + +function testUnescapeBackslash (string $string): void { + if (preg_match(<<<'EOD' + ~(\[)~ + EOD, $string, $matches)) { + assertType("array{non-empty-string, '['}", $matches); + } + + if (preg_match(<<<'EOD' + ~(\d)~ + EOD, $string, $matches)) { + assertType("array{non-empty-string, numeric-string}", $matches); + } + + if (preg_match(<<<'EOD' + ~(\\d)~ + EOD, $string, $matches)) { + assertType("array{non-falsy-string, '\\\d'}", $matches); + } + + if (preg_match(<<<'EOD' + ~(\\\d)~ + EOD, $string, $matches)) { + assertType("array{non-falsy-string, non-falsy-string}", $matches); + } + + if (preg_match(<<<'EOD' + ~(\\\\d)~ + EOD, $string, $matches)) { + assertType("array{non-falsy-string, '\\\\\\\d'}", $matches); + } +} + +function testEscapedDelimiter (string $string): void { + if (preg_match(<<<'EOD' + /(\/)/ + EOD, $string, $matches)) { + assertType("array{non-empty-string, '/'}", $matches); + } + + if (preg_match(<<<'EOD' + ~(\~)~ + EOD, $string, $matches)) { + assertType("array{non-empty-string, '~'}", $matches); + } + + if (preg_match(<<<'EOD' + ~(\[2])~ + EOD, $string, $matches)) { + assertType("array{non-falsy-string, '[2]'}", $matches); + } + + if (preg_match(<<<'EOD' + [(\[2\])] + EOD, $string, $matches)) { + assertType("array{non-falsy-string, '[2]'}", $matches); + } + + if (preg_match(<<<'EOD' + ~(\{2})~ + EOD, $string, $matches)) { + assertType("array{non-falsy-string, '{2}'}", $matches); + } + + if (preg_match(<<<'EOD' + {(\{2\})} + EOD, $string, $matches)) { + assertType("array{non-falsy-string, '{2}'}", $matches); + } + + if (preg_match(<<<'EOD' + ~([a\]])~ + EOD, $string, $matches)) { + assertType("array{non-empty-string, ']'|'a'}", $matches); + } + + if (preg_match(<<<'EOD' + ~([a[])~ + EOD, $string, $matches)) { + assertType("array{non-empty-string, '['|'a'}", $matches); + } + + if (preg_match(<<<'EOD' + ~([a\]b])~ + EOD, $string, $matches)) { + assertType("array{non-empty-string, ']'|'a'|'b'}", $matches); + } + + if (preg_match(<<<'EOD' + ~([a[b])~ + EOD, $string, $matches)) { + assertType("array{non-empty-string, '['|'a'|'b'}", $matches); + } + + if (preg_match(<<<'EOD' + ~([a\[b])~ + EOD, $string, $matches)) { + assertType("array{non-empty-string, '['|'a'|'b'}", $matches); + } + + if (preg_match(<<<'EOD' + [([a\[b])] + EOD, $string, $matches)) { + assertType("array{non-empty-string, '['|'a'|'b'}", $matches); + } + + if (preg_match(<<<'EOD' + {(x\\\{)|(y\\\\\})} + EOD, $string, $matches)) { + assertType("array{non-empty-string, '', 'y\\\\\\\}'}|array{non-empty-string, 'x\\\{'}", $matches); + } +} + +function bugUnescapedDashAfterRange (string $string): void +{ + if (preg_match('/([0-1-y])/', $string, $matches)) { + assertType("array{non-empty-string, non-empty-string}", $matches); + } +} + +function bugEmptySubexpression (string $string): void { + if (preg_match('//', $string, $matches)) { + assertType("array{string}", $matches); // could be array{''} + } + + if (preg_match('/()/', $string, $matches)) { + assertType("array{string, ''}", $matches); // could be array{'', ''} + } + + if (preg_match('/|/', $string, $matches)) { + assertType("array{string}", $matches); // could be array{''} + } + + if (preg_match('~|(a)~', $string, $matches)) { + assertType("array{0: string, 1?: 'a'}", $matches); + } + + if (preg_match('~(a)|~', $string, $matches)) { + assertType("array{0: string, 1?: 'a'}", $matches); + } + + if (preg_match('~(a)||(b)~', $string, $matches)) { + assertType("array{0: string, 1?: 'a'}|array{string, '', 'b'}", $matches); + } + + if (preg_match('~(|(a))~', $string, $matches)) { + assertType("array{0: string, 1: ''|'a', 2?: 'a'}", $matches); + } + + if (preg_match('~((a)|)~', $string, $matches)) { + assertType("array{0: string, 1: ''|'a', 2?: 'a'}", $matches); + } + + if (preg_match('~((a)||(b))~', $string, $matches)) { + assertType("list{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: 'b'}", $matches); + } + + if (preg_match('~((a)|()|(b))~', $string, $matches)) { + assertType("list{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: '', 4?: 'b'}", $matches); + } +} + +function bug11744(string $string): void +{ + if (!preg_match('~^((/[a-z]+)?)~', $string, $matches)) { + return; + } + assertType('array{0: string, 1: string, 2?: non-falsy-string}', $matches); + + if (!preg_match('~^((/[a-z]+)?.*)~', $string, $matches)) { + return; + } + assertType('array{0: string, 1: string, 2?: non-falsy-string}', $matches); + + if (!preg_match('~^((/[a-z]+)?.+)~', $string, $matches)) { + return; + } + assertType('array{0: non-empty-string, 1: non-empty-string, 2?: non-falsy-string}', $matches); +} + +function bug12749(string $str): void +{ + if (preg_match('/[A-Z]/', $str, $match)) { + assertType('array{non-empty-string}', $match); // could be non-falsy-string + } +} + +function bug12749a(string $str): void +{ + if (preg_match('/[A-Z]{2,}/', $str, $match)) { + assertType('array{non-falsy-string}', $match); + } +} + +function bug12749b(string $str): void +{ + if (preg_match('/[0-9][A-Z]/', $str, $match)) { + assertType('array{non-falsy-string}', $match); + } +} + +function bug12749c(string $str): void +{ + if (preg_match('/[0-9][A-Z]?/', $str, $match)) { + assertType('array{non-empty-string}', $match); + } +} + +function bug12749d(string $str): void +{ + if (preg_match('/[0-9]?[A-Z]/', $str, $match)) { + assertType('array{non-falsy-string}', $match); + } +} + +function bug12749e(string $str): void +{ + // no ^ $ delims, therefore can be anything which contains a number + if (preg_match('/[0-9]/', $str, $match)) { + assertType('array{non-empty-string}', $match); + } +} + +function bug12749f(string $str): void +{ + if (preg_match('/^[0-9]$/', $str, $match)) { + assertType('array{non-empty-string}', $match); // could be numeric-string + } +} + +function bug12397(string $string): void { + $m = preg_match('#\b([A-Z]{2,})-(\d+)#', $string, $match); + assertType('list{0?: string, 1?: non-falsy-string, 2?: numeric-string}', $match); +} + +function bug12792(string $string): void { + if (preg_match('~a\Kb~', $string, $match) === 1) { + assertType('array{string}', $match); // could be array{'b'} + } + + if (preg_match('~a\K~', $string, $match) === 1) { + assertType('array{string}', $match); // could be array{''} + } + + if (preg_match('~a\K.+~', $string, $match) === 1) { + assertType('array{string}', $match); // could be array{non-empty-string} + } + + if (preg_match('~a\K.*~', $string, $match) === 1) { + assertType('array{string}', $match); + } + + if (preg_match('~a\K(.+)~', $string, $match) === 1) { + assertType('array{string, non-empty-string}', $match); // could be array{non-empty-string, non-empty-string} + } + + if (preg_match('~a\K(.*)~', $string, $match) === 1) { + assertType('array{string, string}', $match); + } + + if (preg_match('~a\K(.+?)~', $string, $match) === 1) { + assertType('array{string, non-empty-string}', $match); // could be array{non-empty-string, non-empty-string} + } + + if (preg_match('~a\K(.*?)~', $string, $match) === 1) { + assertType('array{string, string}', $match); + } + + if (preg_match('~a\K(?=.+)~', $string, $match) === 1) { + assertType('array{string}', $match); // could be array{''} + } + + if (preg_match('~a\K(?=.*)~', $string, $match) === 1) { + assertType('array{string}', $match); // could be array{''} + } + + if (preg_match('~a(?:x\Kb|c)~', $string, $match) === 1) { + assertType('array{string}', $match); // could be array{'ac'|'b'} + } + + if (preg_match('~a(?:c|x\Kb)~', $string, $match) === 1) { + assertType('array{string}', $match); // could be array{'ac'|'b'} + } + + if (preg_match('~a(y|(?:x\Kb|c))d~', $string, $match) === 1) { + assertType('array{string, non-empty-string}', $match); // could be array{'acd'|'ayd'|'bd', 'c'|'xb'|'y'} + } + + if (preg_match('~a((?:c|x\Kb)|y)d~', $string, $match) === 1) { + assertType('array{string, non-empty-string}', $match); // could be array{'acd'|'ayd'|'bd', 'c'|'xb'|'y'} + } +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php new file mode 100644 index 0000000000..4620565210 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php @@ -0,0 +1,21 @@ += 8.0 + +namespace PregMatchShapesPhp82; + +use function PHPStan\Testing\assertType; + +function doOffsetCaptureWithUnmatchedNull(string $s): void { + // see https://3v4l.org/07rBO#v8.2.9 + if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) { + assertType("array{array{non-falsy-string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); + } + assertType("array{}|array{array{non-falsy-string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); +} + +function doNonAutoCapturingModifier(string $s): void { + if (preg_match('/(?n)(\d+)/', $s, $matches)) { + // should be assertType('array{string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); + } + assertType('array{}|array{non-empty-string, numeric-string}', $matches); +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php new file mode 100644 index 0000000000..1b5dc597b7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -0,0 +1,46 @@ += 8.2 + +namespace PregMatchShapesPhp82; + +use function PHPStan\Testing\assertType; + +// n modifier captures only named groups +// https://php.watch/versions/8.2/preg-n-no-capture-modifier +function doNonAutoCapturingFlag(string $s): void { + if (preg_match('/(\d+)/n', $s, $matches)) { + assertType('array{non-empty-string}', $matches); + } + assertType('array{}|array{non-empty-string}', $matches); + + if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + } + assertType('array{}|array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + + if (preg_match('/(\w)-(?P\d+)-(\w)/n', $s, $matches)) { + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + } + assertType('array{}|array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); +} + +// delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php +function (string $s): void { + if (preg_match('{(\d+)(?P\d+)}n', $s, $matches)) { + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + } +}; +function (string $s): void { + if (preg_match('<(\d+)(?P\d+)>n', $s, $matches)) { + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + } +}; +function (string $s): void { + if (preg_match('((\d+)(?P\d+))n', $s, $matches)) { + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + } +}; +function (string $s): void { + if (preg_match('[(\d+)(?P\d+)]n', $s, $matches)) { + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php new file mode 100644 index 0000000000..d5e650e708 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php @@ -0,0 +1,29 @@ +', $matches); + return ''; + }, + $s + ); +}; + +function (string $s): void { + preg_replace_callback( + '|

(\s*)\w|', + function ($matches) { + assertType('array{non-falsy-string, string}', $matches); + return ''; + }, + $s + ); +}; + +// The flags parameter was added in PHP 7.4 diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php new file mode 100644 index 0000000000..7bd70492ee --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php @@ -0,0 +1,58 @@ +}, 1?: array{''|'foo', int<-1, max>}, 2?: array{''|'bar', int<-1, max>}, 3?: array{'baz', int<-1, max>}}", $matches); + return ''; + }, + $s, + -1, + $count, + PREG_OFFSET_CAPTURE + ); +}; + +function (string $s): void { + preg_replace_callback( + '/(foo)?(bar)?(baz)?/', + function ($matches) { + assertType("array{array{string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); + return ''; + }, + $s, + -1, + $count, + PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL + ); +}; + +function bug12792(string $string) : void { + preg_replace_callback( + '~\'(?:[^\']+|\'\')*+\'\K|\[(\w*)\]~', + function ($matches) { + assertType("array{0: string, 1?: string}", $matches); + return ''; + }, + $string + ); +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php new file mode 100644 index 0000000000..7122c16150 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -0,0 +1,47 @@ +|false', preg_split('/-/', '1-2-3')); + assertType('list|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); + assertType('list}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType('list}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + } + + /** + * @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('list}>|false', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); + + assertType('list}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); + } + + /** + * @param string $pattern + * @param string $subject + * @param int $limit + */ + public static function dynamicFlags($pattern, $subject, $limit = -1) { + $flags = PREG_SPLIT_OFFSET_CAPTURE; + + if ($subject === '1-2-3') { + $flags |= PREG_SPLIT_NO_EMPTY; + } + + assertType('list}>|false', preg_split($pattern, $subject, $limit, $flags)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/preserve-large-constant-array.php b/tests/PHPStan/Analyser/nsrt/preserve-large-constant-array.php new file mode 100644 index 0000000000..a66425e3dd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preserve-large-constant-array.php @@ -0,0 +1,67 @@ + $mixed, + 'shipping_tax' => $arrayMixed, + 'ecotax_tax' => $arrayMixed, + 'wrapping_tax' => $arrayMixed, + ]; + + foreach ($breakdowns as $type => $bd) { + if (empty($bd)) { + assertType('array{product_tax?: mixed, shipping_tax?: array, ecotax_tax?: array, wrapping_tax?: array}', $breakdowns); + unset($breakdowns[$type]); + assertType('array{product_tax?: mixed, shipping_tax?: array, ecotax_tax?: array, wrapping_tax?: array}', $breakdowns); + } + } + + assertType('array{product_tax?: mixed, shipping_tax?: array, ecotax_tax?: array, wrapping_tax?: array}', $breakdowns); + } + + public function doFoo(): void + { + $a = ['foo' => 1, 'bar' => 2]; + assertType('array{foo: 1, bar: 2}', $a); + unset($a['foo']); + assertType('array{bar: 2}', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/printf-errors-php8.php b/tests/PHPStan/Analyser/nsrt/printf-errors-php8.php new file mode 100644 index 0000000000..872332b073 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/printf-errors-php8.php @@ -0,0 +1,11 @@ += 8.0 + +namespace PrintFErrorsPhp8; + +use function PHPStan\Testing\assertType; + +function doFoo() +{ + assertType("string", sprintf('%s')); // error + assertType("string", vsprintf('%s')); // error +} diff --git a/tests/PHPStan/Analyser/data/proc_get_status.php b/tests/PHPStan/Analyser/nsrt/proc_get_status.php similarity index 100% rename from tests/PHPStan/Analyser/data/proc_get_status.php rename to tests/PHPStan/Analyser/nsrt/proc_get_status.php diff --git a/tests/PHPStan/Analyser/data/promoted-properties-types.php b/tests/PHPStan/Analyser/nsrt/promoted-properties-types.php similarity index 92% rename from tests/PHPStan/Analyser/data/promoted-properties-types.php rename to tests/PHPStan/Analyser/nsrt/promoted-properties-types.php index 7581c6dbe2..20c9aa11d1 100644 --- a/tests/PHPStan/Analyser/data/promoted-properties-types.php +++ b/tests/PHPStan/Analyser/nsrt/promoted-properties-types.php @@ -103,3 +103,16 @@ function (Baz $baz): void { assertType('array', $baz->anotherPhpDocArray); assertType('stdClass', $baz->templateProperty); }; + +class PromotedPropertyNotNullable +{ + + public function __construct( + public int $intProp = null, + ) {} + +} + +function (PromotedPropertyNotNullable $p) { + assertType('int', $p->intProp); +}; diff --git a/tests/PHPStan/Analyser/nsrt/property-hooks.php b/tests/PHPStan/Analyser/nsrt/property-hooks.php new file mode 100644 index 0000000000..8e32e4c96d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/property-hooks.php @@ -0,0 +1,377 @@ += 8.4 + +declare(strict_types=1); + +namespace PropertyHooksTypes; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public int $i { + set { + assertType('int', $value); + } + get { + return 1; + } + } + + public int $j { + set (int $val) { + assertType('int', $val); + } + } + + public int $k { + set (int|string $val) { + assertType('int|string', $val); + } + } + + /** @var array */ + public array $l { + set { + assertType('array', $value); + } + get { + return []; + } + } + + /** @var array */ + public array $m { + set (array $val) { + assertType('array', $val); + } + } + + public int $n { + /** @param int|array $val */ + set (int|array $val) { + assertType('array|int', $val); + } + } + +} + +class FooShort +{ + + public int $i { + set => assertType('int', $value); + } + + public int $j { + set (int $val) => assertType('int', $val); + } + + public int $k { + set (int|string $val) => assertType('int|string', $val); + } + + /** @var array */ + public array $l { + set => assertType('array', $value); + } + + /** @var array */ + public array $m { + set (array $val) => assertType('array', $val); + } + + public int $n { + /** @param int|array $val */ + set (int|array $val) => assertType('array|int', $val); + } + +} + +class FooConstructor +{ + + public function __construct( + public int $i { + set { + assertType('int', $value); + } + }, + public int $j { + set (int $val) { + assertType('int', $val); + } + }, + public int $k { + set (int|string $val) { + assertType('int|string', $val); + } + }, + /** @var array */ + public array $l { + set { + assertType('array', $value); + } + get { + return []; + } + }, + /** @var array */ + public array $m { + set (array $val) { + assertType('array', $val); + } + }, + public int $n { + /** @param int|array $val */ + set (int|array $val) { + assertType('array|int', $val); + } + }, + ) { + + } + +} + +class FooConstructorWithParam +{ + + /** + * @param array $l + * @param array $m + */ + public function __construct( + public array $l { + set { + assertType('array', $value); + } + get { + return []; + } + }, + public array $m { + set (array $val) { + assertType('array', $val); + } + }, + ) { + + } + +} + +/** + * @template T of \stdClass + */ +class FooGenerics +{ + + /** @var array */ + public array $m { + set (array $val) { + assertType('array', $val); + } + get { + + } + } + + public int $n { + /** @param int|array $val */ + set (int|array $val) { + assertType('array|int', $val); + } + get { + + } + } + +} + +/** + * @template T of \stdClass + */ +class FooGenericsConstructor +{ + + public function __construct( + /** @var array */ + public array $l { + set { + assertType('array', $value); + } + get { + + } + }, + /** @var array */ + public array $m { + set (array $val) { + assertType('array', $val); + } + get { + + } + }, + public int $n { + /** @param int|array $val */ + set (int|array $val) { + assertType('array|int', $val); + } + get { + + } + }, + ) { + + } + +} + +/** + * @template T of \stdClass + */ +class FooGenericsConstructor2 +{ + + /** + * @param array $l + * @param array $m + */ + public function __construct( + public array $l { + set { + assertType('array', $value); + } + }, + public array $m { + set (array $val) { + assertType('array', $val); + } + }, + public int $n { + /** @param int|array $val */ + set (int|array $val) { + assertType('array|int', $val); + } + }, + ) { + + } + +} + +class FooGenericsConstructorWithT +{ + + /** + * @template T of \stdClass + */ + public function __construct( + /** @var array */ + public array $l { + set { + assertType('array', $value); + } + }, + /** @var array */ + public array $m { + set (array $val) { + assertType('array', $val); + } + }, + ) { + + } + +} + +class FooGenericsConstructorWithT2 +{ + + /** + * @template T of \stdClass + * @param array $l + * @param array $m + */ + public function __construct( + public array $l { + set { + assertType('array', $value); + } + }, + public array $m { + set (array $val) { + assertType('array', $val); + } + }, + ) { + + } + +} + +class CanChangeTypeAfterAssignment +{ + + public int $i; + + public function doFoo(): void + { + assertType('int', $this->i); + $this->i = 1; + assertType('1', $this->i); + } + + public int $virtual { + get { + return 1; + } + set { + $this->i = 1; + } + } + + public function doFoo2(): void + { + assertType('int', $this->virtual); + $this->virtual = 1; + assertType('int', $this->virtual); + } + + public int $backedWithHook { + get { + return $this->backedWithHook + 100; + } + set { + $this->backedWithHook = $this->backedWithHook - 200; + } + } + + public function doFoo3(): void + { + assertType('int', $this->backedWithHook); + $this->backedWithHook = 1; + assertType('int', $this->backedWithHook); + } + +} + +class MagicConstants +{ + + public int $i { + get { + assertType("'\$i::get'", __FUNCTION__); + assertType("'PropertyHooksTypes\\\\MagicConstants::\$i::get'", __METHOD__); + assertType("'i'", __PROPERTY__); + } + set { + assertType("'\$i::set'", __FUNCTION__); + assertType("'PropertyHooksTypes\\\\MagicConstants::\$i::set'", __METHOD__); + assertType("'i'", __PROPERTY__); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/property-template-tag.php b/tests/PHPStan/Analyser/nsrt/property-template-tag.php similarity index 100% rename from tests/PHPStan/Analyser/data/property-template-tag.php rename to tests/PHPStan/Analyser/nsrt/property-template-tag.php diff --git a/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php b/tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php similarity index 91% rename from tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php rename to tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php index 036f9b996d..a2ae0bf2b7 100644 --- a/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php +++ b/tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php @@ -18,7 +18,7 @@ public function doFoo() public function doBar(): void { - assertType('array', $this->doFoo()); + assertType('list', $this->doFoo()); } /** diff --git a/tests/PHPStan/Analyser/nsrt/pure-callable.php b/tests/PHPStan/Analyser/nsrt/pure-callable.php new file mode 100644 index 0000000000..39ef172288 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pure-callable.php @@ -0,0 +1,18 @@ += 0); + \assert($max >= 0); assertType('int<0, max>', random_int(0, $max)); }; diff --git a/tests/PHPStan/Analyser/nsrt/range-int-range.php b/tests/PHPStan/Analyser/nsrt/range-int-range.php new file mode 100644 index 0000000000..f1846aad71 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/range-int-range.php @@ -0,0 +1,61 @@ + $a + * @param int<0,max> $b + */ + public function zeroToMax( + int $a, + int $b + ): void + { + assertType('list>', range($a, $b)); + } + + /** + * @param int<2,10> $a + * @param int<5,20> $b + */ + public function twoToTwenty( + int $a, + int $b + ): void + { + assertType('list>', range($a, $b)); + } + + /** + * @param int<10,30> $a + * @param int<5,20> $b + */ + public function fifteenTo5( + int $a, + int $b + ): void + { + assertType('list>', range($a, $b)); + } + + public function knownRange( + ): void + { + $a = 5; + $b = 10; + assertType('array{5, 6, 7, 8, 9, 10}', range($a, $b)); + } + + public function knownLargeRange( + ): void + { + $a = 5; + $b = 100; + assertType('non-empty-list>', range($a, $b)); + } +} diff --git a/tests/PHPStan/Analyser/data/range-numeric-string.php b/tests/PHPStan/Analyser/nsrt/range-numeric-string.php similarity index 80% rename from tests/PHPStan/Analyser/data/range-numeric-string.php rename to tests/PHPStan/Analyser/nsrt/range-numeric-string.php index faddec206b..bae424e559 100644 --- a/tests/PHPStan/Analyser/data/range-numeric-string.php +++ b/tests/PHPStan/Analyser/nsrt/range-numeric-string.php @@ -16,7 +16,7 @@ public function doFoo( string $b ): void { - assertType('array', range($a, $b)); + assertType('list', range($a, $b)); } } diff --git a/tests/PHPStan/Analyser/nsrt/range-to-string.php b/tests/PHPStan/Analyser/nsrt/range-to-string.php new file mode 100644 index 0000000000..49bb179309 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/range-to-string.php @@ -0,0 +1,22 @@ + $i + * @param int<-10, 10> $ii + * @param int<0, 128> $maxlong + * @param int<0, 129> $toolong + */ + public function sayHello($i, $ii, $maxlong, $toolong): void + { + assertType("'10'|'5'|'6'|'7'|'8'|'9'", (string) $i); + assertType("'-1'|'-10'|'-2'|'-3'|'-4'|'-5'|'-6'|'-7'|'-8'|'-9'|'0'|'1'|'10'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'", (string) $ii); + assertType("'0'|'1'|'10'|'100'|'101'|'102'|'103'|'104'|'105'|'106'|'107'|'108'|'109'|'11'|'110'|'111'|'112'|'113'|'114'|'115'|'116'|'117'|'118'|'119'|'12'|'120'|'121'|'122'|'123'|'124'|'125'|'126'|'127'|'128'|'13'|'14'|'15'|'16'|'17'|'18'|'19'|'2'|'20'|'21'|'22'|'23'|'24'|'25'|'26'|'27'|'28'|'29'|'3'|'30'|'31'|'32'|'33'|'34'|'35'|'36'|'37'|'38'|'39'|'4'|'40'|'41'|'42'|'43'|'44'|'45'|'46'|'47'|'48'|'49'|'5'|'50'|'51'|'52'|'53'|'54'|'55'|'56'|'57'|'58'|'59'|'6'|'60'|'61'|'62'|'63'|'64'|'65'|'66'|'67'|'68'|'69'|'7'|'70'|'71'|'72'|'73'|'74'|'75'|'76'|'77'|'78'|'79'|'8'|'80'|'81'|'82'|'83'|'84'|'85'|'86'|'87'|'88'|'89'|'9'|'90'|'91'|'92'|'93'|'94'|'95'|'96'|'97'|'98'|'99'", (string) $maxlong); + assertType("lowercase-string&numeric-string&uppercase-string", (string) $toolong); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/reflection-type.php b/tests/PHPStan/Analyser/nsrt/reflection-type.php new file mode 100644 index 0000000000..d390747fe6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/reflection-type.php @@ -0,0 +1,15 @@ +getType()); + assertType('ReflectionType|null', $reflectionFunctionAbstract->getReturnType()); + assertType('ReflectionType|null', $reflectionParameter->getType()); +} diff --git a/tests/PHPStan/Analyser/nsrt/reflectionclass-issue-5511-php8.php b/tests/PHPStan/Analyser/nsrt/reflectionclass-issue-5511-php8.php new file mode 100644 index 0000000000..d0a80299b2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/reflectionclass-issue-5511-php8.php @@ -0,0 +1,81 @@ += 8.0 + +declare(strict_types=1); + +namespace Issue5511; + +use function PHPStan\Testing\assertType; + +#[\Attribute] +class Abc +{ +} + +/** + * @param string $str + * @param class-string $className + * @param class-string $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('list>', $classAll); + assertType('list>', $classAbc1); + assertType('list>', $classAbc2); + assertType('list>', $classGCN); + assertType('list>', $classCN); + assertType('list>', $classStr); + assertType('list>', $classNonsense); + + $methodAll = $reflectionMethod->getAttributes(); + $methodAbc = $reflectionMethod->getAttributes(Abc::class); + assertType('list>', $methodAll); + assertType('list>', $methodAbc); + + $paramAll = $reflectionParameter->getAttributes(); + $paramAbc = $reflectionParameter->getAttributes(Abc::class); + assertType('list>', $paramAll); + assertType('list>', $paramAbc); + + $propAll = $reflectionProperty->getAttributes(); + $propAbc = $reflectionProperty->getAttributes(Abc::class); + assertType('list>', $propAll); + assertType('list>', $propAbc); + + $constAll = $reflectionClassConstant->getAttributes(); + $constAbc = $reflectionClassConstant->getAttributes(Abc::class); + assertType('list>', $constAll); + assertType('list>', $constAbc); + + $funcAll = $reflectionFunction->getAttributes(); + $funcAbc = $reflectionFunction->getAttributes(Abc::class); + assertType('list>', $funcAll); + assertType('list>', $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/nsrt/remember-non-nullable-property-non-strict.php b/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php new file mode 100644 index 0000000000..ed949a846f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php @@ -0,0 +1,88 @@ += 8.1 + +declare(strict_types = 0); + +namespace RememberNonNullablePropertyWhenStrictTypesDisabled; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class KeepsPropertyNonNullable { + private readonly int $i; + + public function __construct() + { + $this->i = getIntOrNull(); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +class DontCoercePhpdocType { + /** @var int */ + private $i; + + public function __construct() + { + $this->i = getIntOrNull(); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('mixed', $this->i); + } +} + +function getIntOrNull(): ?int { + if (rand(0, 1) === 0) { + return null; + } + return 1; +} + + +class KeepsPropertyNonNullable2 { + private int|float $i; + + public function __construct() + { + $this->i = getIntOrFloatOrNull(); + } + + public function doFoo(): void { + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } +} + +function getIntOrFloatOrNull(): null|int|float { + if (rand(0, 1) === 0) { + return null; + } + + if (rand(0, 10) === 0) { + return 1.0; + } + return 1; +} + +class NarrowsNativeUnion { + private readonly int|float $i; + + public function __construct() + { + $this->i = getInt(); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +function getInt(): int { + return 1; +} diff --git a/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php b/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php new file mode 100644 index 0000000000..9618bc818f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php @@ -0,0 +1,45 @@ += 8.1 + +declare(strict_types = 0); + +namespace RememberNullablePropertyWhenStrictTypesDisabled; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +interface ObjectDataMapper +{ + /** + * @template OutType of object + * + * @param literal-string&class-string $class + * @param mixed $data + * + * @return OutType + * + * @throws \Exception + */ + public function map(string $class, $data): object; +} + +final class ApiProductController +{ + + protected ?SearchProductsVM $searchProductsVM = null; + + protected static ?SearchProductsVM $searchProductsVMStatic = null; + + public function search(ObjectDataMapper $dataMapper): void + { + $this->searchProductsVM = $dataMapper->map(SearchProductsVM::class, $_REQUEST); + assertType('RememberNullablePropertyWhenStrictTypesDisabled\SearchProductsVM', $this->searchProductsVM); + } + + public function searchStatic(ObjectDataMapper $dataMapper): void + { + self::$searchProductsVMStatic = $dataMapper->map(SearchProductsVM::class, $_REQUEST); + assertType('RememberNullablePropertyWhenStrictTypesDisabled\SearchProductsVM', self::$searchProductsVMStatic); + } +} + +class SearchProductsVM {} diff --git a/tests/PHPStan/Analyser/nsrt/remember-possibly-impure-function-values.php b/tests/PHPStan/Analyser/nsrt/remember-possibly-impure-function-values.php new file mode 100644 index 0000000000..979a2d7d91 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-possibly-impure-function-values.php @@ -0,0 +1,111 @@ +pure() === 1) { + assertType('1', $this->pure()); + } + + if ($this->maybePure() === 1) { + assertType('1', $this->maybePure()); + } + + if ($this->impure() === 1) { + assertType('int', $this->impure()); + } + } + +} + +class FooStatic +{ + + /** @phpstan-pure */ + public static function pure(): int + { + return 1; + } + + public static function maybePure(): int + { + return 1; + } + + /** @phpstan-impure */ + public static function impure(): int + { + return rand(0, 1); + } + + public function test(): void + { + if (self::pure() === 1) { + assertType('1', self::pure()); + } + + if (self::maybePure() === 1) { + assertType('1', self::maybePure()); + } + + if (self::impure() === 1) { + assertType('int', self::impure()); + } + } + +} + +/** @phpstan-pure */ +function pure(): int +{ + return 1; +} + +function maybePure(): int +{ + return 1; +} + +/** @phpstan-impure */ +function impure(): int +{ + return rand(0, 1); +} + +function test(): void +{ + if (pure() === 1) { + assertType('1', pure()); + } + + if (maybePure() === 1) { + assertType('1', maybePure()); + } + + if (impure() === 1) { + assertType('int', impure()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed-hooks.php b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed-hooks.php new file mode 100644 index 0000000000..8f03858767 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed-hooks.php @@ -0,0 +1,35 @@ += 8.4 + +namespace RememberReadOnlyConstructorInPropertyHookBodies; + +use function PHPStan\Testing\assertType; + +class User +{ + public string $name { + get { + assertType('1|2', $this->type); + return $this->name ; + } + set { + if (strlen($value) === 0) { + throw new ValueError("Name must be non-empty"); + } + assertType('1|2', $this->type); + $this->name = $value; + } + } + + private readonly int $type; + + public function __construct( + string $name + ) { + $this->name = $name; + if (rand(0,1)) { + $this->type = 1; + } else { + $this->type = 2; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php new file mode 100644 index 0000000000..7ab2ea364f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php @@ -0,0 +1,109 @@ += 8.2 + +namespace RememberReadOnlyConstructor; + +use function PHPStan\Testing\assertType; + +class HelloWorldReadonlyProperty { + private readonly int $i; + + public function __construct() + { + if (rand(0,1)) { + $this->i = 4; + } else { + $this->i = 10; + } + } + + public function doFoo() { + assertType('4|10', $this->i); + } +} + +readonly class HelloWorldReadonlyClass { + private int $i; + private string $class; + private string $interface; + private string $enum; + private string $trait; + + public function __construct(string $class, string $interface, string $enum, string $trait) + { + if (rand(0,1)) { + $this->i = 4; + } else { + $this->i = 10; + } + + if (!class_exists($class)) { + throw new \LogicException(); + } + $this->class = $class; + + if (!interface_exists($interface)) { + throw new \LogicException(); + } + $this->interface = $interface; + + if (!enum_exists($enum)) { + throw new \LogicException(); + } + $this->enum = $enum; + + if (!trait_exists($trait)) { + throw new \LogicException(); + } + $this->trait = $trait; + } + + public function doFoo() { + assertType('4|10', $this->i); + assertType('class-string', $this->class); + assertType('class-string', $this->interface); + assertType('class-string', $this->enum); + assertType('class-string', $this->trait); + } +} + + +class HelloWorldRegular { + private int $i; + + public function __construct() + { + if (rand(0,1)) { + $this->i = 4; + } else { + $this->i = 10; + } + } + + public function doFoo() { + assertType('int', $this->i); + } +} + +class HelloWorldReadonlyPropertySometimesThrowing { + private readonly int $i; + + public function __construct() + { + if (rand(0,1)) { + $this->i = 4; + + return; + } elseif (rand(10,100)) { + $this->i = 10; + return; + } else { + $this->i = 20; + } + + throw new \LogicException(); + } + + public function doFoo() { + assertType('4|10', $this->i); + } +} diff --git a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php b/tests/PHPStan/Analyser/nsrt/root-scope-maybe-defined.php similarity index 100% rename from tests/PHPStan/Analyser/data/root-scope-maybe-defined.php rename to tests/PHPStan/Analyser/nsrt/root-scope-maybe-defined.php diff --git a/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php new file mode 100644 index 0000000000..c618f6c8d9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php @@ -0,0 +1,63 @@ += 8.0 + +declare(strict_types=1); + +namespace RoundFamilyTestPHP8StrictTypes; + +use function PHPStan\Testing\assertType; + +$maybeNull = null; +if (rand(0, 1)) { + $maybeNull = 1.0; +} + +// Round +assertType('float', round(123)); +assertType('float', round(123.456)); +assertType('float', round($_GET['foo'] / 60)); +assertType('*NEVER*', round('123')); +assertType('*NEVER*', round('123.456')); +assertType('*NEVER*', round(null)); +assertType('float', round($maybeNull)); +assertType('*NEVER*', round(true)); +assertType('*NEVER*', round(false)); +assertType('*NEVER*', round(new \stdClass)); +assertType('*NEVER*', round('')); +assertType('*NEVER*', round(array())); +assertType('*NEVER*', round(array(123))); +assertType('*NEVER*', round()); +assertType('float', round($_GET['foo'])); + +// Ceil +assertType('float', ceil(123)); +assertType('float', ceil(123.456)); +assertType('float', ceil($_GET['foo'] / 60)); +assertType('*NEVER*', ceil('123')); +assertType('*NEVER*', ceil('123.456')); +assertType('*NEVER*', ceil(null)); +assertType('float', ceil($maybeNull)); +assertType('*NEVER*', ceil(true)); +assertType('*NEVER*', ceil(false)); +assertType('*NEVER*', ceil(new \stdClass)); +assertType('*NEVER*', ceil('')); +assertType('*NEVER*', ceil(array())); +assertType('*NEVER*', ceil(array(123))); +assertType('*NEVER*', ceil()); +assertType('float', ceil($_GET['foo'])); + +// Floor +assertType('float', floor(123)); +assertType('float', floor(123.456)); +assertType('float', floor($_GET['foo'] / 60)); +assertType('*NEVER*', floor('123')); +assertType('*NEVER*', floor('123.456')); +assertType('*NEVER*', floor(null)); +assertType('float', floor($maybeNull)); +assertType('*NEVER*', floor(true)); +assertType('*NEVER*', floor(false)); +assertType('*NEVER*', floor(new \stdClass)); +assertType('*NEVER*', floor('')); +assertType('*NEVER*', floor(array())); +assertType('*NEVER*', floor(array(123))); +assertType('*NEVER*', floor()); +assertType('float', floor($_GET['foo'])); diff --git a/tests/PHPStan/Analyser/nsrt/round-php8.php b/tests/PHPStan/Analyser/nsrt/round-php8.php new file mode 100644 index 0000000000..54836b7623 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/round-php8.php @@ -0,0 +1,61 @@ += 8.0 + +namespace RoundFamilyTestPHP8; + +use function PHPStan\Testing\assertType; + +$maybeNull = null; +if (rand(0, 1)) { + $maybeNull = 1.0; +} + +// Round +assertType('float', round(123)); +assertType('float', round(123.456)); +assertType('float', round($_GET['foo'] / 60)); +assertType('float', round('123')); +assertType('float', round('123.456')); +assertType('float', round(null)); +assertType('float', round($maybeNull)); +assertType('float', round(true)); +assertType('float', round(false)); +assertType('*NEVER*', round(new \stdClass)); +assertType('*NEVER*', round('')); +assertType('*NEVER*', round(array())); +assertType('*NEVER*', round(array(123))); +assertType('*NEVER*', round()); +assertType('float', round($_GET['foo'])); + +// Ceil +assertType('float', ceil(123)); +assertType('float', ceil(123.456)); +assertType('float', ceil($_GET['foo'] / 60)); +assertType('float', ceil('123')); +assertType('float', ceil('123.456')); +assertType('float', ceil(null)); +assertType('float', ceil($maybeNull)); +assertType('float', ceil(true)); +assertType('float', ceil(false)); +assertType('*NEVER*', ceil(new \stdClass)); +assertType('*NEVER*', ceil('')); +assertType('*NEVER*', ceil(array())); +assertType('*NEVER*', ceil(array(123))); +assertType('*NEVER*', ceil()); +assertType('float', ceil($_GET['foo'])); + +// Floor +assertType('float', floor(123)); +assertType('float', floor(123.456)); +assertType('float', floor($_GET['foo'] / 60)); +assertType('float', floor('123')); +assertType('float', floor('123.456')); +assertType('float', floor(null)); +assertType('float', floor($maybeNull)); +assertType('float', floor(true)); +assertType('float', floor(false)); +assertType('*NEVER*', floor(new \stdClass)); +assertType('*NEVER*', floor('')); +assertType('*NEVER*', floor(array())); +assertType('*NEVER*', floor(array(123))); +assertType('*NEVER*', floor()); +assertType('float', floor($_GET['foo'])); diff --git a/tests/PHPStan/Analyser/data/round.php b/tests/PHPStan/Analyser/nsrt/round.php similarity index 98% rename from tests/PHPStan/Analyser/data/round.php rename to tests/PHPStan/Analyser/nsrt/round.php index 3552e2e602..3d181ca50a 100644 --- a/tests/PHPStan/Analyser/data/round.php +++ b/tests/PHPStan/Analyser/nsrt/round.php @@ -1,4 +1,4 @@ -', $builder); + assertType('PHPStan\Rules\RuleError', $builder->build()); + + $builder->identifier('test'); + assertType('PHPStan\Rules\RuleErrorBuilder', $builder); + assertType('PHPStan\Rules\IdentifierRuleError', $builder->build()); + + assertType('PHPStan\Rules\IdentifierRuleError', RuleErrorBuilder::message('test')->identifier('test')->build()); + + $builder->tip('test'); + assertType('PHPStan\Rules\RuleErrorBuilder', $builder); + assertType('PHPStan\Rules\IdentifierRuleError&PHPStan\Rules\TipRuleError', $builder->build()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/scope-generalization.php b/tests/PHPStan/Analyser/nsrt/scope-generalization.php new file mode 100644 index 0000000000..5a183e1d1c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/scope-generalization.php @@ -0,0 +1,35 @@ + */ + $foo = []; + for ($i = 0; $i < 3; $i++) { + array_push($foo, 'foo'); + } + assertType('non-empty-array', $foo); +} + +function loopRemovesAccessory(): void +{ + /** @var non-empty-array */ + $foo = []; + for ($i = 0; $i < 3; $i++) { + array_pop($foo); + } + assertType('array', $foo); +} + +function closureRemovesAccessoryOfReferenceParameter(): void +{ + /** @var non-empty-array */ + $foo = []; + static function () use (&$foo) { + assertType('array', $foo); + array_pop($foo); + }; +} diff --git a/tests/PHPStan/Analyser/nsrt/self-out.php b/tests/PHPStan/Analyser/nsrt/self-out.php new file mode 100644 index 0000000000..fa623d2d5a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/self-out.php @@ -0,0 +1,108 @@ + + */ + private array $data; + /** + * @param T $data + */ + public function __construct($data) { + $this->data = [$data]; + } + /** + * @template NewT + * + * @param NewT $data + * + * @phpstan-self-out self + * + * @return void + */ + public function addData($data) { + /** @var self $this */ + $this->data []= $data; + } + /** + * @template NewT + * + * @param NewT $data + * + * @phpstan-self-out self + * + * @return void + */ + public function setData($data) { + /** @var self $this */ + $this->data = [$data]; + } + /** + * @return ($this is a ? void : never) + */ + public function test(): void { + } + + /** + * @phpstan-self-out self + */ + public static function selfOutWithStaticMethod(): void + { + + } +} + +/** + * @template T + * @extends a + */ +class b extends a { + /** + * @param T $data + */ + public function __construct($data) { + parent::__construct($data); + } +} + +function () { + $i = new a(123); + // OK - $i is a<123> + assertType('SelfOut\\a', $i); + assertType('null', $i->test()); + + $i->addData(321); + // OK - $i is a<123|321> + assertType('SelfOut\\a', $i); + assertType('null', $i->test()); + + $i->setData("test"); + // IfThisIsMismatch - Class is not a as required + assertType('SelfOut\\a<\'test\'>', $i); + assertType('never', $i->test()); +}; + +function () { + $i = new b(123); + assertType('SelfOut\\b', $i); + + $i->addData(321); + assertType('SelfOut\\a', $i); + + $i->addData(random_bytes(3)); + assertType('SelfOut\\a', $i); + + $i->setData(true); + assertType('SelfOut\\a', $i); + + $i->selfOutWithStaticMethod(); + assertType('SelfOut\\a', $i); +}; diff --git a/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php new file mode 100644 index 0000000000..f7513c6045 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php @@ -0,0 +1,673 @@ + 'bar']; + settype($x, 'string'); + assertType('*ERROR*', $x); + + // array to int + $x = []; + settype($x, 'int'); + assertType('0', $x); + + $x = ['foo']; + settype($x, 'int'); + assertType('1', $x); + + $x = ['foo' => 'bar']; + settype($x, 'int'); + assertType('1', $x); + + // array to integer + $x = []; + settype($x, 'integer'); + assertType('0', $x); + + $x = ['foo']; + settype($x, 'integer'); + assertType('1', $x); + + $x = ['foo' => 'bar']; + settype($x, 'integer'); + assertType('1', $x); + + // array to float + $x = []; + settype($x, 'float'); + assertType('0.0', $x); + + $x = ['foo']; + settype($x, 'float'); + assertType('1.0', $x); + + $x = ['foo' => 'bar']; + settype($x, 'float'); + assertType('1.0', $x); + + // array to double + $x = []; + settype($x, 'double'); + assertType('0.0', $x); + + $x = ['foo']; + settype($x, 'double'); + assertType('1.0', $x); + + $x = ['foo' => 'bar']; + settype($x, 'double'); + assertType('1.0', $x); + + // array to bool + $x = []; + settype($x, 'bool'); + assertType('false', $x); + + $x = ['foo']; + settype($x, 'bool'); + assertType('true', $x); + + $x = ['foo' => 'bar']; + settype($x, 'bool'); + assertType('true', $x); + + // array to boolean + $x = []; + settype($x, 'boolean'); + assertType('false', $x); + + $x = ['foo']; + settype($x, 'boolean'); + assertType('true', $x); + + $x = ['foo' => 'bar']; + settype($x, 'boolean'); + assertType('true', $x); + + // array to array + $x = []; + settype($x, 'array'); + assertType('array{}', $x); + + $x = ['foo']; + settype($x, 'array'); + assertType("array{'foo'}", $x); + + $x = ['foo' => 'bar']; + settype($x, 'array'); + assertType("array{foo: 'bar'}", $x); + + // array to object + $x = []; + settype($x, 'object'); + assertType('stdClass', $x); + + $x = ['foo']; + settype($x, 'object'); + assertType("stdClass", $x); + + $x = ['foo' => 'bar']; + settype($x, 'object'); + assertType("stdClass", $x); + + // array to null + $x = []; + settype($x, 'null'); + assertType('null', $x); + + $x = ['foo']; + settype($x, 'null'); + assertType('null', $x); + + $x = ['foo' => 'bar']; + settype($x, 'null'); + assertType('null', $x); + + // object to string + $x = new stdClass(); + settype($x, 'string'); + assertType('*ERROR*', $x); + + // object to int + $x = new stdClass(); + settype($x, 'int'); + assertType('*ERROR*', $x); + + // object to integer + $x = new stdClass(); + settype($x, 'integer'); + assertType('*ERROR*', $x); + + // object to float + $x = new stdClass(); + settype($x, 'float'); + assertType('*ERROR*', $x); + + // object to double + $x = new stdClass(); + settype($x, 'double'); + assertType('*ERROR*', $x); + + // object to bool + $x = new stdClass(); + settype($x, 'bool'); + assertType('true', $x); + + // object to boolean + $x = new stdClass(); + settype($x, 'boolean'); + assertType('true', $x); + + // object to array + $x = new stdClass(); + settype($x, 'array'); + assertType('array', $x); + + // object to object + $x = new stdClass(); + settype($x, 'object'); + assertType('stdClass', $x); + + // object to null + $x = new stdClass(); + settype($x, 'null'); + assertType('null', $x); + + // null to string + $x = null; + settype($x, 'string'); + assertType("''", $x); + + // null to int + $x = null; + settype($x, 'int'); + assertType('0', $x); + + // null to integer + $x = null; + settype($x, 'integer'); + assertType('0', $x); + + // null to float + $x = null; + settype($x, 'float'); + assertType('0.0', $x); + + // null to double + $x = null; + settype($x, 'double'); + assertType('0.0', $x); + + // null to bool + $x = null; + settype($x, 'bool'); + assertType('false', $x); + + // null to boolean + $x = null; + settype($x, 'boolean'); + assertType('false', $x); + + // null to array + $x = null; + settype($x, 'array'); + assertType('array{}', $x); + + // null to object + $x = null; + settype($x, 'object'); + assertType('stdClass', $x); + + // null to null + $x = null; + settype($x, 'null'); + assertType('null', $x); + + // Mixed to non-constant. + settype($value, $castTo); + assertType("array|bool|float|int|stdClass|string|null", $value); +} diff --git a/tests/PHPStan/Analyser/data/shadowed-trait-methods.php b/tests/PHPStan/Analyser/nsrt/shadowed-trait-methods.php similarity index 100% rename from tests/PHPStan/Analyser/data/shadowed-trait-methods.php rename to tests/PHPStan/Analyser/nsrt/shadowed-trait-methods.php diff --git a/tests/PHPStan/Analyser/nsrt/shuffle.php b/tests/PHPStan/Analyser/nsrt/shuffle.php new file mode 100644 index 0000000000..6b699e598a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/shuffle.php @@ -0,0 +1,141 @@ +', $arr); + assertNativeType('list', $arr); + assertType('list>', array_keys($arr)); + assertType('list', array_values($arr)); + } + + public function normalArrays2(array $arr): void + { + /** @var non-empty-array $arr */ + shuffle($arr); + assertType('non-empty-list', $arr); + assertNativeType('list', $arr); + assertType('non-empty-list>', array_keys($arr)); + assertType('non-empty-list', array_values($arr)); + } + + public function normalArrays3(array $arr): void + { + /** @var array $arr */ + if (array_key_exists('foo', $arr)) { + shuffle($arr); + assertType('non-empty-list', $arr); + assertNativeType('non-empty-list', $arr); + assertType('non-empty-list>', array_keys($arr)); + assertType('non-empty-list', array_values($arr)); + } + } + + public function normalArrays4(array $arr): void + { + /** @var array $arr */ + if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { + shuffle($arr); + assertType('non-empty-list', $arr); + assertNativeType('non-empty-list', $arr); + assertType('non-empty-list>', array_keys($arr)); + assertType('non-empty-list', array_values($arr)); + } + } + + public function constantArrays1(array $arr): void + { + $arr = []; + shuffle($arr); + assertType('array{}', $arr); + assertNativeType('array{}', $arr); + assertType('array{}', array_keys($arr)); + assertType('array{}', array_values($arr)); + } + + public function constantArrays2(array $arr): void + { + /** @var array{0?: 1, 1?: 2, 2?: 3} $arr */ + shuffle($arr); + assertType('list<1|2|3>', $arr); + assertNativeType('list', $arr); + assertType('list<0|1|2>', array_keys($arr)); + assertType('list<1|2|3>', array_values($arr)); + } + + public function constantArrays3(array $arr): void + { + $arr = [1, 2, 3]; + shuffle($arr); + assertType('non-empty-list<1|2|3>', $arr); + assertNativeType('non-empty-list<1|2|3>', $arr); + assertType('non-empty-list<0|1|2>', array_keys($arr)); + assertType('non-empty-list<1|2|3>', array_values($arr)); + } + + public function constantArrays4(array $arr): void + { + $arr = ['a' => 1, 'b' => 2, 'c' => 3]; + shuffle($arr); + assertType('non-empty-list<1|2|3>', $arr); + assertNativeType('non-empty-list<1|2|3>', $arr); + assertType('non-empty-list<0|1|2>', array_keys($arr)); + assertType('non-empty-list<1|2|3>', array_values($arr)); + } + + public function constantArrays5(array $arr): void + { + $arr = [0 => 1, 3 => 2, 42 => 3]; + shuffle($arr); + assertType('non-empty-list<1|2|3>', $arr); + assertNativeType('non-empty-list<1|2|3>', $arr); + assertType('non-empty-list<0|1|2>', array_keys($arr)); + assertType('non-empty-list<1|2|3>', array_values($arr)); + } + + public function constantArrays6(array $arr): void + { + /** @var array{foo?: 1, bar: 2, }|array{baz: 3, foobar?: 4} $arr */ + shuffle($arr); + assertType('non-empty-list<1|2|3|4>', $arr); + assertNativeType('list', $arr); + assertType('non-empty-list<0|1>', array_keys($arr)); + assertType('non-empty-list<1|2|3|4>', array_values($arr)); + } + + public function mixed($arr): void + { + shuffle($arr); + assertType('list', $arr); + assertNativeType('list', $arr); + assertType('list>', array_keys($arr)); + assertType('list', array_values($arr)); + } + + public function subtractedArray($arr): void + { + if (is_array($arr)) { + shuffle($arr); + assertType('list', $arr); + assertNativeType('list', $arr); + assertType('list>', array_keys($arr)); + assertType('list', array_values($arr)); + } else { + shuffle($arr); + assertType('*ERROR*', $arr); + assertNativeType('*ERROR*', $arr); + assertType('list', array_keys($arr)); + assertType('list', array_values($arr)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/simplexml.php b/tests/PHPStan/Analyser/nsrt/simplexml.php new file mode 100644 index 0000000000..69cea239d8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/simplexml.php @@ -0,0 +1,92 @@ +item); + foreach ($data->item as $item) { + assertType('SimpleXMLElement', $item); + assertType('SimpleXMLElement|null', $item['name']); + } + } + +} + +class Bar extends SimpleXMLElement +{ + + public function getAddressByGps() + { + /** @var self|null $data */ + $data = doFoo(); + + if ($data === null) { + return; + } + + assertType('(SimpleXMLIteratorBug\Bar|null)', $data->item); + foreach ($data->item as $item) { + assertType(self::class, $item); + assertType('SimpleXMLIteratorBug\Bar|null', $item['name']); + } + } + +} + +class Baz +{ + + public function getAddressByGps() + { + /** @var Bar|null $data */ + $data = doFoo(); + + if ($data === null) { + return; + } + + assertType('(SimpleXMLIteratorBug\Bar|null)', $data->item); + foreach ($data->item as $item) { + assertType(Bar::class, $item); + assertType('SimpleXMLIteratorBug\Bar|null', $item['name']); + } + } + +} + +class AsXML +{ + + public function asXML(): void + { + $element = new SimpleXMLElement(''); + + assertType('string|false', $element->asXML()); + + assertType('bool', $element->asXML('/tmp/foo.xml')); + } + + public function saveXML(): void + { + $element = new SimpleXMLElement(''); + + assertType('string|false', $element->saveXML()); + + assertType('bool', $element->saveXML('/tmp/foo.xml')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/sizeof-php8.php b/tests/PHPStan/Analyser/nsrt/sizeof-php8.php new file mode 100644 index 0000000000..a681a9f906 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/sizeof-php8.php @@ -0,0 +1,63 @@ += 8.0 + +namespace sizeof_php8; + +use function PHPStan\Testing\assertType; + + +class Sizeof +{ + /** + * @param int[] $ints + */ + function doFoo1(array $ints): string + { + if (count($ints) <= 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + } + + /** + * @param int[] $ints + */ + function doFoo2(array $ints): string + { + if (sizeof($ints) <= 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + } + + function doFoo3(array $arr): string + { + if (0 != count($arr)) { + assertType('non-empty-array', $arr); + } + return ""; + } + + function doFoo4(array $arr): string + { + if (0 != sizeof($arr)) { + assertType('non-empty-array', $arr); + } + return ""; + } + + function doFoo5(array $arr): void + { + if ([] != $arr) { + assertType('non-empty-array', $arr); + } + assertType('array', $arr); + } + + function doFoo6(array $arr): void + { + if ($arr != []) { + assertType('non-empty-array', $arr); + } + assertType('array', $arr); + } +} diff --git a/tests/PHPStan/Analyser/data/sizeof.php b/tests/PHPStan/Analyser/nsrt/sizeof.php similarity index 97% rename from tests/PHPStan/Analyser/data/sizeof.php rename to tests/PHPStan/Analyser/nsrt/sizeof.php index 1a080946c4..eec773844c 100644 --- a/tests/PHPStan/Analyser/data/sizeof.php +++ b/tests/PHPStan/Analyser/nsrt/sizeof.php @@ -1,4 +1,4 @@ - 1, + 'five' => 5, + 'three' => 3, + ]; + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|3|4|5>', $arr1); + assertNativeType('non-empty-list<1|3|4|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|3|4|5>', $arr2); + assertNativeType('non-empty-list<1|3|4|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|3|4|5>', $arr3); + assertNativeType('non-empty-list<1|3|4|5>', $arr3); + } + + public function constantArrayOptionalKey(): void + { + $arr = [ + 'one' => 1, + 'five' => 5, + ]; + if (rand(0, 1)) { + $arr['two'] = 2; + } + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|2|5>', $arr1); + assertNativeType('non-empty-list<1|2|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|2|5>', $arr2); + assertNativeType('non-empty-list<1|2|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|2|5>', $arr3); + assertNativeType('non-empty-list<1|2|5>', $arr3); + } + + public function constantArrayUnion(): void + { + $arr = rand(0, 1) ? [ + 'one' => 1, + 'five' => 5, + ] : [ + 'two' => 2, + ]; + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|2|5>', $arr1); + assertNativeType('non-empty-list<1|2|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|2|5>', $arr2); + assertNativeType('non-empty-list<1|2|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|2|5>', $arr3); + assertNativeType('non-empty-list<1|2|5>', $arr3); + } + + /** @param array $arr */ + public function normalArray(array $arr): void + { + $arr1 = $arr; + sort($arr1); + assertType('list', $arr1); + assertNativeType('list', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('list', $arr2); + assertNativeType('list', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('list', $arr3); + assertNativeType('list', $arr3); + } + + public function mixed($arr): void + { + $arr1 = $arr; + sort($arr1); + assertType('mixed', $arr1); + assertNativeType('mixed', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('mixed', $arr2); + assertNativeType('mixed', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('mixed', $arr3); + assertNativeType('mixed', $arr3); + } + + public function notArray(): void + { + $arr = 'foo'; + sort($arr); + assertType("'foo'", $arr); + } +} + +class Bar +{ + + /** + * @template T + * @param T&list $array + * @return list + */ + public function doFoo(array $array) + { + assertType('list&T (method Sort\Bar::doFoo(), argument)', $array); + usort($array, function (array $a, array $b) { + return $a['a'] <=> $b['a']; + }); + + assertType('list&T (method Sort\Bar::doFoo(), argument)', $array); + + return $array; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/specified-types-closure-edge.php b/tests/PHPStan/Analyser/nsrt/specified-types-closure-edge.php new file mode 100644 index 0000000000..f4e396bb79 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/specified-types-closure-edge.php @@ -0,0 +1,26 @@ +name instanceof Identifier && $bar->name instanceof Identifier) { + function () use ($call): void { + assertType('PhpParser\Node\Identifier', $call->name); + assertType('mixed', $bar->name); + }; + + assertType('PhpParser\Node\Identifier', $call->name); + } + } + + public function doBar(MethodCall $call, MethodCall $bar): void + { + if ($call->name instanceof Identifier && $bar->name instanceof Identifier) { + $a = 1; + function () use ($call, &$a): void { + assertType('PhpParser\Node\Identifier', $call->name); + assertType('mixed', $bar->name); + }; + + assertType('PhpParser\Node\Identifier', $call->name); + } + } + + public function doBaz(array $arr, string $key): void + { + $arr[$key] = 'test'; + assertType('non-empty-array', $arr); + assertType("'test'", $arr[$key]); + function ($arr) use ($key): void { + assertType('string', $key); + assertType('mixed', $arr); + assertType('mixed', $arr[$key]); + }; + } + public function doBuzz(array $arr, string $key): void + { + if (isset($arr[$key])) { + assertType('array', $arr); + assertType("mixed~null", $arr[$key]); + function () use ($arr, $key): void { + assertType('array', $arr); + assertType("mixed~null", $arr[$key]); + }; + } + } + + public function doBuzz(array $arr, string $key): void + { + if (isset($arr[$key])) { + assertType('array', $arr); + assertType("mixed~null", $arr[$key]); + function ($key) use ($arr): void { + assertType('array', $arr); + assertType("mixed", $arr[$key]); + }; + } + } + +} diff --git a/tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php b/tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php similarity index 88% rename from tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php rename to tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php index 4c512bea06..10b3859354 100644 --- a/tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php +++ b/tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php @@ -1,5 +1,7 @@ = 8.2 + +namespace StandaloneTypes; + +use function PHPStan\Testing\assertType; + +function foo(): null { + return null; +} +function bar(): false { + return false; +} + +class standalone { + public false $f = false; + public null $n = null; + + function foo(): null { + return null; + } + function bar(): false { + return false; + } +} + +function takesNull(null $n) { + assertType('null', $n); +} + +function takesFalse(false $f) { + assertType('false', $f); +} + + +function doFoo() { + assertType('null', foo()); + assertType('false', bar()); + + $s = new standalone(); + + assertType('null', $s->foo()); + assertType('false', $s->bar()); + + assertType('null', $s->n); + assertType('false', $s->f); +} diff --git a/tests/PHPStan/Analyser/nsrt/static-has-method.php b/tests/PHPStan/Analyser/nsrt/static-has-method.php new file mode 100644 index 0000000000..b00640bc91 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/static-has-method.php @@ -0,0 +1,21 @@ +retStaticConst()); + assertType('bool', X::retStaticConst()); + assertType('*ERROR*', $clUnioned->retStaticConst()); // should be bool|int https://github.com/phpstan/phpstan/issues/11687 + + assertType('int', A::retStaticConst(...)()); + assertType('2', B::retStaticConst(...)()); + assertType('2', self::retStaticConst(...)()); + assertType('2', static::retStaticConst(...)()); + assertType('int', parent::retStaticConst(...)()); + assertType('2', $this->retStaticConst(...)()); + assertType('bool', X::retStaticConst(...)()); + assertType('mixed', $clUnioned->retStaticConst(...)()); // should be bool|int https://github.com/phpstan/phpstan/issues/11687 + + assertType('StaticLateBinding\A', A::retStatic()); + assertType('StaticLateBinding\B', B::retStatic()); + assertType('static(StaticLateBinding\B)', self::retStatic()); + assertType('static(StaticLateBinding\B)', static::retStatic()); + assertType('static(StaticLateBinding\B)', parent::retStatic()); + assertType('static(StaticLateBinding\B)', $this->retStatic()); + assertType('bool', X::retStatic()); + assertType('bool|StaticLateBinding\A|StaticLateBinding\X', $clUnioned::retStatic()); // should be bool|StaticLateBinding\A https://github.com/phpstan/phpstan/issues/11687 + + assertType('StaticLateBinding\A', A::retStatic(...)()); + assertType('StaticLateBinding\B', B::retStatic(...)()); + assertType('static(StaticLateBinding\B)', self::retStatic(...)()); + assertType('static(StaticLateBinding\B)', static::retStatic(...)()); + assertType('static(StaticLateBinding\B)', parent::retStatic(...)()); + assertType('static(StaticLateBinding\B)', $this->retStatic(...)()); + assertType('bool', X::retStatic(...)()); + assertType('mixed', $clUnioned::retStatic(...)()); // should be bool|StaticLateBinding\A https://github.com/phpstan/phpstan/issues/11687 + + assertType('static(StaticLateBinding\B)', A::retNonStatic()); + assertType('static(StaticLateBinding\B)', B::retNonStatic()); + assertType('static(StaticLateBinding\B)', self::retNonStatic()); + assertType('static(StaticLateBinding\B)', static::retNonStatic()); + assertType('static(StaticLateBinding\B)', parent::retNonStatic()); + assertType('static(StaticLateBinding\B)', $this->retNonStatic()); + assertType('bool', X::retNonStatic()); + assertType('*ERROR*', $clUnioned->retNonStatic()); // should be bool|static(StaticLateBinding\B) https://github.com/phpstan/phpstan/issues/11687 + + A::outStaticConst($v); + assertType('int', $v); + B::outStaticConst($v); + assertType('2', $v); + self::outStaticConst($v); + assertType('2', $v); + static::outStaticConst($v); + assertType('2', $v); + parent::outStaticConst($v); + assertType('int', $v); + $this->outStaticConst($v); + assertType('2', $v); + X::outStaticConst($v); + assertType('bool', $v); + $clUnioned->outStaticConst($v); + assertType('bool', $v); // should be bool|int + } +} + +class X +{ + public static function retStaticConst(): bool + { + return false; + } + + /** + * @param-out bool $out + */ + public static function outStaticConst(&$out): void + { + $out = false; + } + + public static function retStatic(): bool + { + return false; + } + + public function retNonStatic(): bool + { + return false; + } +} diff --git a/tests/PHPStan/Analyser/data/static-methods.php b/tests/PHPStan/Analyser/nsrt/static-methods.php similarity index 100% rename from tests/PHPStan/Analyser/data/static-methods.php rename to tests/PHPStan/Analyser/nsrt/static-methods.php diff --git a/tests/PHPStan/Analyser/data/static-properties.php b/tests/PHPStan/Analyser/nsrt/static-properties.php similarity index 100% rename from tests/PHPStan/Analyser/data/static-properties.php rename to tests/PHPStan/Analyser/nsrt/static-properties.php diff --git a/tests/PHPStan/Analyser/nsrt/str-casing.php b/tests/PHPStan/Analyser/nsrt/str-casing.php new file mode 100644 index 0000000000..ebdbd8054d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/str-casing.php @@ -0,0 +1,105 @@ += 7.3 + +namespace StrCasingReturnType; + +use function PHPStan\Testing\assertType; + +class Foo { + /** + * @param numeric-string $numericS + * @param non-empty-string $nonE + * @param lowercase-string $lowercaseS + * @param literal-string $literal + * @param 'foo'|'Foo' $edgeUnion + * @param MB_CASE_UPPER|MB_CASE_LOWER|MB_CASE_TITLE|MB_CASE_FOLD|MB_CASE_UPPER_SIMPLE|MB_CASE_LOWER_SIMPLE|MB_CASE_TITLE_SIMPLE|MB_CASE_FOLD_SIMPLE $caseMode + * @param 'aKV'|'hA'|'AH'|'K'|'KV'|'RNKV' $kanaMode + * @param mixed $mixed + */ + public function bar($numericS, $nonE, $lowercaseS, $literal, $edgeUnion, $caseMode, $kanaMode, $mixed) { + assertType("'abc'", strtolower('ABC')); + assertType("'ABC'", strtoupper('abc')); + assertType("'abc'", mb_strtolower('ABC')); + assertType("'ABC'", mb_strtoupper('abc')); + assertType("'abc'", mb_strtolower('ABC', 'UTF-8')); + assertType("'ABC'", mb_strtoupper('abc', 'UTF-8')); + assertType("'abc'", mb_strtolower('Abc')); + assertType("'ABC'", mb_strtoupper('Abc')); + assertType("'aBC'", lcfirst('ABC')); + assertType("'Abc'", ucfirst('abc')); + assertType("'Hello World'", ucwords('hello world')); + assertType("'Hello|World'", ucwords('hello|world', "|")); + assertType("'ČESKÁ REPUBLIKA'", mb_convert_case('Česká republika', MB_CASE_UPPER)); + assertType("'česká republika'", mb_convert_case('Česká republika', MB_CASE_LOWER)); + assertType("non-falsy-string", mb_convert_case('Česká republika', $mixed)); + assertType("'ČESKÁ REPUBLIKA'|'Česká Republika'|'česká republika'", mb_convert_case('Česká republika', $caseMode)); + assertType("'Abc123アイウガギグばびぶ漢字'", mb_convert_kana('Abc123アイウガギグばびぶ漢字')); + assertType("'Abc123アイウガギグばびぶ漢字'", mb_convert_kana('Abc123アイウガギグばびぶ漢字', 'aKV')); + assertType("'Abc123アイウガギグバビブ漢字'", mb_convert_kana('Abc123アイウガギグばびぶ漢字', 'hA')); + assertType("'Abc123アガば漢'|'Abc123あか゛ば漢'|'Abc123アカ゛ば漢'|'Abc123アガば漢'|'Abc123アガバ漢'", mb_convert_kana('Abc123アガば漢', $kanaMode)); + assertType("non-falsy-string", mb_convert_kana('Abc123アガば漢', $mixed)); + + assertType("lowercase-string&numeric-string", strtolower($numericS)); + assertType("numeric-string&uppercase-string", strtoupper($numericS)); + assertType("lowercase-string&numeric-string", mb_strtolower($numericS)); + assertType("numeric-string&uppercase-string", mb_strtoupper($numericS)); + assertType("numeric-string", lcfirst($numericS)); + assertType("numeric-string", ucfirst($numericS)); + assertType("numeric-string", ucwords($numericS)); + assertType("numeric-string&uppercase-string", mb_convert_case($numericS, MB_CASE_UPPER)); + assertType("lowercase-string&numeric-string", mb_convert_case($numericS, MB_CASE_LOWER)); + assertType("numeric-string", mb_convert_case($numericS, $mixed)); + assertType("numeric-string", mb_convert_kana($numericS)); + assertType("numeric-string", mb_convert_kana($numericS, $mixed)); + + assertType("lowercase-string&non-empty-string", strtolower($nonE)); + assertType("non-empty-string&uppercase-string", strtoupper($nonE)); + assertType("lowercase-string&non-empty-string", mb_strtolower($nonE)); + assertType("non-empty-string&uppercase-string", mb_strtoupper($nonE)); + assertType("non-empty-string", lcfirst($nonE)); + assertType("non-empty-string", ucfirst($nonE)); + assertType("non-empty-string", ucwords($nonE)); + assertType("non-empty-string&uppercase-string", mb_convert_case($nonE, MB_CASE_UPPER)); + assertType("lowercase-string&non-empty-string", mb_convert_case($nonE, MB_CASE_LOWER)); + assertType("non-empty-string", mb_convert_case($nonE, $mixed)); + assertType("non-empty-string", mb_convert_kana($nonE)); + assertType("non-empty-string", mb_convert_kana($nonE, $mixed)); + + assertType("lowercase-string", strtolower($literal)); + assertType("uppercase-string", strtoupper($literal)); + assertType("lowercase-string", mb_strtolower($literal)); + assertType("uppercase-string", mb_strtoupper($literal)); + assertType("string", lcfirst($literal)); + assertType("string", ucfirst($literal)); + assertType("string", ucwords($literal)); + assertType("uppercase-string", mb_convert_case($literal, MB_CASE_UPPER)); + assertType("lowercase-string", mb_convert_case($literal, MB_CASE_LOWER)); + assertType("string", mb_convert_case($literal, $mixed)); + assertType("string", mb_convert_kana($literal)); + assertType("string", mb_convert_kana($literal, $mixed)); + + assertType("lowercase-string", strtolower($lowercaseS)); + assertType("uppercase-string", strtoupper($lowercaseS)); + assertType("lowercase-string", mb_strtolower($lowercaseS)); + assertType("uppercase-string", mb_strtoupper($lowercaseS)); + assertType("lowercase-string", lcfirst($lowercaseS)); + assertType("string", ucfirst($lowercaseS)); + assertType("string", ucwords($lowercaseS)); + assertType("uppercase-string", mb_convert_case($lowercaseS, MB_CASE_UPPER)); + assertType("lowercase-string", mb_convert_case($lowercaseS, MB_CASE_LOWER)); + assertType("string", mb_convert_case($lowercaseS, $mixed)); + assertType("lowercase-string", mb_convert_case($lowercaseS, rand(0, 1) ? MB_CASE_LOWER : MB_CASE_LOWER_SIMPLE)); + assertType("string", mb_convert_kana($lowercaseS)); + assertType("string", mb_convert_kana($lowercaseS, $mixed)); + + assertType("'foo'", lcfirst($edgeUnion)); + } + + public function foo() { + // invalid char conversions still lead to non-falsy-string + assertType("lowercase-string&non-falsy-string", mb_strtolower("\xfe\xff\x65\xe5\x67\x2c\x8a\x9e", 'CP1252')); + // valid char sequence, but not support non ASCII / UTF-8 encodings + assertType("non-falsy-string", mb_convert_kana("\x95\x5c\x8c\xbb", 'SJIS-win')); + // invalid UTF-8 sequence + assertType("non-falsy-string", mb_convert_kana("\x95\x5c\x8c\xbb", 'UTF-8')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/str-shuffle.php b/tests/PHPStan/Analyser/nsrt/str-shuffle.php new file mode 100644 index 0000000000..37aa768525 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/str-shuffle.php @@ -0,0 +1,19 @@ += 8.3 + +declare(strict_types = 1); + +namespace StrDecrementFunctionReturn; + +use function PHPStan\Testing\assertType; + +/** + * @param non-empty-string $s + */ +function foo(string $s): void +{ + assertType('non-empty-string', str_decrement($s)); + assertType('*ERROR*', str_decrement('')); + assertType('*ERROR*', str_decrement('0')); + assertType('*ERROR*', str_decrement('0.0')); + assertType('*ERROR*', str_decrement('1.0')); + assertType('*ERROR*', str_decrement('a')); + assertType('*ERROR*', str_decrement('A')); + assertType('*ERROR*', str_decrement('=')); + assertType('*ERROR*', str_decrement('字')); + assertType("'0'", str_decrement('1')); + assertType("'8'", str_decrement('9')); + assertType("'9'", str_decrement('10')); + assertType("'10'", str_decrement('11')); + assertType("'1d'", str_decrement('1e')); + assertType("'1e'", str_decrement('1f')); + assertType("'18'", str_decrement('19')); + assertType("'19'", str_decrement('20')); + assertType("'z'", str_decrement('1a')); + assertType("'1f0'", str_decrement('1f1')); + assertType("'x'", str_decrement('y')); + assertType("'y'", str_decrement('z')); + assertType("'y9'", str_decrement('z0')); + assertType("'z'", str_decrement('aa')); + assertType("'zy'", str_decrement('zz')); +} + +/** + * @param 'b'|'1' $s1 + * @param 1|string $s2 + */ +function union($s1, $s2): void +{ + assertType("'0'|'a'", str_decrement($s1)); + assertType('non-empty-string', str_decrement($s2)); +} + +/** + * @param 'b'|'' $s + */ +function unionContainsInvalidInput($s): void +{ + assertType("'a'", str_decrement($s)); +} diff --git a/tests/PHPStan/Analyser/nsrt/str_increment.php b/tests/PHPStan/Analyser/nsrt/str_increment.php new file mode 100644 index 0000000000..36fde68d6d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/str_increment.php @@ -0,0 +1,56 @@ += 8.3 + +declare(strict_types = 1); + +namespace StrIncrementFunctionReturn; + +use function PHPStan\Testing\assertType; + +/** + * @param non-empty-string $s + */ +function foo(string $s) +{ + assertType('non-falsy-string', str_increment($s)); + assertType('*ERROR*', str_increment('')); + assertType('*ERROR*', str_increment('=')); + assertType('*ERROR*', str_increment('0.0')); + assertType('*ERROR*', str_increment('1.0')); + assertType('*ERROR*', str_increment('字')); + assertType("'1'", str_increment('0')); + assertType("'2'", str_increment('1')); + assertType("'b'", str_increment('a')); + assertType("'B'", str_increment('A')); + assertType("'10'", str_increment('9')); + assertType("'11'", str_increment('10')); + assertType("'20'", str_increment('19')); + assertType("'1b'", str_increment('1a')); + assertType("'1f'", str_increment('1e')); + assertType("'1g'", str_increment('1f')); + assertType("'2a'", str_increment('1z')); + assertType("'10a'", str_increment('9z')); + assertType("'b'", str_increment('a')); + assertType("'1f2'", str_increment('1f1')); + assertType("'z'", str_increment('y')); + assertType("'aa'", str_increment('z')); + assertType("'z1'", str_increment('z0')); + assertType("'aaa'", str_increment('zz')); +} + +/** + * @param 'b'|'1' $s1 + * @param 1|string $s2 + */ +function union($s1, $s2): void +{ + assertType("'2'|'c'", str_increment($s1)); + assertType('non-falsy-string', str_increment($s2)); +} + +/** + * @param 'b'|'' $s + */ +function unionContainsInvalidInput($s): void +{ + assertType("'c'", str_increment($s)); +} diff --git a/tests/PHPStan/Analyser/nsrt/string-offsets.php b/tests/PHPStan/Analyser/nsrt/string-offsets.php new file mode 100644 index 0000000000..449246f707 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/string-offsets.php @@ -0,0 +1,49 @@ + $oneToThree + * @param int<3, 10> $threeToTen + * @param int<10, max> $tenOrMore + * @param int<-10, -5> $negative + * @param int $smallerMinusSix + * @param lowercase-string $lowercase + * + * @return void + */ +function doFoo($oneToThree, $threeToTen, $tenOrMore, $negative, int $smallerMinusSix, int $i, string $lowercase) { + $s = "world"; + if (rand(0, 1)) { + $s = "hello"; + } + + assertType("''|'d'|'e'|'h'|'l'|'o'|'r'|'w'", $s[$i]); + + assertType("'h'|'w'", $s[0]); + + assertType("'e'|'l'|'o'|'r'", $s[$oneToThree]); + assertType('*ERROR*', $s[$tenOrMore]); + assertType("''|'d'|'l'|'o'", $s[$threeToTen]); + assertType("non-empty-string", $s[$negative]); + assertType("*ERROR*", $s[$smallerMinusSix]); + + $longString = "myF5HnJv799kWf8VRI7g97vwnABTwN9y2CzAVELCBfRqyqkdTzXg7BkGXcwuIOscAiT6tSuJGzVZOJnYXvkiKQzYBNjjkCPOzSKXR5YHRlVxV1BetqZz4XOmaH9mtacJ9azNYL6bNXezSBjX13BSZy02SK2udzQLbTPNQwlKadKaNkUxjtWegkb8QDFaXbzH1JENVSLVH0FYd6POBU82X1xu7FDDKYLzwsWJHBGVhG8iugjEGwLj22x5ViosUyKR"; + assertType("non-empty-string", $longString[$i]); + + assertType("lowercase-string&non-empty-string", $lowercase[$i]); +} + +function bug12122() +{ + // see https://3v4l.org/8EMdX + $foo = 'fo'; + assertType('*ERROR*', $foo[2]); + assertType("'o'", $foo[1]); + assertType("'f'", $foo[0]); + assertType("'o'", $foo[-1]); + assertType("'f'", $foo[-2]); + assertType('*ERROR*', $foo[-3]); +} diff --git a/tests/PHPStan/Analyser/nsrt/string-union.php b/tests/PHPStan/Analyser/nsrt/string-union.php new file mode 100644 index 0000000000..5308521baf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/string-union.php @@ -0,0 +1,20 @@ += 7.2 + +namespace StrlenIntRange; + +use function PHPStan\Testing\assertType; + +/** + * @param int<0, 3> $zeroToThree + * @param int<2, 3> $twoOrThree + * @param int<2, max> $twoOrMore + * @param int $maxThree + * @param 10|11 $tenOrEleven + * @param 0|11 $zeroOrEleven + * @param int<-10,-5> $negative + */ +function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven, $zeroOrEleven, int $negative): void +{ + if (strlen($s) >= $zeroToThree) { + assertType('string', $s); + } + if (strlen($s) > $zeroToThree) { + assertType('non-empty-string', $s); + } + + if (strlen($s) >= $twoOrThree) { + assertType('non-falsy-string', $s); + } + if (strlen($s) > $twoOrThree) { + assertType('non-falsy-string', $s); + } + + if (strlen($s) > $twoOrMore) { + assertType('non-falsy-string', $s); + } + + $oneOrMore = $twoOrMore-1; + if (strlen($s) > $oneOrMore) { + assertType('non-falsy-string', $s); + } + if (strlen($s) >= $oneOrMore) { + assertType('non-empty-string', $s); + } + if (strlen($s) <= $oneOrMore) { + assertType('string', $s); + } else { + assertType('non-falsy-string', $s); + } + + if (strlen($s) > $maxThree) { + assertType('string', $s); + } + + if (strlen($s) > $tenOrEleven) { + assertType('non-falsy-string', $s); + } + + if (strlen($s) == $zeroToThree) { + assertType('string', $s); + } + if (strlen($s) === $zeroToThree) { + assertType('string', $s); + } + + if (strlen($s) == $twoOrThree) { + assertType('non-falsy-string', $s); + } + if (strlen($s) === $twoOrThree) { + assertType('non-falsy-string', $s); + } + + if (strlen($s) == $oneOrMore) { + assertType('non-empty-string', $s); + } + if (strlen($s) === $oneOrMore) { + assertType('non-empty-string', $s); + } + + if (strlen($s) == $tenOrEleven) { + assertType('non-falsy-string', $s); + } + if (strlen($s) === $tenOrEleven) { + assertType('non-falsy-string', $s); + } + if ($tenOrEleven == strlen($s)) { + assertType('non-falsy-string', $s); + } + if ($tenOrEleven === strlen($s)) { + assertType('non-falsy-string', $s); + } + + if (strlen($s) == $maxThree) { + assertType('string', $s); + } + if (strlen($s) === $maxThree) { + assertType('string', $s); + } + + if (strlen($s) == $zeroOrEleven) { + assertType('string', $s); + } + if (strlen($s) === $zeroOrEleven) { + assertType('string', $s); + } + + if (strlen($s) == $negative) { + assertType('*NEVER*', $s); + } else { + assertType('string', $s); + } + if (strlen($s) === $negative) { + assertType('*NEVER*', $s); + } else { + assertType('string', $s); + } +} + +/** + * @param int<1, max> $oneOrMore + * @param int<2, max> $twoOrMore + */ +function doFooBar(string $s, array $arr, int $oneOrMore, int $twoOrMore): void +{ + if (count($arr) == $oneOrMore) { + assertType('non-empty-array', $arr); + } + if (count($arr) === $twoOrMore) { + assertType('non-empty-array', $arr); + } + + if (strlen($s) == $twoOrMore) { + assertType('non-falsy-string', $s); + } + if (strlen($s) === $twoOrMore) { + assertType('non-falsy-string', $s); + } + +} diff --git a/tests/PHPStan/Analyser/data/strtotime-return-type-extensions.php b/tests/PHPStan/Analyser/nsrt/strtotime-return-type-extensions.php similarity index 100% rename from tests/PHPStan/Analyser/data/strtotime-return-type-extensions.php rename to tests/PHPStan/Analyser/nsrt/strtotime-return-type-extensions.php diff --git a/tests/PHPStan/Analyser/nsrt/strtr.php b/tests/PHPStan/Analyser/nsrt/strtr.php new file mode 100644 index 0000000000..5bc9fd6679 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/strtr.php @@ -0,0 +1,27 @@ + 'b'])); + assertType('string', strtr($s, ['f' => 'b', 'o' => 'a'])); + + assertType('string', strtr($s, $s, $nonEmptyString)); + assertType('string', strtr($s, $nonEmptyString, $nonEmptyString)); + assertType('string', strtr($s, $nonFalseyString, $nonFalseyString)); + + assertType('non-empty-string', strtr($nonEmptyString, $s, $nonEmptyString)); + assertType('non-empty-string', strtr($nonEmptyString, $nonEmptyString, $nonEmptyString)); + assertType('non-empty-string', strtr($nonEmptyString, $nonFalseyString, $nonFalseyString)); + + assertType('non-empty-string', strtr($nonFalseyString, $s, $nonEmptyString)); + assertType('non-falsy-string', strtr($nonFalseyString, $nonEmptyString, $nonFalseyString)); + assertType('non-falsy-string', strtr($nonFalseyString, $nonFalseyString, $nonFalseyString)); +} diff --git a/tests/PHPStan/Analyser/nsrt/strval.php b/tests/PHPStan/Analyser/nsrt/strval.php new file mode 100644 index 0000000000..b28f31549b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/strval.php @@ -0,0 +1,124 @@ + $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('\'42\'', strval(42)); + assertType('lowercase-string&numeric-string&uppercase-string', strval(rand())); + assertType('numeric-string&uppercase-string', strval(rand() * 0.1)); + assertType('lowercase-string&numeric-string&uppercase-string', strval(strval(rand()))); + assertType('class-string', strval($class)); + assertType('string', strval(new \Exception())); + assertType('*ERROR*', strval(new \stdClass())); + assertType('*ERROR*', strval([])); + assertType('*ERROR*', strval(function() {})); + assertType('string', strval(fopen('php://memory', 'r'))); +} + +function intvalTest(string $string): void +{ + 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])); + assertType('int', intval(new \stdClass())); + assertType('int', intval(function() {})); + assertType('int', intval(fopen('php://memory', 'r'))); +} + +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])); + assertType('float', floatval(new \stdClass())); + assertType('float', floatval(function() {})); + assertType('float', floatval(fopen('php://memory', 'r'))); +} + +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())); + assertType('true', boolval(function() {})); + assertType('bool', boolval(fopen('php://memory', 'r'))); +} + +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)); +} + +/** + * @param array{} $a + * @param array{foo: mixed, bar?: mixed} $b + * @param array{foo?: mixed, bar?: mixed} $c + */ +function constantArrayTest(array $a, array $b, array $c): void +{ + assertType('0', intval($a)); + assertType('0.0', floatval($a)); + assertType('false', boolval($a)); + + assertType('1', intval($b)); + assertType('1.0', floatval($b)); + assertType('true', boolval($b)); + + assertType('0|1', intval($c)); + assertType('0.0|1.0', floatval($c)); + assertType('bool', boolval($c)); +} diff --git a/tests/PHPStan/Analyser/nsrt/superglobals.php b/tests/PHPStan/Analyser/nsrt/superglobals.php new file mode 100644 index 0000000000..ee7aadb686 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/superglobals.php @@ -0,0 +1,69 @@ +', $GLOBALS); + assertType('array', $_SERVER); + assertType('array', $_GET); + assertType('array', $_POST); + assertType('array', $_FILES); + assertType('array', $_COOKIE); + assertType('array', $_SESSION); + assertType('array', $_REQUEST); + assertType('array', $_ENV); + } + + public function canBeOverwritten(): void + { + $GLOBALS = []; + assertType('array{}', $GLOBALS); + assertNativeType('array{}', $GLOBALS); + } + + public function canBePartlyOverwritten(): void + { + $GLOBALS['foo'] = 'foo'; + assertType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS); + assertNativeType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS); + } + + public function canBeNarrowed(): void + { + if (isset($GLOBALS['foo'])) { + assertType("non-empty-array&hasOffsetValue('foo', mixed~null)", $GLOBALS); + assertNativeType("non-empty-array&hasOffset('foo')", $GLOBALS); // https://github.com/phpstan/phpstan/issues/8395 + } else { + assertType('array', $GLOBALS); + assertNativeType('array', $GLOBALS); + } + assertType('array', $GLOBALS); + assertNativeType('array', $GLOBALS); + } + +} + +function functionScope() { + assertType('array', $GLOBALS); + assertNativeType('array', $GLOBALS); +} + +assertType('array', $GLOBALS); +assertNativeType('array', $GLOBALS); + +function badNarrowing() { + if (empty($_GET['id'])) { + echo "b"; + } else { + echo "b"; + } + assertType('array', $_GET); + assertType('mixed', $_GET['id']); +}; diff --git a/tests/PHPStan/Analyser/nsrt/tagged-unions.php b/tests/PHPStan/Analyser/nsrt/tagged-unions.php new file mode 100644 index 0000000000..9926467123 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/tagged-unions.php @@ -0,0 +1,182 @@ +}", $meal); + if ($meal['type'] === 'pizza') { + assertType("array{type: 'pizza', toppings: array}", $meal); + } else { + assertType("array{type: 'pasta', salsa: string}", $meal); + } + assertType("array{type: 'pasta', salsa: string}|array{type: 'pizza', toppings: array}", $meal); + } +} + +class HelloWorld +{ + /** + * @return array{updated: true, id: int}|array{updated: false, id: null} + */ + public function sayHello(): array + { + return ['updated' => false, 'id' => 5]; + } + + public function doFoo() + { + $x = $this->sayHello(); + assertType("array{updated: false, id: null}|array{updated: true, id: int}", $x); + if ($x['updated']) { + assertType('array{updated: true, id: int}', $x); + } + } +} + +/** + * @psalm-type A array{tag: 'A', foo: bool} + * @psalm-type B array{tag: 'B'} + */ +class X { + /** @psalm-param A|B $arr */ + public function ooo(array $arr): void { + assertType("array{tag: 'A', foo: bool}|array{tag: 'B'}", $arr); + if ($arr['tag'] === 'A') { + assertType("array{tag: 'A', foo: bool}", $arr); + } else { + assertType("array{tag: 'B'}", $arr); + } + assertType("array{tag: 'A', foo: bool}|array{tag: 'B'}", $arr); + } +} + +class TipsFromArnaud +{ + + // https://github.com/phpstan/phpstan/issues/7666#issuecomment-1191563801 + + /** + * @param array{a: int}|array{a: int} $a + */ + public function doFoo(array $a): void + { + assertType('array{a: int}', $a); + } + + /** + * @param array{a: int}|array{a: string} $a + */ + public function doFoo2(array $a): void + { + // could be: array{a: int|string} + assertType('array{a: int}|array{a: string}', $a); + } + + /** + * @param array{a: int, b: int}|array{a: string, b: string} $a + */ + public function doFoo3(array $a): void + { + assertType('array{a: int, b: int}|array{a: string, b: string}', $a); + } + + /** + * @param array{a: int, b: string}|array{a: string, b:string} $a + */ + public function doFoo4(array $a): void + { + assertType('array{a: int|string, b: string}', $a); + } + + /** + * @param array{a: int, b: string, c: string}|array{a: string, b: string, c: string} $a + */ + public function doFoo5(array $a): void + { + assertType('array{a: int|string, b: string, c: string}', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/template-constant-bound.php b/tests/PHPStan/Analyser/nsrt/template-constant-bound.php new file mode 100644 index 0000000000..7dcbdf23c7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/template-constant-bound.php @@ -0,0 +1,17 @@ += 8.0 + +namespace TemplateDefault; + +use function PHPStan\Testing\assertType; + +/** + * @template T1 = true + * @template T2 = true + */ +class Test +{ +} + +/** + * @param Test $one + * @param Test $two + * @param Test $three + */ +function foo(Test $one, Test $two, Test $three) +{ + assertType('TemplateDefault\\Test', $one); + assertType('TemplateDefault\\Test', $two); + assertType('TemplateDefault\\Test', $three); +} + + +/** + * @template S = false + * @template T = false + */ +class Builder +{ + /** + * @phpstan-self-out self + */ + public function one(): void + { + } + + /** + * @phpstan-self-out self + */ + public function two(): void + { + } + + /** + * @return ($this is self ? void : never) + */ + public function execute(): void + { + } +} + +class FormData {} +class Form +{ + /** + * @template Data of object = \stdClass + * @param Data|null $values + * @return Data + */ + public function mapValues(object|null $values = null): object + { + $values ??= new \stdClass; + // ... map into $values ... + return $values; + } +} + +function () { + $qb = new Builder(); + assertType('TemplateDefault\\Builder', $qb); + $qb->one(); + assertType('TemplateDefault\\Builder', $qb); + $qb->two(); + assertType('TemplateDefault\\Builder', $qb); + assertType('null', $qb->execute()); +}; + +function () { + $qb = new Builder(); + assertType('TemplateDefault\\Builder', $qb); + $qb->two(); + assertType('TemplateDefault\\Builder', $qb); + $qb->one(); + assertType('TemplateDefault\\Builder', $qb); + assertType('null', $qb->execute()); +}; + +function () { + $qb = new Builder(); + assertType('TemplateDefault\\Builder', $qb); + $qb->one(); + assertType('TemplateDefault\\Builder', $qb); + assertType('never', $qb->execute()); +}; + +function () { + $form = new Form(); + + assertType('TemplateDefault\\FormData', $form->mapValues(new FormData)); + assertType('stdClass', $form->mapValues()); +}; + +/** + * @template T + * @template U = string + */ +interface Foo +{ + /** + * @return U + */ + public function get(): mixed; +} + +/** + * @extends Foo + */ +interface Bar extends Foo +{ +} + +/** + * @extends Foo + */ +interface Baz extends Foo +{ +} + +function (Bar $bar, Baz $baz) { + assertType('string', $bar->get()); + assertType('bool', $baz->get()); +}; diff --git a/tests/PHPStan/Analyser/data/template-null-bound.php b/tests/PHPStan/Analyser/nsrt/template-null-bound.php similarity index 90% rename from tests/PHPStan/Analyser/data/template-null-bound.php rename to tests/PHPStan/Analyser/nsrt/template-null-bound.php index 66f1346914..3456f02a09 100644 --- a/tests/PHPStan/Analyser/data/template-null-bound.php +++ b/tests/PHPStan/Analyser/nsrt/template-null-bound.php @@ -21,6 +21,6 @@ public function doFoo(?int $p): ?int function (Foo $f, ?int $i): void { assertType('null', $f->doFoo(null)); - assertType('int', $f->doFoo(1)); + assertType('1', $f->doFoo(1)); assertType('int|null', $f->doFoo($i)); }; diff --git a/tests/PHPStan/Analyser/data/ternary-specified-types.php b/tests/PHPStan/Analyser/nsrt/ternary-specified-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/ternary-specified-types.php rename to tests/PHPStan/Analyser/nsrt/ternary-specified-types.php diff --git a/tests/PHPStan/Analyser/nsrt/this-subtractable.php b/tests/PHPStan/Analyser/nsrt/this-subtractable.php new file mode 100644 index 0000000000..a3d9a57554 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/this-subtractable.php @@ -0,0 +1,109 @@ +returnStatic(); + assertType('static(ThisSubtractable\Foo)', $s); + + if (!$s instanceof Bar && !$s instanceof Baz) { + assertType('static(ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz))', $s); + } else { + assertType('(static(ThisSubtractable\Foo)&ThisSubtractable\Bar)|(static(ThisSubtractable\Foo)&ThisSubtractable\Baz)', $s); + } + + assertType('static(ThisSubtractable\Foo)', $s); + } + + public function doBaz(self $s) + { + assertType('ThisSubtractable\Foo', $s); + + if (!$s instanceof Lorem && !$s instanceof Ipsum) { + assertType('ThisSubtractable\Foo', $s); + } else { + assertType('(ThisSubtractable\Foo&ThisSubtractable\Ipsum)|(ThisSubtractable\Foo&ThisSubtractable\Lorem)', $s); + } + + assertType('ThisSubtractable\Foo', $s); + } + + public function doBazz(self $s) + { + assertType('ThisSubtractable\Foo', $s); + + if (!$s instanceof Bar && !$s instanceof Baz) { + assertType('ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz)', $s); + } else { + assertType('ThisSubtractable\Bar|ThisSubtractable\Baz', $s); + } + + assertType('ThisSubtractable\Foo', $s); + } + + public function doBazzz(self $s) + { + assertType('ThisSubtractable\Foo', $s); + if (!method_exists($s, 'test123', $s)) { + return; + } + + assertType('ThisSubtractable\Foo&hasMethod(test123)', $s); + + if (!$s instanceof Bar && !$s instanceof Baz) { + assertType('ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz)&hasMethod(test123)', $s); + } else { + assertType('(ThisSubtractable\Bar&hasMethod(test123))|(ThisSubtractable\Baz&hasMethod(test123))', $s); + } + + assertType('(ThisSubtractable\Bar&hasMethod(test123))|(ThisSubtractable\Baz&hasMethod(test123))|(ThisSubtractable\Foo~(ThisSubtractable\Bar|ThisSubtractable\Baz)&hasMethod(test123))', $s); + } + + /** + * @return static + */ + public function returnStatic() + { + return $this; + } + +} + +class Bar extends Foo +{ + +} + +class Baz extends Foo +{ + +} + +interface Lorem +{ + +} + +interface Ipsum +{ + +} diff --git a/tests/PHPStan/Analyser/data/throw-expr.php b/tests/PHPStan/Analyser/nsrt/throw-expr.php similarity index 84% rename from tests/PHPStan/Analyser/data/throw-expr.php rename to tests/PHPStan/Analyser/nsrt/throw-expr.php index 581e8b1d3e..2893fe4ee7 100644 --- a/tests/PHPStan/Analyser/data/throw-expr.php +++ b/tests/PHPStan/Analyser/nsrt/throw-expr.php @@ -15,7 +15,7 @@ public function doFoo(bool $b): void public function doBar(): void { - assertType('*NEVER*', throw new \Exception()); + assertType('never', throw new \Exception()); } } diff --git a/tests/PHPStan/Analyser/data/throw-points/and.php b/tests/PHPStan/Analyser/nsrt/throw-points/and.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/and.php rename to tests/PHPStan/Analyser/nsrt/throw-points/and.php diff --git a/tests/PHPStan/Analyser/data/throw-points/array-dim-fetch.php b/tests/PHPStan/Analyser/nsrt/throw-points/array-dim-fetch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/array-dim-fetch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/array-dim-fetch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/array.php b/tests/PHPStan/Analyser/nsrt/throw-points/array.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/array.php rename to tests/PHPStan/Analyser/nsrt/throw-points/array.php diff --git a/tests/PHPStan/Analyser/data/throw-points/assign-op.php b/tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/assign-op.php rename to tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php diff --git a/tests/PHPStan/Analyser/data/throw-points/assign.php b/tests/PHPStan/Analyser/nsrt/throw-points/assign.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/assign.php rename to tests/PHPStan/Analyser/nsrt/throw-points/assign.php diff --git a/tests/PHPStan/Analyser/data/throw-points/do-while.php b/tests/PHPStan/Analyser/nsrt/throw-points/do-while.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/do-while.php rename to tests/PHPStan/Analyser/nsrt/throw-points/do-while.php diff --git a/tests/PHPStan/Analyser/data/throw-points/for.php b/tests/PHPStan/Analyser/nsrt/throw-points/for.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/for.php rename to tests/PHPStan/Analyser/nsrt/throw-points/for.php diff --git a/tests/PHPStan/Analyser/data/throw-points/foreach.php b/tests/PHPStan/Analyser/nsrt/throw-points/foreach.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/foreach.php rename to tests/PHPStan/Analyser/nsrt/throw-points/foreach.php diff --git a/tests/PHPStan/Analyser/data/throw-points/func-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/func-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/func-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/func-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/if.php b/tests/PHPStan/Analyser/nsrt/throw-points/if.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/if.php rename to tests/PHPStan/Analyser/nsrt/throw-points/if.php diff --git a/tests/PHPStan/Analyser/data/throw-points/method-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/method-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/method-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/method-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/or.php b/tests/PHPStan/Analyser/nsrt/throw-points/or.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/or.php rename to tests/PHPStan/Analyser/nsrt/throw-points/or.php diff --git a/tests/PHPStan/Analyser/data/throw-points/php8/null-safe-method-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/php8/null-safe-method-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/php8/null-safe-method-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/php8/null-safe-method-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/property-fetch.php b/tests/PHPStan/Analyser/nsrt/throw-points/property-fetch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/property-fetch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/property-fetch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/static-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/static-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/static-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/static-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/switch.php b/tests/PHPStan/Analyser/nsrt/throw-points/switch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/switch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/switch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/throw.php b/tests/PHPStan/Analyser/nsrt/throw-points/throw.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/throw.php rename to tests/PHPStan/Analyser/nsrt/throw-points/throw.php diff --git a/tests/PHPStan/Analyser/data/throw-points/try-catch-finally.php b/tests/PHPStan/Analyser/nsrt/throw-points/try-catch-finally.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/try-catch-finally.php rename to tests/PHPStan/Analyser/nsrt/throw-points/try-catch-finally.php diff --git a/tests/PHPStan/Analyser/data/throw-points/try-catch.php b/tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php similarity index 91% rename from tests/PHPStan/Analyser/data/throw-points/try-catch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php index 57faddd9bb..87c0be66eb 100644 --- a/tests/PHPStan/Analyser/data/throw-points/try-catch.php +++ b/tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php @@ -65,18 +65,18 @@ function (): void { $bar = 1; maybeThrows(); } catch (\InvalidArgumentException $e) { - assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); assertType('1|2', $foo); - assertVariableCertainty(TrinaryLogic::createNo(), $bar); + assertVariableCertainty(TrinaryLogic::createMaybe(), $bar); assertVariableCertainty(TrinaryLogic::createNo(), $baz); } catch (\RuntimeException $e) { assertVariableCertainty(TrinaryLogic::createNo(), $foo); - assertVariableCertainty(TrinaryLogic::createNo(), $bar); - assertVariableCertainty(TrinaryLogic::createYes(), $baz); + assertVariableCertainty(TrinaryLogic::createMaybe(), $bar); + assertVariableCertainty(TrinaryLogic::createMaybe(), $baz); assertType('1|2', $baz); } catch (\Throwable $e) { - assertType('Throwable~InvalidArgumentException|RuntimeException', $e); + assertType('Throwable~(InvalidArgumentException|RuntimeException)', $e); assertVariableCertainty(TrinaryLogic::createNo(), $foo); assertVariableCertainty(TrinaryLogic::createYes(), $bar); assertVariableCertainty(TrinaryLogic::createNo(), $baz); @@ -99,7 +99,7 @@ function (): void { throw new \InvalidArgumentException(); } catch (\InvalidArgumentException $e) { assertType('1', $foo); - assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); } }; diff --git a/tests/PHPStan/Analyser/data/throw-points/variable.php b/tests/PHPStan/Analyser/nsrt/throw-points/variable.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/variable.php rename to tests/PHPStan/Analyser/nsrt/throw-points/variable.php diff --git a/tests/PHPStan/Analyser/data/throw-points/while.php b/tests/PHPStan/Analyser/nsrt/throw-points/while.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/while.php rename to tests/PHPStan/Analyser/nsrt/throw-points/while.php diff --git a/tests/PHPStan/Analyser/nsrt/trait-type-alias.php b/tests/PHPStan/Analyser/nsrt/trait-type-alias.php new file mode 100644 index 0000000000..b83fe210fa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/trait-type-alias.php @@ -0,0 +1,54 @@ += 8.0 + +declare(strict_types = 1); + +namespace TriggerErrorPhp8; + +use function PHPStan\Testing\assertType; + +$errorLevels = [E_USER_DEPRECATED, E_USER_ERROR, E_USER_NOTICE, E_USER_WARNING, E_NOTICE, E_WARNING]; + +assertType('true', trigger_error('bar')); +assertType('true', trigger_error('bar', $errorLevels[0])); +assertType('*NEVER*', trigger_error('bar', $errorLevels[1])); +assertType('true', trigger_error('bar', $errorLevels[2])); +assertType('true', trigger_error('bar', $errorLevels[3])); +assertType('*NEVER*', trigger_error('bar', $errorLevels[4])); +assertType('*NEVER*', trigger_error('bar', $errorLevels[5])); diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/nsrt/type-aliases.php similarity index 95% rename from tests/PHPStan/Analyser/data/type-aliases.php rename to tests/PHPStan/Analyser/nsrt/type-aliases.php index a6342387ab..dd7440470f 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/nsrt/type-aliases.php @@ -90,7 +90,7 @@ public function globalAlias($parameter) */ public function localAlias($parameter) { - assertType('callable(string): string|false', $parameter); + assertType('callable(string): (string|false)', $parameter); } /** @@ -98,7 +98,7 @@ public function localAlias($parameter) */ public function nestedLocalAlias($parameter) { - assertType('array', $parameter); + assertType('array', $parameter); } /** @@ -134,7 +134,7 @@ public function invalidImports($parameter1, $parameter2, $parameter3) */ public function conflictingAlias($parameter) { - assertType('*NEVER*', $parameter); + assertType('never', $parameter); } public function __get(string $name) @@ -151,7 +151,7 @@ public function testIntAlias($int) } assertType('int|string', (new Foo)->globalAliasProperty); - assertType('callable(string): string|false', (new Foo)->localAliasProperty); + assertType('callable(string): (string|false)', (new Foo)->localAliasProperty); assertType('Countable&Traversable', (new Foo)->importedAliasProperty); assertType('Countable&Traversable', (new Foo)->reexportedAliasProperty); assertType('TypeAliasesDataset\SubScope\Foo', (new Foo)->scopedAliasProperty); diff --git a/tests/PHPStan/Analyser/data/type-change-after-array-access-assignment.php b/tests/PHPStan/Analyser/nsrt/type-change-after-array-access-assignment.php similarity index 100% rename from tests/PHPStan/Analyser/data/type-change-after-array-access-assignment.php rename to tests/PHPStan/Analyser/nsrt/type-change-after-array-access-assignment.php diff --git a/tests/PHPStan/Analyser/data/uksort-bug.php b/tests/PHPStan/Analyser/nsrt/uksort-bug.php similarity index 100% rename from tests/PHPStan/Analyser/data/uksort-bug.php rename to tests/PHPStan/Analyser/nsrt/uksort-bug.php diff --git a/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php b/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php new file mode 100644 index 0000000000..afda5d2229 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php @@ -0,0 +1,48 @@ + $filteredParameters + */ + public function doFoo(array $filteredParameters, array $a): void + { + $otherFilteredParameters = $filteredParameters; + foreach ($a as $k => $v) { + if (rand(0, 1)) { + unset($otherFilteredParameters[$k]); + } + } + + if (count($otherFilteredParameters) > 0) { + return; + } + + assertType('array{}', $otherFilteredParameters); + assertType('array', $filteredParameters); + } + + public function doBaz(): void + { + $breakdowns = [ + 'a' => (bool) rand(0, 1), + 'b' => (string) rand(0, 1), + 'c' => rand(-1, 1), + 'd' => rand(0, 1), + ]; + + foreach ($breakdowns as $type => $bd) { + if (empty($bd)) { + unset($breakdowns[$type]); + } + } + + assertType("array{a?: bool, b?: '0'|'1', c?: int<-1, 1>, d?: int<0, 1>}", $breakdowns); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php new file mode 100644 index 0000000000..2ddf808da2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php @@ -0,0 +1,22 @@ + $commonStrings + * @param array $uppercaseStrings + */ + public function doFoo(string $s, string $ls, array $commonStrings, array $uppercaseStrings): void + { + assertType('string', implode($s, $commonStrings)); + assertType('string', implode($s, $uppercaseStrings)); + assertType('string', implode($ls, $commonStrings)); + assertType('uppercase-string', implode($ls, $uppercaseStrings)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php new file mode 100644 index 0000000000..7045582dc4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php @@ -0,0 +1,23 @@ += 8.0 + +namespace UppercaseStringSubstr; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param uppercase-string $uppercase + */ + public function doSubstr(string $uppercase): void + { + assertType('uppercase-string', substr($uppercase, 5)); + assertType('uppercase-string', substr($uppercase, -5)); + assertType('uppercase-string', substr($uppercase, 0, 5)); + } + + /** + * @param uppercase-string $uppercase + */ + public function doMbSubstr(string $uppercase): void + { + assertType('uppercase-string', mb_substr($uppercase, 5)); + assertType('uppercase-string', mb_substr($uppercase, -5)); + assertType('uppercase-string', mb_substr($uppercase, 0, 5)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php new file mode 100644 index 0000000000..0c24268faf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php @@ -0,0 +1,29 @@ += 8.1 + +declare(strict_types=1); + +namespace ValueOfEnum; + +use function PHPStan\Testing\assertType; + +enum Country: string +{ + case NL = 'The Netherlands'; + case US = 'United States'; +} + +class Foo { + /** + * @return value-of + */ + function us() + { + return Country::US; + } + + /** + * @param value-of $countryName + */ + function hello($countryName) + { + assertType("'The Netherlands'|'United States'", $countryName); + } + + function doFoo() { + assertType("'The Netherlands'|'United States'", $this->us()); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/value-of-generic.php b/tests/PHPStan/Analyser/nsrt/value-of-generic.php new file mode 100644 index 0000000000..97d966a799 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/value-of-generic.php @@ -0,0 +1,28 @@ + + */ +interface Result +{ + /** + * @return value-of|false + */ + public function getColumn(); +} + +/** + * @param Result $result + * @param Result $emptyResult + */ +function test( + Result $result, + Result $emptyResult +): void { + assertType('int|string|false', $result->getColumn()); + assertType('false', $emptyResult->getColumn()); +} diff --git a/tests/PHPStan/Analyser/data/value-of.php b/tests/PHPStan/Analyser/nsrt/value-of.php similarity index 100% rename from tests/PHPStan/Analyser/data/value-of.php rename to tests/PHPStan/Analyser/nsrt/value-of.php diff --git a/tests/PHPStan/Analyser/data/var-above-declare.php b/tests/PHPStan/Analyser/nsrt/var-above-declare.php similarity index 100% rename from tests/PHPStan/Analyser/data/var-above-declare.php rename to tests/PHPStan/Analyser/nsrt/var-above-declare.php diff --git a/tests/PHPStan/Analyser/data/var-above-use.php b/tests/PHPStan/Analyser/nsrt/var-above-use.php similarity index 100% rename from tests/PHPStan/Analyser/data/var-above-use.php rename to tests/PHPStan/Analyser/nsrt/var-above-use.php diff --git a/tests/PHPStan/Analyser/nsrt/var-in-and-out-of-function.php b/tests/PHPStan/Analyser/nsrt/var-in-and-out-of-function.php new file mode 100644 index 0000000000..6550d139b7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/var-in-and-out-of-function.php @@ -0,0 +1,25 @@ += 8.0 namespace VariadicParameterPHP8; diff --git a/tests/PHPStan/Analyser/nsrt/weakMap.php b/tests/PHPStan/Analyser/nsrt/weakMap.php new file mode 100644 index 0000000000..049e3dfb13 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/weakMap.php @@ -0,0 +1,32 @@ += 8.0 + +declare(strict_types = 1); + +namespace weakMap; + +use WeakMap; +use function PHPStan\Testing\assertType; + +interface Foo {} +interface Bar {} + +/** + * @param WeakMap $weakMap + */ +function weakMapOffsetGetNotNullable(WeakMap $weakMap, Foo $foo): void +{ + $bar = $weakMap[$foo]; + + assertType(Bar::class, $bar); +} + + +/** + * @param WeakMap $weakMap + */ +function weakMapOffsetGetNullable(WeakMap $weakMap, Foo $foo): void +{ + $bar = $weakMap[$foo]; + + assertType( 'weakMap\\Bar|null', $bar); +} diff --git a/tests/PHPStan/Analyser/data/weird-array_key_exists-issue.php b/tests/PHPStan/Analyser/nsrt/weird-array_key_exists-issue.php similarity index 84% rename from tests/PHPStan/Analyser/data/weird-array_key_exists-issue.php rename to tests/PHPStan/Analyser/nsrt/weird-array_key_exists-issue.php index 95d28f327c..b8d7f026ae 100644 --- a/tests/PHPStan/Analyser/data/weird-array_key_exists-issue.php +++ b/tests/PHPStan/Analyser/nsrt/weird-array_key_exists-issue.php @@ -37,11 +37,11 @@ public function doFoo(array $data): array 'abs' => 0, 'rel' => 0, ]; - assertType('non-empty-array<\'Ostatní\', array{abs: int, rel: (float|int)}>', $otherData); + assertType('array{Ostatní: array{abs: 0, rel: 0}}', $otherData); } $otherData[$key]['abs'] += $count; $otherData[$key]['rel'] += $count / $total * 100; - assertType('non-empty-array<\'Ostatní\', array{abs: int, rel: (float|int)}>', $otherData); + assertType('array{Ostatní: array{abs: int, rel: (float|int)}}', $otherData); } $i++; } diff --git a/tests/PHPStan/Analyser/nsrt/weird-strlen-cases.php b/tests/PHPStan/Analyser/nsrt/weird-strlen-cases.php new file mode 100644 index 0000000000..d53f71f973 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/weird-strlen-cases.php @@ -0,0 +1,34 @@ +', strlen($constUnionMixed)); + assertType('3', strlen(123)); + assertType('1', strlen(true)); + assertType('0', strlen(false)); + assertType('0', strlen(null)); + assertType('1', strlen(1.0)); + assertType('4', strlen(1.23)); + assertType('int<1, max>', strlen($float)); + assertType('int<1, max>', strlen($intFloat)); + assertType('int<1, max>', strlen($nonEmptyStringIntFloat)); + assertType('0', strlen($emptyStringFalseNull)); + assertType('int<0, 1>', strlen($emptyStringBoolNull)); + } +} diff --git a/tests/PHPStan/Analyser/param-closure-this-stubs.neon b/tests/PHPStan/Analyser/param-closure-this-stubs.neon new file mode 100644 index 0000000000..bbfb15154d --- /dev/null +++ b/tests/PHPStan/Analyser/param-closure-this-stubs.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/param-closure-this-stubs.stub diff --git a/tests/PHPStan/Analyser/param-out.neon b/tests/PHPStan/Analyser/param-out.neon new file mode 100644 index 0000000000..8d3fe24304 --- /dev/null +++ b/tests/PHPStan/Analyser/param-out.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - param-out.stub diff --git a/tests/PHPStan/Analyser/param-out.stub b/tests/PHPStan/Analyser/param-out.stub new file mode 100644 index 0000000000..297e1620fb --- /dev/null +++ b/tests/PHPStan/Analyser/param-out.stub @@ -0,0 +1,21 @@ +foo(); + $this->x = 5; + $this->y = 5; + $this->z = 5; + } +} diff --git a/tests/PHPStan/Analyser/traits/uninitializedProperty/FooTrait.php b/tests/PHPStan/Analyser/traits/uninitializedProperty/FooTrait.php new file mode 100644 index 0000000000..d216f94304 --- /dev/null +++ b/tests/PHPStan/Analyser/traits/uninitializedProperty/FooTrait.php @@ -0,0 +1,19 @@ += 8.1 + +namespace TraitsUnititializedProperty; + +trait FooTrait +{ + protected readonly int $x; + + /** @readonly */ + protected int $y; + protected int $z; + + public function foo(): void + { + echo $this->x; + echo $this->y; + echo $this->z; + } +} diff --git a/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php b/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php deleted file mode 100644 index ee7dd1f215..0000000000 --- a/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php +++ /dev/null @@ -1,184 +0,0 @@ -deleteCache(); - - if ($this->originalTraitOneContents !== null) { - $this->revertTrait(__DIR__ . '/data/TraitOne.php', $this->originalTraitOneContents); - } - - if ($this->originalTraitTwoContents !== null) { - $this->revertTrait(__DIR__ . '/data/TraitTwo.php', $this->originalTraitTwoContents); - } - } - - public function dataCachingIssue(): array - { - return [ - [ - false, - false, - [], - ], - [ - false, - true, - [ - 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.', - ], - ], - [ - true, - false, - [ - 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.', - ], - ], - [ - true, - true, - [ - 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.', - 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.', - ], - ], - ]; - } - - /** - * @dataProvider dataCachingIssue - * @param bool $changeOne - * @param bool $changeTwo - * @param string[] $expectedErrors - */ - public function testCachingIssue( - bool $changeOne, - bool $changeTwo, - array $expectedErrors - ): void - { - $this->deleteCache(); - [$statusCode, $errors] = $this->runPhpStan(); - $this->assertSame([], $errors); - $this->assertSame(0, $statusCode); - - if ($changeOne) { - $this->originalTraitOneContents = $this->changeTrait(__DIR__ . '/data/TraitOne.php'); - } - if ($changeTwo) { - $this->originalTraitTwoContents = $this->changeTrait(__DIR__ . '/data/TraitTwo.php'); - } - - $fileHelper = new FileHelper(__DIR__); - - $errorPath = $fileHelper->normalizePath(__DIR__ . '/data/TestClassUsingTrait.php'); - [$statusCode, $errors] = $this->runPhpStan(); - - if (count($expectedErrors) === 0) { - $this->assertSame(0, $statusCode); - $this->assertArrayNotHasKey($errorPath, $errors); - return; - } - - $this->assertSame(1, $statusCode); - $this->assertArrayHasKey($errorPath, $errors); - $this->assertSame(count($expectedErrors), $errors[$errorPath]['errors']); - - foreach ($errors[$errorPath]['messages'] as $i => $error) { - $this->assertSame($expectedErrors[$i], $error['message']); - } - } - - /** - * @return array{int, mixed[]} - */ - private function runPhpStan(): array - { - $phpstanBinPath = __DIR__ . '/../../../../bin/phpstan'; - exec(sprintf('%s %s clear-result-cache --configuration %s', escapeshellarg(PHP_BINARY), $phpstanBinPath, escapeshellarg(__DIR__ . '/phpstan.neon')), $clearResultCacheOutputLines, $clearResultCacheExitCode); - if ($clearResultCacheExitCode !== 0) { - throw new \PHPStan\ShouldNotHappenException('Could not clear result cache.'); - } - - exec( - sprintf( - '%s %s analyse --no-progress --level 8 --configuration %s --error-format json %s', - escapeshellarg(PHP_BINARY), - $phpstanBinPath, - escapeshellarg(__DIR__ . '/phpstan.neon'), - escapeshellarg(__DIR__ . '/data') - ), - $output, - $statusCode - ); - $stringOutput = implode("\n", $output); - $json = \Nette\Utils\Json::decode($stringOutput, \Nette\Utils\Json::FORCE_ARRAY); - - return [$statusCode, $json['files']]; - } - - private function deleteCache(): void - { - $dir = __DIR__ . '/tmp/cache'; - if (!file_exists($dir)) { - return; - } - - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator(__DIR__ . '/tmp/cache', RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($files as $fileinfo) { - if ($fileinfo->isDir()) { - rmdir($fileinfo->getRealPath()); - continue; - } - - unlink($fileinfo->getRealPath()); - } - } - - private function changeTrait(string $traitPath): string - { - $originalTraitContents = FileReader::read($traitPath); - $traitContents = str_replace('use stdClass as Foo;', 'use Exception as Foo;', $originalTraitContents); - $result = file_put_contents($traitPath, $traitContents); - if ($result === false) { - $this->fail(sprintf('Could not save file %s', $traitPath)); - } - - return $originalTraitContents; - } - - private function revertTrait(string $traitPath, string $originalTraitContents): void - { - $result = file_put_contents($traitPath, $originalTraitContents); - if ($result === false) { - $this->fail(sprintf('Could not save file %s', $traitPath)); - } - } - -} diff --git a/tests/PHPStan/Analyser/traitsCachingIssue/tmp/.gitignore b/tests/PHPStan/Analyser/traitsCachingIssue/tmp/.gitignore deleted file mode 100644 index d6b7ef32c8..0000000000 --- a/tests/PHPStan/Analyser/traitsCachingIssue/tmp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/tests/PHPStan/Analyser/unknown-mixed-type.neon b/tests/PHPStan/Analyser/unknown-mixed-type.neon new file mode 100644 index 0000000000..a9d8e60640 --- /dev/null +++ b/tests/PHPStan/Analyser/unknown-mixed-type.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 70400 diff --git a/tests/PHPStan/Analyser/usePathConstantsAsConstantString.neon b/tests/PHPStan/Analyser/usePathConstantsAsConstantString.neon new file mode 100644 index 0000000000..12225ce6e6 --- /dev/null +++ b/tests/PHPStan/Analyser/usePathConstantsAsConstantString.neon @@ -0,0 +1,2 @@ +parameters: + usePathConstantsAsConstantString: true diff --git a/tests/PHPStan/Broker/BrokerTest.php b/tests/PHPStan/Broker/BrokerTest.php deleted file mode 100644 index 413cc7ec82..0000000000 --- a/tests/PHPStan/Broker/BrokerTest.php +++ /dev/null @@ -1,90 +0,0 @@ -getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); - - $workingDirectory = __DIR__; - $relativePathHelper = new SimpleRelativePathHelper($workingDirectory); - $fileHelper = new FileHelper($workingDirectory); - $anonymousClassNameHelper = new AnonymousClassNameHelper($fileHelper, $relativePathHelper); - - $classReflectionExtensionRegistryProvider = new DirectClassReflectionExtensionRegistryProvider([], []); - - $setterReflectionProviderProvider = new SetterReflectionProviderProvider(); - $reflectionProvider = new RuntimeReflectionProvider( - $setterReflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $this->createMock(FunctionReflectionFactory::class), - 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), - ); - $setterReflectionProviderProvider->setReflectionProvider($reflectionProvider); - $this->broker = new Broker( - $reflectionProvider, - [], - ); - $classReflectionExtensionRegistryProvider->setBroker($this->broker); - } - - public function testClassNotFound(): void - { - $this->expectException(ClassNotFoundException::class); - $this->expectExceptionMessage('NonexistentClass'); - $this->broker->getClass('NonexistentClass'); - } - - public function testFunctionNotFound(): void - { - $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); - $scope->method('getNamespace') - ->willReturn(null); - $this->broker->getFunction(new Name('nonexistentFunction'), $scope); - } - - public function testClassAutoloadingException(): void - { - $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'; - }, true, true); - $this->broker->hasClass('NonexistentClass'); - } - -} diff --git a/tests/PHPStan/Collectors/DummyCollector.php b/tests/PHPStan/Collectors/DummyCollector.php new file mode 100644 index 0000000000..fede9c8635 --- /dev/null +++ b/tests/PHPStan/Collectors/DummyCollector.php @@ -0,0 +1,24 @@ + + */ +class DummyCollector implements Collector +{ + + public function getNodeType(): string + { + return 'PhpParser\Node\Expr\FuncCall'; + } + + public function processNode(Node $node, Scope $scope) + { + return []; + } + +} diff --git a/tests/PHPStan/Collectors/RegistryTest.php b/tests/PHPStan/Collectors/RegistryTest.php new file mode 100644 index 0000000000..ac7e0c2e9d --- /dev/null +++ b/tests/PHPStan/Collectors/RegistryTest.php @@ -0,0 +1,45 @@ +getCollectors(Node\Expr\FuncCall::class); + $this->assertCount(1, $collectors); + $this->assertSame($collector, $collectors[0]); + + $this->assertCount(0, $registry->getCollectors(Node\Expr\MethodCall::class)); + } + + public function testGetCollectorsWithTwoDifferentInstances(): void + { + $fooCollector = new UniversalCollector(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => ['Foo error']); + $barCollector = new UniversalCollector(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => ['Bar error']); + + $registry = new Registry([ + $fooCollector, + $barCollector, + ]); + + $collectors = $registry->getCollectors(Node\Expr\FuncCall::class); + $this->assertCount(2, $collectors); + $this->assertSame($fooCollector, $collectors[0]); + $this->assertSame($barCollector, $collectors[1]); + + $this->assertCount(0, $registry->getCollectors(Node\Expr\MethodCall::class)); + } + +} diff --git a/tests/PHPStan/Collectors/UniversalCollector.php b/tests/PHPStan/Collectors/UniversalCollector.php new file mode 100644 index 0000000000..d939fe368c --- /dev/null +++ b/tests/PHPStan/Collectors/UniversalCollector.php @@ -0,0 +1,47 @@ + + */ +class UniversalCollector implements Collector +{ + + /** @phpstan-var class-string */ + private $nodeType; + + /** @var (callable(TNodeType, Scope): TValue) */ + private $processNodeCallback; + + /** + * @param class-string $nodeType + * @param (callable(TNodeType, Scope): TValue) $processNodeCallback + */ + public function __construct(string $nodeType, callable $processNodeCallback) + { + $this->nodeType = $nodeType; + $this->processNodeCallback = $processNodeCallback; + } + + public function getNodeType(): string + { + return $this->nodeType; + } + + /** + * @param TNodeType $node + * @return TValue + */ + public function processNode(Node $node, Scope $scope) + { + $callback = $this->processNodeCallback; + return $callback($node, $scope); + } + +} diff --git a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php index 277f63d451..e84ac78e28 100644 --- a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php +++ b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php @@ -3,21 +3,23 @@ namespace PHPStan\Command; use PHPStan\Analyser\ResultCache\ResultCacheClearer; +use PHPStan\Command\ErrorFormatter\CiDetectedErrorFormatter; +use PHPStan\Command\ErrorFormatter\GithubErrorFormatter; use PHPStan\Command\ErrorFormatter\TableErrorFormatter; +use PHPStan\Command\ErrorFormatter\TeamcityErrorFormatter; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\File\FuzzyRelativePathHelper; use PHPStan\File\NullRelativePathHelper; +use PHPStan\File\SimpleRelativePathHelper; 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 PHPStanTestCase @@ -61,10 +63,18 @@ private function runPath(string $path, int $expectedStatusCode): string 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, null); + $errorFormatter = new TableErrorFormatter( + $relativePathHelper, + new SimpleRelativePathHelper(__DIR__), + new CiDetectedErrorFormatter( + new GithubErrorFormatter($relativePathHelper), + new TeamcityErrorFormatter($relativePathHelper), + ), + false, + null, + null, + ); $analysisResult = $analyserApplication->analyse( [$path], true, @@ -76,11 +86,7 @@ private function runPath(string $path, int $expectedStatusCode): string null, $this->createMock(InputInterface::class), ); - if (file_exists($memoryLimitFile)) { - unlink($memoryLimitFile); - } $statusCode = $errorFormatter->formatErrors($analysisResult, $symfonyOutput); - $this->assertSame($expectedStatusCode, $statusCode); rewind($output->getStream()); @@ -89,6 +95,8 @@ private function runPath(string $path, int $expectedStatusCode): string throw new ShouldNotHappenException(); } + $this->assertSame($expectedStatusCode, $statusCode, $contents); + return $contents; } diff --git a/tests/PHPStan/Command/AnalyseCommandTest.php b/tests/PHPStan/Command/AnalyseCommandTest.php index 68abcd2875..f47edea747 100644 --- a/tests/PHPStan/Command/AnalyseCommandTest.php +++ b/tests/PHPStan/Command/AnalyseCommandTest.php @@ -8,6 +8,7 @@ use Throwable; use function chdir; use function getcwd; +use function microtime; use function realpath; use function sprintf; use const DIRECTORY_SEPARATOR; @@ -50,12 +51,24 @@ public function testInvalidAutoloadFile(): void public function testValidAutoloadFile(): void { + $originalDir = getcwd(); + if ($originalDir === false) { + throw new ShouldNotHappenException(); + } + $autoloadFile = __DIR__ . DIRECTORY_SEPARATOR . 'data/autoload-file.php'; - $output = $this->runCommand(0, ['--autoload-file' => $autoloadFile]); - $this->assertStringContainsString('[OK] No errors', $output); - $this->assertStringNotContainsString(sprintf('Autoload file "%s" not found.' . PHP_EOL, $autoloadFile), $output); - $this->assertSame('magic value', SOME_CONSTANT_IN_AUTOLOAD_FILE); + chdir(__DIR__); + + try { + $output = $this->runCommand(0, ['--autoload-file' => $autoloadFile]); + $this->assertStringContainsString('[OK] No errors', $output); + $this->assertStringNotContainsString(sprintf('Autoload file "%s" not found.' . PHP_EOL, $autoloadFile), $output); + $this->assertSame('magic value', SOME_CONSTANT_IN_AUTOLOAD_FILE); + } catch (Throwable $e) { + chdir($originalDir); + throw $e; + } } /** @@ -65,16 +78,28 @@ public static function autoDiscoveryPathsProvider(): array { return [ [ - __DIR__ . '/test-autodiscover', - __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover' . DIRECTORY_SEPARATOR . 'phpstan.neon', + __DIR__ . '/test-autodiscover-dot', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dot' . DIRECTORY_SEPARATOR . '.phpstan.neon', + ], + [ + __DIR__ . '/test-autodiscover-dot-dist', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dot-dist' . DIRECTORY_SEPARATOR . '.phpstan.neon.dist', + ], + [ + __DIR__ . '/test-autodiscover-dot-dist-dot-neon', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dot-dist-dot-neon' . DIRECTORY_SEPARATOR . '.phpstan.dist.neon', ], [ - __DIR__ . '/test-autodiscover-dist', - __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dist' . DIRECTORY_SEPARATOR . 'phpstan.neon.dist', + __DIR__ . '/test-autodiscover-no-dot', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-no-dot' . DIRECTORY_SEPARATOR . 'phpstan.neon', ], [ - __DIR__ . '/test-autodiscover-dist-dot-neon', - __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dist-dot-neon' . DIRECTORY_SEPARATOR . 'phpstan.dist.neon', + __DIR__ . '/test-autodiscover-no-dot-dist', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-no-dot-dist' . DIRECTORY_SEPARATOR . 'phpstan.neon.dist', + ], + [ + __DIR__ . '/test-autodiscover-no-dot-dist-dot-neon', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-no-dot-dist-dot-neon' . DIRECTORY_SEPARATOR . 'phpstan.dist.neon', ], [ __DIR__ . '/test-autodiscover-priority', @@ -84,6 +109,10 @@ public static function autoDiscoveryPathsProvider(): array __DIR__ . '/test-autodiscover-priority-dist-dot-neon', __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-priority-dist-dot-neon' . DIRECTORY_SEPARATOR . 'phpstan.neon', ], + [ + __DIR__ . '/test-autodiscover-priority-dot', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-priority-dot' . DIRECTORY_SEPARATOR . '.phpstan.neon', + ], ]; } @@ -92,13 +121,14 @@ public static function autoDiscoveryPathsProvider(): array */ private function runCommand(int $expectedStatusCode, array $parameters = []): string { - $commandTester = new CommandTester(new AnalyseCommand([])); + $commandTester = new CommandTester(new AnalyseCommand([], microtime(true))); $commandTester->execute([ 'paths' => [__DIR__ . DIRECTORY_SEPARATOR . 'test'], - ] + $parameters); + '--debug' => true, + ] + $parameters, ['debug' => true]); - $this->assertSame($expectedStatusCode, $commandTester->getStatusCode()); + $this->assertSame($expectedStatusCode, $commandTester->getStatusCode(), $commandTester->getDisplay()); return $commandTester->getDisplay(); } diff --git a/tests/PHPStan/Command/AnalysisResultTest.php b/tests/PHPStan/Command/AnalysisResultTest.php index 7ce1fdecd7..2ea3344a9b 100644 --- a/tests/PHPStan/Command/AnalysisResultTest.php +++ b/tests/PHPStan/Command/AnalysisResultTest.php @@ -39,9 +39,13 @@ public function testErrorsAreSortedByFileNameAndLine(): void [], [], [], + [], false, null, true, + 0, + false, + [], ))->getFileSpecificErrors(), ); } diff --git a/tests/PHPStan/Command/CommandHelperTest.php b/tests/PHPStan/Command/CommandHelperTest.php index 8c29068a98..ab4b4b793a 100644 --- a/tests/PHPStan/Command/CommandHelperTest.php +++ b/tests/PHPStan/Command/CommandHelperTest.php @@ -124,6 +124,8 @@ public function testBegin( null, $level, false, + false, + false, ); if ($expectException) { $this->fail(); @@ -184,7 +186,6 @@ public function dataParameters(): array 'paths' => [ __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', ], - 'memoryLimitFile' => __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . '.memory_limit', 'excludePaths' => [ 'analyseAndScan' => [ __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', @@ -203,6 +204,7 @@ public function dataParameters(): array __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'nested' . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'there.php', __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'up.php', ], + 'reportUnmatchedIgnoredErrors' => false, 'ignoreErrors' => [ [ 'message' => '#aaa#', @@ -234,12 +236,12 @@ public function dataParameters(): array __DIR__ . '/exclude-paths/full.neon', [ 'excludePaths' => [ - 'analyse' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', - ], 'analyseAndScan' => [ __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test2', ], + 'analyse' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', + ], ], ], ], @@ -247,13 +249,13 @@ public function dataParameters(): array __DIR__ . '/exclude-paths/including.neon', [ 'excludePaths' => [ + 'analyseAndScan' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test3', + ], 'analyse' => [ __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test2', ], - 'analyseAndScan' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test3', - ], ], ], ], @@ -307,6 +309,8 @@ public function testResolveParameters( null, '0', false, + false, + false, ); $parameters = $result->getContainer()->getParameters(); foreach ($expectedParameters as $name => $expectedValue) { diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index 44d52fb724..ace5a21c5b 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -6,11 +6,23 @@ use Nette\Neon\Neon; use PHPStan\Analyser\Error; use PHPStan\Command\AnalysisResult; +use PHPStan\Command\ErrorsConsoleStyle; +use PHPStan\Command\Symfony\SymfonyOutput; +use PHPStan\Command\Symfony\SymfonyStyle; use PHPStan\File\SimpleRelativePathHelper; +use PHPStan\ShouldNotHappenException; use PHPStan\Testing\ErrorFormatterTestCase; +use PHPUnit\Framework\Assert; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\StreamOutput; +use function fopen; use function mt_srand; +use function rewind; use function shuffle; use function sprintf; +use function str_repeat; +use function stream_get_contents; +use function substr; use function trim; class BaselineNeonErrorFormatterTest extends ErrorFormatterTestCase @@ -62,7 +74,7 @@ public function dataFormatterOutputProvider(): iterable 'path' => 'foo.php', ], [ - 'message' => '#^Foo$#', + 'message' => '#^Foo\$#', 'count' => 1, 'path' => 'foo.php', ], @@ -91,7 +103,7 @@ public function dataFormatterOutputProvider(): iterable 'path' => 'foo.php', ], [ - 'message' => '#^Foo$#', + 'message' => '#^Foo\$#', 'count' => 1, 'path' => 'foo.php', ], @@ -117,12 +129,12 @@ public function testFormatErrors( $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), $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)); } - public function testFormatErrorMessagesRegexEscape(): void { $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); @@ -132,13 +144,18 @@ public function testFormatErrorMessagesRegexEscape(): void ['Escape Regex without file # ~ <> \' ()'], [], [], + [], false, null, true, + 0, + false, + [], ); $formatter->formatErrors( $result, $this->getOutput(), + '', ); self::assertSame( @@ -167,14 +184,19 @@ public function testEscapeDiNeon(): void [], [], [], + [], false, null, true, + 0, + false, + [], ); $formatter->formatErrors( $result, $this->getOutput(), + '', ); self::assertSame( trim( @@ -229,14 +251,19 @@ public function testOutputOrdering(array $errors): void [], [], [], + [], false, null, true, + 0, + false, + [], ); $formatter->formatErrors( $result, $this->getOutput(), + '', ); self::assertSame( trim(Neon::encode([ @@ -284,4 +311,274 @@ public function testOutputOrdering(array $errors): void ); } + /** + * @return Generator}> + */ + public function endOfFileNewlinesProvider(): Generator + { + $existingBaselineContentWithoutEndNewlines = 'parameters: + ignoreErrors: + - + message: "#^Existing error$#" + count: 1 + path: TestfileA'; + + yield 'one error' => [ + 'errors' => [ + new Error('Error #1', 'TestfileA', 1), + ], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n", + 'expectedNewlinesCount' => 1, + ]; + + yield 'no errors' => [ + 'errors' => [], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n", + 'expectedNewlinesCount' => 1, + ]; + + yield 'one error with 2 newlines' => [ + 'errors' => [ + new Error('Error #1', 'TestfileA', 1), + ], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n\n", + 'expectedNewlinesCount' => 2, + ]; + + yield 'no errors with 2 newlines' => [ + 'errors' => [], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n\n", + 'expectedNewlinesCount' => 2, + ]; + + yield 'one error with 0 newlines' => [ + 'errors' => [ + new Error('Error #1', 'TestfileA', 1), + ], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines, + 'expectedNewlinesCount' => 0, + ]; + + yield 'one error with 3 newlines' => [ + 'errors' => [ + new Error('Error #1', 'TestfileA', 1), + ], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n\n\n", + 'expectedNewlinesCount' => 3, + ]; + + yield 'empty existing baseline' => [ + 'errors' => [ + new Error('Error #1', 'TestfileA', 1), + ], + 'existingBaselineContent' => '', + 'expectedNewlinesCount' => 1, + ]; + + yield 'empty existing baseline, no new errors' => [ + 'errors' => [], + 'existingBaselineContent' => '', + 'expectedNewlinesCount' => 1, + ]; + + yield 'empty existing baseline with a newline, no new errors' => [ + 'errors' => [], + 'existingBaselineContent' => "\n", + 'expectedNewlinesCount' => 1, + ]; + + yield 'empty existing baseline with 2 newlines, no new errors' => [ + 'errors' => [], + 'existingBaselineContent' => "\n\n", + 'expectedNewlinesCount' => 2, + ]; + } + + /** + * @dataProvider endOfFileNewlinesProvider + * + * @param list $errors + */ + public function testEndOfFileNewlines( + array $errors, + string $existingBaselineContent, + int $expectedNewlinesCount, + ): void + { + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $result = new AnalysisResult( + $errors, + [], + [], + [], + [], + false, + null, + true, + 0, + false, + [], + ); + + $resource = fopen('php://memory', 'w', false); + if ($resource === false) { + throw new ShouldNotHappenException(); + } + $outputStream = new StreamOutput($resource, StreamOutput::VERBOSITY_NORMAL, false); + + $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $outputStream); + $output = new SymfonyOutput($outputStream, new SymfonyStyle($errorConsoleStyle)); + + $formatter->formatErrors( + $result, + $output, + $existingBaselineContent, + ); + + rewind($outputStream->getStream()); + + $content = stream_get_contents($outputStream->getStream()); + if ($content === false) { + throw new ShouldNotHappenException(); + } + + if ($expectedNewlinesCount > 0) { + Assert::assertSame(str_repeat("\n", $expectedNewlinesCount), substr($content, -$expectedNewlinesCount)); + } + Assert::assertNotSame("\n", substr($content, -($expectedNewlinesCount + 1), 1)); + } + + public function dataFormatErrorsWithIdentifiers(): iterable + { + yield [ + [ + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + (new Error( + 'Foo with identifier', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + (new Error( + 'Foo with identifier', + __DIR__ . '/Foo.php', + 6, + ))->withIdentifier('argument.type'), + ], + [ + 'parameters' => [ + 'ignoreErrors' => [ + [ + 'message' => '#^Foo$#', + 'count' => 2, + 'path' => 'Foo.php', + ], + [ + 'message' => '#^Foo with identifier$#', + 'identifier' => 'argument.type', + 'count' => 2, + 'path' => 'Foo.php', + ], + ], + ], + ], + ]; + + yield [ + [ + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 6, + ))->withIdentifier('argument.byRef'), + (new Error( + 'Foo with another message', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + ], + [ + 'parameters' => [ + 'ignoreErrors' => [ + [ + 'message' => '#^Foo$#', + 'count' => 2, + 'path' => 'Foo.php', + ], + [ + 'message' => '#^Foo with another message$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => 'Foo.php', + ], + [ + 'message' => '#^Foo with same message, different identifier$#', + 'identifier' => 'argument.byRef', + 'count' => 1, + 'path' => 'Foo.php', + ], + [ + 'message' => '#^Foo with same message, different identifier$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => 'Foo.php', + ], + ], + ], + ], + ]; + } + + /** + * @dataProvider dataFormatErrorsWithIdentifiers + * @param list $errors + * @param mixed[] $expectedOutput + */ + public function testFormatErrorsWithIdentifiers(array $errors, array $expectedOutput): void + { + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(__DIR__)); + $formatter->formatErrors( + new AnalysisResult( + $errors, + [], + [], + [], + [], + false, + null, + true, + 0, + true, + [], + ), + $this->getOutput(), + '', + ); + + $this->assertSame($expectedOutput, Neon::decode($this->getOutputContent())); + } + } diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php new file mode 100644 index 0000000000..e9590e9402 --- /dev/null +++ b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php @@ -0,0 +1,180 @@ + '#^Bar$#', + 'count' => 1, + 'path' => __DIR__ . '/../Foo.php', +]; +\$ignoreErrors[] = [ + 'message' => '#^Foo$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; + +return ['parameters' => ['ignoreErrors' => \$ignoreErrors]]; +", + ]; + + yield [ + [ + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + (new Error( + 'Foo with identifier', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + (new Error( + 'Foo with identifier', + __DIR__ . '/Foo.php', + 6, + ))->withIdentifier('argument.type'), + ], + " '#^Foo$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'message' => '#^Foo with identifier$#', + 'identifier' => 'argument.type', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; + +return ['parameters' => ['ignoreErrors' => \$ignoreErrors]]; +", + ]; + + yield [ + [ + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 6, + ))->withIdentifier('argument.byRef'), + (new Error( + 'Foo with another message', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + ], + " '#^Foo$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'message' => '#^Foo with another message$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'message' => '#^Foo with same message, different identifier$#', + 'identifier' => 'argument.byRef', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'message' => '#^Foo with same message, different identifier$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; + +return ['parameters' => ['ignoreErrors' => \$ignoreErrors]]; +", + ]; + } + + /** + * @dataProvider dataFormatErrors + * @param list $errors + */ + public function testFormatErrors(array $errors, string $expectedOutput): void + { + $formatter = new BaselinePhpErrorFormatter(new ParentDirectoryRelativePathHelper(__DIR__)); + $formatter->formatErrors( + new AnalysisResult( + $errors, + [], + [], + [], + [], + false, + null, + true, + 0, + true, + [], + ), + $this->getOutput(), + ); + + $this->assertSame($expectedOutput, $this->getOutputContent()); + } + +} diff --git a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php index 8139e9b6ac..6618b6effe 100644 --- a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php @@ -64,7 +64,7 @@ public function dataFormatterOutputProvider(): iterable - + @@ -80,7 +80,7 @@ public function dataFormatterOutputProvider(): iterable - + ', @@ -98,12 +98,12 @@ public function dataFormatterOutputProvider(): iterable - + - + ', @@ -150,9 +150,13 @@ public function testTraitPath(): void [], [], [], + [], false, null, true, + 0, + false, + [], ), $this->getOutput()); $this->assertXmlStringEqualsXmlString(' @@ -161,4 +165,35 @@ public function testTraitPath(): void ', $this->getOutputContent()); } + public function testIdentifier(): void + { + $formatter = new CheckstyleErrorFormatter(new SimpleRelativePathHelper(__DIR__)); + $error = (new Error( + 'Foo', + __DIR__ . '/FooTrait.php', + 5, + true, + __DIR__ . '/Foo.php', + null, + ))->withIdentifier('argument.type'); + $formatter->formatErrors(new AnalysisResult( + [$error], + [], + [], + [], + [], + false, + null, + true, + 0, + true, + [], + ), $this->getOutput()); + $this->assertXmlStringEqualsXmlString(' + + + +', $this->getOutputContent()); + } + } diff --git a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php index 81d06132d2..3a93977ce5 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php @@ -6,7 +6,6 @@ use PHPStan\File\NullRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; use function sprintf; -use const PHP_VERSION_ID; class GithubErrorFormatterTest extends ErrorFormatterTestCase { @@ -18,10 +17,7 @@ public function dataFormatterOutputProvider(): iterable 0, 0, 0, - ' - [OK] No errors - -', + '', ]; yield [ @@ -29,16 +25,7 @@ public function dataFormatterOutputProvider(): iterable 1, 1, 0, - ' ------ ------------------------------------------------------------------- - Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ------------------------------------------------------------------- - 4 Foo - ------ ------------------------------------------------------------------- - - - [ERROR] Found 1 error - -::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo + '::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo ', ]; @@ -47,16 +34,7 @@ public function dataFormatterOutputProvider(): iterable 1, 0, 1, - ' -- --------------------- - Error - -- --------------------- - first generic error - -- --------------------- - - - [ERROR] Found 1 error - -::error ::first generic error + '::error ::first generic error ', ]; @@ -65,27 +43,9 @@ 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 - ------ --------- - 1 Foo - 5 Bar - Bar2 - ------ --------- - - [ERROR] Found 4 errors - -::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 + '::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 ::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo -::error file=foo.php,line=1,col=0::Foo +::error file=foo.php,line=1,col=0::Foo ::error file=foo.php,line=5,col=0::Bar%0ABar2 ', ]; @@ -95,18 +55,8 @@ public function dataFormatterOutputProvider(): iterable 1, 0, 2, - ' -- ---------------------- - Error - -- ---------------------- - first generic error - second generic error - -- ---------------------- - - - [ERROR] Found 2 errors - -::error ::first generic error -::error ::second generic error + '::error ::first generic error +::error ::second generic ', ]; @@ -115,37 +65,12 @@ 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 - ------ --------- - 1 Foo - 5 Bar - Bar2 - ------ --------- - - -- ---------------------- - Error - -- ---------------------- - first generic error - second generic error - -- ---------------------- - - [ERROR] Found 6 errors - -::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 + '::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 ::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo -::error file=foo.php,line=1,col=0::Foo +::error file=foo.php,line=1,col=0::Foo ::error file=foo.php,line=5,col=0::Bar%0ABar2 ::error ::first generic error -::error ::second generic error +::error ::second generic ', ]; } @@ -162,13 +87,9 @@ public function testFormatErrors( 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, null), ); $this->assertSame($exitCode, $formatter->formatErrors( diff --git a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php index e4d63c28d7..78df3d79dd 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php @@ -88,8 +88,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", @@ -152,8 +152,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", @@ -194,8 +194,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "second generic error", - "fingerprint": "f49870714e8ce889212aefb50f718f88ae63d00dd01c775b7bac86c4466e96f0", + "description": "second generic", + "fingerprint": "adc18b2c27b0ecad40aed7975b165cbe357f0cbba58582af91c0a2e7fa5d77ab", "severity": "major", "location": { "path": "", @@ -236,8 +236,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", @@ -269,8 +269,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "second generic error", - "fingerprint": "f49870714e8ce889212aefb50f718f88ae63d00dd01c775b7bac86c4466e96f0", + "description": "second generic", + "fingerprint": "adc18b2c27b0ecad40aed7975b165cbe357f0cbba58582af91c0a2e7fa5d77ab", "severity": "major", "location": { "path": "", diff --git a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php index d76c8c15b8..ff1626d7d2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php @@ -2,6 +2,9 @@ namespace PHPStan\Command\ErrorFormatter; +use Nette\Utils\Json; +use PHPStan\Analyser\Error; +use PHPStan\Command\AnalysisResult; use PHPStan\Testing\ErrorFormatterTestCase; use function sprintf; @@ -21,7 +24,7 @@ public function dataFormatterOutputProvider(): iterable "errors":0, "file_errors":0 }, - "files":[], + "files":{}, "errors": [] }', ]; @@ -64,7 +67,7 @@ public function dataFormatterOutputProvider(): iterable "errors":1, "file_errors":0 }, - "files":[], + "files":{}, "errors": [ "first generic error" ] @@ -102,14 +105,15 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Foo", + "message": "Foo", "line": 1, "ignorable": true }, { "message": "Bar\nBar2", "line": 5, - "ignorable": true + "ignorable": true, + "tip": "a tip" } ] } @@ -129,10 +133,10 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "file_errors":0 }, - "files":[], + "files":{}, "errors": [ "first generic error", - "second generic error" + "second generic" ] }', ]; @@ -168,21 +172,22 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Foo", + "message": "Foo", "line": 1, "ignorable": true }, { "message": "Bar\nBar2", "line": 5, - "ignorable": true + "ignorable": true, + "tip": "a tip" } ] } }, "errors": [ "first generic error", - "second generic error" + "second generic" ] }', ]; @@ -233,4 +238,26 @@ public function testFormatErrors( $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent(), sprintf('%s: JSON do not match', $message)); } + public function dataFormatTip(): iterable + { + yield ['tip', 'tip']; + yield ['%configurationFile%', '%configurationFile%']; + yield ['this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', 'this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.']; + } + + /** + * @dataProvider dataFormatTip + */ + public function testFormatTip(string $tip, string $expectedTip): void + { + $formatter = new JsonErrorFormatter(false); + $formatter->formatErrors(new AnalysisResult([ + new Error('Foo', '/foo/bar.php', 1, true, null, null, $tip), + ], [], [], [], [], false, null, true, 0, false, []), $this->getOutput()); + + $content = $this->getOutputContent(); + $json = Json::decode($content, Json::FORCE_ARRAY); + $this->assertSame($expectedTip, $json['files']['/foo/bar.php']['messages'][0]['tip']); + } + } diff --git a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php index 87f056da80..f83f162bb2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php @@ -29,7 +29,7 @@ public function dataFormatterOutputProvider(): Generator 0, 0, ' - + ', @@ -74,7 +74,7 @@ public function dataFormatterOutputProvider(): Generator - + @@ -93,7 +93,7 @@ public function dataFormatterOutputProvider(): Generator - + ', @@ -112,7 +112,7 @@ public function dataFormatterOutputProvider(): Generator - + @@ -121,7 +121,7 @@ public function dataFormatterOutputProvider(): Generator - + ', diff --git a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php index 5b61402bbd..ffe1c436d3 100644 --- a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php @@ -11,72 +11,106 @@ class RawErrorFormatterTest extends ErrorFormatterTestCase public function dataFormatterOutputProvider(): iterable { yield [ - 'No errors', - 0, - 0, - 0, - '', + 'message' => 'No errors', + 'exitCode' => 0, + 'numFileErrors' => 0, + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '', ]; yield [ - 'One file error', - 1, - 1, - 0, - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n", + 'message' => 'One file error', + 'exitCode' => 1, + 'numFileErrors' => 1, + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n", ]; yield [ - 'One generic error', - 1, - 0, - 1, - '?:?:first generic error' . "\n", + 'message' => 'One generic error', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 1, + 'verbose' => false, + 'expected' => '?:?:first generic error' . "\n", ]; yield [ - 'Multiple file errors', - 1, - 4, - 0, - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar' . "\nBar2\n" . - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:1:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:5:Bar' . "\nBar2\n", + 'message' => 'Multiple file errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar +Bar2 +/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo +/data/folder/with space/and unicode 😃/project/foo.php:1:Foo +/data/folder/with space/and unicode 😃/project/foo.php:5:Bar +Bar2 +', ]; yield [ - 'Multiple generic errors', - 1, - 0, - 2, - '?:?:first generic error' . "\n" . - '?:?:second generic error' . "\n", + 'message' => 'Multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 2, + 'verbose' => false, + 'expected' => '?:?:first generic error +?:?:second generic +', ]; yield [ - 'Multiple file, multiple generic errors', - 1, - 4, - 2, - '?:?:first generic error' . "\n" . - '?:?:second generic error' . "\n" . - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar' . "\nBar2\n" . - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:1:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:5:Bar' . "\nBar2\n", + 'message' => 'Multiple file, multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 2, + 'verbose' => false, + 'expected' => '?:?:first generic error +?:?:second generic +/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar +Bar2 +/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo +/data/folder/with space/and unicode 😃/project/foo.php:1:Foo +/data/folder/with space/and unicode 😃/project/foo.php:5:Bar +Bar2 +', + ]; + + yield [ + 'message' => 'One file error with tip', + 'exitCode' => 1, + 'numFileErrors' => [5, 6], + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '/data/folder/with space/and unicode 😃/project/foo.php:5:Foobar\Buz +', + ]; + + yield [ + 'message' => 'One file error with tip and verbose', + 'exitCode' => 1, + 'numFileErrors' => [5, 6], + 'numGenericErrors' => 0, + 'verbose' => true, + 'expected' => '/data/folder/with space/and unicode 😃/project/foo.php:5:Foobar\Buz [identifier=foobar.buz] +', ]; } /** * @dataProvider dataFormatterOutputProvider - * + * @param array{int, int}|int $numFileErrors */ public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, + bool $verbose, string $expected, ): void { @@ -84,10 +118,10 @@ public function testFormatErrors( $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput(), + $this->getOutput(false, $verbose), ), sprintf('%s: response code do not match', $message)); - $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); + $this->assertEquals($expected, $this->getOutputContent(false, $verbose), sprintf('%s: output do not match', $message)); } } diff --git a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php index 0e3e23acc0..1ada3c4624 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php @@ -6,38 +6,49 @@ use PHPStan\Command\AnalysisResult; use PHPStan\File\FuzzyRelativePathHelper; use PHPStan\File\NullRelativePathHelper; +use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use function getenv; use function putenv; use function sprintf; -use const PHP_VERSION_ID; class TableErrorFormatterTest extends ErrorFormatterTestCase { + protected function setUp(): void + { + putenv('GITHUB_ACTIONS'); + } + protected function tearDown(): void { putenv('COLUMNS'); + putenv('TERM_PROGRAM'); } public function dataFormatterOutputProvider(): iterable { yield [ - 'No errors', - 0, - 0, - 0, - ' + 'message' => 'No errors', + 'exitCode' => 0, + 'numFileErrors' => 0, + 'numGenericErrors' => 0, + 'verbose' => false, + 'extraEnvVars' => [], + 'expected' => ' [OK] No errors ', ]; yield [ - 'One file error', - 1, - 1, - 0, - ' ------ ------------------------------------------------------------------- + 'message' => 'One file error', + 'exitCode' => 1, + 'numFileErrors' => 1, + 'numGenericErrors' => 0, + 'verbose' => false, + 'extraEnvVars' => [], + 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php ------ ------------------------------------------------------------------- 4 Foo @@ -50,11 +61,13 @@ public function dataFormatterOutputProvider(): iterable ]; yield [ - 'One generic error', - 1, - 0, - 1, - ' -- --------------------- + 'message' => 'One generic error', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 1, + 'verbose' => false, + 'extraEnvVars' => [], + 'expected' => ' -- --------------------- Error -- --------------------- first generic error @@ -67,11 +80,13 @@ public function dataFormatterOutputProvider(): iterable ]; yield [ - 'Multiple file errors', - 1, - 4, - 0, - ' ------ ------------------------------------------------------------------- + 'message' => 'Multiple file errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 0, + 'verbose' => false, + 'extraEnvVars' => [], + 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php ------ ------------------------------------------------------------------- 2 Bar @@ -79,13 +94,14 @@ public function dataFormatterOutputProvider(): iterable 4 Foo ------ ------------------------------------------------------------------- - ------ --------- + ------ ---------- Line foo.php - ------ --------- - 1 Foo + ------ ---------- + 1 Foo 5 Bar Bar2 - ------ --------- + 💡 a tip + ------ ---------- [ERROR] Found 4 errors @@ -93,16 +109,18 @@ public function dataFormatterOutputProvider(): iterable ]; yield [ - 'Multiple generic errors', - 1, - 0, - 2, - ' -- ---------------------- + 'message' => 'Multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 2, + 'verbose' => false, + 'extraEnvVars' => [], + 'expected' => ' -- ----------------------- Error - -- ---------------------- + -- ----------------------- first generic error - second generic error - -- ---------------------- + second generic + -- ----------------------- [ERROR] Found 2 errors @@ -111,11 +129,13 @@ public function dataFormatterOutputProvider(): iterable ]; yield [ - 'Multiple file, multiple generic errors', - 1, - 4, - 2, - ' ------ ------------------------------------------------------------------- + 'message' => 'Multiple file, multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 2, + 'verbose' => false, + 'extraEnvVars' => [], + 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php ------ ------------------------------------------------------------------- 2 Bar @@ -123,65 +143,154 @@ public function dataFormatterOutputProvider(): iterable 4 Foo ------ ------------------------------------------------------------------- - ------ --------- + ------ ---------- Line foo.php - ------ --------- - 1 Foo + ------ ---------- + 1 Foo 5 Bar Bar2 - ------ --------- + 💡 a tip + ------ ---------- - -- ---------------------- + -- ----------------------- Error - -- ---------------------- + -- ----------------------- first generic error - second generic error - -- ---------------------- + second generic + -- ----------------------- [ERROR] Found 6 errors +', + ]; + + yield [ + 'message' => 'One file error, called via Visual Studio Code', + 'exitCode' => 1, + 'numFileErrors' => 1, + 'numGenericErrors' => 0, + 'verbose' => false, + 'extraEnvVars' => ['TERM_PROGRAM=vscode'], + 'expected' => ' ------ ------------------------------------------------------------------- + Line folder with unicode 😃/file name with "spaces" and unicode 😃.php + ------ ------------------------------------------------------------------- + :4 Foo + ------ ------------------------------------------------------------------- + + + [ERROR] Found 1 error + +', + ]; + + yield [ + 'message' => 'One file error with tip', + 'exitCode' => 1, + 'numFileErrors' => [5, 6], + 'numGenericErrors' => 0, + 'verbose' => false, + 'extraEnvVars' => [], + 'expected' => ' ------ --------------- + Line foo.php + ------ --------------- + 5 Foobar\Buz + 🪪 foobar.buz + 💡 a tip + ------ --------------- + + + [ERROR] Found 1 error + +', + ]; + + yield [ + 'message' => 'One file error with tip and verbose', + 'exitCode' => 1, + 'numFileErrors' => [5, 6], + 'numGenericErrors' => 0, + 'verbose' => true, + 'extraEnvVars' => [], + 'expected' => ' ------ --------------- + Line foo.php + ------ --------------- + 5 Foobar\Buz + 🪪 foobar.buz + 💡 a tip + ------ --------------- + + + [ERROR] Found 1 error + ', ]; } /** * @dataProvider dataFormatterOutputProvider - * + * @param array{int, int}|int $numFileErrors + * @param array $extraEnvVars */ public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, + bool $verbose, + array $extraEnvVars, string $expected, ): void { - if (PHP_VERSION_ID >= 80100) { - self::markTestSkipped('Skipped on PHP 8.1 because of different result'); + $formatter = $this->createErrorFormatter(null); + + // NOTE: extra env vars need to be cleared in tearDown() + foreach ($extraEnvVars as $envVar) { + putenv($envVar); } - $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(false, $verbose), ), sprintf('%s: response code do not match', $message)); - $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); + $this->assertEquals($expected, $this->getOutputContent(false, $verbose), sprintf('%s: output do not match', $message)); } public function testEditorUrlWithTrait(): void { - $formatter = new TableErrorFormatter(new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'), false, 'editor://%file%/%line%'); + $formatter = $this->createErrorFormatter('editor://%file%/%line%'); $error = new Error('Test', 'Foo.php (in context of trait)', 12, true, 'Foo.php', 'Bar.php'); - $formatter->formatErrors(new AnalysisResult([$error], [], [], [], false, null, true), $this->getOutput()); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput()); $this->assertStringContainsString('Bar.php', $this->getOutputContent()); } + public function testEditorUrlWithRelativePath(): void + { + if (getenv('TERMINAL_EMULATOR') === 'JetBrains-JediTerm') { + $this->markTestSkipped('PhpStorm console does not support links in console.'); + } + + $formatter = $this->createErrorFormatter('editor://custom/path/%relFile%/%line%'); + $error = new Error('Test', 'Foo.php', 12, true, self::DIRECTORY_PATH . '/rel/Foo.php'); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput(true)); + + $this->assertStringContainsString('editor://custom/path/rel/Foo.php', $this->getOutputContent(true)); + } + + public function testEditorUrlWithCustomTitle(): void + { + $formatter = $this->createErrorFormatter('editor://any', '%relFile%:%line%'); + $error = new Error('Test', 'Foo.php', 12, true, self::DIRECTORY_PATH . '/rel/Foo.php'); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput(true)); + + $this->assertStringContainsString('rel/Foo.php:12', $this->getOutputContent(true)); + } + public function testBug6727(): void { putenv('COLUMNS=30'); - $formatter = new TableErrorFormatter(new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'), false, null); + $formatter = $this->createErrorFormatter(null); $formatter->formatErrors( new AnalysisResult( [ @@ -194,13 +303,34 @@ public function testBug6727(): void [], [], [], + [], false, null, true, + 0, + false, + [], ), $this->getOutput(), ); self::expectNotToPerformAssertions(); } + private function createErrorFormatter(?string $editorUrl, ?string $editorUrlTitle = null): TableErrorFormatter + { + $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'); + + return new TableErrorFormatter( + $relativePathHelper, + new SimpleRelativePathHelper(self::DIRECTORY_PATH), + new CiDetectedErrorFormatter( + new GithubErrorFormatter($relativePathHelper), + new TeamcityErrorFormatter($relativePathHelper), + ), + false, + $editorUrl, + $editorUrlTitle, + ); + } + } diff --git a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php index 37e7e3451c..6543fbae67 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php @@ -18,6 +18,7 @@ public function dataFormatterOutputProvider(): iterable 0, 0, '', + '', ]; yield [ @@ -48,8 +49,8 @@ public function dataFormatterOutputProvider(): iterable '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'2\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'4\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ', ]; @@ -60,7 +61,7 @@ public function dataFormatterOutputProvider(): iterable 2, '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] ##teamcity[inspection typeId=\'phpstan\' message=\'first generic error\' file=\'.\' SEVERITY=\'ERROR\'] -##teamcity[inspection typeId=\'phpstan\' message=\'second generic error\' file=\'.\' SEVERITY=\'ERROR\'] +##teamcity[inspection typeId=\'phpstan\' message=\'second generic\' file=\'.\' SEVERITY=\'ERROR\'] ', ]; @@ -72,22 +73,33 @@ public function dataFormatterOutputProvider(): iterable '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'2\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'4\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ##teamcity[inspection typeId=\'phpstan\' message=\'first generic error\' file=\'.\' SEVERITY=\'ERROR\'] -##teamcity[inspection typeId=\'phpstan\' message=\'second generic error\' file=\'.\' SEVERITY=\'ERROR\'] +##teamcity[inspection typeId=\'phpstan\' message=\'second generic\' file=\'.\' SEVERITY=\'ERROR\'] +', + ]; + + yield [ + 'One file error', + 1, + [4, 2], + 0, + '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Foobar\Buz (🪪 foobar.buz)\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ', ]; } /** * @dataProvider dataFormatterOutputProvider - * + * @param array{int, int}|int $numFileErrors */ public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, string $expected, ): void diff --git a/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon b/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon index ca7c8f9c2c..ff39bfc9d7 100644 --- a/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon +++ b/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon @@ -11,7 +11,10 @@ parameters: path: WindowsNewlines.php - - message: "#^PHPDoc tag @param has invalid value \\(\\)\\: Unexpected token \"\\\\n\\\\t \\* \", expected type at offset 113$#" + message: """ + #^PHPDoc tag @param has invalid value \\(\r + \\$object\\)\\: Unexpected token "\\\\r\\\\n\\\\t \\* ", expected type at offset 113 on line 4$# + """ count: 1 path: WindowsNewlines.php diff --git a/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon b/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon index 3bfe998b6e..398e241bd7 100644 --- a/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon +++ b/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon @@ -11,7 +11,10 @@ parameters: path: UnixNewlines.php - - message: "#^PHPDoc tag @param has invalid value \\(\\)\\: Unexpected token \"\\\\r\\\\n\\\\t \\* \", expected type at offset 110$#" + message: """ + #^PHPDoc tag @param has invalid value \\( + \\$object\\)\\: Unexpected token "\\\\n\\\\t \\* ", expected type at offset 110 on line 4$# + """ count: 1 path: UnixNewlines.php diff --git a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php index 9dff05c44f..39902aa01f 100644 --- a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php +++ b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -100,12 +100,48 @@ public function dataValidate(): array false, false, ], + [ + '~(a\()~', + [], + false, + false, + ], + [ + '~b\\\()~', + [], + false, + true, + ], + [ + '~(c\\\\\()~', + [], + false, + false, + ], [ '~Result of || is always true.~', [], false, true, ], + [ + '~a\||~', + [], + false, + false, + ], + [ + '~b\\\||~', + [], + false, + true, + ], + [ + '~c\\\\\||~', + [], + false, + false, + ], [ '#Method PragmaRX\Notified\Data\Repositories\Notified::firstOrCreateByEvent() should return PragmaRX\Notified\Data\Models\Notified but returns Illuminate\Database\Eloquent\Model|null#', [], @@ -126,7 +162,7 @@ public function testValidate( bool $expectAllErrorsIgnored, ): void { - $grammar = new Read('hoa://Library/Regex/Grammar.pp'); + $grammar = new Read(__DIR__ . '/../../../resources/RegexGrammar.pp'); $parser = Llk::load($grammar); $validator = new IgnoredRegexValidator($parser, self::getContainer()->getByType(TypeStringResolver::class)); diff --git a/tests/PHPStan/Command/data/file-without-errors.php b/tests/PHPStan/Command/data/file-without-errors.php index 4c4ad920ae..08929907d3 100644 --- a/tests/PHPStan/Command/data/file-without-errors.php +++ b/tests/PHPStan/Command/data/file-without-errors.php @@ -1,3 +1,3 @@ = 70300) { - array_splice($expectedFiles, 6, 0, [ - $phpunitFunctions, - ]); - } - $expectedFiles = array_map(static fn (string $path): string => $fileHelper->normalizePath($path), $expectedFiles); sort($expectedFiles); diff --git a/tests/PHPStan/DependencyInjection/ConditionalTagsExtensionTest.php b/tests/PHPStan/DependencyInjection/ConditionalTagsExtensionTest.php new file mode 100644 index 0000000000..5c2b2e28d5 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/ConditionalTagsExtensionTest.php @@ -0,0 +1,35 @@ +getServicesByTag(LazyRegistry::RULE_TAG); + $enabledServices = array_map(static fn ($service) => get_class($service), $enabledServices); + $this->assertNotContains(TestedConditionalServiceDisabled::class, $enabledServices); + $this->assertContains(TestedConditionalServiceEnabled::class, $enabledServices); + $this->assertNotContains(TestedConditionalServiceDisabledDisabled::class, $enabledServices); + $this->assertNotContains(TestedConditionalServiceDisabledEnabled::class, $enabledServices); + $this->assertNotContains(TestedConditionalServiceEnabledDisabled::class, $enabledServices); + $this->assertContains(TestedConditionalServiceEnabledEnabled::class, $enabledServices); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/conditionalTags.neon', + ]; + } + +} diff --git a/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php b/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php new file mode 100644 index 0000000000..112b256ed4 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/IgnoreErrorsTest.php @@ -0,0 +1,25 @@ +assertCount(12, self::getContainer()->getParameter('ignoreErrors')); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/ignoreErrors.neon', + ]; + } + +} diff --git a/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabled.php b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabled.php new file mode 100644 index 0000000000..0d0af6673c --- /dev/null +++ b/tests/PHPStan/DependencyInjection/TestedConditionalServiceDisabled.php @@ -0,0 +1,8 @@ +skipIfNotOnWindows(); - $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, []); + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); } @@ -34,7 +34,7 @@ public function dataExcludeOnWindows(): array ], [ __DIR__ . '/data/excluded-file.php', - [__DIR__], + [__DIR__ . '/*'], true, ], [ @@ -64,7 +64,7 @@ public function dataExcludeOnWindows(): array ], [ __DIR__ . '\data\parse-error.php', - ['tests/PHPStan/File/data'], + ['*/tests/PHPStan/File/data/*'], true, ], [ @@ -99,7 +99,7 @@ public function dataExcludeOnWindows(): array ], [ 'c:\etc\phpstan\dummy-1.php', - ['c:\etc\phpstan\\'], + ['c:\etc\phpstan\\*'], true, ], [ @@ -109,7 +109,7 @@ public function dataExcludeOnWindows(): array ], [ 'c:\etc\phpstan-test\dummy-2.php', - ['c:\etc\phpstan'], + ['c:\etc\phpstan*'], true, ], ]; @@ -127,7 +127,7 @@ public function testFilesAreExcludedFromAnalysingOnUnix( { $this->skipIfNotOnUnix(); - $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, []); + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); } @@ -142,7 +142,7 @@ public function dataExcludeOnUnix(): array ], [ __DIR__ . '/data/excluded-file.php', - [__DIR__], + [__DIR__ . '/*'], true, ], [ @@ -170,11 +170,6 @@ public function dataExcludeOnUnix(): array [__DIR__ . '/data/[pP]arse-[eE]rror.ph[pP]'], true, ], - [ - __DIR__ . '/data/parse-error.php', - ['tests/PHPStan/File/data'], - true, - ], [ __DIR__ . '/data/parse-error.php', [__DIR__ . '/aaa'], @@ -192,7 +187,7 @@ public function dataExcludeOnUnix(): array ], [ '/etc/phpstan/dummy-1.php', - ['/etc/phpstan/'], + ['/etc/phpstan/*'], true, ], [ @@ -202,10 +197,62 @@ public function dataExcludeOnUnix(): array ], [ '/etc/phpstan-test/dummy-2.php', - ['/etc/phpstan'], + ['/etc/phpstan*'], true, ], ]; } + public function dataNoImplicitWildcard(): iterable + { + yield [ + __DIR__ . '/tests/foo.php', + [ + __DIR__ . '/test', + ], + false, + ]; + + yield [ + __DIR__ . '/test/foo.php', + [ + __DIR__ . '/test', + ], + true, + ]; + + yield [ + __DIR__ . '/FileExcluderTest.php', + [ + __DIR__ . '/FileExcluderTest.php', + ], + true, + ]; + + yield [ + __DIR__ . '/tests/foo.php', + [ + __DIR__ . '/test*', + ], + true, + ]; + } + + /** + * @dataProvider dataNoImplicitWildcard + * @param string[] $analyseExcludes + */ + public function testNoImplicitWildcard( + string $filePath, + array $analyseExcludes, + bool $isExcluded, + ): void + { + $this->skipIfNotOnUnix(); + + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); + + $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); + } + } diff --git a/tests/PHPStan/File/FileHelperTest.php b/tests/PHPStan/File/FileHelperTest.php index 5cc0941fd7..409936a8d1 100644 --- a/tests/PHPStan/File/FileHelperTest.php +++ b/tests/PHPStan/File/FileHelperTest.php @@ -20,6 +20,8 @@ public function dataAbsolutizePathOnWindows(): array ['users', 'C:\abcd\users'], ['../lib', 'C:\abcd\../lib'], ['./lib', 'C:\abcd\./lib'], + ['vFs-v1.0://a\b', 'vFs-v1.0://a\b'], + ['./x://a\b', 'C:\abcd\./x://a\b'], ]; } @@ -47,6 +49,8 @@ public function dataAbsolutizePathOnLinuxOrMac(): array ['../lib', '/abcd/../lib'], ['./lib', '/abcd/./lib'], ['phar:///home/users/', 'phar:///home/users/'], + ['vFs-v1.0://a/b', 'vFs-v1.0://a/b'], + ['./x://a/b', '/abcd/./x://a/b'], ]; } @@ -73,6 +77,7 @@ public function dataNormalizePathOnWindows(): array ['/home/users/./phpstan', '\home\users\phpstan'], ['/home/users/../../phpstan/', '\phpstan'], ['./phpstan/', 'phpstan'], + ['vFs-v1.0://a/b', 'vfs-v1.0://a\b'], ]; } @@ -98,6 +103,7 @@ public function dataNormalizePathOnLinuxOrMac(): array ['/home/users/./phpstan', '/home/users/phpstan'], ['/home/users/../../phpstan/', '/phpstan'], ['./phpstan/', 'phpstan'], + ['vFs-v1.0://a/b', 'vfs-v1.0://a/b'], ['phar:///usr/local/bin/phpstan.phar/tmp/cache/../..', 'phar:///usr/local/bin/phpstan.phar'], ['phar:///usr/local/bin/phpstan.phar/tmp/cache/../../..', '/usr/local/bin'], ]; diff --git a/tests/PHPStan/File/RelativePathHelperTest.php b/tests/PHPStan/File/RelativePathHelperTest.php index 5dcb549b8d..5d5b3eca41 100644 --- a/tests/PHPStan/File/RelativePathHelperTest.php +++ b/tests/PHPStan/File/RelativePathHelperTest.php @@ -142,6 +142,40 @@ public function dataGetRelativePath(): array '/usr/app/src/analyzed.php', '/usr/app/src/analyzed.php', ], + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal.php', + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/src', + ], + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/src/index.php', + 'index.php', + ], + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal', + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal/src', + ], + '/Users/ondrej/Downloads/phpstan-wtf/normal/src/index.php', + 'index.php', + ], + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal.php', + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/src', + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/tests', + ], + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/src/index.php', + 'src/index.php', + ], + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal', + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal/src', + '/Users/ondrej/Downloads/phpstan-wtf/normal/tests', + ], + '/Users/ondrej/Downloads/phpstan-wtf/normal/src/index.php', + 'src/index.php', + ], ]; } diff --git a/tests/PHPStan/File/test/.gitkeep b/tests/PHPStan/File/test/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/PHPStan/Fixture/ManyCasesTestEnum.php b/tests/PHPStan/Fixture/ManyCasesTestEnum.php new file mode 100644 index 0000000000..6575f39e69 --- /dev/null +++ b/tests/PHPStan/Fixture/ManyCasesTestEnum.php @@ -0,0 +1,15 @@ += 8.1 + +namespace PHPStan\Fixture; + +enum ManyCasesTestEnum +{ + + case A; + case B; + case C; + case D; + case E; + case F; + +} diff --git a/tests/PHPStan/Generics/GenericsIntegrationTest.php b/tests/PHPStan/Generics/GenericsIntegrationTest.php index e88e973b8f..a100809355 100644 --- a/tests/PHPStan/Generics/GenericsIntegrationTest.php +++ b/tests/PHPStan/Generics/GenericsIntegrationTest.php @@ -5,12 +5,12 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class GenericsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['functions'], @@ -19,6 +19,7 @@ public function dataTopics(): array ['varyingAcceptor'], ['classes'], ['variance'], + ['typeProjections'], ['bug2574'], ['bug2577'], ['bug2620'], diff --git a/tests/PHPStan/Generics/data/bug2620-3.json b/tests/PHPStan/Generics/data/bug2620-3.json index 2906315156..a73d0e83fe 100644 --- a/tests/PHPStan/Generics/data/bug2620-3.json +++ b/tests/PHPStan/Generics/data/bug2620-3.json @@ -4,4 +4,4 @@ "line": 17, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Generics/data/bug2620.php b/tests/PHPStan/Generics/data/bug2620.php index b2f0a6295d..d502138259 100644 --- a/tests/PHPStan/Generics/data/bug2620.php +++ b/tests/PHPStan/Generics/data/bug2620.php @@ -14,6 +14,7 @@ class SomeIterator implements \IteratorAggregate { /** * @return \Traversable */ + #[\ReturnTypeWillChange] public function getIterator() { yield new Bar; } diff --git a/tests/PHPStan/Generics/data/bug2622.php b/tests/PHPStan/Generics/data/bug2622.php index f3aa345443..c92e6faa21 100644 --- a/tests/PHPStan/Generics/data/bug2622.php +++ b/tests/PHPStan/Generics/data/bug2622.php @@ -14,6 +14,7 @@ public function __construct() { $this->values = []; } + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayObject($this->values); } diff --git a/tests/PHPStan/Generics/data/classes-4.json b/tests/PHPStan/Generics/data/classes-4.json new file mode 100644 index 0000000000..3b7dd01977 --- /dev/null +++ b/tests/PHPStan/Generics/data/classes-4.json @@ -0,0 +1,7 @@ +[ + { + "message": "Call to new PHPStan\\Generics\\Classes\\SomeRule() on a separate line has no effect.", + "line": 283, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/typeProjections-0.json b/tests/PHPStan/Generics/data/typeProjections-0.json new file mode 100644 index 0000000000..548c221a62 --- /dev/null +++ b/tests/PHPStan/Generics/data/typeProjections-0.json @@ -0,0 +1,37 @@ +[ + { + "message": "Dumped type: PHPStan\\Generics\\TypeProjections\\B", + "line": 38, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 48, + "ignorable": false + }, + { + "message": "Dumped type: PHPStan\\Generics\\TypeProjections\\A", + "line": 56, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 65, + "ignorable": false + }, + { + "message": "Dumped type: PHPStan\\Generics\\TypeProjections\\B", + "line": 91, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 105, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 119, + "ignorable": false + } +] diff --git a/tests/PHPStan/Generics/data/typeProjections-5.json b/tests/PHPStan/Generics/data/typeProjections-5.json new file mode 100644 index 0000000000..ada4b861f0 --- /dev/null +++ b/tests/PHPStan/Generics/data/typeProjections-5.json @@ -0,0 +1,52 @@ +[ + { + "message": "Parameter #1 $item of method PHPStan\\Generics\\TypeProjections\\Box::pack() expects never, PHPStan\\Generics\\TypeProjections\\B given.", + "line": 37, + "ignorable": true + }, + { + "message": "Parameter #1 $item of method PHPStan\\Generics\\TypeProjections\\Box::pack() expects PHPStan\\Generics\\TypeProjections\\B, PHPStan\\Generics\\TypeProjections\\A given.", + "line": 46, + "ignorable": true + }, + { + "message": "Parameter #1 $item of method PHPStan\\Generics\\TypeProjections\\Box<*>::pack() expects never, PHPStan\\Generics\\TypeProjections\\A given.", + "line": 64, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\A given.", + "line": 94, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\B given.", + "line": 95, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\C given.", + "line": 96, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): PHPStan\\Generics\\TypeProjections\\B, Closure(): PHPStan\\Generics\\TypeProjections\\A given.", + "line": 108, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped<*>::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\A given.", + "line": 122, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped<*>::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\B given.", + "line": 123, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped<*>::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\C given.", + "line": 124, + "ignorable": true + } +] diff --git a/tests/PHPStan/Generics/data/typeProjections.php b/tests/PHPStan/Generics/data/typeProjections.php new file mode 100644 index 0000000000..555c1a50ae --- /dev/null +++ b/tests/PHPStan/Generics/data/typeProjections.php @@ -0,0 +1,125 @@ + $box + */ +function testCovariant(Box $box, B $b): void +{ + $box->pack($b); + dumpType($box->unpack()); +} + +/** + * @param Box $box + */ +function testContravariant(Box $box, A $a, B $b): void +{ + $box->pack($a); + $box->pack($b); + dumpType($box->unpack()); +} + +/** + * @param BoundedBox $box + */ +function testContravariantWithBound(BoundedBox $box): void +{ + dumpType($box->unpack()); +} + +/** + * @param Box<*> $box + */ +function testStar(Box $box, A $a): void +{ + $box->pack($a); + dumpType($box->unpack()); +} + + +/** + * @template T + */ +interface Mapped +{ + /** + * @param callable(T): void $mapper + */ + public function mapIn(callable $mapper): void; + + /** + * @param callable(): T $mapper + */ + public function mapOut(callable $mapper): void; +} + +/** + * @param Mapped $mapped + */ +function testCovariantMapped(Mapped $mapped): void +{ + $mapped->mapIn(function ($foo) { + dumpType($foo); + }); + + $mapped->mapOut(fn() => new A); + $mapped->mapOut(fn() => new B); + $mapped->mapOut(fn() => new C); +} + +/** + * @param Mapped $mapped + */ +function testContravariantMapped(Mapped $mapped): void +{ + $mapped->mapIn(function ($foo) { + dumpType($foo); + }); + + $mapped->mapOut(fn() => new A); + $mapped->mapOut(fn() => new B); + $mapped->mapOut(fn() => new C); +} + +/** + * @param Mapped<*> $mapped + */ +function testStarMapped(Mapped $mapped): void +{ + $mapped->mapIn(function ($foo) { + dumpType($foo); + }); + + $mapped->mapOut(fn() => new A); + $mapped->mapOut(fn() => new B); + $mapped->mapOut(fn() => new C); +} diff --git a/tests/PHPStan/Generics/data/variance-2.json b/tests/PHPStan/Generics/data/variance-2.json index 888e38af9c..3c7d9da4d4 100644 --- a/tests/PHPStan/Generics/data/variance-2.json +++ b/tests/PHPStan/Generics/data/variance-2.json @@ -60,13 +60,18 @@ "ignorable": true }, { - "message": "Template type T is declared as covariant, but occurs in invariant position in parameter v of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::__construct().", - "line": 142, + "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter t of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", + "line": 154, + "ignorable": true + }, + { + "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter w of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", + "line": 154, "ignorable": true }, { "message": "Template type T is declared as covariant, but occurs in invariant position in parameter v of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", - "line": 153, + "line": 154, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance-4.json b/tests/PHPStan/Generics/data/variance-4.json index c6fa368103..7757cc3dea 100644 --- a/tests/PHPStan/Generics/data/variance-4.json +++ b/tests/PHPStan/Generics/data/variance-4.json @@ -1,7 +1,7 @@ [ { "message": "Property PHPStan\\Generics\\Variance\\ConstructorAndStatic::$data is never read, only written.", - "line": 134, + "line": 135, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance-5.json b/tests/PHPStan/Generics/data/variance-5.json index 016235e900..a16b075630 100644 --- a/tests/PHPStan/Generics/data/variance-5.json +++ b/tests/PHPStan/Generics/data/variance-5.json @@ -1,7 +1,7 @@ [ { "message": "Parameter #1 $it of function PHPStan\\Generics\\Variance\\acceptInvariantIterOfDateTimeInterface expects PHPStan\\Generics\\Variance\\InvariantIter, PHPStan\\Generics\\Variance\\InvariantIter given.", - "line": 164, + "line": 165, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance.php b/tests/PHPStan/Generics/data/variance.php index defa4910d3..8b68847f77 100644 --- a/tests/PHPStan/Generics/data/variance.php +++ b/tests/PHPStan/Generics/data/variance.php @@ -127,6 +127,7 @@ function set($v): void; /** * @template-covariant T * @template U + * @phpstan-consistent-constructor */ class ConstructorAndStatic { @@ -151,7 +152,7 @@ public function __construct($t, $u, $v, $w) { * @return Static */ public static function create($t, $u, $v, $w) { - return new self($t, $u, $v, $w); + return new static($t, $u, $v, $w); } } diff --git a/tests/PHPStan/Internal/ArrayHelperTest.php b/tests/PHPStan/Internal/ArrayHelperTest.php new file mode 100644 index 0000000000..6cf63b46a7 --- /dev/null +++ b/tests/PHPStan/Internal/ArrayHelperTest.php @@ -0,0 +1,61 @@ + [ + 'dep2a' => [ + 'dep3a' => null, + ], + 'dep2b' => null, + ], + 'dep1b' => null, + ]; + + ArrayHelper::unsetKeyAtPath($array, ['dep1a', 'dep2a', 'dep3a']); + + $this->assertSame([ + 'dep1a' => [ + 'dep2a' => [], + 'dep2b' => null, + ], + 'dep1b' => null, + ], $array); + + ArrayHelper::unsetKeyAtPath($array, ['dep1a', 'dep2a']); + + $this->assertSame([ + 'dep1a' => [ + 'dep2b' => null, + ], + 'dep1b' => null, + ], $array); + + ArrayHelper::unsetKeyAtPath($array, ['dep1a']); + + $this->assertSame([ + 'dep1b' => null, + ], $array); + + ArrayHelper::unsetKeyAtPath($array, ['dep1b']); + + $this->assertSame([], $array); + } + + public function testUnsetKeyAtPathEmpty(): void + { + $array = []; + + ArrayHelper::unsetKeyAtPath($array, ['foo', 'bar']); + + $this->assertSame([], $array); + } + +} diff --git a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php index 5a7b686525..d8d3c28878 100644 --- a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php +++ b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php @@ -5,12 +5,12 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class InferPrivatePropertyTypeFromConstructorIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['inferPropertyType'], diff --git a/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php b/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php deleted file mode 100644 index ec322d6b09..0000000000 --- a/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php +++ /dev/null @@ -1,40 +0,0 @@ -= 80300) { + $topics[] = ['constantAccesses83']; + } + + return $topics; } public function getDataPath(): string diff --git a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php index bf14a77c9e..d35d7dade7 100644 --- a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php +++ b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php @@ -5,12 +5,12 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class NamedArgumentsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['namedArguments'], @@ -29,7 +29,7 @@ public function getPhpStanExecutablePath(): string public function getPhpStanConfigPath(): string { - return __DIR__ . '/staticReflection.neon'; + return __DIR__ . '/namedArguments.neon'; } protected function shouldAutoloadAnalysedFile(): bool diff --git a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php index 1c4dcf3142..59ded11998 100644 --- a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php +++ b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php @@ -5,12 +5,12 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class StubValidatorIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['stubValidator'], diff --git a/tests/PHPStan/Levels/StubsIntegrationTest.php b/tests/PHPStan/Levels/StubsIntegrationTest.php index 11ad056599..089c9b5495 100644 --- a/tests/PHPStan/Levels/StubsIntegrationTest.php +++ b/tests/PHPStan/Levels/StubsIntegrationTest.php @@ -5,12 +5,12 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class StubsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { require_once __DIR__ . '/data/stubs-functions.php'; diff --git a/tests/PHPStan/Levels/alwaysTrue.neon b/tests/PHPStan/Levels/alwaysTrue.neon deleted file mode 100644 index b385d1c956..0000000000 --- a/tests/PHPStan/Levels/alwaysTrue.neon +++ /dev/null @@ -1,7 +0,0 @@ -includes: - - ../../../conf/bleedingEdge.neon - -parameters: - checkAlwaysTrueCheckTypeFunctionCall: true - checkAlwaysTrueInstanceof: true - checkAlwaysTrueStrictComparison: true diff --git a/tests/PHPStan/Levels/data/acceptTypes-10.json b/tests/PHPStan/Levels/data/acceptTypes-10.json new file mode 100644 index 0000000000..8a1b7a3992 --- /dev/null +++ b/tests/PHPStan/Levels/data/acceptTypes-10.json @@ -0,0 +1,17 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 170, + "ignorable": true + }, + { + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(mixed): mixed given.", + "line": 325, + "ignorable": true + }, + { + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(mixed): mixed given.", + "line": 326, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-4.json b/tests/PHPStan/Levels/data/acceptTypes-4.json new file mode 100644 index 0000000000..fbcb96fc5a --- /dev/null +++ b/tests/PHPStan/Levels/data/acceptTypes-4.json @@ -0,0 +1,22 @@ +[ + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", + "line": 531, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", + "line": 532, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", + "line": 542, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", + "line": 543, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index c9f5fdf981..76aac5c080 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -94,16 +94,6 @@ "line": 251, "ignorable": true }, - { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 319, - "ignorable": true - }, - { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 320, - "ignorable": true - }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float given.", "line": 412, @@ -164,6 +154,11 @@ "line": 585, "ignorable": true }, + { + "message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects static(Levels\\AcceptTypes\\RequireObjectWithoutClassType), object given.", + "line": 648, + "ignorable": true + }, { "message": "Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (-1).", "line": 671, @@ -185,23 +180,13 @@ "ignorable": true }, { - "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, string given.", - "line": 708, - "ignorable": true - }, - { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", "line": 733, "ignorable": true }, - { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", - "line": 735, - "ignorable": true - }, { "message": "Parameter #2 $array of function implode expects array|null, int given.", "line": 763, "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 8345aca34c..ba67e6a9d6 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -19,11 +19,6 @@ "line": 92, "ignorable": true }, - { - "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Foo::doBarArray() expects array, array given.", - "line": 131, - "ignorable": true - }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Foo::doBarArray() expects array, array given.", "line": 132, @@ -40,25 +35,35 @@ "ignorable": true }, { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", "line": 283, "ignorable": true }, { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", "line": 284, "ignorable": true }, { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", "line": 301, "ignorable": true }, { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", "line": 302, "ignorable": true }, + { + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", + "line": 319, + "ignorable": true + }, + { + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", + "line": 320, + "ignorable": true + }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float|int given.", "line": 415, @@ -80,7 +85,7 @@ "ignorable": true }, { - "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", "line": 531, "ignorable": true }, @@ -90,12 +95,12 @@ "ignorable": true }, { - "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", "line": 542, "ignorable": true }, { - "message": "Parameter #1 $foo of method Levels\\AcceptTypes\\Baz::requireFoo() expects Levels\\AcceptTypes\\Foo, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $foo of method Levels\\AcceptTypes\\Baz::requireFoo() expects Levels\\AcceptTypes\\Foo, array|Levels\\AcceptTypes\\Foo given.", "line": 543, "ignorable": true }, @@ -134,11 +139,6 @@ "line": 647, "ignorable": true }, - { - "message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects Levels\\AcceptTypes\\RequireObjectWithoutClassType, object given.", - "line": 648, - "ignorable": true - }, { "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<0, 1>).", "line": 690, @@ -154,9 +154,19 @@ "line": 692, "ignorable": true }, + { + "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, string given.", + "line": 708, + "ignorable": true + }, + { + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", + "line": 735, + "ignorable": true + }, { "message": "Parameter #2 $array of function implode expects array|null, array|int|string given.", "line": 756, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/acceptTypes-8.json b/tests/PHPStan/Levels/data/acceptTypes-8.json index 49eddfd569..10728d1f25 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-8.json +++ b/tests/PHPStan/Levels/data/acceptTypes-8.json @@ -14,6 +14,11 @@ "line": 91, "ignorable": true }, + { + "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Foo::doBarArray() expects array, array given.", + "line": 131, + "ignorable": true + }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, int|null given.", "line": 414, @@ -28,5 +33,15 @@ "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBarArray() expects array, array given.", "line": 495, "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Discussion8209::test1() should return int but returns int|null.", + "line": 771, + "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Discussion8209::test2() should return array but returns array.", + "line": 779, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes.php b/tests/PHPStan/Levels/data/acceptTypes.php index d16184f062..61b2c1fbbb 100644 --- a/tests/PHPStan/Levels/data/acceptTypes.php +++ b/tests/PHPStan/Levels/data/acceptTypes.php @@ -763,3 +763,19 @@ public function invalidType($invalid) { $imploded = implode('abc', $invalid); } } + +class Discussion8209 +{ + public function test1(?int $id): int + { + return $id; + } + + /** + * @return array + */ + public function test2(?int $id): array + { + return [$id]; + } +} diff --git a/tests/PHPStan/Levels/data/arrayAccess-10.json b/tests/PHPStan/Levels/data/arrayAccess-10.json new file mode 100644 index 0000000000..9dc3ca3eb9 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayAccess-10.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot assign offset mixed to SplObjectStorage.", + "line": 43, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-8.json b/tests/PHPStan/Levels/data/arrayDestructuring-8.json index 7842bce806..3b033abd6d 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring-8.json +++ b/tests/PHPStan/Levels/data/arrayDestructuring-8.json @@ -1,7 +1,7 @@ [ { - "message": "Cannot use array destructuring on array|null.", + "message": "Cannot use array destructuring on array|null.", "line": 15, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-10.json b/tests/PHPStan/Levels/data/arrayDimFetches-10.json new file mode 100644 index 0000000000..900a4ee636 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDimFetches-10.json @@ -0,0 +1,22 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 14, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 21, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 27, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 28, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-7.json b/tests/PHPStan/Levels/data/arrayDimFetches-7.json index 25e275b28e..23df32943a 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-7.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-7.json @@ -15,7 +15,7 @@ "ignorable": true }, { - "message": "Offset 'b' does not exist on array{a: 1, b?: 1}.", + "message": "Offset 'b' might not exist on array{a: 1, b?: 1}.", "line": 40, "ignorable": true }, @@ -28,5 +28,10 @@ "message": "Cannot access offset 'foo' on iterable.", "line": 58, "ignorable": true + }, + { + "message": "Cannot access offset 'foo' on iterable.", + "line": 66, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-8.json b/tests/PHPStan/Levels/data/arrayDimFetches-8.json index 9f4c150113..e7e6efcd13 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-8.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-8.json @@ -1,12 +1,12 @@ [ { - "message": "Offset 0 does not exist on array|null.", + "message": "Offset 0 might not exist on array|null.", "line": 15, "ignorable": true }, { - "message": "Offset 0 does not exist on array|null.", + "message": "Offset 0 might not exist on array|null.", "line": 50, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayDimFetches.php b/tests/PHPStan/Levels/data/arrayDimFetches.php index effe56842c..269128a93e 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches.php +++ b/tests/PHPStan/Levels/data/arrayDimFetches.php @@ -54,6 +54,14 @@ public function doBaz($a, $b): void * @param iterable $iterable */ public function iterableOffset($iterable): void + { + var_dump($iterable['foo']); + } + + /** + * @param iterable $iterable + */ + public function iterableOffsetWithUnset($iterable): void { unset($iterable['foo']); } diff --git a/tests/PHPStan/Levels/data/binaryOps.php b/tests/PHPStan/Levels/data/binaryOps.php index 7b04fedbae..fd53d3f946 100644 --- a/tests/PHPStan/Levels/data/binaryOps.php +++ b/tests/PHPStan/Levels/data/binaryOps.php @@ -18,14 +18,14 @@ public function doFoo( $stringOrObject ) { - $int + $int; - $int + $intOrString; - $int + $stringOrObject; - $int + $string; - $string + $string; - $intOrString + $stringOrObject; - $intOrString + $string; - $stringOrObject + $stringOrObject; + $result = $int + $int; + $result = $int + $intOrString; + $result = $int + $stringOrObject; + $result = $int + $string; + $result = $string + $string; + $result = $intOrString + $stringOrObject; + $result = $intOrString + $string; + $result = $stringOrObject + $stringOrObject; } } diff --git a/tests/PHPStan/Levels/data/callableCalls-10-missing.json b/tests/PHPStan/Levels/data/callableCalls-10-missing.json new file mode 100644 index 0000000000..5c7f12b38d --- /dev/null +++ b/tests/PHPStan/Levels/data/callableCalls-10-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/callableCalls.php b/tests/PHPStan/Levels/data/callableCalls.php index 4b22fa723c..52f311d8d8 100644 --- a/tests/PHPStan/Levels/data/callableCalls.php +++ b/tests/PHPStan/Levels/data/callableCalls.php @@ -23,7 +23,7 @@ public function doFoo( $c(); $d(); $f = function (int $i) { - + echo '1'; }; $f(1); $f(1.1); diff --git a/tests/PHPStan/Levels/data/callableVariance-4.json b/tests/PHPStan/Levels/data/callableVariance-4.json new file mode 100644 index 0000000000..1af09ec95a --- /dev/null +++ b/tests/PHPStan/Levels/data/callableVariance-4.json @@ -0,0 +1,27 @@ +[ + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 81, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 82, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 83, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 84, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 85, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/callableVariance-5.json b/tests/PHPStan/Levels/data/callableVariance-5.json index 81682ac6e7..c37c7adeb0 100644 --- a/tests/PHPStan/Levels/data/callableVariance-5.json +++ b/tests/PHPStan/Levels/data/callableVariance-5.json @@ -1,6 +1,6 @@ [ { - "message": "Parameter #1 $ of callable callable(Levels\\CallableVariance\\B): void expects Levels\\CallableVariance\\B, Levels\\CallableVariance\\A given.", + "message": "Parameter #1 of callable callable(Levels\\CallableVariance\\B): void expects Levels\\CallableVariance\\B, Levels\\CallableVariance\\A given.", "line": 14, "ignorable": true }, @@ -39,4 +39,4 @@ "line": 85, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/casts-7.json b/tests/PHPStan/Levels/data/casts-7.json index d9b7a85b78..e2810b0e42 100644 --- a/tests/PHPStan/Levels/data/casts-7.json +++ b/tests/PHPStan/Levels/data/casts-7.json @@ -1,12 +1,12 @@ [ { - "message": "Cannot cast array|(callable(): mixed) to int.", + "message": "Cannot cast array|(callable(): mixed) to int.", "line": 20, "ignorable": true }, { - "message": "Cannot cast array|float|int to string.", + "message": "Cannot cast array|float|int to string.", "line": 21, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/clone-10-missing.json b/tests/PHPStan/Levels/data/clone-10-missing.json new file mode 100644 index 0000000000..40e1203120 --- /dev/null +++ b/tests/PHPStan/Levels/data/clone-10-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.php b/tests/PHPStan/Levels/data/clone.php index 6bd72cfe38..4697debb13 100644 --- a/tests/PHPStan/Levels/data/clone.php +++ b/tests/PHPStan/Levels/data/clone.php @@ -26,14 +26,14 @@ public function doFoo( $mixed ) { - clone $int; - clone $intOrString; - clone $foo; - clone $nullableFoo; - clone $fooOrInt; - clone $nullableInt; - clone $nullableUnion; - clone $mixed; + $result = clone $int; + $result = clone $intOrString; + $result = clone $foo; + $result = clone $nullableFoo; + $result = clone $fooOrInt; + $result = clone $nullableInt; + $result = clone $nullableUnion; + $result = clone $mixed; } } diff --git a/tests/PHPStan/Levels/data/coalesce-10.json b/tests/PHPStan/Levels/data/coalesce-10.json new file mode 100644 index 0000000000..e74887cb13 --- /dev/null +++ b/tests/PHPStan/Levels/data/coalesce-10.json @@ -0,0 +1,12 @@ +[ + { + "message": "Cannot access property $bar on mixed.", + "line": 6, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 11, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/coalesce-2.json b/tests/PHPStan/Levels/data/coalesce-2.json deleted file mode 100644 index 3f41943cf7..0000000000 --- a/tests/PHPStan/Levels/data/coalesce-2.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Access to an undefined property ReflectionClass::$nonexistent.", - "line": 11, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/coalesce-4.json b/tests/PHPStan/Levels/data/coalesce-4.json deleted file mode 100644 index 4d9dd368f4..0000000000 --- a/tests/PHPStan/Levels/data/coalesce-4.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Property ReflectionClass::$name (class-string) on left side of ?? is not nullable.", - "line": 10, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/comparison.php b/tests/PHPStan/Levels/data/comparison.php index d208e6051c..ee10f825be 100644 --- a/tests/PHPStan/Levels/data/comparison.php +++ b/tests/PHPStan/Levels/data/comparison.php @@ -8,29 +8,29 @@ class Foo private const FOO_CONST = 'foo'; /** - * @param \stdClass $object + * @param \stdClass $object * @param int $int - * @param float $float + * @param float $float * @param string $string * @param int|string $intOrString * @param int|\stdClass $intOrObject */ public function doFoo( - \stdClass $object, + \stdClass $object, int $int, float $float, string $string, $intOrString, - $intOrObject + $intOrObject ) { - $object == $int; - $object == $float; - $object == $string; - $object == $intOrString; - $object == $intOrObject; + $result = $object == $int; + $result = $object == $float; + $result = $object == $string; + $result = $object == $intOrString; + $result = $object == $intOrObject; - self::FOO_CONST === 'bar'; + $result = self::FOO_CONST === 'bar'; } public function doBar(\ffmpeg_movie $movie): void diff --git a/tests/PHPStan/Levels/data/constantAccesses-10-missing.json b/tests/PHPStan/Levels/data/constantAccesses-10-missing.json new file mode 100644 index 0000000000..0cc5a3f5d4 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses-10-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/constantAccesses-10.json b/tests/PHPStan/Levels/data/constantAccesses-10.json new file mode 100644 index 0000000000..cf84dbb4c6 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses-10.json @@ -0,0 +1,62 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 6, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 17, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 18, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 20, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 23, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 49, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 50, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 52, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 53, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 55, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 56, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 58, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83-10.json b/tests/PHPStan/Levels/data/constantAccesses83-10.json new file mode 100644 index 0000000000..7d5fcb38d3 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-10.json @@ -0,0 +1,27 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 15, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 16, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 18, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 19, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 20, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83-2.json b/tests/PHPStan/Levels/data/constantAccesses83-2.json new file mode 100644 index 0000000000..e36b08fc79 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "Class constant name in dynamic fetch can only be a string, int given.", + "line": 18, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83-7.json b/tests/PHPStan/Levels/data/constantAccesses83-7.json new file mode 100644 index 0000000000..3dd562d1b3 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-7.json @@ -0,0 +1,7 @@ +[ + { + "message": "Class constant name in dynamic fetch can only be a string, int|string given.", + "line": 19, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83-8.json b/tests/PHPStan/Levels/data/constantAccesses83-8.json new file mode 100644 index 0000000000..e9c93a1ee7 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-8.json @@ -0,0 +1,7 @@ +[ + { + "message": "Class constant name in dynamic fetch can only be a string, string|null given.", + "line": 20, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83.php b/tests/PHPStan/Levels/data/constantAccesses83.php new file mode 100644 index 0000000000..ceb192d073 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83.php @@ -0,0 +1,21 @@ += 8.3 + +namespace Levels\ConstantAccesses83; + +class Foo +{ + + public const FOO_CONSTANT = 'foo'; + +} + +function (Foo $foo, string $a, int $i, int|string $is, ?string $sn): void { + echo Foo::FOO_CONSTANT; + + echo Foo::{$a}; + echo $foo::{$a}; + + echo Foo::{$i}; + echo Foo::{$is}; + echo Foo::{$sn}; +}; diff --git a/tests/PHPStan/Levels/data/echo_-2.json b/tests/PHPStan/Levels/data/echo_-2.json index 1c35f8ef7c..330e326e8e 100644 --- a/tests/PHPStan/Levels/data/echo_-2.json +++ b/tests/PHPStan/Levels/data/echo_-2.json @@ -1,12 +1,12 @@ [ { - "message": "Parameter #1 (array) of echo cannot be converted to string.", + "message": "Parameter #1 (array) of echo cannot be converted to string.", "line": 21, "ignorable": true }, { - "message": "Parameter #2 (array|(callable(): mixed)) of echo cannot be converted to string.", + "message": "Parameter #2 (array|(callable(): mixed)) of echo cannot be converted to string.", "line": 21, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/echo_-7.json b/tests/PHPStan/Levels/data/echo_-7.json index 3f32efd774..275583f027 100644 --- a/tests/PHPStan/Levels/data/echo_-7.json +++ b/tests/PHPStan/Levels/data/echo_-7.json @@ -1,12 +1,12 @@ [ { - "message": "Parameter #3 (array|float|int) of echo cannot be converted to string.", + "message": "Parameter #3 (array|float|int) of echo cannot be converted to string.", "line": 21, "ignorable": true }, { - "message": "Parameter #4 (array|string) of echo cannot be converted to string.", + "message": "Parameter #4 (array|string) of echo cannot be converted to string.", "line": 21, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/encapsedString-2.json b/tests/PHPStan/Levels/data/encapsedString-2.json index 01ea01b977..8035809eb6 100644 --- a/tests/PHPStan/Levels/data/encapsedString-2.json +++ b/tests/PHPStan/Levels/data/encapsedString-2.json @@ -1,11 +1,11 @@ [ { - "message": "Part $array (array) of encapsed string cannot be cast to string.", + "message": "Part $array (array) of encapsed string cannot be cast to string.", "line": 21, "ignorable": true }, { - "message": "Part $arrayOrCallable (array|(callable(): mixed)) of encapsed string cannot be cast to string.", + "message": "Part $arrayOrCallable (array|(callable(): mixed)) of encapsed string cannot be cast to string.", "line": 22, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/encapsedString-7.json b/tests/PHPStan/Levels/data/encapsedString-7.json index 577b87c127..c468ffbc0a 100644 --- a/tests/PHPStan/Levels/data/encapsedString-7.json +++ b/tests/PHPStan/Levels/data/encapsedString-7.json @@ -1,11 +1,11 @@ [ { - "message": "Part $arrayOrFloatOrInt (array|float|int) of encapsed string cannot be cast to string.", + "message": "Part $arrayOrFloatOrInt (array|float|int) of encapsed string cannot be cast to string.", "line": 23, "ignorable": true }, { - "message": "Part $arrayOrString (array|string) of encapsed string cannot be cast to string.", + "message": "Part $arrayOrString (array|string) of encapsed string cannot be cast to string.", "line": 24, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/iterable-7.json b/tests/PHPStan/Levels/data/iterable-7.json index 74440d64d8..5c3c24924d 100644 --- a/tests/PHPStan/Levels/data/iterable-7.json +++ b/tests/PHPStan/Levels/data/iterable-7.json @@ -1,7 +1,7 @@ [ { - "message": "Argument of an invalid type array|false supplied for foreach, only iterables are supported.", + "message": "Argument of an invalid type array|false supplied for foreach, only iterables are supported.", "line": 35, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/iterable-8.json b/tests/PHPStan/Levels/data/iterable-8.json index 17e368b276..0a46f4b75e 100644 --- a/tests/PHPStan/Levels/data/iterable-8.json +++ b/tests/PHPStan/Levels/data/iterable-8.json @@ -1,7 +1,7 @@ [ { - "message": "Argument of an invalid type array|null supplied for foreach, only iterables are supported.", + "message": "Argument of an invalid type array|null supplied for foreach, only iterables are supported.", "line": 26, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/listType-3.json b/tests/PHPStan/Levels/data/listType-3.json new file mode 100644 index 0000000000..c25a0ea723 --- /dev/null +++ b/tests/PHPStan/Levels/data/listType-3.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property Levels\\ListType\\Foo::$list (list) does not accept array.", + "line": 24, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/listType-7.json b/tests/PHPStan/Levels/data/listType-7.json new file mode 100644 index 0000000000..620ac9317f --- /dev/null +++ b/tests/PHPStan/Levels/data/listType-7.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property Levels\\ListType\\Foo::$list (list) does not accept array.", + "line": 25, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/listType-8.json b/tests/PHPStan/Levels/data/listType-8.json new file mode 100644 index 0000000000..388a730546 --- /dev/null +++ b/tests/PHPStan/Levels/data/listType-8.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property Levels\\ListType\\Foo::$list (list) does not accept list.", + "line": 26, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/listType.php b/tests/PHPStan/Levels/data/listType.php new file mode 100644 index 0000000000..2123effeef --- /dev/null +++ b/tests/PHPStan/Levels/data/listType.php @@ -0,0 +1,30 @@ + */ + public $list; + + /** + * @param array $stringKeyArray + * @param array $intKeyArray + * @param list $stringOrNullList + * @param list $stringList + */ + public function doFoo( + array $stringKeyArray, + array $intKeyArray, + array $stringOrNullList, + array $stringList + ): void + { + $this->list = $stringKeyArray; + $this->list = $intKeyArray; + $this->list = $stringOrNullList; + $this->list = $stringList; + } + +} diff --git a/tests/PHPStan/Levels/data/methodCalls-10-missing.json b/tests/PHPStan/Levels/data/methodCalls-10-missing.json new file mode 100644 index 0000000000..47cdcab769 --- /dev/null +++ b/tests/PHPStan/Levels/data/methodCalls-10-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/missingTypes-6.json b/tests/PHPStan/Levels/data/missingTypes-6.json new file mode 100644 index 0000000000..66b17f1111 --- /dev/null +++ b/tests/PHPStan/Levels/data/missingTypes-6.json @@ -0,0 +1,7 @@ +[ + { + "message": "Class MissingTypesLevels\\Foo extends generic class MissingTypesLevels\\Generic but does not specify its types: T", + "line": 13, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/missingTypes.php b/tests/PHPStan/Levels/data/missingTypes.php new file mode 100644 index 0000000000..ed3d9bd3bf --- /dev/null +++ b/tests/PHPStan/Levels/data/missingTypes.php @@ -0,0 +1,16 @@ + of print cannot be converted to string.", "line": 21, "ignorable": true }, { - "message": "Parameter array|(callable(): mixed) of print cannot be converted to string.", + "message": "Parameter array|(callable(): mixed) of print cannot be converted to string.", "line": 22, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/print_-7.json b/tests/PHPStan/Levels/data/print_-7.json index 84c94b44c4..532f660fad 100644 --- a/tests/PHPStan/Levels/data/print_-7.json +++ b/tests/PHPStan/Levels/data/print_-7.json @@ -1,11 +1,11 @@ [ { - "message": "Parameter array|float|int of print cannot be converted to string.", + "message": "Parameter array|float|int of print cannot be converted to string.", "line": 23, "ignorable": true }, { - "message": "Parameter array|string of print cannot be converted to string.", + "message": "Parameter array|string of print cannot be converted to string.", "line": 24, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json new file mode 100644 index 0000000000..fd6f669c7c --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json @@ -0,0 +1,52 @@ +[ + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", + "line": 61, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", + "line": 166, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "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/propertyAccesses-10.json b/tests/PHPStan/Levels/data/propertyAccesses-10.json new file mode 100644 index 0000000000..9581e25ad9 --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-10.json @@ -0,0 +1,57 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 14, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 18, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 32, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 36, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 95, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 186, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 187, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 188, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 198, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 199, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 200, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-2.json b/tests/PHPStan/Levels/data/propertyAccesses-2.json index 95bf5c3c29..1d3376b781 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-2.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-2.json @@ -9,21 +9,41 @@ "line": 36, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 61, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Baz::$foo.", "line": 66, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 166, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Baz::$foo.", "line": 173, diff --git a/tests/PHPStan/Levels/data/propertyAccesses-6.json b/tests/PHPStan/Levels/data/propertyAccesses-6.json index edeb6d1b42..b62abeefd8 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-6.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-6.json @@ -19,11 +19,6 @@ "line": 74, "ignorable": true }, - { - "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::__set() has no return type specified.", - "line": 83, - "ignorable": true - }, { "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::doFoo() has no return type specified.", "line": 93, @@ -39,4 +34,4 @@ "line": 158, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json new file mode 100644 index 0000000000..7d9c064f4b --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json @@ -0,0 +1,22 @@ +[ + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-7.json b/tests/PHPStan/Levels/data/propertyAccesses-7.json index aa6291fdfe..b6df87becb 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-7.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-7.json @@ -38,5 +38,10 @@ "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", "line": 170, "ignorable": true + }, + { + "message": "Access to an undefined property object::$baz.", + "line": 200, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json index 1a8bc8b4b7..fd6f669c7c 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json @@ -1,14 +1,34 @@ [ + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 61, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 166, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", "line": 63, diff --git a/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json index 1a8bc8b4b7..fd6f669c7c 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json @@ -1,14 +1,34 @@ [ + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 58, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 61, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 162, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", "line": 166, "ignorable": true }, + { + "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", + "line": 170, + "ignorable": true + }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", "line": 63, diff --git a/tests/PHPStan/Levels/data/propertyAccesses-9.json b/tests/PHPStan/Levels/data/propertyAccesses-9.json new file mode 100644 index 0000000000..c7c6ae5a96 --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-9.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot access property $foo on mixed.", + "line": 197, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses.php b/tests/PHPStan/Levels/data/propertyAccesses.php index 72c9d4bcda..1c006ab6dc 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses.php +++ b/tests/PHPStan/Levels/data/propertyAccesses.php @@ -174,3 +174,31 @@ public function doBaz() } } + +class ObjectWithIsset +{ + + public function doFoo(): void + { + $test = new \stdClass; + + if (isset($test->foo)) { + echo $test->foo; + echo $test->bar; + echo $test->baz; + } + } + + /** + * @param mixed $test + */ + public function doBar($test): void + { + if (isset($test->foo) && isset($test->bar)) { + echo $test->foo; + echo $test->bar; + echo $test->baz; + } + } + +} diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-10.json b/tests/PHPStan/Levels/data/stringOffsetAccess-10.json new file mode 100644 index 0000000000..cc773c1e06 --- /dev/null +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-10.json @@ -0,0 +1,32 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 13, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 16, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 23, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 27, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 31, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 35, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-2.json b/tests/PHPStan/Levels/data/stringOffsetAccess-2.json new file mode 100644 index 0000000000..9188b4d36d --- /dev/null +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "PHPDoc tag @var with type int|object is not subtype of native type null.", + "line": 7, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-3.json b/tests/PHPStan/Levels/data/stringOffsetAccess-3.json index 9eb7139340..be9fa763e2 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-3.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-3.json @@ -8,5 +8,10 @@ "message": "Offset 12.34 does not exist on 'foo'.", "line": 16, "ignorable": true + }, + { + "message": "Invalid array key type stdClass.", + "line": 59, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-7.json b/tests/PHPStan/Levels/data/stringOffsetAccess-7.json index 1600d8541c..5471fbcf70 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-7.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-7.json @@ -1,12 +1,22 @@ [ { - "message": "Offset 'foo' does not exist on array|string.", + "message": "Offset 'foo' might not exist on array|string.", "line": 27, "ignorable": true }, { - "message": "Offset 12.34 does not exist on array|string.", + "message": "Offset 12.34 might not exist on array|string.", "line": 31, "ignorable": true + }, + { + "message": "Possibly invalid array key type int|object.", + "line": 35, + "ignorable": true + }, + { + "message": "Possibly invalid array key type int|object.", + "line": 55, + "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess.php b/tests/PHPStan/Levels/data/stringOffsetAccess.php index afed1e9694..452e38960a 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess.php +++ b/tests/PHPStan/Levels/data/stringOffsetAccess.php @@ -49,4 +49,12 @@ function () { /** @var mixed $mixed */ $mixed = null; echo $mixed[$maybeInt]; + + /** @var array{foo: 17, bar: 19}|array{baz: 21} $arrayUnion */ + $arrayUnion = []; + echo $arrayUnion[$maybeInt]; + + /** @var array{foo: 17, bar: 19}|array{baz: 21} $arrayUnion */ + $arrayUnion = []; + echo $arrayUnion[new \stdClass()]; }; diff --git a/tests/PHPStan/Levels/data/stubValidator-0.json b/tests/PHPStan/Levels/data/stubValidator-0.json index 0517d298af..ce13d6e997 100644 --- a/tests/PHPStan/Levels/data/stubValidator-0.json +++ b/tests/PHPStan/Levels/data/stubValidator-0.json @@ -20,12 +20,12 @@ "ignorable": false }, { - "message": "Method class@anonymous/stubValidator/stubs.php:27::doFoo() has no return type specified.", + "message": "Method ArrayIterator@anonymous/stubValidator/stubs.php:27::doFoo() has no return type specified.", "line": 30, "ignorable": false }, { - "message": "Parameter $foo of method class@anonymous/stubValidator/stubs.php:27::doFoo() has invalid type StubValidator\\Foooooooo.", + "message": "Parameter $foo of method ArrayIterator@anonymous/stubValidator/stubs.php:27::doFoo() has invalid type StubValidator\\Foooooooo.", "line": 30, "ignorable": false } diff --git a/tests/PHPStan/Levels/data/stubs-functions-4.json b/tests/PHPStan/Levels/data/stubs-functions-4.json new file mode 100644 index 0000000000..dd57bdf13f --- /dev/null +++ b/tests/PHPStan/Levels/data/stubs-functions-4.json @@ -0,0 +1,12 @@ +[ + { + "message": "Call to function StubsIntegrationTest\\foo() on a separate line has no effect.", + "line": 11, + "ignorable": true + }, + { + "message": "Call to function StubsIntegrationTest\\foo() on a separate line has no effect.", + "line": 13, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stubs-methods-5.json b/tests/PHPStan/Levels/data/stubs-methods-5.json index bb016dba1a..26e13c738d 100644 --- a/tests/PHPStan/Levels/data/stubs-methods-5.json +++ b/tests/PHPStan/Levels/data/stubs-methods-5.json @@ -48,5 +48,10 @@ "message": "Parameter #1 $j of method StubsIntegrationTest\\YetYetAnotherFoo::doFoo() expects int, string given.", "line": 226, "ignorable": true + }, + { + "message": "Parameter #1 $int of method StubsIntegrationTest\\ClassUsingStubbedTrait::doFoo() expects int, string given.", + "line": 243, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stubs-methods.php b/tests/PHPStan/Levels/data/stubs-methods.php index 7c993519c7..38d9b6d251 100644 --- a/tests/PHPStan/Levels/data/stubs-methods.php +++ b/tests/PHPStan/Levels/data/stubs-methods.php @@ -225,3 +225,20 @@ function (YetYetAnotherFoo $foo): void { $string = $foo->doFoo('test'); $foo->doFoo($string); }; + +trait StubbedTrait +{ + public function doFoo($int) + { + + } +} + +class ClassUsingStubbedTrait +{ + use StubbedTrait; +} + +function (ClassUsingStubbedTrait $foo): void { + $foo->doFoo('string'); +}; diff --git a/tests/PHPStan/Levels/data/unreachable-4-alwaysTrue.json b/tests/PHPStan/Levels/data/unreachable-4-alwaysTrue.json deleted file mode 100644 index 5f0119364d..0000000000 --- a/tests/PHPStan/Levels/data/unreachable-4-alwaysTrue.json +++ /dev/null @@ -1,102 +0,0 @@ -[ - { - "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", - "line": 11, - "ignorable": true - }, - { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 13, - "ignorable": true - }, - { - "message": "Instanceof between $this(Levels\\Unreachable\\Foo) and Levels\\Unreachable\\Foo will always evaluate to true.", - "line": 20, - "ignorable": true - }, - { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 22, - "ignorable": true - }, - { - "message": "Call to function is_string() with string will always evaluate to true.", - "line": 29, - "ignorable": true - }, - { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 31, - "ignorable": true - }, - { - "message": "If condition is always true.", - "line": 38, - "ignorable": true - }, - { - "message": "If condition is always true.", - "line": 47, - "ignorable": true - }, - { - "message": "Left side of && is always true.", - "line": 59, - "ignorable": true - }, - { - "message": "Right side of && is always true.", - "line": 59, - "ignorable": true - }, - { - "message": "Else branch is unreachable because ternary operator condition is always true.", - "line": 74, - "ignorable": true - }, - { - "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", - "line": 74, - "ignorable": true - }, - { - "message": "Else branch is unreachable because ternary operator condition is always true.", - "line": 79, - "ignorable": true - }, - { - "message": "Instanceof between $this(Levels\\Unreachable\\Bar) and Levels\\Unreachable\\Bar will always evaluate to true.", - "line": 79, - "ignorable": true - }, - { - "message": "Call to function is_string() with string will always evaluate to true.", - "line": 84, - "ignorable": true - }, - { - "message": "Else branch is unreachable because ternary operator condition is always true.", - "line": 84, - "ignorable": true - }, - { - "message": "Ternary operator condition is always true.", - "line": 89, - "ignorable": true - }, - { - "message": "Ternary operator condition is always true.", - "line": 94, - "ignorable": true - }, - { - "message": "Left side of && is always true.", - "line": 102, - "ignorable": true - }, - { - "message": "Right side of && is always true.", - "line": 102, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/unreachable-4.json b/tests/PHPStan/Levels/data/unreachable-4.json index f87bfc8cea..4e6216b466 100644 --- a/tests/PHPStan/Levels/data/unreachable-4.json +++ b/tests/PHPStan/Levels/data/unreachable-4.json @@ -1,17 +1,17 @@ [ { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 13, + "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", + "line": 11, "ignorable": true }, { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 22, + "message": "Instanceof between $this(Levels\\Unreachable\\Foo) and Levels\\Unreachable\\Foo will always evaluate to true.", + "line": 20, "ignorable": true }, { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 31, + "message": "Call to function is_string() with string will always evaluate to true.", + "line": 29, "ignorable": true }, { @@ -19,6 +19,11 @@ "line": 38, "ignorable": true }, + { + "message": "Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.", + "line": 38, + "ignorable": true + }, { "message": "If condition is always true.", "line": 47, @@ -35,20 +40,40 @@ "ignorable": true }, { - "message": "Else branch is unreachable because ternary operator condition is always true.", + "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", + "line": 74, + "ignorable": true + }, + { + "message": "Unused result of ternary operator.", "line": 74, "ignorable": true }, { - "message": "Else branch is unreachable because ternary operator condition is always true.", + "message": "Instanceof between $this(Levels\\Unreachable\\Bar) and Levels\\Unreachable\\Bar will always evaluate to true.", "line": 79, "ignorable": true }, { - "message": "Else branch is unreachable because ternary operator condition is always true.", + "message": "Unused result of ternary operator.", + "line": 79, + "ignorable": true + }, + { + "message": "Call to function is_string() with string will always evaluate to true.", + "line": 84, + "ignorable": true + }, + { + "message": "Unused result of ternary operator.", "line": 84, "ignorable": true }, + { + "message": "Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.", + "line": 89, + "ignorable": true + }, { "message": "Ternary operator condition is always true.", "line": 89, @@ -59,6 +84,11 @@ "line": 94, "ignorable": true }, + { + "message": "Unused result of ternary operator.", + "line": 94, + "ignorable": true + }, { "message": "Left side of && is always true.", "line": 102, @@ -68,5 +98,10 @@ "message": "Right side of && is always true.", "line": 102, "ignorable": true + }, + { + "message": "Unused result of ternary operator.", + "line": 102, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json b/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json deleted file mode 100644 index b285fc696d..0000000000 --- a/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "message": "Method Levels\\Unreachable\\Foo::doStrictComparison() has no return type specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return type specified.", - "line": 18, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return type specified.", - "line": 27, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return type specified.", - "line": 36, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return type specified.", - "line": 45, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return type specified.", - "line": 54, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return type specified.", - "line": 71, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return type specified.", - "line": 77, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return type specified.", - "line": 82, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return type specified.", - "line": 87, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return type specified.", - "line": 92, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return type specified.", - "line": 97, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/variables-10.json b/tests/PHPStan/Levels/data/variables-10.json new file mode 100644 index 0000000000..fd397067b9 --- /dev/null +++ b/tests/PHPStan/Levels/data/variables-10.json @@ -0,0 +1,7 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 7, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/dynamicConstantNames.neon b/tests/PHPStan/Levels/dynamicConstantNames.neon index 89f4491bc4..f52cabb14e 100644 --- a/tests/PHPStan/Levels/dynamicConstantNames.neon +++ b/tests/PHPStan/Levels/dynamicConstantNames.neon @@ -4,3 +4,4 @@ includes: parameters: dynamicConstantNames: - Levels\Comparison\Foo::FOO_CONST + phpVersion: 80300 diff --git a/tests/PHPStan/Levels/namedArguments.neon b/tests/PHPStan/Levels/namedArguments.neon new file mode 100644 index 0000000000..3bc9994f11 --- /dev/null +++ b/tests/PHPStan/Levels/namedArguments.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80000 diff --git a/tests/PHPStan/Levels/staticReflection.neon b/tests/PHPStan/Levels/staticReflection.neon deleted file mode 100644 index 8f93e584da..0000000000 --- a/tests/PHPStan/Levels/staticReflection.neon +++ /dev/null @@ -1,4 +0,0 @@ -parameters: - phpVersion: 80000 - featureToggles: - disableRuntimeReflectionProvider: true diff --git a/tests/PHPStan/Node/AttributeArgRule.php b/tests/PHPStan/Node/AttributeArgRule.php new file mode 100644 index 0000000000..084671311f --- /dev/null +++ b/tests/PHPStan/Node/AttributeArgRule.php @@ -0,0 +1,32 @@ + + */ +class AttributeArgRule implements Rule +{ + + public const ERROR_MESSAGE = 'Found Arg'; + + public function getNodeType(): string + { + return Node\Arg::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('tests.attributeArg') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Node/AttributeArgRuleTest.php b/tests/PHPStan/Node/AttributeArgRuleTest.php new file mode 100644 index 0000000000..fb4c1d99e9 --- /dev/null +++ b/tests/PHPStan/Node/AttributeArgRuleTest.php @@ -0,0 +1,60 @@ + + */ +class AttributeArgRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new AttributeArgRule(); + } + + public function dataRule(): iterable + { + yield [ + __DIR__ . '/data/attributes.php', + AttributeArgRule::ERROR_MESSAGE, + [8, 16, 20, 23, 26, 27, 34, 40], + ]; + } + + /** + * @param int[] $lines + * @dataProvider dataRule + */ + public function testRule(string $file, string $expectedError, array $lines): void + { + $errors = []; + foreach ($lines as $line) { + $errors[] = [$expectedError, $line]; + } + $this->analyse([$file], $errors); + } + + public function testEnumCaseAttribute(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-case-attribute.php'], [ + [ + AttributeArgRule::ERROR_MESSAGE, + 10, + ], + ]); + } + +} diff --git a/tests/PHPStan/Node/AttributeGroupRule.php b/tests/PHPStan/Node/AttributeGroupRule.php new file mode 100644 index 0000000000..5081ff14ee --- /dev/null +++ b/tests/PHPStan/Node/AttributeGroupRule.php @@ -0,0 +1,32 @@ + + */ +class AttributeGroupRule implements Rule +{ + + public const ERROR_MESSAGE = 'Found AttributeGroup'; + + public function getNodeType(): string + { + return Node\AttributeGroup::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('tests.attributeGroup') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Node/AttributeGroupRuleTest.php b/tests/PHPStan/Node/AttributeGroupRuleTest.php new file mode 100644 index 0000000000..d1e3044f44 --- /dev/null +++ b/tests/PHPStan/Node/AttributeGroupRuleTest.php @@ -0,0 +1,45 @@ + + */ +class AttributeGroupRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new AttributeGroupRule(); + } + + public function dataRule(): iterable + { + yield [ + __DIR__ . '/data/attributes.php', + AttributeGroupRule::ERROR_MESSAGE, + [8, 16, 20, 23, 26, 27, 34, 40], + ]; + } + + /** + * @param int[] $lines + * @dataProvider dataRule + */ + public function testRule(string $file, string $expectedError, array $lines): void + { + $errors = []; + foreach ($lines as $line) { + $errors[] = [$expectedError, $line]; + } + $this->analyse([$file], $errors); + } + +} diff --git a/tests/PHPStan/Node/AttributeRule.php b/tests/PHPStan/Node/AttributeRule.php new file mode 100644 index 0000000000..cf9afea21f --- /dev/null +++ b/tests/PHPStan/Node/AttributeRule.php @@ -0,0 +1,32 @@ + + */ +class AttributeRule implements Rule +{ + + public const ERROR_MESSAGE = 'Found Attribute'; + + public function getNodeType(): string + { + return Node\Attribute::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('tests.attribute') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Node/AttributeRuleTest.php b/tests/PHPStan/Node/AttributeRuleTest.php new file mode 100644 index 0000000000..3b189a210d --- /dev/null +++ b/tests/PHPStan/Node/AttributeRuleTest.php @@ -0,0 +1,45 @@ + + */ +class AttributeRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new AttributeRule(); + } + + public function dataRule(): iterable + { + yield [ + __DIR__ . '/data/attributes.php', + AttributeRule::ERROR_MESSAGE, + [8, 16, 20, 23, 26, 27, 34, 40], + ]; + } + + /** + * @param int[] $lines + * @dataProvider dataRule + */ + public function testRule(string $file, string $expectedError, array $lines): void + { + $errors = []; + foreach ($lines as $line) { + $errors[] = [$expectedError, $line]; + } + $this->analyse([$file], $errors); + } + +} diff --git a/tests/PHPStan/Node/FileNodeTest.php b/tests/PHPStan/Node/FileNodeTest.php index 852ec3b136..60a132a7f8 100644 --- a/tests/PHPStan/Node/FileNodeTest.php +++ b/tests/PHPStan/Node/FileNodeTest.php @@ -5,8 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\File\SimpleRelativePathHelper; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Testing\RuleTestCase; use function get_class; @@ -27,7 +27,7 @@ public function getNodeType(): string /** * @param FileNode $node - * @return RuleError[] + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -35,14 +35,16 @@ public function processNode(Node $node, Scope $scope): array $pathHelper = new SimpleRelativePathHelper(__DIR__ . DIRECTORY_SEPARATOR . 'data'); if (!isset($nodes[0])) { return [ - RuleErrorBuilder::message(sprintf('File %s is empty.', $pathHelper->getRelativePath($scope->getFile())))->line(1)->build(), + RuleErrorBuilder::message(sprintf('File %s is empty.', $pathHelper->getRelativePath($scope->getFile())))->line(1) + ->identifier('tests.fileNode') + ->build(), ]; } return [ RuleErrorBuilder::message( sprintf('First node in file %s is: %s', $pathHelper->getRelativePath($scope->getFile()), get_class($nodes[0])), - )->build(), + )->identifier('tests.fileNode')->build(), ]; } diff --git a/tests/PHPStan/Node/ParentStmtTypesRule.php b/tests/PHPStan/Node/ParentStmtTypesRule.php new file mode 100644 index 0000000000..d011615773 --- /dev/null +++ b/tests/PHPStan/Node/ParentStmtTypesRule.php @@ -0,0 +1,34 @@ + + */ +class ParentStmtTypesRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Echo_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message(sprintf( + 'Parents: %s', + implode(', ', array_reverse($node->getAttribute('parentStmtTypes'))), + ))->identifier('tests.parentStmtTypes')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Node/ParentStmtTypesRuleTest.php b/tests/PHPStan/Node/ParentStmtTypesRuleTest.php new file mode 100644 index 0000000000..320a26465f --- /dev/null +++ b/tests/PHPStan/Node/ParentStmtTypesRuleTest.php @@ -0,0 +1,29 @@ + + */ +class ParentStmtTypesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ParentStmtTypesRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/parent-stmt-types.php'], [ + [ + 'Parents: PhpParser\Node\Stmt\If_, PhpParser\Node\Stmt\Function_, PhpParser\Node\Stmt\Namespace_', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Node/TryCatchTypeRule.php b/tests/PHPStan/Node/TryCatchTypeRule.php new file mode 100644 index 0000000000..e71958eedf --- /dev/null +++ b/tests/PHPStan/Node/TryCatchTypeRule.php @@ -0,0 +1,41 @@ + + */ +class TryCatchTypeRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Echo_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $tryCatchTypes = $node->getAttribute('tryCatchTypes'); + $type = null; + if ($tryCatchTypes !== null) { + $type = TypeCombinator::union(...array_map(static fn (string $name) => new ObjectType($name), $tryCatchTypes)); + } + return [ + RuleErrorBuilder::message(sprintf( + 'Try catch type: %s', + $type !== null ? $type->describe(VerbosityLevel::precise()) : 'nothing', + ))->identifier('tests.tryCatchType')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Node/TryCatchTypeRuleTest.php b/tests/PHPStan/Node/TryCatchTypeRuleTest.php new file mode 100644 index 0000000000..e1e649fc1d --- /dev/null +++ b/tests/PHPStan/Node/TryCatchTypeRuleTest.php @@ -0,0 +1,45 @@ + + */ +class TryCatchTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new TryCatchTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/try-catch-type.php'], [ + [ + 'Try catch type: nothing', + 10, + ], + [ + 'Try catch type: LogicException|RuntimeException', + 12, + ], + [ + 'Try catch type: nothing', + 14, + ], + [ + 'Try catch type: LogicException|RuntimeException|TypeError', + 17, + ], + [ + 'Try catch type: LogicException|RuntimeException', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Node/data/attributes.php b/tests/PHPStan/Node/data/attributes.php new file mode 100644 index 0000000000..0103b343c4 --- /dev/null +++ b/tests/PHPStan/Node/data/attributes.php @@ -0,0 +1,41 @@ += 8.0 + +namespace NodeCallbackCalled; + +use ClassAttributes\AttributeWithConstructor; +use FunctionAttributes\Baz; + +#[\Attribute(flags: \Attribute::TARGET_ALL)] +class UniversalAttribute +{ + public function __construct(int $foo) + { + } +} + +#[UniversalAttribute(1)] +class MyClass +{ + + #[UniversalAttribute(2)] + private const MY_CONST = 'const'; + + #[UniversalAttribute(3)] + private string $myProperty; + + #[UniversalAttribute(4)] + public function myMethod(#[UniversalAttribute(5)] string $arg): void + { + + } + +} + +#[UniversalAttribute(6)] +interface MyInterface {} + +#[UniversalAttribute(7)] +trait MyTrait {} + +#[UniversalAttribute(8)] +function myFunction() {} diff --git a/tests/PHPStan/Node/data/enum-case-attribute.php b/tests/PHPStan/Node/data/enum-case-attribute.php new file mode 100644 index 0000000000..93e87ef1f6 --- /dev/null +++ b/tests/PHPStan/Node/data/enum-case-attribute.php @@ -0,0 +1,13 @@ += 8.1 + +namespace EnumCaseAttributeCheck; + +use NodeCallbackCalled\UniversalAttribute; + +enum Foo +{ + + #[UniversalAttribute(1)] + case TEST; + +} diff --git a/tests/PHPStan/Rules/data/node-connecting.php b/tests/PHPStan/Node/data/parent-stmt-types.php similarity index 100% rename from tests/PHPStan/Rules/data/node-connecting.php rename to tests/PHPStan/Node/data/parent-stmt-types.php diff --git a/tests/PHPStan/Node/data/try-catch-type.php b/tests/PHPStan/Node/data/try-catch-type.php new file mode 100644 index 0000000000..150cdc95f7 --- /dev/null +++ b/tests/PHPStan/Node/data/try-catch-type.php @@ -0,0 +1,29 @@ +normalizePath(__DIR__ . '/data/trait-definition.php'); $this->assertJsonStringEqualsJsonString(Json::encode([ @@ -65,6 +74,7 @@ public function testRun(string $command): void 'message' => 'Method ParallelAnalyserIntegrationTest\\Bar::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, + 'identifier' => 'missingType.return', ], ], ], @@ -75,16 +85,21 @@ public function testRun(string $command): void 'message' => 'Method ParallelAnalyserIntegrationTest\\Foo::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, + 'identifier' => 'missingType.return', ], [ 'message' => 'Access to an undefined property ParallelAnalyserIntegrationTest\\Foo::$test.', 'line' => 10, 'ignorable' => true, + 'identifier' => 'property.notFound', + 'tip' => 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', ], [ 'message' => 'Access to an undefined property ParallelAnalyserIntegrationTest\\Foo::$test.', 'line' => 15, 'ignorable' => true, + 'identifier' => 'property.notFound', + 'tip' => 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', ], ], ], diff --git a/tests/PHPStan/Parallel/SchedulerTest.php b/tests/PHPStan/Parallel/SchedulerTest.php index 0d63fc1380..fb1fd626cf 100644 --- a/tests/PHPStan/Parallel/SchedulerTest.php +++ b/tests/PHPStan/Parallel/SchedulerTest.php @@ -73,6 +73,8 @@ public function dataSchedule(): array /** * @dataProvider dataSchedule * @param positive-int $jobSize + * @param positive-int $maximumNumberOfProcesses + * @param positive-int $minimumNumberOfJobsPerProcess * @param 0|positive-int $numberOfFiles * @param array $expectedJobSizes */ diff --git a/tests/PHPStan/Parallel/parallel-analyser.neon b/tests/PHPStan/Parallel/parallel-analyser.neon index f942a62afa..a2f7f00980 100644 --- a/tests/PHPStan/Parallel/parallel-analyser.neon +++ b/tests/PHPStan/Parallel/parallel-analyser.neon @@ -1,3 +1,4 @@ parameters: parallel: jobSize: 1 + tmpDir: %env.PHPSTAN_TMP_DIR% diff --git a/tests/PHPStan/Parser/CachedParserTest.php b/tests/PHPStan/Parser/CachedParserTest.php index 7d8323b458..3b97a1b99a 100644 --- a/tests/PHPStan/Parser/CachedParserTest.php +++ b/tests/PHPStan/Parser/CachedParserTest.php @@ -2,34 +2,33 @@ namespace PHPStan\Parser; +use Generator; use PhpParser\Node; - use PhpParser\Node\Stmt\Namespace_; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\MockObject\MockObject; class CachedParserTest extends PHPStanTestCase { /** * @dataProvider dataParseFileClearCache - * @param int $cachedNodesByStringCountMax - * @param int $cachedNodesByStringCountExpected */ public function testParseFileClearCache( int $cachedNodesByStringCountMax, - int $cachedNodesByStringCountExpected + int $cachedNodesByStringCountExpected, ): void { $parser = new CachedParser( $this->getParserMock(), - $cachedNodesByStringCountMax + $cachedNodesByStringCountMax, ); $this->assertEquals( $cachedNodesByStringCountMax, - $parser->getCachedNodesByStringCountMax() + $parser->getCachedNodesByStringCountMax(), ); // Add strings to cache @@ -39,16 +38,16 @@ public function testParseFileClearCache( $this->assertEquals( $cachedNodesByStringCountExpected, - $parser->getCachedNodesByStringCount() + $parser->getCachedNodesByStringCount(), ); $this->assertCount( $cachedNodesByStringCountExpected, - $parser->getCachedNodesByString() + $parser->getCachedNodesByString(), ); } - public function dataParseFileClearCache(): \Generator + public function dataParseFileClearCache(): Generator { yield 'even' => [ 'cachedNodesByStringCountMax' => 50, @@ -61,10 +60,7 @@ public function dataParseFileClearCache(): \Generator ]; } - /** - * @return Parser&\PHPUnit\Framework\MockObject\MockObject - */ - private function getParserMock(): Parser + private function getParserMock(): Parser&MockObject { $mock = $this->createMock(Parser::class); @@ -74,12 +70,9 @@ private function getParserMock(): Parser return $mock; } - /** - * @return \PhpParser\Node&\PHPUnit\Framework\MockObject\MockObject - */ - private function getPhpParserNodeMock(): \PhpParser\Node + private function getPhpParserNodeMock(): Node&MockObject { - return $this->createMock(\PhpParser\Node::class); + return $this->createMock(Node::class); } public function testParseTheSameFileWithDifferentMethod(): void @@ -89,7 +82,7 @@ public function testParseTheSameFileWithDifferentMethod(): void $fileHelper, self::getContainer()->getService('currentPhpVersionRichParser'), self::getContainer()->getService('currentPhpVersionSimpleDirectParser'), - self::getContainer()->getService('php8Parser') + self::getContainer()->getService('php8Parser'), ); $parser = new CachedParser($pathRoutingParser, 500); $path = $fileHelper->normalizePath(__DIR__ . '/data/test.php'); @@ -97,15 +90,34 @@ public function testParseTheSameFileWithDifferentMethod(): void $contents = FileReader::read($path); $stmts = $parser->parseString($contents); $this->assertInstanceOf(Namespace_::class, $stmts[0]); - $this->assertNull($stmts[0]->stmts[0]->getAttribute('parent')); + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[0]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[0]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[0]->expr->expr); + $this->assertNull($stmts[0]->stmts[0]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); $stmts = $parser->parseFile($path); $this->assertInstanceOf(Namespace_::class, $stmts[0]); - $this->assertInstanceOf(Namespace_::class, $stmts[0]->stmts[0]->getAttribute('parent')); + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[0]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[0]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[0]->expr->expr); + $this->assertSame(1, $stmts[0]->stmts[0]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); + + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[1]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[1]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[1]->expr->expr); + $this->assertSame(2, $stmts[0]->stmts[1]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); $stmts = $parser->parseString($contents); $this->assertInstanceOf(Namespace_::class, $stmts[0]); - $this->assertInstanceOf(Namespace_::class, $stmts[0]->stmts[0]->getAttribute('parent')); + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[0]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[0]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[0]->expr->expr); + $this->assertSame(1, $stmts[0]->stmts[0]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); + + $this->assertInstanceOf(Node\Stmt\Expression::class, $stmts[0]->stmts[1]); + $this->assertInstanceOf(Node\Expr\Assign::class, $stmts[0]->stmts[1]->expr); + $this->assertInstanceOf(Node\Expr\New_::class, $stmts[0]->stmts[1]->expr->expr); + $this->assertSame(2, $stmts[0]->stmts[1]->expr->expr->class->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX)); } } diff --git a/tests/PHPStan/Parser/CleaningParserTest.php b/tests/PHPStan/Parser/CleaningParserTest.php index 7576eb828e..69c0e8af10 100644 --- a/tests/PHPStan/Parser/CleaningParserTest.php +++ b/tests/PHPStan/Parser/CleaningParserTest.php @@ -4,9 +4,9 @@ use PhpParser\Lexer\Emulative; use PhpParser\NodeVisitor\NameResolver; -use PhpParser\Parser\Php7; -use PhpParser\PrettyPrinter\Standard; +use PhpParser\Parser\Php8; use PHPStan\File\FileReader; +use PHPStan\Node\Printer\Printer; use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; use const PHP_VERSION_ID; @@ -52,6 +52,11 @@ public function dataParse(): iterable __DIR__ . '/data/cleaning-php-version-after-74.php', 70400, ], + [ + __DIR__ . '/data/cleaning-property-hooks-before.php', + __DIR__ . '/data/cleaning-property-hooks-after.php', + 80400, + ], ]; } @@ -66,12 +71,14 @@ public function testParse( { $parser = new CleaningParser( new SimpleParser( - new Php7(new Emulative()), + new Php8(new Emulative()), new NameResolver(), + new VariadicMethodsVisitor(), + new VariadicFunctionsVisitor(), ), new PhpVersion($phpVersionId), ); - $printer = new Standard(); + $printer = new Printer(); $ast = $parser->parseFile($beforeFile); $this->assertSame(FileReader::read($afterFile), "prettyPrint($ast) . "\n"); } diff --git a/tests/PHPStan/Parser/ParserTest.php b/tests/PHPStan/Parser/ParserTest.php new file mode 100644 index 0000000000..94fe9a406b --- /dev/null +++ b/tests/PHPStan/Parser/ParserTest.php @@ -0,0 +1,99 @@ + true, + ], + ]; + + yield [ + __DIR__ . '/data/variadic-methods.php', + VariadicMethodsVisitor::ATTRIBUTE_NAME, + [ + 'VariadicMethod\X' => [ + 'implicit_variadic_fn1' => true, + ], + 'VariadicMethod\Z' => [ + 'implicit_variadic_fnZ' => true, + ], + 'class@anonymous:20:30' => [ + 'implicit_variadic_subZ' => true, + ], + 'class@anonymous:42:52' => [ + 'implicit_variadic_fn' => true, + ], + 'class@anonymous:54:58' => [ + 'implicit_variadic_fn' => true, + ], + 'class@anonymous:61:68' => [ + 'implicit_variadic_fn' => true, + ], + ], + ]; + + yield [ + __DIR__ . '/data/variadic-methods-in-enum.php', + VariadicMethodsVisitor::ATTRIBUTE_NAME, + [ + 'VariadicMethodEnum\X' => [ + 'implicit_variadic_fn1' => true, + ], + ], + ]; + } + + /** + * @dataProvider dataVariadicCallLikes + * @param array|array> $expectedVariadics + * @throws ParserErrorsException + */ + public function testSimpleParserVariadicCallLikes(string $file, string $attributeName, array $expectedVariadics): void + { + /** @var SimpleParser $parser */ + $parser = self::getContainer()->getService('currentPhpVersionSimpleParser'); + $ast = $parser->parseFile($file); + $variadics = $ast[0]->getAttribute($attributeName); + $this->assertIsArray($variadics); + $this->assertCount(count($expectedVariadics), $variadics); + foreach ($expectedVariadics as $key => $expectedVariadic) { + $this->assertArrayHasKey($key, $variadics); + $this->assertSame($expectedVariadic, $variadics[$key]); + } + } + + /** + * @dataProvider dataVariadicCallLikes + * @param array|array> $expectedVariadics + * @throws ParserErrorsException + */ + public function testRichParserVariadicCallLikes(string $file, string $attributeName, array $expectedVariadics): void + { + /** @var RichParser $parser */ + $parser = self::getContainer()->getService('currentPhpVersionRichParser'); + $ast = $parser->parseFile($file); + $variadics = $ast[0]->getAttribute($attributeName); + $this->assertIsArray($variadics); + $this->assertCount(count($expectedVariadics), $variadics); + foreach ($expectedVariadics as $key => $expectedVariadic) { + $this->assertArrayHasKey($key, $variadics); + $this->assertSame($expectedVariadic, $variadics[$key]); + } + } + +} diff --git a/tests/PHPStan/Parser/RichParserTest.php b/tests/PHPStan/Parser/RichParserTest.php new file mode 100644 index 0000000000..0bc0b3b4c1 --- /dev/null +++ b/tests/PHPStan/Parser/RichParserTest.php @@ -0,0 +1,546 @@ + null, + ], + ]; + + yield [ + ' null, + ], + ]; + + yield [ + ' null, + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['return.ref'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test', 'test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['identifier', 'identifier2'], + ], + ]; + + yield [ + ' ['identifier', 'identifier2', 'identifier3'], + ], + ]; + + yield [ + ' ['identifier'], + ], + ]; + + yield [ + ' ['identifier'], + ], + ]; + + yield [ + ' ['identifier'], + ], + ]; + + yield [ + ' ['identifier'], + ], + ]; + + yield [ + ' ['identifier'], + ], + ]; + + yield [ + 'myProperty = $b;' . PHP_EOL . + ' }' . PHP_EOL . + '}', + [ + 10 => ['variable.undefined'], + ], + ]; + + yield [ + 'myProperty = $b;' . PHP_EOL . + ' }' . PHP_EOL . + '}', + [ + 13 => ['variable.undefined'], + ], + ]; + } + + /** + * @dataProvider dataLinesToIgnore + * @param array|null> $expectedLines + */ + public function testLinesToIgnore(string $code, array $expectedLines): void + { + /** @var RichParser $parser */ + $parser = self::getContainer()->getService('currentPhpVersionRichParser'); + $ast = $parser->parseString($code); + $lines = $ast[0]->getAttribute('linesToIgnore'); + $this->assertNull($ast[0]->getAttribute('linesToIgnoreParseErrors')); + $this->assertSame($expectedLines, $lines); + } + + public function dataLinesToIgnoreParseErrors(): iterable + { + yield [ + ' ['Unexpected comma (,) after comma (,), expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after comma (,), expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after @phpstan-ignore, expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after comma (,), expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after comma (,), expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected comma (,) after @phpstan-ignore, expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected T_CLOSE_PARENTHESIS after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS'], + ], + ]; + + yield [ + ' ['Unexpected end, unclosed opening parenthesis'], + ], + ]; + + yield [ + ' ['Unexpected T_OPEN_PARENTHESIS after @phpstan-ignore, expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after @phpstan-ignore, expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected comma (,) after @phpstan-ignore, expected identifier'], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER 'čumim' after @phpstan-ignore, expected identifier"], + ], + ]; + + yield [ + ' ['Unexpected end, unclosed opening parenthesis'], + ], + ]; + + yield [ + ' ['Unexpected identifier after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS'], + ], + ]; + + yield [ + ' ['Unexpected T_CLOSE_PARENTHESIS after T_CLOSE_PARENTHESIS, expected comma (,) or end'], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER 'čoun' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER 'čoun' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER 'čičí' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER '--' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER '[comment]' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ['Unexpected T_OPEN_PARENTHESIS after T_CLOSE_PARENTHESIS, expected comma (,) or end'], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER '://example.com' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ['Unexpected end after comma (,), expected identifier'], + ], + ]; + } + + /** + * @dataProvider dataLinesToIgnoreParseErrors + * @param array> $expectedErrors + */ + public function testLinesToIgnoreParseErrors(string $code, array $expectedErrors): void + { + /** @var RichParser $parser */ + $parser = self::getContainer()->getService('currentPhpVersionRichParser'); + $ast = $parser->parseString($code); + $errors = $ast[0]->getAttribute('linesToIgnoreParseErrors'); + $this->assertIsArray($errors); + $this->assertSame($expectedErrors, $errors); + + $lines = $ast[0]->getAttribute('linesToIgnore'); + $this->assertIsArray($lines); + $this->assertCount(0, $lines); + } + +} diff --git a/tests/PHPStan/Parser/data/cleaning-1-after.php b/tests/PHPStan/Parser/data/cleaning-1-after.php index e13a195e7d..1d22a6ac92 100644 --- a/tests/PHPStan/Parser/data/cleaning-1-after.php +++ b/tests/PHPStan/Parser/data/cleaning-1-after.php @@ -39,3 +39,13 @@ public function doFoo() \func_get_args(); } } +class ContainsClosure +{ + public function doFoo() + { + static function () { + yield; + }; + yield; + } +} diff --git a/tests/PHPStan/Parser/data/cleaning-1-before.php b/tests/PHPStan/Parser/data/cleaning-1-before.php index 468db05176..ae93a81b77 100644 --- a/tests/PHPStan/Parser/data/cleaning-1-before.php +++ b/tests/PHPStan/Parser/data/cleaning-1-before.php @@ -67,3 +67,19 @@ public function doFoo() } } } + +class ContainsClosure +{ + + public function doFoo() + { + return static function () { + if (doFoo()) { + echo 'foo'; + } + + yield; + }; + } + +} diff --git a/tests/PHPStan/Parser/data/cleaning-property-hooks-after.php b/tests/PHPStan/Parser/data/cleaning-property-hooks-after.php new file mode 100644 index 0000000000..105bcf5d76 --- /dev/null +++ b/tests/PHPStan/Parser/data/cleaning-property-hooks-after.php @@ -0,0 +1,21 @@ +i; + } + } +} +class FooParam +{ + public function __construct(public int $i { + get { + $this->i; + } + }) + { + } +} diff --git a/tests/PHPStan/Parser/data/cleaning-property-hooks-before.php b/tests/PHPStan/Parser/data/cleaning-property-hooks-before.php new file mode 100644 index 0000000000..7b73ef3d09 --- /dev/null +++ b/tests/PHPStan/Parser/data/cleaning-property-hooks-before.php @@ -0,0 +1,42 @@ +j; + + // backed property, leave this here + return $this->i; + } + } + +} + +class FooParam +{ + + public function __construct( + public int $i { + get { + echo 'irrelevant'; + + // other property, clean up + echo $this->j; + + // backed property, leave this here + return $this->i; + } + } + ) + { + + } + +} diff --git a/tests/PHPStan/Parser/data/test.php b/tests/PHPStan/Parser/data/test.php index a6bee51214..b7ee628d37 100644 --- a/tests/PHPStan/Parser/data/test.php +++ b/tests/PHPStan/Parser/data/test.php @@ -2,7 +2,4 @@ namespace CachedParserBug; -class Foo -{ - -} +$a = new class () {}; $b = new class () {}; diff --git a/tests/PHPStan/Parser/data/variadic-functions.php b/tests/PHPStan/Parser/data/variadic-functions.php new file mode 100644 index 0000000000..d1a572e1e0 --- /dev/null +++ b/tests/PHPStan/Parser/data/variadic-functions.php @@ -0,0 +1,31 @@ += 8.1 + +namespace VariadicMethodEnum; + +enum X { + + function non_variadic_fn1($v) { + } + + function variadic_fn1(...$v) { + } + + function implicit_variadic_fn1() { + $args = func_get_args(); + } +} diff --git a/tests/PHPStan/Parser/data/variadic-methods.php b/tests/PHPStan/Parser/data/variadic-methods.php new file mode 100644 index 0000000000..da6135b967 --- /dev/null +++ b/tests/PHPStan/Parser/data/variadic-methods.php @@ -0,0 +1,68 @@ +assertSame( + $expected->describe(), + $phpVersions->producesWarningForFinalPrivateMethods()->describe(), + ); + } + + public function dataProducesWarningForFinalPrivateMethods(): iterable + { + yield [ + TrinaryLogic::createNo(), + new ConstantIntegerType(70400), + ]; + + yield [ + TrinaryLogic::createYes(), + new ConstantIntegerType(80000), + ]; + + yield [ + TrinaryLogic::createYes(), + new ConstantIntegerType(80100), + ]; + + yield [ + TrinaryLogic::createYes(), + IntegerRangeType::fromInterval(80000, null), + ]; + + yield [ + TrinaryLogic::createMaybe(), + IntegerRangeType::fromInterval(null, 80000), + ]; + + yield [ + TrinaryLogic::createNo(), + IntegerRangeType::fromInterval(70200, 70400), + ]; + + yield [ + TrinaryLogic::createMaybe(), + new UnionType([ + IntegerRangeType::fromInterval(70200, 70400), + IntegerRangeType::fromInterval(80200, 80400), + ]), + ]; + } + +} diff --git a/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php new file mode 100644 index 0000000000..03218eff04 --- /dev/null +++ b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php @@ -0,0 +1,53 @@ +currentWorkingDirectory = $this->getContainer()->getParameter('currentWorkingDirectory'); + } + + public function testGetStubFiles(): void + { + $thirdPartyStubFile = sprintf('%s/vendor/thirdpartyStub.stub', $this->currentWorkingDirectory); + $defaultStubFilesProvider = $this->createDefaultStubFilesProvider(['/projectStub.stub', $thirdPartyStubFile]); + $stubFiles = $defaultStubFilesProvider->getStubFiles(); + $this->assertContains('/projectStub.stub', $stubFiles); + $this->assertContains($thirdPartyStubFile, $stubFiles); + } + + public function testGetProjectStubFiles(): void + { + $thirdPartyStubFile = sprintf('%s/vendor/thirdpartyStub.stub', $this->currentWorkingDirectory); + $defaultStubFilesProvider = $this->createDefaultStubFilesProvider(['/projectStub.stub', $thirdPartyStubFile]); + $projectStubFiles = $defaultStubFilesProvider->getProjectStubFiles(); + $this->assertContains('/projectStub.stub', $projectStubFiles); + $this->assertNotContains($thirdPartyStubFile, $projectStubFiles); + } + + public function testGetProjectStubFilesWhenPathContainsWindowsSeparator(): void + { + $thirdPartyStubFile = sprintf('%s\\vendor\\thirdpartyStub.stub', $this->currentWorkingDirectory); + $defaultStubFilesProvider = $this->createDefaultStubFilesProvider(['/projectStub.stub', $thirdPartyStubFile]); + $projectStubFiles = $defaultStubFilesProvider->getProjectStubFiles(); + $this->assertContains('/projectStub.stub', $projectStubFiles); + $this->assertNotContains($thirdPartyStubFile, $projectStubFiles); + } + + /** + * @param string[] $stubFiles + */ + private function createDefaultStubFilesProvider(array $stubFiles): DefaultStubFilesProvider + { + return new DefaultStubFilesProvider($this->getContainer(), $stubFiles, [$this->currentWorkingDirectory]); + } + +} diff --git a/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php b/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php deleted file mode 100644 index 39f6a91833..0000000000 --- a/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php +++ /dev/null @@ -1,24 +0,0 @@ -messages; - } - - public function log(string $message): void - { - $this->messages[] = $message; - } - -} diff --git a/tests/PHPStan/Process/Runnable/RunnableQueueTest.php b/tests/PHPStan/Process/Runnable/RunnableQueueTest.php deleted file mode 100644 index 43a94f661c..0000000000 --- a/tests/PHPStan/Process/Runnable/RunnableQueueTest.php +++ /dev/null @@ -1,165 +0,0 @@ -queue($one, 1); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(1, $queue->getRunningSize()); - $one->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - $this->assertSame([ - 'Queue not full - looking at first item in the queue', - 'Removing top item from queue - new size is 1', - 'Running process 1', - 'Process 1 finished successfully', - 'Queue empty', - ], $logger->getMessages()); - } - - public function testComplexScenario(): void - { - $logger = new RunnableQueueLoggerStub(); - $queue = new RunnableQueue($logger, 8); - - $one = new RunnableStub('1'); - $two = new RunnableStub('2'); - $three = new RunnableStub('3'); - $four = new RunnableStub('4'); - $queue->queue($one, 4); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(4, $queue->getRunningSize()); - - $queue->queue($two, 2); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(6, $queue->getRunningSize()); - $queue->queue($three, 3); - $this->assertSame(3, $queue->getQueueSize()); - $this->assertSame(6, $queue->getRunningSize()); - $queue->queue($four, 4); - $this->assertSame(7, $queue->getQueueSize()); - $this->assertSame(6, $queue->getRunningSize()); - - $one->finish(); - $this->assertSame(4, $queue->getQueueSize()); - $this->assertSame(5, $queue->getRunningSize()); - - $two->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(7, $queue->getRunningSize()); - - $three->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(4, $queue->getRunningSize()); - - $four->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - - $this->assertSame([ - 0 => 'Queue not full - looking at first item in the queue', - 1 => 'Removing top item from queue - new size is 4', - 2 => 'Running process 1', - 3 => 'Queue not full - looking at first item in the queue', - 4 => 'Removing top item from queue - new size is 6', - 5 => 'Running process 2', - 6 => 'Queue not full - looking at first item in the queue', - 7 => 'Canot remote first item from the queue - it has size 3, current queue size is 6, new size would be 9', - 8 => 'Queue not full - looking at first item in the queue', - 9 => 'Canot remote first item from the queue - it has size 3, current queue size is 6, new size would be 9', - 10 => 'Process 1 finished successfully', - 11 => 'Queue not full - looking at first item in the queue', - 12 => 'Removing top item from queue - new size is 5', - 13 => 'Running process 3', - 14 => 'Process 2 finished successfully', - 15 => 'Queue not full - looking at first item in the queue', - 16 => 'Removing top item from queue - new size is 7', - 17 => 'Running process 4', - 18 => 'Process 3 finished successfully', - 19 => 'Queue empty', - 20 => 'Process 4 finished successfully', - 21 => 'Queue empty', - ], $logger->getMessages()); - } - - public function testCancel(): void - { - $logger = new RunnableQueueLoggerStub(); - $queue = new RunnableQueue($logger, 8); - $one = new RunnableStub('1'); - $promise = $queue->queue($one, 4); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(4, $queue->getRunningSize()); - - $promise->then(static function () use ($logger): void { - $logger->log('Should not happen'); - }, static function (Exception $e) use ($logger): void { - $logger->log(sprintf('Else callback in test called: %s', $e->getMessage())); - }); - $promise->cancel(); - - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - - $this->assertSame([ - 0 => 'Queue not full - looking at first item in the queue', - 1 => 'Removing top item from queue - new size is 4', - 2 => 'Running process 1', - 3 => 'Process 1 finished unsuccessfully: Runnable 1 canceled', - 4 => 'Else callback in test called: Runnable 1 canceled', - 5 => 'Queue empty', - ], $logger->getMessages()); - } - - public function testCancelAll(): void - { - $logger = new RunnableQueueLoggerStub(); - $queue = new RunnableQueue($logger, 6); - $one = new RunnableStub('1'); - $two = new RunnableStub('2'); - $three = new RunnableStub('3'); - $queue->queue($one, 3); - $queue->queue($two, 2); - $queue->queue($three, 3); - - $this->assertSame(3, $queue->getQueueSize()); - $this->assertSame(5, $queue->getRunningSize()); - - $queue->cancelAll(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - - $this->assertSame([ - 0 => 'Queue not full - looking at first item in the queue', - 1 => 'Removing top item from queue - new size is 3', - 2 => 'Running process 1', - 3 => 'Queue not full - looking at first item in the queue', - 4 => 'Removing top item from queue - new size is 5', - 5 => 'Running process 2', - 6 => 'Queue not full - looking at first item in the queue', - 7 => 'Canot remote first item from the queue - it has size 3, current queue size is 5, new size would be 8', - 8 => 'Process 1 finished unsuccessfully: Runnable 1 canceled', - 9 => 'Queue not full - looking at first item in the queue', - 10 => 'Removing top item from queue - new size is 5', - 11 => 'Running process 3', - 12 => 'Process 3 finished unsuccessfully: Runnable 3 canceled', - 13 => 'Queue empty', - 14 => 'Process 2 finished unsuccessfully: Runnable 2 canceled', - 15 => 'Queue empty', - ], $logger->getMessages()); - } - -} diff --git a/tests/PHPStan/Process/Runnable/RunnableStub.php b/tests/PHPStan/Process/Runnable/RunnableStub.php deleted file mode 100644 index 08a3701c90..0000000000 --- a/tests/PHPStan/Process/Runnable/RunnableStub.php +++ /dev/null @@ -1,40 +0,0 @@ -deferred = new Deferred(); - } - - public function getName(): string - { - return $this->name; - } - - public function finish(): void - { - $this->deferred->resolve(); - } - - public function run(): CancellablePromiseInterface - { - /** @var CancellablePromiseInterface */ - return $this->deferred->promise(); - } - - public function cancel(): void - { - $this->deferred->reject(new RunnableCanceledException(sprintf('Runnable %s canceled', $this->getName()))); - } - -} diff --git a/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php new file mode 100644 index 0000000000..58890ef52e --- /dev/null +++ b/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/allowed-sub-types.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/data/allowed-sub-types.neon', + ]; + } + +} diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php index 6027b270ac..36b4402d9e 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php @@ -8,7 +8,6 @@ use AnnotationsMethods\Foo; use AnnotationsMethods\FooInterface; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Testing\PHPStanTestCase; @@ -459,7 +458,7 @@ public function dataMethods(): array ], 'conflictingMethod' => [ 'class' => Bar::class, - 'returnType' => Bar::class, + 'returnType' => Foo::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], @@ -974,7 +973,7 @@ public function testMethods(string $className, array $methods): void $this->assertTrue($class->hasMethod($methodName), sprintf('Method %s() not found in class %s.', $methodName, $className)); $method = $class->getMethod($methodName, $scope); - $selectedParametersAcceptor = ParametersAcceptorSelector::selectSingle($method->getVariants()); + $selectedParametersAcceptor = $method->getOnlyVariant(); $this->assertSame( $expectedMethodData['class'], $method->getDeclaringClass()->getName(), diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php index 14dabbece3..35d8075f8b 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Annotations; +use AnnotationsProperties\Asymmetric; use AnnotationsProperties\Bar; use AnnotationsProperties\Baz; use AnnotationsProperties\BazBaz; @@ -23,43 +24,50 @@ public function dataProperties(): array [ 'otherTest' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Test', + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ 'class' => Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => 'OtherNamespace\Ipsum', 'writable' => true, 'readable' => true, ], 'interfaceProperty' => [ 'class' => FooInterface::class, - 'type' => FooInterface::class, + 'readableType' => FooInterface::class, + 'writableType' => FooInterface::class, 'writable' => true, 'readable' => true, ], 'overridenProperty' => [ 'class' => Foo::class, - 'type' => Foo::class, + 'readableType' => Foo::class, + 'writableType' => Foo::class, 'writable' => true, 'readable' => true, ], 'overridenPropertyWithAnnotation' => [ 'class' => Foo::class, - 'type' => Foo::class, + 'readableType' => Foo::class, + 'writableType' => Foo::class, 'writable' => true, 'readable' => true, ], @@ -70,43 +78,50 @@ public function dataProperties(): array [ 'otherTest' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Test', + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ 'class' => Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => 'OtherNamespace\Ipsum', 'writable' => true, 'readable' => true, ], 'overridenProperty' => [ 'class' => Bar::class, - 'type' => Bar::class, + 'readableType' => Bar::class, + 'writableType' => Bar::class, 'writable' => true, 'readable' => true, ], 'overridenPropertyWithAnnotation' => [ 'class' => Bar::class, - 'type' => Bar::class, + 'readableType' => Bar::class, + 'writableType' => Bar::class, 'writable' => true, 'readable' => true, ], 'conflictingAnnotationProperty' => [ 'class' => Bar::class, - 'type' => Bar::class, + 'readableType' => Foo::class, + 'writableType' => Foo::class, 'writable' => true, 'readable' => true, ], @@ -117,43 +132,50 @@ public function dataProperties(): array [ 'otherTest' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Test', + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ 'class' => Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Dolor', + 'readableType' => 'AnnotationsProperties\Dolor', + 'writableType' => 'AnnotationsProperties\Dolor', 'writable' => true, 'readable' => true, ], 'bazProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Lorem', + 'readableType' => 'AnnotationsProperties\Lorem', + 'writableType' => 'AnnotationsProperties\Lorem', 'writable' => true, 'readable' => true, ], 'traitProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\BazBaz', + 'readableType' => 'AnnotationsProperties\BazBaz', + 'writableType' => 'AnnotationsProperties\BazBaz', 'writable' => true, 'readable' => true, ], 'writeOnlyProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Lorem|null', + 'readableType' => '*NEVER*', + 'writableType' => 'AnnotationsProperties\Lorem|null', 'writable' => true, 'readable' => false, ], @@ -164,49 +186,83 @@ public function dataProperties(): array [ 'otherTest' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Test', + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ 'class' => Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Dolor', + 'readableType' => 'AnnotationsProperties\Dolor', + 'writableType' => 'AnnotationsProperties\Dolor', 'writable' => true, 'readable' => true, ], 'bazProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Lorem', + 'readableType' => 'AnnotationsProperties\Lorem', + 'writableType' => 'AnnotationsProperties\Lorem', 'writable' => true, 'readable' => true, ], 'traitProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\BazBaz', + 'readableType' => 'AnnotationsProperties\BazBaz', + 'writableType' => 'AnnotationsProperties\BazBaz', 'writable' => true, 'readable' => true, ], 'writeOnlyProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Lorem|null', + 'readableType' => '*NEVER*', + 'writableType' => 'AnnotationsProperties\Lorem|null', 'writable' => true, 'readable' => false, ], 'numericBazBazProperty' => [ 'class' => BazBaz::class, - 'type' => 'float|int', + 'readableType' => 'float|int', + 'writableType' => 'float|int', + 'writable' => true, + 'readable' => true, + ], + ], + ], + [ + Asymmetric::class, + [ + 'asymmetricPropertyRw' => [ + 'class' => Asymmetric::class, + 'readableType' => 'int', + 'writableType' => 'int|string', + 'writable' => true, + 'readable' => true, + ], + 'asymmetricPropertyXw' => [ + 'class' => Asymmetric::class, + 'readableType' => 'int', + 'writableType' => 'int|string', + 'writable' => true, + 'readable' => true, + ], + 'asymmetricPropertyRx' => [ + 'class' => Asymmetric::class, + 'readableType' => 'int', + 'writableType' => 'int|string', 'writable' => true, 'readable' => true, ], @@ -227,6 +283,8 @@ public function testProperties(string $className, array $properties): void $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); $scope->method('canAccessProperty')->willReturn(true); + $scope->method('canReadProperty')->willReturn(true); + $scope->method('canWriteProperty')->willReturn(true); foreach ($properties as $propertyName => $expectedPropertyData) { $this->assertTrue( $class->hasProperty($propertyName), @@ -240,9 +298,14 @@ public function testProperties(string $className, array $properties): void sprintf('Declaring class of property $%s does not match.', $propertyName), ); $this->assertSame( - $expectedPropertyData['type'], + $expectedPropertyData['readableType'], $property->getReadableType()->describe(VerbosityLevel::precise()), - sprintf('Type of property %s::$%s does not match.', $property->getDeclaringClass()->getName(), $propertyName), + sprintf('Readable type of property %s::$%s does not match.', $property->getDeclaringClass()->getName(), $propertyName), + ); + $this->assertSame( + $expectedPropertyData['writableType'], + $property->getWritableType()->describe(VerbosityLevel::precise()), + sprintf('Writable type of property %s::$%s does not match.', $property->getDeclaringClass()->getName(), $propertyName), ); $this->assertSame( $expectedPropertyData['readable'], diff --git a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php index 2d108fb0c9..48c4197868 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -2,14 +2,21 @@ namespace PHPStan\Reflection\Annotations; +use DeprecatedAnnotations\Baz; +use DeprecatedAnnotations\BazInterface; use DeprecatedAnnotations\DeprecatedBar; use DeprecatedAnnotations\DeprecatedFoo; use DeprecatedAnnotations\DeprecatedWithMultipleTags; use DeprecatedAnnotations\Foo; use DeprecatedAnnotations\FooInterface; +use DeprecatedAnnotations\SubBazInterface; +use DeprecatedAttributeConstants\FooWithConstants; +use DeprecatedAttributeMethods\FooWithMethods; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; +use const PHP_VERSION_ID; class DeprecatedAnnotationsTest extends PHPStanTestCase { @@ -92,6 +99,8 @@ public function testDeprecatedAnnotations(bool $deprecated, string $className, ? $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); $scope->method('canAccessProperty')->willReturn(true); + $scope->method('canReadProperty')->willReturn(true); + $scope->method('canWriteProperty')->willReturn(true); $this->assertSame($deprecated, $class->isDeprecated()); $this->assertSame($classDeprecation, $class->getDeprecatedDescription()); @@ -141,4 +150,255 @@ public function testDeprecatedMethodsFromInterface(): void $this->assertTrue($class->getNativeMethod('superDeprecated')->isDeprecated()->yes()); } + public function testNotDeprecatedChildMethods(): void + { + $reflectionProvider = $this->createReflectionProvider(); + + $this->assertTrue($reflectionProvider->getClass(BazInterface::class)->getNativeMethod('superDeprecated')->isDeprecated()->yes()); + $this->assertTrue($reflectionProvider->getClass(SubBazInterface::class)->getNativeMethod('superDeprecated')->isDeprecated()->no()); + $this->assertTrue($reflectionProvider->getClass(Baz::class)->getNativeMethod('superDeprecated')->isDeprecated()->no()); + } + + public function dataDeprecatedAttributeAboveFunction(): iterable + { + yield [ + 'DeprecatedAttributeFunctions\\notDeprecated', + TrinaryLogic::createNo(), + null, + ]; + yield [ + 'DeprecatedAttributeFunctions\\foo', + TrinaryLogic::createYes(), + null, + ]; + yield [ + 'DeprecatedAttributeFunctions\\fooWithMessage', + TrinaryLogic::createYes(), + 'msg', + ]; + yield [ + 'DeprecatedAttributeFunctions\\fooWithMessage2', + TrinaryLogic::createYes(), + 'msg2', + ]; + yield [ + 'DeprecatedAttributeFunctions\\fooWithConstantMessage', + TrinaryLogic::createYes(), + 'DeprecatedAttributeFunctions\\fooWithConstantMessage', + ]; + } + + /** + * @dataProvider dataDeprecatedAttributeAboveFunction + * + * @param non-empty-string $functionName + */ + public function testDeprecatedAttributeAboveFunction(string $functionName, TrinaryLogic $isDeprecated, ?string $deprecatedDescription): void + { + require_once __DIR__ . '/data/deprecated-attribute-functions.php'; + + $reflectionProvider = $this->createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name($functionName), null); + $this->assertSame($isDeprecated->describe(), $function->isDeprecated()->describe()); + $this->assertSame($deprecatedDescription, $function->getDeprecatedDescription()); + } + + public function dataDeprecatedAttributeAboveMethod(): iterable + { + yield [ + FooWithMethods::class, + 'notDeprecated', + TrinaryLogic::createNo(), + null, + ]; + yield [ + FooWithMethods::class, + 'foo', + TrinaryLogic::createYes(), + null, + ]; + yield [ + FooWithMethods::class, + 'fooWithMessage', + TrinaryLogic::createYes(), + 'msg', + ]; + yield [ + FooWithMethods::class, + 'fooWithMessage2', + TrinaryLogic::createYes(), + 'msg2', + ]; + } + + /** + * @dataProvider dataDeprecatedAttributeAboveMethod + */ + public function testDeprecatedAttributeAboveMethod(string $className, string $methodName, TrinaryLogic $isDeprecated, ?string $deprecatedDescription): void + { + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $method = $class->getNativeMethod($methodName); + $this->assertSame($isDeprecated->describe(), $method->isDeprecated()->describe()); + $this->assertSame($deprecatedDescription, $method->getDeprecatedDescription()); + } + + public function dataDeprecatedAttributeAboveClassConstant(): iterable + { + yield [ + FooWithConstants::class, + 'notDeprecated', + TrinaryLogic::createNo(), + null, + ]; + yield [ + FooWithConstants::class, + 'foo', + TrinaryLogic::createYes(), + null, + ]; + yield [ + FooWithConstants::class, + 'fooWithMessage', + TrinaryLogic::createYes(), + 'msg', + ]; + yield [ + FooWithConstants::class, + 'fooWithMessage2', + TrinaryLogic::createYes(), + 'msg2', + ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + 'DeprecatedAttributeEnum\\EnumWithDeprecatedCases', + 'foo', + TrinaryLogic::createYes(), + null, + ]; + yield [ + 'DeprecatedAttributeEnum\\EnumWithDeprecatedCases', + 'fooWithMessage', + TrinaryLogic::createYes(), + 'msg', + ]; + yield [ + 'DeprecatedAttributeEnum\\EnumWithDeprecatedCases', + 'fooWithMessage2', + TrinaryLogic::createYes(), + 'msg2', + ]; + } + + /** + * @dataProvider dataDeprecatedAttributeAboveClassConstant + */ + public function testDeprecatedAttributeAboveClassConstant(string $className, string $constantName, TrinaryLogic $isDeprecated, ?string $deprecatedDescription): void + { + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $constant = $class->getConstant($constantName); + $this->assertSame($isDeprecated->describe(), $constant->isDeprecated()->describe()); + $this->assertSame($deprecatedDescription, $constant->getDeprecatedDescription()); + } + + public function dataDeprecatedAttributeAboveEnumCase(): iterable + { + yield [ + 'DeprecatedAttributeEnum\\EnumWithDeprecatedCases', + 'foo', + TrinaryLogic::createYes(), + null, + ]; + yield [ + 'DeprecatedAttributeEnum\\EnumWithDeprecatedCases', + 'fooWithMessage', + TrinaryLogic::createYes(), + 'msg', + ]; + yield [ + 'DeprecatedAttributeEnum\\EnumWithDeprecatedCases', + 'fooWithMessage2', + TrinaryLogic::createYes(), + 'msg2', + ]; + } + + /** + * @dataProvider dataDeprecatedAttributeAboveEnumCase + */ + public function testDeprecatedAttributeAboveEnumCase(string $className, string $caseName, TrinaryLogic $isDeprecated, ?string $deprecatedDescription): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $case = $class->getEnumCase($caseName); + $this->assertSame($isDeprecated->describe(), $case->isDeprecated()->describe()); + $this->assertSame($deprecatedDescription, $case->getDeprecatedDescription()); + } + + public function dataDeprecatedAttributeAbovePropertyHook(): iterable + { + yield [ + 'DeprecatedAttributePropertyHooks\\Foo', + 'i', + 'get', + TrinaryLogic::createNo(), + null, + ]; + yield [ + 'DeprecatedAttributePropertyHooks\\Foo', + 'j', + 'get', + TrinaryLogic::createYes(), + null, + ]; + yield [ + 'DeprecatedAttributePropertyHooks\\Foo', + 'k', + 'get', + TrinaryLogic::createYes(), + 'msg', + ]; + yield [ + 'DeprecatedAttributePropertyHooks\\Foo', + 'l', + 'get', + TrinaryLogic::createYes(), + 'msg2', + ]; + yield [ + 'DeprecatedAttributePropertyHooks\\Foo', + 'm', + 'get', + TrinaryLogic::createYes(), + '$m::get+DeprecatedAttributePropertyHooks\Foo::$m::get+m', + ]; + } + + /** + * @dataProvider dataDeprecatedAttributeAbovePropertyHook + * @param 'get'|'set' $hookName + */ + public function testDeprecatedAttributeAbovePropertyHook(string $className, string $propertyName, string $hookName, TrinaryLogic $isDeprecated, ?string $deprecatedDescription): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $property = $class->getNativeProperty($propertyName); + $hook = $property->getHook($hookName); + $this->assertSame($isDeprecated->describe(), $hook->isDeprecated()->describe()); + $this->assertSame($deprecatedDescription, $hook->getDeprecatedDescription()); + } + } diff --git a/tests/PHPStan/Reflection/Annotations/DeprecatedAttributePhpFunctionFromParserReflectionRuleTest.php b/tests/PHPStan/Reflection/Annotations/DeprecatedAttributePhpFunctionFromParserReflectionRuleTest.php new file mode 100644 index 0000000000..efdfaece70 --- /dev/null +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAttributePhpFunctionFromParserReflectionRuleTest.php @@ -0,0 +1,145 @@ + + */ +class DeprecatedAttributePhpFunctionFromParserReflectionRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new /** @implements Rule */ class implements Rule { + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node instanceof InFunctionNode) { + $reflection = $node->getFunctionReflection(); + } elseif ($node instanceof InClassMethodNode) { + $reflection = $node->getMethodReflection(); + } elseif ($node instanceof InPropertyHookNode) { + $reflection = $node->getHookReflection(); + } else { + return []; + } + + if (!$reflection->isDeprecated()->yes()) { + return [ + RuleErrorBuilder::message('Not deprecated')->identifier('tests.notDeprecated')->build(), + ]; + } + + $description = $reflection->getDeprecatedDescription(); + if ($description === null) { + return [ + RuleErrorBuilder::message('Deprecated')->identifier('tests.deprecated')->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf('Deprecated: %s', $description))->identifier('tests.deprecated')->build(), + ]; + } + + }; + } + + public function testFunctionRule(): void + { + $this->analyse([__DIR__ . '/data/deprecated-attribute-functions.php'], [ + [ + 'Not deprecated', + 7, + ], + [ + 'Deprecated', + 12, + ], + [ + 'Deprecated: msg', + 18, + ], + [ + 'Deprecated: msg2', + 24, + ], + [ + 'Deprecated: DeprecatedAttributeFunctions\\fooWithConstantMessage', + 30, + ], + ]); + } + + public function testMethodRule(): void + { + $this->analyse([__DIR__ . '/data/deprecated-attribute-methods.php'], [ + [ + 'Not deprecated', + 10, + ], + [ + 'Deprecated', + 15, + ], + [ + 'Deprecated: msg', + 21, + ], + [ + 'Deprecated: msg2', + 27, + ], + ]); + } + + public function testPropertyHookRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/deprecated-attribute-property-hooks.php'], [ + [ + 'Not deprecated', + 11, + ], + [ + 'Deprecated', + 17, + ], + [ + 'Deprecated: msg', + 24, + ], + [ + 'Deprecated: msg2', + 31, + ], + [ + 'Deprecated: $m::get+DeprecatedAttributePropertyHooks\Foo::$m::get+m', + 38, + ], + ]); + } + +} diff --git a/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php index fb61a5c90e..77b9d3e008 100644 --- a/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php @@ -4,7 +4,6 @@ use FinalAnnotations\FinalFoo; use FinalAnnotations\Foo; -use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Testing\PHPStanTestCase; @@ -49,6 +48,8 @@ public function testFinalAnnotations(bool $final, string $className, array $fina $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); $scope->method('canAccessProperty')->willReturn(true); + $scope->method('canReadProperty')->willReturn(true); + $scope->method('canWriteProperty')->willReturn(true); $this->assertSame($final, $class->isFinal()); @@ -58,14 +59,4 @@ public function testFinalAnnotations(bool $final, string $className, array $fina } } - public function testFinalUserFunctions(): void - { - require_once __DIR__ . '/data/annotations-final.php'; - - $reflectionProvider = $this->createReflectionProvider(); - - $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 b734fac05c..d7af0d248f 100644 --- a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php @@ -121,6 +121,8 @@ public function testInternalAnnotations(bool $internal, string $className, array $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); $scope->method('canAccessProperty')->willReturn(true); + $scope->method('canReadProperty')->willReturn(true); + $scope->method('canWriteProperty')->willReturn(true); $this->assertSame($internal, $class->isInternal()); diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php index 1095002d92..553c2444ef 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php @@ -215,3 +215,26 @@ public function superDeprecated() } } + +interface BazInterface +{ + /** + * @deprecated Use the SubBazInterface instead. + */ + public function superDeprecated(); +} + +interface SubBazInterface extends BazInterface +{ + /** + * @not-deprecated + */ + public function superDeprecated(); +} + +class Baz implements SubBazInterface +{ + public function superDeprecated() + { + } +} diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-final.php b/tests/PHPStan/Reflection/Annotations/data/annotations-final.php index cb3932500f..f7c7c2da25 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-final.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-final.php @@ -2,19 +2,6 @@ namespace FinalAnnotations; -function foo() -{ - -} - -/** - * @final - */ -function finalFoo() -{ - -} - class Foo { diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php b/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php index fc7363ccfc..9e4adf962f 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php @@ -2,6 +2,7 @@ namespace AnnotationsProperties; +use AllowDynamicProperties; use OtherNamespace\Test as OtherTest; use OtherNamespace\Ipsum; @@ -12,6 +13,7 @@ * @property Ipsum $conflictingProperty * @property Foo $overridenProperty */ +#[AllowDynamicProperties] class Foo implements FooInterface { @@ -55,6 +57,22 @@ class BazBaz extends Baz } +/** + * @property-read int $asymmetricPropertyRw + * @property-write int|string $asymmetricPropertyRw + * + * @property int $asymmetricPropertyXw + * @property-write int|string $asymmetricPropertyXw + * + * @property-read int $asymmetricPropertyRx + * @property int|string $asymmetricPropertyRx + */ +#[AllowDynamicProperties] +class Asymmetric +{ + +} + /** * @property FooInterface $interfaceProperty */ diff --git a/tests/PHPStan/Reflection/Annotations/data/deprecated-attribute-constants.php b/tests/PHPStan/Reflection/Annotations/data/deprecated-attribute-constants.php new file mode 100644 index 0000000000..6de5cc39d6 --- /dev/null +++ b/tests/PHPStan/Reflection/Annotations/data/deprecated-attribute-constants.php @@ -0,0 +1,21 @@ += 8.1 + +namespace DeprecatedAttributeEnum; + +use Deprecated; + +enum EnumWithDeprecatedCases +{ + + #[Deprecated] + case foo; + + #[Deprecated('msg')] + case fooWithMessage; + + #[Deprecated(since: '1.0', message: 'msg2')] + case fooWithMessage2; + +} diff --git a/tests/PHPStan/Reflection/Annotations/data/deprecated-attribute-functions.php b/tests/PHPStan/Reflection/Annotations/data/deprecated-attribute-functions.php new file mode 100644 index 0000000000..a7325b8fc3 --- /dev/null +++ b/tests/PHPStan/Reflection/Annotations/data/deprecated-attribute-functions.php @@ -0,0 +1,34 @@ += 8.4 + +namespace DeprecatedAttributePropertyHooks; + +use Deprecated; + +class Foo +{ + + public int $i { + get { + return 1; + } + } + + public int $j { + #[Deprecated] + get { + return 1; + } + } + + public int $k { + #[Deprecated('msg')] + get { + return 1; + } + } + + public int $l { + #[Deprecated(since: '1.0', message: 'msg2')] + get { + return 1; + } + } + + public int $m { + #[Deprecated(message: __FUNCTION__ . '+' . __METHOD__ . '+' . __PROPERTY__)] + get { + return 1; + } + } + +} diff --git a/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php b/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php new file mode 100644 index 0000000000..b6ee413032 --- /dev/null +++ b/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php @@ -0,0 +1,156 @@ +> + */ +class AnonymousClassReflectionTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new /** @implements Rule */ class (self::createReflectionProvider()) implements Rule { + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->isAnonymous()) { + return []; + } + + Assert::assertTrue($node->getAttribute('anonymousClass')); + + $classReflection = $this->reflectionProvider->getAnonymousClassReflection($node, $scope); + + return [ + RuleErrorBuilder::message(sprintf( + "name: %s\ndisplay name: %s", + $classReflection->getName(), + $classReflection->getDisplayName(), + ))->identifier('test.anonymousClassReflection')->build(), + ]; + } + + }; + } + + public function testReflection(): void + { + $this->analyse([__DIR__ . '/data/anonymous-classes.php'], [ + [ + implode("\n", [ + 'name: AnonymousClass0c307d7b8501323d1d30b0afea7e0578', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:5', + ]), + 5, + ], + [ + implode("\n", [ + 'name: AnonymousClassa16017c480192f8fbf3c03e17840e99c', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:1', + ]), + 7, + ], + [ + implode("\n", [ + 'name: AnonymousClassd68d75f1cdac379350e3027c09a7c5a0', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:2', + ]), + 7, + ], + [ + implode("\n", [ + 'name: AnonymousClass75aa798fed4f30306c14dcf03a50878c', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:3', + ]), + 7, + ], + [ + implode("\n", [ + 'name: AnonymousClass4fcabdc52bfed5f8c101f3f89b2180bd', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:1', + ]), + 9, + ], + [ + implode("\n", [ + 'name: AnonymousClass0e77d7995f4c47dcd5402817970fd7e0', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:2', + ]), + 9, + ], + [ + implode("\n", [ + 'name: AnonymousClass1d622e3ff3a656e68d55eafbd25eaef1', + 'display name: AnonymousClassReflectionTest\A@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:17:1', + ]), + 17, + ], + [ + implode("\n", [ + 'name: AnonymousClass6e1acc8e948827c8d0439a2225fdbdd0', + 'display name: AnonymousClassReflectionTest\A@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:17:2', + ]), + 17, + ], + [ + implode("\n", [ + 'name: AnonymousClass2a49db3d44479dddd8beaea4ea8131fb', + 'display name: AnonymousClassReflectionTest\A@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:19:1', + ]), + 19, + ], + [ + implode("\n", [ + 'name: AnonymousClass337463cf86ee25e526f445630960b336', + 'display name: AnonymousClassReflectionTest\A@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:19:2', + ]), + 19, + ], + [ + implode("\n", [ + 'name: AnonymousClassda3e79cc45f826d60295f848abab37e7', + 'display name: AnonymousClassReflectionTest\U@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:29', + ]), + 29, + ], + [ + implode("\n", [ + 'name: AnonymousClassc06612bf3776bbe5e50870a8c3151186', + 'display name: AnonymousClassReflectionTest\U@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:31', + ]), + 31, + ], + [ + implode("\n", [ + 'name: AnonymousClassbee6eba8c721d73d649fcc9d361f5902', + 'display name: AnonymousClassReflectionTest\V@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:33', + ]), + 33, + ], + ]); + } + +} diff --git a/tests/PHPStan/Reflection/AttributeReflectionFromNodeRuleTest.php b/tests/PHPStan/Reflection/AttributeReflectionFromNodeRuleTest.php new file mode 100644 index 0000000000..756ff9e1b1 --- /dev/null +++ b/tests/PHPStan/Reflection/AttributeReflectionFromNodeRuleTest.php @@ -0,0 +1,113 @@ +> + */ +class AttributeReflectionFromNodeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new /** @implements Rule */ class implements Rule { + + public function getNodeType(): string + { + return NodeAbstract::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node instanceof InClassMethodNode) { + $reflection = $node->getMethodReflection(); + } elseif ($node instanceof InFunctionNode) { + $reflection = $node->getFunctionReflection(); + } else { + return []; + } + + $parts = []; + foreach ($reflection->getAttributes() as $attribute) { + $args = []; + foreach ($attribute->getArgumentTypes() as $argName => $argType) { + $args[] = sprintf('%s: %s', $argName, $argType->describe(VerbosityLevel::precise())); + } + + $parts[] = sprintf('#[%s(%s)]', $attribute->getName(), implode(', ', $args)); + } + + foreach ($reflection->getParameters() as $parameter) { + $parameterAttributes = []; + foreach ($parameter->getAttributes() as $parameterAttribute) { + $parameterArgs = []; + foreach ($parameterAttribute->getArgumentTypes() as $argName => $argType) { + $parameterArgs[] = sprintf('%s: %s', $argName, $argType->describe(VerbosityLevel::precise())); + } + $parameterAttributes[] = sprintf('#[%s(%s)]', $parameterAttribute->getName(), implode(', ', $parameterArgs)); + } + + if (count($parameterAttributes) === 0) { + continue; + } + + $parts[] = sprintf('$%s: %s', $parameter->getName(), implode(', ', $parameterAttributes)); + } + + if (count($parts) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message(implode(', ', $parts))->identifier('test.attributes')->build(), + ]; + } + + }; + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/attribute-reflection.php'], [ + [ + '#[AttributeReflectionTest\MyAttr(one: 7, two: 8)], $test: #[AttributeReflectionTest\MyAttr(one: 9, two: 10)]', + 28, + ], + [ + '#[AttributeReflectionTest\MyAttr()]', + 39, + ], + [ + '#[AttributeReflectionTest\Nonexistent()]', + 44, + ], + [ + '#[AttributeReflectionTest\MyAttr(one: 11, two: 12)]', + 54, + ], + [ + '#[AttributeReflectionTest\MyAttr(one: 28, two: 29)]', + 59, + ], + ]); + } + +} diff --git a/tests/PHPStan/Reflection/AttributeReflectionTest.php b/tests/PHPStan/Reflection/AttributeReflectionTest.php new file mode 100644 index 0000000000..f0c22f45ec --- /dev/null +++ b/tests/PHPStan/Reflection/AttributeReflectionTest.php @@ -0,0 +1,179 @@ +createReflectionProvider(); + + yield [ + $reflectionProvider->getFunction(new Name('AttributeReflectionTest\\myFunction'), null)->getAttributes(), + [ + [MyAttr::class, []], + ], + ]; + + yield [ + $reflectionProvider->getFunction(new Name('AttributeReflectionTest\\myFunction2'), null)->getAttributes(), + [ + ['AttributeReflectionTest\\Nonexistent', []], + ], + ]; + + yield [ + $reflectionProvider->getFunction(new Name('AttributeReflectionTest\\myFunction3'), null)->getAttributes(), + [], + ]; + + yield [ + $reflectionProvider->getFunction(new Name('AttributeReflectionTest\\myFunction4'), null)->getAttributes(), + [ + [ + MyAttr::class, + [ + 'one' => '11', + 'two' => '12', + ], + ], + ], + ]; + + $foo = $reflectionProvider->getClass(Foo::class); + + yield [ + $foo->getAttributes(), + [ + [ + MyAttr::class, + [ + 'one' => '1', + 'two' => '2', + ], + ], + ], + ]; + + yield [ + $foo->getConstant('MY_CONST')->getAttributes(), + [ + [ + MyAttr::class, + [ + 'one' => '3', + 'two' => '4', + ], + ], + ], + ]; + + yield [ + $foo->getNativeProperty('prop')->getAttributes(), + [ + [ + MyAttr::class, + [ + 'one' => '5', + 'two' => '6', + ], + ], + ], + ]; + + yield [ + $foo->getConstructor()->getAttributes(), + [ + [ + MyAttr::class, + [ + 'one' => '7', + 'two' => '8', + ], + ], + ], + ]; + + if (PHP_VERSION_ID >= 80100) { + $enum = $reflectionProvider->getClass('AttributeReflectionTest\\FooEnum'); + + yield [ + $enum->getEnumCase('TEST')->getAttributes(), + [ + [ + MyAttr::class, + [ + 'one' => '15', + 'two' => '16', + ], + ], + ], + ]; + + yield [ + $enum->getEnumCases()['TEST']->getAttributes(), + [ + [ + MyAttr::class, + [ + 'one' => '15', + 'two' => '16', + ], + ], + ], + ]; + } + + yield [ + $foo->getConstructor()->getOnlyVariant()->getParameters()[0]->getAttributes(), + [ + [ + MyAttr::class, + [ + 'one' => '9', + 'two' => '10', + ], + ], + ], + ]; + } + + /** + * @dataProvider dataAttributeReflections + * @param list $attributeReflections + * @param list}> $expectations + */ + public function testAttributeReflections( + array $attributeReflections, + array $expectations, + ): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0'); + } + + $this->assertCount(count($expectations), $attributeReflections); + foreach ($expectations as $i => [$name, $argumentTypes]) { + $attribute = $attributeReflections[$i]; + $this->assertSame($name, $attribute->getName()); + + $attributeArgumentTypes = $attribute->getArgumentTypes(); + $this->assertCount(count($argumentTypes), $attributeArgumentTypes); + + foreach ($argumentTypes as $argumentName => $argumentType) { + $this->assertArrayHasKey($argumentName, $attributeArgumentTypes); + $this->assertSame($argumentType, $attributeArgumentTypes[$argumentName]->describe(VerbosityLevel::precise())); + } + } + } + +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php index 57442ab9ba..6e4c91a783 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php @@ -4,13 +4,17 @@ use PHPStan\BetterReflection\Reflection\ReflectionClass; use PHPStan\BetterReflection\Reflector\DefaultReflector; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Constant\ConstantIntegerType; use TestSingleFileSourceLocator\AFoo; use TestSingleFileSourceLocator\InCondition; +use function class_alias; function testFunctionForLocator(): void // phpcs:disable { - + echo 'test'; } class AutoloadSourceLocatorTest extends PHPStanTestCase @@ -18,7 +22,7 @@ class AutoloadSourceLocatorTest extends PHPStanTestCase public function testAutoloadEverythingInFile(): void { - $locator = new AutoloadSourceLocator(self::getContainer()->getByType(FileNodesFetcher::class), false); + $locator = new AutoloadSourceLocator(self::getContainer()->getByType(FileNodesFetcher::class), true); $reflector = new DefaultReflector($locator); $aFoo = $reflector->reflectClass(AFoo::class); $this->assertNotNull($aFoo->getFileName()); @@ -30,12 +34,24 @@ public function testAutoloadEverythingInFile(): void $someConstant = $reflector->reflectConstant('TestSingleFileSourceLocator\\SOME_CONSTANT'); $this->assertNotNull($someConstant->getFileName()); $this->assertSame('a.php', basename($someConstant->getFileName())); - $this->assertSame(1, $someConstant->getValue()); + + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + $someConstantValue = $initializerExprTypeResolver->getType( + $someConstant->getValueExpression(), + InitializerExprContext::fromGlobalConstant($someConstant), + ); + $this->assertInstanceOf(ConstantIntegerType::class, $someConstantValue); + $this->assertSame(1, $someConstantValue->getValue()); $anotherConstant = $reflector->reflectConstant('TestSingleFileSourceLocator\\ANOTHER_CONSTANT'); $this->assertNotNull($anotherConstant->getFileName()); $this->assertSame('a.php', basename($anotherConstant->getFileName())); - $this->assertSame(2, $anotherConstant->getValue()); + $anotherConstantValue = $initializerExprTypeResolver->getType( + $anotherConstant->getValueExpression(), + InitializerExprContext::fromGlobalConstant($anotherConstant), + ); + $this->assertInstanceOf(ConstantIntegerType::class, $anotherConstantValue); + $this->assertSame(2, $anotherConstantValue->getValue()); $doFooFunctionReflection = $reflector->reflectFunction('TestSingleFileSourceLocator\\doFoo'); $this->assertSame('TestSingleFileSourceLocator\\doFoo', $doFooFunctionReflection->getName()); @@ -53,4 +69,13 @@ class_exists(InCondition::class); $this->assertSame(AFoo::class, $classInCondition->getParentClass()->getName()); } + public function testClassAlias(): void + { + class_alias(AFoo::class, 'A_Foo'); + $locator = new AutoloadSourceLocator(self::getContainer()->getByType(FileNodesFetcher::class), true); + $reflector = new DefaultReflector($locator); + $class = $reflector->reflectClass('A_Foo'); + $this->assertSame(AFoo::class, $class->getName()); + } + } diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php index 8a853c48f4..8ea1afae71 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php @@ -3,10 +3,14 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use OptimizedDirectory\BFoo; +use PHPStan\BetterReflection\Identifier\IdentifierType; +use PHPStan\BetterReflection\Reflection\Reflection; use PHPStan\BetterReflection\Reflector\DefaultReflector; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\Testing\PHPStanTestCase; use TestDirectorySourceLocator\AFoo; +use TestDirectorySourceLocator\EmptyClass; +use function array_map; use function basename; use const PHP_VERSION_ID; @@ -36,6 +40,11 @@ public function dataClass(): iterable BFoo::class, 'b.php', ], + [ + 'TestDirectorySourceLocator\\EmptyClass', + EmptyClass::class, + 'e.php', + ], ]; if (PHP_VERSION_ID < 80100) { @@ -135,6 +144,152 @@ public function testFunctionExists(string $functionName, string $expectedFunctio $this->assertSame($file, basename($functionReflection->getFileName())); } + public function dataConstant(): iterable + { + yield from [ + [ + 'OptimizedDirectory\\SOMETHING', + 'b.php', + ], + [ + 'OptimizedDirectory\\CLASS_CONST', + null, + ], + [ + 'OptimizedDirectory2\\ANYTHING', + 'd.php', + ], + [ + 'NOTHING', + 'd.php', + ], + [ + 'TestDirectorySourceLocator\\Something\\CONSTANT', + '01-constant-in-namespace.php', + ], + [ + 'TestDirectorySourceLocator\\Something\\PUBLIC_CONSTANT', + '01-constant-in-namespace.php', + ], + [ + 'TestDirectorySourceLocator\\Something\\FINAL_PUBLIC_CONSTANT', + '01-constant-in-namespace.php', + ], + [ + 'DEFINE_CONST', + '01-define.php', + ], + [ + 'FQN_DEFINE_CONST', + '01-define.php', + ], + [ + 'DOUBLE_QUOTES_DEFINE_CONST', + '01-define.php', + ], + [ + 'OptimizedDirectory\\DEFINE_CONST', + '01-define.php', + ], + [ + 'OptimizedDirectory\\DEFINE_CONST2', + '01-define.php', + ], + [ + 'DEFINE_THAT_SHOULD_SURVIVE_METHOD_CALL', + '01-define.php', + ], + ]; + } + + /** + * @dataProvider dataConstant + */ + public function testConstant(string $constantName, ?string $expectedFile): void + { + $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); + $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); + $reflector = new DefaultReflector($locator); + + if ($expectedFile === null) { + $this->expectException(IdentifierNotFound::class); + $reflector->reflectConstant($constantName); + } else { + $constantReflection = $reflector->reflectConstant($constantName); + + $this->assertNotNull($constantReflection->getFileName()); + $this->assertSame($expectedFile, basename($constantReflection->getFileName())); + } + } + + public function testLocateIdentifiersByType(): void + { + /** @var OptimizedDirectorySourceLocatorFactory $factory */ + $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); + $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); + $reflector = new DefaultReflector($locator); + + $classIdentifiers = $locator->locateIdentifiersByType( + $reflector, + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + ); + + $expectedClasses = [ + 'TestDirectorySourceLocator\AFoo', + 'OptimizedDirectory\BFoo', + 'CFoo', + 'TestDirectorySourceLocator\EmptyClass', + 'TestDirectorySourceLocator\Something\Whatever', + 'OptimizedDirectory\WithDefineCall', + ]; + if (PHP_VERSION_ID >= 80100) { + $expectedClasses[] = 'OptimizedDirectory\TestEnum'; + $expectedClasses[] = 'OptimizedDirectory\BackedByStringWithoutSpace'; + $expectedClasses[] = 'OptimizedDirectory\UppercaseEnum'; + } + + $actualClasses = array_map(static fn (Reflection $reflection) => $reflection->getName(), $classIdentifiers); + $this->assertEqualsCanonicalizing($expectedClasses, $actualClasses); + + $functionIdentifiers = $locator->locateIdentifiersByType( + $reflector, + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + ); + + $actualFunctions = array_map(static fn (Reflection $reflection) => $reflection->getName(), $functionIdentifiers); + + $this->assertEqualsCanonicalizing([ + 'TestDirectorySourceLocator\doLorem', + 'OptimizedDirectory\doBar', + 'OptimizedDirectory\doBaz', + 'OptimizedDirectory\get_smarty', + 'OptimizedDirectory\get_smarty2', + 'OptimizedDirectory\upperCaseFunction', + ], $actualFunctions); + + $constantIdentifiers = $locator->locateIdentifiersByType( + $reflector, + new IdentifierType(IdentifierType::IDENTIFIER_CONSTANT), + ); + + $actualConstants = array_map(static fn (Reflection $reflection) => $reflection->getName(), $constantIdentifiers); + + $this->assertEqualsCanonicalizing([ + 'NOTHING', + 'OptimizedDirectory\SOMETHING', + 'OptimizedDirectory2\ANYTHING', + 'TestDirectorySourceLocator\Something\CONSTANT', + 'TestDirectorySourceLocator\Something\PUBLIC_CONSTANT', + 'TestDirectorySourceLocator\Something\FINAL_PUBLIC_CONSTANT', + 'DEFINE_CONST', + 'FQN_DEFINE_CONST', + 'DOUBLE_QUOTES_DEFINE_CONST', + 'DEFINE_THAT_SHOULD_SURVIVE_METHOD_CALL', + 'OptimizedDirectory\\DEFINE_CONST', + 'OptimizedDirectory\\DEFINE_CONST2', + ], $actualConstants); + } + public function dataFunctionDoesNotExist(): array { return [ @@ -158,10 +313,6 @@ public function testFunctionDoesNotExist(string $functionName): void 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); diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php index 363bb4ee23..80b588643f 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php @@ -2,13 +2,17 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\BetterReflection\Identifier\IdentifierType; +use PHPStan\BetterReflection\Reflection\Reflection; use PHPStan\BetterReflection\Reflector\DefaultReflector; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\VerbosityLevel; use SingleFileSourceLocatorTestClass; -use stdClass; use TestSingleFileSourceLocator\AFoo; -use function str_replace; +use function array_map; use const PHP_VERSION_ID; class OptimizedSingleFileSourceLocatorTest extends PHPStanTestCase @@ -50,6 +54,79 @@ public function dataClass(): iterable ]; } + public function dataForIdenifiersByType(): iterable + { + yield from [ + 'classes wrapped in conditions' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + [ + 'TestSingleFileSourceLocator\AFoo', + 'TestSingleFileSourceLocator\InCondition', + 'TestSingleFileSourceLocator\InCondition', + 'TestSingleFileSourceLocator\InCondition', + ], + __DIR__ . '/data/a.php', + ], + 'class with function in same file' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + ['SingleFileSourceLocatorTestClass'], + __DIR__ . '/data/b.php', + ], + 'class bug-5525' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + ['Faker\Provider\nl_BE\Text'], + __DIR__ . '/data/bug-5525.php', + ], + 'file without classes' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + [], + __DIR__ . '/data/const.php', + ], + 'plain function in complex file' => [ + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + [ + 'TestSingleFileSourceLocator\doFoo', + ], + __DIR__ . '/data/a.php', + ], + 'function with class in same file' => [ + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + ['singleFileSourceLocatorTestFunction'], + __DIR__ . '/data/b.php', + ], + 'file without functions' => [ + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + [], + __DIR__ . '/data/only-class.php', + ], + 'constants' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CONSTANT), + [ + 'ANOTHER_NAME', + 'ConstFile\ANOTHER_NAME', + 'ConstFile\TABLE_NAME', + 'OPTIMIZED_SFSL_OBJECT_CONSTANT', + 'const_with_dir_const', + ], + __DIR__ . '/data/const.php', + ], + ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield 'enums as classes' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + [ + 'OptimizedDirectory\BackedByStringWithoutSpace', + 'OptimizedDirectory\TestEnum', + 'OptimizedDirectory\UppercaseEnum', + ], + __DIR__ . '/data/directory/enum.php', + ]; + } + /** * @dataProvider dataClass */ @@ -105,39 +182,44 @@ public function dataConst(): array return [ [ 'ConstFile\\TABLE_NAME', - 'resized_images', + "'resized_images'", ], [ 'ANOTHER_NAME', - 'foo_images', + "'foo_images'", ], [ 'ConstFile\\ANOTHER_NAME', - 'bar_images', + "'bar_images'", ], [ 'const_with_dir_const', - str_replace('\\', '/', __DIR__ . '/data'), + 'literal-string&non-falsy-string', ], [ 'OPTIMIZED_SFSL_OBJECT_CONSTANT', - new stdClass(), + 'stdClass', ], ]; } /** * @dataProvider dataConst - * @param mixed $value */ - public function testConst(string $constantName, $value): void + public function testConst(string $constantName, string $valueTypeDescription): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); $locator = $factory->create(__DIR__ . '/data/const.php'); $reflector = new DefaultReflector($locator); $constant = $reflector->reflectConstant($constantName); $this->assertSame($constantName, $constant->getName()); - $this->assertEquals($value, $constant->getValue()); + + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + $valueType = $initializerExprTypeResolver->getType( + $constant->getValueExpression(), + InitializerExprContext::fromGlobalConstant($constant), + ); + $this->assertSame($valueTypeDescription, $valueType->describe(VerbosityLevel::precise())); } public function dataConstUnknown(): array @@ -159,4 +241,28 @@ public function testConstUnknown(string $constantName): void $reflector->reflectConstant($constantName); } + /** + * @dataProvider dataForIdenifiersByType + * @param class-string[] $expectedIdentifiers + */ + public function testLocateIdentifiersByType( + IdentifierType $identifierType, + array $expectedIdentifiers, + string $file, + ): void + { + /** @var OptimizedSingleFileSourceLocatorFactory $factory */ + $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); + $locator = $factory->create($file); + $reflector = new DefaultReflector($locator); + + $reflections = $locator->locateIdentifiersByType( + $reflector, + $identifierType, + ); + + $actualIdentifiers = array_map(static fn (Reflection $reflection) => $reflection->getName(), $reflections); + $this->assertEqualsCanonicalizing($expectedIdentifiers, $actualIdentifiers); + } + } diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/01-constant-in-namespace.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/01-constant-in-namespace.php new file mode 100644 index 0000000000..0785f66561 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/01-constant-in-namespace.php @@ -0,0 +1,9 @@ += 8.1 + +namespace TestDirectorySourceLocator\Something; + +class Whatever +{ + const CONSTANT = 'constant'; + + public const PUBLIC_CONSTANT = 'constant'; + + final public const FINAL_PUBLIC_CONSTANT = 'constant'; +} + diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/02-define-as-method-call.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/02-define-as-method-call.php new file mode 100644 index 0000000000..e1e9307aa2 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/02-define-as-method-call.php @@ -0,0 +1,11 @@ +define('DEFINE_THAT_SHOULD_SURVIVE_METHOD_CALL', 'no_define'); + } +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/b.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/b.php index 390915adeb..6fd6ebbd1b 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/b.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/b.php @@ -5,6 +5,8 @@ class BFoo { + const CLASS_CONST = 'class_const'; + function doBar() { @@ -40,3 +42,5 @@ function & get_smarty2() { } + +const SOMETHING = 'something'; diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/d.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/d.php new file mode 100644 index 0000000000..f8a06beb8d --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/d.php @@ -0,0 +1,15 @@ +getReflector()->reflectClass(Throwable::class); - $this->assertSame(Throwable::class, $reflection->getName()); - } - - public function testFunction(): void - { - $reflection = $this->getReflector()->reflectFunction('htmlspecialchars'); - $this->assertSame('htmlspecialchars', $reflection->getName()); - } - - 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'], - ])); - $astLocator = new Locator($parser); - $sourceStubber = new Php8StubsSourceStubber(); - $phpInternalSourceLocator = new PhpInternalSourceLocator( - $astLocator, - $sourceStubber, - ); - return new DefaultReflector($phpInternalSourceLocator); - } - -} diff --git a/tests/PHPStan/Reflection/ClassReflectionTest.php b/tests/PHPStan/Reflection/ClassReflectionTest.php index 2b7eaf367b..b4d2fa5eb4 100644 --- a/tests/PHPStan/Reflection/ClassReflectionTest.php +++ b/tests/PHPStan/Reflection/ClassReflectionTest.php @@ -27,18 +27,17 @@ 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\Testing\RuleTestCase; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntegerType; +use PHPStan\Type\VerbosityLevel; +use PHPUnit\Framework\TestCase; use ReflectionClass; -use ReflectionEnum; use WrongClassConstantFile\SecuredRouter; use function array_map; use function array_values; +use function count; use const PHP_VERSION_ID; class ClassReflectionTest extends PHPStanTestCase @@ -59,11 +58,8 @@ public function dataHasTraitUse(): array */ public function testHasTraitUse(string $className, bool $has): void { - $broker = $this->createMock(Broker::class); - $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), [], [], $className, new ReflectionClass($className), null, null, null); + $reflectionProvider = $this->createReflectionProvider(); + $classReflection = $reflectionProvider->getClass($className); $this->assertSame($has, $classReflection->hasTraitUse(FooTrait::class)); } @@ -82,33 +78,18 @@ public function dataClassHierarchyDistances(): array ], [ Ipsum::class, - PHP_VERSION_ID < 70400 ? [ Ipsum::class => 0, TraitOne::class => 1, Lorem::class => 2, TraitTwo::class => 3, TraitThree::class => 4, - SecondLoremInterface::class => 5, - FirstLoremInterface::class => 6, + FirstLoremInterface::class => 5, + SecondLoremInterface::class => 6, FirstIpsumInterface::class => 7, ExtendedIpsumInterface::class => 8, SecondIpsumInterface::class => 9, ThirdIpsumInterface::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, ], ], ]; @@ -124,25 +105,8 @@ public function testClassHierarchyDistances( 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), - null, - null, - null, - ); + $reflectionProvider = $this->createReflectionProvider(); + $classReflection = $reflectionProvider->getClass($class); $this->assertSame( $expectedDistances, $classReflection->getClassHierarchyDistances(), @@ -154,7 +118,7 @@ public function testVariadicTraitMethod(): void $reflectionProvider = $this->createReflectionProvider(); $fooReflection = $reflectionProvider->getClass(Foo::class); $variadicMethod = $fooReflection->getNativeMethod('variadicMethod'); - $methodVariant = ParametersAcceptorSelector::selectSingle($variadicMethod->getVariants()); + $methodVariant = $variadicMethod->getOnlyVariant(); $this->assertTrue($methodVariant->isVariadic()); } @@ -213,9 +177,6 @@ public function dataIsAttributeClass(): array */ 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->createReflectionProvider(); $reflection = $reflectionProvider->getClass($className); $this->assertSame($expected, $reflection->isAttributeClass()); @@ -335,7 +296,7 @@ public function testEnumIsFinal(): void $reflectionProvider = $this->createReflectionProvider(); $enum = $reflectionProvider->getClass('PHPStan\Fixture\TestEnum'); $this->assertTrue($enum->isEnum()); - $this->assertInstanceOf(ReflectionEnum::class, $enum->getNativeReflection()); + $this->assertInstanceOf('ReflectionEnum', $enum->getNativeReflection()); // @phpstan-ignore-line Exact error differs on PHP 7.4 and others $this->assertTrue($enum->isFinal()); $this->assertTrue($enum->isFinalByKeyword()); } @@ -351,4 +312,354 @@ public function testBackedEnumType(): void $this->assertInstanceOf(IntegerType::class, $enum->getBackedEnumType()); } + public function testIs(): void + { + $className = static::class; + + $reflectionProvider = $this->createReflectionProvider(); + $classReflection = $reflectionProvider->getClass($className); + + $this->assertTrue($classReflection->is($className)); + $this->assertTrue($classReflection->is(PHPStanTestCase::class)); + $this->assertTrue($classReflection->is(TestCase::class)); + $this->assertFalse($classReflection->is(RuleTestCase::class)); + } + + public function dataPropertyHooks(): iterable + { + if (PHP_VERSION_ID < 80400) { + return; + } + + $reflectionProvider = $this->createReflectionProvider(); + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\Foo'), + 'i', + 'set', + ['int'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\Foo'), + 'i', + 'get', + [], + 'int', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\Foo'), + 'l', + 'get', + [], + 'array', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\Foo'), + 'n', + 'set', + ['array|int'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooShort'), + 'i', + 'set', + ['int'], + 'void', + false, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooShort'), + 'k', + 'set', + ['int|string'], + 'void', + false, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooShort'), + 'l', + 'set', + ['array'], + 'void', + false, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooShort'), + 'm', + 'set', + ['array'], + 'void', + false, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooShort'), + 'n', + 'set', + ['array|int'], + 'void', + false, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructor'), + 'i', + 'set', + ['int'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructor'), + 'j', + 'set', + ['int'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructor'), + 'k', + 'set', + ['int|string'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructor'), + 'l', + 'set', + ['array'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructor'), + 'l', + 'get', + [], + 'array', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructor'), + 'm', + 'set', + ['array'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructor'), + 'n', + 'set', + ['array|int'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructorWithParam'), + 'l', + 'set', + ['array'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructorWithParam'), + 'l', + 'get', + [], + 'array', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooConstructorWithParam'), + 'm', + 'set', + ['array'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooGenerics'), + 'm', + 'set', + ['array'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooGenerics'), + 'n', + 'set', + ['array|int'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooGenerics'), + 'm', + 'get', + [], + 'array', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooGenerics'), + 'n', + 'get', + [], + 'int', + true, + ]; + + $specificFooGenerics = (new GenericObjectType('PropertyHooksTypes\\FooGenerics', [new IntegerType()]))->getClassReflection(); + + yield [ + $specificFooGenerics, + 'n', + 'set', + ['array|int'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooGenerics'), + 'n', + 'get', + [], + 'int', + true, + ]; + + yield [ + $specificFooGenerics, + 'm', + 'set', + ['array'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooGenerics'), + 'm', + 'get', + [], + 'array', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooGenericsConstructor'), + 'l', + 'set', + ['array'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooGenericsConstructor'), + 'm', + 'set', + ['array'], + 'void', + true, + ]; + + yield [ + $reflectionProvider->getClass('PropertyHooksTypes\\FooGenericsConstructor'), + 'n', + 'set', + ['array|int'], + 'void', + true, + ]; + + $specificFooGenericsConstructor = (new GenericObjectType('PropertyHooksTypes\\FooGenericsConstructor', [new IntegerType()]))->getClassReflection(); + + yield [ + $specificFooGenericsConstructor, + 'n', + 'set', + ['array|int'], + 'void', + true, + ]; + + yield [ + $specificFooGenericsConstructor, + 'm', + 'set', + ['array'], + 'void', + true, + ]; + + yield [ + $specificFooGenericsConstructor, + 'm', + 'get', + [], + 'array', + true, + ]; + } + + /** + * @dataProvider dataPropertyHooks + * @param ExtendedPropertyReflection::HOOK_* $hookName + * @param string[] $parameterTypes + */ + public function testPropertyHooks( + ClassReflection $classReflection, + string $propertyName, + string $hookName, + array $parameterTypes, + string $returnType, + bool $isVirtual, + ): void + { + $propertyReflection = $classReflection->getNativeProperty($propertyName); + $this->assertSame($isVirtual, $propertyReflection->isVirtual()->yes()); + + $hookReflection = $propertyReflection->getHook($hookName); + $hookVariant = $hookReflection->getOnlyVariant(); + $this->assertSame($returnType, $hookVariant->getReturnType()->describe(VerbosityLevel::precise())); + $this->assertCount(count($parameterTypes), $hookVariant->getParameters()); + + foreach ($hookVariant->getParameters() as $i => $parameter) { + $this->assertSame($parameterTypes[$i], $parameter->getType()->describe(VerbosityLevel::precise())); + } + } + } diff --git a/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php b/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php new file mode 100644 index 0000000000..77d575716e --- /dev/null +++ b/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php @@ -0,0 +1,58 @@ += 80100 ? TrinaryLogic::createYes() : TrinaryLogic::createNo(), + null, + ]; + + yield [ + new Name('\CURLOPT_FTP_SSL'), + TrinaryLogic::createYes(), + 'use CURLOPT_USE_SSL instead.', + ]; + + yield [ + new Name('\DeprecatedConst\FINE'), + TrinaryLogic::createNo(), + null, + ]; + yield [ + new Name('\DeprecatedConst\MY_CONST'), + TrinaryLogic::createYes(), + null, + ]; + yield [ + new Name('\DeprecatedConst\MY_CONST2'), + TrinaryLogic::createYes(), + "don't use it!", + ]; + } + + /** + * @dataProvider dataDeprecatedConstants + */ + public function testDeprecatedConstants(Name $constName, TrinaryLogic $isDeprecated, ?string $deprecationMessage): void + { + require_once __DIR__ . '/data/deprecated-constant.php'; + + $reflectionProvider = $this->createReflectionProvider(); + + $this->assertTrue($reflectionProvider->hasConstant($constName, null)); + $this->assertSame($isDeprecated->describe(), $reflectionProvider->getConstant($constName, null)->isDeprecated()->describe()); + $this->assertSame($deprecationMessage, $reflectionProvider->getConstant($constName, null)->getDeprecatedDescription()); + } + +} diff --git a/tests/PHPStan/Reflection/Constant/data/deprecated-constant.php b/tests/PHPStan/Reflection/Constant/data/deprecated-constant.php new file mode 100644 index 0000000000..9dfc01dea9 --- /dev/null +++ b/tests/PHPStan/Reflection/Constant/data/deprecated-constant.php @@ -0,0 +1,15 @@ +getClass(NotDeprecatedClass::class); + $attributeDeprecatedClass = $reflectionProvider->getClass(AttributeDeprecatedClass::class); + $phpDocDeprecatedClass = $reflectionProvider->getClass(PhpDocDeprecatedClass::class); // @phpstan-ignore classConstant.deprecatedClass + $phpDocDeprecatedClassWithMessages = $reflectionProvider->getClass(PhpDocDeprecatedClassWithMessage::class); // @phpstan-ignore classConstant.deprecatedClass + $attributeDeprecatedClassWithMessages = $reflectionProvider->getClass(AttributeDeprecatedClassWithMessage::class); + $doubleDeprecatedClass = $reflectionProvider->getClass(DoubleDeprecatedClass::class); // @phpstan-ignore classConstant.deprecatedClass + $doubleDeprecatedClassOnlyPhpDocMessage = $reflectionProvider->getClass(DoubleDeprecatedClassOnlyPhpDocMessage::class); // @phpstan-ignore classConstant.deprecatedClass + $doubleDeprecatedClassOnlyAttributeMessage = $reflectionProvider->getClass(DoubleDeprecatedClassOnlyAttributeMessage::class); // @phpstan-ignore classConstant.deprecatedClass + + $notDeprecatedFunction = $reflectionProvider->getFunction(new FullyQualified('CustomDeprecations\\notDeprecatedFunction'), null); + $phpDocDeprecatedFunction = $reflectionProvider->getFunction(new FullyQualified('CustomDeprecations\\phpDocDeprecatedFunction'), null); + $phpDocDeprecatedFunctionWithMessage = $reflectionProvider->getFunction(new FullyQualified('CustomDeprecations\\phpDocDeprecatedFunctionWithMessage'), null); + $attributeDeprecatedFunction = $reflectionProvider->getFunction(new FullyQualified('CustomDeprecations\\attributeDeprecatedFunction'), null); + $attributeDeprecatedFunctionWithMessage = $reflectionProvider->getFunction(new FullyQualified('CustomDeprecations\\attributeDeprecatedFunctionWithMessage'), null); + $doubleDeprecatedFunction = $reflectionProvider->getFunction(new FullyQualified('CustomDeprecations\\doubleDeprecatedFunction'), null); + $doubleDeprecatedFunctionOnlyAttributeMessage = $reflectionProvider->getFunction(new FullyQualified('CustomDeprecations\\doubleDeprecatedFunctionOnlyAttributeMessage'), null); + $doubleDeprecatedFunctionOnlyPhpDocMessage = $reflectionProvider->getFunction(new FullyQualified('CustomDeprecations\\doubleDeprecatedFunctionOnlyPhpDocMessage'), null); + + $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); + + $scopeForNotDeprecatedClass = $scopeFactory->create(ScopeContext::create('dummy.php')->enterClass($notDeprecatedClass)); + $scopeForDeprecatedClass = $scopeFactory->create(ScopeContext::create('dummy.php')->enterClass($attributeDeprecatedClass)); + $scopeForPhpDocDeprecatedClass = $scopeFactory->create(ScopeContext::create('dummy.php')->enterClass($phpDocDeprecatedClass)); + $scopeForPhpDocDeprecatedClassWithMessages = $scopeFactory->create(ScopeContext::create('dummy.php')->enterClass($phpDocDeprecatedClassWithMessages)); + $scopeForAttributeDeprecatedClassWithMessages = $scopeFactory->create(ScopeContext::create('dummy.php')->enterClass($attributeDeprecatedClassWithMessages)); + $scopeForDoubleDeprecatedClass = $scopeFactory->create(ScopeContext::create('dummy.php')->enterClass($doubleDeprecatedClass)); + $scopeForDoubleDeprecatedClassOnlyNativeMessage = $scopeFactory->create(ScopeContext::create('dummy.php')->enterClass($doubleDeprecatedClassOnlyPhpDocMessage)); + $scopeForDoubleDeprecatedClassOnlyCustomMessage = $scopeFactory->create(ScopeContext::create('dummy.php')->enterClass($doubleDeprecatedClassOnlyAttributeMessage)); + + // class + self::assertFalse($notDeprecatedClass->isDeprecated()); + self::assertNull($notDeprecatedClass->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClass->isDeprecated()); + self::assertNull($attributeDeprecatedClass->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClass->isDeprecated()); + self::assertNull($phpDocDeprecatedClass->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClassWithMessages->isDeprecated()); + self::assertSame('phpdoc', $phpDocDeprecatedClassWithMessages->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClassWithMessages->isDeprecated()); + self::assertSame('attribute', $attributeDeprecatedClassWithMessages->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClass->isDeprecated()); + self::assertSame('attribute', $doubleDeprecatedClass->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyPhpDocMessage->isDeprecated()); + self::assertNull($doubleDeprecatedClassOnlyPhpDocMessage->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyAttributeMessage->isDeprecated()); + self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getDeprecatedDescription()); + + // class constants + self::assertFalse($notDeprecatedClass->getConstant('FOO')->isDeprecated()->yes()); + self::assertNull($notDeprecatedClass->getConstant('FOO')->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClass->getConstant('FOO')->isDeprecated()->yes()); + self::assertNull($attributeDeprecatedClass->getConstant('FOO')->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClass->getConstant('FOO')->isDeprecated()->yes()); + self::assertNull($phpDocDeprecatedClass->getConstant('FOO')->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClassWithMessages->getConstant('FOO')->isDeprecated()->yes()); + self::assertSame('phpdoc', $phpDocDeprecatedClassWithMessages->getConstant('FOO')->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClassWithMessages->getConstant('FOO')->isDeprecated()->yes()); + self::assertSame('attribute', $attributeDeprecatedClassWithMessages->getConstant('FOO')->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClass->getConstant('FOO')->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClass->getConstant('FOO')->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyPhpDocMessage->getConstant('FOO')->isDeprecated()->yes()); + self::assertNull($doubleDeprecatedClassOnlyPhpDocMessage->getConstant('FOO')->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyAttributeMessage->getConstant('FOO')->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getConstant('FOO')->getDeprecatedDescription()); + + // properties + self::assertFalse($notDeprecatedClass->getProperty('foo', $scopeForNotDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($notDeprecatedClass->getProperty('foo', $scopeForNotDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClass->getProperty('foo', $scopeForDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($attributeDeprecatedClass->getProperty('foo', $scopeForDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClass->getProperty('foo', $scopeForPhpDocDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($phpDocDeprecatedClass->getProperty('foo', $scopeForPhpDocDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClassWithMessages->getProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('phpdoc', $phpDocDeprecatedClassWithMessages->getProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClassWithMessages->getProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('attribute', $attributeDeprecatedClassWithMessages->getProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClass->getProperty('foo', $scopeForDoubleDeprecatedClass)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClass->getProperty('foo', $scopeForDoubleDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyPhpDocMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->isDeprecated()->yes()); + self::assertNull($doubleDeprecatedClassOnlyPhpDocMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyAttributeMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->getDeprecatedDescription()); + + // methods + self::assertFalse($notDeprecatedClass->getMethod('foo', $scopeForNotDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($notDeprecatedClass->getMethod('foo', $scopeForNotDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClass->getMethod('foo', $scopeForDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($attributeDeprecatedClass->getMethod('foo', $scopeForDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClass->getMethod('foo', $scopeForPhpDocDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($phpDocDeprecatedClass->getMethod('foo', $scopeForPhpDocDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedClassWithMessages->getMethod('foo', $scopeForPhpDocDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('phpdoc', $phpDocDeprecatedClassWithMessages->getMethod('foo', $scopeForPhpDocDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedClassWithMessages->getMethod('foo', $scopeForAttributeDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('attribute', $attributeDeprecatedClassWithMessages->getMethod('foo', $scopeForAttributeDeprecatedClassWithMessages)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClass->getMethod('foo', $scopeForDoubleDeprecatedClass)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClass->getMethod('foo', $scopeForDoubleDeprecatedClass)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyPhpDocMessage->getMethod('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->isDeprecated()->yes()); + self::assertNull($doubleDeprecatedClassOnlyPhpDocMessage->getMethod('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedClassOnlyAttributeMessage->getMethod('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getMethod('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->getDeprecatedDescription()); + + // functions + self::assertFalse($notDeprecatedFunction->isDeprecated()->yes()); + self::assertNull($notDeprecatedFunction->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedFunction->isDeprecated()->yes()); + self::assertNull($phpDocDeprecatedFunction->getDeprecatedDescription()); + + self::assertTrue($phpDocDeprecatedFunctionWithMessage->isDeprecated()->yes()); + self::assertSame('phpdoc', $phpDocDeprecatedFunctionWithMessage->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedFunction->isDeprecated()->yes()); + self::assertNull($attributeDeprecatedFunction->getDeprecatedDescription()); + + self::assertTrue($attributeDeprecatedFunctionWithMessage->isDeprecated()->yes()); + self::assertSame('attribute', $attributeDeprecatedFunctionWithMessage->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedFunction->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedFunction->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedFunctionOnlyPhpDocMessage->isDeprecated()->yes()); + self::assertNull($doubleDeprecatedFunctionOnlyPhpDocMessage->getDeprecatedDescription()); + + self::assertTrue($doubleDeprecatedFunctionOnlyAttributeMessage->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedFunctionOnlyAttributeMessage->getDeprecatedDescription()); + } + + public function testCustomDeprecationsOfEnumCases(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('PHP 8.1+ is required to test enums.'); + } + + require __DIR__ . '/data/deprecations-enums.php'; + + $reflectionProvider = self::createReflectionProvider(); + + $myEnum = $reflectionProvider->getClass(MyDeprecatedEnum::class); + + self::assertTrue($myEnum->isDeprecated()); + self::assertNull($myEnum->getDeprecatedDescription()); + + self::assertTrue($myEnum->getEnumCase('CustomDeprecated')->isDeprecated()->yes()); + self::assertSame('custom', $myEnum->getEnumCase('CustomDeprecated')->getDeprecatedDescription()); + + self::assertTrue($myEnum->getEnumCase('NativeDeprecated')->isDeprecated()->yes()); + self::assertSame('native', $myEnum->getEnumCase('NativeDeprecated')->getDeprecatedDescription()); + + self::assertTrue($myEnum->getEnumCase('PhpDocDeprecated')->isDeprecated()->yes()); + self::assertNull($myEnum->getEnumCase('PhpDocDeprecated')->getDeprecatedDescription()); // this should not be null + + self::assertFalse($myEnum->getEnumCase('NotDeprecated')->isDeprecated()->yes()); + self::assertNull($myEnum->getEnumCase('NotDeprecated')->getDeprecatedDescription()); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/deprecation-provider.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Reflection/Deprecation/data/CustomDeprecated.php b/tests/PHPStan/Reflection/Deprecation/data/CustomDeprecated.php new file mode 100644 index 0000000000..95997bf046 --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/CustomDeprecated.php @@ -0,0 +1,15 @@ += 8.1 + +namespace CustomDeprecations; + +#[\Attribute(\Attribute::TARGET_ALL)] +class CustomDeprecated { + + public ?string $description; + + public function __construct( + ?string $description = null + ) { + $this->description = $description; + } +} diff --git a/tests/PHPStan/Reflection/Deprecation/data/CustomDeprecationExtension.php b/tests/PHPStan/Reflection/Deprecation/data/CustomDeprecationExtension.php new file mode 100644 index 0000000000..dd0ac38c86 --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/CustomDeprecationExtension.php @@ -0,0 +1,82 @@ += 8.0 + +declare(strict_types = 1); + +namespace PHPStan\Tests; + +use CustomDeprecations\CustomDeprecated; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumUnitCase; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; +use PHPStan\BetterReflection\Reflection\ReflectionConstant; +use PHPStan\Reflection\Deprecation\ClassConstantDeprecationExtension; +use PHPStan\Reflection\Deprecation\ClassDeprecationExtension; +use PHPStan\Reflection\Deprecation\ConstantDeprecationExtension; +use PHPStan\Reflection\Deprecation\Deprecation; +use PHPStan\Reflection\Deprecation\EnumCaseDeprecationExtension; +use PHPStan\Reflection\Deprecation\FunctionDeprecationExtension; +use PHPStan\Reflection\Deprecation\MethodDeprecationExtension; +use PHPStan\Reflection\Deprecation\PropertyDeprecationExtension; + +class CustomDeprecationExtension implements + ConstantDeprecationExtension, + ClassDeprecationExtension, + ClassConstantDeprecationExtension, + MethodDeprecationExtension, + PropertyDeprecationExtension, + FunctionDeprecationExtension, + EnumCaseDeprecationExtension +{ + + public function getClassDeprecation(ReflectionClass|ReflectionEnum $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getConstantDeprecation(ReflectionConstant $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getFunctionDeprecation(ReflectionFunction $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getMethodDeprecation(ReflectionMethod $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getPropertyDeprecation(ReflectionProperty $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getClassConstantDeprecation(ReflectionClassConstant $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + public function getEnumCaseDeprecation(ReflectionEnumBackedCase|ReflectionEnumUnitCase $reflection): ?Deprecation + { + return $this->buildDeprecation($reflection); + } + + private function buildDeprecation($reflection): ?Deprecation + { + foreach ($reflection->getAttributes(CustomDeprecated::class) as $attribute) { + $description = $attribute->getArguments()[0] ?? $attribute->getArguments()['description'] ?? null; + return $description === null + ? Deprecation::create() + : Deprecation::createWithDescription($description); + } + + return null; + } +} diff --git a/tests/PHPStan/Reflection/Deprecation/data/deprecation-provider.neon b/tests/PHPStan/Reflection/Deprecation/data/deprecation-provider.neon new file mode 100644 index 0000000000..6b79cfe3f2 --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/deprecation-provider.neon @@ -0,0 +1,11 @@ +services: + - + class: PHPStan\Tests\CustomDeprecationExtension + tags: + - phpstan.propertyDeprecationExtension + - phpstan.methodDeprecationExtension + - phpstan.classConstantDeprecationExtension + - phpstan.classDeprecationExtension + - phpstan.functionDeprecationExtension + - phpstan.constantDeprecationExtension + - phpstan.enumCaseDeprecationExtension diff --git a/tests/PHPStan/Reflection/Deprecation/data/deprecations-enums.php b/tests/PHPStan/Reflection/Deprecation/data/deprecations-enums.php new file mode 100644 index 0000000000..44710f9e9f --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/deprecations-enums.php @@ -0,0 +1,21 @@ += 8.1 + +namespace CustomDeprecations; + +#[CustomDeprecated] +enum MyDeprecatedEnum: string +{ + #[CustomDeprecated('custom')] + case CustomDeprecated = '1'; + + /** + * @deprecated phpdoc + */ + case PhpDocDeprecated = '2'; + + #[\Deprecated('native')] + case NativeDeprecated = '3'; + + case NotDeprecated = '4'; + +} diff --git a/tests/PHPStan/Reflection/Deprecation/data/deprecations.php b/tests/PHPStan/Reflection/Deprecation/data/deprecations.php new file mode 100644 index 0000000000..9df05b1b41 --- /dev/null +++ b/tests/PHPStan/Reflection/Deprecation/data/deprecations.php @@ -0,0 +1,151 @@ += 8.1 + +namespace CustomDeprecations; + +class NotDeprecatedClass +{ + const FOO = 'foo'; + + private $foo; + + public function foo() {} + +} + + +/** @deprecated */ +class PhpDocDeprecatedClass +{ + + /** @deprecated */ + const FOO = 'foo'; + + /** @deprecated */ + private $foo; + + /** @deprecated */ + public function foo() {} + +} +/** @deprecated phpdoc */ +class PhpDocDeprecatedClassWithMessage +{ + + /** @deprecated phpdoc */ + const FOO = 'foo'; + + /** @deprecated phpdoc */ + private $foo; + + /** @deprecated phpdoc */ + public function foo() {} + +} + +#[CustomDeprecated] +class AttributeDeprecatedClass { + #[CustomDeprecated] + public const FOO = 'foo'; + + #[CustomDeprecated] + private $foo; + + #[CustomDeprecated] + public function foo() {} +} + +#[CustomDeprecated('attribute')] +class AttributeDeprecatedClassWithMessage { + #[CustomDeprecated('attribute')] + const FOO = 'foo'; + + #[CustomDeprecated('attribute')] + private $foo; + + #[CustomDeprecated(description: 'attribute')] + public function foo() {} +} + +/** @deprecated phpdoc */ +#[CustomDeprecated('attribute')] +class DoubleDeprecatedClass +{ + + /** @deprecated phpdoc */ + #[CustomDeprecated('attribute')] + const FOO = 'foo'; + + /** @deprecated phpdoc */ + #[CustomDeprecated('attribute')] + private $foo; + + /** @deprecated phpdoc */ + #[CustomDeprecated('attribute')] + public function foo() {} + +} + +/** @deprecated */ +#[CustomDeprecated('attribute')] +class DoubleDeprecatedClassOnlyAttributeMessage +{ + + /** @deprecated */ + #[CustomDeprecated('attribute')] + const FOO = 'foo'; + + /** @deprecated */ + #[CustomDeprecated('attribute')] + private $foo; + + /** @deprecated */ + #[CustomDeprecated('attribute')] + public function foo() {} + +} + +/** @deprecated phpdoc */ +#[CustomDeprecated()] +class DoubleDeprecatedClassOnlyPhpDocMessage +{ + + /** @deprecated phpdoc */ + #[CustomDeprecated()] + const FOO = 'foo'; + + /** @deprecated phpdoc */ + #[CustomDeprecated()] + private $foo; + + /** @deprecated phpdoc */ + #[CustomDeprecated()] + public function foo() {} + +} + + +function notDeprecatedFunction() {} + +/** @deprecated */ +function phpDocDeprecatedFunction() {} + +/** @deprecated phpdoc */ +function phpDocDeprecatedFunctionWithMessage() {} + +#[CustomDeprecated] +function attributeDeprecatedFunction() {} + +#[CustomDeprecated('attribute')] +function attributeDeprecatedFunctionWithMessage() {} + +/** @deprecated phpdoc */ +#[CustomDeprecated('attribute')] +function doubleDeprecatedFunction() {} + +/** @deprecated */ +#[CustomDeprecated('attribute')] +function doubleDeprecatedFunctionOnlyAttributeMessage() {} + +/** @deprecated phpdoc */ +#[CustomDeprecated()] +function doubleDeprecatedFunctionOnlyPhpDocMessage() {} diff --git a/tests/PHPStan/Reflection/FunctionReflectionTest.php b/tests/PHPStan/Reflection/FunctionReflectionTest.php new file mode 100644 index 0000000000..9f0780f113 --- /dev/null +++ b/tests/PHPStan/Reflection/FunctionReflectionTest.php @@ -0,0 +1,209 @@ +createReflectionProvider(); + + $functionReflection = $reflectionProvider->getFunction(new Node\Name($functionName), null); + $this->assertSame($expectedDoc, $functionReflection->getDocComment()); + } + + public function dataPhpdocMethods(): iterable + { + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + '__construct', + '/** construct doc via stub */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + 'aMethod', + '/** some method phpdoc */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + 'noDocMethod', + null, + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + 'docViaStub', + '/** method doc via stub */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + 'existingDocButStubOverridden', + '/** stub overridden phpdoc */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'aMethod', + '/** some method phpdoc */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'noDocMethod', + null, + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'docViaStub', + '/** method doc via stub */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'existingDocButStubOverridden', + '/** stub overridden phpdoc */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'aMethodInheritanceOverridden', + '/** some inheritance overridden method phpdoc */', + ]; + yield [ + '\\DateTime', + '__construct', + '/** php-src native construct stub overridden phpdoc */', + ]; + yield [ + '\\DateTime', + 'modify', + '/** php-src native method stub overridden phpdoc */', + ]; + } + + /** + * @dataProvider dataPhpdocMethods + */ + public function testMethodHasPhpdoc(string $className, string $methodName, ?string $expectedDocComment): void + { + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $scope = $this->createMock(Scope::class); + $scope->method('isInClass')->willReturn(true); + $scope->method('getClassReflection')->willReturn($class); + $scope->method('canAccessProperty')->willReturn(true); + $scope->method('canReadProperty')->willReturn(true); + $scope->method('canWriteProperty')->willReturn(true); + $classReflection = $reflectionProvider->getClass($className); + + $methodReflection = $classReflection->getMethod($methodName, $scope); + $this->assertSame($expectedDocComment, $methodReflection->getDocComment()); + } + + public function dataFunctionReturnsByReference(): iterable + { + yield ['\\implode', TrinaryLogic::createNo()]; + + yield ['ReturnsByReference\\foo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\refFoo', TrinaryLogic::createYes()]; + } + + /** + * @dataProvider dataFunctionReturnsByReference + * @param non-empty-string $functionName + */ + public function testFunctionReturnsByReference(string $functionName, TrinaryLogic $expectedReturnsByRef): void + { + require_once __DIR__ . '/data/returns-by-reference.php'; + + $reflectionProvider = $this->createReflectionProvider(); + + $functionReflection = $reflectionProvider->getFunction(new Node\Name($functionName), null); + $this->assertSame($expectedReturnsByRef, $functionReflection->returnsByReference()); + } + + public function dataMethodReturnsByReference(): iterable + { + yield ['ReturnsByReference\\X', 'foo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\X', 'refFoo', TrinaryLogic::createYes()]; + + yield ['ReturnsByReference\\SubX', 'foo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\SubX', 'refFoo', TrinaryLogic::createYes()]; + yield ['ReturnsByReference\\SubX', 'subRefFoo', TrinaryLogic::createYes()]; + + yield ['ReturnsByReference\\TraitX', 'traitFoo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\TraitX', 'refTraitFoo', TrinaryLogic::createYes()]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield ['ReturnsByReference\\E', 'enumFoo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\E', 'refEnumFoo', TrinaryLogic::createYes()]; + // cases() method cannot be overridden; https://3v4l.org/ebm83 + yield ['ReturnsByReference\\E', 'cases', TrinaryLogic::createNo()]; + } + + /** + * @dataProvider dataMethodReturnsByReference + */ + public function testMethodReturnsByReference(string $className, string $methodName, TrinaryLogic $expectedReturnsByRef): void + { + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $scope = $this->createMock(Scope::class); + $scope->method('isInClass')->willReturn(true); + $scope->method('getClassReflection')->willReturn($class); + $scope->method('canAccessProperty')->willReturn(true); + $scope->method('canReadProperty')->willReturn(true); + $scope->method('canWriteProperty')->willReturn(true); + $classReflection = $reflectionProvider->getClass($className); + + $methodReflection = $classReflection->getMethod($methodName, $scope); + $this->assertSame($expectedReturnsByRef, $methodReflection->returnsByReference()); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/function-reflection.neon', + ]; + } + +} diff --git a/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php b/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php index f75e021960..94306bc0ec 100644 --- a/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php +++ b/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php @@ -14,7 +14,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -22,7 +21,7 @@ use function get_class; use function sprintf; -class GenericParametersAcceptorResolverTest extends PHPStanTestCase +class GenericParametersAcceptorResolverTest extends PHPStanTestCase { /** @@ -30,7 +29,7 @@ class GenericParametersAcceptorResolverTest extends PHPStanTestCase */ public function dataResolve(): array { - $templateType = static fn (string $name, ?Type $type = null): Type => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $type = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, $type, @@ -316,7 +315,11 @@ public function dataResolve(): array ), new DummyParameter( 'b', - new IntegerType(), + new UnionType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ]), false, PassedByReference::createNo(), true, @@ -324,7 +327,11 @@ public function dataResolve(): array ), ], false, - new IntegerType(), + new UnionType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ]), ), ], 'missing args' => [ @@ -405,10 +412,10 @@ public function dataResolve(): array TemplateTypeMap::createEmpty(), null, [ - new DummyParameter('str', new StringType(), false, null, false, null), + new DummyParameter('str', new ConstantStringType('foooooo'), false, null, false, null), ], false, - new StringType(), + new ConstantStringType('foooooo'), ), ], ]; @@ -420,6 +427,7 @@ public function dataResolve(): array */ public function testResolve(array $argTypes, ParametersAcceptor $parametersAcceptor, ParametersAcceptor $expectedResult): void { + self::getContainer(); // to initialize bleeding edge $result = GenericParametersAcceptorResolver::resolve( $argTypes, $parametersAcceptor, @@ -455,4 +463,11 @@ public function testResolve(array $argTypes, ParametersAcceptor $parametersAccep } } + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + } diff --git a/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php b/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php new file mode 100644 index 0000000000..9288f62e8d --- /dev/null +++ b/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php @@ -0,0 +1,129 @@ + new ConstantIntegerType(1), + ConstantIntegerType::class, + ]; + } + + /** + * @dataProvider dataExplicitNever + * + * @param class-string $resultClass + * @param callable(Expr): Type $callback + */ + public function testExplicitNever(Expr $left, Expr $right, callable $callback, string $resultClass, ?bool $resultIsExplicit = null): void + { + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + + $result = $initializerExprTypeResolver->getPlusType( + $left, + $right, + $callback, + ); + $this->assertInstanceOf($resultClass, $result); + + if (!($result instanceof NeverType)) { + return; + } + + if ($resultIsExplicit === null) { + throw new ShouldNotHappenException(); + } + $this->assertSame($resultIsExplicit, $result->isExplicit()); + } + +} diff --git a/tests/PHPStan/Reflection/MixedTypeTest.php b/tests/PHPStan/Reflection/MixedTypeTest.php index 76991c57a9..6ccc847d84 100644 --- a/tests/PHPStan/Reflection/MixedTypeTest.php +++ b/tests/PHPStan/Reflection/MixedTypeTest.php @@ -13,8 +13,8 @@ class MixedTypeTest extends PHPStanTestCase public function testMixedType(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); } $reflectionProvider = $this->createReflectionProvider(); @@ -24,7 +24,7 @@ public function testMixedType(): void $this->assertTrue($propertyType->isExplicitMixed()); $method = $class->getNativeMethod('doFoo'); - $methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + $methodVariant = $method->getOnlyVariant(); $methodReturnType = $methodVariant->getReturnType(); $this->assertInstanceOf(MixedType::class, $methodReturnType); $this->assertTrue($methodReturnType->isExplicitMixed()); @@ -34,7 +34,7 @@ public function testMixedType(): void $this->assertTrue($methodParameterType->isExplicitMixed()); $function = $reflectionProvider->getFunction(new Name('NativeMixedType\doFoo'), null); - $functionVariant = ParametersAcceptorSelector::selectSingle($function->getVariants()); + $functionVariant = $function->getOnlyVariant(); $functionReturnType = $functionVariant->getReturnType(); $this->assertInstanceOf(MixedType::class, $functionReturnType); $this->assertTrue($functionReturnType->isExplicitMixed()); diff --git a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php index 0703fd4902..1f2e534a45 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -9,6 +9,7 @@ use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -18,6 +19,7 @@ use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; @@ -142,35 +144,6 @@ public function dataSelectFromTypes(): Generator ), ]; - $absVariants = $reflectionProvider->getFunction(new Name('abs'), null)->getVariants(); - yield [ - [ - new FloatType(), - new FloatType(), - ], - $absVariants, - false, - ParametersAcceptorSelector::combineAcceptors($absVariants), - ]; - yield [ - [ - new FloatType(), - new IntegerType(), - new StringType(), - ], - $absVariants, - false, - ParametersAcceptorSelector::combineAcceptors($absVariants), - ]; - yield [ - [ - new StringType(), - ], - $absVariants, - false, - $absVariants[2], - ]; - $strtokVariants = $reflectionProvider->getFunction(new Name('strtok'), null)->getVariants(); yield [ [], @@ -194,11 +167,11 @@ public function dataSelectFromTypes(): Generator new StringType(), PassedByReference::createNo(), false, - null, + new NullType(), ), ], false, - new UnionType([new StringType(), new ConstantBooleanType(false)]), + new UnionType([new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), new ConstantBooleanType(false)]), ), ]; yield [ diff --git a/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php index da08f803b7..dabd3948e0 100644 --- a/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; @@ -13,10 +14,11 @@ class UniversalObjectCratesClassReflectionExtensionTest extends PHPStanTestCase public function testNonexistentClass(): void { $reflectionProvider = $this->createReflectionProvider(); - $extension = new UniversalObjectCratesClassReflectionExtension($reflectionProvider, [ - 'NonexistentClass', - 'stdClass', - ]); + $extension = new UniversalObjectCratesClassReflectionExtension( + $reflectionProvider, + ['NonexistentClass', 'stdClass'], + new AnnotationsPropertiesClassReflectionExtension(), + ); $this->assertTrue($extension->hasProperty($reflectionProvider->getClass(stdClass::class), 'foo')); } @@ -25,9 +27,11 @@ public function testDifferentGetSetType(): void require_once __DIR__ . '/data/universal-object-crates.php'; $reflectionProvider = $this->createReflectionProvider(); - $extension = new UniversalObjectCratesClassReflectionExtension($reflectionProvider, [ - 'UniversalObjectCreates\DifferentGetSetTypes', - ]); + $extension = new UniversalObjectCratesClassReflectionExtension( + $reflectionProvider, + ['UniversalObjectCreates\DifferentGetSetTypes'], + new AnnotationsPropertiesClassReflectionExtension(), + ); $this->assertEquals( new ObjectType('UniversalObjectCreates\DifferentGetSetTypesValue'), @@ -43,4 +47,30 @@ public function testDifferentGetSetType(): void ); } + public function testAnnotationOverrides(): void + { + require_once __DIR__ . '/data/universal-object-crates-annotations.php'; + $className = 'UniversalObjectCratesAnnotations\Model'; + + $reflectionProvider = $this->createReflectionProvider(); + $extension = new UniversalObjectCratesClassReflectionExtension( + $reflectionProvider, + [$className], + new AnnotationsPropertiesClassReflectionExtension(), + ); + + $this->assertEquals( + new StringType(), + $extension + ->getProperty($reflectionProvider->getClass($className), 'foo') + ->getReadableType(), + ); + $this->assertEquals( + new StringType(), + $extension + ->getProperty($reflectionProvider->getClass($className), 'foo') + ->getWritableType(), + ); + } + } diff --git a/tests/PHPStan/Reflection/Php/data/universal-object-crates-annotations.php b/tests/PHPStan/Reflection/Php/data/universal-object-crates-annotations.php new file mode 100644 index 0000000000..09cf01b20b --- /dev/null +++ b/tests/PHPStan/Reflection/Php/data/universal-object-crates-annotations.php @@ -0,0 +1,12 @@ +> */ + public static function data(): iterable + { + $inputFile = self::getTestInputFile(); + $contents = file_get_contents($inputFile); + + if ($contents === false) { + self::fail('Input file \'' . $inputFile . '\' is missing.'); + } + + $parts = explode('-----', $contents); + + for ($i = 1; $i + 1 < count($parts); $i += 2) { + $input = trim($parts[$i]); + $output = trim($parts[$i + 1]); + + yield $input => [ + $input, + $output, + ]; + } + } + + /** @dataProvider data */ + public function test(string $input, string $expectedOutput): void + { + $output = self::generateSymbolDescription($input); + $output = trim($output); + $this->assertSame($expectedOutput, $output); + } + + private static function generateSymbolDescription(string $symbol): string + { + [$type, $name] = explode(' ', $symbol); + + if ($name === '') { + throw new ShouldNotHappenException(); + } + + try { + switch ($type) { + case 'FUNCTION': + return self::generateFunctionDescription($name); + case 'CLASS': + return self::generateClassDescription($name); + case 'METHOD': + return self::generateClassMethodDescription($name); + case 'PROPERTY': + return self::generateClassPropertyDescription($name); + default: + self::fail('Unknown symbol type ' . $type); + } + } catch (Throwable $e) { + // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + if ($e instanceof \PHPUnit\Exception) { + throw $e; + } + + // Skip stack trace - it's not fully consistent between dump and test. + return "Generating symbol description failed:\n" + . get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() . "\n"; + } + } + + public static function dumpOutput(): void + { + $symbolsTxt = file_get_contents(self::getPhpSymbolsFile()); + + if ($symbolsTxt === false) { + throw new ShouldNotHappenException('Cannot read phpSymbols.txt'); + } + + $symbols = explode("\n", $symbolsTxt); + $separator = '-----'; + $contents = ''; + + foreach ($symbols as $line) { + $contents .= $separator . "\n"; + $contents .= $line . "\n"; + $contents .= $separator . "\n"; + $contents .= self::generateSymbolDescription($line); + } + + $result = file_put_contents(self::getTestInputFile(), $contents); + + if ($result !== false) { + return; + } + + throw new ShouldNotHappenException('Failed write dump for reflection golden test.'); + } + + private static function getTestInputFile(): string + { + $fileFromEnv = getenv('REFLECTION_GOLDEN_TEST_FILE'); + + if ($fileFromEnv !== false) { + return $fileFromEnv; + } + + $first = (int) floor(PHP_VERSION_ID / 10000); + $second = (int) (floor(PHP_VERSION_ID % 10000) / 100); + $currentVersion = $first . '.' . $second; + + return __DIR__ . '/data/golden/reflection-' . $currentVersion . '.test'; + } + + private static function getPhpSymbolsFile(): string + { + $fileFromEnv = getenv('REFLECTION_GOLDEN_SYMBOLS_FILE'); + + if ($fileFromEnv !== false) { + return $fileFromEnv; + } + + return __DIR__ . '/data/golden/phpSymbols.txt'; + } + + /** + * @param non-empty-string $functionName + */ + private static function generateFunctionDescription(string $functionName): string + { + $nameNode = new Name($functionName); + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasFunction($nameNode, null)) { + return "MISSING\n"; + } + + $functionReflection = $reflectionProvider->getFunction($nameNode, null); + $result = self::generateFunctionMethodBaseDescription($functionReflection); + + if (! $functionReflection->isBuiltin()) { + $result .= "NOT BUILTIN\n"; + } + + $result .= self::generateVariantsDescription($functionReflection->getName(), $functionReflection->getVariants(), false); + $namedArgumentsVariants = $functionReflection->getNamedArgumentsVariants(); + + if ($namedArgumentsVariants !== null) { + $result .= self::generateVariantsDescription($functionReflection->getName(), $namedArgumentsVariants, true); + } + + return $result; + } + + private static function generateClassDescription(string $className): string + { + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasClass($className)) { + return "MISSING\n"; + } + + $result = ''; + $classReflection = $reflectionProvider->getClass($className); + + if ($classReflection->isDeprecated()) { + $result .= "Deprecated\n"; + } + + if (! $classReflection->isBuiltin()) { + $result .= "Not builtin\n"; + } + + if ($classReflection->isInternal()) { + $result .= "Internal\n"; + } + + if ($classReflection->isImmutable()) { + $result .= "Immutable\n"; + } + + if ($classReflection->hasConsistentConstructor()) { + $result .= "Consistent constructor\n"; + } + + $parentReflection = $classReflection->getParentClass(); + $extends = ''; + + if ($parentReflection !== null) { + $extends = ' extends ' . $parentReflection->getName(); + } + + $attributes = []; + + if ($classReflection->allowsDynamicProperties()) { + $attributes[] = "#[AllowDynamicProperties]\n"; + } + + $attributesTxt = implode('', $attributes); + $abstractTxt = $classReflection->isAbstract() + ? 'abstract ' + : ''; + + switch (true) { + case $classReflection->isEnum(): + $keyword = 'enum'; + break; + case $classReflection->isInterface(): + $keyword = 'interface'; + break; + case $classReflection->isTrait(): + $keyword = 'trait'; + break; + case $classReflection->isClass(): + $keyword = 'class'; + break; + default: + $keyword = self::fail(); + } + + $verbosityLevel = VerbosityLevel::precise(); + $backedEnumType = $classReflection->getBackedEnumType(); + $backedEnumTypeTxt = $backedEnumType !== null + ? ': ' . $backedEnumType->describe($verbosityLevel) + : ''; + $readonlyTxt = $classReflection->isReadOnly() + ? 'readonly ' + : ''; + $interfaceNames = array_keys($classReflection->getImmediateInterfaces()); + $implementsTxt = $interfaceNames !== [] + ? ($classReflection->isInterface() ? ' extends ' : ' implements ') . implode(', ', $interfaceNames) + : ''; + $finalTxt = $classReflection->isFinal() + ? 'final ' + : ''; + $result .= $attributesTxt . $finalTxt . $readonlyTxt . $abstractTxt . $keyword . ' ' + . $classReflection->getName() . $extends . $implementsTxt . $backedEnumTypeTxt . "\n"; + $result .= "{\n"; + $ident = ' '; + + foreach (array_keys($classReflection->getTraits()) as $trait) { + $result .= $ident . 'use ' . $trait . ";\n"; + } + + $result .= "}\n"; + + return $result; + } + + private static function generateClassMethodDescription(string $classMethodName): string + { + [$className, $methodName] = explode('::', $classMethodName); + + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasClass($className)) { + return "MISSING\n"; + } + + $classReflection = $reflectionProvider->getClass($className); + + if (! $classReflection->hasNativeMethod($methodName)) { + return "MISSING\n"; + } + + $methodReflection = $classReflection->getNativeMethod($methodName); + $result = self::generateFunctionMethodBaseDescription($methodReflection); + $verbosityLevel = VerbosityLevel::precise(); + + if ($methodReflection->getSelfOutType() !== null) { + $result .= 'Self out type: ' . $methodReflection->getSelfOutType()->describe($verbosityLevel) . "\n"; + } + + if ($methodReflection->isStatic()) { + $result .= "Static\n"; + } + + switch (true) { + case $methodReflection->isPublic(): + $visibility = 'public'; + break; + case $methodReflection->isPrivate(): + $visibility = 'private'; + break; + default: + $visibility = 'protected'; + break; + } + + $result .= 'Visibility: ' . $visibility . "\n"; + $result .= self::generateVariantsDescription($methodReflection->getName(), $methodReflection->getVariants(), false); + $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); + + if ($namedArgumentsVariants !== null) { + $result .= self::generateVariantsDescription($methodReflection->getName(), $namedArgumentsVariants, true); + } + + return $result; + } + + /** @param FunctionReflection|ExtendedMethodReflection $reflection */ + private static function generateFunctionMethodBaseDescription($reflection): string + { + $result = ''; + + if (! $reflection->isDeprecated()->no()) { + $result .= 'Is deprecated: ' . $reflection->isDeprecated()->describe() . "\n"; + } + + if ($reflection instanceof MethodReflection && ! $reflection->isFinal()->no()) { + $result .= 'Is final: ' . $reflection->isFinal()->describe() . "\n"; + } + + if (! $reflection->isInternal()->no()) { + $result .= 'Is internal: ' . $reflection->isInternal()->describe() . "\n"; + } + + if (is_bool($reflection->isBuiltin()) && $reflection->isBuiltin()) { + $result .= 'Is built-in' . "\n"; + } + + if (!is_bool($reflection->isBuiltin()) && !$reflection->isBuiltin()->no()) { + $result .= 'Is built-in: ' . $reflection->isBuiltin()->describe() . "\n"; + } + + if (! $reflection->returnsByReference()->no()) { + $result .= 'Returns by reference: ' . $reflection->returnsByReference()->describe() . "\n"; + } + + if (! $reflection->hasSideEffects()->no()) { + $result .= 'Has side-effects: ' . $reflection->hasSideEffects()->describe() . "\n"; + } + + if ($reflection->getThrowType() !== null) { + $result .= 'Throw type: ' . $reflection->getThrowType()->describe(VerbosityLevel::precise()) . "\n"; + } + + return $result; + } + + /** @param ExtendedParametersAcceptor[] $variants */ + private static function generateVariantsDescription(string $name, array $variants, bool $isNamedArguments): string + { + $variantCount = count($variants); + $result = $isNamedArguments + ? 'Named arguments variants: ' + : 'Variants: '; + $result .= $variantCount . "\n"; + $variantIdent = ' '; + $verbosityLevel = VerbosityLevel::precise(); + + foreach ($variants as $variant) { + $paramsNative = []; + $paramsPhpDoc = []; + + foreach ($variant->getParameters() as $param) { + $paramsPhpDoc[] = $variantIdent . ' * @param ' . $param->getType()->describe($verbosityLevel) . ' $' . $param->getName() . "\n"; + + if ($param->getOutType() !== null) { + $paramsPhpDoc[] = $variantIdent . ' * @param-out ' . $param->getOutType()->describe($verbosityLevel) . ' $' . $param->getName() . "\n"; + } + + $passedByRef = $param->passedByReference(); + + if ($passedByRef->no()) { + $refDes = ''; + } elseif ($passedByRef->createsNewVariable()) { + $refDes = '&rw'; + } else { + $refDes = '&r'; + } + + $variadicDesc = $param->isVariadic() ? '...' : ''; + $defValueDesc = $param->getDefaultValue() !== null + ? ' = ' . $param->getDefaultValue()->describe($verbosityLevel) + : ''; + + $paramsNative[] = $param->getNativeType()->describe($verbosityLevel) . ' ' . $variadicDesc . $refDes . '$' . $param->getName() . $defValueDesc; + } + + $result .= $variantIdent . "/**\n"; + $result .= implode('', $paramsPhpDoc); + $result .= $variantIdent . ' * @return ' . $variant->getReturnType()->describe($verbosityLevel) . "\n"; + $result .= $variantIdent . " */\n"; + $paramsTxt = implode(', ', $paramsNative); + $result .= $variantIdent . 'function ' . $name . '(' . $paramsTxt . '): ' . $variant->getNativeReturnType()->describe($verbosityLevel) . "\n"; + } + + return $result; + } + + private static function generateClassPropertyDescription(string $propertyName): string + { + [$className, $propertyName] = explode('::', $propertyName); + // remove $ + $propertyName = substr($propertyName, 1); + + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasClass($className)) { + return "MISSING\n"; + } + + $classReflection = $reflectionProvider->getClass($className); + + if (! $classReflection->hasNativeProperty($propertyName)) { + return "MISSING\n"; + } + + $result = ''; + $propertyReflection = $classReflection->getNativeProperty($propertyName); + + if (! $propertyReflection->isDeprecated()->no()) { + $result .= 'Is deprecated: ' . $propertyReflection->isDeprecated()->describe() . "\n"; + } + + if (! $propertyReflection->isInternal()->no()) { + $result .= 'Is internal: ' . $propertyReflection->isDeprecated()->describe() . "\n"; + } + + if ($propertyReflection->isStatic()) { + $result .= "Static\n"; + } + + if ($propertyReflection->isReadOnly()) { + $result .= "Readonly\n"; + } + + switch (true) { + case $propertyReflection->isPublic(): + $visibility = 'public'; + break; + case $propertyReflection->isPrivate(): + $visibility = 'private'; + break; + default: + $visibility = 'protected'; + break; + } + + $result .= 'Visibility: ' . $visibility . "\n"; + $verbosityLevel = VerbosityLevel::precise(); + + if ($propertyReflection->isReadable()) { + $result .= 'Read type: ' . $propertyReflection->getReadableType()->describe($verbosityLevel) . "\n"; + } + + if ($propertyReflection->isWritable()) { + $result .= 'Write type: ' . $propertyReflection->getWritableType()->describe($verbosityLevel) . "\n"; + } + + return $result; + } + + public static function dumpInputSymbols(): void + { + $symbols = self::scrapeInputSymbols(); + $symbolsFile = self::getPhpSymbolsFile(); + @mkdir(dirname($symbolsFile), 0777, true); + $result = file_put_contents($symbolsFile, implode("\n", $symbols)); + + if ($result !== false) { + return; + } + + throw new ShouldNotHappenException('Failed write dump for reflection golden test.'); + } + + /** @return list */ + public static function scrapeInputSymbols(): array + { + $result = array_keys( + self::scrapeInputSymbolsFromFunctionMap() + + self::scrapeInputSymbolsFromPhp8Stubs() + + self::scrapeInputSymbolsFromPhpStormStubs() + + self::scrapeInputSymbolsFromReflection(), + ); + sort($result); + + return $result; + } + + /** @return array */ + private static function scrapeInputSymbolsFromFunctionMap(): array + { + $finder = new Finder(); + $files = $finder->files()->name('functionMap*.php')->in(__DIR__ . '/../../../resources'); + $combinedMap = []; + + foreach ($files as $file) { + if ($file->getBasename() === 'functionMap.php') { + $combinedMap += require $file->getPathname(); + continue; + } + + $deltaMap = require $file->getPathname(); + + // Deltas have new/old sections which contain the same format as the base functionMap.php + foreach ($deltaMap as $functionMap) { + $combinedMap += $functionMap; + } + } + + $result = []; + + foreach (array_keys($combinedMap) as $symbol) { + // skip duplicated variants + if (strpos($symbol, "'") !== false) { + continue; + } + + $parts = explode('::', $symbol); + + switch (count($parts)) { + case 1: + $result['FUNCTION ' . $symbol] = true; + break; + case 2: + $result['CLASS ' . $parts[0]] = true; + $result['METHOD ' . $symbol] = true; + break; + default: + throw new ShouldNotHappenException('Invalid symbol ' . $symbol); + } + } + + return $result; + } + + /** @return array */ + private static function scrapeInputSymbolsFromPhp8Stubs(): array + { + // Currently the Php8StubsMap only adds symbols for later versions, so let's max it. + $map = new Php8StubsMap(PHP_INT_MAX); + $files = []; + + foreach (array_merge($map->classes, $map->functions) as $file) { + $files[] = __DIR__ . '/../../../vendor/phpstan/php-8-stubs/' . $file; + } + + return self::scrapeSymbolsFromStubs($files); + } + + /** @return array */ + private static function scrapeInputSymbolsFromPhpStormStubs(): array + { + $files = []; + + foreach (PhpStormStubsMap::CLASSES as $file) { + $files[] = PhpStormStubsMap::DIR . '/' . $file; + } + + return self::scrapeSymbolsFromStubs($files); + } + + /** @return array */ + private static function scrapeInputSymbolsFromReflection(): array + { + $result = []; + + foreach (get_defined_functions()['internal'] as $function) { + $result['FUNCTION ' . $function] = true; + } + + foreach (get_declared_classes() as $class) { + $reflection = new ReflectionClass($class); + + if ($reflection->getFileName() !== false) { + continue; + } + + $className = $reflection->getName(); + $result['CLASS ' . $className] = true; + + foreach ($reflection->getMethods() as $method) { + $result['METHOD ' . $className . '::' . $method->getName()] = true; + } + + foreach ($reflection->getProperties() as $property) { + $result['PROPERTY ' . $className . '::$' . $property->getName()] = true; + } + } + + return $result; + } + + /** + * @param array $stubFiles + * @return array + */ + private static function scrapeSymbolsFromStubs(array $stubFiles): array + { + $parser = self::getContainer()->getService('defaultAnalysisParser'); + self::assertInstanceOf(Parser::class, $parser); + $visitor = new class () extends NodeVisitorAbstract { + + /** @var array */ + public array $symbols = []; + + private Node\Stmt\ClassLike $classLike; + + public function enterNode(Node $node) + { + if ($node instanceof Node\Stmt\ClassLike && $node->namespacedName !== null) { + $this->symbols['CLASS ' . $node->namespacedName->toString()] = true; + $this->classLike = $node; + } + + if ($node instanceof Node\Stmt\ClassMethod && isset($this->classLike->namespacedName)) { + $this->symbols['METHOD ' . $this->classLike->namespacedName->toString() . '::' . $node->name->name] = true; + } + + if ($node instanceof Node\Stmt\PropertyProperty && isset($this->classLike->namespacedName)) { + $this->symbols['PROPERTY ' . $this->classLike->namespacedName->toString() . '::$' . $node->name->toString()] = true; + } + + if ($node instanceof Node\Stmt\Function_) { + $this->symbols['FUNCTION ' . $node->name->name] = true; + } + + return null; + } + + public function leaveNode(Node $node) + { + if ($node instanceof Node\Stmt\ClassLike) { + unset($this->classLike); + } + + return null; + } + + }; + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); + + foreach ($stubFiles as $file) { + $ast = $parser->parseFile($file); + $traverser->traverse($ast); + } + + return $visitor->symbols; + } + +} diff --git a/tests/PHPStan/Reflection/ReflectionProviderTest.php b/tests/PHPStan/Reflection/ReflectionProviderTest.php index d88cac6925..db8896c9cb 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderTest.php @@ -21,12 +21,10 @@ public function dataFunctionThrowType(): iterable null, ]; - if (PHP_VERSION_ID >= 70200) { - yield [ - 'sodium_crypto_kx_keypair', - new ObjectType('SodiumException'), - ]; - } + yield [ + 'sodium_crypto_kx_keypair', + new ObjectType('SodiumException'), + ]; if (PHP_VERSION_ID >= 80000) { yield [ @@ -47,12 +45,14 @@ public function dataFunctionThrowType(): iterable yield [ 'random_int', - new ObjectType('Exception'), + new ObjectType('Random\RandomException'), ]; } /** * @dataProvider dataFunctionThrowType + * + * @param non-empty-string $functionName */ public function testFunctionThrowType(string $functionName, ?Type $expectedThrowType): void { @@ -75,11 +75,11 @@ public function dataFunctionDeprecated(): iterable if (PHP_VERSION_ID < 80000) { yield 'create_function' => [ 'create_function', - PHP_VERSION_ID >= 70200, + true, ]; yield 'each' => [ 'each', - PHP_VERSION_ID >= 70200, + true, ]; } @@ -98,6 +98,8 @@ public function dataFunctionDeprecated(): iterable /** * @dataProvider dataFunctionDeprecated + * + * @param non-empty-string $functionName */ public function testFunctionDeprecated(string $functionName, bool $isDeprecated): void { @@ -112,7 +114,7 @@ public function dataMethodThrowType(): array [ DateTime::class, '__construct', - new ObjectType('Exception'), + PHP_VERSION_ID >= 80300 ? new ObjectType('DateMalformedStringException') : new ObjectType('Exception'), ], [ DateTime::class, @@ -142,4 +144,18 @@ public function testMethodThrowType(string $className, string $methodName, ?Type ); } + public function testNativeClassConstantTypeInEvaledClass(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + eval('namespace NativeClassConstantInEvaledClass; class Foo { public const int FOO = 1; }'); + + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass('NativeClassConstantInEvaledClass\\Foo'); + $constant = $class->getConstant('FOO'); + $this->assertSame('int', $constant->getValueType()->describe(VerbosityLevel::precise())); + } + } diff --git a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php index 1fa92f8ee7..182a6582e9 100644 --- a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -2,20 +2,29 @@ namespace PHPStan\Reflection\SignatureMap; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\Php\PhpVersion; use PHPStan\Php8StubsMap; use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; +use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; @@ -28,6 +37,7 @@ use function array_map; use function array_merge; use function count; +use const PHP_VERSION_ID; class Php8SignatureMapProviderTest extends PHPStanTestCase { @@ -53,7 +63,7 @@ public function dataFunctions(): array 'variadic' => false, ], ], - new UnionType([ + new BenevolentUnionType([ new ObjectType('CurlHandle'), new ConstantBooleanType(false), ]), @@ -90,15 +100,15 @@ public function dataFunctions(): array new ConstantStringType('error_count'), new ConstantStringType('errors'), ], [ - new IntegerType(), - new ArrayType(new IntegerType(), new StringType()), - new IntegerType(), - new ArrayType(new IntegerType(), new StringType()), + IntegerRangeType::fromInterval(0, null), + new IntersectionType([new ArrayType(IntegerRangeType::fromInterval(0, null), new StringType()), new AccessoryArrayListType()]), + IntegerRangeType::fromInterval(0, null), + new IntersectionType([new ArrayType(IntegerRangeType::fromInterval(0, null), new StringType()), new AccessoryArrayListType()]), ]), ]), new UnionType([ new ConstantBooleanType(false), - new ArrayType(new MixedType(true), new MixedType(true)), + new ArrayType(new MixedType(), new MixedType()), ]), false, ], @@ -134,19 +144,28 @@ public function testFunctions( ): void { $provider = $this->createProvider(); - $signature = $provider->getFunctionSignature($functionName, null); - $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signature); + $reflector = self::getContainer()->getByType(Reflector::class); + $signatures = $provider->getFunctionSignatures($functionName, null, new ReflectionFunction($reflector->reflectFunction($functionName)))['positional']; + $this->assertCount(1, $signatures); + $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signatures[0]); } private function createProvider(): Php8SignatureMapProvider { + $phpVersion = new PhpVersion(80000); + return new Php8SignatureMapProvider( new FunctionSignatureMapProvider( self::getContainer()->getByType(SignatureMapParser::class), - new PhpVersion(80000), + self::getContainer()->getByType(InitializerExprTypeResolver::class), + $phpVersion, + true, ), self::getContainer()->getByType(FileNodesFetcher::class), self::getContainer()->getByType(FileTypeMapper::class), + $phpVersion, + self::getContainer()->getByType(InitializerExprTypeResolver::class), + self::getContainer()->getByType(ReflectionProviderProvider::class), ); } @@ -176,7 +195,8 @@ public function dataMethods(): array 'optional' => true, 'type' => new UnionType([ new ObjectWithoutClassType(), - new StringType(), + new ClassStringType(), + new ConstantStringType('static'), new NullType(), ]), 'nativeType' => new UnionType([ @@ -188,7 +208,7 @@ public function dataMethods(): array 'variadic' => false, ], ], - new UnionType([ + new BenevolentUnionType([ new ObjectType('Closure'), new NullType(), ]), @@ -255,8 +275,9 @@ public function testMethods( ): void { $provider = $this->createProvider(); - $signature = $provider->getMethodSignature($className, $methodName, null); - $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signature); + $signatures = $provider->getMethodSignatures($className, $methodName, null)['positional']; + $this->assertCount(1, $signatures); + $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signatures[0]); } /** @@ -288,7 +309,8 @@ private function assertSignature( public function dataParseAll(): array { - return array_map(static fn (string $file): array => [__DIR__ . '/../../../../vendor/phpstan/php-8-stubs/' . $file], array_merge(Php8StubsMap::CLASSES, Php8StubsMap::FUNCTIONS)); + $map = new Php8StubsMap(PHP_VERSION_ID); + return array_map(static fn (string $file): array => [__DIR__ . '/../../../../vendor/phpstan/php-8-stubs/' . $file], array_merge($map->classes, $map->functions)); } /** diff --git a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php index 0f00bd56f2..00ed1a4159 100644 --- a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php @@ -4,9 +4,16 @@ use DateInterval; use DateTime; +use OutOfBoundsException; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Parser\ParserException; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\PassedByReference; +use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; @@ -48,6 +55,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( 'fields', @@ -56,6 +65,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( 'delimiter', @@ -64,6 +75,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( 'enclosure', @@ -72,6 +85,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( 'escape_char', @@ -80,6 +95,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), ], new IntegerType(), @@ -99,6 +116,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), ], new BooleanType(), @@ -118,6 +137,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createReadsArgument(), false, + null, + null, ), ], new BooleanType(), @@ -140,6 +161,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( 'out', @@ -148,6 +171,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createCreatesNewVariable(), false, + null, + null, ), new ParameterSignature( 'notext', @@ -156,6 +181,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), ], new BooleanType(), @@ -199,6 +226,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( 'arr2', @@ -207,6 +236,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( '...', @@ -215,6 +246,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), true, + null, + null, ), ], new ArrayType(new MixedType(), new MixedType()), @@ -234,6 +267,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( 'event', @@ -242,6 +277,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( '...', @@ -250,6 +287,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), true, + null, + null, ), ], new ResourceType(), @@ -269,6 +308,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( 'args', @@ -277,6 +318,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), true, + null, + null, ), ], new StringType(), @@ -296,6 +339,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), new ParameterSignature( 'args', @@ -304,6 +349,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), true, + null, + null, ), ], new StringType(), @@ -333,6 +380,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createNo(), false, + null, + null, ), ], new StaticType($reflectionProvider->getClass(DateTime::class)), @@ -352,6 +401,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createReadsArgument(), false, + null, + null, ), new ParameterSignature( 'strings', @@ -360,6 +411,8 @@ public function dataGetFunctions(): array new MixedType(), PassedByReference::createReadsArgument(), true, + null, + null, ), ], new BooleanType(), @@ -443,8 +496,9 @@ public function dataParseAll(): array public function testParseAll(int $phpVersionId): void { $parser = self::getContainer()->getByType(SignatureMapParser::class); - $provider = new FunctionSignatureMapProvider($parser, new PhpVersion($phpVersionId)); + $provider = new FunctionSignatureMapProvider($parser, self::getContainer()->getByType(InitializerExprTypeResolver::class), new PhpVersion($phpVersionId), true); $signatureMap = $provider->getSignatureMap(); + $reflector = self::getContainer()->getByType(Reflector::class); $count = 0; foreach (array_keys($signatureMap) as $functionName) { @@ -452,24 +506,52 @@ public function testParseAll(int $phpVersionId): void if (strpos($functionName, '::') !== false) { $parts = explode('::', $functionName); $className = $parts[0]; + $realFunctionName = $parts[1]; + } else { + $realFunctionName = $functionName; + } + + if (strpos($realFunctionName, "'") !== false) { + continue; + } + + if ($realFunctionName === '') { + throw new ShouldNotHappenException(); + } + + $reflectionFunction = null; + + try { + if ($className !== null) { + $method = $reflector->reflectClass($className)->getMethod($realFunctionName); + if ($method !== null) { + $reflectionFunction = new ReflectionMethod($method); + } + } else { + $reflectionFunction = new ReflectionFunction($reflector->reflectFunction($realFunctionName)); + } + } catch (IdentifierNotFound | OutOfBoundsException $e) { + // pass } try { - $signature = $provider->getFunctionSignature($functionName, $className); - $count++; + $signatures = $provider->getFunctionSignatures($functionName, $className, $reflectionFunction)['positional']; + $count += count($signatures); } catch (ParserException $e) { $this->fail(sprintf('Could not parse %s: %s.', $functionName, $e->getMessage())); } - self::assertNotInstanceOf(ErrorType::class, $signature->getReturnType(), $functionName); - $optionalOcurred = false; - foreach ($signature->getParameters() as $parameter) { - if ($parameter->isOptional()) { - $optionalOcurred = true; - } elseif ($optionalOcurred) { - $this->fail(sprintf('%s contains required parameter after optional.', $functionName)); + foreach ($signatures as $signature) { + self::assertNotInstanceOf(ErrorType::class, $signature->getReturnType(), $functionName); + $optionalOcurred = false; + foreach ($signature->getParameters() as $parameter) { + if ($parameter->isOptional()) { + $optionalOcurred = true; + } elseif ($optionalOcurred) { + $this->fail(sprintf('%s contains required parameter after optional.', $functionName)); + } + self::assertNotInstanceOf(ErrorType::class, $parameter->getType(), sprintf('%s (parameter %s)', $functionName, $parameter->getName())); } - self::assertNotInstanceOf(ErrorType::class, $parameter->getType(), sprintf('%s (parameter %s)', $functionName, $parameter->getName())); } } diff --git a/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php index f6c315b219..2d7d3ca357 100644 --- a/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php +++ b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; @@ -36,9 +36,9 @@ public function testMultipleDeprecationsAreJoined(): void $this->assertSame('Deprecated #1 Deprecated #2', $reflection->getDeprecatedDescription()); } - private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): MethodReflection + private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): ExtendedMethodReflection { - $method = $this->createMock(MethodReflection::class); + $method = $this->createMock(ExtendedMethodReflection::class); $method->method('isDeprecated')->willReturn($deprecated); $method->method('getDeprecatedDescription')->willReturn($deprecationText); return $method; diff --git a/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php b/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php index 9df28a8e83..b41d8d9636 100644 --- a/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php +++ b/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; @@ -36,9 +36,9 @@ public function testMultipleDeprecationsAreJoined(): void $this->assertSame('Deprecated #1 Deprecated #2', $reflection->getDeprecatedDescription()); } - private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): MethodReflection + private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): ExtendedMethodReflection { - $method = $this->createMock(MethodReflection::class); + $method = $this->createMock(ExtendedMethodReflection::class); $method->method('isDeprecated')->willReturn($deprecated); $method->method('getDeprecatedDescription')->willReturn($deprecationText); return $method; diff --git a/tests/PHPStan/Reflection/UnionTypesTest.php b/tests/PHPStan/Reflection/UnionTypesTest.php index ca150ccbe4..79fb96b28a 100644 --- a/tests/PHPStan/Reflection/UnionTypesTest.php +++ b/tests/PHPStan/Reflection/UnionTypesTest.php @@ -7,17 +7,12 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -use const PHP_VERSION_ID; class UnionTypesTest extends PHPStanTestCase { public function testUnionTypes(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - require_once __DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'; $reflectionProvider = $this->createReflectionProvider(); @@ -27,7 +22,7 @@ public function testUnionTypes(): void $this->assertSame('bool|int', $propertyType->describe(VerbosityLevel::precise())); $method = $class->getNativeMethod('doFoo'); - $methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + $methodVariant = $method->getOnlyVariant(); $methodReturnType = $methodVariant->getReturnType(); $this->assertInstanceOf(UnionType::class, $methodReturnType); $this->assertSame('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $methodReturnType->describe(VerbosityLevel::precise())); @@ -37,7 +32,7 @@ public function testUnionTypes(): void $this->assertSame('bool|int', $methodParameterType->describe(VerbosityLevel::precise())); $function = $reflectionProvider->getFunction(new Name('NativeUnionTypes\doFoo'), null); - $functionVariant = ParametersAcceptorSelector::selectSingle($function->getVariants()); + $functionVariant = $function->getOnlyVariant(); $functionReturnType = $functionVariant->getReturnType(); $this->assertInstanceOf(UnionType::class, $functionReturnType); $this->assertSame('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $functionReturnType->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Reflection/data/ClassWithInheritedPhpdoc.php b/tests/PHPStan/Reflection/data/ClassWithInheritedPhpdoc.php new file mode 100644 index 0000000000..e4a0c5e696 --- /dev/null +++ b/tests/PHPStan/Reflection/data/ClassWithInheritedPhpdoc.php @@ -0,0 +1,10 @@ +getName() === 'AllowedSubTypesClassReflectionExtensionTest\\Foo'; + } + + public function getAllowedSubTypes(ClassReflection $classReflection): array + { + return [ + new ObjectType('AllowedSubTypesClassReflectionExtensionTest\\Bar'), + new ObjectType('AllowedSubTypesClassReflectionExtensionTest\\Baz'), + new ObjectType('AllowedSubTypesClassReflectionExtensionTest\\Qux'), + ]; + } +} + +function acceptsFoo(Foo $foo): void { + assertType('AllowedSubTypesClassReflectionExtensionTest\\Foo', $foo); + + if ($foo instanceof Bar) { + return; + } + + assertType('AllowedSubTypesClassReflectionExtensionTest\\Foo~AllowedSubTypesClassReflectionExtensionTest\\Bar', $foo); + + if ($foo instanceof Qux) { + return; + } + + assertType('AllowedSubTypesClassReflectionExtensionTest\\Baz', $foo); +} diff --git a/tests/PHPStan/Reflection/data/anonymous-classes.php b/tests/PHPStan/Reflection/data/anonymous-classes.php new file mode 100644 index 0000000000..4024445aec --- /dev/null +++ b/tests/PHPStan/Reflection/data/anonymous-classes.php @@ -0,0 +1,33 @@ += 8.1 + +namespace AttributeReflectionTest; + +enum FooEnum +{ + + #[MyAttr(one: 15, two: 16)] + case TEST; + +} diff --git a/tests/PHPStan/Reflection/data/attribute-reflection.php b/tests/PHPStan/Reflection/data/attribute-reflection.php new file mode 100644 index 0000000000..34ec36599f --- /dev/null +++ b/tests/PHPStan/Reflection/data/attribute-reflection.php @@ -0,0 +1,62 @@ += 8.0 + +namespace AttributeReflectionTest; + +use Attribute; + +#[Attribute] +class MyAttr +{ + + public function __construct($one, $two) + { + + } + +} + +#[MyAttr(1, 2)] +class Foo +{ + + #[MyAttr(one: 3, two: 4)] + public const MY_CONST = 1; + + #[MyAttr(two: 6, one: 5)] + private $prop; + + #[MyAttr(7, 8)] + public function __construct( + #[MyAttr(9, 10)] + int $test + ) + { + + } + +} + +#[MyAttr()] +function myFunction() { + +} + +#[Nonexistent()] +function myFunction2() { + +} + +#[Nonexistent(1, 2)] +function myFunction3() { + +} + +#[MyAttr(11, 12)] +function myFunction4() { + +} + +#[MyAttr(28, two: 29)] +function myFunction5() { + +} diff --git a/tests/PHPStan/Reflection/data/function-reflection.neon b/tests/PHPStan/Reflection/data/function-reflection.neon new file mode 100644 index 0000000000..22af4a6bdf --- /dev/null +++ b/tests/PHPStan/Reflection/data/function-reflection.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - function-reflection.stub diff --git a/tests/PHPStan/Reflection/data/function-reflection.stub b/tests/PHPStan/Reflection/data/function-reflection.stub new file mode 100644 index 0000000000..8e4ed01324 --- /dev/null +++ b/tests/PHPStan/Reflection/data/function-reflection.stub @@ -0,0 +1,48 @@ += 8.1 + +namespace ReturnsByReference; + +enum E { + case E1; + + function enumFoo() {} + + function &refEnumFoo() {} +} diff --git a/tests/PHPStan/Reflection/data/returns-by-reference.php b/tests/PHPStan/Reflection/data/returns-by-reference.php new file mode 100644 index 0000000000..26fb1051b3 --- /dev/null +++ b/tests/PHPStan/Reflection/data/returns-by-reference.php @@ -0,0 +1,28 @@ + @@ -26,7 +27,19 @@ public function processNode(Node $node, Scope $scope): array return []; } - return ['Fail.']; + if (count($node->getArgs()) === 1 && $node->getArgs()[0]->value instanceof Node\Scalar\String_) { + return [ + RuleErrorBuilder::message($node->getArgs()[0]->value->value) + ->identifier('tests.alwaysFail') + ->build(), + ]; + } + + return [ + RuleErrorBuilder::message('Fail.') + ->identifier('tests.alwaysFail') + ->build(), + ]; } } diff --git a/tests/PHPStan/Rules/Api/ApiClassConstFetchRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassConstFetchRuleTest.php new file mode 100644 index 0000000000..f5a0b797ab --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiClassConstFetchRuleTest.php @@ -0,0 +1,46 @@ + + */ +class ApiClassConstFetchRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ApiClassConstFetchRule(new ApiRuleHelper(), $this->createReflectionProvider()); + } + + public function testRuleInPhpStan(): void + { + $this->analyse([__DIR__ . '/data/class-const-fetch-in-phpstan.php'], []); + } + + 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', + ); + + $this->analyse([__DIR__ . '/data/class-const-fetch-out-of-phpstan.php'], [ + [ + 'Accessing PHPStan\Command\AnalyseCommand::class is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', + 16, + $tip, + ], + [ + 'Accessing PHPStan\Analyser\NodeScopeResolver::FOO is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', + 20, + $tip, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index 127fcd7877..e14d95f33e 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -32,12 +32,32 @@ public function testRuleOutOfPhpStan(): void $this->analyse([__DIR__ . '/data/class-implements-out-of-phpstan.php'], [ [ 'Implementing PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 17, + 20, $tip, ], [ 'Implementing PHPStan\Type\Type is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 51, + 54, + $tip, + ], + [ + 'Implementing PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 333, + $tip, + ], + [ + 'Implementing PHPStan\Analyser\Scope is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 338, + $tip, + ], + [ + 'Implementing PHPStan\Reflection\FunctionReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 343, + $tip, + ], + [ + 'Implementing PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 347, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/ApiInstanceofRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstanceofRuleTest.php new file mode 100644 index 0000000000..a22b102c63 --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiInstanceofRuleTest.php @@ -0,0 +1,55 @@ + + */ +class ApiInstanceofRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ApiInstanceofRule(new ApiRuleHelper(), $this->createReflectionProvider()); + } + + public function testRuleInPhpStan(): void + { + $this->analyse([__DIR__ . '/data/instanceof-in-phpstan.php'], []); + } + + 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', + ); + $instanceofTip = sprintf( + "In case of questions how to solve this correctly, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ); + + $this->analyse([__DIR__ . '/data/instanceof-out-of-phpstan.php'], [ + [ + 'Although PHPStan\Reflection\ClassReflection is covered by backward compatibility promise, this instanceof assumption might break because it\'s not guaranteed to always stay the same.', + 17, + $instanceofTip, + ], + [ + 'Asking about instanceof PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', + 21, + $tip, + ], + [ + 'Although PHPStan\Reflection\ClassReflection is covered by backward compatibility promise, this instanceof assumption might break because it\'s not guaranteed to always stay the same.', + 41, + $instanceofTip, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php new file mode 100644 index 0000000000..e12f8aaf14 --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php @@ -0,0 +1,46 @@ + + */ +class ApiInstanceofTypeRuleTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new ApiInstanceofTypeRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $tipText = 'Learn more: https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated'; + $this->analyse([__DIR__ . '/data/instanceof-type.php'], [ + [ + 'Doing instanceof PHPStan\Type\TypeWithClassName is error-prone and deprecated. Use Type::getObjectClassNames() or Type::getObjectClassReflections() instead.', + 20, + $tipText, + ], + [ + 'Doing instanceof phpstan\type\typewithclassname is error-prone and deprecated. Use Type::getObjectClassNames() or Type::getObjectClassReflections() instead.', + 24, + $tipText, + ], + [ + 'Doing instanceof PHPStan\Type\TypeWithClassName is error-prone and deprecated. Use Type::getObjectClassNames() or Type::getObjectClassReflections() instead.', + 36, + $tipText, + ], + [ + 'Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated.', + 40, + $tipText, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php index db20d90013..770c59da3a 100644 --- a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php @@ -32,7 +32,22 @@ public function testRuleOutOfPhpStan(): void $this->analyse([__DIR__ . '/data/interface-extends-out-of-phpstan.php'], [ [ 'Extending PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 8, + 10, + $tip, + ], + [ + 'Extending PHPStan\Type\Type is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 20, + $tip, + ], + [ + 'Extending PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 25, + $tip, + ], + [ + 'Extending PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 30, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php new file mode 100644 index 0000000000..c65e8a6fd4 --- /dev/null +++ b/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class GetTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new GetTemplateTypeRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/get-template-type.php'], [ + [ + 'Call to PHPStan\Type\Type::getTemplateType() references unknown template type TSendd on class Generator.', + 15, + ], + [ + 'Call to PHPStan\Type\ObjectType::getTemplateType() references unknown template type TSendd on class Generator.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleTest.php b/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleTest.php new file mode 100644 index 0000000000..c7fe99bc18 --- /dev/null +++ b/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleTest.php @@ -0,0 +1,30 @@ + + */ +class NodeConnectingVisitorAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NodeConnectingVisitorAttributesRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/node-connecting-visitor.php'], [ + [ + 'Node attribute \'parent\' is no longer available.', + 18, + 'See: https://phpstan.org/blog/preprocessing-ast-for-custom-rules', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/OldPhpParser4ClassRuleTest.php b/tests/PHPStan/Rules/Api/OldPhpParser4ClassRuleTest.php new file mode 100644 index 0000000000..23892389dd --- /dev/null +++ b/tests/PHPStan/Rules/Api/OldPhpParser4ClassRuleTest.php @@ -0,0 +1,29 @@ + + */ +class OldPhpParser4ClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new OldPhpParser4ClassRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/old-php-parser-4-class.php'], [ + [ + 'Class PhpParser\Node\Expr\ArrayItem not found. It has been renamed to PhpParser\Node\ArrayItem in PHP-Parser v5.', + 24, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php b/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php new file mode 100644 index 0000000000..f043e062fc --- /dev/null +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php @@ -0,0 +1,45 @@ + + */ +class RuntimeReflectionFunctionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RuntimeReflectionFunctionRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/runtime-reflection-function.php'], [ + [ + 'Function is_a() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 43, + ], + [ + 'Function is_subclass_of() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 46, + ], + [ + 'Function class_parents() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 49, + ], + [ + 'Function class_implements() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 50, + ], + [ + 'Function class_uses() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 51, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php b/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php new file mode 100644 index 0000000000..ff9861230f --- /dev/null +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php @@ -0,0 +1,83 @@ + + */ +class RuntimeReflectionInstantiationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RuntimeReflectionInstantiationRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $errors = [ + [ + 'Creating new ReflectionMethod is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 43, + ], + [ + 'Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 44, + ], + [ + 'Creating new ReflectionClassConstant is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 45, + ], + [ + 'Creating new ReflectionZendExtension is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 48, + ], + [ + 'Creating new ReflectionExtension is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 49, + ], + [ + 'Creating new ReflectionFunction is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 50, + ], + [ + 'Creating new ReflectionObject is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 51, + ], + [ + 'Creating new ReflectionParameter is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 52, + ], + [ + 'Creating new ReflectionProperty is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 53, + ], + [ + 'Creating new ReflectionGenerator is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 54, + ], + ]; + if (PHP_VERSION_ID >= 80100) { + $errors[] = [ + 'Creating new ReflectionFiber is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 55, + ]; + } + if (PHP_VERSION_ID >= 80000) { + $errors[] = [ + 'Creating new ReflectionEnum is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 56, + ]; + $errors[] = [ + 'Creating new ReflectionEnumBackedCase is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 57, + ]; + } + $this->analyse([__DIR__ . '/data/runtime-reflection-instantiation.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/class-const-fetch-in-phpstan.php b/tests/PHPStan/Rules/Api/data/class-const-fetch-in-phpstan.php new file mode 100644 index 0000000000..2879583dad --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/class-const-fetch-in-phpstan.php @@ -0,0 +1,17 @@ +getTemplateType(Generator::class, 'TSend'); + $type->getTemplateType(Generator::class, 'TSendd'); + } + + public function doBar(ObjectType $type): void + { + $type->getTemplateType(Generator::class, 'TSend'); + $type->getTemplateType(Generator::class, 'TSendd'); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/instanceof-in-phpstan.php b/tests/PHPStan/Rules/Api/data/instanceof-in-phpstan.php new file mode 100644 index 0000000000..bb1ca7a370 --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/instanceof-in-phpstan.php @@ -0,0 +1,22 @@ +getFunction(); + if ($function instanceof MethodReflection) { + + } + } + + public function doBaz($mixed): void + { + if ($mixed instanceof Type) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Api/data/instanceof-type.php b/tests/PHPStan/Rules/Api/data/instanceof-type.php new file mode 100644 index 0000000000..eabc5f6c5e --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/instanceof-type.php @@ -0,0 +1,45 @@ +getAttribute("parent"); + $custom = $node->getAttribute("myCustomAttribute"); + + return []; + } + +} + +class Foo +{ + + public function doFoo(Node $node): void + { + $parent = $node->getAttribute("parent"); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/old-php-parser-4-class.php b/tests/PHPStan/Rules/Api/data/old-php-parser-4-class.php new file mode 100644 index 0000000000..f9f017054f --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/old-php-parser-4-class.php @@ -0,0 +1,32 @@ +getVariants()); // @api above class + ParametersAcceptorSelector::selectFromArgs($f->getVariants()); // @api above class ScopeContext::create(__DIR__ . '/test.php'); // @api above method } diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php deleted file mode 100644 index 75dcc16350..0000000000 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php +++ /dev/null @@ -1,65 +0,0 @@ - - */ -class AppendedArrayItemTypeRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new AppendedArrayItemTypeRule( - new PropertyReflectionFinder(), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), - ); - } - - public function testAppendedArrayItemType(): void - { - $this->analyse( - [__DIR__ . '/data/appended-array-item.php'], - [ - [ - 'Array (array) does not accept string.', - 18, - ], - [ - 'Array (array) does not accept array{1, 2, 3}.', - 20, - ], - [ - 'Array (array) does not accept array{\'AppendedArrayItem\\\\Foo\', \'classMethod\'}.', - 23, - ], - [ - 'Array (array) does not accept array{\'Foo\', \'Hello world\'}.', - 25, - ], - [ - 'Array (array) does not accept string.', - 27, - ], - [ - 'Array (array) does not accept string.', - 32, - ], - [ - 'Array (array) does not accept Closure(): 1.', - 45, - ], - [ - 'Array (array) does not accept AppendedArrayItem\Baz.', - 79, - ], - ], - ); - } - -} diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php deleted file mode 100644 index d74a1ddd8e..0000000000 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php +++ /dev/null @@ -1,71 +0,0 @@ - - */ -class AppendedArrayKeyTypeRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new AppendedArrayKeyTypeRule( - new PropertyReflectionFinder(), - true, - ); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/appended-array-key.php'], [ - [ - 'Array (array) does not accept key int|string.', - 28, - ], - [ - 'Array (array) does not accept key string.', - 30, - ], - [ - 'Array (array) does not accept key int.', - 31, - ], - [ - 'Array (array) does not accept key int|string.', - 33, - ], - [ - 'Array (array) does not accept key 0.', - 38, - ], - [ - '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 5970a3e8f6..3027ce23b7 100644 --- a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php @@ -15,11 +15,11 @@ class ArrayDestructuringRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true); return new ArrayDestructuringRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, false, false), ); } diff --git a/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php new file mode 100644 index 0000000000..0991d178cc --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php @@ -0,0 +1,140 @@ + + */ +class ArrayUnpackingRuleTest extends RuleTestCase +{ + + private bool $checkUnions; + + private bool $checkBenevolentUnions = false; + + protected function getRule(): Rule + { + return new ArrayUnpackingRule( + self::getContainer()->getByType(PhpVersion::class), + new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnions, false, false, $this->checkBenevolentUnions, true), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID >= 80100) { + $this->markTestSkipped('Test requires PHP version <= 8.0'); + } + + $this->checkUnions = true; + $this->checkBenevolentUnions = true; + $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ + [ + 'Array unpacking cannot be used on an array with potential string keys: array{foo: \'bar\', 0: 1, 1: 2, 2: 3}', + 7, + ], + [ + 'Array unpacking cannot be used on an array with string keys: array', + 18, + ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 24, + ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 29, + ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 40, + ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 52, + ], + [ + 'Array unpacking cannot be used on an array with string keys: array{foo: string, bar: int}', + 63, + ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 71, + ], + ]); + } + + public function testRuleDoNotCheckBenevolentUnion(): void + { + if (PHP_VERSION_ID >= 80100) { + $this->markTestSkipped('Test requires PHP version <= 8.0'); + } + + $this->checkUnions = true; + $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ + [ + 'Array unpacking cannot be used on an array with potential string keys: array{foo: \'bar\', 0: 1, 1: 2, 2: 3}', + 7, + ], + [ + 'Array unpacking cannot be used on an array with string keys: array', + 18, + ], + [ + 'Array unpacking cannot be used on an array with string keys: array{foo: string, bar: int}', + 63, + ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 71, + ], + ]); + } + + public function testRuleDoNotCheckUnions(): void + { + if (PHP_VERSION_ID >= 80100) { + $this->markTestSkipped('Test requires PHP version <= 8.0'); + } + + $this->checkUnions = false; + $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ + [ + 'Array unpacking cannot be used on an array with string keys: array', + 18, + ], + [ + 'Array unpacking cannot be used on an array with string keys: array{foo: string, bar: int}', + 63, + ], + ]); + } + + public function dataRuleOnPHP81(): array + { + return [ + [true], + [false], + ]; + } + + /** + * @dataProvider dataRuleOnPHP81 + */ + public function testRuleOnPHP81(bool $checkUnions): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1+'); + } + + $this->checkUnions = $checkUnions; + $this->analyse([__DIR__ . '/data/array-unpacking.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php index a570343822..a99a20557c 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -30,4 +30,14 @@ public function testRule(): void ]); } + public function testBug7913(): void + { + $this->analyse([__DIR__ . '/data/bug-7913.php'], []); + } + + public function testBug8292(): void + { + $this->analyse([__DIR__ . '/data/bug-8292.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php index 6877f37644..87b5a12e5f 100644 --- a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php @@ -2,7 +2,8 @@ namespace PHPStan\Rules\Arrays; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function define; @@ -16,7 +17,7 @@ class DuplicateKeysInLiteralArraysRuleTest extends RuleTestCase protected function getRule(): Rule { return new DuplicateKeysInLiteralArraysRule( - new Standard(), + new ExprPrinter(new Printer()), ); } @@ -44,6 +45,22 @@ public function testDuplicateKeys(): void 'Array has 2 duplicate keys with value 2 ($idx, $idx).', 55, ], + [ + 'Array has 2 duplicate keys with value 0 (0, 0).', + 63, + ], + [ + 'Array has 2 duplicate keys with value 101 (101, 101).', + 67, + ], + [ + 'Array has 2 duplicate keys with value 102 (102, 102).', + 69, + ], + [ + 'Array has 2 duplicate keys with value -41 (-41, -41).', + 76, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/EmptyArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/EmptyArrayItemRuleTest.php deleted file mode 100644 index 51ba629e16..0000000000 --- a/tests/PHPStan/Rules/Arrays/EmptyArrayItemRuleTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class EmptyArrayItemRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new EmptyArrayItemRule(); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/empty-array-item.php'], [ - [ - 'Literal array contains empty item.', - 5, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index 625f884a61..757b25cbd7 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\Arrays; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,7 +15,8 @@ class InvalidKeyInArrayDimFetchRuleTest extends RuleTestCase protected function getRule(): Rule { - return new InvalidKeyInArrayDimFetchRule(true); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true); + return new InvalidKeyInArrayDimFetchRule($ruleLevelHelper, true); } public function testInvalidKey(): void @@ -35,6 +38,60 @@ public function testInvalidKey(): void 'Invalid array key type DateTimeImmutable.', 31, ], + [ + 'Invalid array key type DateTimeImmutable.', + 45, + ], + [ + 'Invalid array key type DateTimeImmutable.', + 46, + ], + [ + 'Invalid array key type DateTimeImmutable.', + 47, + ], + [ + 'Invalid array key type stdClass.', + 47, + ], + [ + 'Invalid array key type DateTimeImmutable.', + 48, + ], + ]); + } + + public function testBug6315(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6315.php'], [ + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 18, + ], + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 19, + ], + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 20, + ], + [ + 'Invalid array key type Bug6315\FooEnum::B.', + 21, + ], + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 21, + ], + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 22, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php index 62cf77d9f4..7a40122d1c 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -62,4 +63,18 @@ public function testInvalidKeyShortArray(): void ]); } + public function testInvalidKeyEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/invalid-key-array-item-enum.php'], [ + [ + 'Invalid array key type InvalidKeyArrayItemEnum\FooEnum::A.', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php index e1feebb198..c907f46ddd 100644 --- a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -15,9 +17,11 @@ class IterableInForeachRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed)); + return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true)); } public function testCheckWithMaybes(): void @@ -28,7 +32,7 @@ public function testCheckWithMaybes(): void 10, ], [ - 'Argument of an invalid type array|false supplied for foreach, only iterables are supported.', + 'Argument of an invalid type list|false supplied for foreach, only iterables are supported.', 19, ], [ @@ -75,4 +79,69 @@ public function testBug6564(): void $this->analyse([__DIR__ . '/data/bug-6564.php'], []); } + public function testBug4335(): void + { + $this->analyse([__DIR__ . '/data/bug-4335.php'], []); + } + + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Argument of an invalid type T of mixed supplied for foreach, only iterables are supported.', + 11, + ], + [ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 14, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 17, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/foreach-mixed.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 5621266e40..941829a685 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -15,13 +15,19 @@ class NonexistentOffsetInArrayDimFetchRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + + private bool $reportPossiblyNonexistentGeneralArrayOffset = false; + + private bool $reportPossiblyNonexistentConstantArrayOffset = false; + protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new NonexistentOffsetInArrayDimFetchRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->reportPossiblyNonexistentGeneralArrayOffset, $this->reportPossiblyNonexistentConstantArrayOffset), true, ); } @@ -84,15 +90,15 @@ public function testRule(): void 145, ], [ - 'Offset \'c\' does not exist on array{c: bool}|array{e: true}.', + 'Offset \'c\' might not exist on array{c: false}|array{c: true}|array{e: true}.', 171, ], [ - 'Offset int does not exist on array{}|array{1: 1, 2: 2}|array{3: 3, 4: 4}.', + 'Offset int might 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 might not exist on array{}|array{1: 1, 2: 2}|array{3: 3, 4: 4}.', 193, ], [ @@ -103,10 +109,6 @@ public function testRule(): void 'Offset \'b\' does not exist on array{a: \'blabla\'}.', 228, ], - [ - 'Offset string does not exist on array.', - 240, - ], [ 'Cannot access offset \'a\' on Closure(): void.', 253, @@ -115,10 +117,6 @@ public function testRule(): void 'Cannot access offset \'a\' on array{a: 1, b: 1}|(Closure(): void).', 258, ], - [ - 'Offset string does not exist on array.', - 308, - ], [ 'Offset null does not exist on array.', 310, @@ -128,7 +126,7 @@ public function testRule(): void 312, ], [ - 'Offset \'baz\' does not exist on array{bar: 1, baz?: 2}.', + 'Offset \'baz\' might not exist on array{bar: 1, baz?: 2}.', 344, ], [ @@ -164,9 +162,13 @@ public function testRule(): void 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}.', + 'Offset \'feature_pretty_version\' might not exist on array{version: non-falsy-string, commit: string|null, pretty_version: string|null, feature_version: non-falsy-string, feature_pretty_version?: string|null}.', 504, ], + [ + "Cannot access offset 'foo' on bool.", + 517, + ], ]); } @@ -182,11 +184,11 @@ public function testStrings(): void 13, ], [ - 'Offset \'foo\' does not exist on array|string.', + 'Offset \'foo\' might not exist on array|string.', 24, ], [ - 'Offset 12.34 does not exist on array|string.', + 'Offset 12.34 might not exist on array|string.', 28, ], ]); @@ -236,9 +238,6 @@ public function testAssignOp(): void public function testCoalesceAssign(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/nonexistent-offset-coalesce-assign.php'], []); } @@ -251,7 +250,7 @@ public function testBug3782(): void { $this->analyse([__DIR__ . '/data/bug-3782.php'], [ [ - 'Cannot access offset (int|string) on Bug3782\HelloWorld.', + 'Cannot access offset (int|string) on $this(Bug3782\HelloWorld)|(ArrayAccess&Bug3782\HelloWorld).', 11, ], ]); @@ -327,14 +326,18 @@ 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, ], + [ + 'Cannot access offset \'permission\' on mixed.', + 39, + ], ]); } @@ -362,4 +365,574 @@ public function testBug3171(): void $this->analyse([__DIR__ . '/data/bug-3171.php'], []); } + public function testBug4747(): void + { + $this->analyse([__DIR__ . '/data/bug-4747.php'], []); + } + + public function testBug6379(): void + { + $this->analyse([__DIR__ . '/data/bug-6379.php'], []); + } + + public function testBug4885(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-4885.php'], []); + } + + public function testBug7000(): void + { + $this->analyse([__DIR__ . '/data/bug-7000.php'], [ + [ + "Offset 'require'|'require-dev' might not exist on array{require?: array, require-dev?: array}.", + 16, + ], + ]); + } + + public function testBug6508(): void + { + $this->analyse([__DIR__ . '/data/bug-6508.php'], []); + } + + public function testBug7229(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7229.php'], [ + [ + 'Cannot access offset string on mixed.', + 24, + ], + ]); + } + + public function testBug7142(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7142.php'], []); + } + + public function testBug6000(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6000.php'], []); + } + + public function testBug5743(): void + { + $this->analyse([__DIR__ . '/../Comparison/data/bug-5743.php'], [ + [ + 'Offset 1|int<3, max> does not exist on array{}.', + 10, + ], + ]); + } + + public function testBug6364(): void + { + $this->analyse([__DIR__ . '/data/bug-6364.php'], []); + } + + public function testBug5758(): void + { + $this->analyse([__DIR__ . '/data/bug-5758.php'], []); + } + + public function testBug5223(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5223.php'], [ + [ + 'Offset \'something\' does not exist on array{categoryKeys: array, tagNames: array}.', + 26, + ], + [ + 'Offset \'something\' does not exist on array{categoryKeys: array, tagNames: array}.', + 27, + ], + [ + 'Offset \'something\' does not exist on array{categoryKeys: array, tagNames: array}.', + 41, + ], + [ + 'Offset \'something\' does not exist on array{categoryKeys: array, tagNames: array}.', + 42, + ], + ]); + } + + public function testBug7469(): void + { + $expected = []; + + if (PHP_VERSION_ID < 80000) { + $expected = [ + [ + "Cannot access offset 'languages' on array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>|false.", + 31, + ], + [ + "Cannot access offset 'languages' on array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>|false.", + 31, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/bug-7469.php'], $expected); + } + + public function testBug7763(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-7763.php'], []); + } + + public function testSpecifyExistentOffsetWhenEnteringForeach(): void + { + $this->analyse([__DIR__ . '/data/specify-existent-offset-when-entering-foreach.php'], []); + } + + public function testBug3872(): void + { + $this->analyse([__DIR__ . '/data/bug-3872.php'], []); + } + + public function testBug6783(): void + { + $this->analyse([__DIR__ . '/data/bug-6783.php'], []); + } + + public function testSlevomatForeachUnsetBug(): void + { + $this->analyse([__DIR__ . '/data/slevomat-foreach-unset-bug.php'], []); + } + + public function testSlevomatForeachArrayKeyExistsBug(): void + { + $this->analyse([__DIR__ . '/data/slevomat-foreach-array-key-exists-bug.php'], []); + } + + public function testBug7954(): void + { + $this->analyse([__DIR__ . '/data/bug-7954.php'], []); + } + + public function testBug8097(): void + { + $this->analyse([__DIR__ . '/data/bug-8097.php'], []); + } + + public function testBug8068(): void + { + $this->analyse([__DIR__ . '/data/bug-8068.php'], [ + [ + "Cannot access offset 'path' on Closure.", + 18, + ], + [ + "Cannot access offset 'path' on iterable.", + 26, + ], + ]); + } + + public function testBug6243(): void + { + $this->analyse([__DIR__ . '/data/bug-6243.php'], []); + } + + public function testBug8356(): void + { + $this->analyse([__DIR__ . '/data/bug-8356.php'], [ + [ + "Offset 'x' might not exist on array|string.", + 7, + ], + ]); + } + + public function testBug6605(): void + { + $this->analyse([__DIR__ . '/data/bug-6605.php'], [ + [ + "Cannot access offset 'invalidoffset' on Bug6605\\X.", + 11, + ], + [ + "Offset 'invalid' does not exist on array{a: array{b: array{5}}}.", + 16, + ], + [ + "Offset 'invalid' does not exist on array{b: array{5}}.", + 17, + ], + ]); + } + + public function testBug9991(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9991.php'], [ + [ + 'Cannot access offset \'title\' on mixed.', + 9, + ], + ]); + } + + public function testBug8166(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8166.php'], [ + [ + 'Offset \'b\' does not exist on array{a: 1}.', + 22, + ], + [ + 'Offset \'b\' does not exist on array<\'a\', string>.', + 23, + ], + ]); + } + + public function testBug10926(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-10926.php'], [ + [ + 'Cannot access offset \'a\' on stdClass.', + 10, + ], + ]); + } + + public function testMixed(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-mixed.php'], [ + [ + 'Cannot access offset 5 on T of mixed.', + 11, + ], + [ + 'Cannot access offset 5 on mixed.', + 16, + ], + [ + 'Cannot access offset 5 on mixed.', + 21, + ], + ]); + } + + public function testOffsetAccessLegal(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-legal.php'], [ + [ + 'Cannot access offset 0 on Closure(): void.', + 7, + ], + [ + 'Cannot access offset 0 on stdClass.', + 12, + ], + [ + 'Cannot access offset 0 on array{\'test\'}|stdClass.', + 96, + ], + [ + 'Cannot access offset 0 on array{\'test\'}|(Closure(): void).', + 98, + ], + ]); + } + + public function testNonExistentParentOffsetAccessLegal(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-legal-non-existent-parent.php'], [ + [ + 'Cannot access offset 0 on parent.', + 9, + ], + ]); + } + + public function dataReportPossiblyNonexistentArrayOffset(): iterable + { + yield [false, false, []]; + yield [false, true, [ + [ + 'Offset string might not exist on array{foo: 1}.', + 20, + ], + ]]; + yield [true, false, [ + [ + "Offset 'foo' might not exist on array.", + 9, + ], + ]]; + yield [true, true, [ + [ + "Offset 'foo' might not exist on array.", + 9, + ], + [ + 'Offset string might not exist on array{foo: 1}.', + 20, + ], + ]]; + } + + /** + * @dataProvider dataReportPossiblyNonexistentArrayOffset + * @param list $errors + */ + public function testReportPossiblyNonexistentArrayOffset(bool $reportPossiblyNonexistentGeneralArrayOffset, bool $reportPossiblyNonexistentConstantArrayOffset, array $errors): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = $reportPossiblyNonexistentGeneralArrayOffset; + $this->reportPossiblyNonexistentConstantArrayOffset = $reportPossiblyNonexistentConstantArrayOffset; + + $this->analyse([__DIR__ . '/data/report-possibly-nonexistent-array-offset.php'], $errors); + } + + public function testBug10997(): void + { + $this->reportPossiblyNonexistentConstantArrayOffset = true; + $this->analyse([__DIR__ . '/data/bug-10997.php'], [ + [ + 'Offset int<0, 4> might not exist on array{1, 2, 3, 4}.', + 15, + ], + ]); + } + + public function testBug11572(): void + { + $this->analyse([__DIR__ . '/data/bug-11572.php'], [ + [ + 'Cannot access an offset on int.', + 45, + ], + [ + 'Cannot access an offset on int<3, 4>.', + 46, + ], + ]); + } + + public function testBug2313(): void + { + $this->analyse([__DIR__ . '/data/bug-2313.php'], []); + } + + public function testBug11655(): void + { + $this->analyse([__DIR__ . '/data/bug-11655.php'], [ + [ + "Offset 3 does not exist on array{non-falsy-string, 'x', array{non-falsy-string, 'x'}}.", + 15, + ], + ]); + } + + public function testBug2634(): void + { + $this->analyse([__DIR__ . '/data/bug-2634.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccess(): void + { + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccess84(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-php84.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccessInvalid(): void + { + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-invalid.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccessInvalid84(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-invalid-php84.php'], []); + } + + public function testBug12122(): void + { + $this->analyse([__DIR__ . '/data/bug-12122.php'], []); + } + + public function testArrayDimFetchAfterArrayKeyFirstOrLast(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/array-dim-after-array-key-first-or-last.php'], [ + [ + 'Offset null does not exist on array{}.', + 19, + ], + ]); + } + + public function testArrayDimFetchAfterCount(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/array-dim-after-count.php'], [ + [ + 'Offset int<0, max> might not exist on list.', + 26, + ], + [ + 'Offset int<-1, max> might not exist on array.', + 35, + ], + [ + 'Offset int<0, max> might not exist on non-empty-array.', + 42, + ], + ]); + } + + public function testArrayDimFetchAfterArraySearch(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/array-dim-after-array-search.php'], [ + [ + 'Offset int|string might not exist on array.', + 20, + ], + ]); + } + + public function testArrayDimFetchOnArrayKeyFirsOrLastOrCount(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/array-dim-fetch-on-array-key-first-last.php'], [ + [ + 'Offset 0|null might not exist on list.', + 12, + ], + [ + 'Offset (int|string) might not exist on non-empty-list.', + 16, + ], + [ + 'Offset int<-1, max> might not exist on non-empty-list.', + 45, + ], + ]); + } + + public function testBug12406(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-12406.php'], []); + } + + public function testBug12406b(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-12406b.php'], [ + [ + 'Offset int<0, max> might not exist on non-empty-list.', + 22, + ], + [ + 'Offset int<0, max> might not exist on non-empty-list.', + 23, + ], + ]); + } + + public function testBug11679(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-11679.php'], []); + } + + public function testBug8649(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-8649.php'], []); + } + + public function testBug11447(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-11447.php'], []); + } + + public function testNarrowSuperglobals(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/narrow-superglobal.php'], []); + } + + public function testBug12605(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-12605.php'], [ + [ + 'Offset 1 might not exist on list.', + 19, + ], + [ + 'Offset 10 might not exist on non-empty-list.', + 26, + ], + ]); + } + + public function testBug11602(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-11602.php'], []); + } + + public function testBug12593(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-12593.php'], []); + } + + public function testBug3747(): void + { + $this->analyse([__DIR__ . '/data/bug-3747.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php index ab4a065cf7..e131c311a4 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php @@ -17,7 +17,7 @@ class OffsetAccessAssignOpRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnions, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnions, false, false, false, true); return new OffsetAccessAssignOpRule($ruleLevelHelper); } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index dff1942e3e..533dd185ae 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php @@ -17,7 +17,7 @@ class OffsetAccessAssignmentRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, false, false, false, true); return new OffsetAccessAssignmentRule($ruleLevelHelper); } @@ -129,7 +129,6 @@ public function testAssignNewOffsetToStubbedClass(): void $this->analyse([__DIR__ . '/data/new-offset-stub.php'], []); } - public function testRuleWithNullsafeVariant(): void { if (PHP_VERSION_ID < 80000) { @@ -145,4 +144,55 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function testBug1714(): void + { + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-1714.php'], []); + } + + public function testBug8015(): void + { + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8015.php'], []); + } + + public function testBug11572(): void + { + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-11572.php'], [ + [ + 'Cannot assign new offset to string.', + 15, + ], + [ + 'Cannot assign new offset to string.', + 16, + ], + [ + 'Cannot assign new offset to string.', + 17, + ], + [ + 'Cannot assign new offset to string.', + 18, + ], + [ + 'Cannot assign new offset to string.', + 19, + ], + [ + 'Cannot assign new offset to string.', + 20, + ], + [ + 'Cannot assign new offset to string.', + 24, + ], + [ + 'Cannot assign new offset to string.', + 36, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php index fdb32e4eb3..ff9ae587d7 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -15,7 +15,7 @@ class OffsetAccessValueAssignmentRuleTest extends RuleTestCase protected function getRule(): Rule { - return new OffsetAccessValueAssignmentRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new OffsetAccessValueAssignmentRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void @@ -66,4 +66,13 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function testBug5655b(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-5655b.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php index 66bacbcfec..15abfb7cef 100644 --- a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -13,16 +15,17 @@ class UnpackIterableInArrayRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new UnpackIterableInArrayRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new UnpackIterableInArrayRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true)); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/unpack-iterable.php'], [ [ 'Only iterables can be unpacked, array|null given.', @@ -53,4 +56,64 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Only iterables can be unpacked, T of mixed given.', + 11, + ], + [ + 'Only iterables can be unpacked, mixed given.', + 12, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Only iterables can be unpacked, mixed given.', + 13, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/unpack-mixed.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/array-dim-after-array-key-first-or-last.php b/tests/PHPStan/Rules/Arrays/data/array-dim-after-array-key-first-or-last.php new file mode 100644 index 0000000000..e27fcfa175 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-dim-after-array-key-first-or-last.php @@ -0,0 +1,75 @@ += 8.0 + +declare(strict_types = 1); + +namespace ArrayDimAfterArrayKeyFirstOrLast; + +class HelloWorld +{ + /** + * @param list $hellos + */ + public function last(array $hellos): string + { + if ($hellos !== []) { + $last = array_key_last($hellos); + return $hellos[$last]; + } else { + $last = array_key_last($hellos); + return $hellos[$last]; + } + } + + /** + * @param array $hellos + */ + public function lastOnArray(array $hellos): string + { + if ($hellos !== []) { + $last = array_key_last($hellos); + return $hellos[$last]; + } + + return 'nothing'; + } + + /** + * @param list $hellos + */ + public function first(array $hellos): string + { + if ($hellos !== []) { + $first = array_key_first($hellos); + return $hellos[$first]; + } + + return 'nothing'; + } + + /** + * @param array $hellos + */ + public function firstOnArray(array $hellos): string + { + if ($hellos !== []) { + $first = array_key_first($hellos); + return $hellos[$first]; + } + + return 'nothing'; + } + + /** + * @param array{first: int, middle: float, last: bool} $hellos + */ + public function shape(array $hellos): int|bool + { + $first = array_key_first($hellos); + $last = array_key_last($hellos); + + if (rand(0,1)) { + return $hellos[$first]; + } + return $hellos[$last]; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-dim-after-array-search.php b/tests/PHPStan/Rules/Arrays/data/array-dim-after-array-search.php new file mode 100644 index 0000000000..3aa2d4c21b --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-dim-after-array-search.php @@ -0,0 +1,37 @@ += 8.0 + +declare(strict_types = 1); + +namespace ArrayDimAfterArraySeach; + +class HelloWorld +{ + public function doFoo(array $arr, string $needle): string + { + if (($key = array_search($needle, $arr, true)) !== false) { + echo $arr[$key]; + } + } + + public function doBar(array $arr, string $needle): string + { + $key = array_search($needle, $arr, true); + if ($key !== false) { + echo $arr[$key]; + } + } + + public function doFooBar(array $arr, string $needle): string + { + if (($key = array_search($needle, $arr, false)) !== false) { + echo $arr[$key]; + } + } + + public function doBaz(array $arr, string $needle): string + { + if (($key = array_search($needle, $arr)) !== false) { + echo $arr[$key]; + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-dim-after-count.php b/tests/PHPStan/Rules/Arrays/data/array-dim-after-count.php new file mode 100644 index 0000000000..4f52d30b24 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-dim-after-count.php @@ -0,0 +1,45 @@ + $hellos + */ + public function works(array $hellos): string + { + if ($hellos === []) { + return 'nothing'; + } + + $count = count($hellos) - 1; + return $hellos[$count]; + } + + /** + * @param list $hellos + */ + public function offByOne(array $hellos): string + { + $count = count($hellos); + return $hellos[$count]; + } + + /** + * @param array $hellos + */ + public function maybeInvalid(array $hellos): string + { + $count = count($hellos) - 1; + echo $hellos[$count]; + + if ($hellos === []) { + return 'nothing'; + } + + $count = count($hellos) - 1; + return $hellos[$count]; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php b/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php new file mode 100644 index 0000000000..82fac73327 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php @@ -0,0 +1,50 @@ + $hellos + */ + public function first(array $hellos, array $anotherArray): string + { + if (rand(0,1)) { + return $hellos[array_key_first($hellos)]; + } + if ($hellos !== []) { + if ($anotherArray !== []) { + return $hellos[array_key_first($anotherArray)]; + } + + return $hellos[array_key_first($hellos)]; + } + return ''; + } + + /** + * @param array $hellos + */ + public function last(array $hellos): string + { + if ($hellos !== []) { + return $hellos[array_key_last($hellos)]; + } + return ''; + } + + /** + * @param list $hellos + */ + public function countOnArray(array $hellos, array $anotherArray): string + { + if ($hellos === []) { + return 'nothing'; + } + + if (rand(0,1)) { + return $hellos[count($anotherArray) - 1]; + } + + return $hellos[count($hellos) - 1]; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-unpacking.php b/tests/PHPStan/Rules/Arrays/data/array-unpacking.php new file mode 100644 index 0000000000..ff2f652cac --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-unpacking.php @@ -0,0 +1,72 @@ + 'bar', 1, 2, 3]; + +$bar = [...$foo]; + +/** @param array $bar */ +function intKeyedArray(array $bar) +{ + $baz = [...$bar]; +} + +/** @param array $bar */ +function stringKeyedArray(array $bar) +{ + $baz = [...$bar]; +} + +/** @param array $bar */ +function benevolentUnionKeyedArray(array $bar) +{ + $baz = [...$bar]; +} + +function mixedKeyedArray(array $bar) +{ + $baz = [...$bar]; +} + +/** + * @param array $foo + * @param array $bar + */ +function multipleUnpacking(array $foo, array $bar) +{ + $baz = [ + ...$bar, + ...$foo, + ]; +} + +/** + * @param array $foo + * @param array $bar + */ +function foo(array $foo, array $bar) +{ + $baz = [ + $bar, + ...$foo + ]; +} + +/** + * @param array{foo: string, bar:int} $foo + * @param array{1, 2, 3, 4} $bar + */ +function unpackingArrayShapes(array $foo, array $bar) +{ + $baz = [ + ...$foo, + ...$bar, + ]; +} + +/** @param array $bar */ +function unionKeyedArray(array $bar) +{ + $baz = [...$bar]; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10926.php b/tests/PHPStan/Rules/Arrays/data/bug-10926.php new file mode 100644 index 0000000000..e8316112d5 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-10926.php @@ -0,0 +1,12 @@ += 8.0 + +namespace Bug10926; + +class HelloWorld +{ + public function sayHello(?\stdClass $date): void + { + $date ??= new \stdClass(); + echo isset($date['a']); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10997.php b/tests/PHPStan/Rules/Arrays/data/bug-10997.php new file mode 100644 index 0000000000..183004bdc2 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-10997.php @@ -0,0 +1,17 @@ + $range + */ +function doInt(int $i, $range): void +{ + $i[] = 1; + $range[] = 1; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11602.php b/tests/PHPStan/Rules/Arrays/data/bug-11602.php new file mode 100644 index 0000000000..4e1252e5b4 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-11602.php @@ -0,0 +1,23 @@ +arr); + if (!isset($this->arr['foo'])) { + $this->arr['foo'] = true; + assertType('array{foo: true}', $this->arr); + } + assertType('array{foo: bool}', $this->arr); + return $this->arr['foo']; // PHPStan realizes optional 'foo' is set + } +} + +class NonworkingExample +{ + /** @var array */ + private array $arr = []; + + public function sayHello(int $index): bool + { + assertType('array', $this->arr); + if (!isset($this->arr[$index]['foo'])) { + $this->arr[$index]['foo'] = true; + assertType('non-empty-array', $this->arr); + } + assertType('array', $this->arr); + return $this->arr[$index]['foo']; // PHPStan does not realize 'foo' is set + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12122.php b/tests/PHPStan/Rules/Arrays/data/bug-12122.php new file mode 100644 index 0000000000..acd1816675 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12122.php @@ -0,0 +1,8 @@ + */ + protected array $words = []; + + public function sayHello(string $word, int $count): void + { + $this->words[$word] ??= 0; + $this->words[$word] += $count; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12406b.php b/tests/PHPStan/Rules/Arrays/data/bug-12406b.php new file mode 100644 index 0000000000..c0012503d0 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12406b.php @@ -0,0 +1,35 @@ +]+>\\n + AuthorDate:[^\\n]+\\n + Commit:[^\\n]+\\n + CommitDate:[^\\n]+\\n\\n + (\s+(?:[^\n]+\n)+)\n + [ ](\\d+)[ ]files?[ ]changed,(?:[ ](\\d+)[ ]insertions?\\(\\+\\),?)?(?:[ ](\\d+)[ ]deletions?\\(-\\))? + ~mx', $s, $matches, PREG_SET_ORDER); + + for ($i = 0; $i < count($matches); $i++) { + $author = $matches[$i][1]; + $files = (int) $matches[$i][3]; + $insertions = (int) ($matches[$i][4] ?? 0); + $deletions = (int) ($matches[$i][5] ?? 0); + + $stats[$author]['commits'] = ($stats[$author]['commits'] ?? 0) + 1; + $stats[$author]['files'] = ($stats[$author]['files'] ?? 0) + $files; + $stats[$author]['insertions'] = ($stats[$author]['insertions'] ?? 0) + $insertions; + $stats[$author]['deletions'] = ($stats[$author]['deletions'] ?? 0) + $deletions; + $stats[$author]['diff'] = ($stats[$author]['diff'] ?? 0) + $insertions - $deletions; + } + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12593.php b/tests/PHPStan/Rules/Arrays/data/bug-12593.php new file mode 100644 index 0000000000..b5ba2616cf --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12593.php @@ -0,0 +1,35 @@ + $indexes + */ + protected function removeArguments(array $indexes): void + { + if (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) { + foreach ($indexes as $index) { + if (isset($_SERVER['argv'][$index])) { + unset($_SERVER['argv'][$index]); + } + } + } + } +} + +class HelloWorld2 +{ + /** + * @param list $indexes + */ + protected function removeArguments(array $indexes): void + { + foreach ($indexes as $index) { + if (isset($_SERVER['argv']) && is_array($_SERVER['argv']) && isset($_SERVER['argv'][$index])) { + unset($_SERVER['argv'][$index]); + } + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12605.php b/tests/PHPStan/Rules/Arrays/data/bug-12605.php new file mode 100644 index 0000000000..c5d31f966c --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12605.php @@ -0,0 +1,37 @@ + + */ +function test(): array +{ + return []; +} + +function doFoo(): void { + $test = test(); + + if (isset($test[3])) { + echo $test[1]; + } + echo $test[1]; +} + +function doFooBar(): void { + $test = test(); + + if (isset($test[4])) { + echo $test[10]; + } +} + +function doBaz(): void { + $test = test(); + + if (array_key_exists(5, $test) && is_int($test[5])) { + echo $test[3]; + } +} + diff --git a/tests/PHPStan/Rules/Arrays/data/bug-1714.php b/tests/PHPStan/Rules/Arrays/data/bug-1714.php new file mode 100644 index 0000000000..e237e416bb --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-1714.php @@ -0,0 +1,18 @@ + $val, 'text' => $text]; + } + $radio['name'] = $data['name']; + return $radio; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-2313.php b/tests/PHPStan/Rules/Arrays/data/bug-2313.php new file mode 100644 index 0000000000..f79d9f0add --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-2313.php @@ -0,0 +1,22 @@ + array()); + + safe_inc($data['apples']['count']); + print_r($data); + + safe_inc($data['apples']['count']); + print_r($data); +}; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-2634.php b/tests/PHPStan/Rules/Arrays/data/bug-2634.php new file mode 100644 index 0000000000..355ce896f6 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-2634.php @@ -0,0 +1,13 @@ + $x */ + private static array $x; + + public function y(): void { + + self::$x = []; + + $this->z(); + + echo self::$x['foo']; + + } + + private function z(): void { + self::$x['foo'] = 'bar'; + } + +} + +$x = new X(); +$x->y(); diff --git a/tests/PHPStan/Rules/Arrays/data/bug-3872.php b/tests/PHPStan/Rules/Arrays/data/bug-3872.php new file mode 100644 index 0000000000..944cba7757 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-3872.php @@ -0,0 +1,19 @@ + '', 'operator' => '']; + if ($item['value']) { + $item['value'] = strtotime($item['value']); + if ($item['operator'] === 'eq') { + echo 'test'; + } + } + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4335.php b/tests/PHPStan/Rules/Arrays/data/bug-4335.php new file mode 100644 index 0000000000..0824514d15 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4335.php @@ -0,0 +1,19 @@ + $v) { + var_dump($k, $v); + } + foreach (class_parents($this) as $k => $v) { + var_dump($k, $v); + } + foreach (class_uses($this) as $k => $v) { + var_dump($k, $v); + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4747.php b/tests/PHPStan/Rules/Arrays/data/bug-4747.php new file mode 100644 index 0000000000..55c55b15e2 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4747.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug4885; + +class Foo +{ + /** @param array{word?: string} $data */ + public function sayHello(array $data): void + { + echo ($data['word'] ?? throw new \RuntimeException('bye')) . ', World!'; + echo 'Again, the word was: ' . $data['word']; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5655b.php b/tests/PHPStan/Rules/Arrays/data/bug-5655b.php new file mode 100644 index 0000000000..3f61021623 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5655b.php @@ -0,0 +1,33 @@ + */ + $list = []; + + $list[] = [ + 'foo' => 'baz', + ]; + +// Case with map... FAIL + + /** @var WeakMap */ + $map = new WeakMap(); + + $map[new stdClass()] = [ + 'foo' => 'foo', + 'bar' => 'bar', + ]; + + $map[new stdClass()] = [ + 'foo' => 'baz', + ]; +} + diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5758.php b/tests/PHPStan/Rules/Arrays/data/bug-5758.php new file mode 100644 index 0000000000..39a4f40ce0 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5758.php @@ -0,0 +1,23 @@ +, classmap?: list} $data */ + $data = []; + + foreach ($data as $key => $value) { + assertType('array|string, array|string>', $data[$key]); + if ($key === 'classmap') { + assertType('list', $data[$key]); + assertType('list', $value); + echo implode(', ', $value); // not working :( + echo implode(', ', $data[$key]); // this works though?! + } + } +}; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6243.php b/tests/PHPStan/Rules/Arrays/data/bug-6243.php new file mode 100644 index 0000000000..1bf44f1400 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6243.php @@ -0,0 +1,22 @@ +|(\ArrayAccess&iterable) */ + private iterable $values; + + /** + * @param list $values + */ + public function update(array $values): void { + foreach ($this->values as $key => $_) { + unset($this->values[$key]); + } + + foreach ($values as $value) { + $this->values[] = $value; + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6315.php b/tests/PHPStan/Rules/Arrays/data/bug-6315.php new file mode 100644 index 0000000000..b3bef3c1c2 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6315.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug6315; + +enum FooEnum +{ + case A; + case B; +} + +/** + * @param array $flatArr + * @param array> $deepArr + * @return void + */ +function foo(array $flatArr, array $deepArr): void +{ + var_dump($flatArr[FooEnum::A]); + var_dump($deepArr[FooEnum::A][5]); + var_dump($deepArr[5][FooEnum::A]); + var_dump($deepArr[FooEnum::A][FooEnum::B]); + $deepArr[FooEnum::A][] = 5; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6364.php b/tests/PHPStan/Rules/Arrays/data/bug-6364.php new file mode 100644 index 0000000000..cf259552ee --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6364.php @@ -0,0 +1,65 @@ + + * } | array{ + * type: 'Type2', + * id: string, + * job?: string, + * extractor?: int + * } | array{ + * type: 'Type3', + * id: string, + * jobs: array + * } | array{ + * type: 'Type4', + * id: string, + * job?: string + * }> $array + */ + public function doFoo(array $array) + { + foreach ($array as $key => $data) { + switch ($data['type']) { + case 'Type1': + assertType("array{type: 'Type1', id: string, jobs: array}", $data); + echo $data['id']; + print_r($data['jobs']); + break; + case 'Type3': + assertType("array{type: 'Type3', id: string, jobs: array}", $data); + $jobs = []; + foreach ($data['jobs'] as $job => $extractor) { + echo $job; + echo $extractor; + } + break; + case 'Type2': + assertType("array{type: 'Type2', id: string, job?: string, extractor?: int}", $data); + echo $data['id']; + echo $data['job'] ?? 'default'; + echo $data['extractor'] ?? 0; + break; + case 'Type4': + assertType("array{type: 'Type4', id: string, job?: string}", $data); + echo $data['id']; + echo $data['job'] ?? 'default'; + break; + default: + throw new \RuntimeException('unknown type: ' . $data['type']); + } + } + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6379.php b/tests/PHPStan/Rules/Arrays/data/bug-6379.php new file mode 100644 index 0000000000..2075ca7bae --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6379.php @@ -0,0 +1,21 @@ + 'bar' + ]; + + $arr = ['a' => ['b' => [5]]]; + var_dump($arr['invalid']['c']); + var_dump($arr['a']['invalid']); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6783.php b/tests/PHPStan/Rules/Arrays/data/bug-6783.php new file mode 100644 index 0000000000..beda63bd95 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6783.php @@ -0,0 +1,31 @@ + + */ +function foo(): array +{ + // something from elsewhere (not in the scope of PHPStan) + return [ + // removing or keeping those lines does/should not change the reporting + 'foo' => [ + 'bar' => true, + ] + ]; +} + +function bar() { + $data = foo(); + $data = $data['foo'] ?? []; // <<< removing this line suppress the error + $data += [ + 'default' => true, + ]; + foreach (['formatted'] as $field) { + $data[$field] = empty($data[$field]) ? false : true; + } + + $bar = $data['bar']; +} + diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7000.php b/tests/PHPStan/Rules/Arrays/data/bug-7000.php new file mode 100644 index 0000000000..73a8af4e52 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-7000.php @@ -0,0 +1,20 @@ +, require-dev?: array} $composer */ + $composer = array(); + /** @var 'require'|'require-dev' $foo */ + $foo = ''; + foreach (array('require', 'require-dev') as $linkType) { + if (isset($composer[$linkType])) { + foreach ($composer[$linkType] as $x) {} // should not report error + foreach ($composer[$foo] as $x) {} // should report error. It can be $linkType = 'require', $foo = 'require-dev' + } + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7142.php b/tests/PHPStan/Rules/Arrays/data/bug-7142.php new file mode 100644 index 0000000000..ca1078ee28 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-7142.php @@ -0,0 +1,26 @@ + 1]; + } + return null; +} + +/** +* @return void +*/ +function foo(){ + if (!is_null($a = $b = $c = maybeNull())){ + echo $a['id']; + echo $b['id']; // 20 "Offset 'id' does not exist on array{id: int}|null." + echo $c['id']; // 21 "Offset 'id' does not exist on array{id: int}|null." + } +} + diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7229.php b/tests/PHPStan/Rules/Arrays/data/bug-7229.php new file mode 100644 index 0000000000..255c71e368 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-7229.php @@ -0,0 +1,37 @@ + array + // where mixed is rather the short write for scalar|array|null which + // can be nested in N-depth (merging several configs) + + /** + * Returns the value from the given array. + * + * @param array|mixed $config The array to search in + * @param array $parts Parts to look for inside the array + * + * @return array|mixed Found value or null if not available + */ + protected function _getValueFromArray( $config, $parts ) + { + // $config type is mixed or array !? + + if ( ( $key = array_shift( $parts ) ) !== null && isset( $config[$key] ) ) { + + // $config type NOT mixed + + if ( count( $parts ) > 0 ) { + return $this->_getValueFromArray( $config[$key], $parts ); + } + + return $config[$key]; + } + + return null; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7469.php b/tests/PHPStan/Rules/Arrays/data/bug-7469.php new file mode 100644 index 0000000000..d5aa696748 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-7469.php @@ -0,0 +1,48 @@ +&hasOffsetValue('languages', non-empty-list)", $data); + + $data['videoOnline'] = normalizePrice($data['videoOnline']); + $data['videoTvc'] = normalizePrice($data['videoTvc']); + $data['radio'] = normalizePrice($data['radio']); + + $data['invoicing'] = $data['invoicing'] === 'ANO'; + assertType("non-empty-array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>&hasOffsetValue('invoicing', bool)&hasOffsetValue('languages', non-empty-list)&hasOffsetValue('radio', mixed)&hasOffsetValue('videoOnline', mixed)&hasOffsetValue('videoTvc', mixed)", $data); +} + +function normalizePrice($value) +{ + return $value; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7763.php b/tests/PHPStan/Rules/Arrays/data/bug-7763.php new file mode 100644 index 0000000000..547c7bc9a1 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-7763.php @@ -0,0 +1,27 @@ += 8.1 + +namespace Bug7763; + +enum MyEnum: int { + case Case1 = 1; + case Case2 = 2; + + public function test(self $enum): string + { + $mapping = array_filter([ + self::Case1->value => $this->maybeNull(), + self::Case2->value => $this->maybeNull(), + ]); + + if (array_key_exists($enum->value, $mapping)) { + return $mapping[$enum->value]; // Offset 1|2 does not exist on array{1?: non-falsy-string, 2?: non-falsy-string} + } + + return ''; + } + + private function maybeNull(): ?string + { + return (bool) rand(0, 1) ? '' : null; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7913.php b/tests/PHPStan/Rules/Arrays/data/bug-7913.php new file mode 100644 index 0000000000..1bd3465b0b --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-7913.php @@ -0,0 +1,17 @@ +, + * admins: array, + * implements: array, + * extends: array, + * instanceof: array, + * uses: array, + * priority: int, + * }> + * @phpstan-type SonataAdminConfigurationOptions = array{ + * confirm_exit: bool, + * default_admin_route: string, + * default_group: string, + * default_icon: string, + * default_translation_domain: string, + * default_label_catalogue: string, + * dropdown_number_groups_per_colums: int, + * form_type: 'standard'|'horizontal', + * html5_validate: bool, + * js_debug: bool, + * list_action_button_content: 'text'|'icon'|'all', + * lock_protection: bool, + * logo_content: 'text'|'icon'|'all', + * mosaic_background: string, + * pager_links: int|null, + * skin: 'skin-black'|'skin-black-light'|'skin-blue'|'skin-blue-light'|'skin-green'|'skin-green-light'|'skin-purple'|'skin-purple-light'|'skin-red'|'skin-red-light'|'skin-yellow'|'skin-yellow-light', + * sort_admins: bool, + * use_bootlint: bool, + * use_icheck: bool, + * use_select2: bool, + * use_stickyforms: bool, + * } + * @phpstan-type SonataAdminConfiguration = array{ + * assets: array{ + * extra_javascripts: list, + * extra_stylesheets: list, + * javascripts: list, + * remove_javascripts: list, + * remove_stylesheets: list, + * stylesheets: list, + * }, + * breadcrumbs: array{ + * child_admin_route: string, + * }, + * dashboard: array{ + * blocks: array{ + * class: string, + * position: string, + * roles: list, + * settings: array, + * type: string, + * }, + * groups: array, + * keep_open: bool, + * on_top: bool, + * provider?: string, + * roles: list + * }>, + * }, + * default_admin_services: array{ + * configuration_pool: string|null, + * datagrid_builder: string|null, + * data_source: string|null, + * field_description_factory: string|null, + * form_contractor: string|null, + * label_translator_strategy: string|null, + * list_builder: string|null, + * menu_factory: string|null, + * model_manager: string|null, + * pager_type: string|null, + * route_builder: string|null, + * route_generator: string|null, + * security_handler: string|null, + * show_builder: string|null, + * translator: string|null, + * }, + * default_controller: string, + * extensions: array, + * filter_persister: string, + * global_search: array{ + * admin_route: string, + * empty_boxes: 'show'|'fade'|'hide', + * }, + * options: SonataAdminConfigurationOptions, + * persist_filters: bool, + * security: array{ + * acl_user_manager: string|null, + * admin_permissions: list, + * information: array>, + * object_permissions: list, + * handler: string, + * role_admin: string, + * role_super_admin: string, + * }, + * search: bool, + * show_mosaic_button: bool, + * templates: array{ + * acl: string, + * action: string, + * action_create: string, + * add_block: string, + * ajax: string, + * base_list_field: string, + * batch: string, + * batch_confirmation: string, + * button_acl: string, + * button_create: string, + * button_edit: string, + * button_history: string, + * button_list: string, + * button_show: string, + * dashboard: string, + * delete: string, + * edit: string, + * filter: string, + * filter_theme: list, + * form_theme: list, + * history: string, + * history_revision_timestamp: string, + * inner_list_row: string, + * knp_menu_template: string, + * layout: string, + * list: string, + * list_block: string, + * outer_list_rows_list: string, + * outer_list_rows_mosaic: string, + * outer_list_rows_tree: string, + * pager_links: string, + * pager_results: string, + * preview: string, + * search: string, + * search_result_block: string, + * select: string, + * short_object_description: string, + * show: string, + * show_compare: string, + * tab_menu_template: string, + * user_block: string, + * }, + * title: string, + * title_logo: string, + * } + **/ +class HelloWorld +{ + /** @param SonataAdminConfiguration $config */ + public function sayHello(array $config): void + { + assertType('string', $config['security']['role_admin']); + + if (false === $config['options']['lock_protection']) { + // things + } + + assertType('string', $config['security']['role_admin']); + + switch ($config['security']['handler']) { + case 'sonata.admin.security.handler.role': + if (0 === \count($config['security']['information'])) { + $config['security']['information'] = [ + 'EDIT' => ['EDIT'], + 'LIST' => ['LIST'], + 'CREATE' => ['CREATE'], + 'VIEW' => ['VIEW'], + 'DELETE' => ['DELETE'], + 'EXPORT' => ['EXPORT'], + 'ALL' => ['ALL'], + ]; + } + + break; + case 'sonata.admin.security.handler.acl': + if (0 === \count($config['security']['information'])) { + $config['security']['information'] = [ + 'GUEST' => ['VIEW', 'LIST'], + 'STAFF' => ['EDIT', 'LIST', 'CREATE'], + 'EDITOR' => ['OPERATOR', 'EXPORT'], + 'ADMIN' => ['MASTER'], + ]; + } + + break; + } + + assertType('string', $config['security']['role_admin']); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8068.php b/tests/PHPStan/Rules/Arrays/data/bug-8068.php new file mode 100644 index 0000000000..96380586ef --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8068.php @@ -0,0 +1,28 @@ + $iterable + */ + public function test3($iterable): bool + { + unset($iterable['path']); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8097.php b/tests/PHPStan/Rules/Arrays/data/bug-8097.php new file mode 100644 index 0000000000..302f2e4de8 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8097.php @@ -0,0 +1,11 @@ + $arr + * + * @return array + */ +function strings(array $arr): array +{ + return $arr; +} + +function (): void { + $x = ['a' => 1]; + + $y = strings($x); + + var_dump($x['b']); + var_dump($y['b']); +}; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8292.php b/tests/PHPStan/Rules/Arrays/data/bug-8292.php new file mode 100644 index 0000000000..783cdc272d --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8292.php @@ -0,0 +1,32 @@ +addOnUnloadCallback(static function() use ($worldId) : void{ + foreach(self::$instances[$worldId] as $cache){ + $cache->caches = []; + } + unset(self::$instances[$worldId]); + }); + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8356.php b/tests/PHPStan/Rules/Arrays/data/bug-8356.php new file mode 100644 index 0000000000..192a71d363 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8356.php @@ -0,0 +1,9 @@ +, psr-4?: array, classmap?: list, files?: list, exclude-from-classmap?: list} + */ +interface CompletePackageInterface { + /** + * Returns an associative array of autoloading rules + * + * {"": {""}} + * + * Type is either "psr-4", "psr-0", "classmap" or "files". Namespaces are mapped to + * directories for autoloading using the type specified. + * + * @return array Mapping of autoloading rules + * @phpstan-return AutoloadRules + */ + public function getAutoload(): array; +} + +class Test { + public function foo (CompletePackageInterface $package): void { + if (\count($package->getAutoload()) > 0) { + $autoloadConfig = $package->getAutoload(); + foreach ($autoloadConfig as $type => $autoloads) { + assertType('array|string, array|string>', $autoloadConfig[$type]); + if ($type === 'psr-0' || $type === 'psr-4') { + + } elseif ($type === 'classmap') { + assertType('list', $autoloadConfig[$type]); + implode(', ', $autoloadConfig[$type]); + } + } + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8649.php b/tests/PHPStan/Rules/Arrays/data/bug-8649.php new file mode 100644 index 0000000000..f23eb8f516 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8649.php @@ -0,0 +1,25 @@ + 'test'], + ['b' => 'asdf'], + ]; + + foreach ($test as $property) { + $firstKey = array_key_first($property); + + if ($firstKey === 'b') { + continue; + } + + echo($property[$firstKey]); + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-9991.php b/tests/PHPStan/Rules/Arrays/data/bug-9991.php new file mode 100644 index 0000000000..c080a1d730 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-9991.php @@ -0,0 +1,14 @@ + 2, + 100 => 3, + 'This key is ignored' => 42, + 4, // Key is 101 + 10 => 5, + 6, // Key is 102 + 101 => 7, + 102 => 8, + ]; + + $foo2 = [ + '-42' => 1, + 2, // The key is -41 + 0 => 3, + -41 => 4, + ]; + + $foo3 = [ + $int => 33, + 0 => 1, + 2, // Because of `$int` key, the key value cannot be known. + 1 => 3, + ]; + + $foo4 = [ + 1, + 2, + 3, + ]; + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/empty-array-item.php b/tests/PHPStan/Rules/Arrays/data/empty-array-item.php deleted file mode 100644 index 4a08a799a8..0000000000 --- a/tests/PHPStan/Rules/Arrays/data/empty-array-item.php +++ /dev/null @@ -1,7 +0,0 @@ -= 8.0 + +namespace ForeachMixed; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + foreach ($t as $v) { + } + + foreach ($explicit as $v) { + } + + foreach ($implicit as $v) { + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php b/tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php new file mode 100644 index 0000000000..f57bd14122 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php @@ -0,0 +1,91 @@ + $val) { echo $array[$i]; } + +/** @var mixed $mixed */ +$mixed = null; +$a[$mixed]; + +/** @var array> $array */ +$array = doFoo(); +$array[new \DateTimeImmutable()][5]; +$array[5][new \DateTimeImmutable()]; +$array[new \stdClass()][new \DateTimeImmutable()]; +$array[new \DateTimeImmutable()][] = 5; diff --git a/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item-enum.php b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item-enum.php new file mode 100644 index 0000000000..f7d2790525 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item-enum.php @@ -0,0 +1,16 @@ += 8.1 + +namespace InvalidKeyArrayItemEnum; + +enum FooEnum +{ + case A; + case B; +} + +function doFoo(): void +{ + $a = [ + FooEnum::A => 5, + ]; +} diff --git a/tests/PHPStan/Rules/Arrays/data/narrow-superglobal.php b/tests/PHPStan/Rules/Arrays/data/narrow-superglobal.php new file mode 100644 index 0000000000..83edeb9fd5 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/narrow-superglobal.php @@ -0,0 +1,16 @@ += 7.4 + */ diff --git a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php index 065d636722..4509a69a2b 100644 --- a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php +++ b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php @@ -508,3 +508,13 @@ private function postprocess(array $versionData): array return $versionData; } } + +class OnBool +{ + + public function doFoo(bool $b) + { + $b['foo'] = 1; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php b/tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php new file mode 100644 index 0000000000..f5189b550f --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php @@ -0,0 +1,11 @@ += 8.0 + +namespace OffsetAccessLegal; + +function closure(): void +{ + (function(){})[0] ?? "error"; +} + +function nonArrayAccessibleObject() +{ + (new \stdClass())[0] ?? "error"; +} + +function arrayAccessibleObject() +{ + (new class implements \ArrayAccess { + public function offsetExists($offset) { + return true; + } + + public function offsetGet($offset) { + return $offset; + } + + public function offsetSet($offset, $value) { + } + + public function offsetUnset($offset) { + } + })[0] ?? "ok"; +} + +function array_(): void +{ + [0][0] ?? "ok"; +} + +function integer(): void +{ + (0)[0] ?? 'ok'; +} + +function float(): void +{ + (0.0)[0] ?? 'ok'; +} + +function null(): void +{ + (null)[0] ?? 'ok'; +} + +function bool(): void +{ + (true)[0] ?? 'ok'; +} + +function void(): void +{ + ((function (){})())[0] ?? 'ok'; +} + +function resource(): void +{ + (tmpfile())[0] ?? 'ok'; +} + +function offsetAccessibleMaybeAndLegal(): void +{ + $arrayAccessible = rand() ? (new class implements \ArrayAccess { + public function offsetExists($offset) { + return true; + } + + public function offsetGet($offset) { + return $offset; + } + + public function offsetSet($offset, $value) { + } + + public function offsetUnset($offset) { + } + }) : false; + + ($arrayAccessible)[0] ?? "ok"; + + (rand() ? "string" : true)[0] ?? "ok"; +} + +function offsetAccessibleMaybeAndIllegal(): void +{ + $arrayAccessible = rand() ? new \stdClass() : ['test']; + + ($arrayAccessible)[0] ?? "error"; + + (rand() ? function(){} : ['test'])[0] ?? "error"; +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-mixed.php b/tests/PHPStan/Rules/Arrays/data/offset-access-mixed.php new file mode 100644 index 0000000000..9f3300ce65 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-mixed.php @@ -0,0 +1,22 @@ += 8.0 + +namespace OffsetAccessMixed; + +/** + * @template T + * @param T $a + */ +function foo(mixed $a): void +{ + var_dump($a[5]); +} + +function foo2(mixed $a): void +{ + var_dump($a[5]); +} + +function foo3($a): void +{ + var_dump($a[5]); +} diff --git a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php new file mode 100644 index 0000000000..fc54d96f00 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php @@ -0,0 +1,60 @@ + 1]; + echo $a[$s]; + } + + /** + * @param array{bool|float|int|string|null} $a + * @return void + */ + public function testConstantArray(array $a): void + { + echo $a[0]; + } + + /** + * @param array $a + * @return void + */ + public function testConstantArray2(array $a): void + { + if (isset($a[0])) { + echo $a[0]; + } + } + + /** + * @param array{0: '9', A: 'Z', a: 'z'} $a + * @param '0'|'A'|'a' $dim + */ + public function testDimUnion(array $a, string $dim): void + { + echo $a[$dim]; + } + + /** + * @param non-empty-list $a + */ + public function nonEmpty(array $a): void + { + echo $a[0]; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php new file mode 100644 index 0000000000..0588be365b --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php @@ -0,0 +1,43 @@ + $itemsCount) { + if ($percentageInterval->isInInterval((float) $changeInPercents)) { + $key = $percentageInterval->getFormatted(); + if (array_key_exists($key, $intervalResults)) { + assertType('array', $intervalResults); + assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); + $intervalResults[$key]['itemsCount'] += $itemsCount; + assertType('non-empty-array', $intervalResults); + assertType('array{itemsCount: (array|float|int), interval: mixed}', $intervalResults[$key]); + } else { + assertType('array', $intervalResults); + assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); + $intervalResults[$key] = [ + 'itemsCount' => $itemsCount, + 'interval' => $percentageInterval, + ]; + assertType('non-empty-array', $intervalResults); + assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); + } + } + } + } + + assertType('array', $intervalResults); + foreach ($intervalResults as $data) { + echo $data['interval']; + } + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-unset-bug.php b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-unset-bug.php new file mode 100644 index 0000000000..e0b2af80c3 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-unset-bug.php @@ -0,0 +1,33 @@ +, isActive: bool, productsCount: int} */ + private $foreignSection; + + public function doFoo() + { + // Detect if foreign countries are visible + foreach ($this->foreignSection['items'] as $foreignCountryNo => $foreignCountryItem) { + if ($foreignCountryItem->count > 0) { + continue; + } + + assertType('array{items: array, isActive: bool, productsCount: int}', $this->foreignSection); + assertType('array', $this->foreignSection['items']); + unset($this->foreignSection['items'][$foreignCountryNo]); + assertType('array{items: array, isActive: bool, productsCount: int}', $this->foreignSection); + assertType('array', $this->foreignSection['items']); + } + + assertType('array{items: array, isActive: bool, productsCount: int}', $this->foreignSection); + assertType('array', $this->foreignSection['items']); + $countriesItems = $this->foreignSection['items']; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/specify-existent-offset-when-entering-foreach.php b/tests/PHPStan/Rules/Arrays/data/specify-existent-offset-when-entering-foreach.php new file mode 100644 index 0000000000..18cfef7aea --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/specify-existent-offset-when-entering-foreach.php @@ -0,0 +1,22 @@ + 0, 'lib-' => 0, 'php' => 99, 'composer' => 99]; + foreach ($hintsToFind as $hintPrefix => $hintCount) { + if (str_starts_with($s, $hintPrefix)) { + if ($hintCount === 0 || $hintCount >= 99) { + $hintsToFind[$hintPrefix]++; + } elseif ($hintCount === 1) { + unset($hintsToFind[$hintPrefix]); + } + } + } + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/unpack-iterable.php b/tests/PHPStan/Rules/Arrays/data/unpack-iterable.php index 84a24f9421..2c2262f6a1 100644 --- a/tests/PHPStan/Rules/Arrays/data/unpack-iterable.php +++ b/tests/PHPStan/Rules/Arrays/data/unpack-iterable.php @@ -1,4 +1,4 @@ -= 7.4 += 8.0 + +namespace UnpackMixed; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + var_dump([...$t]); + var_dump([...$explicit]); + var_dump([...$implicit]); +} diff --git a/tests/PHPStan/Rules/Cast/EchoRuleTest.php b/tests/PHPStan/Rules/Cast/EchoRuleTest.php index 0938e0f633..f08a205a79 100644 --- a/tests/PHPStan/Rules/Cast/EchoRuleTest.php +++ b/tests/PHPStan/Rules/Cast/EchoRuleTest.php @@ -16,7 +16,7 @@ class EchoRuleTest extends RuleTestCase protected function getRule(): Rule { return new EchoRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -47,6 +47,10 @@ public function testEchoRule(): void 'Parameter #1 (\'string\'|array{\'string\'}) of echo cannot be converted to string.', 17, ], + [ + 'Parameter #1 (array{}) of echo cannot be converted to string.', + 29, + ], ]); } diff --git a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php index 67bb979abb..e5d5f11c2a 100644 --- a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -13,10 +15,14 @@ class InvalidCastRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, false)); + return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true)); } public function testRule(): void @@ -34,6 +40,10 @@ public function testRule(): void 'Cannot cast stdClass to float.', 24, ], + [ + 'Cannot cast object to string.', + 35, + ], [ 'Cannot cast Test\\Foo to string.', 41, @@ -64,4 +74,102 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function testCastObjectToString(): void + { + $this->analyse([__DIR__ . '/data/cast-object-to-string.php'], [ + [ + 'Cannot cast object to string.', + 12, + ], + [ + 'Cannot cast object|string to string.', + 13, + ], + ]); + } + + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Cannot cast T to int.', + 11, + ], + [ + 'Cannot cast T to float.', + 13, + ], + [ + 'Cannot cast T to string.', + 14, + ], + [ + 'Cannot cast mixed to int.', + 18, + ], + [ + 'Cannot cast mixed to float.', + 20, + ], + [ + 'Cannot cast mixed to string.', + 21, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Cannot cast mixed to int.', + 25, + ], + [ + 'Cannot cast mixed to float.', + 27, + ], + [ + 'Cannot cast mixed to string.', + 28, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkImplicitMixed = $checkImplicitMixed; + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/mixed-cast.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php index 3080401757..642b296e4f 100644 --- a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php @@ -2,7 +2,8 @@ namespace PHPStan\Rules\Cast; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -17,8 +18,8 @@ class InvalidPartOfEncapsedStringRuleTest extends RuleTestCase protected function getRule(): Rule { return new InvalidPartOfEncapsedStringRule( - new Standard(), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), + new ExprPrinter(new Printer()), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), ); } diff --git a/tests/PHPStan/Rules/Cast/PrintRuleTest.php b/tests/PHPStan/Rules/Cast/PrintRuleTest.php index 1311c3e8ab..fb0e7991fd 100644 --- a/tests/PHPStan/Rules/Cast/PrintRuleTest.php +++ b/tests/PHPStan/Rules/Cast/PrintRuleTest.php @@ -16,7 +16,7 @@ class PrintRuleTest extends RuleTestCase protected function getRule(): Rule { return new PrintRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), ); } diff --git a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php index 923423b1e5..ebab5c0aa6 100644 --- a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php @@ -40,7 +40,7 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param mixed[] $errors + * @param list $errors */ public function testRule(int $phpVersion, array $errors): void { diff --git a/tests/PHPStan/Rules/Cast/data/cast-object-to-string.php b/tests/PHPStan/Rules/Cast/data/cast-object-to-string.php new file mode 100644 index 0000000000..a4a1c74a2d --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/cast-object-to-string.php @@ -0,0 +1,22 @@ += 8.0 + +namespace MixedCast; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + var_dump((int) $t); + var_dump((bool) $t); + var_dump((float) $t); + var_dump((string) $t); + var_dump((array) $t); + var_dump((object) $t); + + var_dump((int) $explicit); + var_dump((bool) $explicit); + var_dump((float) $explicit); + var_dump((string) $explicit); + var_dump((array) $explicit); + var_dump((object) $explicit); + + var_dump((int) $implicit); + var_dump((bool) $implicit); + var_dump((float) $implicit); + var_dump((string) $implicit); + var_dump((array) $implicit); + var_dump((object) $implicit); +} diff --git a/tests/PHPStan/Rules/Classes/AllowedSubTypesRuleTest.php b/tests/PHPStan/Rules/Classes/AllowedSubTypesRuleTest.php new file mode 100644 index 0000000000..403a35e6ff --- /dev/null +++ b/tests/PHPStan/Rules/Classes/AllowedSubTypesRuleTest.php @@ -0,0 +1,37 @@ + + */ +class AllowedSubTypesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AllowedSubTypesRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/allowed-sub-types.php'], [ + [ + 'Type AllowedSubTypes\\Baz is not allowed to be a subtype of AllowedSubTypes\\Foo.', + 11, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../../conf/bleedingEdge.neon', + __DIR__ . '/data/allowed-sub-types.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 755cdeed72..09ab4668c8 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -20,6 +21,10 @@ class ClassAttributesRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); @@ -27,9 +32,8 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -37,17 +41,19 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/class-attributes.php'], [ [ 'Attribute class ClassAttributes\Nonexistent does not exist.', @@ -105,12 +111,16 @@ public function testRule(): void 'Trait ClassAttributes\TraitAsAttribute is not an Attribute class.', 142, ], + [ + 'Attribute class ClassAttributes\FlagsAttributeWithPropertyTarget does not have the class target.', + 164, + ], ]); } public function testRuleForEnums(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80100) { + if (PHP_VERSION_ID < 80100) { $this->markTestSkipped('Test requires PHP 8.1.'); } @@ -126,4 +136,59 @@ public function testRuleForEnums(): void ]); } + public function testBug7171(): void + { + $this->analyse([__DIR__ . '/data/bug-7171.php'], [ + [ + 'Parameter $repositoryClass of attribute class Bug7171\Entity constructor expects class-string>|null, \'stdClass\' given.', + 66, + ], + ]); + } + + public function testAllowDynamicPropertiesAttribute(): void + { + $this->analyse([__DIR__ . '/data/allow-dynamic-properties-attribute.php'], []); + } + + public function testBug12011(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12011.php'], [ + [ + 'Parameter #1 $name of attribute class Bug12011\Table constructor expects string|null, int given.', + 23, + ], + ]); + } + + public function testBug12281(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12281.php'], [ + [ + 'Attribute class AllowDynamicProperties cannot be used with readonly class.', + 05, + ], + [ + 'Attribute class AllowDynamicProperties cannot be used with enum.', + 12, + ], + [ + 'Attribute class AllowDynamicProperties cannot be used with interface.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index 386d9453ab..93190d705a 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -12,7 +13,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -27,9 +27,8 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -37,17 +36,19 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/class-constant-attributes.php'], [ [ 'Attribute class ClassConstantAttributes\Foo does not have the class constant target.', diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index d116959773..7fda6784cf 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -19,8 +21,18 @@ class ClassConstantRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker, true), new PhpVersion($this->phpVersion)); + $reflectionProvider = $this->createReflectionProvider(); + return new ClassConstantRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new PhpVersion($this->phpVersion), + ); } public function testClassConstant(): void @@ -88,10 +100,6 @@ public function testClassConstant(): void public function testClassConstantVisibility(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test does not run on PHP 7.4 because of referencing parent:: without parent class.'); - } - $this->phpVersion = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/class-constant-visibility.php'], [ [ @@ -203,6 +211,10 @@ public function dataClassConstantOnExpression(): array 'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.', 18, ], + [ + 'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.', + 19, + ], ], ], [ @@ -227,23 +239,16 @@ public function dataClassConstantOnExpression(): array /** * @dataProvider dataClassConstantOnExpression - * @param mixed[] $errors + * @param list $errors */ public function testClassConstantOnExpression(int $phpVersion, array $errors): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } $this->phpVersion = $phpVersion; $this->analyse([__DIR__ . '/data/class-constant-on-expr.php'], $errors); } public function testAttributes(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersion = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/class-constant-attribute.php'], [ [ @@ -287,4 +292,193 @@ public function testRuleWithNullsafeVariant(): void $this->analyse([__DIR__ . '/data/class-constant-nullsafe.php'], []); } + public function testBug7675(): void + { + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-7675.php'], []); + } + + public function testBug8034(): void + { + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-8034.php'], [ + [ + 'Access to undefined constant static(Bug8034\HelloWorld)::FIELDS.', + 19, + ], + ]); + } + + public function testClassConstFetchDefined(): void + { + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/class-const-fetch-defined.php'], [ + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 12, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 14, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 16, + ], + [ + 'Access to undefined constant Foo::TEST.', + 17, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 18, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 22, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 24, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 26, + ], + [ + 'Access to undefined constant Foo::TEST.', + 27, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 28, + ], + [ + 'Access to undefined constant Foo::TEST.', + 33, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 36, + ], + [ + 'Access to undefined constant Foo::TEST.', + 37, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 38, + ], + [ + 'Access to undefined constant Foo::TEST.', + 43, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 46, + ], + [ + 'Access to undefined constant Foo::TEST.', + 47, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 48, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 52, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 54, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 56, + ], + [ + 'Access to undefined constant Foo::TEST.', + 57, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 58, + ], + ]); + } + + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; + + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.', + 28, + $tip, + ], + ]); + } + + public function testClassConstantAccessedOnTrait(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2.'); + } + + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/class-constant-accessed-on-trait.php'], [ + [ + 'Cannot access constant TEST on trait ClassConstantAccessedOnTrait\Foo.', + 16, + ], + ]); + } + + public function testDynamicAccess(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->phpVersion = PHP_VERSION_ID; + + $this->analyse([__DIR__ . '/data/dynamic-constant-access.php'], [ + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.', + 20, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::BUZ.', + 20, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.', + 37, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::BUZ.', + 39, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::QUX.', + 41, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::QUX.', + 44, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::BUZ.', + 44, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.', + 44, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/DuplicateClassDeclarationRuleTest.php b/tests/PHPStan/Rules/Classes/DuplicateClassDeclarationRuleTest.php new file mode 100644 index 0000000000..e16ccce93c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/DuplicateClassDeclarationRuleTest.php @@ -0,0 +1,52 @@ + + */ +class DuplicateClassDeclarationRuleTest extends RuleTestCase +{ + + private const FILENAME = __DIR__ . '/data/duplicate-class.php'; + + protected function getRule(): Rule + { + $fileHelper = new FileHelper(__DIR__ . '/data'); + + return new DuplicateClassDeclarationRule( + new DefaultReflector(new OptimizedSingleFileSourceLocator( + self::getContainer()->getByType(FileNodesFetcher::class), + self::FILENAME, + )), + new SimpleRelativePathHelper($fileHelper->normalizePath($fileHelper->getWorkingDirectory(), '/')), + ); + } + + public function testRule(): void + { + $this->analyse([self::FILENAME], [ + [ + "Class DuplicateClassDeclaration\Foo declared multiple times:\n- duplicate-class.php:15\n- duplicate-class.php:20", + 10, + ], + [ + "Class DuplicateClassDeclaration\Foo declared multiple times:\n- duplicate-class.php:10\n- duplicate-class.php:20", + 15, + ], + [ + "Class DuplicateClassDeclaration\Foo declared multiple times:\n- duplicate-class.php:10\n- duplicate-class.php:15", + 20, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php index 32a29444be..5390b57ac9 100644 --- a/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -18,10 +19,6 @@ protected function getRule(): Rule public function testDuplicateDeclarations(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse( [ __DIR__ . '/data/duplicate-declarations.php', @@ -57,10 +54,6 @@ public function testDuplicateDeclarations(): void public function testDuplicatePromotedProperty(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/duplicate-promoted-property.php'], [ [ 'Cannot redeclare property DuplicatedPromotedProperty\Foo::$foo.', @@ -75,8 +68,8 @@ public function testDuplicatePromotedProperty(): void public function testDuplicateEnumCase(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); } $this->analyse([__DIR__ . '/data/duplicate-enum-cases.php'], [ diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php index 0e2a473353..af02a79635 100644 --- a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -19,19 +19,16 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); } - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - - $this->analyse([__DIR__ . '/data/enum-sanity.php'], [ - [ + $expected = [ + /*[ + // reported by AbstractMethodInNonAbstractClassRule 'Enum EnumSanity\EnumWithAbstractMethod contains abstract method foo().', 7, - ], + ],*/ [ 'Enum EnumSanity\EnumWithConstructorAndDestructor contains constructor.', 12, @@ -72,6 +69,84 @@ public function testRule(): void 'Backed enum EnumSanity\BackedEnumWithBoolType can have only "int" or "string" type.', 71, ], + [ + 'Enum EnumSanity\EnumWithSerialize contains magic method __serialize().', + 78, + ], + [ + 'Enum EnumSanity\EnumWithSerialize contains magic method __unserialize().', + 81, + ], + [ + 'Enum EnumSanity\EnumDuplicateValue has duplicate value 1 for cases A, E.', + 86, + ], + [ + 'Enum EnumSanity\EnumDuplicateValue has duplicate value 2 for cases B, C.', + 86, + ], + [ + 'Enum case EnumSanity\EnumInconsistentCaseType::FOO value \'foo\' does not match the "int" type.', + 105, + ], + [ + 'Enum case EnumSanity\EnumInconsistentCaseType::BAR does not have a value but the enum is backed with the "int" type.', + 106, + ], + [ + 'Enum case EnumSanity\EnumInconsistentStringCaseType::BAR does not have a value but the enum is backed with the "string" type.', + 110, + ], + [ + 'Enum EnumSanity\EnumWithValueButNotBacked is not backed, but case FOO has value 1.', + 114, + ], + [ + 'Enum EnumSanity\EnumMayNotSerializable cannot implement the Serializable interface.', + 117, + ], + ]; + + $this->analyse([__DIR__ . '/data/enum-sanity.php'], $expected); + } + + public function testBug9402(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/bug-9402.php'], [ + [ + 'Enum case Bug9402\Foo::Two value \'foo\' does not match the "int" type.', + 13, + ], + ]); + } + + public function testBug11592(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/bug-11592.php'], [ + [ + 'Enum Bug11592\Test2 cannot redeclare native method cases().', + 22, + ], + [ + 'Enum Bug11592\BackedTest2 cannot redeclare native method cases().', + 37, + ], + [ + 'Enum Bug11592\BackedTest2 cannot redeclare native method from().', + 39, + ], + [ + 'Enum Bug11592\BackedTest2 cannot redeclare native method tryFrom().', + 41, + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php index f430161628..83d23411de 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,16 @@ class ExistingClassInClassExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInClassExtendsRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -38,10 +46,6 @@ public function testRule(): void public function testRuleExtendsError(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/extends-error.php'], [ [ 'Class ExtendsError\Foo extends unknown class ExtendsError\Bar.', @@ -79,8 +83,8 @@ public function testFinalByTag(): void public function testEnums(): void { - if (!self::$useStaticReflectionProvider || PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs static reflection and PHP 8.1'); + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); } $this->analyse([__DIR__ . '/data/class-extends-enum.php'], [ @@ -95,4 +99,63 @@ public function testEnums(): void ]); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; + + $this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.', + 34, + $tip, + ], + [ + 'Referencing prefixed Rector class: RectorPrefix202302\AClass.', + 56, + $tip, + ], + [ + 'Referencing prefixed PHP-Scoper class: _PhpScoper19ae93be897e\AClass.', + 59, + $tip, + ], + [ + 'Referencing prefixed PHPUnit class: PHPUnitPHAR\SebastianBergmann\Diff\Exception.', + 62, + 'This is most likely unintentional. Did you mean to type \SebastianBergmann\Diff\Exception?', + ], + [ + 'Referencing prefixed Box class: _HumbugBox02f3b3909847\AClass.', + 73, + $tip, + ], + ]); + } + + public function testReadonly(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('This test needs PHP 8.2'); + } + + $this->analyse([__DIR__ . '/data/extends-readonly-class.php'], [ + [ + 'Readonly class ExtendsReadOnlyClass\Foo extends non-readonly class ExtendsReadOnlyClass\Nonreadonly.', + 25, + ], + [ + 'Non-readonly class ExtendsReadOnlyClass\Bar extends readonly class ExtendsReadOnlyClass\ReadonlyClass.', + 30, + ], + [ + 'Anonymous non-readonly class extends readonly class ExtendsReadOnlyClass\ReadonlyClass.', + 35, + ], + [ + 'Anonymous readonly class extends non-readonly class ExtendsReadOnlyClass\Nonreadonly.', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php index e346e8cb1a..26cf177300 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php @@ -3,8 +3,11 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -12,16 +15,29 @@ class ExistingClassInInstanceOfRuleTest extends RuleTestCase { + private bool $shouldNarrowMethodScopeFromConstructor = true; + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInInstanceOfRule( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, true, ); } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return $this->shouldNarrowMethodScopeFromConstructor; + } + public function testClassDoesNotExist(): void { $this->analyse( @@ -31,7 +47,7 @@ public function testClassDoesNotExist(): void ], [ [ - 'Class InstanceOfNamespace\Bar not found.', + 'Class InstanceOfNamespaceRule\Bar not found.', 7, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], @@ -40,7 +56,7 @@ public function testClassDoesNotExist(): void 9, ], [ - 'Class InstanceOfNamespace\Foo referenced with incorrect case: InstanceOfNamespace\FOO.', + 'Class InstanceOfNamespaceRule\Foo referenced with incorrect case: InstanceOfNamespaceRule\FOO.', 13, ], [ @@ -60,4 +76,41 @@ public function testClassExists(): void $this->analyse([__DIR__ . '/data/instanceof-class-exists.php'], []); } + public function testBug7720(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-7720.php'], [ + [ + 'Instanceof between mixed and trait Bug7720\FooBar will always evaluate to false.', + 17, + ], + ]); + } + + public function testRememberClassExistsFromConstructorDisabled(): void + { + $this->shouldNarrowMethodScopeFromConstructor = false; + + $this->analyse([__DIR__ . '/data/remember-class-exists-from-constructor.php'], [ + [ + 'Class SomeUnknownClass not found.', + 19, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Class SomeUnknownInterface not found.', + 38, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testRememberClassExistsFromConstructor(): void + { + $this->analyse([__DIR__ . '/data/remember-class-exists-from-constructor.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php index 616fbce43c..f6e5ac6b5e 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,16 @@ class ExistingClassInTraitUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInTraitUseRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -34,10 +42,6 @@ public function testClassWithWrongCase(): void public function testTraitUseError(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/trait-use-error.php'], [ [ 'Class TraitUseError\Foo uses unknown trait TraitUseError\FooTrait.', @@ -70,8 +74,8 @@ 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'); + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); } $this->analyse([__DIR__ . '/data/trait-use-enum.php'], [ diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php index 2e8a77f71a..d51a34a621 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,16 @@ class ExistingClassesInClassImplementsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInClassImplementsRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -34,10 +42,6 @@ public function testRule(): void public function testRuleImplementsError(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/implements-error.php'], [ [ 'Class ImplementsError\Foo implements unknown interface ImplementsError\Bar.', @@ -61,8 +65,8 @@ 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'); + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); } $this->analyse([__DIR__ . '/data/class-implements-enum.php'], [ @@ -77,4 +81,20 @@ public function testEnums(): void ]); } + public function testBug8889(): void + { + $this->analyse([__DIR__ . '/data/bug-8889.php'], [ + [ + 'Class Bug8889\HelloWorld implements unknown interface iterable.', + 5, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Class Bug8889\HelloWorld2 implements unknown interface Iterable.', + 8, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php index f2336b860c..7ed5797895 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -18,15 +20,21 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInEnumImplementsRule( - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), $reflectionProvider, + true, ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider || PHP_VERSION_ID < 80100) { - self::markTestSkipped('Test requires PHP 8.1 and static reflection.'); + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); } $this->analyse([__DIR__ . '/data/enum-implements.php'], [ diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php index d1c02403ee..c6a3db9ad4 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,16 @@ class ExistingClassesInInterfaceExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInInterfaceExtendsRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -34,10 +42,6 @@ public function testRule(): void public function testRuleExtendsError(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('This test needs static reflection'); - } - $this->analyse([__DIR__ . '/data/interface-extends-error.php'], [ [ 'Interface InterfaceExtendsError\Foo extends unknown interface InterfaceExtendsError\Bar.', @@ -57,8 +61,8 @@ 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'); + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); } $this->analyse([__DIR__ . '/data/interface-extends-enum.php'], [ diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php new file mode 100644 index 0000000000..93e202ef4e --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -0,0 +1,62 @@ + + */ +class ForbiddenNameCheckExtensionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new InstantiationRule( + $reflectionProvider, + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + ); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge(parent::getAdditionalConfigFiles(), [ + __DIR__ . '/data/forbidden-name-class-extension.neon', + ]); + } + + public function testInternalClassFromExtensions(): void + { + $this->analyse([__DIR__ . '/data/forbidden-name-class-extension.php'], [ + [ + 'Referencing prefixed Doctrine class: App\GeneratedProxy\__CG__\App\TestDoctrineEntity.', + 31, + 'This is most likely unintentional. Did you mean to type \App\TestDoctrineEntity?', + ], + [ + 'Referencing prefixed PHPStan class: _PHPStan_15755dag8c\TestPhpStanEntity.', + 32, + 'This is most likely unintentional. Did you mean to type \TestPhpStanEntity?', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index dde8dcb7b6..2ef0c3d184 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -11,13 +12,17 @@ class ImpossibleInstanceOfRuleTest extends RuleTestCase { - private bool $checkAlwaysTrueInstanceOf; - private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { - return new ImpossibleInstanceOfRule($this->checkAlwaysTrueInstanceOf, $this->treatPhpDocTypesAsCertain); + return new ImpossibleInstanceOfRule( + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -27,7 +32,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool public function testInstanceof(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse( @@ -62,7 +66,7 @@ public function testInstanceof(): void 94, ], [ - 'Instanceof between string and string will always evaluate to false.', + 'Instanceof between string and \'str\' will always evaluate to false.', 98, ], [ @@ -146,14 +150,14 @@ public function testInstanceof(): void 'Instanceof between ImpossibleInstanceOf\Bar and ImpossibleInstanceOf\BarGrandChild will always evaluate to false.', 322, ], - [ + /*[ 'Instanceof between mixed and int results in an error.', 353, ], [ 'Instanceof between mixed and ImpossibleInstanceOf\InvalidTypeTest|int results in an error.', 362, - ], + ],*/ [ 'Instanceof between ImpossibleInstanceOf\Foo and ImpossibleInstanceOf\Foo will always evaluate to true.', 388, @@ -167,14 +171,15 @@ public function testInstanceof(): void [ 'Instanceof between class-string and class-string will always evaluate to false.', 419, + $tipText, ], [ - 'Instanceof between class-string and string will always evaluate to false.', + 'Instanceof between class-string and \'DateTimeInterface\' will always evaluate to false.', 432, $tipText, ], [ - 'Instanceof between DateTimeInterface and string will always evaluate to true.', + 'Instanceof between DateTimeInterface and \'DateTimeInterface\' will always evaluate to true.', 433, $tipText, ], @@ -182,101 +187,8 @@ public function testInstanceof(): void ); } - public function testInstanceofWithoutAlwaysTrue(): void - { - $this->checkAlwaysTrueInstanceOf = false; - $this->treatPhpDocTypesAsCertain = true; - - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->analyse( - [__DIR__ . '/data/impossible-instanceof.php'], - [ - [ - 'Instanceof between ImpossibleInstanceOf\Dolor and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 71, - ], - [ - 'Instanceof between string and ImpossibleInstanceOf\Foo will always evaluate to false.', - 94, - ], - [ - 'Instanceof between string and string will always evaluate to false.', - 98, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Test|null and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 119, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Test|null and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 137, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Test|null and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 155, - ], - [ - 'Instanceof between callable and ImpossibleInstanceOf\FinalClassWithoutInvoke will always evaluate to false.', - 204, - ], - [ - 'Instanceof between *NEVER* and ImpossibleInstanceOf\Lorem will always evaluate to false.', - 228, - ], - [ - 'Instanceof between *NEVER* and ImpossibleInstanceOf\Foo will always evaluate to false.', - 234, - ], - [ - 'Instanceof between *NEVER* and ImpossibleInstanceOf\Bar will always evaluate to false.', - 240, - //$tipText, - ], - [ - 'Instanceof between object and Exception will always evaluate to false.', - 303, - ], - [ - 'Instanceof between object and InvalidArgumentException will always evaluate to false.', - 307, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Bar and ImpossibleInstanceOf\BarChild will always evaluate to false.', - 318, - ], - [ - 'Instanceof between ImpossibleInstanceOf\Bar and ImpossibleInstanceOf\BarGrandChild will always evaluate to false.', - 322, - ], - [ - 'Instanceof between mixed and int results in an error.', - 353, - ], - [ - 'Instanceof between mixed and ImpossibleInstanceOf\InvalidTypeTest|int results in an error.', - 362, - ], - [ - 'Instanceof between class-string and DateTimeInterface will always evaluate to false.', - 418, - $tipText, - ], - [ - 'Instanceof between class-string and class-string will always evaluate to false.', - 419, - ], - [ - 'Instanceof between class-string and string will always evaluate to false.', - 432, - $tipText, - ], - ], - ); - } - public function testDoNotReportTypesFromPhpDocs(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ [ @@ -288,11 +200,11 @@ public function testDoNotReportTypesFromPhpDocs(): void 15, ], [ - 'Instanceof between DateTimeImmutable and DateTimeInterface will always evaluate to true.', + 'Instanceof between DateTimeInterface and DateTimeInterface will always evaluate to true.', 27, ], [ - 'Instanceof between DateTimeImmutable and ImpossibleInstanceofNotPhpDoc\SomeFinalClass will always evaluate to false.', + 'Instanceof between DateTimeInterface and ImpossibleInstanceofNotPhpDoc\SomeFinalClass will always evaluate to false.', 30, ], ]); @@ -300,7 +212,6 @@ public function testDoNotReportTypesFromPhpDocs(): void public function testReportTypesFromPhpDocs(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ [ @@ -334,9 +245,302 @@ public function testReportTypesFromPhpDocs(): void public function testBug3096(): void { - $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3096.php'], []); } + public function testBug6213(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6213.php'], []); + } + + public function testBug5333(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-5333.php'], []); + } + + public function testBug8042(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('This test needs PHP 8.0'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8042.php'], [ + [ + 'Instanceof between Bug8042\B and Bug8042\B will always evaluate to true.', + 18, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between Bug8042\B and Bug8042\B will always evaluate to true.', + 26, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testBug7721(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7721.php'], []); + } + + public function testUnreachableIfBranches(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches.php'], [ + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 5, + ], + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 13, + ], + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 23, + ], + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 37, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testIfBranchesDoNotReportPhpDoc(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 26, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 36, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testIfBranchesReportPhpDoc(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 26, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 36, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 42, + $tipText, + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 52, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 62, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testUnreachableTernaryElse(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch.php'], [ + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 6, + ], + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 9, + ], + ]); + } + + public function testTernaryElseDoNotReportPhpDoc(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 17, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 20, + ], + ]); + } + + public function testTernaryElseReportPhpDoc(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 17, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 19, + $tipText, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 20, + ], + ]); + } + + public function testBug4689(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-4689.php'], []); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Instanceof between Exception and Exception will always evaluate to true.', + 21, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Instanceof between Exception and Exception will always evaluate to true.', + 12, + ], + [ + 'Instanceof between Exception and Exception will always evaluate to true.', + 21, + ], + [ + 'Instanceof between DateTime and DateTime will always evaluate to true.', + 34, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/impossible-instanceof-report-always-true-last-condition.php'], $expectedErrors); + } + + public function testBug10201(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10201.php'], [ + [ + 'Instanceof between string and Bug10201\Hello will always evaluate to false.', + 13, + ], + ]); + } + + public function testBug3632(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/bug-3632.php'], [ + [ + 'Instanceof between Bug3632\NiceClass and Bug3632\NiceClass will always evaluate to true.', + 36, + $tipText, + ], + ]); + } + + public function testNewIsAlwaysFinalClass(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('This test needs PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impossible-instanceof-new-is-always-final.php'], [ + [ + 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar and ImpossibleInstanceofNewIsAlwaysFinal\Foo will always evaluate to false.', + 17, + ], + [ + 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar and ImpossibleInstanceofNewIsAlwaysFinal\Foo will always evaluate to false.', + 33, + ], + [ + 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar and ImpossibleInstanceofNewIsAlwaysFinal\Foo will always evaluate to false.', + 43, + ], + [ + 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar and ImpossibleInstanceofNewIsAlwaysFinal\Foo will always evaluate to false.', + 53, + ], + [ + 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar and ImpossibleInstanceofNewIsAlwaysFinal\Foo will always evaluate to false.', + 63, + ], + [ + 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar|null and ImpossibleInstanceofNewIsAlwaysFinal\Foo will always evaluate to false.', + 73, + ], + [ + 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar|null and ImpossibleInstanceofNewIsAlwaysFinal\Baz will always evaluate to false.', + 88, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/InstantiationCallableRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationCallableRuleTest.php index 427c0b3a91..d30f3a29f6 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationCallableRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationCallableRuleTest.php @@ -18,9 +18,6 @@ protected function getRule(): Rule 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.', diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index fc909c4369..eb547315cf 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -2,8 +2,9 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -21,19 +22,22 @@ class InstantiationRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new InstantiationRule( - $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ); } public function testInstantiation(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test does not run on PHP 7.4 because of referencing parent:: without parent class.'); - } $this->analyse( [__DIR__ . '/data/instantiation.php'], [ @@ -265,10 +269,6 @@ public function testBug4030(): void public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/instantiation-promoted-properties.php'], [ [ 'Parameter #2 $bar of class InstantiationPromotedProperties\Foo constructor expects array, array given.', @@ -278,6 +278,10 @@ public function testPromotedProperties(): void 'Parameter #2 $bar of class InstantiationPromotedProperties\Bar constructor expects array, array given.', 33, ], + [ + 'Parameter #1 $intProp of class InstantiationPromotedProperties\PromotedPropertyNotNullable constructor expects int, null given.', + 46, + ], ]); } @@ -288,8 +292,8 @@ public function testBug4056(): void public function testNamedArguments(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); } $this->analyse([__DIR__ . '/data/instantiation-named-arguments.php'], [ @@ -345,16 +349,12 @@ public function testBug5002(): void public function testBug4681(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/bug-4681.php'], []); } public function testFirstClassCallable(): void { - if (PHP_VERSION_ID < 80100 || !self::$useStaticReflectionProvider) { + if (PHP_VERSION_ID < 80100) { $this->markTestSkipped('Test requires PHP 8.1 and static reflection.'); } @@ -394,4 +394,166 @@ public function testBug6370(): void ]); } + public function testBug5553(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-5553.php'], []); + } + + public function testBug7048(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-7048.php'], [ + [ + 'Unknown parameter $recurrences in call to DatePeriod constructor.', + 21, + ], + [ + 'Missing parameter $end (DateTimeInterface|int) in call to DatePeriod constructor.', + 18, + ], + [ + 'Unknown parameter $isostr in call to DatePeriod constructor.', + 25, + ], + [ + 'Missing parameter $start (string) in call to DatePeriod constructor.', + 24, + ], + [ + 'Parameter #3 $end of class DatePeriod constructor expects DateTimeInterface|int, string given.', + 41, + ], + [ + 'Parameter $end of class DatePeriod constructor expects DateTimeInterface|int, string given.', + 49, + ], + ]); + } + + public function testBug7594(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-7594.php'], []); + } + + public function testBug3311a(): void + { + $this->analyse([__DIR__ . '/data/bug-3311a.php'], [ + [ + 'Parameter #1 $bar of class Bug3311a\Foo constructor expects list, array{1: \'baz\'} given.', + 24, + "array{1: 'baz'} is not a list.", + ], + ]); + } + + public function testBug9341(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9341.php'], []); + } + + public function testBug7574(): void + { + $this->analyse([__DIR__ . '/data/bug-7574.php'], []); + } + + public function testBug9946(): void + { + $this->analyse([__DIR__ . '/data/bug-9946.php'], []); + } + + public function testBug10324(): void + { + $this->analyse([__DIR__ . '/data/bug-10324.php'], [ + [ + 'Parameter #3 $flags of class RecursiveIteratorIterator constructor expects 0|16, 2 given.', + 23, + ], + ]); + } + + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; + + $this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.', + 30, + $tip, + ], + ]); + } + + public function testBug9659(): void + { + $this->analyse([__DIR__ . '/data/bug-9659.php'], []); + } + + public function testBug10248(): void + { + $this->analyse([__DIR__ . '/data/bug-10248.php'], []); + } + + public function testBug11815(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-11815.php'], []); + } + + public function testClassString(): void + { + $this->analyse([__DIR__ . '/data/class-string.php'], [ + [ + 'Parameter #1 $i of class ClassString\A constructor expects int, string given.', + 65, + ], + [ + 'Parameter #1 $i of class ClassString\A constructor expects int, string given.', + 66, + ], + [ + 'Parameter #1 $i of class ClassString\A constructor expects int, string given.', + 67, + ], + [ + 'Parameter #1 $i of class ClassString\C constructor expects int, string given.', + 75, + ], + [ + 'Parameter #1 $i of class ClassString\C constructor expects int, string given.', + 76, + ], + [ + 'Parameter #1 $i of class ClassString\C constructor expects int, string given.', + 77, + ], + [ + 'Parameter #1 $i of class ClassString\A constructor expects int, string given.', + 85, + ], + [ + 'Parameter #1 $i of class ClassString\A constructor expects int, string given.', + 86, + ], + [ + 'Parameter #1 $i of class ClassString\A constructor expects int, string given.', + 87, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php index 37946a3fd6..2ce3e7e268 100644 --- a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -21,9 +22,6 @@ protected function getRule(): Rule public function testNotSupportedOnPhp7(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->phpVersion = 70400; $this->analyse([__DIR__ . '/data/invalid-promoted-properties.php'], [ [ @@ -63,9 +61,6 @@ public function testNotSupportedOnPhp7(): void public function testSupportedOnPhp8(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->phpVersion = 80000; $this->analyse([__DIR__ . '/data/invalid-promoted-properties.php'], [ [ @@ -99,4 +94,29 @@ public function testSupportedOnPhp8(): void ]); } + public function testBug9577(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->phpVersion = 80100; + $this->analyse([__DIR__ . '/data/bug-9577.php'], []); + } + + public function testHooks(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->phpVersion = 80100; + $this->analyse([__DIR__ . '/data/invalid-hooked-properties.php'], [ + [ + 'Promoted properties can be in constructor only.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php index 281e125507..12631f5c74 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -3,6 +3,12 @@ namespace PHPStan\Rules\Classes; use PHPStan\PhpDoc\TypeNodeResolver; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +21,26 @@ class LocalTypeAliasesRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = $this->createReflectionProvider(); + return new LocalTypeAliasesRule( - ['GlobalTypeAlias' => 'int|string'], - $this->createReflectionProvider(), - self::getContainer()->getByType(TypeNodeResolver::class), + new LocalTypeAliasesCheck( + ['GlobalTypeAlias' => 'int|string'], + $this->createReflectionProvider(), + self::getContainer()->getByType(TypeNodeResolver::class), + new MissingTypehintCheck(true, []), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new GenericObjectTypeCheck(), + true, + true, + true, + ), ); } @@ -89,6 +111,40 @@ public function testRule(): void 'Invalid type definition detected in type alias InvalidTypeAlias.', 62, ], + [ + 'Class LocalTypeAliases\MissingTypehints has type alias NoIterableValue with no value type specified in iterable type array.', + 77, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Class LocalTypeAliases\MissingTypehints has type alias NoGenerics with generic class LocalTypeAliases\Generic but does not specify its types: T', + 77, + ], + [ + 'Class LocalTypeAliases\MissingTypehints has type alias NoCallable with no signature specified for callable.', + 77, + ], + [ + 'Type alias A contains unknown class LocalTypeAliases\Nonexistent.', + 87, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Type alias B contains invalid type LocalTypeTraitAliases\Foo.', + 87, + ], + [ + 'Class LocalTypeAliases\Foo referenced with incorrect case: LocalTypeAliases\fOO.', + 87, + ], + [ + 'Type alias A contains unresolvable type.', + 95, + ], + [ + 'Type alias A contains generic type Exception but class Exception is not generic.', + 103, + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php new file mode 100644 index 0000000000..4662acc732 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php @@ -0,0 +1,127 @@ + + */ +class LocalTypeTraitAliasesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new LocalTypeTraitAliasesRule( + new LocalTypeAliasesCheck( + ['GlobalTypeAlias' => 'int|string'], + $this->createReflectionProvider(), + self::getContainer()->getByType(TypeNodeResolver::class), + new MissingTypehintCheck(true, []), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new GenericObjectTypeCheck(), + true, + true, + true, + ), + $this->createReflectionProvider(), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/local-type-trait-aliases.php'], [ + [ + 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeTraitAliases\Bar.', + 23, + ], + [ + 'Type alias GlobalTypeAlias already exists as a global type alias.', + 23, + ], + [ + 'Type alias has an invalid name: int.', + 23, + ], + [ + 'Circular definition detected in type alias RecursiveTypeAlias.', + 23, + ], + [ + 'Circular definition detected in type alias CircularTypeAlias1.', + 23, + ], + [ + 'Circular definition detected in type alias CircularTypeAlias2.', + 23, + ], + [ + 'Cannot import type alias ImportedAliasFromNonClass: class LocalTypeTraitAliases\int does not exist.', + 39, + ], + [ + 'Cannot import type alias ImportedAliasFromUnknownClass: class LocalTypeTraitAliases\UnknownClass does not exist.', + 39, + ], + [ + 'Cannot import type alias ImportedUnknownAlias: type alias does not exist in LocalTypeTraitAliases\Foo.', + 39, + ], + [ + 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeTraitAliases\Baz.', + 39, + ], + [ + 'Type alias GlobalTypeAlias already exists as a global type alias.', + 39, + ], + [ + 'Imported type alias ExportedTypeAlias has an invalid name: int.', + 39, + ], + [ + 'Type alias OverwrittenTypeAlias overwrites an imported type alias of the same name.', + 39, + ], + [ + 'Circular definition detected in type alias CircularTypeAliasImport2.', + 39, + ], + [ + 'Circular definition detected in type alias CircularTypeAliasImport1.', + 47, + ], + [ + 'Invalid type definition detected in type alias InvalidTypeAlias.', + 62, + ], + [ + 'Trait LocalTypeTraitAliases\MissingType has type alias NoIterablueValue with no value type specified in iterable type array.', + 69, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + ]); + } + + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Classes/LocalTypeTraitUseAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeTraitUseAliasesRuleTest.php new file mode 100644 index 0000000000..0748cdb4b6 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/LocalTypeTraitUseAliasesRuleTest.php @@ -0,0 +1,80 @@ + + */ +class LocalTypeTraitUseAliasesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new LocalTypeTraitUseAliasesRule( + new LocalTypeAliasesCheck( + ['GlobalTypeAlias' => 'int|string'], + $this->createReflectionProvider(), + self::getContainer()->getByType(TypeNodeResolver::class), + new MissingTypehintCheck(true, []), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new GenericObjectTypeCheck(), + true, + true, + true, + ), + ); + } + + public function testRule(): void + { + // everything reported by LocalTypeTraitAliasesRule + $this->analyse([__DIR__ . '/data/local-type-trait-aliases.php'], []); + } + + public function testRuleSpecific(): void + { + $this->analyse([__DIR__ . '/data/local-type-trait-use-aliases.php'], [ + [ + 'Type alias A contains unknown class LocalTypeTraitUseAliases\Nonexistent.', + 16, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Type alias B contains invalid type LocalTypeTraitUseAliases\SomeTrait.', + 16, + ], + [ + 'Type alias C contains unresolvable type.', + 16, + ], + [ + 'Type alias D contains generic type Exception but class Exception is not generic.', + 16, + ], + ]); + } + + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Classes/MethodTagRuleTest.php b/tests/PHPStan/Rules/Classes/MethodTagRuleTest.php new file mode 100644 index 0000000000..93e05c3675 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MethodTagRuleTest.php @@ -0,0 +1,110 @@ + + */ +class MethodTagRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new MethodTagRule( + new MethodTagCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), + ); + } + + public function testRule(): void + { + $fooClassLine = 12; + $this->analyse([__DIR__ . '/data/method-tag.php'], [ + [ + 'PHPDoc tag @method for method MethodTag\Foo::doFoo() return type contains unknown class MethodTag\intt.', + $fooClassLine, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @method for method MethodTag\Foo::doBar() parameter #1 $a contains unresolvable type.', + $fooClassLine, + ], + [ + 'PHPDoc tag @method for method MethodTag\Foo::doBaz2() parameter #1 $a default value contains unresolvable type.', + 12, + ], + [ + 'Class MethodTag\Foo has PHPDoc tag @method for method doMissingIterablueValue() return type with no value type specified in iterable type array.', + 12, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'PHPDoc tag @method for method MethodTag\TestGenerics::doA() return type contains generic type Exception but class Exception is not generic.', + 39, + ], + [ + 'Generic type MethodTag\Generic in PHPDoc tag @method for method MethodTag\TestGenerics::doB() return type does not specify all template types of class MethodTag\Generic: T, U', + 39, + ], + [ + 'Generic type MethodTag\Generic in PHPDoc tag @method for method MethodTag\TestGenerics::doC() return type specifies 3 template types, but class MethodTag\Generic supports only 2: T, U', + 39, + ], + [ + 'Type string in generic type MethodTag\Generic in PHPDoc tag @method for method MethodTag\TestGenerics::doD() return type is not subtype of template type T of int of class MethodTag\Generic.', + 39, + ], + [ + 'PHPDoc tag @method for method MethodTag\MissingGenerics::doA() return type contains generic class MethodTag\Generic but does not specify its types: T, U', + 47, + ], + [ + 'Class MethodTag\MissingIterableValue has PHPDoc tag @method for method doA() return type with no value type specified in iterable type array.', + 55, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Class MethodTag\MissingCallableSignature has PHPDoc tag @method for method doA() return type with no signature specified for callable.', + 63, + ], + [ + 'PHPDoc tag @method for method MethodTag\NonexistentClasses::doA() return type contains unknown class MethodTag\Nonexistent.', + 73, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @method for method MethodTag\NonexistentClasses::doB() return type contains invalid type PropertyTagTrait\Foo.', + 73, + ], + [ + 'Class MethodTag\Foo referenced with incorrect case: MethodTag\fOO.', + 73, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/MethodTagTraitRuleTest.php b/tests/PHPStan/Rules/Classes/MethodTagTraitRuleTest.php new file mode 100644 index 0000000000..1ec2063551 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MethodTagTraitRuleTest.php @@ -0,0 +1,60 @@ + + */ +class MethodTagTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new MethodTagTraitRule( + new MethodTagCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), + $reflectionProvider, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-tag-trait.php'], [ + [ + 'Trait MethodTagTrait\Foo has PHPDoc tag @method for method doMissingIterablueValue() return type with no value type specified in iterable type array.', + 12, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + ]); + } + + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591-method-tag.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Classes/MethodTagTraitUseRuleTest.php b/tests/PHPStan/Rules/Classes/MethodTagTraitUseRuleTest.php new file mode 100644 index 0000000000..1696a2515d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MethodTagTraitUseRuleTest.php @@ -0,0 +1,84 @@ + + */ +class MethodTagTraitUseRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new MethodTagTraitUseRule( + new MethodTagCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), + ); + } + + public function testRule(): void + { + $fooTraitLine = 12; + $this->analyse([__DIR__ . '/data/method-tag-trait.php'], [ + [ + 'PHPDoc tag @method for method MethodTagTrait\Foo::doFoo() return type contains unknown class MethodTagTrait\intt.', + $fooTraitLine, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @method for method MethodTagTrait\Foo::doBar() parameter #1 $a contains unresolvable type.', + $fooTraitLine, + ], + [ + 'PHPDoc tag @method for method MethodTagTrait\Foo::doBaz2() parameter #1 $a default value contains unresolvable type.', + $fooTraitLine, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/method-tag-trait-enum.php'], [ + [ + 'PHPDoc tag @method for method MethodTagTraitEnum\Foo::doFoo() return type contains unknown class MethodTagTraitEnum\intt.', + 8, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591-method-tag.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Classes/MixinRuleTest.php b/tests/PHPStan/Rules/Classes/MixinRuleTest.php index f765e94f43..d7c804c237 100644 --- a/tests/PHPStan/Rules/Classes/MixinRuleTest.php +++ b/tests/PHPStan/Rules/Classes/MixinRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -21,12 +23,21 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new MixinRule( - $reflectionProvider, - new ClassCaseSensitivityCheck($reflectionProvider, true), - new GenericObjectTypeCheck(), - new MissingTypehintCheck($reflectionProvider, true, true, true, []), - new UnresolvableTypeHelper(), - true, + new MixinCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), ); } @@ -56,7 +67,6 @@ public function testRule(): void [ 'PHPDoc tag @mixin contains generic class ReflectionClass but does not specify its types: T', 50, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'PHPDoc tag @mixin contains unknown class MixinRule\UnknownestClass.', @@ -84,6 +94,24 @@ public function testRule(): void 'PHPDoc tag @mixin contains non-object type int.', 92, ], + [ + 'Call-site variance of contravariant MixinRule\Foo in generic type MixinRule\Adipiscing in PHPDoc tag @mixin is in conflict with covariant template type T of class MixinRule\Adipiscing.', + 108, + ], + [ + 'Call-site variance of covariant MixinRule\Foo in generic type MixinRule\Adipiscing in PHPDoc tag @mixin is redundant, template type T of class MixinRule\Adipiscing has the same variance.', + 116, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Class MixinRule\NoIterableValue has PHPDoc tag @mixin with no value type specified in iterable type array.', + 124, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Class MixinRule\NoCallableSignature has PHPDoc tag @mixin with no signature specified for callable.', + 132, + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/MixinTraitRuleTest.php b/tests/PHPStan/Rules/Classes/MixinTraitRuleTest.php new file mode 100644 index 0000000000..7ae81a0f18 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MixinTraitRuleTest.php @@ -0,0 +1,55 @@ + + */ +class MixinTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new MixinTraitRule( + new MixinCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), + $reflectionProvider, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/mixin-trait.php'], [ + [ + 'Trait MixinTrait\FooTrait has PHPDoc tag @mixin with no value type specified in iterable type array.', + 14, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/MixinTraitUseRuleTest.php b/tests/PHPStan/Rules/Classes/MixinTraitUseRuleTest.php new file mode 100644 index 0000000000..d11675e208 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MixinTraitUseRuleTest.php @@ -0,0 +1,53 @@ + + */ +class MixinTraitUseRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new MixinTraitUseRule( + new MixinCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/mixin-trait-use.php'], [ + [ + 'PHPDoc tag @mixin contains unresolvable type.', + 22, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php b/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php index 5ba399a0f0..23501ec856 100644 --- a/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php +++ b/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php @@ -34,4 +34,9 @@ public function testRule(): void ]); } + public function testRuleWithConsistentConstructor(): void + { + $this->analyse([__DIR__ . '/data/new-static-consistent-constructor.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php b/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php index 674ed16200..f3e2b90cae 100644 --- a/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php +++ b/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php @@ -19,9 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/non-class-attribute-class.php'], [ [ 'Interface cannot be an Attribute class.', diff --git a/tests/PHPStan/Rules/Classes/PropertyTagRuleTest.php b/tests/PHPStan/Rules/Classes/PropertyTagRuleTest.php new file mode 100644 index 0000000000..04f0ecd7f3 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/PropertyTagRuleTest.php @@ -0,0 +1,143 @@ + + */ +class PropertyTagRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new PropertyTagRule( + new PropertyTagCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), + ); + } + + public function testRule(): void + { + $tipText = 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; + $fooClassLine = 23; + + $this->analyse([__DIR__ . '/data/property-tag.php'], [ + [ + 'PHPDoc tag @property for property PropertyTag\Foo::$a contains unknown class PropertyTag\intt.', + $fooClassLine, + $tipText, + ], + [ + 'PHPDoc tag @property for property PropertyTag\Foo::$b contains unknown class PropertyTag\intt.', + $fooClassLine, + $tipText, + ], + [ + 'PHPDoc tag @property for property PropertyTag\Foo::$c contains unknown class PropertyTag\stringg.', + $fooClassLine, + $tipText, + ], + [ + 'PHPDoc tag @property for property PropertyTag\Foo::$c contains unknown class PropertyTag\intt.', + $fooClassLine, + $tipText, + ], + [ + 'PHPDoc tag @property for property PropertyTag\Foo::$d contains unknown class PropertyTag\intt.', + $fooClassLine, + $tipText, + ], + [ + 'PHPDoc tag @property for property PropertyTag\Foo::$e contains unknown class PropertyTag\stringg.', + $fooClassLine, + $tipText, + ], + [ + 'PHPDoc tag @property for property PropertyTag\Foo::$e contains unknown class PropertyTag\intt.', + $fooClassLine, + $tipText, + ], + [ + 'PHPDoc tag @property-read for property PropertyTag\Foo::$f contains unknown class PropertyTag\intt.', + $fooClassLine, + $tipText, + ], + [ + 'PHPDoc tag @property-write for property PropertyTag\Foo::$g contains unknown class PropertyTag\stringg.', + $fooClassLine, + $tipText, + ], + [ + 'PHPDoc tag @property for property PropertyTag\Bar::$unresolvable contains unresolvable type.', + 31, + ], + [ + 'PHPDoc tag @property for property PropertyTag\TestGenerics::$a contains generic type Exception but class Exception is not generic.', + 51, + ], + [ + 'Generic type PropertyTag\Generic in PHPDoc tag @property for property PropertyTag\TestGenerics::$b does not specify all template types of class PropertyTag\Generic: T, U', + 51, + ], + [ + 'Generic type PropertyTag\Generic in PHPDoc tag @property for property PropertyTag\TestGenerics::$c specifies 3 template types, but class PropertyTag\Generic supports only 2: T, U', + 51, + ], + [ + 'Type string in generic type PropertyTag\Generic in PHPDoc tag @property for property PropertyTag\TestGenerics::$d is not subtype of template type T of int of class PropertyTag\Generic.', + 51, + ], + [ + 'PHPDoc tag @property for property PropertyTag\MissingGenerics::$a contains generic class PropertyTag\Generic but does not specify its types: T, U', + 59, + ], + [ + 'Class PropertyTag\MissingIterableValue has PHPDoc tag @property for property $a with no value type specified in iterable type array.', + 67, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Class PropertyTag\MissingCallableSignature has PHPDoc tag @property for property $a with no signature specified for callable.', + 75, + ], + [ + 'PHPDoc tag @property for property PropertyTag\NonexistentClasses::$a contains unknown class PropertyTag\Nonexistent.', + 85, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @property for property PropertyTag\NonexistentClasses::$b contains invalid type PropertyTagTrait\Foo.', + 85, + ], + [ + 'Class PropertyTag\Foo referenced with incorrect case: PropertyTag\fOO.', + 85, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/PropertyTagTraitRuleTest.php b/tests/PHPStan/Rules/Classes/PropertyTagTraitRuleTest.php new file mode 100644 index 0000000000..def6012d0d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/PropertyTagTraitRuleTest.php @@ -0,0 +1,60 @@ + + */ +class PropertyTagTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new PropertyTagTraitRule( + new PropertyTagCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), + $reflectionProvider, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/property-tag-trait.php'], [ + [ + 'Trait PropertyTagTrait\Foo has PHPDoc tag @property for property $bar with no value type specified in iterable type array.', + 9, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + ]); + } + + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591-property-tag.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Classes/PropertyTagTraitUseRuleTest.php b/tests/PHPStan/Rules/Classes/PropertyTagTraitUseRuleTest.php new file mode 100644 index 0000000000..9231ebd4e4 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/PropertyTagTraitUseRuleTest.php @@ -0,0 +1,59 @@ + + */ +class PropertyTagTraitUseRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new PropertyTagTraitUseRule( + new PropertyTagCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, + true, + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/property-tag-trait.php'], [ + [ + 'PHPDoc tag @property for property PropertyTagTrait\Foo::$foo contains unknown class PropertyTagTrait\intt.', + 9, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591-property-tag.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php b/tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php new file mode 100644 index 0000000000..df5ede79f0 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php @@ -0,0 +1,39 @@ + + */ +class ReadOnlyClassRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ReadOnlyClassRule(self::getContainer()->getByType(PhpVersion::class)); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80200) { + $errors[] = [ + 'Readonly classes are supported only on PHP 8.2 and later.', + 5, + ]; + } + if (PHP_VERSION_ID < 80300) { + $errors[] = [ + 'Anonymous readonly classes are supported only on PHP 8.3 and later.', + 15, + ]; + } + $this->analyse([__DIR__ . '/data/readonly-class.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php new file mode 100644 index 0000000000..0ca40d9fa4 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php @@ -0,0 +1,80 @@ + + */ +class RequireExtendsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RequireExtendsRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $expectedErrors = [ + [ + 'Trait IncompatibleRequireExtends\ValidTrait requires using class to extend IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\InValidTraitUse2 does not.', + 46, + ], + [ + 'Trait IncompatibleRequireExtends\ValidTrait requires using class to extend IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\InValidTraitUse does not.', + 51, + ], + [ + 'Interface IncompatibleRequireExtends\ValidInterface requires implementing class to extend IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\InvalidInterfaceUse2 does not.', + 56, + ], + [ + 'Interface IncompatibleRequireExtends\ValidInterface requires implementing class to extend IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\InvalidInterfaceUse does not.', + 58, + ], + [ + 'Trait IncompatibleRequireExtends\InvalidTrait requires using class to extend IncompatibleRequireExtends\SomeFinalClass, but IncompatibleRequireExtends\InvalidClass2 does not.', + 128, + ], + [ + 'Trait IncompatibleRequireExtends\ValidTrait requires using class to extend IncompatibleRequireExtends\SomeClass, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php:146 does not.', + 146, + ], + [ + 'Trait IncompatibleRequireExtends\ValidPsalmTrait requires using class to extend IncompatibleRequireExtends\SomeClass, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php:163 does not.', + 163, + ], + ]; + + $this->analyse([__DIR__ . '/../PhpDoc/data/incompatible-require-extends.php'], $expectedErrors); + } + + public function testExtendedInterfaceBug(): void + { + $this->analyse([__DIR__ . '/data/bug-10302-extended-interface.php'], [ + [ + 'Interface Bug10302ExtendedInterface\BatchAware requires implementing class to extend Bug10302ExtendedInterface\Model, but Bug10302ExtendedInterface\AnotherModel does not.', + 34, + ], + ]); + } + + public function testExtendedTraitBug(): void + { + $this->analyse([__DIR__ . '/data/bug-10302-extended-trait.php'], [ + [ + 'Trait Bug10302ExtendedTrait\Foo requires using class to extend Bug10302ExtendedTrait\Father, but Bug10302ExtendedTrait\Baz does not.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php new file mode 100644 index 0000000000..6a147e9398 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php @@ -0,0 +1,86 @@ + + */ +class RequireImplementsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RequireImplementsRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $expectedErrors = [ + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InValidTraitUse2 does not.', + 47, + ], + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InvalidEnumTraitUse does not.', + 52, + ], + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InValidTraitUse does not.', + 56, + ], + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:117 does not.', + 117, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait1 requires using class to implement IncompatibleRequireImplements\SomeTrait, but IncompatibleRequireImplements\InvalidTraitUse1 does not.', + 125, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait2 requires using class to implement IncompatibleRequireImplements\SomeEnum, but IncompatibleRequireImplements\InvalidTraitUse2 does not.', + 129, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait3 requires using class to implement IncompatibleRequireImplements\TypeDoesNotExist, but IncompatibleRequireImplements\InvalidTraitUse3 does not.', + 133, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait4 requires using class to implement IncompatibleRequireImplements\SomeClass, but IncompatibleRequireImplements\InvalidTraitUse4 does not.', + 137, + ], + [ + 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface2, but IncompatibleRequireImplements\RequiredInterface@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:164 does not.', + 164, + ], + [ + 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:168 does not.', + 168, + ], + [ + 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface2, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:168 does not.', + 168, + ], + ]; + + $this->analyse([__DIR__ . '/../PhpDoc/data/incompatible-require-implements.php'], $expectedErrors); + } + + public function testExtendedTraitBug(): void + { + $this->analyse([__DIR__ . '/data/bug-10302-extended-implements-trait.php'], [ + [ + 'Trait Bug10302ExtendedImplementsTrait\Foo requires using class to implement Bug10302ExtendedImplementsTrait\Interface1, but Bug10302ExtendedImplementsTrait\Baz does not.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php b/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php index 68795831ae..e22377bd1c 100644 --- a/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php +++ b/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -19,9 +18,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/non-class-attribute-class.php'], [ [ 'Trait cannot be an Attribute class.', diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index de702b005d..542ba55e5e 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,15 +12,19 @@ class UnusedConstructorParametersRuleTest extends RuleTestCase { + private bool $reportExactLine = true; + protected function getRule(): Rule { return new UnusedConstructorParametersRule(new UnusedFunctionParametersCheck( $this->createReflectionProvider(), + $this->reportExactLine, )); } - public function testUnusedConstructorParameters(): void + public function testUnusedConstructorParametersNoExactLine(): void { + $this->reportExactLine = false; $this->analyse([__DIR__ . '/data/unused-constructor-parameters.php'], [ [ 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $unusedParameter.', @@ -34,12 +37,22 @@ public function testUnusedConstructorParameters(): void ]); } - public function testPromotedProperties(): void + public function testUnusedConstructorParameters(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } + $this->analyse([__DIR__ . '/data/unused-constructor-parameters.php'], [ + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $unusedParameter.', + 19, + ], + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $anotherUnusedParameter.', + 20, + ], + ]); + } + public function testPromotedProperties(): void + { $this->analyse([__DIR__ . '/data/unused-constructor-parameters-promoted-properties.php'], []); } @@ -48,4 +61,19 @@ public function testBug1917(): void $this->analyse([__DIR__ . '/data/bug-1917.php'], []); } + public function testBug7165(): void + { + $this->analyse([__DIR__ . '/data/bug-7165.php'], []); + } + + public function testBug10865(): void + { + $this->analyse([__DIR__ . '/data/bug-10865.php'], []); + } + + public function testBug11454(): void + { + $this->analyse([__DIR__ . '/data/bug-11454.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/allow-dynamic-properties-attribute.php b/tests/PHPStan/Rules/Classes/data/allow-dynamic-properties-attribute.php new file mode 100644 index 0000000000..e17ad37e5b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/allow-dynamic-properties-attribute.php @@ -0,0 +1,9 @@ +getName() === 'AllowedSubTypes\\Foo'; + } + + public function getAllowedSubTypes(ClassReflection $classReflection): array + { + return [ + new ObjectType('AllowedSubTypes\\Bar'), + ]; + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-10248.php b/tests/PHPStan/Rules/Classes/data/bug-10248.php new file mode 100644 index 0000000000..ab21ff74b2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-10248.php @@ -0,0 +1,31 @@ +=8.0 + +namespace Bug10248; + +class A { + public function __construct(DateTimeInterface|float $value) { + var_dump($value); + } +} + +class B { + public function __construct(float $value) { + var_dump($value); + } +} + +/** + * @return int + */ +function getInt(): int{return 1;} + +/** + * @return int<0, max> + */ +function getRangeInt(): int{return 1;} + +new A(123); +new A(getInt()); +new A(getRangeInt()); + +new B(getRangeInt()); diff --git a/tests/PHPStan/Rules/Classes/data/bug-10302-extended-implements-trait.php b/tests/PHPStan/Rules/Classes/data/bug-10302-extended-implements-trait.php new file mode 100644 index 0000000000..290d54974a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-10302-extended-implements-trait.php @@ -0,0 +1,33 @@ + $args */ + public function __construct(array $args) { + + var_dump($args); + } +} + +class Test extends TestParent { + + public function __construct(int $a) { + + parent::__construct(get_defined_vars()); + //parent::__construct(func_get_args()); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11454.php b/tests/PHPStan/Rules/Classes/data/bug-11454.php new file mode 100644 index 0000000000..1a7fe447d1 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11454.php @@ -0,0 +1,14 @@ + withTrashed(bool $withTrashed = true) + * @method static Builder onlyTrashed() + * @method static Builder withoutTrashed() + * @method static bool restore() + * @method static static restoreOrCreate(array $attributes = [], array $values = []) + * @method static static createOrRestore(array $attributes = [], array $values = []) + */ +trait SoftDeletes {} + +function test(): void { + assertType('Bug11591MethodTag\\Builder', User::withTrashed()); + assertType('Bug11591MethodTag\\Builder', User::onlyTrashed()); + assertType('Bug11591MethodTag\\Builder', User::withoutTrashed()); + assertType(User::class, User::createOrRestore()); + assertType(User::class, User::restoreOrCreate()); +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php b/tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php new file mode 100644 index 0000000000..c9bf36e246 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php @@ -0,0 +1,26 @@ + $a + * @property static $b + */ +trait SoftDeletes {} + +function test(User $user): void { + assertType('Bug11591PropertyTag\\Builder', $user->a); + assertType(User::class, $user->b); +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11591.php b/tests/PHPStan/Rules/Classes/data/bug-11591.php new file mode 100644 index 0000000000..e41413653a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11591.php @@ -0,0 +1,44 @@ + + */ +trait WithConfig { + /** + * @param SettingsFactory $settings + */ + public function setConfig(callable $settings): void { + $settings($this); + } + + /** + * @param callable(static): array $settings + */ + public function setConfig2(callable $settings): void { + $settings($this); + } + + /** + * @param callable(self): array $settings + */ + public function setConfig3(callable $settings): void { + $settings($this); + } +} + +class A +{ + use WithConfig; +} + +function (A $a): void { + $a->setConfig(function ($who) { + assertType(A::class, $who); + + return []; + }); +}; diff --git a/tests/PHPStan/Rules/Classes/data/bug-11592.php b/tests/PHPStan/Rules/Classes/data/bug-11592.php new file mode 100644 index 0000000000..b94251a495 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11592.php @@ -0,0 +1,47 @@ += 8.1 + +namespace Bug11592; + +trait HelloWorld +{ + abstract public static function cases(): array; + + abstract public static function from(): self; + + abstract public static function tryFrom(): ?self; +} + +enum Test +{ + use HelloWorld; +} + +enum Test2 +{ + + abstract public static function cases(): array; + + abstract public static function from(): self; + + abstract public static function tryFrom(): ?self; + +} + +enum BackedTest: int +{ + use HelloWorld; +} + +enum BackedTest2: int +{ + abstract public static function cases(): array; + + abstract public static function from(): self; + + abstract public static function tryFrom(): ?self; +} + +enum EnumWithAbstractMethod +{ + abstract function foo(); +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11815.php b/tests/PHPStan/Rules/Classes/data/bug-11815.php new file mode 100644 index 0000000000..c08dccb9ea --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11815.php @@ -0,0 +1,43 @@ += 8.2 + +declare(strict_types = 1); + +class Dimensions +{ + public function __construct( + public int $width, + public int $height, + ) { + } +} + +class StoreProcessorResult +{ + public function __construct( + public string $path, + public string $mimetype, + public Dimensions $dimensions, + public int $filesize, + public true|null $identical = null, + ) { + } +} + +/** + * @return array{path: string, identical?: true} + */ +function getPath(): array +{ + $data = ['path' => 'some/path']; + if ((bool)rand(0, 1)) { + $data['identical'] = true; + } + return $data; +} + +$data = getPath(); +$data['dimensions'] = new Dimensions(100, 100); +$data['mimetype'] = 'image/png'; +$data['filesize'] = 123456; + +$dto = new StoreProcessorResult(...$data); diff --git a/tests/PHPStan/Rules/Classes/data/bug-12011.php b/tests/PHPStan/Rules/Classes/data/bug-12011.php new file mode 100644 index 0000000000..94671eec7a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-12011.php @@ -0,0 +1,27 @@ += 8.3 + +namespace Bug12011; + +use Attribute; + +#[Table(self::TABLE_NAME)] +class HelloWorld +{ + private const string TABLE_NAME = 'table'; +} + +#[Attribute(Attribute::TARGET_CLASS)] +final class Table +{ + public function __construct( + public readonly string|null $name = null, + public readonly string|null $schema = null, + ) { + } +} + +#[Table(self::TABLE_NAME)] +class HelloWorld2 +{ + private const int TABLE_NAME = 1; +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-12281.php b/tests/PHPStan/Rules/Classes/data/bug-12281.php new file mode 100644 index 0000000000..293d9e5e41 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-12281.php @@ -0,0 +1,16 @@ += 8.2 + +namespace Bug12281; + +#[\AllowDynamicProperties] +readonly class BlogData { /* … */ } + +/** @readonly */ +#[\AllowDynamicProperties] +class BlogDataPhpdoc { /* … */ } + +#[\AllowDynamicProperties] +enum BlogDataEnum { /* … */ } + +#[\AllowDynamicProperties] +interface BlogDataInterface { /* … */ } diff --git a/tests/PHPStan/Rules/Classes/data/bug-3311a.php b/tests/PHPStan/Rules/Classes/data/bug-3311a.php new file mode 100644 index 0000000000..bb28ed1eb1 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-3311a.php @@ -0,0 +1,25 @@ + + * @psalm-var list + */ + public array $bar = []; + + /** + * @param array $bar + * @psalm-param list $bar + */ + public function __construct(array $bar) + { + $this->bar = $bar; + } +} + +function () { + $instance = new Foo([1 => 'baz']); +}; diff --git a/tests/PHPStan/Rules/Classes/data/bug-3632.php b/tests/PHPStan/Rules/Classes/data/bug-3632.php new file mode 100644 index 0000000000..d21950cd6c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-3632.php @@ -0,0 +1,41 @@ +getReservedKeywordsClass(); + $keywords = new $class(); + if (! $keywords instanceof KeywordList) { + throw new \Exception(); + } + + return $keywords; + } + + /** + * @throws \Exception If not supported on this platform. + * + * @psalm-return class-string + */ + protected function getReservedKeywordsClass(): string + { + throw new \Exception(); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-5333.php b/tests/PHPStan/Rules/Classes/data/bug-5333.php new file mode 100644 index 0000000000..67845acd46 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-5333.php @@ -0,0 +1,122 @@ +', $foo); + assertNativeType('array', $foo); + + assertType('Bug5333\Route', $res); + assertNativeType('Bug5333\Route', $res); + + return $res; + } + + return $foo; + } +} + +class HelloWorld2 +{ + /** + * @var Route|callable():Route + **/ + private $foo; + + /** + * @param Route|callable():Route $foo + **/ + public function setFoo($foo): void + { + assertType('Bug5333\Route|(callable(): Bug5333\Route)', $foo); + assertNativeType('mixed', $foo); + + $this->foo = $foo; + } + + public function getFoo(): Route + { + assertType('Bug5333\Route|(callable(): Bug5333\Route)', $this->foo); + assertNativeType('mixed', $this->foo); + + if (\is_callable($this->foo)) { + assertType('(Bug5333\Route&callable(): mixed)|(callable(): Bug5333\Route)', $this->foo); + assertNativeType('callable(): mixed', $this->foo); + + $res = ($this->foo)(); + assertType('mixed', $res); + assertNativeType('mixed', $res); + if (!$res instanceof Route) { + throw new \Exception(); + } + + return $res; + } + + return $this->foo; + } +} + +class HelloFinalWorld +{ + /** + * @var FinalRoute|callable():FinalRoute + **/ + private $foo; + + /** + * @param FinalRoute|callable():FinalRoute $foo + **/ + public function setFoo($foo): void + { + assertType('Bug5333\FinalRoute|(callable(): Bug5333\FinalRoute)', $foo); + assertNativeType('mixed', $foo); + + $this->foo = $foo; + } + + public function getFoo(): FinalRoute + { + assertType('Bug5333\FinalRoute|(callable(): Bug5333\FinalRoute)', $this->foo); + assertNativeType('mixed', $this->foo); + + if (\is_callable($this->foo)) { + assertType('callable(): Bug5333\FinalRoute', $this->foo); + assertNativeType('callable(): mixed', $this->foo); + + $res = ($this->foo)(); + assertType('Bug5333\FinalRoute', $res); + assertNativeType('mixed', $res); + if (!$res instanceof FinalRoute) { + throw new \Exception(); + } + + return $res; + } + + return $this->foo; + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-5553.php b/tests/PHPStan/Rules/Classes/data/bug-5553.php new file mode 100644 index 0000000000..a1f9d45a0a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-5553.php @@ -0,0 +1,29 @@ += 8.0 + +namespace Bug5553; + +use DatePeriod; +use DateTime; + +class Foo +{ + + public function weekNumberToWorkRange(int $week, int $year): DatePeriod + { + $dto = new DateTime(); + $dto->setISODate($year, $week); + $dto->setTime(8, 0, 0, 0); + + $start = clone $dto; + $dto->modify('+4 days'); + $end = clone $dto; + $end->setTime(18, 0, 0, 0); + + return new DatePeriod( + start: $start, + interval: new \DateInterval('P1D'), + end: $end + ); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-6213.php b/tests/PHPStan/Rules/Classes/data/bug-6213.php new file mode 100644 index 0000000000..1e6141f62e --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-6213.php @@ -0,0 +1,21 @@ +createElement('div', 'content'); + + // Incorrect! This is DOMNode|null not DOMElement|null + // It's also possible to contain a DOMText node, which is not an instance + // of DOMElement, but an instance of DOMNode! + if ($element->firstChild instanceof DOMText) { + // do something + } + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-7048.php b/tests/PHPStan/Rules/Classes/data/bug-7048.php new file mode 100644 index 0000000000..3e3b332420 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-7048.php @@ -0,0 +1,52 @@ += 8.0 + +namespace Bug7048; + +class HelloWorld +{ + public function sayHello(): \DatePeriod + { + return new \DatePeriod( + start: new \DateTime('now'), + interval: new \DateInterval('P1D'), + end: new \DateTime('now + 3 days'), + ); + } + + public function doFoo(): void + { + new \DatePeriod( + start: new \DateTime('now'), + interval: new \DateInterval('P1D'), + recurrences: 5, + ); + + new \DatePeriod( + isostr: 'R4/2012-07-01T00:00:00Z/P7D' + ); + } + + public function allValid(): void + { + $start = new \DateTime('2012-07-01'); + $interval = new \DateInterval('P7D'); + $end = new \DateTime('2012-07-31'); + $recurrences = 4; + $iso = 'R4/2012-07-01T00:00:00Z/P7D'; + + + $period = new \DatePeriod($start, $interval, $recurrences); + $period = new \DatePeriod($start, $interval, $end); + $period = new \DatePeriod($iso); + $period = new \DatePeriod($start, $interval, "foo"); + } + + public function invalid(): void + { + new \DatePeriod( + start: new \DateTime('now'), + interval: new \DateInterval('P1D'), + end: "foo", + ); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-7165.php b/tests/PHPStan/Rules/Classes/data/bug-7165.php new file mode 100644 index 0000000000..3cbf90ed6d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-7165.php @@ -0,0 +1,12 @@ += 8.0 + +namespace Bug7165; + +#[\Attribute] +class MyAttribute +{ + public function __construct(string $name) + { + } +} + diff --git a/tests/PHPStan/Rules/Classes/data/bug-7171.php b/tests/PHPStan/Rules/Classes/data/bug-7171.php new file mode 100644 index 0000000000..3af6ee09e3 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-7171.php @@ -0,0 +1,69 @@ += 8.0 + +namespace Bug7171; + +/** + * @template T of object + */ +class EntityRepository +{ +} + +/** + * @template T of object + * @template-extends EntityRepository + */ +class ServiceEntityRepository extends EntityRepository +{ +} + +/** + * @extends ServiceEntityRepository + */ +class MyRepositoryAttribute extends ServiceEntityRepository +{ +} + +/** + * @extends ServiceEntityRepository + */ +class MyRepositoryExtend extends ServiceEntityRepository +{ +} + +/** + * @template T of object + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class Entity +{ + /** + * @param class-string>|null $repositoryClass + */ + public function __construct(?string $repositoryClass = null) + { + // Logic + echo $repositoryClass; + } +} + +#[Entity(repositoryClass: MyRepositoryAttribute::class)] +class MyEntityAttribute +{ +} + +/** + * @extends Entity + */ +class MyEntityExtend extends Entity +{ + public function __construct() + { + parent::__construct(repositoryClass: MyRepositoryExtend::class); + } +} + +#[Entity(repositoryClass: \stdClass::class)] +class WrongEntity +{ +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-7574.php b/tests/PHPStan/Rules/Classes/data/bug-7574.php new file mode 100644 index 0000000000..a5671051d6 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-7574.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug7594; + +class HelloWorld +{ + + public const ABILITY_BUILD = 0; + public const ABILITY_MINE = 1; + public const ABILITY_DOORS_AND_SWITCHES = 2; + public const ABILITY_OPEN_CONTAINERS = 3; + public const ABILITY_ATTACK_PLAYERS = 4; + public const ABILITY_ATTACK_MOBS = 5; + public const ABILITY_OPERATOR = 6; + public const ABILITY_TELEPORT = 7; + public const ABILITY_INVULNERABLE = 8; + public const ABILITY_FLYING = 9; + public const ABILITY_ALLOW_FLIGHT = 10; + public const ABILITY_INSTABUILD = 11; //??? + public const ABILITY_LIGHTNING = 12; //??? + private const ABILITY_FLY_SPEED = 13; + private const ABILITY_WALK_SPEED = 14; + public const ABILITY_MUTED = 15; + public const ABILITY_WORLD_BUILDER = 16; + public const ABILITY_NO_CLIP = 17; + + public const NUMBER_OF_ABILITIES = 18; + + /** + * @param bool[] $boolAbilities + * @phpstan-param array $boolAbilities + */ + public function __construct( + private array $boolAbilities, + ){} + + /** + * Returns a list of abilities set/overridden by this layer. If the ability value is not set, the index is omitted. + * @return bool[] + * @phpstan-return array + */ + public function getBoolAbilities() : array{ return $this->boolAbilities; } + + public static function decode(int $setAbilities, int $setAbilityValues) : self{ + $boolAbilities = []; + for($i = 0; $i < self::NUMBER_OF_ABILITIES; $i++){ + if($i === self::ABILITY_FLY_SPEED || $i === self::ABILITY_WALK_SPEED){ + continue; + } + if(($setAbilities & (1 << $i)) !== 0){ + $boolAbilities[$i] = ($setAbilityValues & (1 << $i)) !== 0; + } + } + + return new self($boolAbilities); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-7675.php b/tests/PHPStan/Rules/Classes/data/bug-7675.php new file mode 100644 index 0000000000..b14178b381 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-7675.php @@ -0,0 +1,25 @@ +header(SpladeCore::HEADER_SPLADE)) { + return null; + } + + return true; + }, $exceptionHandler, get_class($exceptionHandler)); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-7720.php b/tests/PHPStan/Rules/Classes/data/bug-7720.php new file mode 100644 index 0000000000..ff20bd0e61 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-7720.php @@ -0,0 +1,21 @@ +foo(); + } + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-7721.php b/tests/PHPStan/Rules/Classes/data/bug-7721.php new file mode 100644 index 0000000000..b7b44e64c8 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-7721.php @@ -0,0 +1,17 @@ += 8.1 + +namespace Bug7721; + +final class A { } +final class B { } +final class C +{ + public function __construct(public readonly A|B $value) { } +} + +$c = new C(value: new A()); + +echo match (true) { + $c->value instanceof A => 'A', + $c->value instanceof B => 'B' +}; diff --git a/tests/PHPStan/Rules/Classes/data/bug-8034.php b/tests/PHPStan/Rules/Classes/data/bug-8034.php new file mode 100644 index 0000000000..04668a1599 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-8034.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug8042; + +class A {} +class B {} + +function test(A|B $ab): string { + return match (true) { + $ab instanceof A => 'a', + $ab instanceof B => 'b', + }; +} + +function test2(A|B $ab): string { + return match (true) { + $ab instanceof A => 'a', + $ab instanceof B => 'b', + rand(0, 1) => 'never' + }; +} + +function test3(A|B $ab): string { + return match (true) { + $ab instanceof A => 'a', + $ab instanceof B => 'b', + default => 'never' + }; +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-8889.php b/tests/PHPStan/Rules/Classes/data/bug-8889.php new file mode 100644 index 0000000000..3f3279df1d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-8889.php @@ -0,0 +1,10 @@ += 8.1 + +namespace Bug9402; + +enum Foo: int +{ + + private const MY_CONST = 1; + private const MY_CONST_STRING = 'foo'; + + case Zero = 0; + case One = self::MY_CONST; + case Two = self::MY_CONST_STRING; + +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-9577.php b/tests/PHPStan/Rules/Classes/data/bug-9577.php new file mode 100644 index 0000000000..48220dec90 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9577.php @@ -0,0 +1,40 @@ += 8.1 + +namespace Bug9577; + +trait StringableMessageTrait +{ + public function __construct( + public readonly string $message, + ) { + + } +} + +class SpecializedException +{ + use StringableMessageTrait { + StringableMessageTrait::__construct as __traitConstruct; + } + + public function __construct( + public int $code, + string $message, + ) { + $this->__traitConstruct($message); + } +} + +class SpecializedException2 +{ + use StringableMessageTrait { + StringableMessageTrait::__construct as __traitConstruct; + } + + public function __construct( + public int $code, + string $message, + ) { + //$this->__traitConstruct($message); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-9659.php b/tests/PHPStan/Rules/Classes/data/bug-9659.php new file mode 100644 index 0000000000..b78128fdca --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9659.php @@ -0,0 +1,17 @@ +=8.0 + +namespace Bug9659; + +class HelloWorld +{ + /** + * @param float|null $timeout + */ + public function __construct($timeout = null) + { + var_dump($timeout); + } +} + +new HelloWorld(20); // working +new HelloWorld(random_int(20, 80)); // broken diff --git a/tests/PHPStan/Rules/Classes/data/bug-9946.php b/tests/PHPStan/Rules/Classes/data/bug-9946.php new file mode 100644 index 0000000000..e2cb92f5f2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9946.php @@ -0,0 +1,21 @@ +format('c'); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/class-attributes.php b/tests/PHPStan/Rules/Classes/data/class-attributes.php index 00e162cf4e..c0cf4238fe 100644 --- a/tests/PHPStan/Rules/Classes/data/class-attributes.php +++ b/tests/PHPStan/Rules/Classes/data/class-attributes.php @@ -142,3 +142,27 @@ trait TraitAsAttribute #[TraitAsAttribute] class ClassWithTraitAttribute {} + +#[\Attribute(flags: \Attribute::TARGET_CLASS)] +class FlagsAttributeWithClassTarget +{ + +} + +#[\Attribute(flags: \Attribute::TARGET_PROPERTY)] +class FlagsAttributeWithPropertyTarget +{ + +} + +#[FlagsAttributeWithClassTarget] +class TestFlagsAttribute +{ + +} + +#[FlagsAttributeWithPropertyTarget] +class TestWrongFlagsAttribute +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php b/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php new file mode 100644 index 0000000000..618fc491b2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php @@ -0,0 +1,61 @@ += 8.2 + +namespace ClassConstantAccessedOnTrait; + +trait Foo +{ + public const TEST = 1; +} + +class Bar +{ + use Foo; +} + +function (): void { + echo Foo::TEST; + echo Foo::class; +}; diff --git a/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php b/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php index 7e4d7b8677..137b2f58b7 100644 --- a/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php +++ b/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php @@ -16,6 +16,7 @@ public function doFoo( echo $string::class; echo $stdOrNull::class; echo $stringOrNull::class; + echo 'Foo'::class; } } diff --git a/tests/PHPStan/Rules/Classes/data/class-string.php b/tests/PHPStan/Rules/Classes/data/class-string.php new file mode 100644 index 0000000000..bb07d5954a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-string.php @@ -0,0 +1,87 @@ += 8.0 + +declare(strict_types = 1); + +namespace ClassString; + +class A +{ + public function __construct(public int $i) + { + } +} + +abstract class B +{ + public function __construct(public int $i) + { + } +} + +class C extends B +{ +} + +interface D +{ +} + +class Foo +{ + /** + * @return class-string + */ + public static function returnClassStringA(): string + { + return A::class; + } + + /** + * @return class-string + */ + public static function returnClassStringB(): string + { + return B::class; + } + + /** + * @return class-string + */ + public static function returnClassStringC(): string + { + return C::class; + } + + /** + * @return class-string + */ + public static function returnClassStringD(): string + { + return D::class; + } +} + +$classString = Foo::returnClassStringA(); +$error = new (Foo::returnClassStringA())('O_O'); +$error = new ($classString)('O_O'); +$error = new $classString('O_O'); + +$classString = Foo::returnClassStringB(); +$ok = new (Foo::returnClassStringB())('O_O'); +$ok = new ($classString)('O_O'); +$ok = new $classString('O_O'); + +$classString = Foo::returnClassStringC(); +$error = new (Foo::returnClassStringC())('O_O'); +$error = new ($classString)('O_O'); +$error = new $classString('O_O'); + +$classString = Foo::returnClassStringD(); +$ok = new (Foo::returnClassStringD())('O_O'); +$ok = new ($classString)('O_O'); +$ok = new $classString('O_O'); + +$className = A::class; +$error = new ($className)('O_O'); +$error = new $className('O_O'); +$error = new A('O_O'); diff --git a/tests/PHPStan/Rules/Classes/data/duplicate-class.php b/tests/PHPStan/Rules/Classes/data/duplicate-class.php new file mode 100644 index 0000000000..15a09e6364 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/duplicate-class.php @@ -0,0 +1,23 @@ += 8.3 + +namespace ClassConstantDynamicAccess; + +final class Foo +{ + + private const BAR = 'BAR'; + + /** @var 'FOO'|'BAR'|'BUZ' */ + public $name; + + public function test(string $string, object $obj): void + { + $bar = 'FOO'; + + echo self::{$foo}; + echo self::{$string}; + echo self::{$obj}; + echo self::{$this->name}; + } + + public function testScope(): void + { + $name1 = 'FOO'; + $rand = rand(); + if ($rand === 1) { + $foo = 1; + $name = $name1; + } elseif ($rand === 2) { + $name = 'BUZ'; + } else { + $name = 'QUX'; + } + + if ($name === 'FOO') { + echo self::{$name}; + } elseif ($name === 'BUZ') { + echo self::{$name}; + } else { + echo self::{$name}; + } + + echo self::{$name}; + } + + +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-sanity.php b/tests/PHPStan/Rules/Classes/data/enum-sanity.php index 477ea824d5..1698b595fd 100644 --- a/tests/PHPStan/Rules/Classes/data/enum-sanity.php +++ b/tests/PHPStan/Rules/Classes/data/enum-sanity.php @@ -1,4 +1,4 @@ -= 8.1 namespace EnumSanity; @@ -71,3 +71,53 @@ enum BackedEnumWithFloatType: float enum BackedEnumWithBoolType: bool { } + +enum EnumWithSerialize { + case Bar; + + public function __serialize() { + } + + public function __unserialize(array $data) { + + } +} + +enum EnumDuplicateValue: int { + case A = 1; + case B = 2; + case C = 2; + case D = 3; + case E = 1; +} + +enum ValidIntBackedEnum: int { + case A = 1; + case B = 2; +} + +enum ValidStringBackedEnum: string { + case A = 'A'; + case B = 'B'; +} + +enum EnumInconsistentCaseType: int { + case FOO = 'foo'; + case BAR; +} + +enum EnumInconsistentStringCaseType: string { + case BAR; +} + +enum EnumWithValueButNotBacked { + case FOO = 1; +} + +enum EnumMayNotSerializable implements \Serializable { + + public function serialize() { + } + public function unserialize($data) { + } +} diff --git a/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php new file mode 100644 index 0000000000..cf134c3a2b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php @@ -0,0 +1,41 @@ + 'App\GeneratedProxy\__CG__', + ]; + } + +} + +$doctrineEntity = new \App\GeneratedProxy\__CG__\App\TestDoctrineEntity(); +$phpStanEntity = new \_PHPStan_15755dag8c\TestPhpStanEntity(); diff --git a/tests/PHPStan/Rules/Classes/data/impossible-instanceof-new-is-always-final.php b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-new-is-always-final.php new file mode 100644 index 0000000000..9faf5f715a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-new-is-always-final.php @@ -0,0 +1,91 @@ += 8.0 + +namespace ImpossibleInstanceofNewIsAlwaysFinal; + +interface Foo +{ + +} + +class Bar +{ + +} + +function (): void { + $bar = new Bar(); + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if ($bar::class !== Bar::class) { + return; + } + + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if (Bar::class !== $bar::class) { + return; + } + + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if (get_class($bar) !== Bar::class) { + return; + } + + if ($bar instanceof Foo) { + + } +}; + +function (Bar $bar): void { + if (Bar::class !== get_class($bar)) { + return; + } + + if ($bar instanceof Foo) { + + } +}; + +function (): void { + $bar = null; + if (rand(0,1)===1) { + $bar = new Bar(); + } + if ($bar instanceof Foo) { + + } +}; + +class Baz extends Bar +{ + +} + +function (): void { + $bar = null; + if (rand(0,1)===1) { + $bar = new Bar(); + } + if ($bar instanceof Baz) { + + } +}; diff --git a/tests/PHPStan/Rules/Classes/data/impossible-instanceof-report-always-true-last-condition.php b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-report-always-true-last-condition.php new file mode 100644 index 0000000000..09c434a4fa --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-report-always-true-last-condition.php @@ -0,0 +1,41 @@ += 8.0 + */ class Foo { @@ -62,3 +62,45 @@ class Generic class Invalid { } + +/** @psalm-type MyObject = what{} */ +class InvalidTypeDefinitionToIgnoreBecauseItsAParseErrorAlreadyReportedInInvalidPhpDocTagValueRule +{ + +} + +/** + * @phpstan-type NoIterableValue = array + * @phpstan-type NoGenerics = Generic + * @phpstan-type NoCallable = array + */ +class MissingTypehints +{ + +} + +/** + * @phpstan-type A = Nonexistent + * @phpstan-type B = \LocalTypeTraitAliases\Foo + * @phpstan-type C = fOO + */ +class NonexistentClasses +{ + +} + +/** + * @phpstan-type A = string&int + */ +class UnresolvableExample +{ + +} + +/** + * @phpstan-type A = \Exception + */ +class GenericsCheck +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php new file mode 100644 index 0000000000..6628e0db7c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php @@ -0,0 +1,85 @@ + + */ +trait Foo +{ +} + +/** + * @phpstan-type LocalTypeAlias int + * @phpstan-type ExistingClassAlias \stdClass + * @phpstan-type GlobalTypeAlias bool + * @phpstan-type int \stdClass + * @phpstan-type RecursiveTypeAlias RecursiveTypeAlias[] + * @phpstan-type CircularTypeAlias1 CircularTypeAlias2 + * @phpstan-type CircularTypeAlias2 CircularTypeAlias1 + */ +trait Bar +{ +} + +/** + * @phpstan-import-type ImportedAliasFromNonClass from int + * @phpstan-import-type ImportedAliasFromUnknownClass from UnknownClass + * @phpstan-import-type ImportedUnknownAlias from Foo + * @phpstan-import-type ExportedTypeAlias from Foo as ExistingClassAlias + * @phpstan-import-type ExportedTypeAlias from Foo as GlobalTypeAlias + * @phpstan-import-type ExportedTypeAlias from Foo as OverwrittenTypeAlias + * @phpstan-import-type ExportedTypeAlias from Foo as int + * @phpstan-type OverwrittenTypeAlias string + * @phpstan-import-type CircularTypeAliasImport1 from Qux + * @phpstan-type CircularTypeAliasImport2 CircularTypeAliasImport1 + */ +trait Baz +{ +} + +/** + * @phpstan-import-type CircularTypeAliasImport2 from Baz + * @phpstan-type CircularTypeAliasImport1 CircularTypeAliasImport2 + */ +trait Qux +{ +} + +/** + * @phpstan-template T + * @phpstan-type T never + */ +trait Generic +{ +} + +/** + * @phpstan-type InvalidTypeAlias invalid-type-definition + */ +trait Invalid +{ +} + +/** + * @phpstan-type NoIterablueValue = array + */ +trait MissingType +{ + +} + +class Usages +{ + + use Foo; + use Bar; + use Baz; + use Qux; + use Generic; + use Invalid; + use MissingType; + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php new file mode 100644 index 0000000000..94e019280c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php @@ -0,0 +1,26 @@ + + */ +trait Foo +{ + +} + +class Usage +{ + + use Foo; + +} diff --git a/tests/PHPStan/Rules/Classes/data/method-tag-trait-enum.php b/tests/PHPStan/Rules/Classes/data/method-tag-trait-enum.php new file mode 100644 index 0000000000..9855c17844 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/method-tag-trait-enum.php @@ -0,0 +1,18 @@ += 8.1 + +namespace MethodTagTraitEnum; + +/** + * @method intt doFoo() + */ +trait Foo +{ + +} + +enum FooEnum +{ + + use Foo; + +} diff --git a/tests/PHPStan/Rules/Classes/data/method-tag-trait.php b/tests/PHPStan/Rules/Classes/data/method-tag-trait.php new file mode 100644 index 0000000000..504033696a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/method-tag-trait.php @@ -0,0 +1,30 @@ + doA() + * @method Generic doB() + * @method Generic doC() + * @method Generic doD() + */ +class TestGenerics +{ + +} + +/** + * @method Generic doA() + */ +class MissingGenerics +{ + +} + +/** + * @method Generic doA() + */ +class MissingIterableValue +{ + +} + +/** + * @method Generic doA() + */ +class MissingCallableSignature +{ + +} + +/** + * @method Nonexistent doA() + * @method \PropertyTagTrait\Foo doB() + * @method fOO doC() + */ +class NonexistentClasses +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/mixin-trait-use.php b/tests/PHPStan/Rules/Classes/data/mixin-trait-use.php new file mode 100644 index 0000000000..0b67bfb4fb --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/mixin-trait-use.php @@ -0,0 +1,36 @@ + + * @mixin string&int + */ +trait FooTrait +{ + +} + +class Usages +{ + + use FooTrait; + +} + +function (Usages $u): void { + assertType(Usages::class, $u->get()); +}; diff --git a/tests/PHPStan/Rules/Classes/data/mixin-trait.php b/tests/PHPStan/Rules/Classes/data/mixin-trait.php new file mode 100644 index 0000000000..83c0f0b488 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/mixin-trait.php @@ -0,0 +1,17 @@ + + */ +trait FooTrait +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/mixin.php b/tests/PHPStan/Rules/Classes/data/mixin.php index c770d2f0a1..b6ab1b092b 100644 --- a/tests/PHPStan/Rules/Classes/data/mixin.php +++ b/tests/PHPStan/Rules/Classes/data/mixin.php @@ -93,3 +93,43 @@ interface InterfaceWithMixin { } + +/** + * @template-covariant T + */ +class Adipiscing +{ + +} + +/** + * @mixin Adipiscing + */ +class Elit +{ + +} + +/** + * @mixin Adipiscing + */ +class Elit2 +{ + +} + +/** + * @mixin Dolor + */ +class NoIterableValue +{ + +} + +/** + * @mixin Dolor + */ +class NoCallableSignature +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/new-static-consistent-constructor.php b/tests/PHPStan/Rules/Classes/data/new-static-consistent-constructor.php new file mode 100644 index 0000000000..d6d4c0c9f1 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/new-static-consistent-constructor.php @@ -0,0 +1,25 @@ + $a + * @property Generic $b + * @property Generic $c + * @property Generic $d + */ +class TestGenerics +{ + +} + +/** + * @property Generic $a + */ +class MissingGenerics +{ + +} + +/** + * @property Generic $a + */ +class MissingIterableValue +{ + +} + +/** + * @property Generic $a + */ +class MissingCallableSignature +{ + +} + +/** + * @property Nonexistent $a + * @property \PropertyTagTrait\Foo $b + * @property fOO $c + */ +class NonexistentClasses +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/readonly-class.php b/tests/PHPStan/Rules/Classes/data/readonly-class.php new file mode 100644 index 0000000000..5f26eacd5d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/readonly-class.php @@ -0,0 +1,20 @@ += 8.3 + +namespace ReadonlyClass; + +readonly class Foo +{ + +} + +class Bar +{ + + public function doFoo(): void + { + $c = new readonly class () { + + }; + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/remember-class-exists-from-constructor.php b/tests/PHPStan/Rules/Classes/data/remember-class-exists-from-constructor.php new file mode 100644 index 0000000000..6c7b8cecbf --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/remember-class-exists-from-constructor.php @@ -0,0 +1,44 @@ += 7.4 + +namespace RememberClassExistsFromConstructor; + +use SomeUnknownClass; +use SomeUnknownInterface; + +class UserWithClass +{ + public function __construct( + ) { + if (!class_exists('SomeUnknownClass')) { + throw new \LogicException(); + } + } + + public function doFoo($m): bool + { + if ($m instanceof SomeUnknownClass) { + return false; + } + return true; + } + +} + +class UserWithInterface +{ + public function __construct( + ) { + if (!interface_exists('SomeUnknownInterface')) { + throw new \LogicException(); + } + } + + public function doFoo($m): bool + { + if ($m instanceof SomeUnknownInterface) { + return false; + } + return true; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index 263f0db8d4..3115a897b7 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -13,6 +13,8 @@ class BooleanAndConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new BooleanAndConstantConditionRule( @@ -26,6 +28,8 @@ protected function getRule(): Rule $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -114,6 +118,99 @@ public function testRule(): void 'Right side of && is always true.', 147, ], + [ + 'Left side of && is always true.', + 178, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of && is always true.', + 178, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testRuleLogicalAnd(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/boolean-logical-and.php'], [ + [ + 'Left side of and is always true.', + 15, + ], + [ + 'Right side of and is always true.', + 19, + ], + [ + 'Left side of and is always false.', + 24, + ], + [ + 'Right side of and is always false.', + 27, + ], + [ + 'Result of and is always false.', + 30, + ], + [ + 'Right side of and is always true.', + 33, + ], + [ + 'Right side of and is always true.', + 36, + ], + [ + 'Right side of and is always true.', + 39, + ], + [ + 'Result of and is always false.', + 50, + ], + [ + 'Result of and is always true.', + 54, + $tipText, + ], + [ + 'Result of and is always false.', + 60, + ], + [ + 'Result of and is always true.', + 64, + //$tipText, + ], + [ + 'Result of and is always false.', + 66, + //$tipText, + ], + [ + 'Result of and is always false.', + 125, + ], + [ + 'Left side of and is always false.', + 139, + ], + [ + 'Right side of and is always false.', + 141, + ], + [ + 'Left side of and is always true.', + 145, + ], + [ + 'Right side of and is always true.', + 147, + ], ]); } @@ -193,7 +290,7 @@ public function testBugComposerDependentVariables(): void public function testBug2231(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-2231.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-2231.php'], [ [ 'Result of && is always false.', 21, @@ -224,4 +321,122 @@ public function testBug2870(): void $this->analyse([__DIR__ . '/data/bug-2870.php'], []); } + public function testBug2741(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2741.php'], [ + [ + 'Right side of && is always false.', + 21, + ], + ]); + } + + public function testBug7270(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7270.php'], []); + } + + public function testBug5743(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5743.php'], []); + } + + public function dataBug4969(): iterable + { + yield [false, []]; + yield [true, [ + [ + 'Result of && is always false.', + 15, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]]; + } + + /** + * @dataProvider dataBug4969 + * @param list $expectedErrors + */ + public function testBug4969(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/bug-4969.php'], $expectedErrors); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Left side of && is always true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of && is always true.', + 50, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Result of && is always true.', + 81, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Left side of && is always true.', + 13, + ], + [ + 'Left side of && is always true.', + 23, + ], + [ + 'Right side of && is always true.', + 40, + ], + [ + 'Right side of && is always true.', + 50, + ], + [ + 'Result of && is always true.', + 69, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Result of && is always true.', + 81, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/boolean-and-report-always-true-last-condition.php'], $expectedErrors); + } + + public function testBug5365(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/bug-5365.php'], []); + } + + public function testBug8555(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8555.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index 94c373f7ea..f9bef9b5d1 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -14,6 +13,8 @@ class BooleanNotConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new BooleanNotConstantConditionRule( @@ -27,6 +28,8 @@ protected function getRule(): Rule $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -63,6 +66,11 @@ public function testRule(): void 'Negated boolean expression is always false.', 50, ], + [ + 'Negated boolean expression is always true.', + 67, + 'Remove remaining cases below this one and this error will disappear too.', + ], ]); } @@ -116,12 +124,80 @@ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAs 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'], []); } + public function testBug5317(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5317.php'], [ + [ + 'Negated boolean expression is always false.', + 18, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + public function testBug8797(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8797.php'], []); + } + + public function testBug7937(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7937.php'], []); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Negated boolean expression is always true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Negated boolean expression is always false.', + 40, + ], + [ + 'Negated boolean expression is always false.', + 50, + ], + ]]; + yield [true, [ + [ + 'Negated boolean expression is always true.', + 13, + ], + [ + 'Negated boolean expression is always true.', + 23, + ], + [ + 'Negated boolean expression is always false.', + 40, + ], + [ + 'Negated boolean expression is always false.', + 50, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/boolean-not-report-always-true-last-condition.php'], $expectedErrors); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index c2fffe45d7..ca46233349 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -14,6 +14,8 @@ class BooleanOrConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new BooleanOrConstantConditionRule( @@ -27,6 +29,8 @@ protected function getRule(): Rule $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -106,6 +110,90 @@ public function testRule(): void 'Right side of || is always true.', 85, ], + [ + 'Left side of || is always true.', + 101, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of || is always true.', + 110, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testRuleLogicalOr(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/boolean-logical-or.php'], [ + [ + 'Left side of or is always true.', + 15, + ], + [ + 'Right side of or is always true.', + 19, + ], + [ + 'Left side of or is always false.', + 24, + ], + [ + 'Right side of or is always false.', + 27, + ], + [ + 'Right side of or is always true.', + 30, + ], + [ + 'Result of or is always true.', + 33, + ], + [ + 'Right side of or is always false.', + 36, + ], + [ + 'Right side of or is always false.', + 39, + ], + [ + 'Result of or is always true.', + 50, + $tipText, + ], + [ + 'Result of or is always true.', + 54, + $tipText, + ], + [ + 'Result of or is always true.', + 61, + ], + [ + 'Result of or is always true.', + 65, + ], + [ + 'Left side of or is always false.', + 77, + ], + [ + 'Right side of or is always false.', + 79, + ], + [ + 'Left side of or is always true.', + 83, + ], + [ + 'Right side of or is always true.', + 85, + ], ]); } @@ -186,4 +274,105 @@ public function testBug6258(): void $this->analyse([__DIR__ . '/data/bug-6258.php'], []); } + public function testBug2741(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2741-or.php'], [ + [ + 'Right side of || is always false.', + 21, + ], + ]); + } + + public function testBug7881(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-7881.php'], []); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Left side of || is always true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of || is always true.', + 50, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Result of || is always true.', + 81, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Left side of || is always true.', + 13, + ], + [ + 'Left side of || is always true.', + 23, + ], + [ + 'Right side of || is always true.', + 40, + ], + [ + 'Right side of || is always true.', + 50, + ], + [ + 'Result of || is always true.', + 69, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Result of || is always true.', + 81, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/boolean-or-report-always-true-last-condition.php'], $expectedErrors); + } + + public function testBug6551(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/bug-6551.php'], [ + [ + 'Result of || is always true.', + 49, + ], + [ + 'Result of || is always true.', + 61, + ], + ]); + } + + public function testBug4004(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/bug-4004.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php new file mode 100644 index 0000000000..e49a6100f7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -0,0 +1,245 @@ + + */ +class ConstantLooseComparisonRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + private bool $reportAlwaysTrueInLastCondition = false; + + protected function getRule(): Rule + { + return new ConstantLooseComparisonRule( + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/loose-comparison.php'], [ + [ + "Loose comparison using == between 0 and '0' will always evaluate to true.", + 16, + ], + [ + "Loose comparison using == between 0 and '1' will always evaluate to false.", + 20, + ], + [ + "Loose comparison using == between 0 and '1' will always evaluate to false.", + 27, + ], + [ + "Loose comparison using == between 0 and '1' will always evaluate to false.", + 33, + ], + [ + "Loose comparison using == between 0 and '0' will always evaluate to true.", + 35, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Loose comparison using != between 3 and 3 will always evaluate to false.', + 48, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + public function testBug8485(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8485.php'], [ + [ + 'Loose comparison using == between Bug8485\E::c and Bug8485\E::c will always evaluate to true.', + 21, + ], + [ + 'Loose comparison using == between Bug8485\F::c and Bug8485\E::c will always evaluate to false.', + 26, + ], + [ + 'Loose comparison using == between Bug8485\F::c and Bug8485\E::c will always evaluate to false.', + 31, + ], + [ + 'Loose comparison using == between Bug8485\F and Bug8485\E will always evaluate to false.', + 38, + ], + [ + 'Loose comparison using == between Bug8485\F and Bug8485\E::c will always evaluate to false.', + 43, + ], + ]); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Loose comparison using == between 1 and 1 will always evaluate to true.', + 21, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Loose comparison using == between 1 and 1 will always evaluate to true.', + 12, + ], + [ + 'Loose comparison using == between 1 and 1 will always evaluate to true.', + 21, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/loose-comparison-report-always-true-last-condition.php'], $expectedErrors); + } + + public function dataTreatPhpDocTypesAsCertain(): iterable + { + yield [false, []]; + yield [true, [ + [ + 'Loose comparison using == between 3 and 3 will always evaluate to true.', + 14, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]]; + } + + /** + * @dataProvider dataTreatPhpDocTypesAsCertain + * @param list $expectedErrors + */ + public function testTreatPhpDocTypesAsCertain(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/loose-comparison-treat-phpdoc-types.php'], $expectedErrors); + } + + public function testBug11694(): void + { + $expectedErrors = [ + [ + 'Loose comparison using == between 3 and int<10, 20> will always evaluate to false.', + 17, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Loose comparison using == between int<10, 20> and 3 will always evaluate to false.', + 18, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Loose comparison using == between 23 and int<10, 20> will always evaluate to false.', + 23, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Loose comparison using == between int<10, 20> and 23 will always evaluate to false.', + 24, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Loose comparison using == between null and int<10, 20> will always evaluate to false.', + 26, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Loose comparison using == between int<10, 20> and null will always evaluate to false.', + 27, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]; + + if (PHP_VERSION_ID >= 80000) { + $expectedErrors = array_merge($expectedErrors, [ + [ + "Loose comparison using == between '13foo' and int<10, 20> will always evaluate to false.", + 29, + ], + [ + "Loose comparison using == between int<10, 20> and '13foo' will always evaluate to false.", + 30, + ], + ]); + } + + $expectedErrors = array_merge($expectedErrors, [ + [ + 'Loose comparison using == between \' 3\' and int<10, 20> will always evaluate to false.', + 32, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Loose comparison using == between int<10, 20> and \' 3\' will always evaluate to false.', + 33, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Loose comparison using == between \' 23\' and int<10, 20> will always evaluate to false.', + 38, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Loose comparison using == between int<10, 20> and \' 23\' will always evaluate to false.', + 39, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Loose comparison using == between true and int<10, 20> will always evaluate to true.', + 41, + ], + [ + 'Loose comparison using == between int<10, 20> and true will always evaluate to true.', + 42, + ], + [ + 'Loose comparison using == between false and int<10, 20> will always evaluate to false.', + 44, + ], + [ + 'Loose comparison using == between int<10, 20> and false will always evaluate to false.', + 45, + ], + ]); + + $this->analyse([__DIR__ . '/data/bug-11694.php'], $expectedErrors); + } + + public function testBug8800(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8800.php'], [ + [ + 'Loose comparison using == between 0|1|false and 2 will always evaluate to false.', + 9, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php index 77ce3dad2c..38f3237a45 100644 --- a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -26,6 +26,7 @@ protected function getRule(): Rule $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, + true, ); } diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 9683c239a0..f337950a0f 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,6 +14,8 @@ class ElseIfConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new ElseIfConstantConditionRule( @@ -26,6 +29,8 @@ protected function getRule(): Rule $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -34,16 +39,57 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - - public function testRule(): void + public function dataRule(): iterable { - $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/elseif-condition.php'], [ + yield [false, [ + [ + 'Elseif condition is always true.', + 56, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Elseif condition is always false.', + 73, + ], + [ + 'Elseif condition is always false.', + 77, + ], + ]]; + + yield [true, [ [ 'Elseif condition is always true.', 18, ], - ]); + [ + 'Elseif condition is always true.', + 52, + ], + [ + 'Elseif condition is always true.', + 56, + ], + [ + 'Elseif condition is always false.', + 73, + ], + [ + 'Elseif condition is always false.', + 77, + ], + ]]; + } + + /** + * @dataProvider dataRule + * @param list $expectedErrors + */ + public function testRule(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/elseif-condition.php'], $expectedErrors); } public function testDoNotReportPhpDoc(): void @@ -52,7 +98,8 @@ public function testDoNotReportPhpDoc(): void $this->analyse([__DIR__ . '/data/elseif-condition-not-phpdoc.php'], [ [ 'Elseif condition is always true.', - 18, + 46, + 'Remove remaining cases below this one and this error will disappear too.', ], ]); } @@ -63,11 +110,47 @@ public function testReportPhpDoc(): void $this->analyse([__DIR__ . '/data/elseif-condition-not-phpdoc.php'], [ [ 'Elseif condition is always true.', - 18, + 46, + 'Remove remaining cases below this one and this error will disappear too.', ], [ 'Elseif condition is always true.', - 24, + 56, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testBug11674(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-11674.php'], [ + [ + 'Elseif condition is always false.', + 28, + ], + [ + 'Elseif condition is always false.', + 36, + ], + ]); + } + + public function testBug6947(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6947.php'], [ + [ + 'Elseif condition is always false.', + 13, '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/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 03b7bd365d..25e362f6cc 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -26,6 +27,7 @@ protected function getRule(): Rule $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, + true, ); } @@ -50,6 +52,7 @@ public function testRule(): void [ 'If condition is always true.', 96, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'If condition is always true.', @@ -116,4 +119,70 @@ public function testBug4043(): void ]); } + public function testBug5370(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5370.php'], []); + } + + public function testBug6902(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6902.php'], []); + } + + public function testBug8485(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->treatPhpDocTypesAsCertain = true; + + // reported by ConstantLooseComparisonRule instead + $this->analyse([__DIR__ . '/data/bug-8485.php'], []); + } + + public function testBug4302(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4302.php'], []); + } + + public function testBug7491(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7491.php'], []); + } + + public function testBug2499(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2499.php'], []); + } + + public function testBug10561(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-10561.php'], []); + } + + public function testBug4912(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4912.php'], []); + } + + public function testBug4864(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4864.php'], []); + } + + public function testBug8926(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8926.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index f057e1aa81..219e0450c1 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -5,6 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use stdClass; +use function array_filter; +use function array_map; +use function array_values; +use function count; use const PHP_VERSION_ID; /** @@ -13,10 +17,10 @@ class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase { - private bool $checkAlwaysTrueCheckTypeFunctionCall; - private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new ImpossibleCheckTypeFunctionCallRule( @@ -26,8 +30,9 @@ protected function getRule(): Rule [stdClass::class], $this->treatPhpDocTypesAsCertain, ), - $this->checkAlwaysTrueCheckTypeFunctionCall, $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -38,7 +43,10 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool public function testImpossibleCheckTypeFunctionCall(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + $this->treatPhpDocTypesAsCertain = true; $this->analyse( [__DIR__ . '/data/check-type-function-call.php'], @@ -84,270 +92,203 @@ public function testImpossibleCheckTypeFunctionCall(): void 'Call to function is_string() with string will always evaluate to true.', 140, ], + [ + 'Call to function method_exists() with CheckTypeFunctionCall\Foo and \'test\' will always evaluate to false.', + 176, + ], [ 'Call to function method_exists() with CheckTypeFunctionCall\Foo and \'doFoo\' will always evaluate to true.', 179, ], + [ + 'Call to function method_exists() with CheckTypeFunctionCall\Foo and \'doFoo\' will always evaluate to true.', + 189, + ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\FinalClassWithMethodExists) and \'doFoo\' will always evaluate to true.', - 191, + 201, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\FinalClassWithMethodExists) and \'doBar\' will always evaluate to false.', - 194, + 204, ], [ 'Call to function property_exists() with $this(CheckTypeFunctionCall\FinalClassWithPropertyExists) and \'fooProperty\' will always evaluate to true.', - 209, - ], - [ - 'Call to function property_exists() with $this(CheckTypeFunctionCall\FinalClassWithPropertyExists) and \'barProperty\' will always evaluate to false.', - 212, + 220, ], [ 'Call to function in_array() with arguments int, array{\'foo\', \'bar\'} and true will always evaluate to false.', - 235, + 246, ], [ '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.', - 248, + 255, ], [ 'Call to function in_array() with arguments \'foo\', array{\'foo\'} and true will always evaluate to true.', - 252, + 263, ], [ 'Call to function in_array() with arguments \'foo\', array{\'foo\', \'bar\'} and true will always evaluate to true.', - 256, + 267, ], [ 'Call to function in_array() with arguments \'bar\', array{}|array{\'foo\'} and true will always evaluate to false.', - 320, + 331, ], [ 'Call to function in_array() with arguments \'baz\', array{0: \'bar\', 1?: \'foo\'} and true will always evaluate to false.', - 336, + 347, ], [ 'Call to function in_array() with arguments \'foo\', array{} and true will always evaluate to false.', - 343, + 354, ], [ 'Call to function array_key_exists() with \'a\' and array{a: 1, b?: 2} will always evaluate to true.', - 360, + 371, ], [ 'Call to function array_key_exists() with \'c\' and array{a: 1, b?: 2} will always evaluate to false.', - 366, + 377, ], [ 'Call to function is_string() with mixed will always evaluate to false.', - 560, + 571, ], [ 'Call to function is_callable() with mixed will always evaluate to false.', - 571, + 582, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExists\' and \'testWithStringFirst…\' will always evaluate to true.', - 585, + 596, ], [ 'Call to function method_exists() with \'UndefinedClass\' and string will always evaluate to false.', - 594, + 605, ], [ 'Call to function method_exists() with \'UndefinedClass\' and \'test\' will always evaluate to false.', - 597, + 608, + ], + [ + 'Call to function method_exists() with CheckTypeFunctionCall\MethodExists and \'testWithNewObjectIn…\' will always evaluate to true.', + 620, + ], + [ + 'Call to function method_exists() with CheckTypeFunctionCall\MethodExists and \'undefinedMethod\' will always evaluate to false.', + 623, ], [ 'Call to function method_exists() with CheckTypeFunctionCall\MethodExists and \'testWithNewObjectIn…\' will always evaluate to true.', - 609, + 635, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'method\' will always evaluate to true.', - 624, + 650, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'someAnother\' will always evaluate to true.', - 627, + 653, ], [ 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'unknown\' will always evaluate to false.', - 630, + 656, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', - 633, + 659, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'someAnother\' will always evaluate to true.', - 636, + 662, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 639, + 665, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', - 642, + 668, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'someAnother\' will always evaluate to true.', - 645, + 671, ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 648, + 674, ], [ 'Call to function is_string() with string will always evaluate to true.', - 677, + 703, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function assert() with true will always evaluate to true.', - 692, + 718, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with \'123\' will always evaluate to true.', - 692, + 718, ], [ 'Call to function assert() with false will always evaluate to false.', - 693, + 719, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', - 693, + 719, ], [ 'Call to function assert() with true will always evaluate to true.', - 700, + 726, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with 123|float will always evaluate to true.', - 700, + 726, ], [ 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', - 782, + 809, ], [ 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', - 786, - ], - ], - ); - } - - public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void - { - $this->checkAlwaysTrueCheckTypeFunctionCall = false; - $this->treatPhpDocTypesAsCertain = true; - $this->analyse( - [__DIR__ . '/data/check-type-function-call.php'], - [ - [ - 'Call to function is_int() with string will always evaluate to false.', - 31, - ], - [ - 'Call to function is_callable() with array will always evaluate to false.', - 44, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', - ], - [ - 'Call to function assert() with false will always evaluate to false.', - 48, - ], - [ - 'Call to function is_callable() with \'nonexistentFunction\' will always evaluate to false.', - 87, + 813, ], [ - 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', - 105, - ], - [ - 'Call to function method_exists() with $this(CheckTypeFunctionCall\FinalClassWithMethodExists) and \'doBar\' will always evaluate to false.', - 194, - ], - [ - 'Call to function property_exists() with $this(CheckTypeFunctionCall\FinalClassWithPropertyExists) and \'barProperty\' will always evaluate to false.', - 212, - ], - [ - '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.', - 244, - ], - [ - '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.', - 336, - ], - [ - '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.', - 366, - ], - [ - 'Call to function is_string() with mixed will always evaluate to false.', - 560, - ], - [ - 'Call to function is_callable() with mixed will always evaluate to false.', - 571, - ], - [ - 'Call to function method_exists() with \'UndefinedClass\' and string will always evaluate to false.', - 594, - ], - [ - 'Call to function method_exists() with \'UndefinedClass\' and \'test\' will always evaluate to false.', - 597, - ], - [ - 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'unknown\' will always evaluate to false.', - 630, + 'Call to function testIsInt() with int will always evaluate to true.', + 900, ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 639, - ], - [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 648, - ], - [ - 'Call to function assert() with false will always evaluate to false.', - 693, + 'Call to function is_int() with int will always evaluate to true.', + 914, + 'Remove remaining cases below this one and this error will disappear too.', ], [ - 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', - 693, + 'Call to function in_array() with arguments 1, array and true will always evaluate to false.', + 952, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ], ); } + public function testBug7898(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7898.php'], []); + } + public function testDoNotReportTypesFromPhpDocs(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ [ @@ -359,7 +300,6 @@ public function testDoNotReportTypesFromPhpDocs(): void public function testReportTypesFromPhpDocs(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ [ @@ -371,47 +311,51 @@ public function testReportTypesFromPhpDocs(): void 19, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], + [ + 'Call to function in_array() with arguments int, array and true will always evaluate to false.', + 27, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Call to function in_array() with arguments 1, array and true will always evaluate to false.', + 30, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ]); } public function testBug2550(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2550.php'], []); } public function testBug3994(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3994.php'], []); } public function testBug1613(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-1613.php'], []); } public function testBug2714(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2714.php'], []); } public function testBug4657(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-4657.php'], []); } public function testBug4999(): void { - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = false; $this->analyse([__DIR__ . '/data/bug-4999.php'], []); } @@ -422,7 +366,6 @@ public function testArrayIsList(): void $this->markTestSkipped('Test requires PHP 8.1.'); } - $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/array-is-list.php'], [ [ @@ -443,14 +386,12 @@ public function testArrayIsList(): void 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'], [ [ @@ -466,71 +407,601 @@ public function testBug6305(): void 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, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function in_array() with arguments \'b\', non-empty-array and true will always evaluate to false.', 43, + //'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function in_array() with arguments int, array{} and true will always evaluate to false.', 47, ], + [ + 'Call to function in_array() with arguments int, array and true will always evaluate to false.', + 61, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ]); } 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'], []); } + public function testSlevomatCsInArrayBug(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/slevomat-cs-in-array.php'], []); + } + + public function testNonEmptySpecifiedString(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/non-empty-string-impossible-type.php'], []); + } + + public function testBug2755(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2755.php'], []); + } + + public function testBug7079(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7079.php'], []); + } + + public function testConditionalTypesInference(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/conditional-types-inference.php'], [ + [ + 'Call to function testIsInt() with string will always evaluate to false.', + 49, + ], + [ + 'Call to function testIsNotInt() with string will always evaluate to true.', + 55, + ], + [ + 'Call to function testIsInt() with int will always evaluate to true.', + 66, + ], + [ + 'Call to function testIsNotInt() with int will always evaluate to false.', + 72, + ], + [ + 'Call to function assertIsInt() with int will always evaluate to true.', + 78, + ], + ]); + } + + public function testBug6697(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6697.php'], []); + } + + public function testBug6443(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6443.php'], []); + } + + public function testBug7684(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7684.php'], []); + } + + public function testBug7224(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7224.php'], []); + } + + public function testBug4708(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4708.php'], []); + } + + public function testBug3821(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3821.php'], []); + } + + public function testBug6599(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6599.php'], []); + } + + public function testBug7914(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7914.php'], []); + } + + public function testDocblockAssertEquality(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/docblock-assert-equality.php'], [ + [ + 'Call to function isAnInteger() with int will always evaluate to true.', + 42, + ], + ]); + } + + public function testBug8076(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8076.php'], []); + } + + public function testBug8562(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8562.php'], []); + } + + public function testBug6938(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-6938.php'], []); + } + + public function testBug8727(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8727.php'], []); + } + + public function testBug8474(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8474.php'], []); + } + + public function testBug5695(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-5695.php'], []); + } + + public function testBug8752(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8752.php'], []); + } + + public function testDiscussion9134(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/discussion-9134.php'], []); + } + + public function testImpossibleMethodExistOnGenericClassString(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/impossible-method-exists-on-generic-class-string.php'], [ + [ + "Call to function method_exists() with class-string&literal-string and 'staticAbc' will always evaluate to true.", + 18, + $tipText, + ], + [ + "Call to function method_exists() with class-string&literal-string and 'nonStaticAbc' will always evaluate to true.", + 23, + $tipText, + ], + [ + "Call to function method_exists() with class-string&literal-string and 'nonExistent' will always evaluate to false.", + 34, + $tipText, + ], + [ + "Call to function method_exists() with class-string&literal-string and 'staticAbc' will always evaluate to true.", + 39, + $tipText, + ], + [ + "Call to function method_exists() with class-string&literal-string and 'nonStaticAbc' will always evaluate to true.", + 44, + $tipText, + ], + + ]); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Call to function is_int() with int will always evaluate to true.', + 21, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Call to function is_int() with int will always evaluate to true.', + 12, + ], + [ + 'Call to function is_int() with int will always evaluate to true.', + 21, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/impossible-function-report-always-true-last-condition.php'], $expectedErrors); + } + + public function testObjectShapes(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/property-exists-object-shapes.php'], [ + [ + 'Call to function property_exists() with object{foo: int, bar?: string} and \'baz\' will always evaluate to false.', + 24, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + /** @return list */ + private static function getLooseComparisonAgainsEnumsIssues(): array + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + return [ + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\\FooUnitEnum and array{\'A\'} will always evaluate to false.', + 21, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooUnitEnum, array{\'A\'} and false will always evaluate to false.', + 24, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\\FooBackedEnum and array{\'A\'} will always evaluate to false.', + 27, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooBackedEnum, array{\'A\'} and false will always evaluate to false.', + 30, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum, array{\'A\'} and false will always evaluate to false.', + 33, + ], + [ + 'Call to function in_array() with \'A\' and array{LooseComparisonAgainstEnums\\FooUnitEnum} will always evaluate to false.', + 39, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', + 42, + ], + [ + 'Call to function in_array() with \'A\' and array{LooseComparisonAgainstEnums\\FooBackedEnum} will always evaluate to false.', + 45, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum} and false will always evaluate to false.', + 48, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', + 51, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{bool} will always evaluate to false.', + 57, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', + 60, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooBackedEnum and array{bool} will always evaluate to false.', + 63, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum, array{bool} and false will always evaluate to false.', + 66, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', + 69, + ], + [ + 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooUnitEnum} will always evaluate to false.', + 75, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', + 78, + ], + [ + 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', + 81, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum} and false will always evaluate to false.', + 84, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', + 87, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{null} will always evaluate to false.', + 93, + ], + [ + 'Call to function in_array() with null and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', + 96, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array will always evaluate to false.', + 125, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array and false will always evaluate to false.', + 128, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array and true will always evaluate to false.', + 131, + $tipText, + ], + [ + 'Call to function in_array() with string and array will always evaluate to false.', + 143, + $tipText, + ], + [ + 'Call to function in_array() with arguments string, array and false will always evaluate to false.', + 146, + $tipText, + ], + [ + 'Call to function in_array() with arguments string, array and true will always evaluate to false.', + 149, + $tipText, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::B and non-empty-array will always evaluate to false.', + 159, + $tipText, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::A and non-empty-array will always evaluate to true.', + 162, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array and false will always evaluate to true.', + 165, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array and true will always evaluate to true.', + 168, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array and false will always evaluate to false.', + 171, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array and true will always evaluate to false.', + 174, + 'BUG', + //$tipText, + ], + ]; + } + + public function testLooseComparisonAgainstEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $issues = array_map( + static function (array $i): array { + if (($i[2] ?? null) === 'BUG') { + unset($i[2]); + } + + return $i; + }, + self::getLooseComparisonAgainsEnumsIssues(), + ); + $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); + } + + public function testNonStrictInArray(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662.php'], []); + } + + public function testNonStrictInArrayEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662-enums.php'], [ + [ + "Call to function in_array() with 'NotAnEnumCase' and array will always evaluate to false.", + 19, + $tipText, + ], + [ + "Call to function in_array() with 'NotAnEnumCase' and array will always evaluate to false.", + 62, + $tipText, + ], + [ + 'Call to function in_array() with string and array will always evaluate to false.', + 77, + ], + [ + 'Call to function in_array() with int and array will always evaluate to false.', + 84, + ], + ]); + } + + public function testLooseComparisonAgainstEnumsNoPhpdoc(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->treatPhpDocTypesAsCertain = false; + $issues = self::getLooseComparisonAgainsEnumsIssues(); + $issues = array_values(array_filter($issues, static fn (array $i) => count($i) === 2)); + $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); + } + + public function testBug10502(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-10502.php'], [ + [ + "Call to function is_callable() with array{ArrayObject, 'count'} will always evaluate to true.", + 23, + ], + [ + "Call to function is_callable() with array{1: 'count', 0: ArrayObject} will always evaluate to true.", + 24, + $tipText, + ], + ]); + } + + public function testAlwaysTruePregMatch(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/always-true-preg-match.php'], []); + } + + public function testBug3979(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3979.php'], []); + } + + public function testBug8464(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8464.php'], []); + } + + public function testBug8954(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8954.php'], []); + } + + public function testBugPR3404(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-pr-3404.php'], [ + [ + 'Call to function is_a() with arguments BugPR3404\Location, \'BugPR3404\\\\Location\' and true will always evaluate to true.', + 21, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php index 7d12e3f769..67245ebaba 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php @@ -21,6 +21,7 @@ public function getRule(): Rule true, ), true, + false, true, ); } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php index ef32567c9f..b81646c023 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -21,6 +21,7 @@ public function getRule(): Rule true, ), true, + false, true, ); } @@ -39,10 +40,12 @@ public function testRule(): void [ 'Call to method PHPStan\Tests\AssertionClass::assertNotInt() with int will always evaluate to false.', 30, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method PHPStan\Tests\AssertionClass::assertNotInt() with string will always evaluate to true.', 36, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 1 will always evaluate to true.', @@ -71,10 +74,12 @@ public function testRule(): void [ 'Call to method ImpossibleMethodCall\Foo::isSame() with \'foo\' and \'foo\' will always evaluate to true.', 101, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'foo\' and \'foo\' will always evaluate to false.', 104, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isSame() with array{} and array{} will always evaluate to true.', @@ -84,6 +89,22 @@ public function testRule(): void '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 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 \'\' and \'\' will always evaluate to true.', 174, @@ -100,6 +121,11 @@ public function testRule(): void 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 2 and 2 will always evaluate to false.', 194, ], + [ + 'Call to method ImpossibleMethodCall\ConditionalAlwaysTrue::isInt() with int will always evaluate to true.', + 208, + 'Remove remaining cases below this one and this error will disappear too.', + ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 5db54072a4..6bc2d8e2d5 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,6 +14,8 @@ class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + public function getRule(): Rule { return new ImpossibleCheckTypeMethodCallRule( @@ -22,8 +25,9 @@ public function getRule(): Rule [], $this->treatPhpDocTypesAsCertain, ), - true, $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -47,10 +51,12 @@ public function testRule(): void [ 'Call to method PHPStan\Tests\AssertionClass::assertNotInt() with int will always evaluate to false.', 30, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method PHPStan\Tests\AssertionClass::assertNotInt() with string will always evaluate to true.', 36, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 1 will always evaluate to true.', @@ -79,10 +85,12 @@ public function testRule(): void [ 'Call to method ImpossibleMethodCall\Foo::isSame() with \'foo\' and \'foo\' will always evaluate to true.', 101, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'foo\' and \'foo\' will always evaluate to false.', 104, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isSame() with array{} and array{} will always evaluate to true.', @@ -148,6 +156,11 @@ public function testRule(): void 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 2 and 2 will always evaluate to false.', 194, ], + [ + 'Call to method ImpossibleMethodCall\ConditionalAlwaysTrue::isInt() with int will always evaluate to true.', + 208, + 'Remove remaining cases below this one and this error will disappear too.', + ], ]); } @@ -186,6 +199,84 @@ public function testReportPhpDoc(): void ]); } + public function testBug8169(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8169.php'], [ + [ + 'Call to method Bug8169\HelloWorld::assertString() with string will always evaluate to true.', + 21, + ], + [ + 'Call to method Bug8169\HelloWorld::assertString() with string will always evaluate to true.', + 28, + ], + [ + 'Call to method Bug8169\HelloWorld::assertString() with int will always evaluate to false.', + 35, + ], + ]); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with string will always evaluate to true.', + 25, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with string will always evaluate to true.', + 15, + ], + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with string will always evaluate to true.', + 25, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/impossible-method-report-always-true-last-condition.php'], $expectedErrors); + } + + public function testBug12473(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tip = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/bug-12473.php'], [ + /*[ + 'Call to method ReflectionClass::isSubclassOf() with \'Bug12473\\\\Picture\' will always evaluate to true.', + 39, + $tip, + ],*/ + [ + 'Call to method ReflectionClass::isSubclassOf() with \'Bug12473\\\\PictureProduct\' will always evaluate to false.', + 49, + $tip, + ], + /*[ + 'Call to method ReflectionClass::isSubclassOf() with \'Bug12473\\\\PictureUser\' will always evaluate to true.', + 59, + $tip, + ],*/ + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index 9115a5f9ce..1cdbc1a7ad 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -13,6 +13,8 @@ class ImpossibleCheckTypeStaticMethodCallRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + public function getRule(): Rule { return new ImpossibleCheckTypeStaticMethodCallRule( @@ -22,8 +24,9 @@ public function getRule(): Rule [], $this->treatPhpDocTypesAsCertain, ), - true, $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -60,6 +63,11 @@ public function testRule(): void 'Call to static method PHPStan\Tests\AssertionClass::assertInt() with arguments 1, 2 and 3 will always evaluate to true.', 34, ], + [ + 'Call to static method ImpossibleStaticMethodCall\ConditionalAlwaysTrue::isInt() with int will always evaluate to true.', + 66, + 'Remove remaining cases below this one and this error will disappear too.', + ], ]); } @@ -98,6 +106,44 @@ public function testReportPhpDocs(): void ]); } + public function testAssertUnresolvedGeneric(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/assert-unresolved-generic.php'], []); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Call to static method PHPStan\Tests\AssertionClass::assertInt() with int will always evaluate to true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Call to static method PHPStan\Tests\AssertionClass::assertInt() with int will always evaluate to true.', + 14, + ], + [ + 'Call to static method PHPStan\Tests\AssertionClass::assertInt() with int will always evaluate to true.', + 23, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/impossible-static-method-report-always-true-last-condition.php'], $expectedErrors); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php new file mode 100644 index 0000000000..c191856047 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php @@ -0,0 +1,78 @@ + + */ +class LogicalXorConstantConditionRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + private bool $reportAlwaysTrueInLastCondition = false; + + protected function getRule(): TRule + { + return new LogicalXorConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); + } + + public function testRule(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/logical-xor.php'], [ + [ + 'Left side of xor is always true.', + 14, + ], + [ + 'Right side of xor is always false.', + 14, + ], + [ + 'Left side of xor is always false.', + 17, + ], + [ + 'Right side of xor is always true.', + 17, + ], + [ + 'Left side of xor is always true.', + 20, + $tipText, + ], + [ + 'Right side of xor is always true.', + 20, + $tipText, + ], + [ + 'Left side of xor is always true.', + 24, + ], + [ + 'Right side of xor is always false.', + 24, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php new file mode 100644 index 0000000000..b52785c6f5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php @@ -0,0 +1,49 @@ + + */ +class MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(MatchExpressionRule::class); + } + + public function testBug9357(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9357.php'], []); + } + + public function testBug9007(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9007.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/doNotRememberPossiblyImpureValues.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 2e6618f5b0..d7a005c589 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -12,17 +12,35 @@ class MatchExpressionRuleTest extends RuleTestCase { + private bool $treatPhpDocTypesAsCertain = true; + + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { - return new MatchExpressionRule(true); + return new MatchExpressionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->reportAlwaysTrueInLastCondition, + $this->treatPhpDocTypesAsCertain, + ); } - public function testRule(): void + protected function shouldTreatPhpDocTypesAsCertain(): bool { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } + return $this->treatPhpDocTypesAsCertain; + } + public function testRule(): void + { + $tipText = 'Remove remaining cases below this one and this error will disappear too.'; $this->analyse([__DIR__ . '/data/match-expr.php'], [ [ 'Match arm comparison between 1|2|3 and \'foo\' is always false.', @@ -35,83 +53,53 @@ public function testRule(): void [ 'Match arm comparison between 3 and 3 is always true.', 28, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 29, + $tipText, ], [ 'Match arm comparison between 3 and 3 is always true.', 35, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 36, + $tipText, ], [ 'Match arm comparison between 1 and 1 is always true.', 40, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 41, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 42, + $tipText, ], [ 'Match arm comparison between 1 and 1 is always true.', 46, - ], - [ - 'Match arm is unreachable because previous comparison is always true.', - 47, + $tipText, ], [ 'Match expression does not handle remaining value: 3', 50, ], - [ - 'Match expression does not handle remaining values: 1|2|3', - 55, - ], [ 'Match arm comparison between 1|2 and 3 is always false.', - 65, - ], - [ - 'Match arm comparison between 1 and 1 is always true.', - 70, - ], - [ - 'Match arm comparison between true and false is always false.', - 86, + 61, ], [ - 'Match arm comparison between true and false is always false.', - 92, + 'Match expression does not handle remaining values: 1|2|3', + 78, ], [ 'Match expression does not handle remaining value: true', 90, ], + [ + 'Match expression does not handle remaining values: int|int<2, max>', + 168, + ], ]); } public function testBug5161(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-5161.php'], []); } public function testBug4857(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-4857.php'], [ [ 'Match expression does not handle remaining value: true', @@ -152,8 +140,29 @@ public function testEnums(): void 56, ], [ - 'Match arm comparison between *NEVER* and MatchEnums\Foo is always false.', - 77, + 'Match arm comparison between MatchEnums\Foo::THREE and MatchEnums\Foo::THREE is always true.', + 76, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Match arm comparison between MatchEnums\Foo and MatchEnums\Foo::ONE is always false.', + 85, + ], + [ + 'Match arm comparison between *NEVER* and MatchEnums\DifferentEnum::ONE is always false.', + 95, + ], + [ + 'Match arm comparison between MatchEnums\Foo and MatchEnums\Foo::ONE is always false.', + 104, + ], + [ + 'Match arm comparison between *NEVER* and MatchEnums\Foo::ONE is always false.', + 113, + ], + [ + 'Match arm comparison between *NEVER* and MatchEnums\DifferentEnum::ONE is always false.', + 113, ], ]); } @@ -167,4 +176,345 @@ public function testBug6394(): void $this->analyse([__DIR__ . '/data/bug-6394.php'], []); } + public function testBug6115(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-6115.php'], [ + [ + 'Match expression does not handle remaining value: 3', + 32, + ], + ]); + } + + public function testBug7095(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-7095.php'], []); + } + + public function testBug7176(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-7176.php'], []); + } + + public function testBug6064(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-6064.php'], []); + } + + public function testBug6647(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-6647.php'], []); + } + + public function testBug7622(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7622.php'], []); + } + + public function testBug7698(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7698.php'], []); + } + + public function testBug7746(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7746.php'], []); + } + + public function testBug8240(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8240.php'], [ + [ + 'Match arm comparison between Bug8240\Foo::BAR and Bug8240\Foo::BAR is always true.', + 13, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Match arm comparison between Bug8240\Foo2::BAZ and Bug8240\Foo2::BAZ is always true.', + 28, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testLastArmAlwaysTrue(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Remove remaining cases below this one and this error will disappear too.'; + $this->analyse([__DIR__ . '/data/last-match-arm-always-true.php'], [ + [ + 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Foo)&LastMatchArmAlwaysTrue\Foo::TWO and LastMatchArmAlwaysTrue\Foo::TWO is always true.', + 22, + $tipText, + ], + [ + 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Foo)&LastMatchArmAlwaysTrue\Foo::TWO and LastMatchArmAlwaysTrue\Foo::TWO is always true.', + 31, + $tipText, + ], + [ + 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Foo)&LastMatchArmAlwaysTrue\Foo::TWO and LastMatchArmAlwaysTrue\Foo::TWO is always true.', + 40, + $tipText, + ], + [ + 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Bar)&LastMatchArmAlwaysTrue\Bar::ONE and LastMatchArmAlwaysTrue\Bar::ONE is always true.', + 62, + $tipText, + ], + [ + 'Match arm comparison between 1 and 0 is always false.', + 70, + ], + [ + 'Match expression does not handle remaining value: 1', + 69, + ], + ]); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 49, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 15, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 23, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 45, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 49, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/match-always-true-last-arm.php'], $expectedErrors); + } + + public function testBug8932(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-8932.php'], []); + } + + public function testBug8937(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-8937.php'], []); + } + + public function testBug8900(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-8900.php'], []); + } + + public function testBug4451(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-4451.php'], []); + } + + public function testBug9007(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9007.php'], []); + } + + public function testBug9457(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9457.php'], []); + } + + public function testBug8614(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8614.php'], []); + } + + public function testBug8536(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8536.php'], []); + } + + public function testBug9499(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9499.php'], []); + } + + public function testBug6407(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-6407.php'], []); + } + + public function testBugUnhandledTrueWithComplexCondition(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-unhandled-true-with-complex-condition.php'], []); + } + + public function testBug11246(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11246.php'], []); + } + + public function testBug9879(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9879.php'], []); + } + + public function testBug11313(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11313.php'], []); + } + + public function testBug9436(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-9436.php'], []); + } + + public function testBug11852(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-11852.php'], []); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/match-expr-property-hooks.php'], [ + [ + 'Match expression does not handle remaining value: 3', + 13, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 6f56fdcccd..eb7fe43290 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -12,9 +12,19 @@ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase { + private bool $treatPhpDocTypesAsCertain = true; + protected function getRule(): Rule { - return new NumberComparisonOperatorsConstantConditionRule(); + return new NumberComparisonOperatorsConstantConditionRule( + $this->treatPhpDocTypesAsCertain, + true, + ); + } + + public function testBug8277(): void + { + $this->analyse([__DIR__ . '/data/bug-8277.php'], []); } public function testRule(): void @@ -47,9 +57,6 @@ public function testBug2648Namespace(): void public function testBug5161(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/bug-5161.php'], []); } @@ -90,9 +97,6 @@ public function testBug5707(): void 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'], []); } @@ -101,4 +105,151 @@ public function testBug5295(): void $this->analyse([__DIR__ . '/data/bug-5295.php'], []); } + public function testBug7052(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-7052.php'], [ + [ + 'Comparison operation ">" between Bug7052\Foo::A and Bug7052\Foo::B is always false.', + 16, + ], + [ + 'Comparison operation "<" between Bug7052\Foo::A and Bug7052\Foo::B is always false.', + 17, + ], + [ + 'Comparison operation ">=" between Bug7052\Foo::A and Bug7052\Foo::B is always false.', + 18, + ], + [ + 'Comparison operation "<=" between Bug7052\Foo::A and Bug7052\Foo::B is always false.', + 19, + ], + ]); + } + + public function testBug7044(): void + { + $this->analyse([__DIR__ . '/data/bug-7044.php'], [ + [ + 'Comparison operation "<" between 0 and 0 is always false.', + 15, + ], + ]); + } + + public function testBug3277(): void + { + $this->analyse([__DIR__ . '/data/bug-3277.php'], [ + [ + 'Comparison operation "<" between 5 and 4 is always false.', + 6, + ], + ]); + } + + public function testBug6013(): void + { + $this->analyse([__DIR__ . '/data/bug-6013.php'], []); + } + + public function testBug2851(): void + { + $this->analyse([__DIR__ . '/data/bug-2851.php'], []); + } + + public function testBug8643(): void + { + $this->analyse([__DIR__ . '/data/bug-8643.php'], []); + } + + public function dataTreatPhpDocTypesAsCertain(): iterable + { + yield [ + false, + [], + ]; + yield [ + true, + [ + [ + 'Comparison operation ">=" between int<1, max> and 0 is always true.', + 11, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Comparison operation "<" between int<1, max> and 0 is always false.', + 18, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ], + ]; + } + + /** + * @dataProvider dataTreatPhpDocTypesAsCertain + * @param list $expectedErrors + */ + public function testTreatPhpDocTypesAsCertain(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/number-comparison-treat.php'], $expectedErrors); + } + + public function testBug6776(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-6776.php'], []); + } + + public function testBug7075(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7075.php'], []); + } + + public function testBug8803(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8803.php'], []); + } + + public function testBug8938(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8938.php'], []); + } + + public function testBug5005(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5005.php'], []); + } + + public function testBug6467(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6467.php'], []); + } + + public function testBug6642(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6642.php'], []); + } + + public function testBug9850(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-9850.php'], []); + } + + public function testBug12716(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12716.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index ee5d2dcc42..4c27bfd80f 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_INT_SIZE; @@ -13,16 +14,28 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase { - private bool $checkAlwaysTrueStrictComparison; + private bool $reportAlwaysTrueInLastCondition = false; + + private bool $treatPhpDocTypesAsCertain = true; protected function getRule(): Rule { - return new StrictComparisonOfDifferentTypesRule($this->checkAlwaysTrueStrictComparison); + return new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; } public function testStrictComparison(): void { - $this->checkAlwaysTrueStrictComparison = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse( [__DIR__ . '/data/strict-comparison.php'], [ @@ -49,6 +62,7 @@ public function testStrictComparison(): void [ 'Strict comparison using === between 1 and array|bool|StrictComparison\Collection will always evaluate to false.', 19, + $tipText, ], [ 'Strict comparison using === between true and false will always evaluate to false.', @@ -91,20 +105,22 @@ public function testStrictComparison(): void 140, ], [ - 'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.', - 154, + 'Strict comparison using === between non-empty-array and null will always evaluate to false.', + 150, ], [ - 'Strict comparison using === between non-empty-array and null will always evaluate to false.', - 164, + 'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.', + 161, ], [ 'Strict comparison using !== between StrictComparison\Node|null and false will always evaluate to true.', 212, + $tipText, ], [ 'Strict comparison using !== between StrictComparison\Node|null and false will always evaluate to true.', 255, + $tipText, ], [ 'Strict comparison using !== between stdClass and null will always evaluate to true.', @@ -122,6 +138,10 @@ public function testStrictComparison(): void 'Strict comparison using === between array{X: 1, Y: 2} and array{X: 2, Y: 1} will always evaluate to false.', 300, ], + [ + 'Strict comparison using === between array{X: 1, Y: 2} and array{Y: 2, X: 1} will always evaluate to false.', + 308, + ], [ 'Strict comparison using === between \'/\'|\'\\\\\' and \'//\' will always evaluate to false.', 320, @@ -131,7 +151,7 @@ public function testStrictComparison(): void 335, ], [ - 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', 343, ], [ @@ -169,6 +189,7 @@ public function testStrictComparison(): void [ 'Strict comparison using === between int<0, 1> and 100 will always evaluate to false.', 622, + $tipText, ], [ 'Strict comparison using === between 100 and \'foo\' will always evaluate to false.', @@ -178,10 +199,6 @@ public function testStrictComparison(): void 'Strict comparison using === between int<10, max> and \'foo\' will always evaluate to false.', 635, ], - [ - 'Strict comparison using === between \'foofoofoofoofoofoof…\' and \'foofoofoofoofoofoof…\' will always evaluate to true.', - 654, - ], [ 'Strict comparison using === between string|null and 1 will always evaluate to false.', 685, @@ -197,10 +214,12 @@ public function testStrictComparison(): void [ 'Strict comparison using === between mixed and \'foo\' will always evaluate to false.', 808, + 'Type 1|string has already been eliminated from mixed.', ], [ 'Strict comparison using !== between mixed and 1 will always evaluate to true.', 812, + 'Type 1|string has already been eliminated from mixed.', ], [ 'Strict comparison using === between \'foo\' and \'foo\' will always evaluate to true.', @@ -230,143 +249,46 @@ public function testStrictComparison(): void 'Strict comparison using === between 1000 and 1000 will always evaluate to true.', 910, ], - ], - ); - } - - public function testStrictComparisonWithoutAlwaysTrue(): void - { - $this->checkAlwaysTrueStrictComparison = false; - $this->analyse( - [__DIR__ . '/data/strict-comparison.php'], - [ - [ - 'Strict comparison using === between 1 and \'1\' will always evaluate to false.', - 11, - ], - [ - 'Strict comparison using === between 1 and null will always evaluate to false.', - 14, - ], - [ - 'Strict comparison using === between StrictComparison\Bar and 1 will always evaluate to false.', - 15, - ], - [ - 'Strict comparison using === between 1 and array|bool|StrictComparison\Collection will always evaluate to false.', - 19, - ], - [ - 'Strict comparison using === between true and false will always evaluate to false.', - 30, - ], - [ - 'Strict comparison using === between false and true will always evaluate to false.', - 31, - ], - [ - 'Strict comparison using === between 1.0 and 1 will always evaluate to false.', - 46, - ], - [ - 'Strict comparison using === between 1 and 1.0 will always evaluate to false.', - 47, - ], - [ - 'Strict comparison using === between string and null will always evaluate to false.', - 69, - ], - [ - 'Strict comparison using === between 1|2|3 and null will always evaluate to false.', - 98, - ], - [ - 'Strict comparison using === between non-empty-array and null will always evaluate to false.', - 140, - ], - [ - 'Strict comparison using === between non-empty-array and null will always evaluate to false.', - 164, - ], - [ - 'Strict comparison using === between 1 and 2 will always evaluate to false.', - 284, - ], - [ - '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.', - 300, - ], - [ - 'Strict comparison using === between \'/\'|\'\\\\\' and \'//\' will always evaluate to false.', - 320, - ], - [ - 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', - 335, - ], - [ - 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', - 343, - ], - [ - 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', - 360, - ], - [ - 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', - 368, - ], - [ - 'Strict comparison using === between float and \'string\' will always evaluate to false.', - 386, - ], - [ - 'Strict comparison using === between float and \'string\' will always evaluate to false.', - 394, - ], [ - 'Strict comparison using !== between null and null will always evaluate to false.', - 408, + 'Strict comparison using === between INF and INF will always evaluate to true.', + 979, ], [ - '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.', - 466, + 'Strict comparison using === between NAN and NAN will always evaluate to false.', + 980, ], [ - 'Strict comparison using === between int<0, 1> and 100 will always evaluate to false.', - 622, + 'Strict comparison using !== between INF and INF will always evaluate to false.', + 982, ], [ - 'Strict comparison using === between 100 and \'foo\' will always evaluate to false.', - 624, + 'Strict comparison using !== between NAN and NAN will always evaluate to true.', + 983, ], [ - 'Strict comparison using === between int<10, max> and \'foo\' will always evaluate to false.', - 635, + 'Strict comparison using === between \'foofoofoofoofoofoof…\' and \'foofoofoofoofoofoof…\' will always evaluate to true.', + 996, + 'Remove remaining cases below this one and this error will disappear too.', ], [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 685, + 'Strict comparison using === between lowercase-string|false and \'AB\' will always evaluate to false.', + 1014, + $tipText, ], [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 695, + 'Strict comparison using === between mixed and null will always evaluate to false.', + 1030, + 'Type null has already been eliminated from mixed.', ], [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 705, + 'Strict comparison using !== between mixed and null will always evaluate to true.', + 1034, + 'Type null has already been eliminated from mixed.', ], [ - 'Strict comparison using === between mixed and \'foo\' will always evaluate to false.', - 808, + 'Strict comparison using !== between array{1, mixed, 3} and array{int, null, int} will always evaluate to true.', + 1048, + 'Offset 1: Type null has already been eliminated from mixed.', ], ], ); @@ -374,7 +296,6 @@ public function testStrictComparisonWithoutAlwaysTrue(): void public function testStrictComparisonPhp71(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/strict-comparison-71.php'], [ [ 'Strict comparison using === between null and null will always evaluate to true.', @@ -385,10 +306,6 @@ public function testStrictComparisonPhp71(): void public function testStrictComparisonPropertyNativeTypesPhp74(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/strict-comparison-property-native-types.php'], [ [ 'Strict comparison using === between string and null will always evaluate to false.', @@ -411,13 +328,11 @@ public function testStrictComparisonPropertyNativeTypesPhp74(): void public function testBug2835(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2835.php'], []); } public function testBug1860(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-1860.php'], [ [ 'Strict comparison using === between string and null will always evaluate to false.', @@ -432,31 +347,26 @@ public function testBug1860(): void public function testBug3544(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-3544.php'], []); } public function testBug2675(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2675.php'], []); } public function testBug2220(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2220.php'], []); } public function testBug1707(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-1707.php'], []); } public function testBug3357(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-3357.php'], []); } @@ -465,7 +375,6 @@ 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.', @@ -476,25 +385,21 @@ public function testBug4848(): void 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.', @@ -503,4 +408,617 @@ public function testBug5362(): void ]); } + public function testBug6939(): void + { + if (PHP_VERSION_ID < 80000) { + $this->analyse([__DIR__ . '/data/bug-6939.php'], []); + return; + } + + $this->analyse([__DIR__ . '/data/bug-6939.php'], [ + [ + 'Strict comparison using === between string and false will always evaluate to false.', + 10, + ], + ]); + } + + public function testBug7166(): void + { + $this->analyse([__DIR__ . '/data/bug-7166.php'], []); + } + + public function testBug7555(): void + { + $this->analyse([__DIR__ . '/data/bug-7555.php'], [ + [ + 'Strict comparison using === between 2 and 2 will always evaluate to true.', + 11, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + public function testBug7257(): void + { + $this->analyse([__DIR__ . '/data/bug-7257.php'], []); + } + + public function testBug5474(): void + { + $this->analyse([__DIR__ . '/data/bug-5474.php'], [ + [ + 'Strict comparison using !== between array{test: 1} and array{test: 1} will always evaluate to false.', + 25, + ], + [ + 'Strict comparison using !== between array{test: 1} and array{test: 5} will always evaluate to true.', + 29, + ], + ]); + } + + public function testBug7684(): void + { + $this->analyse([__DIR__ . '/data/bug-7684.php'], []); + } + + public function testBug6181(): void + { + $this->analyse([__DIR__ . '/data/bug-6181.php'], []); + } + + public function testBug2851b(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->analyse([__DIR__ . '/data/bug-2851b.php'], [ + [ + 'Strict comparison using === between 0 and 0 will always evaluate to true.', + 21, + $tipText, + ], + ]); + } + + public function testBug8158(): void + { + $this->analyse([__DIR__ . '/data/bug-8158.php'], []); + } + + public function testBug8485(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8485.php'], [ + [ + 'Strict comparison using === between Bug8485\E::c and Bug8485\E::c will always evaluate to true.', + 19, + 'Use match expression instead. PHPStan will report unhandled enum cases.', + ], + [ + 'Strict comparison using === between Bug8485\F::c and Bug8485\E::c will always evaluate to false.', + 24, + ], + [ + 'Strict comparison using === between Bug8485\F::c and Bug8485\E::c will always evaluate to false.', + 29, + ], + [ + 'Strict comparison using === between Bug8485\F and Bug8485\E will always evaluate to false.', + 36, + ], + [ + 'Strict comparison using === between Bug8485\F and Bug8485\E::c will always evaluate to false.', + 41, + ], + [ + 'Strict comparison using === between Bug8485\FooEnum::C and Bug8485\FooEnum::C will always evaluate to true.', + 67, + "• Remove remaining cases below this one and this error will disappear too.\n• Use match expression instead. PHPStan will report unhandled enum cases.", + ], + [ + 'Strict comparison using === between Bug8485\FooEnum::C and Bug8485\FooEnum::C will always evaluate to true.', + 74, + "• Remove remaining cases below this one and this error will disappear too.\n• Use match expression instead. PHPStan will report unhandled enum cases.", + ], + ]); + } + + public function testBug8516(): void + { + $this->analyse([__DIR__ . '/data/bug-8516.php'], []); + } + + public function testPhpUnitIntegration(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/phpunit-integration.php'], []); + } + + public function testBug8586(): void + { + $this->analyse([__DIR__ . '/data/bug-8586.php'], []); + } + + public function testBug4242(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-4242.php'], []); + } + + public function testBug3633(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/bug-3633.php'], [ + [ + 'Strict comparison using === between class-string and \'Bug3633\\\OtherClass\' will always evaluate to false.', + 37, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\HelloWorld\' and \'Bug3633\\\HelloWorld\' will always evaluate to true.', + 41, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\HelloWorld\' and \'Bug3633\\\OtherClass\' will always evaluate to false.', + 44, + ], + [ + 'Strict comparison using === between class-string and \'Bug3633\\\HelloWorld\' will always evaluate to false.', + 64, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\OtherClass\' and \'Bug3633\\\HelloWorld\' will always evaluate to false.', + 71, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\OtherClass\' and \'Bug3633\\\OtherClass\' will always evaluate to true.', + 74, + $tipText, + ], + [ + 'Strict comparison using === between class-string and \'Bug3633\\\HelloWorld\' will always evaluate to false.', + 93, + $tipText, + ], + [ + 'Strict comparison using === between class-string and \'Bug3633\\\OtherClass\' will always evaluate to false.', + 96, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\FinalClass\' and \'Bug3633\\\FinalClass\' will always evaluate to true.', + 102, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\FinalClass\' and \'Bug3633\\\HelloWorld\' will always evaluate to false.', + 106, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\FinalClass\' and \'Bug3633\\\OtherClass\' will always evaluate to false.', + 109, + $tipText, + ], + [ + 'Strict comparison using !== between \'Bug3633\\\FinalClass\' and \'Bug3633\\\FinalClass\' will always evaluate to false.', + 112, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\FinalClass\' and \'Bug3633\\\FinalClass\' will always evaluate to true.', + 115, + ], + ]); + } + + public function testLastConditionAlwaysTrue(): void + { + $this->analyse([__DIR__ . '/data/strict-comparison-last-condition-always-true.php'], [ + [ + 'Strict comparison using === between \'bar\' and \'bar\' will always evaluate to true.', + 15, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testBug3019(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3019.php'], []); + } + + public function testBug7578(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7578.php'], []); + } + + public function testBug6260(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-6260.php'], []); + } + + public function testBug8736(): void + { + $this->analyse([__DIR__ . '/data/bug-8736.php'], []); + } + + public function dataLastMatchArm(): iterable + { + yield [false, [ + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 36, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + "Strict comparison using === between *NEVER* and 'ccc' will always evaluate to false.", + 38, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 46, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 62, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 79, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 17, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 30, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 36, + ], + [ + "Strict comparison using === between *NEVER* and 'ccc' will always evaluate to false.", + 38, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 46, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 62, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 75, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 79, + ], + ]]; + } + + /** + * @dataProvider dataLastMatchArm + * @param list $expectedErrors + */ + public function testLastMatchArm(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/strict-comparison-last-match-arm.php'], $expectedErrors); + } + + public function testBug8030(): void + { + $this->analyse([__DIR__ . '/data/bug-8030.php'], []); + } + + public function testBug8776Part1(): void + { + $this->analyse([__DIR__ . '/data/bug-8776-1.php'], []); + } + + public function testBug8776Part2(): void + { + $this->analyse([__DIR__ . '/data/bug-8776-2.php'], []); + } + + public function testBug5978(): void + { + if (PHP_VERSION_ID >= 80000) { + $expectedErrors = [ + [ + 'Strict comparison using === between non-empty-string and false will always evaluate to false.', + 7, + ], + [ + 'Strict comparison using === between non-empty-string and null will always evaluate to false.', + 7, + ], + ]; + } else { + $expectedErrors = []; + } + + $this->analyse([__DIR__ . '/data/bug-5978.php'], $expectedErrors); + } + + public function testBug9104(): void + { + $this->analyse([__DIR__ . '/data/bug-9104.php'], [ + [ + 'Strict comparison using === between int<1, max> and 0 will always evaluate to false.', + 12, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + public function testEnumTips(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/strict-comparison-enum-tips.php'], [ + [ + 'Strict comparison using === between StrictComparisonEnumTips\SomeEnum::Two and StrictComparisonEnumTips\SomeEnum::Two will always evaluate to true.', + 52, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testBug9142(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9142.php'], [ + [ + 'Strict comparison using === between $this(Bug9142\MyEnum) and Bug9142\MyEnum::Three will always evaluate to false.', + 18, + ], + [ + 'Strict comparison using === between Bug9142\MyEnum and Bug9142\MyEnum::Three will always evaluate to false.', + 31, + ], + ]); + } + + public function testBug4061(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-4061.php'], []); + } + + public function testBug9723(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9723.php'], []); + } + + public function testBug9723b(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9723b.php'], []); + } + + public function testBug8366(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8366.php'], []); + } + + public function testBug3300(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-3300.php'], []); + } + + public function testBug11035(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11035.php'], [ + [ + "Strict comparison using === between '0' and non-falsy-string will always evaluate to false.", + 39, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + public function testBug9804(): void + { + $this->analyse([__DIR__ . '/data/bug-9804.php'], []); + } + + public function testBug11161(): void + { + $this->analyse([__DIR__ . '/data/bug-11161.php'], []); + } + + public function testBug10697(): void + { + $this->analyse([__DIR__ . '/data/bug-10697.php'], []); + } + + public function testLowercaseString(): void + { + $errors = [ + [ + "Strict comparison using === between lowercase-string and 'AB' will always evaluate to false.", + 10, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using === between 'AB' and lowercase-string will always evaluate to false.", + 11, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using !== between 'AB' and lowercase-string will always evaluate to true.", + 12, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using === between lowercase-string and 'aBc' will always evaluate to false.", + 15, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using !== between lowercase-string and 'aBc' will always evaluate to true.", + 16, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]; + + if (PHP_VERSION_ID < 80000) { + $errors[] = [ + "Strict comparison using === between lowercase-string|false and 'AB' will always evaluate to false.", + 28, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ]; + } else { + $errors[] = [ + "Strict comparison using === between lowercase-string and 'AB' will always evaluate to false.", + 28, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ]; + } + + $this->analyse([__DIR__ . '/data/lowercase-string.php'], $errors); + } + + public function testUppercaseString(): void + { + $errors = [ + [ + "Strict comparison using === between uppercase-string and 'ab' will always evaluate to false.", + 10, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using === between 'ab' and uppercase-string will always evaluate to false.", + 11, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using !== between 'ab' and uppercase-string will always evaluate to true.", + 12, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using === between uppercase-string and 'aBc' will always evaluate to false.", + 15, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using !== between uppercase-string and 'aBc' will always evaluate to true.", + 16, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]; + + if (PHP_VERSION_ID < 80000) { + $errors[] = [ + "Strict comparison using === between uppercase-string|false and 'ab' will always evaluate to false.", + 28, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ]; + } else { + $errors[] = [ + "Strict comparison using === between uppercase-string and 'ab' will always evaluate to false.", + 28, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ]; + } + + $this->analyse([__DIR__ . '/data/uppercase-string.php'], $errors); + } + + public function testBug10493(): void + { + $this->analyse([__DIR__ . '/data/bug-10493.php'], []); + } + + public function testBug7173(): void + { + $this->analyse([__DIR__ . '/data/bug-7173.php'], []); + } + + public function testHashing(): void + { + $this->analyse([__DIR__ . '/data/hashing.php'], [ + [ + "Strict comparison using === between lowercase-string&non-falsy-string and 'ABC' will always evaluate to false.", + 9, + ], + [ + "Strict comparison using === between (lowercase-string&non-falsy-string)|false and 'ABC' will always evaluate to false.", + 12, + ], + [ + "Strict comparison using === between (lowercase-string&non-falsy-string)|(non-falsy-string&numeric-string) and 'A' will always evaluate to false.", + 31, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + public function testBug12772(): void + { + $this->analyse([__DIR__ . '/data/bug-12772.php'], []); + } + + public function testBug12748(): void + { + $this->analyse([__DIR__ . '/data/bug-12748.php'], []); + } + + public function testBug11019(): void + { + $this->analyse([__DIR__ . '/data/bug-11019.php'], []); + } + + public function testBug12946(): void + { + $this->analyse([__DIR__ . '/data/bug-12946.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index 1403768f98..e1e7474e17 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -26,6 +26,7 @@ protected function getRule(): Rule $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, + true, ); } @@ -92,4 +93,16 @@ public function testReportPhpDoc(): void ]); } + public function testBug7580(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7580.php'], []); + } + + public function testBug3370(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3370.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php deleted file mode 100644 index a465f71128..0000000000 --- a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php +++ /dev/null @@ -1,118 +0,0 @@ - - */ -class UnreachableIfBranchesRuleTest extends RuleTestCase -{ - - private bool $treatPhpDocTypesAsCertain; - - protected function getRule(): Rule - { - return new UnreachableIfBranchesRule( - 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->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/unreachable-if-branches.php'], [ - [ - 'Else branch is unreachable because previous condition is always true.', - 15, - ], - [ - 'Elseif branch is unreachable because previous condition is always true.', - 25, - ], - [ - 'Else branch is unreachable because previous condition is always true.', - 27, - ], - [ - 'Elseif branch is unreachable because previous condition is always true.', - 39, - ], - [ - 'Else branch is unreachable because previous condition is always true.', - 41, - ], - ]); - } - - public function testDoNotReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/unreachable-if-branches-not-phpdoc.php'], [ - [ - 'Elseif branch is unreachable because previous condition is always true.', - 18, - ], - [ - 'Else branch is unreachable because previous condition is always true.', - 28, - ], - [ - 'Elseif branch is unreachable because previous condition is always true.', - 38, - ], - ]); - } - - public function testReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = true; - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->analyse([__DIR__ . '/data/unreachable-if-branches-not-phpdoc.php'], [ - [ - 'Elseif branch is unreachable because previous condition is always true.', - 18, - ], - [ - 'Else branch is unreachable because previous condition is always true.', - 28, - ], - [ - 'Elseif branch is unreachable because previous condition is always true.', - 38, - ], - [ - 'Elseif branch is unreachable because previous condition is always true.', - 44, - $tipText, - ], - [ - 'Else branch is unreachable because previous condition is always true.', - 54, - //$tipText, - ], - [ - 'Elseif branch is unreachable because previous condition is always true.', - 64, - //$tipText, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php deleted file mode 100644 index abfca34a8f..0000000000 --- a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php +++ /dev/null @@ -1,93 +0,0 @@ - - */ -class UnreachableTernaryElseBranchRuleTest extends RuleTestCase -{ - - private bool $treatPhpDocTypesAsCertain; - - protected function getRule(): Rule - { - return new UnreachableTernaryElseBranchRule( - 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->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/unreachable-ternary-else-branch.php'], [ - [ - 'Else branch is unreachable because ternary operator condition is always true.', - 6, - ], - [ - 'Else branch is unreachable because ternary operator condition is always true.', - 9, - ], - ]); - } - - public function testDoNotReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ - [ - 'Else branch is unreachable because ternary operator condition is always true.', - 16, - ], - [ - 'Else branch is unreachable because ternary operator condition is always true.', - 17, - ], - ]); - } - - public function testReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = true; - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->analyse([__DIR__ . '/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ - [ - 'Else branch is unreachable because ternary operator condition is always true.', - 16, - ], - [ - 'Else branch is unreachable because ternary operator condition is always true.', - 17, - ], - [ - 'Else branch is unreachable because ternary operator condition is always true.', - 19, - $tipText, - ], - [ - 'Else branch is unreachable because ternary operator condition is always true.', - 20, - $tipText, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php index f75ea691dc..f0ec810999 100644 --- a/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -19,10 +18,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/void-match.php'], [ [ 'Result of match expression (void) is used.', diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php index a85dae7241..4d65f02d2b 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -26,6 +26,7 @@ protected function getRule(): Rule $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, + true, ); } diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php index 54c9c1be40..4a377f1855 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -26,6 +26,7 @@ protected function getRule(): Rule $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, + true, ); } diff --git a/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php b/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php index 4e2acbd198..1ba7c4855f 100644 --- a/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php +++ b/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php @@ -51,8 +51,8 @@ public function specifyTypes( $node->var, $newType, TypeSpecifierContext::createTruthy(), - true - ); + $scope, + )->setAlwaysOverwriteTypes(); } } diff --git a/tests/PHPStan/Rules/Comparison/data/always-true-preg-match.php b/tests/PHPStan/Rules/Comparison/data/always-true-preg-match.php new file mode 100644 index 0000000000..160f21791a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/always-true-preg-match.php @@ -0,0 +1,23 @@ +\S+::\S+)/', $test, $matches)) { + $test = $matches['name']; + } + + return $test; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/assert-unresolved-generic.php b/tests/PHPStan/Rules/Comparison/data/assert-unresolved-generic.php new file mode 100644 index 0000000000..57be8639e5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/assert-unresolved-generic.php @@ -0,0 +1,27 @@ +foo !== null and $this->bar !== null) { + + } + } + +} + +class StringInIsset +{ + + public function doFoo(string $s, string $t) + { + if (isset($s[1]) and isset($t[1])) { + + } + } + +} + +class IssetBug +{ + + public function doFoo(string $alias, array $options = []) + { + list($name, $p) = explode('.', $alias); + if (isset($options['c']) and !\strpos($options['c'], '\\')) { + // ... + } + + if (!isset($options['c']) and \strpos($p, 'X') === 0) { + // ? + } + } + +} + +class IntegerRangeType +{ + + public function doFoo(int $i, float $f) + { + if ($i < 3 and $i > 5) { // can never happen + } + + if ($f > 0 and $f < 1) { + } + } + +} + +class AndInIfCondition +{ + public function andInIfCondition($mixed, int $i): void + { + if (!$mixed) { + if ($mixed and $i) { + } + if ($i and $mixed) { + } + } + if ($mixed) { + if ($mixed and $i) { + } + if ($i and $mixed) { + } + } + } +} + +function getMaybeArray() : ?array { + if (rand(0, 1)) { return [1, 2, 3]; } + return null; +} + +function bug1924() { + $arr = [ + 'a' => getMaybeArray(), + 'b' => getMaybeArray(), + ]; + + if (isset($arr['a']) and isset($arr['b'])) { + } +} + +class Foo +{ + +} + +class Bar +{ + +} + +interface Lorem +{ + +} + +interface Ipsum +{ + +} diff --git a/tests/PHPStan/Rules/Comparison/data/boolean-logical-or.php b/tests/PHPStan/Rules/Comparison/data/boolean-logical-or.php new file mode 100644 index 0000000000..2373f02a4d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-logical-or.php @@ -0,0 +1,89 @@ += 8.1 + +namespace Bug10493; + +class Foo +{ + public function __construct( + private readonly ?string $old, + private readonly ?string $new, + ) + { + } + + public function foo(): ?string + { + $return = sprintf('%s%s', $this->old, $this->new); + + if ($return === '') { + return null; + } + + return $return; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10502.php b/tests/PHPStan/Rules/Comparison/data/bug-10502.php new file mode 100644 index 0000000000..da5e519a34 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10502.php @@ -0,0 +1,26 @@ + $x */ +function doFoo(?ArrayObject $x):void { + $callable1 = [$x, 'count']; + $callable2 = array_reverse($callable1, true); + + var_dump( + is_callable($callable1), + is_callable($callable2) + ); +} + +function doBar():void { + $callable1 = [new ArrayObject([0]), 'count']; + $callable2 = array_reverse($callable1, true); + + var_dump( + is_callable($callable1), + is_callable($callable2) + ); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10561.php b/tests/PHPStan/Rules/Comparison/data/bug-10561.php new file mode 100644 index 0000000000..71f7bf9d4c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10561.php @@ -0,0 +1,33 @@ + $inner_arr1 */ + $inner_arr1 = $arr1['inner_arr']; + /** @var array $inner_arr2 */ + $inner_arr2 = $arr2['inner_arr']; + + if (!$inner_arr1) { + return; + } + if (!$inner_arr2) { + return; + } + + $arr_intersect = array_intersect_key($inner_arr1, $inner_arr2); + if ($arr_intersect) { + echo "not empty\n"; + } else { + echo "empty\n"; + } +} + +$arr1 = ['inner_arr' => ['a' => 'b']]; +$arr2 = ['inner_arr' => ['c' => 'd']]; +func($arr1, $arr2); // Outputs "empty" diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10697.php b/tests/PHPStan/Rules/Comparison/data/bug-10697.php new file mode 100644 index 0000000000..2bc2e574e9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10697.php @@ -0,0 +1,12 @@ +reset(); + assert(static::$a === 1); + $this->reset(); + assert(static::$a === 1); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11161.php b/tests/PHPStan/Rules/Comparison/data/bug-11161.php new file mode 100644 index 0000000000..e6d7a18bed --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11161.php @@ -0,0 +1,33 @@ + $foo1 + * @param Collection $foo2 + */ + public static function compare(Collection $foo1, Collection $foo2): bool + { + return $foo1 === $foo2; + } +} + +/** + * @param Collection $collection + */ +function test(Collection $collection): bool +{ + return Comparator::compare($collection, $collection); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11246.php b/tests/PHPStan/Rules/Comparison/data/bug-11246.php new file mode 100644 index 0000000000..3c718c00ec --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11246.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug11246; + +$var = 0; +foreach ([1, 2, 3, 4, 5] as $index) { + $var++; + + match ($var % 5) { + 1 => 'c27ba0', + 2 => '5b9bd5', + 3 => 'ed7d31', + 4 => 'ffc000', + default => '674ea7', + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11313.php b/tests/PHPStan/Rules/Comparison/data/bug-11313.php new file mode 100644 index 0000000000..84375ca499 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11313.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug11313; + +enum Foo: string +{ + case CaseOne = 'one'; + case CaseTwo = 'two'; +} + +enum Bar: string +{ + case CaseThree = 'Three'; +} + +function test(Foo|Bar $union): bool +{ + return match ($union) { + Bar::CaseThree, + Foo::CaseOne => true, + Foo::CaseTwo => false, + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11674.php b/tests/PHPStan/Rules/Comparison/data/bug-11674.php new file mode 100644 index 0000000000..6156b8e1cb --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11674.php @@ -0,0 +1,40 @@ += 8.0 + +namespace Bug11674; + +class Test { + + private ?string $param; + + function show() : void { + if ((int) $this->param) { + echo 1; + } elseif ($this->param) { + echo 2; // might be "0" + } + } + + function show2() : void { + if ((float) $this->param) { + echo 1; + } elseif ($this->param) { + echo 2; // might be "0" + } + } + + function show3() : void { + if ((bool) $this->param) { + echo 1; + } elseif ($this->param) { + echo 2; // not possible + } + } + + function show4() : void { + if ((string) $this->param) { + echo 1; + } elseif ($this->param) { + echo 2; // not possible + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11694.php b/tests/PHPStan/Rules/Comparison/data/bug-11694.php new file mode 100644 index 0000000000..5c8566f788 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11694.php @@ -0,0 +1,45 @@ + + */ +function test(int $value) : int { + if ($value > 5) { + return 10; + } + + return 20; +} + +if (3 == test(3)) {} +if (test(3) == 3) {} + +if (13 == test(3)) {} +if (test(3) == 13) {} + +if (23 == test(3)) {} +if (test(3) == 23) {} + +if (null == test(3)) {} +if (test(3) == null) {} + +if ('13foo' == test(3)) {} +if (test(3) == '13foo') {} + +if (' 3' == test(3)) {} +if (test(3) == ' 3') {} + +if (' 13' == test(3)) {} +if (test(3) == ' 13') {} + +if (' 23' == test(3)) {} +if (test(3) == ' 23') {} + +if (true == test(3)) {} +if (test(3) == true) {} + +if (false == test(3)) {} +if (test(3) == false) {} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11852.php b/tests/PHPStan/Rules/Comparison/data/bug-11852.php new file mode 100644 index 0000000000..690def3bc4 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11852.php @@ -0,0 +1,13 @@ += 8.0 + +namespace Bug11852; + +function sayHello(int $type, string $activity): int +{ + return match("$type:$activity") { + '159:Work' => 12, + '159:education' => 19, + + default => throw new \InvalidArgumentException("unknown values $type:$activity"), + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12473.php b/tests/PHPStan/Rules/Comparison/data/bug-12473.php new file mode 100644 index 0000000000..250b7c83a7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12473.php @@ -0,0 +1,73 @@ + $fqn */ + $fqn = $pictureType; + if ($fqn === Picture::class) { + return Picture::class; + } + $refl = new \ReflectionClass($fqn); + if (!$refl->isSubclassOf(Picture::class)) { + return null; + } + + return $fqn; +} + +/** + * @param class-string $a + */ +function doFoo(string $a): void { + $r = new ReflectionClass($a); + if ($r->isSubclassOf(Picture::class)) { + + } +} + +/** + * @param class-string $a + */ +function doFoo2(string $a): void { + $r = new ReflectionClass($a); + if ($r->isSubclassOf(PictureProduct::class)) { + + } +} + +/** + * @param class-string $a + */ +function doFoo3(string $a): void { + $r = new ReflectionClass($a); + if ($r->isSubclassOf(PictureUser::class)) { + + } +} + +/** + * @param ReflectionClass $a + * @param class-string $b + * @return void + */ +function doFoo4(ReflectionClass $a, string $b): void { + if ($a->isSubclassOf($b)) { + + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12716.php b/tests/PHPStan/Rules/Comparison/data/bug-12716.php new file mode 100644 index 0000000000..a4429d9d43 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12716.php @@ -0,0 +1,19 @@ += 10) { + var_dump(count($items)); + $items = []; + } + }; + $i = 0; + while ($i++ <= 100) { + $a(); + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12748.php b/tests/PHPStan/Rules/Comparison/data/bug-12748.php new file mode 100644 index 0000000000..bcf15355af --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12748.php @@ -0,0 +1,54 @@ += 8.0 + +namespace Bug12748; + +use SessionHandlerInterface; + +class HelloWorld +{ + public function getHandler(): SessionHandlerInterface + { + return new SessHandler; + } +} + +class SessHandler implements SessionHandlerInterface +{ + + public function close(): bool + { + return true; + } + + public function destroy(string $id): bool + { + return true; + } + + public function gc(int $max_lifetime): int|false + { + return false; + } + + public function open(string $path, string $name): bool + { + return true; + } + + public function read(string $id): string|false + { + return false; + } + + public function write(string $id, string $data): bool + { + return true; + } +} + +$sessionHandler = (new HelloWorld)->getHandler(); +$session = $sessionHandler->read('123'); + +if ($session === false) { + return null; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12772.php b/tests/PHPStan/Rules/Comparison/data/bug-12772.php new file mode 100755 index 0000000000..3a01127243 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12772.php @@ -0,0 +1,17 @@ += 8.1 + +namespace Bug12946; + +interface UserInterface {} +class User implements UserInterface{} + +class UserMapper { + function getFromId(int $id) : ?UserInterface { + return $id === 10 ? new User : null; + } +} + +class GetUserCommand { + + private ?UserInterface $currentUser = null; + + public function __construct( + private readonly UserMapper $userMapper, + private readonly int $id, + ) { + } + + public function __invoke() : UserInterface { + if( $this->currentUser ) { + return $this->currentUser; + } + + $this->currentUser = $this->userMapper->getFromId($this->id); + if( $this->currentUser === null ) { + throw new \Exception; + } + + return $this->currentUser; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-2499.php b/tests/PHPStan/Rules/Comparison/data/bug-2499.php new file mode 100644 index 0000000000..3581c62254 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-2499.php @@ -0,0 +1,11 @@ + 4 ? "test" : null; + } + + function test(): string { + $foo = $this->maybeString(); + ($foo !== null) || ($foo = ""); + return $foo; + } + + function test2(): void + { + $foo = $this->maybeString(); + if (($foo !== null) || ($foo = "")) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-2741.php b/tests/PHPStan/Rules/Comparison/data/bug-2741.php new file mode 100644 index 0000000000..9ef7c92e86 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-2741.php @@ -0,0 +1,26 @@ + 4 ? "test" : null; + } + + function test(): string { + $foo = $this->maybeString(); + ($foo === null) && ($foo = ""); + return $foo; + } + + function test2(): void + { + $foo = $this->maybeString(); + if (($foo === null) && ($foo = "")) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-2755.php b/tests/PHPStan/Rules/Comparison/data/bug-2755.php new file mode 100644 index 0000000000..a5cb1fc83e --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-2755.php @@ -0,0 +1,18 @@ +> $interfaces + * @param array> $classes + */ +function foo(array $interfaces, array $classes): void +{ + foreach ($interfaces as $interface) { + foreach ($classes as $class) { + if (is_subclass_of($class, $interface)) { + + } + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-2851.php b/tests/PHPStan/Rules/Comparison/data/bug-2851.php new file mode 100644 index 0000000000..09f56433e3 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-2851.php @@ -0,0 +1,17 @@ + 0) { + $words .= array_pop($arguments); + if (count($arguments) > 0) { + $words .= ' '; + } + } + + echo $words; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-2851b.php b/tests/PHPStan/Rules/Comparison/data/bug-2851b.php new file mode 100644 index 0000000000..697c8266f2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-2851b.php @@ -0,0 +1,23 @@ + 'A', + 'b' => 'B', + 'c' => 'C', + ]; + + public function get($value) + { + return array_keys(self::MAP, $value) ?: [$value]; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3633.php b/tests/PHPStan/Rules/Comparison/data/bug-3633.php new file mode 100644 index 0000000000..95aedbcf12 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3633.php @@ -0,0 +1,128 @@ +test(); + } +} + +class OtherClass { + use Foo; + + public function bar($obj): void { + if (get_class($this) === HelloWorld::class) { + echo "OK"; + } + if (get_class($this) === OtherClass::class) { + echo "OK"; + } + + if (get_class() === HelloWorld::class) { + echo "OK"; + } + if (get_class() === OtherClass::class) { + echo "OK"; + } + + if (get_class($obj) === HelloWorld::class) { + echo "OK"; + } + if (get_class($obj) === OtherClass::class) { + echo "OK"; + } + + $this->test(); + } +} + +final class FinalClass { + use Foo; + + public function bar($obj): void { + if (get_class($this) === HelloWorld::class) { + echo "OK"; + } + if (get_class($this) === OtherClass::class) { + echo "OK"; + } + if (get_class($this) !== FinalClass::class) { + echo "OK"; + } + if (get_class($this) === FinalClass::class) { + echo "OK"; + } + + if (get_class() === HelloWorld::class) { + echo "OK"; + } + if (get_class() === OtherClass::class) { + echo "OK"; + } + if (get_class() !== FinalClass::class) { + echo "OK"; + } + if (get_class() === FinalClass::class) { + echo "OK"; + } + + if (get_class($obj) === HelloWorld::class) { + echo "OK"; + } + if (get_class($obj) === OtherClass::class) { + echo "OK"; + } + + $this->test(); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3821.php b/tests/PHPStan/Rules/Comparison/data/bug-3821.php new file mode 100644 index 0000000000..3c0f482ecc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3821.php @@ -0,0 +1,13 @@ += 8.1 + +namespace Bug4242; + +class Enum +{ + public const TYPE_A = 1; + public const TYPE_B = 2; + public const TYPE_C = 3; + public const TYPE_D = 4; +} + +class Data +{ + private int $type; + private int $someLoad; + public function __construct(int $type) + { + $this->type=$type; + } + public function getType(): int + { + return $this->type; + } + public function someLoad(int $type): self + { + $this->someLoad=$type; + return $this; + } + public function getSomeLoad(): int + { + return $this->someLoad; + } +} + +class HelloWorld +{ + public function case1(): void + { + $data=(new Data(Enum::TYPE_A)); + if($data->getType()===Enum::TYPE_A){ + $data->someLoad(4); + }elseif(\in_array($data->getType(), [7,8,9,100], true)){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_B){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_C){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_D){ + $data->someLoad(6); + }else{ + return; + } + + + if($data->getType()===Enum::TYPE_A){ + $data->someLoad(4); + }elseif(\in_array($data->getType(), [7,8,9,100], true)){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_B){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_C){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_D){ // expected to work without an error + $data->someLoad(6); + } + + } + + public function case2(): void + { + $data=(new Data(Enum::TYPE_A)); + if($data->getType()===Enum::TYPE_A){ + $data->someLoad(4); + }elseif(\in_array($data->getType(), [7,8,9,100], true)){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_B){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_C){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_D){ + $data->someLoad(6); + }else{ + return; + } + + // code above is the same as in case1. code bellow with sorted elseif's + if($data->getType()===Enum::TYPE_A){ + $data->someLoad(4); + }elseif($data->getType()===Enum::TYPE_B){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_C){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_D){ + $data->someLoad(6); + }elseif(\in_array($data->getType(), [7,8,9,100], true)){ + $data->someLoad(6); + } + + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4302.php b/tests/PHPStan/Rules/Comparison/data/bug-4302.php new file mode 100644 index 0000000000..825bec72b4 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4302.php @@ -0,0 +1,14 @@ += 8.1 + +namespace Bug4451; + +class HelloWorld +{ + public function sayHello(): int + { + $verified = fn(): bool => rand() === 1; + + return match([$verified(), $verified()]) { + [true, true] => 1, + [true, false] => 2, + [false, true] => 3, + [false, false] => 4, + }; + + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4708.php b/tests/PHPStan/Rules/Comparison/data/bug-4708.php new file mode 100644 index 0000000000..2afa164340 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4708.php @@ -0,0 +1,94 @@ + FALSE, + 'dberror' => 'xyz']; + } + else + { + assertType('array|true', $result); + if (!isset($result['bsw'])) + { + assertType('array|true', $result); + $result['bsw'] = 1; + assertType("non-empty-array<1|string>&hasOffsetValue('bsw', 1)", $result); + } + else + { + assertType('non-empty-array&hasOffsetValue(\'bsw\', string)', $result); + $result['bsw'] = (int) $result['bsw']; + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + } + + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + + if (!isset($result['bew'])) + { + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + $result['bew'] = 5; + assertType("non-empty-array&hasOffsetValue('bew', 5)&hasOffsetValue('bsw', int)", $result); + } + else + { + assertType("non-empty-array&hasOffsetValue('bew', int|string)&hasOffsetValue('bsw', int)", $result); + $result['bew'] = (int) $result['bew']; + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + } + + assertType("non-empty-array&hasOffsetValue('bew', int)&hasOffsetValue('bsw', int)", $result); + + foreach (['utc', 'ssi'] as $field) + { + if (array_key_exists($field, $result)) + { + $result[$field] = (int) $result[$field]; + } + } + } + + assertType("non-empty-array", $result); + + return $result; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4864.php b/tests/PHPStan/Rules/Comparison/data/bug-4864.php new file mode 100644 index 0000000000..288e19c21f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4864.php @@ -0,0 +1,25 @@ +isHandled = false; + $this->value = null; + + (function () { + $this->isHandled = true; + $this->value = 'value'; + })(); + + if ($this->isHandled) { + $f($this->value); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4912.php b/tests/PHPStan/Rules/Comparison/data/bug-4912.php new file mode 100644 index 0000000000..cdbb585f9a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4912.php @@ -0,0 +1,27 @@ + $requiredRatio) { + + } elseif ($srcRatio < $requiredRatio) { + + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5317.php b/tests/PHPStan/Rules/Comparison/data/bug-5317.php new file mode 100644 index 0000000000..3fb9272498 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5317.php @@ -0,0 +1,19 @@ +\d+)$#i'; + $subject = 'C 1234567890'; + + $found = (bool)preg_match( $pattern, $subject, $matches ) && isset( $matches['productId'] ); + assertType('bool', $found); +}; + +function (): void { + $matches = []; + $pattern = '#^C\s+(?\d+)$#i'; + $subject = 'C 1234567890'; + + assertType('bool', preg_match( $pattern, $subject, $matches ) ? isset( $matches['productId'] ) : false); +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5370.php b/tests/PHPStan/Rules/Comparison/data/bug-5370.php new file mode 100644 index 0000000000..a3cf0f87d5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5370.php @@ -0,0 +1,18 @@ + 5]; +} + +function (): void { + $data = ['test' => 1]; + $data2 = ['test' => 1]; + $data3 = ['test' => 5]; + + if ($data !== $data2) { + testData($data); + } + + if ($data !== $data3) { + testData($data); + } + + if ($data !== returnData3()) { + testData($data); + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5695.php b/tests/PHPStan/Rules/Comparison/data/bug-5695.php new file mode 100644 index 0000000000..17d33153a5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5695.php @@ -0,0 +1,21 @@ += 7.4 + 0) { + array_shift($data); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6064.php b/tests/PHPStan/Rules/Comparison/data/bug-6064.php new file mode 100644 index 0000000000..564c0fc005 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6064.php @@ -0,0 +1,19 @@ += 8.1 + +namespace Bug6064; + +function (): void { + $result = match( rand() <=> rand() ) { + -1 => 'down', + 0 => 'same', + 1 => 'up' + }; +}; + +function (): void { + $result = match(rand(1, 3)) { + 1 => 'foo', + 2 => 'bar', + 3 => 'baz' + }; +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6115.php b/tests/PHPStan/Rules/Comparison/data/bug-6115.php new file mode 100644 index 0000000000..6611eefda2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6115.php @@ -0,0 +1,64 @@ += 8.0 + +namespace Bug6115; + +class Foo +{ + public function bar() + { + $array = [1, 2, 3]; + try { + foreach ($array as $value) { + $b = match ($value) { + 1 => 0, + 2 => 1, + }; + } + } catch (\UnhandledMatchError $e) { + } + + try { + foreach ($array as $value) { + $b = match ($value) { + 1 => 0, + 2 => 1, + }; + } + } catch (\Error $e) { + } + + try { + foreach ($array as $value) { + $b = match ($value) { + 1 => 0, + 2 => 1, + }; + } + } catch (\Exception $e) { + } + + try { + foreach ($array as $value) { + $b = match ($value) { + 1 => 0, + 2 => 1, + }; + } + } catch (\UnhandledMatchError|\Exception $e) { + } + + try { + try { + foreach ($array as $value) { + $b = match ($value) { + 1 => 0, + 2 => 1, + }; + } + } catch (\Exception $e) { + } + } catch (\UnhandledMatchError $e) { + } + } +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6181.php b/tests/PHPStan/Rules/Comparison/data/bug-6181.php new file mode 100644 index 0000000000..727a094921 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6181.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug6260; + +class Foo{ + public function __construct( + /** @var non-empty-array */ + private array $array + ){ + if(count($array) === 0){ + throw new \InvalidArgumentException(); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6407.php b/tests/PHPStan/Rules/Comparison/data/bug-6407.php new file mode 100644 index 0000000000..99f4ead9b3 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6407.php @@ -0,0 +1,65 @@ += 8.0 + +namespace Bug6407; + +class BookEditPacket +{ + public const TYPE_REPLACE_PAGE = 0; + public const TYPE_ADD_PAGE = 1; + public const TYPE_DELETE_PAGE = 2; + public const TYPE_SWAP_PAGES = 3; + public const TYPE_SIGN_BOOK = 4; + + public int $type; +} + + +class PlayerEditBookEvent +{ + public const ACTION_REPLACE_PAGE = 0; + public const ACTION_ADD_PAGE = 1; + public const ACTION_DELETE_PAGE = 2; + public const ACTION_SWAP_PAGES = 3; + public const ACTION_SIGN_BOOK = 4; +} + +class HelloWorld +{ + private BookEditPacket $packet; + + private function iAmImpure(): void + { + $this->packet->type = 999; + } + + public function sayHello(BookEditPacket $packet): bool + { + $this->packet = $packet; + switch ($packet->type) { + case BookEditPacket::TYPE_REPLACE_PAGE: + $this->iAmImpure(); + break; + case BookEditPacket::TYPE_ADD_PAGE: + break; + case BookEditPacket::TYPE_DELETE_PAGE: + break; + case BookEditPacket::TYPE_SWAP_PAGES: + break; + case BookEditPacket::TYPE_SIGN_BOOK: + break; + default: + return false; + } + + //for redundancy, in case of protocol changes, we don't want to pass these directly + $action = match ($packet->type) { + BookEditPacket::TYPE_REPLACE_PAGE => PlayerEditBookEvent::ACTION_REPLACE_PAGE, + BookEditPacket::TYPE_ADD_PAGE => PlayerEditBookEvent::ACTION_ADD_PAGE, + BookEditPacket::TYPE_DELETE_PAGE => PlayerEditBookEvent::ACTION_DELETE_PAGE, + BookEditPacket::TYPE_SWAP_PAGES => PlayerEditBookEvent::ACTION_SWAP_PAGES, + BookEditPacket::TYPE_SIGN_BOOK => PlayerEditBookEvent::ACTION_SIGN_BOOK, + default => throw new \Error("We already filtered unknown types in the switch above") + }; + return true; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6443.php b/tests/PHPStan/Rules/Comparison/data/bug-6443.php new file mode 100644 index 0000000000..e9e4383463 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6443.php @@ -0,0 +1,23 @@ + $null) { + $success = $values[$index] < $expected; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6473.php b/tests/PHPStan/Rules/Comparison/data/bug-6473.php index 9482373e9d..fba7f6a8be 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-6473.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-6473.php @@ -1,4 +1,4 @@ -= 7.4 + 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + $match = []; + if (false === preg_match('/^c(\d+)$/', $key, $match) || empty($match)) { + continue; + } + var_dump($key); + var_dump($value); + } +}; + +function (): void { + $data = [ + 'c1' => 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + if (false === preg_match('/^c(\d+)$/', $key, $match) || empty($match)) { + continue; + } + var_dump($key); + var_dump($value); + } +}; + +function (): void { + $data = [ + 'c1' => 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + $match = []; + assertType('true', preg_match('/^c(\d+)$/', $key, $match) || empty($match)); + } +}; + +function (): void { + $data = [ + 'c1' => 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + assertType('true', preg_match('/^c(\d+)$/', $key, $match) || empty($match)); + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6599.php b/tests/PHPStan/Rules/Comparison/data/bug-6599.php new file mode 100644 index 0000000000..56d2d24549 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6599.php @@ -0,0 +1,24 @@ += 8.1 + +namespace Bug6647; + +class HelloWorld +{ + public ?int $test1; + public ?int $test2; + + public function getStatusAttribute(): string + { + $compare_against = [ + 't1' => !is_null($this->test1), + 't2' => !is_null($this->test2), + ]; + + $map = fn(bool $t1, bool $t2) => [ + 't1' => $t1, + 't2' => $t2, + ]; + + return match($compare_against) { + $map(true, false) => 'abc', + $map(false, true) => 'def', + + default => + throw new RuntimeException("Unknown status: " . json_encode($compare_against)), + }; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6697.php b/tests/PHPStan/Rules/Comparison/data/bug-6697.php new file mode 100644 index 0000000000..9aa89afafa --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6697.php @@ -0,0 +1,5 @@ + 1, 'b' => 2]; + /** @var array * */ + $array2 = ['a' => 1]; + + $check = function (string $key) use (&$array1, &$array2): bool { + if (!isset($array1[$key], $array2[$key])) { + return false; + } + // ... more conditions here ... + return true; + }; + + if ($check('a')) { + // ... + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6938.php b/tests/PHPStan/Rules/Comparison/data/bug-6938.php new file mode 100644 index 0000000000..1f14920582 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6938.php @@ -0,0 +1,22 @@ + $value + */ +function myFunction($value): void +{ + if (is_string($value)) { + $value = [$value]; + } elseif (is_array($value)) { + // If given an array, filter out anything that isn't a string. + $value = array_filter($value, 'is_string'); + } + + if (! is_array($value)) { + throw new \DomainException('Invalid argument type for $value'); + } + + // Now we know that $value is either a string or an array of strings. +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6939.php b/tests/PHPStan/Rules/Comparison/data/bug-6939.php new file mode 100644 index 0000000000..14d1e9408a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6939.php @@ -0,0 +1,12 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug6947; + +abstract class HelloWorld +{ + public function sayHello(): void + { + if (is_string($this->getValue())) { + + } elseif (is_array($this->getValue())) { + + } + } + + abstract public function getValue():int|float|string|null; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7044.php b/tests/PHPStan/Rules/Comparison/data/bug-7044.php new file mode 100644 index 0000000000..b5efe8c6cb --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7044.php @@ -0,0 +1,21 @@ += 8.1 + +namespace Bug7052; + +enum Foo: int +{ + case A = 1; + case B = 2; +} + +class Bar +{ + + public function doFoo() + { + Foo::A > Foo::B; + Foo::A < Foo::B; + Foo::A >= Foo::B; + Foo::A <= Foo::B; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7075.php b/tests/PHPStan/Rules/Comparison/data/bug-7075.php new file mode 100644 index 0000000000..b4311d4303 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7075.php @@ -0,0 +1,18 @@ + $b */ +function foo(int $b): void { + if ($b > 100) throw new \Exception("bad"); + print "ok"; +} + +/** + * @param int<1,max> $number + */ +function foo2(int $number): void { + if ($number < 1) { + throw new \Exception('Number cannot be less than 1'); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7079.php b/tests/PHPStan/Rules/Comparison/data/bug-7079.php new file mode 100644 index 0000000000..018e084161 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7079.php @@ -0,0 +1,15 @@ + $interfaces + * @param class-string $classes + */ +function foo(string $interfaces, string $classes): bool +{ + return is_subclass_of($interfaces, $classes); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7095.php b/tests/PHPStan/Rules/Comparison/data/bug-7095.php new file mode 100644 index 0000000000..adb974800d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7095.php @@ -0,0 +1,8 @@ += 8.0 + +namespace Bug7095; + +match (isset($foo)) { + true => 'a', + false => 'b', +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7166.php b/tests/PHPStan/Rules/Comparison/data/bug-7166.php new file mode 100644 index 0000000000..707eba5423 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7166.php @@ -0,0 +1,16 @@ + 0, + 'item1' => 0, + ]; + + call_user_func(function () use (&$a1) { + $a1['item2'] = 3; + $a1['item1'] = 1; + }); + + if (['item2' => 3, 'item1' => 1] === $a1) { + throw new \Exception(); + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7176.php b/tests/PHPStan/Rules/Comparison/data/bug-7176.php new file mode 100644 index 0000000000..da799fca57 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7176.php @@ -0,0 +1,28 @@ += 8.1 + +namespace Bug7176; + +enum Suit +{ + case Hearts; + case Diamonds; + case Clubs; + case Spades; +} + +function test(Suit $x): string { + if ($x === Suit::Clubs) { + return 'WORKS'; + } + // Suit::Clubs is correctly eliminated from possible values + + if (in_array($x, [Suit::Spades], true)) { + return 'DOES NOT WORK'; + } + // Suit::Spades is not eliminated from possible values + + return match ($x) { // no error is expected here + Suit::Hearts => 'a', + Suit::Diamonds => 'b', + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7257.php b/tests/PHPStan/Rules/Comparison/data/bug-7257.php new file mode 100644 index 0000000000..f9eb31c19b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7257.php @@ -0,0 +1,31 @@ +current = false; + } + + public function getCurrent(): bool + { + return $this->current; + } + +} + +function (): void { + $a = (bool) rand(0, 1); + $obj = new Foo(); + $a && $obj->setCurrent(); + + var_dump($obj->getCurrent()); +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7491.php b/tests/PHPStan/Rules/Comparison/data/bug-7491.php new file mode 100644 index 0000000000..8990e0f0ff --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7491.php @@ -0,0 +1,14 @@ + $array + */ + public function foo(array $array): void + { + if ([] === $array) { + throw new \InvalidArgumentException(); + } + } + + /** + * @param non-empty-array $array + */ + public function foo2(array $array): void + { + if (0 === count($array)) { + throw new \InvalidArgumentException(); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7580.php b/tests/PHPStan/Rules/Comparison/data/bug-7580.php new file mode 100644 index 0000000000..294ed21160 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7580.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug7622; + +final class AnalyticsKpiType +{ + public const SESSION_COUNT = 'session_count'; + public const MISSION_COUNT = 'mission_count'; + public const SESSION_GAP = 'session_gap'; +} + +class HelloWorld +{ + + /** + * @param AnalyticsKpiType::* $currentKpi + * @param int[] $filteredMemberIds + */ + public function test(string $currentKpi, array $filteredMemberIds): int + { + return match ($currentKpi) { + AnalyticsKpiType::SESSION_COUNT => 12, + AnalyticsKpiType::MISSION_COUNT => 5, + AnalyticsKpiType::SESSION_GAP => 14, + }; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7684.php b/tests/PHPStan/Rules/Comparison/data/bug-7684.php new file mode 100644 index 0000000000..ccf58f5c15 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7684.php @@ -0,0 +1,18 @@ += 8.1 + +namespace Bug7698Match; + +final class A +{ +} + +final class B +{ +} + +final class C +{ +} + +final class Test +{ + public function __construct(public readonly A|B $value) + { + } +} + +function matchIt() +{ + $t = new Test(new A()); + $class = $t->value::class; + echo match ($class) { + A::class => 'A', + B::class => 'B' + }; +} + +function matchGetClassString() +{ + $t = new Test(new A()); + echo match (get_class($t->value)) { + A::class => 'A', + B::class => 'B' + }; +} + +function test(A|B|C $abc): string +{ + $class = $abc::class; + return match ($class) { + A::class => 'A', + B::class => 'B', + C::class => 'C', + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7746.php b/tests/PHPStan/Rules/Comparison/data/bug-7746.php new file mode 100644 index 0000000000..db11456f10 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7746.php @@ -0,0 +1,28 @@ += 8.1 + +namespace Bug7746Match; + +final class A +{ +} + +final class B +{ +} + +final class Test +{ + public function __construct(public readonly A|B $value) + { + } +} + +function matchIt():void +{ + $t = new Test(new A()); + echo match ($t->value::class) { + A::class => 'A', + B::class => 'B' + }; +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7881.php b/tests/PHPStan/Rules/Comparison/data/bug-7881.php new file mode 100644 index 0000000000..36170ce7e5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7881.php @@ -0,0 +1,20 @@ + $base + * + * @return array + */ +function base_str_to_arr(string $str, &$base): array +{ + $arr = []; + + while ('0' === $str || strlen($str) !== 0) { + echo 'toto'; + } + + return $arr; +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7898.php b/tests/PHPStan/Rules/Comparison/data/bug-7898.php new file mode 100644 index 0000000000..16e4b813ce --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7898.php @@ -0,0 +1,182 @@ += 8.0 + +namespace Bug7898; + +use function PHPStan\Testing\assertType; + +class FooEnum +{ + public const FOO_TYPE = 'foo'; + public const APPLICABLE_TAX_AND_FEES_BY_TYPE = [ + 'US' => [ + 'bar' => [ + 'sales_tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'city_tax' => [ + 'type' => 'both', + 'unit' => 'per-room-per-night', + ], + 'resort_fee' => [ + 'type' => 'both', + 'unit' => 'per-room-per-night', + ], + 'additional_tax_or_fee' => [ + 'type' => 'both', + 'unit' => 'per-room-per-night', + ], + ], + 'foo' => [ + 'tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + ], + ], + 'CA' => [ + 'bar' => [ + 'goods_and_services_tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'provincial_sales_tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'harmonized_sales_tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'municipal_and_regional_district_tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'additional_tax_or_fee' => [ + 'type' => 'both', + 'unit' => 'per-room-per-night', + ], + ], + ], + 'SG' => [ + 'bar' => [ + 'service_charge' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + ], + ], + 'TH' => [ + 'bar' => [ + 'service_charge' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'city_tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + ], + ], + 'AE' => [ + 'bar' => [ + 'vat' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'service_charge' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'municipality_fee' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'tourism_fee' => [ + 'type' => 'both', + 'unit' => 'per-room-per-night', + ], + 'destination_fee' => [ + 'type' => 'both', + 'unit' => 'per-room-per-night', + ], + ], + ], + 'BH' => [ + 'bar' => [ + 'vat' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'service_charge' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'city_tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + ], + ], + 'HK' => [ + 'bar' => [ + 'service_charge' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + 'tax' => [ + 'type' => 'rate', + 'unit' => 'per-room-per-night', + ], + ], + ], + 'ES' => [ + 'bar' => [ + 'city_tax' => [ + 'type' => 'both', + 'unit' => 'per-room-per-night', + ], + ], + ], + ]; +} + +class Country +{ + public function __construct(private string $code) + { + } + + public function getCode(): string + { + return $this->code; + } +} + +class Foo +{ + public function __construct(private Country $country) + { + } + + public function getCountryCode(): string + { + return $this->country->getCode(); + } + + public function getHasDaycationTaxesAndFees(): bool + { + assertType("array{US: array{bar: array{sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'both', unit: 'per-room-per-night'}, resort_fee: array{type: 'both', unit: 'per-room-per-night'}, additional_tax_or_fee: array{type: 'both', unit: 'per-room-per-night'}}, foo: array{tax: array{type: 'rate', unit: 'per-room-per-night'}}}, CA: array{bar: array{goods_and_services_tax: array{type: 'rate', unit: 'per-room-per-night'}, provincial_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, harmonized_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, municipal_and_regional_district_tax: array{type: 'rate', unit: 'per-room-per-night'}, additional_tax_or_fee: array{type: 'both', unit: 'per-room-per-night'}}}, SG: array{bar: array{service_charge: array{type: 'rate', unit: 'per-room-per-night'}, tax: array{type: 'rate', unit: 'per-room-per-night'}}}, TH: array{bar: array{service_charge: array{type: 'rate', unit: 'per-room-per-night'}, tax: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'rate', unit: 'per-room-per-night'}}}, AE: array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, municipality_fee: array{type: 'rate', unit: 'per-room-per-night'}, tourism_fee: array{type: 'both', unit: 'per-room-per-night'}, destination_fee: array{type: 'both', unit: 'per-room-per-night'}}}, BH: array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'rate', unit: 'per-room-per-night'}}}, HK: array{bar: array{service_charge: array{type: 'rate', unit: 'per-room-per-night'}, tax: array{type: 'rate', unit: 'per-room-per-night'}}}, ES: array{bar: array{city_tax: array{type: 'both', unit: 'per-room-per-night'}}}}", FooEnum::APPLICABLE_TAX_AND_FEES_BY_TYPE); + assertType("array{bar: array{city_tax: array{type: 'both', unit: 'per-room-per-night'}}|array{sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'both', unit: 'per-room-per-night'}, resort_fee: array{type: 'both', unit: 'per-room-per-night'}, additional_tax_or_fee: array{type: 'both', unit: 'per-room-per-night'}}, foo?: array{tax: array{type: 'rate', unit: 'per-room-per-night'}}}|array{bar: array{goods_and_services_tax: array{type: 'rate', unit: 'per-room-per-night'}, provincial_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, harmonized_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, municipal_and_regional_district_tax: array{type: 'rate', unit: 'per-room-per-night'}, additional_tax_or_fee: array{type: 'both', unit: 'per-room-per-night'}}}|array{bar: array{service_charge: array{type: 'rate', unit: 'per-room-per-night'}, tax: array{type: 'rate', unit: 'per-room-per-night'}, city_tax?: array{type: 'rate', unit: 'per-room-per-night'}}}|array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'rate', unit: 'per-room-per-night'}}}|array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, municipality_fee: array{type: 'rate', unit: 'per-room-per-night'}, tourism_fee: array{type: 'both', unit: 'per-room-per-night'}, destination_fee: array{type: 'both', unit: 'per-room-per-night'}}}", FooEnum::APPLICABLE_TAX_AND_FEES_BY_TYPE[$this->getCountryCode()]); + return array_key_exists(FooEnum::FOO_TYPE, FooEnum::APPLICABLE_TAX_AND_FEES_BY_TYPE[$this->getCountryCode()]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7914.php b/tests/PHPStan/Rules/Comparison/data/bug-7914.php new file mode 100644 index 0000000000..116897491f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7914.php @@ -0,0 +1,14 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug8169; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + /** @phpstan-assert string $var */ + public function assertString(mixed $var): void + { + } + + public function test(mixed $foo): void + { + assertType('mixed', $foo); + $this->assertString($foo); + assertType('string', $foo); + $this->assertString($foo); // should report as always evaluating to true? + assertType('string', $foo); + } + + public function test2(string $foo): void + { + assertType('string', $foo); + $this->assertString($foo); // should report as always evaluating to true? + assertType('string', $foo); + } + + public function test3(int $foo): void + { + assertType('int', $foo); + $this->assertString($foo); // should report as always evaluating to false? + assertType('*NEVER*', $foo); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8240.php b/tests/PHPStan/Rules/Comparison/data/bug-8240.php new file mode 100644 index 0000000000..d54f6244b6 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8240.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug8240; + +enum Foo +{ + case BAR; +} + +function doFoo(Foo $foo): int +{ + return match ($foo) { + Foo::BAR => 5, + default => throw new \Exception('This will not be executed') + }; +} + +enum Foo2 +{ + case BAR; + case BAZ; +} + +function doFoo2(Foo2 $foo): int +{ + return match ($foo) { + Foo2::BAR => 5, + Foo2::BAZ => 15, + default => throw new \Exception('This will not be executed') + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8277.php b/tests/PHPStan/Rules/Comparison/data/bug-8277.php new file mode 100644 index 0000000000..3be373d818 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8277.php @@ -0,0 +1,36 @@ + $stream + * @param positive-int $width + * + * @return Generator + */ +function swindow(iterable $stream, int $width): Generator +{ + $window = []; + foreach ($stream as $value) { + $window[] = $value; + $count = count($window); + + assertType('int<1, max>', $count); + + switch (true) { + case $count > $width: + array_shift($window); + // no break + case $count === $width: + yield $window; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8464.php b/tests/PHPStan/Rules/Comparison/data/bug-8464.php new file mode 100644 index 0000000000..23cd280d7a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8464.php @@ -0,0 +1,18 @@ += 8.0 + +namespace Bug8464; + +final class ObjectUtil +{ + /** + * @param class-string $type + */ + public static function instanceOf(mixed $object, string $type): bool + { + return \is_object($object) + && ( + $object::class === $type || + is_subclass_of($object, $type) + ); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8474.php b/tests/PHPStan/Rules/Comparison/data/bug-8474.php new file mode 100644 index 0000000000..5412e7a695 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8474.php @@ -0,0 +1,32 @@ +data = 'Hello'; + } + } +} + +class Beta extends Alpha +{ + /** @var string|null */ + public $data = null; +} + +class Delta extends Alpha +{ +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8485.php b/tests/PHPStan/Rules/Comparison/data/bug-8485.php new file mode 100644 index 0000000000..ce7cc5fa3c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8485.php @@ -0,0 +1,78 @@ += 8.1 + +namespace Bug8485; + +use function PHPStan\Testing\assertType; + +enum E { + case c; +} + +enum F { + case c; +} + +function shouldError():void { + $e = E::c; + $f = F::c; + + if ($e === E::c) { + } + if ($e == E::c) { + } + + if ($f === $e) { + } + if ($f == $e) { + } + + if ($f === E::c) { + } + if ($f == E::c) { + } +} + +function allGood(E $e, F $f):void { + if ($f === $e) { + } + if ($f == $e) { + } + + if ($f === E::c) { + } + if ($f == E::c) { + } +} + +enum FooEnum +{ + case A; + case B; + case C; +} +function dooFoo(FooEnum $s):void { + if ($s === FooEnum::A) { + } elseif ($s === FooEnum::B) { + } elseif ($s === FooEnum::C) { + } + + if ($s === FooEnum::A) { + } elseif ($s === FooEnum::B) { + } else { + assertType('Bug8485\FooEnum::C', $s); + } + + if ($s === FooEnum::A) { + } elseif ($s === FooEnum::B) { + } elseif ($s === FooEnum::C) { + } else { + assertType('*NEVER*', $s); + } + + if ($s === FooEnum::A) { + } elseif ($s === FooEnum::B) { + } elseif ($s === FooEnum::C) { + } elseif (rand(0, 1)) { + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8516.php b/tests/PHPStan/Rules/Comparison/data/bug-8516.php new file mode 100644 index 0000000000..b0d96a49b1 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8516.php @@ -0,0 +1,18 @@ + ['min_range' => 0]]; + if (filter_var($value, FILTER_VALIDATE_INT, $options) === false) { + return false; + } + // ... + } + if (is_string($value)) { + // ... + } + return true; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8536.php b/tests/PHPStan/Rules/Comparison/data/bug-8536.php new file mode 100644 index 0000000000..9ac6e588d9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8536.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug8536; + +final class A { + public function __construct(public readonly string $id) {} +} +final class B { + public function __construct(public readonly string $name) {} +} + +class Foo +{ + + public function getValue(A|B $obj): string + { + return match(get_class($obj)) { + A::class => $obj->id, + B::class => $obj->name, + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8555.php b/tests/PHPStan/Rules/Comparison/data/bug-8555.php new file mode 100644 index 0000000000..d249968e55 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8555.php @@ -0,0 +1,14 @@ + + */ +function test(int $first, int $second): array +{ + return [ + 'test' => $first && $second ? $first : null, + 'test2' => $first && $second ? $first : null, + ]; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8562.php b/tests/PHPStan/Rules/Comparison/data/bug-8562.php new file mode 100644 index 0000000000..9deeaa6a0d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8562.php @@ -0,0 +1,18 @@ + $a + */ +function a(array $a): void { + $l = (string) array_key_last($a); + $s = substr($l, 0, 2); + if ($s === '') { + ; + } else { + var_dump($s); + } +} + + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8586.php b/tests/PHPStan/Rules/Comparison/data/bug-8586.php new file mode 100644 index 0000000000..2b004a9f56 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8586.php @@ -0,0 +1,38 @@ +getString() === null); + $em->refreshFromAnnotation($foo); + \assert($foo->getString() !== null); + } + + public function sayHello2(Foo $foo, EntityManager $em): void + { + \assert($foo->getString() === null); + $em->refresh($foo); + \assert($foo->getString() !== null); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8614.php b/tests/PHPStan/Rules/Comparison/data/bug-8614.php new file mode 100644 index 0000000000..6eedd79f2a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8614.php @@ -0,0 +1,14 @@ += 8.1 + +namespace Bug8614; + +/** + * @param int|float|bool|string|object|mixed[] $value + */ +function stringify(int|float|bool|string|object|array $value): string +{ + return match (gettype($value)) { + 'integer', 'double', 'boolean', 'string' => (string) $value, + 'object', 'array' => var_export($value, true), + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8643.php b/tests/PHPStan/Rules/Comparison/data/bug-8643.php new file mode 100644 index 0000000000..722c4ef4a3 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8643.php @@ -0,0 +1,19 @@ +message(); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8736.php b/tests/PHPStan/Rules/Comparison/data/bug-8736.php new file mode 100644 index 0000000000..ac6e79e59c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8736.php @@ -0,0 +1,15 @@ + ['min_range' => $minimum]]; + $filtered = filter_var($value, FILTER_VALIDATE_INT, $options); + if ($filtered === false) { + return compact('minimum', 'value'); + } + } + if (isset($schema['maximum'])) { + $maximum = $schema['maximum']; + if (filter_var($maximum, FILTER_VALIDATE_INT) === false) { + throw new LogicException('`maximum` must be `int`'); + } + $options = ['options' => ['max_range' => $maximum]]; + /** @var int|false */ + $filtered = filter_var($value, FILTER_VALIDATE_INT, $options); + if ($filtered === false) { + return compact('maximum', 'value'); + } + } + // ... + } + if (is_string($value)) { + // ... + } + return true; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8776-2.php b/tests/PHPStan/Rules/Comparison/data/bug-8776-2.php new file mode 100644 index 0000000000..50b69fb7cd --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8776-2.php @@ -0,0 +1,24 @@ + ['min_range' => $minimum]]; + $filtered = filter_var($value, FILTER_VALIDATE_INT, $options); + if ($filtered === false) { + return; + } + } + + public function sayWorld(int $value): void + { + $options = ['options' => ['min_range' => 17]]; + $filtered = filter_var($value, FILTER_VALIDATE_INT, $options); + if ($filtered === false) { + return; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8797.php b/tests/PHPStan/Rules/Comparison/data/bug-8797.php new file mode 100644 index 0000000000..853c5ed347 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8797.php @@ -0,0 +1,13 @@ += 8.0 + +namespace Bug8900; + +class Foo +{ + + public function doFoo(): void + { + $test_array = []; + for($index = 0; $index++; $index < random_int(1,100)) { + $test_array[] = 'entry'; + } + + foreach($test_array as $key => $value) { + $key_mod_4 = match($key % 4) { + 0 => '0', + 1 => '1', + 2 => '2', + 3 => '3', + }; + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8926.php b/tests/PHPStan/Rules/Comparison/data/bug-8926.php new file mode 100644 index 0000000000..c5d92bd1e0 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8926.php @@ -0,0 +1,32 @@ +test = false; + (function($arr) { + $this->test = count($arr) == 1; + })($arr); + + + if ($this->test) { + echo "...\n"; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8932.php b/tests/PHPStan/Rules/Comparison/data/bug-8932.php new file mode 100644 index 0000000000..b52a425157 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8932.php @@ -0,0 +1,18 @@ += 8.0 + +namespace Bug8932; + +class HelloWorld +{ + /** + * @param 'A'|'B' $string + */ + public function sayHello(string $string): int + { + return match ($string) { + 'A' => 1, + 'B' => 2, + default => throw new \LogicException(), + }; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8937.php b/tests/PHPStan/Rules/Comparison/data/bug-8937.php new file mode 100644 index 0000000000..161fdc59e7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8937.php @@ -0,0 +1,26 @@ += 8.0 + +namespace Bug8937; + +/** + * @param 'A'|'B' $string + */ +function sayHello(string $string): int +{ + return match ($string) { + 'A' => 1, + 'B' => 2, + }; +} + +/** + * @param array|string $v + */ +function foo(array|string $v): string +{ + return match(true) { + is_string($v) => 'string', + is_array($v) && \array_is_list($v) => 'list', + is_array($v) => 'array', + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8938.php b/tests/PHPStan/Rules/Comparison/data/bug-8938.php new file mode 100644 index 0000000000..8d354b0d81 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8938.php @@ -0,0 +1,17 @@ + 0) { + $firstChar = substr($data, 0, 1); + $data = substr($data, 1); + $returnValue = $returnValue . $firstChar; + } + return $returnValue; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8954.php b/tests/PHPStan/Rules/Comparison/data/bug-8954.php new file mode 100644 index 0000000000..b89b47ba6d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8954.php @@ -0,0 +1,28 @@ + $class + * @param class-string $expected + * + * @return ?class-string + */ +function ensureSubclassOf(?string $class, string $expected): ?string { + if ($class === null) { + return $class; + } + + if (!class_exists($class)) { + throw new \Exception("Class “{$class}” does not exist."); + } + + if (!is_subclass_of($class, $expected)) { + throw new \Exception("Class “{$class}” is not a subclass of “{$expected}”."); + } + + return $class; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9007.php b/tests/PHPStan/Rules/Comparison/data/bug-9007.php new file mode 100644 index 0000000000..ae2fa03d6c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9007.php @@ -0,0 +1,20 @@ += 8.1 + +namespace Bug9007; + +use function PHPStan\Testing\assertType; + +enum Country: string { + case Usa = 'USA'; + case Canada = 'CAN'; + case Mexico = 'MEX'; +} + +function doStuff(string $countryString): int { + assertType(Country::class, Country::from($countryString)); + return match (Country::from($countryString)) { + Country::Usa => 1, + Country::Canada => 2, + Country::Mexico => 3, + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9104.php b/tests/PHPStan/Rules/Comparison/data/bug-9104.php new file mode 100644 index 0000000000..e701ca020b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9104.php @@ -0,0 +1,18 @@ + $list + */ + public function getFirst(array $list): int + { + if (count($list) === 0) { + throw new \LogicException('empty array'); + } + + return $list[0]; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9142.php b/tests/PHPStan/Rules/Comparison/data/bug-9142.php new file mode 100644 index 0000000000..9c149242d7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9142.php @@ -0,0 +1,38 @@ += 8.1 + +namespace Bug9142; + +enum MyEnum: string +{ + + case One = 'one'; + case Two = 'two'; + case Three = 'three'; + + public function thisTypeWithSubtractedEnumCase(): int + { + if ($this === self::Three) { + return -1; + } + + if ($this === self::Three) { + return 0; + } + + return 1; + } + + public function enumTypeWithSubtractedEnumCase(self $self): int + { + if ($self === self::Three) { + return -1; + } + + if ($self === self::Three) { + return 0; + } + + return 1; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9357.php b/tests/PHPStan/Rules/Comparison/data/bug-9357.php new file mode 100644 index 0000000000..c0bae16688 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9357.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug9357; + +enum MyEnum: string { + case A = 'a'; + case B = 'b'; +} + +class My { + /** @phpstan-impure */ + public function getType(): MyEnum { + echo "called!"; + return rand() > 0.5 ? MyEnum::A : MyEnum::B; + } +} + +function test(My $m): void { + echo match ($m->getType()) { + MyEnum::A => 1, + MyEnum::B => 2, + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9436.php b/tests/PHPStan/Rules/Comparison/data/bug-9436.php new file mode 100644 index 0000000000..55846cc903 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9436.php @@ -0,0 +1,15 @@ += 8.0 + +namespace Bug9436; + +$foo = rand(0, 100); + +if (!in_array($foo, [0, 1, 2])) { + exit(); +} + +$bar = match ($foo) { + 0 => 'a', + 1 => 'b', + 2 => 'c', +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9457.php b/tests/PHPStan/Rules/Comparison/data/bug-9457.php new file mode 100644 index 0000000000..f3a456c14f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9457.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug9457; + +class Randomizer { + function bool(): bool { + return rand(0, 1) === 1; + } + + public function doFoo(?self $randomizer): void + { + // Correct + echo match ($randomizer?->bool()) { + true => 'true', + false => 'false', + null => 'null', + }; + + // Correct + echo match ($randomizer?->bool()) { + true => 'true', + false, null => 'false or null', + }; + + // Unexpected error + echo match ($randomizer?->bool()) { + false => 'false', + true, null => 'true or null', + }; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9499.php b/tests/PHPStan/Rules/Comparison/data/bug-9499.php new file mode 100644 index 0000000000..0a833ad59d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9499.php @@ -0,0 +1,52 @@ += 8.1 + +namespace Bug9499; + +use function PHPStan\Testing\assertType; + +enum FooEnum +{ + case A; + case B; + case C; + case D; +} + +class Foo +{ + public function __construct(public readonly FooEnum $f) + { + } +} + +function test(FooEnum $f, Foo $foo): void +{ + $arr = ['f' => $f]; + match ($arr['f']) { + FooEnum::A, FooEnum::B => match ($arr['f']) { + FooEnum::A => 'a', + FooEnum::B => 'b', + }, + default => '', + }; + match ($foo->f) { + FooEnum::A, FooEnum::B => match ($foo->f) { + FooEnum::A => 'a', + FooEnum::B => 'b', + }, + default => '', + }; +} + +function test2(FooEnum $f, Foo $foo): void +{ + $arr = ['f' => $f]; + match ($arr['f']) { + FooEnum::A, FooEnum::B => assertType(FooEnum::class . '::A|' . FooEnum::class . '::B', $arr['f']), + default => '', + }; + match ($foo->f) { + FooEnum::A, FooEnum::B => assertType(FooEnum::class . '::A|' . FooEnum::class . '::B', $foo->f), + default => '', + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9723.php b/tests/PHPStan/Rules/Comparison/data/bug-9723.php new file mode 100644 index 0000000000..eba7197793 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9723.php @@ -0,0 +1,39 @@ += 8.1 + +namespace Bug9723; + +enum State : int +{ + case StateZero = 0; + case StateOne = 1; +} + +function doFoo() { + $state = rand(0,5); + +// First time checking +// $state is 0|1|2|3|4|5 + if ( $state === State::StateZero->value ) + { + echo "No phpstan errors so far!"; + } + + switch ( $state ) + { + case State::StateZero->value: + case State::StateOne->value: + break; + default: + throw new Exception("Error"); + } + +// Second time checking +// $state is State::StateZero->value|State::StateOne->value +// ... or equivalently, $state is 0|1 +// ... but phpstan thinks $state is definitely 0 + if ( $state === State::StateZero->value ) + { + echo "What's changed?"; + } +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9723b.php b/tests/PHPStan/Rules/Comparison/data/bug-9723b.php new file mode 100644 index 0000000000..acbd20b08e --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9723b.php @@ -0,0 +1,48 @@ += 8.1 + +namespace Bug9723b; + +enum State : int +{ + case StateZero = 0; + case StateOne = 1; + case StateTwo = 2; +} + +function doFoo() { + $state = rand(0,5); + +// First time checking +// $state is 0|1|2|3|4|5 + if ( + $state === State::StateZero->value + || $state === State::StateTwo->value + ) + { + echo "No phpstan errors so far!"; + } + + switch ( $state ) + { + case State::StateZero->value: + case State::StateOne->value: + case State::StateTwo->value: + break; + default: + throw new Exception("Error"); + } + +// Second time checking +// $state is State::StateZero->value|State::StateOne->value|State::StateTwo->value +// ... or equivalently, $state is 0|1|2 +// ... but phpstan thinks $state is definitely 0 +// ... and that is is being compared against 0 and 1, not 0 and 2??? + if ( + $state === State::StateZero->value + || $state === State::StateTwo->value + ) + { + echo "What's changed?"; + } +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9804.php b/tests/PHPStan/Rules/Comparison/data/bug-9804.php new file mode 100644 index 0000000000..723f8ba159 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9804.php @@ -0,0 +1,34 @@ +|int<1, max> and 0 will always evaluate to false. + $firstLetterAsInt = (int)substr($someString, 0, 1); + if ($firstLetterAsInt === 0) { + return; + } + } + + public function pass(?string $someString): void + { + // Line below is the only difference to "error" method + if ($someString === null) { + return; + } + + // All ok + $firstLetterAsInt = (int)substr($someString, 0, 1); + if ($firstLetterAsInt === 0) { + return; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9850.php b/tests/PHPStan/Rules/Comparison/data/bug-9850.php new file mode 100644 index 0000000000..2f1ba9e50a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9850.php @@ -0,0 +1,24 @@ += 3) { + // todo + } + } + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9879.php b/tests/PHPStan/Rules/Comparison/data/bug-9879.php new file mode 100644 index 0000000000..3223872658 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9879.php @@ -0,0 +1,18 @@ += 8.1 + +namespace Bug9879; + +final class A { + public function test(): void + { + for($idx = 0; $idx < 6; $idx += 1) { + match($idx % 3) { + 0 => 1, + 1 => 2, + 2 => 0, + }; + } + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-pr-3404.php b/tests/PHPStan/Rules/Comparison/data/bug-pr-3404.php new file mode 100644 index 0000000000..7dd533ff98 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-pr-3404.php @@ -0,0 +1,24 @@ += 8.0 + +namespace BugPR3404; + +interface Location +{ + +} + +/** @return class-string */ +function aaa(): string +{ + +} + +function (Location $l): void { + if (is_a($l, aaa(), true)) { + // might not always be true. $l might be one subtype of Location, aaa() might return a name of a different subtype of Location + } + + if (is_a($l, Location::class, true)) { + // always true + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-unhandled-true-with-complex-condition.php b/tests/PHPStan/Rules/Comparison/data/bug-unhandled-true-with-complex-condition.php new file mode 100644 index 0000000000..60e777703c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-unhandled-true-with-complex-condition.php @@ -0,0 +1,34 @@ += 8.1 + +namespace MatchUnhandledTrueWithComplexCondition; + +enum Bar +{ + + case ONE; + case TWO; + case THREE; + +} + +class Foo +{ + + public Bar $type; + + public function getRand(): int + { + return rand(0, 10); + } + + public function getPriority(): int + { + return match (true) { + $this->type === Bar::ONE => 0, + $this->type === Bar::TWO && $this->getRand() !== 8 => 1, + $this->type === BAR::THREE => 2, + $this->type === BAR::TWO => 3, + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php b/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php index 3321e8929b..0d248e6c8e 100644 --- a/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php @@ -21,4 +21,14 @@ public function doFoo( } } + /** @param array $strings */ + public function checkInArray(int $i, array $strings): void + { + if (in_array($i, $strings, true)) { + } + + if (in_array(1, $strings, true)) { + } + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php index cf6bcad137..e84f4f0901 100644 --- a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php @@ -181,6 +181,16 @@ public function doFoo() } } + public function doBar(Foo $foo) + { + if (method_exists($foo, 'test')) { + + } + if (method_exists($foo, 'doFoo')) { + + } + } + } final class FinalClassWithMethodExists @@ -198,6 +208,7 @@ public function doFoo() } +#[\AllowDynamicProperties] final class FinalClassWithPropertyExists { @@ -615,6 +626,21 @@ public function testWithNewObjectInFirstArgument(): void if (method_exists((new MethodExists()), $string)) { } } + + public function testWithTypehintedObject(MethodExists $methodExists): void + { + /** @var string $string */ + $string = doFoo(); + + if (method_exists($methodExists, 'testWithNewObjectInFirstArgument')) { + } + + if (method_exists($methodExists, 'undefinedMethod')) { + } + + if (method_exists($methodExists, $string)) { + } + } } trait MethodExistsTrait @@ -757,6 +783,7 @@ function doIpsum(array $data): void } +#[\AllowDynamicProperties] class Bug2221 { @@ -845,3 +872,132 @@ public function doBar($std, $stdClassesOrNull): void } } + +class ArraySearch +{ + + /** + * @param int $i + * @param non-empty-array $is + * @return void + */ + public function doFoo(int $i, array $is): void + { + $res = array_search($i, $is, true); + } + +} + +/** + * @phpstan-assert-if-true int $value + */ +function testIsInt(mixed $value): bool +{ + return is_int($value); +} + +function (int $int) { + if (testIsInt($int)) { + + } +}; + +class ConditionalAlwaysTrue +{ + public function sayHello(?int $date): void + { + if ($date === null) { + } elseif (is_int($date)) { // always-true should not be reported because last condition + } + + if ($date === null) { + } elseif (is_int($date)) { // always-true should be reported, because another condition below + } elseif (rand(0,1)) { + } + } +} + +class InArray3 +{ + /** + * @param non-empty-array $nonEmptyInts + * @param array $strings + */ + public function doFoo(int $i, string $s, array $nonEmptyInts, array $strings): void + { + if (in_array($i, $strings)) { + } + + if (in_array($i, $strings, false)) { + } + + if (in_array(5, $strings)) { + } + + if (in_array(5, $strings, false)) { + } + + if (in_array($s, $nonEmptyInts)) { + } + + if (in_array($s, $nonEmptyInts, false)) { + } + + if (in_array('5', $nonEmptyInts)) { + } + + if (in_array('5', $nonEmptyInts, false)) { + } + + if (in_array(1, $strings, true)) { + } + } +} + +function checkSuperGlobals(): void +{ + foreach ($GLOBALS as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_SERVER as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_GET as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_POST as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_FILES as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_COOKIE as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_SESSION as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_REQUEST as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_ENV as $k => $v) { + if (is_int($k)) {} + } +} + +/** + * @param resource $resource + */ +function checkClosedResource($resource): void { + if (!is_resource($resource)) { + + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/docblock-assert-equality.php b/tests/PHPStan/Rules/Comparison/data/docblock-assert-equality.php new file mode 100644 index 0000000000..761ac48b47 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/docblock-assert-equality.php @@ -0,0 +1,47 @@ +isInt($date)) { // always-true should not be reported because last condition + } + + if ($date === null) { + } elseif ($this->isInt($date)) { // always-true should be reported, because another condition below + } elseif (rand(0,1)) { + } + } + + /** + * @phpstan-assert-if-true int $value + */ + public function isInt($value): bool { + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-method-exists-on-generic-class-string.php b/tests/PHPStan/Rules/Comparison/data/impossible-method-exists-on-generic-class-string.php new file mode 100644 index 0000000000..1fcd8d344c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-method-exists-on-generic-class-string.php @@ -0,0 +1,61 @@ +&literal-string $s + */ + public function sayGenericHello(string $s): void + { + // no erros on non-final class + if (method_exists($s, 'nonExistent')) { + $s->nonExistent(); + $s::nonExistent(); + } + + if (method_exists($s, 'staticAbc')) { + $s::staticAbc(); + $s->staticAbc(); + } + + if (method_exists($s, 'nonStaticAbc')) { + $s::nonStaticAbc(); + $s->nonStaticAbc(); + } + } + + /** + * @param class-string&literal-string $s + */ + public function sayFinalGenericHello(string $s): void + { + if (method_exists($s, 'nonExistent')) { + $s->nonExistent(); + $s::nonExistent(); + } + + if (method_exists($s, 'staticAbc')) { + $s::staticAbc(); + $s->staticAbc(); + } + + if (method_exists($s, 'nonStaticAbc')) { + $s::nonStaticAbc(); + $s->nonStaticAbc(); + } + } +} + +class S { + public static function staticAbc():void {} + + public function nonStaticAbc():void {} +} + +final class FinalS { + public static function staticAbc():void {} + + public function nonStaticAbc():void {} +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-method-report-always-true-last-condition.php b/tests/PHPStan/Rules/Comparison/data/impossible-method-report-always-true-last-condition.php new file mode 100644 index 0000000000..6e1b47da7c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-method-report-always-true-last-condition.php @@ -0,0 +1,32 @@ +assertString($s)) { + + } + } + + public function doBar(string $s) + { + $assertion = new AssertionClass; + if (rand(0, 1)) { + + } elseif ($assertion->assertString($s)) { + + } else { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call.php b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call.php index 01a0125a69..3103493358 100644 --- a/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call.php +++ b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call.php @@ -53,3 +53,24 @@ public function nullableInt(): ?int } } + +class ConditionalAlwaysTrue +{ + public function sayHello(?int $date): void + { + if ($date === null) { + } elseif (self::isInt($date)) { // always-true should not be reported because last condition + } + + if ($date === null) { + } elseif (self::isInt($date)) { // always-true should be reported, because another condition below + } elseif (rand(0,1)) { + } + } + + /** + * @phpstan-assert-if-true int $value + */ + static public function isInt($value): bool { + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-static-method-report-always-true-last-condition.php b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-report-always-true-last-condition.php new file mode 100644 index 0000000000..c9beac09ff --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-report-always-true-last-condition.php @@ -0,0 +1,30 @@ += 8.1 + +namespace LastMatchArmAlwaysTrue; + +enum Foo { + + case ONE; + case TWO; + + public function doFoo(): void + { + match ($this) { + self::ONE => 'test', + self::TWO => 'two', + }; + } + + public function doBar(): void + { + match ($this) { + self::ONE => 'test', + self::TWO => 'two', + default => 'three', + }; + } + + public function doBaz(): void + { + match ($this) { + self::ONE => 'test', + self::TWO => 'two', + self::TWO => 'three', + }; + } + + public function doBaz2(): void + { + match ($this) { + self::ONE => 'test', + self::TWO => 'two', + self::TWO => 'three', + default => 'four', + }; + } + +} + +enum Bar { + + case ONE; + + public function doFoo(): void + { + match ($this) { + self::ONE => 'test', + }; + } + + public function doBar(): void + { + match ($this) { + self::ONE => 'test', + default => 'test2', + }; + } + + public function doBaz(): void + { + match (1) { + 0 => 'test', + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/logical-xor.php b/tests/PHPStan/Rules/Comparison/data/logical-xor.php new file mode 100644 index 0000000000..fe63eb640b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/logical-xor.php @@ -0,0 +1,27 @@ += 8.1 + +namespace LooseComparisonAgainstEnums; + +enum FooUnitEnum +{ + case A; + case B; +} + +enum FooBackedEnum: string +{ + case A = 'A'; + case B = 'B'; +} + +class InArrayTest +{ + public function enumVsString(FooUnitEnum $u, FooBackedEnum $b): void + { + if (in_array($u, ['A'])) { + } + + if (in_array($u, ['A'], false)) { + } + + if (in_array($b, ['A'])) { + } + + if (in_array($b, ['A'], false)) { + } + + if (in_array(rand() ? $u : $b, ['A'], false)) { + } + } + + public function stringVsEnum(FooUnitEnum $u, FooBackedEnum $b): void + { + if (in_array('A', [$u])) { + } + + if (in_array('A', [$u], false)) { + } + + if (in_array('A', [$b])) { + } + + if (in_array('A', [$b], false)) { + } + + if (in_array('A', [rand() ? $u : $b], false)) { + } + } + + public function enumVsBool(FooUnitEnum $u, FooBackedEnum $b, bool $bl): void + { + if (in_array($u, [$bl])) { + } + + if (in_array($u, [$bl], false)) { + } + + if (in_array($b, [$bl])) { + } + + if (in_array($b, [$bl], false)) { + } + + if (in_array(rand() ? $u : $b, [$bl], false)) { + } + } + + public function boolVsEnum(FooUnitEnum $u, FooBackedEnum $b, bool $bl): void + { + if (in_array($bl, [$u])) { + } + + if (in_array($bl, [$u], false)) { + } + + if (in_array($bl, [$b])) { + } + + if (in_array($bl, [$b], false)) { + } + + if (in_array($bl, [rand() ? $u : $b], false)) { + } + } + + public function null(FooUnitEnum $u, FooBackedEnum $b): void + { + if (in_array($u, [null])) { + } + + if (in_array(null, [$b])) { + } + } + + public function nullableEnum(?FooUnitEnum $u, string $s): void + { + // null == "" + if (in_array($u, [$s])) { + } + + if (in_array($s, [$u])) { + } + } + + /** + * @param array $strings + * @param array $unitEnums + */ + public function dynamicValues(FooUnitEnum $u, string $s, array $strings, array $unitEnums): void + { + if (in_array($u, $unitEnums)) { + } + + if (in_array($u, $unitEnums, false)) { + } + + if (in_array($u, $unitEnums, true)) { + } + + if (in_array($u, $strings)) { + } + + if (in_array($u, $strings, false)) { + } + + if (in_array($u, $strings, true)) { + } + + if (in_array($s, $strings)) { + } + + if (in_array($s, $strings, false)) { + } + + if (in_array($s, $strings, true)) { + } + + if (in_array($s, $unitEnums)) { + } + + if (in_array($s, $unitEnums, false)) { + } + + if (in_array($s, $unitEnums, true)) { + } + } + + /** + * @param non-empty-array $nonEmptyA + * @return void + */ + public function nonEmptyArray(array $nonEmptyA): void + { + if (in_array(FooUnitEnum::B, $nonEmptyA)) { + } + + if (in_array(FooUnitEnum::A, $nonEmptyA)) { + } + + if (in_array(FooUnitEnum::A, $nonEmptyA, false)) { + } + + if (in_array(FooUnitEnum::A, $nonEmptyA, true)) { + } + + if (in_array(FooUnitEnum::B, $nonEmptyA, false)) { + } + + if (in_array(FooUnitEnum::B, $nonEmptyA, true)) { + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/loose-comparison-report-always-true-last-condition.php b/tests/PHPStan/Rules/Comparison/data/loose-comparison-report-always-true-last-condition.php new file mode 100644 index 0000000000..f7e964231c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/loose-comparison-report-always-true-last-condition.php @@ -0,0 +1,28 @@ += 8.1 + +namespace MatchAlwaysTrueLastArm; + +enum Foo +{ + + case FOO; + case BAR; + + public function doFoo(): void + { + match ($this) { + self::FOO => 1, + self::BAR => 2, + }; + } + + public function doBar(): void + { + match ($this) { + self::FOO => 1, + self::BAR => 2, + default => 3, + }; + } + + public function doBaz(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + // reported by StrictComparisonOfDifferentTypesRule + match (true) { + $a === 'aaa' => 1, + $a === 'bbb' => 2, + }; + } + + public function doMoreConditionsInLastArm(): void + { + match ($this) { + self::FOO, self::BAR => 1, + }; + + match ($this) { + self::FOO, self::BAR => 1, + default => 2, + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/match-enums.php b/tests/PHPStan/Rules/Comparison/data/match-enums.php index d96b958d59..43e765c552 100644 --- a/tests/PHPStan/Rules/Comparison/data/match-enums.php +++ b/tests/PHPStan/Rules/Comparison/data/match-enums.php @@ -78,4 +78,51 @@ public function doBaz(Foo $foo, Foo $bar): int }; } + public function doFoo2(Foo $foo): int + { + return match ($foo) { + Foo::ONE => 'one', + Foo::ONE => 'one2', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doFoo3(Foo $foo): int + { + return match ($foo) { + Foo::ONE => 'one', + DifferentEnum::ONE => 'one2', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doFoo4(Foo $foo): int + { + return match ($foo) { + Foo::ONE, Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doFoo5(Foo $foo): int + { + return match ($foo) { + Foo::ONE, DifferentEnum::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + +} + +enum DifferentEnum: int +{ + + case ONE = 1; + case TWO = 2; + case THREE = 3; + } diff --git a/tests/PHPStan/Rules/Comparison/data/match-expr-property-hooks.php b/tests/PHPStan/Rules/Comparison/data/match-expr-property-hooks.php new file mode 100644 index 0000000000..b59eb1dc3e --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/match-expr-property-hooks.php @@ -0,0 +1,33 @@ += 8.4 + +namespace MatchExprPropertyHooks; + +use UnhandledMatchError; + +class Foo +{ + + /** @var 1|2|3 */ + public int $i { + get { + return match ($this->i) { + 1 => 'foo', + 2 => 'bar', + }; + } + } + + /** + * @var 1|2|3 + */ + public int $j { + /** @throws UnhandledMatchError */ + get { + return match ($this->j) { + 1 => 10, + 2 => 20, + }; + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/match-expr.php b/tests/PHPStan/Rules/Comparison/data/match-expr.php index c56cfb8e34..5970804bb4 100644 --- a/tests/PHPStan/Rules/Comparison/data/match-expr.php +++ b/tests/PHPStan/Rules/Comparison/data/match-expr.php @@ -52,10 +52,6 @@ public function doFoo(int $i): void // unhandled }; - match ($i) { - // unhandled - }; - match ($i) { 1, 2 => null, default => null, // OK @@ -67,7 +63,7 @@ public function doFoo(int $i): void }; match (1) { - 1 => 1, // always true - report with strict-rules + 1 => 1, }; match ($i) { @@ -78,18 +74,22 @@ public function doFoo(int $i): void default => 1, 1 => 2, }; + + match ($i) { + // unhandled + }; } public function doBar(\Exception $e): void { match (true) { - $e instanceof \InvalidArgumentException, $e instanceof \InvalidArgumentException => true, + $e instanceof \InvalidArgumentException, $e instanceof \InvalidArgumentException => true, // reported by ImpossibleInstanceOfRule default => null, }; match (true) { $e instanceof \InvalidArgumentException => true, - $e instanceof \InvalidArgumentException => true, + $e instanceof \InvalidArgumentException => true, // reported by ImpossibleInstanceOfRule }; } @@ -138,3 +138,80 @@ public function doBar(int $i): void { } + +class ThrowsTag { + /** + * @throws \UnhandledMatchError + */ + public function foo(int $bar): void + { + $str = match($bar) { + 1 => 'test' + }; + } + + /** + * @throws \Error + */ + public function bar(int $bar): void + { + $str = match($bar) { + 1 => 'test' + }; + } + + /** + * @throws \Exception + */ + public function baz(int $bar): void + { + $str = match($bar) { + 1 => 'test' + }; + } +} + +function (): string { + $foo = fn(): int => rand(); + $bar = fn(): int => rand(); + return match ($foo <=> $bar) { + 1 => 'up', + 0 => 'neutral', + -1 => 'down', + }; +}; + +final class FinalFoo +{ + +} + +final class FinalBar +{ + +} + +class TestGetClass +{ + + public function doMatch(FinalFoo|FinalBar $class): void + { + match (get_class($class)) { + FinalFoo::class => 1, + FinalBar::class => 2, + }; + } + +} +class TestGetDebugType +{ + + public function doMatch(FinalFoo|FinalBar $class): void + { + match (get_debug_type($class)) { + FinalFoo::class => 1, + FinalBar::class => 2, + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/non-empty-string-impossible-type.php b/tests/PHPStan/Rules/Comparison/data/non-empty-string-impossible-type.php new file mode 100644 index 0000000000..d2092228d2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/non-empty-string-impossible-type.php @@ -0,0 +1,22 @@ += 0) { + } + } + + /** @param positive-int $i */ + public function sayHello2(int $i): void + { + if ($i < 0) { + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/property-exists-object-shapes.php b/tests/PHPStan/Rules/Comparison/data/property-exists-object-shapes.php new file mode 100644 index 0000000000..33d55d212a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/property-exists-object-shapes.php @@ -0,0 +1,29 @@ + [ + self::GROUP_PUBLIC_CONSTANTS, + self::GROUP_PROTECTED_CONSTANTS, + self::GROUP_PRIVATE_CONSTANTS, + ], + self::GROUP_SHORTCUT_STATIC_PROPERTIES => [ + self::GROUP_PUBLIC_STATIC_PROPERTIES, + self::GROUP_PROTECTED_STATIC_PROPERTIES, + self::GROUP_PRIVATE_STATIC_PROPERTIES, + ], + self::GROUP_SHORTCUT_PROPERTIES => [ + self::GROUP_SHORTCUT_STATIC_PROPERTIES, + self::GROUP_PUBLIC_PROPERTIES, + self::GROUP_PROTECTED_PROPERTIES, + self::GROUP_PRIVATE_PROPERTIES, + ], + self::GROUP_SHORTCUT_PUBLIC_METHODS => [ + self::GROUP_PUBLIC_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PUBLIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_METHODS, + self::GROUP_PUBLIC_METHODS, + ], + self::GROUP_SHORTCUT_PROTECTED_METHODS => [ + self::GROUP_PROTECTED_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_METHODS, + self::GROUP_PROTECTED_METHODS, + ], + self::GROUP_SHORTCUT_PRIVATE_METHODS => [ + self::GROUP_PRIVATE_STATIC_METHODS, + self::GROUP_PRIVATE_METHODS, + ], + self::GROUP_SHORTCUT_FINAL_METHODS => [ + self::GROUP_PUBLIC_FINAL_METHODS, + self::GROUP_PROTECTED_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + ], + self::GROUP_SHORTCUT_ABSTRACT_METHODS => [ + self::GROUP_PUBLIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + ], + self::GROUP_SHORTCUT_STATIC_METHODS => [ + self::GROUP_STATIC_CONSTRUCTORS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_METHODS, + self::GROUP_PROTECTED_STATIC_METHODS, + self::GROUP_PRIVATE_STATIC_METHODS, + ], + self::GROUP_SHORTCUT_METHODS => [ + self::GROUP_SHORTCUT_FINAL_METHODS, + self::GROUP_SHORTCUT_ABSTRACT_METHODS, + self::GROUP_SHORTCUT_STATIC_METHODS, + self::GROUP_CONSTRUCTOR, + self::GROUP_DESTRUCTOR, + self::GROUP_PUBLIC_METHODS, + self::GROUP_PROTECTED_METHODS, + self::GROUP_PRIVATE_METHODS, + self::GROUP_MAGIC_METHODS, + ], + ]; + + /** + * @param array $supportedGroups + * @return array + */ + public function unpackShortcut(string $shortcut, array $supportedGroups): array + { + $groups = []; + + foreach (self::SHORTCUTS[$shortcut] as $groupOrShortcut) { + if (in_array($groupOrShortcut, $supportedGroups, true)) { + $groups[] = $groupOrShortcut; + } elseif ( + !array_key_exists($groupOrShortcut, self::SHORTCUTS) + && in_array($groupOrShortcut, self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS], true) + ) { + // Nothing + } else { + $groups = array_merge($groups, $this->unpackShortcut($groupOrShortcut, $supportedGroups)); + } + } + + return $groups; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison-enum-tips.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison-enum-tips.php new file mode 100644 index 0000000000..42e26a99d3 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison-enum-tips.php @@ -0,0 +1,57 @@ += 8.1 + +namespace StrictComparisonEnumTips; + +enum SomeEnum +{ + + case One; + case Two; + + public function exhaustiveWithSafetyCheck(): int + { + // not reported by this rule at all + if ($this === self::One) { + return -1; + } elseif ($this === self::Two) { + return 0; + } else { + throw new \LogicException('New case added, handling missing'); + } + } + + + public function exhaustiveWithSafetyCheck2(): int + { + // not reported by this rule at all + if ($this === self::One) { + return -1; + } + + if ($this === self::Two) { + return 0; + } + + throw new \LogicException('New case added, handling missing'); + } + + public function exhaustiveWithSafetyCheckInMatchAlready(): int + { + // not reported by this rule at all + return match ($this) { + self::One => -1, + self::Two => 0, + default => throw new \LogicException('New case added, handling missing'), + }; + } + + public function exhaustiveWithSafetyCheckInMatchAlready2(self $self): int + { + return match (true) { + $self === self::One => -1, + $self === self::Two => 0, + default => throw new \LogicException('New case added, handling missing'), + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-condition-always-true.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-condition-always-true.php new file mode 100644 index 0000000000..728f3ed2e8 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-condition-always-true.php @@ -0,0 +1,34 @@ += 8.1 + +namespace StrictComparisonLastMatchArm; + +class Foo +{ + + public function doBaz(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + match (true) { + $a === 'aaa' => 1, + $a === 'bbb' => 2, + }; + } + + public function doFoo(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + if ($a === 'aaa') { + + } elseif ($a === 'bbb') { + + } + + if ($a === 'aaa') { + + } elseif ($a === 'bbb') { + + } elseif ($a === 'ccc') { + + } else { + + } + + if ($a === 'aaa') { + + } elseif ($a === 'bbb') { + + } else { + + } + } + + public function doIpsum(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + match (true) { + $a === 'aaa' => 1, + $a === 'bbb' => 2, + default => new \Exception(), + }; + } + + public function doMoreConditionsInLastArm(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + match (true) { + $a === 'aaa', $a === 'bbb' => 1, + }; + + match (true) { + $a === 'aaa', $a === 'bbb' => 1, + default => new \Exception(), + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison-property-native-types.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison-property-native-types.php index e973c1c1da..7321469b26 100644 --- a/tests/PHPStan/Rules/Comparison/data/strict-comparison-property-native-types.php +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison-property-native-types.php @@ -1,4 +1,4 @@ -= 7.4 +returnArray();) { + if ($val === null) { + + } + $val = null; + } + $foo = null; for (;;) { if ($foo !== null) { @@ -159,13 +166,6 @@ public function forWithTypeChange() $foo = new self(); } } - - for (; $val = $this->returnArray();) { - if ($val === null) { - - } - $val = null; - } } private function returnArray(): array @@ -974,3 +974,80 @@ public function doBar(array $a): void } } + +function () { + INF === INF; + NAN === NAN; + + INF !== INF; + NAN !== NAN; +}; + +class ArrayWithLongStrings2 +{ + + public function doFoo() + { + $array = ['foofoofoofoofoofoofoo','foofoofoofoofoofoofob']; + + foreach ($array as $value) { + if ('foofoofoofoofoofoofoo' === $value) { + echo 'nope'; + } elseif ('foofoofoofoofoofoofob' === $value) { + echo 'nop nope'; + } elseif (rand(0, 1) === 0) { + echo 'nope'; + } + } + } + +} + +class TestLiteralStringVerbosityFix +{ + + /** + * @param lowercase-string|false $a + */ + public function doFoo($a): void + { + if ($a === 'AB') { + + } + } + +} + +class SubtractedMixedAgainstNull +{ + + public function doFoo($m): void + { + if ($m === null) { + return; + } + + if ($m === null) { + + } + + if ($m !== null) { + + } + } + + public function doBar($m, int $i, int $j): void + { + if ($m === null) { + return; + } + + $a = [1, $m, 3]; + $b = [$i, null, $j]; + + if ($a !== $b) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/ternary.php b/tests/PHPStan/Rules/Comparison/data/ternary.php index e2c3048ec4..b693f44656 100644 --- a/tests/PHPStan/Rules/Comparison/data/ternary.php +++ b/tests/PHPStan/Rules/Comparison/data/ternary.php @@ -1,6 +1,6 @@ + */ +class ClassAsClassConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ClassAsClassConstantRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/class-as-class-constant.php'], [ + [ + 'A class constant must not be called \'class\'; it is reserved for class name fetching.', + 9, + ], + [ + 'A class constant must not be called \'class\'; it is reserved for class name fetching.', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/ConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ConstantRuleTest.php index e99300cc82..0da9819e08 100644 --- a/tests/PHPStan/Rules/Constants/ConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ConstantRuleTest.php @@ -14,7 +14,12 @@ class ConstantRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ConstantRule(); + return new ConstantRule(true); + } + + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; } public function testConstants(): void @@ -29,14 +34,10 @@ public function testConstants(): void 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Constant DEFINED_CONSTANT not found.', - 13, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - /*[ 'Constant DEFINED_CONSTANT_IF not found.', 21, - ],*/ + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], ]); } @@ -67,4 +68,62 @@ public function testConstEqualsNoNamespace(): void $this->analyse([__DIR__ . '/data/const-equals-no-namespace.php'], []); } + public function testDefinedScopeMerge(): void + { + $this->analyse([__DIR__ . '/data/defined-scope-merge.php'], [ + [ + 'Constant TEST not found.', + 8, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + + [ + 'Constant TEST not found.', + 11, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testRememberedConstructorScope(): void + { + $this->analyse([__DIR__ . '/data/remembered-constructor-scope.php'], [ + [ + 'Constant REMEMBERED_FOO not found.', + 23, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant REMEMBERED_FOO not found.', + 38, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant REMEMBERED_FOO not found.', + 51, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant REMEMBERED_FOO not found.', + 65, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant XYZ22 not found.', + 87, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant XYZ not found.', + 88, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Constant XYZ33 not found.', + 98, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php b/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php new file mode 100644 index 0000000000..4a0b85b34b --- /dev/null +++ b/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php @@ -0,0 +1,70 @@ + + */ +class DynamicClassConstantFetchRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new DynamicClassConstantFetchRule( + self::getContainer()->getByType(PhpVersion::class), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), + ); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80300) { + $errors = [ + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 15, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 16, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 18, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 19, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 20, + ], + ]; + } else { + $errors = [ + [ + 'Class constant name in dynamic fetch can only be a string, int given.', + 18, + ], + [ + 'Class constant name in dynamic fetch can only be a string, int|string given.', + 19, + ], + [ + 'Class constant name in dynamic fetch can only be a string, string|null given.', + 20, + ], + ]; + } + $this->analyse([__DIR__ . '/data/dynamic-class-constant-fetch.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php index 1dc01002f8..1fac7d6798 100644 --- a/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php @@ -40,14 +40,10 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param mixed[] $errors + * @param list $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/FinalPrivateConstantRuleTest.php b/tests/PHPStan/Rules/Constants/FinalPrivateConstantRuleTest.php new file mode 100644 index 0000000000..8e98e04565 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/FinalPrivateConstantRuleTest.php @@ -0,0 +1,27 @@ + */ +class FinalPrivateConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FinalPrivateConstantRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/final-private-const.php'], [ + [ + 'Private constant FinalPrivateConstants\User::FINAL_PRIVATE() cannot be final as it is never overridden by other classes.', + 8, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/MagicConstantContextRuleTest.php b/tests/PHPStan/Rules/Constants/MagicConstantContextRuleTest.php new file mode 100644 index 0000000000..2f598d78b3 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/MagicConstantContextRuleTest.php @@ -0,0 +1,167 @@ + + */ +class MagicConstantContextRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MagicConstantContextRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/magic-constant.php'], [ + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 5, + ], + [ + 'Magic constant __FUNCTION__ is always empty outside a function.', + 6, + ], + [ + 'Magic constant __METHOD__ is always empty outside a function.', + 7, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 9, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 17, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 22, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 26, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 59, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 64, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 78, + ], + [ + 'Magic constant __METHOD__ is always empty outside a function.', + 91, + ], + [ + 'Magic constant __FUNCTION__ is always empty outside a function.', + 92, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 93, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 97, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 101, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 105, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 109, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 115, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 120, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 133, + ], + ]); + } + + public function testGlobalNamespace(): void + { + $this->analyse([__DIR__ . '/data/magic-constant-global-ns.php'], [ + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 5, + ], + [ + 'Magic constant __FUNCTION__ is always empty outside a function.', + 6, + ], + [ + 'Magic constant __METHOD__ is always empty outside a function.', + 7, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 8, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 9, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 16, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 17, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 22, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 25, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 26, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 34, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 46, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 48, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 51, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php index 5d5de8e71a..13e745a3d1 100644 --- a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -14,8 +15,7 @@ class MissingClassConstantTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - return new MissingClassConstantTypehintRule(new MissingTypehintCheck($reflectionProvider, true, true, true, [])); + return new MissingClassConstantTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -29,7 +29,6 @@ public function testRule(): void [ '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.', @@ -38,4 +37,31 @@ public function testRule(): void ]); } + public function testBug8957(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('This test needs PHP 8.2'); + } + $this->analyse([__DIR__ . '/data/bug-8957.php'], []); + } + + public function testRuleShouldNotApplyToNativeTypes(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('This test needs PHP 8.3'); + } + + $this->analyse([__DIR__ . '/data/class-constant-native-type.php'], [ + [ + 'Constant ClassConstantNativeTypeForMissingTypehintRule\Foo::B type has no value type specified in iterable type array.', + 19, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Constant ClassConstantNativeTypeForMissingTypehintRule\Foo::D with generic class ClassConstantNativeTypeForMissingTypehintRule\Bar does not specify its types: T', + 24, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Constants/NativeTypedClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/NativeTypedClassConstantRuleTest.php new file mode 100644 index 0000000000..7e0e4e6103 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/NativeTypedClassConstantRuleTest.php @@ -0,0 +1,36 @@ + + */ +class NativeTypedClassConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new NativeTypedClassConstantRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80300) { + $errors = [ + [ + 'Class constants with native types are supported only on PHP 8.3 and later.', + 10, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/native-typed-class-constant.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php index a4c5d337a4..6ce51b0297 100644 --- a/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php @@ -31,10 +31,6 @@ public function testRule(): void public function testFinal(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $errors = [ [ 'Constant OverridingFinalConstant\Bar::FOO overrides final constant OverridingFinalConstant\Foo::FOO.', @@ -94,4 +90,30 @@ public function testFinal(): void $this->analyse([__DIR__ . '/data/overriding-final-constant.php'], $errors); } + public function testNativeTypes(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/overriding-constant-native-types.php'], [ + [ + 'Native type int|string of constant OverridingConstantNativeTypes\Bar::D is not covariant with native type int of constant OverridingConstantNativeTypes\Foo::D.', + 21, + ], + [ + 'Constant OverridingConstantNativeTypes\Ipsum::B overriding constant OverridingConstantNativeTypes\Lorem::B (int) should also have native type int.', + 37, + ], + [ + 'Constant OverridingConstantNativeTypes\PharChild::BZ2 overriding constant Phar::BZ2 (int) should also have native type int.', + 44, + ], + [ + 'Native type int|string of constant OverridingConstantNativeTypes\PharChild::NONE is not covariant with native type int of constant Phar::NONE.', + 48, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php new file mode 100644 index 0000000000..7942506ca1 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php @@ -0,0 +1,103 @@ + + */ +class ValueAssignedToClassConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ValueAssignedToClassConstantRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/value-assigned-to-class-constant.php'], [ + [ + 'PHPDoc tag @var for constant ValueAssignedToClassConstant\Foo::BAZ with type string is incompatible with value 1.', + 14, + ], + [ + 'PHPDoc tag @var for constant ValueAssignedToClassConstant\Foo::DOLOR with type ValueAssignedToClassConstant\Foo is incompatible with value 1.', + 23, + ], + [ + 'PHPDoc tag @var for constant ValueAssignedToClassConstant\Bar::BAZ with type string is incompatible with value 2.', + 32, + ], + ]); + } + + public function testBug7352(): void + { + $this->analyse([__DIR__ . '/data/bug-7352.php'], []); + } + + public function testBug7352WithSubNamespace(): void + { + $this->analyse([__DIR__ . '/data/bug-7352-with-sub-namespace.php'], []); + } + + public function testBug7273(): void + { + $this->analyse([__DIR__ . '/data/bug-7273.php'], []); + } + + public function testBug7273b(): void + { + $this->analyse([__DIR__ . '/data/bug-7273b.php'], []); + } + + public function testBug5655(): void + { + $this->analyse([__DIR__ . '/data/bug-5655.php'], []); + } + + public function testNativeType(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/value-assigned-to-class-constant-native-type.php'], [ + [ + 'Constant ValueAssignedToClassConstantNativeType\Foo::BAR (int) does not accept value \'bar\'.', + 10, + ], + [ + 'Constant ValueAssignedToClassConstantNativeType\Bar::BAR (int<1, max>) does not accept value 0.', + 21, + ], + [ + 'Constant ValueAssignedToClassConstantNativeType\Floats::BAR (int) does not accept value 1.0.', + 30, + ], + [ + 'PHPDoc tag @var for constant ValueAssignedToClassConstantNativeType\Floats::BAZ with type float is incompatible with value 1.', + 33, + ], + ]); + } + + public function testBug10212(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/bug-10212.php'], [ + [ + 'Constant Bug10212\HelloWorld::B (Bug10212\X\Foo) does not accept value Bug10212\Foo::Bar.', + 15, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-10212.php b/tests/PHPStan/Rules/Constants/data/bug-10212.php new file mode 100644 index 0000000000..40fda3454c --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-10212.php @@ -0,0 +1,22 @@ += 8.3 + +namespace Bug10212; + +use function PHPStan\Testing\assertType; + +enum Foo +{ + case Bar; +} + +class HelloWorld +{ + public const string A = 'foo'; + public const X\Foo B = Foo::Bar; + public const Foo C = Foo::Bar; +} + +function(HelloWorld $hw): void { + assertType(X\Foo::class, $hw::B); + assertType(Foo::class, $hw::C); +}; diff --git a/tests/PHPStan/Rules/Constants/data/bug-5655.php b/tests/PHPStan/Rules/Constants/data/bug-5655.php new file mode 100644 index 0000000000..058d25136f --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-5655.php @@ -0,0 +1,14 @@ + '', + ]; +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-7273.php b/tests/PHPStan/Rules/Constants/data/bug-7273.php new file mode 100644 index 0000000000..b21ffc9d58 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-7273.php @@ -0,0 +1,75 @@ + + */ +abstract class SomeValueProcessor implements ConfigValueProcessorInterface +{ +} + +interface ConfigKey +{ + public const ACTIVITY__EXPORT__TYPE = 'activity.export.type'; + public const ACTIVITY__TAGS__MULTI = 'activity.tags.multi'; + // ... +} + +interface Module +{ + public const ABSENCEREQUEST = 'absencerequest'; + public const ACTIVITY = 'activity'; + // ... +} + +interface ConfigRepositoryInterface +{ + /** + * @var array>, mixed>, + * }> + */ + public const CONFIGURATIONS = [ + ConfigKey::ACTIVITY__EXPORT__TYPE => [ + 'type' => "'SomeExport'|'SomeOtherExport'|null", + 'default' => null, + 'acl' => ['superadmin'], + 'linked_module' => Module::ACTIVITY, + ], + ConfigKey::ACTIVITY__TAGS__MULTI => [ + 'default' => false, + 'acl' => ['admin'], + 'linked_module' => Module::ACTIVITY, + 'value_processors' => [ + SomeValueProcessor::class => ['someOption' => true], + ], + ], + // ... + ]; +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-7273b.php b/tests/PHPStan/Rules/Constants/data/bug-7273b.php new file mode 100644 index 0000000000..136efa9a9e --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-7273b.php @@ -0,0 +1,51 @@ + */ + public const FIRST_EXAMPLE = [ + 'image.png' => [ + 'url' => '/first-link', + 'title' => 'First Link', + ], + 'directory/image.jpg' => [ + 'url' => '/second-link', + 'title' => 'Second Link', + ], + 'another-image.jpg' => [ + 'title' => 'An Image', + ], + ]; + + /** @var array */ + public const SECOND_EXAMPLE = [ + 'image.png' => [ + 'url' => '/first-link', + 'title' => 'First Link', + ], + 'directory/image.jpg' => [ + 'url' => '/second-link', + 'title' => 'Second Link', + ], + 'another-image.jpg' => [ + 'title' => 'An Image', + ], + ]; + + /** @var array{url?: string, title: string}[] */ + public const THIRD_EXAMPLE = [ + 'image.png' => [ + 'url' => '/first-link', + 'title' => 'First Link', + ], + 'directory/image.jpg' => [ + 'url' => '/second-link', + 'title' => 'Second Link', + ], + 'another-image.jpg' => [ + 'title' => 'An Image', + ], + ]; +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-7352-with-sub-namespace.php b/tests/PHPStan/Rules/Constants/data/bug-7352-with-sub-namespace.php new file mode 100644 index 0000000000..a9b87c2cf7 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-7352-with-sub-namespace.php @@ -0,0 +1,11 @@ += 8.2 + +namespace Bug8957; + +use function PHPStan\Testing\assertType; + +enum A: string +{ + case X = 'x'; + case Y = 'y'; +} + +class B { + public const A = [ + A::X->value, + A::Y->value, + ]; + + public function doFoo(): void + { + assertType('array{\'x\', \'y\'}', self::A); + } +} diff --git a/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php b/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php new file mode 100644 index 0000000000..fdf86f77f1 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php @@ -0,0 +1,17 @@ += 8.3 + +namespace ClassConstantNativeTypeForMissingTypehintRule; + +/** @template T */ +class Bar +{ + +} + +const ConstantWithObjectInstanceForNativeType = new Bar(); + +class Foo +{ + + public const array A = []; + + /** @var array */ + public const array B = []; + + public const Bar C = ConstantWithObjectInstanceForNativeType; + + /** @var Bar */ + public const Bar D = ConstantWithObjectInstanceForNativeType; +} diff --git a/tests/PHPStan/Rules/Constants/data/defined-scope-merge.php b/tests/PHPStan/Rules/Constants/data/defined-scope-merge.php new file mode 100644 index 0000000000..9209ddf238 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/defined-scope-merge.php @@ -0,0 +1,11 @@ += 8.3 + +namespace NativeTypedClassConstant; + +class Foo +{ + + public const TEST = 1; + + public const int LOREM = 2; + +} diff --git a/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php b/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php new file mode 100644 index 0000000000..9b8afbbcad --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php @@ -0,0 +1,50 @@ += 8.3 + +namespace OverridingConstantNativeTypes; + +class Foo +{ + + public const int A = 1; + public const int|string B = 1; + public const int|string C = 1; + public const int D = 1; + +} + +class Bar extends Foo +{ + + public const int A = 2; + public const int|string B = 'foo'; + public const int C = 1; + public const int|string D = 1; + +} + +class Lorem +{ + + public const A = 1; + public const int B = 1; + +} + +class Ipsum extends Lorem +{ + + public const int A = 1; + public const B = 1; + +} + +class PharChild extends \Phar +{ + + const BZ2 = 'foo'; // error + + const int GZ = 1; // OK + + const int|string NONE = 1; // error + +} diff --git a/tests/PHPStan/Rules/Constants/data/remembered-constructor-scope.php b/tests/PHPStan/Rules/Constants/data/remembered-constructor-scope.php new file mode 100644 index 0000000000..7be2b0bf7b --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/remembered-constructor-scope.php @@ -0,0 +1,100 @@ += 8.3 + +namespace ValueAssignedToClassConstantNativeType; + +class Foo +{ + + public const int FOO = 1; + + public const int BAR = 'bar'; + +} + +class Bar +{ + + /** @var int<1, max> */ + public const int FOO = 1; + + /** @var int<1, max> */ + public const int BAR = 0; + +} + +class Floats +{ + + public const float FOO = 1; + + public const int BAR = 1.0; + + /** @var float */ + public const BAZ = 1; + + /** @var float */ + public const float LOREM = 1; + +} diff --git a/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant.php b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant.php new file mode 100644 index 0000000000..bdcf81838c --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant.php @@ -0,0 +1,49 @@ + */ + const DOLOR = 1; + +} + +class Bar extends Foo +{ + + const BAR = 2; + + const BAZ = 2; + +} + +class Baz +{ + + /** @var string */ + private const BAZ = 'foo'; + +} + +class Lorem extends Baz +{ + + private const BAZ = 1; + +} diff --git a/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php b/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php index b88771ce6f..40711affec 100644 --- a/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php +++ b/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php @@ -44,6 +44,14 @@ public function test(): void 'Instantiating DateTime with 2020-04-31 produces a warning: The parsed date was invalid', 20, ],*/ + [ + 'Instantiating DateTime with 2020.11.17 produces an error: Double time specification', + 22, + ], + [ + 'Instantiating DateTimeImmutable with 2020.11.17 produces an error: Double time specification', + 23, + ], ], ); } diff --git a/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..ac630c4880 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,37 @@ + + */ +class CallToConstructorStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToConstructorStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-constructor-without-impure-points.php'], [ + [ + 'Call to new CallToConstructorWithoutImpurePoints\Foo() on a separate line has no effect.', + 15, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureNewCollector($this->createReflectionProvider()), + new ConstructorWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..3c5fe7a2b9 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,37 @@ + + */ +class CallToFunctionStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToFunctionStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-function-without-impure-points.php'], [ + [ + 'Call to function CallToFunctionWithoutImpurePoints\myFunc() on a separate line has no effect.', + 29, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureFuncCallCollector($this->createReflectionProvider()), + new FunctionWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..923c2b7883 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,107 @@ + + */ +class CallToMethodStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToMethodStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-method-without-impure-points.php'], [ + [ + 'Call to method CallToMethodWithoutImpurePoints\finalX::myFunc() on a separate line has no effect.', + 7, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\finalX::myFunc() on a separate line has no effect.', + 8, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\finalX::myFunc() on a separate line has no effect.', + 21, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\finalX::myFunc() on a separate line has no effect.', + 27, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\foo::finalFunc() on a separate line has no effect.', + 30, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 35, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFinalBaseFunc() on a separate line has no effect.', + 36, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 39, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\finalSubSubY::mySubSubFunc() on a separate line has no effect.', + 40, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFinalBaseFunc() on a separate line has no effect.', + 41, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFinalBaseFunc() on a separate line has no effect.', + 61, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\AbstractFoo::myFunc() on a separate line has no effect.', + 139, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\CallsPrivateMethodWithoutImpurePoints::doBar() on a separate line has no effect.', + 147, + ], + ]); + } + + public function testBug11011(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-11011.php'], [ + [ + 'Call to method Bug11011\AnotherPureImpl::doFoo() on a separate line has no effect.', + 32, + ], + ]); + } + + public function testBug12379(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-12379.php'], []); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureMethodCallCollector(), + new MethodWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..74258d25a1 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,69 @@ + + */ +class CallToStaticMethodStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToStaticMethodStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-static-method-without-impure-points.php'], [ + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 6, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 7, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 16, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 18, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 20, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\SubSubY::mySubSubFunc() on a separate line has no effect.', + 21, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 48, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 53, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 58, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureStaticCallCollector(), + new MethodWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php index 9e58ac9288..2e082297d1 100644 --- a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php @@ -2,10 +2,10 @@ namespace PHPStan\Rules\DeadCode; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,7 +15,7 @@ class NoopRuleTest extends RuleTestCase protected function getRule(): Rule { - return new NoopRule(new Standard()); + return new NoopRule(new ExprPrinter(new Printer())); } public function testRule(): void @@ -77,15 +77,42 @@ public function testRule(): void 'Expression "(string) 1" on a separate line does not do anything.', 30, ], + [ + 'Unused result of "xor" operator.', + 32, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of "and" operator.', + 35, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of "or" operator.', + 38, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of ternary operator.', + 40, + ], + [ + 'Unused result of ternary operator.', + 41, + ], + [ + 'Unused result of "||" operator.', + 46, + ], + [ + 'Unused result of "&&" operator.', + 49, + ], ]); } public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-property-fetch-noop.php'], [ [ 'Expression "$ref?->name" on a separate line does not do anything.', @@ -94,4 +121,36 @@ public function testNullsafe(): void ]); } + public function testRuleImpurePoints(): void + { + $this->analyse([__DIR__ . '/data/noop-impure-points.php'], [ + [ + 'Unused result of "&&" operator.', + 12, + ], + [ + 'Expression "$b()" on a separate line does not do anything.', + 59, + ], + [ + 'Expression "new class…" on a separate line does not do anything.', + 98, + ], + [ + 'Expression "new class…" on a separate line does not do anything.', + 104, + ], + ]); + } + + public function testBug11001(): void + { + $this->analyse([__DIR__ . '/data/bug-11001.php'], []); + } + + public function testBug11361(): void + { + $this->analyse([__DIR__ . '/data/bug-11361.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementNextStatementsRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementNextStatementsRuleTest.php new file mode 100644 index 0000000000..36aa85b6bb --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementNextStatementsRuleTest.php @@ -0,0 +1,94 @@ + + */ +class UnreachableStatementNextStatementsRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new class implements Rule { + + public function getNodeType(): string + { + return UnreachableStatementNode::class; + } + + /** + * @param UnreachableStatementNode $node + */ + public function processNode(Node $node, Scope $scope): array + { + $errors = [ + RuleErrorBuilder::message('First unreachable') + ->identifier('tests.nextUnreachableStatements') + ->build(), + ]; + + foreach ($node->getNextStatements() as $nextStatement) { + $errors[] = RuleErrorBuilder::message('Another unreachable') + ->line($nextStatement->getStartLine()) + ->identifier('tests.nextUnreachableStatements') + ->build(); + } + + return $errors; + } + + }; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/multiple_unreachable.php'], [ + [ + 'First unreachable', + 14, + ], + [ + 'Another unreachable', + 15, + ], + [ + 'Another unreachable', + 17, + ], + [ + 'Another unreachable', + 22, + ], + ]); + } + + public function testRuleTopLevel(): void + { + $this->analyse([__DIR__ . '/data/multiple_unreachable_top_level.php'], [ + [ + 'First unreachable', + 9, + ], + [ + 'Another unreachable', + 10, + ], + [ + 'Another unreachable', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 32599a1c70..ec97b0481a 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -41,7 +41,19 @@ public function testRule(): void ], [ 'Unreachable statement - code above always terminates.', - 71, + 44, + ], + [ + 'Unreachable statement - code above always terminates.', + 58, + ], + [ + 'Unreachable statement - code above always terminates.', + 93, + ], + [ + 'Unreachable statement - code above always terminates.', + 157, ], ]); } @@ -120,4 +132,113 @@ public function testBug4370(): void $this->analyse([__DIR__ . '/data/bug-4370.php'], []); } + public function testBug7188(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7188.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 22, + ], + ]); + } + + public function testBug8620(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8620.php'], []); + } + + public function testBug4002(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002.php'], []); + } + + public function testBug4002Two(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002-2.php'], []); + } + + public function testBug4002Three(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002-3.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 13, + ], + ]); + } + + public function testBug4002Four(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002-4.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 9, + ], + ]); + } + + public function testBug4002Class(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002_class.php'], []); + } + + public function testBug4002Interface(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002_interface.php'], []); + } + + public function testBug4002Trait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002_trait.php'], []); + } + + public function testBug8319(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8319.php'], []); + } + + public function testBug8966(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8966.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 8, + ], + ]); + } + + public function testBug11179(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-11179.php'], []); + } + + public function testBug11992(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-11992.php'], []); + } + + public function testMultipleUnreachable(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/multiple_unreachable.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php index 4f5d8e8d46..9ef9924063 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\DeadCode; -use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtension; use PHPStan\Rules\Constants\DirectAlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\Rule; @@ -22,7 +22,7 @@ protected function getRule(): Rule new DirectAlwaysUsedClassConstantsExtensionProvider([ new class() implements AlwaysUsedClassConstantsExtension { - public function isAlwaysUsed(ConstantReflection $constant): bool + public function isAlwaysUsed(ClassConstantReflection $constant): bool { return $constant->getDeclaringClass()->getName() === TestExtension::class && $constant->getName() === 'USED'; @@ -51,10 +51,6 @@ public function testRule(): void 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'], []); } @@ -78,4 +74,42 @@ public function testBug6758(): void $this->analyse([__DIR__ . '/data/bug-6758.php'], []); } + public function testBug8204(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-8204.php'], []); + } + + public function testBug9005(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9005.php'], []); + } + + public function testBug9765(): void + { + $this->analyse([__DIR__ . '/data/bug-9765.php'], []); + } + + public function testDynamicConstantFetch(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/unused-private-constant-dynamic-fetch.php'], [ + [ + 'Constant UnusedPrivateConstantDynamicFetch\Baz::FOO is unused.', + 32, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php index e821d28851..ca57318a59 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -2,6 +2,9 @@ namespace PHPStan\Rules\DeadCode; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Methods\AlwaysUsedMethodExtension; +use PHPStan\Rules\Methods\DirectAlwaysUsedMethodExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -14,7 +17,19 @@ class UnusedPrivateMethodRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedPrivateMethodRule(); + return new UnusedPrivateMethodRule( + new DirectAlwaysUsedMethodExtensionProvider([ + new class() implements AlwaysUsedMethodExtension { + + public function isAlwaysUsed(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->is('UnusedPrivateMethod\IgnoredByExtension') + && $methodReflection->getName() === 'foo'; + } + + }, + ]), + ); } public function testRule(): void @@ -38,7 +53,11 @@ public function testRule(): void ], [ 'Method UnusedPrivateMethod\Lorem::doBaz() is unused.', - 97, + 99, + ], + [ + 'Method UnusedPrivateMethod\IgnoredByExtension::bar() is unused.', + 181, ], ]); } @@ -50,16 +69,12 @@ public function testBug3630(): void public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-unused-private-method.php'], []); } public function testFirstClassCallable(): void { - if (PHP_VERSION_ID < 80100 && !self::$useStaticReflectionProvider) { + if (PHP_VERSION_ID < 80100) { $this->markTestSkipped('Test requires PHP 8.1.'); } @@ -80,4 +95,52 @@ public function testEnums(): void ]); } + public function testBug7389(): void + { + $this->analyse([__DIR__ . '/data/bug-7389.php'], [ + [ + 'Method Bug7389\HelloWorld::getTest() is unused.', + 11, + ], + [ + 'Method Bug7389\HelloWorld::getTest1() is unused.', + 23, + ], + ]); + } + + public function testBug8346(): void + { + $this->analyse([__DIR__ . '/data/bug-8346.php'], []); + } + + public function testFalsePositiveWithTraitUse(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/unused-method-false-positive-with-trait.php'], []); + } + + public function testBug6039(): void + { + $this->analyse([__DIR__ . '/data/bug-6039.php'], []); + } + + public function testBug9765(): void + { + $this->analyse([__DIR__ . '/data/bug-9765.php'], []); + } + + public function testBug11802(): void + { + $this->analyse([__DIR__ . '/data/bug-11802b.php'], [ + [ + 'Method Bug11802b\HelloWorld::doBar() is unused.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php index 792e014d60..c83a84d425 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -22,6 +22,8 @@ class UnusedPrivatePropertyRuleTest extends RuleTestCase /** @var string[] */ private array $alwaysReadTags; + private bool $checkUninitializedProperties = false; + protected function getRule(): Rule { return new UnusedPrivatePropertyRule( @@ -55,18 +57,15 @@ public function isInitialized(PropertyReflection $property, string $propertyName ]), $this->alwaysWrittenTags, $this->alwaysReadTags, - true, + $this->checkUninitializedProperties, ); } public function testRule(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4 or static reflection.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; + $this->checkUninitializedProperties = true; $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; @@ -189,9 +188,6 @@ public function testTrait(): void public function testBug3636(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; @@ -206,10 +202,6 @@ public function testBug3636(): void public function testPromotedProperties(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = ['@get']; $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; @@ -224,15 +216,18 @@ public function testPromotedProperties(): void public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; $this->analyse([__DIR__ . '/data/nullsafe-unused-private-property.php'], []); } + public function testBug3654(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-3654.php'], []); + } + public function testBug5935(): void { $this->alwaysWrittenTags = []; @@ -242,22 +237,185 @@ public function testBug5935(): void public function testBug5337(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->checkUninitializedProperties = true; + $this->analyse([__DIR__ . '/data/bug-5337.php'], []); + } + + public function testBug5971(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-5971.php'], []); + } + + public function testBug6107(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-6107.php'], []); + } + + public function testBug8204(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-8204.php'], []); + } + + public function testBug8850(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-8850.php'], []); + } + + public function testBug9409(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-9409.php'], []); + } + + public function testBug9765(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-9765.php'], []); + } + + public function testBug10059(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-10059.php'], []); + } + + public function testBug10628(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-10628.php'], []); + } + + public function testBug8781(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-8781.php'], []); + } + + public function testBug9361(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-9361.php'], []); + } + + public function testBug7251(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-7251.php'], []); + } + + public function testBug11802(): void + { + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-11802.php'], [ + [ + 'Property Bug11802\HelloWorld::$isFinal is never read, only written.', + 8, + $tip, + ], + ]); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); } + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; + $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; - $this->analyse([__DIR__ . '/data/bug-5337.php'], [ + + $this->analyse([__DIR__ . '/data/property-hooks-unused-property.php'], [ + [ + 'Property PropertyHooksUnusedProperty\FooUnused::$a is unused.', + 32, + $tip, + ], + [ + 'Property PropertyHooksUnusedProperty\FooOnlyRead::$a is never written, only read.', + 46, + $tip, + ], + [ + 'Property PropertyHooksUnusedProperty\FooOnlyWritten::$a is never read, only written.', + 65, + $tip, + ], + [ + 'Property PropertyHooksUnusedProperty\ReadInAnotherPropertyHook2::$bar is never written, only read.', + 95, + $tip, + ], + [ + 'Property PropertyHooksUnusedProperty\WrittenInAnotherPropertyHook::$bar is never read, only written.', + 105, + $tip, + ], + ]); + } + + public function testBug12621(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + + $this->analyse([__DIR__ . '/data/bug-12621.php'], []); + } + + public function testBug12702(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + + $this->analyse([__DIR__ . '/data/bug-12702.php'], [ [ - 'Property Bug5337\Clazz::$prefix is never read, only written.', - 7, - 'See: https://phpstan.org/developing-extensions/always-read-written-properties', + 'Readable property Bug12702\Foo2::$i is never read.', + 43, ], [ - 'Property Bug5337\Foo::$field is unused.', - 20, - 'See: https://phpstan.org/developing-extensions/always-read-written-properties', + 'Writable property Bug12702\Bar2::$i is never written.', + 54, ], ]); } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-10059.php b/tests/PHPStan/Rules/DeadCode/data/bug-10059.php new file mode 100644 index 0000000000..fdc6335e27 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-10059.php @@ -0,0 +1,20 @@ += 8.1 + +namespace Bug10059; + +use DateTimeImmutable; + +class Foo +{ + public function __construct( + private readonly DateTimeImmutable $startDateTime + ) { + } + + public function bar(): void + { + declare(ticks=5) { + echo $this->startDateTime->format('Y-m-d H:i:s.u'), PHP_EOL; + } + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-10628.php b/tests/PHPStan/Rules/DeadCode/data/bug-10628.php new file mode 100644 index 0000000000..3fee75ba1e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-10628.php @@ -0,0 +1,32 @@ += 8.0 + +namespace Bug10628; + +use stdClass; + +interface Bar +{ + + public function bazName(): string; + +} + +final class Foo +{ + public function __construct( + private Bar $bar, + ) { + } + + public function __invoke(): stdClass + { + return $this->getMixed()->get( + name: $this->bar->bazName(), + ); + } + + public function getMixed(): mixed + { + + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11001.php b/tests/PHPStan/Rules/DeadCode/data/bug-11001.php new file mode 100644 index 0000000000..4b39b689c1 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11001.php @@ -0,0 +1,38 @@ +foo); + })(); + } + +} + +class Foo2 +{ + public function test(): void + { + \Closure::bind(fn () => $this->status = 5, $this)(); + } + + public function test2(): void + { + \Closure::bind(function () { + $this->status = 5; + }, $this)(); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11011.php b/tests/PHPStan/Rules/DeadCode/data/bug-11011.php new file mode 100644 index 0000000000..af820bde68 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11011.php @@ -0,0 +1,35 @@ += 8.0 + +namespace Bug11011; + +final class ImpureImpl { + /** @phpstan-impure */ + public function doFoo() { + echo "yes"; + $_SESSION['ab'] = 1; + } +} + +final class PureImpl { + public function doFoo(): bool { + return true; + } +} + +final class AnotherPureImpl { + public function doFoo(): bool { + return true; + } +} + +class User { + function doBar(PureImpl|ImpureImpl $f): bool { + $f->doFoo(); + return true; + } + + function doBar2(PureImpl|AnotherPureImpl $f): bool { + $f->doFoo(); + return true; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11179.php b/tests/PHPStan/Rules/DeadCode/data/bug-11179.php new file mode 100644 index 0000000000..aba6adb265 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11179.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug11802; + +class HelloWorld +{ + public function __construct( + private bool $isFinal, + private bool $used + ) + { + } + + public function doFoo(HelloWorld $x, $y): void + { + if ($y !== 'isFinal') { + $s = $x->{$y}; + } + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11802b.php b/tests/PHPStan/Rules/DeadCode/data/bug-11802b.php new file mode 100644 index 0000000000..68bc97f2ac --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11802b.php @@ -0,0 +1,19 @@ += 8.0 + +namespace Bug11802b; + +class HelloWorld +{ + public function __construct( + ) {} + + private function doBar():void {} + + private function doFooBar():void {} + + public function doFoo(HelloWorld $x, $y): void { + if ($y !== 'doBar') { + $s = $x->$y(); + } + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11992.php b/tests/PHPStan/Rules/DeadCode/data/bug-11992.php new file mode 100644 index 0000000000..e6c35c967d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11992.php @@ -0,0 +1,37 @@ +valid(); + $it->next() + ) { + printf("name: %s\n", $it->getFilename()); + } + printf("done\n"); +} + +exampleA(); +exampleB(); +exampleC(); diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-12379.php b/tests/PHPStan/Rules/DeadCode/data/bug-12379.php new file mode 100644 index 0000000000..f8dc4ede85 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-12379.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug12379; + +class HelloWorld +{ + use myTrait{ + myTrait::__construct as private __myTraitConstruct; + } + + public function __construct( + int $entityManager + ){ + $this->__myTraitConstruct($entityManager); + } +} + +trait myTrait{ + public function __construct( + private readonly int $entityManager + ){} +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-12621.php b/tests/PHPStan/Rules/DeadCode/data/bug-12621.php new file mode 100644 index 0000000000..bcb8ff1958 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-12621.php @@ -0,0 +1,21 @@ += 8.4 + +declare(strict_types=1); + +namespace Bug12621; + +final class Test +{ + private string $a { + get => $this->a ??= $this->b; + } + + public function __construct( + private readonly string $b + ) {} + + public function test(): string + { + return $this->a; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-12702.php b/tests/PHPStan/Rules/DeadCode/data/bug-12702.php new file mode 100644 index 0000000000..1b5896c788 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-12702.php @@ -0,0 +1,61 @@ += 8.4 + +namespace Bug12702; + +class Foo +{ + /** + * @var string[] + */ + public array $x = []; + private ?string $i { get => $this->x[$this->k] ?? null; } + private int $k = 0; + + public function x(): void { + echo $this->i; + } +} + +class Bar +{ + /** + * @var string[] + */ + public array $x = []; + private ?string $i { + set { + $this->x[$this->k] = $value; + } + } + private int $k = 0; + + public function x(): void { + $this->i = 'foo'; + } +} + +class Foo2 +{ + /** + * @var string[] + */ + public array $x = []; + private ?string $i { get => $this->x[$this->k] ?? null; } + private int $k = 0; + +} + +class Bar2 +{ + /** + * @var string[] + */ + public array $x = []; + private ?string $i { + set { + $this->x[$this->k] = $value; + } + } + private int $k = 0; + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-3636.php b/tests/PHPStan/Rules/DeadCode/data/bug-3636.php index dd40b218ed..9e5ba43b0a 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-3636.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-3636.php @@ -1,4 +1,4 @@ -= 7.4 +id = $id; + } + + public function jsonSerialize(): array + { + return \get_object_vars($this); + } +} + +class Bar implements \JsonSerializable +{ + + /** + * @var int + */ + private $id; + + public function __construct(int $id) + { + $this->id = $id; + } + + public function jsonSerialize(): void + { + \array_walk($this, static function ($key, $value) { + }); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php b/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php new file mode 100644 index 0000000000..f16716af43 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php @@ -0,0 +1,11 @@ += 7.4 +test = []; + } + + public function read(): bool + { + return empty($this->test); + } +} + +class TestIsset +{ + private ?string $test; + + public function write(string $string): void + { + $this->test = $string; + } + + public function read(): bool + { + return isset($this->test); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-6039.php b/tests/PHPStan/Rules/DeadCode/data/bug-6039.php new file mode 100644 index 0000000000..ab20c61a30 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-6039.php @@ -0,0 +1,31 @@ += 8.0 + +namespace Bug6039; + +trait Foo +{ + public function showFoo(): void + { + echo 'foo' . self::postFoo(); + } + + abstract private static function postFoo(): string; +} + +class UseFoo +{ + use Foo { + showFoo as showFooTrait; + } + + public function showFoo(): void + { + echo 'fooz'; + $this->showFooTrait(); + } + + private static function postFoo(): string + { + return 'postFoo'; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-6107.php b/tests/PHPStan/Rules/DeadCode/data/bug-6107.php new file mode 100644 index 0000000000..ada0eb2c6e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-6107.php @@ -0,0 +1,18 @@ +item = $item; + } + + public function handle(): void + { + $value = $this->item->value ?? 'custom value'; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-7188.php b/tests/PHPStan/Rules/DeadCode/data/bug-7188.php new file mode 100644 index 0000000000..dd607b23c6 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-7188.php @@ -0,0 +1,23 @@ +setToOne($this->bar); + } + + private function setToOne(&$var) + { + $var = 1; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-7389.php b/tests/PHPStan/Rules/DeadCode/data/bug-7389.php new file mode 100644 index 0000000000..ff778fcbe1 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-7389.php @@ -0,0 +1,27 @@ + + */ + private function getTest(string $test): array + { + return [ + '', + '', + ]; + } + + /** + * @param string $test test + * @return string + */ + private function getTest1(string $test): string + { + return 'test1'; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8204.php b/tests/PHPStan/Rules/DeadCode/data/bug-8204.php new file mode 100644 index 0000000000..98391086fc --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8204.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug8204; + +function f(string ...$parameters) : void { +} + +class HelloWorld +{ + private const FOO = 'foo'; + private string $bar = 'bar'; + + public function foobar(): void + { + f( + foo: self::FOO, + bar: $this->bar, + ); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8319.php b/tests/PHPStan/Rules/DeadCode/data/bug-8319.php new file mode 100644 index 0000000000..f8e767d47f --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8319.php @@ -0,0 +1,11 @@ +sayhello('world'); + } + + private function sayHello(string $name): string + { + return 'Hello ' . $name; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8620.php b/tests/PHPStan/Rules/DeadCode/data/bug-8620.php new file mode 100644 index 0000000000..44bc78cd45 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8620.php @@ -0,0 +1,33 @@ + + */ + private $stdOut; + + /** + * @var string + */ + private $command; + + /** + * @param string $command + */ + public function __construct($command) + { + $this->command = $command; + } + + public function run(): void + { + exec($this->command, $this->stdOut); + } + + /** + * @return array + */ + public function wait(): array + { + return $this->stdOut; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8850.php b/tests/PHPStan/Rules/DeadCode/data/bug-8850.php new file mode 100644 index 0000000000..96ab4f318a --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8850.php @@ -0,0 +1,50 @@ +security = $security; + } +} + +trait QueryBuilderHelperTrait +{ + use OrganisationExtensionHelperTrait; +} + +trait OrganisationExtensionHelperTrait +{ + use UserHelperTrait; + + public function getOrganisationIds(): void + { + $user = $this->getUser(); + } +} + +trait UserHelperTrait +{ + public function getUser(): string + { + $user = $this->security->getUser(); + + return 'foo'; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8966.php b/tests/PHPStan/Rules/DeadCode/data/bug-8966.php new file mode 100644 index 0000000000..a50e2b1c2e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8966.php @@ -0,0 +1,8 @@ += 8.1 + +namespace Bug9005; + +enum Test: string +{ + private const PREFIX = 'my-stuff-'; + + case TESTING = self::PREFIX . 'test'; + + case TESTING2 = self::PREFIX . 'test2'; +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-9361.php b/tests/PHPStan/Rules/DeadCode/data/bug-9361.php new file mode 100644 index 0000000000..c7a5e26247 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-9361.php @@ -0,0 +1,58 @@ +Bound = &$var; + + return $this; + } + + /** + * @param mixed $value + * @return $this + */ + public function setValue($value) + { + if ($this->Bound !== $value) { + $this->Bound = $value; + } + + return $this; + } +} + +class Command +{ + /** + * @var mixed + */ + private $Value; + + /** + * @return Option[] + */ + public function getOptions() + { + return [ + (new Option())->bind($this->Value), + ]; + } + + public function run(): void + { + $value = $this->Value; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-9409.php b/tests/PHPStan/Rules/DeadCode/data/bug-9409.php new file mode 100644 index 0000000000..849ef0af2d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-9409.php @@ -0,0 +1,22 @@ + $tempDir */ + private static $tempDir = []; + + public function getTempDir(string $name): ?string + { + if (isset($this::$tempDir[$name])) { + return $this::$tempDir[$name]; + } + + $path = ''; + + $this::$tempDir[$name] = $path; + + return $path; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-9765.php b/tests/PHPStan/Rules/DeadCode/data/bug-9765.php new file mode 100644 index 0000000000..10fbbcf45d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-9765.php @@ -0,0 +1,71 @@ +add($arg); + }; + } + + public function do(): void + { + $c = self::runner(); + print $c->bindTo($this)(5); + } + + private function add(int $a): int + { + return $a + 1; + } + +} + +class HelloWorld2 +{ + + /** @var int */ + private $foo; + + public static function runner(): \Closure + { + return function (int $arg) { + if ($arg > 0) { + $this->foo = $arg; + } else { + echo $this->foo; + } + }; + } + + public function do(): void + { + $c = self::runner(); + print $c->bindTo($this)(5); + } + +} + +class HelloWorld3 +{ + + private const FOO = 1; + + public static function runner(): \Closure + { + return function (int $arg) { + echo $this::FOO; + }; + } + + public function do(): void + { + $c = self::runner(); + print $c->bindTo($this)(5); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points.php new file mode 100644 index 0000000000..10b9622a2d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points.php @@ -0,0 +1,16 @@ +myFunc(); + $x->myFUNC(); + $x->throwingFUNC(); + $x->throwingFunc(); + $x->funcWithRef(); + $x->impureFunc(); + $x->callingImpureFunc(); + + $a = $x->myFunc(); + + $xy = new y(); + if (rand(0,1)) { + $xy = new finalX(); + } + $xy->myFunc(); + + $xy = new Y(); // case-insensitive class name + if (rand(0,1)) { + $xy = new finalX(); + } + $xy->myFunc(); + + $foo = new Foo(); + $foo->finalFunc(); + $foo->finalThrowingFunc(); + $foo->throwingFunc(); + + $subY = new subY(); + $subY->myFunc(); + $subY->myFinalBaseFunc(); + + $subSubY = new finalSubSubY(); + $subSubY->myFunc(); + $subSubY->mySubSubFunc(); + $subSubY->myFinalBaseFunc(); +}; + +function (y $xy, finalX $finalX): void { + if (rand(0,1)) { + $xy = $finalX; + } + $xy->myFunc(); +}; + +function (Y $xy, finalX $finalX): void { + // case-insensitive class name + if (rand(0,1)) { + $xy = $finalX; + } + $xy->myFunc(); +}; + +function (subY $subY): void { + $subY->myFunc(); + $subY->myFinalBaseFunc(); +}; + +class y +{ + function myFunc() + { + } + final function myFinalBaseFunc() + { + } +} + +class subY extends y { +} + +final class finalSubSubY extends subY { + function mySubSubFunc() + { + } +} + +final class finalX { + function myFunc() + { + } + + function throwingFunc() + { + throw new \Exception(); + } + + function funcWithRef(&$a) + { + } + + /** @phpstan-impure */ + function impureFunc() + { + } + + function callingImpureFunc() + { + $this->impureFunc(); + } +} + +class foo +{ + final function finalFunc() + { + } + + final function finalThrowingFunc() + { + throw new \Exception(); + } + + function throwingFunc() + { + throw new \Exception(); + } +} + +abstract class AbstractFoo +{ + + function myFunc() + { + } + +} +final class FinalFoo extends AbstractFoo +{ + +} + +function (FinalFoo $foo): void { + $foo->myFunc(); +}; + +class CallsPrivateMethodWithoutImpurePoints +{ + + public function doFoo(): void + { + $this->doBar(); + } + + private function doBar(): int + { + return 1; + } + +} + +class TestIgnoring +{ + + public function doFoo(): void + { + $this->doBar(); // @phpstan-ignore method.resultUnused + } + + private function doBar(): int + { + return 1; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php new file mode 100644 index 0000000000..3dfcff73d4 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php @@ -0,0 +1,122 @@ +i = 1; + } +} + +class ChildOfParentWithConstructor extends ParentWithConstructor +{ + public function __construct() + { + parent::__construct(); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/multiple_unreachable.php b/tests/PHPStan/Rules/DeadCode/data/multiple_unreachable.php new file mode 100644 index 0000000000..0e9ab15119 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/multiple_unreachable.php @@ -0,0 +1,23 @@ +doBar(); + $b && $this->doBaz(); + $b && $this->doLorem(); + } + + /** + * @phpstan-pure + */ + public function doBar(): bool + { + return true; + } + + /** + * @phpstan-impure + */ + public function doBaz(): bool + { + return true; + } + + public function doLorem(): bool + { + return true; + } + + public function doExit(): void + { + exit(1); + } + + public function doAssign(bool $b): void + { + $b ? $a = 1 : ''; + $b ? $this->foo = 1 : ''; + } + + public function doClosures(int $i): void + { + $a = static function () { + echo '1'; + }; + $a(); + + $b = static function () { + return 1 + 1; + }; + $b(); + + $ref = 1; + $c = static function () use (&$ref) { + $ref++; + }; + $c(); + + $d = function () { + self::$foo = 1; + }; + $d(); + + $e = function () { + self::$staticProp = 1; + }; + $e(); + + $i(); + } + + public function doFunctionWithByRef(bool $b, array $a): void + { + $func = $b ? 'array_unshift' : 'array_push'; + $func($a, 1); + } + + public function anonymousClassWithSideEffect(): void + { + new class () { + public function __construct() + { + echo '1'; + } + }; + } + + public function anonymousClassWithoutConstructor(): void + { + new class () { + }; + } + + public function anonymousClassWithPureConstructor(): void + { + new class () { + + /** @var int */ + private $i; + + public function __construct() + { + $this->i = 1; + } + + }; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/noop.php b/tests/PHPStan/Rules/DeadCode/data/noop.php index c025831720..5eb7d42d46 100644 --- a/tests/PHPStan/Rules/DeadCode/data/noop.php +++ b/tests/PHPStan/Rules/DeadCode/data/noop.php @@ -2,7 +2,7 @@ namespace DeadCodeNoop; -function (stdClass $foo) { +function (stdClass $foo, bool $a, bool $b) { $foo->foo(); $arr = []; @@ -28,4 +28,24 @@ function (stdClass $foo) { Foo::TEST; (string) 1; + + $r = $a xor $b; + + $s = $a and doFoo(); + $t = $a and $b; + + $s = $a or doFoo(); + $t = $a or $b; + + $a ? $b : $s; + $a ?: $b; + $a ? doFoo() : $s; + $a ? $b : doFoo(); + $a ? doFoo() : doBar(); + + $a || $b; + $a || doFoo(); + + $a && $b; + $a && doFoo(); }; diff --git a/tests/PHPStan/Rules/DeadCode/data/property-hooks-unused-property.php b/tests/PHPStan/Rules/DeadCode/data/property-hooks-unused-property.php new file mode 100644 index 0000000000..6b856eb132 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/property-hooks-unused-property.php @@ -0,0 +1,112 @@ += 8.4 + +namespace PropertyHooksUnusedProperty; + +class FooUsed +{ + + private int $a { + get { + return $this->a + 100; + } + set { + $this->a = $value - 100; + } + } + + public function setA(int $a): void + { + $this->a = $a; + } + + public function getA(): int + { + return $this->a; + } + +} + +class FooUnused +{ + + private int $a { + get { + return $this->a + 100; + } + set { + $this->a = $value - 100; + } + } + +} + +class FooOnlyRead +{ + + private int $a { + get { + return $this->a + 100; + } + set { + $this->a = $value - 100; + } + } + + public function getA(): int + { + return $this->a; + } + +} + +class FooOnlyWritten +{ + + private int $a { + get { + return $this->a + 100; + } + set { + $this->a = $value - 100; + } + } + + public function setA(int $a): void + { + $this->a = $a; + } + +} + +class ReadInAnotherPropertyHook +{ + public function __construct( + private readonly string $bar, + ) {} + + public string $virtualProperty { + get => $this->bar; + } +} + +class ReadInAnotherPropertyHook2 +{ + + private string $bar; + + public string $virtualProperty { + get => $this->bar; + } +} + +class WrittenInAnotherPropertyHook +{ + + private string $bar; + + public string $virtualProperty { + set { + $this->bar = 'test'; + } + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unreachable.php b/tests/PHPStan/Rules/DeadCode/data/unreachable.php index 69b6c3c8bc..48f39153f1 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unreachable.php +++ b/tests/PHPStan/Rules/DeadCode/data/unreachable.php @@ -36,6 +36,28 @@ public function doLorem() // this is why... } + public function doLorem2(string $foo) + { + return; + // this is why... + + echo $foo; + } + + public function doLorem3() + { + return; + ; + } + + public function doLorem4(string $foo) + { + return; + ; + + echo $foo; + } + /** * @param \stdClass[] $all */ @@ -114,3 +136,25 @@ private function somethingAboutDateTime(\DateTime $dt): bool } } + +class LastElseIf +{ + + /** + * @param 'a'|'b'|'c' $s + * @return void + */ + public function doFoo(string $s): void + { + if ($s === 'a') { + return; + } elseif ($s === 'b') { + return; + } elseif ($s === 'c') { + return; + } + + echo "test"; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-method-false-positive-with-trait.php b/tests/PHPStan/Rules/DeadCode/data/unused-method-false-positive-with-trait.php new file mode 100644 index 0000000000..24e2c3256d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-method-false-positive-with-trait.php @@ -0,0 +1,72 @@ += 8.1 + +namespace UnusedMethodFalsePositiveWithTrait; + +use ReflectionEnum; + +enum LocalOnlineReservationTime: string +{ + + use LabeledEnumTrait; + + case MORNING = 'morning'; + case AFTERNOON = 'afternoon'; + case EVENING = 'evening'; + + public static function getPeriodForHour(string $hour): self + { + $hour = self::hourToNumber($hour); + + throw new \Exception('Internal error'); + } + + private static function hourToNumber(string $hour): int + { + return (int) str_replace(':', '', $hour); + } + +} + +trait LabeledEnumTrait +{ + + use EnumTrait; + +} + +trait EnumTrait +{ + + /** + * @return list + */ + public static function getDeprecatedEnums(): array + { + static $cache = []; + if ($cache === []) { + $reflection = new ReflectionEnum(self::class); + $cases = $reflection->getCases(); + + foreach ($cases as $case) { + $docComment = $case->getDocComment(); + if ($docComment === false || !str_contains($docComment, '@deprecated')) { + continue; + } + $cache[] = self::from($case->getBackingValue()); + } + } + + return $cache; + } + + public function isDeprecated(): bool + { + return $this->equalsAny(self::getDeprecatedEnums()); + } + + public function equalsAny(...$that): bool + { + return in_array($this, $that, true); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-dynamic-fetch.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-dynamic-fetch.php new file mode 100644 index 0000000000..089efcb892 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-dynamic-fetch.php @@ -0,0 +1,39 @@ += 8.3 + +namespace UnusedPrivateConstantDynamicFetch; + +class Foo +{ + + private const FOO = 1; + + public function doFoo(string $s): void + { + echo self::{$s}; + } + +} + +class Bar +{ + + private const FOO = 1; + + public function doFoo(self $a, string $s): void + { + echo $a::{$s}; + } + +} + +class Baz +{ + + private const FOO = 1; + + public function doFoo(\stdClass $a, string $s): void + { + echo $a::{$s}; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php index be6f11bdd0..ce43e40a90 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php @@ -80,8 +80,10 @@ private function doFoo() public function doBar(string $name) { - $cb = [$this, $name]; - $cb(); + if ($name === 'doFoo') { + $cb = [$this, $name]; + $cb(); + } } } @@ -154,3 +156,29 @@ private function doLorem() } } + +class StaticMethod +{ + + private static function doFoo(): void + { + + } + + public function doTest(): void + { + $this::doFoo(); + } + +} + +class IgnoredByExtension +{ + private function foo(): void + { + } + + private function bar(): void + { + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php index e5b8841eb9..97aa923a37 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php @@ -1,4 +1,4 @@ -= 7.4 + + */ +class DebugScopeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DebugScopeRule($this->createReflectionProvider()); + } + + public function testRuleInPhpStanNamespace(): void + { + $this->analyse([__DIR__ . '/data/debug-scope.php'], [ + [ + 'Scope is empty', + 7, + ], + [ + implode("\n", [ + '$a (Yes): int', + '$b (Yes): int', + '$debug (Yes): bool', + 'native $a (Yes): int', + 'native $b (Yes): int', + 'native $debug (Yes): bool', + ]), + 10, + ], + [ + implode("\n", [ + '$a (Yes): int', + '$b (Yes): int', + '$debug (Yes): bool', + '$c (Maybe): 1', + 'native $a (Yes): int', + 'native $b (Yes): int', + 'native $debug (Yes): bool', + 'native $c (Maybe): 1', + 'condition about $c #1: if $debug=false then $c is *ERROR* (No)', + 'condition about $c #2: if $debug=true then $c is 1 (Yes)', + ]), + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Debug/DumpPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/Debug/DumpPhpDocTypeRuleTest.php new file mode 100644 index 0000000000..ed56b46bd7 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/DumpPhpDocTypeRuleTest.php @@ -0,0 +1,106 @@ + + */ +class DumpPhpDocTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DumpPhpDocTypeRule($this->createReflectionProvider(), new Printer()); + } + + public function testRuleSymbols(): void + { + $this->analyse([__DIR__ . '/data/dump-phpdoc-type.php'], [ + [ + "Dumped type: array{'': ''}", + 5, + ], + [ + "Dumped type: array{'\0': 'NUL', NUL: '\0'}", + 6, + ], + [ + "Dumped type: array{'\001': 'SOH', SOH: '\001'}", + 7, + ], + [ + "Dumped type: array{'\t': 'HT', HT: '\t'}", + 8, + ], + [ + "Dumped type: array{' ': 'SP', SP: ' '}", + 11, + ], + [ + "Dumped type: array{'foo ': 'ends with SP', ' foo': 'starts with SP', ' foo ': 'surrounded by SP', foo: 'no SP'}", + 12, + ], + [ + "Dumped type: array{'foo?': 'foo?'}", + 15, + ], + [ + "Dumped type: array{shallwedance: 'yes'}", + 16, + ], + [ + "Dumped type: array{'shallwedance?': 'yes'}", + 17, + ], + [ + "Dumped type: array{'Shall we dance': 'yes'}", + 18, + ], + [ + "Dumped type: array{'Shall we dance?': 'yes'}", + 19, + ], + [ + "Dumped type: array{shall_we_dance: 'yes'}", + 20, + ], + [ + "Dumped type: array{'shall_we_dance?': 'yes'}", + 21, + ], + [ + "Dumped type: array{shall-we-dance: 'yes'}", + 22, + ], + [ + "Dumped type: array{'shall-we-dance?': 'yes'}", + 23, + ], + [ + "Dumped type: array{'Let\'s go': 'Let\'s go'}", + 24, + ], + [ + "Dumped type: array{Foo\\Bar: 'Foo\\\\Bar'}", + 25, + ], + [ + "Dumped type: array{'3.14': 3.14}", + 26, + ], + [ + 'Dumped type: array{1: true, 0: false}', + 27, + ], + [ + 'Dumped type: T', + 36, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php index d611afb9ef..7a2ff3aa6a 100644 --- a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php +++ b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php @@ -23,10 +23,6 @@ public function testRuleInPhpStanNamespace(): void 'Dumped type: non-empty-array', 10, ], - [ - 'Missing argument for PHPStan\dumpType() function call.', - 11, - ], ]); } @@ -54,4 +50,56 @@ public function testRuleInUse(): void ]); } + public function testBug7803(): void + { + $this->analyse([__DIR__ . '/data/bug-7803.php'], [ + [ + 'Dumped type: int<4, max>', + 11, + ], + [ + 'Dumped type: non-empty-array', + 12, + ], + [ + 'Dumped type: int<4, max>', + 13, + ], + ]); + } + + public function testBug10377(): void + { + $this->analyse([__DIR__ . '/data/bug-10377.php'], [ + [ + 'Dumped type: array', + 22, + ], + [ + 'Dumped type: array', + 34, + ], + ]); + } + + public function testBug11179(): void + { + $this->analyse([__DIR__ . '/../DeadCode/data/bug-11179.php'], [ + [ + 'Dumped type: string', + 9, + ], + ]); + } + + public function testBug11179NoNamespace(): void + { + $this->analyse([__DIR__ . '/data/bug-11179-no-namespace.php'], [ + [ + 'Dumped type: string', + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php b/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php index bf1ea7711f..aec3ed1500 100644 --- a/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php +++ b/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php @@ -32,13 +32,17 @@ public function testRule(): void 37, ], [ - 'Expected variable certainty Yes, actual: No', + 'Expected variable $b certainty Yes, actual: No', 45, ], [ - 'Expected variable certainty Maybe, actual: No', + 'Expected variable $b certainty Maybe, actual: No', 46, ], + [ + "Expected offset 'firstName' certainty No, actual: Yes", + 65, + ], ]); } diff --git a/tests/PHPStan/Rules/Debug/data/bug-10377.php b/tests/PHPStan/Rules/Debug/data/bug-10377.php new file mode 100644 index 0000000000..b9850376e4 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/bug-10377.php @@ -0,0 +1,37 @@ + $additionalProperties + */ + public function addAdditionalProperties(array $additionalProperties): void + { + \PHPStan\dumpType($additionalProperties); + } +} + +trait RequestParameters +{ + + /** + * @param array $additionalProperties + */ + public function addAdditionalProperties(array $additionalProperties): void + { + \PHPStan\dumpType($additionalProperties); + } + +} diff --git a/tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php b/tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php new file mode 100644 index 0000000000..c7f0ad68f8 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php @@ -0,0 +1,13 @@ + $headers */ +function headers(array $headers): void +{ + if (count($headers) >= 4) { + dumpType(count($headers)); + dumpType($headers); + dumpType(count($headers)); + } +} diff --git a/tests/PHPStan/Rules/Debug/data/debug-scope.php b/tests/PHPStan/Rules/Debug/data/debug-scope.php new file mode 100644 index 0000000000..0e7b8663aa --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/debug-scope.php @@ -0,0 +1,17 @@ + '']); +dumpPhpDocType(["\0" => 'NUL', 'NUL' => "\0"]); +dumpPhpDocType(["\x01" => 'SOH', 'SOH' => "\x01"]); +dumpPhpDocType(["\t" => 'HT', 'HT' => "\t"]); + +// Space +dumpPhpDocType([" " => 'SP', 'SP' => ' ']); +dumpPhpDocType(["foo " => 'ends with SP', " foo" => 'starts with SP', " foo " => 'surrounded by SP', 'foo' => 'no SP']); + +// Punctuation marks +dumpPhpDocType(["foo?" => 'foo?']); +dumpPhpDocType(["shallwedance" => 'yes']); +dumpPhpDocType(["shallwedance?" => 'yes']); +dumpPhpDocType(["Shall we dance" => 'yes']); +dumpPhpDocType(["Shall we dance?" => 'yes']); +dumpPhpDocType(["shall_we_dance" => 'yes']); +dumpPhpDocType(["shall_we_dance?" => 'yes']); +dumpPhpDocType(["shall-we-dance" => 'yes']); +dumpPhpDocType(["shall-we-dance?" => 'yes']); +dumpPhpDocType(['Let\'s go' => "Let's go"]); +dumpPhpDocType(['Foo\\Bar' => 'Foo\\Bar']); +dumpPhpDocType(['3.14' => 3.14]); +dumpPhpDocType([true => true, false => false]); + +/** + * @template T + * @param T $value + * @return T + */ +function id($value) +{ + dumpPhpDocType($value); + + return $value; +} diff --git a/tests/PHPStan/Rules/Debug/data/file-asserts.php b/tests/PHPStan/Rules/Debug/data/file-asserts.php index abd8e54d07..10289de586 100644 --- a/tests/PHPStan/Rules/Debug/data/file-asserts.php +++ b/tests/PHPStan/Rules/Debug/data/file-asserts.php @@ -46,4 +46,23 @@ public function doBaz($a): void assertVariableCertainty(TrinaryLogic::createMaybe(), $b); } + /** + * @param array{firstName: string, lastName?: string, sub: array{other: string}} $context + */ + public function arrayOffset(array $context) : void + { + assertVariableCertainty(TrinaryLogic::createYes(), $context['firstName']); + assertVariableCertainty(TrinaryLogic::createYes(), $context['sub']); + assertVariableCertainty(TrinaryLogic::createYes(), $context['sub']['other']); + + assertVariableCertainty(TrinaryLogic::createMaybe(), $context['lastName']); + assertVariableCertainty(TrinaryLogic::createMaybe(), $context['nonexistent']['somethingElse']); + + assertVariableCertainty(TrinaryLogic::createNo(), $context['sub']['nonexistent']); + assertVariableCertainty(TrinaryLogic::createNo(), $context['email']); + + // Deliberate error: + assertVariableCertainty(TrinaryLogic::createNo(), $context['firstName']); + } + } diff --git a/tests/PHPStan/Rules/DirectRegistryTest.php b/tests/PHPStan/Rules/DirectRegistryTest.php new file mode 100644 index 0000000000..c1ac9fc109 --- /dev/null +++ b/tests/PHPStan/Rules/DirectRegistryTest.php @@ -0,0 +1,49 @@ +getRules(Node\Expr\FuncCall::class); + $this->assertCount(1, $rules); + $this->assertSame($rule, $rules[0]); + + $this->assertCount(0, $registry->getRules(Node\Expr\MethodCall::class)); + } + + public function testGetRulesWithTwoDifferentInstances(): void + { + $fooRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => [ + RuleErrorBuilder::message('Foo error')->identifier('tests.fooRule')->build(), + ]); + $barRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => [ + RuleErrorBuilder::message('Bar error')->identifier('tests.barRule')->build(), + ]); + + $registry = new DirectRegistry([ + $fooRule, + $barRule, + ]); + + $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(Node\Expr\MethodCall::class)); + } + +} diff --git a/tests/PHPStan/Rules/DummyCollector.php b/tests/PHPStan/Rules/DummyCollector.php new file mode 100644 index 0000000000..92f0e162a4 --- /dev/null +++ b/tests/PHPStan/Rules/DummyCollector.php @@ -0,0 +1,30 @@ + + */ +class DummyCollector implements Collector +{ + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->name instanceof Node\Identifier) { + return null; + } + + return $node->name->toString(); + } + +} diff --git a/tests/PHPStan/Rules/DummyCollectorRule.php b/tests/PHPStan/Rules/DummyCollectorRule.php new file mode 100644 index 0000000000..22b66a7809 --- /dev/null +++ b/tests/PHPStan/Rules/DummyCollectorRule.php @@ -0,0 +1,50 @@ + + */ +class DummyCollectorRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $data = $node->get(DummyCollector::class); + $methods = []; + foreach ($data as $methodNames) { + foreach ($methodNames as $methodName) { + if (!isset($methods[$methodName])) { + $methods[$methodName] = 0; + } + + $methods[$methodName]++; + } + } + + $parts = []; + foreach ($methods as $methodName => $count) { + $parts[] = sprintf('%d× %s', $count, $methodName); + } + + return [ + RuleErrorBuilder::message(implode(', ', $parts)) + ->file(__DIR__ . '/data/dummy-collector.php') + ->line(5) + ->identifier('tests.dummyCollector') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DummyCollectorRuleTest.php b/tests/PHPStan/Rules/DummyCollectorRuleTest.php new file mode 100644 index 0000000000..175a44d686 --- /dev/null +++ b/tests/PHPStan/Rules/DummyCollectorRuleTest.php @@ -0,0 +1,35 @@ + + */ +class DummyCollectorRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DummyCollectorRule(); + } + + protected function getCollectors(): array + { + return [ + new DummyCollector(), + ]; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/dummy-collector.php'], [ + [ + '2× doFoo, 2× doBar', + 5, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 1669ec0c2a..5de7d45b5d 100644 --- a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\EnumCases; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -27,9 +28,8 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80100), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -37,14 +37,20 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80100) { + if (PHP_VERSION_ID < 80100) { $this->markTestSkipped('Test requires PHP 8.1.'); } diff --git a/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php new file mode 100644 index 0000000000..33a117fd17 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php @@ -0,0 +1,83 @@ + + */ +class AbilityToDisableImplicitThrowsTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + [], + ), true); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/ability-to-disable-implicit-throws.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 17, + ], + ]); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/unthrown-exception-property-hooks-implicit-throws-disabled.php'], [ + [ + 'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.', + 23, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.', + 38, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.', + 53, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.', + 68, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.', + 74, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.', + 94, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.', + 115, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/data/ability-to-disable-implicit-throws.neon'], + ); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php new file mode 100644 index 0000000000..6b32128600 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php @@ -0,0 +1,46 @@ + + */ +class CatchWithUnthrownExceptionRuleStubsTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + [], + ), true); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/catch-with-unthrown-exception-stubs.php'], [ + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 44, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 55, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/catch-with-unthrown-exception-stubs.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 97540e5a4b..f9b8b96d7c 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -2,6 +2,8 @@ namespace PHPStan\Rules\Exceptions; +use Error; +use InvalidArgumentException; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -12,9 +14,20 @@ class CatchWithUnthrownExceptionRuleTest extends RuleTestCase { + private bool $reportUncheckedExceptionDeadCatch = true; + + /** @var string[] */ + private array $uncheckedExceptionClasses = []; + protected function getRule(): Rule { - return new CatchWithUnthrownExceptionRule(); + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + $this->uncheckedExceptionClasses, + [], + [], + ), $this->reportUncheckedExceptionDeadCatch); } public function testRule(): void @@ -108,6 +121,148 @@ public function testRule(): void 'Dead catch - Exception is never thrown in the try block.', 532, ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 555, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 629, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 647, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 741, + ], + [ + 'Dead catch - ArithmeticError is never thrown in the try block.', + 762, + ], + ]); + } + + public function testRuleWithoutReportingUncheckedException(): void + { + $this->reportUncheckedExceptionDeadCatch = false; + $this->uncheckedExceptionClasses = [ + InvalidArgumentException::class, + Error::class, + ]; + + $this->analyse([__DIR__ . '/data/unthrown-exception.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 12, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 21, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 38, + ], + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 49, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 71, + ], + [ + 'Dead catch - DomainException is never thrown in the try block.', + 117, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 119, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 171, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 180, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 224, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 312, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 344, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 375, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 380, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 398, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 432, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 437, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 485, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 532, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 555, + ], + ]); + } + + public function testMultiCatch(): void + { + $this->analyse([__DIR__ . '/data/unthrown-exception-multi.php'], [ + [ + 'Dead catch - LogicException is never thrown in the try block.', + 12, + ], + [ + 'Dead catch - OverflowException is never thrown in the try block.', + 36, + ], + [ + 'Dead catch - JsonException is never thrown in the try block.', + 58, + ], + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 120, + ], + [ + 'Dead catch - InvalidArgumentException is already caught above.', + 145, + ], + [ + 'Dead catch - InvalidArgumentException is already caught above.', + 156, + ], ]); } @@ -144,12 +299,17 @@ public function testBug4863(): void $this->analyse([__DIR__ . '/data/bug-4863.php'], []); } - public function testBug4814(): void + public function testBug5866(): void { - if (PHP_VERSION_ID < 70300) { - $this->markTestSkipped('Test requires PHP 7.3.'); + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); } + $this->analyse([__DIR__ . '/data/bug-5866.php'], []); + } + + public function testBug4814(): void + { $this->analyse([__DIR__ . '/data/bug-4814.php'], [ [ 'Dead catch - JsonException is never thrown in the try block.', @@ -158,12 +318,18 @@ public function testBug4814(): void ]); } - public function testThrowExpression(): void + public function testBug9066(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } + $this->analyse([__DIR__ . '/data/bug-9066.php'], [ + [ + 'Dead catch - OutOfBoundsException is never thrown in the try block.', + 28, + ], + ]); + } + public function testThrowExpression(): void + { $this->analyse([__DIR__ . '/data/dead-catch-throw-expr.php'], [ [ 'Dead catch - InvalidArgumentException is never thrown in the try block.', @@ -184,10 +350,6 @@ public function testDeadCatch(): void 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.', @@ -198,18 +360,18 @@ public function testFirstClassCallables(): void 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, + 63, ], [ 'Dead catch - Exception is never thrown in the try block.', - 77, + 78, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 85, ], ]); } @@ -228,6 +390,24 @@ public function testBug5903(): void ]); } + public function testBug6115(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-6115.php'], [ + [ + 'Dead catch - UnhandledMatchError is never thrown in the try block.', + 20, + ], + [ + 'Dead catch - UnhandledMatchError is never thrown in the try block.', + 28, + ], + ]); + } + public function testBug6262(): void { $this->analyse([__DIR__ . '/data/bug-6262.php'], []); @@ -235,10 +415,6 @@ public function testBug6262(): void 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.', @@ -267,4 +443,219 @@ public function testBug6256(): void ]); } + public function testBug6791(): void + { + $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 + { + $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, + ], + ]); + } + + public function testBug6349(): void + { + $this->analyse([__DIR__ . '/data/bug-6349.php'], [ + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 29, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 33, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 44, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 48, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 106, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 110, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 121, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 125, + ], + [ + // throw point not implemented yet, because there is no way to narrow float value by !== 0.0 + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 139, + ], + [ + // throw point not implemented yet, because there is no way to narrow float value by !== 0.0 + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 143, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 172, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 176, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 187, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 191, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 249, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 253, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 264, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 268, + ], + [ + // throw point not implemented yet, because there is no way to narrow float value by !== 0.0 + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 282, + ], + [ + // throw point not implemented yet, because there is no way to narrow float value by !== 0.0 + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 286, + ], + ]); + } + + public function testMagicMethods(): void + { + $this->analyse([__DIR__ . '/data/dead-catch-magic-methods.php'], [ + [ + 'Dead catch - Exception is never thrown in the try block.', + 22, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 65, + ], + ]); + } + + public function testBug9406(): void + { + $this->analyse([__DIR__ . '/data/bug-9406.php'], []); + } + + public function testBug5650(): void + { + $this->analyse([__DIR__ . '/data/bug-5650.php'], [ + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 24, + ], + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 32, + ], + ]); + } + + public function testBug9568(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-9568.php'], []); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + self::markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/unthrown-exception-property-hooks.php'], [ + [ + 'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.', + 27, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooks\SomeException is never thrown in the try block.', + 39, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.', + 53, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooks\SomeException is never thrown in the try block.', + 65, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.', + 107, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.', + 128, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.', + 154, + ], + [ + 'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.', + 175, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php index 6af5ae9a2c..1ec6854a48 100644 --- a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,10 +16,16 @@ class CaughtExceptionExistenceRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new CaughtExceptionExistenceRule( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, true, ); } @@ -52,4 +60,17 @@ public function testBug3690(): void $this->analyse([__DIR__ . '/data/bug-3690.php'], []); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \PrefixedRuntimeException?'; + + $this->analyse([__DIR__ . '/../Classes/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\PrefixedRuntimeException.', + 19, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php index 9f195bb503..ba61e2900a 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; +use function sprintf; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -27,7 +29,7 @@ protected function getRule(): Rule public function testRule(): void { - $this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], [ + $errors = [ [ 'Method MissingExceptionMethodThrows\Foo::doBaz() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', 23, @@ -40,7 +42,32 @@ public function testRule(): void 'Method MissingExceptionMethodThrows\Foo::doLorem2() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', 34, ], - ]); + [ + sprintf( + 'Method MissingExceptionMethodThrows\Foo::dateTimeZoneDoesThrows() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', + PHP_VERSION_ID >= 80300 ? 'DateInvalidTimeZoneException' : 'Exception', + ), + 95, + ], + [ + sprintf( + 'Method MissingExceptionMethodThrows\Foo::dateIntervalDoesThrows() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', + PHP_VERSION_ID >= 80300 ? 'DateMalformedIntervalStringException' : 'Exception', + ), + 105, + ], + ]; + if (PHP_VERSION_ID >= 80300) { + $errors[] = [ + 'Method MissingExceptionMethodThrows\Foo::dateTimeModifyDoesThrows() throws checked exception DateMalformedStringException but it\'s missing from the PHPDoc @throws tag.', + 121, + ]; + $errors[] = [ + 'Method MissingExceptionMethodThrows\Foo::dateTimeModifyDoesThrows() throws checked exception DateMalformedStringException but it\'s missing from the PHPDoc @throws tag.', + 122, + ]; + } + $this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], $errors); } } diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRuleTest.php new file mode 100644 index 0000000000..cf01fb3852 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInPropertyHookThrowsRuleTest.php @@ -0,0 +1,55 @@ + + */ +class MissingCheckedExceptionInPropertyHookThrowsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingCheckedExceptionInPropertyHookThrowsRule( + new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [ShouldNotHappenException::class], + [], + [], + )), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/missing-exception-property-hook-throws.php'], [ + [ + 'Get hook for property MissingExceptionPropertyHookThrows\Foo::$k throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', + 25, + ], + [ + 'Set hook for property MissingExceptionPropertyHookThrows\Foo::$l throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', + 32, + ], + [ + 'Get hook for property MissingExceptionPropertyHookThrows\Foo::$m throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', + 38, + ], + [ + 'Get hook for property MissingExceptionPropertyHookThrows\Foo::$n throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', + 43, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php b/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php new file mode 100644 index 0000000000..8139960e66 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php @@ -0,0 +1,66 @@ + + */ +class NoncapturingCatchRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoncapturingCatchRule(); + } + + public function dataRule(): array + { + return [ + [ + 70400, + [ + [ + 'Non-capturing catch is supported only on PHP 8.0 and later.', + 12, + ], + [ + 'Non-capturing catch is supported only on PHP 8.0 and later.', + 21, + ], + ], + ], + [ + 80000, + [], + ], + ]; + } + + /** + * @dataProvider dataRule + * + * @param list $expectedErrors + */ + public function testRule(int $phpVersion, array $expectedErrors): void + { + $testVersion = new PhpVersion($phpVersion); + $runtimeVersion = new PhpVersion(PHP_VERSION_ID); + if ( + $testVersion->getMajorVersionId() !== $runtimeVersion->getMajorVersionId() + || $testVersion->getMinorVersionId() !== $runtimeVersion->getMinorVersionId() + ) { + $this->markTestSkipped('Test requires PHP version ' . $testVersion->getMajorVersionId() . '.' . $testVersion->getMinorVersionId() . '.*'); + } + + $this->analyse([ + __DIR__ . '/data/noncapturing-catch.php', + __DIR__ . '/data/bug-8663.php', + ], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php new file mode 100644 index 0000000000..69f97f9d0e --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php @@ -0,0 +1,74 @@ + + */ +class ThrowExprTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ThrowExprTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true)); + } + + public function testRule(): void + { + $this->analyse( + [__DIR__ . '/data/throw-values.php'], + [ + [ + 'Invalid type int to throw.', + 29, + ], + [ + 'Invalid type ThrowExprValues\InvalidException to throw.', + 32, + ], + [ + 'Invalid type ThrowExprValues\InvalidInterfaceException to throw.', + 35, + ], + [ + 'Invalid type Exception|null to throw.', + 38, + ], + [ + 'Throwing object of an unknown class ThrowExprValues\NonexistentClass.', + 44, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Invalid type int to throw.', + 65, + ], + ], + ); + } + + public function testClassExists(): void + { + $this->analyse([__DIR__ . '/data/throw-class-exists.php'], []); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/throw-values-nullsafe.php'], [ + [ + 'Invalid type Exception|null to throw.', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php index ced6add41a..1f32de8fe4 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -41,14 +40,10 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function testRule(int $phpVersion, array $expectedErrors): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $this->phpVersion = new PhpVersion($phpVersion); $this->analyse([__DIR__ . '/data/throw-expr.php'], $expectedErrors); } diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php index 5b78999ddd..ff6c4416a6 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php @@ -87,7 +87,7 @@ public function dataRule(): array /** * @dataProvider dataRule * @param string[] $checkedExceptionClasses - * @param mixed[] $errors + * @param list $errors */ public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void { diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php index feecdd2ac1..5a2dcb0429 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use ThrowsVoidMethod\MyException; +use UnhandledMatchError; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -87,7 +89,7 @@ public function dataRule(): array /** * @dataProvider dataRule * @param string[] $checkedExceptionClasses - * @param mixed[] $errors + * @param list $errors */ public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void { @@ -96,4 +98,14 @@ public function testRule(bool $missingCheckedExceptionInThrows, array $checkedEx $this->analyse([__DIR__ . '/data/throws-void-method.php'], $errors); } + public function testBug6910(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->missingCheckedExceptionInThrows = false; + $this->checkedExceptionClasses = [UnhandledMatchError::class]; + $this->analyse([__DIR__ . '/data/bug-6910.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php new file mode 100644 index 0000000000..6072db088b --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php @@ -0,0 +1,119 @@ + + */ +class ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest extends RuleTestCase +{ + + private bool $missingCheckedExceptionInThrows; + + /** @var string[] */ + private array $checkedExceptionClasses; + + protected function getRule(): Rule + { + return new ThrowsVoidPropertyHookWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + $this->checkedExceptionClasses, + ), $this->missingCheckedExceptionInThrows); + } + + public function dataRule(): array + { + return [ + [ + true, + [], + [], + ], + [ + false, + ['DifferentException'], + [ + [ + 'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', + 18, + ], + [ + 'Get hook for property ThrowsVoidPropertyHook\Foo::$j throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', + 26, + ], + ], + ], + [ + true, + ['ThrowsVoidPropertyHook\\MyException'], + [], + ], + [ + true, + ['DifferentException'], + [ + [ + 'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', + 18, + ], + [ + 'Get hook for property ThrowsVoidPropertyHook\Foo::$j throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', + 26, + ], + ], + ], + [ + false, + [], + [ + [ + 'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', + 18, + ], + [ + 'Get hook for property ThrowsVoidPropertyHook\Foo::$j throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', + 26, + ], + ], + ], + [ + false, + ['ThrowsVoidPropertyHook\\MyException'], + [ + [ + 'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', + 18, + ], + [ + 'Get hook for property ThrowsVoidPropertyHook\Foo::$j throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', + 26, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param string[] $checkedExceptionClasses + * @param list $errors + */ + public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; + $this->checkedExceptionClasses = $checkedExceptionClasses; + $this->analyse([__DIR__ . '/data/throws-void-property-hook.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php index 82871e6145..8de4ae7bed 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php @@ -11,14 +11,20 @@ class TooWideFunctionThrowTypeRuleTest extends RuleTestCase { + private bool $implicitThrows = true; + protected function getRule(): Rule { - return new TooWideFunctionThrowTypeRule(new TooWideThrowTypeCheck()); + return new TooWideFunctionThrowTypeRule(new TooWideThrowTypeCheck($this->implicitThrows)); } public function testRule(): void { $this->analyse([__DIR__ . '/data/too-wide-throws-function.php'], [ + [ + 'Function TooWideThrowsFunction\doFoo3() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 20, + ], [ 'Function TooWideThrowsFunction\doFoo4() has DomainException in PHPDoc @throws tag but it\'s not thrown.', 26, diff --git a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php index eddbd31bf6..c3f2887c98 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -12,14 +13,20 @@ class TooWideMethodThrowTypeRuleTest extends RuleTestCase { + private bool $implicitThrows = true; + protected function getRule(): Rule { - return new TooWideMethodThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck()); + return new TooWideMethodThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck($this->implicitThrows)); } public function testRule(): void { $this->analyse([__DIR__ . '/data/too-wide-throws-method.php'], [ + [ + 'Method TooWideThrowsMethod\Foo::doFoo3() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 23, + ], [ 'Method TooWideThrowsMethod\Foo::doFoo4() has DomainException in PHPDoc @throws tag but it\'s not thrown.', 29, @@ -40,7 +47,62 @@ public function testRule(): void 'Method TooWideThrowsMethod\ParentClass::doFoo() has LogicException in PHPDoc @throws tag but it\'s not thrown.', 77, ], + [ + 'Method TooWideThrowsMethod\ImmediatelyCalledCallback::doFoo2() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 167, + ], + ]); + } + + public function testBug6233(): void + { + $this->analyse([__DIR__ . '/data/bug-6233.php'], []); + } + + public function testImmediatelyCalledArrowFunction(): void + { + $this->analyse([__DIR__ . '/data/immediately-called-arrow-function.php'], [ + [ + 'Method ImmediatelyCalledArrowFunction\ImmediatelyCalledCallback::doFoo2() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 19, + ], ]); } + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/immediately-called-fcc.php'], []); + } + + public static function dataRuleLookOnlyForExplicitThrowPoints(): iterable + { + yield [ + true, + [], + ]; + yield [ + false, + [ + [ + 'Method TooWideThrowsExplicit\Foo::doFoo() has Exception in PHPDoc @throws tag but it\'s not thrown.', + 11, + ], + ], + ]; + } + + /** + * @dataProvider dataRuleLookOnlyForExplicitThrowPoints + * @param list $errors + */ + public function testRuleLookOnlyForExplicitThrowPoints(bool $implicitThrows, array $errors): void + { + $this->implicitThrows = $implicitThrows; + $this->analyse([__DIR__ . '/data/too-wide-throws-explicit.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/TooWidePropertyHookThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWidePropertyHookThrowTypeRuleTest.php new file mode 100644 index 0000000000..a74effd6e0 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/TooWidePropertyHookThrowTypeRuleTest.php @@ -0,0 +1,57 @@ + + */ +class TooWidePropertyHookThrowTypeRuleTest extends RuleTestCase +{ + + private bool $implicitThrows = true; + + protected function getRule(): Rule + { + return new TooWidePropertyHookThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck($this->implicitThrows)); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/too-wide-throws-property-hook.php'], [ + [ + 'Get hook for property TooWideThrowsPropertyHook\Foo::$c has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 26, + ], + [ + 'Get hook for property TooWideThrowsPropertyHook\Foo::$d has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 33, + ], + [ + 'Get hook for property TooWideThrowsPropertyHook\Foo::$g has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 58, + ], + [ + 'Get hook for property TooWideThrowsPropertyHook\Foo::$h has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 68, + ], + [ + 'Get hook for property TooWideThrowsPropertyHook\Foo::$j has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 76, + ], + [ + 'Get hook for property TooWideThrowsPropertyHook\Foo::$k has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 83, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/catch-with-unthrown-exception-stubs.neon b/tests/PHPStan/Rules/Exceptions/catch-with-unthrown-exception-stubs.neon new file mode 100644 index 0000000000..f21387ab6a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/catch-with-unthrown-exception-stubs.neon @@ -0,0 +1,5 @@ +parameters: + exceptions: + implicitThrows: false + stubFiles: + - data/catch-with-unthrown-exception-stubs.stub diff --git a/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.neon b/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.neon new file mode 100644 index 0000000000..790db39dd6 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.neon @@ -0,0 +1,3 @@ +parameters: + exceptions: + implicitThrows: false diff --git a/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.php b/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.php new file mode 100644 index 0000000000..0c5d4c1c26 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.php @@ -0,0 +1,25 @@ +method(); + } catch (\Throwable $e) { // Dead catch - Throwable is never thrown in the try block. + + } + } + + public function method(): void + { + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-4852.php b/tests/PHPStan/Rules/Exceptions/data/bug-4852.php index 058f6c1a1f..dfc01e41c8 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-4852.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-4852.php @@ -61,9 +61,17 @@ public function offsetUnset ($offset) {} try { $buz[] = 'value'; } catch (Exception $e) { - // not dead + // dead because $buz cannot be a subclass } +function (MaybeThrows2 $buz): void { + try { + $buz[] = 'value'; + } catch (Exception $e) { + // not dead + } +}; + $baz = new DefinitelyNoThrows(); try { $baz[] = 'value'; diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-5650.php b/tests/PHPStan/Rules/Exceptions/data/bug-5650.php new file mode 100644 index 0000000000..9a9be87c4a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5650.php @@ -0,0 +1,35 @@ += 8.0 + +namespace Bug5866; + +use InvalidArgumentException; +use JsonException; + +class Foo +{ + + /** + * @param string $contents + */ + public function decode($contents) { + try { + $parsed = json_decode($contents, true, flags: JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY | JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new InvalidArgumentException('Unable to decode contents'); + } + } + + /** + * @param string $contents + */ + public function decode2($contents) { + try { + $parsed = json_decode($contents, depth: 123, flags: JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY | JSON_THROW_ON_ERROR, associative: true,); + } catch (JsonException $exception) { + throw new InvalidArgumentException('Unable to decode contents'); + } + } + + /** + * @param string $contents + */ + public function encode($contents) { + try { + $encoded = json_encode($contents, depth: 2, flags: JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY | JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new InvalidArgumentException('Unable to encode contents'); + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-5903.php b/tests/PHPStan/Rules/Exceptions/data/bug-5903.php index b4c12e3877..0300b6ecc8 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-5903.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5903.php @@ -2,7 +2,7 @@ namespace Bug5903; -class Test +final class Test { /** @var \Traversable */ protected $traversable; diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6115.php b/tests/PHPStan/Rules/Exceptions/data/bug-6115.php new file mode 100644 index 0000000000..4dc6a14235 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6115.php @@ -0,0 +1,30 @@ += 8.0 + +namespace Bug6115; + +$a = 5; +try { + $b = match ($a) { + 1 => [0], + 2 => [1], + 3 => [2], + }; +} catch (\UnhandledMatchError $e) { + // not dead +} + +try { + $b = match ($a) { + default => [0], + }; +} catch (\UnhandledMatchError $e) { + // dead +} + +try { + $b = match ($a) { + 5 => [0], + }; +} catch (\UnhandledMatchError $e) { + // dead +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6233.php b/tests/PHPStan/Rules/Exceptions/data/bug-6233.php new file mode 100644 index 0000000000..99fdf171a1 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6233.php @@ -0,0 +1,44 @@ += 7.4 +|int<1, max> $value + */ + public function nonZeroIntegerRange1(int $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int $value + */ + public function nonZeroIntegerRange2(int $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int $value + */ + public function zeroIncludedIntegerRange(int $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param array $values + */ + public function sayHello(array $values): float + { + try { + return 99 / $values['a']; + } catch (\DivisionByZeroError $e) { + return 0.0; + } + try { + return 99 % $values['a']; + } catch (\DivisionByZeroError $e) { + return 0.0; + } + } + + /** + * @param '0' $value + */ + public function numericZeroString(string $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param '1' $value + */ + public function numericNonZeroString(string $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param float $value + */ + public function floatValue(float $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param float $value + */ + public function floatNonZeroValue(float $value): void + { + if ($value === 0.0) { + return; + } + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } +} + +class TestAssignOp +{ + /** + * @param int $value + */ + public function integer($val, int $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int|int<1, max> $value + */ + public function nonZeroIntegerRange1($val, int $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int $value + */ + public function nonZeroIntegerRange2($val, int $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int $value + */ + public function zeroIncludedIntegerRange($val, int $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param array $values + */ + public function sayHello($val, array $values): float + { + try { + return $val /= $values['a']; + } catch (\DivisionByZeroError $e) { + return 0.0; + } + try { + return $val %= $values['a']; + } catch (\DivisionByZeroError $e) { + return 0.0; + } + } + + /** + * @param '0' $value + */ + public function numericZeroString($val, string $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param '1' $value + */ + public function numericNonZeroString($val, string $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param float $value + */ + public function floatValue($val, float $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param float $value + */ + public function floatNonZeroValue($val, float $value): void + { + if ($value === 0.0) { + return; + } + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6786.php b/tests/PHPStan/Rules/Exceptions/data/bug-6786.php new file mode 100644 index 0000000000..a1b87e2320 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6786.php @@ -0,0 +1,24 @@ +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..300aad76b2 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6791.php @@ -0,0 +1,43 @@ + */ + 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/bug-6910.php b/tests/PHPStan/Rules/Exceptions/data/bug-6910.php new file mode 100644 index 0000000000..b51f2d054c --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6910.php @@ -0,0 +1,43 @@ += 8.0 + +namespace Bug6910; + +class RedFish {} + +class BlueFish {} + +class Net { + public RedFish|BlueFish $heldFish; + public int $prop; + + /** + * @throws void + */ + public function dropFish(): void { + match ($this->heldFish instanceof RedFish) { + true => 'hello', + false => 'world', + }; + } + + /** + * @throws void + * @param 'hello'|'world' $string + */ + public function issetFish(string $string): void { + match ($string === 'hello') { + true => 'hello', + false => 'world', + }; + } + + /** + * @throws void + */ + public function anotherFish(bool $bool): void { + match ($bool) { + true => 'hello', + false => 'world', + }; + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-8663.php b/tests/PHPStan/Rules/Exceptions/data/bug-8663.php new file mode 100644 index 0000000000..d865ebbb93 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-8663.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug8663; + +/** + * Provides example to demonstrate an issue with PHPStan. + */ +class StanExample2 +{ + + /** + * An exception is caught but not captured. + * + * That's OK for PHP 8 but not for 7.4 - PHPStan does not report the issue. + */ + public function catchExceptionsWithoutCapturing(): void + { + try { + print 'Lets do something nasty here.'; + throw new \Exception('This is nasty'); + } catch (\Exception) { + print 'Exception occured'; + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-9066.php b/tests/PHPStan/Rules/Exceptions/data/bug-9066.php new file mode 100644 index 0000000000..60990327ac --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-9066.php @@ -0,0 +1,32 @@ +get('1'); + } catch (\OutOfBoundsException $e) { + + } + } + public function removeMayThrow() + { $map = new \Ds\Map(); + try { + $map->remove('1'); + } catch (\OutOfBoundsException $e) { + + } + } + public function neverThrows() + { $map = new \Ds\Map(); + try { + $map->get('1', null); + $map->remove('1', null); + } catch (\OutOfBoundsException $e) { + + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-9406.php b/tests/PHPStan/Rules/Exceptions/data/bug-9406.php new file mode 100644 index 0000000000..0bf40f3c92 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-9406.php @@ -0,0 +1,37 @@ += 8.0 + +namespace Bug9568; + +class Json +{ + public function decode( + string $jsonString, + bool $associative = false, + int $flags = JSON_THROW_ON_ERROR, + callable $onDecodeFail = null + ): mixed { + try { + return json_decode( + json: $jsonString, + associative: $associative, + flags: $flags, + ); + } catch (\Throwable $exception) { + if (isset($onDecodeFail)) { + return $onDecodeFail($exception); + } + } + + return null; + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.php b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.php new file mode 100644 index 0000000000..e9e6908560 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.php @@ -0,0 +1,72 @@ +transactional(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function doFoo2(): void + { + try { + \MyFunction\doFoo(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function doFoo3(array $a): void + { + try { + uksort($a, function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function doFoo4(\Ds\Deque $deque): void + { + try { + $deque->filter(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.stub b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.stub new file mode 100644 index 0000000000..44993928da --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.stub @@ -0,0 +1,49 @@ + + * @param-immediately-invoked-callable $callback + */ + public function filter(callable $callback = null): Deque + { + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/dead-catch-magic-methods.php b/tests/PHPStan/Rules/Exceptions/data/dead-catch-magic-methods.php new file mode 100644 index 0000000000..930e862583 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/dead-catch-magic-methods.php @@ -0,0 +1,69 @@ +magicMethod1(); + } catch (\Exception $e) { + + } + + try { + ClassWithMagicMethod::staticMagicMethod1(); + } catch (\Exception $e) { + // No error since `implicitThrows: true` is used by default + } + } +} + +/** + * @method void magicMethod2(); + * @method static void staticMagicMethod2(); + */ +class ClassWithMagicMethod2 +{ + /** + * @throws \Exception + */ + public function __call($name, $arguments) + { + throw new \Exception(); + } + + /** + * @throws void + */ + public static function __callStatic($name, $arguments) + { + } + + public function test() + { + try { + (new ClassWithMagicMethod2())->magicMethod2(); + } catch (\Exception $e) { + + } + + try { + ClassWithMagicMethod2::staticMagicMethod2(); + } catch (\Exception $e) { + + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/immediately-called-arrow-function.php b/tests/PHPStan/Rules/Exceptions/data/immediately-called-arrow-function.php new file mode 100644 index 0000000000..307639c826 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/immediately-called-arrow-function.php @@ -0,0 +1,41 @@ += 8.0 + +namespace ImmediatelyCalledArrowFunction; + +class ImmediatelyCalledCallback +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(array $a): void + { + array_map(fn () => throw new \InvalidArgumentException(), $a); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(array $a): void + { + $cb = fn () => throw new \InvalidArgumentException(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(array $a): void + { + $f = fn () => throw new \InvalidArgumentException(); + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(array $a): void + { + (fn () => throw new \InvalidArgumentException())(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/immediately-called-fcc.php b/tests/PHPStan/Rules/Exceptions/data/immediately-called-fcc.php new file mode 100644 index 0000000000..35d36afcac --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/immediately-called-fcc.php @@ -0,0 +1,82 @@ += 8.1 + +namespace ImmediatelyCalledFcc; + +class Foo +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(): void + { + $f = function () { + throw new \InvalidArgumentException(); + }; + $g = $f(...); + $g(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(): void + { + $f = fn () => throw new \InvalidArgumentException(); + $g = $f(...); + $g(); + } + + /** + * @throws \InvalidArgumentException + */ + public function throwsInvalidArgumentException() + { + throw new \InvalidArgumentException(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(): void + { + $f = $this->throwsInvalidArgumentException(...); + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(): void + { + $f = alsoThrowsInvalidArgumentException(...); + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo5(): void + { + $f = [$this, 'throwsInvalidArgumentException']; + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo6(): void + { + $f = 'ImmediatelyCalledFcc\\alsoThrowsInvalidArgumentException'; + $f(); + } + +} + +/** + * @throws \InvalidArgumentException + */ +function alsoThrowsInvalidArgumentException() +{ + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php index 66c280bf79..21cfdf1072 100644 --- a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php @@ -57,4 +57,69 @@ public function doDolor(): void } } + public function doSit(): void + { + try { + $this->throwsInterface(); + } catch (\Throwable $e) { + + } + } + + public function doSit2(): void + { + try { + $this->throwsInterface(); + } catch (\InvalidArgumentException $e) { + + } catch (\Throwable $e) { + + } + } + + /** + * @throws \ExtendsThrowable\ExtendsThrowable + */ + private function throwsInterface(): void + { + + } + + public function dateTimeZoneDoesNotThrow(): void + { + new \DateTimeZone('UTC'); + } + + public function dateTimeZoneDoesThrows(string $tz): void + { + new \DateTimeZone($tz); + } + + public function dateTimeZoneDoesNotThrowCaseInsensitive(): void + { + new \DaTetImezOnE('UTC'); + } + + public function dateIntervalDoesThrows(string $i): void + { + new \DateInterval($i); + } + + public function dateIntervalDoeNotThrow(): void + { + new \DateInterval('P7D'); + } + + public function dateTimeModifyDoeNotThrow(\DateTime $dt, \DateTimeImmutable $dti): void + { + $dt->modify('+1 day'); + $dti->modify('+1 day'); + } + + public function dateTimeModifyDoesThrows(\DateTime $dt, \DateTimeImmutable $dti, string $m): void + { + $dt->modify($m); + $dti->modify($m); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/missing-exception-property-hook-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-property-hook-throws.php new file mode 100644 index 0000000000..773f849d74 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-property-hook-throws.php @@ -0,0 +1,46 @@ += 8.4 + +namespace MissingExceptionPropertyHookThrows; + +class Foo +{ + + public int $i { + /** @throws \InvalidArgumentException */ + get { + throw new \InvalidArgumentException(); // ok + } + } + + public int $j { + /** @throws \LogicException */ + set { + throw new \InvalidArgumentException(); // ok + } + } + + public int $k { + /** @throws \RuntimeException */ + get { + throw new \InvalidArgumentException(); // error + } + } + + public int $l { + /** @throws \RuntimeException */ + set { + throw new \InvalidArgumentException(); // error + } + } + + public int $m { + get { + throw new \InvalidArgumentException(); // error + } + } + + public int $n { + get => throw new \InvalidArgumentException(); // error + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/noncapturing-catch.php b/tests/PHPStan/Rules/Exceptions/data/noncapturing-catch.php new file mode 100644 index 0000000000..a9e8a482cb --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/noncapturing-catch.php @@ -0,0 +1,17 @@ += 8.0 + +namespace NoncapturingCatch; + +class HelloWorld +{ + + public function hello(): void + { + try { + throw new \Exception('Hello'); + } catch (\Exception) { + echo 'Hi!'; + } + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/throw-class-exists.php b/tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php similarity index 83% rename from tests/PHPStan/Rules/Variables/data/throw-class-exists.php rename to tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php index f819307398..39c9dd13dc 100644 --- a/tests/PHPStan/Rules/Variables/data/throw-class-exists.php +++ b/tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php @@ -1,6 +1,6 @@ = 8.0 -namespace ThrowValuesNullsafe; +namespace ThrowExprValuesNullsafe; class Bar { diff --git a/tests/PHPStan/Rules/Variables/data/throw-values.php b/tests/PHPStan/Rules/Exceptions/data/throw-values.php similarity index 92% rename from tests/PHPStan/Rules/Variables/data/throw-values.php rename to tests/PHPStan/Rules/Exceptions/data/throw-values.php index 5582923fa2..39d51de3ca 100644 --- a/tests/PHPStan/Rules/Variables/data/throw-values.php +++ b/tests/PHPStan/Rules/Exceptions/data/throw-values.php @@ -1,6 +1,6 @@ -= 8.0 -namespace ThrowValues; +namespace ThrowExprValues; class InvalidException {}; interface InvalidInterfaceException {}; @@ -60,3 +60,7 @@ function (\stdClass $foo) { /** @var \Exception */ throw $foo; }; + +function (?\stdClass $foo) { + echo $foo ?? throw 1; +}; diff --git a/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php b/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php new file mode 100644 index 0000000000..08e3f10940 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php @@ -0,0 +1,29 @@ += 8.4 + +namespace ThrowsVoidPropertyHook; + +class MyException extends \Exception +{ + +} + +class Foo +{ + + public int $i { + /** + * @throws void + */ + get { + throw new MyException(); + } + } + + public int $j { + /** + * @throws void + */ + get => throw new MyException(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-explicit.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-explicit.php new file mode 100644 index 0000000000..834df2b1f0 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-explicit.php @@ -0,0 +1,22 @@ +doBar(); + } + + public function doBar(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-function.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-function.php index f4bae0e0ae..d537543139 100644 --- a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-function.php +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-function.php @@ -17,7 +17,7 @@ function doFoo2(): void // ok } /** @throws \InvalidArgumentException */ -function doFoo3(): void // ok +function doFoo3(): void // new LogicException cannot be InvalidArgumentException { throw new \LogicException(); } @@ -64,3 +64,9 @@ function doFoo9(): void // error - DomainException unused { } + +/** @throws \InvalidArgumentException */ +function doFoo10(\LogicException $e): void // ok +{ + throw $e; +} diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php index 819d89c87b..5e28b2186e 100644 --- a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php @@ -20,7 +20,7 @@ public function doFoo2(): void // ok } /** @throws \InvalidArgumentException */ - public function doFoo3(): void // ok + public function doFoo3(): void // // new LogicException cannot be InvalidArgumentException { throw new \LogicException(); } @@ -147,3 +147,49 @@ public function doBaz(): void } } + +class ImmediatelyCalledCallback +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(array $a): void + { + array_map(function () { + throw new \InvalidArgumentException(); + }, $a); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(array $a): void + { + $cb = function () { + throw new \InvalidArgumentException(); + }; + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(array $a): void + { + $f = function () { + throw new \InvalidArgumentException(); + }; + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(array $a): void + { + (function () { + throw new \InvalidArgumentException(); + })(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php new file mode 100644 index 0000000000..6de7bc4073 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php @@ -0,0 +1,95 @@ += 8.4 + +namespace TooWideThrowsPropertyHook; + +use DomainException; + +class Foo +{ + + public int $a { + /** @throws \InvalidArgumentException */ + get { + throw new \InvalidArgumentException(); + } + } + + public int $b { + /** @throws \LogicException */ + get { + throw new \InvalidArgumentException(); + } + } + + public int $c { + /** @throws \InvalidArgumentException */ + get { + throw new \LogicException(); // new LogicException cannot be InvalidArgumentException + } + } + + public int $d { + /** @throws \InvalidArgumentException|\DomainException */ + get { // error - DomainException unused + throw new \InvalidArgumentException(); + } + } + + public int $e { + /** @throws void */ + get { // ok - picked up by different rule + throw new \InvalidArgumentException(); + } + } + + public int $f { + /** @throws \InvalidArgumentException|\DomainException */ + get { + if (rand(0, 1)) { + throw new \InvalidArgumentException(); + } + + throw new DomainException(); + } + } + + public int $g { + /** @throws \DomainException */ + get { // error - DomainException unused + throw new \InvalidArgumentException(); + } + } + + public int $h { + /** + * @throws \InvalidArgumentException + * @throws \DomainException + */ + get { // error - DomainException unused + throw new \InvalidArgumentException(); + } + } + + + public int $j { + /** @throws \DomainException */ + get { // error - DomainException unused + + } + } + + public int $k { + /** @throws \DomainException */ + get => 11; // error - DomainException unused + } + + public int $l { + /** @throws \InvalidArgumentException */ + get { + throw $this->logicException; + } + } + + public \LogicException $logicException; + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/union-type-error.php b/tests/PHPStan/Rules/Exceptions/data/union-type-error.php new file mode 100644 index 0000000000..cad8c5348c --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/union-type-error.php @@ -0,0 +1,29 @@ += 8.0 + +declare(strict_types = 1); + +namespace UnionTypeError; + +final 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-multi.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-multi.php new file mode 100644 index 0000000000..c0893a8ba1 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-multi.php @@ -0,0 +1,187 @@ +throwLogicRangeJsonExceptions(); + + } catch (\JsonException $t) { + + } catch (\RangeException | \LogicException $t) { + + } + } + + public function doBaz() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\JsonException $t) { + + } catch (\RangeException | \LogicException | \OverflowException $t) { // overflow not thrown + + } + } + + public function doBag() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\RuntimeException $t) { + + } catch (\LogicException | \JsonException $t) { + + } + } + + public function doZag() + { + try { + throw new \RangeException(); + + } catch (\RuntimeException | \JsonException $t) { // json not thrown + + } + } + + public function doBal() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\RuntimeException | \JsonException $t) { + + } catch (\InvalidArgumentException $t) { + + } + } + + public function doBap() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\InvalidArgumentException $t) { + + } catch (\RuntimeException | \JsonException $t) { + + } + } + + public function doZaz() + { + try { + \ThrowPoints\Helpers\maybeThrows(); + } catch (\LogicException $e) { + + } + } + + public function doZab() + { + try { + \ThrowPoints\Helpers\maybeThrows(); + } catch (\InvalidArgumentException | \LogicException $e) { + + } + } + + public function someThrowableTest(): void + { + try { + $this->throwIae(); + } catch (\InvalidArgumentException | \Exception $e) { + + } catch (\Throwable $e) { + + } + } + + public function someThrowableTest2(): void + { + try { + $this->throwIae(); + } catch (\RuntimeException | \Throwable $e) { + // IAE is not runtime, dead + } catch (\Throwable $e) { + + } + } + + public function someThrowableTest3(): void + { + try { + $this->throwIae(); + } catch (\Throwable $e) { + + } catch (\Throwable $e) { + + } + } + + + public function someThrowableTest4(): void + { + try { + $this->throwIae(); + } catch (\Throwable $e) { + + } catch (\InvalidArgumentException $e) { + + } + } + + public function someThrowableTest5(): void + { + try { + $this->throwIae(); + } catch (\InvalidArgumentException $e) { + + } catch (\InvalidArgumentException $e) { + + } + } + + public function someThrowableTest6(): void + { + try { + $this->throwIae(); + } catch (\InvalidArgumentException | \Exception $e) { + // catch can be simplified, this is not reported + } + } + + /** + * @throws \RangeException + * @throws \LogicException + * @throws \JsonException + */ + private function throwLogicRangeJsonExceptions(): void + { + + } + + /** @throws \InvalidArgumentException */ + public function throwIae(): void + { + + } + + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks-implicit-throws-disabled.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks-implicit-throws-disabled.php new file mode 100644 index 0000000000..6d47003630 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks-implicit-throws-disabled.php @@ -0,0 +1,120 @@ += 8.4 + +namespace UnthrownExceptionPropertyHooksImplicitThrowsDisabled; + +class MyCustomException extends \Exception +{ + +} + +class SomeException extends \Exception +{ + +} + +class Foo +{ + public int $i; + + public function doFoo(): void + { + try { + echo $this->i; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + } + + public int $k { + get { + return 1; + } + } + + public function doBaz(): void + { + try { + echo $this->k; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + } + + private int $l { + get { + return $this->l; + } + } + + public function doLorem(): void + { + try { + echo $this->l; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + } + + final public int $m { + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + + try { + $this->m = 1; + } catch (MyCustomException) { // unthrown - set hook does not exist + + } + } + +} + +final class FinalFoo +{ + + public int $m { + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // unthrown - implicit @throws disabled + + } + } + +} + +class ThrowsVoid +{ + + public int $m { + /** @throws void */ + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // unthrown + + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks.php new file mode 100644 index 0000000000..547a47eece --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-property-hooks.php @@ -0,0 +1,200 @@ += 8.4 + +namespace UnthrownExceptionPropertyHooks; + +class MyCustomException extends \Exception +{ + +} + +class SomeException extends \Exception +{ + +} + +class Foo +{ + + public int $i { + /** @throws MyCustomException */ + get { + if (rand(0, 1)) { + throw new MyCustomException(); + } + + try { + return $this->i; + } catch (MyCustomException) { // unthrown - @throws does not apply to direct access in the hook + + } + } + } + + public function doFoo(): void + { + try { + $a = $this->i; + } catch (MyCustomException) { + + } catch (SomeException) { // unthrown + + } + } + + public int $j { + /** @throws MyCustomException */ + set { + if (rand(0, 1)) { + throw new MyCustomException(); + } + + try { + $this->j = $value; + } catch (MyCustomException) { // unthrown - @throws does not apply to direct access in the hook + + } + } + } + + public function doBar(int $v): void + { + try { + $this->j = $v; + } catch (MyCustomException) { + + } catch (SomeException) { // unthrown + + } + } + + public int $k { + get { + return 1; + } + } + + public function doBaz(): void + { + try { + echo $this->k; + } catch (MyCustomException) { // can be thrown - implicit @throws + + } + + try { + $this->k = 1; + } catch (MyCustomException) { // can be thrown - subclass might introduce a set hook + + } + } + + private int $l { + get { + return $this->l; + } + } + + public function doLorem(): void + { + try { + echo $this->l; + } catch (MyCustomException) { // can be thrown - implicit @throws + + } + + try { + $this->l = 1; + } catch (MyCustomException) { // unthrown - set hook does not exist + + } + } + + final public int $m { + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // can be thrown - implicit @throws + + } + + try { + $this->m = 1; + } catch (MyCustomException) { // unthrown - set hook does not exist + + } + } + +} + +final class FinalFoo +{ + + public int $m { + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // can be thrown - implicit @throws + + } + + try { + $this->m = 1; + } catch (MyCustomException) { // unthrown - set hook does not exist + + } + } + +} + +class ThrowsVoid +{ + + public int $m { + /** @throws void */ + get { + return $this->m; + } + } + + public function doIpsum(): void + { + try { + echo $this->m; + } catch (MyCustomException) { // unthrown + + } + } + +} + +class Dynamic +{ + + public function doFoo(object $o, string $s): void + { + try { + echo $o->$s; + } catch (MyCustomException) { // implicit throw point + + } + + try { + $o->$s = 1; + } catch (MyCustomException) { // implicit throw point + + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php index 78236201ac..0327fcb086 100644 --- a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php @@ -544,3 +544,249 @@ public function doBar(string $s) } } + +class TestCaseInsensitiveClassNames +{ + + public function doFoo(): void + { + try { + new \SimpleXmlElement(''); + } catch (\Exception $e) { + + } + } + + public function doBar(): void + { + try { + new \SimpleXmlElement('foo'); + } catch (\Exception $e) { + + } + } + + public function doBaz(string $string): void + { + try { + new \SimpleXmlElement($string); + } catch (\Exception $e) { + + } + } + +} + +/** @throws void */ +function acceptCallable(callable $cb): void +{ + +} + +/** + * @throws void + * @param-later-invoked-callable $cb + */ +function acceptCallableAndCallLater(callable $cb): void +{ + +} + +class CallCallable +{ + + /** + * @throws void + */ + public function doFoo(callable $cb): void + { + try { + $cb(); + } catch (\Exception $e) { + + } + } + + public function passCallableToFunction(): void + { + try { + // immediately called by default + acceptCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function passCallableToFunction2(): void + { + try { + // later called thanks to @param-later-invoked-callable + acceptCallableAndCallLater(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + /** @throws void */ + public function acceptCallable(callable $cb): void + { + + } + + public function passCallableToMethod(): void + { + try { + // later called by default + $this->acceptCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + /** + * @throws void + * @param-immediately-invoked-callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable extends CallCallable +{ + + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable2 extends CallCallable +{ + + /** + * @param callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable3 extends CallCallable +{ + + /** + * @param callable $cb + * @param-later-invoked-callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // later called thanks to @param-later-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class TestIntdivWithRange +{ + /** + * @param int $int + * @param int $negativeInt + * @param int<1, max> $positiveInt + */ + public function doFoo(int $int, int $negativeInt, int $positiveInt): void + { + try { + intdiv($int, $positiveInt); + intdiv($positiveInt, $negativeInt); + intdiv($negativeInt, $positiveInt); + intdiv($positiveInt, $positiveInt); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($int, $negativeInt); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($negativeInt, $negativeInt); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($positiveInt, $int); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($negativeInt, $int); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($int, '-1,5'); + } catch (\ArithmeticError $e) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php new file mode 100644 index 0000000000..e0a51fc2a8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php @@ -0,0 +1,102 @@ + + */ +class ArrayFilterRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new ArrayFilterRule( + $this->createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + true, + ); + } + + public function testFile(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $expectedErrors = [ + [ + 'Parameter #1 $array (array{1, 3}) to function array_filter does not contain falsy values, the array will always stay the same.', + 11, + ], + [ + 'Parameter #1 $array (array{\'test\'}) to function array_filter does not contain falsy values, the array will always stay the same.', + 12, + ], + [ + 'Parameter #1 $array (array{true, true}) to function array_filter does not contain falsy values, the array will always stay the same.', + 17, + ], + [ + 'Parameter #1 $array (array{stdClass}) to function array_filter does not contain falsy values, the array will always stay the same.', + 18, + ], + [ + 'Parameter #1 $array (array) to function array_filter does not contain falsy values, the array will always stay the same.', + 20, + $tipText, + ], + [ + 'Parameter #1 $array (array{0}) to function array_filter contains falsy values only, the result will always be an empty array.', + 23, + ], + [ + 'Parameter #1 $array (array{null}) to function array_filter contains falsy values only, the result will always be an empty array.', + 24, + ], + [ + 'Parameter #1 $array (array{null, null}) to function array_filter contains falsy values only, the result will always be an empty array.', + 25, + ], + [ + 'Parameter #1 $array (array{null, 0}) to function array_filter contains falsy values only, the result will always be an empty array.', + 26, + ], + [ + 'Parameter #1 $array (array) to function array_filter contains falsy values only, the result will always be an empty array.', + 27, + $tipText, + ], + [ + 'Parameter #1 $array (array{}) to function array_filter is empty, call has no effect.', + 28, + ], + ]; + + $this->analyse([__DIR__ . '/data/array_filter_empty.php'], $expectedErrors); + } + + public function testBug2065WithPhpDocTypesAsCertain(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $expectedErrors = [ + [ + 'Parameter #1 $array (array) to function array_filter does not contain falsy values, the array will always stay the same.', + 12, + $tipText, + ], + ]; + + $this->analyse([__DIR__ . '/data/bug-array-filter.php'], $expectedErrors); + } + + public function testBug2065WithoutPhpDocTypesAsCertain(): void + { + $this->treatPhpDocTypesAsCertain = false; + + $this->analyse([__DIR__ . '/data/bug-array-filter.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php new file mode 100644 index 0000000000..ec6ed43e38 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php @@ -0,0 +1,94 @@ + + */ +class ArrayValuesRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new ArrayValuesRule( + $this->createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + true, + ); + } + + public function testFile(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $expectedErrors = [ + [ + 'Parameter #1 $array (array{0, 1, 3}) of array_values is already a list, call has no effect.', + 8, + ], + [ + 'Parameter #1 $array (array{1, 3}) of array_values is already a list, call has no effect.', + 9, + ], + [ + 'Parameter #1 $array (array{\'test\'}) of array_values is already a list, call has no effect.', + 10, + ], + [ + 'Parameter #1 $array (array{\'\', \'test\'}) of array_values is already a list, call has no effect.', + 12, + ], + [ + 'Parameter #1 $array (list) of array_values is already a list, call has no effect.', + 14, + $tipText, + ], + [ + 'Parameter #1 $array (array{0}) of array_values is already a list, call has no effect.', + 17, + ], + [ + 'Parameter #1 $array (array{null, null}) of array_values is already a list, call has no effect.', + 19, + ], + [ + 'Parameter #1 $array (array{null, 0}) of array_values is already a list, call has no effect.', + 20, + ], + [ + 'Parameter #1 $array (array{}) to function array_values is empty, call has no effect.', + 21, + ], + [ + 'Parameter #1 $array (array{}) to function array_values is empty, call has no effect.', + 25, + $tipText, + ], + ]; + + if (PHP_VERSION_ID >= 80000) { + $expectedErrors[] = [ + 'Parameter #1 $array (list) of array_values is already a list, call has no effect.', + 28, + $tipText, + ]; + } else { + $expectedErrors[] = [ + 'Parameter #1 $array (true) to function array_values is empty, call has no effect.', + 27, + ]; + $expectedErrors[] = [ + 'Parameter #1 $array (true) to function array_values is empty, call has no effect.', + 28, + ]; + } + + $this->analyse([__DIR__ . '/data/array_values_list.php'], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index 8d91e66080..a46163b89c 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -12,7 +13,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -27,9 +27,8 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -37,17 +36,19 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/arrow-function-attributes.php'], [ [ 'Attribute class ArrowFunctionAttributes\Foo does not have the function target.', diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php index 22cca6e10e..d046acb657 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php @@ -19,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/arrow-function-nullsafe-by-ref.php'], [ [ 'Nullsafe cannot be returned by reference.', diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php index b82fea8ce4..a5f2fa7232 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php @@ -22,14 +22,14 @@ protected function getRule(): Rule false, true, false, + false, + false, + true, ))); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/arrow-functions-return-type.php'], [ [ 'Anonymous function should return string but returns int.', @@ -39,16 +39,46 @@ public function testRule(): void 'Anonymous function should return int but returns string.', 14, ], + ]); } - public function testBug3261(): void + public function testRuleNever(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); } + $this->analyse([__DIR__ . '/data/arrow-function-never-return.php'], [ + [ + 'Anonymous function should never return but return statement found.', + 12, + ], + ]); + } + + public function testBug3261(): void + { $this->analyse([__DIR__ . '/data/bug-3261.php'], []); } + public function testBug8179(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8179.php'], []); + } + + public function testBugSpaceship(): void + { + $this->analyse([__DIR__ . '/data/bug-spaceship.php'], []); + } + + public function testBugFunctionMethodConstants(): void + { + $this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 1ef3486ea4..26b4a4b906 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -22,12 +21,11 @@ class CallCallablesRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, false, true); return new CallCallablesRule( new FunctionCallParametersCheck( $ruleLevelHelper, new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -42,10 +40,7 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID >= 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped(); - } - $this->analyse([__DIR__ . '/data/callables.php'], [ + $errors = [ [ 'Trying to invoke string but it might not be a callable.', 17, @@ -108,44 +103,65 @@ public function testRule(): void 106, ], [ - 'Trying to invoke CallCallables\Baz but it might not be a callable.', + 'Trying to invoke CallCallables\Baz but it\'s not a callable.', 113, ], + [ + 'Trying to invoke CallCallables\Baz but it might not be a callable.', + 122, + ], [ 'Trying to invoke array{object, \'bar\'} but it might not be a callable.', - 131, + 140, ], [ 'Closure invoked with 0 parameters, 3 required.', - 146, + 155, ], [ 'Closure invoked with 1 parameter, 3 required.', - 147, + 156, ], [ 'Closure invoked with 2 parameters, 3 required.', - 148, + 157, ], [ 'Trying to invoke array{object, \'yo\'} but it might not be a callable.', - 163, + 172, ], [ 'Trying to invoke array{object, \'yo\'} but it might not be a callable.', - 167, + 176, ], [ 'Trying to invoke array{\'CallCallables\\\\CallableInForeach\', \'bar\'|\'foo\'} but it might not be a callable.', - 179, + 188, ], - ]); + [ + 'Trying to invoke array{\'CallCallables\\\\ConstantArrayUnionCallables\'|\'DateTimeImmutable\', \'doFoo\'} but it might not be a callable.', + 214, + ], + [ + 'Trying to invoke array{\'CallCallables\\\ConstantArrayUnionCallables\', \'doBaz\'|\'doFoo\'} but it might not be a callable.', + 221, + ], + ]; + + if (PHP_VERSION_ID >= 80000) { + $errors[] = [ + 'Trying to invoke array{\'CallCallables\\\ConstantArrayUnionCallables\'|\'CallCallables\\\ConstantArrayUnionCallablesTest\', \'doBar\'|\'doFoo\'} but it\'s not a callable.', + 229, + ]; + } + + $this->analyse([__DIR__ . '/data/callables.php'], $errors); } public function testNamedArguments(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); } $this->analyse([__DIR__ . '/data/callables-named-arguments.php'], [ @@ -179,7 +195,7 @@ public function dataBug3566(): array true, [ [ - 'Parameter #1 $ of closure expects int, TMemberType given.', + 'Parameter #1 of closure expects int, TMemberType given.', 29, ], ], @@ -193,7 +209,7 @@ public function dataBug3566(): array /** * @dataProvider dataBug3566 - * @param mixed[] $errors + * @param list $errors */ public function testBug3566(bool $checkExplicitMixed, array $errors): void { @@ -223,10 +239,6 @@ public function testBug1849(): void 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', @@ -243,9 +255,6 @@ public function testFirstClassCallables(): void 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.', @@ -262,4 +271,54 @@ public function testBug6701(): void ]); } + public function testStaticCallInFunctions(): void + { + $this->analyse([__DIR__ . '/data/static-call-in-functions.php'], []); + } + + public function testBug5867(): void + { + $this->analyse([__DIR__ . '/data/bug-5867.php'], []); + } + + public function testBug6485(): void + { + $this->analyse([__DIR__ . '/data/bug-6485.php'], [ + [ + 'Parameter #1 of closure expects never, TBlockType of Bug6485\Block given.', + 33, + ], + ]); + } + + public function testBug6633(): void + { + $this->analyse([__DIR__ . '/data/bug-6633.php'], []); + } + + public function testBug3818b(): void + { + $this->analyse([__DIR__ . '/data/bug-3818b.php'], []); + } + + public function testBug9594(): void + { + $this->analyse([__DIR__ . '/data/bug-9594.php'], []); + } + + public function testBug9614(): void + { + $this->analyse([__DIR__ . '/data/bug-9614.php'], []); + } + + public function testBug10814(): void + { + $this->analyse([__DIR__ . '/data/bug-10814.php'], [ + [ + 'Parameter #1 of closure expects DateTime, DateTimeImmutable given.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 35d1e30694..409535685f 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -10,6 +9,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function sprintf; use const PHP_VERSION_ID; /** @@ -20,12 +20,14 @@ class CallToFunctionParametersRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new CallToFunctionParametersRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } @@ -163,7 +165,7 @@ public function testCallToWeirdFunctions(): void 11, ], [ - 'Function fputcsv invoked with 1 parameter, 2-5 required.', + sprintf('Function fputcsv invoked with 1 parameter, 2-%d required.', PHP_VERSION_ID >= 80100 ? 6 : 5), 12, ], [ @@ -270,14 +272,8 @@ public function testCallToWeirdFunctions(): void $this->analyse([__DIR__ . '/data/call-to-weird-functions.php'], $errors); } - /** - * @requires PHP 7.1.1 - */ public function testUnpackOnAfter711(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70101) { - $this->markTestSkipped('This test requires PHP >= 7.1.1'); - } $this->analyse([__DIR__ . '/data/unpack.php'], [ [ 'Function unpack invoked with 0 parameters, 2-3 required.', @@ -302,15 +298,15 @@ public function testPassingNonVariableToParameterPassedByReference(): void 'Parameter #1 $array of function reset expects array|object, null given.', 39, ], + [ + 'Parameter #1 $s of function PassedByReference\bar expects string, int given.', + 48, + ], ]); } public function testImplodeOnPhp74(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $errors = [ [ 'Parameter #1 $glue of function implode expects string, array given.', @@ -321,9 +317,6 @@ public function testImplodeOnPhp74(): void 8, ], ]; - if (PHP_VERSION_ID < 70400) { - $errors = []; - } if (PHP_VERSION_ID >= 80000) { $errors = [ [ @@ -338,11 +331,6 @@ public function testImplodeOnPhp74(): void public function testImplodeOnLessThanPhp74(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test skipped on 7.4.'); - } - - $errors = []; if (PHP_VERSION_ID >= 80000) { $errors = [ [ @@ -350,7 +338,7 @@ public function testImplodeOnLessThanPhp74(): void 8, ], ]; - } elseif (PHP_VERSION_ID >= 70400) { + } else { $errors = [ [ 'Parameter #1 $glue of function implode expects string, array given.', @@ -433,7 +421,6 @@ public function testFputCsv(): void ]); } - public function testPutCsvWithStringable(): void { if (PHP_VERSION_ID < 80000) { @@ -489,8 +476,8 @@ public function testGenericFunction(): void public function testNamedArguments(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); } $errors = [ @@ -507,17 +494,25 @@ public function testNamedArguments(): void 14, ], ]; - if (PHP_VERSION_ID < 80000) { - $errors[] = [ - 'Missing parameter $arr1 (array) in call to function array_merge.', - 14, - ]; - } require_once __DIR__ . '/data/named-arguments-define.php'; $this->analyse([__DIR__ . '/data/named-arguments.php'], $errors); } + public function testNamedArgumentsAfterUnpacking(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/named-arguments-after-unpacking.php'], [ + [ + 'Argument for parameter $b has already been passed.', + 14, + ], + ]); + } + public function testBug4514(): void { $this->analyse([__DIR__ . '/data/bug-4514.php'], []); @@ -550,6 +545,11 @@ public function testBug3608(): void $this->analyse([__DIR__ . '/data/bug-3608.php'], []); } + public function testBug3631(): void + { + $this->analyse([__DIR__ . '/data/bug-3631.php'], []); + } + public function testBug3920(): void { $this->analyse([__DIR__ . '/data/bug-3920.php'], []); @@ -568,37 +568,38 @@ public function testArrayReduceCallback(): void { $this->analyse([__DIR__ . '/data/array_reduce.php'], [ [ - 'Parameter #2 $callback of function array_reduce expects callable(string, int): string, Closure(string, string): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string, 1|2|3): string, Closure(string, string): string given.', 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 13, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 22, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], ]); } public function testArrayReduceArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/array_reduce_arrow.php'], [ [ - 'Parameter #2 $callback of function array_reduce expects callable(string, int): string, Closure(string, string): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string, 1|2|3): string, Closure(string, string): string given.', 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 11, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 18, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], ]); } @@ -607,30 +608,63 @@ public function testArrayWalkCallback(): void { $this->analyse([__DIR__ . '/data/array_walk.php'], [ [ - 'Parameter #2 $callback of function array_walk expects callable(int, string, mixed): mixed, Closure(stdClass, float): \'\' given.', + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(stdClass, float): \'\' given.', 6, ], [ - 'Parameter #2 $callback of function array_walk expects callable(int, string, string): mixed, Closure(int, string, int): \'\' given.', + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\', \'extra\'): mixed, Closure(int, string, int): \'\' given.', 14, ], + [ + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, string, int): \'\' given.', + 23, + 'Parameter #3 $extra of passed callable is required but accepting callable does not have that parameter. It will be called without it.', + ], ]); } public function testArrayWalkArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/array_walk_arrow.php'], [ [ - 'Parameter #2 $callback of function array_walk expects callable(int, string, mixed): mixed, Closure(stdClass, float): \'\' given.', + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(stdClass, float): \'\' given.', 6, ], [ - 'Parameter #2 $callback of function array_walk expects callable(int, string, string): mixed, Closure(int, string, int): \'\' given.', + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\', \'extra\'): mixed, Closure(int, string, int): \'\' given.', 12, ], + [ + 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, string, int): \'\' given.', + 19, + 'Parameter #3 $extra of passed callable is required but accepting callable does not have that parameter. It will be called without it.', + ], + ]); + } + + public function testArrayUdiffCallback(): void + { + $this->analyse([__DIR__ . '/data/array_udiff.php'], [ + [ + 'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(string, string): string given.', + 6, + ], + [ + 'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(int, int): (literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string) given.', + 14, + ], + [ + 'Parameter #1 $arr1 of function array_udiff expects array<(int&TK)|(string&TK), string>, null given.', + 20, + ], + [ + 'Parameter #2 $arr2 of function array_udiff expects array<(int&TK)|(string&TK), string>, null given.', + 21, + ], + [ + 'Parameter #3 $data_comp_func of function array_udiff expects callable(string, string): int, Closure(string, int): non-empty-string given.', + 22, + ], ]); } @@ -638,19 +672,41 @@ public function testPregReplaceCallback(): void { $this->analyse([__DIR__ . '/data/preg_replace_callback.php'], [ [ - 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(string): string given.', + '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 testMbEregReplaceCallback(): void + { + $this->analyse([__DIR__ . '/data/mb_ereg_replace_callback.php'], [ + [ + 'Parameter #2 $callback of function mb_ereg_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.', + 'Parameter #2 $callback of function mb_ereg_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.', + 'Parameter #2 $callback of function mb_ereg_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.', + 'Parameter #2 $callback of function mb_ereg_replace_callback expects callable(array): string, Closure(): void given.', 25, ], ]); @@ -660,7 +716,7 @@ public function testUasortCallback(): void { $this->analyse([__DIR__ . '/data/uasort.php'], [ [ - 'Parameter #2 $callback of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function uasort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 7, ], ]); @@ -668,13 +724,9 @@ public function testUasortCallback(): void public function testUasortArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/uasort_arrow.php'], [ [ - 'Parameter #2 $callback of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function uasort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 7, ], ]); @@ -684,7 +736,7 @@ public function testUsortCallback(): void { $this->analyse([__DIR__ . '/data/usort.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function usort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 14, ], ]); @@ -692,13 +744,9 @@ public function testUsortCallback(): void public function testUsortArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/usort_arrow.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function usort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 14, ], ]); @@ -708,7 +756,7 @@ public function testUksortCallback(): void { $this->analyse([__DIR__ . '/data/uksort.php'], [ [ - 'Parameter #2 $callback of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', + 'Parameter #2 $callback of function uksort expects callable(\'one\'|\'three\'|\'two\', \'one\'|\'three\'|\'two\'): int, Closure(stdClass, stdClass): 1 given.', 14, ], [ @@ -720,13 +768,9 @@ public function testUksortCallback(): void public function testUksortArrowFunctionCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/uksort_arrow.php'], [ [ - 'Parameter #2 $callback of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', + 'Parameter #2 $callback of function uksort expects callable(\'one\'|\'three\'|\'two\', \'one\'|\'three\'|\'two\'): int, Closure(stdClass, stdClass): 1 given.', 14, ], [ @@ -749,10 +793,6 @@ public function testVaryingAcceptor(): void public function testBug3660(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-3660.php'], [ [ 'Parameter #1 $string of function strlen expects string, int given.', @@ -789,14 +829,11 @@ public function testExplode(): void 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.', + "Parameter #1 \$command of function proc_open expects list|string, array{something: 'bogus', in: 'here'} given.", 6, + "Type #1 from the union: array{something: 'bogus', in: 'here'} is not a list.", ], ]); } @@ -845,25 +882,134 @@ public function testArrayFilterCallback(bool $checkExplicitMixed): void $this->checkExplicitMixed = $checkExplicitMixed; $errors = [ [ - 'Parameter #2 $callback of function array_filter expects callable(int): mixed, Closure(string): true given.', + 'Parameter #2 $callback of function array_filter expects (callable(int): bool)|null, Closure(string): true given.', 17, ], ]; if ($checkExplicitMixed) { $errors[] = [ - 'Parameter #2 $callback of function array_filter expects callable(mixed): mixed, Closure(int): true given.', + 'Parameter #2 $callback of function array_filter expects (callable(mixed): bool)|null, Closure(int): true given.', 20, + 'Type #1 from the union: Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', ]; } $this->analyse([__DIR__ . '/data/array_filter_callback.php'], $errors); } - public function testBug5356(): void + public function testArrayAllCallback(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test skipped on lower version than 8.4 (needs array_all function)'); + } + + $this->analyse([__DIR__ . '/data/array_all.php'], [ + [ + 'Parameter #2 $callback of function array_all expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_all expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_all expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_all expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 52, + ], + [ + 'Parameter #2 $callback of function array_all expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 55, + ], + ]); + } + + public function testArrayAnyCallback(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test skipped on lower version than 8.4 (needs array_any function)'); } + $this->analyse([__DIR__ . '/data/array_any.php'], [ + [ + 'Parameter #2 $callback of function array_any expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_any expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_any expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_any expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 52, + ], + [ + 'Parameter #2 $callback of function array_any expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 55, + ], + ]); + } + + public function testArrayFindCallback(): void + { + $this->analyse([__DIR__ . '/data/array_find.php'], [ + [ + 'Parameter #2 $callback of function array_find expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 52, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 55, + ], + ]); + } + + public function testArrayFindKeyCallback(): void + { + $this->analyse([__DIR__ . '/data/array_find_key.php'], [ + [ + 'Parameter #2 $callback of function array_find_key expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 52, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 55, + ], + ]); + } + + public function testBug5356(): void + { $this->analyse([__DIR__ . '/data/bug-5356.php'], [ [ 'Parameter #1 $callback of function array_map expects (callable(string): mixed)|null, Closure(array): \'a\' given.', @@ -890,7 +1036,7 @@ public function testBug2782(): void { $this->analyse([__DIR__ . '/data/bug-2782.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(stdClass, stdClass): int, Closure(int, int): -1|1 given.', + 'Parameter #2 $callback of function usort expects callable(stdClass, stdClass): int, Closure(int, int): (-1|1) given.', 13, ], ]); @@ -948,10 +1094,6 @@ public function testCallUserFuncArray(): void 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'], []); } @@ -967,4 +1109,1137 @@ public function testBug4413(): void ]); } + public function testBug6383(): void + { + $this->analyse([__DIR__ . '/data/bug-6383.php'], []); + } + + public function testBug6448(): void + { + $errors = []; + if (PHP_VERSION_ID < 80100) { + $errors[] = [ + 'Function fputcsv invoked with 6 parameters, 2-5 required.', + 28, + ]; + } + $this->analyse([__DIR__ . '/data/bug-6448.php'], $errors); + } + + public function testBug7017(): void + { + $errors = []; + if (PHP_VERSION_ID < 80100) { + $errors[] = [ + 'Parameter #1 $finfo of function finfo_close expects resource, finfo given.', + 7, + ]; + } + $this->analyse([__DIR__ . '/data/bug-7017.php'], $errors); + } + + public function testBug4371(): void + { + $errors = [ + [ + 'Parameter #1 $object_or_class of function is_a expects object, string given.', + 14, + ], + [ + 'Parameter #1 $object_or_class of function is_a expects object, string given.', + 22, + ], + ]; + + if (PHP_VERSION_ID < 80000) { + // php 7.x had different parameter names + $errors = [ + [ + 'Parameter #1 $object_or_string of function is_a expects object, string given.', + 14, + ], + [ + 'Parameter #1 $object_or_string of function is_a expects object, string given.', + 22, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/bug-4371.php'], $errors); + } + + public function testIsSubclassAllowString(): void + { + $errors = [ + [ + 'Parameter #1 $object_or_class of function is_subclass_of expects object, string given.', + 11, + ], + [ + 'Parameter #1 $object_or_class of function is_subclass_of expects object, string given.', + 14, + ], + [ + 'Parameter #1 $object_or_class of function is_subclass_of expects object, string given.', + 17, + ], + ]; + + if (PHP_VERSION_ID < 80000) { + // php 7.x had different parameter names + $errors = [ + [ + 'Parameter #1 $object_or_string of function is_subclass_of expects object, string given.', + 11, + ], + [ + 'Parameter #1 $object_or_string of function is_subclass_of expects object, string given.', + 14, + ], + [ + 'Parameter #1 $object_or_string of function is_subclass_of expects object, string given.', + 17, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/is-subclass-allow-string.php'], $errors); + } + + public function testBug6987(): void + { + $this->analyse([__DIR__ . '/data/bug-6987.php'], []); + } + + public function testDiscussion7450WithoutCheckExplicitMixed(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/discussion-7450.php'], []); + } + + public function testDiscussion7450WithCheckExplicitMixed(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/discussion-7450.php'], [ + [ + 'Parameter #1 $foo of function Discussion7450\foo expects array{policy: non-empty-string, entitlements: array}, array{policy: mixed, entitlements: mixed} given.', + 18, + "• Offset 'policy' (non-empty-string) does not accept type mixed. +• Offset 'entitlements' (array) does not accept type mixed.", + ], + [ + 'Parameter #1 $foo of function Discussion7450\foo expects array{policy: non-empty-string, entitlements: array}, array{policy: mixed, entitlements: mixed} given.', + 28, + "• Offset 'policy' (non-empty-string) does not accept type mixed. +• Offset 'entitlements' (array) does not accept type mixed.", + ], + ]); + } + + public function testBug7211(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7211.php'], []); + } + + public function testBug5474(): void + { + $this->analyse([__DIR__ . '/../Comparison/data/bug-5474.php'], []); + } + + public function testBug6261(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6261.php'], []); + } + + public function testBug6781(): void + { + $this->analyse([__DIR__ . '/data/bug-6781.php'], []); + } + + public function testBug2343(): void + { + $this->analyse([__DIR__ . '/data/bug-2343.php'], []); + } + + public function testBug7676(): void + { + $this->analyse([__DIR__ . '/data/bug-7676.php'], []); + } + + public function testBug7138(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + $this->analyse([__DIR__ . '/data/bug-7138.php'], []); + } + + public function testBug2911(): void + { + $this->analyse([__DIR__ . '/data/bug-2911.php'], [ + [ + 'Parameter #1 $array of function Bug2911\bar expects array{bar: string}, non-empty-array given.', + 23, + ], + ]); + } + + public function testBug7156(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7156.php'], []); + } + + public function testBug7973(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7973.php'], []); + } + + public function testBug7562(): void + { + $this->analyse([__DIR__ . '/data/bug-7562.php'], []); + } + + public function testBug7823(): void + { + $this->analyse([__DIR__ . '/data/bug-7823.php'], [ + [ + 'Parameter #1 $s of function Bug7823\sayHello expects literal-string, class-string given.', + 34, + ], + ]); + } + + public function testCurlSetOpt(): void + { + $this->analyse([__DIR__ . '/data/curl_setopt.php'], [ + [ + 'Parameter #3 $value of function curl_setopt expects 0|2, bool given.', + 10, + ], + [ + 'Parameter #3 $value of function curl_setopt expects non-empty-string, int given.', + 16, + ], + [ + 'Parameter #3 $value of function curl_setopt expects array, int given.', + 17, + ], + [ + 'Parameter #3 $value of function curl_setopt expects non-empty-string, null given.', + 18, + ], + [ + 'Parameter #3 $value of function curl_setopt expects string, int given.', + 19, + ], + [ + 'Parameter #3 $value of function curl_setopt expects string, int given.', + 20, + ], + [ + 'Parameter #3 $value of function curl_setopt expects bool, int given.', + 22, + ], + [ + 'Parameter #3 $value of function curl_setopt expects bool, string given.', + 23, + ], + [ + 'Parameter #3 $value of function curl_setopt expects int, string given.', + 25, + ], + [ + 'Parameter #3 $value of function curl_setopt expects array, string given.', + 27, + ], + [ + 'Parameter #3 $value of function curl_setopt expects resource, string given.', + 29, + ], + [ + 'Parameter #3 $value of function curl_setopt expects array|string, int given.', + 31, + ], + [ + 'Parameter #3 $value of function curl_setopt expects non-empty-string, \'\' given.', + 33, + ], + [ + 'Parameter #3 $value of function curl_setopt expects non-empty-string|null, \'\' given.', + 34, + ], + [ + 'Parameter #3 $value of function curl_setopt expects array, array given.', + 77, + ], + ]); + } + + public function testBug8280(): void + { + $this->analyse([__DIR__ . '/data/bug-8280.php'], []); + } + + public function testBug8389(): void + { + $this->analyse([__DIR__ . '/data/bug-8389.php'], []); + } + + public function testBug8449(): void + { + $this->analyse([__DIR__ . '/data/bug-8449.php'], []); + } + + public function testBug5288(): void + { + $this->analyse([__DIR__ . '/data/bug-5288.php'], []); + } + + public function testBug5986(): void + { + $this->analyse([__DIR__ . '/data/bug-5986.php'], [ + [ + 'Parameter #1 $data of function Bug5986\test2 expects array{mov?: int, appliesTo?: string, expireDate?: string|null, effectiveFrom?: int, merchantId?: int, link?: string, channel?: string, voucherExternalId?: int}, array{mov?: int, appliesTo?: string, expireDate?: string|null, effectiveFrom?: string, merchantId?: int, link?: string, channel?: string, voucherExternalId?: int} given.', + 18, + "Offset 'effectiveFrom' (int) does not accept type string.", + ], + ]); + } + + public function testBug7239(): void + { + $tipText = 'array{} is empty.'; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7239.php'], [ + [ + 'Parameter #1 ...$arg1 of function max expects non-empty-array, array{} given.', + 16, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function min expects non-empty-array, array{} given.', + 17, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function max expects non-empty-array, array{} given.', + 23, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function min expects non-empty-array, array{} given.', + 24, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function max expects non-empty-array, array{} given.', + 34, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function min expects non-empty-array, array{} given.', + 35, + $tipText, + ], + ]); + } + + public function testFilterInputType(): void + { + $errors = [ + [ + 'Parameter #1 $type of function filter_input expects 0|1|2|4|5, -1 given.', + 16, + ], + [ + 'Parameter #1 $type of function filter_input expects 0|1|2|4|5, int given.', + 17, + ], + [ + 'Parameter #1 $type of function filter_input_array expects 0|1|2|4|5, -1 given.', + 28, + ], + [ + 'Parameter #1 $type of function filter_input_array expects 0|1|2|4|5, int given.', + 29, + ], + ]; + + if (PHP_VERSION_ID < 80000) { + $errors = []; + } + + $this->analyse([__DIR__ . '/data/filter-input-type.php'], $errors); + } + + public function testBug9283(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9283.php'], []); + } + + public function testBug9380(): void + { + $errors = [ + [ + 'Parameter #2 $message_type of function error_log expects 0|1|3|4, 2 given.', + 7, + ], + ]; + + if (PHP_VERSION_ID < 80000) { + $errors = []; + } + + $this->analyse([__DIR__ . '/data/bug-9380.php'], $errors); + } + + public function testBenevolentSuperglobalKeys(): void + { + $this->analyse([__DIR__ . '/data/benevolent-superglobal-keys.php'], []); + } + + public function testFileParams(): void + { + $this->analyse([__DIR__ . '/data/file.php'], [ + [ + 'Parameter #2 $flags of function file expects 0|1|2|3|4|5|6|7|16|17|18|19|20|21|22|23, 8 given.', + 16, + ], + ]); + } + + public function testFlockParams(): void + { + $this->analyse([__DIR__ . '/data/flock.php'], [ + [ + 'Parameter #2 $operation of function flock expects int<0, 7>, 8 given.', + 45, + ], + ]); + } + + public function testJsonValidate(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3'); + } + + $this->analyse([__DIR__ . '/data/json_validate.php'], [ + [ + 'Parameter #2 $depth of function json_validate expects int<1, max>, 0 given.', + 6, + ], + [ + 'Parameter #3 $flags of function json_validate expects 0|1048576, 2 given.', + 7, + ], + ]); + } + + public function testBug4612(): void + { + $this->analyse([__DIR__ . '/data/bug-4612.php'], []); + } + + public function testBug2508(): void + { + $this->analyse([__DIR__ . '/data/bug-2508.php'], []); + } + + public function testBug6175(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-6175.php'], []); + } + + public function testBug9699(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/bug-9699.php'], [ + [ + 'Parameter #1 $f of function Bug9699\int_int_int_string expects Closure(int, int, int, string): int, Closure(int, int, int ...): int given.', + 19, + ], + ]); + } + + public function testBug9133(): void + { + $this->analyse([__DIR__ . '/data/bug-9133.php'], [ + [ + 'Parameter #1 $value of function Bug9133\assertNever expects never, int given.', + 29, + ], + ]); + } + + public function testBug9803(): void + { + $this->analyse([__DIR__ . '/data/bug-9803.php'], []); + } + + public function testBug9018(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9018.php'], [ + [ + 'Unknown parameter $str1 in call to function levenshtein.', + 13, + ], + [ + 'Unknown parameter $str2 in call to function levenshtein.', + 13, + ], + [ + 'Missing parameter $string1 (string) in call to function levenshtein.', + 13, + ], + [ + 'Missing parameter $string2 (string) in call to function levenshtein.', + 13, + ], + ]); + } + + public function testBug9399(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9399.php'], []); + } + + public function testBug9559(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9559.php'], []); + } + + public function testBug9923(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9923.php'], []); + } + + public function testBug9823(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9823.php'], []); + } + + public function testNamedParametersForMultiVariantFunctions(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/call-to-function-named-params-multivariant.php'], []); + } + + public function testBug9793(): void + { + $errors = []; + + if (PHP_VERSION_ID < 80200) { + $errors = [ + [ + 'Parameter #1 $iterator of function iterator_to_array expects Traversable, array given.', + 13, + ], + [ + 'Parameter #1 $iterator of function iterator_to_array expects Traversable, array|Iterator given.', + 14, + ], + [ + 'Parameter #1 $iterator of function iterator_count expects Traversable, array given.', + 15, + ], + [ + 'Parameter #1 $iterator of function iterator_count expects Traversable, array|Iterator given.', + 16, + ], + ]; + } + + $errors[] = [ + 'Parameter #1 $iterator of function iterator_apply expects Traversable, array given.', + 17, + ]; + $errors[] = [ + 'Parameter #1 $iterator of function iterator_apply expects Traversable, array|Iterator given.', + 18, + ]; + + $this->analyse([__DIR__ . '/data/bug-9793.php'], $errors); + } + + public function testCallToArrayFilterWithNullCallback(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/array_filter_null_callback.php'], []); + } + + public function testBug10171(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-10171.php'], [ + [ + 'Unknown parameter $samesite in call to function setcookie.', + 12, + ], + [ + 'Function setcookie invoked with 9 parameters, 1-7 required.', + 13, + ], + [ + 'Unknown parameter $samesite in call to function setrawcookie.', + 25, + ], + [ + 'Function setrawcookie invoked with 9 parameters, 1-7 required.', + 26, + ], + ]); + } + + public function testBug6720(): void + { + $this->analyse([__DIR__ . '/data/bug-6720.php'], []); + } + + public function testBug8659(): void + { + $this->analyse([__DIR__ . '/data/bug-8659.php'], []); + } + + public function testBug9580(): void + { + $this->analyse([__DIR__ . '/data/bug-9580.php'], []); + } + + public function testBug7283(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-7283.php'], []); + } + + public function testBug9697(): void + { + $this->analyse([__DIR__ . '/data/bug-9697.php'], []); + } + + public function testDiscussion10454(): void + { + $this->analyse([__DIR__ . '/data/discussion-10454.php'], [ + [ + "Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.", + 13, + ], + [ + "Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.", + 23, + ], + ]); + } + + public function testBug10527(): void + { + $this->analyse([__DIR__ . '/data/bug-10527.php'], []); + } + + public function testBug10626(): void + { + $this->analyse([__DIR__ . '/data/bug-10626.php'], [ + [ + 'Parameter #1 $value of function Bug10626\intByValue expects int, string given.', + 16, + ], + [ + 'Parameter #1 $value of function Bug10626\intByReference expects int, string given.', + 17, + ], + ]); + } + + public function testArgon2PasswordHash(): void + { + $this->analyse([__DIR__ . '/data/argon2id-password-hash.php'], []); + } + + public function testBug4960(): void + { + $this->analyse([__DIR__ . '/data/bug-4960.php'], []); + } + + public function testParamClosureThis(): void + { + $this->analyse([__DIR__ . '/data/function-call-param-closure-this.php'], [ + [ + 'Parameter #1 $cb of function FunctionCallParamClosureThis\acceptClosure expects bindable closure, static closure given.', + 18, + ], + [ + 'Parameter #1 $cb of function FunctionCallParamClosureThis\acceptClosure expects bindable closure, static closure given.', + 23, + ], + ]); + } + + public function testBug10297(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-10297.php'], []); + } + + public function testBug10974(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-10974.php'], []); + } + + public function testCountArrayShift(): void + { + if (PHP_VERSION_ID < 80000) { + $errors = [ + [ + 'Parameter #1 $var of function count expects array|Countable, array|false given.', + 8, + ], + [ + 'Parameter #1 $var of function count expects array|Countable, array|false given.', + 16, + ], + ]; + } else { + $errors = [ + [ + 'Parameter #1 $value of function count expects array|Countable, array|false given.', + 8, + ], + [ + 'Parameter #1 $value of function count expects array|Countable, array|false given.', + 16, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/count-array-shift.php'], $errors); + } + + public function testArrayDiffUassoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_diff_uassoc.php'], [ + [ + 'Parameter #3 $data_comp_func of function array_diff_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $data_comp_func of function array_diff_uassoc expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayDiffUkey(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_diff_ukey.php'], [ + [ + 'Parameter #3 $key_comp_func of function array_diff_ukey expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $key_comp_func of function array_diff_ukey expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayIntersectUassoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_intersect_uassoc.php'], [ + [ + 'Parameter #3 $key_compare_func of function array_intersect_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $key_compare_func of function array_intersect_uassoc expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayIntersectUkey(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_intersect_ukey.php'], [ + [ + 'Parameter #3 $key_compare_func of function array_intersect_ukey expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $key_compare_func of function array_intersect_ukey expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayUdiffAssoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_udiff_assoc.php'], [ + [ + 'Parameter #3 $key_comp_func of function array_udiff_assoc expects callable(1|2, 1|2): int, Closure(string, string): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $key_comp_func of function array_udiff_assoc expects callable(1|2|3|4|5, 1|2|3|4|5): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayUdiffUasssoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_udiff_uassoc.php'], [ + [ + 'Parameter #3 $data_comp_func of function array_udiff_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 28, + ], + [ + 'Parameter #4 $key_comp_func of function array_udiff_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 31, + ], + [ + 'Parameter #3 $data_comp_func of function array_udiff_uassoc expects callable(1|2|3|4|5, 1|2|3|4|5): int, Closure(string, string): int<-1, 1> given.', + 39, + ], + [ + 'Parameter #4 $key_comp_func of function array_udiff_uassoc expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 42, + ], + ]); + } + + public function testArrayUintersectAssoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_uintersect_assoc.php'], [ + [ + 'Parameter #3 $data_compare_func of function array_uintersect_assoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $data_compare_func of function array_uintersect_assoc expects callable(1|2|3|4, 1|2|3|4): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayUintersectUassoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_uintersect_uassoc.php'], [ + [ + 'Parameter #3 $data_compare_func of function array_uintersect_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 28, + ], + [ + 'Parameter #4 $key_compare_func of function array_uintersect_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 31, + ], + [ + 'Parameter #3 $data_compare_func of function array_uintersect_uassoc expects callable(1|2|3|4|5, 1|2|3|4|5): int, Closure(string, string): int<-1, 1> given.', + 39, + ], + [ + 'Parameter #4 $key_compare_func of function array_uintersect_uassoc expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 42, + ], + ]); + } + + public function testArrayUintersect(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_uintersect.php'], [ + [ + 'Parameter #3 $data_compare_func of function array_uintersect expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $data_compare_func of function array_uintersect expects callable(1|2|3|4, 1|2|3|4): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testBug7707(): void + { + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-7707.php'], []); + } + + public function testNoNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/no-named-arguments.php'], [ + [ + 'Function NoNamedArgumentsFunction\\foo invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 14, + ], + [ + 'Function NoNamedArgumentsFunction\foo invoked with unpacked array with string key, but it\'s not allowed because of @no-named-arguments.', + 24, + ], + [ + 'Function NoNamedArgumentsFunction\foo invoked with unpacked array with possibly string key, but it\'s not allowed because of @no-named-arguments.', + 25, + ], + [ + 'Function NoNamedArgumentsFunction\\foo invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 29, + ], + ]); + } + + public function testBug11056(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-11056.php'], []); + } + + public function testBug11506(): void + { + $this->analyse([__DIR__ . '/data/bug-11506.php'], []); + } + + public function testBug11559(): void + { + $this->analyse([__DIR__ . '/data/bug-11559.php'], []); + } + + public function testBug10499(): void + { + $this->analyse([__DIR__ . '/data/bug-10499.php'], []); + } + + public function testBug11559b(): void + { + $this->analyse([__DIR__ . '/data/bug-11559b.php'], [ + [ + 'Function Bug11559b\maybe_variadic_fn invoked with 5 parameters, 0 required.', + 14, + ], + [ + 'Function Bug11559b\maybe_variadic_fn4 invoked with 2 parameters, 0 required.', + 65, + ], + ]); + } + + public function testBug9224(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9224.php'], []); + } + + public function testBug7082(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7082.php'], [ + [ + 'Parameter #1 $val of function Bug7082\takesStr expects string, mixed given.', + 11, + ], + ]); + } + + public function testBug11759(): void + { + $this->analyse([__DIR__ . '/data/bug-11759.php'], []); + } + + public function testBug12051(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12051.php'], []); + } + + public function testBug8046(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8046.php'], []); + } + + public function testBug11942(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11942.php'], []); + } + + public function testBug11418(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11418.php'], []); + } + + public function testBug9167(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9167.php'], []); + } + + public function testBug3107(): void + { + $this->analyse([__DIR__ . '/data/bug-3107.php'], []); + } + + public function testBug12676(): void + { + $errors = [ + [ + 'Parameter #1 $array is passed by reference so it does not accept @readonly property Bug12676\A::$a.', + 15, + ], + [ + 'Parameter #1 $array is passed by reference so it does not accept @readonly property Bug12676\B::$readonlyArr.', + 25, + ], + [ + 'Parameter #1 $array is passed by reference so it does not accept static @readonly property Bug12676\C::$readonlyArr.', + 35, + ], + ]; + + if (PHP_VERSION_ID < 80000) { + $errors = [ + [ + 'Parameter #1 $array_arg is passed by reference so it does not accept @readonly property Bug12676\A::$a.', + 15, + ], + [ + 'Parameter #1 $array_arg is passed by reference so it does not accept @readonly property Bug12676\B::$readonlyArr.', + 25, + ], + [ + 'Parameter #1 $array_arg is passed by reference so it does not accept static @readonly property Bug12676\C::$readonlyArr.', + 35, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/bug-12676.php'], $errors); + } + + public function testBug12499(): void + { + $this->analyse([__DIR__ . '/data/bug-12499.php'], []); + } + + public function testBug7522(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7522.php'], []); + } + + public function testBug12847(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12847.php'], [ + [ + 'Parameter #1 $array of function Bug12847\doSomething expects non-empty-array, mixed given.', + 32, + 'mixed is empty.', + ], + [ + 'Parameter #1 $array of function Bug12847\doSomething expects non-empty-array, mixed given.', + 39, + 'mixed is empty.', + ], + [ + 'Parameter #1 $array of function Bug12847\doSomethingWithInt expects non-empty-array, non-empty-array given.', + 61, + ], + [ + 'Parameter #1 $array of function Bug12847\doSomethingWithInt expects non-empty-array, non-empty-array given.', + 67, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php index d05508237c..85c1f400ee 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php @@ -24,6 +24,33 @@ public function testRule(): void 'Call to function sprintf() on a separate line has no effect.', 13, ], + [ + 'Call to function var_export() on a separate line has no effect.', + 24, + ], + [ + 'Call to function print_r() on a separate line has no effect.', + 26, + ], + ]); + + if (PHP_VERSION_ID < 80000) { + return; + } + + $this->analyse([__DIR__ . '/data/function-call-statement-no-side-effects-8.0.php'], [ + [ + 'Call to function var_export() on a separate line has no effect.', + 19, + ], + [ + 'Call to function print_r() on a separate line has no effect.', + 20, + ], + [ + 'Call to function highlight_string() on a separate line has no effect.', + 21, + ], ]); } @@ -44,9 +71,17 @@ public function testPhpDoc(): void 10, ], [ - 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pureAndThrowsVoid() on a separate line has no effect.', + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure4() on a separate line has no effect.', 11, ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure5() on a separate line has no effect.', + 12, + ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pureAndThrowsVoid() on a separate line has no effect.', + 13, + ], ]); } @@ -58,10 +93,6 @@ public function testBug4455(): void 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.', diff --git a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php index a29f282847..6707068e04 100644 --- a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php @@ -14,7 +14,12 @@ class CallToNonExistentFunctionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToNonExistentFunctionRule($this->createReflectionProvider(), true); + return new CallToNonExistentFunctionRule($this->createReflectionProvider(), true, true); + } + + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; } public function testEmptyFile(): void @@ -35,7 +40,6 @@ public function testCallToNonexistentFunction(): void 'Function foobarNonExistentFunction not found.', 5, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], ]); } @@ -47,7 +51,6 @@ public function testCallToNonexistentNestedFunction(): void 'Function barNonExistentFunction not found.', 5, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], ]); } @@ -73,10 +76,6 @@ public function testCallToIncorrectCaseFunctionName(): void public function testMatchExprAnalysis(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/match-expr-analysis.php'], [ [ 'Function lorem not found.', @@ -101,6 +100,61 @@ public function testMatchExprAnalysis(): void ]); } + public function testCallToRemovedFunctionsOnPhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/removed-functions-from-php8.php'], [ + [ + 'Function convert_cyr_string not found.', + 3, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function ezmlm_hash not found.', + 4, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function fgetss not found.', + 5, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function get_magic_quotes_gpc not found.', + 6, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function hebrevc not found.', + 7, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function imap_header not found.', + 8, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function ldap_control_paged_result not found.', + 9, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function ldap_control_paged_result_response not found.', + 10, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function restore_include_path not found.', + 11, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + public function testCreateFunctionPhp8(): void { if (PHP_VERSION_ID < 80000) { @@ -161,4 +215,63 @@ public function testBug3576(): void ]); } + public function testBug7952(): void + { + $this->analyse([__DIR__ . '/data/bug-7952.php'], []); + } + + public function testBug8058(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2'); + } + + $this->analyse([__DIR__ . '/../Methods/data/bug-8058.php'], []); + } + + public function testBug8058b(): void + { + if (PHP_VERSION_ID >= 80200) { + $this->markTestSkipped('Test requires PHP before 8.2'); + } + + $this->analyse([__DIR__ . '/../Methods/data/bug-8058.php'], [ + [ + 'Function mysqli_execute_query not found.', + 13, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testBug8205(): void + { + $this->analyse([__DIR__ . '/data/bug-8205.php'], []); + } + + public function testBug10003(): void + { + $this->analyse([__DIR__ . '/data/bug-10003.php'], [ + [ + 'Call to function MongoDB\Driver\Monitoring\addSubscriber() with incorrect case: MONGODB\Driver\Monitoring\addSubscriber', + 10, + ], + [ + 'Call to function MongoDB\Driver\Monitoring\addSubscriber() with incorrect case: mongodb\driver\monitoring\addsubscriber', + 14, + ], + ]); + } + + public function testRememberFunctionExistsFromConstructor(): void + { + $this->analyse([__DIR__ . '/data/remember-function-exists-from-constructor.php'], [ + [ + 'Function another_unknown_function not found.', + 32, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php new file mode 100644 index 0000000000..7ee37a1458 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -0,0 +1,111 @@ + + */ +class CallUserFuncRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new CallUserFuncRule($reflectionProvider, new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true)); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/call-user-func.php'], [ + [ + 'Callable passed to call_user_func() invoked with 0 parameters, 1 required.', + 15, + ], + [ + 'Parameter #1 $i of callable passed to call_user_func() expects int, string given.', + 17, + ], + [ + 'Parameter $i of callable passed to call_user_func() expects int, string given.', + 18, + ], + [ + 'Parameter $i of callable passed to call_user_func() expects int, string given.', + 19, + ], + [ + 'Unknown parameter $j in call to callable passed to call_user_func().', + 22, + ], + [ + 'Missing parameter $i (int) in call to callable passed to call_user_func().', + 22, + ], + [ + 'Callable passed to call_user_func() invoked with 0 parameters, 2-4 required.', + 30, + ], + [ + 'Callable passed to call_user_func() invoked with 1 parameter, 2-4 required.', + 31, + ], + [ + 'Callable passed to call_user_func() invoked with 0 parameters, at least 2 required.', + 40, + ], + [ + 'Callable passed to call_user_func() invoked with 1 parameter, at least 2 required.', + 41, + ], + [ + 'Result of callable passed to call_user_func() (void) is used.', + 43, + ], + ]); + } + + public function testBug7057(): void + { + $this->analyse([__DIR__ . '/data/bug-7057.php'], []); + } + + public function testNoNamedArguments(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/no-named-arguments-call-user-func.php'], [ + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 29, + ], + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 30, + ], + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 31, + ], + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 32, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index dadea27325..360ed89a3c 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -12,7 +13,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -27,9 +27,8 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -37,17 +36,19 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/closure-attributes.php'], [ [ 'Attribute class ClosureAttributes\Foo does not have the function target.', diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index fd3bb65cea..938020a0ba 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -15,7 +15,7 @@ class ClosureReturnTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false))); + return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true))); } public function testClosureReturnTypeRule(): void @@ -30,15 +30,15 @@ public function testClosureReturnTypeRule(): void 28, ], [ - 'Anonymous function should return ClosureReturnTypes\Foo but returns ClosureReturnTypes\Bar.', + 'Anonymous function should return ClosureReturnTypes\Bar&ClosureReturnTypes\Foo but returns ClosureReturnTypes\Bar.', 35, ], [ - 'Anonymous function should return SomeOtherNamespace\Foo but returns ClosureReturnTypes\Foo.', + 'Anonymous function should return ClosureReturnTypes\Foo&SomeOtherNamespace\Foo but returns ClosureReturnTypes\Foo.', 39, ], [ - 'Anonymous function should return SomeOtherNamespace\Baz but returns ClosureReturnTypes\Foo.', + 'Anonymous function should return ClosureReturnTypes\Foo&SomeOtherNamespace\Baz but returns ClosureReturnTypes\Foo.', 46, ], [ @@ -93,4 +93,44 @@ public function testBug3891(): void $this->analyse([__DIR__ . '/data/bug-3891.php'], []); } + public function testBug6806(): void + { + $this->analyse([__DIR__ . '/data/bug-6806.php'], []); + } + + public function testBug4739(): void + { + $this->analyse([__DIR__ . '/data/bug-4739.php'], []); + } + + public function testBug4739b(): void + { + $this->analyse([__DIR__ . '/data/bug-4739b.php'], []); + } + + public function testBug5753(): void + { + $this->analyse([__DIR__ . '/data/bug-5753.php'], []); + } + + public function testBug6559(): void + { + $this->analyse([__DIR__ . '/data/bug-6559.php'], []); + } + + public function testBug6902(): void + { + $this->analyse([__DIR__ . '/data/bug-6902.php'], []); + } + + public function testBug7220(): void + { + $this->analyse([__DIR__ . '/data/bug-7220.php'], []); + } + + public function testBugFunctionMethodConstants(): void + { + $this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/DuplicateFunctionDeclarationRuleTest.php b/tests/PHPStan/Rules/Functions/DuplicateFunctionDeclarationRuleTest.php new file mode 100644 index 0000000000..5a78c1d1aa --- /dev/null +++ b/tests/PHPStan/Rules/Functions/DuplicateFunctionDeclarationRuleTest.php @@ -0,0 +1,52 @@ + + */ +class DuplicateFunctionDeclarationRuleTest extends RuleTestCase +{ + + private const FILENAME = __DIR__ . '/data/duplicate-function.php'; + + protected function getRule(): Rule + { + $fileHelper = new FileHelper(__DIR__ . '/data'); + + return new DuplicateFunctionDeclarationRule( + new DefaultReflector(new OptimizedSingleFileSourceLocator( + self::getContainer()->getByType(FileNodesFetcher::class), + self::FILENAME, + )), + new SimpleRelativePathHelper($fileHelper->normalizePath($fileHelper->getWorkingDirectory(), '/')), + ); + } + + public function testRule(): void + { + $this->analyse([self::FILENAME], [ + [ + "Function DuplicateFunctionDeclaration\\foo declared multiple times:\n- duplicate-function.php:10\n- duplicate-function.php:15\n- duplicate-function.php:20", + 10, + ], + [ + "Function DuplicateFunctionDeclaration\\foo declared multiple times:\n- duplicate-function.php:10\n- duplicate-function.php:15\n- duplicate-function.php:20", + 15, + ], + [ + "Function DuplicateFunctionDeclaration\\foo declared multiple times:\n- duplicate-function.php:10\n- duplicate-function.php:15\n- duplicate-function.php:20", + 20, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index b7fedcce95..158d763831 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,15 +22,27 @@ class ExistingClassesInArrowFunctionTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInArrowFunctionTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + new PhpVersion(PHP_VERSION_ID), + ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/arrow-function-typehints.php'], [ [ 'Parameter $bar of anonymous function has invalid type ArrowFunctionExistingClassesInTypehints\Bar.', @@ -66,14 +80,10 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param mixed[] $errors + * @param list $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); } @@ -83,7 +93,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 17, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 19, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 25, + ], + ], ], [ 80000, @@ -100,6 +123,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 11, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 9, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 11, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 15, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 9, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 11, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 15, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 19, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 23, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], ], ], ]; @@ -107,12 +240,12 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param mixed[] $errors + * @param list $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); } $this->phpVersionId = $phpVersionId; @@ -149,17 +282,34 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $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); } + public function testNever(): void + { + $errors = []; + if (PHP_VERSION_ID < 80100) { + $errors = [ + [ + 'Anonymous function has invalid return type ArrowFunctionNever\never.', + 6, + ], + ]; + } elseif (PHP_VERSION_ID < 80200) { + $errors = [ + [ + 'Never return type in arrow function is supported only on PHP 8.2 and later.', + 6, + ], + ]; + } + $this->analyse([__DIR__ . '/data/arrow-function-never.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index 0508d55708..988b42b6a9 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,22 @@ class ExistingClassesInClosureTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInClosureTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInClosureTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void @@ -68,9 +84,6 @@ public function testValidTypehintPhp71(): void ]); } - /** - * @requires PHP 7.2 - */ public function testValidTypehintPhp72(): void { $this->analyse([__DIR__ . '/data/closure-7.2-typehints.php'], []); @@ -78,9 +91,6 @@ public function testValidTypehintPhp72(): void public function testVoidParameterTypehint(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } $this->analyse([__DIR__ . '/data/void-parameter-typehint.php'], [ [ 'Parameter $param of anonymous function has invalid type void.', @@ -114,14 +124,10 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param mixed[] $errors + * @param list $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); } @@ -131,7 +137,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 29, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 33, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 45, + ], + ], ], [ 80000, @@ -148,6 +167,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 17, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 45, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 45, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 45, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], ], ], ]; @@ -155,10 +284,14 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param mixed[] $errors + * @param list $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/required-parameter-after-optional-closures.php'], $errors); } @@ -193,17 +326,35 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $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); } + public function testDeprecatedImplicitlyNullableParameterType(): void + { + if (PHP_VERSION_ID < 80400) { + self::markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/closure-implicitly-nullable.php'], [ + [ + 'Deprecated in PHP 8.4: Parameter #3 $c (int) is implicitly nullable via default value null.', + 13, + ], + [ + 'Deprecated in PHP 8.4: Parameter #5 $e (int|string) is implicitly nullable via default value null.', + 15, + ], + [ + 'Deprecated in PHP 8.4: Parameter #7 $g (stdClass) is implicitly nullable via default value null.', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 8744cf88be..f03764a657 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,22 @@ class ExistingClassesInTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void @@ -156,9 +172,6 @@ public function testWithoutNamespace(): void public function testVoidParameterTypehint(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } $this->analyse([__DIR__ . '/data/void-parameter-typehint.php'], [ [ 'Parameter $param of function VoidParameterTypehint\doFoo() has invalid type void.', @@ -192,14 +205,10 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param mixed[] $errors + * @param list $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); } @@ -209,7 +218,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Function RequiredAfterOptional\doAmet() uses native union types but they're supported only on PHP 8.0 and later.", + 34, + ], + [ + "Function RequiredAfterOptional\doConsectetur() uses native union types but they're supported only on PHP 8.0 and later.", + 38, + ], + [ + "Function RequiredAfterOptional\doSed() uses native union types but they're supported only on PHP 8.0 and later.", + 50, + ], + ], ], [ 80000, @@ -226,6 +248,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 18, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 14, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 18, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 30, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 50, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 14, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 18, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 30, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 38, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 46, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 50, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 50, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], ], ], ]; @@ -233,13 +365,14 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param mixed[] $errors + * @param list $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.'); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); } + $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/required-parameter-after-optional.php'], $errors); } @@ -274,17 +407,103 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $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 dataTrueTypes(): array + { + return [ + [ + 80200, + [], + ], + ]; + } + + public function testTrueTypehint(): void + { + if (PHP_VERSION_ID >= 80200) { + $errors = []; + } else { + $errors = [ + [ + 'Function NativeTrueType\alwaysTrue() has invalid return type NativeTrueType\true.', + 5, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/true-typehint.php'], $errors); + } + + public function testConditionalReturnType(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/conditional-return-type.php'], [ + [ + 'Template type T of function FunctionConditionalReturnType\notGet() is not referenced in a parameter.', + 17, + ], + ]); + } + + public function testTemplateInParamOut(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'Template type S of function ParamOutTemplate\uselessGeneric() is not referenced in a parameter.', + 9, + ], + ]); + } + + public function testParamOutClasses(): void + { + $this->analyse([__DIR__ . '/data/param-out-classes.php'], [ + [ + 'Parameter $p of function ParamOutClasses\doFoo() has invalid type ParamOutClasses\Nonexistent.', + 20, + ], + [ + 'Parameter $q of function ParamOutClasses\doFoo() has invalid type ParamOutClasses\FooTrait.', + 20, + ], + [ + 'Class ParamOutClasses\Foo referenced with incorrect case: ParamOutClasses\fOO.', + 20, + ], + ]); + } + + public function testParamClosureThisClasses(): void + { + $this->analyse([__DIR__ . '/data/param-closure-this-classes.php'], [ + [ + 'Parameter $a of function ParamClosureThisClassesFunctions\doFoo() has invalid type ParamClosureThisClassesFunctions\Nonexistent.', + 21, + ], + [ + 'Parameter $b of function ParamClosureThisClassesFunctions\doFoo() has invalid type ParamClosureThisClassesFunctions\FooTrait.', + 22, + ], + [ + 'Class ParamClosureThisClassesFunctions\Foo referenced with incorrect case: ParamClosureThisClassesFunctions\fOO.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 3dd6443b03..2fc38ca188 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -12,7 +13,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -27,9 +27,8 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -37,17 +36,19 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/function-attributes.php'], [ [ 'Attribute class FunctionAttributes\Foo does not have the function target.', diff --git a/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php index b19b9b0c2b..18bd5ed206 100644 --- a/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php @@ -20,7 +20,7 @@ protected function getRule(): Rule return new FunctionCallableRule( $reflectionProvider, - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new PhpVersion(PHP_VERSION_ID), true, true, @@ -32,10 +32,6 @@ 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.', diff --git a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php deleted file mode 100644 index c151af825f..0000000000 --- a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -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/ImplodeParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..8f2d3415b6 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php @@ -0,0 +1,113 @@ + + */ +class ImplodeParameterCastableToStringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new ImplodeParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false, true))); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/implode-param-castable-to-string-functions-named-args.php'], [ + [ + 'Parameter $array of function implode expects array, array> given.', + 8, + ], + [ + 'Parameter $separator of function implode expects array, array> given.', + 9, + ], + [ + 'Parameter $array of function implode expects array, array> given.', + 10, + ], + [ + 'Parameter $array of function implode expects array, array> given.', + 11, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/implode-param-castable-to-string-functions-enum.php'], [ + [ + 'Parameter #2 $array of function implode expects array, array given.', + 12, + ], + ]); + } + + public function testImplode(): 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, + ], + ]); + } + + public function testBug6000(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-6000.php'], []); + } + + public function testBug8467a(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-8467a.php'], []); + } + + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], [ + [ + 'Parameter #2 $array of function implode expects array, array given.', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRuleTest.php new file mode 100644 index 0000000000..b6a79b6817 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRuleTest.php @@ -0,0 +1,29 @@ + + */ +class IncompatibleArrowFunctionDefaultParameterTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleArrowFunctionDefaultParameterTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-default-parameter-type-arrow-functions.php'], [ + [ + 'Default value of the parameter #1 $i (string) of anonymous function is incompatible with type int.', + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php new file mode 100644 index 0000000000..88ce97861e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php @@ -0,0 +1,29 @@ + + */ +class IncompatibleClosureFunctionDefaultParameterTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleClosureDefaultParameterTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-default-parameter-type-closure.php'], [ + [ + 'Default value of the parameter #1 $i (string) of anonymous function is incompatible with type int.', + 19, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/InvalidLexicalVariablesInClosureUseRuleTest.php b/tests/PHPStan/Rules/Functions/InvalidLexicalVariablesInClosureUseRuleTest.php new file mode 100644 index 0000000000..5efe470207 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/InvalidLexicalVariablesInClosureUseRuleTest.php @@ -0,0 +1,117 @@ + + */ +class InvalidLexicalVariablesInClosureUseRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidLexicalVariablesInClosureUseRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/invalid-lexical-variables-in-closure-use.php'], [ + [ + 'Cannot use $this as lexical variable.', + 25, + ], + [ + 'Cannot use superglobal variable $GLOBALS as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_COOKIE as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_ENV as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_FILES as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_GET as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_POST as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_REQUEST as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_SERVER as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_SESSION as lexical variable.', + 35, + ], + [ + 'Cannot use lexical variable $baz since a parameter with the same name already exists.', + 55, + ], + [ + 'Cannot use $this as lexical variable.', + 68, + ], + [ + 'Cannot use superglobal variable $GLOBALS as lexical variable.', + 81, + ], + [ + 'Cannot use superglobal variable $_COOKIE as lexical variable.', + 82, + ], + [ + 'Cannot use superglobal variable $_ENV as lexical variable.', + 83, + ], + [ + 'Cannot use superglobal variable $_FILES as lexical variable.', + 84, + ], + [ + 'Cannot use superglobal variable $_GET as lexical variable.', + 85, + ], + [ + 'Cannot use superglobal variable $_POST as lexical variable.', + 86, + ], + [ + 'Cannot use superglobal variable $_REQUEST as lexical variable.', + 87, + ], + [ + 'Cannot use superglobal variable $_SERVER as lexical variable.', + 88, + ], + [ + 'Cannot use superglobal variable $_SESSION as lexical variable.', + 89, + ], + [ + 'Cannot use lexical variable $baz since a parameter with the same name already exists.', + 111, + ], + [ + 'Cannot use lexical variable $bar since a parameter with the same name already exists.', + 112, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php index 44967df450..c88fb550da 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php @@ -14,8 +14,7 @@ class MissingFunctionParameterTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); + return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -37,52 +36,63 @@ public function testRule(): void [ 'Function MissingFunctionParameterTypehint\missingArrayTypehint() has parameter $a with no value type specified in iterable type array.', 36, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingPhpDocIterableTypehint() has parameter $a with no value type specified in iterable type array.', 44, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\unionTypeWithUnknownArrayValueTypehint() has parameter $a with no value type specified in iterable type array.', 60, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\acceptsGenericInterface() has parameter $i with generic interface MissingFunctionParameterTypehint\GenericInterface but does not specify its types: T, U', 111, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionParameterTypehint\acceptsGenericClass() has parameter $c with generic class MissingFunctionParameterTypehint\GenericClass but does not specify its types: A, B', 130, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionParameterTypehint\missingIterableTypehint() has parameter $iterable with no value type specified in iterable type iterable.', 135, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingIterableTypehintPhpDoc() has parameter $iterable with no value type specified in iterable type iterable.', 143, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingTraversableTypehint() has parameter $traversable with no value type specified in iterable type Traversable.', 148, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingTraversableTypehintPhpDoc() has parameter $traversable with no value type specified in iterable type Traversable.', 156, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingCallableSignature() has parameter $cb with no signature specified for callable.', 161, ], + [ + 'Function MissingParamOutType\oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 173, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Function MissingParamOutType\generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 181, + ], + [ + 'Function MissingParamClosureThisType\generics() has @param-closure-this PHPDoc tag for parameter $cb with generic class ReflectionClass but does not specify its types: T', + 191, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php index c5c43617c2..2b64aba5ba 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php @@ -14,8 +14,7 @@ class MissingFunctionReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); + return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -33,22 +32,19 @@ public function testRule(): void [ 'Function MissingFunctionReturnTypehint\unionTypeWithUnknownArrayValueTypehint() return type has no value type specified in iterable type array.', 51, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionReturnTypehint\returnsGenericInterface() return type with generic interface MissingFunctionReturnTypehint\GenericInterface does not specify its types: T, U', 70, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionReturnTypehint\returnsGenericClass() return type with generic class MissingFunctionReturnTypehint\GenericClass does not specify its types: A, B', 89, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionReturnTypehint\genericGenericMissingTemplateArgs() return type with generic class MissingFunctionReturnTypehint\GenericClass does not specify its types: A, B', 105, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionReturnTypehint\closureWithNoPrototype() return type has no signature specified for Closure.', diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index d386e3f05e..7f8e3cef19 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -12,7 +13,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -27,9 +27,8 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -37,17 +36,19 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/param-attributes.php'], [ [ 'Attribute class ParamAttributes\Foo does not have the parameter target.', @@ -64,4 +65,14 @@ public function testRule(): void ]); } + public function testSensitiveParameterAttribute(): void + { + $this->analyse([__DIR__ . '/data/sensitive-parameter.php'], []); + } + + public function testBug10298(): void + { + $this->analyse([__DIR__ . '/data/bug-10298.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php new file mode 100644 index 0000000000..e652375414 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php @@ -0,0 +1,176 @@ + + */ +class ParameterCastableToNumberRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false, true))); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/param-castable-to-number-functions.php'], $this->hackPhp74ErrorMessages([ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array> given.', + 20, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 21, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 22, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 23, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 24, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 25, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array> given.', + 27, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 28, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 29, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 30, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 31, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 32, + ], + ])); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-number-functions-named-args.php'], [ + [ + 'Parameter $array of function array_sum expects an array of values castable to number, array> given.', + 7, + ], + [ + 'Parameter $array of function array_product expects an array of values castable to number, array> given.', + 8, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-number-functions-enum.php'], [ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 12, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 13, + ], + ]); + } + + public function testBug11883(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11883.php'], [ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 13, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 14, + ], + ]); + } + + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackPhp74ErrorMessages([ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 16, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 22, + ], + ])); + } + + /** + * @param list $errors + * @return list + */ + private function hackPhp74ErrorMessages(array $errors): array + { + if (PHP_VERSION_ID >= 80000) { + return $errors; + } + + return array_map(static function (array $error): array { + $error[0] = str_replace( + [ + '$array of function array_sum', + '$array of function array_product', + 'array', + ], + [ + '$input of function array_sum', + '$input of function array_product', + 'array', + ], + $error[0], + ); + + return $error; + }, $errors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..368ce6b286 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php @@ -0,0 +1,256 @@ + + */ +class ParameterCastableToStringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new ParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false, true))); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/param-castable-to-string-functions.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_intersect expects an array of values castable to string, array given.', + 16, + ], + [ + 'Parameter #2 $arrays of function array_intersect expects an array of values castable to string, array given.', + 17, + ], + [ + 'Parameter #3 of function array_intersect expects an array of values castable to string, array given.', + 18, + ], + [ + 'Parameter #2 $arrays of function array_diff expects an array of values castable to string, array given.', + 19, + ], + [ + 'Parameter #2 $arrays of function array_diff_assoc expects an array of values castable to string, array given.', + 20, + ], + [ + 'Parameter #1 $keys of function array_combine expects an array of values castable to string, array> given.', + 22, + ], + [ + 'Parameter #1 $array of function natsort expects an array of values castable to string, array> given.', + 24, + ], + [ + 'Parameter #1 $array of function natcasesort expects an array of values castable to string, array> given.', + 25, + ], + [ + 'Parameter #1 $array of function array_count_values expects an array of values castable to string, array> given.', + 26, + ], + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array> given.', + 27, + ], + ])); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-string-functions-named-args.php'], [ + [ + 'Parameter $keys of function array_combine expects an array of values castable to string, array> given.', + 7, + ], + [ + 'Parameter $keys of function array_fill_keys expects an array of values castable to string, array> given.', + 9, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-string-functions-enum.php'], [ + [ + 'Parameter #1 $array of function array_intersect expects an array of values castable to string, array given.', + 12, + ], + [ + 'Parameter #2 $arrays of function array_intersect expects an array of values castable to string, array given.', + 13, + ], + [ + 'Parameter #3 of function array_intersect expects an array of values castable to string, array given.', + 14, + ], + [ + 'Parameter #2 $arrays of function array_diff expects an array of values castable to string, array given.', + 15, + ], + [ + 'Parameter #2 $arrays of function array_diff_assoc expects an array of values castable to string, array given.', + 16, + ], + [ + 'Parameter #1 $keys of function array_combine expects an array of values castable to string, array given.', + 18, + ], + [ + 'Parameter #1 $array of function natsort expects an array of values castable to string, array given.', + 20, + ], + [ + 'Parameter #1 $array of function natcasesort expects an array of values castable to string, array given.', + 21, + ], + [ + 'Parameter #1 $array of function array_count_values expects an array of values castable to string, array given.', + 22, + ], + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', + 23, + ], + ]); + } + + public function testBug5848(): void + { + $this->analyse([__DIR__ . '/data/bug-5848.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_diff expects an array of values castable to string, array given.', + 8, + ], + [ + 'Parameter #2 $arrays of function array_diff expects an array of values castable to string, array given.', + 8, + ], + ])); + } + + public function testBug3946(): void + { + $this->analyse([__DIR__ . '/data/bug-3946.php'], [ + [ + 'Parameter #1 $keys of function array_combine expects an array of values castable to string, array|string> given.', + 8, + ], + ]); + } + + public function testBug11111(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11111.php'], [ + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', + 23, + ], + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', + 26, + ], + ]); + } + + public function testBug11141(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11141.php'], [ + [ + 'Parameter #1 $array of function array_diff expects an array of values castable to string, array given.', + 22, + ], + [ + 'Parameter #2 $arrays of function array_diff expects an array of values castable to string, array given.', + 22, + ], + ]); + } + + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_intersect expects an array of values castable to string, array given.', + 34, + ], + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', + 40, + ], + ])); + } + + /** + * @param list $errors + * @return list + */ + private function hackParameterNames(array $errors): array + { + if (PHP_VERSION_ID >= 80000) { + return $errors; + } + + return array_map(static function (array $error): array { + $error[0] = str_replace( + [ + '$array of function array_diff', + '$array of function array_diff_assoc', + '$array of function array_intersect', + '$arrays of function array_intersect', + '$arrays of function array_diff', + '$arrays of function array_diff_assoc', + '$array of function natsort', + '$array of function natcasesort', + '$array of function array_count_values', + '#3 of function array_intersect', + ], + [ + '$arr1 of function array_diff', + '$arr1 of function array_diff_assoc', + '$arr1 of function array_intersect', + '$arr2 of function array_intersect', + '$arr2 of function array_diff', + '$arr2 of function array_diff_assoc', + '$array_arg of function natsort', + '$array_arg of function natcasesort', + '$input of function array_count_values', + '#3 $args of function array_intersect', + ], + $error[0], + ); + + return $error; + }, $errors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php new file mode 100644 index 0000000000..d9f2375bfc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php @@ -0,0 +1,74 @@ + + */ +class PrintfArrayParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PrintfArrayParametersRule( + new PrintfHelper(new PhpVersion(PHP_VERSION_ID)), + $this->createReflectionProvider(), + ); + } + + public function testFile(): void + { + $this->analyse([__DIR__ . '/data/vprintf.php'], [ + [ + 'Call to vsprintf contains 2 placeholders, 1 value given.', + 10, + ], + [ + 'Call to vsprintf contains 0 placeholders, 1 value given.', + 11, + ], + [ + 'Call to vsprintf contains 1 placeholder, 2 values given.', + 12, + ], + [ + 'Call to vsprintf contains 2 placeholders, 1 value given.', + 13, + ], + [ + 'Call to vsprintf contains 2 placeholders, 0 values given.', + 14, + ], + [ + 'Call to vsprintf contains 2 placeholders, 0 values given.', + 15, + ], + [ + 'Call to vsprintf contains 4 placeholders, 0 values given.', + 16, + ], + [ + 'Call to vsprintf contains 5 placeholders, 2 values given.', + 18, + ], + [ + 'Call to vsprintf contains 1 placeholder, 2 values given.', + 21, + ], + [ + 'Call to vsprintf contains 1 placeholder, 1-2 values given.', + 29, + ], + [ + 'Call to vprintf contains 2 placeholders, 1 value given.', + 34, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index d9effb60e0..252f2919ec 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -15,7 +15,10 @@ class PrintfParametersRuleTest extends RuleTestCase protected function getRule(): Rule { - return new PrintfParametersRule(new PhpVersion(PHP_VERSION_ID)); + return new PrintfParametersRule( + new PrintfHelper(new PhpVersion(PHP_VERSION_ID)), + $this->createReflectionProvider(), + ); } public function testFile(): void @@ -110,4 +113,14 @@ public function testBug4717(): void $this->analyse([__DIR__ . '/data/bug-4717.php'], $errors); } + public function testBug2342(): void + { + $this->analyse([__DIR__ . '/data/bug-2342.php'], [ + [ + 'Call to sprintf contains 1 placeholder, 0 values given.', + 5, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php index 4b9ab4e23c..40c0526e25 100644 --- a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_INT_SIZE; @@ -14,7 +15,7 @@ class RandomIntParametersRuleTest extends RuleTestCase protected function getRule(): Rule { - return new RandomIntParametersRule($this->createReflectionProvider(), true); + return new RandomIntParametersRule($this->createReflectionProvider(), new PhpVersion(80000), true); } public function testFile(): void diff --git a/tests/PHPStan/Rules/Functions/RedefinedParametersRuleTest.php b/tests/PHPStan/Rules/Functions/RedefinedParametersRuleTest.php new file mode 100644 index 0000000000..3c68e09b7b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/RedefinedParametersRuleTest.php @@ -0,0 +1,37 @@ + + */ +class RedefinedParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RedefinedParametersRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/redefined-parameters.php'], [ + [ + 'Redefinition of parameter $foo.', + 11, + ], + [ + 'Redefinition of parameter $bar.', + 13, + ], + [ + 'Redefinition of parameter $baz.', + 15, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ReturnNullsafeByRefRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnNullsafeByRefRuleTest.php index 9d31dc75d2..a3b8f1db9e 100644 --- a/tests/PHPStan/Rules/Functions/ReturnNullsafeByRefRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnNullsafeByRefRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -19,10 +20,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/return-null-safe-by-ref.php'], [ [ 'Nullsafe cannot be returned by reference.', @@ -39,4 +36,18 @@ public function testRule(): void ]); } + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/return-null-safe-by-ref-property-hooks.php'], [ + [ + 'Nullsafe cannot be returned by reference.', + 13, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index c31f70b445..b5ca981736 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,14 +14,20 @@ class ReturnTypeRuleTest extends RuleTestCase { + private bool $checkNullables; + + private bool $checkExplicitMixed; + 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(), $this->checkNullables, false, true, $this->checkExplicitMixed, false, false, true))); } public function testReturnTypeRule(): void { require_once __DIR__ . '/data/returnTypes.php'; + $this->checkNullables = true; + $this->checkExplicitMixed = false; $this->analyse([__DIR__ . '/data/returnTypes.php'], [ [ 'Function ReturnTypes\returnInteger() should return int but returns string.', @@ -67,6 +74,8 @@ public function testReturnTypeRule(): void public function testReturnTypeRulePhp70(): void { + $this->checkExplicitMixed = false; + $this->checkNullables = true; $this->analyse([__DIR__ . '/data/returnTypes-7.0.php'], [ [ 'Function ReturnTypes\Php70\returnInteger() should return int but empty return statement found.', @@ -77,22 +86,24 @@ public function testReturnTypeRulePhp70(): void public function testIsGenerator(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - + $this->checkExplicitMixed = false; + $this->checkNullables = true; $this->analyse([__DIR__ . '/data/is-generator.php'], []); } public function testBug2568(): void { require_once __DIR__ . '/data/bug-2568.php'; + $this->checkExplicitMixed = false; + $this->checkNullables = true; $this->analyse([__DIR__ . '/data/bug-2568.php'], []); } public function testBug2723(): void { require_once __DIR__ . '/data/bug-2723.php'; + $this->checkExplicitMixed = false; + $this->checkNullables = true; $this->analyse([__DIR__ . '/data/bug-2723.php'], [ [ 'Function Bug2723\baz() should return Bug2723\Bar> but returns Bug2723\BarOfFoo.', @@ -103,12 +114,249 @@ public function testBug2723(): void public function testBug5706(): void { + $this->checkExplicitMixed = false; + $this->checkNullables = true; $this->analyse([__DIR__ . '/data/bug-5706.php'], []); } public function testBug5844(): void { + $this->checkExplicitMixed = false; + $this->checkNullables = true; $this->analyse([__DIR__ . '/data/bug-5844.php'], []); } + public function testBug7218(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-7218.php'], []); + } + + public function testBug5751(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5751.php'], []); + } + + public function testBug3931(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-3931.php'], []); + } + + public function testBug3801(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-3801.php'], [ + [ + 'Function Bug3801\do_foo() should return array{bool, null}|array{null, bool} but returns array{false, true}.', + 17, + '• Type #1 from the union: Offset 1 (null) does not accept type true. +• Type #2 from the union: Offset 0 (null) does not accept type false.', + ], + [ + 'Function Bug3801\do_foo() should return array{bool, null}|array{null, bool} but returns array{false, false}.', + 21, + '• Type #1 from the union: Offset 1 (null) does not accept type false. +• Type #2 from the union: Offset 0 (null) does not accept type false.', + ], + ]); + } + + public function testListWithNullablesChecked(): void + { + $this->checkExplicitMixed = false; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/return-list-nullables.php'], [ + [ + 'Function ReturnListNullables\doFoo() should return array|null but returns list.', + 16, + ], + ]); + } + + public function testListWithNullablesUnchecked(): void + { + $this->checkExplicitMixed = false; + $this->checkNullables = false; + $this->analyse([__DIR__ . '/data/return-list-nullables.php'], []); + } + + public function testBug6787(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-6787.php'], [ + [ + 'Function Bug6787\f() should return T of DateTimeInterface but returns DateTime.', + 11, + 'Type DateTime is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug6568(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-6568.php'], [ + [ + 'Function Bug6568\test() should return T of array but returns array.', + 12, + 'Type array is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug7766(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-7766.php'], [ + [ + "Function Bug7766\problem() should return array but returns array{array{id: 1, created: DateTimeImmutable, updated: DateTimeImmutable, valid_from: DateTimeImmutable, valid_till: DateTimeImmutable, string: 'string', other_string: 'string', another_string: 'string', ...}}.", + 20, + "Offset 'count' (int<0, max>) does not accept type '4'.", + ], + ]); + } + + public function testBug8846(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-8846.php'], []); + } + + public function testBug10077(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-10077.php'], [ + [ + 'Function Bug10077\mergeMediaQueries() should return list|null but returns list.', + 56, + ], + ]); + } + + public function testBug8683(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-8683.php'], []); + } + + public function testBug7984(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-7984.php'], []); + } + + public function testBug5594(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5594.php'], []); + } + + public function testBug5592(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5592.php'], []); + } + + public function testBug10732(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-10732.php'], []); + } + + public function testBug10960(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-10960.php'], []); + } + + public function testBug11518(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-11518.php'], []); + } + + public function testBug8881(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-8881.php'], []); + } + + public function testBug11126(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-11126.php'], []); + } + + public function testBug11032(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-11032.php'], []); + } + + public function testBug11549(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-11549.php'], []); + } + + public function testBug11301(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-11301.php'], [ + [ + 'Function Bug11301\cString() should return array but returns array.', + 35, + ], + ]); + } + + public function testBug12274(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-12274.php'], [ + [ + 'Function Bug12274\getItemsByModifiedIndex() should return non-empty-list but returns non-empty-array, int>.', + 36, + 'non-empty-array, int> might not be a list.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..6aee2ae8a3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php @@ -0,0 +1,189 @@ + + */ +class SortParameterCastableToStringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new SortParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false, true))); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array> given.', + 16, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + 19, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array given.', + 20, + ], + [ + 'Parameter #1 $array of function rsort expects an array of values castable to string, list> given.', + 21, + ], + [ + 'Parameter #1 $array of function asort expects an array of values castable to string, list> given.', + 22, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to string, array> given.', + 23, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + 25, + ], + [ + 'Parameter #1 $array of function rsort expects an array of values castable to string, list> given.', + 26, + ], + [ + 'Parameter #1 $array of function asort expects an array of values castable to string, list> given.', + 27, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to float, array given.', + 31, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to string and float, array given.', + 32, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + 33, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string and float, list> given.', + 34, + ], + ])); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions-named-args.php'], [ + [ + 'Parameter $array of function array_unique expects an array of values castable to string, array> given.', + 7, + ], + [ + 'Parameter $array of function sort expects an array of values castable to string, array> given.', + 9, + ], + [ + 'Parameter $array of function rsort expects an array of values castable to string, list> given.', + 10, + ], + [ + 'Parameter $array of function asort expects an array of values castable to string, list> given.', + 11, + ], + [ + 'Parameter $array of function arsort expects an array of values castable to string, array> given.', + 12, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions-enum.php'], [ + [ + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array given.', + 12, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array given.', + 14, + ], + [ + 'Parameter #1 $array of function rsort expects an array of values castable to string, list given.', + 15, + ], + [ + 'Parameter #1 $array of function asort expects an array of values castable to string, list given.', + 16, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to string, array given.', + 17, + ], + ]); + } + + public function testBug11167(): void + { + $this->analyse([__DIR__ . '/data/bug-11167.php'], []); + } + + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array given.', + 46, + ], + ])); + } + + /** + * @param list $errors + * @return list + */ + private function hackParameterNames(array $errors): array + { + if (PHP_VERSION_ID >= 80000) { + return $errors; + } + + return array_map(static function (array $error): array { + $error[0] = str_replace( + [ + '$array of function sort', + '$array of function rsort', + '$array of function asort', + '$array of function arsort', + ], + [ + '$array_arg of function sort', + '$array_arg of function rsort', + '$array_arg of function asort', + '$array_arg of function arsort', + ], + $error[0], + ); + + return $error; + }, $errors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php index 0d033268d8..38a555afda 100644 --- a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php @@ -14,7 +14,7 @@ class UnusedClosureUsesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck($this->createReflectionProvider())); + return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck($this->createReflectionProvider(), true)); } public function testUnusedClosureUses(): void @@ -22,11 +22,11 @@ public function testUnusedClosureUses(): void $this->analyse([__DIR__ . '/data/unused-closure-uses.php'], [ [ 'Anonymous function has an unused use $unused.', - 3, + 6, ], [ 'Anonymous function has an unused use $anotherUnused.', - 3, + 7, ], [ 'Anonymous function has an unused use $usedInClosureUse.', diff --git a/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php b/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php new file mode 100644 index 0000000000..422103bd37 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php @@ -0,0 +1,54 @@ + + */ +class UselessFunctionReturnValueRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UselessFunctionReturnValueRule( + $this->createReflectionProvider(), + ); + } + + public function testUselessReturnValue(): void + { + $this->analyse([__DIR__ . '/data/useless-fn-return.php'], [ + [ + 'Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', + 47, + ], + [ + 'Return value of function var_export() is always null and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', + 56, + ], + [ + 'Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', + 64, + ], + ]); + } + + public function testUselessReturnValuePhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/useless-fn-return-php8.php'], [ + [ + 'Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', + 18, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/VariadicParametersDeclarationRuleTest.php b/tests/PHPStan/Rules/Functions/VariadicParametersDeclarationRuleTest.php new file mode 100644 index 0000000000..7955385f36 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/VariadicParametersDeclarationRuleTest.php @@ -0,0 +1,37 @@ + + */ +class VariadicParametersDeclarationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new VariadicParametersDeclarationRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/variadic-parameters-declaration.php'], [ + [ + 'Only the last parameter can be variadic.', + 7, + ], + [ + 'Only the last parameter can be variadic.', + 11, + ], + [ + 'Only the last parameter can be variadic.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php b/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php new file mode 100644 index 0000000000..e50e81894c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php @@ -0,0 +1,7 @@ += 8.4 + +// ok +array_all( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_all( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_all( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_all( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_all( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_all($array, fn ($value, $key) => $key === 0); + + // ok + array_all($array, fn (string $value, int $key) => $key === 0); + + // ok + array_all($array, fn (string $value) => $value === 'foo'); + + // bad parameters + array_all($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_all($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_any.php b/tests/PHPStan/Rules/Functions/data/array_any.php new file mode 100644 index 0000000000..1c267ffc62 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_any.php @@ -0,0 +1,56 @@ += 8.4 + +// ok +array_any( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_any( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_any( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_any( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_any( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_any($array, fn ($value, $key) => $key === 0); + + // ok + array_any($array, fn (string $value, int $key) => $key === 0); + + // ok + array_any($array, fn (string $value) => $value === 'foo'); + + // bad parameters + array_any($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_any($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_diff_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_diff_uassoc.php new file mode 100644 index 0000000000..257fc17c85 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_diff_uassoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_diff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_diff_ukey.php b/tests/PHPStan/Rules/Functions/data/array_diff_ukey.php new file mode 100644 index 0000000000..8a98630a88 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_diff_ukey.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_diff_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_filter_empty.php b/tests/PHPStan/Rules/Functions/data/array_filter_empty.php new file mode 100644 index 0000000000..3bcb4dd6ea --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_filter_empty.php @@ -0,0 +1,28 @@ + $objectsOrNull */ +$objectsOrNull = []; +/** @var array $falsey */ +$falsey = []; + +array_filter([0,1,3]); +array_filter([1,3]); +array_filter(['test']); +array_filter(['', 'test']); +array_filter([null, 'test']); +array_filter([false, 'test']); +array_filter([true, false]); +array_filter([true, true]); +array_filter([new \stdClass()]); +array_filter([new \stdClass(), null]); +array_filter($objects); +array_filter($objectsOrNull); + +array_filter([0]); +array_filter([null]); +array_filter([null, null]); +array_filter([null, 0]); +array_filter($falsey); +array_filter([]); diff --git a/tests/PHPStan/Rules/Functions/data/array_filter_null_callback.php b/tests/PHPStan/Rules/Functions/data/array_filter_null_callback.php new file mode 100644 index 0000000000..9acd434233 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_filter_null_callback.php @@ -0,0 +1,3 @@ += 8.4 + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find($array, fn ($value, $key) => $key === 0); + + // ok + array_find($array, fn (string $value, int $key) => $key === 0); + + // ok + array_find($array, fn (string $value) => $key === 0); + + // bad parameters + array_find($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_find_key.php b/tests/PHPStan/Rules/Functions/data/array_find_key.php new file mode 100644 index 0000000000..ab2b7df3fb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_find_key.php @@ -0,0 +1,56 @@ += 8.4 + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find_key($array, fn ($value, $key) => $key === 0); + + // ok + array_find_key($array, fn (string $value, int $key) => $key === 0); + + // ok + array_find_key($array, fn (string $value) => $value === 'foo'); + + // bad parameters + array_find_key($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find_key($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php new file mode 100644 index 0000000000..9f8ba1bfa3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php b/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php new file mode 100644 index 0000000000..4d81086d24 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php b/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php index 0606d7a275..d93d7b6ffc 100644 --- a/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/array_reduce_arrow.php @@ -1,4 +1,4 @@ -= 7.4 + $b; + }, +); + +array_udiff( + ["25","26"], + ["26","27"], + 'strcasecmp', +); diff --git a/tests/PHPStan/Rules/Functions/data/array_udiff_assoc.php b/tests/PHPStan/Rules/Functions/data/array_udiff_assoc.php new file mode 100644 index 0000000000..7827734e59 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_udiff_assoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_udiff_assoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_udiff_assoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_udiff_assoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_udiff_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_udiff_uassoc.php new file mode 100644 index 0000000000..f4767621c2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_udiff_uassoc.php @@ -0,0 +1,45 @@ + 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_udiff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + }, +); + +array_udiff_uassoc( + ['a' => 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_udiff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_uintersect.php b/tests/PHPStan/Rules/Functions/data/array_uintersect.php new file mode 100644 index 0000000000..dbe5307a82 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_uintersect.php @@ -0,0 +1,33 @@ + $b; + } +); + +array_uintersect( + [1, 2, 3], + [1, 2, 3, 4], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect( + ['a', 'b'], + ['c', 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect( + [1, 2, 3], + [1, 2, 3, 4], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_uintersect_assoc.php b/tests/PHPStan/Rules/Functions/data/array_uintersect_assoc.php new file mode 100644 index 0000000000..e410f3827e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_uintersect_assoc.php @@ -0,0 +1,33 @@ + 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_uintersect_assoc( + [1, 2, 3], + [1, 2, 3, 4], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect_assoc( + ['a' => 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect_assoc( + [1, 2, 3], + [1, 2, 3, 4], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_uintersect_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_uintersect_uassoc.php new file mode 100644 index 0000000000..f079aec189 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_uintersect_uassoc.php @@ -0,0 +1,45 @@ + 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_uintersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + }, +); + +array_uintersect_uassoc( + ['a' => 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_values_list.php b/tests/PHPStan/Rules/Functions/data/array_values_list.php new file mode 100644 index 0000000000..7eef89c9cd --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_values_list.php @@ -0,0 +1,28 @@ + $list */ +$list = [1, 2, 3]; +/** @var list $list */ +$array = ['a' => 1, 'b' => 2, 'c' => 3]; + +array_values([0,1,3]); +array_values([1,3]); +array_values(['test']); +array_values(['a' => 'test']); +array_values(['', 'test']); +array_values(['a' => '', 'b' => 'test']); +array_values($list); +array_values($array); + +array_values([0]); +array_values(['a' => null, 'b' => null]); +array_values([null, null]); +array_values([null, 0]); +array_values([]); + +/** @var array{} $empty */ +$empty = doFoo(); +array_values($empty); + +array_values(unused: true, array: $array); +array_values(unused: true, array: $list); diff --git a/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php b/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php index ca75b4998b..22a69296f1 100644 --- a/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php @@ -1,4 +1,4 @@ -= 7.4 + 1, 'bar' => 2]; array_walk( diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php b/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php index fa1ec0f17a..30f1a93884 100644 --- a/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php @@ -1,4 +1,4 @@ -= 7.4 += 8.1 + +namespace ArrowFunctionNeverReturn; + +class Baz +{ + + public function doFoo(): void + { + $f = fn () => throw new \Exception(); + $g = fn (): never => throw new \Exception(); + $g = fn (): never => 1; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-never.php b/tests/PHPStan/Rules/Functions/data/arrow-function-never.php new file mode 100644 index 0000000000..da2cc65566 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-never.php @@ -0,0 +1,7 @@ + throw new \Exception(); +}; diff --git a/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php b/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php index 1ff3081dc5..29886b7dab 100644 --- a/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php +++ b/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php @@ -1,4 +1,4 @@ -= 7.4 + yield $value; diff --git a/tests/PHPStan/Rules/Functions/data/benevolent-superglobal-keys.php b/tests/PHPStan/Rules/Functions/data/benevolent-superglobal-keys.php new file mode 100644 index 0000000000..b5a3c485c8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/benevolent-superglobal-keys.php @@ -0,0 +1,87 @@ + $v) { + trim($k); + } + + foreach ($_SERVER as $k => $v) { + trim($k); + } + + foreach ($_GET as $k => $v) { + trim($k); + } + + foreach ($_POST as $k => $v) { + trim($k); + } + + foreach ($_FILES as $k => $v) { + trim($k); + } + + foreach ($_COOKIE as $k => $v) { + trim($k); + } + + foreach ($_SESSION as $k => $v) { + trim($k); + } + + foreach ($_REQUEST as $k => $v) { + trim($k); + } + + foreach ($_ENV as $k => $v) { + trim($k); + } +} + +function benevolentKeysOfSuperglobalsInt(): void +{ + foreach ($GLOBALS as $k => $v) { + acceptInt($k); + } + + foreach ($_SERVER as $k => $v) { + acceptInt($k); + } + + foreach ($_GET as $k => $v) { + acceptInt($k); + } + + foreach ($_POST as $k => $v) { + acceptInt($k); + } + + foreach ($_FILES as $k => $v) { + acceptInt($k); + } + + foreach ($_COOKIE as $k => $v) { + acceptInt($k); + } + + foreach ($_SESSION as $k => $v) { + acceptInt($k); + } + + foreach ($_REQUEST as $k => $v) { + acceptInt($k); + } + + foreach ($_ENV as $k => $v) { + acceptInt($k); + } +} + +function acceptInt(int $i): void +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10003.php b/tests/PHPStan/Rules/Functions/data/bug-10003.php new file mode 100644 index 0000000000..af669a63ed --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10003.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug10077; + +interface MediaQueryMergeResult +{ +} + + +enum MediaQuerySingletonMergeResult implements MediaQueryMergeResult +{ + case empty; + case unrepresentable; +} + +// In actual code, this is a final class implementing its methods +abstract class CssMediaQuery implements MediaQueryMergeResult +{ + abstract public function merge(CssMediaQuery $other): MediaQueryMergeResult; +} + + +/** + * Returns a list of queries that selects for contexts that match both + * $queries1 and $queries2. + * + * Returns the empty list if there are no contexts that match both $queries1 + * and $queries2, or `null` if there are contexts that can't be represented + * by media queries. + * + * @param CssMediaQuery[] $queries1 + * @param CssMediaQuery[] $queries2 + * + * @return list|null + */ +function mergeMediaQueries(array $queries1, array $queries2): ?array +{ + $queries = []; + + foreach ($queries1 as $query1) { + foreach ($queries2 as $query2) { + $result = $query1->merge($query2); + + if ($result === MediaQuerySingletonMergeResult::empty) { + continue; + } + + if ($result === MediaQuerySingletonMergeResult::unrepresentable) { + return null; + } + + $queries[] = $result; + } + } + + return $queries; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10171.php b/tests/PHPStan/Rules/Functions/data/bug-10171.php new file mode 100644 index 0000000000..45a5545184 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10171.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug10171; + +setcookie("name", "value", 0, "/", secure: true, httponly: true); +setcookie('name', expires_or_options: ['samesite' => 'lax']); + +setrawcookie("name", "value", 0, "/", secure: true, httponly: true); +setrawcookie('name', expires_or_options: ['samesite' => 'lax']); + +// Wrong +setcookie('name', samesite: 'lax'); +setcookie( + 'aaa', + 'bbb', + 10, + '/', + 'example.com', + true, + false, + 'lax', + 1, +); + +setrawcookie('name', samesite: 'lax'); +setrawcookie( + 'aaa', + 'bbb', + 10, + '/', + 'example.com', + true, + false, + 'lax', + 1, +); diff --git a/tests/PHPStan/Rules/Functions/data/bug-10297.php b/tests/PHPStan/Rules/Functions/data/bug-10297.php new file mode 100644 index 0000000000..ed3a8cf3dd --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10297.php @@ -0,0 +1,86 @@ + $stream + * @param callable(T, K): iterable $fn + * + * @return Generator + */ +function scollect(iterable $stream, callable $fn): Generator +{ + foreach ($stream as $key => $value) { + yield from $fn($value, $key); + } +} + +/** + * @template K of array-key + * @template T + * @template L of array-key + * @template U + * + * @param array $array + * @param callable(T, K): iterable $fn + * + * @return array + */ +function collectWithKeys(array $array, callable $fn): array +{ + $map = []; + $counter = 0; + + try { + foreach (scollect($array, $fn) as $key => $value) { + $map[$key] = $value; + ++$counter; + } + } catch (TypeError) { + throw new UnexpectedValueException('The key yielded in the callable is not compatible with the type "array-key".'); + } + + if ($counter !== count($map)) { + throw new UnexpectedValueException( + 'Data loss occurred because of duplicated keys. Use `collect()` if you do not care about ' . + 'the yielded keys, or use `scollect()` if you need to support duplicated keys (as arrays cannot).', + ); + } + + return $map; +} + +class SomeUnitTest +{ + /** + * @return iterable + */ + public static function someProvider(): iterable + { + $unsupportedTypes = [ + // this one does not work: + 'Not a Number' => NAN, + // these work: + 'Infinity' => INF, + stdClass::class => new stdClass(), + self::class => self::class, + 'hello there' => 'hello there', + 'array' => [[42]], + ]; + + yield from collectWithKeys($unsupportedTypes, static function (mixed $value, string $type): iterable { + $error = sprintf('Some %s error message', $type); + + yield sprintf('"%s" something something', $type) => [$value, [$error, $error, $error]]; + }); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10298.php b/tests/PHPStan/Rules/Functions/data/bug-10298.php new file mode 100644 index 0000000000..dfbfa7979e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10298.php @@ -0,0 +1,18 @@ +, 1: list} $tuple + */ + public function sayHello(array $tuple): void + { + array_map(fn (string $first, string $second) => $first . $second, ...$tuple); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10626.php b/tests/PHPStan/Rules/Functions/data/bug-10626.php new file mode 100644 index 0000000000..e0e855c825 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10626.php @@ -0,0 +1,17 @@ + $items + * @return void + */ + public function __construct(protected array $items = []) {} + + /** + * Run a map over each of the items. + * + * @template TMapValue + * + * @param callable(TValue): TMapValue $callback + * @return static + */ + public function map(callable $callback) + { + return new self(array_map($callback, $this->items)); + } +} + +/** + * I'd expect this to work? + * + * @param Collection> $collection + * @return Collection> + */ +function current(Collection $collection): Collection +{ + return $collection->map(fn(array $item) => $item); +} + +/** + * Removing the Typehint works + * + * @param Collection> $collection + * @return Collection> + */ +function removeTypeHint(Collection $collection): Collection +{ + return $collection->map(fn($item) => $item); +} + +/** + * Typehint works for simple type + * + * @param Collection $collection + * @return Collection + */ +function simplerType(Collection $collection): Collection +{ + return $collection->map(fn(string $item) => $item); +} + +/** + * Typehint works for arrays + * + * @param array> $collection + * @return array> + */ +function useArraysInstead(array $collection): array +{ + return array_map( + fn(array $item) => $item, + $collection, + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10814.php b/tests/PHPStan/Rules/Functions/data/bug-10814.php new file mode 100644 index 0000000000..a1c7ed8cbe --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10814.php @@ -0,0 +1,12 @@ + 'bar']); +lowerCaseKey(['FOO' => 'bar']); diff --git a/tests/PHPStan/Rules/Functions/data/bug-10974.php b/tests/PHPStan/Rules/Functions/data/bug-10974.php new file mode 100644 index 0000000000..5e93af420a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10974.php @@ -0,0 +1,27 @@ += 8.0 + +namespace Bug10974; + +function non(): void {} +function single(string $str): void {} +/** @param non-empty-array $strs */ +function multiple(array $strs): void {} + +/** @param array $arr */ +function test(array $arr): void +{ + match (count($arr)) + { + 0 => non(), + 1 => single(reset($arr)), + default => multiple($arr) + }; + + if (empty($arr)) { + non(); + } elseif (count($arr) === 1) { + single(reset($arr)); + } else { + multiple($arr); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11032.php b/tests/PHPStan/Rules/Functions/data/bug-11032.php new file mode 100644 index 0000000000..ea967f31ee --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11032.php @@ -0,0 +1,42 @@ + + */ + private $promise = null; + + /** + * @return PromiseInterface + */ + public function promise(): PromiseInterface + { + return $this->promise; + } +} + +/** + * @template T + * @param iterable $tasks + * @return PromiseInterface> + */ +function parallel(iterable $tasks): PromiseInterface +{ + /** @var Deferred> $deferred*/ + $deferred = new Deferred(); + + return $deferred->promise(); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11056.php b/tests/PHPStan/Rules/Functions/data/bug-11056.php new file mode 100644 index 0000000000..6c4b8abea1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11056.php @@ -0,0 +1,59 @@ +|string $class + * @return ($class is class-string ? T : mixed) + */ +function createA(string $class) { + return new $class(); +} + +/** + * @template T + * @param class-string $class + * @return T + */ +function createB(string $class) { + return new $class(); +} + +/** + * @param Item[] $values + */ +function receive(array $values): void { } + +receive( + array_map( + createA(...), + [ A::class, B::class, C::class ] + ) +); + +receive( + array_map( + createB(...), + [ A::class, B::class, C::class ] + ) +); + +receive( + array_map( + static fn($val) => createA($val), + [ A::class, B::class, C::class ] + ) +); + +receive( + array_map( + static fn($val) => createB($val), + [ A::class, B::class, C::class ] + ) +); diff --git a/tests/PHPStan/Rules/Functions/data/bug-11111.php b/tests/PHPStan/Rules/Functions/data/bug-11111.php new file mode 100644 index 0000000000..c36a4ae778 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11111.php @@ -0,0 +1,26 @@ += 8.1 + +namespace Bug11111; + +enum Language: string +{ + case ENG = 'eng'; + case FRE = 'fre'; + case GER = 'ger'; + case ITA = 'ita'; + case SPA = 'spa'; + case DUT = 'dut'; + case DAN = 'dan'; +} + +/** @var Language[] $langs */ +$langs = [ + Language::ENG, + Language::GER, + Language::DAN, +]; + +$array = array_fill_keys($langs, null); +unset($array[Language::GER]); + +var_dump(array_fill_keys([Language::ITA, Language::DUT], null)); diff --git a/tests/PHPStan/Rules/Functions/data/bug-11126.php b/tests/PHPStan/Rules/Functions/data/bug-11126.php new file mode 100644 index 0000000000..8569f55ac0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11126.php @@ -0,0 +1,41 @@ + + */ + public function map(callable $callback): Collection { + return $this; + } +} + +/** + * @param Collection> $in + * @return Collection> + */ +function foo(Collection $in): Collection { + return $in->map(static fn ($v) => $v); +} + +/** + * @param Collection> $in + * @return Collection> + */ +function bar(Collection $in): Collection { + return $in->map(value(...)); +} + +/** + * @param int<0, max> $in + * @return int<0, max> + */ +function value(int $in): int { + return $in; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11141.php b/tests/PHPStan/Rules/Functions/data/bug-11141.php new file mode 100644 index 0000000000..f9eaddf4fb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11141.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug11141; + +enum Language: string +{ + case ENG = 'eng'; + case FRE = 'fre'; + case GER = 'ger'; + case ITA = 'ita'; + case SPA = 'spa'; + case DUT = 'dut'; + case DAN = 'dan'; +} + +$langs = [ + Language::ENG, + Language::GER, + Language::DAN, +]; + +$result = array_diff($langs, [Language::DAN]); diff --git a/tests/PHPStan/Rules/Functions/data/bug-11167.php b/tests/PHPStan/Rules/Functions/data/bug-11167.php new file mode 100644 index 0000000000..8afdd21029 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11167.php @@ -0,0 +1,5 @@ + + */ +function cInt(): array +{ + $a = ['12345']; + $b = ['abc']; + + return array_combine($a, $b); +} + +/** + * @return array + */ +function cInt2(): array +{ + $a = ['12345', 123]; + $b = ['abc', 'def']; + + return array_combine($a, $b); +} + +/** + * @return array + */ +function cString(): array +{ + $a = ['12345']; + $b = ['abc']; + + return array_combine($a, $b); +} + + +/** + * @return array + */ +function cString2(): array +{ + $a = ['12345', 123, 'a']; + $b = ['abc', 'def', 'xy']; + + return array_combine($a, $b); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11418.php b/tests/PHPStan/Rules/Functions/data/bug-11418.php new file mode 100755 index 0000000000..8172892d95 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11418.php @@ -0,0 +1,9 @@ + 42 ]); diff --git a/tests/PHPStan/Rules/Functions/data/bug-11883.php b/tests/PHPStan/Rules/Functions/data/bug-11883.php new file mode 100644 index 0000000000..a14174777b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11883.php @@ -0,0 +1,14 @@ += 8.1 + +namespace Bug11883; + +enum SomeEnum: int +{ + case A = 1; + case B = 2; +} + +$enums1 = [SomeEnum::A, SomeEnum::B]; + +var_dump(array_sum($enums1)); +var_dump(array_product($enums1)); diff --git a/tests/PHPStan/Rules/Functions/data/bug-11942.php b/tests/PHPStan/Rules/Functions/data/bug-11942.php new file mode 100644 index 0000000000..33bd0ff1ab --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11942.php @@ -0,0 +1,23 @@ + $a */ +function foo($a): void { + print "ok\n"; +} + +/** + * @param array $a + */ +function bar($a): void { + foo($a); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-12146.php b/tests/PHPStan/Rules/Functions/data/bug-12146.php new file mode 100644 index 0000000000..bd0bf858c3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12146.php @@ -0,0 +1,49 @@ +|array $validArrayUnion valid + * @param array|array<\stdClass> $invalidArrayUnion invalid, report + * @param ?array<\stdClass> $nullableInvalidArray invalid, but don't report because it's reported by CallToFunctionParametersRule + * @param array<\stdClass>|\SplFixedArray $arrayOrSplArray invalid, but don't report because it's reported by CallToFunctionParametersRule + * @return void + */ +function foo($mixed, $validArrayUnion, $invalidArrayUnion, $nullableInvalidArray, $arrayOrSplArray) { + var_dump(array_sum($mixed)); + var_dump(array_sum($validArrayUnion)); + var_dump(array_sum($invalidArrayUnion)); + var_dump(array_sum($nullableInvalidArray)); + var_dump(array_sum($arrayOrSplArray)); + + var_dump(array_product($mixed)); + var_dump(array_product($validArrayUnion)); + var_dump(array_product($invalidArrayUnion)); + var_dump(array_product($nullableInvalidArray)); + var_dump(array_product($arrayOrSplArray)); + + var_dump(implode(',', $mixed)); + var_dump(implode(',', $validArrayUnion)); + var_dump(implode(',', $invalidArrayUnion)); + var_dump(implode(',', $nullableInvalidArray)); + var_dump(implode(',', $arrayOrSplArray)); + + var_dump(array_intersect($mixed, [5])); + var_dump(array_intersect($validArrayUnion, [5])); + var_dump(array_intersect($invalidArrayUnion, [5])); + var_dump(array_intersect($nullableInvalidArray, [5])); + var_dump(array_intersect($arrayOrSplArray, [5])); + + var_dump(array_fill_keys($mixed, 1)); + var_dump(array_fill_keys($validArrayUnion, 1)); + var_dump(array_fill_keys($invalidArrayUnion, 1)); + var_dump(array_fill_keys($nullableInvalidArray, 1)); + var_dump(array_fill_keys($arrayOrSplArray, 1)); + + var_dump(array_unique($mixed)); + var_dump(array_unique($validArrayUnion)); + var_dump(array_unique($invalidArrayUnion)); + var_dump(array_unique($nullableInvalidArray)); + var_dump(array_unique($arrayOrSplArray)); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-12499.php b/tests/PHPStan/Rules/Functions/data/bug-12499.php new file mode 100644 index 0000000000..1322e06e4f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12499.php @@ -0,0 +1,23 @@ + */ + public array $a; + + public function __construct() { + $this->a = ['b' => 2, 'a' => 1]; + ksort($this->a); + } +} + +class B { + /** @readonly */ + public array $readonlyArr; + + public function __construct() { + $this->readonlyArr = ['b' => 2, 'a' => 1]; + ksort($this->readonlyArr); + } +} + +class C { + /** @readonly */ + static public array $readonlyArr; + + public function __construct() { + self::$readonlyArr = ['b' => 2, 'a' => 1]; + ksort(self::$readonlyArr); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-12847.php b/tests/PHPStan/Rules/Functions/data/bug-12847.php new file mode 100644 index 0000000000..c4880d83f6 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12847.php @@ -0,0 +1,69 @@ + $array + */ + $array = [ + 'abc' => 'def' + ]; + + if (isset($array['def'])) { + doSomething($array); + } +} + +function doFoo(array $array):void { + if (isset($array['def'])) { + doSomething($array); + } +} + +function doFooBar(array $array):void { + if (array_key_exists('foo', $array) && $array['foo'] === 17) { + doSomething($array); + } +} + +function doImplicitMixed($mixed):void { + if (isset($mixed['def'])) { + doSomething($mixed); + } +} + +function doExplicitMixed(mixed $mixed): void +{ + if (isset($mixed['def'])) { + doSomething($mixed); + } +} + +/** + * @param non-empty-array $array + */ +function doSomething(array $array): void +{ + +} + +/** + * @param non-empty-array $array + */ +function doSomethingWithInt(array $array): void +{ + +} + +function doFooBarInt(array $array):void { + if (array_key_exists('foo', $array) && $array['foo'] === 17) { + doSomethingWithInt($array); // expect error, because our array is not sealed + } +} + +function doFooBarString(array $array):void { + if (array_key_exists('foo', $array) && $array['foo'] === "hello") { + doSomethingWithInt($array); // expect error, because our array is not sealed + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-2342.php b/tests/PHPStan/Rules/Functions/data/bug-2342.php new file mode 100644 index 0000000000..440b888d02 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-2342.php @@ -0,0 +1,5 @@ +real_connect( + null, + null, + null, + null, + null, + null, + \MYSQLI_CLIENT_SSL + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-2911.php b/tests/PHPStan/Rules/Functions/data/bug-2911.php new file mode 100644 index 0000000000..194b8a3c0a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-2911.php @@ -0,0 +1,31 @@ + $array + */ +function foo(array $array): void { + $array['bar'] = 'string'; + + // 'bar' is always set, should not complain here + bar($array); +} + + +/** + * @param array $array + */ +function foo2(array $array): void { + $array['foo'] = 'string'; + + // 'bar' is always set, should not complain here + bar($array); +} + + +/** + * @param array{bar: string} $array + */ +function bar(array $array): void { +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3107.php b/tests/PHPStan/Rules/Functions/data/bug-3107.php new file mode 100644 index 0000000000..12ed0edfd0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3107.php @@ -0,0 +1,23 @@ +val = $mixed; + + $a = []; + $a[$holder->val] = 1; + take($a); +} + +/** @param array $a */ +function take($a): void {} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3261.php b/tests/PHPStan/Rules/Functions/data/bug-3261.php index 5e5c5f874e..b5fc443ff0 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-3261.php +++ b/tests/PHPStan/Rules/Functions/data/bug-3261.php @@ -1,4 +1,4 @@ -= 7.4 + + */ +function someFunc(bool $flag): array +{ + $ids = [ + ['fa', 'foo', 'baz'] + ]; + + if ($flag) { + $ids[] = ['foo', 'bar', 'baz']; + + } + + if (count($ids) > 1) { + return array_intersect(...$ids); + } + + return $ids[0]; +} + +var_dump(someFunc(true)); +var_dump(someFunc(false)); diff --git a/tests/PHPStan/Rules/Functions/data/bug-3660.php b/tests/PHPStan/Rules/Functions/data/bug-3660.php index c42021e7dd..6eb3bf468f 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-3660.php +++ b/tests/PHPStan/Rules/Functions/data/bug-3660.php @@ -1,4 +1,4 @@ -= 7.4 +handleA(...) : $this->handleB(...); + + $method($obj); + } + + private function handleA(A $a): void + { + } + + private function handleB(B $b): void + { + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3931.php b/tests/PHPStan/Rules/Functions/data/bug-3931.php new file mode 100644 index 0000000000..424c7ca236 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3931.php @@ -0,0 +1,26 @@ + $arr + * @return void + */ +function test(array $arr): void +{ + $r = addSomeKey($arr, 1); + assertType("array{mykey: int}", $r); // could be better, the T part currently disappears +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3946.php b/tests/PHPStan/Rules/Functions/data/bug-3946.php new file mode 100644 index 0000000000..bdb12cccb0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3946.php @@ -0,0 +1,8 @@ + $array */ +$array = []; + +foreach ($array as $k => $v) { + if (check($k) && isset($prev)) { + $array[$prev] = $v; + } + + $prev = $k; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-4739.php b/tests/PHPStan/Rules/Functions/data/bug-4739.php new file mode 100644 index 0000000000..e8d0fdfbaa --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4739.php @@ -0,0 +1,19 @@ + $value) { + if ($predicate($value)) { + yield $key => $value; + } + } +} + + +class Record { + /** + * @var boolean + */ + public $isInactive; + /** + * @var string + */ + public $name; +} + +function doFoo() { + $emails = []; + $records = []; + filter( + function (Record $domain) use (&$emails): bool { + if (!isset($emails[$domain->name])) { + $emails[$domain->name] = TRUE; + return TRUE; + } + return !$domain->isInactive; + }, + $records + ); + $test = (bool) mt_rand(0, 1); + filter( + function (bool $arg) use (&$emails): bool { + if (empty($emails)) { + return TRUE; + } + return $arg; + }, + $records + ); + filter( + function (bool $arg) use ($emails): bool { + if (empty($emails)) { + return TRUE; + } + return $arg; + }, + $records + ); + $test = (bool) mt_rand(0, 1); + filter( + function (bool $arg) use (&$test): bool { + if ($test) { + return TRUE; + } + return $arg; + }, + $records + ); + filter( + function (bool $arg) use ($test): bool { + if ($test) { + return TRUE; + } + return $arg; + }, + $records + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-4960.php b/tests/PHPStan/Rules/Functions/data/bug-4960.php new file mode 100644 index 0000000000..703fb32b87 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4960.php @@ -0,0 +1,14 @@ + 11); + + password_hash($password, PASSWORD_DEFAULT, $options); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5288.php b/tests/PHPStan/Rules/Functions/data/bug-5288.php new file mode 100644 index 0000000000..a116082737 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5288.php @@ -0,0 +1,54 @@ +get_iterator(); + $data = array_map( + function ($value): void {}, + iterator_to_array($iterator) + ); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5356.php b/tests/PHPStan/Rules/Functions/data/bug-5356.php index 158e2f7532..c5123a08d1 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-5356.php +++ b/tests/PHPStan/Rules/Functions/data/bug-5356.php @@ -1,4 +1,4 @@ -= 7.4 + $map + * @return numeric-string + */ +function mapGet(\Ds\Map $map, \Ds\Hashable $key): string +{ + return $map->get($key, '0'); +} + +/** + * @template TDefault + * @param TDefault $default + * @return numeric-string|TDefault + */ +function getFooOrDefault($default) { + if ((bool) random_int(0, 1)) { + /** @var numeric-string */ + $foo = '5'; + return $foo; + } else { + return $default; + } +} + +function doStuff(): int +{ + /** + * @var \Ds\Map + */ + $map = new \Ds\Map(); + + return $map->get('foo', 1); +} + +/** + * @return numeric-string + */ +function doStuff1(): string { + /** @var numeric-string */ + $foo = '12'; + return getFooOrDefault($foo); +} + +/** + * @return numeric-string + */ +function doStuff2(): string { + return getFooOrDefault('12'); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5594.php b/tests/PHPStan/Rules/Functions/data/bug-5594.php new file mode 100644 index 0000000000..19dd58ed83 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5594.php @@ -0,0 +1,14 @@ + + */ +function createIterator(array $items): ArrayIterator +{ + return new ArrayIterator($items); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5751.php b/tests/PHPStan/Rules/Functions/data/bug-5751.php new file mode 100644 index 0000000000..31ed2db881 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5751.php @@ -0,0 +1,19 @@ +test(); diff --git a/tests/PHPStan/Rules/Functions/data/bug-5867.php b/tests/PHPStan/Rules/Functions/data/bug-5867.php new file mode 100644 index 0000000000..b389d8d967 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5867.php @@ -0,0 +1,16 @@ + */ + public function getCreateTableSQL(): array + { + $sqls = array_merge( + $this->a(), + parent::b() // @phpstan-ignore-line + ); + + return $sqls; + } +} + +class A { + public function a(): mixed { + throw new \Exception(); + } + + /** @return null */ + public function b() { + throw new \Exception(); + } +} + +class B extends A +{ + use T; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6261.php b/tests/PHPStan/Rules/Functions/data/bug-6261.php new file mode 100644 index 0000000000..055f809075 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6261.php @@ -0,0 +1,15 @@ + 'a', + 'checked' => false, + 'only_in_country' => ['DE'], + ], + [ + 'value' => 'b', + 'checked' => false, + 'only_in_country' => ['BE', 'CH', 'DE', 'DK', 'FR', 'NL', 'SE'], + ], + [ + 'value' => 'c', + 'checked' => false, + ], + ]; + + foreach ($options as $key => $option) { + if (isset($option['only_in_country']) + && !in_array($country, $option['only_in_country'], true)) { + unset($options[$key]); + + continue; + } + } + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6448.php b/tests/PHPStan/Rules/Functions/data/bug-6448.php new file mode 100644 index 0000000000..c2529c8713 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6448.php @@ -0,0 +1,30 @@ +stream = $stream; + } + + /** + * @param array $fields + */ + public function sayHello( + array $fields, + string $delimiter, + string $enclosure, + string $escape, + string $eol + ): int|false { + return fputcsv($this->stream, $fields, $delimiter, $enclosure, $escape, $eol); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6485.php b/tests/PHPStan/Rules/Functions/data/bug-6485.php new file mode 100644 index 0000000000..3eb28ea9f1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6485.php @@ -0,0 +1,35 @@ +> + */ + private array $serializers = []; + + /** + * @phpstan-template TBlockType of Block + * @phpstan-param TBlockType $block + */ + public function serialize(Block $block) : CompoundTag{ + $class = get_class($block); + $serializer = $this->serializers[$class][$block->getTypeId()] ?? null; + + if($serializer === null){ + //TODO: use a proper exception type for this + throw new \InvalidArgumentException("No serializer registered for this block (this is probably a plugin bug)"); + } + + return $serializer($block); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6559.php b/tests/PHPStan/Rules/Functions/data/bug-6559.php new file mode 100644 index 0000000000..0fcef8ca3d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6559.php @@ -0,0 +1,13 @@ + true]; + + $find = function(string $key) use (&$array) { + return $array[$key] ?? null; + }; + + $find('a') ?? false; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6568.php b/tests/PHPStan/Rules/Functions/data/bug-6568.php new file mode 100644 index 0000000000..399e3111cc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6568.php @@ -0,0 +1,13 @@ +name . $this->version; + } +} + +class ServiceRedis +{ + public function __construct( + private string $name, + private string $version, + private bool $persistent, + ) {} + + public function touchAll() : string{ + return $this->persistent ? $this->name : $this->version; + } +} + +function test(?string $type = NULL) : void { + $types = [ + 'solr' => [ + 'label' => 'SOLR Search', + 'data_class' => CreateServiceSolrData::class, + 'to_entity' => function (CreateServiceSolrData $data) { + assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation"); + return new ServiceSolr($data->name, $data->version); + }, + ], + 'redis' => [ + 'label' => 'Redis', + 'data_class' => CreateServiceRedisData::class, + 'to_entity' => function (CreateServiceRedisData $data) { + assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation"); + return new ServiceRedis($data->name, $data->version, $data->persistent); + }, + ], + ]; + + if ($type === NULL || !isset($types[$type])) { + throw new \RuntimeException("404 or choice form here"); + } + + $data = new $types[$type]['data_class'](); + + $service = $types[$type]['to_entity']($data); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6720.php b/tests/PHPStan/Rules/Functions/data/bug-6720.php new file mode 100644 index 0000000000..0fc2e546c6 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6720.php @@ -0,0 +1,11 @@ + 1 + 1, "value1" => 2 + 2,]; +} + +/** + * This just demonstrates function call that returns an array with string + * string keys value0 and value1 and integer values. + * + * @return array{value0: int, value1: int} + */ +function getArray() : array +{ + return [ + "value0" => random_int(0, 100), + "value1" => random_int(0, 100) + ]; +} + +/** @return array{value0: int, value1: int} */ +function getNext() : array +{ + // starting values, e.g. some kind of baseline + $startValues = ["value0" => 1, "value1" => 2]; + + // current maximum values + $currentMaxValues = getArray(); + + // if current values equals starting values, then don't increment + if ($currentMaxValues === $startValues) { + return $startValues; + } + + // increment and return new values + return increment($startValues); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6787.php b/tests/PHPStan/Rules/Functions/data/bug-6787.php new file mode 100644 index 0000000000..2f4e1d940c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6787.php @@ -0,0 +1,12 @@ + [ 'type' => 'a' ], 'second' => [ 'type' => 'b' ] ]; + + $types = array_fill_keys($types, true); + $defs = array_filter($defs, function($def) use(&$types) { return isset($types[$def['type']]); }); +}; diff --git a/tests/PHPStan/Rules/Functions/data/bug-6902.php b/tests/PHPStan/Rules/Functions/data/bug-6902.php new file mode 100644 index 0000000000..2d079f2286 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6902.php @@ -0,0 +1,23 @@ + 1, 'b' => 2]; + /** @var array **/ + $array2 = ['a' => 1]; + + $check = function(string $key) use (&$array1, &$array2): bool { + if (!isset($array1[$key], $array2[$key])) { + return false; + } + // ... more conditions here ... + return true; + }; + + if ($check('a')) { + // ... + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6987.php b/tests/PHPStan/Rules/Functions/data/bug-6987.php new file mode 100644 index 0000000000..c07ab40ced --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6987.php @@ -0,0 +1,38 @@ + 123123123, + 'DISABLED' => 555555, + 'CANCELLED' => 11111, + ]; + + $map = []; + foreach($availableValues as $key => $value){ + $map[transformKey(strtolower($key))] = $value; + } + + return $map; + + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7017.php b/tests/PHPStan/Rules/Functions/data/bug-7017.php new file mode 100644 index 0000000000..ae048fbe64 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7017.php @@ -0,0 +1,9 @@ + $data + */ +function foobar(array $data): void +{ + if (!array_key_exists('value', $data) || !is_string($data['value'])) { + throw new \RuntimeException(); + } + + assertType("non-empty-array&hasOffsetValue('value', string)", $data); + + foo($data); +} + +function foobar2(mixed $data): void +{ + if (!is_array($data) || !array_key_exists('value', $data) || !is_string($data['value'])) { + throw new \RuntimeException(); + } + + assertType("non-empty-array&hasOffsetValue('value', string)", $data); + + foo($data); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7211.php b/tests/PHPStan/Rules/Functions/data/bug-7211.php new file mode 100644 index 0000000000..f5c9f8a61e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7211.php @@ -0,0 +1,25 @@ += 8.1 + +namespace Bug7211; + +enum Foo { + case Bar; + case Baz; + case Gar; + case Gaz; + + public function startsWithB(): bool + { + return inArray($this, [static::Baz, static::Bar]); + } +} + +/** + * @template T + * @psalm-param T $needle + * @psalm-param array $haystack + */ +function inArray(mixed $needle, array $haystack): bool +{ + return \in_array($needle, $haystack, true); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7218.php b/tests/PHPStan/Rules/Functions/data/bug-7218.php new file mode 100644 index 0000000000..7887d4501d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7218.php @@ -0,0 +1,13 @@ + */ +function getFoo(): Foo +{ + /** @var Foo */ + return new Foo(); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7220.php b/tests/PHPStan/Rules/Functions/data/bug-7220.php new file mode 100644 index 0000000000..d52c0c916a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7220.php @@ -0,0 +1,20 @@ + $value) { + if ($predicate($value)) { + yield $key => $value; + } + } +} + +function getFiltered(): \Iterator { + $already_seen = []; + return filter(function (string $value) use (&$already_seen): bool { + $result = !isset($already_seen[$value]); + $already_seen[$value] = TRUE; + return $result; + }, ['a', 'b', 'a']); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7283.php b/tests/PHPStan/Rules/Functions/data/bug-7283.php new file mode 100644 index 0000000000..ecf155d62a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7283.php @@ -0,0 +1,22 @@ + + */ +function onlyTrue(mixed $value): array +{ + return array_fill(0, 5, $value); +} + +/** + * @param array $values + */ +function needTrue(array $values): void {} + +function (): void { + needTrue(onlyTrue(true)); +}; diff --git a/tests/PHPStan/Rules/Functions/data/bug-7522.php b/tests/PHPStan/Rules/Functions/data/bug-7522.php new file mode 100644 index 0000000000..cff0bc5897 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7522.php @@ -0,0 +1,8 @@ + $p + * @template T of object + */ +function ng($p): void +{ +} + +function doFoo() { + ok(''); + ng(''); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7676.php b/tests/PHPStan/Rules/Functions/data/bug-7676.php new file mode 100644 index 0000000000..0f34f2aa31 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7676.php @@ -0,0 +1,7 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_diff_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_intersect_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_intersect_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_udiff_assoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + } + )); + + var_dump(array_udiff_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + }, + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_uintersect_assoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + } + )); + + var_dump(array_uintersect_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + }, + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_uintersect( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + } + )); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7766.php b/tests/PHPStan/Rules/Functions/data/bug-7766.php new file mode 100644 index 0000000000..143862df62 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7766.php @@ -0,0 +1,32 @@ +, + * other_count: int<0, max> + * }> + */ +function problem(): array { + return [[ + 'id' => 1, + 'created' => new \DateTimeImmutable(), + 'updated' => new \DateTimeImmutable(), + 'valid_from' => new \DateTimeImmutable(), + 'valid_till' => new \DateTimeImmutable(), + 'string' => 'string', + 'other_string' => 'string', + 'another_string' => 'string', + 'count' => '4', + 'other_count' => 3, + ]]; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7823.php b/tests/PHPStan/Rules/Functions/data/bug-7823.php new file mode 100644 index 0000000000..4e46d5780d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7823.php @@ -0,0 +1,55 @@ += 8.0 + +namespace Bug7823; + +use function PHPStan\Testing\assertType; + +/** + * @param literal-string $s + */ +function sayHello(string $s): void +{ +} + +class A +{ +} + +/** + * @param T $t + * + * @template T of A + */ +function x($t): void +{ + assertType('class-string&literal-string', $t::class); + sayHello($t::class); +} + +/** + * @param class-string $t + */ +function y($t): void +{ + sayHello($t); +} + +/** + * @param Z $t + * + * @template Z + */ +function z($t): void +{ + assertType('class-string&literal-string', $t::class); + sayHello($t::class); +} + +/** + * @param object $o + */ +function a($o): void +{ + assertType('class-string&literal-string', $o::class); + sayHello($o::class); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7952.php b/tests/PHPStan/Rules/Functions/data/bug-7952.php new file mode 100644 index 0000000000..69dced87b1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7952.php @@ -0,0 +1,29 @@ +null], + ['timespec'=>'2020-01-01T01:02:03+08:00'], + ]; + + $result = []; + foreach($rows as $row) { + $result[] = ($row['timespec'] ?? null) !== null ? createFromString($row['timespec']) : null; + } +}; diff --git a/tests/PHPStan/Rules/Functions/data/bug-7984.php b/tests/PHPStan/Rules/Functions/data/bug-7984.php new file mode 100644 index 0000000000..2ba1d267c7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7984.php @@ -0,0 +1,20 @@ + 7]; + +var_dump(add(...$args, b: 8)); diff --git a/tests/PHPStan/Rules/Functions/data/bug-8179.php b/tests/PHPStan/Rules/Functions/data/bug-8179.php new file mode 100644 index 0000000000..b69568ec38 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8179.php @@ -0,0 +1,43 @@ += 8.1 + +namespace Bug8179; + +enum Row +{ + case I; + case II; + case III; +} + +enum Column +{ + case A; + case B; + case C; +} + +function prepareMatrix(): array +{ + $matrix = array_fill_keys( + array_map(fn($v) => $v->name, Row::cases()), + array_fill_keys(array_map(fn($v) => $v->name, Column::cases()), null) + ); + + foreach ($matrix as $row => $columns) { + foreach ($columns as $column => $value) { + $matrix[$row][$column] = $row.$column; + } + } + + return $matrix; +} + +function showMatrix(array $matrix): void +{ + foreach ($matrix as $rows) { + foreach ($rows as $cell) { + echo $cell." \t"; + } + echo PHP_EOL; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-8205.php b/tests/PHPStan/Rules/Functions/data/bug-8205.php new file mode 100644 index 0000000000..9c8cfaec64 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8205.php @@ -0,0 +1,22 @@ +takes(function () { + test123(); + }); + } + + $tc->takes(function () { + if(function_exists('test123')) { + test123(); + } + }); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-8280.php b/tests/PHPStan/Rules/Functions/data/bug-8280.php new file mode 100644 index 0000000000..5808b4df72 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8280.php @@ -0,0 +1,18 @@ + $var + */ +function foo($var): void {} + +/** @var string|list|null $var */ +if (null !== $var) { + assertType('list', (array) $var); + foo((array) $var); // should work the same as line below + assertType('list', !is_array($var) ? [$var] : $var); + foo(!is_array($var) ? [$var] : $var); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-8389.php b/tests/PHPStan/Rules/Functions/data/bug-8389.php new file mode 100644 index 0000000000..43823068e5 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8389.php @@ -0,0 +1,50 @@ + $a */ +$a = [1]; +/** @var list $b */ +$b = [2]; + +array_push($a, ...$b); + +/** + * @param list $parameter + */ +function test(array $parameter): void +{ +} + +test($a); diff --git a/tests/PHPStan/Rules/Functions/data/bug-8659.php b/tests/PHPStan/Rules/Functions/data/bug-8659.php new file mode 100644 index 0000000000..619f3e40d3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8659.php @@ -0,0 +1,30 @@ + + */ +function myCallableFunction() { + return ['test1' => 4, 'test2' => 45, 'test3' => 3, 'total' => 52]; +} + +/** + * @return array<'test1'|'test2'|'test3'|'total', int> + */ +function IwillCallTheCallable() { + return theCaller(fn () => myCallableFunction()); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-8846.php b/tests/PHPStan/Rules/Functions/data/bug-8846.php new file mode 100644 index 0000000000..a4fc961d4c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8846.php @@ -0,0 +1,24 @@ += 8.0 + +namespace Bug9018; + +// This works +echo levenshtein('test1', 'test2'); + +// This works but fails analysis +echo levenshtein(string1: 'test1', string2: 'test2'); + +// This passes analysis but throws an error +// Warning: Uncaught Error: Unknown named parameter $str1 in php shell code:1 +echo levenshtein(str1: 'test1', str2: 'test2'); diff --git a/tests/PHPStan/Rules/Functions/data/bug-9133.php b/tests/PHPStan/Rules/Functions/data/bug-9133.php new file mode 100644 index 0000000000..a17b650920 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9133.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug3425; + +class HelloWorld +{ + /** @param array $arr */ + public function sayHello(array $arr): void + { + array_map(abs(...), $arr); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9283.php b/tests/PHPStan/Rules/Functions/data/bug-9283.php new file mode 100644 index 0000000000..e365f2a34a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9283.php @@ -0,0 +1,10 @@ += 8.0 + +namespace Bug9283; + +/** + * @param \Stringable $obj + */ +function test(object $obj): string { + return strval($obj); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9380.php b/tests/PHPStan/Rules/Functions/data/bug-9380.php new file mode 100644 index 0000000000..bcd48e6938 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9380.php @@ -0,0 +1,9 @@ += 8.0 + +namespace Bug9399; + +setlocale(category: LC_ALL, locales: 'nl_NL'); diff --git a/tests/PHPStan/Rules/Functions/data/bug-9559.php b/tests/PHPStan/Rules/Functions/data/bug-9559.php new file mode 100644 index 0000000000..8f452e90f0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9559.php @@ -0,0 +1,12 @@ + "3" ])); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9580.php b/tests/PHPStan/Rules/Functions/data/bug-9580.php new file mode 100644 index 0000000000..1b6feb16fc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9580.php @@ -0,0 +1,22 @@ + [1, 2, 3], + 'greet' => fn (int $value) => 'I am '.$value, + ], + [ + 'elements' => ['hello', 'world'], + 'greet' => fn (string $value) => 'I am '.$value, + ], + ]; + + foreach ($data as $entry) { + foreach ($entry['elements'] as $element) { + $entry['greet']($element); + } + } + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9614.php b/tests/PHPStan/Rules/Functions/data/bug-9614.php new file mode 100644 index 0000000000..1209501f2e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9614.php @@ -0,0 +1,27 @@ + function() { + return 'test'; + }, + 'foo' => function($a) { + return 'foo'; + }, + 'bar' => function($a, $b) { + return 'bar'; + } + ]; + + if (!isset($funcs[$key])) { + return ''; + } + + return $funcs[$key]($a, $b); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9697.php b/tests/PHPStan/Rules/Functions/data/bug-9697.php new file mode 100644 index 0000000000..9f1c85e0e1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9697.php @@ -0,0 +1,19 @@ + $a - $b; + + usort($oldItems, $comparator); + + array_udiff( + $oldItems, + $newItems, + $comparator, + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9699.php b/tests/PHPStan/Rules/Functions/data/bug-9699.php new file mode 100644 index 0000000000..09307d4c47 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9699.php @@ -0,0 +1,19 @@ += 8.1 + +namespace Bug9699; + +function withVariadicParam(int $a, int $b, int ...$rest): int +{ + return array_sum([$a, $b, ...$rest]); +} + +/** + * @param \Closure(int, int, int, string): int $f + */ +function int_int_int_string(\Closure $f): void +{ + $f(0, 0, 0, ''); +} + +// false negative: expected issue here +int_int_int_string(withVariadicParam(...)); diff --git a/tests/PHPStan/Rules/Functions/data/bug-9793.php b/tests/PHPStan/Rules/Functions/data/bug-9793.php new file mode 100644 index 0000000000..dbcfc62b8b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9793.php @@ -0,0 +1,19 @@ + $arr + * @param \Iterator<\stdClass>|array<\stdClass> $itOrArr + */ +function foo(array $arr, $itOrArr): void +{ + \iterator_to_array($arr); + \iterator_to_array($itOrArr); + echo \iterator_count($arr); + echo \iterator_count($itOrArr); + \iterator_apply($arr, fn ($x) => $x); + \iterator_apply($itOrArr, fn ($x) => $x); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9803.php b/tests/PHPStan/Rules/Functions/data/bug-9803.php new file mode 100644 index 0000000000..6e02f6ea99 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9803.php @@ -0,0 +1,28 @@ +', $keys); + } + + assertType('array', $keys); + $theKeys = array_keys($keys); + assertType('list', $theKeys); +} + + diff --git a/tests/PHPStan/Rules/Functions/data/bug-9823.php b/tests/PHPStan/Rules/Functions/data/bug-9823.php new file mode 100644 index 0000000000..3c8b3cfb77 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9823.php @@ -0,0 +1,5 @@ += 8.0 + +namespace Bug9923; + +echo join(separator: ' ', array: ['a', 'b', 'c']); +echo implode(separator: ' ', array: ['a', 'b', 'c']); diff --git a/tests/PHPStan/Rules/Functions/data/bug-anonymous-function-method-constant.php b/tests/PHPStan/Rules/Functions/data/bug-anonymous-function-method-constant.php new file mode 100644 index 0000000000..31ef2b5629 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-anonymous-function-method-constant.php @@ -0,0 +1,14 @@ + __FUNCTION__; +$b = fn() => __METHOD__; + +$c = function() { return __FUNCTION__; }; +$d = function() { return __METHOD__; }; + +\PHPStan\Testing\assertType("'{closure}'", $a()); +\PHPStan\Testing\assertType("'{closure}'", $b()); +\PHPStan\Testing\assertType("'{closure}'", $c()); +\PHPStan\Testing\assertType("'{closure}'", $d()); diff --git a/tests/PHPStan/Rules/Functions/data/bug-array-filter.php b/tests/PHPStan/Rules/Functions/data/bug-array-filter.php new file mode 100644 index 0000000000..cb95969e30 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-array-filter.php @@ -0,0 +1,14 @@ + $a <=> $b); diff --git a/tests/PHPStan/Rules/Functions/data/call-to-function-named-params-multivariant.php b/tests/PHPStan/Rules/Functions/data/call-to-function-named-params-multivariant.php new file mode 100644 index 0000000000..71cd2b6653 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/call-to-function-named-params-multivariant.php @@ -0,0 +1,67 @@ += 8.0 + +namespace CallToFunctionNamedParamsMultiVariant; + +// docs say that it's not compatible with named params, but it actually works +setcookie(name: 'aaa', value: 'bbb', expires_or_options: ['httponly' => true]); +setrawcookie(name: 'aaa1', value: 'bbb', expires_or_options: ['httponly' => true]); +var_dump(abs(num: 5)); +var_dump(array_rand(array: [5])); +var_dump(array_rand(array: [5], num: 1)); +var_dump(getenv(name: 'aaa', local_only: true)); +$cal = new \IntlGregorianCalendar(); +var_dump(intlcal_set(calendar: $cal, month: 5, year: 6)); +var_dump(join(separator: 'a', array: [])); +var_dump(join(separator: ['aaa', 'bbb'])); +var_dump(implode(separator: 'a', array: [])); +var_dump(implode(separator: ['aaa', 'bbb'])); +var_dump(levenshtein(string1: 'aaa', string2: 'bbb', insertion_cost: 1, deletion_cost: 1, replacement_cost: 1)); +var_dump(levenshtein(string1: 'aaa', string2: 'bbb')); +// Is it possible to call it with multiple named args? +var_dump(max(value: [5, 6])); +session_set_cookie_params(lifetime_or_options: []); +session_set_cookie_params(lifetime_or_options: 1, path: '/'); +session_set_save_handler(open: new class implements \SessionHandlerInterface { + public function close(): bool + { + return true; + } + + public function destroy(string $id): bool + { + return true; + } + + public function gc(int $max_lifetime): int|false + { + return 0; + } + + public function open(string $path, string $name): bool + { + return true; + } + + public function read(string $id): string|false + { + return true; + } + + public function write(string $id, string $data): bool + { + return true; + } + +}, close: true); +setlocale(category: 0, locales: 'aaa'); +setlocale(category: 0, locales: []); +sscanf(string: 'aaa', format: 'aaa'); +$context = fopen('php://input', 'r'); +assert($context !== false); +stream_context_set_option(context: $context, wrapper_or_options: []); +stream_context_set_option(context: $context, wrapper_or_options: 'aaa', option_name: "aaa", value: 'aaa'); +var_dump(strtok(string: 'bbb aaa ccc', token: 'a')); +// docs say it's not compatible with named params, but it actually works +var_dump(strtok(string: 'a')); +var_dump(strtr(string: 'aaa', from: 'a', to: 'b')); +var_dump(strtr(string: 'aaa', from: ['a' => 'b'])); diff --git a/tests/PHPStan/Rules/Functions/data/call-user-func.php b/tests/PHPStan/Rules/Functions/data/call-user-func.php new file mode 100644 index 0000000000..a0606ba59f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/call-user-func.php @@ -0,0 +1,46 @@ += 8.0 + +namespace CallUserFuncRule; + +use function call_user_func; + +class Foo +{ + + public function doFoo(): void + { + $f = function (int $i): void { + + }; + call_user_func($f); + call_user_func($f, 1); + call_user_func($f, 'foo'); + call_user_func($f, i: 'foo'); + call_user_func(i: 'foo', callback: $f); + call_user_func($f, i: 1); + call_user_func(i: 1, callback: $f); + call_user_func($f, j: 1); + } + + public function doBar(): void + { + $f = function (int $i, $j, $g = 2, $h = 3): void { + }; + + call_user_func($f); + call_user_func($f, 1); + call_user_func($f, 2, 'foo'); + } + + public function doVariadic(): void + { + $f = function ($i, $j, ...$params): void { + }; + + call_user_func($f); + call_user_func($f, 1); + call_user_func($f, 2, 'foo'); + $result = call_user_func($f, 2, 'foo'); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/callables.php b/tests/PHPStan/Rules/Functions/data/callables.php index 002a281566..def053f902 100644 --- a/tests/PHPStan/Rules/Functions/data/callables.php +++ b/tests/PHPStan/Rules/Functions/data/callables.php @@ -117,6 +117,15 @@ public function doBar() } } + public function doBaz(Baz $baz) + { + $baz(); + + if (method_exists($baz, '__invoke')) { + $baz(); + } + } + } class MethodExistsCheckFirst @@ -186,3 +195,51 @@ public function doFoo(bool $foo = true): void } } + +class ConstantArrayUnionCallables +{ + + public function doFoo(): void + { + } + + public function doBar(): void + { + } + + public function invalidClass(): void + { + $class = rand(0, 1) ? __CLASS__ : \DateTimeImmutable::class; + $callable = [$class, 'doFoo']; + $callable(); + } + + public function invalidMethod(): void + { + $method = rand(0, 1) ? 'doFoo' : 'doBaz'; + $callable = [__CLASS__, $method]; + $callable(); + } + + public function classAndMethodValid(): void + { + $class = rand(0, 1) ? __CLASS__ : ConstantArrayUnionCallablesTest::class; + $method = rand(0, 1) ? 'doFoo' : 'doBar'; + $callable = [$class, $method]; + $callable(); + } + +} + +class ConstantArrayUnionCallablesTest +{ + + public function doFoo(): void + { + } + + public function doBar(): void + { + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/closure-7.2-typehints.php b/tests/PHPStan/Rules/Functions/data/closure-7.2-typehints.php index 93fcc3cf86..c6ae38b538 100644 --- a/tests/PHPStan/Rules/Functions/data/closure-7.2-typehints.php +++ b/tests/PHPStan/Rules/Functions/data/closure-7.2-typehints.php @@ -1,4 +1,4 @@ -= 7.2 += 8.0 + +namespace ClosureImplicitNullable; + +class Foo +{ + + public function doFoo(): void + { + $c = function ( + $a = null, + int $b = 1, + int $c = null, + mixed $d = null, + int|string $e = null, + int|string|null $f = null, + \stdClass $g = null, + ?\stdClass $h = null, + ): void { + + }; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/conditional-return-type.php b/tests/PHPStan/Rules/Functions/data/conditional-return-type.php new file mode 100644 index 0000000000..d33c6eb128 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/conditional-return-type.php @@ -0,0 +1,19 @@ + ? T : mixed) + */ +function get(string $id): mixed +{ +} + +/** + * @template T + * @return ($id is not class-string ? T : mixed) + */ +function notGet(string $id): mixed +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/count-array-shift.php b/tests/PHPStan/Rules/Functions/data/count-array-shift.php new file mode 100644 index 0000000000..fcbb82b2ae --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/count-array-shift.php @@ -0,0 +1,19 @@ +|false $a */ +function foo($a): void +{ + while (count($a) > 0) { + array_shift($a); + } +} + +/** @param non-empty-array|false $a */ +function bar($a): void +{ + while (count($a) > 0) { + array_shift($a); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/curl_setopt.php b/tests/PHPStan/Rules/Functions/data/curl_setopt.php new file mode 100644 index 0000000000..bc5b8f6cea --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/curl_setopt.php @@ -0,0 +1,84 @@ + 'bar')); + curl_setopt($curl, CURLOPT_POSTFIELDS, ''); + curl_setopt($curl, CURLOPT_POSTFIELDS, 'para1=val1¶2=val2'); + curl_setopt($curl, CURLOPT_COOKIEFILE, ''); + curl_setopt($curl, CURLOPT_PRE_PROXY, ''); + curl_setopt($curl, CURLOPT_PROXY, ''); + curl_setopt($curl, CURLOPT_PRIVATE, ''); + curl_setopt($curl, CURLOPT_ENCODING, ''); + curl_setopt($curl, CURLOPT_ACCEPT_ENCODING, ''); + } + + public function bug9263() { + $curl = curl_init(); + + $header_dictionary = [ + 'Accept' => 'application/json', + ]; + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_dictionary); + + $header_list = [ + 'Accept: application/json', + ]; + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_list); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/discussion-10454.php b/tests/PHPStan/Rules/Functions/data/discussion-10454.php new file mode 100644 index 0000000000..67adaf0dd0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/discussion-10454.php @@ -0,0 +1,29 @@ + $_POST['policy'], // shouldn't this and the next line be an unsafe offset access? + 'entitlements' => $_POST['entitlements'], +]; +assertType('mixed', $_POST['policy']); +assertType('array{policy: mixed, entitlements: mixed}', $args); +foo($args); // I'd expect this to be reported too + +/** @var mixed $mixed */ +$mixed = null; +$args = [ + 'policy' => $mixed, + 'entitlements' => $mixed, +]; +assertType('mixed', $mixed); +assertType('array{policy: mixed, entitlements: mixed}', $args); +foo($args); diff --git a/tests/PHPStan/Rules/Functions/data/duplicate-function.php b/tests/PHPStan/Rules/Functions/data/duplicate-function.php new file mode 100644 index 0000000000..35efa010bf --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/duplicate-function.php @@ -0,0 +1,23 @@ + 1); + acceptClosure(static fn () => 1); +}; diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php new file mode 100644 index 0000000000..497de17310 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php @@ -0,0 +1,36 @@ + [ + 'method' => 'POST', + 'header' => 'Content-Type: application/json', + 'content' => json_encode($data, JSON_THROW_ON_ERROR), + ], + ])); + file_get_contents($url, false, null); + var_export([]); + var_export([], true); + print_r([]); + print_r([], true); } public function doBar(string $s) diff --git a/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-enum.php b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-enum.php new file mode 100644 index 0000000000..9916cd2e2e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-enum.php @@ -0,0 +1,13 @@ += 8.1 + +namespace ImplodeParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + implode(',', [FooEnum::A]); +} diff --git a/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..362f02ea8b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php @@ -0,0 +1,18 @@ += 8.0 + +namespace ImplodeParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + // implode weirdness + implode(array: [['a']], separator: ','); + implode(separator: [['a']]); + implode(',', array: [['a']]); + implode(separator: ',', array: [['']]); +} + +function wrongNumberOfArguments(): void +{ + implode(array: ','); + join(array: ','); +} diff --git a/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-arrow-functions.php b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-arrow-functions.php new file mode 100644 index 0000000000..a5723019fa --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-arrow-functions.php @@ -0,0 +1,16 @@ + '1'; + $g = fn (?int $i = null) => '1'; + $h = fn (int $i = 5) => '1'; + $i = fn (int $i = 'foo') => '1'; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-closure.php b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-closure.php new file mode 100644 index 0000000000..6043b39fc9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-closure.php @@ -0,0 +1,24 @@ +foo())(); + }; + } + + /** + * @return \Closure(): array + */ + public function superglobals(): \Closure + { + return function () use ($GLOBALS, $_COOKIE, $_ENV, $_FILES, $_GET, $_POST, $_REQUEST, $_SERVER, $_SESSION): array { + return array_merge( + $GLOBALS, + $_COOKIE, + $_ENV, + $_FILES, + $_GET, + $_POST, + $_REQUEST, + $_SERVER, + $_SESSION, + ); + }; + } + + /** + * @return \Closure(int, string, bool): bool + */ + public function sameAsParameter(): \Closure + { + return function (int $foo, string $bar, bool $baz) use ($baz): bool { + return $baz; + }; + } + + /** + * @return \Closure(): string + */ + public function multilineThis(): \Closure + { + $message = 'hello'; + + return function () use ( + $this, + $message + ): string { + return ($this->foo())() . $message; + }; + } + + /** + * @return \Closure(): array + */ + public function multilineSuperglobals(): \Closure + { + return function () use ( + $GLOBALS, + $_COOKIE, + $_ENV, + $_FILES, + $_GET, + $_POST, + $_REQUEST, + $_SERVER, + $_SESSION + ): array { + return array_merge( + $GLOBALS, + $_COOKIE, + $_ENV, + $_FILES, + $_GET, + $_POST, + $_REQUEST, + $_SERVER, + $_SESSION, + ); + }; + } + + /** + * @return \Closure(int, string, bool): bool + */ + public function multilineSameAsParameter(): \Closure + { + return function (int $foo, string $bar, bool $baz) use ( + $baz, + $bar, + ): bool { + return (bool) ($baz . $bar); + }; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/is-subclass-allow-string.php b/tests/PHPStan/Rules/Functions/data/is-subclass-allow-string.php new file mode 100644 index 0000000000..af80c07eb5 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/is-subclass-allow-string.php @@ -0,0 +1,24 @@ + $a + * @param-out array $a + */ + function oneArray(&$a): void { + + } + + /** + * @param mixed $a + * @param-out \ReflectionClass $a + */ + function generics(&$a): void { + + } +} + +namespace MissingParamClosureThisType { + /** + * @param-closure-this \ReflectionClass $cb + * @param callable(): void $cb + */ + function generics(callable $cb): void + { + + } +} diff --git a/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php b/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php new file mode 100755 index 0000000000..29d9ac8b4e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php @@ -0,0 +1,14 @@ + 2, 'a' => 1], d: 40)); // 46 + +var_dump(foo(...[1, 2], b: 20)); // Fatal error. Named parameter $b overwrites previous argument diff --git a/tests/PHPStan/Rules/Functions/data/no-named-arguments-call-user-func.php b/tests/PHPStan/Rules/Functions/data/no-named-arguments-call-user-func.php new file mode 100644 index 0000000000..d385b739c9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/no-named-arguments-call-user-func.php @@ -0,0 +1,33 @@ += 8.1 + +namespace NoNamedArgumentsCallUserFunc; + +use function call_user_func; + +/** + * @no-named-arguments + */ +function foo(int $i): void +{ + +} + +class Foo +{ + + /** + * @no-named-arguments + */ + public function doFoo(int $i): void + { + + } + +} + +function (Foo $f): void { + call_user_func(foo(...), i: 1); + call_user_func('NoNamedArgumentsCallUserFunc\\foo', i: 1); + call_user_func([$f, 'doFoo'], i: 1); + call_user_func($f->doFoo(...), i: 1); +}; diff --git a/tests/PHPStan/Rules/Functions/data/no-named-arguments.php b/tests/PHPStan/Rules/Functions/data/no-named-arguments.php new file mode 100644 index 0000000000..843530132e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/no-named-arguments.php @@ -0,0 +1,30 @@ += 8.0 + +namespace NoNamedArgumentsFunction; + +/** + * @no-named-arguments + */ +function foo(int $i): void +{ + +} + +function (): void { + foo(i: 5); +}; + +/** + * @param array $a + * @param array $b + * @param array $c + */ +function bar(array $a, array $b, array $c): void +{ + foo(...$a); + foo(...$b); + foo(...$c); + + foo(...[0 => 1]); + foo(...['i' => 1]); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php new file mode 100644 index 0000000000..91e9f1f686 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php @@ -0,0 +1,14 @@ += 8.1 + +namespace ParamCastableToNumberFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + array_sum([FooEnum::A]); + array_product([FooEnum::A]); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php new file mode 100644 index 0000000000..4fdc546062 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php @@ -0,0 +1,15 @@ += 8.0 + +namespace ParamCastableToNumberFunctionsNamedArgs; + +function invalidUsages() +{ + var_dump(array_sum(array: [[0]])); + var_dump(array_product(array: [[0]])); +} + +function validUsages() +{ + var_dump(array_sum(array: [1])); + var_dump(array_product(array: [1])); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php new file mode 100644 index 0000000000..9e7c5da4d2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php @@ -0,0 +1,45 @@ +7.7'), 5, 5.5, null])); + var_dump(array_product(['5.5', false, true, new \SimpleXMLElement('7.7'), 5, 5.5, null])); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php new file mode 100644 index 0000000000..bbf189cf96 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php @@ -0,0 +1,24 @@ += 8.1 + +namespace ParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + array_intersect([FooEnum::A], ['a']); + array_intersect(['a'], [FooEnum::A]); + array_intersect(['a'], [], [FooEnum::A]); + array_diff(['a'], [FooEnum::A]); + array_diff_assoc(['a'], [FooEnum::A]); + + array_combine([FooEnum::A], [['b']]); + $arr1 = [FooEnum::A]; + natsort($arr1); + natcasesort($arr1); + array_count_values($arr1); + array_fill_keys($arr1, 5); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..b8790a475d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php @@ -0,0 +1,23 @@ += 8.0 + +namespace ParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + array_combine(values: [['b']], keys: [['a']]); + $arr1 = [['a']]; + array_fill_keys(value: 5, keys: $arr1); +} + +function wrongNumberOfArguments(): void +{ + array_combine(values: [[5]]); + array_fill_keys(value: [5]); +} + +function validUsages() +{ + array_combine(values: [['b']], keys: ['a']); + $arr1 = ['a']; + array_fill_keys(value: 5, keys: $arr1); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php new file mode 100644 index 0000000000..008c2d0142 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php @@ -0,0 +1,63 @@ + (int) $bar; + + return function (string $baz, int $baz) use ($callback): int { + return $callback($baz, []); + }; + } + + /** + * @return \Closure(string, bool): int + */ + public function bar(string $pipe, int $count): \Closure + { + $cb = fn (int $a, string $b): int => $a + (int) $b; + + return function (string $c, bool $d) use ($cb, $pipe, $count): int { + return $cb((int) $d, $c) + $cb($count, $pipe); + }; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/remember-function-exists-from-constructor.php b/tests/PHPStan/Rules/Functions/data/remember-function-exists-from-constructor.php new file mode 100644 index 0000000000..1d8640a9ae --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/remember-function-exists-from-constructor.php @@ -0,0 +1,35 @@ += 7.4 + +namespace RememberFunctionExistsFromConstructor; + +class User +{ + public function __construct( + ) { + if (!function_exists('some_unknown_function')) { + throw new \LogicException(); + } + } + + public function doFoo(): void + { + some_unknown_function(); + } + +} + +class FooUser +{ + public function __construct( + ) { + if (!function_exists('another_unknown_function')) { + echo 'Function another_unknown_function does not exist'; + } + } + + public function doFoo(): void + { + another_unknown_function(); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/removed-functions-from-php8.php b/tests/PHPStan/Rules/Functions/data/removed-functions-from-php8.php new file mode 100644 index 0000000000..459dd98d28 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/removed-functions-from-php8.php @@ -0,0 +1,11 @@ += 7.4 += 8.0 namespace RequiredAfterOptional; @@ -9,3 +9,17 @@ fn (int $foo = 1, $bar): int => 1; // not OK fn (bool $foo = true, $bar): int => 1; // not OK + +fn (?int $foo = 1, $bar): int => 1; // not OK + +fn (?int $foo = null, $bar): int => 1; // not OK + +fn (int|null $foo = 1, $bar): int => 1; // not OK + +fn (int|null $foo = null, $bar): int => 1; // not OK + +fn (mixed $foo = 1, $bar): int => 1; // not OK + +fn (mixed $foo = null, $bar): int => 1; // not OK + +fn (int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): int => 1; // not OK diff --git a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php index fdd7db4709..da96ec6909 100644 --- a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php @@ -1,4 +1,4 @@ -= 8.0 namespace RequiredAfterOptional; @@ -17,3 +17,31 @@ function (int $foo = 1, $bar): void // not OK function(bool $foo = true, $bar): void // not OK { }; + +function (?int $foo = 1, $bar): void // not OK +{ +}; + +function (?int $foo = null, $bar): void // not OK +{ +}; + +function (int|null $foo = 1, $bar): void // not OK +{ +}; + +function (int|null $foo = null, $bar): void // not OK +{ +}; + +function (mixed $foo = 1, $bar): void // not OK +{ +}; + +function (mixed $foo = null, $bar): void // not OK +{ +}; + +function (int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK +{ +}; diff --git a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php index 373a441903..8937d262a7 100644 --- a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php @@ -1,4 +1,4 @@ -= 8.0 namespace RequiredAfterOptional; @@ -22,3 +22,31 @@ function doLorem(bool $foo = true, $bar): void // not OK function doIpsum(bool $foo = true, ...$bar): void // OK { } + +function doDolor(?int $foo = 1, $bar): void // not OK +{ +} + +function doSit(?int $foo = null, $bar): void // not OK +{ +} + +function doAmet(int|null $foo = 1, $bar): void // not OK +{ +} + +function doConsectetur(int|null $foo = null, $bar): void // not OK +{ +} + +function doAdipiscing(mixed $foo = 1, $bar): void // not OK +{ +} + +function doElit(mixed $foo = null, $bar): void // not OK +{ +} + +function doSed(int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/return-list-nullables.php b/tests/PHPStan/Rules/Functions/data/return-list-nullables.php new file mode 100644 index 0000000000..ae6762061f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/return-list-nullables.php @@ -0,0 +1,17 @@ + $x + * @return array|null + */ +function doFoo(array $x): ?array +{ + $list = []; + foreach ($x as $v) { + $list[] = $v; + } + + return $list; +} diff --git a/tests/PHPStan/Rules/Functions/data/return-null-safe-by-ref-property-hooks.php b/tests/PHPStan/Rules/Functions/data/return-null-safe-by-ref-property-hooks.php new file mode 100644 index 0000000000..f902fa9f00 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/return-null-safe-by-ref-property-hooks.php @@ -0,0 +1,16 @@ += 8.4 + +namespace ReturnNullSafeByRefPropertyHools; + +use stdClass; + +class Foo +{ + public int $i { + &get { + $foo = new stdClass(); + + return $foo?->foo; + } + } +} diff --git a/tests/PHPStan/Rules/Functions/data/sensitive-parameter.php b/tests/PHPStan/Rules/Functions/data/sensitive-parameter.php new file mode 100644 index 0000000000..473c143255 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sensitive-parameter.php @@ -0,0 +1,13 @@ += 8.1 + +namespace SortParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages():void +{ + array_unique(['a', FooEnum::A]); + $arr1 = [FooEnum::A]; + sort($arr1, SORT_STRING); + rsort($arr1, SORT_LOCALE_STRING); + asort($arr1, SORT_STRING | SORT_FLAG_CASE); + arsort($arr1, SORT_LOCALE_STRING | SORT_FLAG_CASE); +} + +function validUsages(): void +{ + $arr = [FooEnum::A, 1]; + array_unique($arr, SORT_REGULAR); + sort($arr, SORT_REGULAR); + rsort($arr, 128); +} diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..2a69d861ce --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php @@ -0,0 +1,32 @@ += 8.0 + +namespace SortParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + array_unique(flags: SORT_STRING, array: [['a'], ['b']]); + $arr1 = [['a']]; + sort(flags: SORT_STRING, array: $arr1); + rsort(flags: SORT_STRING, array: $arr1); + asort(flags: SORT_STRING, array: $arr1); + arsort(flags: SORT_STRING, array: $arr1); +} + +function wrongNumberOfArguments(): void +{ + array_unique(flags: SORT_STRING); + sort(flags: SORT_STRING); + rsort(flags: SORT_STRING); + asort(flags: SORT_STRING); + arsort(flags: SORT_STRING); +} + +function validUsages() +{ + array_unique(flags: SORT_STRING, array: ['a', 'b']); + $arr1 = ['a']; + sort(flags: SORT_STRING, array: $arr1); + rsort(flags: SORT_STRING, array: $arr1); + asort(flags: SORT_STRING, array: $arr1); + arsort(flags: SORT_STRING, array: $arr1); +} diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php new file mode 100644 index 0000000000..e1e0ff0dca --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php @@ -0,0 +1,71 @@ + static function ($one, $two) { + return $one ?? $two; + }, + 'b' => static function ($one, $two, $three) { + return $one ?? $two ?? $three; + }, + ]; + + foreach (['c', 'd', 'e'] as $name) { + self::$resolvers[$name] = static function ($one, $two) { + return self::$resolvers['a']($one, $two); + }; + + self::$resolvers[$name] = static fn ($one, $two) => self::$resolvers['a']($one, $two); + } + } + + return self::$resolvers; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/true-typehint.php b/tests/PHPStan/Rules/Functions/data/true-typehint.php new file mode 100644 index 0000000000..da84a9cb26 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/true-typehint.php @@ -0,0 +1,8 @@ += 8.2 + +namespace NativeTrueType; + +function alwaysTrue(): true +{ + return true; +} diff --git a/tests/PHPStan/Rules/Functions/data/uasort_arrow.php b/tests/PHPStan/Rules/Functions/data/uasort_arrow.php index ad064939e7..c23a622d90 100644 --- a/tests/PHPStan/Rules/Functions/data/uasort_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/uasort_arrow.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 8.0 + +namespace UselessFunctionReturnPhp8; + +class FooClass +{ + public function explicitReturnNamed(): void + { + error_log("Email-Template couldn't be found by parameters:" . print_r(return: true, value: [ + 'template' => 1, + 'spracheid' => 2, + ]) + ); + } + + public function explicitNoReturnNamed(): void + { + error_log("Email-Template couldn't be found by parameters:" . print_r(return: false, value: [ + 'template' => 1, + 'spracheid' => 2, + ]) + ); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/useless-fn-return.php b/tests/PHPStan/Rules/Functions/data/useless-fn-return.php new file mode 100644 index 0000000000..204371923b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/useless-fn-return.php @@ -0,0 +1,71 @@ + 1, + 'spracheid' => 2, + ], true) + ); + + $x = print_r([ + 'template' => 1, + 'spracheid' => 2, + ], true); + + print_r([ + 'template' => 1, + 'spracheid' => 2, + ]); + + error_log( + "Email-Template couldn't be found by parameters:" . print_r([ + 'template' => 1, + 'spracheid' => 2, + ], $bool) + ); + + print_r([ + 'template' => 1, + 'spracheid' => 2, + ], $bool); + + $x = print_r([ + 'template' => 1, + 'spracheid' => 2, + ], $bool); + } + + public function missesReturn(): void + { + error_log( + "Email-Template couldn't be found by parameters:" . print_r([ + 'template' => 1, + 'spracheid' => 2, + ]) + ); + } + + public function missesReturnVarDump(): string + { + return "Email-Template couldn't be found by parameters:" . var_export([ + 'template' => 1, + 'spracheid' => 2, + ]); + } + + public function explicitNoReturn(): void + { + error_log("Email-Template couldn't be found by parameters:" . print_r([ + 'template' => 1, + 'spracheid' => 2, + ], false) + ); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/usort_arrow.php b/tests/PHPStan/Rules/Functions/data/usort_arrow.php index d75df390c7..66f079fdbd 100644 --- a/tests/PHPStan/Rules/Functions/data/usort_arrow.php +++ b/tests/PHPStan/Rules/Functions/data/usort_arrow.php @@ -1,4 +1,4 @@ -= 7.4 +format('j. n. Y'); + } + + public function variadicParamAtEnd(int $number, int ...$numbers): void + { + } +} + +function variadicFunction(int ...$a, string $b): void +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/vprintf.php b/tests/PHPStan/Rules/Functions/data/vprintf.php new file mode 100644 index 0000000000..de3f640310 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/vprintf.php @@ -0,0 +1,64 @@ +createReflectionProvider(), true, false, true, false), true); + return new YieldFromTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), true); } public function testRule(): void @@ -39,6 +39,7 @@ public function testRule(): void [ 'Generator expects value type array{DateTime, DateTime, stdClass, DateTimeImmutable}, array{0: DateTime, 1: DateTime, 2: stdClass, 4: DateTimeImmutable} given.', 74, + 'Array does not have offset 3.', ], [ 'Result of yield from (void) is used.', @@ -47,4 +48,9 @@ public function testRule(): void ]); } + public function testBug11517(): void + { + $this->analyse([__DIR__ . '/data/bug-11517.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index 15a07d334c..500199d28e 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -14,7 +14,7 @@ class YieldTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new YieldTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new YieldTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void @@ -47,6 +47,7 @@ public function testRule(): void [ 'Generator expects value type array{0: DateTime, 1: DateTime, 2: stdClass, 4: DateTimeImmutable}, array{DateTime, DateTime, stdClass, DateTimeImmutable} given.', 25, + 'Array does not have offset 4.', ], [ 'Result of yield (void) is used.', @@ -59,4 +60,15 @@ public function testRule(): void ]); } + public function testBug7484(): void + { + $this->analyse([__DIR__ . '/data/bug-7484.php'], [ + [ + 'Generator expects key type K of int|string, (K of int)|string given.', + 21, + 'Type string is not always the same as K. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generators/data/bug-11517.php b/tests/PHPStan/Rules/Generators/data/bug-11517.php new file mode 100644 index 0000000000..56b64a5bd0 --- /dev/null +++ b/tests/PHPStan/Rules/Generators/data/bug-11517.php @@ -0,0 +1,30 @@ + + */ + public function bug(): iterable + { + yield from []; + } + + /** + * @return iterable + */ + public function fine(): iterable + { + yield from []; + } + + /** + * @return iterable + */ + public function finetoo(): iterable + { + yield from []; + } +} diff --git a/tests/PHPStan/Rules/Generators/data/bug-7484.php b/tests/PHPStan/Rules/Generators/data/bug-7484.php new file mode 100644 index 0000000000..a0d8889a9c --- /dev/null +++ b/tests/PHPStan/Rules/Generators/data/bug-7484.php @@ -0,0 +1,23 @@ + $iterable + * @return iterable + */ +function changeKeyCase( + iterable $iterable, + int $case = CASE_LOWER +): iterable { + $callable = $case === CASE_LOWER ? 'strtolower' : 'strtoupper'; + foreach ($iterable as $key => $value) { + if (is_string($key)) { + $key = $callable($key); + } + + yield $key => $value; + } +} diff --git a/tests/PHPStan/Rules/Generators/data/yield.php b/tests/PHPStan/Rules/Generators/data/yield.php index c74eb39013..226f07de3b 100644 --- a/tests/PHPStan/Rules/Generators/data/yield.php +++ b/tests/PHPStan/Rules/Generators/data/yield.php @@ -18,7 +18,7 @@ public function doFoo(): \Generator } /** - * @return\Generator + * @return \Generator */ public function doArrayShape(): \Generator { diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index fda1c25d6f..dd82f5bada 100644 --- a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -18,8 +19,9 @@ protected function getRule(): Rule $this->createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true, + new UnresolvableTypeHelper(), [], + true, ), new CrossCheckInterfacesHelper(), ); @@ -43,7 +45,6 @@ public function testRuleExtends(): void [ 'Class ClassAncestorsExtends\FooWrongClassExtended extends generic class ClassAncestorsExtends\FooGeneric but does not specify its types: T, U', 43, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Class ClassAncestorsExtends\FooWrongTypeInExtendsTag @extends tag contains incompatible type class-string.', @@ -52,7 +53,6 @@ public function testRuleExtends(): void [ 'Class ClassAncestorsExtends\FooWrongTypeInExtendsTag extends generic class ClassAncestorsExtends\FooGeneric but does not specify its types: T, U', 51, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type ClassAncestorsExtends\FooGeneric in PHPDoc tag @extends does not specify all template types of class ClassAncestorsExtends\FooGeneric: T, U', @@ -89,16 +89,46 @@ public function testRuleExtends(): void [ 'Class ClassAncestorsExtends\FooExtendsGenericClass extends generic class ClassAncestorsExtends\FooGeneric but does not specify its types: T, U', 174, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Template type T is declared as covariant, but occurs in invariant position in extended type ClassAncestorsExtends\FooGeneric8 of class ClassAncestorsExtends\FooGeneric9.', 192, ], + [ + 'Template type T is declared as contravariant, but occurs in covariant position in extended type ClassAncestorsExtends\FooGeneric8 of class ClassAncestorsExtends\FooGeneric10.', + 201, + ], + [ + 'Template type T is declared as contravariant, but occurs in invariant position in extended type ClassAncestorsExtends\FooGeneric8 of class ClassAncestorsExtends\FooGeneric10.', + 201, + ], [ 'Class ClassAncestorsExtends\FilterIteratorChild extends generic class FilterIterator but does not specify its types: TKey, TValue, TIterator', - 197, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + 215, + ], + [ + 'Class ClassAncestorsExtends\FooObjectStorage @extends tag contains incompatible type ClassAncestorsExtends\FooObjectStorage.', + 226, + ], + [ + 'Class ClassAncestorsExtends\FooObjectStorage extends generic class SplObjectStorage but does not specify its types: TObject, TData', + 226, + ], + [ + 'Class ClassAncestorsExtends\FooCollection @extends tag contains incompatible type ClassAncestorsExtends\FooCollection&iterable.', + 239, + ], + [ + 'Class ClassAncestorsExtends\FooCollection extends generic class ClassAncestorsExtends\AbstractFooCollection but does not specify its types: T', + 239, + ], + [ + 'Call-site variance annotation of covariant Throwable in generic type ClassAncestorsExtends\FooGeneric in PHPDoc tag @extends is not allowed.', + 246, + ], + [ + 'PHPDoc tag @extends has invalid type ClassAncestorsExtends\FooTrait.', + 259, ], ]); } @@ -121,12 +151,10 @@ public function testRuleImplements(): void [ 'Class ClassAncestorsImplements\FooWrongClassImplemented implements generic interface ClassAncestorsImplements\FooGeneric but does not specify its types: T, U', 52, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Class ClassAncestorsImplements\FooWrongClassImplemented implements generic interface ClassAncestorsImplements\FooGeneric3 but does not specify its types: T, W', 52, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Class ClassAncestorsImplements\FooWrongTypeInImplementsTag @implements tag contains incompatible type class-string.', @@ -135,7 +163,6 @@ public function testRuleImplements(): void [ 'Class ClassAncestorsImplements\FooWrongTypeInImplementsTag implements generic interface ClassAncestorsImplements\FooGeneric but does not specify its types: T, U', 60, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type ClassAncestorsImplements\FooGeneric in PHPDoc tag @implements does not specify all template types of interface ClassAncestorsImplements\FooGeneric: T, U', @@ -180,12 +207,31 @@ public function testRuleImplements(): void [ 'Class ClassAncestorsImplements\FooImplementsGenericInterface implements generic interface ClassAncestorsImplements\FooGeneric but does not specify its types: T, U', 198, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Template type T is declared as covariant, but occurs in invariant position in implemented type ClassAncestorsImplements\FooGeneric9 of class ClassAncestorsImplements\FooGeneric10.', 216, ], + [ + 'Class ClassAncestorsImplements\FooIterator @implements tag contains incompatible type ClassAncestorsImplements\FooIterator&iterable.', + 222, + ], + [ + 'Class ClassAncestorsImplements\FooIterator implements generic interface Iterator but does not specify its types: TKey, TValue', + 222, + ], + [ + 'Class ClassAncestorsImplements\FooCollection @implements tag contains incompatible type ClassAncestorsImplements\FooCollection&iterable.', + 235, + ], + [ + 'Class ClassAncestorsImplements\FooCollection implements generic interface ClassAncestorsImplements\AbstractFooCollection but does not specify its types: T', + 235, + ], + [ + 'Call-site variance annotation of covariant Throwable in generic type ClassAncestorsImplements\FooGeneric in PHPDoc tag @implements is not allowed.', + 242, + ], ]); } @@ -224,4 +270,19 @@ public function testScalarClassName(): void $this->analyse([__DIR__ . '/data/scalar-class-name.php'], []); } + public function testBug8473(): void + { + $this->analyse([__DIR__ . '/data/bug-8473.php'], []); + } + + public function testBug11552(): void + { + $this->analyse([__DIR__ . '/data/bug-11552.php'], [ + [ + 'Class Bug11552\SomeResult @extends tag contains unresolvable type.', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index f9d99309ad..0ef409ddf8 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -3,8 +3,11 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -14,13 +17,18 @@ class ClassTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new ClassTemplateTypeRule( new TemplateTypeCheck( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), new GenericObjectTypeCheck(), $typeAliasResolver, true, @@ -71,6 +79,27 @@ public function testRule(): void 'PHPDoc tag @template for anonymous class cannot have existing type alias TypeAlias as its name.', 78, ], + [ + 'Call-site variance of covariant int in generic type ClassTemplateType\Consecteur in PHPDoc tag @template U is redundant, template type T of class ClassTemplateType\Consecteur has the same variance.', + 113, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type ClassTemplateType\Consecteur in PHPDoc tag @template W is in conflict with covariant template type T of class ClassTemplateType\Consecteur.', + 113, + ], + [ + 'PHPDoc tag @template T for class ClassTemplateType\Elit has invalid default type ClassTemplateType\Zazzzu.', + 121, + ], + [ + 'Default type bool in PHPDoc tag @template T for class ClassTemplateType\Venenatis is not subtype of bound type object.', + 129, + ], + [ + 'PHPDoc tag @template V for class ClassTemplateType\Mauris does not have a default type but follows an optional @template U.', + 139, + ], ]); } @@ -110,4 +139,17 @@ public function testInInterface(): void $this->analyse([__DIR__ . '/data/interface-template.php'], []); } + public function testBug10049(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-10049.php'], [ + [ + 'PHPDoc tag @template for class Bug10049\SimpleEntity cannot have existing class Bug10049\SimpleEntity as its name.', + 8, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php index 22524bd2ce..18181f9bae 100644 --- a/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -19,8 +20,9 @@ protected function getRule(): Rule $this->createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true, + new UnresolvableTypeHelper(), [], + true, ), new CrossCheckInterfacesHelper(), ); @@ -44,7 +46,6 @@ public function testRule(): void [ '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', @@ -54,6 +55,10 @@ public function testRule(): void 'Enum EnumGenericAncestors\Foo7 has @extends tag, but cannot extend anything.', 64, ], + [ + 'Call-site variance annotation of covariant EnumGenericAncestors\NonGeneric in generic type EnumGenericAncestors\Generic in PHPDoc tag @implements is not allowed.', + 93, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 22cb75b8b4..af46e7e0f5 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -15,12 +17,23 @@ class FunctionTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new FunctionTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -47,6 +60,27 @@ public function testRule(): void 'PHPDoc tag @template T for function FunctionTemplateType\nullNotSupported() with bound type null is not supported.', 68, ], + [ + 'Call-site variance of covariant int in generic type FunctionTemplateType\GenericCovariant in PHPDoc tag @template U is redundant, template type T of class FunctionTemplateType\GenericCovariant has the same variance.', + 94, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type FunctionTemplateType\GenericCovariant in PHPDoc tag @template W is in conflict with covariant template type T of class FunctionTemplateType\GenericCovariant.', + 94, + ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\invalidDefault() has invalid default type FunctionTemplateType\Zazzzu.', + 102, + ], + [ + 'Default type bool in PHPDoc tag @template T for function FunctionTemplateType\outOfBoundsDefault() is not subtype of bound type object.', + 110, + ], + [ + 'PHPDoc tag @template V for function FunctionTemplateType\requiredAfterOptional() does not have a default type but follows an optional @template U.', + 120, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php index 63855f7c05..f82610ea0b 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -18,8 +19,9 @@ protected function getRule(): Rule $this->createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true, + new UnresolvableTypeHelper(), [], + true, ), new CrossCheckInterfacesHelper(), ); @@ -108,6 +110,10 @@ public function testRuleImplements(): void 'Interface InterfaceAncestorsImplements\FooGenericGeneric8 has @implements tag, but can not implement any interface, must extend from it.', 182, ], + [ + 'Interface InterfaceAncestorsImplements\FooTypeProjection has @implements tag, but can not implement any interface, must extend from it.', + 190, + ], ]); } @@ -129,12 +135,10 @@ public function testRuleExtends(): void [ 'Interface InterfaceAncestorsExtends\FooWrongClassImplemented extends generic interface InterfaceAncestorsExtends\FooGeneric but does not specify its types: T, U', 52, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Interface InterfaceAncestorsExtends\FooWrongClassImplemented extends generic interface InterfaceAncestorsExtends\FooGeneric3 but does not specify its types: T, W', 52, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Interface InterfaceAncestorsExtends\FooWrongTypeInImplementsTag @extends tag contains incompatible type class-string.', @@ -143,7 +147,6 @@ public function testRuleExtends(): void [ 'Interface InterfaceAncestorsExtends\FooWrongTypeInImplementsTag extends generic interface InterfaceAncestorsExtends\FooGeneric but does not specify its types: T, U', 60, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type InterfaceAncestorsExtends\FooGeneric in PHPDoc tag @extends does not specify all template types of interface InterfaceAncestorsExtends\FooGeneric: T, U', @@ -188,12 +191,15 @@ public function testRuleExtends(): void [ 'Interface InterfaceAncestorsExtends\ExtendsGenericInterface extends generic interface InterfaceAncestorsExtends\FooGeneric but does not specify its types: T, U', 197, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Template type T is declared as covariant, but occurs in invariant position in extended type InterfaceAncestorsExtends\FooGeneric9 of interface InterfaceAncestorsExtends\FooGeneric10.', 215, ], + [ + 'Call-site variance annotation of covariant LogicException in generic type InterfaceAncestorsExtends\FooGeneric in PHPDoc tag @extends is not allowed.', + 223, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index 243a7bd69f..3823a214f7 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,11 +16,22 @@ class InterfaceTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new InterfaceTemplateTypeRule( - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -45,6 +58,27 @@ public function testRule(): void 'PHPDoc tag @template for interface InterfaceTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.', 45, ], + [ + 'Call-site variance of covariant int in generic type InterfaceTemplateType\Covariant in PHPDoc tag @template U is redundant, template type T of interface InterfaceTemplateType\Covariant has the same variance.', + 74, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InterfaceTemplateType\Covariant in PHPDoc tag @template W is in conflict with covariant template type T of interface InterfaceTemplateType\Covariant.', + 74, + ], + [ + 'PHPDoc tag @template T for interface InterfaceTemplateType\InvalidDefault has invalid default type InterfaceTemplateType\Zazzzu.', + 82, + ], + [ + 'Default type bool in PHPDoc tag @template T for interface InterfaceTemplateType\OutOfBoundsDefault is not subtype of bound type object.', + 90, + ], + [ + 'PHPDoc tag @template V for interface InterfaceTemplateType\RequiredAfterOptional does not have a default type but follows an optional @template U.', + 100, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php index b0b64eb766..ec893f0947 100644 --- a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -22,25 +23,229 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/method-signature-variance.php'], [ [ - 'Template type T is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\C::a().', - 25, + 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type U in in method MethodSignatureVariance\C::b().', + 16, ], [ - 'Template type T is declared as covariant, but occurs in invariant position in parameter b of method MethodSignatureVariance\C::a().', - 25, + 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type U in in method MethodSignatureVariance\C::c().', + 22, ], + ]); + + $this->analyse([__DIR__ . '/data/method-signature-variance-invariant.php'], []); + + $this->analyse([__DIR__ . '/data/method-signature-variance-covariant.php'], [ [ - 'Template type T is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\C::a().', - 25, + 'Template type X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\Covariant\C::a().', + 35, ], [ - 'Template type W is declared as covariant, but occurs in contravariant position in parameter d of method MethodSignatureVariance\C::a().', - 25, + 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\Covariant\C::a().', + 35, ], [ - 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type U in in method MethodSignatureVariance\C::b().', + 'Template type X is declared as covariant, but occurs in invariant position in parameter e of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter f of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter h of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter i of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter j of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter k of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter l of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\Covariant\C::c().', + 41, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\Covariant\C::e().', + 47, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::f().', + 50, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\Covariant\C::h().', + 56, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::j().', + 62, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::k().', + 65, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::l().', + 68, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::m().', + 71, + ], + ]); + + $this->analyse([__DIR__ . '/data/method-signature-variance-contravariant.php'], [ + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter b of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter d of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter e of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter g of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter i of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter j of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter k of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter l of method MethodSignatureVariance\Contravariant\C::a().', 35, ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::b().', + 38, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::d().', + 44, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::f().', + 50, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::g().', + 53, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::i().', + 59, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::j().', + 62, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::k().', + 65, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::l().', + 68, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::m().', + 71, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in param-out type of parameter a of method MethodSignatureVariance\Contravariant\C::paramOut().', + 79, + ], + ]); + + $this->analyse([__DIR__ . '/data/method-signature-variance-constructor.php'], []); + + $this->analyse([__DIR__ . '/data/method-signature-variance-static.php'], [ + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\StaticMethod\B::a().', + 43, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\StaticMethod\B::a().', + 43, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\StaticMethod\B::c().', + 49, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter b of method MethodSignatureVariance\StaticMethod\C::a().', + 62, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\StaticMethod\C::b().', + 65, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\StaticMethod\C::d().', + 71, + ], + ]); + } + + public function testBug8880(): void + { + $this->analyse([__DIR__ . '/data/bug-8880.php'], [ + [ + 'Template type T is declared as covariant, but occurs in contravariant position in parameter items of method Bug8880\IProcessor::processItems().', + 17, + ], + ]); + } + + public function testBug9161(): void + { + $this->analyse([__DIR__ . '/data/bug-9161.php'], []); + } + + public function testPr2465(): void + { + $this->analyse([__DIR__ . '/data/pr-2465.php'], [ + [ + 'Template type T is declared as covariant, but occurs in invariant position in parameter thing of method Pr2465\UnitOfTest::foo().', + 16, + ], + ]); + } + + public function testBug10609(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-10609.php'], [ + [ + 'Template type A is declared as covariant, but occurs in contravariant position in parameter fn of method Bug10609\Collection::tap().', + 13, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php new file mode 100644 index 0000000000..758c11548d --- /dev/null +++ b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php @@ -0,0 +1,64 @@ + + */ +class MethodTagTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + + return new MethodTagTemplateTypeRule( + new MethodTagTemplateTypeCheck( + self::getContainer()->getByType(FileTypeMapper::class), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-tag-template.php'], [ + [ + 'PHPDoc tag @method template U for method MethodTagTemplate\HelloWorld::sayHello() has invalid bound type MethodTagTemplate\Nonexisting.', + 13, + ], + [ + 'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::sayHello() cannot have existing class stdClass as its name.', + 13, + ], + [ + 'PHPDoc tag @method template T for method MethodTagTemplate\HelloWorld::sayHello() shadows @template T for class MethodTagTemplate\HelloWorld.', + 13, + ], + [ + 'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::typeAlias() cannot have existing type alias TypeAlias as its name.', + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeTraitRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeTraitRuleTest.php new file mode 100644 index 0000000000..470d48adc5 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeTraitRuleTest.php @@ -0,0 +1,65 @@ + + */ +class MethodTagTemplateTypeTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + + return new MethodTagTemplateTypeTraitRule( + new MethodTagTemplateTypeCheck( + self::getContainer()->getByType(FileTypeMapper::class), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), + $reflectionProvider, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-tag-trait-template.php'], [ + [ + 'PHPDoc tag @method template U for method MethodTagTraitTemplate\HelloWorld::sayHello() has invalid bound type MethodTagTraitTemplate\Nonexisting.', + 11, + ], + [ + 'PHPDoc tag @method template for method MethodTagTraitTemplate\HelloWorld::sayHello() cannot have existing class stdClass as its name.', + 11, + ], + [ + 'PHPDoc tag @method template T for method MethodTagTraitTemplate\HelloWorld::sayHello() shadows @template T for class MethodTagTraitTemplate\HelloWorld.', + 11, + ], + [ + 'PHPDoc tag @method template for method MethodTagTraitTemplate\HelloWorld::typeAlias() cannot have existing type alias TypeAlias as its name.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index c46eb033db..8450ba5f3e 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -15,12 +17,23 @@ class MethodTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new MethodTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -53,6 +66,27 @@ public function testRule(): void 'PHPDoc tag @template for method MethodTemplateType\Ipsum::doFoo() cannot have existing type alias ImportedAlias as its name.', 85, ], + [ + 'Call-site variance of covariant int in generic type MethodTemplateType\Dolor in PHPDoc tag @template U is redundant, template type T of class MethodTemplateType\Dolor has the same variance.', + 109, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type MethodTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class MethodTemplateType\Dolor.', + 109, + ], + [ + 'PHPDoc tag @template T for method MethodTemplateType\InvalidDefault::invalid() has invalid default type MethodTemplateType\Zazzzu.', + 122, + ], + [ + 'Default type bool in PHPDoc tag @template T for method MethodTemplateType\InvalidDefault::outOfBounds() is not subtype of bound type object.', + 130, + ], + [ + 'PHPDoc tag @template V for method MethodTemplateType\InvalidDefault::requiredAfterOptional() does not have a default type but follows an optional @template U.', + 140, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php new file mode 100644 index 0000000000..0708ec3095 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php @@ -0,0 +1,141 @@ + + */ +class PropertyVarianceRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertyVarianceRule( + self::getContainer()->getByType(VarianceCheck::class), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/property-variance.php'], [ + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$a.', + 51, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$b.', + 54, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$c.', + 57, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$d.', + 60, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$a.', + 80, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$b.', + 83, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$c.', + 86, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$d.', + 89, + ], + ]); + } + + public function testPromoted(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/property-variance-promoted.php'], [ + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$a.', + 58, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$b.', + 59, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$c.', + 60, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$d.', + 61, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$a.', + 84, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$b.', + 85, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$c.', + 86, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$d.', + 87, + ], + ]); + } + + public function testReadOnly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/property-variance-readonly.php'], [ + [ + 'Template type X is declared as covariant, but occurs in contravariant position in property PropertyVariance\ReadOnly\B::$b.', + 45, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\ReadOnly\B::$d.', + 51, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$a.', + 62, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$c.', + 68, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\ReadOnly\C::$d.', + 71, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\D::$a.', + 86, + ], + ]); + } + + public function testBug9153(): void + { + $this->analyse([__DIR__ . '/data/bug-9153.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 135d9e78be..335e1f707c 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -15,12 +17,23 @@ class TraitTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new TraitTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -49,6 +62,27 @@ public function testRule(): void 'PHPDoc tag @template for trait TraitTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.', 45, ], + [ + 'Call-site variance of covariant int in generic type TraitTemplateType\Dolor in PHPDoc tag @template U is redundant, template type T of class TraitTemplateType\Dolor has the same variance.', + 64, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type TraitTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class TraitTemplateType\Dolor.', + 64, + ], + [ + 'PHPDoc tag @template T for trait TraitTemplateType\Adipiscing has invalid default type TraitTemplateType\Zazzzu.', + 72, + ], + [ + 'Default type bool in PHPDoc tag @template T for trait TraitTemplateType\Elit is not subtype of bound type object.', + 80, + ], + [ + 'PHPDoc tag @template V for trait TraitTemplateType\Consecteur does not have a default type but follows an optional @template U.', + 90, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php index aead3360e2..76dc75145f 100644 --- a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -20,8 +21,9 @@ protected function getRule(): Rule $this->createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true, + new UnresolvableTypeHelper(), [], + true, ), ); } @@ -40,7 +42,6 @@ public function testRule(): void [ 'Class UsedTraits\Baz uses generic trait UsedTraits\GenericTrait but does not specify its types: T', 38, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type UsedTraits\GenericTrait in PHPDoc tag @use specifies 2 template types, but trait UsedTraits\GenericTrait supports only 1: T', @@ -53,7 +54,10 @@ public function testRule(): void [ 'Trait UsedTraits\NestedTrait uses generic trait UsedTraits\GenericTrait but does not specify its types: T', 54, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Call-site variance annotation of covariant Throwable in generic type UsedTraits\GenericTrait in PHPDoc tag @use is not allowed.', + 69, ], ]); } diff --git a/tests/PHPStan/Rules/Generics/data/bug-10049.php b/tests/PHPStan/Rules/Generics/data/bug-10049.php new file mode 100644 index 0000000000..e32a2d7399 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-10049.php @@ -0,0 +1,41 @@ += 8.1 + +namespace Bug10049; + +/** + * @template SELF of SimpleEntity + */ +abstract class SimpleEntity +{ + /** + * @param SimpleTable $table + */ + public function __construct(protected readonly SimpleTable $table) + { + } +} + +/** + * @template-covariant E of SimpleEntity + */ +class SimpleTable +{ + /** + * @template ENTITY of SimpleEntity + * + * @param class-string $className + * + * @return SimpleTable + */ + public static function table(string $className, string $name): SimpleTable + { + return new SimpleTable($className, $name); + } + + /** + * @param class-string $className + */ + private function __construct(readonly string $className, readonly string $table) + { + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-10609.php b/tests/PHPStan/Rules/Generics/data/bug-10609.php new file mode 100644 index 0000000000..c62a9e0a10 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-10609.php @@ -0,0 +1,16 @@ + + */ +class SomeResult extends Result { + +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-3769.php b/tests/PHPStan/Rules/Generics/data/bug-3769.php index 1101ad3547..aeb273d479 100644 --- a/tests/PHPStan/Rules/Generics/data/bug-3769.php +++ b/tests/PHPStan/Rules/Generics/data/bug-3769.php @@ -29,6 +29,7 @@ function foo( $a = assertType('array', stringValues($foo)); $a = assertType('array', stringValues($bar)); $a = assertType('array', stringValues($baz)); + echo 'test'; }; /** @@ -37,6 +38,7 @@ function foo( */ function fooUnion($foo): void { $a = assertType('T of Exception|stdClass (function Bug3769\fooUnion(), argument)', $foo); + echo 'test'; } /** @@ -70,8 +72,8 @@ function stringBound(string $a) } function (): void { - $a = assertType('int', mixedBound(1)); - $a = assertType('string', mixedBound('str')); + $a = assertType('1', mixedBound(1)); + $a = assertType('\'str\'', mixedBound('str')); $a = assertType('1', intBound(1)); $a = assertType('\'str\'', stringBound('str')); }; diff --git a/tests/PHPStan/Rules/Generics/data/bug-6301.php b/tests/PHPStan/Rules/Generics/data/bug-6301.php new file mode 100644 index 0000000000..8d32598294 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-6301.php @@ -0,0 +1,30 @@ +str((string) $i)); + assertType('non-empty-string', $this->str($nonEmpty)); + assertType('numeric-string', $this->str($numericString)); + assertType('literal-string', $this->str($literalString)); + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-8473.php b/tests/PHPStan/Rules/Generics/data/bug-8473.php new file mode 100644 index 0000000000..a4c46fe246 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-8473.php @@ -0,0 +1,23 @@ + */ +class AccountCollection extends Paginator +{ +} + +class AccountEntity +{} diff --git a/tests/PHPStan/Rules/Generics/data/bug-8880.php b/tests/PHPStan/Rules/Generics/data/bug-8880.php new file mode 100644 index 0000000000..4f3b1d39f5 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-8880.php @@ -0,0 +1,34 @@ + $items + * @return void + */ + function processItems($items); +} + +/** @implements IProcessor */ +final class StringPrinter implements IProcessor { + function processItems($items) { + foreach ($items as $s) + putStrLn($s); + } +} + +/** + * @param IProcessor $p + * @return void + */ +function callWithInt($p) { + $p->processItems([1]); +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-9153.php b/tests/PHPStan/Rules/Generics/data/bug-9153.php new file mode 100644 index 0000000000..d78363dd69 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-9153.php @@ -0,0 +1,22 @@ + + * + * @immutable + */ +final class LanguageProperty +{ + /** @var Value */ + public $value; + + /** + * @param Value $value + */ + public function __construct($value) + { + $this->value = $value; + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-9161.php b/tests/PHPStan/Rules/Generics/data/bug-9161.php new file mode 100644 index 0000000000..05c2fb07ba --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-9161.php @@ -0,0 +1,39 @@ += 8.0 + +namespace Bug9161; + +/** + * @template-covariant TKey of int|string + * @template-covariant TValue + */ +final class Map +{ + /** + * @param array $items + */ + public function __construct( + private array $items = [], + ) { + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->items; + } + + /** + * @return list + */ + public function toPairs(): array + { + $pairs = []; + foreach ($this->items as $key => $value) { + $pairs[] = [$key, $value]; + } + + return $pairs; + } +} diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php index caa906001a..c04a5665a4 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php @@ -194,6 +194,24 @@ class FooGeneric9 extends FooGeneric8 } +/** + * @template-contravariant T + * @extends FooGeneric8 + */ +class FooGeneric10 extends FooGeneric8 +{ + +} + +/** + * @template T + * @extends FooGeneric8 + */ +class FooGeneric11 extends FooGeneric8 +{ + +} + class FilterIteratorChild extends \FilterIterator { @@ -203,3 +221,55 @@ public function accept() } } + +/** @extends FooObjectStorage */ +class FooObjectStorage extends \SplObjectStorage +{ +} + +/** + * @template T + * @implements \Iterator + */ +abstract class AbstractFooCollection implements \Iterator +{ +} + +/** @extends FooCollection */ +class FooCollection extends AbstractFooCollection +{ +} + +/** + * @extends FooGeneric + */ +class FooTypeProjection extends FooGeneric +{ + +} + +trait FooTrait +{ + +} + +/** + * @extends FooGeneric + */ +class TraitInExtends extends FooGeneric +{ + +} + +/** + * @template T = string + */ +class FooGenericDefault +{ + +} + +class FooGenericExtendsDefault extends FooGenericDefault +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php index 716c58c2b7..abbc514279 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php @@ -217,3 +217,43 @@ class FooGeneric10 implements FooGeneric9 { } + +/** @implements FooIterator */ +class FooIterator implements \Iterator +{ +} + +/** + * @template T + * @implements \Iterator + */ +interface AbstractFooCollection extends \Iterator +{ +} + +/** @implements FooCollection */ +class FooCollection implements AbstractFooCollection +{ +} + +/** + * @implements FooGeneric + */ +class FooTypeProjection implements FooGeneric +{ +} + +/** + * @template T = string + */ +interface FooGenericDefault +{ +} + +interface FooGenericExtendsDefault extends FooGenericDefault +{ +} + +class FooGenericImplementsDefault implements FooGenericDefault +{ +} diff --git a/tests/PHPStan/Rules/Generics/data/class-template.php b/tests/PHPStan/Rules/Generics/data/class-template.php index 752983b26a..06400bd536 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -79,3 +79,64 @@ class Dolor { }; + +/** + * @template T of 'string' + */ +class Sit +{ + +} + +/** + * @template T of 5 + */ +class Amet +{ + +} + +/** + * @template-covariant T + */ +class Consecteur +{ + +} + +/** + * @template T of Consecteur + * @template U of Consecteur + * @template V of Consecteur<*> + * @template W of Consecteur + */ +class Adipiscing +{ + +} + +/** + * @template T = Zazzzu + */ +class Elit +{ + +} + +/** + * @template T of object = bool + */ +class Venenatis +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +class Mauris +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php index 01066a0e88..1cda1bcbcd 100644 --- a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php +++ b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php @@ -86,3 +86,24 @@ public function getIterator() } } + +/** + * @implements Generic + */ +enum TypeProjection implements Generic +{ + +} + +/** + * @template T = string + */ +interface GenericDefault +{ + +} + +enum Foo9 implements GenericDefault +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index 7124e4c138..8a1ff456f9 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -75,3 +75,49 @@ function nullableUnionSupported() { } + +/** @template T of object{foo: int} */ +function objectShapes() +{ + +} + +/** @template-covariant T */ +class GenericCovariant {} + +/** + * @template T of GenericCovariant + * @template U of GenericCovariant + * @template V of GenericCovariant<*> + * @template W of GenericCovariant + */ +function typeProjections() +{ + +} + +/** + * @template T = Zazzzu + */ +function invalidDefault() +{ + +} + +/** + * @template T of object = bool + */ +function outOfBoundsDefault() +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +function requiredAfterOptional() +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/interface-ancestors-extends.php b/tests/PHPStan/Rules/Generics/data/interface-ancestors-extends.php index 510c9451b8..64e5e92129 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/interface-ancestors-extends.php @@ -216,3 +216,11 @@ interface FooGeneric10 extends FooGeneric9 { } + +/** + * @extends FooGeneric + */ +interface FooTypeProjection extends FooGeneric +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/interface-ancestors-implements.php b/tests/PHPStan/Rules/Generics/data/interface-ancestors-implements.php index 262e07e56a..4d15ae57c0 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-ancestors-implements.php +++ b/tests/PHPStan/Rules/Generics/data/interface-ancestors-implements.php @@ -183,3 +183,11 @@ interface FooGenericGeneric8 { } + +/** + * @implements FooGeneric + */ +interface FooTypeProjection +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/interface-template.php b/tests/PHPStan/Rules/Generics/data/interface-template.php index ed7f3ef667..7f0da436e7 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -58,3 +58,46 @@ interface UnionBound { } + +/** @template-covariant T */ +interface Covariant +{ + +} + +/** + * @template T of Covariant + * @template U of Covariant + * @template V of Covariant<*> + * @template W of Covariant + */ +interface TypeProjections +{ + +} + +/** + * @template T = Zazzzu + */ +interface InvalidDefault +{ + +} + +/** + * @template T of object = bool + */ +interface OutOfBoundsDefault +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +interface RequiredAfterOptional +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-constructor.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-constructor.php new file mode 100644 index 0000000000..57d4dfc3b9 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-constructor.php @@ -0,0 +1,72 @@ + $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} +} + +/** @template-covariant X */ +class B { + /** + * @param X $a + * @param In $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} +} + +/** @template-contravariant X */ +class C { + /** + * @param X $a + * @param In $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php new file mode 100644 index 0000000000..311958192a --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php @@ -0,0 +1,83 @@ + $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function a($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} + + /** @return X */ + function b() {} + + /** @return In */ + function c() {} + + /** @return In> */ + function d() {} + + /** @return In> */ + function e() {} + + /** @return In> */ + function f() {} + + /** @return Out */ + function g() {} + + /** @return Out> */ + function h() {} + + /** @return Out> */ + function i() {} + + /** @return Out> */ + function j() {} + + /** @return Invariant */ + function k() {} + + /** @return Invariant> */ + function l() {} + + /** @return Invariant> */ + function m() {} + + /** @return X */ + private function n() {} + + /** + * @param-out X $a + */ + public function paramOut(&$a) + { + + } +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-covariant.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-covariant.php new file mode 100644 index 0000000000..4837dbba5d --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-covariant.php @@ -0,0 +1,75 @@ + $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function a($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} + + /** @return X */ + function b() {} + + /** @return In */ + function c() {} + + /** @return In> */ + function d() {} + + /** @return In> */ + function e() {} + + /** @return In> */ + function f() {} + + /** @return Out */ + function g() {} + + /** @return Out> */ + function h() {} + + /** @return Out> */ + function i() {} + + /** @return Out> */ + function j() {} + + /** @return Invariant */ + function k() {} + + /** @return Invariant> */ + function l() {} + + /** @return Invariant> */ + function m() {} + + /** @param X $n */ + private function n($n) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-invariant.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-invariant.php new file mode 100644 index 0000000000..54dd3e4eb7 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-invariant.php @@ -0,0 +1,70 @@ + $b + * @param In> $c + * @param In> $d + * @param In $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + * @return X + */ + function a($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} + + /** @return In */ + function b() {} + + /** @return In>*/ + function c() {} + + /** @return In>*/ + function d() {} + + /** @return In */ + function e() {} + + /** @return Out */ + function f() {} + + /** @return Out>*/ + function g() {} + + /** @return Out>*/ + function h() {} + + /** @return Out */ + function i() {} + + /** @return Invariant */ + function j() {} + + /** @return Invariant>*/ + function k() {} + + /** @return Invariant>*/ + function l() {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-static.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-static.php new file mode 100644 index 0000000000..692f6672f2 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-static.php @@ -0,0 +1,72 @@ + $b + * @param Out $c + */ + static function a($a, $b, $c) {} + + /** @return X */ + static function b() {} + + /** @return In */ + static function c() {} + + /** @return Out */ + static function d() {} +} + +/** @template-covariant X */ +class B { + /** + * @param X $a + * @param In $b + * @param Out $c + */ + static function a($a, $b, $c) {} + + /** @return X */ + static function b() {} + + /** @return In */ + static function c() {} + + /** @return Out */ + static function d() {} +} + +/** @template-contravariant X */ +class C { + /** + * @param X $a + * @param In $b + * @param Out $c + */ + static function a($a, $b, $c) {} + + /** @return X */ + static function b() {} + + /** @return In */ + static function c() {} + + /** @return Out */ + static function d() {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance.php index c783c69191..e7ffcbfaa2 100644 --- a/tests/PHPStan/Rules/Generics/data/method-signature-variance.php +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance.php @@ -2,37 +2,22 @@ namespace MethodSignatureVariance; -/** @template-covariant T */ -interface Out { -} - -/** @template T */ -interface Invariant { -} - -/** - * @template-covariant T - * @template-covariant W of \DateTimeInterface - */ class C { /** - * @param Out $a - * @param Invariant $b - * @param T $c - * @param W $d - * @return T + * @template U + * @return void */ - function a($a, $b, $c, $d) { - return $c; - } + function a() {} + /** * @template-covariant U - * @param Out $a - * @param Invariant $b - * @param U $c - * @return U + * @return void + */ + function b() {} + + /** + * @template-contravariant U + * @return void */ - function b($a, $b, $c) { - return $c; - } + function c() {} } diff --git a/tests/PHPStan/Rules/Generics/data/method-tag-template.php b/tests/PHPStan/Rules/Generics/data/method-tag-template.php new file mode 100644 index 0000000000..77a6f202c2 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-tag-template.php @@ -0,0 +1,15 @@ +(T $a, U $b, stdClass $c) + * @method void typeAlias(TypeAlias $a) + */ +class HelloWorld +{ +} diff --git a/tests/PHPStan/Rules/Generics/data/method-tag-trait-template.php b/tests/PHPStan/Rules/Generics/data/method-tag-trait-template.php new file mode 100644 index 0000000000..57a93beb5a --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-tag-trait-template.php @@ -0,0 +1,13 @@ +(T $a, U $b, stdClass $c) + * @method void typeAlias(TypeAlias $a) + */ +trait HelloWorld +{ +} diff --git a/tests/PHPStan/Rules/Generics/data/method-template.php b/tests/PHPStan/Rules/Generics/data/method-template.php index fc6c4c87e2..edf5d62201 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -88,3 +88,58 @@ public function doFoo() } } + +/** + * @template-covariant T + */ +class Dolor +{ + +} + +class Sit +{ + + /** + * @template T of Dolor + * @template U of Dolor + * @template V of Dolor<*> + * @template W of Dolor + */ + public function doSit() + { + + } + +} + +class InvalidDefault +{ + + /** + * @template T = Zazzzu + */ + public function invalid() + { + + } + + /** + * @template T of object = bool + */ + public function outOfBounds() + { + + } + + /** + * @template T + * @template U = string + * @template V + */ + public function requiredAfterOptional() + { + + } + +} diff --git a/tests/PHPStan/Rules/Generics/data/pr-2465.php b/tests/PHPStan/Rules/Generics/data/pr-2465.php new file mode 100644 index 0000000000..fe07d78e9d --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/pr-2465.php @@ -0,0 +1,17 @@ +> $thing + */ + public function foo(InvariantThing $thing): void {} +} diff --git a/tests/PHPStan/Rules/Generics/data/property-variance-promoted.php b/tests/PHPStan/Rules/Generics/data/property-variance-promoted.php new file mode 100644 index 0000000000..fa56e56822 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/property-variance-promoted.php @@ -0,0 +1,93 @@ += 8.0 + +namespace PropertyVariance\Promoted; + +/** @template-contravariant T */ +interface In { +} + +/** @template-covariant T */ +interface Out { +} + +/** @template T */ +interface Invariant { +} + +/** + * @template X + */ +class A { + /** + * @param X $a + * @param In $b + * @param Out $c + * @param Invariant $d + * @param X $e + * @param In $f + * @param Out $g + * @param Invariant $h + */ + public function __construct( + public $a, + public $b, + public $c, + public $d, + private $e, + private $f, + private $g, + private $h, + ) {} +} + +/** + * @template-covariant X + */ +class B { + /** + * @param X $a + * @param In $b + * @param Out $c + * @param Invariant $d + * @param X $e + * @param In $f + * @param Out $g + * @param Invariant $h + */ + public function __construct( + public $a, + public $b, + public $c, + public $d, + private $e, + private $f, + private $g, + private $h, + ) {} +} + +/** + * @template-contravariant X + */ +class C { + /** + * @param X $a + * @param In $b + * @param Out $c + * @param Invariant $d + * @param X $e + * @param In $f + * @param Out $g + * @param Invariant $h + */ + public function __construct( + public $a, + public $b, + public $c, + public $d, + private $e, + private $f, + private $g, + private $h, + ) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/property-variance-readonly.php b/tests/PHPStan/Rules/Generics/data/property-variance-readonly.php new file mode 100644 index 0000000000..faefc2bd8b --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/property-variance-readonly.php @@ -0,0 +1,89 @@ += 8.1 + +namespace PropertyVariance\ReadOnly; + +/** @template-contravariant T */ +interface In { +} + +/** @template-covariant T */ +interface Out { +} + +/** @template T */ +interface Invariant { +} + +/** + * @template X + */ +class A { + /** @var X */ + public readonly mixed $a; + + /** @var In */ + public readonly mixed $b; + + /** @var Out */ + public readonly mixed $c; + + /** @var Invariant */ + public readonly mixed $d; + + /** @var Invariant */ + private readonly mixed $e; +} + +/** + * @template-covariant X + */ +class B { + /** @var X */ + public readonly mixed $a; + + /** @var In */ + public readonly mixed $b; + + /** @var Out */ + public readonly mixed $c; + + /** @var Invariant */ + public readonly mixed $d; + + /** @var Invariant */ + private readonly mixed $e; +} + +/** + * @template-contravariant X + */ +class C { + /** @var X */ + public readonly mixed $a; + + /** @var In */ + public readonly mixed $b; + + /** @var Out */ + public readonly mixed $c; + + /** @var Invariant */ + public readonly mixed $d; + + /** @var Invariant */ + private readonly mixed $e; +} + +/** + * @template-contravariant X + */ +class D { + /** + * @param X $a + * @param X $b + */ + public function __construct( + public readonly mixed $a, + private readonly mixed $b, + ) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/property-variance.php b/tests/PHPStan/Rules/Generics/data/property-variance.php new file mode 100644 index 0000000000..a7c9406b8e --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/property-variance.php @@ -0,0 +1,102 @@ + */ + public $b; + + /** @var Out */ + public $c; + + /** @var Invariant */ + public $d; + + /** @var X */ + private $e; + + /** @var In */ + private $f; + + /** @var Out */ + private $g; + + /** @var Invariant */ + private $h; +} + +/** + * @template-covariant X + */ +class B { + /** @var X */ + public $a; + + /** @var In */ + public $b; + + /** @var Out */ + public $c; + + /** @var Invariant */ + public $d; + + /** @var X */ + private $e; + + /** @var In */ + private $f; + + /** @var Out */ + private $g; + + /** @var Invariant */ + private $h; +} + +/** + * @template-contravariant X + */ +class C { + /** @var X */ + public $a; + + /** @var In */ + public $b; + + /** @var Out */ + public $c; + + /** @var Invariant */ + public $d; + + /** @var X */ + private $e; + + /** @var In */ + private $f; + + /** @var Out */ + private $g; + + /** @var Invariant */ + private $h; +} diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index 8870b4dd98..39126a88f2 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -46,3 +46,48 @@ trait Ipsum { } + +/** + * @template-covariant T + */ +class Dolor +{ + +} + +/** + * @template T of Dolor + * @template U of Dolor + * @template V of Dolor<*> + * @template W of Dolor + */ +trait Sit +{ + +} + +/** + * @template T = Zazzzu + */ +trait Adipiscing +{ + +} + +/** + * @template T of object = bool + */ +trait Elit +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +trait Consecteur +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/used-traits.php b/tests/PHPStan/Rules/Generics/data/used-traits.php index 855d38aa02..f34fb5ffb9 100644 --- a/tests/PHPStan/Rules/Generics/data/used-traits.php +++ b/tests/PHPStan/Rules/Generics/data/used-traits.php @@ -61,3 +61,25 @@ class Ipsum use NestedTrait; } + +class Dolor +{ + + /** @use GenericTrait */ + use GenericTrait; + +} + +/** + * @template T = string + */ +trait GenericDefault +{ +} + +class Sit +{ + + use GenericDefault; + +} diff --git a/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php b/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php new file mode 100644 index 0000000000..c34760e39e --- /dev/null +++ b/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php @@ -0,0 +1,55 @@ + + */ +class IgnoreParseErrorRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IgnoreParseErrorRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/ignore-parse-error.php'], [ + [ + 'Parse error in @phpstan-ignore: Unexpected comma (,) after comma (,), expected identifier', + 10, + ], + [ + 'Parse error in @phpstan-ignore: Unexpected T_CLOSE_PARENTHESIS after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS', + 13, + ], + [ + 'Parse error in @phpstan-ignore: Unexpected end, unclosed opening parenthesis', + 19, + ], + [ + 'Parse error in @phpstan-ignore: Unexpected T_OTHER \'čičí\' after @phpstan-ignore, expected identifier', + 23, + ], + [ + 'Parse error in @phpstan-ignore: Unexpected end after @phpstan-ignore, expected identifier', + 27, + ], + ]); + } + + public function testRuleWithUnusedTrait(): void + { + $this->analyse([__DIR__ . '/data/ignore-parse-error-trait.php'], [ + [ + 'Parse error in @phpstan-ignore: Unexpected comma (,) after comma (,), expected identifier', + 10, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Ignore/data/ignore-parse-error-trait.php b/tests/PHPStan/Rules/Ignore/data/ignore-parse-error-trait.php new file mode 100644 index 0000000000..176089f480 --- /dev/null +++ b/tests/PHPStan/Rules/Ignore/data/ignore-parse-error-trait.php @@ -0,0 +1,13 @@ + + */ +class RestrictedInternalClassConstantUsageExtensionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(RestrictedClassConstantUsageRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/class-constant-internal-tag.php'], [ + [ + 'Access to internal constant ClassConstantInternalTagOne\Foo::INTERNAL from outside its root namespace ClassConstantInternalTagOne.', + 49, + ], + [ + 'Access to constant FOO of internal class ClassConstantInternalTagOne\FooInternal from outside its root namespace ClassConstantInternalTagOne.', + 54, + ], + [ + 'Access to internal constant ClassConstantInternalTagOne\Foo::INTERNAL from outside its root namespace ClassConstantInternalTagOne.', + 62, + ], + + [ + 'Access to constant FOO of internal class ClassConstantInternalTagOne\FooInternal from outside its root namespace ClassConstantInternalTagOne.', + 67, + ], + [ + 'Access to internal constant FooWithInternalClassConstantWithoutNamespace::INTERNAL.', + 89, + ], + [ + 'Access to constant FOO of internal class FooInternalWithClassConstantWithoutNamespace.', + 94, + ], + [ + 'Access to internal constant FooWithInternalClassConstantWithoutNamespace::INTERNAL.', + 102, + ], + [ + 'Access to constant FOO of internal class FooInternalWithClassConstantWithoutNamespace.', + 107, + ], + ]); + } + + public function testStaticPropertyAccessOnInternalSubclass(): void + { + $this->analyse([__DIR__ . '/data/class-constant-access-on-internal-subclass.php'], [ + [ + 'Access to constant BAR of internal class ClassConstantAccessOnInternalSubclassOne\Bar from outside its root namespace ClassConstantAccessOnInternalSubclassOne.', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/InternalTag/RestrictedInternalFunctionUsageExtensionTest.php b/tests/PHPStan/Rules/InternalTag/RestrictedInternalFunctionUsageExtensionTest.php new file mode 100644 index 0000000000..aa8d49eae2 --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/RestrictedInternalFunctionUsageExtensionTest.php @@ -0,0 +1,42 @@ + + */ +class RestrictedInternalFunctionUsageExtensionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(RestrictedFunctionUsageRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/function-internal-tag.php'], [ + [ + 'Call to internal function FunctionInternalTagOne\doInternal() from outside its root namespace FunctionInternalTagOne.', + 35, + ], + [ + 'Call to internal function FunctionInternalTagOne\doInternal() from outside its root namespace FunctionInternalTagOne.', + 44, + ], + [ + 'Call to internal function doInternalWithoutNamespace().', + 60, + ], + [ + 'Call to internal function doInternalWithoutNamespace().', + 69, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/InternalTag/RestrictedInternalMethodUsageExtensionTest.php b/tests/PHPStan/Rules/InternalTag/RestrictedInternalMethodUsageExtensionTest.php new file mode 100644 index 0000000000..8605c44c9b --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/RestrictedInternalMethodUsageExtensionTest.php @@ -0,0 +1,59 @@ + + */ +class RestrictedInternalMethodUsageExtensionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(RestrictedMethodUsageRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-internal-tag.php'], [ + [ + 'Call to internal method MethodInternalTagOne\Foo::doInternal() from outside its root namespace MethodInternalTagOne.', + 58, + ], + [ + 'Call to method doFoo() of internal class MethodInternalTagOne\FooInternal from outside its root namespace MethodInternalTagOne.', + 63, + ], + [ + 'Call to internal method MethodInternalTagOne\Foo::doInternal() from outside its root namespace MethodInternalTagOne.', + 71, + ], + + [ + 'Call to method doFoo() of internal class MethodInternalTagOne\FooInternal from outside its root namespace MethodInternalTagOne.', + 76, + ], + [ + 'Call to internal method FooWithInternalMethodWithoutNamespace::doInternal().', + 107, + ], + [ + 'Call to method doFoo() of internal class FooInternalWithoutNamespace.', + 112, + ], + [ + 'Call to internal method FooWithInternalMethodWithoutNamespace::doInternal().', + 120, + ], + [ + 'Call to method doFoo() of internal class FooInternalWithoutNamespace.', + 125, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/InternalTag/RestrictedInternalPropertyUsageExtensionTest.php b/tests/PHPStan/Rules/InternalTag/RestrictedInternalPropertyUsageExtensionTest.php new file mode 100644 index 0000000000..d3ee69910e --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/RestrictedInternalPropertyUsageExtensionTest.php @@ -0,0 +1,59 @@ + + */ +class RestrictedInternalPropertyUsageExtensionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(RestrictedPropertyUsageRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/property-internal-tag.php'], [ + [ + 'Access to internal property PropertyInternalTagOne\Foo::$internal from outside its root namespace PropertyInternalTagOne.', + 49, + ], + [ + 'Access to property $foo of internal class PropertyInternalTagOne\FooInternal from outside its root namespace PropertyInternalTagOne.', + 54, + ], + [ + 'Access to internal property PropertyInternalTagOne\Foo::$internal from outside its root namespace PropertyInternalTagOne.', + 62, + ], + + [ + 'Access to property $foo of internal class PropertyInternalTagOne\FooInternal from outside its root namespace PropertyInternalTagOne.', + 67, + ], + [ + 'Access to internal property FooWithInternalPropertyWithoutNamespace::$internal.', + 89, + ], + [ + 'Access to property $foo of internal class FooInternalWithPropertyWithoutNamespace.', + 94, + ], + [ + 'Access to internal property FooWithInternalPropertyWithoutNamespace::$internal.', + 102, + ], + [ + 'Access to property $foo of internal class FooInternalWithPropertyWithoutNamespace.', + 107, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/InternalTag/RestrictedInternalStaticMethodUsageExtensionTest.php b/tests/PHPStan/Rules/InternalTag/RestrictedInternalStaticMethodUsageExtensionTest.php new file mode 100644 index 0000000000..0f73bf967a --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/RestrictedInternalStaticMethodUsageExtensionTest.php @@ -0,0 +1,69 @@ + + */ +class RestrictedInternalStaticMethodUsageExtensionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(RestrictedStaticMethodUsageRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/static-method-internal-tag.php'], [ + [ + 'Call to internal static method StaticMethodInternalTagOne\Foo::doInternal() from outside its root namespace StaticMethodInternalTagOne.', + 58, + ], + [ + 'Call to static method doFoo() of internal class StaticMethodInternalTagOne\FooInternal from outside its root namespace StaticMethodInternalTagOne.', + 63, + ], + [ + 'Call to internal static method StaticMethodInternalTagOne\Foo::doInternal() from outside its root namespace StaticMethodInternalTagOne.', + 71, + ], + + [ + 'Call to static method doFoo() of internal class StaticMethodInternalTagOne\FooInternal from outside its root namespace StaticMethodInternalTagOne.', + 76, + ], + [ + 'Call to internal static method FooWithInternalStaticMethodWithoutNamespace::doInternal().', + 107, + ], + [ + 'Call to static method doFoo() of internal class FooInternalStaticWithoutNamespace.', + 112, + ], + [ + 'Call to internal static method FooWithInternalStaticMethodWithoutNamespace::doInternal().', + 120, + ], + [ + 'Call to static method doFoo() of internal class FooInternalStaticWithoutNamespace.', + 125, + ], + ]); + } + + public function testStaticMethodCallOnInternalSubclass(): void + { + $this->analyse([__DIR__ . '/data/static-method-call-on-internal-subclass.php'], [ + [ + 'Call to static method doBar() of internal class StaticMethodCallOnInternalSubclassOne\Bar from outside its root namespace StaticMethodCallOnInternalSubclassOne.', + 34, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/InternalTag/RestrictedInternalStaticPropertyUsageExtensionTest.php b/tests/PHPStan/Rules/InternalTag/RestrictedInternalStaticPropertyUsageExtensionTest.php new file mode 100644 index 0000000000..6b56240a7f --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/RestrictedInternalStaticPropertyUsageExtensionTest.php @@ -0,0 +1,69 @@ + + */ +class RestrictedInternalStaticPropertyUsageExtensionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(RestrictedStaticPropertyUsageRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/static-property-internal-tag.php'], [ + [ + 'Access to internal static property StaticPropertyInternalTagOne\Foo::$internal from outside its root namespace StaticPropertyInternalTagOne.', + 49, + ], + [ + 'Access to static property $foo of internal class StaticPropertyInternalTagOne\FooInternal from outside its root namespace StaticPropertyInternalTagOne.', + 54, + ], + [ + 'Access to internal static property StaticPropertyInternalTagOne\Foo::$internal from outside its root namespace StaticPropertyInternalTagOne.', + 62, + ], + + [ + 'Access to static property $foo of internal class StaticPropertyInternalTagOne\FooInternal from outside its root namespace StaticPropertyInternalTagOne.', + 67, + ], + [ + 'Access to internal static property FooWithInternalStaticPropertyWithoutNamespace::$internal.', + 89, + ], + [ + 'Access to static property $foo of internal class FooInternalWithStaticPropertyWithoutNamespace.', + 94, + ], + [ + 'Access to internal static property FooWithInternalStaticPropertyWithoutNamespace::$internal.', + 102, + ], + [ + 'Access to static property $foo of internal class FooInternalWithStaticPropertyWithoutNamespace.', + 107, + ], + ]); + } + + public function testStaticPropertyAccessOnInternalSubclass(): void + { + $this->analyse([__DIR__ . '/data/static-property-access-on-internal-subclass.php'], [ + [ + 'Access to static property $bar of internal class StaticPropertyAccessOnInternalSubclassOne\Bar from outside its root namespace StaticPropertyAccessOnInternalSubclassOne.', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/InternalTag/data/class-constant-access-on-internal-subclass.php b/tests/PHPStan/Rules/InternalTag/data/class-constant-access-on-internal-subclass.php new file mode 100644 index 0000000000..d20ac7eb18 --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/data/class-constant-access-on-internal-subclass.php @@ -0,0 +1,30 @@ +doInternal(); + $foo->doNotInternal(); + }; + + function (FooInternal $foo): void { + $foo->doFoo(); + }; + +} + +namespace MethodInternalTagOne\Test { + + function (\MethodInternalTagOne\Foo $foo): void { + $foo->doInternal(); + $foo->doNotInternal(); + }; + + function (\MethodInternalTagOne\FooInternal $foo): void { + $foo->doFoo(); + }; +} + +namespace MethodInternalTagTwo { + + function (\MethodInternalTagOne\Foo $foo): void { + $foo->doInternal(); + $foo->doNotInternal(); + }; + + function (\MethodInternalTagOne\FooInternal $foo): void { + $foo->doFoo(); + }; + +} + +namespace { + + function (\MethodInternalTagOne\Foo $foo): void { + $foo->doInternal(); + $foo->doNotInternal(); + }; + + function (\MethodInternalTagOne\FooInternal $foo): void { + $foo->doFoo(); + }; + + class FooWithInternalMethodWithoutNamespace + { + /** @internal */ + public function doInternal() + { + + } + + public function doNotInternal() + { + + } + } + + /** + * @internal + */ + class FooInternalWithoutNamespace + { + + public function doFoo(): void + { + + } + + } + + function (FooWithInternalMethodWithoutNamespace $foo): void { + $foo->doInternal(); + $foo->doNotInternal(); + }; + + function (FooInternalWithoutNamespace $foo): void { + $foo->doFoo(); + }; + +} + +namespace SomeNamespace { + + function (\FooWithInternalMethodWithoutNamespace $foo): void { + $foo->doInternal(); + $foo->doNotInternal(); + }; + + function (\FooInternalWithoutNamespace $foo): void { + $foo->doFoo(); + }; + +} diff --git a/tests/PHPStan/Rules/InternalTag/data/property-internal-tag.php b/tests/PHPStan/Rules/InternalTag/data/property-internal-tag.php new file mode 100644 index 0000000000..bf3b0921a1 --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/data/property-internal-tag.php @@ -0,0 +1,110 @@ +internal; + $foo->notInternal; + }; + + function (FooInternal $foo): void { + $foo->foo; + }; + +} + +namespace PropertyInternalTagOne\Test { + + function (\PropertyInternalTagOne\Foo $foo): void { + $foo->internal; + $foo->notInternal; + }; + + function (\PropertyInternalTagOne\FooInternal $foo): void { + $foo->foo; + }; +} + +namespace PropertyInternalTagTwo { + + function (\PropertyInternalTagOne\Foo $foo): void { + $foo->internal; + $foo->notInternal; + }; + + function (\PropertyInternalTagOne\FooInternal $foo): void { + $foo->foo; + }; + +} + +namespace { + + function (\PropertyInternalTagOne\Foo $foo): void { + $foo->internal; + $foo->notInternal; + }; + + function (\PropertyInternalTagOne\FooInternal $foo): void { + $foo->foo; + }; + + class FooWithInternalPropertyWithoutNamespace + { + /** @internal */ + public $internal; + + public $notInternal; + } + + /** + * @internal + */ + class FooInternalWithPropertyWithoutNamespace + { + + public $foo; + + } + + function (FooWithInternalPropertyWithoutNamespace $foo): void { + $foo->internal; + $foo->notInternal; + }; + + function (FooInternalWithPropertyWithoutNamespace $foo): void { + $foo->foo; + }; + +} + +namespace SomeNamespace { + + function (\FooWithInternalPropertyWithoutNamespace $foo): void { + $foo->internal; + $foo->notInternal; + }; + + function (\FooInternalWithPropertyWithoutNamespace $foo): void { + $foo->foo; + }; + +} diff --git a/tests/PHPStan/Rules/InternalTag/data/static-method-call-on-internal-subclass.php b/tests/PHPStan/Rules/InternalTag/data/static-method-call-on-internal-subclass.php new file mode 100644 index 0000000000..fce54639f7 --- /dev/null +++ b/tests/PHPStan/Rules/InternalTag/data/static-method-call-on-internal-subclass.php @@ -0,0 +1,36 @@ + @@ -18,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/continue-break.php'], [ [ 'Keyword break used outside of a loop or a switch statement.', @@ -50,4 +47,34 @@ public function testRule(): void ]); } + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/continue-break-property-hook.php'], [ + [ + 'Keyword break used outside of a loop or a switch statement.', + 13, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 15, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 24, + ], + [ + 'Keyword continue used outside of a loop or a switch statement.', + 26, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 35, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php b/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php new file mode 100644 index 0000000000..7aac5f8019 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php @@ -0,0 +1,107 @@ + + */ +class DeclareStrictTypesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DeclareStrictTypesRule(new ExprPrinter(new Printer())); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/declare-position.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 5, + ], + ]); + } + + public function testRule2(): void + { + $this->analyse([__DIR__ . '/data/declare-position2.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 1, + ], + ]); + } + + public function testNested(): void + { + $this->analyse([__DIR__ . '/data/declare-position-nested.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 7, + ], + [ + 'Declare strict_types must be the very first statement.', + 12, + ], + ]); + } + + public function testValidPosition(): void + { + $this->analyse([__DIR__ . '/data/declare-position-valid.php'], []); + } + + public function testTicks(): void + { + $this->analyse([__DIR__ . '/data/declare-ticks.php'], []); + } + + public function testMulti(): void + { + $this->analyse([__DIR__ . '/data/declare-multi.php'], []); + } + + public function testShebang(): void + { + $this->analyse([__DIR__ . '/data/declare-shebang.php'], []); + $this->analyse([__DIR__ . '/data/declare-shebang2.php'], []); + $this->analyse([__DIR__ . '/data/declare-shebang3.php'], []); + } + + public function testHtmlBeforeDecalre(): void + { + $this->analyse([__DIR__ . '/data/declare-inline-html.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 2, + ], + ]); + } + + public function testNonsense(): void + { + $this->analyse([__DIR__ . '/data/declare-strict-nonsense.php'], [ + [ + "Declare strict_types must have 0 or 1 as its value, 'foo' given.", + 1, + ], + ]); + } + + public function testNonsenseBool(): void + { + $this->analyse([__DIR__ . '/data/declare-strict-nonsense-bool.php'], [ + [ + 'Declare strict_types must have 0 or 1 as its value, \true given.', + 1, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php new file mode 100644 index 0000000000..732819b506 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php @@ -0,0 +1,139 @@ + + */ +class RequireFileExistsRuleTest extends RuleTestCase +{ + + private string $currentWorkingDirectory = __DIR__ . '/../'; + + protected function getRule(): Rule + { + return new RequireFileExistsRule($this->currentWorkingDirectory); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../Analyser/usePathConstantsAsConstantString.neon', + ]; + } + + public function testBasicCase(): void + { + $this->analyse([__DIR__ . '/data/require-file-simple-case.php'], [ + [ + 'Path in include() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 11, + ], + [ + 'Path in include_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 12, + ], + [ + 'Path in require() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 13, + ], + [ + 'Path in require_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 14, + ], + ]); + } + + public function testFileDoesNotExistConditionally(): void + { + $this->analyse([__DIR__ . '/data/require-file-conditionally.php'], [ + [ + 'Path in include() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 9, + ], + [ + 'Path in include_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 10, + ], + [ + 'Path in require() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 11, + ], + [ + 'Path in require_once() "a-file-that-does-not-exist.php" is not a file or it does not exist.', + 12, + ], + ]); + } + + public function testRelativePath(): void + { + $this->analyse([__DIR__ . '/data/require-file-relative-path.php'], [ + [ + 'Path in include() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', + 8, + ], + [ + 'Path in include_once() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', + 9, + ], + [ + 'Path in require() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', + 10, + ], + [ + 'Path in require_once() "data/include-me-to-prove-you-work.txt" is not a file or it does not exist.', + 11, + ], + ]); + } + + public function testRelativePathWithIncludePath(): void + { + $includePaths = [realpath(__DIR__)]; + $includePaths[] = get_include_path(); + + set_include_path(implode(PATH_SEPARATOR, $includePaths)); + + try { + $this->analyse([__DIR__ . '/data/require-file-relative-path.php'], []); + } finally { + set_include_path($includePaths[1]); + } + } + + public function testRelativePathWithSameWorkingDirectory(): void + { + $this->currentWorkingDirectory = __DIR__; + $this->analyse([__DIR__ . '/data/require-file-relative-path.php'], []); + } + + public function testBug11738(): void + { + $this->analyse([__DIR__ . '/data/bug-11738/bug-11738.php'], []); + } + + public function testBug12203(): void + { + $this->analyse([__DIR__ . '/data/bug-12203.php'], [ + [ + 'Path in require_once() "../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 5, + ], + [ + 'Path in require_once() "' . __DIR__ . DIRECTORY_SEPARATOR . 'data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 6, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Keywords/data/bug-11738-included.php b/tests/PHPStan/Rules/Keywords/data/bug-11738-included.php new file mode 100644 index 0000000000..a4abe2dafc --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/bug-11738-included.php @@ -0,0 +1,2 @@ += 8.4 + +namespace ContinueBreakPropertyHook; + +class Foo +{ + + public int $bar { + set (int $foo) { + foreach ([1, 2, 3] as $val) { + switch ($foo) { + case 1: + break 3; + default: + break 3; + } + } + } + } + + public int $baz { + get { + if (rand(0, 1)) { + break; + } else { + continue; + } + } + } + + public int $ipsum { + get { + foreach ([1, 2, 3] as $val) { + function (): void { + break; + }; + } + } + } + +} + +class ValidUsages +{ + + public int $i { + set (int $foo) { + switch ($foo) { + case 1: + break; + default: + break; + } + + foreach ([1, 2, 3] as $val) { + if (rand(0, 1)) { + break; + } else { + continue; + } + } + + for ($i = 0; $i < 5; $i++) { + if (rand(0, 1)) { + break; + } else { + continue; + } + } + + while (true) { + if (rand(0, 1)) { + break; + } else { + continue; + } + } + + do { + if (rand(0, 1)) { + break; + } else { + continue; + } + } while (true); + } + } + + public int $j { + set (int $foo) { + foreach ([1, 2, 3] as $val) { + switch ($foo) { + case 1: + break 2; + default: + break 2; + } + } + } + } + +} diff --git a/tests/PHPStan/Rules/Keywords/data/declare-inline-html.php b/tests/PHPStan/Rules/Keywords/data/declare-inline-html.php new file mode 100644 index 0000000000..9c270f6a18 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/declare-inline-html.php @@ -0,0 +1,2 @@ +some html + @@ -20,16 +21,13 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->analyse([__DIR__ . '/data/abstract-method.php'], [ [ 'Non-abstract class AbstractMethod\Bar contains abstract method doBar().', 15, ], [ - 'Non-abstract class AbstractMethod\Baz contains abstract method doBar().', + 'Interface AbstractMethod\Baz contains abstract method doBar().', 22, ], ]); @@ -63,4 +61,62 @@ public function testBug4214(): void $this->analyse([__DIR__ . '/data/bug-4214.php'], []); } + public function testNonAbstractMethodWithNoBody(): void + { + $this->analyse([__DIR__ . '/data/bug-4244.php'], [ + [ + 'Non-abstract method HelloWorld::sayHello() must contain a body.', + 5, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/method-in-enum-without-body.php'], [ + [ + 'Non-abstract method MethodInEnumWithoutBody\Foo::doFoo() must contain a body.', + 8, + ], + [ + 'Enum MethodInEnumWithoutBody\Foo contains abstract method doBar().', + 10, + ], + ]); + } + + public function testBug11592(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->analyse([__DIR__ . '/../Classes/data/bug-11592.php'], [ + [ + 'Enum Bug11592\Test contains abstract method from().', + 9, + ], + [ + 'Enum Bug11592\Test contains abstract method tryFrom().', + 11, + ], + [ + 'Enum Bug11592\Test2 contains abstract method from().', + 24, + ], + [ + 'Enum Bug11592\Test2 contains abstract method tryFrom().', + 26, + ], + [ + 'Enum Bug11592\EnumWithAbstractMethod contains abstract method foo().', + 46, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/AbstractPrivateMethodRuleTest.php b/tests/PHPStan/Rules/Methods/AbstractPrivateMethodRuleTest.php new file mode 100644 index 0000000000..8154e0b77d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/AbstractPrivateMethodRuleTest.php @@ -0,0 +1,31 @@ + */ +class AbstractPrivateMethodRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AbstractPrivateMethodRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/abstract-private-method.php'], [ + [ + 'Private method PrivateAbstractMethod\HelloWorld::sayPrivate() cannot be abstract.', + 12, + ], + [ + 'Private method PrivateAbstractMethod\fooInterface::sayPrivate() cannot be abstract.', + 24, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php deleted file mode 100644 index 0272593c76..0000000000 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php +++ /dev/null @@ -1,61 +0,0 @@ - - */ -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 93d25e5e48..decb237d34 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -26,18 +25,47 @@ class CallMethodsRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; - private int $phpVersion = PHP_VERSION_ID; + private bool $checkImplicitMixed = false; protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } + public function testIsCallablePhp7(): void + { + if (PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('Test requires PHP 7.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([ __DIR__ . '/data/call-methods-is-callable.php'], []); + } + + public function testIsCallablePhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([ __DIR__ . '/data/call-methods-is-callable.php'], [ + [ + 'Parameter #1 $str of method TestMethodsIsCallable\CheckIsCallable::test() expects callable(): mixed, \'Test…\' given.', + 10, + ], + ]); + } + public function testCallMethods(): void { $this->checkThisOnly = false; @@ -426,14 +454,17 @@ public function testCallMethods(): void [ 'Parameter #1 $i of method Test\SubtractedMixed::requireInt() expects int, mixed given.', 1277, + 'Type int has already been eliminated from mixed.', ], [ 'Parameter #1 $i of method Test\SubtractedMixed::requireInt() expects int, mixed given.', 1284, + 'Type int|string has already been eliminated from mixed.', ], [ 'Parameter #1 $parameter of method Test\SubtractedMixed::requireIntOrString() expects int|string, mixed given.', 1285, + 'Type int|string has already been eliminated from mixed.', ], [ 'Parameter #2 $b of method Test\ExpectsExceptionGenerics::expectsExceptionUpperBound() expects Exception, Throwable given.', @@ -469,12 +500,17 @@ public function testCallMethods(): void 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ - 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array{\'foo\'}|null given.', + 'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.', + 1512, + ], + [ + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ 'Parameter #1 $members of method Test\\ParameterTypeCheckVerbosity::doBar() expects array, array given.', 1589, + "Array does not have offset 'id'.", ], [ 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', @@ -505,6 +541,30 @@ public function testCallMethods(): void 'Parameter #1 $code of method Test\\ValueOfParam::foo() expects \'John F. Kennedy…\'|\'La Guardia Airport\', \'Newark Liberty…\' given.', 1802, ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, numeric-string given.', + 1844, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, \'0\' given.', + 1845, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, string given.', + 1846, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, non-empty-string given.', + 1847, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, literal-string given.', + 1848, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, int given.', + 1849, + ], ]); } @@ -733,14 +793,17 @@ public function testCallMethodsOnThisOnly(): void [ 'Parameter #1 $i of method Test\SubtractedMixed::requireInt() expects int, mixed given.', 1277, + 'Type int has already been eliminated from mixed.', ], [ 'Parameter #1 $i of method Test\SubtractedMixed::requireInt() expects int, mixed given.', 1284, + 'Type int|string has already been eliminated from mixed.', ], [ 'Parameter #1 $parameter of method Test\SubtractedMixed::requireIntOrString() expects int|string, mixed given.', 1285, + 'Type int|string has already been eliminated from mixed.', ], [ 'Parameter #2 $b of method Test\ExpectsExceptionGenerics::expectsExceptionUpperBound() expects Exception, Throwable given.', @@ -764,12 +827,17 @@ public function testCallMethodsOnThisOnly(): void 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ - 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array{\'foo\'}|null given.', + 'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.', + 1512, + ], + [ + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ 'Parameter #1 $members of method Test\\ParameterTypeCheckVerbosity::doBar() expects array, array given.', 1589, + "Array does not have offset 'id'.", ], [ 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', @@ -800,6 +868,30 @@ public function testCallMethodsOnThisOnly(): void 'Parameter #1 $code of method Test\\ValueOfParam::foo() expects \'John F. Kennedy…\'|\'La Guardia Airport\', \'Newark Liberty…\' given.', 1802, ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, numeric-string given.', + 1844, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, \'0\' given.', + 1845, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, string given.', + 1846, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, non-empty-string given.', + 1847, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, literal-string given.', + 1848, + ], + [ + 'Parameter #1 $string of method Test\NonFalsyString::acceptsNonFalsyString() expects non-falsy-string, int given.', + 1849, + ], ]); } @@ -879,14 +971,15 @@ public function testClosureBind(): void 'Call to an undefined method CallClosureBind\Foo::nonexistentMethod().', 44, ], + [ + 'Parameter #2 $newScope of method Closure::bindTo() expects \'static\'|class-string|object|null, \'CallClosureBind\\\Bar3\' given.', + 74, + ], ]); } public function testArrowFunctionClosureBind(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1469,6 +1562,10 @@ public function dataExplicitMixed(): array 'Cannot call method foo() on mixed.', 17, ], + [ + 'Cannot call method foo() on T of mixed.', + 26, + ], [ 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', 43, @@ -1480,6 +1577,7 @@ public function dataExplicitMixed(): array [ 'Parameter #1 $cb of method CheckExplicitMixedMethodCall\CallableMixed::doFoo() expects callable(mixed): void, Closure(int): void given.', 133, + 'Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', ], [ 'Parameter #1 $cb of method CheckExplicitMixedMethodCall\CallableMixed::doBar2() expects callable(): int, Closure(): mixed given.', @@ -1496,12 +1594,12 @@ public function dataExplicitMixed(): array /** * @dataProvider dataExplicitMixed - * @param mixed[] $errors + * @param list $errors */ public function testExplicitMixed(bool $checkExplicitMixed, array $errors): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); } $this->checkThisOnly = false; @@ -1511,6 +1609,46 @@ public function testExplicitMixed(bool $checkExplicitMixed, array $errors): void $this->analyse([__DIR__ . '/data/check-explicit-mixed.php'], $errors); } + public function dataImplicitMixed(): array + { + return [ + [ + true, + [ + [ + 'Cannot call method foo() on mixed.', + 16, + ], + [ + 'Parameter #1 $i of method CheckImplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', + 42, + ], + [ + 'Parameter #1 $cb of method CheckImplicitMixedMethodCall\CallableMixed::doBar2() expects callable(): int, Closure(): mixed given.', + 139, + ], + ], + ], + [ + false, + [], + ], + ]; + } + + /** + * @dataProvider dataImplicitMixed + * @param list $errors + */ + public function testImplicitMixed(bool $checkImplicitMixed, array $errors): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/check-implicit-mixed.php'], $errors); + } + public function testBug3409(): void { $this->checkThisOnly = false; @@ -1550,11 +1688,6 @@ public function testBug3415Two(): void public function testBug3445(): void { - if (!self::$useStaticReflectionProvider) { - if (PHP_VERSION_ID < 70300) { - $this->markTestSkipped('PHP looks at the parameter value non-lazily before PHP 7.3.'); - } - } $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1585,9 +1718,6 @@ public function testBug3481(): void public function testBug3683(): void { - if (self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires hybrid reflection.'); - } $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1626,10 +1756,6 @@ public function testStringableStrictTypes(): void public function testMatchExpressionVoidIsUsed(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1647,10 +1773,6 @@ public function testMatchExpressionVoidIsUsed(): void public function testNullSafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1668,6 +1790,14 @@ public function testNullSafe(): void 'Parameter #1 $passedByRef of method NullsafeMethodCall\Foo::doBaz() is passed by reference, so it expects variables only.', 27, ], + [ + 'Cannot call method foo() on null.', + 33, + ], + [ + 'Cannot call method foo() on null.', + 34, + ], ]); } @@ -1676,9 +1806,6 @@ public function testDisallowNamedArguments(): void if (PHP_VERSION_ID >= 80000) { $this->markTestSkipped('Test requires PHP earlier than 8.0.'); } - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->checkThisOnly = false; $this->checkNullables = true; @@ -1692,16 +1819,29 @@ public function testDisallowNamedArguments(): void ]); } + public function testDisallowNamedArgumentsInPhpVersionScope(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/disallow-named-arguments-php-version-scope.php'], [ + [ + 'Named arguments are supported only on PHP 8.0 and later.', + 26, + ], + ]); + } + public function testNamedArguments(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); } $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->phpVersion = 80000; $this->analyse([__DIR__ . '/data/named-arguments.php'], [ [ @@ -1781,7 +1921,7 @@ public function testNamedArguments(): void 91, ], [ - 'Parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 'Named argument foo for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', 91, ], [ @@ -1796,15 +1936,19 @@ public function testNamedArguments(): void 'Unpacked argument (...) cannot be followed by a non-unpacked argument.', 94, ], + [ + 'Named argument foo for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 95, + ], + [ + 'Named argument bar for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 95, + ], ]); } public function testBug4199(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1819,10 +1963,6 @@ public function testBug4199(): void public function testBug4188(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -1837,7 +1977,7 @@ public function testOnlyRelevantUnableToResolveTemplateType(): void $this->checkUnionTypes = true; $this->analyse([__DIR__ . '/data/only-relevant-unable-to-resolve-template-type.php'], [ [ - 'Parameter #1 $a of method OnlyRelevantUnableToResolve\Foo::doBaz() expects array, int given.', + 'Parameter #1 $a of method OnlyRelevantUnableToResolve\Foo::doBaz() expects array, int given.', 41, ], [ @@ -1901,7 +2041,7 @@ public function testBug4557(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4557.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4557.php'], []); } public function testBug4209(): void @@ -1909,7 +2049,7 @@ public function testBug4209(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4209.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4209.php'], []); } public function testBug4209Two(): void @@ -1917,7 +2057,7 @@ public function testBug4209Two(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4209-2.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4209-2.php'], []); } public function testBug3321(): void @@ -1925,7 +2065,7 @@ public function testBug3321(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-3321.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3321.php'], []); } public function testBug4498(): void @@ -1933,7 +2073,7 @@ public function testBug4498(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4498.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4498.php'], []); } public function testBug3922(): void @@ -1941,7 +2081,7 @@ public function testBug3922(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-3922.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3922.php'], [ [ 'Parameter #1 $query of method Bug3922\FooQueryHandler::handle() expects Bug3922\FooQuery, Bug3922\BarQuery given.', 63, @@ -1954,7 +2094,7 @@ public function testBug4642(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4642.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4642.php'], []); } public function testBug4008(): void @@ -1975,14 +2115,14 @@ public function testBug3546(): void public function testBug4800(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->phpVersion = 80000; + + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/bug-4800.php'], [ [ 'Missing parameter $bar (string) in call to method Bug4800\HelloWorld2::a().', @@ -2018,10 +2158,6 @@ 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; @@ -2065,10 +2201,11 @@ public function testGenericObjectLowerBound(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/generic-object-lower-bound.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/generic-object-lower-bound.php'], [ [ 'Parameter #1 $c of method GenericObjectLowerBound\Foo::doFoo() expects GenericObjectLowerBound\Collection, GenericObjectLowerBound\Collection given.', 48, + 'Template type T on class GenericObjectLowerBound\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], ]); } @@ -2096,26 +2233,35 @@ public function testBug5536(): void 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.', + 64, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], [ 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', 68, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], [ 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', 72, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], + [ + 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', + 81, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], - /*[ + [ 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', 85, - ],*/ + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], ]); } @@ -2146,11 +2292,11 @@ public function testLiteralString(): void 58, ], [ - 'Parameter #1 $a of method LiteralStringMethod\Foo::requireArrayOfLiteralStrings() expects array, array given.', + '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.', + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, array given.', 65, ], [ @@ -2244,10 +2390,6 @@ public function testBug5868(): void public function testBug5460(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2256,10 +2398,6 @@ public function testBug5460(): void 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; @@ -2287,6 +2425,34 @@ public function testEnums(): void 'Call to an undefined method CallMethodInEnum\Bar::doNonexistent().', 22, ], + [ + 'Parameter #1 $countryName of method CallMethodInEnum\FooCall::hello() expects \'The Netherlands\'|\'United States\', CallMethodInEnum\CountryNo::NL given.', + 63, + ], + [ + 'Parameter #1 $countryMap of method CallMethodInEnum\FooCall::helloArray() expects array<\'The Netherlands\'|\'United States\', bool>, array{abc: true} given.', + 66, + ], + [ + 'Parameter #1 $countryMap of method CallMethodInEnum\FooCall::helloArray() expects array<\'The Netherlands\'|\'United States\', bool>, array{abc: 123} given.', + 67, + ], + [ + 'Parameter #1 $countryMap of method CallMethodInEnum\FooCall::helloArray() expects array<\'The Netherlands\'|\'United States\', bool>, array{true} given.', + 70, + ], + [ + 'Parameter #1 $one of method CallMethodInEnum\TestPassingEnums::requireOne() expects CallMethodInEnum\TestPassingEnums::ONE, $this(CallMethodInEnum\TestPassingEnums)&CallMethodInEnum\TestPassingEnums given.', + 91, + ], + [ + 'Parameter #1 $one of method CallMethodInEnum\TestPassingEnums::requireOne() expects CallMethodInEnum\TestPassingEnums::ONE, $this(CallMethodInEnum\TestPassingEnums)&CallMethodInEnum\TestPassingEnums given.', + 99, + ], + [ + 'Parameter #1 $one of method CallMethodInEnum\TestPassingEnums::requireOne() expects CallMethodInEnum\TestPassingEnums::ONE, $this(CallMethodInEnum\TestPassingEnums)&CallMethodInEnum\TestPassingEnums given.', + 106, + ], ]); } @@ -2299,7 +2465,7 @@ public function testBug6239(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-6293.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6293.php'], []); } public function testBug6306(): void @@ -2395,10 +2561,6 @@ public function testBug6464(): void public function testBug6423(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; @@ -2464,4 +2626,1004 @@ public function testGenericsInferCollectionLevel8(): void ]); } + public function testGenericVariance(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/generic-variance.php'], [ + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant, GenericVarianceCall\Invariant given.', + 45, + ], + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant, GenericVarianceCall\Invariant given.', + 53, + 'Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::covariant() expects GenericVarianceCall\Covariant, GenericVarianceCall\Covariant given.', + 60, + ], + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::contravariant() expects GenericVarianceCall\Contravariant, GenericVarianceCall\Contravariant given.', + 83, + ], + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::invariantArray() expects array{GenericVarianceCall\Invariant}, array{GenericVarianceCall\Invariant} given.', + 97, + 'Offset 0 (GenericVarianceCall\Invariant) does not accept type GenericVarianceCall\Invariant: Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], + ]); + } + + public function testBug6904(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6904.php'], []); + } + + public function testBug6917(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6917.php'], []); + } + + public function testBug3284(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-3284.php'], []); + } + + public function testUnresolvableParameter(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/unresolvable-parameter.php'], [ + [ + 'Parameter #2 $v of method UnresolvableParameter\HelloWorld::foo() contains unresolvable type.', + 18, + ], + [ + 'Parameter #2 $v of method UnresolvableParameter\HelloWorld::foo() contains unresolvable type.', + 19, + ], + [ + 'Parameter #2 $v of method UnresolvableParameter\HelloWorld::foo() expects 1, 0 given.', + 21, + ], + ]); + } + + public function testConditionalComplexTemplates(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/conditional-complex-templates.php'], []); + } + + public function testBug6291(): 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-6291.php'], []); + } + + public function testBug1517(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-1517.php'], []); + } + + public function testBug7593(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7593.php'], []); + } + + public function testBug6946(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6946.php'], []); + } + + public function testBug5754(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5754.php'], []); + } + + public function testBug7600(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7600.php'], []); + } + + public function testBug8058(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2'); + } + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-8058.php'], []); + } + + public function testBug8058b(): void + { + if (PHP_VERSION_ID >= 80200) { + $this->markTestSkipped('Test requires PHP before 8.2'); + } + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-8058.php'], [ + [ + 'Call to an undefined method mysqli::execute_query().', + 11, + ], + ]); + } + + public function testArrayCastListTypes(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/array-cast-list-types.php'], []); + } + + public function testBug5623(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-5623.php'], []); + } + + public function testImagick(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/imagick.php'], []); + } + + public function testImagickPixel(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/imagick-pixel.php'], []); + } + + public function testNewInstanceArgsIssue8679(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/reflection-class-issue-8679.php'], []); + } + + public function testNonEmptyArray(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/non-empty-array.php'], [ + [ + 'Parameter #1 $nonEmpty of method AcceptNonEmptyArray\Foo::requireNonEmpty() expects non-empty-array, array given.', + 15, + 'array might be empty.', + ], + [ + 'Parameter #1 $nonEmpty of method AcceptNonEmptyArray\Foo::requireNonEmpty() expects non-empty-array, array{} given.', + 17, + 'array{} is empty.', + ], + ]); + } + + public function testBug8752(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8752.php'], [ + [ + 'Cannot call method abc() on class-string.', + 18, + ], + ]); + } + + public function dataCallablesWithoutCheckNullables(): iterable + { + yield [false, false, []]; + yield [true, false, []]; + + $errors = [ + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar() expects callable(float|null): (float|null), Closure(float): float given.', + 25, + 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz() expects Closure(float|null): (float|null), Closure(float): float given.', + 28, + 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar2() expects callable(float|null): float, Closure(float|null): (float|null) given.', + 32, + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz2() expects Closure(float|null): float, Closure(float|null): (float|null) given.', + 35, + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar2() expects callable(float|null): float, Closure(float): float given.', + 45, + 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz2() expects Closure(float|null): float, Closure(float): float given.', + 48, + 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', + ], + ]; + yield [false, true, $errors]; + yield [true, true, $errors]; + } + + /** + * @dataProvider dataCallablesWithoutCheckNullables + * @param list $expectedErrors + */ + public function testCallablesWithoutCheckNullables(bool $checkNullables, bool $checkUnionTypes, array $expectedErrors): void + { + $this->checkThisOnly = false; + $this->checkNullables = $checkNullables; + $this->checkUnionTypes = $checkUnionTypes; + $this->analyse([__DIR__ . '/data/callables-without-check-nullables.php'], $expectedErrors); + } + + public function testBug8713(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8713.php'], []); + } + + public function testCannotCallOnGenericClassString(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/../Comparison/data/impossible-method-exists-on-generic-class-string.php'], [ + [ + 'Cannot call method nonExistent() on class-string.', + 14, + ], + [ + 'Cannot call method staticAbc() on class-string.', + 20, + ], + [ + 'Cannot call method nonStaticAbc() on class-string.', + 25, + ], + [ + 'Cannot call method nonExistent() on class-string.', + 35, + ], + [ + 'Cannot call method staticAbc() on class-string.', + 41, + ], + [ + 'Cannot call method nonStaticAbc() on class-string.', + 46, + ], + ]); + } + + public function testBug8888(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8888.php'], []); + } + + public function testBug9542(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9542.php'], []); + } + + public function testTrickyCallables(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/tricky-callables.php'], [ + [ + 'Parameter #1 $cb of method TrickyCallables\Foo::doBar() expects callable(string|null): void, callable(string): void given.', + 13, + 'Type string of parameter #1 of passed callable needs to be same or wider than parameter type string|null of accepting callable.', + ], + [ + 'Parameter #1 $cb of method TrickyCallables\Bar::doBar() expects callable(string=): void, callable(string): void given.', + 34, + 'Parameter #1 of passed callable is required but the parameter of accepting callable is optional. It might be called without it.', + ], + [ + 'Parameter #1 $cb of method TrickyCallables\Baz::doBar() expects callable(): void, callable(string): void given.', + 55, + 'Parameter #1 of passed callable is required but accepting callable does not have that parameter. It will be called without it.', + ], + [ + 'Parameter #1 $filter of method TrickyCallables\TwoErrorsAtOnce::run() expects callable(int|string=): bool, Closure(int): true given.', + 83, + '• Parameter #1 $key of passed callable is required but the parameter of accepting callable is optional. It might be called without it. +• Type int of parameter #1 $key of passed callable needs to be same or wider than parameter type int|string of accepting callable.', + ], + ]); + } + + public function testObjectShapes(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/object-shapes.php'], [ + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, stdClass given.', + 13, + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, Exception given.', + 14, + PHP_VERSION_ID >= 80200 ? 'Exception does not have property $foo.' : 'Exception might not have property $foo.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, Exception given.', + 15, + 'Exception might not have property $foo.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object{foo: string, bar: int} given.', + 37, + 'Property ($foo) type int does not accept type string.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object{foo?: int, bar: string} given.', + 38, + 'object{foo?: int, bar: string} might not have property $foo.', + ], + [ + 'Parameter #1 $std of method ObjectShapesAcceptance\Foo::requireStdClass() expects stdClass, object{foo: string, bar: int} given.', + 41, + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object{foo: string, bar: int}&stdClass given.', + 44, + 'Property ($foo) type int does not accept type string.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object given.', + 55, + '• object might not have property $foo. +• object might not have property $bar.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, stdClass given.', + 56, + ], + [ + 'Parameter #1 $bar of method ObjectShapesAcceptance\Bar::requireBar() expects ObjectShapesAcceptance\Bar, object{a: int} given.', + 72, + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Bar::doBar() expects object{a: string}, ObjectShapesAcceptance\Bar given.', + 78, + 'Property ($a) type string does not accept type int.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doBar() expects object{a: int}, $this(ObjectShapesAcceptance\Baz) given.', + 106, + 'Property ObjectShapesAcceptance\Baz::$a is not public.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doBaz() expects object{b: int}, $this(ObjectShapesAcceptance\Baz) given.', + 107, + 'Property ObjectShapesAcceptance\Baz::$b is static.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doLorem() expects object{c: int}, $this(ObjectShapesAcceptance\Baz) given.', + 108, + 'Property ObjectShapesAcceptance\Baz::$c is not readable.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doIpsum() expects object{d: array{foo: string}}, $this(ObjectShapesAcceptance\Baz) given.', + 109, + 'Property ($d) type array{foo: string} does not accept type array{foo: int}: Offset \'foo\' (string) does not accept type int.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\OptionalProperty::doBar() expects object{foo?: int}, object{foo?: string} given.', + 157, + 'Property ($foo) type int does not accept type string.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\OptionalProperty::doBaz() expects object{foo: int}, object{foo?: string} given.', + 158, + 'Property ($foo) type int does not accept type string.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\TestAcceptance::doFoo() expects object{foo: int}, Traversable given.', + 210, + 'Traversable might not have property $foo.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\TestAcceptance::doFoo() expects object{foo: int}, ObjectShapesAcceptance\FinalClass given.', + 211, + PHP_VERSION_ID < 80200 ? 'ObjectShapesAcceptance\FinalClass might not have property $foo.' : 'ObjectShapesAcceptance\FinalClass does not have property $foo.', + ], + ]); + } + + public function testBug9951(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9951.php'], [ + [ + 'Parameter #1 $field of method Bug9951\Cl::addCondition() expects array|Bug9951\AbstractScope|Bug9951\Expressionable|string, mixed given.', + 26, + ], + [ + 'Parameter #1 $field of method Bug9951\Cl::addCondition() expects array|Bug9951\AbstractScope|Bug9951\Expressionable|string, object|string|null given.', + 31, + ], + ]); + } + + public function testTypedClassConstants(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/return-type-class-constant.php'], []); + } + + public function testNamedParametersForMultiVariantFunctions(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/call-methods-named-params-multivariant.php'], [ + [ + 'Unknown parameter $options in call to method XSLTProcessor::setParameter().', + 10, + ], + [ + 'Missing parameter $name (array) in call to method XSLTProcessor::setParameter().', + 10, + ], + [ + 'Unknown parameter $colno in call to method PDO::query().', + 15, + ], + [ + 'Unknown parameter $className in call to method PDO::query().', + 17, + ], + [ + 'Unknown parameter $constructorArgs in call to method PDO::query().', + 17, + ], + [ + 'Unknown parameter $className in call to method PDOStatement::setFetchMode().', + 22, + ], + ]); + } + + public function testBug5518(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-5518.php'], []); + } + + public function testRequireExtends(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/../Properties/data/require-extends.php'], [ + [ + 'Call to an undefined method RequireExtends\MyInterface::doesNotExist().', + 43, + ], + ]); + } + + public function testRequireImplements(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/../Properties/data/require-implements.php'], [ + [ + 'Call to an undefined method RequireImplements\MyBaseClass::doesNotExist().', + 44, + ], + ]); + } + + public function testBug6371(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-6371.php'], [ + [ + 'Parameter #1 $t of method Bug6371\HelloWorld::compare() expects int, true given.', + 24, + ], + [ + 'Parameter #2 $k of method Bug6371\HelloWorld::compare() expects string, false given.', + 24, + ], + ]); + } + + public function testBugTemplateMixedUnionIntersect(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-template-mixed-union-intersect.php'], [ + [ + 'Call to an undefined method BugTemplateMixedUnionIntersect\FooInterface&T of mixed::bar().', + 17, + ], + [ + 'Call to an undefined method BugTemplateMixedUnionIntersect\FooInterface::bar().', + 20, + ], + [ + 'Cannot call method foo() on BugTemplateMixedUnionIntersect\FooInterface|T of mixed.', + 23, + ], + [ + 'Cannot call method foo() on mixed.', + 25, + ], + ]); + } + + public function testBug9009(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-9009.php'], []); + } + + public function testBuSplObjectStorageRemove(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-SplObjectStorage-remove.php'], [ + // removeNoIntersect should be reported, but unfortunately it cannot be expressed by the type system. + ]); + } + + public function testClosureBindToParamClosureThis(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/closure-bind-to-param-closure-this.php'], [ + [ + 'Parameter #1 $newThis of method Closure::bindTo() expects stdClass, ClosureBindToParamClosureThis\Foo given.', + 23, + ], + ]); + } + + public function testPureCallable(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/pure-callable-accepts.php'], [ + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureCallable() expects pure-callable(): mixed, callable(): mixed given.', + 33, + ], + [ + 'Parameter #1 $i of method PureCallableMethodAccepts\Foo::acceptsInt() expects int, callable given.', + 35, + ], + [ + 'Parameter #1 $i of method PureCallableMethodAccepts\Foo::acceptsInt() expects int, callable given.', + 36, + ], + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureCallable() expects pure-callable(): mixed, Closure(): 1 given.', + 41, + ], + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureClosure() expects pure-Closure, Closure(): 1 given.', + 61, + ], + ]); + } + + public function testClosureParameterGenerics(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/closure-parameter-generics.php'], []); + } + + public function testNoNamedArguments(): 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/no-named-arguments.php'], [ + [ + 'Method NoNamedArgumentsMethod\Foo::doFoo() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 32, + ], + [ + 'Method NoNamedArgumentsMethod\Bar::doFoo() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 33, + ], + ]); + } + + public function testTraitMixin(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/trait-mixin.php'], []); + } + + public function testLowercaseString(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/lowercase-string.php'], [ + [ + 'Parameter #1 $s of method LowercaseString\Bar::acceptLowercaseString() expects lowercase-string, \'NotLowerCase\' given.', + 26, + ], + [ + 'Parameter #1 $s of method LowercaseString\Bar::acceptLowercaseString() expects lowercase-string, string given.', + 28, + ], + [ + 'Parameter #1 $s of method LowercaseString\Bar::acceptLowercaseString() expects lowercase-string, numeric-string given.', + 30, + ], + ]); + } + + public function testUppercaseString(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/uppercase-string.php'], [ + [ + 'Parameter #1 $s of method UppercaseString\Bar::acceptUppercaseString() expects uppercase-string, \'NotUpperCase\' given.', + 26, + ], + [ + 'Parameter #1 $s of method UppercaseString\Bar::acceptUppercaseString() expects uppercase-string, string given.', + 28, + ], + [ + 'Parameter #1 $s of method UppercaseString\Bar::acceptUppercaseString() expects uppercase-string, numeric-string given.', + 30, + ], + ]); + } + + public function testBug10159(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-10159.php'], []); + } + + public function testBug1953(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-1953.php'], [ + [ + 'Cannot call method bar() on string.', + 12, + ], + ]); + } + + public function testBug11559c(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-11559c.php'], [ + [ + 'Method class@anonymous/tests/PHPStan/Rules/Methods/data/bug-11559c.php:6:1::regular_fn() invoked with 3 parameters, 1 required.', + 15, + ], + ]); + } + + public function testBug4801(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-4801.php'], []); + } + + public function testBug12544(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12544.php'], [ + [ + 'Call to private method somethingElse() of class Bug12544\Bar.', + 20, + ], + ]); + } + + public function testBug12691(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-12691.php'], []); + } + + public function testBug12422(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12422.php'], []); + } + + public function testBug6828(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-6828.php'], []); + } + + public function testDynamicCall(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/dynamic-call.php'], [ + [ + 'Call to an undefined method MethodsDynamicCall\Foo::bar().', + 23, + ], + [ + 'Call to an undefined method MethodsDynamicCall\Foo::doBar().', + 26, + ], + [ + 'Call to an undefined method MethodsDynamicCall\Foo::doBuz().', + 26, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, int|string given.', + 53, + ], + [ + 'Parameter #1 $s of method MethodsDynamicCall\Foo::doQux() expects string, int given.', + 54, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, string given.', + 55, + ], + ]); + } + + public function testBug12884(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12884.php'], []); + } + + public function testBu12793(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12793.php'], []); + } + + public function testBug12880(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12880.php'], []); + } + + public function testBug12940(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12940.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index b62f8a8348..b9999f8610 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -2,8 +2,9 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -11,6 +12,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -23,21 +26,41 @@ class CallStaticMethodsRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new CallStaticMethodsRule( - new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new StaticMethodCallCheck( + $reflectionProvider, + $ruleLevelHelper, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + true, + ), + new FunctionCallParametersCheck( + $ruleLevelHelper, + new NullsafeCheck(), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, + true, + true, + true, + ), ); } public function testCallStaticMethods(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test does not run on PHP 7.4 because of referencing parent:: without parent class.'); - } $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/call-static-methods.php'], [ [ @@ -227,7 +250,7 @@ public function testCallStaticMethods(): void 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Call to an undefined static method static(CallStaticMethods\CallWithStatic)::nonexistent().', + 'Call to an undefined static method CallStaticMethods\CallWithStatic::nonexistent().', 344, ], ]); @@ -390,12 +413,12 @@ public function testBug2164(): void public function testNamedArguments(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/static-method-named-arguments.php'], [ [ 'Missing parameter $j (int) in call to static method StaticMethodNamedArguments\Foo::doFoo().', @@ -431,6 +454,10 @@ public function testBug4550(): void public function testBug1971(): void { + if (PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('Test requires PHP 7.x'); + } + $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-1971.php'], [ [ @@ -440,6 +467,29 @@ public function testBug1971(): void ]); } + public function testBug1971Php8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-1971.php'], [ + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{\'Bug1971\\\HelloWorld\', \'sayHello\'} given.', + 14, + ], + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello\'} given.', + 15, + ], + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello2\'} given.', + 16, + ], + ]); + } + public function testBug5259(): void { $this->checkThisOnly = false; @@ -460,10 +510,6 @@ public function testBug4886(): void 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 @@ -479,10 +525,6 @@ public function testBug5893(): void 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; @@ -503,4 +545,372 @@ public function testBug5757(): void $this->analyse([__DIR__ . '/data/bug-5757.php'], []); } + public function testDiscussion7004(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/discussion-7004.php'], [ + [ + 'Parameter #1 $data of static method Discussion7004\Foo::fromArray1() expects array, array given.', + 46, + ], + [ + 'Parameter #1 $data of static method Discussion7004\Foo::fromArray2() expects array{array{newsletterName: string, subscriberCount: int}}, array given.', + 47, + ], + [ + 'Parameter #1 $data of static method Discussion7004\Foo::fromArray3() expects array{newsletterName: string, subscriberCount: int}, array given.', + 48, + ], + ]); + } + + public function testTemplateTypeInOneBranchOfConditional(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/template-type-in-one-branch-of-conditional.php'], [ + [ + 'Parameter #1 $params of static method TemplateTypeInOneBranchOfConditional\DriverManager::getConnection() expects array{wrapperClass?: class-string}, array{wrapperClass: \'stdClass\'} given.', + 27, + "Offset 'wrapperClass' (class-string) does not accept type string.", + ], + [ + 'Unable to resolve the template type T in call to method static method TemplateTypeInOneBranchOfConditional\DriverManager::getConnection()', + 27, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + ]); + } + + public function testBug7489(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7489.php'], []); + } + + public function testHasMethodStaticCall(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/static-has-method.php'], [ + [ + 'Call to an undefined static method StaticHasMethodCall\rex_var::doesNotExist().', + 38, + ], + [ + 'Call to an undefined static method StaticHasMethodCall\rex_var::doesNotExist().', + 48, + ], + ]); + } + + public function testBug1267(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-1267.php'], []); + } + + public function testBug6147(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-6147.php'], []); + } + + public function testBug5781(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-5781.php'], [ + [ + 'Parameter #1 $param of static method Bug5781\Foo::bar() expects array{a: bool, b: bool, c: bool, d: bool, e: bool, f: bool, g: bool, h: bool, ...}, array{} given.', + 17, + "Array does not have offset 'a'.", + ], + ]); + } + + public function testBug8296(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-8296.php'], [ + [ + 'Parameter #1 $objects of static method Bug8296\VerifyLoginTask::continueDump() expects array, array given.', + 12, + ], + [ + 'Parameter #1 $string of static method Bug8296\VerifyLoginTask::stringByRef() expects string, int given.', + 15, + ], + ]); + } + + public function testRequireExtends(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + + $this->analyse([__DIR__ . '/../Properties/data/require-extends.php'], [ + [ + 'Call to an undefined static method RequireExtends\MyInterface::doesNotExistStatic().', + 44, + ], + ]); + } + + public function testRequireImplements(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + + $this->analyse([__DIR__ . '/../Properties/data/require-implements.php'], [ + [ + 'Call to an undefined static method RequireImplements\MyBaseClass::doesNotExistStatic().', + 45, + ], + ]); + } + + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Cannot call static method foo() on mixed.', + 17, + ], + [ + 'Cannot call static method foo() on T of mixed.', + 26, + ], + [ + 'Parameter #1 $i of static method CallStaticMethodMixed\Bar::doBar() expects int, mixed given.', + 43, + ], + [ + 'Only iterables can be unpacked, mixed given in argument #1.', + 52, + ], + [ + 'Parameter #1 $i of static method CallStaticMethodMixed\Bar::doBar() expects int, T given.', + 81, + ], + [ + 'Only iterables can be unpacked, T of mixed given in argument #1.', + 84, + ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callAcceptsExplicitMixed() expects callable(mixed): void, Closure(int): void given.', + 134, + 'Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', + ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callReturnsInt() expects callable(): int, Closure(): mixed given.', + 161, + ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callReturnsInt() expects callable(): int, Closure(): mixed given.', + 168, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Cannot call static method foo() on mixed.', + 16, + ], + [ + 'Parameter #1 $i of static method CallStaticMethodMixed\Bar::doBar() expects int, mixed given.', + 42, + ], + [ + 'Only iterables can be unpacked, mixed given in argument #1.', + 51, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkThisOnly = false; + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/call-static-method-mixed.php'], $errors); + } + + public function testBugWrongMethodNameWithTemplateMixed(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-wrong-method-name-with-template-mixed.php'], [ + [ + 'Call to an undefined static method T of mixed&UnitEnum::from().', + 14, + ], + [ + 'Call to an undefined static method T of mixed&UnitEnum::from().', + 25, + ], + [ + 'Call to an undefined static method T of object&UnitEnum::from().', + 36, + ], + [ + 'Call to an undefined static method UnitEnum::from().', + 43, + ], + ]); + } + + public function testConditionalParam(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/conditional-param.php'], [ + [ + 'Parameter #1 $demoArg of static method ConditionalParam\HelloWorld::replaceCallback() expects string, true given.', + 16, + ], + [ + 'Parameter #1 $demoArg of static method ConditionalParam\HelloWorld::replaceCallback() expects bool, string given.', + 20, + ], + [ + // wrong + 'Parameter #1 $demoArg of static method ConditionalParam\HelloWorld::replaceCallback() expects string, true given.', + 22, + ], + ]); + } + + public function testClosureBindParamClosureThis(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/closure-bind-param-closure-this.php'], [ + [ + 'Parameter #2 $newThis of static method Closure::bind() expects stdClass, ClosureBindParamClosureThis\Foo given.', + 25, + ], + ]); + } + + public function testClosureBind(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/closure-bind.php'], [ + [ + 'Parameter #3 $newScope of static method Closure::bind() expects \'static\'|class-string|object|null, \'CallClosureBind\\\Bar3\' given.', + 68, + ], + ]); + } + + public function testBug10872(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-10872.php'], []); + } + + public function testBug12015(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12015.php'], []); + } + + public function testDynamicCall(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/dynamic-call.php'], [ + [ + 'Call to an undefined static method MethodsDynamicCall\Foo::bar().', + 33, + ], + [ + 'Call to an undefined static method MethodsDynamicCall\Foo::doBar().', + 36, + ], + [ + 'Call to an undefined static method MethodsDynamicCall\Foo::doBuz().', + 36, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, int|string given.', + 58, + ], + [ + 'Parameter #1 $s of static method MethodsDynamicCall\Foo::doQux() expects string, int given.', + 59, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, string given.', + 60, + ], + ]); + } + + public function testRestrictedInternalClassNameUsage(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/../InternalTag/data/static-method-call-on-internal-subclass.php'], [ + [ + 'Call to static method doFoo() on internal class StaticMethodCallOnInternalSubclassOne\Bar.', + 33, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php index 917cc77f8b..c7c0e8f89a 100644 --- a/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php @@ -23,6 +23,14 @@ public function testRule(): void 'Call to Exception::__construct() on a separate line has no effect.', 6, ], + [ + 'Call to new PDOStatement() on a separate line has no effect.', + 11, + ], + [ + 'Call to new stdClass() on a separate line has no effect.', + 12, + ], [ 'Call to ConstructorStatementNoSideEffects\ConstructorWithPure::__construct() on a separate line has no effect.', 57, @@ -31,6 +39,10 @@ public function testRule(): void 'Call to ConstructorStatementNoSideEffects\ConstructorWithPureAndThrowsVoid::__construct() on a separate line has no effect.', 58, ], + [ + 'Call to new ConstructorStatementNoSideEffects\NoConstructor() on a separate line has no effect.', + 68, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php index 09a4010d7e..67bd722602 100644 --- a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; use const PHP_VERSION_ID; /** @@ -15,7 +16,7 @@ class CallToMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true)); } public function testRule(): void @@ -46,10 +47,6 @@ public function testRule(): void public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-method-call-statement-no-side-effects.php'], [ [ 'Call to method Exception::getMessage() on a separate line has no effect.', @@ -68,15 +65,23 @@ public function testPhpDoc(): void $this->analyse([__DIR__ . '/data/method-call-statement-no-side-effects-phpdoc.php'], [ [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure1() on a separate line has no effect.', - 39, + 55, ], [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure2() on a separate line has no effect.', - 40, + 56, ], [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure3() on a separate line has no effect.', - 41, + 57, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure4() on a separate line has no effect.', + 58, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure5() on a separate line has no effect.', + 59, ], ]); } @@ -86,12 +91,28 @@ public function testBug4455(): void $this->analyse([__DIR__ . '/data/bug-4455.php'], []); } - public function testFirstClassCallables(): void + public function testBug11503(): void { - if (PHP_VERSION_ID < 80100) { - self::markTestSkipped('Test requires PHP 8.1.'); + $errors = [ + ['Call to method DateTimeImmutable::add() on a separate line has no effect.', 10], + ['Call to method DateTimeImmutable::modify() on a separate line has no effect.', 11], + ['Call to method DateTimeImmutable::setDate() on a separate line has no effect.', 12], + ['Call to method DateTimeImmutable::setISODate() on a separate line has no effect.', 13], + ['Call to method DateTimeImmutable::setTime() on a separate line has no effect.', 14], + ['Call to method DateTimeImmutable::setTimestamp() on a separate line has no effect.', 15], + ['Call to method DateTimeImmutable::setTimezone() on a separate line has no effect.', 17], + ]; + if (PHP_VERSION_ID < 80300) { + $errors = array_merge([ + ['Call to method DateTimeImmutable::sub() on a separate line has no effect.', 9], + ], $errors); } + $this->analyse([__DIR__ . '/data/bug-11503.php'], $errors); + } + + public function testFirstClassCallables(): void + { $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.', diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php index d7588b2ae5..1fa0216870 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -17,7 +16,7 @@ protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new CallToStaticMethodStatementWithoutSideEffectsRule( - new RuleLevelHelper($broker, true, false, true, false), + new RuleLevelHelper($broker, true, false, true, false, false, false, true), $broker, ); } @@ -29,10 +28,6 @@ public function testRule(): void 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', 12, ], - [ - 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', - 13, - ], [ 'Call to method DateTime::format() on a separate line has no effect.', 23, @@ -45,19 +40,27 @@ public function testPhpDoc(): void $this->analyse([__DIR__ . '/data/static-method-call-statement-no-side-effects-phpdoc.php'], [ [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure1() on a separate line has no effect.', - 39, + 55, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure2() on a separate line has no effect.', - 40, + 56, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure3() on a separate line has no effect.', - 41, + 57, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure4() on a separate line has no effect.', + 58, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure5() on a separate line has no effect.', + 59, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\PureThrows::pureAndThrowsVoid() on a separate line has no effect.', - 67, + 85, ], ]); } @@ -69,10 +72,6 @@ public function testBug4455(): void 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.', diff --git a/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php new file mode 100644 index 0000000000..adcb69cdbe --- /dev/null +++ b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php @@ -0,0 +1,58 @@ + */ +class ConsistentConstructorRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ConsistentConstructorRule( + self::getContainer()->getByType(MethodParameterComparisonHelper::class), + self::getContainer()->getByType(MethodVisibilityComparisonHelper::class), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/consistent-constructor.php'], [ + [ + sprintf('Parameter #1 $b (int) of method ConsistentConstructor\Bar2::__construct() is not %s with parameter #1 $b (string) of method ConsistentConstructor\Bar::__construct().', 'contravariant'), + 13, + ], + [ + 'Method ConsistentConstructor\Foo2::__construct() overrides method ConsistentConstructor\Foo1::__construct() but misses parameter #1 $a.', + 32, + ], + [ + 'Parameter #1 $i of method ConsistentConstructor\ParentWithoutConstructorChildWithConstructorRequiredParams::__construct() is not optional.', + 58, + ], + [ + 'Method ConsistentConstructor\FakeConnection::__construct() overrides method ConsistentConstructor\Connection::__construct() but misses parameter #1 $i.', + 78, + ], + ]); + } + + public function testRuleNoErrors(): void + { + $this->analyse([__DIR__ . '/data/consistent-constructor-no-errors.php'], []); + } + + public function testBug12137(): void + { + $this->analyse([__DIR__ . '/data/bug-12137.php'], [ + [ + 'Private method Bug12137\ChildClass::__construct() overriding protected method Bug12137\ParentClass::__construct() should be protected or public.', + 20, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/ConstructorReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ConstructorReturnTypeRuleTest.php new file mode 100644 index 0000000000..335972e370 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/ConstructorReturnTypeRuleTest.php @@ -0,0 +1,37 @@ + + */ +class ConstructorReturnTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ConstructorReturnTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/constructor-return-type.php'], [ + [ + 'Constructor of class ConstructorReturnType\Bar has a return type.', + 17, + ], + [ + 'Constructor of class ConstructorReturnType\UsesFooTrait has a return type.', + 26, + ], + [ + 'Original constructor of trait ConstructorReturnType\BarTrait has a return type.', + 35, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index bced1041d4..170920bbf6 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,15 +22,26 @@ class ExistingClassesInTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test does not run on PHP 7.4 because of referencing parent:: without parent class.'); - } $this->analyse([__DIR__ . '/data/typehints.php'], [ [ 'Method TestMethodTypehints\FooMethodTypehints::foo() has invalid return type TestMethodTypehints\NonexistentClass.', @@ -87,19 +100,19 @@ public function testExistingClassInTypehint(): void 67, ], [ - 'Class stdClass referenced with incorrect case: STDClass.', + 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\fOOMethodTypehints.', 76, ], [ - 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\fOOMethodTypehints.', + 'Class stdClass referenced with incorrect case: STDClass.', 76, ], [ - 'Class stdClass referenced with incorrect case: stdclass.', + 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\fOOMethodTypehintS.', 76, ], [ - 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\fOOMethodTypehintS.', + 'Class stdClass referenced with incorrect case: stdclass.', 76, ], [ @@ -153,9 +166,6 @@ public function testExistingClassInIterableTypehint(): void public function testVoidParameterTypehint(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection'); - } $this->analyse([__DIR__ . '/data/void-parameter-typehint.php'], [ [ 'Parameter $param of method VoidParameterTypehintMethod\Foo::doFoo() has invalid type void.', @@ -166,10 +176,6 @@ public function testVoidParameterTypehint(): void public function dataNativeUnionTypes(): array { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - return []; - } - return [ [ 70400, @@ -193,7 +199,7 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param mixed[] $errors + * @param list $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void { @@ -206,7 +212,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Method RequiredAfterOptional\Foo::doAmet() uses native union types but they're supported only on PHP 8.0 and later.", + 33, + ], + [ + "Method RequiredAfterOptional\Foo::doConsectetur() uses native union types but they're supported only on PHP 8.0 and later.", + 37, + ], + [ + "Method RequiredAfterOptional\Foo::doSed() uses native union types but they're supported only on PHP 8.0 and later.", + 49, + ], + ], ], [ 80000, @@ -223,6 +242,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 21, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 49, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 8, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 49, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 49, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 8, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 45, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 49, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 49, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 49, + ], ], ], ]; @@ -230,10 +359,14 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param mixed[] $errors + * @param list $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/required-parameter-after-optional.php'], $errors); } @@ -256,19 +389,19 @@ public function dataIntersectionTypes(): array 80100, [ [ - 'Parameter $a of method MethodIntersectionTypes\Foo::doBar() has unresolvable native type.', + 'Parameter $a of method MethodIntersectionTypes\FooClass::doBar() has unresolvable native type.', 33, ], [ - 'Method MethodIntersectionTypes\Foo::doBar() has unresolvable native return type.', + 'Method MethodIntersectionTypes\FooClass::doBar() has unresolvable native return type.', 33, ], [ - 'Parameter $a of method MethodIntersectionTypes\Foo::doBaz() has unresolvable native type.', + 'Parameter $a of method MethodIntersectionTypes\FooClass::doBaz() has unresolvable native type.', 38, ], [ - 'Method MethodIntersectionTypes\Foo::doBaz() has unresolvable native return type.', + 'Method MethodIntersectionTypes\FooClass::doBaz() has unresolvable native return type.', 38, ], ], @@ -278,14 +411,10 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $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); @@ -305,4 +434,184 @@ public function testEnums(): void ]); } + public function testTrueTypehint(): void + { + if (PHP_VERSION_ID >= 80200) { + $errors = []; + } elseif (PHP_VERSION_ID >= 80000) { + $errors = [ + [ + 'Parameter $v of method NativeTrueType\Truthy::foo() has invalid type NativeTrueType\true.', + 10, + ], + [ + 'Method NativeTrueType\Truthy::foo() has invalid return type NativeTrueType\true.', + 10, + ], + [ + 'Parameter $trueUnion of method NativeTrueType\Truthy::trueUnion() has invalid type NativeTrueType\true.', + 14, + ], + [ + 'Method NativeTrueType\Truthy::trueUnionReturn() has invalid return type NativeTrueType\true.', + 31, + ], + ]; + } else { + $errors = [ + [ + 'Parameter $v of method NativeTrueType\Truthy::foo() has invalid type NativeTrueType\true.', + 10, + ], + [ + 'Method NativeTrueType\Truthy::foo() has invalid return type NativeTrueType\true.', + 10, + ], + [ + "Method NativeTrueType\Truthy::trueUnion() uses native union types but they're supported only on PHP 8.0 and later.", + 14, + ], + [ + 'Parameter $trueUnion of method NativeTrueType\Truthy::trueUnion() has invalid type NativeTrueType\true.', + 14, + ], + [ + 'Parameter $trueUnion of method NativeTrueType\Truthy::trueUnion() has invalid type NativeTrueType\null.', + 14, + ], + [ + "Method NativeTrueType\Truthy::trueUnionReturn() uses native union types but they're supported only on PHP 8.0 and later.", + 31, + ], + [ + 'Method NativeTrueType\Truthy::trueUnionReturn() has invalid return type NativeTrueType\true.', + 31, + ], + [ + 'Method NativeTrueType\Truthy::trueUnionReturn() has invalid return type NativeTrueType\null.', + 31, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/true-typehint.php'], $errors); + } + + public function testConditionalReturnType(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/conditional-return-type.php'], [ + [ + 'Template type T of method MethodConditionalReturnType\Container::notGet() is not referenced in a parameter.', + 17, + ], + ]); + } + + public function testBug7519(): void + { + $this->analyse([__DIR__ . '/data/bug-7519.php'], []); + } + + public function testTemplateInParamOut(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'Template type T of method ParamOutTemplate\FooBar::uselessLocalTemplate() is not referenced in a parameter.', + 22, + ], + ]); + } + + public function testParamOutClasses(): void + { + $this->analyse([__DIR__ . '/data/param-out-classes.php'], [ + [ + 'Parameter $p of method ParamOutClassesMethods\Bar::doFoo() has invalid type ParamOutClassesMethods\Nonexistent.', + 23, + ], + [ + 'Parameter $q of method ParamOutClassesMethods\Bar::doFoo() has invalid type ParamOutClassesMethods\FooTrait.', + 23, + ], + [ + 'Class ParamOutClassesMethods\Foo referenced with incorrect case: ParamOutClassesMethods\fOO.', + 23, + ], + ]); + } + + public function testParamClosureThisClasses(): void + { + $this->analyse([__DIR__ . '/data/param-closure-this-classes.php'], [ + [ + 'Parameter $a of method ParamClosureThisClasses\Bar::doFoo() has invalid type ParamClosureThisClasses\Nonexistent.', + 24, + ], + [ + 'Parameter $b of method ParamClosureThisClasses\Bar::doFoo() has invalid type ParamClosureThisClasses\FooTrait.', + 25, + ], + [ + 'Class ParamClosureThisClasses\Foo referenced with incorrect case: ParamClosureThisClasses\fOO.', + 26, + ], + ]); + } + + public function testSelfOut(): void + { + $this->analyse([__DIR__ . '/data/self-out.php'], [ + [ + 'Method SelfOutClasses\Foo::doFoo() has invalid @phpstan-self-out type SelfOutClasses\Nonexistent.', + 16, + ], + [ + 'Method SelfOutClasses\Foo::doBar() has invalid @phpstan-self-out type SelfOutClasses\FooTrait.', + 24, + ], + [ + 'Class SelfOutClasses\Foo referenced with incorrect case: SelfOutClasses\fOO.', + 32, + ], + ]); + } + + public function testDeprecatedImplicitlyNullableParameterType(): void + { + if (PHP_VERSION_ID < 80400) { + self::markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/method-implicitly-nullable.php'], [ + [ + 'Deprecated in PHP 8.4: Parameter #3 $c (int) is implicitly nullable via default value null.', + 13, + ], + [ + 'Deprecated in PHP 8.4: Parameter #5 $e (int|string) is implicitly nullable via default value null.', + 15, + ], + [ + 'Deprecated in PHP 8.4: Parameter #7 $g (stdClass) is implicitly nullable via default value null.', + 17, + ], + ]); + } + + public function testBug12501(): void + { + if (PHP_VERSION_ID < 80400) { + self::markTestSkipped('This test needs PHP 8.4.'); + } + $this->analyse([__DIR__ . '/data/bug-12501.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php new file mode 100644 index 0000000000..dddd414b72 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php @@ -0,0 +1,34 @@ + */ +class FinalPrivateMethodRuleConfigPhpTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FinalPrivateMethodRule(); + } + + public function testRulePhpVersions(): void + { + $this->analyse([__DIR__ . '/data/final-private-method-config-phpversion.php'], [ + [ + 'Private method FinalPrivateMethodConfigPhpVersions\PhpVersionViaNEONConfg::foo() cannot be final as it is never overridden by other classes.', + 8, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/final-private-php-version.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php new file mode 100644 index 0000000000..05be45a380 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php @@ -0,0 +1,75 @@ + */ +class FinalPrivateMethodRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FinalPrivateMethodRule(); + } + + public function dataRule(): array + { + return [ + [ + 70400, + [], + ], + [ + 80000, + [ + [ + 'Private method FinalPrivateMethod\Foo::foo() cannot be final as it is never overridden by other classes.', + 8, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param list $errors + */ + public function testRule(int $phpVersion, array $errors): void + { + $testVersion = new PhpVersion($phpVersion); + $runtimeVersion = new PhpVersion(PHP_VERSION_ID); + + if ( + $testVersion->getMajorVersionId() !== $runtimeVersion->getMajorVersionId() + || $testVersion->getMinorVersionId() !== $runtimeVersion->getMinorVersionId() + ) { + $this->markTestSkipped('Test requires PHP version ' . $testVersion->getMajorVersionId() . '.' . $testVersion->getMinorVersionId() . '.*'); + } + + $this->analyse([__DIR__ . '/data/final-private-method.php'], $errors); + } + + public function testRulePhpVersions(): void + { + $this->analyse([__DIR__ . '/data/final-private-method-phpversions.php'], [ + [ + 'Private method FinalPrivateMethodPhpVersions\FooBarPhp8orHigher::foo() cannot be final as it is never overridden by other classes.', + 9, + ], + [ + 'Private method FinalPrivateMethodPhpVersions\FooBarPhp74OrHigher::foo() cannot be final as it is never overridden by other classes.', + 29, + ], + [ + 'Private method FinalPrivateMethodPhpVersions\FooBarBaz::foo() cannot be final as it is never overridden by other classes.', + 39, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php index 7e106f88c5..0f6d5b81f1 100644 --- a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php @@ -48,10 +48,6 @@ public function testBug2573(): void 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.', @@ -62,10 +58,6 @@ public function testNewInInitializers(): void 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.', @@ -75,7 +67,20 @@ public function testDefaultValueForPromotedProperty(): void 'Default value of the parameter #2 $foo (string) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', 10, ], + [ + 'Default value of the parameter #4 $intProp (null) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', + 12, + ], ]); } + public function testBug10956(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-10956.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index 2aec99cf6b..b2a85a50c3 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -12,7 +13,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -20,8 +20,6 @@ class MethodAttributesRuleTest extends RuleTestCase { - private int $phpVersion; - protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); @@ -29,9 +27,8 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -39,19 +36,19 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $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.', @@ -62,11 +59,6 @@ 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 index 87bb854dfb..8b558e4512 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -19,7 +19,7 @@ class MethodCallableRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true); return new MethodCallableRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), @@ -32,10 +32,6 @@ 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.', diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index b48c1e4b53..2d7d7f1a5e 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -19,10 +20,18 @@ class MethodSignatureRuleTest extends RuleTestCase protected function getRule(): Rule { + $phpVersion = new PhpVersion(PHP_VERSION_ID); + + $phpClassReflectionExtension = self::getContainer()->getByType(PhpClassReflectionExtension::class); + return new OverridingMethodRule( - new PhpVersion(PHP_VERSION_ID), - new MethodSignatureRule($this->reportMaybes, $this->reportStatic), + $phpVersion, + new MethodSignatureRule($phpClassReflectionExtension, $this->reportMaybes, $this->reportStatic), true, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + $phpClassReflectionExtension, + false, ); } @@ -211,24 +220,21 @@ public function testBug3997(): void $this->analyse([__DIR__ . '/data/bug-3997.php'], [ [ '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, + 35, ], [ '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, + 49, ], [ '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, + 63, ], ]); } public function testBug4003(): void { - if (PHP_VERSION_ID < 70200 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.2 or later.'); - } $this->reportMaybes = true; $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4003.php'], [ @@ -237,7 +243,7 @@ public function testBug4003(): void 15, ], [ - PHP_VERSION_ID < 70200 ? 'Parameter #1 $test (mixed) of method Bug4003\Ipsum::doFoo() does not match parameter #1 $test (int) of method Bug4003\Lorem::doFoo().' : 'Parameter #1 $test (string) of method Bug4003\Ipsum::doFoo() should be compatible with parameter $test (int) of method Bug4003\Lorem::doFoo()', + 'Parameter #1 $test (string) of method Bug4003\Ipsum::doFoo() should be compatible with parameter $test (int) of method Bug4003\Lorem::doFoo()', 38, ], ]); @@ -245,9 +251,6 @@ public function testBug4003(): void public function testBug4017(): void { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->reportMaybes = true; $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4017.php'], []); @@ -316,7 +319,7 @@ public function testBug4707(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4707.php'], [ [ - 'Return type (array) of method Bug4707\Block2::getChildren() should be compatible with return type (array>) of method Bug4707\ParentNodeInterface::getChildren()', + 'Return type (list) of method Bug4707\Block2::getChildren() should be compatible with return type (list>) of method Bug4707\ParentNodeInterface::getChildren()', 38, ], ]); @@ -328,7 +331,7 @@ public function testBug4707Covariant(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4707-covariant.php'], [ [ - 'Return type (array) of method Bug4707Covariant\Block2::getChildren() should be covariant with return type (array>) of method Bug4707Covariant\ParentNodeInterface::getChildren()', + 'Return type (list) of method Bug4707Covariant\Block2::getChildren() should be covariant with return type (list>) of method Bug4707Covariant\ParentNodeInterface::getChildren()', 38, ], ]); @@ -350,10 +353,6 @@ public function testBug4729(): void 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'], []); @@ -361,13 +360,225 @@ public function testBug4854(): void 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'], []); + } + + public function testOverridenMethodWithConditionalReturnType(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/overriden-method-with-conditional-return-type.php'], [ + [ + 'Return type (($p is int ? stdClass : string)) of method OverridenMethodWithConditionalReturnType\Bar2::doFoo() should be compatible with return type (($p is int ? int : string)) of method OverridenMethodWithConditionalReturnType\Foo::doFoo()', + 37, + ], + ]); + } + + public function testBug7652(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); } $this->reportMaybes = true; $this->reportStatic = true; - $this->analyse([__DIR__ . '/data/memcache-pool-get.php'], []); + $this->analyse([__DIR__ . '/data/bug-7652.php'], [ + [ + 'Return type mixed of method Bug7652\Options::offsetGet() is not covariant with tentative return type mixed of method ArrayAccess,value-of>::offsetGet().', + 23, + 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', + ], + [ + 'Parameter #1 $offset (TOffset of key-of) of method Bug7652\Options::offsetSet() should be contravariant with parameter $offset (key-of|null) of method ArrayAccess,value-of>::offsetSet()', + 30, + ], + ]); + } + + public function testBug7103(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-7103.php'], []); + } + + public function testListReturnTypeCovariance(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/list-return-type-covariance.php'], [ + [ + 'Return type (array) of method ListReturnTypeCovariance\ListChild::returnsList() should be covariant with return type (list) of method ListReturnTypeCovariance\ListParent::returnsList()', + 17, + ], + ]); + } + + public function testRuleError(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/rule-error-signature.php'], [ + [ + 'Return type (array) of method RuleErrorSignature\Baz::processNode() should be covariant with return type (list) of method PHPStan\Rules\Rule::processNode()', + 64, + 'Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Return type (array) of method RuleErrorSignature\Lorem::processNode() should be compatible with return type (list) of method PHPStan\Rules\Rule::processNode()', + 85, + 'Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Return type (array) of method RuleErrorSignature\Ipsum::processNode() should be covariant with return type (list) of method PHPStan\Rules\Rule::processNode()', + 106, + 'Errors are missing identifiers. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Return type (array) of method RuleErrorSignature\Dolor::processNode() should be covariant with return type (list) of method PHPStan\Rules\Rule::processNode()', + 127, + 'Return type must be a list. See: https://phpstan.org/blog/using-rule-error-builder', + ], + ]); + } + + public function testBug9905(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-9905.php'], []); + } + + public function testTraits(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/overriding-trait-methods-phpdoc.php'], [ + [ + 'Parameter #1 $i (non-empty-string) of method OverridingTraitMethodsPhpDoc\Bar::doBar() should be contravariant with parameter $i (string) of method OverridingTraitMethodsPhpDoc\Foo::doBar()', + 33, + ], + ]); + } + + public function testBug10166(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-10166.php'], [ + [ + 'Return type Bug10166\ReturnTypeClass2|null of method Bug10166\ReturnTypeClass2::createSelf() is not covariant with return type Bug10166\ReturnTypeClass2 of method Bug10166\ReturnTypeTrait::createSelf().', + 23, + ], + ]); + } + + public function testBug10184(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-10184.php'], []); + } + + public function testBug10208(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-10208.php'], []); + } + + public function testBug6462(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-6462.php'], []); + } + + public function testBug4396(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-4396.php'], []); + } + + public function testBug3580(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-3580.php'], []); + } + + public function testOverridenAbstractTraitMethodPhpDoc(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/overriden-abstract-trait-method-phpdoc.php'], []); + } + + public function testGenericStaticType(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/method-signature-generic-static-type.php'], []); + } + + public function testBug10240(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-10240.php'], []); + } + + public function testBug10488(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-10488.php'], []); + } + + public function testBug12073(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-12073.php'], []); } } diff --git a/tests/PHPStan/Rules/Methods/MethodVisibilityInInterfaceRuleTest.php b/tests/PHPStan/Rules/Methods/MethodVisibilityInInterfaceRuleTest.php new file mode 100644 index 0000000000..290e1c3364 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MethodVisibilityInInterfaceRuleTest.php @@ -0,0 +1,31 @@ + */ +class MethodVisibilityInInterfaceRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MethodVisibilityInInterfaceRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/visibility-in-interace.php'], [ + [ + 'Method VisibilityInInterface\FooInterface::sayPrivate() cannot use non-public visibility in interface.', + 7, + ], + [ + 'Method VisibilityInInterface\FooInterface::sayProtected() cannot use non-public visibility in interface.', + 8, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php new file mode 100644 index 0000000000..989d8e0a52 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php @@ -0,0 +1,41 @@ + + */ +class MissingMagicSerializationMethodsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingMagicSerializationMethodsRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/missing-serialization.php'], [ + [ + 'Non-abstract class MissingMagicSerializationMethods\myObj implements the Serializable interface, but does not implement __serialize().', + 14, + 'See https://wiki.php.net/rfc/phase_out_serializable', + ], + [ + 'Non-abstract class MissingMagicSerializationMethods\myObj implements the Serializable interface, but does not implement __unserialize().', + 14, + 'See https://wiki.php.net/rfc/phase_out_serializable', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php index 8ed66a80ae..babaadaae2 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php @@ -19,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/missing-method-impl.php'], [ [ 'Non-abstract class MissingMethodImpl\Baz contains abstract method doBaz() from class MissingMethodImpl\Baz.', @@ -33,7 +29,7 @@ public function testRule(): void 24, ], [ - 'Non-abstract class class@anonymous/tests/PHPStan/Rules/Methods/data/missing-method-impl.php:41 contains abstract method doFoo() from interface MissingMethodImpl\Foo.', + 'Non-abstract class MissingMethodImpl\Foo@anonymous/tests/PHPStan/Rules/Methods/data/missing-method-impl.php:41 contains abstract method doFoo() from interface MissingMethodImpl\Foo.', 41, ], ]); @@ -51,8 +47,8 @@ public function testBug3958(): void public function testEnums(): void { - if (!self::$useStaticReflectionProvider || PHP_VERSION_ID < 80100) { - $this->markTestSkipped('This test needs static reflection and PHP 8.1'); + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); } $this->analyse([__DIR__ . '/data/missing-method-impl-enum.php'], [ @@ -63,4 +59,9 @@ public function testEnums(): void ]); } + public function testBug11665(): void + { + $this->analyse([__DIR__ . '/data/bug-11665.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index 28be69c2fa..fce941c3ce 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,8 +14,7 @@ class MissingMethodParameterTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingMethodParameterTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); + return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -45,32 +43,49 @@ public function testRule(): void [ 'Method MissingMethodParameterTypehint\Foo::unionTypeWithUnknownArrayValueTypehint() has parameter $a with no value type specified in iterable type array.', 58, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Method MissingMethodParameterTypehint\Bar::acceptsGenericInterface() has parameter $i with generic interface MissingMethodParameterTypehint\GenericInterface but does not specify its types: T, U', 91, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\Bar::acceptsGenericClass() has parameter $c with generic class MissingMethodParameterTypehint\GenericClass but does not specify its types: A, B', 101, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\CollectionIterableAndGeneric::acceptsCollection() has parameter $collection with generic interface DoctrineIntersectionTypeIsSupertypeOf\Collection but does not specify its types: TKey, T', 111, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\CollectionIterableAndGeneric::acceptsCollection2() has parameter $collection with generic interface DoctrineIntersectionTypeIsSupertypeOf\Collection but does not specify its types: TKey, T', 119, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\CallableSignature::doFoo() has parameter $cb with no signature specified for callable.', 180, ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 207, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 215, + ], + [ + 'Method MissingMethodParameterTypehint\MissingParamClosureThisType::generics() has @param-closure-this PHPDoc tag for parameter $cb with generic class ReflectionClass but does not specify its types: T', + 226, + ], + [ + 'Method MissingMethodParameterTypehint\MissingPureClosureSignatureType::doFoo() has parameter $cb with no signature specified for Closure.', + 238, + ], + [ + 'Method MissingMethodParameterTypehint\Baz::acceptsGenericWithSomeDefaults() has parameter $c with generic class MissingMethodParameterTypehint\GenericClassWithSomeDefaults but does not specify its types: T, U (1-2 required)', + 270, + ], ]; $this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors); @@ -78,19 +93,16 @@ public function testRule(): void public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } $this->analyse([__DIR__ . '/data/missing-typehint-promoted-properties.php'], [ [ 'Method MissingTypehintPromotedProperties\Foo::__construct() has parameter $foo with no value type specified in iterable type array.', 8, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Method MissingTypehintPromotedProperties\Bar::__construct() has parameter $foo with no value type specified in iterable type array.', 21, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], ]); } @@ -101,12 +113,11 @@ public function testDeepInspectTypes(): void [ 'Method DeepInspectTypes\Foo::doFoo() has parameter $foo with no value type specified in iterable type iterable.', 11, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ '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, ], ]); } @@ -126,4 +137,15 @@ public function testFilterIteratorChildClass(): void $this->analyse([__DIR__ . '/data/filter-iterator-child-class.php'], []); } + public function testBug7662(): void + { + $this->analyse([__DIR__ . '/data/bug-7662.php'], [ + [ + 'Method Bug7662\Foo::__construct() has parameter $bar with no value type specified in iterable type array.', + 6, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php index 21e95e34e3..572b38c33a 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -14,8 +15,7 @@ class MissingMethodReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingMethodReturnTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); + return new MissingMethodReturnTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -40,22 +40,24 @@ public function testRule(): void [ 'Method MissingMethodReturnTypehint\Foo::unionTypeWithUnknownArrayValueTypehint() return type has no value type specified in iterable type array.', 46, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Method MissingMethodReturnTypehint\Bar::returnsGenericInterface() return type with generic interface MissingMethodReturnTypehint\GenericInterface does not specify its types: T, U', 79, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodReturnTypehint\Bar::returnsGenericClass() return type with generic class MissingMethodReturnTypehint\GenericClass does not specify its types: A, B', 89, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodReturnTypehint\CallableSignature::doFoo() return type has no signature specified for callable.', 99, ], + [ + 'Method MissingMethodReturnTypehint\Baz::returnsGenericWithSomeDefaults() return type with generic class MissingMethodReturnTypehint\GenericClassWithSomeDefaults does not specify its types: T, U (1-2 required)', + 142, + ], ]); } @@ -66,7 +68,7 @@ public function testIndirectInheritanceBug2740(): void public function testArrayTypehintWithoutNullInPhpDoc(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/array-typehint-without-null-in-phpdoc.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/array-typehint-without-null-in-phpdoc.php'], []); } public function testBug4415(): void @@ -84,4 +86,39 @@ public function testBug5436(): void $this->analyse([__DIR__ . '/data/bug-5436.php'], []); } + public function testBug4758(): void + { + $this->analyse([__DIR__ . '/data/bug-4758.php'], []); + } + + public function testBug9571(): void + { + $this->analyse([__DIR__ . '/data/bug-9571.php'], []); + } + + public function testBug9571PhpDocs(): void + { + $this->analyse([__DIR__ . '/data/bug-9571-phpdocs.php'], []); + } + + public function testGenericStatic(): void + { + $this->analyse([__DIR__ . '/data/missing-return-type-generic-static.php'], [ + [ + 'Method MissingReturnTypeGenericStatic\Foo::doFoo() return type has no value type specified in iterable type array.', + 12, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + ]); + } + + public function testBug9657(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9657.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodSelfOutTypeRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodSelfOutTypeRuleTest.php new file mode 100644 index 0000000000..cc74d519e9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MissingMethodSelfOutTypeRuleTest.php @@ -0,0 +1,39 @@ + + */ +class MissingMethodSelfOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new MissingMethodSelfOutTypeRule(new MissingTypehintCheck(true, [])); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/missing-method-self-out-type.php'], [ + [ + 'Method MissingMethodSelfOutType\Foo::doFoo() has PHPDoc tag @phpstan-self-out with no value type specified in iterable type array.', + 14, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Method MissingMethodSelfOutType\Foo::doFoo2() has PHPDoc tag @phpstan-self-out with generic class MissingMethodSelfOutType\Foo but does not specify its types: T', + 22, + ], + [ + 'Method MissingMethodSelfOutType\Foo::doFoo3() has PHPDoc tag @phpstan-self-out with no signature specified for callable.', + 30, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php index ac0b89323f..5c308ea5ad 100644 --- a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php @@ -19,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-method-call-rule.php'], [ [ 'Using nullsafe method call on non-nullable type Exception. Use -> instead.', @@ -31,4 +27,47 @@ public function testRule(): void ]); } + public function testNullsafeVsScalar(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/nullsafe-vs-scalar.php'], []); + } + + public function testBug8664(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8664.php'], []); + } + + public function testBug9293(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9293.php'], []); + } + + public function testBug6922b(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-6922b.php'], []); + } + + public function testBug8523(): void + { + $this->analyse([__DIR__ . '/data/bug-8523.php'], []); + } + + public function testBug8523b(): void + { + $this->analyse([__DIR__ . '/data/bug-8523b.php'], []); + } + + public function testBug8523c(): void + { + $this->analyse([__DIR__ . '/data/bug-8523c.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index c926d2f7b4..c63d2ea3eb 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -3,10 +3,10 @@ namespace PHPStan\Rules\Methods; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function array_filter; -use function array_merge; use function array_values; use const PHP_VERSION_ID; @@ -18,12 +18,22 @@ class OverridingMethodRuleTest extends RuleTestCase private int $phpVersionId; + private bool $checkMissingOverrideMethodAttribute = false; + protected function getRule(): Rule { + $phpVersion = new PhpVersion($this->phpVersionId); + + $phpClassReflectionExtension = self::getContainer()->getByType(PhpClassReflectionExtension::class); + return new OverridingMethodRule( - new PhpVersion($this->phpVersionId), - new MethodSignatureRule(true, true), + $phpVersion, + new MethodSignatureRule($phpClassReflectionExtension, true, true), false, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + $phpClassReflectionExtension, + $this->checkMissingOverrideMethodAttribute, ); } @@ -48,10 +58,6 @@ public function dataOverridingFinalMethod(): array */ public function testOverridingFinalMethod(int $phpVersion, string $contravariantMessage): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $errors = [ [ 'Method OverridingFinalMethod\Bar::doFoo() overrides final method OverridingFinalMethod\Foo::doFoo().', @@ -86,7 +92,7 @@ public function testOverridingFinalMethod(int $phpVersion, string $contravariant 115, ], [ - 'Parameter #1 $size (int) of method OverridingFinalMethod\FixedArray::setSize() is not ' . $contravariantMessage . ' with parameter #1 $size (mixed) of method SplFixedArray::setSize().', + 'Parameter #1 $size (int) of method OverridingFinalMethod\FixedArray::setSize() is not ' . $contravariantMessage . ' with parameter #1 $size (mixed) of method SplFixedArray::setSize().', 125, ], [ @@ -122,7 +128,11 @@ public function testOverridingFinalMethod(int $phpVersion, string $contravariant 280, ], [ - 'Parameter #1 $index (int) of method OverridingFinalMethod\FixedArrayOffsetExists::offsetExists() is not ' . $contravariantMessage . ' with parameter #1 $offset (mixed) of method ArrayAccess::offsetExists().', + 'Method OverridingFinalMethod\ExtendsFinalWithAnnotation::doFoo() overrides @final method OverridingFinalMethod\FinalWithAnnotation::doFoo().', + 303, + ], + [ + 'Parameter #1 $index (int) of method OverridingFinalMethod\FixedArrayOffsetExists::offsetExists() is not ' . $contravariantMessage . ' with parameter #1 $index (mixed) of method SplFixedArray::offsetExists().', 313, ], ]; @@ -219,7 +229,7 @@ public function dataParameterContravariance(): array /** * @dataProvider dataParameterContravariance - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function testParameterContravariance( string $file, @@ -227,8 +237,8 @@ public function testParameterContravariance( array $expectedErrors, ): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); } $this->phpVersionId = $phpVersion; @@ -281,17 +291,13 @@ public function dataReturnTypeCovariance(): array /** * @dataProvider dataReturnTypeCovariance - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function testReturnTypeCovariance( int $phpVersion, array $expectedErrors, ): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->phpVersionId = $phpVersion; $this->analyse([__DIR__ . '/data/return-type-covariance.php'], $expectedErrors); } @@ -301,10 +307,6 @@ public function testReturnTypeCovariance( */ public function testParle(int $phpVersion, string $contravariantMessage, string $covariantMessage): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->phpVersionId = $phpVersion; $this->analyse([__DIR__ . '/data/parle.php'], [ [ @@ -347,29 +349,14 @@ public function testBug3478(): void public function testBug3629(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test require static reflection.'); - } $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/bug-3629.php'], []); } public function testVariadics(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->phpVersionId = PHP_VERSION_ID; - $errors = []; - if (PHP_VERSION_ID < 70200) { - $errors[] = [ - 'Parameter #2 $lang (mixed) of method OverridingVariadics\Translator::translate() does not match parameter #2 $parameters (string) of method OverridingVariadics\ITranslator::translate().', - 24, - ]; - } - - $errors = array_merge($errors, [ + $errors = [ [ 'Parameter #2 $lang of method OverridingVariadics\OtherTranslator::translate() is not optional.', 34, @@ -386,14 +373,7 @@ public function testVariadics(): void 'Parameter #2 $lang of method OverridingVariadics\YetAnotherTranslator::translate() is not variadic.', 54, ], - ]); - - if (PHP_VERSION_ID < 70200) { - $errors[] = [ - 'Parameter #2 $lang (mixed) of method OverridingVariadics\YetAnotherTranslator::translate() does not match parameter #2 $parameters (string) of method OverridingVariadics\ITranslator::translate().', - 54, - ]; - } + ]; $this->analyse([__DIR__ . '/data/overriding-variadics.php'], $errors); } @@ -456,13 +436,10 @@ public function dataLessOverridenParametersWithVariadic(): array /** * @dataProvider dataLessOverridenParametersWithVariadic - * @param mixed[] $errors + * @param list $errors */ public function testLessOverridenParametersWithVariadic(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/less-parameters-variadics.php'], $errors); } @@ -488,13 +465,10 @@ public function dataParameterTypeWidening(): array /** * @dataProvider dataParameterTypeWidening - * @param mixed[] $errors + * @param list $errors */ public function testParameterTypeWidening(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/parameter-type-widening.php'], $errors); } @@ -514,15 +488,40 @@ public function dataTentativeReturnTypes(): array 80100, [ [ - 'Return type mixed of method TentativeReturnTypes\Foo::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', + 'Return type mixed of method TentativeReturnTypes\Foo::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', 8, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], [ - 'Return type string of method TentativeReturnTypes\Lorem::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', + 'Return type string of method TentativeReturnTypes\Lorem::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', 40, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], + [ + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::current() is not covariant with tentative return type mixed of method Iterator::current().', + 75, + 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', + ], + [ + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::next() is not covariant with tentative return type void of method Iterator::next().', + 79, + 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', + ], + [ + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::key() is not covariant with tentative return type mixed of method Iterator::key().', + 83, + 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', + ], + [ + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::valid() is not covariant with tentative return type bool of method Iterator::valid().', + 87, + 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', + ], + [ + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::rewind() is not covariant with tentative return type void of method Iterator::rewind().', + 91, + 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', + ], ], ], ]; @@ -530,17 +529,16 @@ public function dataTentativeReturnTypes(): array /** * @dataProvider dataTentativeReturnTypes - * @param mixed[] $errors + * @param list $errors */ public function testTentativeReturnTypes(int $phpVersionId, array $errors): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - if (PHP_VERSION_ID < 80100) { $errors = []; } + if ($phpVersionId > PHP_VERSION_ID) { + $this->markTestSkipped(); + } $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/tentative-return-types.php'], $errors); @@ -558,4 +556,282 @@ public function testBug6264(): void $this->analyse([__DIR__ . '/data/bug-6264.php'], []); } + public function testBug7717(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-7717.php'], []); + } + + public function testBug6104(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-6104.php'], []); + } + + public function testBug9391(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-9391.php'], []); + } + + public function testBugWithIndirectPrototype(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/overriding-indirect-prototype.php'], [ + [ + 'Return type mixed of method OverridingIndirectPrototype\Baz::doFoo() is not covariant with return type string of method OverridingIndirectPrototype\Bar::doFoo().', + 28, + ], + ]); + } + + public function testBug10043(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-10043.php'], [ + [ + 'Method Bug10043\C::foo() overrides final method Bug10043\B::foo().', + 17, + ], + ]); + } + + public function testBug7859(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-7859.php'], [ + [ + 'Method Bug7859\ExtendingClassImplementingSomeInterface::getList() overrides method Bug7859\ImplementingSomeInterface::getList() but misses parameter #2 $b.', + 21, + ], + [ + 'Method Bug7859\ExtendingClassNotImplementingSomeInterface::getList() overrides method Bug7859\NotImplementingSomeInterface::getList() but misses parameter #2 $b.', + 37, + ], + ]); + } + + public function testBug8081(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-8081.php'], [ + [ + 'Return type mixed of method Bug8081\three::foo() is not covariant with return type array of method Bug8081\two::foo().', + 21, + ], + ]); + } + + public function testBug8500(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-8500.php'], [ + [ + 'Return type mixed of method Bug8500\DBOHB::test() is not covariant with return type Bug8500\DBOA of method Bug8500\DBOHA::test().', + 30, + ], + ]); + } + + public function testBug9014(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-9014.php'], [ + [ + 'Method Bug9014\Bar::test() overrides method Bug9014\Foo::test() but misses parameter #2 $test.', + 16, + ], + [ + 'Return type mixed of method Bug9014\extended::renderForUser() is not covariant with return type string of method Bug9014\middle::renderForUser().', + 42, + ], + ]); + } + + public function testBug9135(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-9135.php'], [ + [ + 'Method Bug9135\Sub::sayHello() overrides @final method Bug9135\HelloWorld::sayHello().', + 15, + ], + ]); + } + + public function testBug10101(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-10101.php'], [ + [ + 'Return type mixed of method Bug10101\B::next() is not covariant with return type void of method Bug10101\A::next().', + 10, + ], + ]); + } + + public function testBug9615(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $tipText = 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.'; + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-9615.php'], [ + [ + 'Return type mixed of method Bug9615\ExpectComplaintsHere::accept() is not covariant with tentative return type bool of method FilterIterator>::accept().', + 19, + $tipText, + ], + [ + 'Return type mixed of method Bug9615\ExpectComplaintsHere::getChildren() is not covariant with tentative return type RecursiveIterator|null of method RecursiveIterator::getChildren().', + 20, + $tipText, + ], + ]); + } + + public function testBug10149(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $errors = []; + if (PHP_VERSION_ID >= 80300) { + $errors = [ + [ + 'Method Bug10149\StdSat::__get() has #[\Override] attribute but does not override any method.', + 10, + ], + ]; + } + $this->analyse([__DIR__ . '/data/bug-10149.php'], $errors); + } + + public function testTraits(): void + { + $errors = []; + if (PHP_VERSION_ID >= 80000) { + $errors = [ + [ + 'Parameter #1 $i (int) of method OverridingTraitMethods\Bar::doBar() is not contravariant with parameter #1 $i (string) of method OverridingTraitMethods\Foo::doBar().', + 27, + ], + [ + 'Parameter #1 $i (int) of method OverridingTraitMethods\Baz::doBar() is not contravariant with parameter #1 $i (string) of method OverridingTraitMethods\FooPrivate::doBar().', + 45, + ], + [ + 'Static method OverridingTraitMethods\Ipsum::doBar() overrides non-static method OverridingTraitMethods\Foo::doBar().', + 65, + ], + [ + 'Non-static method OverridingTraitMethods\Dolor::doBar() overrides static method OverridingTraitMethods\FooStatic::doBar().', + 80, + ], + ]; + } + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/overriding-trait-methods.php'], $errors); + } + + public function testOverrideAttribute(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/override-attribute.php'], [ + [ + 'Method OverrideAttribute\Bar::test2() has #[\Override] attribute but does not override any method.', + 24, + ], + [ + 'Method OverrideAttribute\ChildOfParentWithConstructor::__construct() has #[\Override] attribute but does not override any method.', + 42, + ], + ]); + } + + public function dataCheckMissingOverrideAttribute(): iterable + { + yield [false, 80000, []]; + yield [true, 80000, []]; + yield [false, 80300, []]; + yield [true, 80300, [ + [ + 'Method CheckMissingOverrideAttr\Bar::doFoo() overrides method CheckMissingOverrideAttr\Foo::doFoo() but is missing the #[\Override] attribute.', + 18, + ], + [ + 'Method CheckMissingOverrideAttr\ChildOfParentWithAbstractConstructor::__construct() overrides method CheckMissingOverrideAttr\ParentWithAbstractConstructor::__construct() but is missing the #[\Override] attribute.', + 49, + ], + ]]; + } + + /** + * @dataProvider dataCheckMissingOverrideAttribute + * @param list $errors + */ + public function testCheckMissingOverrideAttribute(bool $checkMissingOverrideMethodAttribute, int $phpVersionId, array $errors): void + { + $this->checkMissingOverrideMethodAttribute = $checkMissingOverrideMethodAttribute; + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/check-missing-override-attr.php'], $errors); + } + + public function testBug10153(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $errors = []; + if (PHP_VERSION_ID >= 80000) { + $errors = [ + [ + 'Return type Bug10153\MyClass2|null of method Bug10153\MyClass2::drc() is not covariant with return type Bug10153\MyClass2 of method Bug10153\MyTrait::drc().', + 24, + ], + ]; + } + $this->analyse([__DIR__ . '/data/bug-10153.php'], $errors); + } + + public function testBug12471(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->checkMissingOverrideMethodAttribute = true; + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-12471.php'], []); + } + + public function testBug10165(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-10165.php'], []); + } + + public function testBug9524(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-9524.php'], []); + } + + public function testSimpleXmlElementChildClass(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/simple-xml-element-child.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 7939a7c5a5..548589722e 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -18,9 +18,11 @@ class ReturnTypeRuleTest extends RuleTestCase private bool $checkUnionTypes = true; + private bool $checkBenevolentUnionTypes = false; + protected function getRule(): Rule { - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, $this->checkExplicitMixed))); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, $this->checkExplicitMixed, false, $this->checkBenevolentUnionTypes, true))); } public function testReturnTypeRule(): void @@ -235,54 +237,77 @@ public function testReturnTypeRule(): void 759, ], [ - 'Method ReturnTypes\ArrayFillKeysIssue::getIPs2() should return array> but returns array>.', + 'Method ReturnTypes\ArrayFillKeysIssue::getIPs2() should return array> but returns array>.', 817, ], [ 'Method ReturnTypes\AssertThisInstanceOf::doBar() should return $this(ReturnTypes\AssertThisInstanceOf) but returns ReturnTypes\AssertThisInstanceOf&ReturnTypes\FooInterface.', - 840, + 839, ], [ - 'Method ReturnTypes\NestedArrayCheck::doFoo() should return array but returns array>.', - 860, + 'Method ReturnTypes\NestedArrayCheck::doFoo() should return array but returns array>.', + 859, ], [ 'Method ReturnTypes\NestedArrayCheck::doBar() should return array but returns array>.', - 875, + 874, ], [ 'Method ReturnTypes\Foo2::returnIntFromParent() should return int but returns string.', - 950, + 949, ], [ 'Method ReturnTypes\Foo2::returnIntFromParent() should return int but returns ReturnTypes\integer.', - 953, + 952, ], [ 'Method ReturnTypes\VariableOverwrittenInForeach::doFoo() should return int but returns int|string.', - 1011, + 1010, ], [ 'Method ReturnTypes\VariableOverwrittenInForeach::doBar() should return int but returns int|string.', - 1026, + 1025, ], [ 'Method ReturnTypes\ReturnStaticGeneric::instanceReturnsStatic() should return static(ReturnTypes\ReturnStaticGeneric) but returns ReturnTypes\ReturnStaticGeneric.', - 1066, + 1065, ], [ 'Method ReturnTypes\NeverReturn::doFoo() should never return but return statement found.', - 1241, + 1240, ], [ 'Method ReturnTypes\NeverReturn::doBaz3() should never return but return statement found.', - 1254, + 1253, ], ]); } + public function testMisleadingMixedType(): void + { + if (PHP_VERSION_ID >= 80000) { + $errors = []; + } else { + $errors = [ + [ + 'Method MethodMisleadingMixedReturn\Foo::misleadingMixedReturnType() should return MethodMisleadingMixedReturn\mixed but returns int.', + 11, + ], + [ + 'Method MethodMisleadingMixedReturn\Foo::misleadingMixedReturnType() should return MethodMisleadingMixedReturn\mixed but returns true.', + 14, + ], + ]; + } + $this->analyse([__DIR__ . '/data/method-misleading-mixed-return.php'], $errors); + } + public function testMisleadingTypehintsInClassWithoutNamespace(): void { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/misleadingTypehints.php'], [ [ 'Method FooWithoutNamespace::misleadingBoolReturnType() should return boolean but returns true.', @@ -362,9 +387,6 @@ public function testMergeInheritedPhpDocs(): void public function testReturnTypeRulePhp70(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->analyse([__DIR__ . '/data/returnTypes-7.0.php'], [ [ 'Method ReturnTypes\FooPhp70::returnInteger() should return int but empty return statement found.', @@ -415,6 +437,7 @@ public function testBug3117(): void [ 'Method Bug3117\SimpleTemporal::adjustInto() should return T of Bug3117\Temporal but returns $this(Bug3117\SimpleTemporal).', 35, + 'Type $this(Bug3117\SimpleTemporal) is not always the same as T. It breaks the contract for some argument types, typically subtypes.', ], ]); } @@ -424,6 +447,11 @@ public function testBug3034(): void $this->analyse([__DIR__ . '/data/bug-3034.php'], []); } + public function testBug3951(): void + { + $this->analyse([__DIR__ . '/data/bug-3951.php'], []); + } + public function testInferArrayKey(): void { $this->analyse([__DIR__ . '/data/infer-array-key.php'], []); @@ -432,17 +460,25 @@ public function testInferArrayKey(): void public function testBug4590(): void { $this->analyse([__DIR__ . '/data/bug-4590.php'], [ + [ + 'Method Bug4590\OkResponse::testGenericStatic() should return static(Bug4590\OkResponse>) but returns static(Bug4590\OkResponse).', + 36, + 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], [ 'Method Bug4590\\Controller::test1() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', - 39, + 47, + 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], [ 'Method Bug4590\\Controller::test2() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', - 47, + 55, + 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], [ 'Method Bug4590\\Controller::test3() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', - 55, + 63, + 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], ]); } @@ -494,15 +530,17 @@ public function testBug3118(): void public function testBug4795(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } $this->analyse([__DIR__ . '/data/bug-4795.php'], []); } public function testBug4803(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4803.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4803.php'], []); + } + + public function testBug7020(): void + { + $this->analyse([__DIR__ . '/data/bug-7020.php'], []); } public function testBug2573(): void @@ -512,9 +550,10 @@ public function testBug2573(): void public function testBug4603(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); } + $this->analyse([__DIR__ . '/data/bug-4603.php'], []); } @@ -554,7 +593,7 @@ public function dataBug5218(): array /** * @dataProvider dataBug5218 - * @param mixed[] $errors + * @param list $errors */ public function testBug5218(bool $checkExplicitMixed, array $errors): void { @@ -569,10 +608,6 @@ public function testBug5979(): void 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'], []); } @@ -629,31 +664,18 @@ public function testBug6266(): void 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'], [ [ @@ -665,42 +687,573 @@ public function testBug5065ExplicitMixed(): void 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->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-6635.php'], []); + } + + public function testBug6552(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6552.php'], []); + } + + public function testConditionalTypes(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/return-rule-conditional-types.php'], [ + [ + 'Method ReturnRuleConditionalTypes\Foo::doFoo() should return int|string but returns stdClass.', + 15, + ], + [ + 'Method ReturnRuleConditionalTypes\Bar::doFoo() should return int|string but returns stdClass.', + 29, + ], + [ + 'Method ReturnRuleConditionalTypes\Bar2::doFoo() should return int|string but returns stdClass.', + 43, + ], + ]); + } + + public function testBug7265(): void + { + $this->analyse([__DIR__ . '/data/bug-7265.php'], []); + } + + public function testBug7460(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7460.php'], []); + } + + public function testBug4117(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-4117.php'], []); + } + + public function testBug5232(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5232.php'], []); + } + + public function testBug7511(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7511.php'], []); + } + + public function testTaggedUnions(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/tagged-unions.php'], [ + [ + 'Method TaggedUnionReturnCheck\HelloWorld::sayHello() should return array{updated: false, id: null}|array{updated: true, id: int} but returns array{updated: false, id: 5}.', + 12, + "• Type #1 from the union: Offset 'id' (null) does not accept type int. +• Type #2 from the union: Offset 'updated' (true) does not accept type false.", + ], + ]); + } + + public function testBug7904(): void + { + if (PHP_VERSION_ID < 80000) { $this->markTestSkipped('Test requires PHP 8.0.'); } + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7904.php'], []); + } + + public function testBug7996(): void + { $this->checkExplicitMixed = false; - $this->analyse([__DIR__ . '/data/bug-6635.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7996.php'], []); + } + + public function testBug6358(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6358.php'], [ + [ + 'Method Bug6358\HelloWorld::sayHello() should return list but returns array{1: stdClass}.', + 14, + 'array{1: stdClass} is not a list.', + ], + ]); + } + + public function testBug8071(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8071.php'], [ + [ + // there should be no errors + 'Method Bug8071\Inheritance::inherit() should return array but returns array.', + 17, + 'Type string is not always the same as TValues. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug3499(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3499.php'], []); + } + + public function testBug8174(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8174.php'], [ + [ + "Method Bug8174\HelloWorld::filterList() should return list but returns array, '23423'>.", + 21, + "array, '23423'> might not be a list.", + ], + ]); + } + + public function testBug7519(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7519.php'], []); + } + + public function testBug8223(): void + { + $this->checkBenevolentUnionTypes = true; + + $errors = []; + if (PHP_VERSION_ID < 80300) { + $errors = [ + [ + 'Method Bug8223\HelloWorld::sayHello() should return DateTimeImmutable but returns (DateTimeImmutable|false).', + 11, + ], + [ + 'Method Bug8223\HelloWorld::sayHello2() should return array but returns array.', + 21, + ], + ]; + } + $this->analyse([__DIR__ . '/data/bug-8223.php'], $errors); + } + + public function testBug8146bErrors(): void + { + $this->checkBenevolentUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-8146b-errors.php'], [ + [ + "Method Bug8146bError\LocationFixtures::getData() should return array, coordinates: array{lat: float, lng: float}}>> but returns array{Bács-Kiskun: array{Ágasegyháza: array{constituencies: array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}, coordinates: array{lat: 46.8386043, lng: 19.4502899}}, Akasztó: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.6898175, lng: 19.205086}}, Apostag: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.8812652, lng: 18.9648478}}, Bácsalmás: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1250396, lng: 19.3357509}}, Bácsbokod: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.1234737, lng: 19.155708}}, Bácsborsód: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.0989373, lng: 19.1566725}}, Bácsszentgyörgy: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 45.9746039, lng: 19.0398066}}, Bácsszőlős: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1352003, lng: 19.4215997}}, ...}, Baranya: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Békés: array{Almáskamarás: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4617785, lng: 21.092448}}, Battonya: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.2902462, lng: 21.0199215}}, Békés: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.6704899, lng: 21.0434996}}, Békéscsaba: array{constituencies: array{'Békés 1.'}, coordinates: array{lat: 46.6735939, lng: 21.0877309}}, Békéssámson: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4208677, lng: 20.6176498}}, Békésszentandrás: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.8715996, lng: 20.48336}}, Bélmegyer: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.8726019, lng: 21.1832832}}, Biharugra: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.9691009, lng: 21.5987651}}, ...}, Borsod-Abaúj-Zemplén: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Budapest: array{Budapest I. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.4968219, lng: 19.037458}}, Budapest II. ker.: array{constituencies: array{'Budapest 03.', 'Budapest 04.'}, coordinates: array{lat: 47.5393329, lng: 18.986934}}, Budapest III. ker.: array{constituencies: array{'Budapest 04.', 'Budapest 10.'}, coordinates: array{lat: 47.5671768, lng: 19.0368517}}, Budapest IV. ker.: array{constituencies: array{'Budapest 11.', 'Budapest 12.'}, coordinates: array{lat: 47.5648915, lng: 19.0913149}}, Budapest V. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.5002319, lng: 19.0520181}}, Budapest VI. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.509863, lng: 19.0625813}}, Budapest VII. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.5027289, lng: 19.073376}}, Budapest VIII. ker.: array{constituencies: array{'Budapest 01.', 'Budapest 06.'}, coordinates: array{lat: 47.4894184, lng: 19.070668}}, ...}, Csongrád-Csanád: array{Algyő: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3329625, lng: 20.207889}}, Ambrózfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3501417, lng: 20.7313995}}, Apátfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.173317, lng: 20.5800472}}, Árpádhalom: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.6158286, lng: 20.547733}}, Ásotthalom: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.1995983, lng: 19.7833756}}, Baks: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.5518708, lng: 20.1064166}}, Balástya: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.4261828, lng: 20.004933}}, Bordány: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.3194213, lng: 19.9227063}}, ...}, Fejér: array{Aba: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 47.0328193, lng: 18.522359}}, Adony: array{constituencies: array{'Fejér 4.'}, coordinates: array{lat: 47.119831, lng: 18.8612469}}, Alap: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.8075763, lng: 18.684028}}, Alcsútdoboz: array{constituencies: array{'Fejér 3.'}, coordinates: array{lat: 47.4277067, lng: 18.6030325}}, Alsószentiván: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.7910573, lng: 18.732161}}, Bakonycsernye: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.321719, lng: 18.0907379}}, Bakonykúti: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.2458464, lng: 18.195769}}, Balinka: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.3135736, lng: 18.1907168}}, ...}, Győr-Moson-Sopron: array{Abda: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.6962149, lng: 17.5445786}}, Acsalag: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.676095, lng: 17.1977771}}, Ágfalva: array{constituencies: array{'Győr-Moson-Sopron 4.'}, coordinates: array{lat: 47.688862, lng: 16.5110233}}, Agyagosszergény: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.608545, lng: 16.9409912}}, Árpás: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5134127, lng: 17.3931579}}, Ásványráró: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.8287695, lng: 17.499195}}, Babót: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5752269, lng: 17.0758604}}, Bágyogszovát: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5866036, lng: 17.3617273}}, ...}, ...}.", + 12, + "Offset 'constituencies' (non-empty-list) does not accept type array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}.", + ], + ]); + } + + public function testBug8573(): void + { + $this->analyse([__DIR__ . '/data/bug-8573.php'], []); + } + + public function testBug8632(): void + { + $this->analyse([__DIR__ . '/data/bug-8632.php'], []); + } + + public function testBug7857(): void + { + $this->analyse([__DIR__ . '/data/bug-7857.php'], []); + } + + public function testBug8879(): void + { + $this->analyse([__DIR__ . '/data/bug-8879.php'], []); + } + + public function testBug9011(): void + { + $errors = []; + if (PHP_VERSION_ID < 80000) { + $errors = [ + [ + 'Method Bug9011\HelloWorld::getX() should return array but returns false.', + 16, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/bug-9011.php'], $errors); + } + + public function testMagicSerialization(): void + { + $this->analyse([__DIR__ . '/data/magic-serialization.php'], [ + [ + 'Method MagicSerialization\WrongSignature::__serialize() should return array but returns string.', + 23, + ], + [ + 'Method MagicSerialization\WrongSignature::__unserialize() with return type void returns string but should not return anything.', + 28, + ], + ]); + } + + public function testBug7574(): void + { + $this->analyse([__DIR__ . '/../Classes/data/bug-7574.php'], []); + } + + public function testMagicSignatures(): void + { + $this->analyse([__DIR__ . '/data/magic-signatures.php'], [ + [ + 'Method MagicSignatures\WrongSignature::__isset() should return bool but returns string.', + 39, + ], + [ + 'Method MagicSignatures\WrongSignature::__clone() with return type void returns string but should not return anything.', + 43, + ], + [ + 'Method MagicSignatures\WrongSignature::__debugInfo() should return array|null but returns string.', + 47, + ], + [ + 'Method MagicSignatures\WrongSignature::__set() with return type void returns string but should not return anything.', + 51, + ], + [ + 'Method MagicSignatures\WrongSignature::__set_state() should return object but returns string.', + 55, + ], + [ + 'Method MagicSignatures\WrongSignature::__sleep() should return array but returns string.', + 59, + ], + [ + 'Method MagicSignatures\WrongSignature::__unset() with return type void returns string but should not return anything.', + 63, + ], + [ + 'Method MagicSignatures\WrongSignature::__wakeup() with return type void returns string but should not return anything.', + 67, + ], + ]); + } + + public function testLists(): void + { + $this->analyse([__DIR__ . '/data/return-list.php'], [ + [ + "Method ReturnList\Foo::getList1() should return list but returns array{0?: 'foo', 1?: 'bar'}.", + 10, + "array{0?: 'foo', 1?: 'bar'} might not be a list.", + ], + [ + "Method ReturnList\Foo::getList2() should return list but returns array{0?: 'foo', 1?: 'bar'}.", + 19, + "array{0?: 'foo', 1?: 'bar'} might not be a list.", + ], + ]); + } + + public function testConditionalListRule(): void + { + $this->analyse([__DIR__ . '/data/return-list-rule.php'], []); + } + + public function testBug6856(): void + { + $this->analyse([__DIR__ . '/data/bug-6856.php'], []); + } + + public function testRuleError(): void + { + $this->analyse([__DIR__ . '/data/return-rule-error.php'], [ + [ + "Method ReturnRuleError\Bar::processNode() should return list but returns array{'foo'}.", + 47, + 'Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Method ReturnRuleError\Baz::processNode() should return list but returns array{PHPStan\Rules\RuleError}.', + 66, + 'Error is missing an identifier. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Method ReturnRuleError\Lorem::processNode() should return list but returns array{1: PHPStan\Rules\IdentifierRuleError}.', + 88, + 'array{1: PHPStan\Rules\IdentifierRuleError} is not a list.', + ], + ]); + } + + public function testBug6175(): void + { + $this->analyse([__DIR__ . '/data/bug-6175.php'], []); + } + + public function testBug9766(): void + { + $this->checkBenevolentUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-9766.php'], []); + } + + public function testWrongListTip(): void + { + $this->analyse([__DIR__ . '/data/wrong-list-tip.php'], [ + [ + 'Method WrongListTip\Test::doFoo() should return list but returns list.', + 23, + ], + [ + 'Method WrongListTip\Test2::doFoo() should return non-empty-array but returns non-empty-array.', + 44, + ], + [ + 'Method WrongListTip\Test3::doFoo() should return non-empty-list but returns array.', + 67, + "• array might not be a list.\n• array might be empty.", + ], + ]); + } + + public function testArrowFunctionReturningVoidClosure(): void + { + $this->analyse([__DIR__ . '/data/arrow-function-returning-void-closure.php'], []); + } + + public function testBug6653(): void + { + $this->analyse([__DIR__ . '/data/bug-6653.php'], []); + } + + public function testBug10291(): void + { + $this->analyse([__DIR__ . '/data/bug-10291.php'], []); + } + + public function testBug5008(): void + { + $this->analyse([__DIR__ . '/data/bug-5008.php'], []); + } + + public function testArrayPushPreservesList(): void + { + $this->analyse([__DIR__ . '/data/array-push-preserves-list.php'], []); + } + + public function testBug10721(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10721.php'], []); + } + + public function testBug11491(): void + { + $this->analyse([__DIR__ . '/data/bug-11491.php'], []); + } + + public function testBug3759(): void + { + $this->analyse([__DIR__ . '/data/bug-3759.php'], []); + } + + public function testBug11337(): void + { + $this->analyse([__DIR__ . '/data/bug-11337.php'], []); + } + + public function testBug10715(): void + { + $this->analyse([__DIR__ . '/data/bug-10715.php'], []); + } + + public function testBug10653(): void + { + $this->analyse([__DIR__ . '/data/bug-10653.php'], []); + } + + public function testBug11663(): void + { + $this->analyse([__DIR__ . '/data/bug-11663.php'], []); + } + + public function testBug11857(): void + { + $this->analyse([__DIR__ . '/data/bug-11857-builder.php'], []); + } + + public function testBug12223(): void + { + $this->analyse([__DIR__ . '/data/bug-12223.php'], []); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + self::markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-return.php'], [ + [ + 'Get hook for property PropertyHooksReturn\Foo::$i should return int but returns string.', + 11, + ], + [ + 'Set hook for property PropertyHooksReturn\Foo::$i with return type void returns int but should not return anything.', + 21, + ], + [ + 'Get hook for property PropertyHooksReturn\Foo::$s should return non-empty-string but returns \'\'.', + 29, + ], + [ + 'Get hook for property PropertyHooksReturn\GenericFoo::$a should return T of PropertyHooksReturn\Foo but returns PropertyHooksReturn\Foo.', + 48, + 'Type PropertyHooksReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Get hook for property PropertyHooksReturn\GenericFoo::$b should return T of PropertyHooksReturn\Foo but returns PropertyHooksReturn\Foo.', + 63, + 'Type PropertyHooksReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Get hook for property PropertyHooksReturn\GenericFoo::$c should return T of PropertyHooksReturn\Foo but returns PropertyHooksReturn\Foo.', + 73, + 'Type PropertyHooksReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testShortGetPropertyHook(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/short-get-property-hook-return.php'], [ + [ + 'Get hook for property ShortGetPropertyHookReturn\Foo::$i should return int but returns string.', + 9, + ], + [ + 'Get hook for property ShortGetPropertyHookReturn\Foo::$s should return non-empty-string but returns \'\'.', + 18, + ], + [ + 'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$a should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.', + 36, + 'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$b should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.', + 50, + 'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$c should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.', + 59, + 'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug1O580(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-10580.php'], [ + [ + 'Method Bug10580\FooA::fooThisInterface() should return $this(Bug10580\FooA) but returns Bug10580\FooA.', + 18, + ], + [ + 'Method Bug10580\FooA::fooThisClass() should return $this(Bug10580\FooA) but returns Bug10580\FooA.', + 19, + ], + [ + 'Method Bug10580\FooA::fooThisSelf() should return $this(Bug10580\FooA) but returns Bug10580\FooA.', + 20, + ], + [ + 'Method Bug10580\FooA::fooThisStatic() should return $this(Bug10580\FooA) but returns Bug10580\FooA.', + 21, + ], + [ + 'Method Bug10580\FooB::fooThisInterface() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 27, + ], + [ + 'Method Bug10580\FooB::fooThisClass() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 29, + ], + [ + 'Method Bug10580\FooB::fooThisSelf() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 31, + ], + [ + 'Method Bug10580\FooB::fooThisStatic() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 33, + ], + [ + 'Method Bug10580\FooB::fooThis() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 35, + ], + ]); + } + + public function testBug4443(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-4443.php'], [ + [ + 'Method Bug4443\HelloWorld::getArray() should return array but returns array|null.', + 22, + ], + ]); + } + + public function testBug12928(): void + { + $this->analyse([__DIR__ . '/data/bug-12928.php'], [ + [ + 'Method Bug12928\FooBarBaz::render() should return non-empty-string but returns string.', + 59, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php index 55eb7f75ec..8f3161de63 100644 --- a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,10 +22,22 @@ class StaticMethodCallableRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true); return new StaticMethodCallableRule( - new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), true, true), + new StaticMethodCallCheck( + $reflectionProvider, + $ruleLevelHelper, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + true, + ), new PhpVersion($this->phpVersion), ); } @@ -33,9 +47,6 @@ 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'], [ [ @@ -97,4 +108,14 @@ public function testRule(): void ]); } + public function testBug8752(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8752.php'], []); + } + + public function testCallsOnGenericClassString(): void + { + $this->analyse([__DIR__ . '/../Comparison/data/impossible-method-exists-on-generic-class-string.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/VirtualNullsafeMethodCallTest.php b/tests/PHPStan/Rules/Methods/VirtualNullsafeMethodCallTest.php new file mode 100644 index 0000000000..bfb55be65b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/VirtualNullsafeMethodCallTest.php @@ -0,0 +1,56 @@ + + */ +class VirtualNullsafeMethodCallTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new /** @implements Rule */ class implements Rule { + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->getAttribute('virtualNullsafeMethodCall') === true) { + return [RuleErrorBuilder::message('Nullable method call detected')->identifier('ruleTest.VirtualNullsafeMethod')->build()]; + } + + return [RuleErrorBuilder::message('Regular method call detected')->identifier('ruleTest.VirtualNullsafeMethod')->build()]; + } + + }; + } + + public function testAttribute(): void + { + $this->analyse([ __DIR__ . '/data/virtual-nullsafe-method-call.php'], [ + [ + 'Regular method call detected', + 3, + ], + [ + 'Nullable method call detected', + 4, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/abstract-private-method.php b/tests/PHPStan/Rules/Methods/data/abstract-private-method.php new file mode 100644 index 0000000000..afb7d91eba --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/abstract-private-method.php @@ -0,0 +1,27 @@ +sayHello(); + $this->sayWorld(); + } + + abstract private function sayPrivate() : void; + abstract protected function sayProtected() : void; + abstract public function sayPublic() : void; +} + +trait fooTrait{ + abstract private function sayPrivate() : void; + abstract protected function sayProtected() : void; + abstract public function sayPublic() : void; +} + +interface fooInterface { + abstract private function sayPrivate() : void; + abstract protected function sayProtected() : void; + abstract public function sayPublic() : void; +} diff --git a/tests/PHPStan/Rules/Methods/data/array-cast-list-types.php b/tests/PHPStan/Rules/Methods/data/array-cast-list-types.php new file mode 100644 index 0000000000..6e0c308721 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/array-cast-list-types.php @@ -0,0 +1,29 @@ + $var + */ + public function foo($var): void {} + + /** + * @param literal-string $literalString + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + * @param numeric-string $numericString + * @param resource $resource + */ + public function bar(string $literalString, string $nonEmptyString, string $nonFalsyString, string $numericString, $resource) { + $this->foo((array) true); + $this->foo((array) $literalString); + $this->foo((array) 1.0); + $this->foo((array) 1); + $this->foo((array) $resource); + $this->foo((array) (fn () => 'closure')); + $this->foo((array) $nonEmptyString); + $this->foo((array) $nonFalsyString); + $this->foo((array) $numericString); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php b/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php new file mode 100644 index 0000000000..d892ab794d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php @@ -0,0 +1,83 @@ + $a + * @return list + */ + public function doFoo(array $a): array + { + array_push($a, ...$a); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo2(array $a): array + { + array_push($a, ...[1, 2, 3]); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo3(array $a): array + { + $b = [1, 2, 3]; + array_push($b, ...$a); + + return $b; + } + +} + +class Bar +{ + + /** + * @param list $a + * @return list + */ + public function doFoo(array $a): array + { + array_unshift($a, ...$a); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo2(array $a): array + { + array_unshift($a, ...[1, 2, 3]); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo3(array $a): array + { + $b = [1, 2, 3]; + array_unshift($b, ...$a); + + return $b; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/arrow-function-bind.php b/tests/PHPStan/Rules/Methods/data/arrow-function-bind.php index 449d409b2c..a71fa7d809 100644 --- a/tests/PHPStan/Rules/Methods/data/arrow-function-bind.php +++ b/tests/PHPStan/Rules/Methods/data/arrow-function-bind.php @@ -1,4 +1,4 @@ -= 7.4 + $this->returnVoid(); + } + + public function returnVoid(): void + { + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10043.php b/tests/PHPStan/Rules/Methods/data/bug-10043.php new file mode 100644 index 0000000000..9980c932ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10043.php @@ -0,0 +1,18 @@ +{$name}; + } +} + +class StdSat extends \stdClass +{ + use WarnDynamicPropertyTrait; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10153.php b/tests/PHPStan/Rules/Methods/data/bug-10153.php new file mode 100644 index 0000000000..e2b5f06840 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10153.php @@ -0,0 +1,28 @@ +someMethod()->methodFromChild(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-10165.php b/tests/PHPStan/Rules/Methods/data/bug-10165.php new file mode 100644 index 0000000000..c414f64533 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10165.php @@ -0,0 +1,13 @@ + */ + abstract public function foo(): Collection; +} + +class Baz +{ + /** @use FooTrait */ + use FooTrait; + + /** @return Collection */ + public function foo(): Collection + { + /** @var Collection */ + return new Collection(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10208.php b/tests/PHPStan/Rules/Methods/data/bug-10208.php new file mode 100644 index 0000000000..abc33152ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10208.php @@ -0,0 +1,24 @@ +key = null; + + return $this; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10240.php b/tests/PHPStan/Rules/Methods/data/bug-10240.php new file mode 100644 index 0000000000..60047d13a6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10240.php @@ -0,0 +1,35 @@ += 8.0 + +namespace Bug10240; + +interface MyInterface +{ + /** + * @phpstan-param truthy-string $truthyStrParam + */ + public function doStuff( + string $truthyStrParam, + ): void; +} + +trait MyTrait +{ + /** + * @phpstan-param truthy-string $truthyStrParam + */ + abstract public function doStuff( + string $truthyStrParam, + ): void; +} + +class MyClass implements MyInterface +{ + use MyTrait; + + public function doStuff( + string $truthyStrParam, + ): void + { + // ... + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10291.php b/tests/PHPStan/Rules/Methods/data/bug-10291.php new file mode 100644 index 0000000000..cb23fe92cf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10291.php @@ -0,0 +1,25 @@ +myrand(); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10488.php b/tests/PHPStan/Rules/Methods/data/bug-10488.php new file mode 100644 index 0000000000..fec3d30001 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10488.php @@ -0,0 +1,17 @@ += 8.0 + +namespace Bug10488; + +trait Bar +{ + /** + * @param array $data + */ + + abstract protected function test(array $data): void; +} + +abstract class Foo +{ + use Bar; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10580.php b/tests/PHPStan/Rules/Methods/data/bug-10580.php new file mode 100644 index 0000000000..b9c479000a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10580.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug10580; + +interface FooI { + /** @return $this */ + public function fooThisInterface(): FooI; + /** @return $this */ + public function fooThisClass(): FooI; + /** @return $this */ + public function fooThisSelf(): self; + /** @return $this */ + public function fooThisStatic(): static; +} + +final class FooA implements FooI +{ + public function fooThisInterface(): FooI { return new FooA(); } + public function fooThisClass(): FooA { return new FooA(); } + public function fooThisSelf(): self { return new FooA(); } + public function fooThisStatic(): static { return new FooA(); } +} + +final class FooB implements FooI +{ + /** @return $this */ + public function fooThisInterface(): FooI { return new FooB(); } + /** @return $this */ + public function fooThisClass(): FooB { return new FooB(); } + /** @return $this */ + public function fooThisSelf(): self { return new FooB(); } + /** @return $this */ + public function fooThisStatic(): static { return new FooB(); } + /** @return $this */ + public function fooThis(): static { return new FooB(); } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10653.php b/tests/PHPStan/Rules/Methods/data/bug-10653.php new file mode 100644 index 0000000000..3aaa3c735b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10653.php @@ -0,0 +1,42 @@ +throwOnFailure($this->mayFail()); + } + + /** + * @template T + * + * @param T $result + * @return (T is false ? never : T) + */ + public function throwOnFailure($result) + { + if ($result === false) { + throw new Exception('Operation failed'); + } + return $result; + } + + /** + * @return stdClass|false + */ + public function mayFail() + { + $this->Counter++; + return $this->Counter % 2 ? new stdClass() : false; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10715.php b/tests/PHPStan/Rules/Methods/data/bug-10715.php new file mode 100644 index 0000000000..82cd7f66ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10715.php @@ -0,0 +1,30 @@ + $word + * + * @return ($word is array ? array : string) + */ + public static function wgtrim(string|array $word): string|array + { + if (\is_array($word)) { + return array_map(static::wgtrim(...), $word); + } + + return 'word'; + } + + /** + * @param array{foo: array, bar: string} $array + * + * @return array{foo: array, bar: string} + */ + public static function example(array $array): array + { + return array_map(static::wgtrim(...), $array); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10872.php b/tests/PHPStan/Rules/Methods/data/bug-10872.php new file mode 100644 index 0000000000..9b1e68ce17 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10872.php @@ -0,0 +1,42 @@ + + */ + public function getRow(): array + { + return []; + } + + /** + * @template ExpectedType of array + * @param ExpectedType $expected + * @param array $actual + * @psalm-assert =ExpectedType $actual + */ + public static function assertSame(array $expected, array $actual): void + { + if ($actual !== $expected) { + throw new \Exception(); + } + } + + public function testEscapeIdentifier(): void + { + $names = [ + 'foo', + '2', + ]; + + $expected = array_combine($names, array_fill(0, count($names), 'x')); + + self::assertSame( + $expected, + $this->getRow() + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10956.php b/tests/PHPStan/Rules/Methods/data/bug-10956.php new file mode 100644 index 0000000000..459d5bd6c2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10956.php @@ -0,0 +1,19 @@ += 8.1 +declare(strict_types = 1); + +namespace Bug11337; + +use function array_filter; + +class Foo +{ + + /** + * @return array<\stdClass> + */ + public function testFunction(): array + { + $objects = [ + new \stdClass(), + null, + new \stdClass(), + null, + ]; + + return array_filter($objects, is_object(...)); + } + + /** + * @return array<1|2> + */ + public function testMethod(): array + { + $objects = [ + 1, + 2, + -4, + 0, + -1, + ]; + + return array_filter($objects, $this->isPositive(...)); + } + + /** + * @return array<'foo'|'bar'> + */ + public function testStaticMethod(): array + { + $objects = [ + '', + 'foo', + '', + 'bar', + ]; + + return array_filter($objects, self::isNonEmptyString(...)); + } + + /** + * @phpstan-assert-if-true int<1, max> $n + */ + private function isPositive(int $n): bool + { + return $n > 0; + } + + /** + * @phpstan-assert-if-true non-empty-string $str + */ + private static function isNonEmptyString(string $str): bool + { + return \strlen($str) > 0; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11491.php b/tests/PHPStan/Rules/Methods/data/bug-11491.php new file mode 100644 index 0000000000..9369b36610 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11491.php @@ -0,0 +1,49 @@ +id; + } + + /** @return non-empty-string */ + public function name(): string + { + return $this->name; + } + + /** @return non-empty-string */ + public function toFacetValue(): string + { + return sprintf( + '%s%s%s', + $this->name, + self::SEPARATOR, + $this->id, + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11503.php b/tests/PHPStan/Rules/Methods/data/bug-11503.php new file mode 100644 index 0000000000..d37f48cf07 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11503.php @@ -0,0 +1,19 @@ +sub($interval); + $date->add($interval); + $date->modify('+1 day'); + $date->setDate(2024, 8, 13); + $date->setISODate(2024, 1); + $date->setTime(0, 0, 0, 0); + $date->setTimestamp(1); + $zone = new \DateTimeZone('UTC'); + $date->setTimezone($zone); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11559c.php b/tests/PHPStan/Rules/Methods/data/bug-11559c.php new file mode 100644 index 0000000000..54e3ba27b0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11559c.php @@ -0,0 +1,16 @@ +implicit_variadic_fn(1, 2, 3); + $c->regular_fn(1, 2, 3); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11663.php b/tests/PHPStan/Rules/Methods/data/bug-11663.php new file mode 100644 index 0000000000..ac2ed03a93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11663.php @@ -0,0 +1,79 @@ +where('test'); + } + + /** + * @param __benevolent $template + * @return __benevolent + */ + public function test2($template) + { + return $template->where('test'); + } + + + /** + * @template T of A|B + * @param T $ab + * @return T + */ + function foo(A|B $ab): A|B + { + return $ab->doFoo(); + } + + /** + * @template T of __benevolent + * @param T $ab + * @return T + */ + function foo2(A|B $ab): A|B + { + return $ab->doFoo(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11665.php b/tests/PHPStan/Rules/Methods/data/bug-11665.php new file mode 100644 index 0000000000..1926a10ac5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11665.php @@ -0,0 +1,7 @@ += 8.0 + +namespace Bug11857Builder; + +class Foo +{ + + /** + * @param array $attributes + * @return $this + */ + public function filter(array $attributes): static + { + return $this; + } + + /** + * @param array $attributes + * @return $this + */ + public function filterUsingRequest(array $attributes): static + { + return $this->filter($attributes); + } + +} + +final class FinalFoo +{ + + /** + * @param array $attributes + * @return $this + */ + public function filter(array $attributes): static + { + return $this; + } + + /** + * @param array $attributes + * @return $this + */ + public function filterUsingRequest(array $attributes): static + { + return $this->filter($attributes); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12015.php b/tests/PHPStan/Rules/Methods/data/bug-12015.php new file mode 100644 index 0000000000..c2a5618cd8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12015.php @@ -0,0 +1,21 @@ + $field + */ + abstract public function field(array $field): static; +} + +class GroupBuilder +{ + use HasFieldBuildersTrait; + + /** @var array */ + private array $group = []; + + private function __construct() + { + } + + /** + * @param array $field + */ + public function field(array $field): static + { + if (! is_array($this->group['fields'] ?? null)) { + $this->group['fields'] = []; + } + + $this->group['fields'][] = $field; + + return $this; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12137.php b/tests/PHPStan/Rules/Methods/data/bug-12137.php new file mode 100755 index 0000000000..eacb78bbf3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12137.php @@ -0,0 +1,23 @@ + + */ + public function sayHello(): array + { + $a = [1 => 'foo', 3 => 'bar', 5 => 'baz']; + return array_map(static fn(string $s, int $i): string => $s . $i, $a, array_keys($a)); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12422.php b/tests/PHPStan/Rules/Methods/data/bug-12422.php new file mode 100644 index 0000000000..19ae6e4e6a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12422.php @@ -0,0 +1,28 @@ += 8.1 + +namespace Bug12422; + +enum MyEnum +{ + case A; + case B; +} + +class MyClass +{ + public function fooo(): void + { + } +} + +function test(MyEnum $enum, ?MyClass $bar): void +{ + if ($enum === MyEnum::A && $bar === null) { + return; + } + + match ($enum) { + MyEnum::A => $bar->fooo(), + MyEnum::B => null, + }; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12471.php b/tests/PHPStan/Rules/Methods/data/bug-12471.php new file mode 100644 index 0000000000..fd242cc639 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12471.php @@ -0,0 +1,40 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug12501; + +final readonly class EmptyObject { + public function __construct( + public null $value1 = null, + ) {} +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12544.php b/tests/PHPStan/Rules/Methods/data/bug-12544.php new file mode 100644 index 0000000000..56860b669d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12544.php @@ -0,0 +1,21 @@ +hello(); + $bar->somethingElse(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-1267.php b/tests/PHPStan/Rules/Methods/data/bug-1267.php new file mode 100644 index 0000000000..bd903afccc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-1267.php @@ -0,0 +1,30 @@ +{$column}(); + } + } + } +} + +class Model {} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12880.php b/tests/PHPStan/Rules/Methods/data/bug-12880.php new file mode 100644 index 0000000000..b82ecb820e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12880.php @@ -0,0 +1,31 @@ +test([1, 2, 3, true]); + } + + /** + * @param list $ids + */ + private function test(array $ids): void + { + $ids = array_unique($ids); + \PHPStan\dumpType($ids); + $ids = array_slice($ids, 0, 5); + \PHPStan\dumpType($ids); + $this->expectList($ids); + } + + /** + * @param list $ids + */ + private function expectList(array $ids): void + { + var_dump($ids); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12884.php b/tests/PHPStan/Rules/Methods/data/bug-12884.php new file mode 100644 index 0000000000..20ec2cac5f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12884.php @@ -0,0 +1,40 @@ + $levels */ + public function __construct( + private LoggerInterface $logger, + public array $levels = [] + ) {} + + public function log(string $level, string $message): void + { + if (!in_array($level, $this->levels, true)) { + $level = LogLevel::INFO; + } + $this->logger->log($level, $message); + } +} + +interface LoggerInterface +{ + /** + * @param 'emergency'|'alert'|'critical'|'error'|'warning'|'notice'|'info'|'debug' $level + */ + public function log($level, string|\Stringable $message): void; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12928.php b/tests/PHPStan/Rules/Methods/data/bug-12928.php new file mode 100644 index 0000000000..2b6e038fe9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12928.php @@ -0,0 +1,68 @@ + $replace + * + * @return non-falsy-string + */ + public function render(string $code, array $replace): string + { + return str_replace( + [ + '__DIR__', + '__FILE__', + ], + $replace, + $code, + ); + } +} + + +class FooBarBaz { + /** + * @param non-empty-string $phptFile + * @param non-empty-string $code + * + * @return non-empty-string + */ + public function render(string $code, array $replace): string + { + return str_replace( + [ + '__DIR__', + '__FILE__', + ], + $replace, + $code, + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12940.php b/tests/PHPStan/Rules/Methods/data/bug-12940.php new file mode 100644 index 0000000000..ad00e11c1b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12940.php @@ -0,0 +1,43 @@ + $className + * @return T + */ + public static function makeInstance(string $className, mixed ...$args): object + { + return new $className(...$args); + } +} + +class PageRenderer +{ + public function setTemplateFile(string $path): void + { + } + + public function setLanguage(string $lang): void + { + } +} + +class TypoScriptFrontendController +{ + + protected ?PageRenderer $pageRenderer = null; + + public function initializePageRenderer(): void + { + if ($this->pageRenderer !== null) { + return; + } + $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); + $this->pageRenderer->setTemplateFile('EXT:frontend/Resources/Private/Templates/MainPage.html'); + $this->pageRenderer->setLanguage('DE'); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-1517.php b/tests/PHPStan/Rules/Methods/data/bug-1517.php new file mode 100644 index 0000000000..990e8a3305 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-1517.php @@ -0,0 +1,16 @@ + true; +}; + +$gen2 = static function () { + yield false => false; +}; + +$ait = new AppendIterator(); + +$ait->append($gen1()); +$ait->append($gen2()); diff --git a/tests/PHPStan/Rules/Methods/data/bug-1953.php b/tests/PHPStan/Rules/Methods/data/bug-1953.php new file mode 100644 index 0000000000..c70978996c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-1953.php @@ -0,0 +1,13 @@ +bar(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-3284.php b/tests/PHPStan/Rules/Methods/data/bug-3284.php new file mode 100644 index 0000000000..b8989dfc8b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3284.php @@ -0,0 +1,22 @@ +sayHello(['b' => 'name']); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3499.php b/tests/PHPStan/Rules/Methods/data/bug-3499.php new file mode 100644 index 0000000000..2aff67f920 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3499.php @@ -0,0 +1,21 @@ +foo->getClone(); + } +} + + diff --git a/tests/PHPStan/Rules/Methods/data/bug-3759.php b/tests/PHPStan/Rules/Methods/data/bug-3759.php new file mode 100644 index 0000000000..ddbafbefdc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3759.php @@ -0,0 +1,31 @@ + ['x' => 'x'], + 'minor' => ['y' => 'y'], + 'patch' => ['z' => 'z'], + ]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3951.php b/tests/PHPStan/Rules/Methods/data/bug-3951.php new file mode 100644 index 0000000000..bfb50473ce --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3951.php @@ -0,0 +1,36 @@ +filter($subject); + } + + /** + * @param TSubject $subject + * + * @return TSubject|null + * + * @template TSubject as Filterable + */ + public function filter(Filterable $subject) : ?Filterable + { + return (rand(0,1) ? null : $subject); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4008.php b/tests/PHPStan/Rules/Methods/data/bug-4008.php index 7d93147d5f..68414bb3cb 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4008.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4008.php @@ -29,3 +29,12 @@ class OtherGenericClass{} abstract class BaseModel{} class Model extends BaseModel{} + +/** + * @template T of Model + * @extends GenericClass + */ +class ChildGenericGenericClass extends GenericClass +{ + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4083.php b/tests/PHPStan/Rules/Methods/data/bug-4083.php index c3a6141896..42bf6d0393 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4083.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4083.php @@ -1,4 +1,4 @@ -= 7.4 + + */ +class GenericList implements IteratorAggregate +{ + /** @var array */ + protected $items = []; + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->items); + } + + /** + * @return ?T + */ + public function broken(int $key) + { + $item = $this->items[$key] ?? null; + if ($item) { + } + + return $item; + } + + /** + * @return ?T + */ + public function works(int $key) + { + $item = $this->items[$key] ?? null; + + return $item; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4188.php b/tests/PHPStan/Rules/Methods/data/bug-4188.php index 6b0744876c..8181dbc4a4 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4188.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4188.php @@ -1,4 +1,4 @@ -= 7.4 +value = $value; + } + + final public static function fromString(string $value): self + { + return new static($value); + } +} + +final class ClassB extends ClassC +{ +} + +final class ClassA +{ + public function classB(): ClassB + { + return ClassB::fromString("any"); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4443.php b/tests/PHPStan/Rules/Methods/data/bug-4443.php new file mode 100644 index 0000000000..9f7ff6a28e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4443.php @@ -0,0 +1,26 @@ + */ + private static ?array $arr = null; + + private static function setup(): void + { + self::$arr = null; + } + + /** @return array */ + public static function getArray(): array + { + if (self::$arr === null) { + self::$arr = []; + self::setup(); + } + return self::$arr; + } +} + +HelloWorld::getArray(); diff --git a/tests/PHPStan/Rules/Methods/data/bug-4590.php b/tests/PHPStan/Rules/Methods/data/bug-4590.php index a3db4a3445..ed2d79d790 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4590.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4590.php @@ -27,6 +27,14 @@ public function getBody() { return $this->body; } + + /** + * @return static> + */ + public static function testGenericStatic() + { + return new static(["ok" => "hello"]); + } } class Controller diff --git a/tests/PHPStan/Rules/Methods/data/bug-4758.php b/tests/PHPStan/Rules/Methods/data/bug-4758.php new file mode 100644 index 0000000000..e45329dd1f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4758.php @@ -0,0 +1,26 @@ + + */ + public function doStuff(): array + { + return [[]]; + } +} + +trait TraitTwo +{ + use TraitOne { + TraitOne::doStuff as doStuffFromTraitOne; + } +} + +class SomeController +{ + use TraitTwo; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4801.php b/tests/PHPStan/Rules/Methods/data/bug-4801.php new file mode 100644 index 0000000000..a09d113367 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4801.php @@ -0,0 +1,25 @@ + + */ + public function work(?callable $a): I; +} + +/** + * @param I $i + */ +function x(I $i) { + assertType('Bug4801\\I', $i->work(null)); + assertType('Bug4801\\I', $i->work(fn(string $a) => (int) $a)); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5008.php b/tests/PHPStan/Rules/Methods/data/bug-5008.php new file mode 100644 index 0000000000..f14d3aef2b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5008.php @@ -0,0 +1,14 @@ + $b; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5089.php b/tests/PHPStan/Rules/Methods/data/bug-5089.php index e3578f2235..eb5306ece8 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-5089.php +++ b/tests/PHPStan/Rules/Methods/data/bug-5089.php @@ -16,6 +16,6 @@ public function encode(string $foo): array public function test(): void { - assertType('*NEVER*', $this->encode('foo')); + assertType('never', $this->encode('foo')); } } diff --git a/tests/PHPStan/Rules/Methods/data/bug-5232.php b/tests/PHPStan/Rules/Methods/data/bug-5232.php new file mode 100644 index 0000000000..4089988ff7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5232.php @@ -0,0 +1,26 @@ += 7.4 +', $col); $newCol = $col->map(static fn(string $var): string => $var . 'bar'); - assertType('Bug5372\Collection', $newCol); + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); $newCol = $col->map(static fn(string $var): string => $classString); @@ -77,11 +77,11 @@ public function doBar(string $literalString) { $col = new Collection(['foo', 'bar']); $newCol = $col->map(static fn(string $var): string => $literalString); - assertType('Bug5372\Collection', $newCol); + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); $newCol = $col->map2(static fn(string $var): string => $literalString); - assertType('Bug5372\Collection', $newCol); // should be literal-string + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-5518.php b/tests/PHPStan/Rules/Methods/data/bug-5518.php new file mode 100644 index 0000000000..749501df95 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5518.php @@ -0,0 +1,28 @@ + */ +interface TypeNonEmptyString extends TypeParse +{ +} + +interface Params +{ + /** + * @param TypeParse $type + * @template T + */ + public function get(TypeParse ...$type): void; +} + +class Test { + public function exec(Params $params, TypeNonEmptyString $string): void { + $params->get($string); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5623.php b/tests/PHPStan/Rules/Methods/data/bug-5623.php new file mode 100644 index 0000000000..3c9277e611 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5623.php @@ -0,0 +1,16 @@ +format(DateTimeInterface::ATOM); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5754.php b/tests/PHPStan/Rules/Methods/data/bug-5754.php new file mode 100644 index 0000000000..61189455d0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5754.php @@ -0,0 +1,70 @@ +queue = $context->createQueue('test'); + $context->declareQueue($this->queue); + } elseif ($context instanceof SqsContext) { + $this->queue = $context->createQueue('test'); + $context->declareQueue($this->queue); + } else { + throw new RuntimeException('nope'); + } + } +} + +/** not working **/ +class Fail +{ + /** + * @var SnsQsQueue|SqsQueue + */ + private $queue; + + public function __construct(Context $context) + { + if ($context instanceof SnsQsContext) { + $this->queue = $context->createQueue('test'); + } elseif ($context instanceof SqsContext) { + $this->queue = $context->createQueue('test'); + } else { + throw new RuntimeException('nope'); + } + $context->declareQueue($this->queue); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5757.php b/tests/PHPStan/Rules/Methods/data/bug-5757.php index 64fa9b4de3..d0b84715f0 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-5757.php +++ b/tests/PHPStan/Rules/Methods/data/bug-5757.php @@ -22,7 +22,7 @@ class Foo public function doFoo() { - assertType('iterable>', Helper::chunk([1], 3)); + assertType('iterable>', Helper::chunk([1], 3)); assertType('iterable>', Helper::chunk([], 3)); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-5781.php b/tests/PHPStan/Rules/Methods/data/bug-5781.php new file mode 100644 index 0000000000..3fba572fe6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5781.php @@ -0,0 +1,20 @@ += 7.4 + + */ +final class TemporalFormatLoader_Unexpected implements FormatLoader { + public function addElement(string $name, mixed $element): static { + return $this; + } +} + +/** + * Working as expected. + * + * @implements FormatLoader + */ +class TemporalFormatLoader_Expected implements FormatLoader { + public function addElement(string $name, mixed $element): static { + return $this; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6147.php b/tests/PHPStan/Rules/Methods/data/bug-6147.php new file mode 100644 index 0000000000..c5ee6e021c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6147.php @@ -0,0 +1,22 @@ + $class + * @return void + */ + public static function invokeController(string $class): void + { + if (/* Http::methodIs ("post") && */ method_exists($class, "methodPost")) { + $class::methodPost(); // Call to an undefined static method ControllerInterface::methodPost() + } + } +} + +interface ControllerInterface +{ + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6175.php b/tests/PHPStan/Rules/Methods/data/bug-6175.php new file mode 100644 index 0000000000..b077a74e34 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6175.php @@ -0,0 +1,36 @@ +foo()); + } +} + +class Model +{ + use RefsTrait; + + /** + * @return B|C + */ + public function foo2() + { + return new A(); // @phpstan-ignore-line + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6249.php b/tests/PHPStan/Rules/Methods/data/bug-6249.php index d25561ebc3..7d2db1ed3e 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-6249.php +++ b/tests/PHPStan/Rules/Methods/data/bug-6249.php @@ -1,4 +1,4 @@ -= 7.4 += 8.0 + +namespace Bug6291; + +interface Table {} + +final class ArticlesTable implements Table +{ + public function __construct(private TableManager $tableManager) {} + public function find(ArticlePrimaryKey $primaryKey): ?object + { + return $this->tableManager->find($this, $primaryKey); + } +} + +/** @template TableType of Table */ +interface PrimaryKey {} + +/** @implements PrimaryKey */ +final class ArticlePrimaryKey implements PrimaryKey {} + +class TableManager +{ + /** + * @template TableType of Table + * @param TableType $table + * @param PrimaryKey $primaryKey + */ + public function find(Table $table, PrimaryKey $primaryKey): ?object + { + return null; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6358.php b/tests/PHPStan/Rules/Methods/data/bug-6358.php new file mode 100644 index 0000000000..cab6391c6e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6358.php @@ -0,0 +1,16 @@ + + */ + public function sayHello(): array + { + return [1 => new stdClass]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6371.php b/tests/PHPStan/Rules/Methods/data/bug-6371.php new file mode 100644 index 0000000000..aa1d09f62f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6371.php @@ -0,0 +1,25 @@ + $hw + * @return void + */ +function foo (HelloWorld $hw): void { + $hw->compare(1, 'foo'); + $hw->compare(true, false); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-6423.php b/tests/PHPStan/Rules/Methods/data/bug-6423.php index 59641a6626..8d8a4205ab 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-6423.php +++ b/tests/PHPStan/Rules/Methods/data/bug-6423.php @@ -1,4 +1,4 @@ -= 7.4 +|false + */ + public function sayHello() + { + $test = $this->getTest(); + return $this->filterEvent('sayHello', $test); + } + + /** + * @template TValue of mixed + * @param TValue $value + * @return TValue + */ + private function filterEvent(string $eventName, $value) + { + // do event + return $value; + } + + /** + * @return array|false + */ + private function getTest() + { + $failure = random_int(0, PHP_INT_MAX) % 2 ? true : false; + if ($failure === true) { + return false; + } + return ['foo' => 123]; + } +} + +class HelloWorld2 +{ + /** + * @return array|false + */ + public function sayHello() + { + $test = $this->getTest(); + return $this->filterEvent('sayHello', $test); + } + + /** + * @template TValue of mixed + * @param TValue $value + * @return TValue + */ + private function filterEvent(string $eventName, $value) + { + // do event + return $value; + } + + /** + * @return array|false + */ + private function getTest() + { + $failure = random_int(0, PHP_INT_MAX) % 2 ? true : false; + if ($failure === true) { + return false; + } + return ['foo' => 123]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6828.php b/tests/PHPStan/Rules/Methods/data/bug-6828.php new file mode 100644 index 0000000000..738766d8a7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6828.php @@ -0,0 +1,51 @@ += 8.1 + +declare(strict_types=1); + +namespace Bug6828; + +/** @template T */ +interface Option +{ + /** + * @template U + * @param \Closure(T):U $c + * @return Option + */ + function map(\Closure $c); +} + +/** + * @template T + * @template E + */ +abstract class Result +{ + /** @return T */ + function unwrap() + { + + } + + /** + * @template U + * @param U $v + * @return Result + */ + static function ok($v) + { + + } +} + +/** + * @template U + * @template F + * @param Result, F> $result + * @return Option> + */ +function f(Result $result): Option +{ + /** @var Option> */ + return $result->unwrap()->map(Result::ok(...)); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6856.php b/tests/PHPStan/Rules/Methods/data/bug-6856.php new file mode 100644 index 0000000000..4422538425 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6856.php @@ -0,0 +1,52 @@ + + */ + use TraitA { + a as renamed; + } + + public function a(): ClassB { + return $this->renamed(); + } + + public function b(): ClassB { + return $this->test(); + } +} + +class ClassB { + // empty +} + +function (ClassA $a): void { + assertType(ClassB::class, $a->a()); + assertType(ClassB::class, $a->renamed()); + assertType(ClassB::class, $a->test()); + assertType(ClassB::class, $a->b()); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-6904.php b/tests/PHPStan/Rules/Methods/data/bug-6904.php new file mode 100644 index 0000000000..92b95583ff --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6904.php @@ -0,0 +1,48 @@ +&Selectable + */ + public Collection&Selectable $items; + + /** + * @param Selectable $selectable + * @return TValue + * + * @template TValue + */ + private function matchOne(Selectable $selectable) + { + return $selectable->first(); + } + + public function run(): void + { + assertType('stdClass', $this->matchOne($this->items)); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6917.php b/tests/PHPStan/Rules/Methods/data/bug-6917.php new file mode 100644 index 0000000000..e6054498eb --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6917.php @@ -0,0 +1,48 @@ + $admin + * @phpstan-return T + */ + public function setAdmin(AdminInterface $admin): object; +} + +class Hello implements HelloInterface +{ + /** @inheritdoc */ + public function setAdmin(AdminInterface $admin): object + { + return $admin->getObject(); + } +} + +class MockObject {} + +class Foo +{ + /** + * @var MockObject&AdminInterface + */ + public $admin; + + public function test(): void + { + $hello = new Hello(); + assertType('stdClass', $hello->setAdmin($this->admin)); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6922b.php b/tests/PHPStan/Rules/Methods/data/bug-6922b.php new file mode 100644 index 0000000000..ccb086c07c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6922b.php @@ -0,0 +1,26 @@ +isFirstOptionActive() === false || + $configuration?->isSecondOptionActive() === false) + { + // .... + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6946.php b/tests/PHPStan/Rules/Methods/data/bug-6946.php new file mode 100644 index 0000000000..5d4f4394e4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6946.php @@ -0,0 +1,44 @@ + + */ +class Bar extends Foo {} + +/** + * @extends Foo + */ +class Baz extends Foo {} + +function test(bool $barOrBaz): void +{ + if ($barOrBaz) { + $inner = new B(); + $upper = new Bar(); + } else { + $inner = new C(); + $upper = new Baz(); + } + + $upper->apply($inner); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7020.php b/tests/PHPStan/Rules/Methods/data/bug-7020.php new file mode 100644 index 0000000000..f7ecdd8d25 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7020.php @@ -0,0 +1,18 @@ + ...$templatePaths + * @return array + */ + public static function merge(array ...$templatePaths): array + { + $mergedTemplatePaths = array_replace(...$templatePaths); + ksort($mergedTemplatePaths); + + return $mergedTemplatePaths; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7103.php b/tests/PHPStan/Rules/Methods/data/bug-7103.php new file mode 100644 index 0000000000..2ac31b839f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7103.php @@ -0,0 +1,40 @@ + $class + * @return R + */ + public function find(string $class, int $id): object; +} + +interface Entity {} + +/** + * @phpstan-template T of Entity + * @phpstan-implements Manager + */ +abstract class MyManager implements Manager +{ + /** + * @phpstan-template R of T + * @phpstan-param class-string $class + * @phpstan-return R + */ + public function find(string $class, int $id): object + { + /** @phpstan-var R $object */ + $object = $this->get($id); + + return $object; + } + + abstract public function get(int $id): object; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7265.php b/tests/PHPStan/Rules/Methods/data/bug-7265.php new file mode 100644 index 0000000000..06012e6349 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7265.php @@ -0,0 +1,25 @@ + $tgs + * + * @return array + * + * @throws \Exception + */ + public function computeForFrontByPosition($tgs) + { + /** @phpstan-var array $res */ + $res = []; + + foreach ($tgs as $tgItem) { + $position = $tgItem->getPosition(); + + if (!isset($res[$position])) { + $res[$position] = $tgItem; + } else { + /** @phpstan-var T $tgItemToKeep */ + $tgItemToKeep = $this->compare($tgItem, $res[$position]); + $res[$position] = $tgItemToKeep; + } + } + ksort($res); + + return $res; + } + + /** + * @phpstan-template T of PositionEntityInterface&TgEntityInterface + * + * @param iterable $tgs + * + * @return array + * + * @throws \Exception + */ + public function computeForFrontByPosition2($tgs) + { + /** @phpstan-var array $res */ + $res = []; + + foreach ($tgs as $tgItem) { + $position = $tgItem->getPosition(); + + if (!isset($res[$position])) { + $res[$position] = $tgItem; + } else { + /** @phpstan-var T $tgItemToKeep */ + $tgItemToKeep = $this->compare($tgItem, $res[$position]); + $res[$position] = $tgItemToKeep; + } + } + ksort($res); + + return $res; + } + + /** + * @phpstan-template S of TgEntityInterface + * @phpstan-param S $nextTg + * @phpstan-param S $currentTg + * @phpstan-return S + */ + abstract protected function compare(TgEntityInterface $nextTg, TgEntityInterface $currentTg): TgEntityInterface; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7489.php b/tests/PHPStan/Rules/Methods/data/bug-7489.php new file mode 100644 index 0000000000..02d910826a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7489.php @@ -0,0 +1,9 @@ +bindTo(null, null)(); diff --git a/tests/PHPStan/Rules/Methods/data/bug-7511.php b/tests/PHPStan/Rules/Methods/data/bug-7511.php new file mode 100644 index 0000000000..217cedf373 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7511.php @@ -0,0 +1,87 @@ + $tgs + * + * @return array + * + * @throws \Exception + */ + public function computeForFrontByPosition($tgs) + { + /** @phpstan-var array $res */ + $res = []; + + assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), parameter)', $res[1]); + + foreach ($tgs as $tgItem) { + $position = $tgItem->getPosition(); + + if (!isset($res[$position])) { + assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), argument)', $tgItem); + $res[$position] = $tgItem; + } else { + assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), argument)', $tgItem); + assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), parameter)', $res[$position]); + $tgItemToKeep = $this->compare($tgItem, $res[$position]); + assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), parameter)', $tgItemToKeep); + $res[$position] = $tgItemToKeep; + } + } + ksort($res); + + assertType('array', $res); + + return $res; + } + + /** + * @phpstan-template T of PositionEntityInterface&TgEntityInterface + * + * @param iterable $tgs + * + * @return array + * + * @throws \Exception + */ + public function computeForFrontByPosition2($tgs) + { + /** @phpstan-var array $res */ + $res = []; + + assertType('array', $res); + + foreach ($tgs as $tgItem) { + $position = $tgItem->getPosition(); + + assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition2(), parameter)', $res[$position]); + if (isset($res[$position])) { + assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition2(), parameter)', $res[$position]); + } + } + assertType('array', $res); + + return $res; + } + + /** + * @phpstan-template S of TgEntityInterface + * @phpstan-param S $nextTg + * @phpstan-param S $currentTg + * @phpstan-return S + */ + abstract protected function compare(TgEntityInterface $nextTg, TgEntityInterface $currentTg): TgEntityInterface; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7519.php b/tests/PHPStan/Rules/Methods/data/bug-7519.php new file mode 100644 index 0000000000..c1020b84f8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7519.php @@ -0,0 +1,30 @@ + + * + * @extends FilterIterator + */ +class A extends FilterIterator { + public function accept(): bool { + return true; + } + + public function key() { + $key = parent::key(); + + return $key; + } + + public function current() { + $current = parent::current(); + + return $current; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7593.php b/tests/PHPStan/Rules/Methods/data/bug-7593.php new file mode 100644 index 0000000000..f5424c507d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7593.php @@ -0,0 +1,33 @@ += 8.0 + +namespace Bug7593; + +/** @template T */ +class Collection { + /** @param T $item */ + public function add(mixed $item): void {} +} + +class Foo {} +class Bar {} + +class CollectionManager +{ + /** + * @param Collection $fooCollection + * @param Collection $barCollection + */ + public function __construct( + private Collection $fooCollection, + private Collection $barCollection, + ) {} + + public function updateCollection(Foo|Bar $foobar): void + { + (match(get_class($foobar)) { + Foo::class => $this->fooCollection, + Bar::class => $this->barCollection, + default => throw new LogicException(), + })->add($foobar); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7600.php b/tests/PHPStan/Rules/Methods/data/bug-7600.php new file mode 100644 index 0000000000..5cd0a400e8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7600.php @@ -0,0 +1,28 @@ + $array */ + public function __construct(public array $array) {} + + /** @return T */ + public function getFirst(): mixed + { + return $this->array[0]; + } + + /** @param T $item */ + public function remove(mixed $item): void {} +} + +function (): void { + $ints = new Collection([1, 2]); + $strings = new Collection(['foo', 'bar']); + + $collection = rand(0, 1) === 0 ? $ints : $strings; + + $collection->remove($collection->getFirst()); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-7652.php b/tests/PHPStan/Rules/Methods/data/bug-7652.php new file mode 100644 index 0000000000..de20d71647 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7652.php @@ -0,0 +1,38 @@ +, value-of> + */ +interface Options extends \ArrayAccess { + + /** + * @param key-of $offset + */ + public function offsetExists(mixed $offset): bool; + + /** + * @template TOffset of key-of + * @param TOffset $offset + * @return TArray[TOffset] + */ + public function offsetGet(mixed $offset); + + /** + * @template TOffset of key-of + * @param TOffset $offset + * @param TArray[TOffset] $value + */ + public function offsetSet(mixed $offset, mixed $value): void; + + /** + * @template TOffset of key-of + * @param TOffset $offset + */ + public function offsetUnset(mixed $offset): void; + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7662.php b/tests/PHPStan/Rules/Methods/data/bug-7662.php new file mode 100644 index 0000000000..826ba3fe84 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7662.php @@ -0,0 +1,7 @@ + + */ +final class Implementation implements TestInterface { + public function aFunction(): static + { + return $this; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7857.php b/tests/PHPStan/Rules/Methods/data/bug-7857.php new file mode 100644 index 0000000000..269bbd5a87 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7857.php @@ -0,0 +1,17 @@ + $page], + $perPage !== null ? ['perPage' => $perPage] : [] + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7859.php b/tests/PHPStan/Rules/Methods/data/bug-7859.php new file mode 100644 index 0000000000..0cb1fb7f60 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7859.php @@ -0,0 +1,40 @@ += 8.0 + +namespace Bug7904; + +interface Test { + public static function create(): static; +} + +$impl = new class implements Test { + public static function create(): static { + return new self(); + } +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-8058.php b/tests/PHPStan/Rules/Methods/data/bug-8058.php new file mode 100644 index 0000000000..b2334de680 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8058.php @@ -0,0 +1,15 @@ +execute_query($s); + + \mysqli_execute_query($mysqli, $s); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8071.php b/tests/PHPStan/Rules/Methods/data/bug-8071.php new file mode 100644 index 0000000000..2acbb838ca --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8071.php @@ -0,0 +1,48 @@ +> $items + * + * @return array + * + * @template TKey of array-key + * @template TValues of scalar|null + */ + public static function inherit(array $items): array + { + return array_reduce( + $items, + [self::class, 'callBack'], + ) ?? []; + } + + /** + * @param array|null $carry + * @param array $current + * + * @return array + * + * @template TKey of array-key + * @template TValues of scalar|null + */ + private static function callBack(array|null $carry, array $current): array + { + if ($carry === null) { + return $current; + } + + foreach ($carry as $key => $value) { + if ($value !== null) { + continue; + } + + $carry[$key] = $current[$key]; + } + + return $carry; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8081.php b/tests/PHPStan/Rules/Methods/data/bug-8081.php new file mode 100644 index 0000000000..575ef69d38 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8081.php @@ -0,0 +1,24 @@ + + */ + public function foo() { + return []; + } +} + +class two extends one { + public function foo(): array { + return []; + } +} + +class three extends two { + public function foo() { + return []; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8146b-errors.php b/tests/PHPStan/Rules/Methods/data/bug-8146b-errors.php new file mode 100644 index 0000000000..27509dcc96 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8146b-errors.php @@ -0,0 +1,5544 @@ +, coordinates: array{lat: float, lng: float}}>> */ + public function getData(): array + { + return [ + 'Bács-Kiskun' => [ + 'Ágasegyháza' => [ + 'constituencies' => ['Bács-Kiskun 4.', true, false, new X(), null], // expected type errors + 'coordinates' => ['lat' => 46.8386043, 'lng' => 19.4502899], + ], + 'Akasztó' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6898175, 'lng' => 19.205086], + ], + 'Apostag' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8812652, 'lng' => 18.9648478], + ], + 'Bácsalmás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1250396, 'lng' => 19.3357509], + ], + 'Bácsbokod' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1234737, 'lng' => 19.155708], + ], + 'Bácsborsód' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0989373, 'lng' => 19.1566725], + ], + 'Bácsszentgyörgy' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9746039, 'lng' => 19.0398066], + ], + 'Bácsszőlős' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1352003, 'lng' => 19.4215997], + ], + 'Baja' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1817951, 'lng' => 18.9543051], + ], + 'Ballószög' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8619947, 'lng' => 19.5726144], + ], + 'Balotaszállás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3512041, 'lng' => 19.5403558], + ], + 'Bátmonostor' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1057304, 'lng' => 18.9238311], + ], + 'Bátya' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4891741, 'lng' => 18.9579127], + ], + 'Bócsa' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6113504, 'lng' => 19.4826419], + ], + 'Borota' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2657107, 'lng' => 19.2233598], + ], + 'Bugac' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6883076, 'lng' => 19.6833655], + ], + 'Bugacpusztaháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7022143, 'lng' => 19.6356538], + ], + 'Császártöltés' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4222869, 'lng' => 19.1815532], + ], + 'Csátalja' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0363238, 'lng' => 18.9469006], + ], + 'Csávoly' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1912599, 'lng' => 19.1451178], + ], + 'Csengőd' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.71532, 'lng' => 19.2660933], + ], + 'Csikéria' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.121679, 'lng' => 19.473777], + ], + 'Csólyospálos' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4180837, 'lng' => 19.8402638], + ], + 'Dávod' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9976187, 'lng' => 18.9176479], + ], + 'Drágszél' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4653889, 'lng' => 19.0382659], + ], + 'Dunaegyháza' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8383215, 'lng' => 18.9605216], + ], + 'Dunafalva' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.081562, 'lng' => 18.7782526], + ], + 'Dunapataj' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6422106, 'lng' => 18.9989393], + ], + 'Dunaszentbenedek' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.593856, 'lng' => 18.8935322], + ], + 'Dunatetétlen' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.7578624, 'lng' => 19.0932563], + ], + 'Dunavecse' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.9133047, 'lng' => 18.9731873], + ], + 'Dusnok' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3893659, 'lng' => 18.960842], + ], + 'Érsekcsanád' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2541554, 'lng' => 18.9835293], + ], + 'Érsekhalma' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3472701, 'lng' => 19.1247379], + ], + 'Fajsz' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.4157936, 'lng' => 18.9191954], + ], + 'Felsőlajos' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0647473, 'lng' => 19.4944348], + ], + 'Felsőszentiván' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1966179, 'lng' => 19.1873616], + ], + 'Foktő' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5268759, 'lng' => 18.9196874], + ], + 'Fülöpháza' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8914016, 'lng' => 19.4432493], + ], + 'Fülöpjakab' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.742058, 'lng' => 19.7227232], + ], + 'Fülöpszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8195701, 'lng' => 19.2372115], + ], + 'Gara' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0349999, 'lng' => 19.0393411], + ], + 'Gátér' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.680435, 'lng' => 19.9596412], + ], + 'Géderlak' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6072512, 'lng' => 18.9135762], + ], + 'Hajós' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4001409, 'lng' => 19.1193255], + ], + 'Harkakötöny' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4634053, 'lng' => 19.6069951], + ], + 'Harta' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6960997, 'lng' => 19.0328195], + ], + 'Helvécia' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8360977, 'lng' => 19.620438], + ], + 'Hercegszántó' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9482057, 'lng' => 18.9389127], + ], + 'Homokmégy' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4892762, 'lng' => 19.0730421], + ], + 'Imrehegy' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4867668, 'lng' => 19.3056372], + ], + 'Izsák' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8020009, 'lng' => 19.3546225], + ], + 'Jakabszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7602785, 'lng' => 19.6055301], + ], + 'Jánoshalma' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2974544, 'lng' => 19.3250656], + ], + 'Jászszentlászló' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.5672659, 'lng' => 19.7590541], + ], + 'Kalocsa' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5281229, 'lng' => 18.9840376], + ], + 'Kaskantyú' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6711891, 'lng' => 19.3895391], + ], + 'Katymár' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0344636, 'lng' => 19.2087609], + ], + 'Kecel' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5243135, 'lng' => 19.2451963], + ], + 'Kecskemét' => [ + 'constituencies' => ['Bács-Kiskun 2.', 'Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8963711, 'lng' => 19.6896861], + ], + 'Kelebia' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1958608, 'lng' => 19.6066291], + ], + 'Kéleshalom' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3641795, 'lng' => 19.2831241], + ], + 'Kerekegyháza' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9385747, 'lng' => 19.4770208], + ], + 'Kiskőrös' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6224967, 'lng' => 19.2874568], + ], + 'Kiskunfélegyháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7112802, 'lng' => 19.8515196], + ], + 'Kiskunhalas' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4354409, 'lng' => 19.4834284], + ], + 'Kiskunmajsa' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4904848, 'lng' => 19.7366569], + ], + 'Kisszállás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2791272, 'lng' => 19.4908079], + ], + 'Kömpöc' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4640167, 'lng' => 19.8665681], + ], + 'Kunadacs' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.956503, 'lng' => 19.2880496], + ], + 'Kunbaja' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.0848391, 'lng' => 19.4213713], + ], + 'Kunbaracs' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9891493, 'lng' => 19.3999584], + ], + 'Kunfehértó' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.362671, 'lng' => 19.4141949], + ], + 'Kunpeszér' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0611502, 'lng' => 19.2753764], + ], + 'Kunszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7627801, 'lng' => 19.7532925], + ], + 'Kunszentmiklós' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0244473, 'lng' => 19.1235997], + ], + 'Ladánybene' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0344239, 'lng' => 19.456807], + ], + 'Lajosmizse' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0248225, 'lng' => 19.5559232], + ], + 'Lakitelek' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8710339, 'lng' => 19.9930216], + ], + 'Madaras' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0554833, 'lng' => 19.2633403], + ], + 'Mátételke' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1614675, 'lng' => 19.2802263], + ], + 'Mélykút' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2132295, 'lng' => 19.3814176], + ], + 'Miske' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4434918, 'lng' => 19.0315752], + ], + 'Móricgát' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6233704, 'lng' => 19.6885382], + ], + 'Nagybaracska' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0444015, 'lng' => 18.9048387], + ], + 'Nemesnádudvar' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3348444, 'lng' => 19.0542114], + ], + 'Nyárlőrinc' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8611255, 'lng' => 19.8773125], + ], + 'Ordas' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6364524, 'lng' => 18.9504602], + ], + 'Öregcsertő' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.515272, 'lng' => 19.1090595], + ], + 'Orgovány' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7497582, 'lng' => 19.4746024], + ], + 'Páhi' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.7136232, 'lng' => 19.3856937], + ], + 'Pálmonostora' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6265115, 'lng' => 19.9425525], + ], + 'Petőfiszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6243457, 'lng' => 19.8596537], + ], + 'Pirtó' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5139604, 'lng' => 19.4301958], + ], + 'Rém' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2470804, 'lng' => 19.1416684], + ], + 'Solt' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8021967, 'lng' => 19.0108147], + ], + 'Soltszentimre' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.769786, 'lng' => 19.2840433], + ], + 'Soltvadkert' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5789287, 'lng' => 19.3938029], + ], + 'Sükösd' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2832039, 'lng' => 18.9942907], + ], + 'Szabadszállás' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8763076, 'lng' => 19.2232539], + ], + 'Szakmár' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5543652, 'lng' => 19.0742847], + ], + 'Szalkszentmárton' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9754928, 'lng' => 19.0171018], + ], + 'Szank' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.5557842, 'lng' => 19.6668956], + ], + 'Szentkirály' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.9169398, 'lng' => 19.9175371], + ], + 'Szeremle' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1436504, 'lng' => 18.8810207], + ], + 'Tabdi' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6818019, 'lng' => 19.3042672], + ], + 'Tass' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0184485, 'lng' => 19.0281253], + ], + 'Tataháza' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.173167, 'lng' => 19.3024716], + ], + 'Tázlár' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5509533, 'lng' => 19.5159844], + ], + 'Tiszaalpár' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8140236, 'lng' => 19.9936556], + ], + 'Tiszakécske' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.9358726, 'lng' => 20.0969279], + ], + 'Tiszaug' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8537215, 'lng' => 20.052921], + ], + 'Tompa' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2060507, 'lng' => 19.5389553], + ], + 'Újsolt' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8706098, 'lng' => 19.1186222], + ], + 'Újtelek' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5911716, 'lng' => 19.0564597], + ], + 'Uszód' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5704972, 'lng' => 18.9038275], + ], + 'Városföld' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8174844, 'lng' => 19.7597893], + ], + 'Vaskút' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1080968, 'lng' => 18.9861524], + ], + 'Zsana' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3802847, 'lng' => 19.6600846], + ], + ], + 'Baranya' => [ + 'Abaliget' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1428711, 'lng' => 18.1152298], + ], + 'Adorjás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8509119, 'lng' => 18.0617924], + ], + 'Ág' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2962836, 'lng' => 18.2023275], + ], + 'Almamellék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1603198, 'lng' => 17.8765681], + ], + 'Almáskeresztúr' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1199547, 'lng' => 17.8958453], + ], + 'Alsómocsolád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.313518, 'lng' => 18.2481993], + ], + 'Alsószentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7912208, 'lng' => 18.3065816], + ], + 'Apátvarasd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1856469, 'lng' => 18.47932], + ], + 'Aranyosgadány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.007757, 'lng' => 18.1195466], + ], + 'Áta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9367366, 'lng' => 18.2985608], + ], + 'Babarc' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0042229, 'lng' => 18.5527511], + ], + 'Babarcszőlős' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.898699, 'lng' => 18.1360284], + ], + 'Bakóca' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2074891, 'lng' => 18.0002016], + ], + 'Bakonya' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0850942, 'lng' => 18.082286], + ], + 'Baksa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9554293, 'lng' => 18.0909794], + ], + 'Bánfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.994691, 'lng' => 17.8798792], + ], + 'Bár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0482419, 'lng' => 18.7119502], + ], + 'Baranyahídvég' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8461886, 'lng' => 18.0229597], + ], + 'Baranyajenő' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2734519, 'lng' => 18.0469416], + ], + 'Baranyaszentgyörgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2461345, 'lng' => 18.0119839], + ], + 'Basal' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0734372, 'lng' => 17.7832659], + ], + 'Belvárdgyula' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9750659, 'lng' => 18.4288438], + ], + 'Beremend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7877528, 'lng' => 18.4322322], + ], + 'Berkesd' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0766759, 'lng' => 18.4078442], + ], + 'Besence' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8956421, 'lng' => 17.9654588], + ], + 'Bezedek' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8653948, 'lng' => 18.5854023], + ], + 'Bicsérd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0216488, 'lng' => 18.0779429], + ], + 'Bikal' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3329154, 'lng' => 18.2845332], + ], + 'Birján' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0007461, 'lng' => 18.3739733], + ], + 'Bisse' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9082449, 'lng' => 18.2603363], + ], + 'Boda' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0796449, 'lng' => 18.0477749], + ], + 'Bodolyabér' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.196906, 'lng' => 18.1189705], + ], + 'Bogád' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0858618, 'lng' => 18.3215439], + ], + 'Bogádmindszent' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9069292, 'lng' => 18.0382456], + ], + 'Bogdása' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8756825, 'lng' => 17.7892759], + ], + 'Boldogasszonyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1826055, 'lng' => 17.8379176], + ], + 'Bóly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9654045, 'lng' => 18.5166166], + ], + 'Borjád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9356423, 'lng' => 18.4708549], + ], + 'Bosta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9500492, 'lng' => 18.2104193], + ], + 'Botykapeterd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0499466, 'lng' => 17.8662441], + ], + 'Bükkösd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1100188, 'lng' => 17.9925218], + ], + 'Bürüs' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9653278, 'lng' => 17.7591739], + ], + 'Csányoszró' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8810774, 'lng' => 17.9101381], + ], + 'Csarnóta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8949174, 'lng' => 18.2163121], + ], + 'Csebény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1893582, 'lng' => 17.9275209], + ], + 'Cserdi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0808529, 'lng' => 17.9911191], + ], + 'Cserkút' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0756664, 'lng' => 18.1340119], + ], + 'Csertő' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.093457, 'lng' => 17.8034587], + ], + 'Csonkamindszent' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0518017, 'lng' => 17.9658056], + ], + 'Cún' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8122974, 'lng' => 18.0678543], + ], + 'Dencsháza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.993512, 'lng' => 17.8347772], + ], + 'Dinnyeberki' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0972962, 'lng' => 17.9563165], + ], + 'Diósviszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8774861, 'lng' => 18.1640495], + ], + 'Drávacsehi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8130167, 'lng' => 18.1666181], + ], + 'Drávacsepely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8308297, 'lng' => 18.1352308], + ], + 'Drávafok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8860365, 'lng' => 17.7636317], + ], + 'Drávaiványi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8470684, 'lng' => 17.8159164], + ], + 'Drávakeresztúr' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8386967, 'lng' => 17.7580104], + ], + 'Drávapalkonya' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8033438, 'lng' => 18.1790753], + ], + 'Drávapiski' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8396577, 'lng' => 18.0989657], + ], + 'Drávaszabolcs' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.803275, 'lng' => 18.2093234], + ], + 'Drávaszerdahely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8363562, 'lng' => 18.1638527], + ], + 'Drávasztára' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8230964, 'lng' => 17.8220692], + ], + 'Dunaszekcső' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0854783, 'lng' => 18.7542203], + ], + 'Egerág' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9834452, 'lng' => 18.3039561], + ], + 'Egyházasharaszti' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8097356, 'lng' => 18.3314381], + ], + 'Egyházaskozár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3319023, 'lng' => 18.3178591], + ], + 'Ellend' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0580138, 'lng' => 18.3760682], + ], + 'Endrőc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9296401, 'lng' => 17.7621758], + ], + 'Erdősmárok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.055568, 'lng' => 18.5458091], + ], + 'Erdősmecske' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1768439, 'lng' => 18.5109755], + ], + 'Erzsébet' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1004339, 'lng' => 18.4587621], + ], + 'Fazekasboda' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1230108, 'lng' => 18.4850924], + ], + 'Feked' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1626797, 'lng' => 18.5588015], + ], + 'Felsőegerszeg' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2539122, 'lng' => 18.1335751], + ], + 'Felsőszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8513101, 'lng' => 17.7034033], + ], + 'Garé' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9180881, 'lng' => 18.1956808], + ], + 'Gerde' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9904428, 'lng' => 18.0255496], + ], + 'Gerényes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.3070289, 'lng' => 18.1848981], + ], + 'Geresdlak' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1107897, 'lng' => 18.5268599], + ], + 'Gilvánfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9184356, 'lng' => 17.9622098], + ], + 'Gödre' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2899579, 'lng' => 17.9723779], + ], + 'Görcsöny' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9709725, 'lng' => 18.133486], + ], + 'Görcsönydoboka' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0709275, 'lng' => 18.6275109], + ], + 'Gordisa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7970748, 'lng' => 18.2354868], + ], + 'Gyód' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9979549, 'lng' => 18.1781638], + ], + 'Gyöngyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9601196, 'lng' => 17.9506649], + ], + 'Gyöngyösmellék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9868644, 'lng' => 17.7014751], + ], + 'Harkány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8534053, 'lng' => 18.2348372], + ], + 'Hásságy' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0330172, 'lng' => 18.388848], + ], + 'Hegyhátmaróc' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3109929, 'lng' => 18.3362487], + ], + 'Hegyszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9036373, 'lng' => 18.086797], + ], + 'Helesfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0894523, 'lng' => 17.9770167], + ], + 'Hetvehely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1332155, 'lng' => 18.0432466], + ], + 'Hidas' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2574631, 'lng' => 18.4937015], + ], + 'Himesháza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0797595, 'lng' => 18.5805933], + ], + 'Hirics' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8247516, 'lng' => 17.9934259], + ], + 'Hobol' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0197823, 'lng' => 17.7724266], + ], + 'Homorúd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.981847, 'lng' => 18.7887766], + ], + 'Horváthertelend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1751748, 'lng' => 17.9272893], + ], + 'Hosszúhetény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1583167, 'lng' => 18.3520974], + ], + 'Husztót' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1711511, 'lng' => 18.0932139], + ], + 'Ibafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1552456, 'lng' => 17.9179873], + ], + 'Illocska' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.800591, 'lng' => 18.5233576], + ], + 'Ipacsfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8345382, 'lng' => 18.2055561], + ], + 'Ivánbattyán' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9077809, 'lng' => 18.4176354], + ], + 'Ivándárda' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.831643, 'lng' => 18.5922589], + ], + 'Kacsóta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0390809, 'lng' => 17.9544689], + ], + 'Kákics' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9028359, 'lng' => 17.8568313], + ], + 'Kárász' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2667559, 'lng' => 18.3188548], + ], + 'Kásád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7793743, 'lng' => 18.3991912], + ], + 'Katádfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9970924, 'lng' => 17.8692171], + ], + 'Kátoly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0634292, 'lng' => 18.4496796], + ], + 'Kékesd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1007579, 'lng' => 18.4720006], + ], + 'Kémes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8241919, 'lng' => 18.1031607], + ], + 'Kemse' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8237775, 'lng' => 17.9119613], + ], + 'Keszü' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 46.0160053, 'lng' => 18.1918765], + ], + 'Kétújfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9643465, 'lng' => 17.7128738], + ], + 'Királyegyháza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9975029, 'lng' => 17.9670799], + ], + 'Kisasszonyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9467478, 'lng' => 18.0062386], + ], + 'Kisbeszterce' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2054937, 'lng' => 18.033257], + ], + 'Kisbudmér' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9132933, 'lng' => 18.4468642], + ], + 'Kisdér' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9397014, 'lng' => 18.1280256], + ], + 'Kisdobsza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0279686, 'lng' => 17.654966], + ], + 'Kishajmás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2000972, 'lng' => 18.0807394], + ], + 'Kisharsány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8597428, 'lng' => 18.3628602], + ], + 'Kisherend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9657006, 'lng' => 18.3308199], + ], + 'Kisjakabfalva' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8961294, 'lng' => 18.4347874], + ], + 'Kiskassa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9532763, 'lng' => 18.3984025], + ], + 'Kislippó' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8309942, 'lng' => 18.5387451], + ], + 'Kisnyárád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0369956, 'lng' => 18.5642298], + ], + 'Kisszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8245119, 'lng' => 18.0223384], + ], + 'Kistamási' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0118086, 'lng' => 17.7210893], + ], + 'Kistapolca' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8215113, 'lng' => 18.383003], + ], + 'Kistótfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9080691, 'lng' => 18.3097841], + ], + 'Kisvaszar' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2748571, 'lng' => 18.2126962], + ], + 'Köblény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2948258, 'lng' => 18.303697], + ], + 'Kökény' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9995372, 'lng' => 18.2057648], + ], + 'Kölked' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9489796, 'lng' => 18.7058024], + ], + 'Komló' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1929788, 'lng' => 18.2512139], + ], + 'Kórós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8666591, 'lng' => 18.0818986], + ], + 'Kovácshida' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8322528, 'lng' => 18.1852847], + ], + 'Kovácsszénája' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1714525, 'lng' => 18.1099753], + ], + 'Kővágószőlős' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0824433, 'lng' => 18.1242335], + ], + 'Kővágótöttös' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0859181, 'lng' => 18.1005597], + ], + 'Kozármisleny' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0412574, 'lng' => 18.2872228], + ], + 'Lánycsók' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0073964, 'lng' => 18.624077], + ], + 'Lapáncsa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8187417, 'lng' => 18.4965793], + ], + 'Liget' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2346633, 'lng' => 18.1924669], + ], + 'Lippó' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.863493, 'lng' => 18.5702136], + ], + 'Liptód' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.044203, 'lng' => 18.5153709], + ], + 'Lothárd' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0015129, 'lng' => 18.3534664], + ], + 'Lovászhetény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1573687, 'lng' => 18.4736022], + ], + 'Lúzsok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8386895, 'lng' => 17.9448893], + ], + 'Mágocs' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3507989, 'lng' => 18.2282954], + ], + 'Magyarbóly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8424536, 'lng' => 18.4905327], + ], + 'Magyaregregy' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2497645, 'lng' => 18.3080926], + ], + 'Magyarhertelend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1887919, 'lng' => 18.1496193], + ], + 'Magyarlukafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1692382, 'lng' => 17.7566367], + ], + 'Magyarmecske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9444333, 'lng' => 17.963957], + ], + 'Magyarsarlós' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0412482, 'lng' => 18.3527956], + ], + 'Magyarszék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1966719, 'lng' => 18.1955889], + ], + 'Magyartelek' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9438384, 'lng' => 17.9834231], + ], + 'Majs' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9090894, 'lng' => 18.59764], + ], + 'Mánfa' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1620219, 'lng' => 18.2424376], + ], + 'Maráza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0767639, 'lng' => 18.5102704], + ], + 'Márfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8597093, 'lng' => 18.184506], + ], + 'Máriakéménd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0275242, 'lng' => 18.4616888], + ], + 'Markóc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8633597, 'lng' => 17.7628134], + ], + 'Marócsa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9143499, 'lng' => 17.8155625], + ], + 'Márok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8776725, 'lng' => 18.5052153], + ], + 'Martonfa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1162762, 'lng' => 18.373108], + ], + 'Matty' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7959854, 'lng' => 18.2646823], + ], + 'Máza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2674701, 'lng' => 18.3987184], + ], + 'Mecseknádasd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.22466, 'lng' => 18.4653855], + ], + 'Mecsekpölöske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2232838, 'lng' => 18.2117379], + ], + 'Mekényes' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3905907, 'lng' => 18.3338629], + ], + 'Merenye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.069313, 'lng' => 17.6981454], + ], + 'Meződ' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2898147, 'lng' => 18.1028572], + ], + 'Mindszentgodisa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2270491, 'lng' => 18.070952], + ], + 'Mohács' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0046295, 'lng' => 18.6794304], + ], + 'Molvány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0294158, 'lng' => 17.7455964], + ], + 'Monyoród' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0115276, 'lng' => 18.4781726], + ], + 'Mozsgó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1148249, 'lng' => 17.8457585], + ], + 'Nagybudmér' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9378397, 'lng' => 18.4443309], + ], + 'Nagycsány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.871837, 'lng' => 17.9441308], + ], + 'Nagydobsza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0290366, 'lng' => 17.6672107], + ], + 'Nagyhajmás' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.372206, 'lng' => 18.2898052], + ], + 'Nagyharsány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8466947, 'lng' => 18.3947776], + ], + 'Nagykozár' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.067814, 'lng' => 18.316561], + ], + 'Nagynyárád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9447148, 'lng' => 18.578055], + ], + 'Nagypall' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1474016, 'lng' => 18.4539234], + ], + 'Nagypeterd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0459728, 'lng' => 17.8979423], + ], + 'Nagytótfalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8638406, 'lng' => 18.3426767], + ], + 'Nagyváty' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0617075, 'lng' => 17.93209], + ], + 'Nemeske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.020198, 'lng' => 17.7129695], + ], + 'Nyugotszenterzsébet' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0747959, 'lng' => 17.9096635], + ], + 'Óbánya' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2220338, 'lng' => 18.4084838], + ], + 'Ócsárd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9341296, 'lng' => 18.1533436], + ], + 'Ófalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2210918, 'lng' => 18.534029], + ], + 'Okorág' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9262423, 'lng' => 17.8761913], + ], + 'Okorvölgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.15235, 'lng' => 18.0600392], + ], + 'Olasz' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0128298, 'lng' => 18.4122965], + ], + 'Old' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7893924, 'lng' => 18.3526547], + ], + 'Orfű' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1504207, 'lng' => 18.1423992], + ], + 'Oroszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2201904, 'lng' => 18.122659], + ], + 'Ózdfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9288431, 'lng' => 18.0210679], + ], + 'Palé' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2603608, 'lng' => 18.0690432], + ], + 'Palkonya' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8968607, 'lng' => 18.3899099], + ], + 'Palotabozsok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1275672, 'lng' => 18.6416844], + ], + 'Páprád' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8927275, 'lng' => 18.0103745], + ], + 'Patapoklosi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0753051, 'lng' => 17.7415323], + ], + 'Pécs' => [ + 'constituencies' => ['Baranya 2.', 'Baranya 1.'], + 'coordinates' => ['lat' => 46.0727345, 'lng' => 18.232266], + ], + 'Pécsbagota' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9906469, 'lng' => 18.0728758], + ], + 'Pécsdevecser' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9585177, 'lng' => 18.3839237], + ], + 'Pécsudvard' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 46.0108323, 'lng' => 18.2750737], + ], + 'Pécsvárad' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1591341, 'lng' => 18.4185199], + ], + 'Pellérd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.034172, 'lng' => 18.1551531], + ], + 'Pereked' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0940085, 'lng' => 18.3768639], + ], + 'Peterd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9726228, 'lng' => 18.3606704], + ], + 'Pettend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0001576, 'lng' => 17.7011535], + ], + 'Piskó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8112973, 'lng' => 17.9384454], + ], + 'Pócsa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9100922, 'lng' => 18.4699792], + ], + 'Pogány' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9827333, 'lng' => 18.2568939], + ], + 'Rádfalva' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8598624, 'lng' => 18.1252323], + ], + 'Regenye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.969783, 'lng' => 18.1685228], + ], + 'Romonya' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0871177, 'lng' => 18.3391112], + ], + 'Rózsafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0227215, 'lng' => 17.8889708], + ], + 'Sámod' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8536384, 'lng' => 18.0384521], + ], + 'Sárok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8414254, 'lng' => 18.6119412], + ], + 'Sásd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2563232, 'lng' => 18.1024778], + ], + 'Sátorhely' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9417452, 'lng' => 18.6330768], + ], + 'Sellye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.873291, 'lng' => 17.8494986], + ], + 'Siklós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8555814, 'lng' => 18.2979721], + ], + 'Siklósbodony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9105251, 'lng' => 18.1202589], + ], + 'Siklósnagyfalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.820428, 'lng' => 18.3636246], + ], + 'Somberek' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0812348, 'lng' => 18.6586781], + ], + 'Somogyapáti' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0920041, 'lng' => 17.7506787], + ], + 'Somogyhárságy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1623103, 'lng' => 17.7731873], + ], + 'Somogyhatvan' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1120284, 'lng' => 17.7126553], + ], + 'Somogyviszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1146313, 'lng' => 17.7636375], + ], + 'Sósvertike' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8340815, 'lng' => 17.8614028], + ], + 'Sumony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9675435, 'lng' => 17.9146319], + ], + 'Szabadszentkirály' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0059012, 'lng' => 18.0435247], + ], + 'Szágy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2244706, 'lng' => 17.9469817], + ], + 'Szajk' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9921175, 'lng' => 18.5328986], + ], + 'Szalánta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9471908, 'lng' => 18.2376181], + ], + 'Szalatnak' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2903675, 'lng' => 18.2809735], + ], + 'Szaporca' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8135724, 'lng' => 18.1045054], + ], + 'Szárász' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3487743, 'lng' => 18.3727487], + ], + 'Szászvár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2739639, 'lng' => 18.3774781], + ], + 'Szava' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9024581, 'lng' => 18.1738569], + ], + 'Szebény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1296283, 'lng' => 18.5879918], + ], + 'Szederkény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9986735, 'lng' => 18.4530663], + ], + 'Székelyszabar' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0471326, 'lng' => 18.6012321], + ], + 'Szellő' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0744167, 'lng' => 18.4609549], + ], + 'Szemely' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0083381, 'lng' => 18.3256717], + ], + 'Szentdénes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0079644, 'lng' => 17.9271651], + ], + 'Szentegát' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9754975, 'lng' => 17.8244079], + ], + 'Szentkatalin' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.174384, 'lng' => 18.0505714], + ], + 'Szentlászló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1540417, 'lng' => 17.8331512], + ], + 'Szentlőrinc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0403123, 'lng' => 17.9897756], + ], + 'Szigetvár' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0487727, 'lng' => 17.7983466], + ], + 'Szilágy' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1009525, 'lng' => 18.4065405], + ], + 'Szilvás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9616358, 'lng' => 18.1981701], + ], + 'Szőke' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9604273, 'lng' => 18.1867423], + ], + 'Szőkéd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9645154, 'lng' => 18.2884592], + ], + 'Szörény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9683861, 'lng' => 17.6819713], + ], + 'Szulimán' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1264433, 'lng' => 17.805449], + ], + 'Szűr' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.099254, 'lng' => 18.5809615], + ], + 'Tarrós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2806564, 'lng' => 18.1425225], + ], + 'Tékes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2866262, 'lng' => 18.1744149], + ], + 'Teklafalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9493136, 'lng' => 17.7287585], + ], + 'Tengeri' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9263477, 'lng' => 18.087938], + ], + 'Tésenfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8127763, 'lng' => 18.1178921], + ], + 'Téseny' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9515499, 'lng' => 18.0479966], + ], + 'Tófű' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3094872, 'lng' => 18.3576794], + ], + 'Tormás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2309543, 'lng' => 17.9937201], + ], + 'Tótszentgyörgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0521798, 'lng' => 17.7178541], + ], + 'Töttös' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9150433, 'lng' => 18.5407584], + ], + 'Túrony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9054082, 'lng' => 18.2309533], + ], + 'Udvar' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.900472, 'lng' => 18.6594842], + ], + 'Újpetre' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.934779, 'lng' => 18.3636323], + ], + 'Vajszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8592442, 'lng' => 17.9868205], + ], + 'Várad' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9743574, 'lng' => 17.7456586], + ], + 'Varga' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2475508, 'lng' => 18.1424694], + ], + 'Vásárosbéc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1825351, 'lng' => 17.7246441], + ], + 'Vásárosdombó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.3064752, 'lng' => 18.1334675], + ], + 'Vázsnok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2653395, 'lng' => 18.1253751], + ], + 'Vejti' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8096089, 'lng' => 17.9682522], + ], + 'Vékény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2695945, 'lng' => 18.3423454], + ], + 'Velény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9807601, 'lng' => 18.0514344], + ], + 'Véménd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1551161, 'lng' => 18.6190866], + ], + 'Versend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9953039, 'lng' => 18.5115869], + ], + 'Villány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8700399, 'lng' => 18.453201], + ], + 'Villánykövesd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8823189, 'lng' => 18.425812], + ], + 'Vokány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9133714, 'lng' => 18.3364685], + ], + 'Zádor' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9623692, 'lng' => 17.6579278], + ], + 'Zaláta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8111976, 'lng' => 17.8901202], + ], + 'Zengővárkony' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1728638, 'lng' => 18.4320077], + ], + 'Zók' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0104261, 'lng' => 18.0965422], + ], + ], + 'Békés' => [ + 'Almáskamarás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4617785, 'lng' => 21.092448], + ], + 'Battonya' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.2902462, 'lng' => 21.0199215], + ], + 'Békés' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.6704899, 'lng' => 21.0434996], + ], + 'Békéscsaba' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6735939, 'lng' => 21.0877309], + ], + 'Békéssámson' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4208677, 'lng' => 20.6176498], + ], + 'Békésszentandrás' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8715996, 'lng' => 20.48336], + ], + 'Bélmegyer' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8726019, 'lng' => 21.1832832], + ], + 'Biharugra' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9691009, 'lng' => 21.5987651], + ], + 'Bucsa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.2047017, 'lng' => 20.9970391], + ], + 'Csabacsűd' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8244161, 'lng' => 20.6485242], + ], + 'Csabaszabadi' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.574811, 'lng' => 20.951145], + ], + 'Csanádapáca' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5409397, 'lng' => 20.8852553], + ], + 'Csárdaszállás' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8647568, 'lng' => 20.9374853], + ], + 'Csorvás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6308376, 'lng' => 20.8340929], + ], + 'Dévaványa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.0313217, 'lng' => 20.9595443], + ], + 'Doboz' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7343152, 'lng' => 21.2420659], + ], + 'Dombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3415879, 'lng' => 21.1342664], + ], + 'Dombiratos' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4195218, 'lng' => 21.1178789], + ], + 'Ecsegfalva' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.14789, 'lng' => 20.9239261], + ], + 'Elek' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.5291929, 'lng' => 21.2487556], + ], + 'Füzesgyarmat' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.1051107, 'lng' => 21.2108329], + ], + 'Gádoros' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.6667476, 'lng' => 20.5961159], + ], + 'Gerendás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5969212, 'lng' => 20.8593687], + ], + 'Geszt' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8831763, 'lng' => 21.5794915], + ], + 'Gyomaendrőd' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.9317797, 'lng' => 20.8113125], + ], + 'Gyula' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.6473027, 'lng' => 21.2784255], + ], + 'Hunya' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.812869, 'lng' => 20.8458337], + ], + 'Kamut' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.7619186, 'lng' => 20.9798143], + ], + 'Kardos' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.7941712, 'lng' => 20.715629], + ], + 'Kardoskút' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.498573, 'lng' => 20.7040158], + ], + 'Kaszaper' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4598817, 'lng' => 20.8251944], + ], + 'Kertészsziget' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.1542945, 'lng' => 21.0610234], + ], + 'Kétegyháza' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5417887, 'lng' => 21.1810736], + ], + 'Kétsoprony' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.7208319, 'lng' => 20.8870273], + ], + 'Kevermes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4167579, 'lng' => 21.1818484], + ], + 'Kisdombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3693244, 'lng' => 21.0996778], + ], + 'Kondoros' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.7574628, 'lng' => 20.7972363], + ], + 'Körösladány' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.9607513, 'lng' => 21.0767574], + ], + 'Körösnagyharsány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.0080391, 'lng' => 21.6417355], + ], + 'Köröstarcsa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8780314, 'lng' => 21.02402], + ], + 'Körösújfalu' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9659419, 'lng' => 21.3988486], + ], + 'Kötegyán' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.738284, 'lng' => 21.481692], + ], + 'Kunágota' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4234015, 'lng' => 21.0467553], + ], + 'Lőkösháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4297019, 'lng' => 21.2318793], + ], + 'Magyarbánhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4577279, 'lng' => 20.968734], + ], + 'Magyardombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3794548, 'lng' => 21.0743712], + ], + 'Medgyesbodzás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5186797, 'lng' => 20.9596371], + ], + 'Medgyesegyháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4967576, 'lng' => 21.0271996], + ], + 'Méhkerék' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7735176, 'lng' => 21.4435935], + ], + 'Mezőberény' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.825687, 'lng' => 21.0243614], + ], + 'Mezőgyán' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8709809, 'lng' => 21.5257366], + ], + 'Mezőhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3172449, 'lng' => 20.8173892], + ], + 'Mezőkovácsháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4093003, 'lng' => 20.9112692], + ], + 'Murony' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.760463, 'lng' => 21.0411739], + ], + 'Nagybánhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.460095, 'lng' => 20.902578], + ], + 'Nagykamarás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4727168, 'lng' => 21.1213871], + ], + 'Nagyszénás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.6722161, 'lng' => 20.6734381], + ], + 'Okány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8982798, 'lng' => 21.3467384], + ], + 'Örménykút' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.830573, 'lng' => 20.7344497], + ], + 'Orosháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5684222, 'lng' => 20.6544927], + ], + 'Pusztaföldvár' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5251751, 'lng' => 20.8024526], + ], + 'Pusztaottlaka' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5386606, 'lng' => 21.0060316], + ], + 'Sarkad' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7374245, 'lng' => 21.3810771], + ], + 'Sarkadkeresztúr' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8107081, 'lng' => 21.3841932], + ], + 'Szabadkígyós' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.601522, 'lng' => 21.0753003], + ], + 'Szarvas' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8635641, 'lng' => 20.5526535], + ], + 'Szeghalom' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.0239347, 'lng' => 21.1666571], + ], + 'Tarhos' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8132012, 'lng' => 21.2109597], + ], + 'Telekgerendás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6566167, 'lng' => 20.9496242], + ], + 'Tótkomlós' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4107596, 'lng' => 20.7363644], + ], + 'Újkígyós' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5899757, 'lng' => 21.0242728], + ], + 'Újszalonta' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8128247, 'lng' => 21.4908762], + ], + 'Végegyháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3882623, 'lng' => 20.8699923], + ], + 'Vésztő' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9244546, 'lng' => 21.2628502], + ], + 'Zsadány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9230248, 'lng' => 21.4873156], + ], + ], + 'Borsod-Abaúj-Zemplén' => [ + 'Abaújalpár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3065157, 'lng' => 21.232147], + ], + 'Abaújkér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3033478, 'lng' => 21.2013068], + ], + 'Abaújlak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4051818, 'lng' => 20.9548056], + ], + 'Abaújszántó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2792184, 'lng' => 21.1874523], + ], + 'Abaújszolnok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3730791, 'lng' => 20.9749255], + ], + 'Abaújvár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5266538, 'lng' => 21.3150208], + ], + 'Abod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3928646, 'lng' => 20.7923344], + ], + 'Aggtelek' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4686657, 'lng' => 20.5040699], + ], + 'Alacska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2157484, 'lng' => 20.6502945], + ], + 'Alsóberecki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3437614, 'lng' => 21.6905164], + ], + 'Alsódobsza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1799523, 'lng' => 21.0026817], + ], + 'Alsógagy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4052855, 'lng' => 21.0255485], + ], + 'Alsóregmec' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4634336, 'lng' => 21.6181953], + ], + 'Alsószuha' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3726027, 'lng' => 20.5044038], + ], + 'Alsótelekes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4105212, 'lng' => 20.6547156], + ], + 'Alsóvadász' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2401438, 'lng' => 20.9043765], + ], + 'Alsózsolca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.0748263, 'lng' => 20.8850624], + ], + 'Arka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3562385, 'lng' => 21.252529], + ], + 'Arló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1746548, 'lng' => 20.2560308], + ], + 'Arnót' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1319962, 'lng' => 20.859401], + ], + 'Ároktő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7284812, 'lng' => 20.9423131], + ], + 'Aszaló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2177554, 'lng' => 20.9624804], + ], + 'Baktakék' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3675199, 'lng' => 21.0288911], + ], + 'Balajt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3210349, 'lng' => 20.7866111], + ], + 'Bánhorváti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2260139, 'lng' => 20.504815], + ], + 'Bánréve' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2986902, 'lng' => 20.3560194], + ], + 'Baskó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3326787, 'lng' => 21.336418], + ], + 'Becskeháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5294979, 'lng' => 20.8354743], + ], + 'Bekecs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1534102, 'lng' => 21.1762263], + ], + 'Berente' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2385836, 'lng' => 20.6700776], + ], + 'Beret' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3458722, 'lng' => 21.0235103], + ], + 'Berzék' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0240535, 'lng' => 20.9528886], + ], + 'Bőcs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0442332, 'lng' => 20.9683874], + ], + 'Bodroghalom' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3009977, 'lng' => 21.707044], + ], + 'Bodrogkeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1630176, 'lng' => 21.3595899], + ], + 'Bodrogkisfalud' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1789303, 'lng' => 21.3617788], + ], + 'Bodrogolaszi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2867085, 'lng' => 21.5160527], + ], + 'Bódvalenke' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5424028, 'lng' => 20.8041838], + ], + 'Bódvarákó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5111514, 'lng' => 20.7358047], + ], + 'Bódvaszilas' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5377629, 'lng' => 20.7312757], + ], + 'Bogács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9030764, 'lng' => 20.5312356], + ], + 'Boldogkőújfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3193629, 'lng' => 21.242022], + ], + 'Boldogkőváralja' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3380634, 'lng' => 21.2367554], + ], + 'Boldva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.218091, 'lng' => 20.7886144], + ], + 'Borsodbóta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2121829, 'lng' => 20.3960602], + ], + 'Borsodgeszt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9559428, 'lng' => 20.6944004], + ], + 'Borsodivánka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.701045, 'lng' => 20.6547148], + ], + 'Borsodnádasd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1191717, 'lng' => 20.2529566], + ], + 'Borsodszentgyörgy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1892068, 'lng' => 20.2073894], + ], + 'Borsodszirák' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2610318, 'lng' => 20.7676252], + ], + 'Bózsva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4743356, 'lng' => 21.468268], + ], + 'Bükkábrány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8884157, 'lng' => 20.6810544], + ], + 'Bükkaranyos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9866329, 'lng' => 20.7794609], + ], + 'Bükkmogyorósd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1291531, 'lng' => 20.3563552], + ], + 'Bükkszentkereszt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0668164, 'lng' => 20.6324773], + ], + 'Bükkzsérc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9587559, 'lng' => 20.5025627], + ], + 'Büttös' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4783127, 'lng' => 21.0110122], + ], + 'Cigánd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2558937, 'lng' => 21.8889241], + ], + 'Csenyéte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4345165, 'lng' => 21.0412334], + ], + 'Cserépfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9413093, 'lng' => 20.5347083], + ], + 'Cserépváralja' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9325883, 'lng' => 20.5598918], + ], + 'Csernely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1438586, 'lng' => 20.3390005], + ], + 'Csincse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8883234, 'lng' => 20.768705], + ], + 'Csobád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2796877, 'lng' => 21.0269782], + ], + 'Csobaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0485163, 'lng' => 21.3382189], + ], + 'Csokvaomány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1666711, 'lng' => 20.3744746], + ], + 'Damak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3168034, 'lng' => 20.8216124], + ], + 'Dámóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3748294, 'lng' => 22.0336128], + ], + 'Debréte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5000066, 'lng' => 20.8661035], + ], + 'Dédestapolcsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1804582, 'lng' => 20.4850166], + ], + 'Detek' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3336841, 'lng' => 21.0176305], + ], + 'Domaháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1836193, 'lng' => 20.1055583], + ], + 'Dövény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3469512, 'lng' => 20.5431344], + ], + 'Dubicsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2837745, 'lng' => 20.4940325], + ], + 'Edelény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2934391, 'lng' => 20.7385817], + ], + 'Egerlövő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7203221, 'lng' => 20.6175935], + ], + 'Égerszög' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.442896, 'lng' => 20.5875195], + ], + 'Emőd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9380038, 'lng' => 20.8154444], + ], + 'Encs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3259442, 'lng' => 21.1133006], + ], + 'Erdőbénye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2662769, 'lng' => 21.3547995], + ], + 'Erdőhorváti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3158739, 'lng' => 21.4272709], + ], + 'Fáj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4219028, 'lng' => 21.0747972], + ], + 'Fancsal' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3552347, 'lng' => 21.064671], + ], + 'Farkaslyuk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1876627, 'lng' => 20.3086509], + ], + 'Felsőberecki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3595718, 'lng' => 21.6950761], + ], + 'Felsődobsza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2555859, 'lng' => 21.0764245], + ], + 'Felsőgagy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4289932, 'lng' => 21.0128468], + ], + 'Felsőkelecsény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3600051, 'lng' => 20.5939689], + ], + 'Felsőnyárád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3299583, 'lng' => 20.5995966], + ], + 'Felsőregmec' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4915243, 'lng' => 21.6056225], + ], + 'Felsőtelekes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4058831, 'lng' => 20.6352386], + ], + 'Felsővadász' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3709811, 'lng' => 20.9195765], + ], + 'Felsőzsolca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1041265, 'lng' => 20.8595396], + ], + 'Filkeháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4960919, 'lng' => 21.4888024], + ], + 'Fony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3910341, 'lng' => 21.2865504], + ], + 'Forró' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3233535, 'lng' => 21.0880493], + ], + 'Fulókércs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4308674, 'lng' => 21.1049891], + ], + 'Füzér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.539654, 'lng' => 21.4547936], + ], + 'Füzérkajata' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5182556, 'lng' => 21.5000318], + ], + 'Füzérkomlós' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5126205, 'lng' => 21.4532344], + ], + 'Füzérradvány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.483741, 'lng' => 21.530474], + ], + 'Gadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4006289, 'lng' => 20.9296444], + ], + 'Gagyapáti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.409096, 'lng' => 21.0017182], + ], + 'Gagybátor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.433303, 'lng' => 20.94859], + ], + 'Gagyvendégi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4285166, 'lng' => 20.972405], + ], + 'Galvács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4190767, 'lng' => 20.7767621], + ], + 'Garadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4174625, 'lng' => 21.17463], + ], + 'Gelej' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.828655, 'lng' => 20.7755503], + ], + 'Gesztely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1026673, 'lng' => 20.9654647], + ], + 'Gibárt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3153245, 'lng' => 21.1603909], + ], + 'Girincs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9691368, 'lng' => 20.9846965], + ], + 'Golop' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2374312, 'lng' => 21.1893372], + ], + 'Gömörszőlős' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3730427, 'lng' => 20.4276758], + ], + 'Gönc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4727097, 'lng' => 21.2735417], + ], + 'Göncruszka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4488786, 'lng' => 21.239774], + ], + 'Györgytarló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2053902, 'lng' => 21.6316333], + ], + 'Halmaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2464584, 'lng' => 20.9983349], + ], + 'Hangács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2896949, 'lng' => 20.8314625], + ], + 'Hangony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2290868, 'lng' => 20.198029], + ], + 'Háromhuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3780662, 'lng' => 21.4283347], + ], + 'Harsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9679177, 'lng' => 20.7418041], + ], + 'Hegymeg' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3314259, 'lng' => 20.8614048], + ], + 'Hejce' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4234865, 'lng' => 21.2816978], + ], + 'Hejőbába' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9059201, 'lng' => 20.9452436], + ], + 'Hejőkeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9610209, 'lng' => 20.8772681], + ], + 'Hejőkürt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8564708, 'lng' => 20.9930661], + ], + 'Hejőpapi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8972354, 'lng' => 20.9054713], + ], + 'Hejőszalonta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9388389, 'lng' => 20.8822344], + ], + 'Hercegkút' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3340476, 'lng' => 21.5301233], + ], + 'Hernádbűd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2966038, 'lng' => 21.137896], + ], + 'Hernádcéce' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3587807, 'lng' => 21.1976117], + ], + 'Hernádkak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0892117, 'lng' => 20.9635617], + ], + 'Hernádkércs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2420151, 'lng' => 21.0501362], + ], + 'Hernádnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0716822, 'lng' => 20.9742345], + ], + 'Hernádpetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4815086, 'lng' => 21.1622472], + ], + 'Hernádszentandrás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2890724, 'lng' => 21.0949074], + ], + 'Hernádszurdok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.48169, 'lng' => 21.2071561], + ], + 'Hernádvécse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4406714, 'lng' => 21.1687099], + ], + 'Hét' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.282992, 'lng' => 20.3875674], + ], + 'Hidasnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5029778, 'lng' => 21.2293013], + ], + 'Hidvégardó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5598883, 'lng' => 20.8395348], + ], + 'Hollóháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5393716, 'lng' => 21.4144474], + ], + 'Homrogd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2834505, 'lng' => 20.9125329], + ], + 'Igrici' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8673926, 'lng' => 20.8831705], + ], + 'Imola' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4201572, 'lng' => 20.5516409], + ], + 'Ináncs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2861362, 'lng' => 21.0681971], + ], + 'Irota' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3964482, 'lng' => 20.8752667], + ], + 'Izsófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3087892, 'lng' => 20.6536072], + ], + 'Jákfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3316408, 'lng' => 20.569496], + ], + 'Járdánháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1551033, 'lng' => 20.2477262], + ], + 'Jósvafő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4826254, 'lng' => 20.5504479], + ], + 'Kács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9574786, 'lng' => 20.6145847], + ], + 'Kánó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4276397, 'lng' => 20.5991681], + ], + 'Kány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5151651, 'lng' => 21.0143542], + ], + 'Karcsa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3131571, 'lng' => 21.7953512], + ], + 'Karos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3312141, 'lng' => 21.7406654], + ], + 'Kazincbarcika' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2489437, 'lng' => 20.6189771], + ], + 'Kázsmárk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2728658, 'lng' => 20.9760294], + ], + 'Kéked' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5447244, 'lng' => 21.3500526], + ], + 'Kelemér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3551802, 'lng' => 20.4296357], + ], + 'Kenézlő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2004193, 'lng' => 21.5311235], + ], + 'Keresztéte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4989547, 'lng' => 20.950696], + ], + 'Kesznyéten' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9694339, 'lng' => 21.0413905], + ], + 'Királd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2393694, 'lng' => 20.3764361], + ], + 'Kiscsécs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9678112, 'lng' => 21.011133], + ], + 'Kisgyőr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0096251, 'lng' => 20.6874073], + ], + 'Kishuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4503449, 'lng' => 21.4814089], + ], + 'Kiskinizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2508135, 'lng' => 21.0345918], + ], + 'Kisrozvágy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3491303, 'lng' => 21.9390758], + ], + 'Kissikátor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1946631, 'lng' => 20.1302306], + ], + 'Kistokaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0397115, 'lng' => 20.8410079], + ], + 'Komjáti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5452009, 'lng' => 20.7618268], + ], + 'Komlóska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3404486, 'lng' => 21.4622875], + ], + 'Kondó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1880491, 'lng' => 20.6438586], + ], + 'Korlát' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3779667, 'lng' => 21.2457327], + ], + 'Köröm' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9842491, 'lng' => 20.9545886], + ], + 'Kovácsvágás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.45352, 'lng' => 21.5283164], + ], + 'Krasznokvajda' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4705256, 'lng' => 20.9714153], + ], + 'Kupa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3316226, 'lng' => 20.9145594], + ], + 'Kurityán' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.310505, 'lng' => 20.62573], + ], + 'Lácacséke' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3664002, 'lng' => 21.9934562], + ], + 'Ládbesenyő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3432268, 'lng' => 20.7859308], + ], + 'Lak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3480907, 'lng' => 20.8662135], + ], + 'Legyesbénye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1564545, 'lng' => 21.1530692], + ], + 'Léh' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2906948, 'lng' => 20.9807054], + ], + 'Lénárddaróc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1486722, 'lng' => 20.3728301], + ], + 'Litka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4544802, 'lng' => 21.0584273], + ], + 'Mád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1922445, 'lng' => 21.2759773], + ], + 'Makkoshotyka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3571928, 'lng' => 21.5164187], + ], + 'Mályi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0175678, 'lng' => 20.8292414], + ], + 'Mályinka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1545567, 'lng' => 20.4958901], + ], + 'Martonyi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4702379, 'lng' => 20.7660532], + ], + 'Megyaszó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1875185, 'lng' => 21.0547033], + ], + 'Méra' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3565901, 'lng' => 21.1469291], + ], + 'Meszes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.438651, 'lng' => 20.7950688], + ], + 'Mezőcsát' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8207081, 'lng' => 20.9051607], + ], + 'Mezőkeresztes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8262301, 'lng' => 20.6884043], + ], + 'Mezőkövesd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8074617, 'lng' => 20.5698525], + ], + 'Mezőnagymihály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8062776, 'lng' => 20.7308177], + ], + 'Mezőnyárád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8585625, 'lng' => 20.6764688], + ], + 'Mezőzombor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1501209, 'lng' => 21.2575954], + ], + 'Mikóháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4617944, 'lng' => 21.592572], + ], + 'Miskolc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.', 'Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1034775, 'lng' => 20.7784384], + ], + 'Mogyoróska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3759799, 'lng' => 21.3296401], + ], + 'Monaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3061021, 'lng' => 20.9348205], + ], + 'Monok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2099439, 'lng' => 21.149252], + ], + 'Múcsony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2758139, 'lng' => 20.6716209], + ], + 'Muhi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9778997, 'lng' => 20.9293321], + ], + 'Nagybarca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2476865, 'lng' => 20.5280319], + ], + 'Nagycsécs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9601505, 'lng' => 20.9482798], + ], + 'Nagyhuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4290026, 'lng' => 21.492424], + ], + 'Nagykinizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2344766, 'lng' => 21.0335706], + ], + 'Nagyrozvágy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3404683, 'lng' => 21.9228458], + ], + 'Négyes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7013, 'lng' => 20.7040224], + ], + 'Nekézseny' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1689694, 'lng' => 20.4291357], + ], + 'Nemesbikk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8876867, 'lng' => 20.9661155], + ], + 'Novajidrány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.396674, 'lng' => 21.1688256], + ], + 'Nyékládháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9933002, 'lng' => 20.8429935], + ], + 'Nyésta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3702622, 'lng' => 20.9514276], + ], + 'Nyíri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4986982, 'lng' => 21.440883], + ], + 'Nyomár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.275559, 'lng' => 20.8198353], + ], + 'Olaszliszka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2419377, 'lng' => 21.4279754], + ], + 'Onga' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1194769, 'lng' => 20.9065655], + ], + 'Ónod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0024425, 'lng' => 20.9146535], + ], + 'Ormosbánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3322064, 'lng' => 20.6493181], + ], + 'Oszlár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.8740321, 'lng' => 21.0332202], + ], + 'Ózd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2241439, 'lng' => 20.2888698], + ], + 'Pácin' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3306334, 'lng' => 21.8337743], + ], + 'Pálháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4717353, 'lng' => 21.507078], + ], + 'Pamlény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.493024, 'lng' => 20.9282949], + ], + 'Pányok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5298401, 'lng' => 21.3478472], + ], + 'Parasznya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1688229, 'lng' => 20.6402064], + ], + 'Pere' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2845544, 'lng' => 21.1211586], + ], + 'Perecse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5027869, 'lng' => 20.9845634], + ], + 'Perkupa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4712725, 'lng' => 20.6862819], + ], + 'Prügy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0824191, 'lng' => 21.2428751], + ], + 'Pusztafalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5439277, 'lng' => 21.4860599], + ], + 'Pusztaradvány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4679248, 'lng' => 21.1338715], + ], + 'Putnok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2939007, 'lng' => 20.4333508], + ], + 'Radostyán' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1787774, 'lng' => 20.6532017], + ], + 'Ragály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4041753, 'lng' => 20.5211463], + ], + 'Rakaca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4617206, 'lng' => 20.8848555], + ], + 'Rakacaszend' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4611034, 'lng' => 20.8378744], + ], + 'Rásonysápberencs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.304802, 'lng' => 20.9934828], + ], + 'Rátka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2156932, 'lng' => 21.2267141], + ], + 'Regéc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.392191, 'lng' => 21.3436481], + ], + 'Répáshuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0507939, 'lng' => 20.5254934], + ], + 'Révleányvár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3230427, 'lng' => 22.0416695], + ], + 'Ricse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3251432, 'lng' => 21.9687588], + ], + 'Rudabánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3747405, 'lng' => 20.6206118], + ], + 'Rudolftelep' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3092868, 'lng' => 20.6711602], + ], + 'Sajóbábony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1742691, 'lng' => 20.734572], + ], + 'Sajóecseg' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.190065, 'lng' => 20.772827], + ], + 'Sajógalgóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2929878, 'lng' => 20.5323886], + ], + 'Sajóhídvég' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0026817, 'lng' => 20.9495863], + ], + 'Sajóivánka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2654174, 'lng' => 20.5799268], + ], + 'Sajókápolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1952827, 'lng' => 20.6848853], + ], + 'Sajókaza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2864119, 'lng' => 20.5851277], + ], + 'Sajókeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1694996, 'lng' => 20.7768886], + ], + 'Sajólád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0402765, 'lng' => 20.9024513], + ], + 'Sajólászlófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1848765, 'lng' => 20.6736002], + ], + 'Sajómercse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2461305, 'lng' => 20.414773], + ], + 'Sajónémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.270659, 'lng' => 20.3811845], + ], + 'Sajóörös' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9515653, 'lng' => 21.0219599], + ], + 'Sajópálfala' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.163139, 'lng' => 20.8458093], + ], + 'Sajópetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0351497, 'lng' => 20.8878767], + ], + 'Sajópüspöki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.280186, 'lng' => 20.3400614], + ], + 'Sajósenye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1960682, 'lng' => 20.8185281], + ], + 'Sajószentpéter' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2188772, 'lng' => 20.7092248], + ], + 'Sajószöged' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9458004, 'lng' => 20.9946112], + ], + 'Sajóvámos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1802021, 'lng' => 20.8298154], + ], + 'Sajóvelezd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2714818, 'lng' => 20.4593985], + ], + 'Sály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9527979, 'lng' => 20.6597197], + ], + 'Sárazsadány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2684871, 'lng' => 21.497789], + ], + 'Sárospatak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3196929, 'lng' => 21.5687308], + ], + 'Sáta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1876567, 'lng' => 20.3914051], + ], + 'Sátoraljaújhely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3960601, 'lng' => 21.6551122], + ], + 'Selyeb' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3381582, 'lng' => 20.9541317], + ], + 'Semjén' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3521396, 'lng' => 21.9671011], + ], + 'Serényfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3071589, 'lng' => 20.3852844], + ], + 'Sima' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2996969, 'lng' => 21.3030527], + ], + 'Sóstófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.156243, 'lng' => 20.9870638], + ], + 'Szakácsi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3820531, 'lng' => 20.8614571], + ], + 'Szakáld' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9431182, 'lng' => 20.908997], + ], + 'Szalaszend' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3859709, 'lng' => 21.1243501], + ], + 'Szalonna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4500484, 'lng' => 20.7394926], + ], + 'Szászfa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4704359, 'lng' => 20.9418168], + ], + 'Szegi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.1953737, 'lng' => 21.3795562], + ], + 'Szegilong' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2162488, 'lng' => 21.3965639], + ], + 'Szemere' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4661495, 'lng' => 21.099542], + ], + 'Szendrő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4046962, 'lng' => 20.7282046], + ], + 'Szendrőlád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3433366, 'lng' => 20.7419436], + ], + 'Szentistván' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7737632, 'lng' => 20.6579694], + ], + 'Szentistvánbaksa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2227558, 'lng' => 21.0276456], + ], + 'Szerencs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1590429, 'lng' => 21.2048872], + ], + 'Szikszó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1989312, 'lng' => 20.9298039], + ], + 'Szin' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4972791, 'lng' => 20.6601922], + ], + 'Szinpetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4847097, 'lng' => 20.625043], + ], + 'Szirmabesenyő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1509585, 'lng' => 20.7957903], + ], + 'Szögliget' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5215045, 'lng' => 20.6770697], + ], + 'Szőlősardó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.443484, 'lng' => 20.6278686], + ], + 'Szomolya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8919105, 'lng' => 20.4949334], + ], + 'Szuhafő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4082703, 'lng' => 20.4515974], + ], + 'Szuhakálló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2835218, 'lng' => 20.6523991], + ], + 'Szuhogy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3842029, 'lng' => 20.6731282], + ], + 'Taktabáj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0621903, 'lng' => 21.3112131], + ], + 'Taktaharkány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0876121, 'lng' => 21.129918], + ], + 'Taktakenéz' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0508677, 'lng' => 21.2167146], + ], + 'Taktaszada' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1103437, 'lng' => 21.1735733], + ], + 'Tállya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2352295, 'lng' => 21.2260996], + ], + 'Tarcal' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1311328, 'lng' => 21.3418021], + ], + 'Tard' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8784711, 'lng' => 20.598937], + ], + 'Tardona' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1699442, 'lng' => 20.531454], + ], + 'Telkibánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4854061, 'lng' => 21.3574907], + ], + 'Teresztenye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4463436, 'lng' => 20.6031689], + ], + 'Tibolddaróc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9206758, 'lng' => 20.6355357], + ], + 'Tiszabábolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.689752, 'lng' => 20.813906], + ], + 'Tiszacsermely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2336812, 'lng' => 21.7945686], + ], + 'Tiszadorogma' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.6839826, 'lng' => 20.8661184], + ], + 'Tiszakarád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2061184, 'lng' => 21.7213149], + ], + 'Tiszakeszi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7879554, 'lng' => 20.9904672], + ], + 'Tiszaladány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0621067, 'lng' => 21.4101619], + ], + 'Tiszalúc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0358262, 'lng' => 21.0648204], + ], + 'Tiszapalkonya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.8849204, 'lng' => 21.0557818], + ], + 'Tiszatardos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0406385, 'lng' => 21.379655], + ], + 'Tiszatarján' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8329217, 'lng' => 21.0014346], + ], + 'Tiszaújváros' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9159846, 'lng' => 21.0427447], + ], + 'Tiszavalk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.6888504, 'lng' => 20.751499], + ], + 'Tokaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1172148, 'lng' => 21.4089015], + ], + 'Tolcsva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2841513, 'lng' => 21.4488452], + ], + 'Tomor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3258904, 'lng' => 20.8823733], + ], + 'Tornabarakony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4922432, 'lng' => 20.8192157], + ], + 'Tornakápolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4616855, 'lng' => 20.617706], + ], + 'Tornanádaska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5611186, 'lng' => 20.7846392], + ], + 'Tornaszentandrás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5226438, 'lng' => 20.7790226], + ], + 'Tornaszentjakab' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5244312, 'lng' => 20.8729813], + ], + 'Tornyosnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5202757, 'lng' => 21.2506927], + ], + 'Trizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4251253, 'lng' => 20.4958645], + ], + 'Újcsanálos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1380468, 'lng' => 21.0036907], + ], + 'Uppony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2155013, 'lng' => 20.434654], + ], + 'Vadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2733247, 'lng' => 20.5552218], + ], + 'Vágáshuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4264605, 'lng' => 21.545222], + ], + 'Vajdácska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3196383, 'lng' => 21.6541401], + ], + 'Vámosújfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2575496, 'lng' => 21.4524394], + ], + 'Varbó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1631678, 'lng' => 20.6217693], + ], + 'Varbóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4644075, 'lng' => 20.6450152], + ], + 'Vatta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9228447, 'lng' => 20.7389995], + ], + 'Vilmány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4166062, 'lng' => 21.2302229], + ], + 'Vilyvitány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4952223, 'lng' => 21.5589737], + ], + 'Viss' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2176861, 'lng' => 21.5069652], + ], + 'Viszló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4939386, 'lng' => 20.8862569], + ], + 'Vizsoly' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3845496, 'lng' => 21.2158416], + ], + 'Zádorfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3860789, 'lng' => 20.4852484], + ], + 'Zalkod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.1857296, 'lng' => 21.4592752], + ], + 'Zemplénagárd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.36024, 'lng' => 22.0709646], + ], + 'Ziliz' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2511796, 'lng' => 20.7922106], + ], + 'Zsujta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4997896, 'lng' => 21.2789138], + ], + 'Zubogy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3792388, 'lng' => 20.5758141], + ], + ], + 'Budapest' => [ + 'Budapest I. ker.' => [ + 'constituencies' => ['Budapest 01.'], + 'coordinates' => ['lat' => 47.4968219, 'lng' => 19.037458], + ], + 'Budapest II. ker.' => [ + 'constituencies' => ['Budapest 03.', 'Budapest 04.'], + 'coordinates' => ['lat' => 47.5393329, 'lng' => 18.986934], + ], + 'Budapest III. ker.' => [ + 'constituencies' => ['Budapest 04.', 'Budapest 10.'], + 'coordinates' => ['lat' => 47.5671768, 'lng' => 19.0368517], + ], + 'Budapest IV. ker.' => [ + 'constituencies' => ['Budapest 11.', 'Budapest 12.'], + 'coordinates' => ['lat' => 47.5648915, 'lng' => 19.0913149], + ], + 'Budapest V. ker.' => [ + 'constituencies' => ['Budapest 01.'], + 'coordinates' => ['lat' => 47.5002319, 'lng' => 19.0520181], + ], + 'Budapest VI. ker.' => [ + 'constituencies' => ['Budapest 05.'], + 'coordinates' => ['lat' => 47.509863, 'lng' => 19.0625813], + ], + 'Budapest VII. ker.' => [ + 'constituencies' => ['Budapest 05.'], + 'coordinates' => ['lat' => 47.5027289, 'lng' => 19.073376], + ], + 'Budapest VIII. ker.' => [ + 'constituencies' => ['Budapest 01.', 'Budapest 06.'], + 'coordinates' => ['lat' => 47.4894184, 'lng' => 19.070668], + ], + 'Budapest IX. ker.' => [ + 'constituencies' => ['Budapest 01.', 'Budapest 06.'], + 'coordinates' => ['lat' => 47.4649279, 'lng' => 19.0916229], + ], + 'Budapest X. ker.' => [ + 'constituencies' => ['Budapest 09.', 'Budapest 14.'], + 'coordinates' => ['lat' => 47.4820909, 'lng' => 19.1575028], + ], + 'Budapest XI. ker.' => [ + 'constituencies' => ['Budapest 02.', 'Budapest 18.'], + 'coordinates' => ['lat' => 47.4593099, 'lng' => 19.0187389], + ], + 'Budapest XII. ker.' => [ + 'constituencies' => ['Budapest 03.'], + 'coordinates' => ['lat' => 47.4991199, 'lng' => 18.990459], + ], + 'Budapest XIII. ker.' => [ + 'constituencies' => ['Budapest 11.', 'Budapest 07.'], + 'coordinates' => ['lat' => 47.5355105, 'lng' => 19.0709266], + ], + 'Budapest XIV. ker.' => [ + 'constituencies' => ['Budapest 08.', 'Budapest 13.'], + 'coordinates' => ['lat' => 47.5224569, 'lng' => 19.114709], + ], + 'Budapest XV. ker.' => [ + 'constituencies' => ['Budapest 12.'], + 'coordinates' => ['lat' => 47.5589, 'lng' => 19.1193], + ], + 'Budapest XVI. ker.' => [ + 'constituencies' => ['Budapest 13.'], + 'coordinates' => ['lat' => 47.5183029, 'lng' => 19.191941], + ], + 'Budapest XVII. ker.' => [ + 'constituencies' => ['Budapest 14.'], + 'coordinates' => ['lat' => 47.4803, 'lng' => 19.2667001], + ], + 'Budapest XVIII. ker.' => [ + 'constituencies' => ['Budapest 15.'], + 'coordinates' => ['lat' => 47.4281229, 'lng' => 19.2098429], + ], + 'Budapest XIX. ker.' => [ + 'constituencies' => ['Budapest 09.', 'Budapest 16.'], + 'coordinates' => ['lat' => 47.4457289, 'lng' => 19.1430149], + ], + 'Budapest XX. ker.' => [ + 'constituencies' => ['Budapest 16.'], + 'coordinates' => ['lat' => 47.4332879, 'lng' => 19.1193169], + ], + 'Budapest XXI. ker.' => [ + 'constituencies' => ['Budapest 17.'], + 'coordinates' => ['lat' => 47.4243579, 'lng' => 19.066142], + ], + 'Budapest XXII. ker.' => [ + 'constituencies' => ['Budapest 18.'], + 'coordinates' => ['lat' => 47.425, 'lng' => 19.031667], + ], + 'Budapest XXIII. ker.' => [ + 'constituencies' => ['Budapest 17.'], + 'coordinates' => ['lat' => 47.3939599, 'lng' => 19.122523], + ], + ], + 'Csongrád-Csanád' => [ + 'Algyő' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3329625, 'lng' => 20.207889], + ], + 'Ambrózfalva' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3501417, 'lng' => 20.7313995], + ], + 'Apátfalva' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.173317, 'lng' => 20.5800472], + ], + 'Árpádhalom' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6158286, 'lng' => 20.547733], + ], + 'Ásotthalom' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.1995983, 'lng' => 19.7833756], + ], + 'Baks' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5518708, 'lng' => 20.1064166], + ], + 'Balástya' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4261828, 'lng' => 20.004933], + ], + 'Bordány' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3194213, 'lng' => 19.9227063], + ], + 'Csanádalberti' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3267872, 'lng' => 20.7068631], + ], + 'Csanádpalota' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2407708, 'lng' => 20.7228873], + ], + 'Csanytelek' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6014883, 'lng' => 20.1114379], + ], + 'Csengele' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5411505, 'lng' => 19.8644533], + ], + 'Csongrád-Csanád' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7084264, 'lng' => 20.1436061], + ], + 'Derekegyház' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.580238, 'lng' => 20.3549845], + ], + 'Deszk' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2179603, 'lng' => 20.2404106], + ], + 'Dóc' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.437292, 'lng' => 20.1363129], + ], + 'Domaszék' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2466283, 'lng' => 19.9990365], + ], + 'Eperjes' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7076258, 'lng' => 20.5621489], + ], + 'Fábiánsebestyén' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6748615, 'lng' => 20.455037], + ], + 'Felgyő' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6616513, 'lng' => 20.1097394], + ], + 'Ferencszállás' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2158295, 'lng' => 20.3553359], + ], + 'Földeák' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3184223, 'lng' => 20.4929019], + ], + 'Forráskút' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3655956, 'lng' => 19.9089055], + ], + 'Hódmezővásárhely' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.4181262, 'lng' => 20.3300315], + ], + 'Királyhegyes' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2717114, 'lng' => 20.6126302], + ], + 'Kistelek' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4694781, 'lng' => 19.9804365], + ], + 'Kiszombor' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1856953, 'lng' => 20.4265486], + ], + 'Klárafalva' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.220953, 'lng' => 20.3255224], + ], + 'Kövegy' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2246141, 'lng' => 20.6840764], + ], + 'Kübekháza' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1500892, 'lng' => 20.276983], + ], + 'Magyarcsanád' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1698824, 'lng' => 20.6132706], + ], + 'Makó' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2219071, 'lng' => 20.4809265], + ], + 'Maroslele' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2698362, 'lng' => 20.3418589], + ], + 'Mártély' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.4682451, 'lng' => 20.2416146], + ], + 'Mindszent' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5227585, 'lng' => 20.1895798], + ], + 'Mórahalom' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2179218, 'lng' => 19.88372], + ], + 'Nagyér' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3703008, 'lng' => 20.729605], + ], + 'Nagylak' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1737713, 'lng' => 20.7111982], + ], + 'Nagymágocs' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5857132, 'lng' => 20.4833875], + ], + 'Nagytőke' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7552639, 'lng' => 20.2860999], + ], + 'Óföldeák' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2985957, 'lng' => 20.4369086], + ], + 'Ópusztaszer' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4957061, 'lng' => 20.0665358], + ], + 'Öttömös' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2808756, 'lng' => 19.6826038], + ], + 'Pitvaros' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3194853, 'lng' => 20.7385996], + ], + 'Pusztamérges' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3280134, 'lng' => 19.6849699], + ], + 'Pusztaszer' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5515959, 'lng' => 19.9870098], + ], + 'Röszke' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.1873773, 'lng' => 20.037455], + ], + 'Ruzsa' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2890678, 'lng' => 19.7481121], + ], + 'Sándorfalva' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.3635951, 'lng' => 20.1032227], + ], + 'Szatymaz' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.3426558, 'lng' => 20.0391941], + ], + 'Szeged' => [ + 'constituencies' => ['Csongrád-Csanád 2.', 'Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2530102, 'lng' => 20.1414253], + ], + 'Szegvár' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5816447, 'lng' => 20.2266415], + ], + 'Székkutas' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.5063976, 'lng' => 20.537673], + ], + 'Szentes' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.654789, 'lng' => 20.2637492], + ], + 'Tiszasziget' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1720458, 'lng' => 20.1618289], + ], + 'Tömörkény' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6166243, 'lng' => 20.0436896], + ], + 'Újszentiván' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1859286, 'lng' => 20.1835123], + ], + 'Üllés' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3355015, 'lng' => 19.8489644], + ], + 'Zákányszék' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2752726, 'lng' => 19.8883111], + ], + 'Zsombó' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3284014, 'lng' => 19.9766186], + ], + ], + 'Fejér' => [ + 'Aba' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0328193, 'lng' => 18.522359], + ], + 'Adony' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.119831, 'lng' => 18.8612469], + ], + 'Alap' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8075763, 'lng' => 18.684028], + ], + 'Alcsútdoboz' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4277067, 'lng' => 18.6030325], + ], + 'Alsószentiván' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7910573, 'lng' => 18.732161], + ], + 'Bakonycsernye' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.321719, 'lng' => 18.0907379], + ], + 'Bakonykúti' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2458464, 'lng' => 18.195769], + ], + 'Balinka' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3135736, 'lng' => 18.1907168], + ], + 'Baracs' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9049033, 'lng' => 18.8752931], + ], + 'Baracska' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2824737, 'lng' => 18.7598901], + ], + 'Beloiannisz' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.183143, 'lng' => 18.8245727], + ], + 'Besnyő' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.1892568, 'lng' => 18.7936832], + ], + 'Bicske' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4911792, 'lng' => 18.6370142], + ], + 'Bodajk' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3209663, 'lng' => 18.2339242], + ], + 'Bodmér' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4489857, 'lng' => 18.5383832], + ], + 'Cece' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7698199, 'lng' => 18.6336808], + ], + 'Csabdi' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.5229299, 'lng' => 18.6085371], + ], + 'Csákberény' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3506861, 'lng' => 18.3265064], + ], + 'Csákvár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3941468, 'lng' => 18.4602445], + ], + 'Csókakő' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3533961, 'lng' => 18.2693867], + ], + 'Csór' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2049913, 'lng' => 18.2557813], + ], + 'Csősz' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0382791, 'lng' => 18.414533], + ], + 'Daruszentmiklós' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.87194, 'lng' => 18.8568642], + ], + 'Dég' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8707664, 'lng' => 18.4445717], + ], + 'Dunaújváros' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 46.9619059, 'lng' => 18.9355227], + ], + 'Előszállás' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8276091, 'lng' => 18.8280627], + ], + 'Enying' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9326943, 'lng' => 18.2414807], + ], + 'Ercsi' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.2482238, 'lng' => 18.8912626], + ], + 'Etyek' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4467098, 'lng' => 18.751179], + ], + 'Fehérvárcsurgó' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2904264, 'lng' => 18.2645262], + ], + 'Felcsút' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4541851, 'lng' => 18.5865775], + ], + 'Füle' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0535367, 'lng' => 18.2480871], + ], + 'Gánt' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3902121, 'lng' => 18.387061], + ], + 'Gárdony' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.196537, 'lng' => 18.6115195], + ], + 'Gyúró' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3700577, 'lng' => 18.7384824], + ], + 'Hantos' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9943127, 'lng' => 18.6989263], + ], + 'Igar' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7757642, 'lng' => 18.5137348], + ], + 'Iszkaszentgyörgy' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2399338, 'lng' => 18.2987232], + ], + 'Isztimér' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2787058, 'lng' => 18.1955966], + ], + 'Iváncsa' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.153376, 'lng' => 18.8270434], + ], + 'Jenő' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1047531, 'lng' => 18.2453199], + ], + 'Kajászó' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3234883, 'lng' => 18.7221054], + ], + 'Káloz' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9568415, 'lng' => 18.4853961], + ], + 'Kápolnásnyék' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2398554, 'lng' => 18.6764288], + ], + 'Kincsesbánya' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2632477, 'lng' => 18.2764679], + ], + 'Kisapostag' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8940766, 'lng' => 18.9323135], + ], + 'Kisláng' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9598173, 'lng' => 18.3860884], + ], + 'Kőszárhegy' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0926048, 'lng' => 18.341234], + ], + 'Kulcs' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0541246, 'lng' => 18.9197178], + ], + 'Lajoskomárom' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.841585, 'lng' => 18.3355393], + ], + 'Lepsény' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9918514, 'lng' => 18.2469618], + ], + 'Lovasberény' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3109278, 'lng' => 18.5527924], + ], + 'Magyaralmás' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2913027, 'lng' => 18.3245512], + ], + 'Mány' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.5321762, 'lng' => 18.6555811], + ], + 'Martonvásár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3164516, 'lng' => 18.7877558], + ], + 'Mátyásdomb' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9228626, 'lng' => 18.3470929], + ], + 'Mezőfalva' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9323938, 'lng' => 18.7771045], + ], + 'Mezőkomárom' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8276482, 'lng' => 18.2934472], + ], + 'Mezőszentgyörgy' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9920267, 'lng' => 18.2795568], + ], + 'Mezőszilas' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8166957, 'lng' => 18.4754679], + ], + 'Moha' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2437717, 'lng' => 18.3313907], + ], + 'Mór' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.374928, 'lng' => 18.2036035], + ], + 'Nadap' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2585056, 'lng' => 18.6167437], + ], + 'Nádasdladány' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1341786, 'lng' => 18.2394077], + ], + 'Nagykarácsony' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8706425, 'lng' => 18.7725518], + ], + 'Nagylók' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9764964, 'lng' => 18.64115], + ], + 'Nagyveleg' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.361797, 'lng' => 18.111061], + ], + 'Nagyvenyim' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 46.9571015, 'lng' => 18.8576229], + ], + 'Óbarok' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4922397, 'lng' => 18.5681206], + ], + 'Pákozd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2172004, 'lng' => 18.5430768], + ], + 'Pátka' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2752462, 'lng' => 18.4950339], + ], + 'Pázmánd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.283645, 'lng' => 18.654854], + ], + 'Perkáta' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0482285, 'lng' => 18.784294], + ], + 'Polgárdi' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0601257, 'lng' => 18.2993645], + ], + 'Pusztaszabolcs' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.1408918, 'lng' => 18.7601638], + ], + 'Pusztavám' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.4297438, 'lng' => 18.2317401], + ], + 'Rácalmás' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0243223, 'lng' => 18.9350709], + ], + 'Ráckeresztúr' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2729155, 'lng' => 18.8330106], + ], + 'Sárbogárd' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.879104, 'lng' => 18.6213353], + ], + 'Sáregres' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.783236, 'lng' => 18.5935136], + ], + 'Sárkeresztes' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2517488, 'lng' => 18.3541822], + ], + 'Sárkeresztúr' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0025252, 'lng' => 18.5479461], + ], + 'Sárkeszi' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1582764, 'lng' => 18.284968], + ], + 'Sárosd' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0414738, 'lng' => 18.6488144], + ], + 'Sárszentágota' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9706742, 'lng' => 18.5634969], + ], + 'Sárszentmihály' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1537282, 'lng' => 18.3235014], + ], + 'Seregélyes' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.1100586, 'lng' => 18.5788431], + ], + 'Soponya' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0120427, 'lng' => 18.4543505], + ], + 'Söréd' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.322683, 'lng' => 18.280508], + ], + 'Sukoró' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2425436, 'lng' => 18.6022803], + ], + 'Szabadbattyán' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1175572, 'lng' => 18.3681061], + ], + 'Szabadegyháza' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0770131, 'lng' => 18.6912379], + ], + 'Szabadhídvég' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8210159, 'lng' => 18.2798938], + ], + 'Szár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4791911, 'lng' => 18.5158147], + ], + 'Székesfehérvár' => [ + 'constituencies' => ['Fejér 2.', 'Fejér 1.'], + 'coordinates' => ['lat' => 47.1860262, 'lng' => 18.4221358], + ], + 'Tabajd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4045316, 'lng' => 18.6302011], + ], + 'Tác' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0794264, 'lng' => 18.403381], + ], + 'Tordas' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3440943, 'lng' => 18.7483302], + ], + 'Újbarok' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4791337, 'lng' => 18.5585574], + ], + 'Úrhida' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1298384, 'lng' => 18.3321437], + ], + 'Vajta' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7227758, 'lng' => 18.6618091], + ], + 'Vál' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3624339, 'lng' => 18.6766737], + ], + 'Velence' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2300924, 'lng' => 18.6506424], + ], + 'Vereb' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.318485, 'lng' => 18.6197301], + ], + 'Vértesacsa' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3700218, 'lng' => 18.5792793], + ], + 'Vértesboglár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4291347, 'lng' => 18.5235823], + ], + 'Zámoly' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3168103, 'lng' => 18.408371], + ], + 'Zichyújfalu' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.1291991, 'lng' => 18.6692222], + ], + ], + 'Győr-Moson-Sopron' => [ + 'Abda' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6962149, 'lng' => 17.5445786], + ], + 'Acsalag' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.676095, 'lng' => 17.1977771], + ], + 'Ágfalva' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.688862, 'lng' => 16.5110233], + ], + 'Agyagosszergény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.608545, 'lng' => 16.9409912], + ], + 'Árpás' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5134127, 'lng' => 17.3931579], + ], + 'Ásványráró' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8287695, 'lng' => 17.499195], + ], + 'Babót' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5752269, 'lng' => 17.0758604], + ], + 'Bágyogszovát' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5866036, 'lng' => 17.3617273], + ], + 'Bakonygyirót' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4181388, 'lng' => 17.8055502], + ], + 'Bakonypéterd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4667076, 'lng' => 17.7967619], + ], + 'Bakonyszentlászló' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.3892006, 'lng' => 17.8032754], + ], + 'Barbacs' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6455476, 'lng' => 17.297216], + ], + 'Beled' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4662675, 'lng' => 17.0959263], + ], + 'Bezenye' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9609867, 'lng' => 17.216211], + ], + 'Bezi' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6737572, 'lng' => 17.3921093], + ], + 'Bodonhely' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5655752, 'lng' => 17.4072124], + ], + 'Bogyoszló' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5609657, 'lng' => 17.1850606], + ], + 'Bőny' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6516279, 'lng' => 17.8703841], + ], + 'Börcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6862052, 'lng' => 17.4988893], + ], + 'Bősárkány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6881947, 'lng' => 17.2507143], + ], + 'Cakóháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6967121, 'lng' => 17.2863758], + ], + 'Cirák' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4779219, 'lng' => 17.0282338], + ], + 'Csáfordjánosfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4151998, 'lng' => 16.9510595], + ], + 'Csapod' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5162077, 'lng' => 16.9234546], + ], + 'Csér' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4169765, 'lng' => 16.9330737], + ], + 'Csikvánd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4666335, 'lng' => 17.4546305], + ], + 'Csorna' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6103234, 'lng' => 17.2462444], + ], + 'Darnózseli' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8493957, 'lng' => 17.4273958], + ], + 'Dénesfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4558445, 'lng' => 17.0335351], + ], + 'Dör' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5979168, 'lng' => 17.2991911], + ], + 'Dunakiliti' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9659588, 'lng' => 17.2882641], + ], + 'Dunaremete' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8761957, 'lng' => 17.4375005], + ], + 'Dunaszeg' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7692554, 'lng' => 17.5407805], + ], + 'Dunaszentpál' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7771623, 'lng' => 17.5043978], + ], + 'Dunasziget' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9359671, 'lng' => 17.3617867], + ], + 'Ebergőc' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5635832, 'lng' => 16.81167], + ], + 'Écs' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5604415, 'lng' => 17.7072193], + ], + 'Edve' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4551126, 'lng' => 17.135508], + ], + 'Egyed' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5192845, 'lng' => 17.3396861], + ], + 'Egyházasfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.46243, 'lng' => 16.7679871], + ], + 'Enese' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6461219, 'lng' => 17.4235267], + ], + 'Farád' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6064483, 'lng' => 17.2003347], + ], + 'Fehértó' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6759514, 'lng' => 17.3453497], + ], + 'Feketeerdő' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9355702, 'lng' => 17.2783691], + ], + 'Felpéc' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5225976, 'lng' => 17.5993517], + ], + 'Fenyőfő' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.3490387, 'lng' => 17.7656259], + ], + 'Fertőboz' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.633426, 'lng' => 16.6998899], + ], + 'Fertőd' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.61818, 'lng' => 16.8741418], + ], + 'Fertőendréd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6054618, 'lng' => 16.9085891], + ], + 'Fertőhomok' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6196363, 'lng' => 16.7710445], + ], + 'Fertőrákos' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.7209654, 'lng' => 16.6488128], + ], + 'Fertőszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5895578, 'lng' => 16.8730712], + ], + 'Fertőszéplak' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6172442, 'lng' => 16.8405708], + ], + 'Gönyű' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.7334344, 'lng' => 17.8243403], + ], + 'Gyalóka' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4427372, 'lng' => 16.696223], + ], + 'Gyarmat' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4604024, 'lng' => 17.4964917], + ], + 'Gyömöre' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4982876, 'lng' => 17.564804], + ], + 'Győr' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.', 'Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.6874569, 'lng' => 17.6503974], + ], + 'Győrasszonyfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4950098, 'lng' => 17.8072327], + ], + 'Győrladamér' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7545651, 'lng' => 17.5633004], + ], + 'Gyóró' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4916519, 'lng' => 17.0236667], + ], + 'Győrság' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5751529, 'lng' => 17.7515893], + ], + 'Győrsövényház' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6909394, 'lng' => 17.3734235], + ], + 'Győrszemere' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.551813, 'lng' => 17.5635661], + ], + 'Győrújbarát' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6076284, 'lng' => 17.6389745], + ], + 'Győrújfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.722197, 'lng' => 17.6054524], + ], + 'Győrzámoly' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7434268, 'lng' => 17.5770199], + ], + 'Halászi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8903231, 'lng' => 17.3256673], + ], + 'Harka' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6339566, 'lng' => 16.5986264], + ], + 'Hédervár' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.831062, 'lng' => 17.4541026], + ], + 'Hegyeshalom' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9117445, 'lng' => 17.156071], + ], + 'Hegykő' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6188466, 'lng' => 16.7940292], + ], + 'Hidegség' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6253847, 'lng' => 16.740935], + ], + 'Himod' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5200248, 'lng' => 17.0064434], + ], + 'Hövej' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5524954, 'lng' => 17.0166402], + ], + 'Ikrény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6539897, 'lng' => 17.5281764], + ], + 'Iván' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.445549, 'lng' => 16.9096056], + ], + 'Jánossomorja' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7847917, 'lng' => 17.1298642], + ], + 'Jobaháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5799316, 'lng' => 17.1886952], + ], + 'Kajárpéc' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4888221, 'lng' => 17.6350057], + ], + 'Kapuvár' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5912437, 'lng' => 17.0301952], + ], + 'Károlyháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8032696, 'lng' => 17.3446363], + ], + 'Kimle' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8172115, 'lng' => 17.3676625], + ], + 'Kisbabot' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5551791, 'lng' => 17.4149558], + ], + 'Kisbajcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7450615, 'lng' => 17.6800942], + ], + 'Kisbodak' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8963234, 'lng' => 17.4196192], + ], + 'Kisfalud' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.2041959, 'lng' => 18.494568], + ], + 'Kóny' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6307264, 'lng' => 17.3596093], + ], + 'Kópháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6385359, 'lng' => 16.6451629], + ], + 'Koroncó' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5999604, 'lng' => 17.5284792], + ], + 'Kunsziget' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7385858, 'lng' => 17.5176565], + ], + 'Lázi' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4661979, 'lng' => 17.8346909], + ], + 'Lébény' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7360651, 'lng' => 17.3905652], + ], + 'Levél' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8949275, 'lng' => 17.2001946], + ], + 'Lipót' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8615868, 'lng' => 17.4603528], + ], + 'Lövő' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5107966, 'lng' => 16.7898395], + ], + 'Maglóca' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6625685, 'lng' => 17.2751221], + ], + 'Magyarkeresztúr' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5200063, 'lng' => 17.1660121], + ], + 'Máriakálnok' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8596905, 'lng' => 17.3237666], + ], + 'Markotabödöge' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6815136, 'lng' => 17.3116772], + ], + 'Mecsér' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.796671, 'lng' => 17.4744842], + ], + 'Mérges' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6012809, 'lng' => 17.4438455], + ], + 'Mezőörs' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.568844, 'lng' => 17.8821253], + ], + 'Mihályi' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5142703, 'lng' => 17.0958265], + ], + 'Mórichida' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5127896, 'lng' => 17.4218174], + ], + 'Mosonmagyaróvár' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8681469, 'lng' => 17.2689169], + ], + 'Mosonszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7294576, 'lng' => 17.4242231], + ], + 'Mosonszolnok' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8511108, 'lng' => 17.1735793], + ], + 'Mosonudvar' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8435379, 'lng' => 17.224348], + ], + 'Nagybajcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7639168, 'lng' => 17.686613], + ], + 'Nagycenk' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6081549, 'lng' => 16.6979223], + ], + 'Nagylózs' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5654858, 'lng' => 16.76965], + ], + 'Nagyszentjános' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.7100868, 'lng' => 17.8681808], + ], + 'Nemeskér' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.483855, 'lng' => 16.8050771], + ], + 'Nyalka' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5443407, 'lng' => 17.8091081], + ], + 'Nyúl' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5832389, 'lng' => 17.6862095], + ], + 'Osli' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6385609, 'lng' => 17.0755158], + ], + 'Öttevény' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7255506, 'lng' => 17.4899552], + ], + 'Páli' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4774264, 'lng' => 17.1695082], + ], + 'Pannonhalma' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.549497, 'lng' => 17.7552412], + ], + 'Pásztori' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5553919, 'lng' => 17.2696728], + ], + 'Pázmándfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5710798, 'lng' => 17.7810865], + ], + 'Pér' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6111604, 'lng' => 17.8049747], + ], + 'Pereszteg' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.594289, 'lng' => 16.7354028], + ], + 'Petőháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5965785, 'lng' => 16.8954138], + ], + 'Pinnye' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5855193, 'lng' => 16.7706082], + ], + 'Potyond' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.549377, 'lng' => 17.1821874], + ], + 'Püski' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8846385, 'lng' => 17.4070152], + ], + 'Pusztacsalád' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4853081, 'lng' => 16.9013644], + ], + 'Rábacsanak' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5256113, 'lng' => 17.2902872], + ], + 'Rábacsécsény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5879598, 'lng' => 17.4227941], + ], + 'Rábakecöl' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4324946, 'lng' => 17.1126349], + ], + 'Rábapatona' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6314656, 'lng' => 17.4797584], + ], + 'Rábapordány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5574649, 'lng' => 17.3262502], + ], + 'Rábasebes' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4392738, 'lng' => 17.2423807], + ], + 'Rábaszentandrás' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4596327, 'lng' => 17.3272097], + ], + 'Rábaszentmihály' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5775103, 'lng' => 17.4312379], + ], + 'Rábaszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5381909, 'lng' => 17.417513], + ], + 'Rábatamási' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5893387, 'lng' => 17.1699767], + ], + 'Rábcakapi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7079835, 'lng' => 17.2755839], + ], + 'Rajka' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9977901, 'lng' => 17.1983996], + ], + 'Ravazd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5162349, 'lng' => 17.7512699], + ], + 'Répceszemere' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4282026, 'lng' => 16.9738943], + ], + 'Répcevis' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4427966, 'lng' => 16.6731972], + ], + 'Rétalap' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6072246, 'lng' => 17.9071507], + ], + 'Röjtökmuzsaj' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5543502, 'lng' => 16.8363467], + ], + 'Románd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4484049, 'lng' => 17.7909987], + ], + 'Sarród' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6315873, 'lng' => 16.8613408], + ], + 'Sikátor' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4370828, 'lng' => 17.8510581], + ], + 'Sobor' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4768368, 'lng' => 17.3752902], + ], + 'Sokorópátka' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4892381, 'lng' => 17.6953943], + ], + 'Sopron' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6816619, 'lng' => 16.5844795], + ], + 'Sopronhorpács' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4831854, 'lng' => 16.7359058], + ], + 'Sopronkövesd' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5460504, 'lng' => 16.7432859], + ], + 'Sopronnémeti' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5364397, 'lng' => 17.2070182], + ], + 'Szakony' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4262848, 'lng' => 16.7154462], + ], + 'Szany' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4620733, 'lng' => 17.3027671], + ], + 'Szárföld' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5933239, 'lng' => 17.1221243], + ], + 'Szerecseny' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4628425, 'lng' => 17.5536197], + ], + 'Szil' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.501622, 'lng' => 17.233297], + ], + 'Szilsárkány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5396552, 'lng' => 17.2545808], + ], + 'Táp' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5168299, 'lng' => 17.8292989], + ], + 'Tápszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4930151, 'lng' => 17.8524913], + ], + 'Tarjánpuszta' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5062161, 'lng' => 17.7869857], + ], + 'Tárnokréti' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.7217546, 'lng' => 17.3078226], + ], + 'Tényő' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5407376, 'lng' => 17.6490009], + ], + 'Tét' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5198967, 'lng' => 17.5108553], + ], + 'Töltéstava' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6273335, 'lng' => 17.7343778], + ], + 'Újkér' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4573295, 'lng' => 16.8187647], + ], + 'Újrónafő' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8101728, 'lng' => 17.2015241], + ], + 'Und' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.488856, 'lng' => 16.6961552], + ], + 'Vadosfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4986805, 'lng' => 17.1287654], + ], + 'Vág' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4469264, 'lng' => 17.2121765], + ], + 'Vámosszabadi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7571476, 'lng' => 17.6507532], + ], + 'Várbalog' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8347267, 'lng' => 17.0720923], + ], + 'Vásárosfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4537986, 'lng' => 17.1158473], + ], + 'Vének' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7392272, 'lng' => 17.7556608], + ], + 'Veszkény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5969056, 'lng' => 17.0891913], + ], + 'Veszprémvarsány' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4290248, 'lng' => 17.8287245], + ], + 'Vitnyéd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5863882, 'lng' => 16.9832151], + ], + 'Völcsej' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.496503, 'lng' => 16.7604595], + ], + 'Zsebeháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.511293, 'lng' => 17.191017], + ], + 'Zsira' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4580482, 'lng' => 16.6766466], + ], + ], + 'Hajdú-Bihar' => [ + 'Álmosd' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4167788, 'lng' => 21.9806107], + ], + 'Ártánd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1241958, 'lng' => 21.7568167], + ], + 'Bagamér' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4498231, 'lng' => 21.9942012], + ], + 'Bakonszeg' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1900613, 'lng' => 21.4442102], + ], + 'Balmazújváros' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.6145296, 'lng' => 21.3417333], + ], + 'Báránd' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2936964, 'lng' => 21.2288584], + ], + 'Bedő' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1634194, 'lng' => 21.7502785], + ], + 'Berekböszörmény' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0615952, 'lng' => 21.6782301], + ], + 'Berettyóújfalu' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2196438, 'lng' => 21.5362812], + ], + 'Bihardancsháza' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2291246, 'lng' => 21.3159659], + ], + 'Biharkeresztes' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1301236, 'lng' => 21.7219423], + ], + 'Biharnagybajom' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2108104, 'lng' => 21.2302309], + ], + 'Bihartorda' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.215994, 'lng' => 21.3526252], + ], + 'Bocskaikert' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6435949, 'lng' => 21.659878], + ], + 'Bojt' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1927968, 'lng' => 21.7327485], + ], + 'Csökmő' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0315111, 'lng' => 21.2892817], + ], + 'Darvas' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1017037, 'lng' => 21.3374554], + ], + 'Debrecen' => [ + 'constituencies' => ['Hajdú-Bihar 3.', 'Hajdú-Bihar 1.', 'Hajdú-Bihar 2.'], + 'coordinates' => ['lat' => 47.5316049, 'lng' => 21.6273124], + ], + 'Derecske' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3533886, 'lng' => 21.5658524], + ], + 'Ebes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4709086, 'lng' => 21.490457], + ], + 'Egyek' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.6258313, 'lng' => 20.8907463], + ], + 'Esztár' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2837051, 'lng' => 21.7744117], + ], + 'Földes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2896801, 'lng' => 21.3633025], + ], + 'Folyás' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8086696, 'lng' => 21.1371809], + ], + 'Fülöp' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.5981409, 'lng' => 22.0546557], + ], + 'Furta' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1300357, 'lng' => 21.460144], + ], + 'Gáborján' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2360716, 'lng' => 21.6622765], + ], + 'Görbeháza' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8200025, 'lng' => 21.2359976], + ], + 'Hajdúbagos' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3947066, 'lng' => 21.6643329], + ], + 'Hajdúböszörmény' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.6718908, 'lng' => 21.5126637], + ], + 'Hajdúdorog' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8166047, 'lng' => 21.4980694], + ], + 'Hajdúhadház' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6802292, 'lng' => 21.6675179], + ], + 'Hajdúnánás' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.843004, 'lng' => 21.4242691], + ], + 'Hajdúsámson' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6049148, 'lng' => 21.7597325], + ], + 'Hajdúszoboszló' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4435369, 'lng' => 21.3965516], + ], + 'Hajdúszovát' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3903463, 'lng' => 21.4764161], + ], + 'Hencida' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2507004, 'lng' => 21.6989732], + ], + 'Hortobágy' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.5868751, 'lng' => 21.1560332], + ], + 'Hosszúpályi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3947673, 'lng' => 21.7346539], + ], + 'Kaba' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3565391, 'lng' => 21.2726765], + ], + 'Kismarja' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2463277, 'lng' => 21.8214627], + ], + 'Kokad' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4054409, 'lng' => 21.9336174], + ], + 'Komádi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0055271, 'lng' => 21.4944772], + ], + 'Konyár' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3213954, 'lng' => 21.6691634], + ], + 'Körösszakál' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0178012, 'lng' => 21.5932398], + ], + 'Körösszegapáti' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0396539, 'lng' => 21.6317831], + ], + 'Létavértes' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.3835171, 'lng' => 21.8798767], + ], + 'Magyarhomorog' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0222187, 'lng' => 21.5480518], + ], + 'Mezőpeterd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.165025, 'lng' => 21.6200633], + ], + 'Mezősas' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1104156, 'lng' => 21.5671344], + ], + 'Mikepércs' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.4406335, 'lng' => 21.6366773], + ], + 'Monostorpályi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3984198, 'lng' => 21.7764527], + ], + 'Nádudvar' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4259381, 'lng' => 21.1616779], + ], + 'Nagyhegyes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.539228, 'lng' => 21.345552], + ], + 'Nagykereki' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1863168, 'lng' => 21.7922805], + ], + 'Nagyrábé' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2043078, 'lng' => 21.3306582], + ], + 'Nyírábrány' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.541423, 'lng' => 22.0128317], + ], + 'Nyíracsád' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6039774, 'lng' => 21.9715154], + ], + 'Nyíradony' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6899404, 'lng' => 21.9085991], + ], + 'Nyírmártonfalva' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.5862503, 'lng' => 21.8964914], + ], + 'Pocsaj' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2851817, 'lng' => 21.8122198], + ], + 'Polgár' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8679381, 'lng' => 21.1141038], + ], + 'Püspökladány' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3216529, 'lng' => 21.1185953], + ], + 'Sáp' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2549739, 'lng' => 21.3555868], + ], + 'Sáránd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.4062312, 'lng' => 21.6290631], + ], + 'Sárrétudvari' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2406806, 'lng' => 21.1866058], + ], + 'Szentpéterszeg' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2386719, 'lng' => 21.6178971], + ], + 'Szerep' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2278774, 'lng' => 21.1407795], + ], + 'Téglás' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.7109686, 'lng' => 21.6727776], + ], + 'Tépe' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.32046, 'lng' => 21.5714076], + ], + 'Tetétlen' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3148595, 'lng' => 21.3069162], + ], + 'Tiszacsege' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.6997085, 'lng' => 20.9917041], + ], + 'Tiszagyulaháza' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.942524, 'lng' => 21.1428152], + ], + 'Told' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1180165, 'lng' => 21.6413048], + ], + 'Újiráz' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 46.9870862, 'lng' => 21.3556353], + ], + 'Újléta' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4650261, 'lng' => 21.8733489], + ], + 'Újszentmargita' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.7266767, 'lng' => 21.1047788], + ], + 'Újtikos' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.9176202, 'lng' => 21.171571], + ], + 'Vámospércs' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.525345, 'lng' => 21.8992474], + ], + 'Váncsod' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2011182, 'lng' => 21.6400459], + ], + 'Vekerd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0959975, 'lng' => 21.4017741], + ], + 'Zsáka' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1340418, 'lng' => 21.4307824], + ], + ], + 'Heves' => [ + 'Abasár' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7989023, 'lng' => 20.0036779], + ], + 'Adács' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6922284, 'lng' => 19.9779484], + ], + 'Aldebrő' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7891428, 'lng' => 20.2302555], + ], + 'Andornaktálya' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8499325, 'lng' => 20.4105243], + ], + 'Apc' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7933298, 'lng' => 19.6955737], + ], + 'Átány' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.6156875, 'lng' => 20.3620368], + ], + 'Atkár' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7209651, 'lng' => 19.8912361], + ], + 'Balaton' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 46.8302679, 'lng' => 17.7340438], + ], + 'Bátor' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.99076, 'lng' => 20.2627351], + ], + 'Bekölce' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0804457, 'lng' => 20.268156], + ], + 'Bélapátfalva' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0578657, 'lng' => 20.3500536], + ], + 'Besenyőtelek' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.6994693, 'lng' => 20.4300342], + ], + 'Boconád' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6414895, 'lng' => 20.1877312], + ], + 'Bodony' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9420912, 'lng' => 20.0199927], + ], + 'Boldog' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6031287, 'lng' => 19.687521], + ], + 'Bükkszék' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9915393, 'lng' => 20.1765126], + ], + 'Bükkszenterzsébet' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0532811, 'lng' => 20.1622924], + ], + 'Bükkszentmárton' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0715382, 'lng' => 20.3310312], + ], + 'Csány' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6474142, 'lng' => 19.8259607], + ], + 'Demjén' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8317294, 'lng' => 20.3313872], + ], + 'Detk' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7489442, 'lng' => 20.0983332], + ], + 'Domoszló' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8288666, 'lng' => 20.1172988], + ], + 'Dormánd' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7203119, 'lng' => 20.4174779], + ], + 'Ecséd' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7307237, 'lng' => 19.7684767], + ], + 'Eger' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9025348, 'lng' => 20.3772284], + ], + 'Egerbakta' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9341404, 'lng' => 20.2918134], + ], + 'Egerbocs' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0263467, 'lng' => 20.2598999], + ], + 'Egercsehi' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0545478, 'lng' => 20.261522], + ], + 'Egerfarmos' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7177802, 'lng' => 20.5358914], + ], + 'Egerszalók' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8702275, 'lng' => 20.3241673], + ], + 'Egerszólát' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8902473, 'lng' => 20.2669774], + ], + 'Erdőkövesd' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0391241, 'lng' => 20.1013656], + ], + 'Erdőtelek' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6852656, 'lng' => 20.3115369], + ], + 'Erk' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6101796, 'lng' => 20.076668], + ], + 'Fedémes' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0320282, 'lng' => 20.1878653], + ], + 'Feldebrő' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8128253, 'lng' => 20.2363322], + ], + 'Felsőtárkány' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9734513, 'lng' => 20.41906], + ], + 'Füzesabony' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7495339, 'lng' => 20.4150668], + ], + 'Gyöngyös' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7772651, 'lng' => 19.9294927], + ], + 'Gyöngyöshalász' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7413068, 'lng' => 19.9227242], + ], + 'Gyöngyösoroszi' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8263987, 'lng' => 19.8928817], + ], + 'Gyöngyöspata' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8140904, 'lng' => 19.7923335], + ], + 'Gyöngyössolymos' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8160489, 'lng' => 19.9338831], + ], + 'Gyöngyöstarján' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8132903, 'lng' => 19.8664265], + ], + 'Halmajugra' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7634173, 'lng' => 20.0523104], + ], + 'Hatvan' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6656965, 'lng' => 19.676666], + ], + 'Heréd' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7081485, 'lng' => 19.6327042], + ], + 'Heves' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.5971694, 'lng' => 20.280156], + ], + 'Hevesaranyos' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0109153, 'lng' => 20.2342809], + ], + 'Hevesvezekény' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.5570546, 'lng' => 20.3580453], + ], + 'Hort' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6890439, 'lng' => 19.7842632], + ], + 'Istenmezeje' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0845673, 'lng' => 20.0515347], + ], + 'Ivád' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0203013, 'lng' => 20.0612654], + ], + 'Kál' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7318239, 'lng' => 20.2608866], + ], + 'Kápolna' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7584202, 'lng' => 20.2459749], + ], + 'Karácsond' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7282318, 'lng' => 20.0282488], + ], + 'Kerecsend' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7947277, 'lng' => 20.3444695], + ], + 'Kerekharaszt' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6623104, 'lng' => 19.6253721], + ], + 'Kisfüzes' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9881653, 'lng' => 20.1267373], + ], + 'Kisköre' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.4984608, 'lng' => 20.4973609], + ], + 'Kisnána' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8506469, 'lng' => 20.1457821], + ], + 'Kömlő' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 46.1929788, 'lng' => 18.2512139], + ], + 'Kompolt' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7415463, 'lng' => 20.2406377], + ], + 'Lőrinci' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7390261, 'lng' => 19.6756557], + ], + 'Ludas' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7300788, 'lng' => 20.0910629], + ], + 'Maklár' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8054074, 'lng' => 20.410901], + ], + 'Markaz' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8222206, 'lng' => 20.0582311], + ], + 'Mátraballa' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9843833, 'lng' => 20.0225017], + ], + + ], + ]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8174.php b/tests/PHPStan/Rules/Methods/data/bug-8174.php new file mode 100644 index 0000000000..e7305490ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8174.php @@ -0,0 +1,23 @@ + $list + * @return list + */ + public function filterList(array $list): array { + $filtered = array_filter($list, function ($elem) { + return $elem === '23423'; + }); + assertType("array, '23423'>", $filtered); // this is not a list + assertType("list<'23423'>", array_values($filtered)); // this is a list + + // why am I allowed to return not a list then? + return $filtered; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8223.php b/tests/PHPStan/Rules/Methods/data/bug-8223.php new file mode 100644 index 0000000000..679f123c67 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8223.php @@ -0,0 +1,30 @@ +modify($modify); + } + + /** + * @return array<\DateTimeImmutable> + */ + public function sayHello2(string $modify): array + { + $date = new \DateTimeImmutable(); + + return [$date->modify($modify)]; + } + + public function test() + { + $r = new HelloWorld(); + + $r->sayHello('ss'); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8296.php b/tests/PHPStan/Rules/Methods/data/bug-8296.php new file mode 100644 index 0000000000..b257780358 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8296.php @@ -0,0 +1,29 @@ + new stdClass(), + "b" => true + ]; + self::continueDump($dummy); + + $string = 12345; + self::stringByRef($string); + } + + /** + * @phpstan-param array $objects + * @phpstan-param-out array $objects + */ + private static function continueDump(array &$objects) : void{ + + } + + private static function stringByRef(string &$string) : void{ + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8500.php b/tests/PHPStan/Rules/Methods/data/bug-8500.php new file mode 100644 index 0000000000..b10f9c7def --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8500.php @@ -0,0 +1,33 @@ +foo(); + } + + public function bar(): void + { + self::$instance = null; + } + + public function baz(): void + { + self::$instance = new HelloWorld(); + + $this->bar(); + + self::$instance?->foo(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8523b.php b/tests/PHPStan/Rules/Methods/data/bug-8523b.php new file mode 100644 index 0000000000..a007fd2661 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8523b.php @@ -0,0 +1,24 @@ +save(); + } +} + +(new HelloWorld())->save(); diff --git a/tests/PHPStan/Rules/Methods/data/bug-8523c.php b/tests/PHPStan/Rules/Methods/data/bug-8523c.php new file mode 100644 index 0000000000..ea88940446 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8523c.php @@ -0,0 +1,26 @@ +save(); + } +} + +(new HelloWorld())->save(); diff --git a/tests/PHPStan/Rules/Methods/data/bug-8573.php b/tests/PHPStan/Rules/Methods/data/bug-8573.php new file mode 100644 index 0000000000..19b6ca97ed --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8573.php @@ -0,0 +1,28 @@ + $data + */ + public static function __set_state(array $data): static + { + $obj = new static(); + + return $obj; + } +} + +class B extends A +{ + public static function __set_state(array $data): static + { + $obj = parent::__set_state($data); + + return $obj; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8632.php b/tests/PHPStan/Rules/Methods/data/bug-8632.php new file mode 100644 index 0000000000..17c2aa50f6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8632.php @@ -0,0 +1,26 @@ + 1, + 'categories' => ['news'], + ]; + } else { + $arr = []; + } + + return array_merge($arr, []); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8713.php b/tests/PHPStan/Rules/Methods/data/bug-8713.php new file mode 100644 index 0000000000..49aa966c10 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8713.php @@ -0,0 +1,13 @@ += 8.0 + +namespace Bug8713; + +class Foo +{ + public function foo(): void + { + $query = "SELECT * FROM `foo`"; + $pdo = new \PDO("dsn"); + $pdo->query(query: $query); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8879.php b/tests/PHPStan/Rules/Methods/data/bug-8879.php new file mode 100644 index 0000000000..a59e0c866c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8879.php @@ -0,0 +1,15 @@ +someTest('foo'); + } + return; + } +} + +class A +{ + use SomeTrait; + + public function someTest(string $foo, string $bar): void {} +} + +class B +{ + use SomeTrait; + + public function someTest(string $foo): void {} +} + +class Test +{ + public function test(): void + { + $a = new A(); + $a->test(); + $b = new B(); + $b->test(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9009.php b/tests/PHPStan/Rules/Methods/data/bug-9009.php new file mode 100644 index 0000000000..a60ca63003 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9009.php @@ -0,0 +1,26 @@ +addHook($fx); + } +} + +(new Hook())->addHookDynamic(function (Hook $hook) { + return new \stdClass(); +}); diff --git a/tests/PHPStan/Rules/Methods/data/bug-9011.php b/tests/PHPStan/Rules/Methods/data/bug-9011.php new file mode 100644 index 0000000000..8f5195809f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9011.php @@ -0,0 +1,18 @@ + + */ + public function __debugInfo(): array + { + return [ + 'a' => 1, + ]; + } + + /** + * @return array + */ + public function __serialize(): array + { + return [ + 'a' => $this->a + ]; + } +} + +class B extends A +{ + private $b; + + public function __debugInfo(): array + { + return [ + 'b' => 2, + ]; + } + + public function __serialize(): array + { + return [ + 'b' => $this->b + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9524.php b/tests/PHPStan/Rules/Methods/data/bug-9524.php new file mode 100644 index 0000000000..9713e71a61 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9524.php @@ -0,0 +1,11 @@ +getMessage(); + } + + public function testClass(TranslatableInterface $translatable): void + { + if ($translatable::class !== TranslatableMessage::class) { + assertType('Bug9542\TranslatableInterface', $translatable); + return; + } + + assertType('Bug9542\TranslatableMessage', $translatable); + $translatable->getMessage(); + } + + public function testClassReverse(TranslatableInterface $translatable): void + { + if (TranslatableMessage::class !== $translatable::class) { + assertType('Bug9542\TranslatableInterface', $translatable); + return; + } + + assertType('Bug9542\TranslatableMessage', $translatable); + $translatable->getMessage(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9571-phpdocs.php b/tests/PHPStan/Rules/Methods/data/bug-9571-phpdocs.php new file mode 100644 index 0000000000..e8732082d3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9571-phpdocs.php @@ -0,0 +1,23 @@ + $properties + * + * @return $this + */ + public function setDefaults(array $properties) + { + return $this; + } +} + +class FactoryTestDefMock +{ + use DiContainerTrait { + setDefaults as _setDefaults; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9571.php b/tests/PHPStan/Rules/Methods/data/bug-9571.php new file mode 100644 index 0000000000..e9c1e40f55 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9571.php @@ -0,0 +1,19 @@ +baseConstructor(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9615.php b/tests/PHPStan/Rules/Methods/data/bug-9615.php new file mode 100644 index 0000000000..87e1faadf9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9615.php @@ -0,0 +1,21 @@ += 8.0 + +namespace Bug9657; + +/** + * @template T + */ +trait Convertable +{ + /** + * @return T + */ + abstract public function toOther(): mixed; +} + +final class Thing +{ + /** @use Convertable> */ + use Convertable; + + public function toOther(): array + { + return []; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9766.php b/tests/PHPStan/Rules/Methods/data/bug-9766.php new file mode 100644 index 0000000000..2be59b73dc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9766.php @@ -0,0 +1,25 @@ + $items + */ + public function __construct( + private iterable $items, + ) { + // empty + } + + /** + * @return iterable + */ + protected function getItems(): iterable { + return $this->items; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9905.php b/tests/PHPStan/Rules/Methods/data/bug-9905.php new file mode 100644 index 0000000000..c018929266 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9905.php @@ -0,0 +1,22 @@ + 'user', 'extra' => 'readonly']; + } +} + +class NeverExtra implements Foo { + /** @return array{field: string} */ + public function get(): array { + return ['field' => 'user']; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9951.php b/tests/PHPStan/Rules/Methods/data/bug-9951.php new file mode 100644 index 0000000000..ab7096f27e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9951.php @@ -0,0 +1,33 @@ +|string|Expressionable $field + * @param ($field is string|Expressionable ? ($value is null ? mixed : string) : never) $operator + * @param ($operator is string ? mixed : never) $value + */ + public function addCondition($field, $operator = null, $value = null): void + { + } + + public function testStr(string $field, bool $value): void + { + $this->addCondition($field, $value); + } + + public function testMixed(mixed $field, bool $value): void + { + $this->addCondition($field, $value); + } + + public function testMixedAsUnion(string|object|null $field, bool $value): void + { + $this->addCondition($field, $value); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-SplObjectStorage-remove.php b/tests/PHPStan/Rules/Methods/data/bug-SplObjectStorage-remove.php new file mode 100644 index 0000000000..3470979dd6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-SplObjectStorage-remove.php @@ -0,0 +1,80 @@ + */ +class HelloWorld +{ + /** @var ObjectStorage */ + private \SplObjectStorage $foo; + + /** + * @param ObjectStorage $other + * @return ObjectStorage + */ + public function removeSame(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removeNarrower(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removeWider(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removePossibleIntersect(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removeNoIntersect(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-template-mixed-union-intersect.php b/tests/PHPStan/Rules/Methods/data/bug-template-mixed-union-intersect.php new file mode 100644 index 0000000000..ce0c88fbe2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-template-mixed-union-intersect.php @@ -0,0 +1,26 @@ += 8.0 + +namespace BugTemplateMixedUnionIntersect; + +interface FooInterface +{ + public function foo(): int; +} + +/** + * @template T of mixed + * @param T $a + */ +function foo(mixed $a, FooInterface $b, mixed $c): void +{ + if ($a instanceof FooInterface) { + var_dump($a->bar()); + } + if ($c instanceof FooInterface) { + var_dump($c->bar()); + } + $d = rand() > 1 ? $a : $b; + var_dump($d->foo()); + $d = rand() > 1 ? $c : $b; + var_dump($d->foo()); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-wrong-method-name-with-template-mixed.php b/tests/PHPStan/Rules/Methods/data/bug-wrong-method-name-with-template-mixed.php new file mode 100644 index 0000000000..e4148acbb2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-wrong-method-name-with-template-mixed.php @@ -0,0 +1,46 @@ += 8.1 + +namespace BugWrongMethodNameWithTemplateMixed; + +class HelloWorld +{ + /** + * @template T + * @param T $val + */ + public function foo(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + /** + * @template T of mixed + * @param T $val + */ + public function foo2(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + /** + * @template T of object + * @param T $val + */ + public function foo3(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + public function foo4(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php b/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php index c7a2393a67..5fc2439152 100644 --- a/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php +++ b/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php @@ -30,3 +30,81 @@ enum Bar use FooTrait; } + +enum Country: string +{ + case NL = 'The Netherlands'; + case US = 'United States'; +} + +enum CountryNo: int +{ + case NL = 1; + case US = 2; +} + +enum FooCall { + /** + * @param value-of $countryName + */ + function hello(string $countryName): void + { + // ... + } + + /** + * @param array, bool> $countryMap + */ + function helloArray(array $countryMap): void { + // ... + } + + function doFooArray() { + $this->hello(CountryNo::NL); + + // 'abc' does not match value-of + $this->helloArray(['abc' => true]); + $this->helloArray(['abc' => 123]); + + // wrong key type + $this->helloArray([true]); + } +} + +enum TestPassingEnums { + case ONE; + case TWO; + + /** + * @param self::ONE $one + * @return void + */ + public function requireOne(self $one): void + { + + } + + public function doFoo(): void + { + match ($this) { + self::ONE => $this->requireOne($this), + self::TWO => $this->requireOne($this), + }; + } + + public function doFoo2(): void + { + match ($this) { + self::ONE => $this->requireOne($this), + default => $this->requireOne($this), + }; + } + + public function doFoo3(): void + { + match ($this) { + self::TWO => $this->requireOne($this), + default => $this->requireOne($this), + }; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/call-methods-is-callable.php b/tests/PHPStan/Rules/Methods/data/call-methods-is-callable.php new file mode 100644 index 0000000000..66127c810b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-methods-is-callable.php @@ -0,0 +1,21 @@ +test('Test\CheckIsCallable::test'); + } + + public function testClosure(\Closure $closure) + { + $this->testClosure(function () { + + }); + } + +} + diff --git a/tests/PHPStan/Rules/Methods/data/call-methods-named-params-multivariant.php b/tests/PHPStan/Rules/Methods/data/call-methods-named-params-multivariant.php new file mode 100644 index 0000000000..e9377cb61a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-methods-named-params-multivariant.php @@ -0,0 +1,22 @@ += 8.0 + +namespace CallMethodsNamedParamsMultivariant; + + +$xslt = new \XSLTProcessor(); +$xslt->setParameter(namespace: 'ns', name:'aaa', value: 'bbb'); +$xslt->setParameter(namespace: 'ns', name: ['aaa' => 'bbb']); +// wrong +$xslt->setParameter(namespace: 'ns', options: ['aaa' => 'bbb']); + +$pdo = new \PDO('123'); +$pdo->query(query: 'SELECT 1', fetchMode: \PDO::FETCH_ASSOC); +// wrong +$pdo->query(query: 'SELECT 1', fetchMode: \PDO::FETCH_ASSOC, colno: 1); +// wrong +$pdo->query(query: 'SELECT 1', fetchMode: \PDO::FETCH_ASSOC, className: 'Foo', constructorArgs: []); + +$stmt = new \PDOStatement(); +$stmt->setFetchMode(mode: 5); +// wrong +$stmt->setFetchMode(mode: 5, className: 'aa'); diff --git a/tests/PHPStan/Rules/Methods/data/call-methods.php b/tests/PHPStan/Rules/Methods/data/call-methods.php index 081b913ea1..12c26595e3 100644 --- a/tests/PHPStan/Rules/Methods/data/call-methods.php +++ b/tests/PHPStan/Rules/Methods/data/call-methods.php @@ -656,7 +656,7 @@ public function test(callable $str) { $this->test('date'); $this->test('nonexistentFunction'); - $this->test('Test\CheckIsCallable::test'); + // $this->test('Test\CheckIsCallable::test'); differs between php7/8; tested separately $this->test('Test\CheckIsCallable::test2'); } @@ -1802,3 +1802,56 @@ public function test(): void $this->foo('Newark Liberty International'); } } + +class WeirdArrayBug +{ + + /** @param string[] $strings */ + public function doBar($m, array $a, bool $b, array $strings) + { + $needles = [$m]; + foreach ($a as $v) { + if ($b) { + $needles = array_merge($needles, $strings); + } + } + + $this->doFoo($needles); + } + + /** + * @param array|string $strings + * @return void + */ + public function doFoo($strings) + { + + } + +} + +class NonFalsyString { + /** + * @param '0' $literalZero + * @param numeric-string $numericS + * @param non-falsy-string $nonFalsey + * @param non-empty-string $nonEmpty + * @param literal-string $literalString + */ + public function doFoo($literalZero, string $s, string $nonFalsey, $numericS, $nonEmpty, $literalString, int $i) { + $this->acceptsNonFalsyString($nonFalsey); + + $this->acceptsNonFalsyString($numericS); + $this->acceptsNonFalsyString($literalZero); + $this->acceptsNonFalsyString($s); + $this->acceptsNonFalsyString($nonEmpty); + $this->acceptsNonFalsyString($literalString); + $this->acceptsNonFalsyString($i); + } + + /** + * @param non-falsy-string $string + */ + public function acceptsNonFalsyString(string $string) { + } +} diff --git a/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php b/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php new file mode 100644 index 0000000000..4dcb333ed1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php @@ -0,0 +1,171 @@ += 8.0 + +namespace CallStaticMethodMixed; + +class Foo +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + $implicit::foo(); + $explicit::foo(); + } + + /** + * @template T + * @param T $t + */ + public function doBar($t): void + { + $t::foo(); + } + +} + +class Bar +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + self::doBar($implicit); + self::doBar($explicit); + + self::acceptImplicitMixed($implicit); + self::acceptImplicitMixed($explicit); + + self::acceptExplicitMixed($implicit); + self::acceptExplicitMixed($explicit); + + self::acceptVariadicArguments(...$implicit); + self::acceptVariadicArguments(...$explicit); + } + + public static function doBar(int $i): void + { + + } + + public static function acceptImplicitMixed($mixed): void + { + + } + + public static function acceptExplicitMixed(mixed $mixed): void + { + + } + + public static function acceptVariadicArguments(mixed... $args): void + { + + } + + /** + * @template T + * @param T $t + */ + public function doLorem($t): void + { + self::doBar($t); + self::acceptImplicitMixed($t); + self::acceptExplicitMixed($t); + self::acceptVariadicArguments(...$t); + } + +} + +class CallableMixed +{ + + /** + * @param callable(mixed): void $cb + */ + public static function callAcceptsExplicitMixed(callable $cb): void + { + + } + + /** + * @param callable(int): void $cb + */ + public static function callAcceptsInt(callable $cb): void + { + + } + + /** + * @param callable(): mixed $cb + */ + public static function callReturnsExplicitMixed(callable $cb): void + { + + } + + public static function callReturnsImplicitMixed(callable $cb): void + { + + } + + /** + * @param callable(): int $cb + */ + public static function callReturnsInt(callable $cb): void + { + + } + + public static function doLorem(int $i, mixed $explicitMixed, $implicitMixed): void + { + $acceptsInt = function (int $i): void { + + }; + self::callAcceptsExplicitMixed($acceptsInt); + self::callAcceptsInt($acceptsInt); + + $acceptsExplicitMixed = function (mixed $m): void { + + }; + self::callAcceptsExplicitMixed($acceptsExplicitMixed); + self::callAcceptsInt($acceptsExplicitMixed); + + $acceptsImplicitMixed = function ($m): void { + + }; + self::callAcceptsExplicitMixed($acceptsImplicitMixed); + self::callAcceptsInt($acceptsImplicitMixed); + + $returnsInt = function () use ($i): int { + return $i; + }; + self::callReturnsExplicitMixed($returnsInt); + self::callReturnsImplicitMixed($returnsInt); + self::callReturnsInt($returnsInt); + + $returnsExplicitMixed = function () use ($explicitMixed): mixed { + return $explicitMixed; + }; + self::callReturnsExplicitMixed($returnsExplicitMixed); + self::callReturnsImplicitMixed($returnsExplicitMixed); + self::callReturnsInt($returnsExplicitMixed); + + $returnsImplicitMixed = function () use ($implicitMixed): mixed { + return $implicitMixed; + }; + self::callReturnsExplicitMixed($returnsImplicitMixed); + self::callReturnsImplicitMixed($returnsImplicitMixed); + self::callReturnsInt($returnsImplicitMixed); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/call-static-methods.php b/tests/PHPStan/Rules/Methods/data/call-static-methods.php index ddcf7e2c2d..0fadae62c3 100644 --- a/tests/PHPStan/Rules/Methods/data/call-static-methods.php +++ b/tests/PHPStan/Rules/Methods/data/call-static-methods.php @@ -345,3 +345,11 @@ public function doBar() } } + +class Bug2759 { + public function sayHello(string $html): void + { + $dom = \DOMDocument::loadHTML($html, LIBXML_NOWARNING | LIBXML_NONET | LIBXML_NOERROR); + } +} + diff --git a/tests/PHPStan/Rules/Methods/data/callables-without-check-nullables.php b/tests/PHPStan/Rules/Methods/data/callables-without-check-nullables.php new file mode 100644 index 0000000000..e4d1cc923c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/callables-without-check-nullables.php @@ -0,0 +1,89 @@ +doBar(function (?float $f): ?float { + return $f; + }); + $this->doBaz(function (?float $f): ?float { + return $f; + }); + $this->doBar(function (?float $f): float { + return $f; + }); + $this->doBaz(function (?float $f): float { + return $f; + }); + + $this->doBar(function (float $f): float { + return $f; + }); + $this->doBaz(function (float $f): float { + return $f; + }); + + $this->doBar2(function (?float $f): ?float { + return $f; + }); + $this->doBaz2(function (?float $f): ?float { + return $f; + }); + $this->doBar2(function (?float $f): float { + return $f; + }); + $this->doBaz2(function (?float $f): float { + return $f; + }); + + $this->doBar2(function (float $f): float { + return $f; + }); + $this->doBaz2(function (float $f): float { + return $f; + }); + } + + /** + * @param callable(float|null): (float|null) $cb + * @return void + */ + public function doBar(callable $cb): void + { + + } + + /** + * @param Closure(float|null): (float|null) $cb + * @return void + */ + public function doBaz(Closure $cb): void + { + + } + + /** + * @param callable(float|null): float $cb + * @return void + */ + public function doBar2(callable $cb): void + { + + } + + /** + * @param Closure(float|null): float $cb + * @return void + */ + public function doBaz2(Closure $cb): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/check-implicit-mixed.php b/tests/PHPStan/Rules/Methods/data/check-implicit-mixed.php new file mode 100644 index 0000000000..1737c6e015 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/check-implicit-mixed.php @@ -0,0 +1,142 @@ += 8.0 + +namespace CheckImplicitMixedMethodCall; + +class Foo +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + $implicit->foo(); + $explicit->foo(); + } + + /** + * @template T + * @param T $t + */ + public function doBar($t): void + { + $t->foo(); + } + +} + +class Bar +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + $this->doBar($implicit); + $this->doBar($explicit); + + $this->doBaz($implicit); + $this->doBaz($explicit); + } + + public function doBar(int $i): void + { + + } + + public function doBaz($mixed): void + { + + } + + /** + * @template T + * @param T $t + */ + public function doLorem($t): void + { + $this->doBar($t); + $this->doBaz($t); + } + +} + +class TemplateMixed +{ + + /** + * @template T + * @param T $t + */ + public function doFoo($t): void + { + $this->doBar($t); + } + + public function doBar($mixed): void + { + $this->doFoo($mixed); + } + +} + +class CallableMixed +{ + + /** + * @param callable(int): void $cb + */ + public function doBar(callable $cb): void + { + + } + + /** + * @param callable() $cb + */ + public function doFoo2(callable $cb): void + { + + } + + /** + * @param callable(): int $cb + */ + public function doBar2(callable $cb): void + { + + } + + public function doLorem(int $i, $m): void + { + $acceptsInt = function (int $i): void { + + }; + $this->doBar($acceptsInt); + + $acceptsMixed = function ($m): void { + + }; + $this->doBar($acceptsMixed); + + $returnsInt = function () use ($i): int { + return $i; + }; + $this->doFoo2($returnsInt); + $this->doBar2($returnsInt); + + $returnsMixed = function () use ($m) { + return $m; + }; + $this->doFoo2($returnsMixed); + $this->doBar2($returnsMixed); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php b/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php new file mode 100644 index 0000000000..0206b87068 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php @@ -0,0 +1,51 @@ +bindTo(new self()); // not checked + + // overwritten + $b = function (): void { + + }; + $b->bindTo(new self()); // not checked + + $c->bindTo(new \stdClass()); // ok + $c->bindTo(new self()); // error + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/closure-bind.php b/tests/PHPStan/Rules/Methods/data/closure-bind.php index f6aa9e05b7..57b9ccf5ff 100644 --- a/tests/PHPStan/Rules/Methods/data/closure-bind.php +++ b/tests/PHPStan/Rules/Methods/data/closure-bind.php @@ -49,4 +49,33 @@ public function fooMethod(): Foo })->call(new Foo()); } + public function x(): bool + { + return 1.0; + } + + public function testClassString(): bool + { + $fx = function () { + return $this->x(); + }; + + $res = 0.0; + $res += \Closure::bind($fx, $this)(); + $res += \Closure::bind($fx, $this, 'static')(); + $res += \Closure::bind($fx, $this, Foo2::class)(); + $res += \Closure::bind($fx, $this, 'CallClosureBind\Bar2')(); + $res += \Closure::bind($fx, $this, 'CallClosureBind\Bar3')(); + + $res += $fx->bindTo($this)(); + $res += $fx->bindTo($this, 'static')(); + $res += $fx->bindTo($this, Foo2::class)(); + $res += $fx->bindTo($this, 'CallClosureBind\Bar2')(); + $res += $fx->bindTo($this, 'CallClosureBind\Bar3')(); + + return $res; + } + } + +class Bar2 extends Bar {} diff --git a/tests/PHPStan/Rules/Methods/data/closure-parameter-generics.php b/tests/PHPStan/Rules/Methods/data/closure-parameter-generics.php new file mode 100644 index 0000000000..29ee3b1663 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/closure-parameter-generics.php @@ -0,0 +1,38 @@ +retryableTransaction(function (Transaction $tr) { + return $tr; + }); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/conditional-complex-templates.php b/tests/PHPStan/Rules/Methods/data/conditional-complex-templates.php new file mode 100644 index 0000000000..1593317e68 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/conditional-complex-templates.php @@ -0,0 +1,33 @@ + + */ + public function then(callable $onFulfilled = null, callable $onRejected = null); +} + +/** + * @param PromiseInterface $promise + */ +function test(PromiseInterface $promise): void +{ + $passThroughBoolFn = static fn (bool $bool): bool => $bool; + + assertType('ConditionalComplexTemplates\PromiseInterface', $promise->then($passThroughBoolFn)); + assertType('ConditionalComplexTemplates\PromiseInterface', $promise->then()->then($passThroughBoolFn)); +} diff --git a/tests/PHPStan/Rules/Methods/data/conditional-param.php b/tests/PHPStan/Rules/Methods/data/conditional-param.php new file mode 100644 index 0000000000..06adf7b93e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/conditional-param.php @@ -0,0 +1,23 @@ + $flags + */ + public static function replaceCallback($demoArg, int $flags = 0): void + {} +} + +function (): void { + HelloWorld::replaceCallback(true); // correct, error expected + HelloWorld::replaceCallback("string"); // correct + + HelloWorld::replaceCallback(true, PREG_OFFSET_CAPTURE); // correct + HelloWorld::replaceCallback("string", PREG_OFFSET_CAPTURE); // correct, error expected + + HelloWorld::replaceCallback(true, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL); // should not report error +}; diff --git a/tests/PHPStan/Rules/Methods/data/conditional-return-type.php b/tests/PHPStan/Rules/Methods/data/conditional-return-type.php new file mode 100644 index 0000000000..809cbd0798 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/conditional-return-type.php @@ -0,0 +1,18 @@ + ? T : mixed) + */ + function get(string $id): mixed; + + /** + * @template T + * @return ($id is not class-string ? T : mixed) + */ + function notGet(string $id): mixed; +} diff --git a/tests/PHPStan/Rules/Methods/data/consistent-constructor-no-errors.php b/tests/PHPStan/Rules/Methods/data/consistent-constructor-no-errors.php new file mode 100644 index 0000000000..8b680847b4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/consistent-constructor-no-errors.php @@ -0,0 +1,50 @@ += 80000) { + class Foo + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} else { + class FooBar + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/discussion-7004.php b/tests/PHPStan/Rules/Methods/data/discussion-7004.php new file mode 100644 index 0000000000..145ed7f694 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/discussion-7004.php @@ -0,0 +1,50 @@ + $data + */ + public static function fromArray1(array $data): void + { + assertType('array', $data); + } + + /** + * @param array{array{newsletterName: string, subscriberCount: int}} $data + */ + public static function fromArray2(array $data): void + { + assertType('array{array{newsletterName: string, subscriberCount: int}}', $data); + } + + /** + * @param array{newsletterName: string, subscriberCount: int} $data + */ + public static function fromArray3(array $data): void + { + assertType('array{newsletterName: string, subscriberCount: int}', $data); + } +} + +class Bar +{ + /** + * @param mixed $data + */ + public function doSomething($data): void + { + if (!is_array($data)) { + return; + } + + assertType('array', $data); + Foo::fromArray1($data); + Foo::fromArray2($data); + Foo::fromArray3($data); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/dynamic-call.php b/tests/PHPStan/Rules/Methods/data/dynamic-call.php new file mode 100644 index 0000000000..3a917c0c81 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/dynamic-call.php @@ -0,0 +1,62 @@ +$foo(); + echo $this->$string(); + echo $this->$obj(); + echo $this->{self::$name}(); + } + + public function testStaticCall(string $string, object $obj): void + { + $foo = 'bar'; + + echo self::$foo(); + echo self::$string(); + echo self::$obj(); + echo self::{self::$name}(); + } + + public function testScope(): void + { + $param1 = 1; + $param2 = 'str'; + $name1 = 'doFoo'; + if (rand(0, 1)) { + $name = $name1; + $param = $param1; + } else { + $name = 'doQux'; + $param = $param2; + } + + $this->$name($param); // ok + $this->$name1($param); + $this->$name($param1); + $this->$name($param2); + + self::$name($param); // ok + self::$name1($param); + self::$name($param1); + self::$name($param2); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php b/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php new file mode 100644 index 0000000000..6880568c93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php @@ -0,0 +1,11 @@ += 80000) { + class FooBarPhp8orHigher + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID < 80000) { + class FooBarPhp7 + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID > 70400) { + class FooBarPhp74OrHigher + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 80100) { + class FooBarBaz + { + + final private function foo(): void + { + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/final-private-method.php b/tests/PHPStan/Rules/Methods/data/final-private-method.php new file mode 100644 index 0000000000..bb06450219 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/final-private-method.php @@ -0,0 +1,33 @@ + $param + */ + public function invariant(Invariant $param): void + { + } + + /** + * @param Covariant $param + */ + public function covariant(Covariant $param): void + { + } + + /** + * @param Contravariant $param + */ + public function contravariant(Contravariant $param): void + { + } + + public function testInvariant(): void + { + /** @var Invariant $invariantA */ + $invariantA = new Invariant(); + $this->invariant($invariantA); + + /** @var Invariant $invariantB */ + $invariantB = new Invariant(); + $this->invariant($invariantB); + + /** @var Invariant $invariantC */ + $invariantC = new Invariant(); + $this->invariant($invariantC); + } + + public function testCovariant(): void + { + /** @var Covariant $covariantA */ + $covariantA = new Covariant(); + $this->covariant($covariantA); + + /** @var Covariant $covariantB */ + $covariantB = new Covariant(); + $this->covariant($covariantB); + + /** @var Covariant $covariantC */ + $covariantC = new Covariant(); + $this->covariant($covariantC); + } + + public function testContravariant(): void + { + /** @var Contravariant $contravariantA */ + $contravariantA = new Contravariant(); + $this->contravariant($contravariantA); + + /** @var Contravariant $contravariantB */ + $contravariantB = new Contravariant(); + $this->contravariant($contravariantB); + + /** @var Contravariant $contravariantC */ + $contravariantC = new Contravariant(); + $this->contravariant($contravariantC); + } + + /** + * @param array{Invariant} $param + */ + public function invariantArray(array $param): void + { + } + + public function testInvariantArray(): void + { + /** @var Invariant $invariantC */ + $invariantC = new Invariant(); + $this->invariantArray([$invariantC]); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/imagick-pixel.php b/tests/PHPStan/Rules/Methods/data/imagick-pixel.php new file mode 100644 index 0000000000..1f6504f1c8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/imagick-pixel.php @@ -0,0 +1,16 @@ +getColor(); + $pixel->getColor(0); + $pixel->getColor(1); + $pixel->getColor(2); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/imagick.php b/tests/PHPStan/Rules/Methods/data/imagick.php new file mode 100644 index 0000000000..572a3e8f4f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/imagick.php @@ -0,0 +1,13 @@ + */ + public function returnsList(); + + /** @return array */ + public function returnsArray(); +} + +interface ListChild extends ListParent +{ + /** @return array */ + public function returnsList(); + + /** @return list */ + public function returnsArray(); +} diff --git a/tests/PHPStan/Rules/Methods/data/lowercase-string.php b/tests/PHPStan/Rules/Methods/data/lowercase-string.php new file mode 100644 index 0000000000..40c475e9e5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/lowercase-string.php @@ -0,0 +1,33 @@ +acceptLowercaseString('NotLowerCase'); + $this->acceptLowercaseString('lowercase'); + $this->acceptLowercaseString($string); + $this->acceptLowercaseString($lowercaseString); + $this->acceptLowercaseString($numericString); + $this->acceptLowercaseString($nonEmptyLowercaseString); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/magic-serialization.php b/tests/PHPStan/Rules/Methods/data/magic-serialization.php new file mode 100644 index 0000000000..cadddfb876 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/magic-serialization.php @@ -0,0 +1,30 @@ + */ + public function __serialize(): array + { + return []; + } + + /** @param array $data */ + public function __unserialize(array $data): void + { + } +} + +class WrongSignature { + + public function __serialize() + { + return ''; + } + + public function __unserialize($data) + { + return ''; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/magic-signatures.php b/tests/PHPStan/Rules/Methods/data/magic-signatures.php new file mode 100644 index 0000000000..0d71caa470 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/magic-signatures.php @@ -0,0 +1,70 @@ +pure1('test'); (new Bzz())->pure2('test'); (new Bzz())->pure3('test'); + (new Bzz())->pure4('test'); + (new Bzz())->pure5('test'); }; diff --git a/tests/PHPStan/Rules/Methods/data/method-implicitly-nullable.php b/tests/PHPStan/Rules/Methods/data/method-implicitly-nullable.php new file mode 100644 index 0000000000..f5690b2c72 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-implicitly-nullable.php @@ -0,0 +1,23 @@ += 8.1 + +namespace MethodInEnumWithoutBody; + +enum Foo +{ + + public function doFoo(): void; + + abstract public function doBar(): void; + +} diff --git a/tests/PHPStan/Rules/Methods/data/method-misleading-mixed-return.php b/tests/PHPStan/Rules/Methods/data/method-misleading-mixed-return.php new file mode 100644 index 0000000000..e7c1b2141a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-misleading-mixed-return.php @@ -0,0 +1,21 @@ + + */ + public function doFoo() + { + + } + +} + +/** + * @template T + * @extends Foo + */ +class Bar extends Foo +{ + + /** + * @return static + */ + public function doFoo() + { + + } + +} + +/** + * @template T + * @extends Foo + */ +final class FinalBar extends Foo +{ + + /** + * @return static + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php index 8346689258..27fa039ef4 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php @@ -197,3 +197,79 @@ public function unserialize($data): void } } + +class MissingParamOutType { + + /** + * @param array $a + * @param-out array $a + */ + function oneArray(&$a): void { + + } + + /** + * @param mixed $a + * @param-out \ReflectionClass $a + */ + function generics(&$a): void { + + } +} + +class MissingParamClosureThisType { + + /** + * @param-closure-this \ReflectionClass $cb + * @param callable(): void $cb + */ + function generics(callable $cb): void + { + + } + +} + +class MissingPureClosureSignatureType { + + /** + * @param pure-Closure $cb + */ + function doFoo(\Closure $cb): void + { + + } + +} + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + public function acceptsGenericWithDefault(GenericClassWithDefault $i) + { + + } + + public function acceptsGenericWithSomeDefaults(GenericClassWithSomeDefaults $c) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php index 5b708cad89..480373825a 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php @@ -113,3 +113,35 @@ public function doFoo(): \Traversable } } + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + public function returnsGenericWithDefault(): GenericClassWithDefault + { + + } + + public function returnsGenericWithSomeDefaults(): GenericClassWithSomeDefaults + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-self-out-type.php b/tests/PHPStan/Rules/Methods/data/missing-method-self-out-type.php new file mode 100644 index 0000000000..a0c83d7e3f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-method-self-out-type.php @@ -0,0 +1,35 @@ + + */ + public function doFoo(): void + { + + } + + /** + * @phpstan-self-out self + */ + public function doFoo2(): void + { + + } + + /** + * @phpstan-self-out Foo&callable + */ + public function doFoo3(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-return-type-generic-static.php b/tests/PHPStan/Rules/Methods/data/missing-return-type-generic-static.php new file mode 100644 index 0000000000..688556c99b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-return-type-generic-static.php @@ -0,0 +1,17 @@ + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-serialization.php b/tests/PHPStan/Rules/Methods/data/missing-serialization.php new file mode 100644 index 0000000000..e4c53064d5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-serialization.php @@ -0,0 +1,40 @@ += 8.1 + +namespace MissingMagicSerializationMethods; + +use Serializable; + +abstract class abstractObj implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } +} + +class myObj implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } +} + +enum myEnum implements Serializable { + case X; + case Y; + + public function serialize() { + } + public function unserialize($data) { + } +} + +abstract class allGood implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } + public function __serialize() { + } + public function __unserialize($data) { + } +} diff --git a/tests/PHPStan/Rules/Methods/data/named-arguments.php b/tests/PHPStan/Rules/Methods/data/named-arguments.php index f5c779f171..25d9ef362b 100644 --- a/tests/PHPStan/Rules/Methods/data/named-arguments.php +++ b/tests/PHPStan/Rules/Methods/data/named-arguments.php @@ -92,6 +92,7 @@ public function doDolor(): void $this->doIpsum(...['a' => 1, 'foo' => 'foo']); $this->doIpsum(...['b' => 1, 'foo' => 'foo']); $this->doIpsum(...[1, 2], 'foo'); + $this->doIpsum(1, 2, foo: 1, bar: 2); } } diff --git a/tests/PHPStan/Rules/Methods/data/no-named-arguments.php b/tests/PHPStan/Rules/Methods/data/no-named-arguments.php new file mode 100644 index 0000000000..e28e91d1d8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/no-named-arguments.php @@ -0,0 +1,34 @@ += 8.0 + +namespace NoNamedArgumentsMethod; + +class Foo +{ + + /** + * @no-named-arguments + */ + public function doFoo(int $i): void + { + + } + +} + +/** + * @no-named-arguments + */ +class Bar +{ + + public function doFoo(int $i): void + { + + } + +} + +function (Foo $f, Bar $b): void { + $f->doFoo(i: 1); + $b->doFoo(i: 1); +}; diff --git a/tests/PHPStan/Rules/Methods/data/non-empty-array.php b/tests/PHPStan/Rules/Methods/data/non-empty-array.php new file mode 100644 index 0000000000..a2e1045a91 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/non-empty-array.php @@ -0,0 +1,29 @@ + $mightBeEmpty + * @param non-empty-array $nonEmpty + * @return void + */ + public function doFoo(array $mightBeEmpty, array $nonEmpty) + { + $this->requireNonEmpty($mightBeEmpty); + $this->requireNonEmpty($nonEmpty); + $this->requireNonEmpty([]); + $this->requireNonEmpty([123]); + } + + /** + * @param non-empty-array $nonEmpty + */ + public function requireNonEmpty(array $nonEmpty) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php index dbe6ef395d..0eb89b8ae8 100644 --- a/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php +++ b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php @@ -27,4 +27,11 @@ public function doLorem(?self $selfOrNull): void $this->doBaz($selfOrNull?->test->test); } + public function doNull(): void + { + $null = null; + $null->foo(); + $null?->foo(); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/object-shapes.php b/tests/PHPStan/Rules/Methods/data/object-shapes.php new file mode 100644 index 0000000000..8df09d5f93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/object-shapes.php @@ -0,0 +1,215 @@ +doBar(new stdClass()); + $this->doBar(new Exception()); + $this->doBar($e); + } + + /** + * @param object{foo: int, bar: string} $o + */ + public function doBar($o): void + { + + } + + /** + * @param object{foo: string, bar: int} $o + * @param object{foo?: int, bar: string} $p + * @param object{foo: int, bar: string} $q + */ + public function doBaz( + $o, + $p, + $q + ): void + { + $this->doBar($o); + $this->doBar($p); + $this->doBar($q); + + $this->requireStdClass($o); + $this->requireStdClass((object) []); + $this->doBar((object) ['foo' => 1, 'bar' => 'bar']); // OK + $this->doBar((object) ['foo' => 'foo', 'bar' => 1]); // Error + $this->acceptsObject($o); + } + + public function requireStdClass(stdClass $std): void + { + + } + + public function acceptsObject(object $o): void + { + $this->doBar($o); + $this->doBar(new \stdClass()); + } + +} + +class Bar +{ + + /** @var int */ + public $a; + + /** + * @param object{a: int} $o + */ + public function doFoo(object $o): void + { + $this->requireBar($o); + } + + public function requireBar(self $bar): void + { + $this->doFoo($bar); + $this->doBar($bar); + } + + /** + * @param object{a: string} $o + */ + public function doBar(object $o): void + { + + } + +} + +/** + * @property-write int $c + */ +#[\AllowDynamicProperties] +class Baz +{ + + /** @var int */ + protected $a; + + /** @var array{foo: int} */ + public $d; + + public function doFoo(): void + { + $this->doBar($this); + $this->doBaz($this); + $this->doLorem($this); + $this->doIpsum($this); + } + + /** + * @param object{a: int} $o + */ + public function doBar(object $o): void + { + + } + + /** @var int */ + public static $b; + + /** + * @param object{b: int} $o + */ + public function doBaz(object $o): void + { + + } + + /** + * @param object{c: int} $o + */ + public function doLorem(object $o): void + { + + } + + /** + * @param object{d: array{foo: string}} $o + */ + public function doIpsum(object $o): void + { + + } + +} + +class OptionalProperty +{ + + /** + * @param object{foo?: string} $o + */ + public function doFoo(object $o): void + { + $this->doBar($o); + $this->doBaz($o); + } + + /** + * @param object{foo?: int} $o + */ + public function doBar(object $o): void + { + + } + + /** + * @param object{foo: int} $o + */ + public function doBaz(object $o): void + { + + } + +} + +final class FinalClass +{ + +} + +class ClassWithFooIntProperty +{ + + /** @var int */ + public $foo; + +} + +class TestAcceptance +{ + + /** + * @param object{foo: int} $o + * @return void + */ + public function doFoo(object $o): void + { + + } + + public function doBar( + \Traversable $traversable, + FinalClass $finalClass, + ClassWithFooIntProperty $classWithFooIntProperty + ) + { + $this->doFoo($traversable); + $this->doFoo($finalClass); + $this->doFoo($classWithFooIntProperty); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/override-attribute.php b/tests/PHPStan/Rules/Methods/data/override-attribute.php new file mode 100644 index 0000000000..ca16bdba5b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/override-attribute.php @@ -0,0 +1,60 @@ + + */ +trait FooTrait +{ + /** + * Offset checker + * + * @phpstan-param Offset $offset + * @return bool + * @template Offset of key-of + */ + abstract public function offsetExists(mixed $offset): bool; +} + +/** + * @template DataArray of array + * @phpstan-type DataKey key-of + * @phpstan-type DataValue DataArray[DataKey] + */ +class FooClass +{ + + /** @phpstan-use FooTrait */ + use FooTrait; + + /** @phpstan-var DataArray|array{} */ + public array $data = []; + + + /** + * Data checker + * + * @phpstan-param Offset $offset + * @return bool + * @template Offset of key-of + */ + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->data); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/overriden-method-with-conditional-return-type.php b/tests/PHPStan/Rules/Methods/data/overriden-method-with-conditional-return-type.php new file mode 100644 index 0000000000..63bc6f403f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriden-method-with-conditional-return-type.php @@ -0,0 +1,42 @@ += 8.0 + +namespace OverridingIndirectPrototype; + +class Foo +{ + + public function doFoo(): mixed + { + + } + +} + +class Bar extends Foo +{ + + public function doFoo(): string + { + + } + +} + +class Baz extends Bar +{ + + public function doFoo(): mixed + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/overriding-trait-methods-phpdoc.php b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods-phpdoc.php new file mode 100644 index 0000000000..8a84a36c65 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods-phpdoc.php @@ -0,0 +1,38 @@ += 8.0 + +namespace OverridingTraitMethodsPhpDoc; + +trait Foo +{ + + public function doFoo(int $i): int + { + + } + + abstract public function doBar(string $i): int; + +} + +class Bar +{ + + use Foo; + + /** + * @param positive-int $i + */ + public function doFoo(int $i): string + { + // ok, trait method not abstract + } + + /** + * @param non-empty-string $i + */ + public function doBar(string $i): int + { + // error + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/overriding-trait-methods.php b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods.php new file mode 100644 index 0000000000..6e66526370 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods.php @@ -0,0 +1,84 @@ += 8.0 + +namespace OverridingTraitMethods; + +trait Foo +{ + + public function doFoo(string $i): int + { + + } + + abstract public function doBar(string $i): int; + +} + +class Bar +{ + + use Foo; + + public function doFoo(int $i): string + { + // ok, trait method not abstract + } + + public function doBar(int $i): int + { + // error + } + +} + +trait FooPrivate +{ + + abstract private function doBar(string $i): int; + +} + +class Baz +{ + use FooPrivate; + + private function doBar(int $i): int + { + + } +} + +class Lorem +{ + use Foo; + + protected function doBar(string $i): int + { + + } +} + +class Ipsum +{ + use Foo; + + public static function doBar(string $i): int + { + + } +} + +trait FooStatic +{ + abstract public static function doBar(string $i): int; +} + +class Dolor +{ + use FooStatic; + + public function doBar(string $i): int + { + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/param-closure-this-classes.php b/tests/PHPStan/Rules/Methods/data/param-closure-this-classes.php new file mode 100644 index 0000000000..f36ffbfb1f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/param-closure-this-classes.php @@ -0,0 +1,32 @@ += 8.4 + +namespace PropertyHooksReturn; + +class Foo +{ + + public int $i { + get { + if (rand(0, 1)) { + return 'foo'; + } + + return 1; + } + set { + if (rand(0, 1)) { + return; + } + + return 1; + } + } + + /** @var non-empty-string */ + public string $s { + get { + if (rand(0, 1)) { + return ''; + } + + return 'foo'; + } + } + +} + +/** + * @template T of Foo + */ +class GenericFoo +{ + + /** @var T */ + public Foo $a { + get { + if (rand(0, 1)) { + return new Foo(); + } + + return $this->a; + } + } + + /** + * @param T $c + */ + public function __construct( + /** @var T */ + public Foo $b { + get { + if (rand(0, 1)) { + return new Foo(); + } + + return $this->b; + } + }, + + public Foo $c { + get { + if (rand(0, 1)) { + return new Foo(); + } + + return $this->c; + } + } + ) + { + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php b/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php new file mode 100644 index 0000000000..3c801d2090 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php @@ -0,0 +1,68 @@ +acceptsCallable($cb); + $this->acceptsCallable($pureCb); + $this->acceptsPureCallable($cb); + $this->acceptsPureCallable($pureCb); + $this->acceptsInt($cb); + $this->acceptsInt($pureCb); + + $this->acceptsPureCallable(function (): int { + return 1; + }); + $this->acceptsPureCallable(function (): int { + sleep(1); + + return 1; + }); + } + + /** + * @param pure-Closure $cb + */ + public function acceptsPureClosure(\Closure $cb): void + { + + } + + public function doFoo2(): void + { + $this->acceptsPureClosure(function (): int { + return 1; + }); + $this->acceptsPureClosure(function (): int { + sleep(1); + + return 1; + }); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/reflection-class-issue-8679.php b/tests/PHPStan/Rules/Methods/data/reflection-class-issue-8679.php new file mode 100644 index 0000000000..32c3937b52 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/reflection-class-issue-8679.php @@ -0,0 +1,45 @@ +test1.$this->test2.$this->test3."\n"; + } +} + +class FooClassSimpleFactory +{ + /** + * @param array $options Options for MyClass + */ + public static function getClassA(array $options = []): FooClass + { + return (new \ReflectionClass('FooClass'))->newInstanceArgs($options); + } + + /** + * @param array $options Options for MyClass + */ + public static function getClassB(array $options = []): FooClass + { + return (new \ReflectionClass('FooClass'))->newInstanceArgs($options); + } + + /** + * @param array $options Options for MyClass + */ + public static function getClassC(array $options = []): FooClass + { + return (new \ReflectionClass('FooClass'))->newInstanceArgs($options); + } +} \ No newline at end of file diff --git a/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php b/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php index 647a05f3af..d93cdd1bd8 100644 --- a/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php +++ b/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php @@ -1,4 +1,4 @@ - 8.0 namespace RequiredAfterOptional; @@ -22,4 +22,31 @@ public function doLorem(bool $foo = true, $bar): void // not OK { } + public function doDolor(?int $foo = 1, $bar): void // not OK + { + } + + public function doSit(?int $foo = null, $bar): void // not OK + { + } + + public function doAmet(int|null $foo = 1, $bar): void // not OK + { + } + + public function doConsectetur(int|null $foo = null, $bar): void // not OK + { + } + + public function doAdipiscing(mixed $foo = 1, $bar): void // not OK + { + } + + public function doElit(mixed $foo = null, $bar): void // not OK + { + } + + public function doSed(int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK + { + } } diff --git a/tests/PHPStan/Rules/Methods/data/return-list-rule.php b/tests/PHPStan/Rules/Methods/data/return-list-rule.php new file mode 100644 index 0000000000..4827058bdf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-list-rule.php @@ -0,0 +1,87 @@ + + */ +class BinaryOpEnumValueRule implements Rule +{ + + /** @var class-string */ + private string $className; + + /** + * @param class-string $operator + */ + public function __construct(string $operator, ?string $okMessage = null) + { + $this->className = $operator; + } + + public function getNodeType(): string + { + return $this->className; + } + + /** + * @param BinaryOp $node + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + $leftType = $scope->getType($node->left); + $rightType = $scope->getType($node->right); + $isDirectCompareType = true; + + if (!$this->isEnumWithValue($leftType) || !$this->isEnumWithValue($rightType)) { + $isDirectCompareType = false; + } + + $errors = []; + $leftError = $this->processOpExpression($node->left, $leftType, $node->getOperatorSigil()); + $rightError = $this->processOpExpression($node->right, $rightType, $node->getOperatorSigil()); + + if ($leftError !== null) { + $errors[] = $leftError; + } + + if ($rightError !== null && $rightError !== $leftError) { + $errors[] = $rightError; + } + + if (!$isDirectCompareType && $errors === []) { + return []; + } + + if ($isDirectCompareType && $errors === []) { + $errors[] = sprintf( + 'Cannot compare %s to %s', + $leftType->describe(VerbosityLevel::typeOnly()), + $rightType->describe(VerbosityLevel::typeOnly()), + ); + } + + return array_map(static fn (string $message) => RuleErrorBuilder::message($message)->build(), $errors); + } + + private function processOpExpression(Expr $expression, Type $expressionType, string $sigil): ?string + { + return null; + } + + private function isEnumWithValue(Type $type): bool + { + return false; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/return-list.php b/tests/PHPStan/Rules/Methods/data/return-list.php new file mode 100644 index 0000000000..1e13f0b7bf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-list.php @@ -0,0 +1,21 @@ + */ + public function getList1(): array + { + return array_filter(['foo', 'bar'], 'file_exists'); + } + + /** + * @param array $array + * @return list + */ + public function getList2(array $array): array + { + return array_intersect_key(['foo', 'bar'], $array); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/return-rule-conditional-types.php b/tests/PHPStan/Rules/Methods/data/return-rule-conditional-types.php new file mode 100644 index 0000000000..129ad19e24 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-rule-conditional-types.php @@ -0,0 +1,49 @@ + + */ +class Foo implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // ok + return [ + RuleErrorBuilder::message('foo') + ->identifier('abc') + ->build(), + ]; + } + +} + +/** + * @implements Rule + */ +class Bar implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // not ok - returns plain string + return ['foo']; + } + +} + +/** + * @implements Rule + */ +class Baz implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // not ok - missing identifier + return [ + RuleErrorBuilder::message('foo') + ->build(), + ]; + } + +} + +/** + * @implements Rule + */ +class Lorem implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // not ok - not a list + return [ + 1 => RuleErrorBuilder::message('foo') + ->identifier('abc') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/return-type-class-constant.php b/tests/PHPStan/Rules/Methods/data/return-type-class-constant.php new file mode 100644 index 0000000000..59a5d51884 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-type-class-constant.php @@ -0,0 +1,31 @@ += 8.3 + +namespace ReturnTypeClassConstant; + +use function PHPStan\Testing\assertType; + +enum Foo +{ + + const static FOO = Foo::A; + + case A; + + public function returnStatic(): static + { + assertType('ReturnTypeClassConstant\Foo::A', self::FOO); + return self::FOO; + } + + public function returnStatic2(self $self): static + { + assertType('ReturnTypeClassConstant\Foo::A', $self::FOO); + return $self::FOO; + } + +} + +function (Foo $foo): void { + assertType('ReturnTypeClassConstant\Foo::A', Foo::FOO); + assertType('ReturnTypeClassConstant\Foo::A', $foo::FOO); +}; diff --git a/tests/PHPStan/Rules/Methods/data/returnTypes.php b/tests/PHPStan/Rules/Methods/data/returnTypes.php index 26b59ee6ec..e7030f8ad8 100644 --- a/tests/PHPStan/Rules/Methods/data/returnTypes.php +++ b/tests/PHPStan/Rules/Methods/data/returnTypes.php @@ -375,7 +375,7 @@ public function misleadingIntReturnType(): \ReturnTypes\integer } } - public function misleadingMixedReturnType(): mixed + /*public function misleadingMixedReturnType(): mixed { if (rand(0, 1)) { return 1; @@ -386,7 +386,7 @@ public function misleadingMixedReturnType(): mixed if (rand(0, 1)) { return new mixed(); } - } + }*/ } class FooChild extends Foo @@ -833,9 +833,8 @@ public function doFoo() /** * @return $this */ - public function doBar() + public function doBar(self $otherInstance) { - $otherInstance = new self(); assert($otherInstance instanceof FooInterface); return $otherInstance; } @@ -1256,3 +1255,28 @@ public function doBaz3(): string } } + +interface MySQLiAffectedRowsReturnTypeInterface +{ + /** + * @return int|numeric-string + */ + function exec(\mysqli $connection, string $sql); +} + +final class MySQLiAffectedRowsReturnType implements MySQLiAffectedRowsReturnTypeInterface +{ + /** + * @return int<0, max>|numeric-string + */ + function exec(\mysqli $mysqli, string $sql) + { + $result = $mysqli->query($sql); + + if ($result === false || 0 > $mysqli->affected_rows) { + throw new \RuntimeException(); + } + + return $mysqli->affected_rows; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/rule-error-signature.php b/tests/PHPStan/Rules/Methods/data/rule-error-signature.php new file mode 100644 index 0000000000..1fae783da6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/rule-error-signature.php @@ -0,0 +1,132 @@ + + */ +class Foo implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // ok + } + +} + +/** + * @implements Rule + */ +class Bar implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + // also ok + } + +} + +/** + * @implements Rule + */ +class Baz implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return (string|RuleError)[] errors + */ + public function processNode(Node $node, Scope $scope): array + { + // old return type - not ok + } + +} + +/** + * @implements Rule + */ +class Lorem implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return string[] + */ + public function processNode(Node $node, Scope $scope): array + { + // just strings - not ok + } + +} + +/** + * @implements Rule + */ +class Ipsum implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return RuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + // no identifiers - not ok + } + +} + +/** + * @implements Rule + */ +class Dolor implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return IdentifierRuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + // not a list - not ok + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/self-out.php b/tests/PHPStan/Rules/Methods/data/self-out.php new file mode 100644 index 0000000000..887ee2fbb0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/self-out.php @@ -0,0 +1,37 @@ += 8.4 + +namespace ShortGetPropertyHookReturn; + +class Foo +{ + + public int $i { + get => 'foo'; + } + + public int $i2 { + get => 1; + } + + /** @var non-empty-string */ + public string $s { + get => ''; + } + + /** @var non-empty-string */ + public string $s2 { + get => 'foo'; + } + +} + +/** + * @template T of Foo + */ +class GenericFoo +{ + + /** @var T */ + public Foo $a { + get => new Foo(); + } + + /** @var T */ + public Foo $a2 { + get => $this->a2; + } + + /** + * @param T $c + */ + public function __construct( + /** @var T */ + public Foo $b { + get => new Foo(); + }, + + /** @var T */ + public Foo $b2 { + get => $this->b2; + }, + + public Foo $c { + get => new Foo(); + }, + + public Foo $c2 { + get => $this->c2; + } + ) + { + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php b/tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php new file mode 100644 index 0000000000..1251bf5933 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php @@ -0,0 +1,20 @@ +escapeInput($value), $namespace); + } + + private function escapeInput(?string $value): ?string + { + if ($value === null) { + return null; + } + return htmlspecialchars((string) normalizer_normalize($value), ENT_XML1, 'UTF-8'); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/static-has-method.php b/tests/PHPStan/Rules/Methods/data/static-has-method.php new file mode 100644 index 0000000000..177bde0ab4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-has-method.php @@ -0,0 +1,51 @@ + false, 'id' => 5]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/template-type-in-one-branch-of-conditional.php b/tests/PHPStan/Rules/Methods/data/template-type-in-one-branch-of-conditional.php new file mode 100644 index 0000000000..5acc6e55d1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/template-type-in-one-branch-of-conditional.php @@ -0,0 +1,29 @@ +} $params + * @phpstan-return ($params is array{wrapperClass:mixed} ? T : Connection) + * @template T of Connection + */ + public static function getConnection(array $params): Connection { + return new Connection(); + } + + public static function test(): void + { + assertType(Connection::class, DriverManager::getConnection([])); + assertType(ChildConnection::class, DriverManager::getConnection(['wrapperClass' => ChildConnection::class])); + assertType(Connection::class, DriverManager::getConnection(['wrapperClass' => stdClass::class])); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/tentative-return-types.php b/tests/PHPStan/Rules/Methods/data/tentative-return-types.php index d42f319ae0..0471f4a578 100644 --- a/tests/PHPStan/Rules/Methods/data/tentative-return-types.php +++ b/tests/PHPStan/Rules/Methods/data/tentative-return-types.php @@ -43,3 +43,66 @@ public function getIterator(): string } } + +class TypedIterator implements \Iterator +{ + + public function current(): mixed + { + } + + public function next(): void + { + } + + public function key(): mixed + { + } + + public function valid(): bool + { + } + + public function rewind(): void + { + } + +} + +class UntypedIterator implements \Iterator +{ + + public function current() + { + } + + public function next() + { + } + + public function key() + { + } + + public function valid() + { + } + + public function rewind() + { + } + +} + + +abstract class MetadataFilter extends \FilterIterator +{ + /** + * @return \ArrayIterator + */ + #[\ReturnTypeWillChange] + public function getInnerIterator() + { + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/trait-mixin.php b/tests/PHPStan/Rules/Methods/data/trait-mixin.php new file mode 100644 index 0000000000..1aa5c3b428 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/trait-mixin.php @@ -0,0 +1,44 @@ + + */ +trait FooTrait +{ + +} + +class Usages +{ + + use FooTrait; + +} + +class ChildUsages extends Usages +{ + +} + +function (Usages $u): void { + assertType(Usages::class, $u->get()); +}; + +function (ChildUsages $u): void { + assertType(ChildUsages::class, $u->get()); +}; diff --git a/tests/PHPStan/Rules/Methods/data/tricky-callables.php b/tests/PHPStan/Rules/Methods/data/tricky-callables.php new file mode 100644 index 0000000000..189c65c71c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/tricky-callables.php @@ -0,0 +1,84 @@ +doBar($cb); + } + + /** + * @param callable(string|null): void $cb + */ + public function doBar(callable $cb) + { + + } + +} + +class Bar +{ + + /** + * @param callable(string): void $cb + */ + public function doFoo(callable $cb) + { + $this->doBar($cb); + } + + /** + * @param callable(string=): void $cb + */ + public function doBar(callable $cb) + { + + } + +} + +class Baz +{ + + /** + * @param callable(string): void $cb + */ + public function doFoo(callable $cb) + { + $this->doBar($cb); + } + + /** + * @param callable(): void $cb + */ + public function doBar(callable $cb) + { + + } + +} + +final class TwoErrorsAtOnce +{ + /** + * @param callable(string|int $key=): bool $filter + */ + public function run(callable $filter): void + { + } +} + +function (TwoErrorsAtOnce $t): void { + $filter = static fn (): bool => true; + $t->run($filter); + + $filter = static fn (int $key): bool => true; + $t->run($filter); +}; diff --git a/tests/PHPStan/Rules/Methods/data/true-typehint.php b/tests/PHPStan/Rules/Methods/data/true-typehint.php new file mode 100644 index 0000000000..780c0a776a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/true-typehint.php @@ -0,0 +1,44 @@ += 8.2 + +namespace NativeTrueType; + +use function PHPStan\Testing\assertType; + +class Truthy { + public true $truthy = true; + + public function foo(true $v): true { + assertType('true', $v); + } + + function trueUnion(true|null $trueUnion): void + { + assertType('true|null', $trueUnion); + + if (is_null($trueUnion)) { + assertType('null', $trueUnion); + return; + } + + if (is_bool($trueUnion)) { + assertType('true', $trueUnion); + return; + } + + assertType('*NEVER*', $trueUnion); + } + + function trueUnionReturn(): true|null + { + if (rand(1, 0)) { + return true; + } + return null; + } +} + +function foo(Truthy $truthy) { + assertType('true', $truthy->truthy); + assertType('true', $truthy->foo(true)); + assertType('true|null', $truthy->trueUnionReturn()); +} diff --git a/tests/PHPStan/Rules/Methods/data/unresolvable-parameter.php b/tests/PHPStan/Rules/Methods/data/unresolvable-parameter.php new file mode 100644 index 0000000000..9e44d39cf5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/unresolvable-parameter.php @@ -0,0 +1,23 @@ + $v + */ + public function foo($p, $v): void + { + } +} + +function (HelloWorld $foo) { + $foo->foo(0, 0); + $foo->foo('', 0); + $foo->foo([], 0); + $foo->foo([1], 0); + $foo->foo([1], 1); +}; diff --git a/tests/PHPStan/Rules/Methods/data/uppercase-string.php b/tests/PHPStan/Rules/Methods/data/uppercase-string.php new file mode 100644 index 0000000000..de7500ee8c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/uppercase-string.php @@ -0,0 +1,33 @@ +acceptUppercaseString('NotUpperCase'); + $this->acceptUppercaseString('UPPERCASE'); + $this->acceptUppercaseString($string); + $this->acceptUppercaseString($uppercaseString); + $this->acceptUppercaseString($numericString); + $this->acceptUppercaseString($nonEmptyLowercaseString); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/virtual-nullsafe-method-call.php b/tests/PHPStan/Rules/Methods/data/virtual-nullsafe-method-call.php new file mode 100644 index 0000000000..e5cd99d7a4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/virtual-nullsafe-method-call.php @@ -0,0 +1,4 @@ += 8.0 + +$foo->regularCall(); +$foo?->nullsafeCall(); diff --git a/tests/PHPStan/Rules/Methods/data/visibility-in-interace.php b/tests/PHPStan/Rules/Methods/data/visibility-in-interace.php new file mode 100644 index 0000000000..73ea93d662 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/visibility-in-interace.php @@ -0,0 +1,30 @@ + + */ + public function doFoo(): array + { + return $this->listOfBars(); + } + + /** + * @return list + */ + public function listOfBars(): array + { + return []; + } + +} + +class Test2 +{ + + /** + * @return non-empty-array + */ + public function doFoo(): array + { + return $this->nonEmptyArrayOfBars(); + } + + /** + * @return non-empty-array + */ + public function nonEmptyArrayOfBars(): array + { + /** @var Bar $b */ + $b = doFoo(); + return [$b]; + } + +} + +class Test3 +{ + + /** + * @return non-empty-list + */ + public function doFoo(): array + { + return $this->nonEmptyArrayOfBars(); + } + + /** + * @return array + */ + public function nonEmptyArrayOfBars(): array + { + return []; + } + +} diff --git a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php index 3d3d3e1b6e..23bb2c73cb 100644 --- a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php +++ b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php @@ -40,7 +40,11 @@ public function testRule(): void ], [ 'Method MissingReturn\Foo::doLorem() should return int but return statement is missing.', - 36, + 39, + ], + [ + 'Method MissingReturn\Foo::doLorem() should return int but return statement is missing.', + 47, ], [ 'Anonymous function should return int but return statement is missing.', @@ -106,6 +110,18 @@ public function testRule(): void 'Method MissingReturn\NeverReturn::doBaz2() should always throw an exception or terminate script execution but doesn\'t do that.', 481, ], + [ + 'Method MissingReturn\MorePreciseMissingReturnLines::doFoo() should return int but return statement is missing.', + 514, + ], + [ + 'Method MissingReturn\MorePreciseMissingReturnLines::doFoo() should return int but return statement is missing.', + 515, + ], + [ + 'Method MissingReturn\MorePreciseMissingReturnLines::doFoo2() should return int but return statement is missing.', + 524, + ], ]); } @@ -137,6 +153,16 @@ public function testMissingMixedReturnInEmptyBody(): void ]); } + public function testBug3488(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-3488.php'], []); + } + public function testBug3669(): void { $this->checkExplicitMixedMissingReturn = true; @@ -235,12 +261,12 @@ public function dataCheckPhpDocMissingReturn(): array /** * @dataProvider dataCheckPhpDocMissingReturn - * @param mixed[] $errors + * @param list $errors */ public function testCheckPhpDocMissingReturn(bool $checkPhpDocMissingReturn, array $errors): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); } $this->checkExplicitMixedMissingReturn = true; @@ -265,13 +291,13 @@ public function dataModelMixin(): array */ public function testModelMixin(bool $checkExplicitMixedMissingReturn): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); } $this->checkExplicitMixedMissingReturn = $checkExplicitMixedMissingReturn; $this->checkPhpDocMissingReturn = true; - $this->analyse([__DIR__ . '/../../Analyser/data/model-mixin.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/model-mixin.php'], [ [ 'Method ModelMixin\Model::__callStatic() should return mixed but return statement is missing.', 13, @@ -279,4 +305,90 @@ public function testModelMixin(bool $checkExplicitMixedMissingReturn): void ]); } + public function testBug6257(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->checkExplicitMixedMissingReturn = true; + $this->checkPhpDocMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-6257.php'], [ + [ + 'Function ReturnTypes\sometimesThrows() should always throw an exception or terminate script execution but doesn\'t do that.', + 27, + ], + ]); + } + + public function testBug7384(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->checkPhpDocMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-7384.php'], []); + } + + public function testBug9309(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-9309.php'], []); + } + + public function testBug6807(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-6807.php'], []); + } + + public function testBug8463(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-8463.php'], []); + } + + public function testBug9374(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-9374.php'], []); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/property-hooks-missing-return.php'], [ + [ + 'Get hook for property PropertyHooksMissingReturn\Foo::$i should return int but return statement is missing.', + 10, + ], + [ + 'Get hook for property PropertyHooksMissingReturn\Foo::$j should return int but return statement is missing.', + 23, + ], + ]); + } + + public function testBug3488Two(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-3488-2.php'], [ + [ + 'Method Bug3488\C::invalidCase() should return int but return statement is missing.', + 30, + ], + ]); + } + + public function testBug12722(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-12722.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Missing/data/bug-12722.php b/tests/PHPStan/Rules/Missing/data/bug-12722.php new file mode 100644 index 0000000000..6c42edbd19 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-12722.php @@ -0,0 +1,32 @@ += 8.1 + +namespace Bug12722; + +enum states { + case state1; + case statealmost1; + case state3; +} + +class HelloWorld +{ + public function intentional_fallthrough(states $state): int + { + switch($state) { + + case states::state1: //intentional fall-trough this case... + case states::statealmost1: return 1; + case states::state3: return 3; + } + } + + public function no_fallthrough(states $state): int + { + switch($state) { + + case states::state1: return 1; + case states::statealmost1: return 1; + case states::state3: return 3; + } + } +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-3488-2.php b/tests/PHPStan/Rules/Missing/data/bug-3488-2.php new file mode 100644 index 0000000000..87d3466236 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-3488-2.php @@ -0,0 +1,52 @@ += 8.1 + +namespace Bug3488; + +enum EnumWithThreeCases { + case ValueA; + case ValueB; + case ValueC; +} + +function testFunction(EnumWithThreeCases $var) : int +{ + switch ($var) { + case EnumWithThreeCases::ValueA: + // some other code + return 1; + case EnumWithThreeCases::ValueB: + // some other code + return 2; + case EnumWithThreeCases::ValueC: + // some other code + return 3; + } +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-6257.php b/tests/PHPStan/Rules/Missing/data/bug-6257.php new file mode 100644 index 0000000000..acb1023c88 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-6257.php @@ -0,0 +1,31 @@ += 8.0 + +namespace ReturnTypes; + +/** + * @return never + */ +function alwaysThrow() { + match(true) { + true => throw new \Exception(), + }; +} + +/** + * @return never + */ +function alwaysThrow2() { + match(rand(0, 1)) { + 0 => throw new \Exception(), + }; +} + +/** + * @return never + */ +function sometimesThrows() { + match(rand(0, 1)) { + 0 => throw new \Exception(), + default => 'test', + }; +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-6807.php b/tests/PHPStan/Rules/Missing/data/bug-6807.php new file mode 100644 index 0000000000..d7ffdde9dd --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-6807.php @@ -0,0 +1,16 @@ + 5) + throw new Exception(); + + if (rand() == 1) + return 5; + } +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-7384.php b/tests/PHPStan/Rules/Missing/data/bug-7384.php new file mode 100644 index 0000000000..3ef533d5bc --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-7384.php @@ -0,0 +1,26 @@ += 8.4 + +namespace PropertyHooksMissingReturn; + +class Foo +{ + + public int $i { + get { + if (rand(0, 1)) { + + } else { + return 1; + } + } + + set { + // set hook returns void + } + } + + public int $j { + get { + + } + } + + public int $ok { + get { + return $this->ok + 1; + } + } + +} diff --git a/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php new file mode 100644 index 0000000000..ce58341c06 --- /dev/null +++ b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php @@ -0,0 +1,92 @@ + + */ +final class UsedNamesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UsedNamesRule(); + } + + public function testSimpleUses(): void + { + $this->analyse([__DIR__ . '/data/simple-uses.php'], [ + [ + 'Cannot declare class SomeNamespace\SimpleUses because the name is already in use.', + 7, + ], + ]); + } + + public function testGroupedUses(): void + { + $this->analyse([__DIR__ . '/data/grouped-uses.php'], [ + [ + 'Cannot declare interface SomeNamespace\GroupedUses because the name is already in use.', + 10, + ], + ]); + } + + public function testSimpleUsesUnderClass(): void + { + $this->analyse([__DIR__ . '/data/simple-uses-under-class.php'], [ + [ + 'Cannot use SomeOtherNamespace\UsesUnderClass as SimpleUsesUnderClass because the name is already in use.', + 9, + ], + ]); + } + + public function testGroupedUsesUnderClass(): void + { + $this->analyse([__DIR__ . '/data/grouped-uses-under-class.php'], [ + [ + 'Cannot use SomeOtherNamespace\FooBar as FooBar because the name is already in use.', + 14, + ], + [ + 'Cannot use SomeOtherNamespace\UsesUnderClass as GroupedUsesUnderClass because the name is already in use.', + 15, + ], + ]); + } + + public function testNoNamespace(): void + { + $this->analyse([__DIR__ . '/data/no-namespace.php'], [ + [ + 'Cannot declare class NoNamespace because the name is already in use.', + 5, + ], + [ + 'Cannot declare class NoNamespace because the name is already in use.', + 9, + ], + ]); + } + + public function testMultipleNamespaces(): void + { + $this->analyse([__DIR__ . '/data/multiple-namespaces.php'], [ + [ + 'Cannot declare trait FirstNamespace\MultipleNamespaces because the name is already in use.', + 24, + ], + ]); + } + + public function testIgnoreUseFunctionAndConstant(): void + { + $this->analyse([__DIR__ . '/data/ignore-use-function-and-constant.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php b/tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php new file mode 100644 index 0000000000..0c96a7b5ac --- /dev/null +++ b/tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php @@ -0,0 +1,16 @@ +createReflectionProvider(); - return new ExistingNamesInGroupUseRule($broker, new ClassCaseSensitivityCheck($broker, true), true); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingNamesInGroupUseRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php index 7389c4b58e..13fa5a254b 100644 --- a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php +++ b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Namespaces; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,8 +16,18 @@ class ExistingNamesInUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingNamesInUseRule($broker, new ClassCaseSensitivityCheck($broker, true), true); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingNamesInUseRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ); } public function testRule(): void @@ -47,4 +59,17 @@ public function testRule(): void ]); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \PrefixedRuntimeException?'; + + $this->analyse([__DIR__ . '/../Classes/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\PrefixedRuntimeException.', + 14, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/NodeConnectingRule.php b/tests/PHPStan/Rules/NodeConnectingRule.php deleted file mode 100644 index 86dd273d89..0000000000 --- a/tests/PHPStan/Rules/NodeConnectingRule.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ -class NodeConnectingRule implements Rule -{ - - public function getNodeType(): string - { - return Node\Stmt\Echo_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - return [ - sprintf( - 'Parent: %s, previous: %s, next: %s', - get_class($node->getAttribute('parent')), - get_class($node->getAttribute('previous')), - get_class($node->getAttribute('next')), - ), - ]; - } - -} diff --git a/tests/PHPStan/Rules/NodeConnectingRuleTest.php b/tests/PHPStan/Rules/NodeConnectingRuleTest.php deleted file mode 100644 index 410b58b36a..0000000000 --- a/tests/PHPStan/Rules/NodeConnectingRuleTest.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -class NodeConnectingRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new NodeConnectingRule(); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/node-connecting.php'], [ - [ - 'Parent: PhpParser\Node\Stmt\If_, previous: PhpParser\Node\Stmt\Switch_, next: PhpParser\Node\Stmt\Foreach_', - 11, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php index 3bda92c21b..8dfd60fc26 100644 --- a/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php @@ -19,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->analyse([__DIR__ . '/data/invalid-assign-var.php'], [ [ 'Nullsafe operator cannot be on left side of assignment.', diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index 0e88a2e23b..cc27629dcf 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -2,7 +2,8 @@ namespace PHPStan\Rules\Operators; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -14,11 +15,15 @@ class InvalidBinaryOperationRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { return new InvalidBinaryOperationRule( - new Standard(), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), + new ExprPrinter(new Printer()), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), ); } @@ -102,11 +107,11 @@ public function testRule(): void 127, ], [ - 'Binary operation "." between array|string and \'xyz\' results in an error.', + 'Binary operation "." between list|string and \'xyz\' results in an error.', 134, ], [ - 'Binary operation "+" between (array|string) and 1 results in an error.', + 'Binary operation "+" between (list|string) and 1 results in an error.', 136, ], [ @@ -241,6 +246,26 @@ public function testRule(): void 'Binary operation "/" between 10 and literal-string results in an error.', 222, ], + [ + 'Binary operation "+" between int and array{} results in an error.', + 259, + ], + [ + 'Binary operation "%" between array and 3 results in an error.', + 267, + ], + [ + 'Binary operation "%" between 3 and array results in an error.', + 268, + ], + [ + 'Binary operation "%" between object and 3 results in an error.', + 270, + ], + [ + 'Binary operation "%" between 3 and object results in an error.', + 271, + ], ]); } @@ -254,6 +279,11 @@ public function testBug3515(): void $this->analyse([__DIR__ . '/data/bug-3515.php'], []); } + public function testBug8827(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8827.php'], []); + } + public function testRuleWithNullsafeVariant(): void { if (PHP_VERSION_ID < 80000) { @@ -268,4 +298,519 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function testBug5309(): void + { + $this->analyse([__DIR__ . '/data/bug-5309.php'], []); + } + + public function testBinaryMixed(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/invalid-binary-mixed.php'], [ + [ + 'Binary operation "." between T and \'a\' results in an error.', + 11, + ], + [ + 'Binary operation ".=" between \'a\' and T results in an error.', + 13, + ], + [ + 'Binary operation "**" between T and 2 results in an error.', + 15, + ], + [ + 'Binary operation "*" between T and 2 results in an error.', + 16, + ], + [ + 'Binary operation "/" between T and 2 results in an error.', + 17, + ], + [ + 'Binary operation "%" between T and 2 results in an error.', + 18, + ], + [ + 'Binary operation "+" between T and 2 results in an error.', + 19, + ], + [ + 'Binary operation "-" between T and 2 results in an error.', + 20, + ], + [ + 'Binary operation "<<" between T and 2 results in an error.', + 21, + ], + [ + 'Binary operation ">>" between T and 2 results in an error.', + 22, + ], + [ + 'Binary operation "&" between T and 2 results in an error.', + 23, + ], + [ + 'Binary operation "|" between T and 2 results in an error.', + 24, + ], + [ + 'Binary operation "+=" between 5 and T results in an error.', + 26, + ], + [ + 'Binary operation "-=" between 5 and T results in an error.', + 29, + ], + [ + 'Binary operation "*=" between 5 and T results in an error.', + 32, + ], + [ + 'Binary operation "**=" between 5 and T results in an error.', + 35, + ], + [ + 'Binary operation "/=" between 5 and T results in an error.', + 38, + ], + [ + 'Binary operation "%=" between 5 and T results in an error.', + 41, + ], + [ + 'Binary operation "&=" between 5 and T results in an error.', + 44, + ], + [ + 'Binary operation "|=" between 5 and T results in an error.', + 47, + ], + [ + 'Binary operation "^=" between 5 and T results in an error.', + 50, + ], + [ + 'Binary operation "<<=" between 5 and T results in an error.', + 53, + ], + [ + 'Binary operation ">>=" between 5 and T results in an error.', + 56, + ], + [ + 'Binary operation "." between mixed and \'a\' results in an error.', + 61, + ], + [ + 'Binary operation ".=" between \'a\' and mixed results in an error.', + 63, + ], + [ + 'Binary operation "**" between mixed and 2 results in an error.', + 65, + ], + [ + 'Binary operation "*" between mixed and 2 results in an error.', + 66, + ], + [ + 'Binary operation "/" between mixed and 2 results in an error.', + 67, + ], + [ + 'Binary operation "%" between mixed and 2 results in an error.', + 68, + ], + [ + 'Binary operation "+" between mixed and 2 results in an error.', + 69, + ], + [ + 'Binary operation "-" between mixed and 2 results in an error.', + 70, + ], + [ + 'Binary operation "<<" between mixed and 2 results in an error.', + 71, + ], + [ + 'Binary operation ">>" between mixed and 2 results in an error.', + 72, + ], + [ + 'Binary operation "&" between mixed and 2 results in an error.', + 73, + ], + [ + 'Binary operation "|" between mixed and 2 results in an error.', + 74, + ], + [ + 'Binary operation "+=" between 5 and mixed results in an error.', + 76, + ], + [ + 'Binary operation "-=" between 5 and mixed results in an error.', + 79, + ], + [ + 'Binary operation "*=" between 5 and mixed results in an error.', + 82, + ], + [ + 'Binary operation "**=" between 5 and mixed results in an error.', + 85, + ], + [ + 'Binary operation "/=" between 5 and mixed results in an error.', + 88, + ], + [ + 'Binary operation "%=" between 5 and mixed results in an error.', + 91, + ], + [ + 'Binary operation "&=" between 5 and mixed results in an error.', + 94, + ], + [ + 'Binary operation "|=" between 5 and mixed results in an error.', + 97, + ], + [ + 'Binary operation "^=" between 5 and mixed results in an error.', + 100, + ], + [ + 'Binary operation "<<=" between 5 and mixed results in an error.', + 103, + ], + [ + 'Binary operation ">>=" between 5 and mixed results in an error.', + 106, + ], + [ + 'Binary operation "." between mixed and \'a\' results in an error.', + 111, + ], + [ + 'Binary operation ".=" between \'a\' and mixed results in an error.', + 113, + ], + [ + 'Binary operation "**" between mixed and 2 results in an error.', + 115, + ], + [ + 'Binary operation "*" between mixed and 2 results in an error.', + 116, + ], + [ + 'Binary operation "/" between mixed and 2 results in an error.', + 117, + ], + [ + 'Binary operation "%" between mixed and 2 results in an error.', + 118, + ], + [ + 'Binary operation "+" between mixed and 2 results in an error.', + 119, + ], + [ + 'Binary operation "-" between mixed and 2 results in an error.', + 120, + ], + [ + 'Binary operation "<<" between mixed and 2 results in an error.', + 121, + ], + [ + 'Binary operation ">>" between mixed and 2 results in an error.', + 122, + ], + [ + 'Binary operation "&" between mixed and 2 results in an error.', + 123, + ], + [ + 'Binary operation "|" between mixed and 2 results in an error.', + 124, + ], + [ + 'Binary operation "+=" between 5 and mixed results in an error.', + 126, + ], + [ + 'Binary operation "-=" between 5 and mixed results in an error.', + 129, + ], + [ + 'Binary operation "*=" between 5 and mixed results in an error.', + 132, + ], + [ + 'Binary operation "**=" between 5 and mixed results in an error.', + 135, + ], + [ + 'Binary operation "/=" between 5 and mixed results in an error.', + 138, + ], + [ + 'Binary operation "%=" between 5 and mixed results in an error.', + 141, + ], + [ + 'Binary operation "&=" between 5 and mixed results in an error.', + 144, + ], + [ + 'Binary operation "|=" between 5 and mixed results in an error.', + 147, + ], + [ + 'Binary operation "^=" between 5 and mixed results in an error.', + 150, + ], + [ + 'Binary operation "<<=" between 5 and mixed results in an error.', + 153, + ], + [ + 'Binary operation ">>=" between 5 and mixed results in an error.', + 156, + ], + ]); + } + + public function testBug7538(): void + { + $this->analyse([__DIR__ . '/data/bug-7538.php'], [ + [ + 'Binary operation "%" between stdClass and stdClass results in an error.', + 7, + ], + ]); + } + + public function testBug10440(): void + { + $this->analyse([__DIR__ . '/data/bug-10440.php'], [ + [ + 'Binary operation "%" between array{} and array{\'\'} results in an error.', + 8, + ], + ]); + } + + public function testBenevolentUnion(): void + { + $this->analyse([__DIR__ . '/data/binary-op-benevolent-union.php'], [ + [ + 'Binary operation "+" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\Foo results in an error.', + 12, + ], + [ + 'Binary operation "+=" between BinaryOpBenevolentUnion\Foo and (array|bool|int|object|resource) results in an error.', + 24, + ], + [ + 'Binary operation "**" between (array|bool|int|object|resource) and array{} results in an error.', + 42, + ], + [ + 'Binary operation "**" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 43, + ], + [ + 'Binary operation "**=" between array{} and (array|bool|int|object|resource) results in an error.', + 52, + ], + [ + 'Binary operation "**=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 55, + ], + [ + 'Binary operation "*" between (array|bool|int|object|resource) and array{} results in an error.', + 73, + ], + [ + 'Binary operation "*" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 74, + ], + [ + 'Binary operation "*=" between array{} and (array|bool|int|object|resource) results in an error.', + 83, + ], + [ + 'Binary operation "*=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 86, + ], + [ + 'Binary operation "/" between (array|bool|int|object|resource) and array{} results in an error.', + 104, + ], + [ + 'Binary operation "/" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 105, + ], + [ + 'Binary operation "/=" between array{} and (array|bool|int|object|resource) results in an error.', + 114, + ], + [ + 'Binary operation "/=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 117, + ], + [ + 'Binary operation "%" between (array|bool|int|object|resource) and array{} results in an error.', + 135, + ], + [ + 'Binary operation "%" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 136, + ], + [ + 'Binary operation "%=" between array{} and (array|bool|int|object|resource) results in an error.', + 145, + ], + [ + 'Binary operation "%=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 148, + ], + [ + 'Binary operation "-" between (array|bool|int|object|resource) and array{} results in an error.', + 166, + ], + [ + 'Binary operation "-" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 167, + ], + [ + 'Binary operation "-=" between array{} and (array|bool|int|object|resource) results in an error.', + 176, + ], + [ + 'Binary operation "-=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 179, + ], + [ + 'Binary operation "." between (array|bool|int|object|resource) and array{} results in an error.', + 197, + ], + [ + 'Binary operation "." between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 198, + ], + [ + 'Binary operation ".=" between array{} and (array|bool|int|object|resource) results in an error.', + 207, + ], + [ + 'Binary operation ".=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 210, + ], + [ + 'Binary operation "<<" between (array|bool|int|object|resource) and array{} results in an error.', + 228, + ], + [ + 'Binary operation "<<" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 229, + ], + [ + 'Binary operation "<<=" between array{} and (array|bool|int|object|resource) results in an error.', + 238, + ], + [ + 'Binary operation "<<=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 241, + ], + [ + 'Binary operation ">>" between (array|bool|int|object|resource) and array{} results in an error.', + 259, + ], + [ + 'Binary operation ">>" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 260, + ], + [ + 'Binary operation ">>=" between array{} and (array|bool|int|object|resource) results in an error.', + 269, + ], + [ + 'Binary operation ">>=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 272, + ], + [ + 'Binary operation "&" between (array|bool|int|object|resource) and array{} results in an error.', + 290, + ], + [ + 'Binary operation "&" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 291, + ], + [ + 'Binary operation "&=" between array{} and (array|bool|int|object|resource) results in an error.', + 300, + ], + [ + 'Binary operation "&=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 303, + ], + [ + 'Binary operation "^" between (array|bool|int|object|resource) and array{} results in an error.', + 321, + ], + [ + 'Binary operation "^" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 322, + ], + [ + 'Binary operation "^=" between array{} and (array|bool|int|object|resource) results in an error.', + 331, + ], + [ + 'Binary operation "^=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 334, + ], + [ + 'Binary operation "|" between (array|bool|int|object|resource) and array{} results in an error.', + 352, + ], + [ + 'Binary operation "|" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 353, + ], + [ + 'Binary operation "|=" between array{} and (array|bool|int|object|resource) results in an error.', + 362, + ], + [ + 'Binary operation "|=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 365, + ], + ]); + } + + public function testBug7863(): void + { + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-7863.php'], [ + [ + 'Binary operation "+" between mixed and array results in an error.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php index 03f6bece97..3305d725c4 100644 --- a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php @@ -16,7 +16,7 @@ class InvalidComparisonOperationRuleTest extends RuleTestCase protected function getRule(): Rule { return new InvalidComparisonOperationRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -143,6 +143,14 @@ public function testRule(): void 'Comparison operation "<" between array and array|int results in an error.', 98, ], + [ + 'Comparison operation ">" between array{1} and 2147483647|9223372036854775807 results in an error.', + 115, + ], + [ + 'Comparison operation "<" between numeric-string and DateTimeImmutable results in an error.', + 119, + ], ]); } @@ -160,4 +168,9 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function testBug11119(): void + { + $this->analyse([__DIR__ . '/data/bug-11119.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php index 7eda97b50c..63a9630c09 100644 --- a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\Operators; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -11,9 +13,15 @@ class InvalidIncDecOperationRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new InvalidIncDecOperationRule(false); + return new InvalidIncDecOperationRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), + ); } public function testRule(): void @@ -31,6 +39,112 @@ public function testRule(): void 'Cannot use ++ on stdClass.', 17, ], + [ + 'Cannot use ++ on InvalidIncDec\\ClassWithToString.', + 19, + ], + [ + 'Cannot use -- on InvalidIncDec\\ClassWithToString.', + 21, + ], + [ + 'Cannot use ++ on array{}.', + 23, + ], + [ + 'Cannot use -- on array{}.', + 25, + ], + [ + 'Cannot use ++ on resource.', + 28, + ], + [ + 'Cannot use -- on resource.', + 32, + ], + ]); + } + + public function testMixed(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/invalid-inc-dec-mixed.php'], [ + [ + 'Cannot use ++ on T of mixed.', + 12, + ], + [ + 'Cannot use ++ on T of mixed.', + 14, + ], + [ + 'Cannot use -- on T of mixed.', + 16, + ], + [ + 'Cannot use -- on T of mixed.', + 18, + ], + [ + 'Cannot use ++ on mixed.', + 24, + ], + [ + 'Cannot use ++ on mixed.', + 26, + ], + [ + 'Cannot use -- on mixed.', + 28, + ], + [ + 'Cannot use -- on mixed.', + 30, + ], + [ + 'Cannot use ++ on mixed.', + 36, + ], + [ + 'Cannot use ++ on mixed.', + 38, + ], + [ + 'Cannot use -- on mixed.', + 40, + ], + [ + 'Cannot use -- on mixed.', + 42, + ], + ]); + } + + public function testUnion(): void + { + $this->analyse([__DIR__ . '/data/invalid-inc-dec-union.php'], [ + [ + 'Cannot use ++ on array|bool|float|int|object|string|null.', + 24, + ], + [ + 'Cannot use -- on array|bool|float|int|object|string|null.', + 26, + ], + [ + 'Cannot use ++ on (array|object).', + 29, + ], + [ + 'Cannot use -- on (array|object).', + 31, + ], ]); } diff --git a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php index fbc6a265f9..5b1b47c41e 100644 --- a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\Operators; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -11,9 +13,15 @@ class InvalidUnaryOperationRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new InvalidUnaryOperationRule(); + return new InvalidUnaryOperationRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), + ); } public function testRule(): void @@ -39,6 +47,128 @@ public function testRule(): void 'Unary operation "~" on array{} results in an error.', 24, ], + [ + 'Unary operation "~" on bool results in an error.', + 36, + ], + [ + 'Unary operation "+" on array results in an error.', + 38, + ], + [ + 'Unary operation "-" on array results in an error.', + 39, + ], + [ + 'Unary operation "~" on array results in an error.', + 40, + ], + [ + 'Unary operation "+" on object results in an error.', + 42, + ], + [ + 'Unary operation "-" on object results in an error.', + 43, + ], + [ + 'Unary operation "~" on object results in an error.', + 44, + ], + [ + 'Unary operation "+" on resource results in an error.', + 50, + ], + [ + 'Unary operation "-" on resource results in an error.', + 51, + ], + [ + 'Unary operation "~" on resource results in an error.', + 52, + ], + [ + 'Unary operation "~" on null results in an error.', + 61, + ], + ]); + } + + public function testMixed(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkImplicitMixed = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/invalid-unary-mixed.php'], [ + [ + 'Unary operation "+" on T results in an error.', + 11, + ], + [ + 'Unary operation "-" on T results in an error.', + 12, + ], + [ + 'Unary operation "~" on T results in an error.', + 13, + ], + [ + 'Unary operation "+" on mixed results in an error.', + 18, + ], + [ + 'Unary operation "-" on mixed results in an error.', + 19, + ], + [ + 'Unary operation "~" on mixed results in an error.', + 20, + ], + [ + 'Unary operation "+" on mixed results in an error.', + 25, + ], + [ + 'Unary operation "-" on mixed results in an error.', + 26, + ], + [ + 'Unary operation "~" on mixed results in an error.', + 27, + ], + ]); + } + + public function testUnion(): void + { + $this->analyse([__DIR__ . '/data/unary-union.php'], [ + [ + 'Unary operation "+" on array|bool|float|int|object|string|null results in an error.', + 21, + ], + [ + 'Unary operation "-" on array|bool|float|int|object|string|null results in an error.', + 22, + ], + [ + 'Unary operation "~" on array|bool|float|int|object|string|null results in an error.', + 23, + ], + [ + 'Unary operation "+" on (array|object) results in an error.', + 25, + ], + [ + 'Unary operation "-" on (array|object) results in an error.', + 26, + ], + [ + 'Unary operation "~" on (array|object) results in an error.', + 27, + ], ]); } diff --git a/tests/PHPStan/Rules/Operators/data/binary-op-benevolent-union.php b/tests/PHPStan/Rules/Operators/data/binary-op-benevolent-union.php new file mode 100644 index 0000000000..1163df914a --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/binary-op-benevolent-union.php @@ -0,0 +1,377 @@ +|int|object|bool|resource> $benevolent + */ +function plus($benevolent, Foo $object): void +{ + echo $benevolent + 1; + echo $benevolent + []; + echo $benevolent + $object; + echo $benevolent + '123'; + echo $benevolent + 1.23; + echo $benevolent + $benevolent; + + $a = 1; + $a += $benevolent; + + $a = []; + $a += $benevolent; + + $a = $object; + $a += $benevolent; + + $a = '123'; + $a += $benevolent; + + $a = 1.23; + $a += $benevolent; + + $a = $benevolent; + $a += $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function exponent($benevolent, Foo $object): void +{ + echo $benevolent ** 1; + echo $benevolent ** []; + echo $benevolent ** $object; + echo $benevolent ** '123'; + echo $benevolent ** 1.23; + echo $benevolent ** $benevolent; + + $a = 1; + $a **= $benevolent; + + $a = []; + $a **= $benevolent; + + $a = $object; + $a **= $benevolent; + + $a = '123'; + $a **= $benevolent; + + $a = 1.23; + $a **= $benevolent; + + $a = $benevolent; + $a **= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function mul($benevolent, Foo $object): void +{ + echo $benevolent * 1; + echo $benevolent * []; + echo $benevolent * $object; + echo $benevolent * '123'; + echo $benevolent * 1.23; + echo $benevolent * $benevolent; + + $a = 1; + $a *= $benevolent; + + $a = []; + $a *= $benevolent; + + $a = $object; + $a *= $benevolent; + + $a = '123'; + $a *= $benevolent; + + $a = 1.23; + $a *= $benevolent; + + $a = $benevolent; + $a *= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function div($benevolent, Foo $object): void +{ + echo $benevolent / 1; + echo $benevolent / []; + echo $benevolent / $object; + echo $benevolent / '123'; + echo $benevolent / 1.23; + echo $benevolent / $benevolent; + + $a = 1; + $a /= $benevolent; + + $a = []; + $a /= $benevolent; + + $a = $object; + $a /= $benevolent; + + $a = '123'; + $a /= $benevolent; + + $a = 1.23; + $a /= $benevolent; + + $a = $benevolent; + $a /= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function mod($benevolent, Foo $object): void +{ + echo $benevolent % 1; + echo $benevolent % []; + echo $benevolent % $object; + echo $benevolent % '123'; + echo $benevolent % 1.23; + echo $benevolent % $benevolent; + + $a = 1; + $a %= $benevolent; + + $a = []; + $a %= $benevolent; + + $a = $object; + $a %= $benevolent; + + $a = '123'; + $a %= $benevolent; + + $a = 1.23; + $a %= $benevolent; + + $a = $benevolent; + $a %= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function minus($benevolent, Foo $object): void +{ + echo $benevolent - 1; + echo $benevolent - []; + echo $benevolent - $object; + echo $benevolent - '123'; + echo $benevolent - 1.23; + echo $benevolent - $benevolent; + + $a = 1; + $a -= $benevolent; + + $a = []; + $a -= $benevolent; + + $a = $object; + $a -= $benevolent; + + $a = '123'; + $a -= $benevolent; + + $a = 1.23; + $a -= $benevolent; + + $a = $benevolent; + $a -= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function concat($benevolent, Foo $object): void +{ + echo $benevolent . 1; + echo $benevolent . []; + echo $benevolent . $object; + echo $benevolent . '123'; + echo $benevolent . 1.23; + echo $benevolent . $benevolent; + + $a = 1; + $a .= $benevolent; + + $a = []; + $a .= $benevolent; + + $a = $object; + $a .= $benevolent; + + $a = '123'; + $a .= $benevolent; + + $a = 1.23; + $a .= $benevolent; + + $a = $benevolent; + $a .= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function lshift($benevolent, Foo $object): void +{ + echo $benevolent << 1; + echo $benevolent << []; + echo $benevolent << $object; + echo $benevolent << '123'; + echo $benevolent << 1.23; + echo $benevolent << $benevolent; + + $a = 1; + $a <<= $benevolent; + + $a = []; + $a <<= $benevolent; + + $a = $object; + $a <<= $benevolent; + + $a = '123'; + $a <<= $benevolent; + + $a = 1<<23; + $a <<= $benevolent; + + $a = $benevolent; + $a <<= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function rshift($benevolent, Foo $object): void +{ + echo $benevolent >> 1; + echo $benevolent >> []; + echo $benevolent >> $object; + echo $benevolent >> '123'; + echo $benevolent >> 1.23; + echo $benevolent >> $benevolent; + + $a = 1; + $a >>= $benevolent; + + $a = []; + $a >>= $benevolent; + + $a = $object; + $a >>= $benevolent; + + $a = '123'; + $a >>= $benevolent; + + $a = 1>>23; + $a >>= $benevolent; + + $a = $benevolent; + $a >>= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function bitAnd($benevolent, Foo $object): void +{ + echo $benevolent & 1; + echo $benevolent & []; + echo $benevolent & $object; + echo $benevolent & '123'; + echo $benevolent & 1.23; + echo $benevolent & $benevolent; + + $a = 1; + $a &= $benevolent; + + $a = []; + $a &= $benevolent; + + $a = $object; + $a &= $benevolent; + + $a = '123'; + $a &= $benevolent; + + $a = 1.23; + $a &= $benevolent; + + $a = $benevolent; + $a &= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function bitXor($benevolent, Foo $object): void +{ + echo $benevolent ^ 1; + echo $benevolent ^ []; + echo $benevolent ^ $object; + echo $benevolent ^ '123'; + echo $benevolent ^ 1.23; + echo $benevolent ^ $benevolent; + + $a = 1; + $a ^= $benevolent; + + $a = []; + $a ^= $benevolent; + + $a = $object; + $a ^= $benevolent; + + $a = '123'; + $a ^= $benevolent; + + $a = 1.23; + $a ^= $benevolent; + + $a = $benevolent; + $a ^= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function bitOr($benevolent, Foo $object): void +{ + echo $benevolent | 1; + echo $benevolent | []; + echo $benevolent | $object; + echo $benevolent | '123'; + echo $benevolent | 1.23; + echo $benevolent | $benevolent; + + $a = 1; + $a |= $benevolent; + + $a = []; + $a |= $benevolent; + + $a = $object; + $a |= $benevolent; + + $a = '123'; + $a |= $benevolent; + + $a = 1.23; + $a |= $benevolent; + + $a = $benevolent; + $a |= $benevolent; +} + +class Foo {} diff --git a/tests/PHPStan/Rules/Operators/data/bug-10440.php b/tests/PHPStan/Rules/Operators/data/bug-10440.php new file mode 100644 index 0000000000..704cb8a695 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-10440.php @@ -0,0 +1,8 @@ + ($carry instanceof DateTime && $carry < $time) ? $carry : $time, + null + ); +}; diff --git a/tests/PHPStan/Rules/Operators/data/bug-5309.php b/tests/PHPStan/Rules/Operators/data/bug-5309.php new file mode 100644 index 0000000000..3ed4a5a833 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-5309.php @@ -0,0 +1,17 @@ + 0) { + $x += 1; + } + if ($x > 0) { + return 5 / $x; + } + + return 1.0; +} + diff --git a/tests/PHPStan/Rules/Operators/data/bug-7538.php b/tests/PHPStan/Rules/Operators/data/bug-7538.php new file mode 100644 index 0000000000..4ead552b2b --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-7538.php @@ -0,0 +1,7 @@ + 0; + var_dump($a ** 2); + var_dump($a * 2); + var_dump($a / 2); + var_dump($a % 2); + var_dump($a + 2); + var_dump($a - 2); + var_dump($a << 2); + var_dump($a >> 2); + var_dump($a & 2); + var_dump($a | 2); + $c = 5; + $c += $a; + + $c = 5; + $c -= $a; + + $c = 5; + $c *= $a; + + $c = 5; + $c **= $a; + + $c = 5; + $c /= $a; + + $c = 5; + $c %= $a; + + $c = 5; + $c &= $a; + + $c = 5; + $c |= $a; + + $c = 5; + $c ^= $a; + + $c = 5; + $c <<= $a; + + $c = 5; + $c >>= $a; +} + +function explicitMixed(mixed $a): void +{ + var_dump($a . 'a'); + $b = 'a'; + $b .= $a; + $bool = rand() > 0; + var_dump($a ** 2); + var_dump($a * 2); + var_dump($a / 2); + var_dump($a % 2); + var_dump($a + 2); + var_dump($a - 2); + var_dump($a << 2); + var_dump($a >> 2); + var_dump($a & 2); + var_dump($a | 2); + $c = 5; + $c += $a; + + $c = 5; + $c -= $a; + + $c = 5; + $c *= $a; + + $c = 5; + $c **= $a; + + $c = 5; + $c /= $a; + + $c = 5; + $c %= $a; + + $c = 5; + $c &= $a; + + $c = 5; + $c |= $a; + + $c = 5; + $c ^= $a; + + $c = 5; + $c <<= $a; + + $c = 5; + $c >>= $a; +} + +function implicitMixed($a): void +{ + var_dump($a . 'a'); + $b = 'a'; + $b .= $a; + $bool = rand() > 0; + var_dump($a ** 2); + var_dump($a * 2); + var_dump($a / 2); + var_dump($a % 2); + var_dump($a + 2); + var_dump($a - 2); + var_dump($a << 2); + var_dump($a >> 2); + var_dump($a & 2); + var_dump($a | 2); + $c = 5; + $c += $a; + + $c = 5; + $c -= $a; + + $c = 5; + $c *= $a; + + $c = 5; + $c **= $a; + + $c = 5; + $c /= $a; + + $c = 5; + $c %= $a; + + $c = 5; + $c &= $a; + + $c = 5; + $c |= $a; + + $c = 5; + $c ^= $a; + + $c = 5; + $c <<= $a; + + $c = 5; + $c >>= $a; +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary.php b/tests/PHPStan/Rules/Operators/data/invalid-binary.php index 3069c82ce8..60d71d4ba1 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-binary.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary.php @@ -254,3 +254,19 @@ function benevolentPlus(array $a, int $i): void { echo $k + $i; } }; + +function (int $int) { + $int + []; +}; + +function testMod(array $a, object $o): void { + echo 4 % 3; + echo '4' % 3; + echo 4 % '3'; + + echo $a % 3; + echo 3 % $a; + + echo $o % 3; + echo 3 % $o; +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-comparison.php b/tests/PHPStan/Rules/Operators/data/invalid-comparison.php index 5c2ac5b346..57f42e6560 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-comparison.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-comparison.php @@ -106,3 +106,15 @@ function (array $a, array $b) { $a == $b; $a < $b; }; + +$xml = new SimpleXMLElement('1'); +$xml->a->b == 1; +$xml->a->b > 1; + +function (): void { + [1] > PHP_INT_MAX; +}; + +function (\DateTimeImmutable $d, \DateTimeImmutable $e): void { + $d->format('U') < $e; +}; diff --git a/tests/PHPStan/Rules/Operators/data/invalid-inc-dec-mixed.php b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec-mixed.php new file mode 100644 index 0000000000..4e190cc73c --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec-mixed.php @@ -0,0 +1,43 @@ + $benevolentUnion + * @param string|int|float|bool|null $okUnion + * @param scalar|null|array|object $union + * @param __benevolent $badBenevolentUnion + */ +function foo($benevolentUnion, $okUnion, $union, $badBenevolentUnion): void +{ + $a = $benevolentUnion; + $a++; + $a = $benevolentUnion; + --$a; + + $a = $okUnion; + $a++; + $a = $okUnion; + --$a; + + $a = $union; + $a++; + $a = $union; + --$a; + + $a = $badBenevolentUnion; + $a++; + $a = $badBenevolentUnion; + --$a; +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php index 9e551e875f..aee9fba6fa 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php @@ -2,7 +2,7 @@ namespace InvalidIncDec; -function ($a, int $i, ?float $j, string $str, \stdClass $std) { +function ($a, int $i, ?float $j, string $str, \stdClass $std, \SimpleXMLElement $simpleXMLElement) { $a++; $b = [1]; @@ -15,4 +15,41 @@ function ($a, int $i, ?float $j, string $str, \stdClass $std) { $j++; $str++; $std++; + $classWithToString = new ClassWithToString(); + $classWithToString++; + $classWithToString = new ClassWithToString(); + --$classWithToString; + $arr = []; + $arr++; + $arr = []; + --$arr; + + if (($f = fopen('php://stdin', 'r')) !== false) { + $f++; + } + + if (($f = fopen('php://stdin', 'r')) !== false) { + --$f; + } + + $bool = true; + $bool++; + $bool = false; + --$bool; + $null = null; + $null++; + $null = null; + --$null; + $a = $simpleXMLElement; + $a++; + $a = $simpleXMLElement; + --$a; }; + +class ClassWithToString +{ + public function __toString(): string + { + return 'foo'; + } +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php b/tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php new file mode 100644 index 0000000000..a82faa213a --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php @@ -0,0 +1,28 @@ + $benevolentUnion + * @param numeric-string|int|float $okUnion + * @param scalar|null|array|object $union + * @param __benevolent $badBenevolentUnion + */ +function foo($benevolentUnion, $okUnion, $union, $badBenevolentUnion): void +{ + +$benevolentUnion; + -$benevolentUnion; + ~$benevolentUnion; + + +$okUnion; + -$okUnion; + ~$okUnion; + + +$union; + -$union; + ~$union; + + +$badBenevolentUnion; + -$badBenevolentUnion; + ~$badBenevolentUnion; +} diff --git a/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php b/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php new file mode 100644 index 0000000000..2bc6ddba35 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php @@ -0,0 +1,85 @@ + + */ +class FunctionAssertRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + $reflectionProvider = $this->createReflectionProvider(); + return new FunctionAssertRule(new AssertRuleHelper( + $initializerExprTypeResolver, + $reflectionProvider, + new UnresolvableTypeHelper(), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new MissingTypehintCheck(true, []), + new GenericObjectTypeCheck(), + true, + true, + )); + } + + public function testRule(): void + { + require_once __DIR__ . '/data/function-assert.php'; + $this->analyse([__DIR__ . '/data/function-assert.php'], [ + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 8, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 17, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 26, + ], + [ + 'Asserted type int|string for $i with type int does not narrow down the type.', + 42, + ], + [ + 'Asserted type string for $i with type int can never happen.', + 49, + ], + [ + 'Assert references unknown parameter $j.', + 56, + ], + [ + 'Asserted negated type int for $i with type int can never happen.', + 63, + ], + [ + 'Asserted negated type string for $i with type int does not narrow down the type.', + 70, + ], + [ + 'PHPDoc tag @phpstan-assert for $array has no value type specified in iterable type list.', + 88, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php new file mode 100644 index 0000000000..0d4b491158 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php @@ -0,0 +1,71 @@ + + */ +class FunctionConditionalReturnTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FunctionConditionalReturnTypeRule(new ConditionalReturnTypeRuleHelper()); + } + + public function testRule(): void + { + require_once __DIR__ . '/data/function-conditional-return-type.php'; + $this->analyse([__DIR__ . '/data/function-conditional-return-type.php'], [ + [ + 'Conditional return type uses subject type stdClass which is not part of PHPDoc @template tags.', + 37, + ], + [ + 'Conditional return type references unknown parameter $j.', + 45, + ], + [ + 'Condition "int is int" in conditional return type is always true.', + 53, + ], + [ + 'Condition "T of int is int" in conditional return type is always true.', + 63, + ], + [ + 'Condition "T of int is int" in conditional return type is always true.', + 73, + ], + [ + 'Condition "int is not int" in conditional return type is always false.', + 81, + ], + [ + 'Condition "int is string" in conditional return type is always false.', + 89, + ], + [ + 'Condition "T of int is string" in conditional return type is always false.', + 99, + ], + [ + 'Condition "T of int is string" in conditional return type is always false.', + 109, + ], + [ + 'Condition "int is not string" in conditional return type is always true.', + 117, + ], + ]); + } + + public function testBug8609(): void + { + $this->analyse([__DIR__ . '/data/bug-8609-function.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php index 71e78e7ef6..a66be60a2d 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -25,20 +26,26 @@ public function testRule(): void 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.', + 12, ], + ]); + } + + public function testNativeType(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/incompatible-class-constant-phpdoc-native-type.php'], [ [ - '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 IncompatibleClassConstantPhpDocNativeType\Foo::BAZ with type string is incompatible with native type int.', + 14, ], [ - 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Bar::BAZ with type string is incompatible with value 2.', - 35, + 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDocNativeType\Foo::LOREM with type int|string is not subtype of native type int.', + 17, ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRuleTest.php new file mode 100644 index 0000000000..d19443e644 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleParamImmediatelyInvokedCallableRuleTest.php @@ -0,0 +1,60 @@ + + */ +class IncompatibleParamImmediatelyInvokedCallableRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new IncompatibleParamImmediatelyInvokedCallableRule( + self::getContainer()->getByType(FileTypeMapper::class), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-param-immediately-invoked-callable.php'], [ + [ + 'PHPDoc tag @param-immediately-invoked-callable references unknown parameter: $b', + 21, + ], + [ + 'PHPDoc tag @param-later-invoked-callable references unknown parameter: $c', + 21, + ], + [ + 'PHPDoc tag @param-immediately-invoked-callable is for parameter $b with non-callable type int.', + 30, + ], + [ + 'PHPDoc tag @param-later-invoked-callable is for parameter $b with non-callable type int.', + 39, + ], + [ + 'PHPDoc tag @param-immediately-invoked-callable references unknown parameter: $b', + 59, + ], + [ + 'PHPDoc tag @param-later-invoked-callable references unknown parameter: $c', + 59, + ], + [ + 'PHPDoc tag @param-immediately-invoked-callable is for parameter $b with non-callable type int.', + 68, + ], + [ + 'PHPDoc tag @param-later-invoked-callable is for parameter $b with non-callable type int.', + 77, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index d7cf8d5470..b5ed921568 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -2,7 +2,11 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\Generics\TemplateTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -16,10 +20,29 @@ class IncompatiblePhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new IncompatiblePhpDocTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new GenericObjectTypeCheck(), - new UnresolvableTypeHelper(), + new IncompatiblePhpDocTypeCheck( + new GenericObjectTypeCheck(), + new UnresolvableTypeHelper(), + new GenericCallableRuleHelper( + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), + ), ); } @@ -154,6 +177,32 @@ public function testRule(): void 283, 'Write @template TFoo of int to fix this.', ], + [ + 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @param for parameter $foo is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', + 301, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @return is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', + 301, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @param for parameter $foo is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', + 319, + ], + [ + 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @return is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', + 319, + ], + [ + 'PHPDoc tag @param for parameter $cb contains unresolvable type.', + 328, + ], + [ + 'PHPDoc tag @param for parameter $cl contains unresolvable type.', + 328, + ], ]); } @@ -169,18 +218,11 @@ public function testBug3753(): void 'PHPDoc tag @param for parameter $foo contains unresolvable type.', 20, ], - [ - 'PHPDoc tag @param for parameter $bars contains unresolvable type.', - 28, - ], ]); } public function testTemplateTypeNativeTypeObject(): void { - 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.', @@ -204,4 +246,244 @@ public function testEnums(): void ]); } + public function testValueOfEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/value-of-enum.php'], [ + [ + 'PHPDoc tag @param for parameter $shouldError with type string is incompatible with native type int.', + 29, + ], + [ + 'PHPDoc tag @param for parameter $shouldError with type int is incompatible with native type string.', + 36, + ], + ]); + } + + public function testConditionalReturnType(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('This test needs PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/incompatible-conditional-return-type.php'], [ + [ + 'PHPDoc tag @return with type ($p is int ? int : string) is not subtype of native type int.', + 25, + ], + ]); + } + + public function testParamOut(): void + { + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'PHPDoc tag @param-out references unknown parameter: $z', + 23, + ], + [ + 'Parameter $i for PHPDoc tag @param-out is not passed by reference.', + 37, + ], + [ + 'PHPDoc tag @param-out for parameter $i contains generic type Exception but class Exception is not generic.', + 51, + ], + [ + 'Generic type ParamOutPhpDocRule\FooBar in PHPDoc tag @param-out for parameter $i does not specify all template types of class ParamOutPhpDocRule\FooBar: T, TT', + 58, + ], + [ + 'Type mixed in generic type ParamOutPhpDocRule\FooBar in PHPDoc tag @param-out for parameter $i is not subtype of template type T of int of class ParamOutPhpDocRule\FooBar.', + 58, + ], + [ + 'Generic type ParamOutPhpDocRule\FooBar in PHPDoc tag @param-out for parameter $i does not specify all template types of class ParamOutPhpDocRule\FooBar: T, TT', + 65, + ], + + ]); + } + + public function testBug10097(): void + { + $this->analyse([__DIR__ . '/data/bug-10097.php'], []); + } + + public function testGenericCallables(): void + { + $this->analyse([__DIR__ . '/data/generic-callables-incompatible.php'], [ + [ + 'PHPDoc tag @param for parameter $existingClass template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 11, + ], + [ + 'PHPDoc tag @param for parameter $existingTypeAlias template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 18, + ], + [ + 'PHPDoc tag @param for parameter $invalidBoundType template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 25, + ], + [ + 'PHPDoc tag @param for parameter $notSupported template T of Closure(T): T with bound type null is not supported.', + 32, + ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\testShadowFunction.', + 40, + ], + [ + 'PHPDoc tag @param-out for parameter $existingClass template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 47, + ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for method GenericCallablesIncompatible\Test::testShadowMethod.', + 60, + ], + [ + 'PHPDoc tag @param for parameter $shadows template U of Closure(T): T shadows @template U for class GenericCallablesIncompatible\Test.', + 60, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T shadows @template T for method GenericCallablesIncompatible\Test::testShadowMethodReturn.', + 68, + ], + [ + 'PHPDoc tag @return template U of Closure(T): T shadows @template U for class GenericCallablesIncompatible\Test.', + 68, + ], + [ + 'PHPDoc tag @return template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 76, + ], + [ + 'PHPDoc tag @return template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 83, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 90, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T with bound type null is not supported.', + 97, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\testShadowFunctionReturn.', + 105, + ], + [ + 'PHPDoc tag @param for parameter $existingClass template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 117, + ], + [ + 'PHPDoc tag @param for parameter $existingTypeAlias template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 124, + ], + [ + 'PHPDoc tag @param for parameter $invalidBoundType template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 131, + ], + [ + 'PHPDoc tag @param for parameter $notSupported template T of Closure(T): T with bound type null is not supported.', + 138, + ], + [ + 'PHPDoc tag @return template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 145, + ], + [ + 'PHPDoc tag @return template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 152, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 159, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T with bound type null is not supported.', + 166, + ], + [ + 'PHPDoc tag @param-out for parameter $existingClass template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsParamOut.', + 175, + ], + [ + 'PHPDoc tag @param-out for parameter $existingClasses template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsParamOutArray.', + 183, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsReturnArray.', + 191, + ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for class GenericCallablesIncompatible\Test3.', + 203, + ], + ]); + } + + public function testBug10622(): void + { + $this->analyse([__DIR__ . '/data/bug-10622.php'], []); + } + + public function testBug10622B(): void + { + $this->analyse([__DIR__ . '/data/bug-10622b.php'], []); + } + + public function testParamClosureThis(): void + { + $this->analyse([__DIR__ . '/data/param-closure-this.php'], [ + [ + 'PHPDoc tag @param-closure-this references unknown parameter: $b', + 20, + ], + [ + 'PHPDoc tag @param-closure-this for parameter $i contains unresolvable type.', + 27, + ], + [ + 'PHPDoc tag @param-closure-this for parameter $i contains unresolvable type.', + 34, + ], + [ + 'PHPDoc tag @param-closure-this is for parameter $i with non-Closure type string.', + 41, + ], + [ + 'PHPDoc tag @param-closure-this for parameter $i contains generic type Exception but class Exception is not generic.', + 48, + ], + [ + 'Generic type ParamClosureThisPhpDocRule\FooBar in PHPDoc tag @param-closure-this for parameter $i does not specify all template types of class ParamClosureThisPhpDocRule\FooBar: T, TT', + 55, + ], + [ + 'Type mixed in generic type ParamClosureThisPhpDocRule\FooBar in PHPDoc tag @param-closure-this for parameter $i is not subtype of template type T of int of class ParamClosureThisPhpDocRule\FooBar.', + 55, + ], + [ + 'Generic type ParamClosureThisPhpDocRule\FooBar in PHPDoc tag @param-closure-this for parameter $i does not specify all template types of class ParamClosureThisPhpDocRule\FooBar: T, TT', + 62, + ], + ]); + } + + public function testGenericStatic(): void + { + $this->analyse([__DIR__ . '/data/incompatible-phpdoc-generic-static.php'], [ + [ + 'Generic type static(IncompatiblePhpDocGenericStatic\Foo) in PHPDoc tag @return specifies 2 template types, but class IncompatiblePhpDocGenericStatic\Foo supports only 1: T', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRuleTest.php new file mode 100644 index 0000000000..e10eff3ff1 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyHookPhpDocTypeRuleTest.php @@ -0,0 +1,91 @@ + + */ +class IncompatiblePropertyHookPhpDocTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver([], $reflectionProvider); + + return new IncompatiblePropertyHookPhpDocTypeRule( + self::getContainer()->getByType(FileTypeMapper::class), + new IncompatiblePhpDocTypeCheck( + new GenericObjectTypeCheck(), + new UnresolvableTypeHelper(), + new GenericCallableRuleHelper( + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), + ), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/incompatible-property-hook-phpdoc-types.php'], [ + [ + 'PHPDoc tag @return with type string is incompatible with native type int.', + 10, + ], + [ + 'PHPDoc tag @return with type string is incompatible with native type void.', + 17, + ], + [ + 'PHPDoc tag @param for parameter $value with type string is incompatible with native type int.', + 27, + ], + [ + 'Parameter $value for PHPDoc tag @param-out is not passed by reference.', + 27, + ], + [ + 'PHPDoc tag @param for parameter $value contains unresolvable type.', + 34, + ], + [ + 'PHPDoc tag @param for parameter $value contains generic type Exception but class Exception is not generic.', + 41, + ], + [ + 'PHPDoc tag @param for parameter $value template T of callable(T): T shadows @template T for class IncompatiblePropertyHookPhpDocTypes\GenericFoo.', + 54, + ], + [ + 'PHPDoc tag @param for parameter $value template of callable<\stdClass of mixed>(T): T cannot have existing class \stdClass as its name.', + 61, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php index 80cd2c3ed9..b0ed51d449 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\Generics\TemplateTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,7 +18,27 @@ class IncompatiblePropertyPhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new IncompatiblePropertyPhpDocTypeRule(new GenericObjectTypeCheck(), new UnresolvableTypeHelper()); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + + return new IncompatiblePropertyPhpDocTypeRule( + new GenericObjectTypeCheck(), + new UnresolvableTypeHelper(), + new GenericCallableRuleHelper( + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), + ); } public function testRule(): void @@ -57,14 +80,20 @@ public function testRule(): void 'PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$unknownClassConstant2 contains unresolvable type.', 45, ], + [ + 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$genericRedundantTypeProjection is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', + 51, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$genericIncompatibleTypeProjection is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', + 57, + ], ]); } public function testNativeTypes(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/incompatible-property-native-types.php'], [ [ 'PHPDoc tag @var for property IncompatiblePhpDocPropertyNativeType\Foo::$selfTwo with type object is not subtype of native type IncompatiblePhpDocPropertyNativeType\Foo.', @@ -88,10 +117,6 @@ public function testNativeTypes(): void public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/incompatible-property-promoted.php'], [ [ 'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$bar contains unresolvable type.', @@ -129,16 +154,47 @@ public function testPromotedProperties(): void 'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$unknownClassConstant2 contains unresolvable type.', 49, ], + [ + 'PHPDoc type for property InvalidPhpDocPromotedProperties\BazWithProperty::$bar with type string is incompatible with native type int.', + 61, + ], ]); } public function testBug4227(): void { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/bug-4227.php'], []); } + public function testBug7240(): void + { + $this->analyse([__DIR__ . '/data/bug-7240.php'], []); + } + + public function testGenericCallables(): void + { + $this->analyse([__DIR__ . '/data/generic-callable-properties.php'], [ + [ + 'PHPDoc tag @var template T of Closure(T): T shadows @template T for class GenericCallableProperties\Test.', + 16, + ], + [ + 'PHPDoc tag @var template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 21, + ], + [ + 'PHPDoc tag @var template of callable(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 26, + ], + [ + 'PHPDoc tag @var template TNull of callable(TNull): TNull with bound type null is not supported.', + 31, + ], + [ + 'PHPDoc tag @var template TInvalid of callable(TInvalid): TInvalid has invalid bound type GenericCallableProperties\Invalid.', + 36, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php new file mode 100644 index 0000000000..2ab4973cef --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php @@ -0,0 +1,62 @@ + + */ +class IncompatibleSelfOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleSelfOutTypeRule(new UnresolvableTypeHelper(), new GenericObjectTypeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-self-out-type.php'], [ + [ + 'Self-out type int of method IncompatibleSelfOutType\A::three is not subtype of IncompatibleSelfOutType\A.', + 23, + ], + [ + 'Self-out type IncompatibleSelfOutType\A|null of method IncompatibleSelfOutType\A::four is not subtype of IncompatibleSelfOutType\A.', + 28, + ], + [ + 'PHPDoc tag @phpstan-self-out is not supported above static method IncompatibleSelfOutType\Foo::selfOutStatic().', + 38, + ], + [ + 'PHPDoc tag @phpstan-self-out for method IncompatibleSelfOutType\Foo::doFoo() contains unresolvable type.', + 46, + ], + [ + 'PHPDoc tag @phpstan-self-out for method IncompatibleSelfOutType\Foo::doBar() contains unresolvable type.', + 54, + ], + [ + 'PHPDoc tag @phpstan-self-out contains generic type IncompatibleSelfOutType\GenericCheck but class IncompatibleSelfOutType\GenericCheck is not generic.', + 67, + ], + [ + 'Generic type IncompatibleSelfOutType\GenericCheck2 in PHPDoc tag @phpstan-self-out does not specify all template types of class IncompatibleSelfOutType\GenericCheck2: T, U', + 84, + ], + [ + 'Generic type IncompatibleSelfOutType\GenericCheck2, string> in PHPDoc tag @phpstan-self-out specifies 3 template types, but class IncompatibleSelfOutType\GenericCheck2 supports only 2: T, U', + 92, + ], + [ + 'Type string in generic type IncompatibleSelfOutType\GenericCheck2 in PHPDoc tag @phpstan-self-out is not subtype of template type U of int of class IncompatibleSelfOutType\GenericCheck2.', + 100, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php index fda7477219..e91e647054 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -26,11 +27,42 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/invalid-phpstan-doc.php'], [ [ 'Unknown PHPDoc tag: @phpstan-extens', - 7, + 6, ], [ 'Unknown PHPDoc tag: @phpstan-pararm', - 14, + 11, + ], + [ + 'Unknown PHPDoc tag: @phpstan-varr', + 43, + ], + [ + 'Unknown PHPDoc tag: @phpstan-varr', + 46, + ], + [ + 'Unknown PHPDoc tag: @phpstan-varr', + 56, + ], + ]); + } + + public function testBug8697(): void + { + $this->analyse([__DIR__ . '/data/bug-8697.php'], []); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/invalid-phpstan-tag-property-hooks.php'], [ + [ + 'Unknown PHPDoc tag: @phpstan-what', + 9, ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php index f3d5726d44..be63bff8e2 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -25,72 +26,76 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/invalid-phpdoc.php'], [ [ - 'PHPDoc tag @param has invalid value (): Unexpected token "\n * ", expected type at offset 13', - 25, + 'PHPDoc tag @param has invalid value (): Unexpected token "\n * ", expected type at offset 13 on line 2', + 6, ], [ - 'PHPDoc tag @param has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 24', - 25, + 'PHPDoc tag @param has invalid value (A & B | C $paramNameA): Unexpected token "|", expected variable at offset 72 on line 5', + 9, ], [ - 'PHPDoc tag @param has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 43', - 25, + 'PHPDoc tag @param has invalid value ((A & B $paramNameB): Unexpected token "$paramNameB", expected \')\' at offset 105 on line 6', + 10, ], [ - 'PHPDoc tag @param has invalid value (A & B | C $paramNameA): Unexpected token "|", expected variable at offset 72', - 25, + 'PHPDoc tag @param has invalid value (~A & B $paramNameC): Unexpected token "~A", expected type at offset 127 on line 7', + 11, ], [ - 'PHPDoc tag @param has invalid value ((A & B $paramNameB): Unexpected token "$paramNameB", expected \')\' at offset 105', - 25, + 'PHPDoc tag @var has invalid value (): Unexpected token "\n * ", expected type at offset 156 on line 9', + 13, ], [ - 'PHPDoc tag @param has invalid value (~A & B $paramNameC): Unexpected token "~A", expected type at offset 127', - 25, + 'PHPDoc tag @var has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 165 on line 10', + 14, ], [ - 'PHPDoc tag @var has invalid value (): Unexpected token "\n * ", expected type at offset 156', - 25, + 'PHPDoc tag @var has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 182 on line 11', + 15, ], [ - 'PHPDoc tag @var has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 165', - 25, + 'PHPDoc tag @return has invalid value (): Unexpected token "\n * ", expected type at offset 208 on line 13', + 17, ], [ - 'PHPDoc tag @var has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 182', - 25, + 'PHPDoc tag @return has invalid value ([int, string]): Unexpected token "[", expected type at offset 220 on line 14', + 18, ], [ - 'PHPDoc tag @return has invalid value (): Unexpected token "\n * ", expected type at offset 208', - 25, + 'PHPDoc tag @return has invalid value (A & B | C): Unexpected token "|", expected TOKEN_OTHER at offset 251 on line 15', + 19, ], [ - 'PHPDoc tag @return has invalid value ([int, string]): Unexpected token "[", expected type at offset 220', - 25, + 'PHPDoc tag @var has invalid value (\\\Foo|\Bar $test): Unexpected token "\\\\\\\Foo|\\\Bar", expected type at offset 9 on line 1', + 28, ], [ - 'PHPDoc tag @return has invalid value (A & B | C): Unexpected token "|", expected TOKEN_OTHER at offset 251', - 25, + 'PHPDoc tag @var has invalid value (callable(int)): Unexpected token "(", expected TOKEN_HORIZONTAL_WS at offset 17 on line 1', + 58, ], [ - 'PHPDoc tag @var has invalid value (\\\Foo|\Bar $test): Unexpected token "\\\\\\\Foo|\\\Bar", expected type at offset 9', - 29, + 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18 on line 1', + 61, ], - /*[ - 'PHPDoc tag @var has invalid value ...', - 59, - ],*/ [ - 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', - 62, + 'PHPDoc tag @throws has invalid value ((\Exception): Unexpected token "*/", expected \')\' at offset 24 on line 1', + 71, ], [ - '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 on line 1', + 80, ], [ - 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', - 81, + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 88, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 91, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 101, ], ]); } @@ -105,4 +110,53 @@ public function testBug4731WithoutFirstTag(): void $this->analyse([__DIR__ . '/data/bug-4731-no-first-tag.php'], []); } + public function testInvalidTypeInTypeAlias(): void + { + $this->analyse([__DIR__ . '/data/invalid-type-type-alias.php'], [ + [ + 'PHPDoc tag @phpstan-type InvalidFoo has invalid value: Unexpected token "{", expected TOKEN_PHPDOC_EOL at offset 65 on line 3', + 7, + ], + ]); + } + + public function testIgnoreWithinPhpDoc(): void + { + $this->analyse([__DIR__ . '/data/ignore-line-within-phpdoc.php'], []); + } + + public function testBug6299(): void + { + $this->analyse([__DIR__ . '/data/bug-6299.php'], [ + [ + "PHPDoc tag @phpstan-return has invalid value (array{'numeric': stdClass[], 'branches': array{'names': string[], 'exclude': bool}}}|int): Unexpected token \"}\", expected TOKEN_HORIZONTAL_WS at offset 107 on line 2", + 10, + ], + ]); + } + + public function testBug6692(): void + { + $this->analyse([__DIR__ . '/data/bug-6692.php'], [ + [ + 'PHPDoc tag @return has invalid value ($this): Unexpected token "<", expected TOKEN_HORIZONTAL_WS at offset 21 on line 2', + 11, + ], + ]); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/invalid-phpdoc-property-hooks.php'], [ + [ + 'PHPDoc tag @return has invalid value (Test(): Unexpected token "(", expected TOKEN_HORIZONTAL_WS at offset 16 on line 1', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php index f4442b4005..ec26814c8f 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; @@ -18,16 +20,22 @@ class InvalidPhpDocVarTagTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new InvalidPhpDocVarTagTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), new GenericObjectTypeCheck(), - new MissingTypehintCheck($broker, true, true, true, []), + new MissingTypehintCheck(true, []), new UnresolvableTypeHelper(), true, true, + true, ); } @@ -86,16 +94,28 @@ public function testRule(): void [ 'PHPDoc tag @var for variable $test has no value type specified in iterable type array.', 58, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'PHPDoc tag @var for variable $test contains generic class InvalidPhpDocDefinitions\FooGeneric but does not specify its types: T, U', 61, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ - 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for variable $test is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', 67, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for variable $test is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', + 73, + ], + [ + 'PHPDoc tag @var for variable $test contains generic class InvalidPhpDocDefinitions\FooGenericWithSomeDefaults but does not specify its types: T, U (1-2 required)', + 79, + ], + [ + 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 85, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); @@ -148,10 +168,18 @@ public function testBug6252(): void 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'], []); } + public function testBug9055(): void + { + $this->analyse([__DIR__ . '/data/bug-9055.php'], [ + [ + 'PHPDoc tag @var for variable $x contains unknown class Bug9055\uncheckedNotExisting.', + 16, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php index 90120196da..e378f873b6 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php @@ -9,6 +9,7 @@ use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\VerbosityLevel; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -77,6 +78,28 @@ public function testInheritedPhpDocs(): void ]); } + public function testThrowsWithRequireExtends(): void + { + $this->analyse([__DIR__ . '/data/throws-with-require.php'], [ + [ + 'PHPDoc tag @throws with type ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable', + 25, + ], + [ + 'PHPDoc tag @throws with type DateTimeInterface|ThrowsWithRequire\\RequiresExtendsExceptionInterface is not subtype of Throwable', + 39, + ], + [ + 'PHPDoc tag @throws with type Exception|ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable', + 46, + ], + [ + 'PHPDoc tag @throws with type Iterator&ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable', + 74, + ], + ]); + } + public function dataMergeInheritedPhpDocs(): array { return [ @@ -115,4 +138,18 @@ public function testMergeInheritedPhpDocs( $this->assertSame($expectedType, $throwsType->describe(VerbosityLevel::precise())); } + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/invalid-throws-property-hook.php'], [ + [ + 'PHPDoc tag @throws with type DateTimeImmutable is not subtype of Throwable', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php new file mode 100644 index 0000000000..99005c23bf --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php @@ -0,0 +1,156 @@ + + */ +class MethodAssertRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + $reflectionProvider = $this->createReflectionProvider(); + return new MethodAssertRule(new AssertRuleHelper( + $initializerExprTypeResolver, + $reflectionProvider, + new UnresolvableTypeHelper(), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new MissingTypehintCheck(true, []), + new GenericObjectTypeCheck(), + true, + true, + )); + } + + public function testRule(): void + { + require_once __DIR__ . '/data/method-assert.php'; + $this->analyse([__DIR__ . '/data/method-assert.php'], [ + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 10, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 19, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 28, + ], + [ + 'Asserted type int|string for $i with type int does not narrow down the type.', + 44, + ], + [ + 'Asserted type string for $i with type int can never happen.', + 51, + ], + [ + 'Assert references unknown parameter $j.', + 58, + ], + [ + 'Asserted negated type int for $i with type int can never happen.', + 65, + ], + [ + 'Asserted negated type string for $i with type int does not narrow down the type.', + 72, + ], + [ + 'PHPDoc tag @phpstan-assert for $this->fooProp contains unresolvable type.', + 94, + ], + [ + 'PHPDoc tag @phpstan-assert-if-true for $a contains unresolvable type.', + 94, + ], + [ + 'PHPDoc tag @phpstan-assert for $a contains unknown class MethodAssert\Nonexistent.', + 105, + ], + [ + 'PHPDoc tag @phpstan-assert for $b contains invalid type MethodAssert\FooTrait.', + 105, + ], + [ + 'Class MethodAssert\Foo referenced with incorrect case: MethodAssert\fOO.', + 105, + ], + [ + 'Assert references unknown $this->barProp.', + 105, + ], + [ + 'Assert references unknown parameter $this.', + 113, + ], + [ + 'PHPDoc tag @phpstan-assert for $m contains generic type Exception but class Exception is not generic.', + 131, + ], + [ + 'Generic type MethodAssert\FooBar in PHPDoc tag @phpstan-assert for $m does not specify all template types of class MethodAssert\FooBar: T, TT', + 138, + ], + [ + 'Type mixed in generic type MethodAssert\FooBar in PHPDoc tag @phpstan-assert for $m is not subtype of template type T of int of class MethodAssert\FooBar.', + 138, + ], + [ + 'Generic type MethodAssert\FooBar in PHPDoc tag @phpstan-assert for $m does not specify all template types of class MethodAssert\FooBar: T, TT', + 145, + ], + [ + 'Generic type MethodAssert\FooBar in PHPDoc tag @phpstan-assert for $m specifies 3 template types, but class MethodAssert\FooBar supports only 2: T, TT', + 152, + ], + [ + 'PHPDoc tag @phpstan-assert for $m has no value type specified in iterable type array.', + 194, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'PHPDoc tag @phpstan-assert for $m contains generic class MethodAssert\FooBar but does not specify its types: T, TT', + 202, + ], + [ + 'PHPDoc tag @phpstan-assert for $m has no signature specified for callable.', + 210, + ], + ]); + } + + public function testBug10573(): void + { + $this->analyse([__DIR__ . '/data/bug-10573.php'], []); + } + + public function testBug10214(): void + { + $this->analyse([__DIR__ . '/data/bug-10214.php'], []); + } + + public function testBug10594(): void + { + $this->analyse([__DIR__ . '/data/bug-10594.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php new file mode 100644 index 0000000000..ff465ea7e5 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php @@ -0,0 +1,108 @@ + + */ +class MethodConditionalReturnTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MethodConditionalReturnTypeRule(new ConditionalReturnTypeRuleHelper()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-conditional-return-type.php'], [ + [ + 'Conditional return type uses subject type stdClass which is not part of PHPDoc @template tags.', + 48, + ], + [ + 'Conditional return type references unknown parameter $j.', + 65, + ], + [ + 'Condition "int is int" in conditional return type is always true.', + 73, + ], + [ + 'Condition "T of int is int" in conditional return type is always true.', + 83, + ], + [ + 'Condition "T of int is int" in conditional return type is always true.', + 93, + ], + [ + 'Condition "int is not int" in conditional return type is always false.', + 101, + ], + [ + 'Condition "int is string" in conditional return type is always false.', + 114, + ], + [ + 'Condition "T of int is string" in conditional return type is always false.', + 124, + ], + [ + 'Condition "T of int is string" in conditional return type is always false.', + 134, + ], + [ + 'Condition "int is not string" in conditional return type is always true.', + 142, + ], + [ + 'Condition "array{foo: string} is array{foo: int}" in conditional return type is always false.', + 156, + ], + [ + 'Condition "int is int" in conditional return type is always true.', + 185, + ], + ]); + } + + public function testBug8284(): void + { + $this->analyse([__DIR__ . '/data/bug-8284.php'], [ + [ + 'Conditional return type references unknown parameter $callable.', + 14, + ], + ]); + } + + public function testBug8609(): void + { + $this->analyse([__DIR__ . '/data/bug-8609.php'], []); + } + + public function testBug8408(): void + { + $this->analyse([__DIR__ . '/data/bug-8408.php'], []); + } + + public function testBug7310(): void + { + $this->analyse([__DIR__ . '/data/bug-7310.php'], []); + } + + public function testBug11939(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11939.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php new file mode 100644 index 0000000000..7893423227 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php @@ -0,0 +1,87 @@ + + */ +class RequireExtendsDefinitionClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new RequireExtendsDefinitionClassRule( + new RequireExtendsCheck( + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/incompatible-require-extends.php'], [ + [ + 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeTrait.', + 8, + ], + [ + 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeInterface.', + 13, + ], + [ + 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeEnum.', + 18, + ], + [ + 'PHPDoc tag @phpstan-require-extends contains unknown class IncompatibleRequireExtends\TypeDoesNotExist.', + 23, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @phpstan-require-extends contains non-object type int.', + 34, + ], + [ + 'PHPDoc tag @phpstan-require-extends is only valid on trait or interface.', + 39, + ], + [ + 'PHPDoc tag @phpstan-require-extends is only valid on trait or interface.', + 44, + ], + [ + 'PHPDoc tag @phpstan-require-extends cannot contain final class IncompatibleRequireExtends\SomeFinalClass.', + 121, + ], + [ + 'PHPDoc tag @phpstan-require-extends contains non-object type IncompatibleRequireExtends\UnresolvableExtendsInterface&stdClass.', + 135, + ], + [ + 'PHPDoc tag @phpstan-require-extends can only be used once.', + 178, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php new file mode 100644 index 0000000000..93e516021a --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php @@ -0,0 +1,59 @@ + + */ +class RequireExtendsDefinitionTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new RequireExtendsDefinitionTraitRule( + $reflectionProvider, + new RequireExtendsCheck( + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/incompatible-require-extends.php'], [ + [ + 'PHPDoc tag @phpstan-require-extends cannot contain final class IncompatibleRequireExtends\SomeFinalClass.', + 126, + ], + [ + 'PHPDoc tag @phpstan-require-extends contains non-object type *NEVER*.', + 140, + ], + [ + 'PHPDoc tag @phpstan-require-extends can only be used once.', + 171, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionClassRuleTest.php new file mode 100644 index 0000000000..fcba96a92e --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionClassRuleTest.php @@ -0,0 +1,38 @@ + + */ +class RequireImplementsDefinitionClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RequireImplementsDefinitionClassRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/incompatible-require-implements.php'], [ + [ + 'PHPDoc tag @phpstan-require-implements is only valid on trait.', + 40, + ], + [ + 'PHPDoc tag @phpstan-require-implements is only valid on trait.', + 45, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php new file mode 100644 index 0000000000..4104983d3f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php @@ -0,0 +1,72 @@ + + */ +class RequireImplementsDefinitionTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new RequireImplementsDefinitionTraitRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $expectedErrors = [ + [ + 'PHPDoc tag @phpstan-require-implements cannot contain non-interface type IncompatibleRequireImplements\SomeTrait.', + 8, + ], + [ + 'PHPDoc tag @phpstan-require-implements cannot contain non-interface type IncompatibleRequireImplements\SomeEnum.', + 13, + ], + [ + 'PHPDoc tag @phpstan-require-implements contains unknown class IncompatibleRequireImplements\TypeDoesNotExist.', + 18, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @phpstan-require-implements cannot contain non-interface type IncompatibleRequireImplements\SomeClass.', + 24, + ], + [ + 'PHPDoc tag @phpstan-require-implements contains non-object type int.', + 29, + ], + [ + 'PHPDoc tag @phpstan-require-implements contains non-object type *NEVER*.', + 34, + ], + ]; + + $this->analyse([__DIR__ . '/data/incompatible-require-implements.php'], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php new file mode 100644 index 0000000000..d9307d0bc7 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php @@ -0,0 +1,103 @@ + + */ +class VarTagChangedExpressionTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper( + self::getContainer()->getByType(TypeNodeResolver::class), + self::getContainer()->getByType(FileTypeMapper::class), + $this->createReflectionProvider(), + true, + true, + )); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/var-tag-changed-expr-type.php'], [ + [ + 'PHPDoc tag @var with type string is not subtype of native type int.', + 17, + ], + [ + 'PHPDoc tag @var with type string is not subtype of type int.', + 37, + ], + [ + 'PHPDoc tag @var with type string is not subtype of native type int.', + 54, + ], + [ + 'PHPDoc tag @var with type string is not subtype of native type int.', + 73, + ], + ]); + } + + public function testAssignOfDifferentVariable(): void + { + $this->analyse([__DIR__ . '/data/wrong-var-native-type.php'], [ + [ + 'PHPDoc tag @var with type string is not subtype of type int.', + 95, + ], + ]); + } + + public function testBug10130(): void + { + $this->analyse([__DIR__ . '/data/bug-10130.php'], [ + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 14, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 17, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array{id: int}.', + 20, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 23, + ], + ]); + } + + public function testBug12708(): void + { + $this->analyse([__DIR__ . '/data/bug-12708.php'], [ + [ + "PHPDoc tag @var with type list is not subtype of native type array{1: 'b', 2: 'c'}.", + 12, + ], + [ + "PHPDoc tag @var with type list is not subtype of native type array{0: 'a', 2: 'c'}.", + 18, + ], + [ + "PHPDoc tag @var with type list is not subtype of native type array{-1: 'z', 0: 'a', 1: 'b', 2: 'c'}.", + 24, + ], + [ + "PHPDoc tag @var with type list is not subtype of native type array{0: 'a', -1: 'z', 1: 'b', 2: 'c'}.", + 30, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index 63164b9bda..ea87452e90 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -13,16 +14,35 @@ class WrongVariableNameInVarTagRuleTest extends RuleTestCase { + private bool $checkTypeAgainstPhpDocType = false; + + private bool $strictWideningCheck = false; + protected function getRule(): Rule { return new WrongVariableNameInVarTagRule( self::getContainer()->getByType(FileTypeMapper::class), + new VarTagTypeRuleHelper( + self::getContainer()->getByType(TypeNodeResolver::class), + self::getContainer()->getByType(FileTypeMapper::class), + $this->createReflectionProvider(), + $this->checkTypeAgainstPhpDocType, + $this->strictWideningCheck, + ), ); } public function testRule(): void { $this->analyse([__DIR__ . '/data/wrong-variable-name-var.php'], [ + [ + 'PHPDoc tag @var with type int is not subtype of native type void.', + 11, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type void.', + 14, + ], [ 'Variable $foo in PHPDoc tag @var does not match assigned variable $test.', 17, @@ -63,65 +83,69 @@ public function testRule(): void 'Variable $foo in PHPDoc tag @var does not exist.', 109, ], + [ + 'PHPDoc tag @var with type int is not subtype of native type void.', + 120, + ], [ 'Multiple PHPDoc @var tags above single variable assignment are not supported.', - 125, + 126, ], [ 'Variable $b in PHPDoc tag @var does not exist.', - 134, + 135, ], [ 'PHPDoc tag @var does not specify variable name.', - 155, + 156, ], [ 'PHPDoc tag @var does not specify variable name.', - 176, + 177, ], [ 'Variable $foo in PHPDoc tag @var does not exist.', - 210, + 211, ], [ 'PHPDoc tag @var above foreach loop does not specify variable name.', - 234, + 235, ], [ 'Variable $foo in PHPDoc tag @var does not exist.', - 248, + 249, ], [ 'Variable $bar in PHPDoc tag @var does not exist.', - 248, + 249, ], [ 'Variable $slots in PHPDoc tag @var does not exist.', - 262, + 263, ], [ 'Variable $slots in PHPDoc tag @var does not exist.', - 268, + 269, ], [ 'PHPDoc tag @var above assignment does not specify variable name.', - 274, + 275, ], [ 'Variable $slots in PHPDoc tag @var does not match assigned variable $itemSlots.', - 280, + 281, ], [ 'PHPDoc tag @var above a class has no effect.', - 300, + 301, ], [ 'PHPDoc tag @var above a method has no effect.', - 304, + 305, ], [ 'PHPDoc tag @var above a function has no effect.', - 312, + 313, ], ]); } @@ -174,6 +198,43 @@ public function testBug4505(): void $this->analyse([__DIR__ . '/data/bug-4505.php'], []); } + public function testBug12458(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = true; + + $this->analyse([__DIR__ . '/data/bug-12458.php'], []); + } + + public function testBug11015(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = true; + + $this->analyse([__DIR__ . '/data/bug-11015.php'], []); + } + + public function testBug10861(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = true; + + $this->analyse([__DIR__ . '/data/bug-10861.php'], []); + } + + public function testBug11535(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = true; + + $this->analyse([__DIR__ . '/data/bug-11535.php'], [ + [ + 'PHPDoc tag @var with type Closure(string): array is not subtype of native type Closure(string): array{1, 2, 3}.', + 6, + ], + ]); + } + public function testEnums(): void { if (PHP_VERSION_ID < 80100) { @@ -188,4 +249,329 @@ public function testEnums(): void ]); } + public function dataReportWrongType(): iterable + { + $nativeCheckOnly = [ + [ + 'PHPDoc tag @var with type string|null is not subtype of native type string.', + 14, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type SplObjectStorage.', + 23, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 26, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of native type array.', + 38, + ], + [ + 'PHPDoc tag @var with type string is not subtype of native type 1.', + 99, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type string.', + 109, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 148, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type PHPStan\Type\Type|null.', + 186, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType|null but it\'s error-prone and dangerous.', + 189, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 192, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\ObjectType|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 195, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\Type|null is not subtype of native type PHPStan\Type\ObjectType|null.', + 201, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\ObjectType|null is not subtype of type PHPStan\Type\Generic\GenericObjectType|null.', + 204, + ], + ]; + + yield [false, false, $nativeCheckOnly]; + yield [false, true, $nativeCheckOnly]; + yield [true, false, [ + [ + 'PHPDoc tag @var with type string|null is not subtype of native type string.', + 14, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type SplObjectStorage.', + 23, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 26, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 29, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 35, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of native type array.', + 38, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of type Iterator.', + 44, + ], + /*[ + // reported by VarTagChangedExpressionTypeRule + 'PHPDoc tag @var with type string is not subtype of type int.', + 95, + ],*/ + [ + 'PHPDoc tag @var with type string is not subtype of native type 1.', + 99, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type string.', + 109, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 137, + ], + [ + 'PHPDoc tag @var with type string is not subtype of type int.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 148, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 160, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 163, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type PHPStan\Type\Type|null.', + 186, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType|null but it\'s error-prone and dangerous.', + 189, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 192, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\ObjectType|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 195, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\Type|null is not subtype of native type PHPStan\Type\ObjectType|null.', + 201, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\ObjectType|null is not subtype of type PHPStan\Type\Generic\GenericObjectType|null.', + 204, + ], + ]]; + yield [true, true, [ + [ + 'PHPDoc tag @var with type string|null is not subtype of native type string.', + 14, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type SplObjectStorage.', + 23, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 26, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 29, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 32, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 35, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of native type array.', + 38, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of type Iterator.', + 44, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 47, + ], + /*[ + // reported by VarTagChangedExpressionTypeRule + 'PHPDoc tag @var with type string is not subtype of type int.', + 95, + ],*/ + [ + 'PHPDoc tag @var with type string is not subtype of native type 1.', + 99, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type string.', + 109, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 122, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 137, + ], + [ + 'PHPDoc tag @var with type string is not subtype of type int.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 148, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 154, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 157, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 160, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 163, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type PHPStan\Type\Type|null.', + 186, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType|null but it\'s error-prone and dangerous.', + 189, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 192, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\ObjectType|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 195, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\Type|null is not subtype of native type PHPStan\Type\ObjectType|null.', + 201, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\ObjectType|null is not subtype of type PHPStan\Type\Generic\GenericObjectType|null.', + 204, + ], + [ + 'PHPDoc tag @var with type array|null is not subtype of type array{id: int}|null.', + 235, + ], + ]]; + } + + /** + * @dataProvider dataPermutateCheckTypeAgainst + */ + public function testEmptyArrayInitWithWiderPhpDoc(bool $checkTypeAgainstPhpDocType): void + { + $this->checkTypeAgainstPhpDocType = $checkTypeAgainstPhpDocType; + $this->analyse([__DIR__ . '/data/var-above-empty-array-widening.php'], [ + [ + 'PHPDoc tag @var with type int is not subtype of native type array{}.', + 24, + ], + ]); + } + + public function dataPermutateCheckTypeAgainst(): iterable + { + yield [true]; + yield [false]; + } + + /** + * @dataProvider dataReportWrongType + * @param list $expectedErrors + */ + public function testReportWrongType( + bool $checkTypeAgainstPhpDocType, + bool $strictWideningCheck, + array $expectedErrors, + ): void + { + $this->checkTypeAgainstPhpDocType = $checkTypeAgainstPhpDocType; + $this->strictWideningCheck = $strictWideningCheck; + $this->analyse([__DIR__ . '/data/wrong-var-native-type.php'], $expectedErrors); + } + + public function testBug12457(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = true; + $this->analyse([__DIR__ . '/data/bug-12457.php'], [ + [ + 'PHPDoc tag @var with type array{numeric-string} is not subtype of type array{lowercase-string&numeric-string&uppercase-string}.', + 13, + ], + [ + 'PHPDoc tag @var with type callable(): string is not subtype of type callable(): numeric-string&lowercase-string&uppercase-string.', + 22, + ], + ]); + } + + public function testNewIsAlwaysFinalClass(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = true; + $this->analyse([__DIR__ . '/data/new-is-always-final-var-tag-type.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10097.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10097.php new file mode 100644 index 0000000000..db239d2cce --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10097.php @@ -0,0 +1,37 @@ + + */ + public function getMessageType(): string; +} + + +/** + * @extends Consumer + */ +interface SomeMessageConsumer extends Consumer +{ +} + +/** + * @return list> + */ +function getConsumers(SomeMessageConsumer $consumerA): array +{ + return [$consumerA]; +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10130.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10130.php new file mode 100644 index 0000000000..7fbc69f30f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10130.php @@ -0,0 +1,26 @@ + $map + * @param list $list + * @param array{id: int} $shape + * @param list $listOfShapes + */ + public function doFoo($map, $list, $shape, $listOfShapes): void + { + /** @var mixed[] $map */ + if ($map) {} + + /** @var mixed[] $list */ + if ($list) {} + + /** @var mixed[] $shape */ + if ($shape) {} + + /** @var mixed[] $listOfShapes */ + if ($listOfShapes) {} + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10214.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10214.php new file mode 100644 index 0000000000..b1292d2ba5 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10214.php @@ -0,0 +1,42 @@ + + */ +class PostVoter extends Voter { + function supports($attribute, $subject): bool + { + return $attribute === 'POST_READ' && $subject instanceof Post; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10594.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10594.php new file mode 100644 index 0000000000..dfc27264bb --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10594.php @@ -0,0 +1,34 @@ +getParameters(); + if (count($parameters) >= 2) { + return $parameters[1]->getType() !== null && ($parameters[1]->getType() instanceof ReflectionNamedType && $parameters[1]->getType()->getName() !== 'string'); + } + return true; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10622.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10622.php new file mode 100644 index 0000000000..22caf7f0a2 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10622.php @@ -0,0 +1,30 @@ + : SupportCollection) + */ + public function map(callable $callback) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10622b.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10622b.php new file mode 100644 index 0000000000..23110ad14c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10622b.php @@ -0,0 +1,71 @@ + + */ +class FooBoxedArray +{ + /** @var T */ + private $value; + + /** + * @param T $value + */ + public function __construct(array $value) + { + $this->value = $value; + } + + /** + * @return T + */ + public function get(): array + { + return $this->value; + } +} + +/** + * @template TKey of object|array + * @template TValue of object|array + */ +class FooMap +{ + /** + * @var array)>, + * \WeakReference<(TValue is object ? TValue : FooBoxedArray)> + * }> + */ + protected $weakKvByIndex = []; + + /** + * @template T of TKey|TValue + * + * @param T $value + * + * @return (T is object ? T : FooBoxedArray) + */ + protected function boxValue($value): object + { + return is_array($value) + ? new FooBoxedArray($value) + : $value; + } + + /** + * @template T of TKey|TValue + * + * @param (T is object ? T : FooBoxedArray) $value + * + * @return T + */ + protected function unboxValue(object $value) + { + return $value instanceof FooBoxedArray + ? $value->get() + : $value; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10861.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10861.php new file mode 100644 index 0000000000..a9116442b1 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10861.php @@ -0,0 +1,23 @@ + $array1 + * @param-out array $array1 + */ + public function sayHello(array &$array1): void + { + $values_1 = $array1; + + $values_1 = array_filter($values_1, function (mixed $value): bool { + return $value !== []; + }); + + /** @var array $values_1 */ + $array1 = $values_1; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-11015.php b/tests/PHPStan/Rules/PhpDoc/data/bug-11015.php new file mode 100644 index 0000000000..2b648f77d7 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-11015.php @@ -0,0 +1,25 @@ +fetch(); + if (empty($b)) { + return; + } + + /** @var array $b */ + echo $b['a']; + } + + public function sayHello2(PDOStatement $date): void + { + $b = $date->fetch(); + + /** @var array $b */ + echo $b['a']; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-11535.php b/tests/PHPStan/Rules/PhpDoc/data/bug-11535.php new file mode 100644 index 0000000000..17feae9b10 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-11535.php @@ -0,0 +1,18 @@ + */ +$a = function(string $b) { + return [1,2,3]; +}; + +/** @var \Closure(array): array */ +$a = function(array $b) { + return $b; +}; + +/** @var \Closure(string): string */ +$a = function(string $b) { + return $b; +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-11939.php b/tests/PHPStan/Rules/PhpDoc/data/bug-11939.php new file mode 100644 index 0000000000..759c3b5bb5 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-11939.php @@ -0,0 +1,36 @@ += 8.1 + +declare(strict_types=1); + +namespace Bug11939; + +enum What +{ + case This; + case That; + + /** + * @return ($this is self::This ? 'here' : 'there') + */ + public function where(): string + { + return match ($this) { + self::This => 'here', + self::That => 'there' + }; + } +} + +class Where +{ + /** + * @return ($what is What::This ? 'here' : 'there') + */ + public function __invoke(What $what): string + { + return match ($what) { + What::This => 'here', + What::That => 'there' + }; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-12457.php b/tests/PHPStan/Rules/PhpDoc/data/bug-12457.php new file mode 100644 index 0000000000..2d1564036f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-12457.php @@ -0,0 +1,24 @@ + $a + */ + public function test(array $a): void + { + /** @var \Closure(): list $c */ + $c = function () use ($a): array { + return $a; + }; + } + + /** + * @template T of HelloWorld + * @param list $a + */ + public function testGeneric(array $a): void + { + /** @var \Closure(): list $c */ + $c = function () use ($a): array { + return $a; + }; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-12708.php b/tests/PHPStan/Rules/PhpDoc/data/bug-12708.php new file mode 100644 index 0000000000..435359de5e --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-12708.php @@ -0,0 +1,31 @@ + */ + return [0 => 'a', 1 => 'b', 2 => 'c']; +} + +function do1() +{ + /** @var list */ + return [1 => 'b', 2 => 'c']; +} + +function do2() +{ + /** @var list */ + return [0 => 'a', 2 => 'c']; +} + +function do3() +{ + /** @var list */ + return [-1 => 'z', 0 => 'a', 1 => 'b', 2 => 'c']; +} + +function do4() +{ + /** @var list */ + return [0 => 'a', -1 => 'z', 1 => 'b', 2 => 'c']; +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php b/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php index 440577cae6..41bc50eae0 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php @@ -1,4 +1,4 @@ -= 7.4 + [], 'branches' => ['names' => [], 'exclude' => false]]; + } + else { + return 0; + } + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-6692.php b/tests/PHPStan/Rules/PhpDoc/data/bug-6692.php new file mode 100644 index 0000000000..41bd197ed9 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-6692.php @@ -0,0 +1,27 @@ += 8.0 + +namespace Bug6692; + +/** + * @template T + */ +class Wrapper +{ + /** + * @return $this + */ + public function change(): static + { + return $this; + } +} + +/** + * @template T + * @extends Wrapper + * + * @method self change() + */ +class SubWrapper extends Wrapper +{ +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php b/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php new file mode 100644 index 0000000000..f5a764e1c9 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php @@ -0,0 +1,36 @@ + + */ +class A +{ + /** @var TypeArrayMinMaxSet */ + protected array $var; +} + +class B extends A +{ + protected array $var = ["year" => ["min" => 1990, "max" => 2200]]; +} + +class AbstractC extends A +{ +} + +class CBroken extends AbstractC +{ + protected array $var = ["year" => ["min" => 1990, "max" => 2200]]; +} + +/** @phpstan-import-type TypeArrayMinMaxSet from A */ +class CWorks extends AbstractC +{ + /** @var TypeArrayMinMaxSet */ + protected array $var = ["year" => ["min" => 1990, "max" => 2200]]; +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-7310.php b/tests/PHPStan/Rules/PhpDoc/data/bug-7310.php new file mode 100644 index 0000000000..cab561b15c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-7310.php @@ -0,0 +1,33 @@ + + */ +class ObjectWithMetadata { + + /** @var M */ + private $metadata; + + /** + * @param M $metadata + */ + public function __construct( + array $metadata + ) { + $this->metadata = $metadata; + } + + /** + * @template K of string + * @template D + * @param K $key + * @param D $default + * @return (M[K] is not null ? M[K] : D) + */ + public function getMeta(string $key, mixed $default = null): mixed + { + return $this->metadata[ $key ] ?? $default; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-8284.php b/tests/PHPStan/Rules/PhpDoc/data/bug-8284.php new file mode 100644 index 0000000000..b372aa1f82 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-8284.php @@ -0,0 +1,39 @@ +}>):string) : (callable(array):string)) $replacement + * @param string $subject + * @param int $count Set by method + * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set + */ + public static function replaceCallback($pattern, callable $replacement, $subject, int $limit = -1, int &$count = null, int $flags = 0): string + { + if (!is_scalar($subject)) { + throw new \TypeError(''); + } + + $result = preg_replace_callback($pattern, $replacement, $subject, $limit, $count, $flags | PREG_UNMATCHED_AS_NULL); + if ($result === null) { + throw new \RuntimeException; + } + + return $result; + } +} + +function () { + HelloWorld::replaceCallback('{a+}', function ($match): string { + \PHPStan\dumpType($match); + return (string)$match[0][0]; + }, 'abcaddsa', -1, $count, PREG_OFFSET_CAPTURE); + + HelloWorld::replaceCallback('{a+}', function ($match): string { + \PHPStan\dumpType($match); + return (string)$match[0]; + }, 'abcaddsa', -1, $count); +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-8408.php b/tests/PHPStan/Rules/PhpDoc/data/bug-8408.php new file mode 100644 index 0000000000..c70a3cd146 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-8408.php @@ -0,0 +1,17 @@ +|list> + * @param T $bar + * + * @return (T[0] is string ? array{T} : T) + */ +function foo(array $bar) : array{ return is_string($bar[0]) ? [$bar] : $bar; } + +function(): void { + assertType("array{array{'foo', 'bar'}}", foo(['foo', 'bar'])); + assertType("array{array{'foo', 'bar'}, array{'xyz', 'asd'}}", foo([['foo','bar'],['xyz','asd']])); +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-8609.php b/tests/PHPStan/Rules/PhpDoc/data/bug-8609.php new file mode 100644 index 0000000000..ac6e4eb891 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-8609.php @@ -0,0 +1,34 @@ +value = $value; + } + + /** + * @template C of int + * + * @param C $coefficient + * + * @return ( + * T is positive-int + * ? (C is positive-int ? positive-int : negative-int) + * : T is negative-int + * ? (C is positive-int ? negative-int : positive-int) + * : (T is 0 ? 0 : int) + * ) + * ) + */ + public function multiply(int $coefficient): int { + return $this->value * $coefficient; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-8697.php b/tests/PHPStan/Rules/PhpDoc/data/bug-8697.php new file mode 100644 index 0000000000..3478c66524 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-8697.php @@ -0,0 +1,34 @@ + $array + * @param string $message + * + * @phpstan-impure + * + * @psalm-assert list $array + */ +function isList($array, $message = ''): void +{ + if (!array_is_list($array)) { + + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/function-conditional-return-type.php b/tests/PHPStan/Rules/PhpDoc/data/function-conditional-return-type.php new file mode 100644 index 0000000000..d668992b5f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/function-conditional-return-type.php @@ -0,0 +1,120 @@ += 8.0 + +namespace GenericCallableProperties; + +use Closure; +use stdClass; + +/** + * @template T + */ +class Test +{ + /** + * @var Closure(T): T + */ + private Closure $shadows; + + /** + * @var Closure(stdClass): stdClass + */ + private Closure $existingClass; + + /** + * @var callable(TypeAlias): TypeAlias + */ + private $typeAlias; + + /** + * @var callable(TNull): TNull + */ + private $unsupported; + + /** + * @var callable(TInvalid): TInvalid + */ + private $invalid; + + /** + * @param Closure(T): T $notReported + */ + public function __construct(private Closure $notReported) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php new file mode 100644 index 0000000000..238d822894 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php @@ -0,0 +1,204 @@ + 8.0 + +namespace GenericCallablesIncompatible; + +use Closure; +use stdClass; + +/** + * @param Closure(stdClass $val): stdClass $existingClass + */ +function existingClass(Closure $existingClass): void +{ +} + +/** + * @param Closure(TypeAlias $val): TypeAlias $existingTypeAlias + */ +function existingTypeAlias(Closure $existingTypeAlias): void +{ +} + +/** + * @param Closure(T $val): T $invalidBoundType + */ +function invalidBoundType(Closure $invalidBoundType): void +{ +} + +/** + * @param Closure(T $val): T $notSupported + */ +function notSupported(Closure $notSupported): void +{ +} + +/** + * @template T + * @param Closure(T $val): T $shadows + */ +function testShadowFunction(Closure $shadows): void +{ +} + +/** + * @param-out Closure(stdClass $val): stdClass $existingClass + */ +function existingClassParamOut(Closure &$existingClass): void +{ +} + +/** + * @template U + */ +class Test +{ + /** + * @template T + * @param Closure(T $val): T $shadows + */ + function testShadowMethod(Closure $shadows): void + { + } + + /** + * @template T + * @return Closure(T $val): T + */ + function testShadowMethodReturn(): Closure + { + } +} + +/** + * @return Closure(stdClass $val): stdClass + */ +function existingClassReturn(): Closure +{ +} + +/** + * @return Closure(TypeAlias $val): TypeAlias + */ +function existingTypeAliasReturn(): Closure +{ +} + +/** + * @return Closure(T $val): T + */ +function invalidBoundTypeReturn(): Closure +{ +} + +/** + * @return Closure(T $val): T + */ +function notSupportedReturn(): Closure +{ +} + +/** + * @template T + * @return Closure(T $val): T + */ +function testShadowFunctionReturn(): Closure +{ +} + +/** + * @template U + */ +class Test2 +{ + /** + * @param Closure(stdClass $val): stdClass $existingClass + */ + public function existingClass(Closure $existingClass): void + { + } + + /** + * @param Closure(TypeAlias $val): TypeAlias $existingTypeAlias + */ + public function existingTypeAlias(Closure $existingTypeAlias): void + { + } + + /** + * @param Closure(T $val): T $invalidBoundType + */ + public function invalidBoundType(Closure $invalidBoundType): void + { + } + + /** + * @param Closure(T $val): T $notSupported + */ + public function notSupported(Closure $notSupported): void + { + } + + /** + * @return Closure(stdClass $val): stdClass + */ + public function existingClassReturn(): Closure + { + } + + /** + * @return Closure(TypeAlias $val): TypeAlias + */ + public function existingTypeAliasReturn(): Closure + { + } + + /** + * @return Closure(T $val): T + */ + public function invalidBoundTypeReturn(): Closure + { + } + + /** + * @return Closure(T $val): T + */ + public function notSupportedReturn(): Closure + { + } +} + +/** + * @template T + * @param-out Closure(T $val): T $existingClass + */ +function shadowsParamOut(Closure &$existingClass): void +{ +} + +/** + * @template T + * @param-out list(T $val): T> $existingClasses + */ +function shadowsParamOutArray(array &$existingClasses): void +{ +} + +/** + * @template T + * @return list(T $val): T> + */ +function shadowsReturnArray(): array +{ +} + +/** + * @template T + */ +class Test3 +{ + /** + * @param Closure(T): T $shadows + */ + public function __construct(private Closure $shadows) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php new file mode 100644 index 0000000000..13b81b9f00 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php @@ -0,0 +1,28 @@ += 8.3 + +namespace IncompatibleClassConstantPhpDocNativeType; + +class Foo +{ + + public const int FOO = 1; + + /** @var positive-int */ + public const int BAR = 1; + + /** @var non-empty-string */ + public const int BAZ = 1; + + /** @var int|string */ + public const int LOREM = 1; + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php index c4f163e773..d2b9714425 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php @@ -8,45 +8,7 @@ class Foo /** @var self&\stdClass */ const FOO = 1; - /** @var int */ - const BAR = 1; - - const NO_TYPE = 'string'; - - /** @var string */ - const BAZ = 1; - - /** @var string|int */ - const LOREM = 1; - - /** @var int */ - const IPSUM = self::LOREM; // resolved to 1, I'd prefer string|int - /** @var self */ const DOLOR = 1; } - -class Bar extends Foo -{ - - const BAR = 2; - - const BAZ = 2; - -} - -class Baz -{ - - /** @var string */ - private const BAZ = 'foo'; - -} - -class Lorem extends Baz -{ - - private const BAZ = 1; - -} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-conditional-return-type.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-conditional-return-type.php new file mode 100644 index 0000000000..01f3cda99a --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-conditional-return-type.php @@ -0,0 +1,30 @@ += 8.0 + +namespace IncompatibleConditionalReturnType; + +class Foo +{ + + /** + * @return ($p is int ? int : string) + */ + public function doFoo($p): int|string + { + + } + +} + + +class Bar +{ + + /** + * @return ($p is int ? int : string) + */ + public function doFoo($p): int + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php new file mode 100644 index 0000000000..79cd19f116 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-param-immediately-invoked-callable.php @@ -0,0 +1,80 @@ + + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-hook-phpdoc-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-hook-phpdoc-types.php new file mode 100644 index 0000000000..b1ce3b8762 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-hook-phpdoc-types.php @@ -0,0 +1,66 @@ += 8.4 + +namespace IncompatiblePropertyHookPhpDocTypes; + +class Foo +{ + + public int $i { + /** @return string */ + get { + return $this->i; + } + } + + public int $j { + /** @return string */ + set { + $this->j = 1; + } + } + + public int $k { + /** + * @param string $value + * @param-out int $value + */ + set { + $this->k = 1; + } + } + + public int $l { + /** @param \stdClass&\Exception $value */ + set { + + } + } + + public \Exception $m { + /** @param \Exception $value */ + set { + + } + } + +} + +/** @template T */ +class GenericFoo +{ + + public int $n { + /** @param int|callable(T): T $value */ + set (int|callable $value) { + + } + } + + public int $o { + /** @param int|callable<\stdClass>(T): T $value */ + set (int|callable $value) { + + } + } + +} 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 56c5f7bc60..5c6746437b 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php @@ -1,4 +1,4 @@ -= 7.4 + */ + private $genericCompatibleInvariantType; + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric */ + private $genericRedundantTypeProjection; + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric<*> */ + private $genericCompatibleStarProjection; + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric */ + private $genericIncompatibleTypeProjection; + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-promoted.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-promoted.php index f42989946a..ebf011438b 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-promoted.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-promoted.php @@ -50,3 +50,17 @@ public function __construct( ) { } } + +class BazWithProperty +{ + + /** + * @param int $foo + * @param string $bar + */ + public function __construct(private int $foo, private int $bar) + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php new file mode 100644 index 0000000000..eb362e5b61 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php @@ -0,0 +1,178 @@ += 8.1 + +namespace IncompatibleRequireExtends; + +/** + * @phpstan-require-extends SomeTrait + */ +interface InvalidInterface1 {} + +/** + * @phpstan-require-extends SomeInterface + */ +interface InvalidInterface2 {} + +/** + * @phpstan-require-extends SomeEnum + */ +interface InvalidInterface3 {} + +/** + * @phpstan-require-extends TypeDoesNotExist + */ +interface InvalidInterface4 {} + +/** + * @template T + * @phpstan-require-extends SomeClass + */ +interface InvalidInterface5 {} + +/** + * @phpstan-require-extends int + */ +interface InvalidInterface6 {} + +/** + * @phpstan-require-extends SomeClass + */ +class InvalidClass {} + +/** + * @phpstan-require-extends SomeClass + */ +enum InvalidEnum {} + +class InValidTraitUse2 +{ + use ValidTrait; +} + +class InValidTraitUse extends SomeOtherClass +{ + use ValidTrait; +} + +class InvalidInterfaceUse2 implements ValidInterface {} + +class InvalidInterfaceUse extends SomeOtherClass implements ValidInterface {} + +class ValidInterfaceUse extends SomeClass implements ValidInterface {} + +class ValidTraitUse extends SomeClass +{ + use ValidTrait; +} + +class ValidTraitUse2 extends SomeSubClass +{ + use ValidTrait; +} +/** + * @phpstan-require-extends SomeClass + */ +interface ValidInterface {} + +/** + * @phpstan-require-extends SomeClass + */ +trait ValidTrait {} + + + +interface SomeInterface +{ + +} + +trait SomeTrait +{ + +} + +class SomeClass +{ + +} + +final class SomeFinalClass +{ + +} + +class SomeSubClass extends SomeClass +{ + +} + +class SomeOtherClass +{ + +} + +enum SomeEnum +{ + +} + +/** + * @phpstan-require-extends SomeFinalClass + */ +interface InvalidInterface7 {} + +/** + * @phpstan-require-extends SomeFinalClass + */ +trait InvalidTrait {} + +class InvalidClass2 { + use InvalidTrait; +} + +/** + * @phpstan-require-extends self&\stdClass + */ +interface UnresolvableExtendsInterface {} + +/** + * @phpstan-require-extends self&\stdClass + */ +trait UnresolvableExtendsTrait {} + +class InvalidClass3 { + use UnresolvableExtendsTrait; +} + +new class { + use ValidTrait; +}; + +new class extends SomeClass { + use ValidTrait; +}; + +/** + * @psalm-require-extends SomeClass + */ +trait ValidPsalmTrait {} + +new class extends SomeClass { + use ValidPsalmTrait; +}; + +new class { + use ValidPsalmTrait; +}; + +/** + * @phpstan-require-extends SomeClass + * @phpstan-require-extends SomeOtherClass + */ +trait TooMuchExtends {} + +/** + * @phpstan-require-extends SomeClass + * @phpstan-require-extends SomeOtherClass + * @phpstan-require-extends SomeOtherClass + */ +interface TooMuchExtendsIface {} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php new file mode 100644 index 0000000000..246d01a6e8 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php @@ -0,0 +1,170 @@ += 8.1 + +namespace IncompatibleRequireImplements; + +/** + * @phpstan-require-implements SomeTrait + */ +trait InvalidTrait1 {} + +/** + * @phpstan-require-implements SomeEnum + */ +trait InvalidTrait2 {} + +/** + * @phpstan-require-implements TypeDoesNotExist + */ +trait InvalidTrait3 {} + +/** + * @template T + * @phpstan-require-implements SomeClass + */ +trait InvalidTrait4 {} + +/** + * @phpstan-require-implements int + */ +trait InvalidTrait5 {} + +/** + * @phpstan-require-implements self&\stdClass + */ +trait InvalidTrait6 {} + + +/** + * @phpstan-require-implements SomeClass + */ +class InvalidClass {} + +/** + * @phpstan-require-implements SomeClass + */ +enum InvalidEnum {} + +class InValidTraitUse2 +{ + use ValidTrait; +} + +enum InvalidEnumTraitUse { + use ValidTrait; +} + +class InValidTraitUse extends SomeOtherClass implements WrongInterface +{ + use ValidTrait; +} + +class ValidTraitUse extends SomeClass implements RequiredInterface +{ + use ValidTrait; +} + +class ValidTraitUse2 extends ValidTraitUse +{ +} + +class ValidTraitUse3 extends ValidTraitUse +{ + use ValidTrait; +} + +/** + * @phpstan-require-implements RequiredInterface + */ +trait ValidTrait {} + +interface WrongInterface +{ + +} + +interface RequiredInterface +{ + +} + +interface SomeInterface +{ + +} + +trait SomeTrait +{ + +} + +class SomeClass {} + +class SomeSubClass extends SomeClass +{ + +} + +class SomeOtherClass +{ + +} + +enum SomeEnum +{ + +} + +new class { + use ValidTrait; +}; + +new class implements RequiredInterface { + use ValidTrait; +}; + +class InvalidTraitUse1 { + use InvalidTrait1; +} + +class InvalidTraitUse2 { + use InvalidTrait2; +} + +class InvalidTraitUse3 { + use InvalidTrait3; +} + +class InvalidTraitUse4 { + use InvalidTrait4; +} + +class InvalidTraitUse5 { + use InvalidTrait5; +} + +class InvalidTraitUse6 { + use InvalidTrait6; +} + +interface RequiredInterface2 +{ + +} + +/** + * @psalm-require-implements RequiredInterface + * @psalm-require-implements RequiredInterface2 + */ +trait ValidPsalmTrait {} + +new class implements RequiredInterface, RequiredInterface2 { + use ValidPsalmTrait; +}; + +new class implements RequiredInterface { + use ValidPsalmTrait; +}; + +new class { + use ValidPsalmTrait; +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php new file mode 100644 index 0000000000..c60ff3ce6c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php @@ -0,0 +1,105 @@ + + */ + public function two($param); + + /** + * @phpstan-self-out int + */ + public function three(); + + /** + * @phpstan-self-out self|null + */ + public function four(); +} + +/** + * @template T + */ +class Foo +{ + + /** @phpstan-self-out self */ + public static function selfOutStatic(): void + { + + } + + /** + * @phpstan-self-out int&string + */ + public function doFoo(): void + { + + } + + /** + * @phpstan-self-out self + */ + public function doBar(): void + { + + } + +} + +class GenericCheck +{ + + /** + * @phpstan-self-out self + */ + public function doFoo(): void + { + + } + +} + +/** + * @template T of \Exception + * @template U of int + */ +class GenericCheck2 +{ + + /** + * @phpstan-self-out self<\InvalidArgumentException> + */ + public function doFoo(): void + { + + } + + /** + * @phpstan-self-out self<\InvalidArgumentException, positive-int, string> + */ + public function doFoo2(): void + { + + } + + /** + * @phpstan-self-out self<\InvalidArgumentException, string> + */ + public function doFoo3(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php index 5f7fd45b73..078e004d26 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php @@ -284,3 +284,48 @@ function genericWrongBound(int $i) { } + +/** + * @param \InvalidPhpDocDefinitions\FooCovariantGeneric $foo + * @return \InvalidPhpDocDefinitions\FooCovariantGeneric + */ +function genericCompatibleInvariantType($foo) +{ + +} + +/** + * @param \InvalidPhpDocDefinitions\FooCovariantGeneric $foo + * @return \InvalidPhpDocDefinitions\FooCovariantGeneric + */ +function genericRedundantTypeProjection($foo) +{ + +} + +/** + * @param \InvalidPhpDocDefinitions\FooCovariantGeneric<*> $foo + * @return \InvalidPhpDocDefinitions\FooCovariantGeneric<*> + */ +function genericCompatibleStarProjection($foo) +{ + +} + +/** + * @param \InvalidPhpDocDefinitions\FooCovariantGeneric $foo + * @return \InvalidPhpDocDefinitions\FooCovariantGeneric + */ +function genericIncompatibleTypeProjection($foo) +{ + +} + +/** + * @param pure-callable(): void $cb + * @param pure-Closure(): void $cl + */ +function pureCallableCannotReturnVoid(callable $cb, \Closure $cl): void +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php index 7eff223ae0..74ad37a779 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php @@ -15,3 +15,28 @@ class FooGeneric { } + +/** + * @template-covariant T + */ +class FooCovariantGeneric +{ + +} + +/** + * @template T = string + */ +class FooGenericWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class FooGenericWithSomeDefaults +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-property-hooks.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-property-hooks.php new file mode 100644 index 0000000000..f145c5d437 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-property-hooks.php @@ -0,0 +1,15 @@ += 8.4 + +namespace InvalidPhpDocPropertyHooks; + +class Foo +{ + + public int $i { + /** @return Test( */ + get { + + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php index 15a61e603e..8b0cf8bd81 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php @@ -81,3 +81,25 @@ class ClassConstant const FOO = 1; } + +class AboveProperty +{ + + /** @var (Foo& */ + private $foo; + + /** @var (Foo& */ + private const TEST = 1; + +} + +class AboveReturn +{ + + public function doFoo(): string + { + /** @var (Foo& */ + return doFoo(); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php index fa2e510986..60a35c8178 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php @@ -36,3 +36,25 @@ function any() } } + +class AboveProperty +{ + + /** @phpstan-varr 1 */ + private $foo; + + /** @phpstan-varr 1 */ + private const TEST = 1; + +} + +class AboveReturn +{ + + public function doFoo(): string + { + /** @phpstan-varr string */ + return doFoo(); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-tag-property-hooks.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-tag-property-hooks.php new file mode 100644 index 0000000000..1221fe7b43 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-tag-property-hooks.php @@ -0,0 +1,15 @@ += 8.4 + +namespace InvalidPHPStanTagPropertyHooks; + +class Foo +{ + + public int $i { + /** @phpstan-what what */ + get { + + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-throws-property-hook.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-throws-property-hook.php new file mode 100644 index 0000000000..c40b13aa9f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-throws-property-hook.php @@ -0,0 +1,22 @@ += 8.4 + +namespace InvalidThrowsPropertyHook; + +class Foo +{ + + public int $i { + /** @throws \InvalidArgumentException */ + get { + return 1; + } + } + + public int $j { + /** @throws \DateTimeImmutable */ + get { + return 1; + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php new file mode 100644 index 0000000000..b99b3e5577 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php @@ -0,0 +1,20 @@ + $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric<*> $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooGenericWithDefault $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooGenericWithSomeDefaults $test */ + $test = doFoo(); } public function doBar($foo) diff --git a/tests/PHPStan/Rules/PhpDoc/data/method-assert.php b/tests/PHPStan/Rules/PhpDoc/data/method-assert.php new file mode 100644 index 0000000000..ad07ee066d --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/method-assert.php @@ -0,0 +1,215 @@ +fooProp + */ + public function doFoo($a): bool + { + + } + + /** + * @phpstan-assert Nonexistent $a + * @phpstan-assert FooTrait $b + * @phpstan-assert fOO $c + * @phpstan-assert Foo $this->barProp + */ + public function doBar($a, $b, $c): bool + { + + } + + /** + * @phpstan-assert !null $this->fooProp + */ + public static function doBaz(): void + { + + } + +} + +trait FooTrait +{ + +} + +class InvalidGenerics +{ + + /** + * @phpstan-assert \Exception $m + */ + function invalidPhpstanAssertGeneric($m) { + + } + + /** + * @phpstan-assert FooBar $m + */ + function invalidPhpstanAssertWrongGenericParams($m) { + + } + + /** + * @phpstan-assert FooBar $m + */ + function invalidPhpstanAssertNotAllGenericParams($m) { + + } + + /** + * @phpstan-assert FooBar $m + */ + function invalidPhpstanAssertMoreGenericParams($m) { + + } + +} + + +/** + * @template T of int + * @template TT of string + */ +class FooBar { + /** + * @param-out T $s + */ + function genericClassFoo(mixed &$s): void + { + } + + /** + * @template S of self + * @param-out S $s + */ + function genericSelf(mixed &$s): void + { + } + + /** + * @template S of static + * @param-out S $s + */ + function genericStatic(mixed &$s): void + { + } +} + +class MissingTypes +{ + + /** + * @phpstan-assert array $m + */ + public function doFoo($m): void + { + + } + + /** + * @phpstan-assert FooBar $m + */ + public function doBar($m): void + { + + } + + /** + * @phpstan-assert callable $m + */ + public function doBaz($m): void + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php b/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php new file mode 100644 index 0000000000..7829e8c71a --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php @@ -0,0 +1,189 @@ + ? true : false) + */ + public function foo(): bool + { + + } + +} + +class ParamOut +{ + + /** + * @param-out ($i is int ? 1 : 2) $out + */ + public function doFoo(int $i, &$out) { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/new-is-always-final-var-tag-type.php b/tests/PHPStan/Rules/PhpDoc/data/new-is-always-final-var-tag-type.php new file mode 100644 index 0000000000..3b705fbf07 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/new-is-always-final-var-tag-type.php @@ -0,0 +1,23 @@ +returnStatic(); +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/param-closure-this.php b/tests/PHPStan/Rules/PhpDoc/data/param-closure-this.php new file mode 100644 index 0000000000..46ad5bde9c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/param-closure-this.php @@ -0,0 +1,72 @@ + $i + */ +function invalidParamClosureThisGeneric(callable $i) { + +} + +/** + * @param-closure-this FooBar $i + */ +function invalidParamClosureThisWrongGenericParams(callable $i) { + +} + +/** + * @param-closure-this FooBar $i + */ +function invalidParamClosureThisNotAllGenericParams(callable $i) { + +} + +/** + * @template T of int + * @template TT of string + */ +class FooBar { + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/param-out.php b/tests/PHPStan/Rules/PhpDoc/data/param-out.php new file mode 100644 index 0000000000..5c4da69694 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/param-out.php @@ -0,0 +1,115 @@ + $i + */ +function unresolvableParamOutType(int &$i) { + +} + +/** + * @param-out \Exception $i + */ +function invalidParamOutGeneric(int &$i) { + +} + +/** + * @param-out FooBar $i + */ +function invalidParamOutWrongGenericParams(int &$i) { + +} + +/** + * @param-out FooBar $i + */ +function invalidParamOutNotAllGenericParams(int &$i) { + +} + +/** + * @template T of int + * @template TT of string + */ +class FooBar { + /** + * @param-out T $s + */ + function genericClassFoo(mixed &$s): void + { + } + + /** + * @template S of self + * @param-out S $s + */ + function genericSelf(mixed &$s): void + { + } + + /** + * @template S of static + * @param-out S $s + */ + function genericStatic(mixed &$s): void + { + } +} + +class C { + /** + * @var \Closure|null + */ + private $onCancel; + + public function __construct() { + $this->foo($this->onCancel); + } + + /** + * @param mixed $onCancel + * @param-out \Closure $onCancel + */ + public function foo(&$onCancel) : void { + $onCancel = function (): void {}; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php index 823fb21600..215b330ff9 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php +++ b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php @@ -1,4 +1,4 @@ -= 7.4 += 8.1 + +namespace ValueOfEnumPhpdoc; + +enum Country: string +{ + case NL = 'The Netherlands'; + case US = 'United States'; +} + +enum CountryNo: int +{ + case NL = 1; + case US = 2; +} + +class Foo { + /** + * @param value-of $countryName + */ + function hello(string $countryName): void + { + // ... + } + + /** + * @param value-of $shouldError + */ + function helloError(int $shouldError): void + { + // ... + } + /** + * @param value-of $shouldError + */ + function helloError2(string $shouldError): void + { + // ... + } + + function doFoo() { + $this->hello(Country::NL); + } +} + diff --git a/tests/PHPStan/Rules/PhpDoc/data/var-above-empty-array-widening.php b/tests/PHPStan/Rules/PhpDoc/data/var-above-empty-array-widening.php new file mode 100644 index 0000000000..2664d8968e --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/var-above-empty-array-widening.php @@ -0,0 +1,29 @@ + $a */ +$a = []; + +/** @var array{string, int} $a */ +$a = []; + +/** @var int $a */ +$a = []; + +$translationsTree = []; + +/** @var array $byRef */ +$byRef = &$translationsTree; diff --git a/tests/PHPStan/Rules/PhpDoc/data/var-tag-changed-expr-type.php b/tests/PHPStan/Rules/PhpDoc/data/var-tag-changed-expr-type.php new file mode 100644 index 0000000000..d8d72956fc --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/var-tag-changed-expr-type.php @@ -0,0 +1,78 @@ +foo; + } + + public function doBar() + { + /** @var string */ + return $this->foo; + } + +} + +class Baz +{ + + public function doFoo(int $foo) + { + /** @var int $foo */ + return $this->doBar($foo); + } + + public function doBar(int $foo) + { + /** @var string $foo */ + return $this->doFoo($foo); + } + +} + +class Lorem +{ + + public function doFoo(int $foo) + { + /** @var int $foo */ + if ($foo) { + + } + } + + public function doBar(int $foo) + { + /** @var string $foo */ + if ($foo) { + + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/wrong-var-native-type.php b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-native-type.php new file mode 100644 index 0000000000..2edb6ba496 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-native-type.php @@ -0,0 +1,238 @@ +doBar(); + + /** @var string|null $stringOrNull */ + $stringOrNull = $this->doBar(); + + /** @var string|null $null */ + $null = null; + + /** @var \SplObjectStorage<\stdClass, array{int, string}> $running */ + $running = new \SplObjectStorage(); + + /** @var \stdClass $running2 */ + $running2 = new \SplObjectStorage(); + + /** @var int $int */ + $int = 'foo'; + + /** @var int $test */ + $test = $this->doBaz(); + + /** @var array $ints */ + $ints = $this->returnsListOfIntegers(); + + /** @var array $strings */ + $strings = $this->returnsListOfIntegers(); + + /** @var \Iterator $intIterator */ + $intIterator = $this->returnsListOfIntegers(); + + /** @var \Iterator $intIterator */ + $intIterator2 = $this->returnsIteratorOfIntegers(); + + /** @var \Iterator $stringIterator */ + $stringIterator = $this->returnsIteratorOfIntegers(); + + /** @var int[] $ints2 */ + $ints2 = $this->returnsArrayOfIntegers(); + } + + public function doBar(): string + { + + } + + /** + * @return string + */ + public function doBaz() + { + + } + + /** + * @return list + */ + public function returnsListOfIntegers(): array + { + + } + + /** + * @return \Iterator + */ + public function returnsIteratorOfIntegers(): \Iterator + { + + } + + /** @return array */ + public function returnsArrayOfIntegers(): array + { + + } + + /** @param int[] $integers */ + public function trickyForeachCase(array $integers): void + { + foreach ($integers as $int) { + /** @var int $int */ + $a = new \stdClass(); + } + + foreach ($integers as $int) { + /** @var string $int */ + $a = new \stdClass(); + } + + /** @var string */ + $nameless = 1; + } + + public function testArrayDestructuring(int $i, string $s): void + { + /** + * @var int $a + * @var string $b + * @var int $c + */ + [$a, $b, $c] = [$i, $s, $s]; + } + + /** + * @param array $a + */ + public function testForeach(array $a): void + { + /** + * @var string[] $a + * @var int $k + * @var string $v + */ + foreach ($a as $k => $v) { + + } + } + + /** + * @param array $a + */ + public function testForeach2(array $a): void + { + /** + * @var int[] $a + * @var string $k + * @var int $v + */ + foreach ($a as $k => $v) { + + } + } + + public function testStatic(): void + { + /** @var int $a */ + static $a = 1; + + /** @var int $b */ + static $b = 'foo'; + } + + public function iterablesRecursively(): void + { + /** @var array> $a */ + $a = $this->arrayOfLists(); + + /** @var array> $b */ + $b = $this->arrayOfLists(); + + /** @var array> $c */ + $c = $this->arrayOfLists(); + + /** @var array<\Traversable> $d */ + $d = $this->arrayOfLists(); + } + + /** @return array> */ + private function arrayOfLists(): array + { + + } + +} + +class PHPStanType +{ + + public function doFoo(): void + { + /** @var \PHPStan\Type\Type $a */ + $a = $this->doBar(); // not narrowing - ok + + /** @var \PHPStan\Type\Type|null $b */ + $b = $this->doBar(); // not narrowing - ok + + /** @var \stdClass $c */ + $c = $this->doBar(); // not subtype - error + + /** @var \PHPStan\Type\ObjectType|null $d */ + $d = $this->doBar(); // narrowing Type - error + + /** @var \PHPStan\Type\ObjectType $e */ + $e = $this->doBar(); // narrowing Type - error + + /** @var \PHPStan\Type\ObjectType $f */ + $f = $this->doBaz(); // not narrowing - does not have to error but currently does + + /** @var \PHPStan\Type\ObjectType|null $g */ + $g = $this->doBaz(); // not narrowing - ok + + /** @var \PHPStan\Type\Type|null $g */ + $g = $this->doBaz(); // generalizing - not ok + + /** @var \PHPStan\Type\ObjectType|null $h */ + $h = $this->doBazPhpDoc(); // generalizing - not ok + } + + public function doBar(): ?\PHPStan\Type\Type + { + + } + + public function doBaz(): ?\PHPStan\Type\ObjectType + { + + } + + /** + * @return \PHPStan\Type\Generic\GenericObjectType|null + */ + public function doBazPhpDoc() + { + + } + +} + +class Ipsum +{ + /** + * @param array{id: int}|null $b + */ + public function doFoo($b): void + { + /** @var mixed[]|null $a */ + $a = $b; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php b/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php index ceffc3b89c..30f7dd3182 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php +++ b/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php @@ -79,16 +79,16 @@ public function doBaz() static $var; /** @var int */ - static $var; + static $var2; /** @var int */ - static $var, $bar; + static $var3, $bar; /** * @var int * @var string */ - static $var, $bar; + static $var4, $bar2; /** @var int $foo */ static $test; @@ -115,6 +115,7 @@ public function multiplePrefixedTagsAreFine() * @var int * @phpstan-var int * @psalm-var int + * @phan-var int */ $test = doFoo(); // OK @@ -313,3 +314,20 @@ function doFoo(): void { } + +class VarTagAboveLiteralArray +{ + + public function doFoo(): void + { + /** @var array */ + $arr = ['' => 'empty', 1 => '1']; + } + + public function doFoo2(): void + { + /** @var array */ + $arr = ['' => 'empty', 1 => '1']; + } + +} diff --git a/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php b/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php new file mode 100644 index 0000000000..2f580113f5 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php @@ -0,0 +1,42 @@ + + */ +class FunctionNeverRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FunctionNeverRule(new NeverRuleHelper()); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1 or greater.'); + } + + $this->analyse([__DIR__ . '/data/function-never.php'], [ + [ + 'Function FunctionNever\doBar() always throws an exception, it should have return type "never".', + 18, + ], + [ + 'Function FunctionNever\callsNever() always terminates script execution, it should have return type "never".', + 23, + ], + [ + 'Function FunctionNever\doBaz() always terminates script execution, it should have return type "never".', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php new file mode 100644 index 0000000000..83e315479d --- /dev/null +++ b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php @@ -0,0 +1,42 @@ + + */ +class MethodNeverRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MethodNeverRule(new NeverRuleHelper()); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1 or greater.'); + } + + $this->analyse([__DIR__ . '/data/method-never.php'], [ + [ + 'Method MethodNever\Foo::doBar() always throws an exception, it should have return type "never".', + 21, + ], + [ + 'Method MethodNever\Foo::callsNever() always terminates script execution, it should have return type "never".', + 26, + ], + [ + 'Method MethodNever\Foo::doBaz() always terminates script execution, it should have return type "never".', + 31, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/NoPhpCodeRuleTest.php b/tests/PHPStan/Rules/Playground/NoPhpCodeRuleTest.php new file mode 100644 index 0000000000..146c26325e --- /dev/null +++ b/tests/PHPStan/Rules/Playground/NoPhpCodeRuleTest.php @@ -0,0 +1,34 @@ + + */ +class NoPhpCodeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoPhpCodeRule(); + } + + public function testEmptyFile(): void + { + $this->analyse([__DIR__ . '/data/empty.php'], []); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/no-php-code.php'], [ + [ + 'The example does not contain any PHP code. Did you forget the opening > + */ +class PromoteParameterRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PromoteParameterRule( + new UninitializedPropertyRule(new ConstructorsHelper( + self::getContainer(), + [], + )), + ClassPropertiesNode::class, + false, + 'checkUninitializedProperties', + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/promote-parameter.php'], [ + [ + 'Class PromoteParameter\Foo has an uninitialized property $test. Give it default value or assign it in the constructor.', + 8, + 'This error would be reported if the checkUninitializedProperties: true parameter was enabled in your %configurationFile%.', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/StaticVarWithoutTypeRuleTest.php b/tests/PHPStan/Rules/Playground/StaticVarWithoutTypeRuleTest.php new file mode 100644 index 0000000000..4ef6334828 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/StaticVarWithoutTypeRuleTest.php @@ -0,0 +1,34 @@ + + */ +class StaticVarWithoutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new StaticVarWithoutTypeRule(self::getContainer()->getByType(FileTypeMapper::class)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/static-var-without-type.php'], [ + [ + 'Static variable needs to be typed with PHPDoc @var tag.', + 23, + ], + [ + 'Static variable needs to be typed with PHPDoc @var tag.', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/data/empty.php b/tests/PHPStan/Rules/Playground/data/empty.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/PHPStan/Rules/Playground/data/function-never.php b/tests/PHPStan/Rules/Playground/data/function-never.php new file mode 100644 index 0000000000..68087469ae --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/function-never.php @@ -0,0 +1,50 @@ + */ +function yields(): \Generator +{ + while (true) { + yield rand(); + } +} diff --git a/tests/PHPStan/Rules/Playground/data/method-never.php b/tests/PHPStan/Rules/Playground/data/method-never.php new file mode 100644 index 0000000000..8353da4d4a --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/method-never.php @@ -0,0 +1,57 @@ +doFoo(); + } + + public function doBaz() + { + while (true) { + + } + } + + public function onlySometimes() + { + if (rand(0, 1)) { + return; + } + + throw new \Exception(); + } + + /** + * @return \Generator + */ + public function yields(): \Generator + { + while(true) { + yield 1; + } + } + +} diff --git a/tests/PHPStan/Rules/Playground/data/no-php-code.php b/tests/PHPStan/Rules/Playground/data/no-php-code.php new file mode 100644 index 0000000000..1211cfbd4b --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/no-php-code.php @@ -0,0 +1,4 @@ +class Foo +{ + private int $foo; +} diff --git a/tests/PHPStan/Rules/Playground/data/promote-parameter.php b/tests/PHPStan/Rules/Playground/data/promote-parameter.php new file mode 100644 index 0000000000..da1ea8ad08 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/promote-parameter.php @@ -0,0 +1,10 @@ +createReflectionProvider(); return new AccessPropertiesInAssignRule( - new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), true), + new AccessPropertiesCheck($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new PhpVersion(PHP_VERSION_ID), true, true), ); } public function testRule(): void { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/access-properties-assign.php'], [ [ 'Access to an undefined property TestAccessPropertiesAssign\AccessPropertyWithDimFetch::$foo.', 10, + $tipText, ], [ 'Access to an undefined property TestAccessPropertiesAssign\AccessPropertyWithDimFetch::$foo.', 15, + $tipText, ], ]); } public function testRuleAssignOp(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/access-properties-assign-op.php'], [ [ 'Access to an undefined property TestAccessProperties\AssignOpNonexistentProperty::$flags.', 15, + $tipText, ], ]); } public function testRuleExpressionNames(): void { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/properties-from-variable-into-object.php'], [ [ 'Access to an undefined property PropertiesFromVariableIntoObject\Foo::$noop.', 26, + $tipText, ], ]); } public function testRuleExpressionNames2(): void { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/properties-from-array-into-object.php'], [ [ 'Access to an undefined property PropertiesFromArrayIntoObject\Foo::$noop.', 42, + $tipText, ], [ 'Access to an undefined property PropertiesFromArrayIntoObject\Foo::$noop.', 54, + $tipText, ], [ 'Access to an undefined property PropertiesFromArrayIntoObject\Foo::$noop.', 69, + $tipText, ], [ 'Access to an undefined property PropertiesFromArrayIntoObject\Foo::$noop.', 110, + $tipText, + ], + ]); + } + + public function testBug4492(): void + { + $this->analyse([__DIR__ . '/data/bug-4492.php'], []); + } + + public function testObjectShapes(): void + { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + $this->analyse([__DIR__ . '/data/properties-object-shapes.php'], [ + [ + 'Access to an undefined property object{foo: int, bar?: string}::$bar.', + 19, + $tipText, + ], + [ + 'Access to an undefined property object{foo: int, bar?: string}::$baz.', + 20, + $tipText, + ], + ]); + } + + public function testConflictingAnnotationProperty(): void + { + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], []); + } + + public function testBug10477(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10477.php'], []); + } + + public function testAsymmetricVisibility(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/write-asymmetric-visibility.php'], [ + [ + 'Assign to private(set) property $this(WriteAsymmetricVisibility\Bar)::$a.', + 26, + ], + [ + 'Assign to private(set) property WriteAsymmetricVisibility\Foo::$a.', + 34, + ], + [ + 'Assign to protected(set) property WriteAsymmetricVisibility\Foo::$b.', + 35, + ], + [ + 'Access to private property $c of parent class WriteAsymmetricVisibility\ReadonlyProps.', + 64, + ], + [ + 'Assign to protected(set) property WriteAsymmetricVisibility\ReadonlyProps::$a.', + 70, + ], + [ + 'Access to protected property WriteAsymmetricVisibility\ReadonlyProps::$b.', + 71, + ], + [ + 'Access to private property WriteAsymmetricVisibility\ReadonlyProps::$c.', + 72, + ], + [ + 'Assign to private(set) property WriteAsymmetricVisibility\ArrayProp::$a.', + 83, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 582c681e92..3fab4606d8 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -2,9 +2,11 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; use const PHP_VERSION_ID; /** @@ -17,157 +19,161 @@ class AccessPropertiesRuleTest extends RuleTestCase private bool $checkUnionTypes; + private bool $checkDynamicProperties; + protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - return new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false), true); + return new AccessPropertiesRule(new AccessPropertiesCheck($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false, false, false, true), new PhpVersion(PHP_VERSION_ID), true, $this->checkDynamicProperties)); } public function testAccessProperties(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ [ 'Access to an undefined property TestAccessProperties\BarAccessProperties::$loremipsum.', - 23, + 24, + $tipText, ], [ 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', - 24, + 25, ], [ 'Cannot access property $propertyOnString on string.', - 31, + 32, ], [ 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', - 42, + 43, ], [ 'Access to protected property TestAccessProperties\FooAccessProperties::$bar.', - 43, + 44, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$baz.', - 49, + 50, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$nonexistent.', - 52, + 53, + $tipText, ], [ 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', - 58, + 59, ], [ 'Access to protected property TestAccessProperties\FooAccessProperties::$bar.', - 59, + 60, ], [ 'Access to property $foo on an unknown class TestAccessProperties\UnknownClass.', - 63, + 64, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyBaz.', - 68, + 69, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyNonexistent.', - 70, + 71, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', - 76, + 77, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', - 77, + 78, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', - 80, + 81, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', - 83, + 84, + $tipText, ], [ 'Access to property $test on an unknown class TestAccessProperties\FirstUnknownClass.', - 146, + 147, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to property $test on an unknown class TestAccessProperties\SecondUnknownClass.', - 146, + 147, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to an undefined property TestAccessProperties\WithFooAndBarProperty|TestAccessProperties\WithFooProperty::$bar.', - 176, + 177, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\SomeInterface&TestAccessProperties\WithFooProperty::$bar.', - 193, + 194, + $tipText, ], [ 'Cannot access property $ipsum on TestAccessProperties\FooAccessProperties|null.', - 207, + 208, ], [ 'Cannot access property $foo on null.', - 220, + 221, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$lorem.', - 247, + 248, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$dolor.', - 250, - ], - [ - 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', - 264, - ], - [ - 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', - 266, - ], - [ - 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', - 270, + 251, + $tipText, ], [ 'Cannot access property $bar on TestAccessProperties\NullCoalesce|null.', - 272, + 274, ], [ 'Cannot access property $foo on TestAccessProperties\NullCoalesce|null.', - 272, + 274, ], [ 'Cannot access property $foo on TestAccessProperties\NullCoalesce|null.', - 272, - ], - [ - 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', - 299, + 274, ], [ - 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', - 386, + 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:297::$barProperty.', + 302, + $tipText, ], [ 'Cannot access property $selfOrNull on TestAccessProperties\RevertNonNullabilityForIsset|null.', - 402, + 407, ], [ - 'Cannot access property $array on stdClass|null.', - 412, + 'Access to an undefined property object::$baz.', + 438, + $tipText, ], ], ); @@ -177,123 +183,123 @@ public function testAccessPropertiesWithoutUnionTypes(): void { $this->checkThisOnly = false; $this->checkUnionTypes = false; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ [ 'Access to an undefined property TestAccessProperties\BarAccessProperties::$loremipsum.', - 23, + 24, + $tipText, ], [ 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', - 24, + 25, ], [ 'Cannot access property $propertyOnString on string.', - 31, + 32, ], [ 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', - 42, + 43, ], [ 'Access to protected property TestAccessProperties\FooAccessProperties::$bar.', - 43, + 44, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$baz.', - 49, + 50, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$nonexistent.', - 52, + 53, + $tipText, ], [ 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', - 58, + 59, ], [ 'Access to protected property TestAccessProperties\FooAccessProperties::$bar.', - 59, + 60, ], [ 'Access to property $foo on an unknown class TestAccessProperties\UnknownClass.', - 63, + 64, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyBaz.', - 68, + 69, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyNonexistent.', - 70, + 71, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', - 76, + 77, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', - 77, + 78, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', - 80, + 81, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', - 83, + 84, + $tipText, ], [ 'Access to property $test on an unknown class TestAccessProperties\FirstUnknownClass.', - 146, + 147, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to property $test on an unknown class TestAccessProperties\SecondUnknownClass.', - 146, + 147, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to an undefined property TestAccessProperties\SomeInterface&TestAccessProperties\WithFooProperty::$bar.', - 193, + 194, + $tipText, ], [ 'Cannot access property $foo on null.', - 220, + 221, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$lorem.', - 247, + 248, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$dolor.', - 250, - ], - [ - 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', - 264, - ], - [ - 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', - 266, - ], - [ - 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', - 270, + 251, + $tipText, ], [ 'Cannot access property $bar on TestAccessProperties\NullCoalesce|null.', - 272, - ], - [ - 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', - 299, + 274, ], [ - 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', - 386, + 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:297::$barProperty.', + 302, + $tipText, ], ], ); @@ -301,15 +307,16 @@ public function testAccessPropertiesWithoutUnionTypes(): void public function testRuleAssignOp(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/access-properties-assign-op.php'], [ [ 'Access to an undefined property TestAccessProperties\AssignOpNonexistentProperty::$flags.', 10, + $tipText, ], ]); } @@ -318,29 +325,43 @@ public function testAccessPropertiesOnThisOnly(): void { $this->checkThisOnly = true; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ [ 'Access to an undefined property TestAccessProperties\BarAccessProperties::$loremipsum.', - 23, - ], - [ - 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', 24, + $tipText, ], [ - 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', - 386, + 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', + 25, ], ], ); } + public function testBug12692(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = false; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-12692.php'], [[ + 'Non-static access to static property Bug12692\Foo::$static.', + 14, + ]]); + } + public function testAccessPropertiesAfterIsNullInBooleanOr(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/access-properties-after-isnull.php'], [ [ 'Cannot access property $fooProperty on null.', @@ -353,10 +374,12 @@ public function testAccessPropertiesAfterIsNullInBooleanOr(): void [ 'Access to an undefined property AccessPropertiesAfterIsNull\Foo::$barProperty.', 28, + $tipText, ], [ 'Access to an undefined property AccessPropertiesAfterIsNull\Foo::$barProperty.', 31, + $tipText, ], [ 'Cannot access property $fooProperty on null.', @@ -369,10 +392,12 @@ public function testAccessPropertiesAfterIsNullInBooleanOr(): void [ 'Access to an undefined property AccessPropertiesAfterIsNull\Foo::$barProperty.', 47, + $tipText, ], [ 'Access to an undefined property AccessPropertiesAfterIsNull\Foo::$barProperty.', 50, + $tipText, ], ]); } @@ -381,10 +406,14 @@ public function testDateIntervalChildProperties(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/date-interval-child-properties.php'], [ [ 'Access to an undefined property AccessPropertiesDateIntervalChild\DateIntervalChild::$nonexistent.', 14, + $tipText, ], ]); } @@ -393,6 +422,7 @@ public function testClassExists(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/access-properties-class-exists.php'], [ [ @@ -422,10 +452,14 @@ public function testMixin(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/mixin.php'], [ [ 'Access to an undefined property MixinProperties\GenericFoo::$namee.', - 51, + 55, + $tipText, ], ]); } @@ -434,22 +468,22 @@ public function testBug3947(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-3947.php'], []); } public function testNullSafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/nullsafe-property-fetch.php'], [ [ 'Access to an undefined property NullsafePropertyFetch\Foo::$baz.', 13, + $tipText, ], [ 'Cannot access property $bar on string.', @@ -467,6 +501,14 @@ public function testNullSafe(): void 'Cannot access property $bar on string.', 22, ], + [ + 'Cannot access property $foo on null.', + 28, + ], + [ + 'Cannot access property $foo on null.', + 29, + ], ]); } @@ -474,17 +516,15 @@ public function testBug3371(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-3371.php'], []); } public function testBug4527(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-4527.php'], []); } @@ -492,10 +532,10 @@ public function testBug4808(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-4808.php'], []); } - public function testBug5868(): void { if (PHP_VERSION_ID < 80000) { @@ -503,6 +543,7 @@ public function testBug5868(): void } $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-5868.php'], [ [ 'Cannot access property $child on Bug5868PropertyFetch\Foo|null.', @@ -531,14 +572,19 @@ public function testBug6385(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/bug-6385.php'], [ [ 'Access to an undefined property UnitEnum::$value.', 43, + $tipText, ], [ 'Access to an undefined property Bug6385\ActualUnitEnum::$value.', 47, + $tipText, ], ]); } @@ -550,7 +596,451 @@ public function testBug6566(): void } $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-6566.php'], []); } + public function testBug6899(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $errors = [ + [ + 'Cannot access property $prop on string.', + 13, + ], + [ + 'Cannot access property $prop on string.', + 14, + ], + [ + 'Cannot access property $prop on string.', + 15, + ], + ]; + $this->analyse([__DIR__ . '/data/bug-6899.php'], $errors); + } + + public function testBug6026(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-6026.php'], []); + } + + public function testBug3659(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $errors = []; + $this->analyse([__DIR__ . '/data/bug-3659.php'], $errors); + } + + public function dataDynamicProperties(): array + { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + $errors = [ + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 14, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 15, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 16, + $tipText, + ], + ]; + + $errorsWithMore = array_merge([ + [ + 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', + 9, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', + 10, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', + 11, + $tipText, + ], + ], $errors); + + $errors[] = [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 29, + $tipText, + ]; + + $errorsWithMore = array_merge($errorsWithMore, [ + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 20, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 21, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 22, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 29, + $tipText, + ], + ]); + + $errorsWithMore = array_merge($errorsWithMore, [ + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 32, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 33, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 34, + $tipText, + ], + ]); + + $otherErrors = [ + [ + 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', + 42, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', + 43, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', + 44, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', + 47, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', + 48, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', + 49, + $tipText, + ], + ]; + + return [ + [false, PHP_VERSION_ID < 80200 ? [ + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 29, + $tipText, + ], + ] : array_merge($errors, $otherErrors)], + [true, array_merge($errorsWithMore, $otherErrors)], + ]; + } + + /** + * @dataProvider dataDynamicProperties + * @param list $errors + */ + public function testDynamicProperties(bool $checkDynamicProperties, array $errors): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = $checkDynamicProperties; + $this->analyse([__DIR__ . '/data/dynamic-properties.php'], $errors); + } + + public function testBug4559(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $errors = []; + $this->analyse([__DIR__ . '/data/bug-4559.php'], $errors); + } + + public function testBug3171(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-3171.php'], []); + } + + public function testBug3171OnDynamicProperties(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-3171.php'], []); + } + + public function dataTrueAndFalse(): array + { + return [ + [true], + [false], + ]; + } + + /** + * @dataProvider dataTrueAndFalse + */ + public function testPhp82AndDynamicProperties(bool $b): void + { + $errors = []; + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + if (PHP_VERSION_ID >= 80200) { + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\ClassA::$properties.', + 34, + $tipText, + ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 71, + $tipText, + ]; + if ($b) { + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 78, + $tipText, + ]; + } + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.', + 112, + $tipText, + ]; + } elseif ($b) { + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 71, + $tipText, + ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 78, + $tipText, + ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.', + 112, + $tipText, + ]; + } + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = $b; + $this->analyse([__DIR__ . '/data/php-82-dynamic-properties.php'], $errors); + } + + /** + * @dataProvider dataTrueAndFalse + */ + public function testPhp82AndDynamicPropertiesAllow(bool $b): void + { + $errors = []; + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + if ($b) { + $errors[] = [ + 'Access to an undefined property Php82DynamicPropertiesAllow\HelloWorld::$world.', + 75, + $tipText, + ]; + } + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = $b; + $this->analyse([__DIR__ . '/data/php-82-dynamic-properties-allow.php'], $errors); + } + + public function testBug2435(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-2435.php'], []); + } + + public function testBug7640(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-7640.php'], []); + } + + public function testBug3572(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-3572.php'], []); + } + + public function testBug393(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-393.php'], []); + } + + public function testObjectShapes(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + $this->analyse([__DIR__ . '/data/properties-object-shapes.php'], [ + [ + 'Access to an undefined property object{foo: int, bar?: string}::$bar.', + 15, + $tipText, + ], + [ + 'Access to an undefined property object{foo: int, bar?: string}::$baz.', + 16, + $tipText, + ], + ]); + } + + public function testConflictingAnnotationProperty(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], []); + } + + public function testBug8536(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/../Comparison/data/bug-8536.php'], []); + } + + public function testRequireExtends(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + $this->analyse([__DIR__ . '/data/require-extends.php'], [ + [ + 'Access to an undefined property RequireExtends\MyInterface::$bar.', + 36, + $tipText, + ], + ]); + } + + public function testBug8629(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-8629.php'], []); + } + + public function testBug9694(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-9694.php'], []); + } + + public function testTraitMixin(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/trait-mixin.php'], []); + } + + public function testAsymmetricVisibility(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/read-asymmetric-visibility.php'], []); + } + + public function testNewIsAlwaysFinalClass(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2.'); + } + + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/null-coalesce-new-is-always-final.php'], [ + [ + 'Access to an undefined property NullCoalesceIsAlwaysFinal\Foo::$bar.', + 12, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + ]); + } + + public function testPropertyExists(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/property-exists.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php index 3a9a495b7a..355cf0dfa2 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php @@ -3,10 +3,11 @@ namespace PHPStan\Rules\Properties; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -18,7 +19,17 @@ protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); return new AccessStaticPropertiesInAssignRule( - new AccessStaticPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), new ClassCaseSensitivityCheck($reflectionProvider, true)), + new AccessStaticPropertiesRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + ), ); } @@ -38,9 +49,6 @@ public function testRule(): void 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.', diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 44fe1c71fb..bae249fd95 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -3,10 +3,11 @@ namespace PHPStan\Rules\Properties; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -19,16 +20,19 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new AccessStaticPropertiesRule( $reflectionProvider, - new RuleLevelHelper($reflectionProvider, true, false, true, false), - new ClassCaseSensitivityCheck($reflectionProvider, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ); } public function testAccessStaticProperties(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) { - $this->markTestSkipped('Test does not run on PHP 7.4 because of referencing parent:: without parent class.'); - } $this->analyse([__DIR__ . '/data/access-static-properties.php'], [ [ 'Access to an undefined static property FooAccessStaticProperties::$bar.', @@ -46,6 +50,10 @@ public function testAccessStaticProperties(): void 'Static access to instance property FooAccessStaticProperties::$loremIpsum.', 26, ], + [ + 'Static access to instance property FooAccessStaticProperties::$loremIpsum.', + 32, + ], [ 'IpsumAccessStaticProperties::ipsum() accesses parent::$lorem but IpsumAccessStaticProperties does not extend any class.', 42, @@ -59,34 +67,86 @@ public function testAccessStaticProperties(): void 47, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$baz.', + 49, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$baz.', + 52, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$baz.', 53, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$nonexistent.', + 54, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$nonexistent.', 55, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyBaz.', + 60, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyBaz.', 63, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyNonexistent.', + 64, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$emptyNonexistent.', 65, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', + 70, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', 71, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', + 71, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', + 72, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', 72, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherNonexistent.', + 73, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', 75, ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', + 75, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', + 76, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', + 77, + ], + [ + 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', + 78, + ], [ 'Access to an undefined static property static(IpsumAccessStaticProperties)::$anotherEmptyNonexistent.', 78, @@ -160,10 +220,18 @@ public function testAccessStaticProperties(): void 'Access to an undefined static property ClassOrString|string::$unknownProperty.', 141, ], + [ + 'Cannot access static property $anotherProperty on ClassOrString|false.', + 150, + ], [ 'Static access to instance property ClassOrString::$instanceProperty.', 152, ], + [ + 'Access to an undefined static property AccessInIsset::$foo.', + 178, + ], [ 'Access to an undefined static property AccessInIsset::$foo.', 185, @@ -181,14 +249,27 @@ public function testAccessStaticProperties(): void 'Access to an undefined static property static(AccessWithStatic)::$nonexistent.', 224, ], + [ + 'Access to an undefined static property DoesNotAllowDynamicProperties::$foo.', + 234, + ], + [ + 'Access to an undefined static property AllowsDynamicProperties::$foo.', + 248, + ], + [ + 'Static access to instance property ParentClassWithInstanceProperty::$i.', + 267, + ], + [ + 'Access to an undefined static property ParentClassWithInstanceProperty::$j.', + 268, + ], ]); } 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.', @@ -207,4 +288,28 @@ public function testBug5143(): void $this->analyse([__DIR__ . '/data/bug-5143.php'], []); } + public function testBug6809(): void + { + $this->analyse([__DIR__ . '/data/bug-6809.php'], [ + [ + 'Access to an undefined static property static(Bug6809\HelloWorld)::$coolClass.', + 7, + ], + ]); + } + + public function testBug8333(): void + { + $this->analyse([__DIR__ . '/data/bug-8333.php'], [ + [ + 'Access to an undefined static property static(Bug8333\BarAccessProperties)::$loremipsum.', + 68, + ], + [ + 'Access to private static property $foo of parent class Bug8333\FooAccessProperties.', + 69, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/Bug7074Test.php b/tests/PHPStan/Rules/Properties/Bug7074Test.php new file mode 100644 index 0000000000..55df196d56 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/Bug7074Test.php @@ -0,0 +1,39 @@ + + */ +class Bug7074Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-7074.php'], [ + [ + 'Property Bug7074\SomeModel2::$primaryKey (array|string) does not accept default value of type array.', + 23, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/bug-7074.neon'], + ); + } + +} diff --git a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php index 9f9e006fcb..ad1a9b43a9 100644 --- a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php @@ -14,7 +14,7 @@ class DefaultValueTypesAssignedToPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true)); } public function testDefaultValueTypesAssignedToProperties(): void @@ -37,9 +37,6 @@ public function testDefaultValueTypesAssignedToProperties(): void public function testDefaultValueForNativePropertyType(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } $this->analyse([__DIR__ . '/data/default-value-for-native-property-type.php'], [ [ 'Property DefaultValueForNativePropertyType\Foo::$foo (DateTime) does not accept default value of type null.', @@ -58,4 +55,14 @@ public function testBug5607(): void ]); } + public function testBug7933(): void + { + $this->analyse([__DIR__ . '/data/bug-7933.php'], []); + } + + public function testBug10987(): void + { + $this->analyse([__DIR__ . '/data/bug-10987.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php index 7623c72f7e..873654f585 100644 --- a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -19,14 +21,20 @@ class ExistingClassesInPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInPropertiesRule( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersion), true, false, + true, ); } @@ -48,22 +56,22 @@ public function testNonexistentClass(): void 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Property PropertiesTypes\Foo::$dolors has unknown class PropertiesTypes\Dolor as its type.', + 'Property PropertiesTypes\Foo::$dolors has unknown class PropertiesTypes\Ipsum as its type.', 21, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Property PropertiesTypes\Foo::$dolors has unknown class PropertiesTypes\Ipsum as its type.', + 'Property PropertiesTypes\Foo::$dolors has unknown class PropertiesTypes\Dolor as its type.', 21, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Property PropertiesTypes\Foo::$fooWithWrongCase has unknown class PropertiesTypes\BAR as its type.', + 'Property PropertiesTypes\Foo::$fooWithWrongCase has unknown class PropertiesTypes\Fooo as its type.', 24, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Property PropertiesTypes\Foo::$fooWithWrongCase has unknown class PropertiesTypes\Fooo as its type.', + 'Property PropertiesTypes\Foo::$fooWithWrongCase has unknown class PropertiesTypes\BAR as its type.', 24, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], @@ -95,10 +103,6 @@ public function testNonexistentClass(): void public function testNativeTypes(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/properties-native-types.php'], [ [ 'Property PropertiesNativeTypes\Foo::$bar has unknown class PropertiesNativeTypes\Bar as its type.', @@ -120,10 +124,6 @@ public function testNativeTypes(): void public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/properties-promoted-types.php'], [ [ 'Property PromotedPropertiesExistingClasses\Foo::$baz has invalid type PromotedPropertiesExistingClasses\SomeTrait.', @@ -168,14 +168,10 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $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/ExistingClassesInPropertyHookTypehintsRuleTest.php b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertyHookTypehintsRuleTest.php new file mode 100644 index 0000000000..ddb533c25f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertyHookTypehintsRuleTest.php @@ -0,0 +1,67 @@ + + */ +class ExistingClassesInPropertyHookTypehintsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInPropertyHookTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new PhpVersion(PHP_VERSION_ID), + true, + false, + ), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/existing-classes-property-hooks.php'], [ + [ + 'Parameter $v of set hook for property ExistingClassesPropertyHooks\Foo::$i has invalid type ExistingClassesPropertyHooks\Nonexistent.', + 9, + ], + [ + 'Parameter $v of set hook for property ExistingClassesPropertyHooks\Foo::$j has unresolvable native type.', + 15, + ], + [ + 'Get hook for property ExistingClassesPropertyHooks\Foo::$k has invalid return type ExistingClassesPropertyHooks\Undefined.', + 22, + ], + [ + 'Parameter $value of set hook for property ExistingClassesPropertyHooks\Foo::$l has invalid type ExistingClassesPropertyHooks\Undefined.', + 29, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/GetNonVirtualPropertyHookReadRuleTest.php b/tests/PHPStan/Rules/Properties/GetNonVirtualPropertyHookReadRuleTest.php new file mode 100644 index 0000000000..64745b60d9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/GetNonVirtualPropertyHookReadRuleTest.php @@ -0,0 +1,47 @@ + + */ +class GetNonVirtualPropertyHookReadRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new GetNonVirtualPropertyHookReadRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/get-non-virtual-property-hook-read.php'], [ + [ + 'Get hook for non-virtual property GetNonVirtualPropertyHookRead\Foo::$k does not read its value.', + 24, + ], + [ + 'Get hook for non-virtual property GetNonVirtualPropertyHookRead\Foo::$l does not read its value.', + 30, + ], + ]); + } + + public function testAbstractProperty(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/get-abstract-property-hook-read.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Properties/InvalidCallablePropertyTypeRuleTest.php b/tests/PHPStan/Rules/Properties/InvalidCallablePropertyTypeRuleTest.php new file mode 100644 index 0000000000..581a70e179 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/InvalidCallablePropertyTypeRuleTest.php @@ -0,0 +1,41 @@ + + */ +class InvalidCallablePropertyTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidCallablePropertyTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/invalid-callable-property-type.php'], [ + [ + 'Property InvalidCallablePropertyType\HelloWorld::$a cannot have callable in its type declaration.', + 9, + ], + [ + 'Property InvalidCallablePropertyType\HelloWorld::$b cannot have callable in its type declaration.', + 12, + ], + [ + 'Property InvalidCallablePropertyType\HelloWorld::$c cannot have callable in its type declaration.', + 15, + ], + [ + 'Property InvalidCallablePropertyType\HelloWorld::$callback cannot have callable in its type declaration.', + 23, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php index ebea655346..e17100bc38 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,8 +14,7 @@ class MissingPropertyTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingPropertyTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); + return new MissingPropertyTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -37,21 +35,28 @@ public function testRule(): void [ 'Property MissingPropertyTypehint\ChildClass::$unionProp type has no value type specified in iterable type array.', 32, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Property MissingPropertyTypehint\Bar::$foo with generic interface MissingPropertyTypehint\GenericInterface does not specify its types: T, U', - 74, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + 77, ], [ 'Property MissingPropertyTypehint\Bar::$baz with generic class MissingPropertyTypehint\GenericClass does not specify its types: A, B', - 80, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + 83, ], [ 'Property MissingPropertyTypehint\CallableSignature::$cb type has no signature specified for callable.', - 93, + 96, + ], + [ + 'Property MissingPropertyTypehint\NestedArrayInProperty::$args type has no value type specified in iterable type array.', + 106, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Property MissingPropertyTypehint\Baz::$bar with generic class MissingPropertyTypehint\GenericClassWithSomeDefaults does not specify its types: T, U (1-2 required)', + 134, ], ]); } @@ -63,20 +68,7 @@ public function testBug3402(): void public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/promoted-properties-missing-typehint.php'], [ - [ - 'Property PromotedPropertiesMissingTypehint\Foo::$lorem has no type specified.', - 15, - ], - [ - 'Property PromotedPropertiesMissingTypehint\Foo::$ipsum type has no value type specified in iterable type array.', - 16, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, - ], - ]); + $this->analyse([__DIR__ . '/data/promoted-properties-missing-typehint.php'], []); } } diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php new file mode 100644 index 0000000000..a72e7d23f2 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -0,0 +1,155 @@ + + */ +class MissingReadOnlyByPhpDocPropertyAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingReadOnlyByPhpDocPropertyAssignRule( + new ConstructorsHelper( + self::getContainer(), + [ + 'MissingReadOnlyPropertyAssignPhpDoc\\TestCase::setUp', + ], + ), + ); + } + + protected function getReadWritePropertiesExtensions(): array + { + return [ + new class() implements ReadWritePropertiesExtension { + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + private function isEntityId(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssignPhpDoc\\Entity' + && in_array($propertyName, ['id'], true); + } + + }, + ]; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/missing-readonly-property-assign-phpdoc.php'], [ + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\Foo has an uninitialized @readonly property $unassigned. Assign it in the constructor.', + 16, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\Foo has an uninitialized @readonly property $unassigned2. Assign it in the constructor.', + 19, + ], + [ + 'Access to an uninitialized @readonly property MissingReadOnlyPropertyAssignPhpDoc\Foo::$readBeforeAssigned.', + 36, + ], + [ + '@readonly property MissingReadOnlyPropertyAssignPhpDoc\Foo::$doubleAssigned is already assigned.', + 40, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\BarDoubleAssignInSetter has an uninitialized @readonly property $foo. Assign it in the constructor.', + 57, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\AssignOp has an uninitialized @readonly property $foo. Assign it in the constructor.', + 85, + ], + [ + 'Access to an uninitialized @readonly property MissingReadOnlyPropertyAssignPhpDoc\AssignOp::$foo.', + 92, + ], + [ + 'Access to an uninitialized @readonly property MissingReadOnlyPropertyAssignPhpDoc\AssignOp::$bar.', + 94, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\Immutable has an uninitialized @readonly property $unassigned. Assign it in the constructor.', + 119, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\Immutable has an uninitialized @readonly property $unassigned2. Assign it in the constructor.', + 121, + ], + [ + 'Access to an uninitialized @readonly property MissingReadOnlyPropertyAssignPhpDoc\Immutable::$readBeforeAssigned.', + 131, + ], + [ + '@readonly property MissingReadOnlyPropertyAssignPhpDoc\Immutable::$doubleAssigned is already assigned.', + 135, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\FooTraitClass has an uninitialized @readonly property $unassigned. Assign it in the constructor.', + 156, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\FooTraitClass has an uninitialized @readonly property $unassigned2. Assign it in the constructor.', + 159, + ], + [ + 'Access to an uninitialized @readonly property MissingReadOnlyPropertyAssignPhpDoc\FooTraitClass::$readBeforeAssigned.', + 188, + ], + [ + '@readonly property MissingReadOnlyPropertyAssignPhpDoc\FooTraitClass::$doubleAssigned is already assigned.', + 192, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\A has an uninitialized @readonly property $a. Assign it in the constructor.', + 233, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\B has an uninitialized @readonly property $b. Assign it in the constructor.', + 240, + ], + [ + 'Access to an uninitialized @readonly property MissingReadOnlyPropertyAssignPhpDoc\B::$b.', + 244, + ], + [ + '@readonly property MissingReadOnlyPropertyAssignPhpDoc\C::$c is already assigned.', + 257, + ], + ]); + } + + public function testRuleIgnoresNativeReadonly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/missing-readonly-property-assign-phpdoc-and-native.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php index 71e1c09473..f389c3f628 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -2,8 +2,12 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Reflection\ConstructorsHelper; +use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function in_array; +use function strpos; use const PHP_VERSION_ID; /** @@ -12,11 +16,75 @@ class MissingReadOnlyPropertyAssignRuleTest extends RuleTestCase { + private bool $shouldNarrowMethodScopeFromConstructor = false; + protected function getRule(): Rule { - return new MissingReadOnlyPropertyAssignRule([ - 'MissingReadOnlyPropertyAssign\\TestCase::setUp', - ]); + return new MissingReadOnlyPropertyAssignRule( + new ConstructorsHelper( + self::getContainer(), + [ + 'MissingReadOnlyPropertyAssign\\TestCase::setUp', + 'Bug10523\\Controller::init', + 'Bug10523\\MultipleWrites::init', + 'Bug10523\\SingleWriteInConstructorCalledMethod::init', + ], + ), + ); + } + + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return $this->shouldNarrowMethodScopeFromConstructor; + } + + protected function getReadWritePropertiesExtensions(): array + { + return [ + new class() implements ReadWritePropertiesExtension { + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + private function isEntityId(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssign\\Entity' + && in_array($propertyName, ['id'], true); + } + + }, + new class() implements ReadWritePropertiesExtension { + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isInitialized($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->isPublic() && + strpos($property->getDocComment() ?? '', '@init') !== false; + } + + }, + ]; } public function testRule(): void @@ -46,6 +114,10 @@ public function testRule(): void 'Class MissingReadOnlyPropertyAssign\BarDoubleAssignInSetter has an uninitialized readonly property $foo. Assign it in the constructor.', 53, ], + [ + 'Class MissingReadOnlyPropertyAssign\AssignOp has an uninitialized readonly property $foo. Assign it in the constructor.', + 79, + ], [ 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\AssignOp::$foo.', 85, @@ -54,7 +126,289 @@ public function testRule(): void 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\AssignOp::$bar.', 87, ], + [ + 'Class MissingReadOnlyPropertyAssign\FooTraitClass has an uninitialized readonly property $unassigned. Assign it in the constructor.', + 114, + ], + [ + 'Class MissingReadOnlyPropertyAssign\FooTraitClass has an uninitialized readonly property $unassigned2. Assign it in the constructor.', + 116, + ], + [ + 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\FooTraitClass::$readBeforeAssigned.', + 145, + ], + [ + 'Readonly property MissingReadOnlyPropertyAssign\FooTraitClass::$doubleAssigned is already assigned.', + 149, + ], + [ + 'Readonly property MissingReadOnlyPropertyAssign\AdditionalAssignOfReadonlyPromotedProperty::$x is already assigned.', + 188, + ], + [ + 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\MethodCalledFromConstructorBeforeAssign::$foo.', + 226, + ], + [ + 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\MethodCalledTwice::$foo.', + 244, + ], + [ + 'Class MissingReadOnlyPropertyAssign\PropertyAssignedOnDifferentObjectUninitialized has an uninitialized readonly property $foo. Assign it in the constructor.', + 264, + ], + ]); + } + + public function testBug7119(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-7119.php'], []); + } + + public function testBug7314(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-7314.php'], []); + } + + public function testBug8412(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8412.php'], []); + } + + public function testBug8958(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8958.php'], []); + } + + public function testBug8563(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8563.php'], []); + } + + public function testBug6402(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6402.php'], [ + [ + 'Access to an uninitialized readonly property Bug6402\SomeModel2::$views.', + 28, + ], + ]); + } + + public function testBug7198(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-7198.php'], []); + } + + public function testBug7649(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-7649.php'], [ + [ + 'Class Bug7649\Foo has an uninitialized readonly property $bar. Assign it in the constructor.', + 7, + ], + ]); + } + + public function testBug9577(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/../Classes/data/bug-9577.php'], [ + [ + 'Class Bug9577\SpecializedException2 has an uninitialized readonly property $message. Assign it in the constructor.', + 8, + ], + ]); + } + + public function testAnonymousReadonlyClass(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/missing-readonly-anonymous-class-property-assign.php'], [ + [ + 'Class class@anonymous/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php:10 has an uninitialized readonly property $foo. Assign it in the constructor.', + 11, + ], + ]); + } + + public function testBug10523(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-10523.php'], [ + [ + 'Readonly property Bug10523\MultipleWrites::$userAccount is already assigned.', + 55, + ], + ]); + } + + public function testBug10822(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-10822.php'], []); + } + + public function testRedeclaredReadonlyProperties(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/redeclare-readonly-property.php'], [ + [ + 'Readonly property RedeclareReadonlyProperty\B1::$myProp is already assigned.', + 16, + ], + [ + 'Readonly property RedeclareReadonlyProperty\B5::$myProp is already assigned.', + 50, + ], + [ + 'Readonly property RedeclareReadonlyProperty\B7::$myProp is already assigned.', + 70, + ], + [ + 'Readonly property RedeclareReadonlyProperty\A@anonymous/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php:117::$myProp is already assigned.', + 121, + ], + [ + 'Class RedeclareReadonlyProperty\B16 has an uninitialized readonly property $myProp. Assign it in the constructor.', + 195, + ], + [ + 'Class RedeclareReadonlyProperty\C17 has an uninitialized readonly property $aProp. Assign it in the constructor.', + 218, + ], + [ + 'Class RedeclareReadonlyProperty\B18 has an uninitialized readonly property $aProp. Assign it in the constructor.', + 233, + ], + ]); + } + + public function testRedeclaredPropertiesOfReadonlyClass(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2.'); + } + + $this->analyse([__DIR__ . '/data/redeclare-property-of-readonly-class.php'], [ + [ + 'Readonly property RedeclarePropertyOfReadonlyClass\B1::$promotedProp is already assigned.', + 15, + ], + ]); + } + + public function testBug8101(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8101.php'], [ + [ + 'Readonly property Bug8101\B::$myProp is already assigned.', + 12, + ], + ]); + } + + public function testBug9863(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9863.php'], [ + [ + 'Readonly property Bug9863\ReadonlyChildWithoutIsset::$foo is already assigned.', + 17, + ], + [ + 'Class Bug9863\ReadonlyParentWithIsset has an uninitialized readonly property $foo. Assign it in the constructor.', + 23, + ], + [ + 'Access to an uninitialized readonly property Bug9863\ReadonlyParentWithIsset::$foo.', + 28, + ], ]); } + public function testBug10048(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->shouldNarrowMethodScopeFromConstructor = true; + $this->analyse([__DIR__ . '/data/bug-10048.php'], []); + } + + public function testBug11828(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->shouldNarrowMethodScopeFromConstructor = true; + $this->analyse([__DIR__ . '/data/bug-11828.php'], []); + } + + public function testBug9864(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9864.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php index febeff7d62..07300834bf 100644 --- a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php +++ b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php @@ -19,10 +19,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/nullsafe-property-fetch-rule.php'], [ [ 'Using nullsafe property access on non-nullable type Exception. Use -> instead.', @@ -33,11 +29,57 @@ public function testRule(): void public function testBug6020(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->analyse([__DIR__ . '/data/bug-6020.php'], []); + } + + public function testBug7109(): void + { + if (PHP_VERSION_ID < 80000) { $this->markTestSkipped('Test requires PHP 8.0.'); } - $this->analyse([__DIR__ . '/data/bug-6020.php'], []); + $this->analyse([__DIR__ . '/data/bug-7109.php'], []); + } + + public function testBug5172(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5172.php'], []); + } + + public function testBug7980(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/../../Analyser/data/bug-7980.php'], []); + } + + public function testBug8517(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8517.php'], []); + } + + public function testBug9105(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9105.php'], []); + } + + public function testBug6922(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6922.php'], []); } } diff --git a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php index ab31ff4b23..c5f5cd929c 100644 --- a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php @@ -2,9 +2,11 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function sprintf; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -16,15 +18,15 @@ class OverridingPropertyRuleTest extends RuleTestCase protected function getRule(): Rule { - return new OverridingPropertyRule(true, $this->reportMaybes); + return new OverridingPropertyRule( + self::getContainer()->getByType(PhpVersion::class), + 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'], [ [ @@ -155,7 +157,7 @@ public function dataRulePHPDocTypes(): array /** * @dataProvider dataRulePHPDocTypes - * @param mixed[] $errors + * @param list $errors */ public function testRulePHPDocTypes(bool $reportMaybes, array $errors): void { @@ -163,4 +165,119 @@ public function testRulePHPDocTypes(bool $reportMaybes, array $errors): void $this->analyse([__DIR__ . '/data/overriding-property-phpdoc.php'], $errors); } + public function testBug7839(): void + { + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/bug-7839.php'], []); + } + + public function testBug7692(): void + { + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/bug-7692.php'], []); + } + + public function testFinal(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/overriding-final-property.php'], [ + [ + 'Property OverridingFinalProperty\Bar::$a overrides final property OverridingFinalProperty\Foo::$a.', + 21, + ], + [ + 'Property OverridingFinalProperty\Bar::$b overrides final property OverridingFinalProperty\Foo::$b.', + 23, + ], + [ + 'Property OverridingFinalProperty\Bar::$c overrides final property OverridingFinalProperty\Foo::$c.', + 25, + ], + [ + 'Property OverridingFinalProperty\Bar::$d overrides final property OverridingFinalProperty\Foo::$d.', + 27, + ], + ]); + } + + public function testPropertyPrototypeFromInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/property-prototype-from-interface.php'], [ + [ + 'Type string of property Bug12466\Bar::$a is not the same as type int of overridden property Bug12466\Foo::$a.', + 15, + ], + [ + 'Property Bug12466\TestMoreProps::$a overriding writable property Bug12466\MoreProps::$a also has to be writable.', + 34, + ], + [ + 'Property Bug12466\TestMoreProps::$b overriding readable property Bug12466\MoreProps::$b also has to be readable.', + 41, + ], + [ + 'Property Bug12466\TestMoreProps::$c overriding writable property Bug12466\MoreProps::$c also has to be writable.', + 48, + ], + ]); + } + + public function testBug12466(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $tip = sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ); + + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/bug-12466.php'], [ + [ + 'Type int|string|null of property Bug12466OverridenProperty\Baz::$onlyGet is not covariant with type int|string of overridden property Bug12466OverridenProperty\Foo::$onlyGet.', + 34, + ], + [ + 'Type int of property Bug12466OverridenProperty\Baz::$onlySet is not contravariant with type int|string of overridden property Bug12466OverridenProperty\Foo::$onlySet.', + 40, + ], + [ + 'PHPDoc type array of property Bug12466OverridenProperty\BazWithPhpDocs::$onlyGet is not covariant with PHPDoc type array of overridden property Bug12466OverridenProperty\FooWithPhpDocs::$onlyGet.', + 82, + $tip, + ], + [ + 'PHPDoc type array of property Bug12466OverridenProperty\BazWithPhpDocs::$onlySet is not contravariant with PHPDoc type array of overridden property Bug12466OverridenProperty\FooWithPhpDocs::$onlySet.', + 89, + $tip, + ], + ]); + } + + public function testBug12586(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/bug-12586.php'], [ + [ + 'Readonly property Bug12586\FooImpl::$baz overrides readwrite property Bug12586\Foo::$baz.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php new file mode 100644 index 0000000000..5cfb5e7b58 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php @@ -0,0 +1,217 @@ + + */ +class PropertiesInInterfaceRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertiesInInterfaceRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testPhp83AndPropertiesInInterface(): void + { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/properties-in-interface.php'], [ + [ + 'Interfaces can include properties only on PHP 8.4 and later.', + 7, + ], + [ + 'Interfaces can include properties only on PHP 8.4 and later.', + 9, + ], + [ + 'Interfaces can include properties only on PHP 8.4 and later.', + 11, + ], + [ + 'Interfaces can include properties only on PHP 8.4 and later.', + 13, + ], + ]); + } + + public function testPhp83AndPropertyHooksInInterface(): void + { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-in-interface.php'], [ + [ + 'Interfaces can include properties only on PHP 8.4 and later.', + 7, + ], + [ + 'Interfaces can include properties only on PHP 8.4 and later.', + 9, + ], + ]); + } + + public function testPhp84AndPropertiesInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/properties-in-interface.php'], [ + [ + 'Interfaces can only include hooked properties.', + 9, + ], + [ + 'Interfaces can only include hooked properties.', + 11, + ], + [ + 'Interfaces can only include hooked properties.', + 13, + ], + ]); + } + + public function testPhp84AndNonPublicPropertyHooksInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-visibility-in-interface.php'], [ + [ + 'Interfaces cannot include non-public properties.', + 7, + ], + [ + 'Interfaces cannot include non-public properties.', + 9, + ], + ]); + } + + public function testPhp84AndPropertyHooksWithBodiesInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-bodies-in-interface.php'], [ + [ + 'Interfaces cannot include property hooks with bodies.', + 7, + ], + [ + 'Interfaces cannot include property hooks with bodies.', + 13, + ], + ]); + } + + public function testPhp84AndReadonlyPropertyHooksInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/readonly-property-hooks-in-interface.php'], [ + [ + 'Interfaces cannot include readonly hooked properties.', + 7, + ], + [ + 'Interfaces cannot include readonly hooked properties.', + 9, + ], + [ + 'Interfaces cannot include readonly hooked properties.', + 11, + ], + ]); + } + + public function testPhp84AndFinalPropertyHooksInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/final-property-hooks-in-interface.php'], [ + [ + 'Interfaces cannot include final properties.', + 7, + ], + [ + 'Interfaces cannot include final properties.', + 9, + ], + [ + 'Interfaces cannot include final properties.', + 11, + ], + [ + 'Property hook cannot be both abstract and final.', + 13, + ], + [ + 'Property hook cannot be both abstract and final.', + 17, + ], + ]); + } + + public function testPhp84AndExplicitAbstractProperty(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/property-in-interface-explicit-abstract.php'], [ + [ + 'Property in interface cannot be explicitly abstract.', + 8, + ], + ]); + } + + public function testPhp84AndStaticHookedPropertyInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/static-hooked-property-in-interface.php'], [ + [ + 'Hooked properties cannot be static.', + 7, + ], + [ + 'Hooked properties cannot be static.', + 9, + ], + [ + 'Hooked properties cannot be static.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertyAssignRefRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAssignRefRuleTest.php new file mode 100644 index 0000000000..3d78abbee9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyAssignRefRuleTest.php @@ -0,0 +1,69 @@ + + */ +class PropertyAssignRefRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertyAssignRefRule(new PhpVersion(PHP_VERSION_ID), new PropertyReflectionFinder()); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/property-assign-ref.php'], [ + [ + 'Property PropertyAssignRef\Foo::$foo with private visibility is assigned by reference.', + 25, + ], + [ + 'Property PropertyAssignRef\Foo::$bar with protected(set) visibility is assigned by reference.', + 26, + ], + [ + 'Property PropertyAssignRef\Baz::$a with protected visibility is assigned by reference.', + 41, + ], + [ + 'Property PropertyAssignRef\Baz::$b with private visibility is assigned by reference.', + 42, + ], + ]); + } + + public function testAsymmetricVisibility(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/property-assign-ref-asymmetric.php'], [ + [ + 'Property PropertyAssignRefAsymmetric\Foo::$a with private(set) visibility is assigned by reference.', + 28, + ], + [ + 'Property PropertyAssignRefAsymmetric\Foo::$a with private(set) visibility is assigned by reference.', + 36, + ], + [ + 'Property PropertyAssignRefAsymmetric\Foo::$b with protected(set) visibility is assigned by reference.', + 37, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index 104ecb16ee..00874ab151 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -2,16 +2,16 @@ namespace PHPStan\Rules\Properties; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -26,9 +26,8 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -36,17 +35,19 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, ), ); } public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->analyse([__DIR__ . '/data/property-attributes.php'], [ [ 'Attribute class PropertyAttributes\Foo does not have the property target.', @@ -55,4 +56,18 @@ public function testRule(): void ]); } + public function testDeprecatedAttribute(): void + { + $this->analyse([__DIR__ . '/data/property-attributes-deprecated.php'], [ + [ + 'Attribute class DeprecatedPropertyAttribute\DoSomethingTheOldWay is deprecated.', + 16, + ], + [ + 'Attribute class DeprecatedPropertyAttribute\DoSomethingTheOldWayWithDescription is deprecated: Use something else please', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php new file mode 100644 index 0000000000..7a0e80e653 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -0,0 +1,64 @@ + + */ +class PropertyHookAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new PropertyHookAttributesRule( + new AttributesCheck( + $reflectionProvider, + new FunctionCallParametersCheck( + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + new NullsafeCheck(), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, + true, + true, + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + ), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/property-hook-attributes.php'], [ + [ + 'Attribute class PropertyHookAttributes\Foo does not have the method target.', + 27, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php new file mode 100644 index 0000000000..e0b65f8bd1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php @@ -0,0 +1,327 @@ + + */ +class PropertyInClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertyInClassRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testPhpLessThan84AndHookedPropertiesInClass(): void + { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/hooked-properties-in-class.php'], [ + [ + 'Property hooks are supported only on PHP 8.4 and later.', + 7, + ], + ]); + } + + public function testPhp84AndHookedPropertiesWithoutBodiesInClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/hooked-properties-without-bodies-in-class.php'], [ + [ + 'Non-abstract properties cannot include hooks without bodies.', + 7, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 9, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 15, + ], + ]); + } + + public function testPhp84AndNonAbstractHookedPropertiesInClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/non-abstract-hooked-properties-in-class.php'], [ + [ + 'Non-abstract properties cannot include hooks without bodies.', + 7, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 9, + ], + ]); + } + + public function testPhp84AndAbstractHookedPropertiesInClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-hooked-properties-in-class.php'], [ + [ + 'Non-abstract classes cannot include abstract properties.', + 7, + ], + [ + 'Non-abstract classes cannot include abstract properties.', + 9, + ], + ]); + } + + public function testPhp84AndNonAbstractHookedPropertiesInAbstractClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/non-abstract-hooked-properties-in-abstract-class.php'], [ + [ + 'Non-abstract properties cannot include hooks without bodies.', + 7, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 9, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 25, + ], + ]); + } + + public function testPhp84AndAbstractNonHookedPropertiesInAbstractClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-non-hooked-properties-in-abstract-class.php'], [ + [ + 'Only hooked properties can be declared abstract.', + 7, + ], + [ + 'Only hooked properties can be declared abstract.', + 9, + ], + ]); + } + + public function testPhp84AndAbstractHookedPropertiesWithBodies(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-hooked-properties-with-bodies.php'], [ + [ + 'Abstract properties must specify at least one abstract hook.', + 7, + ], + [ + 'Abstract properties must specify at least one abstract hook.', + 12, + ], + ]); + } + + public function testPhp84AndReadonlyHookedProperties(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/readonly-property-hooks.php'], [ + [ + 'Hooked properties cannot be readonly.', + 7, + ], + [ + 'Hooked properties cannot be readonly.', + 12, + ], + [ + 'Hooked properties cannot be readonly.', + 14, + ], + [ + 'Hooked properties cannot be readonly.', + 19, + ], + [ + 'Hooked properties cannot be readonly.', + 24, + ], + ]); + } + + public function testPhp84AndVirtualHookedProperties(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/virtual-hooked-properties.php'], [ + [ + 'Virtual hooked properties cannot have a default value.', + 17, + ], + ]); + } + + public function testPhp84AndStaticHookedProperties(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/static-hooked-properties.php'], [ + [ + 'Hooked properties cannot be static.', + 7, + ], + [ + 'Hooked properties cannot be static.', + 15, + ], + ]); + } + + public function testPhp84AndPrivateFinalHookedProperties(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/private-final-property-hooks.php'], [ + [ + 'Property cannot be both final and private.', + 7, + ], + [ + 'Private property cannot have a final hook.', + 11, + ], + ]); + } + + public function testPhp84AndAbstractFinalHookedProperties(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-final-property-hook.php'], [ + [ + 'Property cannot be both abstract and final.', + 7, + ], + ]); + } + + public function testPhp84AndAbstractPrivateHookedProperties(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-private-property-hook.php'], [ + [ + 'Property cannot be both abstract and private.', + 7, + ], + ]); + } + + public function testPhp84AndAbstractFinalHookedPropertiesParseError(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + // errors when parsing with php-parser, see https://github.com/nikic/PHP-Parser/issues/1071 + $this->analyse([__DIR__ . '/data/abstract-final-property-hook-parse-error.php'], [ + [ + 'Cannot use the final modifier on an abstract class member on line 7', + 7, + ], + ]); + } + + public function testPhp84FinalProperties(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/final-properties.php'], [ + [ + 'Property cannot be both final and private.', + 7, + ], + ]); + } + + public function testBeforePhp84FinalProperties(): void + { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + + $this->analyse([__DIR__ . '/data/final-properties.php'], [ + [ + 'Final properties are supported only on PHP 8.4 and later.', + 7, + ], + [ + 'Final properties are supported only on PHP 8.4 and later.', + 8, + ], + [ + 'Final properties are supported only on PHP 8.4 and later.', + 9, + ], + ]); + } + + public function testPhp84FinalPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/final-property-hooks.php'], [ + [ + 'Cannot use the final modifier on an abstract class member on line 19', + 19, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php new file mode 100644 index 0000000000..d000d4f3f3 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php @@ -0,0 +1,71 @@ + + */ +class ReadOnlyByPhpDocPropertyAssignRefRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyByPhpDocPropertyAssignRefRule(new PropertyReflectionFinder()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/readonly-assign-ref-phpdoc.php'], [ + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\Foo::$foo is assigned by reference.', + 22, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\Foo::$bar is assigned by reference.', + 23, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\Foo::$bar is assigned by reference.', + 34, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\Immutable::$foo is assigned by reference.', + 51, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\Immutable::$bar is assigned by reference.', + 52, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\A::$a is assigned by reference.', + 66, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\B::$b is assigned by reference.', + 79, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\A::$a is assigned by reference.', + 80, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\C::$c is assigned by reference.', + 93, + ], + ]); + } + + public function testRuleIgnoresNativeReadonly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/readonly-assign-ref-phpdoc-and-native.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php new file mode 100644 index 0000000000..0aecf4c09c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -0,0 +1,203 @@ + + */ +class ReadOnlyByPhpDocPropertyAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyByPhpDocPropertyAssignRule( + new PropertyReflectionFinder(), + new ConstructorsHelper( + self::getContainer(), + [ + 'ReadonlyPropertyAssignPhpDoc\\TestCase::setUp', + ], + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/readonly-assign-phpdoc.php'], [ + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$foo is assigned outside of the constructor.', + 47, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$phan is assigned outside of the constructor.', + 49, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$bar is assigned outside of its declaring class.', + 61, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', + 62, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$psalm is assigned outside of its declaring class.', + 63, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$phan is assigned outside of its declaring class.', + 64, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$bar is assigned outside of its declaring class.', + 69, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$psalm is assigned outside of its declaring class.', + 70, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$phan is assigned outside of its declaring class.', + 71, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', + 78, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\FooArrays::$details is assigned outside of the constructor.', + 97, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\FooArrays::$details is assigned outside of the constructor.', + 98, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\NotThis::$foo is not assigned on $this.', + 128, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\PostInc::$foo is assigned outside of the constructor.', + 144, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\PostInc::$foo is assigned outside of the constructor.', + 145, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\PostInc::$foo is assigned outside of the constructor.', + 147, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\ListAssign::$foo is assigned outside of the constructor.', + 168, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\ListAssign::$foo is assigned outside of the constructor.', + 173, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', + 183, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', + 184, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Immutable::$foo is assigned outside of the constructor.', + 247, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\B::$b is assigned outside of the constructor.', + 279, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\A::$a is assigned outside of its declaring class.', + 280, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\C::$c is assigned outside of the constructor.', + 293, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\ArrayAccessPropertyFetch::$storage is assigned outside of the constructor.', + 311, + ], + ]); + } + + public function testRuleIgnoresNativeReadonly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/readonly-assign-phpdoc-and-native.php'], []); + } + + public function testBug7361(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-7361.php'], [ + [ + '@readonly property Bug7361\Example::$foo is assigned outside of the constructor.', + 12, + ], + ]); + } + + public function testFeature7648(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/feature-7648.php'], []); + } + + public function testFeature11775(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/feature-11775.php'], [ + [ + '@readonly property Feature11775\FooImmutable::$i is assigned outside of the constructor.', + 22, + ], + [ + '@readonly property Feature11775\FooReadonly::$i is assigned outside of the constructor.', + 43, + ], + ]); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-readonly-by-phpdoc-assign.php'], [ + [ + '@readonly property PropertyHooksReadonlyByPhpDocAssign\Foo::$i is assigned outside of the constructor.', + 15, + ], + [ + '@readonly property PropertyHooksReadonlyByPhpDocAssign\Foo::$j is assigned outside of the constructor.', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php new file mode 100644 index 0000000000..c2dce590b6 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php @@ -0,0 +1,69 @@ + + */ +class ReadOnlyByPhpDocPropertyRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyByPhpDocPropertyRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/read-only-property-phpdoc.php'], [ + [ + '@readonly property cannot have a default value.', + 21, + ], + [ + '@readonly property cannot have a default value.', + 39, + ], + [ + '@readonly property cannot have a default value.', + 46, + ], + [ + '@readonly property cannot have a default value.', + 53, + ], + ]); + } + + public function testRuleIgnoresNativeReadonly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/read-only-property-phpdoc-and-native.php'], []); + } + + public function testRuleAllowedPrivateMutation(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/read-only-property-phpdoc-allowed-private-mutation.php'], [ + [ + '@readonly property cannot have a default value.', + 9, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php index c0dae45d97..e8acef73f8 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php @@ -23,7 +23,7 @@ public function testRule(): void $this->markTestSkipped('Test requires PHP 8.1.'); } - $this->analyse([__DIR__ . '/data/readonly-assign-ref.php'], [ + $errors = [ [ 'Readonly property ReadOnlyPropertyAssignRef\Foo::$foo is assigned by reference.', 14, @@ -32,11 +32,17 @@ public function testRule(): void 'Readonly property ReadOnlyPropertyAssignRef\Foo::$bar is assigned by reference.', 15, ], - [ + ]; + + if (PHP_VERSION_ID < 80400) { + // reported by PropertyAssignRefRule on 8.4+ + $errors[] = [ 'Readonly property ReadOnlyPropertyAssignRef\Foo::$bar is assigned by reference.', 26, - ], - ]); + ]; + } + + $this->analyse([__DIR__ . '/data/readonly-assign-ref.php'], $errors); } } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index b1efad947d..d54ae3a02f 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -2,8 +2,10 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function array_merge; use const PHP_VERSION_ID; /** @@ -14,7 +16,15 @@ class ReadOnlyPropertyAssignRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ReadOnlyPropertyAssignRule(new PropertyReflectionFinder()); + return new ReadOnlyPropertyAssignRule( + new PropertyReflectionFinder(), + new ConstructorsHelper( + self::getContainer(), + [ + 'ReadonlyPropertyAssign\\TestCase::setUp', + ], + ), + ); } public function testRule(): void @@ -23,7 +33,7 @@ public function testRule(): void self::markTestSkipped('Test requires PHP 8.1'); } - $this->analyse([__DIR__ . '/data/readonly-assign.php'], [ + $errors = [ [ 'Readonly property ReadonlyPropertyAssign\Foo::$foo is assigned outside of the constructor.', 21, @@ -40,10 +50,17 @@ public function testRule(): void 'Readonly property ReadonlyPropertyAssign\Foo::$bar is assigned outside of its declaring class.', 39, ], - [ + ]; + + if (PHP_VERSION_ID < 80400) { + // reported by AccessPropertiesInAssignRule on 8.4+ + $errors[] = [ 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', 46, - ], + ]; + } + + $errors = array_merge($errors, [ [ 'Readonly property ReadonlyPropertyAssign\FooArrays::$details is assigned outside of the constructor.', 64, @@ -76,7 +93,7 @@ public function testRule(): void 'Readonly property ReadonlyPropertyAssign\ListAssign::$foo is assigned outside of the constructor.', 127, ], - [ + /*[ 'Readonly property ReadonlyPropertyAssign\FooEnum::$name is assigned outside of the constructor.', 140, ], @@ -91,16 +108,87 @@ public function testRule(): void [ 'Readonly property ReadonlyPropertyAssign\FooEnum::$value is assigned outside of its declaring class.', 152, - ], - [ + ],*/ + ]); + + if (PHP_VERSION_ID < 80400) { + // reported by AccessPropertiesInAssignRule on 8.4+ + $errors[] = [ 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', 162, - ], - [ + ]; + $errors[] = [ 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', 163, + ]; + } + + $errors[] = [ + 'Readonly property ReadonlyPropertyAssign\ArrayAccessPropertyFetch::$storage is assigned outside of the constructor.', + 212, + ]; + + $this->analyse([__DIR__ . '/data/readonly-assign.php'], $errors); + } + + public function testFeature7648(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/feature-7648.php'], [ + [ + 'Readonly property Feature7648\Request::$offset is assigned outside of the constructor.', + 23, ], ]); } + public function testReadOnlyClasses(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/readonly-class-assign.php'], [ + [ + 'Readonly property ReadonlyClassPropertyAssign\Foo::$foo is assigned outside of the constructor.', + 21, + ], + ]); + } + + public function testBug6773(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6773.php'], [ + [ + 'Readonly property Bug6773\Repository::$data is assigned outside of the constructor.', + 16, + ], + ]); + } + + public function testBug8929(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8929.php'], []); + } + + public function testBug12537(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-12537.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php index 99dfd8b766..30f9606744 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php @@ -49,6 +49,14 @@ public function dataRule(): array 'Readonly properties are supported only on PHP 8.1 and later.', 16, ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 23, + ], + [ + 'Readonly property cannot be static.', + 23, + ], ], ], [ @@ -62,6 +70,10 @@ public function dataRule(): array 'Readonly property cannot have a default value.', 10, ], + [ + 'Readonly property cannot be static.', + 23, + ], ], ], ]; @@ -69,16 +81,22 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param mixed[] $errors + * @param list $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); } + /** + * @dataProvider dataRule + * @param list $errors + */ + public function testRuleReadonlyClass(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/read-only-property-readonly-class.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php index 946fa2a776..83c919af1f 100644 --- a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php @@ -17,7 +17,7 @@ class ReadingWriteOnlyPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ReadingWriteOnlyPropertiesRule(new PropertyDescriptor(), new PropertyReflectionFinder(), new RuleLevelHelper($this->createReflectionProvider(), true, $this->checkThisOnly, true, false), $this->checkThisOnly); + return new ReadingWriteOnlyPropertiesRule(new PropertyDescriptor(), new PropertyReflectionFinder(), new RuleLevelHelper($this->createReflectionProvider(), true, $this->checkThisOnly, true, false, false, false, true), $this->checkThisOnly); } public function testPropertyMustBeReadableInAssignOp(): void @@ -26,11 +26,11 @@ public function testPropertyMustBeReadableInAssignOp(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 22, + 27, ], [ 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 32, + 40, ], ]); } @@ -41,7 +41,7 @@ public function testPropertyMustBeReadableInAssignOpCheckThisOnly(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 22, + 27, ], ]); } @@ -52,11 +52,11 @@ public function testReadingWriteOnlyProperties(): void $this->analyse([__DIR__ . '/data/reading-write-only-properties.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 17, + 23, ], [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 22, + 29, ], ]); } @@ -67,22 +67,43 @@ public function testReadingWriteOnlyPropertiesCheckThisOnly(): void $this->analyse([__DIR__ . '/data/reading-write-only-properties.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 17, + 23, ], ]); } public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/reading-write-only-properties-nullsafe.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 9, + 10, + ], + ]); + } + + public function testConflictingAnnotationProperty(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], []); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/reading-write-only-hooked-properties.php'], [ + [ + 'Property ReadingWriteOnlyHookedProperties\Foo::$i is not readable.', + 16, + ], + [ + 'Property ReadingWriteOnlyHookedProperties\Bar::$i is not readable.', + 34, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/SetNonVirtualPropertyHookAssignRuleTest.php b/tests/PHPStan/Rules/Properties/SetNonVirtualPropertyHookAssignRuleTest.php new file mode 100644 index 0000000000..bdfcaf5f27 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/SetNonVirtualPropertyHookAssignRuleTest.php @@ -0,0 +1,38 @@ + + */ +class SetNonVirtualPropertyHookAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new SetNonVirtualPropertyHookAssignRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/set-non-virtual-property-hook-assign.php'], [ + [ + 'Set hook for non-virtual property SetNonVirtualPropertyHookAssign\Foo::$k does not assign value to it.', + 24, + ], + [ + 'Set hook for non-virtual property SetNonVirtualPropertyHookAssign\Foo::$k2 does not always assign value to it.', + 34, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/SetPropertyHookParameterRuleTest.php b/tests/PHPStan/Rules/Properties/SetPropertyHookParameterRuleTest.php new file mode 100644 index 0000000000..0b879f0ad5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/SetPropertyHookParameterRuleTest.php @@ -0,0 +1,68 @@ + + */ +class SetPropertyHookParameterRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new SetPropertyHookParameterRule(new MissingTypehintCheck(true, []), true, true); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/set-property-hook-parameter.php'], [ + [ + 'Parameter $v of set hook has a native type but the property SetPropertyHookParameter\Bar::$a does not.', + 41, + ], + [ + 'Parameter $v of set hook does not have a native type but the property SetPropertyHookParameter\Bar::$b does.', + 47, + ], + [ + 'Native type string of set hook parameter $v is not contravariant with native type int of property SetPropertyHookParameter\Bar::$c.', + 53, + ], + [ + 'Native type string of set hook parameter $v is not contravariant with native type int|string of property SetPropertyHookParameter\Bar::$d.', + 59, + ], + [ + 'Type int<1, max> of set hook parameter $v is not contravariant with type int of property SetPropertyHookParameter\Bar::$e.', + 66, + ], + [ + 'Type array|int<1, max> of set hook parameter $v is not contravariant with type int of property SetPropertyHookParameter\Bar::$f.', + 73, + ], + [ + 'Set hook for property SetPropertyHookParameter\MissingTypes::$f has parameter $v with no value type specified in iterable type array.', + 123, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Set hook for property SetPropertyHookParameter\MissingTypes::$g has parameter $value with generic class SetPropertyHookParameter\GenericFoo but does not specify its types: T', + 129, + ], + [ + 'Set hook for property SetPropertyHookParameter\MissingTypes::$h has parameter $value with no signature specified for callable.', + 135, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php deleted file mode 100644 index fe8c016469..0000000000 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ -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 d1a9331dc3..8d050e7636 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -17,7 +17,7 @@ class TypesAssignedToPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed), new PropertyDescriptor(), new PropertyReflectionFinder()); + return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, false, true), new PropertyReflectionFinder()); } public function testTypesAssignedToProperties(): void @@ -107,6 +107,21 @@ public function testTypesAssignedToProperties(): void 'Property PropertiesAssignedTypes\AppendToArrayAccess::$collection2 (ArrayAccess&Countable) does not accept Countable.', 376, ], + [ + 'Property PropertiesAssignedTypes\ParamOutAssign::$foo (list) does not accept string.', + 400, + 'string is not a list.', + ], + [ + 'Property PropertiesAssignedTypes\ParamOutAssign::$foo2 (list>) does not accept string.', + 410, + 'string is not a list.', + ], + [ + 'Property PropertiesAssignedTypes\ParamOutAssign::$foo2 (list>) does not accept non-empty-list|string>.', + 415, + 'list|string might not be a list.', + ], ]); } @@ -115,11 +130,11 @@ public function testBug1216(): void $this->analyse([__DIR__ . '/data/bug-1216.php'], [ [ 'Property Bug1216PropertyTest\Baz::$untypedBar (string) does not accept int.', - 35, + 38, ], [ 'Property Bug1216PropertyTest\Dummy::$foo (Exception) does not accept stdClass.', - 59, + 62, ], ]); } @@ -140,21 +155,9 @@ public function testTypesAssignedToPropertiesExpressionNames(): void 66, ], [ - 'Property PropertiesFromArrayIntoObject\Foo::$float_test (float) does not accept float|int|string.', - 69, - ], - [ - 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float|int|string.', - 69, - ], - [ - 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept float|int|string.', + 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept string.', 69, ], - [ - 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept (float|int).', - 73, - ], [ 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float.', 83, @@ -216,15 +219,34 @@ public function testBug3777(): void 168, ], ]); + + $this->analyse([__DIR__ . '/data/bug-3777-static.php'], [ + [ + 'Static property Bug3777Static\Bar::$foo (Bug3777Static\Foo) does not accept Bug3777Static\Fooo.', + 58, + ], + [ + 'Static property Bug3777Static\Ipsum::$ipsum (Bug3777Static\Lorem) does not accept Bug3777Static\Lorem.', + 95, + ], + [ + 'Static property Bug3777Static\Ipsum2::$lorem2 (Bug3777Static\Lorem2) does not accept Bug3777Static\Lorem2.', + 129, + ], + [ + 'Static property Bug3777Static\Ipsum2::$ipsum2 (Bug3777Static\Lorem2) does not accept Bug3777Static\Lorem2.', + 131, + ], + [ + 'Static property Bug3777Static\Ipsum3::$ipsum3 (Bug3777Static\Lorem3) does not accept Bug3777Static\Lorem3.', + 168, + ], + ]); } 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, @@ -304,7 +326,7 @@ public function testAppendedArrayItemType(): void 45, ], [ - 'Property AppendedArrayItem\Baz::$staticProperty (array) does not accept array.', + 'Property AppendedArrayItem\Baz::$staticProperty (array) does not accept array.', 79, ], ], @@ -327,17 +349,16 @@ public function testBug5804(): void 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, + "Offset 'age' (int) does not accept type string.", ], [ - 'Property Bug6286\HelloWorld::$nestedDetails (array) does not accept non-empty-array.', + "Property Bug6286\HelloWorld::\$nestedDetails (array) does not accept non-empty-array.", 22, + "Offset 'age' (int) does not accept type int|string.", ], ]); } @@ -372,10 +393,6 @@ public function testBug3703(): void 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'], []); } @@ -384,6 +401,11 @@ public function testBug3339(): void $this->analyse([__DIR__ . '/data/bug-3339.php'], []); } + public function testBug5336(): void + { + $this->analyse([__DIR__ . '/data/bug-5336.php'], []); + } + public function testBug6117(): void { $this->analyse([__DIR__ . '/data/bug-6117.php'], []); @@ -394,7 +416,7 @@ 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>.', + 'Property GenericObjectUnspecifiedTemplateTypes\Foo::$obj (GenericObjectUnspecifiedTemplateTypes\MyObject) does not accept GenericObjectUnspecifiedTemplateTypes\MyObject<(int|string), mixed>.', 13, ], [ @@ -415,4 +437,346 @@ public function testGenericObjectWithUnspecifiedTemplateTypesLevel8(): void ]); } + public function testBug5382(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-5382.php'], []); + } + + public function testBug6757(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6757.php'], []); + } + + public function testBug4526(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-4526.php'], []); + } + + public function testBug7200(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7200.php'], []); + } + + public function testBug4680(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-4680.php'], []); + } + + public function testBug3383(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3383.php'], []); + } + + public function testBug6356(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6356.php'], []); + } + + public function testBug6356b(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6356b.php'], [ + [ + 'Property Bug6356b\HelloWorld2::$details (array{name: string, age: int}) does not accept array{name: string, age: \'Forty-two\'}.', + 19, + "Offset 'age' (int) does not accept type string.", + ], + [ + 'Property Bug6356b\HelloWorld2::$nestedDetails (array) does not accept non-empty-array.', + 21, + "Offset 'age' (int) does not accept type int|string.", + ], + [ + 'Property Bug6356b\HelloWorld2::$nestedDetails (array) does not accept non-empty-array.', + 26, + "Offset 'age' (int) does not accept type int|string.", + ], + ]); + } + + public function testIntegerRangesAndConstants(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/int-ranges-and-constants.php'], [ + [ + 'Property IntegerRangesAndConstants\HelloWorld::$i (0|1|3) does not accept int<0, 3>.', + 17, + ], + [ + 'Property IntegerRangesAndConstants\HelloWorld::$x (int<0, 3>) does not accept 0|1|2|3|string.', + 42, + ], + [ + 'Property IntegerRangesAndConstants\HelloWorld::$x (int<0, 3>) does not accept 0|1|2|3|bool.', + 43, + ], + [ + 'Property IntegerRangesAndConstants\HelloWorld::$x (int<0, 3>) does not accept 0|1|3|bool.', + 44, + ], + [ + 'Property IntegerRangesAndConstants\HelloWorld::$x (int<0, 3>) does not accept 0|1|3|4.', + 45, + ], + ]); + } + + public function testBug3311b(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3311b.php'], [ + [ + 'Property Bug3311b\Foo::$bar (list) does not accept non-empty-array, string>.', + 16, + 'non-empty-array, string> might not be a list.', + ], + ]); + } + + public function testBug7789(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7789.php'], []); + } + + public function testBug9131(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9131.php'], []); + } + + public function testBug8222(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8222.php'], []); + } + + public function testWritingReadonlyProperty(): void + { + $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ + [ + 'Property WritingToReadOnlyProperties\Foo::$usualProperty (int) does not accept string.', + 24, + ], + [ + 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty (int) does not accept string.', + 27, + ], + [ + 'Property WritingToReadOnlyProperties\Foo::$usualProperty (int) does not accept string.', + 34, + ], + [ + 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty (int) does not accept string.', + 40, + ], + ]); + } + + public function testBug8190(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8190.php'], []); + } + + public function testBug8074(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8074.php'], []); + } + + public function testBug7087(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7087.php'], []); + } + + public function testUnset(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/property-type-after-unset.php'], [ + [ + 'Property PropertyTypeAfterUnset\Foo::$nonEmpty (non-empty-array) does not accept array.', + 19, + 'array might be empty.', + ], + [ + 'Property PropertyTypeAfterUnset\Foo::$listProp (list) does not accept array, int>.', + 20, + 'array, int> might not be a list.', + ], + [ + 'Property PropertyTypeAfterUnset\Foo::$nestedListProp (array>) does not accept array, int>>.', + 21, + 'array, int> might not be a list.', + ], + ]); + } + + public function testGenericsInCallableInConstructor(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generics-in-callable-in-constructor.php'], []); + } + + public function testBug10686(): void + { + $this->analyse([__DIR__ . '/data/bug-10686.php'], []); + } + + public function testBug11275(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-11275.php'], [ + [ + 'Property Bug11275\D::$b (list) does not accept array.', + 50, + 'array might not be a list.', + ], + ]); + } + + public function testBug11617(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-11617.php'], [ + [ + 'Property Bug11617\HelloWorld::$params (array) does not accept array|string>.', + 14, + ], + [ + 'Property Bug11617\HelloWorld::$params (array) does not accept array|string>.', + 16, + ], + [ + 'Property Bug11617\HelloWorld::$params (array) does not accept array|string>.', + 21, + ], + ]); + } + + public function testBug4174(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-4174.php'], []); + } + + public function testBug12131(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12131.php'], [ + [ + 'Property Bug12131\Test::$array (non-empty-list) does not accept non-empty-array, int>.', + 29, + 'non-empty-array, int> might not be a list.', + ], + ]); + } + + public function testBug6398(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6398.php'], []); + } + + public function testBug6571(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6571.php'], []); + } + + public function testBug12565(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12565.php'], []); + } + + public function testShortBodySetHook(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/short-set-property-hook-assign.php'], [ + [ + 'Property ShortSetPropertyHookAssign\Foo::$i (int) does not accept string.', + 9, + ], + [ + 'Property ShortSetPropertyHookAssign\Foo::$s (non-empty-string) does not accept \'\'.', + 18, + ], + [ + 'Property ShortSetPropertyHookAssign\GenericFoo::$a (T of ShortSetPropertyHookAssign\Foo) does not accept ShortSetPropertyHookAssign\Foo.', + 36, + 'Type ShortSetPropertyHookAssign\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Property ShortSetPropertyHookAssign\GenericFoo::$b (T of ShortSetPropertyHookAssign\Foo) does not accept ShortSetPropertyHookAssign\Foo.', + 50, + 'Type ShortSetPropertyHookAssign\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Property ShortSetPropertyHookAssign\GenericFoo::$c (T of ShortSetPropertyHookAssign\Foo) does not accept ShortSetPropertyHookAssign\Foo.', + 59, + 'Type ShortSetPropertyHookAssign\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/assign-hooked-properties.php'], [ + [ + 'Property AssignHookedProperties\Foo::$i (int) does not accept array|int.', + 11, + ], + [ + 'Property AssignHookedProperties\Foo::$j (int) does not accept array|int.', + 19, + ], + [ + 'Property AssignHookedProperties\Foo::$i (array|int) does not accept array.', + 27, + ], + [ + 'Property AssignHookedProperties\FooGenerics::$a (int) does not accept string.', + 52, + ], + [ + 'Property AssignHookedProperties\FooGenerics::$a (T) does not accept int.', + 61, + 'Type int is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Property AssignHookedProperties\FooGenericsParam::$a (array) does not accept array|int.', + 76, + ], + [ + 'Property AssignHookedProperties\FooGenericsParam::$a (array|int) does not accept array.', + 91, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index 901b14816a..22c15b707b 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -2,9 +2,11 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function strpos; use const PHP_VERSION_ID; /** @@ -16,38 +18,73 @@ class UninitializedPropertyRuleTest extends RuleTestCase protected function getRule(): Rule { return new UninitializedPropertyRule( - new DirectReadWritePropertiesExtensionProvider([ - new class() implements ReadWritePropertiesExtension { + new ConstructorsHelper( + self::getContainer(), + [ + 'UninitializedProperty\\TestCase::setUp', + 'Bug9619\\AdminPresenter::startup', + 'Bug9619\\AdminPresenter2::startup', + 'Bug9619\\AdminPresenter3::startup', + 'Bug9619\\AdminPresenter3::startup2', + ], + ), + ); + } - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return false; - } + protected function getReadWritePropertiesExtensions(): array + { + return [ + new class() implements ReadWritePropertiesExtension { - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return false; - } + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return false; + } - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $property->getDeclaringClass()->getName() === 'UninitializedProperty\\TestExtension' && $propertyName === 'inited'; - } + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return false; + } - }, - ]), - [ - 'UninitializedProperty\\TestCase::setUp', - ], - ); + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'UninitializedProperty\\TestExtension' && $propertyName === 'inited'; + } + + }, + + // bug-9619 + new class() implements ReadWritePropertiesExtension { + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isInitialized($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->isPublic() && + strpos($property->getDocComment() ?? '', '@inject') !== false; + } + + }, + ]; } - public function testRule(): void + public static function getAdditionalConfigFiles(): array { - if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } + return [ + __DIR__ . '/uninitialized-property-rule.neon', + ]; + } + public function testRule(): void + { $this->analyse([__DIR__ . '/data/uninitialized-property.php'], [ [ 'Class UninitializedProperty\Foo has an uninitialized property $bar. Give it default value or assign it in the constructor.', @@ -69,25 +106,133 @@ public function testRule(): void 'Class UninitializedProperty\TestExtension has an uninitialized property $uninited. Give it default value or assign it in the constructor.', 122, ], + [ + 'Class UninitializedProperty\FooTraitClass has an uninitialized property $bar. Give it default value or assign it in the constructor.', + 157, + ], + [ + 'Class UninitializedProperty\FooTraitClass has an uninitialized property $baz. Give it default value or assign it in the constructor.', + 159, + ], + /*[ + 'Access to an uninitialized property UninitializedProperty\InitializedInPublicSetterNonFinalClass::$foo.', + 278, + ],*/ + [ + 'Class UninitializedProperty\SometimesInitializedInPrivateSetter has an uninitialized property $foo. Give it default value or assign it in the constructor.', + 286, + ], + [ + 'Access to an uninitialized property UninitializedProperty\SometimesInitializedInPrivateSetter::$foo.', + 303, + ], + [ + 'Class UninitializedProperty\EarlyReturn has an uninitialized property $foo. Give it default value or assign it in the constructor.', + 372, + ], ]); } public function testPromotedProperties(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0'); - } - $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'], []); } + public function testReadOnlyPhpDoc(): void + { + // reported by a different rule + $this->analyse([__DIR__ . '/data/uninitialized-property-readonly-phpdoc.php'], []); + } + + public function testBug7219(): void + { + $this->analyse([__DIR__ . '/data/bug-7219.php'], [ + [ + 'Class Bug7219\Foo has an uninitialized property $id. Give it default value or assign it in the constructor.', + 8, + ], + [ + 'Class Bug7219\Foo has an uninitialized property $email. Give it default value or assign it in the constructor.', + 15, + ], + ]); + } + + public function testAdditionalConstructorsExtension(): void + { + $this->analyse([__DIR__ . '/data/uninitialized-property-additional-constructors.php'], [ + [ + 'Class TestInitializedProperty\TestAdditionalConstructor has an uninitialized property $one. Give it default value or assign it in the constructor.', + 07, + ], + [ + 'Class TestInitializedProperty\TestAdditionalConstructor has an uninitialized property $three. Give it default value or assign it in the constructor.', + 11, + ], + ]); + } + + public function testEfabricaLatteBug(): void + { + $this->analyse([__DIR__ . '/data/efabrica-latte-bug.php'], []); + } + + public function testBug9619(): void + { + $this->analyse([__DIR__ . '/data/bug-9619.php'], [ + [ + 'Access to an uninitialized property Bug9619\AdminPresenter3::$user.', + 55, + ], + ]); + } + + public function testBug9831(): void + { + $this->analyse([__DIR__ . '/data/bug-9831.php'], [ + [ + 'Access to an uninitialized property Bug9831\Foo::$bar.', + 12, + ], + ]); + } + + public function testRedeclareReadonlyProperties(): void + { + $this->analyse([__DIR__ . '/data/redeclare-readonly-property.php'], [ + [ + 'Class RedeclareReadonlyProperty\B19 has an uninitialized property $prop2. Give it default value or assign it in the constructor.', + 249, + ], + [ + 'Access to an uninitialized property RedeclareReadonlyProperty\B19::$prop2.', + 260, + ], + ]); + } + + public function testBug12336(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-12336.php'], []); + } + + public function testBug12547(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-12547.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/VirtualNullsafePropertyFetchTest.php b/tests/PHPStan/Rules/Properties/VirtualNullsafePropertyFetchTest.php new file mode 100644 index 0000000000..12c42b7aea --- /dev/null +++ b/tests/PHPStan/Rules/Properties/VirtualNullsafePropertyFetchTest.php @@ -0,0 +1,56 @@ + + */ +class VirtualNullsafePropertyFetchTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new /** @implements Rule */ class implements Rule { + + public function getNodeType(): string + { + return PropertyFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->getAttribute('virtualNullsafePropertyFetch') === true) { + return [RuleErrorBuilder::message('Nullable property fetch detected')->identifier('ruleTest.VirtualNullsafeProperty')->build()]; + } + + return [RuleErrorBuilder::message('Regular property fetch detected')->identifier('ruleTest.VirtualNullsafeProperty')->build()]; + } + + }; + } + + public function testAttribute(): void + { + $this->analyse([ __DIR__ . '/data/virtual-nullsafe-property-fetch.php'], [ + [ + 'Regular property fetch detected', + 3, + ], + [ + 'Nullable property fetch detected', + 4, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php index 83f1a922a2..b11e08f127 100644 --- a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -16,7 +17,7 @@ class WritingToReadOnlyPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new WritingToReadOnlyPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), new PropertyDescriptor(), new PropertyReflectionFinder(), $this->checkThisOnly); + return new WritingToReadOnlyPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), new PropertyDescriptor(), new PropertyReflectionFinder(), $this->checkThisOnly); } public function testCheckThisOnlyProperties(): void @@ -25,11 +26,11 @@ public function testCheckThisOnlyProperties(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 15, + 20, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 16, + 21, ], ]); } @@ -40,25 +41,80 @@ public function testCheckAllProperties(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 15, + 20, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 16, + 21, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 25, + 30, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 26, + 31, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 35, + 43, + ], + ]); + } + + public function testObjectShapes(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/properties-object-shapes.php'], [ + [ + 'Property object{foo: int, bar?: string}::$foo is not writable.', + 18, + ], + [ + 'Property object{foo: int}|stdClass::$foo is not writable.', + 42, + ], + ]); + } + + public function testConflictingAnnotationProperty(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], [ + /*[ + 'Property ConflictingAnnotationProperty\PropertyWithAnnotation::$test is not writable.', + 27, + ],*/ + ]); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/writing-to-read-only-hooked-properties.php'], [ + [ + 'Property WritingToReadOnlyHookedProperties\Foo::$i is not writable.', + 16, + ], + [ + 'Property WritingToReadOnlyHookedProperties\Bar::$i is not writable.', + 32, ], ]); } + public function testBug12553(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/../Variables/data/bug-12553.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/bug-7074.neon b/tests/PHPStan/Rules/Properties/bug-7074.neon new file mode 100644 index 0000000000..328df07063 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/bug-7074.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/bug-7074.stub diff --git a/tests/PHPStan/Rules/Properties/data/UninitializedPropertyAdditionalConstructorsExtensions.php b/tests/PHPStan/Rules/Properties/data/UninitializedPropertyAdditionalConstructorsExtensions.php new file mode 100644 index 0000000000..316a4e136e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/UninitializedPropertyAdditionalConstructorsExtensions.php @@ -0,0 +1,20 @@ +getName() === 'TestInitializedProperty\\TestAdditionalConstructor') { + return ['setTwo']; + } + + return []; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php new file mode 100644 index 0000000000..230de9d816 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php @@ -0,0 +1,10 @@ += 8.4 + +namespace AbstractFinalHookParseError; + +abstract class User +{ + final abstract public string $bar { + get; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php new file mode 100644 index 0000000000..baba303bf1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-final-property-hook.php @@ -0,0 +1,15 @@ += 8.4 + +namespace AbstractFinalHook; + +abstract class User +{ + abstract public string $foo { + final get; + } +} + +abstract class Foo +{ + abstract public int $i { final get { return 1;} set; } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php new file mode 100644 index 0000000000..d035d36810 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php @@ -0,0 +1,10 @@ + $this->name; + set => $this->name = $value; + } + + public abstract string $lastName { + get => $this->lastName; + set => $this->lastName = $value; + } + + public abstract string $middleName { + get => $this->name; + set; + } + + public abstract string $familyName { + get; + set; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php new file mode 100644 index 0000000000..b34e66a886 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php @@ -0,0 +1,10 @@ += 8.4 + +namespace AbstractPrivateHook; + +abstract class Foo +{ + abstract private int $i { get; } + abstract protected int $ii { get; } + abstract public int $iii { get; } +} diff --git a/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php b/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php index 434d25be5d..1189bc2232 100644 --- a/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php +++ b/tests/PHPStan/Rules/Properties/data/access-properties-after-isnull.php @@ -33,22 +33,22 @@ public function doFoo($foo) } while (is_null($foo) && $foo->fooProperty) { - + break; } while (is_null($foo) || $foo->fooProperty) { - + break; } while (!is_null($foo) && $foo->fooProperty) { - + break; } while (!is_null($foo) || $foo->fooProperty) { - + break; } while (is_null($foo) || $foo->barProperty) { - + break; } while (!is_null($foo) && $foo->barProperty) { - + break; } } diff --git a/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php b/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php index 7af71ae1b8..ddac393d08 100644 --- a/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php +++ b/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php @@ -1,4 +1,4 @@ -= 7.4 +foo) && isset($m->bar)) { + echo $m->foo; + echo $m->bar; + echo $m->baz; + } + } + +} 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 index 0cf096c2e1..9d065b72f3 100644 --- a/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php +++ b/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php @@ -1,4 +1,4 @@ -= 7.4 += 8.4 + +namespace AssignHookedProperties; + +class Foo +{ + + public int $i { + /** @param array|int $val */ + set (array|int $val) { + $this->i = $val; // only int allowed + } + } + + public int $j { + /** @param array|int $val */ + set (array|int $val) { + $this->i = $val; // this is okay - hook called + $this->j = $val; // only int allowed + } + } + + public function doFoo(): void + { + $this->i = ['foo']; // okay + $this->i = 1; // okay + $this->i = [1]; // not okay + } + +} + +/** + * @template T + */ +class FooGenerics +{ + + /** @var T */ + public $a { + set { + $this->a = $value; + } + } + + /** + * @param FooGenerics $f + * @return void + */ + public static function doFoo(self $f): void + { + $f->a = 1; + $f->a = 'foo'; + } + + /** + * @param T $t + */ + public function doBar($t): void + { + $this->a = $t; + $this->a = 1; + } + +} + +/** + * @template T + */ +class FooGenericsParam +{ + + /** @var array */ + public array $a { + /** @param array|int $value */ + set (array|int $value) { + $this->a = $value; // not ok + + if (is_array($value)) { + $this->a = $value; // ok + } + } + } + + /** + * @param FooGenericsParam $f + * @return void + */ + public static function doFoo(self $f): void + { + $f->a = [1]; // ok + $f->a = ['foo']; // not ok + } + + /** + * @param T $t + */ + public function doBar($t): void + { + $this->a = [$t]; // ok + $this->a = 1; // ok + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-10048.php b/tests/PHPStan/Rules/Properties/data/bug-10048.php new file mode 100644 index 0000000000..d537fb6527 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10048.php @@ -0,0 +1,26 @@ += 8.1 + +namespace Bug10048; + +class Foo { + private readonly string $bar; + private readonly \Closure $callback; + public function __construct() { + $this->bar = "hi"; + $this->useBar(); + echo $this->bar; + $this->callback = function() { + $this->useBar(); + }; + } + + private function useBar(): void { + echo $this->bar; + } + + public function useCallback(): void { + call_user_func($this->callback); + } +} + +(new Foo())->useCallback(); diff --git a/tests/PHPStan/Rules/Properties/data/bug-10523.php b/tests/PHPStan/Rules/Properties/data/bug-10523.php new file mode 100644 index 0000000000..a5e88ebcd2 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10523.php @@ -0,0 +1,84 @@ += 8.1 + +namespace Bug10523; + +final class Controller +{ + private readonly B $userAccount; + + public function __construct() + { + $this->userAccount = new B(); + } + + public function init(): void + { + $this->redirectIfNkdeCheckoutNotAllowed(); + $this->redirectIfNoShoppingBasketPresent(); + } + + private function redirectIfNkdeCheckoutNotAllowed(): void + { + + } + + private function redirectIfNoShoppingBasketPresent(): void + { + $x = $this->userAccount; + } + +} + +class B {} + +final class MultipleWrites +{ + private readonly B $userAccount; + + public function __construct() + { + $this->userAccount = new B(); + } + + public function init(): void + { + $this->redirectIfNkdeCheckoutNotAllowed(); + $this->redirectIfNoShoppingBasketPresent(); + } + + private function redirectIfNkdeCheckoutNotAllowed(): void + { + } + + private function redirectIfNoShoppingBasketPresent(): void + { + $this->userAccount = new B(); + } + +} + + +final class SingleWriteInConstructorCalledMethod +{ + private readonly B $userAccount; + + public function __construct() + { + } + + public function init(): void + { + $this->redirectIfNkdeCheckoutNotAllowed(); + $this->redirectIfNoShoppingBasketPresent(); + } + + private function redirectIfNkdeCheckoutNotAllowed(): void + { + } + + private function redirectIfNoShoppingBasketPresent(): void + { + $this->userAccount = new B(); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-10686.php b/tests/PHPStan/Rules/Properties/data/bug-10686.php new file mode 100644 index 0000000000..0d6922d5da --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10686.php @@ -0,0 +1,33 @@ += 7.4 + +namespace Bug10686; + +class Model {} + +/** + * @template T of object|array + */ +class WeakAnalysingMap +{ + /** @var list */ + public array $values = []; +} + +class Reference +{ + /** @var WeakAnalysingMap */ + private static WeakAnalysingMap $analysingTheirModelMap; + + public function createAnalysingTheirModel(): Model + { + if ((self::$analysingTheirModelMap ?? null) === null) { + self::$analysingTheirModelMap = new WeakAnalysingMap(); + } + + $theirModel = new Model(); + + self::$analysingTheirModelMap->values[] = $theirModel; + + return end(self::$analysingTheirModelMap->values); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-10822.php b/tests/PHPStan/Rules/Properties/data/bug-10822.php new file mode 100644 index 0000000000..35ed77467b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10822.php @@ -0,0 +1,294 @@ += 8.1 + +namespace Bug10822; + +enum Deprecation: string +{ + case callString = 'call-string'; + case userAuthored = 'user-authored'; +} + +interface FileLocation +{ + public function getOffset(): int; + + public function getLine(): int; + + public function getColumn(): int; +} + +interface FileSpan +{ + public function getSourceUrl(): ?string; + + public function getStart(): FileLocation; + + public function getEnd(): FileLocation; +} + +final class Frame +{ + public function __construct(private readonly string $url, private readonly ?int $line, private readonly ?int $column, private readonly ?string $member) + { + } + + public function getMember(): ?string + { + return $this->member; + } + + public function getLocation(): string + { + $library = $this->url; + + if ($this->line === null) { + return $library; + } + + if ($this->column === null) { + return $library . ' ' . $this->line; + } + + return $library . ' ' . $this->line . ':' . $this->column; + } +} + +final class Trace +{ + /** + * @param list $frames + */ + public function __construct(public readonly array $frames) + { + } +} + +interface DeprecationAwareLoggerInterface +{ + public function warn(string $message, bool $deprecation = false, ?FileSpan $span = null, ?Trace $trace = null): void; + + public function warnForDeprecation(Deprecation $deprecation, string $message, ?FileSpan $span = null, ?Trace $trace = null): void; +} + +interface AstNode +{ + public function getSpan(): FileSpan; +} + +final class SassScriptException extends \Exception +{ +} + +final class SassRuntimeException extends \Exception +{ + public readonly FileSpan $span; + public readonly Trace $sassTrace; + + public function __construct(string $message, FileSpan $span, ?Trace $sassTrace = null, ?\Throwable $previous = null) + { + $this->span = $span; + $this->sassTrace = $sassTrace ?? new Trace([]); + + parent::__construct($message, 0, $previous); + } +} + +interface SassCallable +{ + public function getName(): string; +} + +class BuiltInCallable implements SassCallable +{ + /** + * @param callable(list): Value $callback + */ + public static function function (string $name, string $arguments, callable $callback): BuiltInCallable + { + return new BuiltInCallable($name, [[$arguments, $callback]]); + } + + /** + * @param list): Value}> $overloads + */ + private function __construct(private readonly string $name, public readonly array $overloads) + { + } + + public function getName(): string + { + return $this->name; + } +} + +abstract class Value +{ + public function assertString(string $name): SassString + { + throw new SassScriptException("\$$name: this is not a string."); + } +} + +final class SassString extends Value +{ + public function __construct( + private readonly string $text, + public readonly bool $hasQuotes, + ) + { + } + + public function getText(): string + { + return $this->text; + } + + public function assertString(string $name): SassString + { + return $this; + } +} + +final class SassMixin extends Value +{ + public function __construct(public readonly SassCallable $callable) + { + } +} + +interface ImportCache +{ + public function humanize(string $uri): string; +} + +final class Environment +{ + public function getMixin(string $name): SassCallable + { + throw new \BadMethodCallException('not implemented yet'); + } +} + +class EvaluateVisitor +{ + private readonly ImportCache $importCache; + + /** + * @var array + */ + public array $builtInFunctions = []; + + private readonly DeprecationAwareLoggerInterface $logger; + + /** + * @var array> + */ + private array $warningsEmitted = []; + + private Environment $environment; + + private string $member = "root stylesheet"; + + private ?AstNode $callableNode = null; + + /** + * @var list + */ + private array $stack = []; + + public function __construct(ImportCache $importCache, DeprecationAwareLoggerInterface $logger) + { + $this->importCache = $importCache; + $this->logger = $logger; + $this->environment = new Environment(); + + // These functions are defined in the context of the evaluator because + // they need access to the environment or other local state. + $metaFunctions = [ + BuiltInCallable::function('get-mixin', '$name', function ($arguments) { + $name = $arguments[0]->assertString('name'); + + \assert($this->callableNode !== null); + $callable = $this->addExceptionSpan($this->callableNode, function () use ($name) { + return $this->environment->getMixin(str_replace('_', '-', $name->getText())); + }); + + return new SassMixin($callable); + }), + ]; + + foreach ($metaFunctions as $function) { + $this->builtInFunctions[$function->getName()] = $function; + } + } + + private function stackFrame(string $member, FileSpan $span): Frame + { + $url = $span->getSourceUrl(); + + if ($url !== null) { + $url = $this->importCache->humanize($url); + } + + return new Frame( + $url ?? $span->getSourceUrl() ?? '-', + $span->getStart()->getLine() + 1, + $span->getStart()->getColumn() + 1, + $member + ); + } + + private function stackTrace(?FileSpan $span = null): Trace + { + $frames = []; + + foreach ($this->stack as [$member, $nodeWithSpan]) { + $frames[] = $this->stackFrame($member, $nodeWithSpan->getSpan()); + } + + if ($span !== null) { + $frames[] = $this->stackFrame($this->member, $span); + } + + return new Trace(array_reverse($frames)); + } + + public function warn(string $message, FileSpan $span, ?Deprecation $deprecation = null): void + { + $spanString = ($span->getSourceUrl() ?? '') . "\0" . $span->getStart()->getOffset() . "\0" . $span->getEnd()->getOffset(); + + if (isset($this->warningsEmitted[$message][$spanString])) { + return; + } + $this->warningsEmitted[$message][$spanString] = true; + + $trace = $this->stackTrace($span); + + if ($deprecation === null) { + $this->logger->warn($message, false, $span, $trace); + } else { + $this->logger->warnForDeprecation($deprecation, $message, $span, $trace); + } + } + + /** + * Runs $callback, and converts any {@see SassScriptException}s it throws to + * {@see SassRuntimeException}s with $nodeWithSpan's source span. + * + * @template T + * + * @param callable(): T $callback + * + * @return T + * + * @throws SassRuntimeException + */ + private function addExceptionSpan(AstNode $nodeWithSpan, callable $callback, bool $addStackFrame = true) + { + try { + return $callback(); + } catch (SassScriptException $e) { + throw new SassRuntimeException($e->getMessage(), $nodeWithSpan->getSpan(), $this->stackTrace($addStackFrame ? $nodeWithSpan->getSpan() : null), $e); + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-10987.php b/tests/PHPStan/Rules/Properties/data/bug-10987.php new file mode 100644 index 0000000000..f3856d7303 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10987.php @@ -0,0 +1,21 @@ + + */ + private array $b; + + public function __construct(B ...$b) + { + $this->b = $b; + } +} + +final class B +{ +} + +final class C +{ + /** + * @var list + */ + private array $b; + + /** + * @no-named-arguments + */ + public function __construct(B ...$b) + { + $this->b = $b; + } +} + +final class D +{ + /** + * @var list + */ + private array $b; + + public function __construct(B ...$b) + { + $this->b = $b; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-11617.php b/tests/PHPStan/Rules/Properties/data/bug-11617.php new file mode 100644 index 0000000000..e0854ad0d7 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11617.php @@ -0,0 +1,23 @@ + + */ + private $params; + + public function sayHello(string $query): void + { + \parse_str($query, $this->params); + \parse_str($query, $tmp); + $this->params = $tmp; + + /** @var array $foo */ + $foo = []; + \parse_str($query, $foo); + $this->params = $foo; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-11828.php b/tests/PHPStan/Rules/Properties/data/bug-11828.php new file mode 100644 index 0000000000..0a030d7bd0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11828.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug11828; + +class Dummy +{ + /** + * @var callable + */ + private $callable; + private readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; + + $this->callable = function () { + $foo = $this->getFoo(); + }; + } + + public function getFoo(): int + { + return $this->foo; + } + + public function getCallable(): callable + { + return $this->callable; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-12131.php b/tests/PHPStan/Rules/Properties/data/bug-12131.php new file mode 100755 index 0000000000..6f7f8d83d8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12131.php @@ -0,0 +1,31 @@ += 7.4 + +namespace Bug12131; + +class Test +{ + /** + * @var non-empty-list + */ + public array $array; + + public function __construct() + { + $this->array = array_fill(0, 10, 1); + } + + public function setAtZero(): void + { + $this->array[0] = 1; + } + + public function setAtOne(): void + { + $this->array[1] = 1; + } + + public function setAtTwo(): void + { + $this->array[2] = 1; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-1216.php b/tests/PHPStan/Rules/Properties/data/bug-1216.php index 756a4b6bf7..0338e8373a 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-1216.php +++ b/tests/PHPStan/Rules/Properties/data/bug-1216.php @@ -2,6 +2,8 @@ namespace Bug1216PropertyTest; +use AllowDynamicProperties; + abstract class Foo { /** @@ -25,6 +27,7 @@ trait Bar * @property string $bar * @property string $untypedBar */ +#[AllowDynamicProperties] class Baz extends Foo { diff --git a/tests/PHPStan/Rules/Properties/data/bug-12336.php b/tests/PHPStan/Rules/Properties/data/bug-12336.php new file mode 100644 index 0000000000..5e7380d9ce --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12336.php @@ -0,0 +1,7 @@ += 8.4 + +namespace Bug12336; + +abstract class ListItem { + abstract public int $item { get; } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-12466.php b/tests/PHPStan/Rules/Properties/data/bug-12466.php new file mode 100644 index 0000000000..3722477119 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12466.php @@ -0,0 +1,95 @@ += 8.4 + +namespace Bug12466OverridenProperty; + +interface Foo +{ + + public int|string $onlyGet { get; } + + public int|string $onlySet { set; } + +} + +class Bar implements Foo +{ + + public int $onlyGet { + get { + return 1; + } + } + + public int|string|null $onlySet { + set { + $this->onlySet = $value; + } + } + +} + +class Baz implements Foo +{ + + public int|string|null $onlyGet { + get { + return null; + } + } + + public int $onlySet { + set { + $this->onlySet = $value; + } + } + +} + +interface FooWithPhpDocs +{ + + /** @var array */ + public array $onlyGet { get; } + + /** @var array */ + public array $onlySet { set; } + +} + +class BarWithPhpDocs implements FooWithPhpDocs +{ + + /** @var array */ + public array $onlyGet { + get { + return []; + } + } + + /** @var array */ + public array $onlySet { + set { + $this->onlySet = $value; + } + } + +} + +class BazWithPhpDocs implements FooWithPhpDocs +{ + + /** @var array */ + public array $onlyGet { + get { + return []; + } + } + + /** @var array */ + public array $onlySet { + set { + $this->onlySet = $value; + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-12537.php b/tests/PHPStan/Rules/Properties/data/bug-12537.php new file mode 100755 index 0000000000..85ae54496e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12537.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug12537; + +use WeakMap; + +class Metadata { + /** + * @var WeakMap + */ + private readonly WeakMap $storage; + + public function __construct() { + $this->storage = new WeakMap(); + } + + public function set(stdClass $class, int $value): void { + $this->storage[$class] = $value; + } + + public function get(stdClass $class): mixed { + return $this->storage[$class] ?? null; + } +} + +$class = new stdClass(); +$meta = new Metadata(); + +$meta->set($class, 123); + +var_dump($meta->get($class)); diff --git a/tests/PHPStan/Rules/Properties/data/bug-12547.php b/tests/PHPStan/Rules/Properties/data/bug-12547.php new file mode 100644 index 0000000000..d4f0950960 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12547.php @@ -0,0 +1,11 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug12547; + +class Example { + public \DateTimeImmutable $noon { + get => new \DateTimeImmutable('12:00'); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-12565.php b/tests/PHPStan/Rules/Properties/data/bug-12565.php new file mode 100755 index 0000000000..12fafa7469 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12565.php @@ -0,0 +1,50 @@ + + */ +class ArrayLike implements \ArrayAccess { + + /** @var EntryType[] */ + private array $values = []; + public function offsetExists(mixed $offset): bool + { + return isset($this->values[$offset]); + } + + public function offsetGet(mixed $offset): EntryType + { + return $this->values[$offset] ?? new EntryType(); + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->values[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + unset($this->values[$offset]); + } +} + +class Wrapper { + public ?ArrayLike $myArrayLike; + + public function __construct() + { + $this->myArrayLike = new ArrayLike(); + + } +} + +$baz = new Wrapper(); +$baz->myArrayLike = new ArrayLike(); +$baz->myArrayLike[1] = new EntryType(); +$baz->myArrayLike[1]->title = "Test"; diff --git a/tests/PHPStan/Rules/Properties/data/bug-12586.php b/tests/PHPStan/Rules/Properties/data/bug-12586.php new file mode 100644 index 0000000000..e2eac6ff7f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12586.php @@ -0,0 +1,27 @@ += 8.4 + +declare(strict_types=1); + +namespace Bug12586; + +interface Foo +{ + public string $bar { + get; + } + + public string $baz { + get; + set; + } +} + +readonly class FooImpl implements Foo +{ + public function __construct( + public string $bar, + public string $baz, + ) + { + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-12692.php b/tests/PHPStan/Rules/Properties/data/bug-12692.php new file mode 100644 index 0000000000..237f71e684 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12692.php @@ -0,0 +1,17 @@ +static; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-2435.php b/tests/PHPStan/Rules/Properties/data/bug-2435.php new file mode 100644 index 0000000000..0370be5b97 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-2435.php @@ -0,0 +1,15 @@ +root->root !== null; + } +} + +class Bar extends Foo { +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-3171.php b/tests/PHPStan/Rules/Properties/data/bug-3171.php new file mode 100644 index 0000000000..e7e90f7fdd --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3171.php @@ -0,0 +1,20 @@ +property->someArray['test'] ?? 'test'; + } +} + diff --git a/tests/PHPStan/Rules/Properties/data/bug-3311b.php b/tests/PHPStan/Rules/Properties/data/bug-3311b.php new file mode 100644 index 0000000000..30e0eed390 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3311b.php @@ -0,0 +1,17 @@ + + * @psalm-var list + */ + public array $bar = []; +} + +function () { + $instance = new Foo; + $instance->bar[1] = 'baz'; +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-3383.php b/tests/PHPStan/Rules/Properties/data/bug-3383.php new file mode 100644 index 0000000000..6ba907d2bb --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3383.php @@ -0,0 +1,13 @@ +classification = random_int(0, 3); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-3572.php b/tests/PHPStan/Rules/Properties/data/bug-3572.php new file mode 100644 index 0000000000..2602896c62 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3572.php @@ -0,0 +1,24 @@ +field = $value; + } + + public static function castToB(A $a): B + { + $self = new B(); + $self->field = $a->field; + return $self; + } +} + +class B extends A +{ +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-3659.php b/tests/PHPStan/Rules/Properties/data/bug-3659.php new file mode 100644 index 0000000000..de03b1cd42 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3659.php @@ -0,0 +1,17 @@ +func2($obj->someProperty ?? null); + } + + public function func2(?string $param): void + { + echo $param ?? 'test'; + } +} + diff --git a/tests/PHPStan/Rules/Properties/data/bug-3777-static.php b/tests/PHPStan/Rules/Properties/data/bug-3777-static.php new file mode 100644 index 0000000000..0cc9c7b930 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3777-static.php @@ -0,0 +1,172 @@ + + */ + public static $dates; + + public function __construct() + { + static::$dates = new \SplObjectStorage(); + assertType('SplObjectStorage', static::$dates); + } +} + +/** @template T of object */ +class Foo +{ + + public function __construct() + { + + } + +} + +/** @template T of object */ +class Fooo +{ + +} + +class Bar +{ + + /** @var Foo<\stdClass> */ + private static $foo; + + /** @var Fooo<\stdClass> */ + private static $fooo; + + public function __construct() + { + static::$foo = new Foo(); + assertType('Bug3777Static\Foo', static::$foo); + + static::$fooo = new Fooo(); + assertType('Bug3777Static\Fooo', static::$fooo); + } + + public function doBar() + { + static::$foo = new Fooo(); + assertType('Bug3777Static\Fooo', static::$foo); + } + +} + +/** + * @template T of object + * @template U of object + */ +class Lorem +{ + + /** + * @param T $t + * @param U $u + */ + public function __construct($t, $u) + { + + } + +} + +class Ipsum +{ + + /** @var Lorem<\stdClass, \Exception> */ + private static $lorem; + + /** @var Lorem<\stdClass, \Exception> */ + private static $ipsum; + + public function __construct() + { + static::$lorem = new Lorem(new \stdClass, new \Exception()); + assertType('Bug3777Static\Lorem', static::$lorem); + static::$ipsum = new Lorem(new \Exception(), new \stdClass); + assertType('Bug3777Static\Lorem', static::$ipsum); + } + +} + +/** + * @template T of object + * @template U of object + */ +class Lorem2 +{ + + /** + * @param T $t + */ + public function __construct($t) + { + + } + +} + +class Ipsum2 +{ + + /** @var Lorem2<\stdClass, \Exception> */ + private static $lorem2; + + /** @var Lorem2<\stdClass, \Exception> */ + private static $ipsum2; + + public function __construct() + { + static::$lorem2 = new Lorem2(new \stdClass); + assertType('Bug3777Static\Lorem2', static::$lorem2); + static::$ipsum2 = new Lorem2(new \Exception()); + assertType('Bug3777Static\Lorem2', static::$ipsum2); + } + +} + +/** + * @template T of object + * @template U of object + */ +class Lorem3 +{ + + /** + * @param T $t + * @param U $u + */ + public function __construct($t, $u) + { + + } + +} + +class Ipsum3 +{ + + /** @var Lorem3<\stdClass, \Exception> */ + private static $lorem3; + + /** @var Lorem3<\stdClass, \Exception> */ + private static $ipsum3; + + public function __construct() + { + static::$lorem3 = new Lorem3(new \stdClass, new \Exception()); + assertType('Bug3777Static\Lorem3', static::$lorem3); + static::$ipsum3 = new Lorem3(new \Exception(), new \stdClass()); + assertType('Bug3777Static\Lorem3', static::$ipsum3); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-393.php b/tests/PHPStan/Rules/Properties/data/bug-393.php new file mode 100644 index 0000000000..530f7054b8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-393.php @@ -0,0 +1,34 @@ +privateProperty = 123; + }, + null, + Foo::class + ))(); + + (\Closure::bind( + static function () { + $bar = new Bar(); + $bar->privateProperty = 123; + }, + null, + Foo::class + ))(); +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4174.php b/tests/PHPStan/Rules/Properties/data/bug-4174.php new file mode 100644 index 0000000000..9e4d4e7b09 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4174.php @@ -0,0 +1,95 @@ + + */ + public static function getArrayConstAsKey(): array { + return [ + self::NUMBER_TYPE_OFF => 'Off', + self::NUMBER_TYPE_HEAD => 'Head', + self::NUMBER_TYPE_POSITION => 'Position', + ]; + } + + /** + * @return list + */ + public static function getArrayConstAsValue(): array { + return [ + self::NUMBER_TYPE_OFF, + self::NUMBER_TYPE_HEAD, + self::NUMBER_TYPE_POSITION, + ]; + } + + public function checkConstViaArrayKey(): void + { + $numberArray = self::getArrayConstAsKey(); + + // --- + + $newvalue = $this->getIntFromPost('newValue'); + + if ($newvalue && array_key_exists($newvalue, $numberArray)) { + $this->newValue = $newvalue; + } + + if (isset($numberArray[$newvalue])) { + $this->newValue = $newvalue; + } + + // --- + + $newvalue = $this->getIntFromPostWithoutNull('newValue'); + + if ($newvalue && array_key_exists($newvalue, $numberArray)) { + $this->newValue = $newvalue; + } + + if (isset($numberArray[$newvalue])) { + $this->newValue = $newvalue; + } + } + + public function checkConstViaArrayValue(): void + { + $numberArray = self::getArrayConstAsValue(); + + // --- + + $newvalue = $this->getIntFromPost('newValue'); + + if ($newvalue && in_array($newvalue, $numberArray, true)) { + $this->newValue = $newvalue; + } + + // --- + + $newvalue = $this->getIntFromPostWithoutNull('newValue'); + + if ($newvalue && in_array($newvalue, $numberArray, true)) { + $this->newValue = $newvalue; + } + } + + public function getIntFromPost(string $key): ?int { + return isset($_POST[$key]) ? (int)$_POST[$key] : null; + } + + public function getIntFromPostWithoutNull(string $key): int { + return isset($_POST[$key]) ? (int)$_POST[$key] : 0; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4492.php b/tests/PHPStan/Rules/Properties/data/bug-4492.php new file mode 100644 index 0000000000..e253137077 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4492.php @@ -0,0 +1,51 @@ +prop = $prop; + } + + public function getProp(): string + { + return $this->prop; + } +} + +trait PropMangler +{ + /** @var string */ + protected $prop; + + public function mangleProp(): void + { + $this->prop = 'Improved ' . $this->prop; + } +} + +class B extends A +{ + use PropMangler; +} + +class C extends A +{ + /** @var B b */ + public $b; + + public function __construct() + { + $this->b = new B; + } + + public function accessesBProp(): void + { + $this->b->prop = "This works"; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4526.php b/tests/PHPStan/Rules/Properties/data/bug-4526.php new file mode 100644 index 0000000000..86604b4c65 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4526.php @@ -0,0 +1,19 @@ +|null + */ + private $map; + + public function __construct(){ + $this->map = new SplObjectStorage; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4559.php b/tests/PHPStan/Rules/Properties/data/bug-4559.php new file mode 100644 index 0000000000..e3c0b6952f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4559.php @@ -0,0 +1,14 @@ +error->code)) { + echo $response->error->message ?? ''; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4680.php b/tests/PHPStan/Rules/Properties/data/bug-4680.php new file mode 100644 index 0000000000..b353ded26e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4680.php @@ -0,0 +1,18 @@ +|null */ + private $collection1; + + /** @var \SplObjectStorage */ + private $collection2; + + public function __construct() + { + $this->collection1 = new \SplObjectStorage(); + $this->collection2 = new \SplObjectStorage(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-5336.php b/tests/PHPStan/Rules/Properties/data/bug-5336.php new file mode 100644 index 0000000000..7255342b42 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5336.php @@ -0,0 +1,50 @@ +query = $query; + } +} + +abstract class Test +{ + /** + * @var Pager + */ + private $pager; + + /** + * @template T of object + * @param class-string $originalClassName + * @return T&Stub + */ + abstract public function createStub(string $originalClassName): Stub; + + public function sayHello(): void + { + $query = $this->createStub(ProxyQueryInterface::class); + $this->pager = new Pager($query); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-5382.php b/tests/PHPStan/Rules/Properties/data/bug-5382.php new file mode 100644 index 0000000000..7ec41886da --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5382.php @@ -0,0 +1,73 @@ + + */ + public array $providers; + + /** + * @param non-empty-list<\stdClass>|null $providers + */ + public function __construct(?array $providers = null) + { + $this->providers = $providers ?? [ + new \stdClass(), + ]; + } +} + +class HelloWorld2 +{ + /** + * @var non-empty-list<\stdClass> + */ + public array $providers; + + /** + * @param non-empty-list<\stdClass>|null $providers + */ + public function __construct(?array $providers = null) + { + $this->providers = $providers ?? [ + new \stdClass(), + ]; + + $this->providers = $providers ?: [ + new \stdClass(), + ]; + + $this->providers = $providers !== null ? $providers : [ + new \stdClass(), + ]; + + $providers ??= [ + new \stdClass(), + ]; + $this->providers = $providers; + } +} + +class HelloWorld3 +{ + /** + * @var non-empty-list<\stdClass> + */ + public array $providers; + + /** + * @param non-empty-list<\stdClass>|null $providers + */ + public function __construct(?array $providers = null) + { + /** @var non-empty-list<\stdClass> $newList */ + $newList = [new \stdClass()]; + $newList2 = [new \stdClass()]; + + $this->providers = $providers ?? $newList; + $this->providers = $providers ?? $newList2; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6026.php b/tests/PHPStan/Rules/Properties/data/bug-6026.php new file mode 100644 index 0000000000..cc939a8036 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6026.php @@ -0,0 +1,23 @@ +datalen ?? 0; + $bucketLen = isset($bucket->datalen) ? $bucket->datalen : 0; + + return true; + } +} + diff --git a/tests/PHPStan/Rules/Properties/data/bug-6286.php b/tests/PHPStan/Rules/Properties/data/bug-6286.php index d349fb17d6..4967cd8a22 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-6286.php +++ b/tests/PHPStan/Rules/Properties/data/bug-6286.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 +> */ + private $lists; + + public function main(): void + { + for ($type = 0; $type < self::ENUM_COUNT; ++$type) + { + $this->lists[$type][] = true; + } + + print_r($this->lists); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6356b.php b/tests/PHPStan/Rules/Properties/data/bug-6356b.php new file mode 100644 index 0000000000..dee6859d3f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6356b.php @@ -0,0 +1,31 @@ + + */ + public array $nestedDetails; + + public function doSomething(): void + { + $this->details ['name'] = 'Douglas Adams'; + $this->details ['age'] = 'Forty-two'; + + $this->nestedDetails [] = [ + 'name' => 'Bilbo Baggins', + 'age' => 'Eleventy-one', + ]; + + $this->nestedDetails [12] ['age'] = 'Twelve'; + $this->nestedDetails [] ['age'] = 'Five'; + + $this->nestedDetails [99] ['name'] = 'nothing'; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6398.php b/tests/PHPStan/Rules/Properties/data/bug-6398.php new file mode 100644 index 0000000000..b1b824d541 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6398.php @@ -0,0 +1,32 @@ +>|null + */ + private static $threadLocalStorage = null; + + /** + * @param mixed $complexData the data to store + */ + protected function storeLocal(string $key, $complexData) : void{ + if(self::$threadLocalStorage === null){ + self::$threadLocalStorage = new \ArrayObject(); + } + self::$threadLocalStorage[spl_object_id($this)][$key] = $complexData; + } + + /** + * @return mixed + */ + protected function fetchLocal(string $key){ + $id = spl_object_id($this); + if(self::$threadLocalStorage === null or !isset(self::$threadLocalStorage[$id][$key])){ + throw new \InvalidArgumentException("No matching thread-local data found on this thread"); + } + + return self::$threadLocalStorage[$id][$key]; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6402.php b/tests/PHPStan/Rules/Properties/data/bug-6402.php new file mode 100644 index 0000000000..16f9c8c2aa --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6402.php @@ -0,0 +1,32 @@ += 8.1 + +namespace Bug6402; + +class SomeModel +{ + public readonly ?int $views; + + public function __construct(string $mode, int $views) + { + if ($mode === 'mode1') { + $this->views = $views; + } else { + $this->views = null; + } + } +} + +class SomeModel2 +{ + public readonly ?int $views; + + public function __construct(string $mode, int $views) + { + if ($mode === 'mode1') { + $this->views = $views; + } else { + echo $this->views; + $this->views = null; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6571.php b/tests/PHPStan/Rules/Properties/data/bug-6571.php new file mode 100644 index 0000000000..3ce06cc10d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6571.php @@ -0,0 +1,31 @@ += 7.4 + +namespace Bug6571; + +interface ClassLoader{} + +class HelloWorld +{ + /** @var \Threaded|\ClassLoader[]|null */ + private ?\Threaded $classLoaders = null; + + /** + * @param \ClassLoader[] $autoloaders + */ + public function setClassLoaders(?array $autoloaders = null) : void{ + if($autoloaders === null){ + $autoloaders = []; + } + + if($this->classLoaders === null){ + $this->classLoaders = new \Threaded(); + }else{ + foreach($this->classLoaders as $k => $autoloader){ + unset($this->classLoaders[$k]); + } + } + foreach($autoloaders as $autoloader){ + $this->classLoaders[] = $autoloader; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6757.php b/tests/PHPStan/Rules/Properties/data/bug-6757.php new file mode 100644 index 0000000000..1c5e30f734 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6757.php @@ -0,0 +1,17 @@ + */ + public Set $a; + + public function __construct() + { + $this->a = new Set(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6773.php b/tests/PHPStan/Rules/Properties/data/bug-6773.php new file mode 100644 index 0000000000..e83ed3166a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6773.php @@ -0,0 +1,18 @@ += 8.1 + +namespace Bug6773; + +final class Repository +{ + /** + * @param array $data + */ + public function __construct(private readonly array $data) + { + } + + public function remove(string $key): void + { + unset($this->data[$key]); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6809.php b/tests/PHPStan/Rules/Properties/data/bug-6809.php new file mode 100644 index 0000000000..54671e11e2 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6809.php @@ -0,0 +1,14 @@ +prop); + $string->prop ?? ""; + empty($string->prop); + } + + /** + * @param string|object $maybeString + * @return void + */ + public function bar($maybeString): void + { + isset($maybeString->prop); + $maybeString->prop ?? ""; + empty($maybeString->prop); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6922.php b/tests/PHPStan/Rules/Properties/data/bug-6922.php new file mode 100644 index 0000000000..5e0a49eec2 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6922.php @@ -0,0 +1,25 @@ += 8.1 + +namespace Bug6922; + +class Person { + + public function __construct( + public readonly string $name, + public readonly bool $isDeveloper, + public readonly bool $isAdmin + ) { + + } +} + +class Proof +{ + public function test(?Person $person): void + { + if ($person?->isDeveloper === FALSE || + $person?->isAdmin === FALSE) { + echo "Bug"; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7074.php b/tests/PHPStan/Rules/Properties/data/bug-7074.php new file mode 100644 index 0000000000..b59ce834f1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7074.php @@ -0,0 +1,24 @@ + + */ + protected $primaryKey; +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7087.php b/tests/PHPStan/Rules/Properties/data/bug-7087.php new file mode 100644 index 0000000000..209bfa62e0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7087.php @@ -0,0 +1,40 @@ += 8.1 + +namespace Bug7087; + +class Foo { + /** + * @var array, mixed> $array1 + */ + public readonly array $array1; + /** + * @var array, mixed> $array2 + */ + public readonly array $array2; + + /** + * @param array, mixed> $param + */ + public function __construct(array $param) { + $this->array1 = $this->foo($param); + $this->array2 = $this->bar($param); + } + + /** + * @param array, mixed> $param + * @return array, mixed> + */ + private function foo(array $param): array { + return $param; + } + + /** + * @template IKey + * @template IValue + * @param array $param + * @return array + */ + private function bar(array $param): array { + return $param; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7109.php b/tests/PHPStan/Rules/Properties/data/bug-7109.php new file mode 100644 index 0000000000..6c539b60f8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7109.php @@ -0,0 +1,77 @@ += 8.0 + +namespace Bug7109; + +class HelloWorld +{ + public int $aaa = 5; + /** + * @return HelloWorld|null + */ + public function get(): ?HelloWorld + { + return rand() ? $this : null; + } + public function sayHello(): void + { + $this->get()?->aaa ?? 6; + isset($this->get()?->aaa) ?: 6; + empty($this->get()?->aaa) ?: 6; + } + + public function moreExamples(): void + { + $foo = null; + if (rand(0, 1)) { + $foo = new self(); + } + $foo->get()?->aaa ?? 6; + isset($foo->get()?->aaa) ?: 6; + empty($foo->get()?->aaa) ?: 6; + } + + public function getNotNull(): HelloWorld + { + return $this; + } + + public function notNullableExamples(): void + { + $this->getNotNull()?->aaa ?? 6; + isset($this->getNotNull()?->aaa) ?: 6; + empty($this->getNotNull()?->aaa) ?: 6; + } + + /** @var positive-int */ + public int $notFalsy = 5; + + public function emptyNotFalsy(): void + { + $foo = null; + if (rand(0, 1)) { + $foo = new self(); + } + empty($foo->get()?->notFalsy) ?: 6; + } + + public function emptyNotFalsy2(): void + { + empty($this->getNotNull()?->notFalsy) ?: 6; + } + + public ?HelloWorld $prop = null; + public function edgeCaseWithMethodCall(): void + { + // only ?->aaa should be reported + $this->get()?->prop?->get()?->aaa ?? 'edge'; + isset($this->get()?->prop?->get()?->aaa) ?: 'edge'; + empty($this->get()?->prop?->get()?->aaa) ?: 'edge'; + } + + public function fetchByExpr(): void + { + $this?->{'aaa'} ?? 'edge'; + isset($this?->{'aaa'}) ?: 'edge'; + empty($this?->{'aaa'}) ?: 'edge'; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7119.php b/tests/PHPStan/Rules/Properties/data/bug-7119.php new file mode 100644 index 0000000000..4788009633 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7119.php @@ -0,0 +1,23 @@ += 8.1 +declare(strict_types=1); + +namespace Bug7119; + +final class FooBar +{ + private readonly mixed $value; + + /** + * @param array{value: mixed} $data + */ + public function __construct(array $data) + { + //$this->value = $data['value']; // This triggers no PHPStan error. + ['value' => $this->value] = $data; // This triggers PHPStan error. + } + + public function getValue(): mixed + { + return $this->value; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7190.php b/tests/PHPStan/Rules/Properties/data/bug-7190.php new file mode 100644 index 0000000000..7857f45a73 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7190.php @@ -0,0 +1,22 @@ + $array + */ + public function sayHello(array $array, MyObject $object): int + { + if (!isset($array[$object->getId()])) { + return 1; + } + + return $array[$object->getId()] ?? 2; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7198.php b/tests/PHPStan/Rules/Properties/data/bug-7198.php new file mode 100644 index 0000000000..75d9ab0af5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7198.php @@ -0,0 +1,88 @@ += 8.1 + +namespace Bug7198; + +trait TestTrait { + public function foo(): void + { + $this->callee->foo(); + } +} + +class TestCallee { + public function foo(): void + { + echo "FOO\n"; + } +} + +class TestCaller { + use TestTrait; + + public function __construct(private readonly TestCallee $callee) + { + $this->foo(); + } +} + +class TestCaller2 { + public function foo(): void + { + $this->callee->foo(); + } + + public function __construct(private readonly TestCallee $callee) + { + $this->foo(); + } +} + +class TestCaller3 { + + public function __construct(private readonly TestCallee $callee) + { + $this->foo(); + } + + public function foo(): void + { + $this->callee->foo(); + } +} + +trait Identifiable +{ + public readonly int $id; + + public function __construct() + { + $this->id = rand(); + } +} + +trait CreateAware +{ + public readonly \DateTimeImmutable $createdAt; + + public function __construct() + { + $this->createdAt = new \DateTimeImmutable(); + } +} + +abstract class Entity +{ + use Identifiable { + Identifiable::__construct as private __identifiableConstruct; + } + + use CreateAware { + CreateAware::__construct as private __createAwareConstruct; + } + + public function __construct() + { + $this->__identifiableConstruct(); + $this->__createAwareConstruct(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7200.php b/tests/PHPStan/Rules/Properties/data/bug-7200.php new file mode 100644 index 0000000000..3444a33d9b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7200.php @@ -0,0 +1,21 @@ += 8.0 + +namespace Bug7200; + +class HelloWorld +{ + /** + * @param class-string|null $class + */ + public function __construct(public ?Model $model = null, public ?string $class = null) + { + if ($model instanceof One && $model instanceof Two && $model instanceof Three) { + $this->class ??= $model::class; + } + } +} + +class Model {} +interface One {} +interface Two {} +interface Three {} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7219.php b/tests/PHPStan/Rules/Properties/data/bug-7219.php new file mode 100644 index 0000000000..cb663e6697 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7219.php @@ -0,0 +1,31 @@ +email; + } + + + public function setEmail(string $email): void + { + $this->email = $email; + } +} + +$foo = new Foo(); +echo $foo->getEmail(); // error Typed property must not be accessed before initialization +echo $foo->id; diff --git a/tests/PHPStan/Rules/Properties/data/bug-7314.php b/tests/PHPStan/Rules/Properties/data/bug-7314.php new file mode 100644 index 0000000000..25771639da --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7314.php @@ -0,0 +1,24 @@ += 8.1 + +namespace Bug7314; + +class UserId1 +{ + public function __construct( + public readonly int $id, + ) { + } +} + +trait HasId +{ + public function __construct( + public readonly int $id, + ) { + } +} +class UserId2 +{ + use HasId; +} + diff --git a/tests/PHPStan/Rules/Properties/data/bug-7318.php b/tests/PHPStan/Rules/Properties/data/bug-7318.php new file mode 100644 index 0000000000..f9dc46d102 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7318.php @@ -0,0 +1,33 @@ + $types */ + $types = ['Foo' => ['prop' => ['unique' => true]]]; + + if ($types['Bar']['prop']['unique'] ?? false) { + } + + if (isset($types['Bar']['prop']['unique'])) { + } + + if (empty($types['Bar']['prop']['unique'])) { + } + + /** @var array{Bar: array{prop: array{unique: boolean}}} $types */ + $types = ['Bar' => ['prop' => ['unique' => true]]]; + + if ($types['Bar']['prop']['unique'] ?? false) { + } + + if (isset($types['Bar']['prop']['unique'])) { + } + + if (empty($types['Bar']['prop']['unique'])) { + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7361.php b/tests/PHPStan/Rules/Properties/data/bug-7361.php new file mode 100644 index 0000000000..23c530167d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7361.php @@ -0,0 +1,14 @@ += 8.1 + +namespace Bug7361; + +class Example { + public function __construct( + /** @readonly */ + public int $foo + ) {} + + public function doStuff(): void { + $this->foo = 7; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7640.php b/tests/PHPStan/Rules/Properties/data/bug-7640.php new file mode 100644 index 0000000000..6321a23416 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7640.php @@ -0,0 +1,57 @@ += 8.0 + +namespace Bug7640; + +class C +{ +} + +class P +{ + private ?C $_connection = null; + + public function getConnection(): C + { + $this->_connection = new C(); + + return $this->_connection; + } + + public static function connect(): P + { + return new P(); + } + + public static function assertInstanceOf(object $object): static + { + if (!$object instanceof static) { + throw new \TypeError('Object is not an instance of static class'); + } + + return $object; + } +} + +abstract class TestCase +{ + protected function createPWithLazyConnect(): void + { + new class() extends P + { + public function __construct() + { + } + + public function getConnection(): C + { + \Closure::bind(function () { + if ($this->_connection === null) { + $connection = P::assertInstanceOf(P::connect())->_connection; + } + }, null, P::class)(); + + return parent::getConnection(); + } + }; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7649.php b/tests/PHPStan/Rules/Properties/data/bug-7649.php new file mode 100644 index 0000000000..b8f4ec0bfd --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7649.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug7649; + +class Foo +{ + public readonly string $bar; + + public function __construct(bool $flag) + { + if ($flag) { + $this->bar = 'baz'; + } else { + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7692.php b/tests/PHPStan/Rules/Properties/data/bug-7692.php new file mode 100644 index 0000000000..c8fbe7a5a7 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7692.php @@ -0,0 +1,54 @@ + + */ + protected static $entityClass; + } +} + +namespace BaseNamespace7692\Entity { + + interface EntityBaseInterface + { + + } +} + +namespace DeepInheritingNamespace7692 { + + use InheritingNamespace7692\TheIntermediateService; + use DeepInheritingNamespace7692\Entity\TheEntity; + + final class TheChildService extends TheIntermediateService + { + protected static $entityClass = TheEntity::class; + } +} + +namespace DeepInheritingNamespace7692\Entity { + + use BaseNamespace7692\Entity\EntityBaseInterface; + + final class TheEntity implements EntityBaseInterface + { + + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7789.php b/tests/PHPStan/Rules/Properties/data/bug-7789.php new file mode 100644 index 0000000000..6fdf12b477 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7789.php @@ -0,0 +1,14 @@ +table); + assertType('Bug7839\\A|string', $b->table); + assertType('Bug7839\\A|string', $c->table); +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-7933.php b/tests/PHPStan/Rules/Properties/data/bug-7933.php new file mode 100644 index 0000000000..4437eb7d83 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7933.php @@ -0,0 +1,617 @@ + + */ + private static array $countryISOMapping = [ + 'AC' => CountryIso3Enum::ASC, + 'AD' => CountryIso3Enum::AND, + 'AE' => CountryIso3Enum::ARE, + 'AF' => CountryIso3Enum::AFG, + 'AG' => CountryIso3Enum::ATG, + 'AI' => CountryIso3Enum::AIA, + 'AL' => CountryIso3Enum::ALB, + 'AM' => CountryIso3Enum::ARM, + 'AN' => CountryIso3Enum::ANT, + 'AO' => CountryIso3Enum::AGO, + 'AQ' => CountryIso3Enum::ATA, + 'AR' => CountryIso3Enum::ARG, + 'AS' => CountryIso3Enum::ASM, + 'AT' => CountryIso3Enum::AUT, + 'AU' => CountryIso3Enum::AUS, + 'AW' => CountryIso3Enum::ABW, + 'AX' => CountryIso3Enum::ALA, + 'AZ' => CountryIso3Enum::AZE, + 'BA' => CountryIso3Enum::BIH, + 'BB' => CountryIso3Enum::BRB, + 'BD' => CountryIso3Enum::BGD, + 'BE' => CountryIso3Enum::BEL, + 'BF' => CountryIso3Enum::BFA, + 'BG' => CountryIso3Enum::BGR, + 'BH' => CountryIso3Enum::BHR, + 'BI' => CountryIso3Enum::BDI, + 'BJ' => CountryIso3Enum::BEN, + 'BL' => CountryIso3Enum::BLM, + 'BM' => CountryIso3Enum::BMU, + 'BN' => CountryIso3Enum::BRN, + 'BO' => CountryIso3Enum::BOL, + 'BQ' => CountryIso3Enum::BES, + 'BR' => CountryIso3Enum::BRA, + 'BS' => CountryIso3Enum::BHS, + 'BT' => CountryIso3Enum::BTN, + 'BU' => CountryIso3Enum::BUR, + 'BV' => CountryIso3Enum::BVT, + 'BW' => CountryIso3Enum::BWA, + 'BY' => CountryIso3Enum::BLR, + 'BZ' => CountryIso3Enum::BLZ, + 'CA' => CountryIso3Enum::CAN, + 'CC' => CountryIso3Enum::CCK, + 'CD' => CountryIso3Enum::COD, + 'CE' => CountryIso3Enum::CEE, + 'CF' => CountryIso3Enum::CAF, + 'CG' => CountryIso3Enum::COG, + 'CH' => CountryIso3Enum::CHE, + 'CI' => CountryIso3Enum::CIV, + 'CK' => CountryIso3Enum::COK, + 'CL' => CountryIso3Enum::CHL, + 'CM' => CountryIso3Enum::CMR, + 'CN' => CountryIso3Enum::CHN, + 'CO' => CountryIso3Enum::COL, + 'CP' => CountryIso3Enum::CPT, + 'CR' => CountryIso3Enum::CRI, + 'CS' => CountryIso3Enum::SCG, + 'CU' => CountryIso3Enum::CUB, + 'CV' => CountryIso3Enum::CPV, + 'CW' => CountryIso3Enum::CUW, + 'CX' => CountryIso3Enum::CXR, + 'CY' => CountryIso3Enum::CYP, + 'CZ' => CountryIso3Enum::CZE, + 'DE' => CountryIso3Enum::DEU, + 'DG' => CountryIso3Enum::DGA, + 'DJ' => CountryIso3Enum::DJI, + 'DK' => CountryIso3Enum::DNK, + 'DM' => CountryIso3Enum::DMA, + 'DO' => CountryIso3Enum::DOM, + 'DZ' => CountryIso3Enum::DZA, + 'EC' => CountryIso3Enum::ECU, + 'EE' => CountryIso3Enum::EST, + 'EG' => CountryIso3Enum::EGY, + 'EH' => CountryIso3Enum::ESH, + 'ER' => CountryIso3Enum::ERI, + 'ES' => CountryIso3Enum::ESP, + 'ET' => CountryIso3Enum::ETH, + 'FI' => CountryIso3Enum::FIN, + 'FJ' => CountryIso3Enum::FJI, + 'FK' => CountryIso3Enum::FLK, + 'FM' => CountryIso3Enum::FSM, + 'FO' => CountryIso3Enum::FRO, + 'FR' => CountryIso3Enum::FRA, + 'FX' => CountryIso3Enum::FXX, + 'GA' => CountryIso3Enum::GAB, + 'GB' => CountryIso3Enum::GBR, + 'GD' => CountryIso3Enum::GRD, + 'GE' => CountryIso3Enum::GEO, + 'GF' => CountryIso3Enum::GUF, + 'GG' => CountryIso3Enum::GGY, + 'GH' => CountryIso3Enum::GHA, + 'GI' => CountryIso3Enum::GIB, + 'GL' => CountryIso3Enum::GRL, + 'GM' => CountryIso3Enum::GMB, + 'GN' => CountryIso3Enum::GIN, + 'GP' => CountryIso3Enum::GLP, + 'GQ' => CountryIso3Enum::GNQ, + 'GR' => CountryIso3Enum::GRC, + 'GS' => CountryIso3Enum::SGS, + 'GT' => CountryIso3Enum::GTM, + 'GU' => CountryIso3Enum::GUM, + 'GW' => CountryIso3Enum::GNB, + 'GY' => CountryIso3Enum::GUY, + 'HK' => CountryIso3Enum::HKG, + 'HM' => CountryIso3Enum::HMD, + 'HN' => CountryIso3Enum::HND, + 'HR' => CountryIso3Enum::HRV, + 'HT' => CountryIso3Enum::HTI, + 'HU' => CountryIso3Enum::HUN, + 'ID' => CountryIso3Enum::IDN, + 'IE' => CountryIso3Enum::IRL, + 'IL' => CountryIso3Enum::ISR, + 'IM' => CountryIso3Enum::IMN, + 'IN' => CountryIso3Enum::IND, + 'IO' => CountryIso3Enum::IOT, + 'IQ' => CountryIso3Enum::IRQ, + 'IR' => CountryIso3Enum::IRN, + 'IS' => CountryIso3Enum::ISL, + 'IT' => CountryIso3Enum::ITA, + 'JE' => CountryIso3Enum::JEY, + 'JM' => CountryIso3Enum::JAM, + 'JO' => CountryIso3Enum::JOR, + 'JP' => CountryIso3Enum::JPN, + 'KE' => CountryIso3Enum::KEN, + 'KG' => CountryIso3Enum::KGZ, + 'KH' => CountryIso3Enum::KHM, + 'KI' => CountryIso3Enum::KIR, + 'KM' => CountryIso3Enum::COM, + 'KN' => CountryIso3Enum::KNA, + 'KP' => CountryIso3Enum::PRK, + 'KR' => CountryIso3Enum::KOR, + 'KW' => CountryIso3Enum::KWT, + 'KY' => CountryIso3Enum::CYM, + 'KZ' => CountryIso3Enum::KAZ, + 'LA' => CountryIso3Enum::LAO, + 'LB' => CountryIso3Enum::LBN, + 'LC' => CountryIso3Enum::LCA, + 'LI' => CountryIso3Enum::LIE, + 'LK' => CountryIso3Enum::LKA, + 'LR' => CountryIso3Enum::LBR, + 'LS' => CountryIso3Enum::LSO, + 'LT' => CountryIso3Enum::LTU, + 'LU' => CountryIso3Enum::LUX, + 'LV' => CountryIso3Enum::LVA, + 'LY' => CountryIso3Enum::LBY, + 'MA' => CountryIso3Enum::MAR, + 'MC' => CountryIso3Enum::MCO, + 'MD' => CountryIso3Enum::MDA, + 'ME' => CountryIso3Enum::MNE, + 'MF' => CountryIso3Enum::MAF, + 'MG' => CountryIso3Enum::MDG, + 'MH' => CountryIso3Enum::MHL, + 'MK' => CountryIso3Enum::MKD, + 'ML' => CountryIso3Enum::MLI, + 'MM' => CountryIso3Enum::MMR, + 'MN' => CountryIso3Enum::MNG, + 'MO' => CountryIso3Enum::MAC, + 'MP' => CountryIso3Enum::MNP, + 'MQ' => CountryIso3Enum::MTQ, + 'MR' => CountryIso3Enum::MRT, + 'MS' => CountryIso3Enum::MSR, + 'MT' => CountryIso3Enum::MLT, + 'MU' => CountryIso3Enum::MUS, + 'MV' => CountryIso3Enum::MDV, + 'MW' => CountryIso3Enum::MWI, + 'MX' => CountryIso3Enum::MEX, + 'MY' => CountryIso3Enum::MYS, + 'MZ' => CountryIso3Enum::MOZ, + 'NA' => CountryIso3Enum::NAM, + 'NC' => CountryIso3Enum::NCL, + 'NE' => CountryIso3Enum::NER, + 'NF' => CountryIso3Enum::NFK, + 'NG' => CountryIso3Enum::NGA, + 'NI' => CountryIso3Enum::NIC, + 'NL' => CountryIso3Enum::NLD, + 'NO' => CountryIso3Enum::NOR, + 'NP' => CountryIso3Enum::NPL, + 'NR' => CountryIso3Enum::NRU, + 'NT' => CountryIso3Enum::NTZ, + 'NU' => CountryIso3Enum::NIU, + 'NZ' => CountryIso3Enum::NZL, + 'OM' => CountryIso3Enum::OMN, + 'PA' => CountryIso3Enum::PAN, + 'PE' => CountryIso3Enum::PER, + 'PF' => CountryIso3Enum::PYF, + 'PG' => CountryIso3Enum::PNG, + 'PH' => CountryIso3Enum::PHL, + 'PK' => CountryIso3Enum::PAK, + 'PL' => CountryIso3Enum::POL, + 'PM' => CountryIso3Enum::SPM, + 'PN' => CountryIso3Enum::PCN, + 'PR' => CountryIso3Enum::PRI, + 'PS' => CountryIso3Enum::PSE, + 'PT' => CountryIso3Enum::PRT, + 'PW' => CountryIso3Enum::PLW, + 'PY' => CountryIso3Enum::PRY, + 'QA' => CountryIso3Enum::QAT, + 'RE' => CountryIso3Enum::REU, + 'RO' => CountryIso3Enum::ROU, + 'RS' => CountryIso3Enum::SRB, + 'RU' => CountryIso3Enum::RUS, + 'RW' => CountryIso3Enum::RWA, + 'SA' => CountryIso3Enum::SAU, + 'SB' => CountryIso3Enum::SLB, + 'SC' => CountryIso3Enum::SYC, + 'SD' => CountryIso3Enum::SDN, + 'SE' => CountryIso3Enum::SWE, + 'SG' => CountryIso3Enum::SGP, + 'SH' => CountryIso3Enum::SHN, + 'SI' => CountryIso3Enum::SVN, + 'SJ' => CountryIso3Enum::SJM, + 'SK' => CountryIso3Enum::SVK, + 'SL' => CountryIso3Enum::SLE, + 'SM' => CountryIso3Enum::SMR, + 'SN' => CountryIso3Enum::SEN, + 'SO' => CountryIso3Enum::SOM, + 'SR' => CountryIso3Enum::SUR, + 'SS' => CountryIso3Enum::SSD, + 'ST' => CountryIso3Enum::STP, + 'SU' => CountryIso3Enum::SUN, + 'SV' => CountryIso3Enum::SLV, + 'SX' => CountryIso3Enum::SXM, + 'SY' => CountryIso3Enum::SYR, + 'SZ' => CountryIso3Enum::SWZ, + 'TA' => CountryIso3Enum::TAA, + 'TC' => CountryIso3Enum::TCA, + 'TD' => CountryIso3Enum::TCD, + 'TF' => CountryIso3Enum::ATF, + 'TG' => CountryIso3Enum::TGO, + 'TH' => CountryIso3Enum::THA, + 'TJ' => CountryIso3Enum::TJK, + 'TK' => CountryIso3Enum::TKL, + 'TL' => CountryIso3Enum::TLS, + 'TM' => CountryIso3Enum::TKM, + 'TN' => CountryIso3Enum::TUN, + 'TO' => CountryIso3Enum::TON, + 'TR' => CountryIso3Enum::TUR, + 'TT' => CountryIso3Enum::TTO, + 'TV' => CountryIso3Enum::TUV, + 'TW' => CountryIso3Enum::TWN, + 'TZ' => CountryIso3Enum::TZA, + 'UA' => CountryIso3Enum::UKR, + 'UG' => CountryIso3Enum::UGA, + 'UK' => CountryIso3Enum::GBR, + 'UM' => CountryIso3Enum::UMI, + 'US' => CountryIso3Enum::USA, + 'UY' => CountryIso3Enum::URY, + 'UZ' => CountryIso3Enum::UZB, + 'VA' => CountryIso3Enum::VAT, + 'VC' => CountryIso3Enum::VCT, + 'VE' => CountryIso3Enum::VEN, + 'VG' => CountryIso3Enum::VGB, + 'VI' => CountryIso3Enum::VIR, + 'VN' => CountryIso3Enum::VNM, + 'VU' => CountryIso3Enum::VUT, + 'WF' => CountryIso3Enum::WLF, + 'WS' => CountryIso3Enum::WSM, + 'XK' => CountryIso3Enum::XXK, + 'YE' => CountryIso3Enum::YEM, + 'YT' => CountryIso3Enum::MYT, + 'YU' => CountryIso3Enum::YUG, + 'ZA' => CountryIso3Enum::ZAF, + 'ZM' => CountryIso3Enum::ZMB, + 'ZR' => CountryIso3Enum::ZAR, + 'ZW' => CountryIso3Enum::ZWE, + ]; +} + +class EnumValueMapper +{ + + /** + * @return array + */ + public function getMap(): array + { + return self::$map; + } + + /** @phpstan-var array */ + private static $map = [ + 'foo3' => MyEnum::VALUE_1, + 'foo2' => MyEnum::VALUE_2, + 'foo1' => MyEnum::VALUE_3, + ]; +} + +class MyEnum +{ + public const VALUE_1 = 'value1'; + public const VALUE_2 = 'value2'; + public const VALUE_3 = 'value3'; +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8074.php b/tests/PHPStan/Rules/Properties/data/bug-8074.php new file mode 100644 index 0000000000..7bedabd03b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8074.php @@ -0,0 +1,103 @@ += 8.0 + +namespace Bug8074; + +use ReflectionClass; +use ReflectionClassConstant; +use TypeError; +use UnexpectedValueException; + +/** + * @template K + * @template T + * @template L + * @template U + * + * @param iterable $stream + * @param callable(T, K): iterable $fn + * + * @return \Generator + */ +function scollect(iterable $stream, callable $fn): \Generator +{ + foreach ($stream as $key => $value) { + yield from $fn($value, $key); + } +} + +/** + * @template K of array-key + * @template T + * @template L of array-key + * @template U + * + * @param array $array + * @param callable(T, K): iterable $fn + * + * @return array + */ +function collectWithKeys(array $array, callable $fn): array +{ + $values = []; + $counter = 0; + + foreach (scollect($array, $fn) as $key => $value) { + try { + $values[$key] = $value; + } catch (TypeError $e) { + throw new UnexpectedValueException('The key yielded in the callable is not compatible with the type "array-key".'); + } + + ++$counter; + } + + if ($counter !== count($values)) { + throw new UnexpectedValueException( + 'Data loss occurred because of duplicated keys. Use `collect()` if you do not care about ' . + 'the yielded keys, or use `scollect()` if you need to support duplicated keys (as arrays cannot).' + ); + } + + return $values; +} + +function __(string $message, bool $capitalize = true): string +{ + // some fake translation function + return $capitalize ? ucfirst($message) : $message; +} + +final class CsvExport +{ + public const COLUMN_A = 'something'; + public const COLUMN_B = 'else'; + public const COLUMN_C = 'entirely'; + + /** + * @var array The translated header as value + */ + private static array $headers; + + /** + * @return array + */ + public static function getHeaders(): array + { + if (!isset(self::$headers)) { + /** Using [at]var array $headers here would fix the inspection */ + $headers = collectWithKeys( + (new ReflectionClass(self::class))->getReflectionConstants(), + static function (ReflectionClassConstant $constant): iterable { + /** @var self::COLUMN_* $value */ + $value = $constant->getValue(); + + yield $value => __(sprintf('activities.export.urbanus.csv_header.%s', $value), capitalize: false); + }, + ); + + self::$headers = $headers; + } + + return self::$headers; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8101.php b/tests/PHPStan/Rules/Properties/data/bug-8101.php new file mode 100644 index 0000000000..09010120d9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8101.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug8101; + +class A { + public function __construct(public readonly int $myProp) {} +} + +class B extends A { + // This should be reported as an error, as a readonly prop cannot be redeclared. + public function __construct(public readonly int $myProp) { + parent::__construct($myProp); + } +} + +$foo = new B(7); diff --git a/tests/PHPStan/Rules/Properties/data/bug-8190.php b/tests/PHPStan/Rules/Properties/data/bug-8190.php new file mode 100644 index 0000000000..d248584926 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8190.php @@ -0,0 +1,57 @@ +ownerBackup = $ownerBackup ?? [ + 'name' => 'Deleted', + ]; + } + + + /** + * @param OwnerBackup|null $ownerBackup + */ + public function setOwnerBackup(?array $ownerBackup): void + { + $this->ownerBackup = $ownerBackup ?: [ + 'name' => 'Deleted', + ]; + } + + /** + * @param OwnerBackup|null $ownerBackup + */ + public function setOwnerBackupWorksForSomeReason(?array $ownerBackup): void + { + $this->ownerBackup = $ownerBackup !== null ? $ownerBackup : [ + 'name' => 'Deleted', + ]; + } + + /** + * @param OwnerBackup|null $ownerBackup + */ + public function setOwnerBackupAlsoWorksForSomeReason(?array $ownerBackup): void + { + if ($ownerBackup) { + $this->ownerBackup = $ownerBackup; + } else { + $this->ownerBackup = [ + 'name' => 'Deleted', + ]; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8222.php b/tests/PHPStan/Rules/Properties/data/bug-8222.php new file mode 100644 index 0000000000..047614c377 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8222.php @@ -0,0 +1,14 @@ + */ + public array $values; + + public function addValue(string $value): void + { + $this->values[] = $value; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8333.php b/tests/PHPStan/Rules/Properties/data/bug-8333.php new file mode 100644 index 0000000000..ee2395a389 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8333.php @@ -0,0 +1,75 @@ += 8.1 + +namespace Bug8412; + +use InvalidArgumentException; + +enum Zustand: string +{ + case Failed = 'failed'; + case Pending = 'pending'; +} + +final class HelloWorld +{ + public readonly ?int $value; + + public function __construct(Zustand $zustand) + { + $this->value = match ($zustand) { + Zustand::Failed => 1, + Zustand::Pending => 2, + default => throw new InvalidArgumentException('Unknown Zustand: ' . $zustand->value), + }; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8563.php b/tests/PHPStan/Rules/Properties/data/bug-8563.php new file mode 100644 index 0000000000..bfd0f75b07 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8563.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug8563; + +class BankAccount { + + readonly string $bic; + readonly string $iban; + readonly string $label; + + function __construct(object $data = new \stdClass) { + $this->bic = $data->bic ?? ""; + $this->iban = $data->iban ?? ""; + $this->label = $data->label ?? ""; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8629.php b/tests/PHPStan/Rules/Properties/data/bug-8629.php new file mode 100644 index 0000000000..b8fc89ee21 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8629.php @@ -0,0 +1,10 @@ +nodeType); +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-8929.php b/tests/PHPStan/Rules/Properties/data/bug-8929.php new file mode 100755 index 0000000000..4138ce73c9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8929.php @@ -0,0 +1,19 @@ += 8.1 + +namespace Bug8929; + +class Test +{ + /** @var \WeakMap */ + protected readonly \WeakMap $cache; + + public function __construct() + { + $this->cache = new \WeakMap(); + } + + public function add(object $key, mixed $value): void + { + $this->cache[$key] = $value; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8958.php b/tests/PHPStan/Rules/Properties/data/bug-8958.php new file mode 100644 index 0000000000..21b375bd53 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8958.php @@ -0,0 +1,69 @@ += 8.1 + +namespace Bug8958; + +interface TimeRangeInterface +{ + public function getStart(): \DateTimeInterface; + + public function getEnd(): \DateTimeInterface; +} + +trait TimeRangeTrait +{ + private readonly \DateTimeImmutable $start; + + private readonly \DateTimeImmutable $end; + + public function getStart(): \DateTimeImmutable + { + return $this->start; // @phpstan-ignore-line + } + + public function getEnd(): \DateTimeImmutable + { + return $this->end; // @phpstan-ignore-line + } + + private function initTimeRange( + \DateTimeInterface $start, + \DateTimeInterface $end + ): void { + $this->start = \DateTimeImmutable::createFromInterface($start); // @phpstan-ignore-line + $this->end = \DateTimeImmutable::createFromInterface($end); // @phpstan-ignore-line + } +} + +class Foo implements TimeRangeInterface { + use TimeRangeTrait; + + public function __construct(\DateTimeInterface $start, \DateTimeInterface $end) + { + $this->initTimeRange($start, $end); + } +} + +class Bar implements TimeRangeInterface { + use TimeRangeTrait; + + public function __construct( + private TimeRangeInterface $first, + private TimeRangeInterface $second, + ?\DateTimeInterface $start = null, + \DateTimeInterface $end = null + ) { + $this->initTimeRange( + $start ?? max($first->getStart(), $second->getStart()), + $end ?? min($first->getEnd(), $second->getEnd()), + ); + } + + public function getFirst(): TimeRangeInterface + { + return $this->first; + } + public function getSecond(): TimeRangeInterface + { + return $this->second; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-9131.php b/tests/PHPStan/Rules/Properties/data/bug-9131.php new file mode 100644 index 0000000000..e636ede694 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9131.php @@ -0,0 +1,13 @@ +, string> */ + public array $l = []; + + public function add(string $s): void { + $this->l[] = $s; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-9619.php b/tests/PHPStan/Rules/Properties/data/bug-9619.php new file mode 100644 index 0000000000..62e2cd5e7e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9619.php @@ -0,0 +1,59 @@ +user->isLoggedIn()) { + // do something + } + } +} + +class AdminPresenter2 +{ + private User $user; + + public function __construct(User $user) + { + $this->user = $user; + } + + public function startup() + { + // do not report uninitialized property - it's initialized for sure + if (!$this->user->isLoggedIn()) { + // do something + } + } +} + +class AdminPresenter3 +{ + private \stdClass $user; + + public function startup() + { + $this->user = new \stdClass(); + } + + public function startup2() + { + // we cannot be sure which additional constructor gets called first + if (!$this->user->loggedIn) { + // do something + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-9694.php b/tests/PHPStan/Rules/Properties/data/bug-9694.php new file mode 100644 index 0000000000..96cd448073 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9694.php @@ -0,0 +1,20 @@ += 8.0 + +class TotpEnrollment +{ + public bool $confirmed; +} + +class User +{ + public ?TotpEnrollment $totpEnrollment; +} + +function () { + $user = new User(); + + return match ($user->totpEnrollment === null) { + true => false, + false => $user->totpEnrollment->confirmed, + }; +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-9831.php b/tests/PHPStan/Rules/Properties/data/bug-9831.php new file mode 100644 index 0000000000..da1161be77 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9831.php @@ -0,0 +1,21 @@ += 8.1 + +namespace Bug9831; + +class Foo +{ + private string $bar; + + public function __construct() + { + $var = function (): void { + echo $this->bar; + }; + + $this->bar = '123'; + + $var = function (): void { + echo $this->bar; + }; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-9863.php b/tests/PHPStan/Rules/Properties/data/bug-9863.php new file mode 100644 index 0000000000..49d8f404ad --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9863.php @@ -0,0 +1,49 @@ += 8.1 + +namespace Bug9863; + +class ReadonlyParentWithoutIsset +{ + public function __construct( + public readonly int $foo + ) {} +} + +class ReadonlyChildWithoutIsset extends ReadonlyParentWithoutIsset +{ + public function __construct( + public readonly int $foo = 42 + ) { + parent::__construct($foo); + } +} + +class ReadonlyParentWithIsset +{ + public readonly int $foo; + + public function __construct( + int $foo + ) { + if (! isset($this->foo)) { + $this->foo = $foo; + } + } +} + +class ReadonlyChildWithIsset extends ReadonlyParentWithIsset +{ + public function __construct( + public readonly int $foo = 42 + ) { + parent::__construct($foo); + } +} + +$a = new ReadonlyParentWithoutIsset(0); +$b = new ReadonlyChildWithoutIsset(); +$c = new ReadonlyChildWithoutIsset(1); + +$x = new ReadonlyParentWithIsset(2); +$y = new ReadonlyChildWithIsset(); +$z = new ReadonlyChildWithIsset(3); diff --git a/tests/PHPStan/Rules/Properties/data/bug-9864.php b/tests/PHPStan/Rules/Properties/data/bug-9864.php new file mode 100644 index 0000000000..4790a1a5ae --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9864.php @@ -0,0 +1,31 @@ += 8.2 + +namespace Bug9864; + +readonly abstract class UuidValueObject +{ + public function __construct(public string $value) + { + $this->ensureIsValidUuid($value); + } + + private function ensureIsValidUuid(string $value): void + { + } +} + + +final readonly class ProductId extends UuidValueObject +{ + public string $value; + + public function __construct( + string $value + ) { + parent::__construct($value); + } +} + +var_dump(new ProductId('test')); + +// property is assigned on parent class, no need to reassing, specially for readonly properties diff --git a/tests/PHPStan/Rules/Properties/data/conflicting-annotation-property.php b/tests/PHPStan/Rules/Properties/data/conflicting-annotation-property.php new file mode 100644 index 0000000000..9fd7174acb --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/conflicting-annotation-property.php @@ -0,0 +1,28 @@ +test = 1; + } + + public function doFoo2() + { + echo $this->test; + } + +} + +function (PropertyWithAnnotation $p): void { + echo $p->test; + $p->test = 1; +}; diff --git a/tests/PHPStan/Rules/Properties/data/dynamic-properties.php b/tests/PHPStan/Rules/Properties/data/dynamic-properties.php new file mode 100644 index 0000000000..d354c028ea --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/dynamic-properties.php @@ -0,0 +1,51 @@ +dynamicProperty); + empty($this->dynamicProperty); + $this->dynamicProperty ?? 'test'; + + $bar = new Bar(); + isset($bar->dynamicProperty); + empty($bar->dynamicProperty); + $bar->dynamicProperty ?? 'test'; + } + + public function doBaz(Bar $bar) { + isset($bar->dynamicProperty); + empty($bar->dynamicProperty); + $bar->dynamicProperty ?? 'test'; + } +} + +#[\AllowDynamicProperties] +class Baz { + public function doBaz() { + echo $this->dynamicProperty; + } + public function doBar() { + isset($this->dynamicProperty); + empty($this->dynamicProperty); + $this->dynamicProperty ?? 'test'; + } +} + +final class FinalBar {} + +final class FinalFoo { + public function doBar() { + isset($this->dynamicProperty); + empty($this->dynamicProperty); + $this->dynamicProperty ?? 'test'; + + $bar = new FinalBar(); + isset($bar->dynamicProperty); + empty($bar->dynamicProperty); + $bar->dynamicProperty ?? 'test'; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/efabrica-latte-bug.php b/tests/PHPStan/Rules/Properties/data/efabrica-latte-bug.php new file mode 100644 index 0000000000..cc205ab42c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/efabrica-latte-bug.php @@ -0,0 +1,100 @@ + */ + private array $templateFiles = []; + + /** + * @param string[] $analysedPaths + */ + public function __construct(FileExcluder $fileExcluder, array $analysedPaths, bool $reportUnanalysedTemplates) + { + $this->fileExcluder = $fileExcluder; + $this->analysedPaths = $analysedPaths; + $this->reportUnanalysedTemplates = $reportUnanalysedTemplates; + foreach ($this->getExistingTemplates() as $file) { + $this->templateFiles[$file] = false; + } + } + + public function isExcludedFromAnalysing(string $path): bool + { + return $this->fileExcluder->isExcludedFromAnalysing($path); + } + + public function templateAnalysed(string $path): void + { + $path = realpath($path) ?: $path; + $this->templateFiles[$path] = true; + } + + /** + * @return string[] + */ + public function getExistingTemplates(): array + { + $files = []; + foreach ($this->analysedPaths as $analysedPath) { + if (!is_dir($analysedPath)) { + continue; + } + /** @var SplFileInfo $file */ + foreach (Finder::findFiles('*.latte')->from($analysedPath) as $file) { + $filePath = (string)$file; + if ($this->isExcludedFromAnalysing($filePath)) { + continue; + } + $files[] = $filePath; + } + } + $files = array_unique($files); + sort($files); + return $files; + } + + /** + * @return string[] + */ + public function getAnalysedTemplates(): array + { + return array_keys(array_filter($this->templateFiles, function (bool $val) { + return $val; + })); + } + + /** + * @return string[] + */ + public function getUnanalysedTemplates(): array + { + return array_keys(array_filter($this->templateFiles, function (bool $val) { + return !$val; + })); + } + + /** + * @return string[] + */ + public function getReportedUnanalysedTemplates(): array + { + if ($this->reportUnanalysedTemplates) { + return $this->getUnanalysedTemplates(); + } else { + return []; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/existing-classes-property-hooks.php b/tests/PHPStan/Rules/Properties/data/existing-classes-property-hooks.php new file mode 100644 index 0000000000..a818f22c1e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/existing-classes-property-hooks.php @@ -0,0 +1,34 @@ += 8.4 + +namespace ExistingClassesPropertyHooks; + +class Foo +{ + + public int $i { + set (Nonexistent $v) { + + } + } + + public \stdClass $j { + set (\stdClass&\Exception $v) { + + } + } + + /** @var Undefined */ + public $k { + get { + + } + } + + /** @var Undefined */ + public $l { + set { + + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/feature-11775.php b/tests/PHPStan/Rules/Properties/data/feature-11775.php new file mode 100644 index 0000000000..a8a4bb8555 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/feature-11775.php @@ -0,0 +1,45 @@ += 7.4 + +namespace Feature11775; + +/** @immutable */ +class FooImmutable +{ + private int $i; + + public function __construct(int $i) + { + $this->i = $i; + } + + public function getId(): int + { + return $this->i; + } + + public function setId(): void + { + $this->i = 5; + } +} + +/** @readonly */ +class FooReadonly +{ + private int $i; + + public function __construct(int $i) + { + $this->i = $i; + } + + public function getId(): int + { + return $this->i; + } + + public function setId(): void + { + $this->i = 5; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/feature-7648.php b/tests/PHPStan/Rules/Properties/data/feature-7648.php new file mode 100644 index 0000000000..8596ec9a1a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/feature-7648.php @@ -0,0 +1,25 @@ += 8.1 + +namespace Feature7648; + +/** @immutable */ +class Request +{ + use OffsetTrait; + + public function __construct(int $offset) + { + $this->populateOffsets($offset); + } +} + +/** @immutable */ +trait OffsetTrait +{ + public readonly int $offset; + + private function populateOffsets(int $offset): void + { + $this->offset = $offset; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/final-properties.php b/tests/PHPStan/Rules/Properties/data/final-properties.php new file mode 100644 index 0000000000..1e04ef49b6 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/final-properties.php @@ -0,0 +1,11 @@ + $this->firstName; + set => $this->firstName; + } + + public final string $middleName { get => $this->middleName; } + + public final string $lastName { set => $this->lastName; } +} + +abstract class HiWorld +{ + public abstract final string $firstName { get { return 'jake'; } set; } +} + +final class GoodMorningWorld +{ + public string $firstName { + get => $this->firstName; + set => $this->firstName; + } +} 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 index 89dff7836d..2250cdd3ff 100644 --- a/tests/PHPStan/Rules/Properties/data/generic-object-unspecified-template-types.php +++ b/tests/PHPStan/Rules/Properties/data/generic-object-unspecified-template-types.php @@ -5,12 +5,12 @@ class Foo { - /** @var \ArrayObject */ + /** @var MyObject */ private $obj; public function __construct() { - $this->obj = new \ArrayObject(); + $this->obj = new MyObject(); } } @@ -91,3 +91,15 @@ public function doBar() } } + +/** + * @template TKey of array-key + * @template TValue + */ +class MyObject +{ + /** + * @param array|object $input + */ + public function __construct($input = null) { } +} diff --git a/tests/PHPStan/Rules/Properties/data/generics-in-callable-in-constructor.php b/tests/PHPStan/Rules/Properties/data/generics-in-callable-in-constructor.php new file mode 100644 index 0000000000..42dc135584 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/generics-in-callable-in-constructor.php @@ -0,0 +1,40 @@ + */ + private $differ; + + public function doFoo(): void + { + $this->differ = new Differ(static function ($a, $b) { + return false; + }); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/get-abstract-property-hook-read.php b/tests/PHPStan/Rules/Properties/data/get-abstract-property-hook-read.php new file mode 100644 index 0000000000..4c26243016 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/get-abstract-property-hook-read.php @@ -0,0 +1,15 @@ += 8.4 + +namespace GetAbstractPropertyHook; + +class NonFinalClass +{ + public string $publicProperty; +} + +abstract class Foo extends NonFinalClass +{ + abstract public string $publicProperty { + get; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/get-non-virtual-property-hook-read.php b/tests/PHPStan/Rules/Properties/data/get-non-virtual-property-hook-read.php new file mode 100644 index 0000000000..76ceabe408 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/get-non-virtual-property-hook-read.php @@ -0,0 +1,57 @@ += 8.4 + +namespace GetNonVirtualPropertyHookRead; + +class Foo +{ + + public int $i { + // backed, read and written + get => $this->i + 1; + set => $this->i + $value; + } + + public int $j { + // virtual + get => 1; + set { + $this->a = $value; + } + } + + public int $k { + // backed, not read + get => 1; + set => $value + 1; + } + + public int $l { + // backed, not read, long get + get { + return 1; + } + set => $value + 1; + } + + public int $m { + // it is okay to only read it sometimes + get { + if (rand(0, 1)) { + return 1; + } + + return $this->m; + } + set => $value + 1; + } + +} + +class GetHookIsNotPresentAtAll +{ + public int $i { + set { + $this->i = $value + 10; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php b/tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php new file mode 100644 index 0000000000..65c27eb50c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php @@ -0,0 +1,11 @@ + $this->name; + set => $this->name = $value; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php b/tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php new file mode 100644 index 0000000000..24238e5c14 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php @@ -0,0 +1,20 @@ +i = random_int(0, 3); + $this->j = random_int(0, 3); + $this->k = random_int(0, 3); + $this->l = random_int(0, 3); + } + + /** + * @var int<0,3> + */ + public $x = 0; + + /** + * @param 0| $a + * @param 0|1 $b + * @param 0|1|3 $c + * @param 0|1|2|3|string $j + * @param 0|1|bool|2|3 $k + * @param 0|1|bool|3 $l + * @param 0|1|3|4 $m + */ + public function test2($a, $b, $c, $j, $k, $l, $m): void { + $this->x = $a; + $this->x = $b; + $this->x = $c; + + $this->x = $j; + $this->x = $k; + $this->x = $l; + $this->x = $m; + } + + const I_1=1; + const I_2=2; + + /** @param int-mask $flag */ + public function sayHello($flag): void + { + $this->x = $flag; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php b/tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php new file mode 100644 index 0000000000..053c0bfb1c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php @@ -0,0 +1,31 @@ +a = $closure; + $this->b = $closure; + $this->c = $closure; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php index 79cba0ab31..374397cd0a 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php +++ b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php @@ -42,6 +42,9 @@ class PrefixedTags /** @psalm-var int */ private $fooPsalm; + /** @phan-var int */ + private $fooPhan; + } /** @@ -93,3 +96,41 @@ class CallableSignature private $cb; } + +class NestedArrayInProperty +{ + + /** + * @var list|null + */ + public $args; + +} + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + /** @var \MissingPropertyTypehint\GenericClassWithDefault */ + private $foo; + + /** @var \MissingPropertyTypehint\GenericClassWithSomeDefaults */ + private $bar; + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php new file mode 100644 index 0000000000..3c5a6bfccf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php @@ -0,0 +1,15 @@ += 8.3 + +namespace MissingReadonlyAnonymousClassPropertyAssign; + +class Foo +{ + + public function doFoo(): void + { + $c = new readonly class () { + public int $foo; + }; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc-and-native.php new file mode 100644 index 0000000000..aa15ff16f0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc-and-native.php @@ -0,0 +1,51 @@ += 8.1 + +namespace MissingReadOnlyPropertyAssignPhpDocAndNative; + +class Foo +{ + + /** @readonly */ + private readonly int $assigned; + + private int $unassignedButNotReadOnly; + + private int $readBeforeAssignedNotReadOnly; + + /** @readonly */ + private readonly int $unassigned; + + /** @readonly */ + private readonly int $unassigned2; + + /** @readonly */ + private readonly int $readBeforeAssigned; + + /** @readonly */ + private readonly int $doubleAssigned; + + private int $doubleAssignedNotReadOnly; + + public function __construct() + { + $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; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php new file mode 100644 index 0000000000..82d55e48fa --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php @@ -0,0 +1,260 @@ +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 +{ + + /** @readonly */ + private int $foo; + + public function setFoo(int $i) + { + // reported in ReadOnlyPropertyAssignRule + $this->foo = $i; + $this->foo = $i; + } + +} + +class TestCase +{ + + /** @readonly */ + private int $foo; + + protected function setUp(): void + { + $this->foo = 1; + } + +} + +class AssignOp +{ + + /** @readonly */ + private int $foo; + + /** @readonly */ + private ?int $bar; + + public function __construct(int $foo) + { + $this->foo .= $foo; + + $this->bar = $this->bar ?? 3; + } + + +} + +class AssignRef +{ + + /** @readonly */ + private int $foo; + + public function __construct(int $foo) + { + $this->foo = &$foo; + } + +} + +/** @phpstan-immutable */ +class Immutable +{ + + private int $assigned; + + private int $unassigned; + + private int $unassigned2; + + private int $readBeforeAssigned; + + private int $doubleAssigned; + + public function __construct() + { + $this->assigned = 1; + + echo $this->readBeforeAssigned; + $this->readBeforeAssigned = 1; + + $this->doubleAssigned = 1; + $this->doubleAssigned = 2; + } + + public function setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} + +trait FooTrait +{ + + /** @readonly */ + private int $assigned; + + private int $unassignedButNotReadOnly; + + private int $readBeforeAssignedNotReadOnly; + + /** @readonly */ + private int $unassigned; + + /** @readonly */ + private int $unassigned2; + + /** @readonly */ + private int $readBeforeAssigned; + + /** @readonly */ + private int $doubleAssigned; + + private int $doubleAssignedNotReadOnly; + + public function setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} + +class FooTraitClass +{ + + use FooTrait; + + public function __construct() + { + $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; + } + +} + + +class Entity +{ + + /** @readonly */ + private int $id; // does not complain about being uninitialized because of a ReadWritePropertiesExtension + +} + +trait BarTrait +{ + + /** @readonly */ + public int $foo; + + public function __construct() + { + $this->foo = 17; + } + +} + +class BarClass +{ + + use BarTrait; + +} + +/** @immutable */ +class A +{ + + public string $a; + +} + +class B extends A +{ + + public string $b; + + public function __construct() + { + $b = $this->b; + } + +} + +class C extends B +{ + + public string $c; + + public function __construct() + { + $this->c = ''; + $this->c = ''; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php index e6671cf9c5..cded620d17 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php @@ -101,3 +101,202 @@ public function __construct(int $foo) } } + +trait FooTrait +{ + + 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 setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} + +class FooTraitClass +{ + + use FooTrait; + + 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; + } + +} + +class Entity +{ + + private readonly int $id; // does not complain about being uninitialized because of a ReadWritePropertiesExtension + +} + +trait BarTrait +{ + + public readonly int $foo; + + public function __construct(public readonly int $bar) + { + $this->foo = 17; + } + +} + +class BarClass +{ + + use BarTrait; + +} + +class AdditionalAssignOfReadonlyPromotedProperty +{ + + public function __construct(private readonly int $x) + { + $this->x = 2; + } + +} + +class MethodCalledFromConstructorAfterAssign +{ + + + private readonly int $foo; + + public function __construct() + { + $this->foo = 1; + $this->doFoo(); + } + + public function doFoo(): void + { + echo $this->foo; + } + +} + +class MethodCalledFromConstructorBeforeAssign +{ + + + private readonly int $foo; + + public function __construct() + { + $this->doFoo(); + $this->foo = 1; + } + + public function doFoo(): void + { + echo $this->foo; + } + +} + +class MethodCalledTwice +{ + private readonly int $foo; + + public function __construct() + { + $this->doFoo(); + $this->foo = 1; + $this->doFoo(); + } + + public function doFoo(): void + { + echo $this->foo; + } +} + +class PropertyAssignedOnDifferentObject +{ + + private readonly int $foo; + + public function __construct(self $self) + { + $self->foo = 1; + $this->foo = 2; + } + +} + +class PropertyAssignedOnDifferentObjectUninitialized +{ + + private readonly int $foo; + + public function __construct(self $self) + { + $self->foo = 1; + } + +} + +class AccessToPropertyOnDifferentObject +{ + + private readonly int $foo; + + public function __construct(self $self) + { + echo $self->getFoo(); + $this->foo = 1; + } + + public function getFoo(): int + { + return $this->foo; + } + +} + +class PropertyHasInitPhpDocButIsAlsoAssignedInConstructor +{ + + /** @init */ + public readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/mixin.php b/tests/PHPStan/Rules/Properties/data/mixin.php index e6ba546647..21c94ffb73 100644 --- a/tests/PHPStan/Rules/Properties/data/mixin.php +++ b/tests/PHPStan/Rules/Properties/data/mixin.php @@ -2,6 +2,8 @@ namespace MixinProperties; +use AllowDynamicProperties; + class Foo { @@ -12,6 +14,7 @@ class Foo /** * @mixin Foo */ +#[AllowDynamicProperties] class Bar { @@ -34,6 +37,7 @@ function (Baz $baz): void { * @template T * @mixin T */ +#[AllowDynamicProperties] class GenericFoo { diff --git a/tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-abstract-class.php b/tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-abstract-class.php new file mode 100644 index 0000000000..a29e8e769d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-abstract-class.php @@ -0,0 +1,35 @@ +bar ?? 'no'; +}; diff --git a/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php index 9c6bbb66c4..d2a693fe64 100644 --- a/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php +++ b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php @@ -22,4 +22,11 @@ public function doBar(string $string, ?string $nullableString): void echo $nullableString?->bar ?? 4; } + public function doNull(): void + { + $null = null; + $null->foo; + $null?->foo; + } + } diff --git a/tests/PHPStan/Rules/Properties/data/overriding-final-property.php b/tests/PHPStan/Rules/Properties/data/overriding-final-property.php new file mode 100644 index 0000000000..02d7467e57 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/overriding-final-property.php @@ -0,0 +1,29 @@ + $properties + */ +trait TraitA { + /** + * @var array + */ + public array $items = []; +} + +/** + * @phpstan-use TraitA + */ +#[AllowDynamicProperties] +class ClassA { + /** + * @phpstan-use TraitA + */ + use TraitA; +} + +class ClassB { + public function test(): void { + // empty + } +} + +function (): void { + foreach ((new ClassA())->properties as $property) { + $property->test(); + } + + foreach ((new ClassA())->items as $item) { + $item->test(); + } +}; + +#[AllowDynamicProperties] +class HelloWorld +{ + public function __get(string $attribute): mixed + { + if($attribute == "world") + { + return "Hello World"; + } + throw new \Exception("Attribute '{$attribute}' is invalid"); + } + + + public function __isset(string $attribute) + { + try { + if (!isset($this->{$attribute})) { + $x = $this->{$attribute}; + } + + return isset($this->{$attribute}); + } catch (\Exception $e) { + return false; + } + } +} + +function (): void { + $hello = new HelloWorld(); + if(isset($hello->world)) + { + echo $hello->world; + } +}; diff --git a/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php new file mode 100644 index 0000000000..44bd919027 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php @@ -0,0 +1,116 @@ + $properties + */ +trait TraitA { + /** + * @var array + */ + public array $items = []; +} + +/** + * @phpstan-use TraitA + */ +class ClassA { + /** + * @phpstan-use TraitA + */ + use TraitA; +} + +class ClassB { + public function test(): void { + // empty + } +} + +function (): void { + foreach ((new ClassA())->properties as $property) { + $property->test(); + } + + foreach ((new ClassA())->items as $item) { + $item->test(); + } +}; + +class HelloWorld +{ + public function __get(string $attribute): mixed + { + if($attribute == "world") + { + return "Hello World"; + } + throw new \Exception("Attribute '{$attribute}' is invalid"); + } + + + public function __isset(string $attribute) + { + try { + if (!isset($this->{$attribute})) { + $x = $this->{$attribute}; + } + + return isset($this->{$attribute}); + } catch (\Exception $e) { + return false; + } + } +} + +function (): void { + $hello = new HelloWorld(); + if(isset($hello->world)) + { + echo $hello->world; + } +}; + +function (HelloWorld $hello): void { + if(isset($hello->world)) + { + echo $hello->world; + } +}; + +final class FinalHelloWorld +{ + public function __get(string $attribute): mixed + { + if($attribute == "world") + { + return "Hello World"; + } + throw new \Exception("Attribute '{$attribute}' is invalid"); + } + + + public function __isset(string $attribute) + { + try { + if (!isset($this->{$attribute})) { + $x = $this->{$attribute}; + } + + return isset($this->{$attribute}); + } catch (\Exception $e) { + return false; + } + } +} + +function (): void { + $hello = new FinalHelloWorld(); + if(isset($hello->world)) + { + echo $hello->world; + } +}; diff --git a/tests/PHPStan/Rules/Properties/data/private-final-property-hooks.php b/tests/PHPStan/Rules/Properties/data/private-final-property-hooks.php new file mode 100644 index 0000000000..1ce2830a95 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/private-final-property-hooks.php @@ -0,0 +1,36 @@ += 8.4 + +namespace PrivateFinalHook; + +final class User +{ + final private string $privatePropGet = 'mailto: example.org' { + get => 'private:' . $this->privatePropGet; + } + + private string $private = 'mailto: example.org' { + final set => 'private:' . $this->private; + get => 'private:' . $this->private; + } + + protected string $protected = 'mailto: example.org' { + final get => 'protected:' . $this->protected; + } + + public string $public = 'mailto: example.org' { + final get => 'public:' . $this->public; + } + + private string $email = 'mailto: example.org' { + get => 'mailto:' . $this->email; + } + + function doFoo(): void + { + $u = new User; + var_dump($u->private); + var_dump($u->protected); + var_dump($u->public); + var_dump($u->email); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php b/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php index 3686d4d244..14b3ad66e4 100644 --- a/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php +++ b/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php @@ -376,3 +376,48 @@ public function foo(): void $this->collection2[] = 2; } } + +class ParamOutAssign +{ + + /** @var list */ + private $foo; + + /** @var list> */ + private $foo2; + + /** + * @param mixed $a + * @param-out string $a + */ + public function paramOut(&$a): void + { + + } + + public function doFoo(): void + { + $this->paramOut($this->foo); + } + + public function doFoo2(): void + { + $this->paramOut($this->foo[0]); + } + + public function doBar(): void + { + $this->paramOut($this->foo2); + } + + public function doBar2(): void + { + $this->paramOut($this->foo2[0]); + } + + public function doBar3(): void + { + $this->paramOut($this->foo2[0][0]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/properties-in-interface.php b/tests/PHPStan/Rules/Properties/data/properties-in-interface.php new file mode 100644 index 0000000000..4d104487fb --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/properties-in-interface.php @@ -0,0 +1,14 @@ += 7.4 +foo; + echo $o->bar; + echo $o->baz; + + $o->foo = 1; + $o->bar = 2; + $o->baz = 3; + } + + /** + * @param object{foo: int}&\stdClass $o + * @return void + */ + public function doIntersection(object $o): void + { + echo $o->foo; + + $o->foo = 1; + } + + /** + * @param object{foo: int}|\stdClass $o + * @return void + */ + public function doUnion(object $o): void + { + echo $o->foo; + + $o->foo = 1; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-assign-ref-asymmetric.php b/tests/PHPStan/Rules/Properties/data/property-assign-ref-asymmetric.php new file mode 100644 index 0000000000..8163bb9f00 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-assign-ref-asymmetric.php @@ -0,0 +1,39 @@ += 8.4 + +namespace PropertyAssignRefAsymmetric; + +class Foo +{ + + private(set) int $a; + + protected(set) int $b; + + public(set) int $c; + + public function doFoo() + { + $foo = &$this->a; + $bar = &$this->b; + $bar = &$this->c; + } + +} + +class Bar extends Foo +{ + + public function doBar(Foo $foo) + { + $foo = &$this->a; + $bar = &$this->b; + $bar = &$this->c; + } + +} + +function (Foo $foo): void { + $a = &$foo->a; + $b = &$foo->b; + $c = &$foo->c; +}; diff --git a/tests/PHPStan/Rules/Properties/data/property-assign-ref.php b/tests/PHPStan/Rules/Properties/data/property-assign-ref.php new file mode 100644 index 0000000000..1b49683a01 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-assign-ref.php @@ -0,0 +1,43 @@ += 8.1 + +namespace PropertyAssignRef; + +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; + } + +} + +class Baz +{ + + protected $a; + + private $b; + +} + +function (Baz $b): void { + $z = &$b->a; + $zz = &$b->b; +}; diff --git a/tests/PHPStan/Rules/Properties/data/property-attributes-deprecated.php b/tests/PHPStan/Rules/Properties/data/property-attributes-deprecated.php new file mode 100644 index 0000000000..1492d7809d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-attributes-deprecated.php @@ -0,0 +1,29 @@ +{$column}; + } + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php new file mode 100644 index 0000000000..495cc793b0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php @@ -0,0 +1,57 @@ += 8.4 + +namespace PropertyHookAttributes; + +#[\Attribute(\Attribute::TARGET_CLASS)] +class Foo +{ + +} + +#[\Attribute(\Attribute::TARGET_METHOD)] +class Bar +{ + +} + +#[\Attribute(\Attribute::TARGET_ALL)] +class Baz +{ + +} + +class Lorem +{ + + public int $i { + #[Foo] + get { + + } + } + +} + +class Ipsum +{ + + public int $i { + #[Bar] + get { + + } + } + +} + +class Dolor +{ + + public int $i { + #[Baz] + get { + + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php b/tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php new file mode 100644 index 0000000000..58af100248 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php @@ -0,0 +1,20 @@ += 8.4 + +namespace PropertyHooksReadonlyByPhpDocAssign; + +class Foo +{ + + /** @readonly */ + public int $i { + get { + return $this->i + 1; + } + set { + $self = new self(); + $self->i = 1; + + $this->j = 2; + $this->i = $value - 1; + } + } + + /** @readonly */ + public int $j; + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php b/tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php new file mode 100644 index 0000000000..cd70c3b514 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php @@ -0,0 +1,12 @@ += 8.4 + +namespace Bug12466; + +interface Foo +{ + + public int $a { get; set;} + +} + +class Bar implements Foo +{ + + public string $a; + +} + +interface MoreProps +{ + + public int $a { get; set; } + + public int $b { get; } + + public int $c { set; } + +} + +class TestMoreProps implements MoreProps +{ + + // not writable + public int $a { + get { + return 1; + } + } + + // not readable + public int $b { + set { + $this->a = 1; + } + } + + // not writable + public int $c { + get { + return 1; + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php new file mode 100644 index 0000000000..5c2cee2758 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php @@ -0,0 +1,40 @@ + */ + private $nonEmpty; + + /** @var list */ + private $listProp; + + /** @var array> */ + private $nestedListProp; + + public function doFoo(int $i, int $j) + { + unset($this->nonEmpty[$i]); + unset($this->listProp[$i]); + unset($this->nestedListProp[$i][$j]); + } + +} + +class Bar +{ + + /** @var array> */ + private $prop; + + /** + * @param int|string $key + */ + public function doFoo($key): void + { + unset($this->prop[$key]['foo']); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-asymmetric-visibility.php b/tests/PHPStan/Rules/Properties/data/read-asymmetric-visibility.php new file mode 100644 index 0000000000..ee38e072ce --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-asymmetric-visibility.php @@ -0,0 +1,37 @@ += 8.4 + +namespace ReadAsymmetricVisibility; + +class Foo +{ + + public private(set) int $a; + public protected(set) int $b; + public public(set) int $c; + + public function doFoo(): void + { + echo $this->a; + echo $this->b; + echo $this->c; + } + +} + +class Bar extends Foo +{ + + public function doBar(): void + { + echo $this->a; + echo $this->b; + echo $this->c; + } + +} + +function (Foo $foo): void { + echo $foo->a; + echo $foo->b; + echo $foo->c; +}; diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-allowed-private-mutation.php b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-allowed-private-mutation.php new file mode 100644 index 0000000000..435a73706b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-allowed-private-mutation.php @@ -0,0 +1,30 @@ += 8.0 + +namespace ReadOnlyPropertyPhpDocAllowedPrivateMutation; + +class A +{ + + /** @phpstan-readonly */ + public array $a = []; + +} + +class B +{ + + /** + * @phpstan-readonly + * @phpstan-allow-private-mutation + */ + public array $a = []; + +} + +class C +{ + + /** @phpstan-readonly-allow-private-mutation */ + public array $a = []; + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php new file mode 100644 index 0000000000..3e3c94a909 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php @@ -0,0 +1,15 @@ += 8.0 + +namespace ReadOnlyPropertyPhpDoc; + +class Foo +{ + + /** + * @readonly + * @var int + */ + private $foo; + + /** @readonly */ + private $bar; + + /** + * @readonly + * @var int + */ + private $baz = 0; + +} + +final class ErrorResponse +{ + public function __construct( + /** @readonly */ + public string $message = '' + ) + { + } +} + +/** @immutable */ +class A +{ + + public string $a = ''; + +} + +class B extends A +{ + + public string $b = ''; + +} + +class C extends B +{ + + public string $c = ''; + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php b/tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php new file mode 100644 index 0000000000..1c3a6dae11 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php @@ -0,0 +1,24 @@ += 8.2 + +namespace ReadOnlyPropertyReadonlyClass; + +readonly class Foo +{ + + private int $foo; + private $bar; + private int $baz = 0; + +} + +readonly final class ErrorResponse +{ + public function __construct(public string $message = '') + { + } +} + +readonly class StaticReadonlyProperty +{ + private static int $foo; +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property.php b/tests/PHPStan/Rules/Properties/data/read-only-property.php index 979cf94e91..20cc6d1c0f 100644 --- a/tests/PHPStan/Rules/Properties/data/read-only-property.php +++ b/tests/PHPStan/Rules/Properties/data/read-only-property.php @@ -17,3 +17,8 @@ public function __construct(public readonly string $message = '') { } } + +class StaticReadonlyProperty +{ + private readonly static int $foo; +} diff --git a/tests/PHPStan/Rules/Properties/data/reading-write-only-hooked-properties.php b/tests/PHPStan/Rules/Properties/data/reading-write-only-hooked-properties.php new file mode 100644 index 0000000000..9f695e4573 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-hooked-properties.php @@ -0,0 +1,51 @@ += 8.4 + +namespace ReadingWriteOnlyHookedProperties; + +interface Foo +{ + + public int $i { + // virtual, not readable + set; + } + +} + +function (Foo $f): void { + echo $f->i; +}; + +class Bar +{ + + public int $other; + + public int $i { + // virtual, not readable + set { + $this->other = 1; + } + } + +} + +function (Bar $b): void { + echo $b->i; +}; + +class Baz +{ + + public int $i { + // backed, readable + set { + $this->i = 1; + } + } + +} + +function (Baz $b): void { + $b->i = 1; +}; diff --git a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php index 33051d7ead..2042e0005f 100644 --- a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php @@ -6,5 +6,6 @@ function (?Foo $foo): void { echo $foo?->readOnlyProperty; echo $foo?->usualProperty; + echo $foo?->asymmetricProperty; echo $foo?->writeOnlyProperty; }; diff --git a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php index 9dd1f23695..cc2b202f82 100644 --- a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php @@ -2,11 +2,16 @@ namespace ReadingWriteOnlyProperties; +use AllowDynamicProperties; + /** * @property-read int $readOnlyProperty * @property int $usualProperty + * @property-read int $asymmetricProperty + * @property-write int|string $asymmetricProperty * @property-write int $writeOnlyProperty */ +#[AllowDynamicProperties] class Foo { @@ -14,11 +19,13 @@ public function doFoo() { echo $this->readOnlyProperty; echo $this->usualProperty; + echo $this->asymmetricProperty; echo $this->writeOnlyProperty; $self = new self(); echo $self->readOnlyProperty; echo $self->usualProperty; + echo $self->asymmetricProperty; echo $self->writeOnlyProperty; } diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc-and-native.php new file mode 100644 index 0000000000..7de6b1ae03 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc-and-native.php @@ -0,0 +1,27 @@ += 8.1 + +namespace ReadonlyPropertyAssignPhpDocAndNative; + +class Foo +{ + + /** @readonly */ + private readonly int $foo; + + /** @readonly */ + protected readonly int $bar; + + /** @readonly */ + 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 + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php new file mode 100644 index 0000000000..c390bbb6da --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php @@ -0,0 +1,314 @@ +foo = $foo; // constructor - fine + $this->psalm = $foo; // constructor - fine + $this->phan = $foo; // constructor - fine + } + + public function setFoo(int $foo): void + { + $this->foo = $foo; // setter - report + $this->psalm = $foo; // do not report -allowed private mutation + $this->phan = $foo; // setter - report + } + +} + +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 + $this->psalm = $bar; // report - not in declaring class + $this->phan = $bar; // report - not in declaring class + } + + public function setBar(int $bar): void + { + $this->bar = $bar; // report - not in declaring class + $this->psalm = $bar; // report - not in declaring class + $this->phan = $bar; // report - not in declaring class + } + +} + +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} + * @readonly + */ + public $details; + + public function __construct() + { + $this->details = ['name' => 'Foo', 'age' => 25]; + } + + public function doSomething(): void + { + $this->details['name'] = 'Bob'; + $this->details['age'] = 42; + } + +} + +class NotReadonly +{ + + /** @var int */ + private $foo; + + public function setFoo(int $foo): void + { + $this->foo = $foo; // do not report - not readonly + } + +} + +class NotThis +{ + + /** + * @var int + * @readonly + */ + private $foo; + + public function __construct(int $foo) + { + $self = new self(1); + $self->foo = $foo; // report - not $this + } + +} + +class PostInc +{ + + /** + * @var int + * @readonly + */ + private $foo; + + public function doFoo(): void + { + $this->foo++; + --$this->foo; + + $this->foo += 5; + } + +} + +class ListAssign +{ + + /** + * @var int + * @readonly + */ + private $foo; + + public function __construct() + { + [$this->foo] = [1]; + } + + public function setFoo() + { + [$this->foo] = [1]; + } + + public function setBar() + { + list($this->foo) = [1]; + } + +} + +class AssignRefOutsideClass +{ + + public function doFoo(Foo $foo, int $i) + { + $foo->baz = 5; + $foo->baz = &$i; + } + +} + +class Unserialization +{ + + /** + * @var int + * @readonly + */ + private $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 + } + +} + +class TestCase +{ + + /** + * @var int + * @readonly + */ + private $foo; + + protected function setUp(): void + { + $this->foo = 1; // additional constructor - fine + } + +} + +/** @phpstan-immutable */ +class Immutable +{ + + /** @var int */ + private $foo; + + protected $bar; + + public $baz; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + public function setFoo(int $foo): void + { + $this->foo = $foo; // setter - report + } + +} + +/** @immutable */ +class A +{ + + /** @var string */ + public $a; + + public function __construct() { + $this->a = ''; // constructor - fine + } + +} + +class B extends A +{ + + /** @var string */ + public $b; + + public function __construct() + { + parent::__construct(); + $this->b = ''; // constructor - fine + } + + public function mod() + { + $this->b = 'input'; // setter - report + $this->a = 'input2'; // setter - report + } + +} + +class C extends B +{ + + /** @var string */ + public $c; + + public function mod() + { + $this->c = 'input'; // setter - report + } + +} + +class ArrayAccessPropertyFetch +{ + + /** @readonly */ + private \ArrayObject $storage; + + public function __construct() { + $this->storage = new \ArrayObject(); + } + + public function set(\stdClass $class, int $value): void { + $this->storage[$class] = $value; + unset($this->storage[$class]); + $this->storage = new \WeakMap(); // invalid + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc-and-native.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc-and-native.php new file mode 100644 index 0000000000..2ce9bb2e08 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc-and-native.php @@ -0,0 +1,20 @@ += 8.1 + +namespace ReadOnlyPropertyAssignRefPhpDocAndNative; + +class Foo +{ + + /** @readonly */ + private readonly int $foo; + + /** @readonly */ + public readonly int $bar; + + public function doFoo() + { + $foo = &$this->foo; + $bar = &$this->bar; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php new file mode 100644 index 0000000000..26bec0835d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php @@ -0,0 +1,96 @@ +foo; + $bar = &$this->bar; + } + +} + +class Bar +{ + + public function doBar(Foo $foo) + { + $a = &$foo->foo; // private + $b = &$foo->bar; + } + +} + +/** @phpstan-immutable */ +class Immutable +{ + + /** @var int */ + private $foo; + + /** @var int */ + public $bar; + + public function doFoo() + { + $foo = &$this->foo; + $bar = &$this->bar; + } + +} + +/** @immutable */ +class A +{ + + /** @var string */ + public $a; + + public function mod() + { + $a = &$this->a; + } + +} + +class B extends A +{ + + /** @var string */ + public $b; + + public function mod() + { + $b = &$this->b; + $a = &$this->a; + } + +} + +class C extends B +{ + + /** @var string */ + public $c; + + public function mod() + { + $c = &$this->c; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign.php b/tests/PHPStan/Rules/Properties/data/readonly-assign.php index cfead51c27..e23655217c 100644 --- a/tests/PHPStan/Rules/Properties/data/readonly-assign.php +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign.php @@ -184,3 +184,32 @@ public function __unserialize(array $data) : void } } + +class TestCase +{ + + private readonly int $foo; + + protected function setUp(): void + { + $this->foo = 1; // additional constructor - fine + } + +} + +class ArrayAccessPropertyFetch +{ + + private readonly \ArrayObject $storage; + + public function __construct() { + $this->storage = new \ArrayObject(); + } + + public function set(\stdClass $class, int $value): void { + $this->storage[$class] = $value; + unset($this->storage[$class]); + $this->storage = new \WeakMap(); // invalid + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-class-assign.php b/tests/PHPStan/Rules/Properties/data/readonly-class-assign.php new file mode 100644 index 0000000000..d34cbcd863 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-class-assign.php @@ -0,0 +1,24 @@ += 8.2 + +namespace ReadonlyClassPropertyAssign; + +readonly class Foo +{ + + private int $foo; + + protected int $bar; + + public int $baz; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + public function setFoo(int $foo): void + { + $this->foo = $foo; // setter - report + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-property-hooks-in-interface.php b/tests/PHPStan/Rules/Properties/data/readonly-property-hooks-in-interface.php new file mode 100644 index 0000000000..53aa244fa9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-property-hooks-in-interface.php @@ -0,0 +1,12 @@ + $this->firstName; + set => $this->firstName; + } + + public readonly string $middleName { get => $this->middleName; } + + public readonly string $lastName { set => $this->lastName; } +} + +abstract class HiWorld +{ + public abstract readonly string $firstName { get { return 'jake'; } set; } +} + +readonly class GoodMorningWorld +{ + public string $firstName { + get => $this->firstName; + set => $this->firstName; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/redeclare-property-of-readonly-class.php b/tests/PHPStan/Rules/Properties/data/redeclare-property-of-readonly-class.php new file mode 100644 index 0000000000..672de2c5e8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/redeclare-property-of-readonly-class.php @@ -0,0 +1,50 @@ += 8.2 + +namespace RedeclarePropertyOfReadonlyClass; + +readonly class A { + public function __construct(public int $promotedProp) + { + } +} + +readonly class B1 extends A { + // $promotedProp is written twice + public function __construct(public int $promotedProp) + { + parent::__construct(5); + } +} + +readonly class B2 extends A { + // Don't get confused by standard parameter with same name + public function __construct(int $promotedProp) + { + parent::__construct($promotedProp); + } +} + +readonly class B3 extends A { + // This is allowed, because we don't write to the property. + public int $promotedProp; + + public function __construct() + { + parent::__construct(7); + } +} + +readonly class B4 extends A { + // The second write is not from the constructor. It is an error, but it is handled by different rule. + public int $promotedProp; + + public function __construct() + { + parent::__construct(7); + } + + public function set(): void + { + $this->promotedProp = 7; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php b/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php new file mode 100644 index 0000000000..a9e611d3b9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php @@ -0,0 +1,262 @@ += 8.1 + +namespace RedeclareReadonlyProperty; + +class A { + protected readonly string $nonPromotedProp; + + public function __construct(public readonly int $myProp) { + $this->nonPromotedProp = 'aaa'; + } +} + +class B1 extends A { + // This should be reported as an error, as a readonly prop cannot be redeclared. + public function __construct(public readonly int $myProp) { + parent::__construct($myProp); + } +} + +class B2 extends A { + // different property + public function __construct(public readonly int $foo) { + parent::__construct($foo); + } +} + +class B3 extends A { + // We don't call the parent constructor, so it's fine. + public function __construct(public readonly int $myProp) { + } +} + +class B4 extends A { + protected readonly string + $foo, + // We can't detect this at the moment. + $nonPromotedProp; + public function __construct() { + $this->foo = 'xyz'; + $this->nonPromotedProp = 'bbb'; + parent::__construct(5); + } +} + +class B5 extends A { + // Error: we can't both write the property ourselves and call the parent constructor. + public readonly int $myProp; + public function __construct() { + $this->myProp = 7; + parent::__construct(5); + } +} + +class B6 extends A { + // This is fine - we don't call parent constructor; + public readonly int $myProp; + public function __construct() { + $this->myProp = 5; + } +} + +class B7 extends A { + // Call parent constructor indirectly. + public function __construct(public readonly int $myProp) { + $this->foo(); + } + + private function foo(): void + { + A::__construct(5); + } +} + +class B8 extends A { + // Don't get confused by prop declaration in anonymous class. + public function __construct() { + parent::__construct(5); + $c = new class { + public function __construct(public readonly int $myProp) + { + } + }; + } +} + +class B9 extends A { + // Don't get confused by constructor call in anonymous class + public readonly int $myProp; + public function __construct() { + $this->myProp = 5; + $c = new class extends A { + public function __construct() + { + parent::__construct(5); + } + }; + } +} + +class B10 extends A { + // Don't get confused by promoted properties in anonymous class + public function __construct() { + parent::__construct(5); + $c = new class (5) { + public function __construct(public readonly int $myProp) + { + } + }; + } +} + +class B11 extends A { + // This is fine - we don't call the parent constructor. + public readonly int $myProp; + public function __construct() { + $this->myProp = 5; + $c = new class ('aaa') extends A { + // Detect redeclaration even inside anonymous classes. + public function __construct(public readonly int $myProp) + { + parent::__construct(5); + } + }; + } +} + +class A12 { + public function __construct(public readonly int $aProp) + { + } +} + +class B12 extends A12 { + public function __construct(public readonly int $bProp) + { + parent::__construct(15); + } +} + +class C12 extends B12 { + // This is OK, because we call A12's constructor, not B12's. + public function __construct(public readonly int $bProp) { + A12::__construct(15); + } +} + +class B12_1 extends A12 { + public function __construct(public readonly int $bProp) + { + parent::__construct(15); + } +} + +class C12_1 extends B12_1 { + // This is an error, but we can't detect it at the moment. + public function __construct(public readonly int $aProp) { + parent::__construct(15); + } +} + +class A13 { + public function __construct(private readonly int $privateProp) + { + } +} + +class B13 extends A13 { + // This is OK, A's prop is private + public function __construct(public readonly int $privateProp) + { + parent::__construct(15); + } +} + +class B14 { + public function __construct(public readonly int $myProp) { + // Don't get confused by same property in non-parent's constructor. + A::__construct(7); + } +} + +class B15 extends A { + public function __construct(public readonly int $myProp) { + self:foo(); + } + + public static function foo(): void + { + // Don't get confused by calling the parent constructor from static scope. + parent::__construct(7); + } +} + +class B16 extends A { + public readonly int $myProp; + + public function __construct(A $other) { + // Don't get confused by calling the constructor on other object. + $other::__construct(7); + $other->__construct(7); + } +} + +class A17 { + public function __construct(public readonly int $aProp) + { + } +} + +class B17 extends A17 { + public function __construct() + { + } +} + +class C17 extends B17 { + // Error: $aProp may be unassigned, because B's constructor may not call A's + public readonly int $aProp; + + public function __construct() { + parent::__construct(); + } +} + +class A18 { + public function __construct(private readonly int $aProp) + { + } +} + +class B18 extends A18 { + // Make surer that we don't get confused by parent's private property. + public readonly int $aProp; + + public function __construct() + { + parent::__construct(7); + } +} + +class A19 { + public function __construct(public int $prop1, public int $prop2) + { + } +} + +class B19 extends A19 { + public int $prop1; + public int $prop2; + + public function __construct() + { + if (rand()) { + parent::__construct(5, 6); + } else { + $this->prop1 = 7; + } + + // Error: this may not be assigned + var_dump($this->prop2); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/require-extends.php b/tests/PHPStan/Rules/Properties/data/require-extends.php new file mode 100644 index 0000000000..a9d22d0eb5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/require-extends.php @@ -0,0 +1,47 @@ +bar; + return $obj->foo; +} + + +function callFoo(MyInterface $obj): string +{ + echo $obj->doesNotExist(); + echo MyInterface::doesNotExistStatic(); + echo MyInterface::doSomethingStatic(); + return $obj->doSomething(); +} diff --git a/tests/PHPStan/Rules/Properties/data/require-implements.php b/tests/PHPStan/Rules/Properties/data/require-implements.php new file mode 100644 index 0000000000..8ac4da18e0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/require-implements.php @@ -0,0 +1,48 @@ +bar; + return $obj->foo; +} + +function callFoo(MyBaseClass $obj): string +{ + echo $obj->doesNotExist(); + echo $obj::doesNotExistStatic(); + echo $obj::doSomethingStatic(); + return $obj->doSomething(); +} diff --git a/tests/PHPStan/Rules/Properties/data/set-non-virtual-property-hook-assign.php b/tests/PHPStan/Rules/Properties/data/set-non-virtual-property-hook-assign.php new file mode 100644 index 0000000000..56133fb556 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/set-non-virtual-property-hook-assign.php @@ -0,0 +1,75 @@ += 8.4 + +namespace SetNonVirtualPropertyHookAssign; + +class Foo +{ + + public int $i { + get { + return 1; + } + set { + // virtual property + $this->j = $value; + } + } + + public int $j; + + public int $k { + get { + return $this->k + 1; + } + set { + // backed property, missing assign should be reported + $this->j = $value; + } + } + + public int $k2 { + get { + return $this->k2 + 1; + } + set { + // backed property, missing assign should be reported + if (rand(0, 1)) { + return; + } + + $this->k2 = $value; + } + } + + public int $k3 { + get { + return $this->k3 + 1; + } + set { + // backed property, always assigned (or throws) + if (rand(0, 1)) { + throw new \Exception(); + } + + $this->k3 = $value; + } + } + + public int $k4 { + get { + return $this->k4 + 1; + } + set { + // backed property, always assigned + $this->k4 = $value; + } + } + + public int $k5 { + get { + return $this->k4 + 1; + } + set => $value; // short body always assigns + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/set-property-hook-parameter.php b/tests/PHPStan/Rules/Properties/data/set-property-hook-parameter.php new file mode 100644 index 0000000000..12c82ddc0a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/set-property-hook-parameter.php @@ -0,0 +1,140 @@ + $v */ + set (int|array $v) { + + } + } + + public $ok4 { + set ($v) { + + } + } + +} + +class Bar +{ + + public $a { + set (int $v) { + + } + } + + public int $b { + set ($v) { + + } + } + + public int $c { + set (string $v) { + + } + } + + public int|string $d { + set (string $v) { + + } + } + + public int $e { + /** @param positive-int $v */ + set (int $v) { + + } + } + + public int $f { + /** @param positive-int|array $v */ + set (int|array $v) { + + } + } + +} + +/** + * @template T + */ +class GenericFoo +{ + +} + +class MissingTypes +{ + + public array $a { + set { // do not report, taken care of above the property + } + } + + /** @var array */ + public array $b { + set { // do not report, inherited from property + } + } + + public array $c { + set (array $v) { // do not report, taken care of above the property + + } + } + + /** @var array */ + public array $d { + set (array $v) { // do not report, inherited from property + + } + } + + public int $e { + /** @param array $v */ + set (int|array $v) { // do not report, type specified + + } + } + + public int $f { + set (int|array $v) { // report + + } + } + + public int $g { + set (int|GenericFoo $value) { // report + + } + } + + public int $h { + set (int|callable $value) { // report + + } + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/short-set-property-hook-assign.php b/tests/PHPStan/Rules/Properties/data/short-set-property-hook-assign.php new file mode 100644 index 0000000000..0e305bf104 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/short-set-property-hook-assign.php @@ -0,0 +1,69 @@ += 8.4 + +namespace ShortSetPropertyHookAssign; + +class Foo +{ + + public int $i { + set => 'foo'; + } + + public int $i2 { + set => 1; + } + + /** @var non-empty-string */ + public string $s { + set => ''; + } + + /** @var non-empty-string */ + public string $s2 { + set => 'foo'; + } + +} + +/** + * @template T of Foo + */ +class GenericFoo +{ + + /** @var T */ + public Foo $a { + set => new Foo(); + } + + /** @var T */ + public Foo $a2 { + set => $this->a2; + } + + /** + * @param T $c + */ + public function __construct( + /** @var T */ + public Foo $b { + set => new Foo(); + }, + + /** @var T */ + public Foo $b2 { + set => $this->b2; + }, + + public Foo $c { + set => new Foo(); + }, + + public Foo $c2 { + set => $this->c2; + } + ) + { + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/static-hooked-properties.php b/tests/PHPStan/Rules/Properties/data/static-hooked-properties.php new file mode 100644 index 0000000000..101f4b3b28 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/static-hooked-properties.php @@ -0,0 +1,18 @@ + $this->foo; + set => $this->foo = $value; + } +} + +abstract class HiWorld +{ + public static string $foo { + get => 'dummy'; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/static-hooked-property-in-interface.php b/tests/PHPStan/Rules/Properties/data/static-hooked-property-in-interface.php new file mode 100644 index 0000000000..66f45edf78 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/static-hooked-property-in-interface.php @@ -0,0 +1,12 @@ + + */ +trait FooTrait +{ + +} + +#[\AllowDynamicProperties] +class Usages +{ + + use FooTrait; + +} + +class ChildUsages extends Usages +{ + +} + +function (Usages $u): void { + assertType(Usages::class, $u->a); +}; + +function (ChildUsages $u): void { + assertType(ChildUsages::class, $u->a); +}; diff --git a/tests/PHPStan/Rules/Properties/data/uninitialized-property-additional-constructors.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property-additional-constructors.php new file mode 100644 index 0000000000..8af919ecb3 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property-additional-constructors.php @@ -0,0 +1,22 @@ +two = $value; + } + + public function setThree(int $value): void + { + $this->three = $value; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly-phpdoc.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly-phpdoc.php new file mode 100644 index 0000000000..e39340d26c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly-phpdoc.php @@ -0,0 +1,65 @@ +bar; + $this->bar = 1; + } + +} + +/** @phpstan-immutable */ +class Immutable +{ + + private int $bar; + + public function __construct() + { + + } + +} + +/** @immutable */ +class A +{ + + public string $a; + +} + +class B extends A +{ + + public string $b; + +} + +class C extends B +{ + + public string $c; + +} diff --git a/tests/PHPStan/Rules/Properties/data/uninitialized-property.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property.php index 671ed79c8a..eea8ff632d 100644 --- a/tests/PHPStan/Rules/Properties/data/uninitialized-property.php +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property.php @@ -1,4 +1,4 @@ -= 7.4 +baz = 1; + } + +} + +class FooTraitClass +{ + + use FooTrait; + + public function __construct() + { + $this->foo = 1; + } + +} + +class ItemsCrate +{ + + /** + * @var int[] + */ + private array $items; + + /** + * @param int[] $items + */ + public function __construct( + array $items + ) + { + $this->items = $items; + $this->sortItems(); + } + + private function sortItems(): void + { + usort($this->items, static function ($a, $b): int { + return $a <=> $b; + }); + } + + public function addItem(int $i): void + { + $this->items[] = $i; + $this->sortItems(); + } + +} + +class InitializedInPrivateSetter +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + $this->foo = 1; + } + + public function doSomething() + { + echo $this->foo; + } + +} + +final class InitializedInPublicSetterFinalClass +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + public function setFoo() + { + $this->foo = 1; + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class InitializedInPublicSetterNonFinalClass +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + public function setFoo() + { + $this->foo = 1; + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class SometimesInitializedInPrivateSetter +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + if (rand(0, 1)) { + $this->foo = 1; + } + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class ConfuseNodeScopeResolverWithAnonymousClass +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + $c = new class () { + public function setFoo() + { + } + }; + $this->foo = 1; + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class ThrowInConstructor1 +{ + + private int $foo; + + public function __construct() + { + if (rand(0, 1)) { + $this->foo = 1; + return; + } + + throw new \Exception; + } + +} + +class ThrowInConstructor2 +{ + + private int $foo; + + public function __construct() + { + if (rand(0, 1)) { + throw new \Exception; + } + + $this->foo = 1; + } + +} + +class EarlyReturn +{ + + private int $foo; + + public function __construct() + { + if (rand(0, 1)) { + return; + } + + $this->foo = 1; + } + +} + +class NeverInConstructor +{ + + private int $foo; + + public function __construct() + { + if (rand(0, 1)) { + $this->foo = 1; + return; + } + + $this->returnNever(); + } + + /** + * @return never + */ + private function returnNever() + { + throw new \Exception(); + } + +} + +class InitializedInPrivateSetterWithThrow +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + if (rand(0, 1)) { + $this->foo = 1; + return; + } + + throw new \Exception(); + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class InitializedInPrivateSetterWithReturnNever +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + if (rand(0, 1)) { + $this->foo = 1; + return; + } + + $this->returnNever(); + } + + public function doSomething() + { + echo $this->foo; + } + + /** + * @return never + */ + private function returnNever() + { + throw new \Exception(); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/virtual-hooked-properties.php b/tests/PHPStan/Rules/Properties/data/virtual-hooked-properties.php new file mode 100644 index 0000000000..4c69f70d60 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/virtual-hooked-properties.php @@ -0,0 +1,22 @@ + $this->firstName; + set => $this->firstName = $value; + } + + public string $middleName { + get => $this->middleName; + set => $this->middleName = $value; + } + + public string $lastName = 'Doe' { + get => 'Smith'; + } + + public string $maidenName = 'Brown'; +} diff --git a/tests/PHPStan/Rules/Properties/data/virtual-nullsafe-property-fetch.php b/tests/PHPStan/Rules/Properties/data/virtual-nullsafe-property-fetch.php new file mode 100644 index 0000000000..a06b89d8b1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/virtual-nullsafe-property-fetch.php @@ -0,0 +1,4 @@ += 8.0 + +$foo->regularFetch; +$foo?->nullsafeFetch; diff --git a/tests/PHPStan/Rules/Properties/data/write-asymmetric-visibility.php b/tests/PHPStan/Rules/Properties/data/write-asymmetric-visibility.php new file mode 100644 index 0000000000..a6273caf09 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/write-asymmetric-visibility.php @@ -0,0 +1,84 @@ += 8.4 + +namespace WriteAsymmetricVisibility; + +class Foo +{ + + public private(set) int $a; + public protected(set) int $b; + public public(set) int $c; + + public function doFoo(): void + { + $this->a = 1; + $this->b = 1; + $this->c = 1; + } + +} + +class Bar extends Foo +{ + + public function doBar(): void + { + $this->a = 1; + $this->b = 1; + $this->c = 1; + } + +} + +function (Foo $foo): void { + $foo->a = 1; + $foo->b = 1; + $foo->c = 1; +}; + +class ReadonlyProps +{ + + public readonly int $a; + + protected readonly int $b; + + private readonly int $c; + + public function doFoo(): void + { + $this->a = 1; + $this->b = 1; + $this->c = 1; + } + +} + +class ChildReadonlyProps extends ReadonlyProps +{ + + public function doBar(): void + { + $this->a = 1; + $this->b = 1; + $this->c = 1; + } + +} + +function (ReadonlyProps $foo): void { + $foo->a = 1; + $foo->b = 1; + $foo->c = 1; +}; + +class ArrayProp +{ + + public private(set) array $a = []; + +} + +function (ArrayProp $foo): void { + $foo->a[] = 1; +}; diff --git a/tests/PHPStan/Rules/Properties/data/writing-to-read-only-hooked-properties.php b/tests/PHPStan/Rules/Properties/data/writing-to-read-only-hooked-properties.php new file mode 100644 index 0000000000..06953a9661 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/writing-to-read-only-hooked-properties.php @@ -0,0 +1,49 @@ += 8.4 + +namespace WritingToReadOnlyHookedProperties; + +interface Foo +{ + + public int $i { + // virtual, not writable + get; + } + +} + +function (Foo $f): void { + $f->i = 1; +}; + +class Bar +{ + + public int $i { + // virtual, not writable + get { + return 1; + } + } + +} + +function (Bar $b): void { + $b->i = 1; +}; + +class Baz +{ + + public int $i { + // backed, writable + get { + return $this->i + 1; + } + } + +} + +function (Baz $b): void { + $b->i = 1; +}; diff --git a/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php b/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php index 23ea31efdc..2de103f0b5 100644 --- a/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php +++ b/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php @@ -2,11 +2,16 @@ namespace WritingToReadOnlyProperties; +use AllowDynamicProperties; + /** * @property-read int $readOnlyProperty * @property int $usualProperty + * @property-read int $asymmetricProperty + * @property-write int|string $asymmetricProperty * @property-write int $writeOnlyProperty */ +#[AllowDynamicProperties] class Foo { @@ -28,6 +33,9 @@ public function doFoo() $self->usualProperty = 1; $self->usualProperty .= 1; + $self->asymmetricProperty = "1"; + $self->asymmetricProperty = 1; + $self->writeOnlyProperty = 1; $self->writeOnlyProperty .= 1; @@ -35,4 +43,9 @@ public function doFoo() $self->readOnlyProperty = &$s; } + public function doObjectShape() + { + + } + } diff --git a/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon new file mode 100644 index 0000000000..6362843ec8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Rules\Properties\TestInitializedProperty + tags: + - phpstan.additionalConstructorsExtension diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php new file mode 100644 index 0000000000..c310f6177c --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -0,0 +1,170 @@ + + */ +class PureFunctionRuleTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new PureFunctionRule(new FunctionPurityCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/pure-function.php'], [ + [ + 'Function PureFunction\doFoo() is marked as pure but parameter $p is passed by reference.', + 8, + ], + [ + 'Impure echo in pure function PureFunction\doFoo().', + 10, + ], + [ + 'Function PureFunction\doFoo2() is marked as pure but returns void.', + 16, + ], + [ + 'Impure exit in pure function PureFunction\doFoo2().', + 18, + ], + [ + 'Impure property assignment in pure function PureFunction\doFoo3().', + 26, + ], + [ + 'Possibly impure call to a callable in pure function PureFunction\testThese().', + 60, + ], + [ + 'Possibly impure call to a callable in pure function PureFunction\testThese().', + 61, + ], + [ + 'Impure call to function PureFunction\impureFunction() in pure function PureFunction\testThese().', + 63, + ], + [ + 'Impure call to function PureFunction\voidFunction() in pure function PureFunction\testThese().', + 64, + ], + [ + 'Possibly impure call to function PureFunction\possiblyImpureFunction() in pure function PureFunction\testThese().', + 65, + ], + [ + 'Possibly impure call to unknown function in pure function PureFunction\testThese().', + 66, + ], + [ + 'Function PureFunction\actuallyPure() is marked as impure but does not have any side effects.', + 72, + ], + [ + 'Function PureFunction\emptyVoidFunction() returns void but does not have any side effects.', + 84, + ], + [ + 'Impure access to superglobal variable in pure function PureFunction\pureButAccessSuperGlobal().', + 102, + ], + [ + 'Impure access to superglobal variable in pure function PureFunction\pureButAccessSuperGlobal().', + 103, + ], + [ + 'Impure access to superglobal variable in pure function PureFunction\pureButAccessSuperGlobal().', + 105, + ], + [ + 'Impure global variable in pure function PureFunction\functionWithGlobal().', + 118, + ], + [ + 'Impure static variable in pure function PureFunction\functionWithStaticVariable().', + 128, + ], + [ + 'Possibly impure call to a Closure in pure function PureFunction\callsClosures().', + 139, + ], + [ + 'Possibly impure call to a Closure in pure function PureFunction\callsClosures().', + 140, + ], + [ + 'Impure output between PHP opening and closing tags in pure function PureFunction\justContainsInlineHtml().', + 160, + ], + ]); + } + + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/first-class-callable-pure-function.php'], [ + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 61, + ], + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 64, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 70, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 73, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 75, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 81, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 84, + ], + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 90, + ], + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 93, + ], + [ + 'Possibly impure call to a callable in pure function FirstClassCallablePureFunction\callCallbackImmediately().', + 102, + ], + ]); + } + + public function testBug11361(): void + { + $this->analyse([__DIR__ . '/data/bug-11361-pure.php'], [ + [ + 'Impure call to a Closure with by-ref parameter in pure function Bug11361Pure\foo().', + 14, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php new file mode 100644 index 0000000000..19d1eed263 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -0,0 +1,215 @@ + + */ +class PureMethodRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + public function getRule(): Rule + { + return new PureMethodRule(new FunctionPurityCheck()); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testRule(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/pure-method.php'], [ + [ + 'Method PureMethod\Foo::doFoo() is marked as pure but parameter $p is passed by reference.', + 11, + ], + [ + 'Impure echo in pure method PureMethod\Foo::doFoo().', + 13, + ], + [ + 'Method PureMethod\Foo::doFoo2() is marked as pure but returns void.', + 19, + ], + [ + 'Impure die in pure method PureMethod\Foo::doFoo2().', + 21, + ], + [ + 'Impure property assignment in pure method PureMethod\Foo::doFoo3().', + 29, + ], + [ + 'Impure call to method PureMethod\Foo::voidMethod() in pure method PureMethod\Foo::doFoo4().', + 71, + ], + [ + 'Impure call to method PureMethod\Foo::impureVoidMethod() in pure method PureMethod\Foo::doFoo4().', + 72, + ], + [ + 'Possibly impure call to method PureMethod\Foo::returningMethod() in pure method PureMethod\Foo::doFoo4().', + 73, + ], + [ + 'Impure call to method PureMethod\Foo::impureReturningMethod() in pure method PureMethod\Foo::doFoo4().', + 75, + ], + [ + 'Possibly impure call to unknown method in pure method PureMethod\Foo::doFoo4().', + 76, + ], + [ + 'Impure call to method PureMethod\Foo::voidMethod() in pure method PureMethod\Foo::doFoo5().', + 84, + ], + [ + 'Impure call to method PureMethod\Foo::impureVoidMethod() in pure method PureMethod\Foo::doFoo5().', + 85, + ], + [ + 'Possibly impure call to method PureMethod\Foo::returningMethod() in pure method PureMethod\Foo::doFoo5().', + 86, + ], + [ + 'Impure call to method PureMethod\Foo::impureReturningMethod() in pure method PureMethod\Foo::doFoo5().', + 88, + ], + [ + 'Possibly impure call to unknown method in pure method PureMethod\Foo::doFoo5().', + 89, + ], + [ + 'Impure instantiation of class PureMethod\ImpureConstructor in pure method PureMethod\TestConstructors::doFoo().', + 140, + ], + [ + 'Possibly impure instantiation of class PureMethod\PossiblyImpureConstructor in pure method PureMethod\TestConstructors::doFoo().', + 141, + ], + [ + 'Possibly impure instantiation of unknown class in pure method PureMethod\TestConstructors::doFoo().', + 142, + ], + [ + 'Method PureMethod\ActuallyPure::doFoo() is marked as impure but does not have any side effects.', + 153, + ], + [ + 'Impure echo in pure method PureMethod\ExtendingClass::pure().', + 183, + ], + [ + 'Method PureMethod\ExtendingClass::impure() is marked as impure but does not have any side effects.', + 187, + ], + [ + 'Method PureMethod\ClassWithVoidMethods::privateEmptyVoidFunction() returns void but does not have any side effects.', + 214, + ], + [ + 'Impure assign to superglobal variable in pure method PureMethod\ClassWithVoidMethods::purePostGetAssign().', + 230, + ], + [ + 'Impure assign to superglobal variable in pure method PureMethod\ClassWithVoidMethods::purePostGetAssign().', + 231, + ], + [ + 'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doFoo().', + 295, + ], + [ + 'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doFoo().', + 296, + ], + [ + 'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().', + 330, + ], + [ + 'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().', + 330, + ], + [ + 'Impure static property access in pure method PureMethod\StaticMethodAccessingStaticProperty::getA().', + 388, + ], + [ + 'Impure property assignment in pure method PureMethod\StaticMethodAssigningStaticProperty::getA().', + 409, + ], + ]); + } + + public function testPureConstructor(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/pure-constructor.php'], [ + [ + 'Impure static property access in pure method PureConstructor\Foo::__construct().', + 19, + ], + [ + 'Impure property assignment in pure method PureConstructor\Foo::__construct().', + 19, + ], + [ + 'Method PureConstructor\Bar::__construct() is marked as impure but does not have any side effects.', + 30, + ], + [ + 'Impure property assignment in pure method PureConstructor\AssignOtherThanThis::__construct().', + 49, + ], + ]); + } + + public function testImpureAssignRef(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impure-assign-ref.php'], [ + [ + 'Possibly impure property assignment by reference in pure method ImpureAssignRef\HelloWorld::bar6().', + 49, + ], + ]); + } + + /** + * @dataProvider dataBug11207 + */ + public function testBug11207(bool $treatPhpDocTypesAsCertain): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/bug-11207.php'], []); + } + + public function dataBug11207(): array + { + return [ + [true], + [false], + ]; + } + + public function testBug12048(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12048.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Pure/data/bug-11207.php b/tests/PHPStan/Rules/Pure/data/bug-11207.php new file mode 100644 index 0000000000..e69bdb5573 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-11207.php @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug11207; + +final class FilterData +{ + /** + * @phpstan-pure + */ + private function __construct( + public ?int $type, + public bool $hasValue, + public mixed $value = null + ) { + } + + /** + * @param array{type?: int|numeric-string|null, value?: mixed} $data + * @phpstan-pure + */ + public static function fromArray(array $data): self + { + if (isset($data['type'])) { + if (!\is_int($data['type']) && (!\is_string($data['type']) || !is_numeric($data['type']))) { + throw new \InvalidArgumentException(sprintf( + 'The "type" parameter MUST be of type "integer" or "null", "%s" given.', + \gettype($data['type']) + )); + } + + $type = (int) $data['type']; + } else { + $type = null; + } + + return new self($type, \array_key_exists('value', $data), $data['value'] ?? null); + } +} diff --git a/tests/PHPStan/Rules/Pure/data/bug-11361-pure.php b/tests/PHPStan/Rules/Pure/data/bug-11361-pure.php new file mode 100644 index 0000000000..f8a52fffe5 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-11361-pure.php @@ -0,0 +1,17 @@ += 8.1 + +namespace FirstClassCallablePureFunction; + +class Foo +{ + + /** + * @phpstan-pure + */ + function pureFunction() + { + + } + + /** + * @phpstan-impure + */ + function impureFunction() + { + echo ''; + } + + function voidFunction(): void + { + echo 'test'; + } + +} + +/** + * @phpstan-pure + */ +function pureFunction() +{ + +} + +/** + * @phpstan-impure + */ +function impureFunction() +{ + echo ''; +} + +function voidFunction(): void +{ + echo 'test'; +} + +/** + * @phpstan-pure + */ +function testThese(Foo $foo) +{ + $cb = $foo->pureFunction(...); + $cb(); + + $cb = $foo->impureFunction(...); + $cb(); + + $cb = $foo->voidFunction(...); + $cb(); + + $cb = pureFunction(...); + $cb(); + + $cb = impureFunction(...); + $cb(); + + $cb = voidFunction(...); + $cb(); + + callCallbackImmediately($cb); + + $cb = 'FirstClassCallablePureFunction\\pureFunction'; + $cb(); + + $cb = 'FirstClassCallablePureFunction\\impureFunction'; + $cb(); + + $cb = 'FirstClassCallablePureFunction\\voidFunction'; + $cb(); + + $cb = [$foo, 'pureFunction']; + $cb(); + + $cb = [$foo, 'impureFunction']; + $cb(); + + $cb = [$foo, 'voidFunction']; + $cb(); +} + +/** + * @phpstan-pure + * @return int + */ +function callCallbackImmediately(callable $cb): int +{ + return $cb(); +} diff --git a/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php b/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php new file mode 100644 index 0000000000..15b1dac588 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php @@ -0,0 +1,63 @@ + */ + public array $arr = []; + /** @var array */ + public array $objectArr = []; + public static int $staticValue = 0; + /** @var array */ + public static array $staticArr = []; + + private function bar1(): void + { + $value = &$this->value; + $value = 1; + } + + private function bar2(): void + { + $value = &$this->arr[0]; + $value = 1; + } + + private function bar3(): void + { + $value = &self::$staticValue; + $value = 1; + } + + private function bar4(): void + { + $value = &self::$staticArr[0]; + $value = 1; + } + + private function bar5(self $other): void + { + $value = &$other->value; + $value = 1; + } + + /** @phpstan-pure */ + private function bar6(): int + { + $value = &$this->objectArr[0]->foo; + + return 1; + } + + public function foo(): void + { + $this->bar1(); + $this->bar2(); + $this->bar3(); + $this->bar4(); + $this->bar5(new self()); + $this->bar6(); + } +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-constructor.php b/tests/PHPStan/Rules/Pure/data/pure-constructor.php new file mode 100644 index 0000000000..71045fd3ed --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-constructor.php @@ -0,0 +1,51 @@ += 8.0 + +namespace PureConstructor; + +class Foo +{ + + private string $prop; + + public static $staticProp = 1; + + /** @phpstan-pure */ + public function __construct( + public int $test, + string $prop, + ) + { + $this->prop = $prop; + self::$staticProp++; + } + +} + +class Bar +{ + + private string $prop; + + /** @phpstan-impure */ + public function __construct( + public int $test, + string $prop, + ) + { + $this->prop = $prop; + } + +} + +class AssignOtherThanThis +{ + private int $i = 0; + + /** @phpstan-pure */ + public function __construct( + self $other, + ) + { + $other->i = 1; + } +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-function.php b/tests/PHPStan/Rules/Pure/data/pure-function.php new file mode 100644 index 0000000000..6a4bb319b3 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-function.php @@ -0,0 +1,166 @@ +foo = 'test'; +} + +/** + * @phpstan-pure + */ +function pureFunction() +{ + +} + +/** + * @phpstan-impure + */ +function impureFunction() +{ + echo ''; +} + +function voidFunction(): void +{ + echo 'test'; +} + +function possiblyImpureFunction() +{ + +} + +/** + * @phpstan-pure + */ +function testThese(string $s, callable $cb) +{ + $s(); + $cb(); + pureFunction(); + impureFunction(); + voidFunction(); + possiblyImpureFunction(); + unknownFunction(); +} + +/** + * @phpstan-impure + */ +function actuallyPure() +{ + +} + +function voidFunctionThatThrows(): void +{ + if (rand(0, 1)) { + throw new \Exception(); + } +} + +function emptyVoidFunction(): void +{ + $a = 1 + 1; +} + +/** + * @phpstan-assert !null $a + */ +function emptyVoidFunctionWithAssertTag(?int $a): void +{ + +} + +/** + * @phpstan-pure + */ +function pureButAccessSuperGlobal(): int +{ + $a = $_POST['bla']; + $_POST['test'] = 1; + + return $_POST['test']; +} + +function emptyVoidFunctionWithByRefParameter(&$a): void +{ + +} + +/** + * @phpstan-pure + */ +function functionWithGlobal(): int +{ + global $db; + + return 1; +} + +/** + * @phpstan-pure + */ +function functionWithStaticVariable(): int +{ + static $v = 1; + + return $v; +} + +/** + * @phpstan-pure + * @param \Closure(): int $closure2 + */ +function callsClosures(\Closure $closure1, \Closure $closure2): int +{ + $closure1(); + return $closure2(); +} + +/** + * @phpstan-pure + * @param pure-callable $cb + * @param pure-Closure $closure + * @return int + */ +function callsPureCallableIdentifierTypeNode(callable $cb, \Closure $closure): int +{ + $cb(); + $closure(); +} + + +/** @phpstan-pure */ +function justContainsInlineHtml() +{ + ?> + + + + + + foo = 'test'; + } + + public function voidMethod(): void + { + echo '1'; + } + + /** + * @phpstan-impure + */ + public function impureVoidMethod(): void + { + echo ''; + } + + public function returningMethod(): int + { + + } + + /** + * @phpstan-pure + */ + public function pureReturningMethod(): int + { + + } + + /** + * @phpstan-impure + */ + public function impureReturningMethod(): int + { + echo ''; + } + + /** + * @phpstan-pure + */ + public function doFoo4() + { + $this->voidMethod(); + $this->impureVoidMethod(); + $this->returningMethod(); + $this->pureReturningMethod(); + $this->impureReturningMethod(); + $this->unknownMethod(); + } + + /** + * @phpstan-pure + */ + public function doFoo5() + { + self::voidMethod(); + self::impureVoidMethod(); + self::returningMethod(); + self::pureReturningMethod(); + self::impureReturningMethod(); + self::unknownMethod(); + } + + +} + +class PureConstructor +{ + + /** + * @phpstan-pure + */ + public function __construct() + { + + } + +} + +class ImpureConstructor +{ + + /** + * @phpstan-impure + */ + public function __construct() + { + echo ''; + } + +} + +class PossiblyImpureConstructor +{ + + public function __construct() + { + + } + +} + +class TestConstructors +{ + + /** + * @phpstan-pure + */ + public function doFoo(string $s) + { + new PureConstructor(); + new ImpureConstructor(); + new PossiblyImpureConstructor(); + new $s(); + } + +} + +class ActuallyPure +{ + + /** + * @phpstan-impure + */ + public function doFoo() + { + + } + +} + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + public function pure(): int + { + echo 'test'; + return 1; + } + + public function impure(): int + { + return 1; + } + +} + +class ClassWithVoidMethods +{ + + public function voidFunctionThatThrows(): void + { + if (rand(0, 1)) { + throw new \Exception(); + } + } + + public function emptyVoidFunction(): void + { + + } + + protected function protectedEmptyVoidFunction(): void + { + + } + + private function privateEmptyVoidFunction(): void + { + $a = 1 + 1; + } + + private function setPostAndGet(array $post = [], array $get = []): void + { + $_POST = $post; + $_GET = $get; + } + + /** + * @phpstan-pure + */ + public function purePostGetAssign(array $post = [], array $get = []): int + { + $_POST = $post; + $_GET = $get; + + return 1; + } + +} + +class NoMagicMethods +{ + +} + +class PureMagicMethods +{ + + /** + * @phpstan-pure + */ + public function __toString(): string + { + return 'one'; + } + +} + +class MaybePureMagicMethods +{ + + public function __toString(): string + { + return 'one'; + } + +} + +class ImpureMagicMethods +{ + + /** + * @phpstan-impure + */ + public function __toString(): string + { + sleep(1); + return 'one'; + } + +} + +class TestMagicMethods +{ + + /** + * @phpstan-pure + */ + public function doFoo( + NoMagicMethods $no, + PureMagicMethods $pure, + MaybePureMagicMethods $maybe, + ImpureMagicMethods $impure + ) + { + (string) $no; + (string) $pure; + (string) $maybe; + (string) $impure; + } + +} + +class NoConstructor +{ + +} + +class TestNoConstructor +{ + + /** + * @phpstan-pure + */ + public function doFoo(): int + { + new NoConstructor(); + + return 1; + } + +} + +class MaybeCallableFromUnion +{ + + /** + * @phpstan-pure + * @param callable|string $p + */ + public function doFoo($p): int + { + $p(); + + return 1; + } + +} + +class VoidMethods +{ + + private function doFoo(): void + { + + } + + private function doBar(): void + { + \PHPStan\dumpType(1); + } + + private function doBaz(): void + { + // nop + ; + + // nop + ; + + // nop + ; + } + +} + +class AssertingImpureVoidMethod +{ + + /** + * @param mixed $value + * @phpstan-assert array $value + * @phpstan-impure + */ + public function assertSth($value): void + { + + } + +} + +class StaticMethodAccessingStaticProperty +{ + /** @var int */ + public static $a = 0; + /** + * @phpstan-pure + */ + public static function getA(): int + { + return self::$a; + } + + /** + * @phpstan-impure + */ + public static function getB(): int + { + return self::$a; + } +} + +class StaticMethodAssigningStaticProperty +{ + /** @var int */ + public static $a = 0; + /** + * @phpstan-pure + */ + public static function getA(): int + { + self::$a = 1; + + return 1; + } + + /** + * @phpstan-impure + */ + public static function getB(): int + { + self::$a = 1; + + return 1; + } +} diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php index 4c17fc2f0c..99f777b2ff 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\Regex\RegexExpressionHelper; +use function sprintf; use const PHP_VERSION_ID; /** @@ -14,209 +16,191 @@ class RegularExpressionPatternRuleTest extends RuleTestCase protected function getRule(): Rule { - return new RegularExpressionPatternRule(); + return new RegularExpressionPatternRule( + self::getContainer()->getByType(RegexExpressionHelper::class), + ); } - public function testValidRegexPatternBefore73(): void + public function testValidRegexPattern(): void { - if (PHP_VERSION_ID >= 70300) { - $this->markTestSkipped('This test requires PHP < 7.3.0'); + $messagePart = 'alphanumeric or backslash'; + if (PHP_VERSION_ID >= 80200) { + $messagePart = 'alphanumeric, backslash, or NUL'; + } + if (PHP_VERSION_ID >= 80400) { + $messagePart = 'alphanumeric, backslash, or NUL byte'; } $this->analyse( [__DIR__ . '/data/valid-regex-pattern.php'], [ [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 6, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 7, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 11, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 12, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 16, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 17, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 21, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 22, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 26, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 27, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 29, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 29, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 32, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 33, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 35, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 35, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 38, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 39, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 41, ], [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 41, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 43, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 43, ], - ], - ); - } - - public function testValidRegexPatternAfter73(): void - { - if (PHP_VERSION_ID < 70300) { - $this->markTestSkipped('This test requires PHP >= 7.3.0'); - } - - $this->analyse( - [__DIR__ . '/data/valid-regex-pattern.php'], - [ - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 6, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 7, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 11, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 12, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 16, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 17, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 21, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 22, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 26, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 27, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 29, - ], - [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 29, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 32, - ], [ 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 33, - ], - [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 35, + 43, ], [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 35, + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok(?:.*)', $messagePart), + 57, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 38, + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok(?:.*)', $messagePart), + 58, ], [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 39, + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 7 in pattern: ~((?:.*)~', + 59, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 41, + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok(?:.*)nono', $messagePart), + 61, ], [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 41, + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok(?:.*)nope', $messagePart), + 62, ], [ - 'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok', - 43, + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 7 in pattern: ~((?:.*)~', + 63, ], + ], + ); + } + + /** + * @param list $errors + * @dataProvider dataArrayShapePatterns + */ + public function testArrayShapePatterns(string $file, array $errors): void + { + $this->analyse( + [$file], + $errors, + ); + } + + public function dataArrayShapePatterns(): iterable + { + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_match_all_shapes.php', + [], + ]; + + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_match_shapes.php', + [ [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 43, + "Regex pattern is invalid: Unknown modifier 'y' in pattern: /(foo)(bar)(baz)/xyz", + 124, ], ], - ); + ]; + + if (PHP_VERSION_ID >= 80000) { + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_match_shapes_php80.php', + [], + ]; + } + + if (PHP_VERSION_ID >= 80200) { + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_match_shapes_php82.php', + [], + ]; + } + + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_replace_callback_shapes.php', + [], + ]; + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_replace_callback_shapes-php72.php', + [], + ]; } } diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php new file mode 100644 index 0000000000..38197c1aa6 --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php @@ -0,0 +1,98 @@ + + */ +class RegularExpressionQuotingRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RegularExpressionQuotingRule( + $this->createReflectionProvider(), + self::getContainer()->getByType(RegexExpressionHelper::class), + ); + } + + public function testRule(): void + { + $this->analyse( + [__DIR__ . '/data/preg-quote.php'], + [ + [ + 'Call to preg_quote() is missing delimiter & to be effective.', + 6, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 7, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 11, + ], + [ + 'Call to preg_quote() is missing delimiter & to be effective.', + 12, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 18, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 20, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 21, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 22, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 23, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 24, + ], + [ + 'Call to preg_quote() is missing delimiter parameter to be effective.', + 77, + ], + ], + ); + } + + public function testRulePhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse( + [__DIR__ . '/data/preg-quote-php8.php'], + [ + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 6, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 7, + ], + ], + ); + } + +} diff --git a/tests/PHPStan/Rules/Regexp/data/preg-quote-php8.php b/tests/PHPStan/Rules/Regexp/data/preg-quote-php8.php new file mode 100644 index 0000000000..83e19cf03b --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/data/preg-quote-php8.php @@ -0,0 +1,13 @@ += 8.0 + +namespace PregQuotingPhp8; + +function doFoo(string $s, callable $cb): void { // errors + preg_split(subject: $s, pattern: '&' . preg_quote('&oops', '/') . 'pattern&'); + preg_split(subject: $s, pattern: '&' . preg_quote(delimiter: '/', str: '&oops') . 'pattern&'); +} + +function ok(string $s): void { // ok + preg_split(subject: $s, pattern: '&' . preg_quote('&oops', '&') . 'pattern&'); + preg_split(subject: $s, pattern: '&' . preg_quote(delimiter: '&', str: '&oops') . 'pattern&'); +} diff --git a/tests/PHPStan/Rules/Regexp/data/preg-quote.php b/tests/PHPStan/Rules/Regexp/data/preg-quote.php new file mode 100644 index 0000000000..5333d72f58 --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/data/preg-quote.php @@ -0,0 +1,96 @@ +getRules(Node\Expr\FuncCall::class); - $this->assertCount(1, $rules); - $this->assertSame($rule, $rules[0]); - - $this->assertCount(0, $registry->getRules(Node\Expr\MethodCall::class)); - } - - public function testGetRulesWithTwoDifferentInstances(): void - { - $fooRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => ['Foo error']); - $barRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => ['Bar error']); - - $registry = new Registry([ - $fooRule, - $barRule, - ]); - - $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(Node\Expr\MethodCall::class)); - } - -} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php new file mode 100644 index 0000000000..cd24bbb3be --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php @@ -0,0 +1,43 @@ + + */ +class RestrictedClassConstantUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new RestrictedClassConstantUsageRule( + self::getContainer(), + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, true, true, false, true), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/restricted-class-constant.php'], [ + [ + 'Cannot access FOO', + 17, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRuleTest.php new file mode 100644 index 0000000000..d3ca0c99a3 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRuleTest.php @@ -0,0 +1,45 @@ + + */ +class RestrictedFunctionCallableUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RestrictedFunctionCallableUsageRule( + self::getContainer(), + $this->createReflectionProvider(), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/restricted-function-callable.php'], [ + [ + 'Cannot call doFoo', + 7, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionUsageRuleTest.php new file mode 100644 index 0000000000..7bab882e70 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionUsageRuleTest.php @@ -0,0 +1,40 @@ + + */ +class RestrictedFunctionUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RestrictedFunctionUsageRule( + self::getContainer(), + $this->createReflectionProvider(), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/restricted-function.php'], [ + [ + 'Cannot call doFoo', + 17, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php new file mode 100644 index 0000000000..c5de137c29 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php @@ -0,0 +1,45 @@ + + */ +class RestrictedMethodCallableUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new RestrictedMethodCallableUsageRule( + self::getContainer(), + $this->createReflectionProvider(), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/restricted-method-callable.php'], [ + [ + 'Cannot call doFoo', + 13, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php new file mode 100644 index 0000000000..fd88cbd72b --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php @@ -0,0 +1,40 @@ + + */ +class RestrictedMethodUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new RestrictedMethodUsageRule( + self::getContainer(), + $this->createReflectionProvider(), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/restricted-method.php'], [ + [ + 'Cannot call doFoo', + 13, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php new file mode 100644 index 0000000000..177a5d3414 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php @@ -0,0 +1,40 @@ + + */ +class RestrictedPropertyUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new RestrictedPropertyUsageRule( + self::getContainer(), + $this->createReflectionProvider(), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/restricted-property.php'], [ + [ + 'Cannot access $foo', + 17, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php new file mode 100644 index 0000000000..804042289a --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php @@ -0,0 +1,48 @@ + + */ +class RestrictedStaticMethodCallableUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + return new RestrictedStaticMethodCallableUsageRule( + self::getContainer(), + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, true, true, false, true), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/restricted-method-callable.php'], [ + [ + 'Cannot call doFoo', + 36, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php new file mode 100644 index 0000000000..fdefc81398 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php @@ -0,0 +1,43 @@ + + */ +class RestrictedStaticMethodUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + return new RestrictedStaticMethodUsageRule( + self::getContainer(), + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, true, true, false, true), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/restricted-method.php'], [ + [ + 'Cannot call doFoo', + 36, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php new file mode 100644 index 0000000000..e1b057bf6b --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php @@ -0,0 +1,43 @@ + + */ +class RestrictedStaticPropertyUsageRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + return new RestrictedStaticPropertyUsageRule( + self::getContainer(), + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, true, true, false, true), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/restricted-property.php'], [ + [ + 'Cannot access $foo', + 34, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/restricted-usage.neon', + ...parent::getAdditionalConfigFiles(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/ClassConstantExtension.php b/tests/PHPStan/Rules/RestrictedUsage/data/ClassConstantExtension.php new file mode 100644 index 0000000000..033b140dab --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/ClassConstantExtension.php @@ -0,0 +1,25 @@ +getName() !== 'FOO') { + return null; + } + + return RestrictedUsage::create('Cannot access FOO', 'restrictedUsage.foo'); + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/FunctionExtension.php b/tests/PHPStan/Rules/RestrictedUsage/data/FunctionExtension.php new file mode 100644 index 0000000000..555b5b0a52 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/FunctionExtension.php @@ -0,0 +1,25 @@ +getName() !== 'RestrictedUsage\\doFoo') { + return null; + } + + return RestrictedUsage::create('Cannot call doFoo', 'restrictedUsage.doFoo'); + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/MethodExtension.php b/tests/PHPStan/Rules/RestrictedUsage/data/MethodExtension.php new file mode 100644 index 0000000000..4fee1f8eef --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/MethodExtension.php @@ -0,0 +1,25 @@ +getName() !== 'doFoo') { + return null; + } + + return RestrictedUsage::create('Cannot call doFoo', 'restrictedUsage.doFoo'); + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/PropertyExtension.php b/tests/PHPStan/Rules/RestrictedUsage/data/PropertyExtension.php new file mode 100644 index 0000000000..52cfb126b6 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/PropertyExtension.php @@ -0,0 +1,25 @@ +getName() !== 'foo') { + return null; + } + + return RestrictedUsage::create('Cannot access $foo', 'restrictedUsage.foo'); + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/restricted-class-constant.php b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-class-constant.php new file mode 100644 index 0000000000..dd6f1c8402 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-class-constant.php @@ -0,0 +1,20 @@ += 8.1 + +namespace RestrictedUsage; + +function (): void { + doNonexistent(...); + doFoo(...); + doBar(...); +}; diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/restricted-function.php b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-function.php new file mode 100644 index 0000000000..5026200f05 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-function.php @@ -0,0 +1,19 @@ += 8.1 + +namespace RestrictedMethodCallableUsage; + +class Foo +{ + + public function doTest(Nonexistent $c): void + { + $c->test(...); + $this->doNonexistent(...); + $this->doBar(...); + $this->doFoo(...); + } + + public function doBar(): void + { + + } + + public function doFoo(): void + { + + } + +} + +class FooStatic +{ + + public static function doTest(): void + { + Nonexistent::test(...); + self::doNonexistent(...); + self::doBar(...); + self::doFoo(...); + } + + public static function doBar(): void + { + + } + + public static function doFoo(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/restricted-method.php b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-method.php new file mode 100644 index 0000000000..70e14b0e03 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-method.php @@ -0,0 +1,49 @@ +test(); + $this->doNonexistent(); + $this->doBar(); + $this->doFoo(); + } + + public function doBar(): void + { + + } + + public function doFoo(): void + { + + } + +} + +class FooStatic +{ + + public static function doTest(): void + { + Nonexistent::test(); + self::doNonexistent(); + self::doBar(); + self::doFoo(); + } + + public static function doBar(): void + { + + } + + public static function doFoo(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/restricted-property.php b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-property.php new file mode 100644 index 0000000000..8f8e41e1ad --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-property.php @@ -0,0 +1,37 @@ +test; + $this->doNonexistent; + $this->bar; + $this->foo; + } + +} + +class FooStatic +{ + + public static $bar; + + public static $foo; + + public static function doTest(): void + { + Nonexistent::$test; + self::$nonexistent; + self::$bar; + self::$foo; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/restricted-usage.neon b/tests/PHPStan/Rules/RestrictedUsage/restricted-usage.neon new file mode 100644 index 0000000000..0287117161 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/restricted-usage.neon @@ -0,0 +1,17 @@ +services: + - + class: RestrictedUsage\ClassConstantExtension + tags: + - phpstan.restrictedClassConstantUsageExtension + - + class: RestrictedUsage\MethodExtension + tags: + - phpstan.restrictedMethodUsageExtension + - + class: RestrictedUsage\FunctionExtension + tags: + - phpstan.restrictedFunctionUsageExtension + - + class: RestrictedUsage\PropertyExtension + tags: + - phpstan.restrictedPropertyUsageExtension diff --git a/tests/PHPStan/Rules/RuleErrorBuilderTest.php b/tests/PHPStan/Rules/RuleErrorBuilderTest.php index 345b48f77e..59826f7172 100644 --- a/tests/PHPStan/Rules/RuleErrorBuilderTest.php +++ b/tests/PHPStan/Rules/RuleErrorBuilderTest.php @@ -20,30 +20,30 @@ public function testMessageAndLineAndBuild(): void $ruleError = $builder->build(); $this->assertSame('Foo', $ruleError->getMessage()); - $this->assertInstanceOf(LineRuleError::class, $ruleError); + $this->assertInstanceOf(LineRuleError::class, $ruleError); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame(25, $ruleError->getLine()); } public function testMessageAndFileAndBuild(): void { - $builder = RuleErrorBuilder::message('Foo')->file('Bar.php'); + $builder = RuleErrorBuilder::message('Foo')->file(__FILE__); $ruleError = $builder->build(); $this->assertSame('Foo', $ruleError->getMessage()); - $this->assertInstanceOf(FileRuleError::class, $ruleError); - $this->assertSame('Bar.php', $ruleError->getFile()); + $this->assertInstanceOf(FileRuleError::class, $ruleError); // @phpstan-ignore method.alreadyNarrowedType + $this->assertSame(__FILE__, $ruleError->getFile()); } public function testMessageAndLineAndFileAndBuild(): void { - $builder = RuleErrorBuilder::message('Foo')->line(25)->file('Bar.php'); + $builder = RuleErrorBuilder::message('Foo')->line(25)->file(__FILE__); $ruleError = $builder->build(); $this->assertSame('Foo', $ruleError->getMessage()); - $this->assertInstanceOf(LineRuleError::class, $ruleError); - $this->assertInstanceOf(FileRuleError::class, $ruleError); + $this->assertInstanceOf(LineRuleError::class, $ruleError); // @phpstan-ignore method.alreadyNarrowedType + $this->assertInstanceOf(FileRuleError::class, $ruleError); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame(25, $ruleError->getLine()); - $this->assertSame('Bar.php', $ruleError->getFile()); + $this->assertSame(__FILE__, $ruleError->getFile()); } } diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackRule.php b/tests/PHPStan/Rules/ScopeFunctionCallStackRule.php new file mode 100644 index 0000000000..573602c8dd --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackRule.php @@ -0,0 +1,38 @@ + */ +class ScopeFunctionCallStackRule implements Rule +{ + + public function getNodeType(): string + { + return Throw_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $messages = []; + foreach ($scope->getFunctionCallStack() as $reflection) { + if ($reflection instanceof FunctionReflection) { + $messages[] = $reflection->getName(); + continue; + } + + $messages[] = sprintf('%s::%s', $reflection->getDeclaringClass()->getDisplayName(), $reflection->getName()); + } + + return [ + RuleErrorBuilder::message(implode("\n", $messages))->identifier('dummy')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackRuleTest.php b/tests/PHPStan/Rules/ScopeFunctionCallStackRuleTest.php new file mode 100644 index 0000000000..d59d22835e --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackRuleTest.php @@ -0,0 +1,41 @@ + + */ +class ScopeFunctionCallStackRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ScopeFunctionCallStackRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/scope-function-call-stack.php'], [ + [ + "var_dump\nprint_r\nsleep", + 7, + ], + [ + "var_dump\nprint_r\nsleep", + 10, + ], + [ + "var_dump\nprint_r\nsleep", + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRule.php b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRule.php new file mode 100644 index 0000000000..b26df715b8 --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRule.php @@ -0,0 +1,42 @@ + */ +class ScopeFunctionCallStackWithParametersRule implements Rule +{ + + public function getNodeType(): string + { + return Throw_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $messages = []; + foreach ($scope->getFunctionCallStackWithParameters() as [$reflection, $parameter]) { + if ($parameter === null) { + throw new ShouldNotHappenException(); + } + if ($reflection instanceof FunctionReflection) { + $messages[] = sprintf('%s ($%s)', $reflection->getName(), $parameter->getName()); + continue; + } + + $messages[] = sprintf('%s::%s ($%s)', $reflection->getDeclaringClass()->getDisplayName(), $reflection->getName(), $parameter->getName()); + } + + return [ + RuleErrorBuilder::message(implode("\n", $messages))->identifier('dummy')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php new file mode 100644 index 0000000000..38a0aecd61 --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php @@ -0,0 +1,41 @@ + + */ +class ScopeFunctionCallStackWithParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ScopeFunctionCallStackWithParametersRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/scope-function-call-stack.php'], [ + [ + "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", + 7, + ], + [ + "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", + 10, + ], + [ + "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php index cf82486a1e..3abc8d8643 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -19,9 +18,6 @@ protected function getRule(): Rule public function testRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } $this->analyse([__DIR__ . '/data/tooWideArrowFunctionReturnType.php'], [ [ 'Anonymous function never returns null so it can be removed from the return type.', diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php new file mode 100644 index 0000000000..a7406ad81b --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php @@ -0,0 +1,43 @@ + + */ +class TooWideFunctionParameterOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TooWideFunctionParameterOutTypeRule(new TooWideParameterOutTypeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/too-wide-function-parameter-out.php'], [ + [ + 'Function TooWideFunctionParameterOut\doBar() never assigns null to &$p so it can be removed from the by-ref type.', + 10, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Function TooWideFunctionParameterOut\doBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 18, + ], + [ + 'Function TooWideFunctionParameterOut\doLorem() never assigns null to &$p so it can be removed from the by-ref type.', + 23, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Function TooWideFunctionParameterOut\bug10699() never assigns 20 to &$out so it can be removed from the @param-out type.', + 48, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index 8b6438796d..9c86812e92 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -44,6 +44,20 @@ public function testRule(): void 'Function TooWideFunctionReturnType\dolor6() never returns null so it can be removed from the return type.', 79, ], + [ + 'Function TooWideFunctionReturnType\conditionalType() never returns string so it can be removed from the return type.', + 90, + ], + ]); + } + + public function testBug11980(): void + { + $this->analyse([__DIR__ . '/data/bug-11980-function.php'], [ + [ + 'Function Bug11980Function\process2() never returns void so it can be removed from the return type.', + 34, + ], ]); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php new file mode 100644 index 0000000000..cc2ef739c8 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php @@ -0,0 +1,53 @@ + + */ +class TooWideMethodParameterOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TooWideMethodParameterOutTypeRule(new TooWideParameterOutTypeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/too-wide-method-parameter-out.php'], [ + [ + 'Method TooWideMethodParameterOut\Foo::doBar() never assigns null to &$p so it can be removed from the by-ref type.', + 13, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\Foo::doBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 21, + ], + [ + 'Method TooWideMethodParameterOut\Foo::doLorem() never assigns null to &$p so it can be removed from the by-ref type.', + 26, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\Foo::bug10699() never assigns 20 to &$out so it can be removed from the @param-out type.', + 37, + ], + ]); + } + + public function testBug10684(): void + { + $this->analyse([__DIR__ . '/data/bug-10684.php'], []); + } + + public function testBug10687(): void + { + $this->analyse([__DIR__ . '/data/bug-10687.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index 0f3ff533a1..a98c638d24 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -12,9 +12,11 @@ class TooWideMethodReturnTypehintRuleTest extends RuleTestCase { + private bool $checkProtectedAndPublicMethods = true; + protected function getRule(): Rule { - return new TooWideMethodReturnTypehintRule(true); + return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods); } public function testPrivate(): void @@ -44,6 +46,10 @@ public function testPrivate(): void 'Method TooWideMethodReturnType\Foo::dolor6() never returns null so it can be removed from the return type.', 86, ], + [ + 'Method TooWideMethodReturnType\ConditionalTypeClass::conditionalType() never returns string so it can be removed from the return type.', + 119, + ], ]); } @@ -97,4 +103,101 @@ public function testBug6158(): void $this->analyse([__DIR__ . '/data/bug-6158.php'], []); } + public function testBug6175(): void + { + $this->analyse([__DIR__ . '/data/bug-6175.php'], []); + } + + public function dataAlwaysCheckFinal(): iterable + { + yield [ + false, + [ + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\Foo::test() never returns null so it can be removed from the return type.', + 8, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test() never returns null so it can be removed from the return type.', + 28, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test2() never returns null so it can be removed from the return type.', + 33, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test3() never returns null so it can be removed from the return type.', + 38, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test() never returns null so it can be removed from the return type.', + 48, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test2() never returns null so it can be removed from the return type.', + 53, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test3() never returns null so it can be removed from the return type.', + 58, + ], + ], + ]; + + yield [ + true, + [ + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\Foo::test() never returns null so it can be removed from the return type.', + 8, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test() never returns null so it can be removed from the return type.', + 28, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test2() never returns null so it can be removed from the return type.', + 33, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test3() never returns null so it can be removed from the return type.', + 38, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test() never returns null so it can be removed from the return type.', + 48, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test2() never returns null so it can be removed from the return type.', + 53, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test3() never returns null so it can be removed from the return type.', + 58, + ], + ], + ]; + } + + /** + * @dataProvider dataAlwaysCheckFinal + * @param list $expectedErrors + */ + public function testAlwaysCheckFinal(bool $checkProtectedAndPublicMethods, array $expectedErrors): void + { + $this->checkProtectedAndPublicMethods = $checkProtectedAndPublicMethods; + $this->analyse([__DIR__ . '/data/method-too-wide-return-always-check-final.php'], $expectedErrors); + } + + public function testBug11980(): void + { + $this->checkProtectedAndPublicMethods = true; + $this->analyse([__DIR__ . '/data/bug-11980.php'], [ + [ + 'Method Bug11980\Demo::process2() never returns void so it can be removed from the return type.', + 37, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php new file mode 100644 index 0000000000..1171abd564 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php @@ -0,0 +1,64 @@ + + */ +class TooWidePropertyTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new TooWidePropertyTypeRule( + new DirectReadWritePropertiesExtensionProvider([]), + new PropertyReflectionFinder(), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/too-wide-property-type.php'], [ + [ + 'Property TooWidePropertyType\Foo::$foo (int|string) is never assigned string so it can be removed from the property type.', + 9, + ], + /*[ + 'Property TooWidePropertyType\Foo::$barr (int|null) is never assigned null so it can be removed from the property type.', + 15, + ], + [ + 'Property TooWidePropertyType\Foo::$barrr (int|null) is never assigned null so it can be removed from the property type.', + 18, + ],*/ + [ + 'Property TooWidePropertyType\Foo::$baz (int|null) is never assigned null so it can be removed from the property type.', + 20, + ], + [ + 'Property TooWidePropertyType\Bar::$c (int|null) is never assigned int so it can be removed from the property type.', + 45, + ], + [ + 'Property TooWidePropertyType\Bar::$d (int|null) is never assigned null so it can be removed from the property type.', + 47, + ], + ]); + } + + public function testBug11667(): void + { + $this->analyse([__DIR__ . '/data/bug-11667.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php new file mode 100644 index 0000000000..4335dabd72 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php @@ -0,0 +1,41 @@ +hook)(); + } catch (HookBreaker $e) { + $brokenBy = $e; + + return $e->getReturnValue(); + } + } + + return $return; + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10687.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10687.php new file mode 100644 index 0000000000..eb8f9c5f42 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10687.php @@ -0,0 +1,20 @@ +|null */ + private $matches = null; + + public function match(string $string): void { + preg_match('/Hello (\w+)/', $string, $this->matches); + } + + /** @return list|null */ + public function get(): ?array { + return $this->matches; + } +} + +final class HelloWorld2 { + /** @var list|null */ + private $matches = null; + + public function match(string $string): void { + $this->paramOut($this->matches); + } + + /** + * @param mixed $a + * @param-out list $a + */ + public function paramOut(&$a): void + { + + } + + /** @return list|null */ + public function get(): ?array { + return $this->matches; + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php new file mode 100644 index 0000000000..aaafea889d --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php @@ -0,0 +1,70 @@ +> $tokens + * @param int $stackPtr + * + * @return int|void + */ +function process($tokens, $stackPtr) +{ + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } +} + +/** + * @param array> $tokens + * @param int $stackPtr + * + * @return int|void + */ +function process2($tokens, $stackPtr) +{ + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return null; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } + + return 1; +} + +/** @return int|void */ +function process3( int $code ) { + + if ( $code === \T_CLASS ) { + return process_class( $code ); + } + + process_function( $code ); +} + +/** @return int */ +function process_class(int $code) { + return $code; +} + +/** @return void */ +function process_function(int $code) { +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php new file mode 100644 index 0000000000..d45bb08576 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php @@ -0,0 +1,74 @@ +> $tokens + * @param int $stackPtr + * + * @return int|void + */ + public function process($tokens, $stackPtr) + { + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } + } + + /** + * @param array> $tokens + * @param int $stackPtr + * + * @return int|void + */ + public function process2($tokens, $stackPtr) + { + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return null; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } + + return 1; + } + + /** @return int|void */ + public function process3( int $code ) { + + if ( $code === \T_CLASS ) { + return $this->process_class( $code ); + } + + $this->process_function( $code ); + } + + /** @return int */ + public function process_class(int $code) { + return $code; + } + + /** @return void */ + public function process_function(int $code) { + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php index 3dd1798fc2..0bcc2f80df 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php @@ -1,4 +1,4 @@ -= 7.4 +value; + } +} + +class HelloWorld2 +{ + use SomeTrait; + private string $value = ''; + public function sayIt(): void + { + echo $this->sayHello(); + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/method-too-wide-return-always-check-final.php b/tests/PHPStan/Rules/TooWideTypehints/data/method-too-wide-return-always-check-final.php new file mode 100644 index 0000000000..9a2093ad1d --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/method-too-wide-return-always-check-final.php @@ -0,0 +1,63 @@ += 8.0 + +namespace TooWidePropertyType; + +class Foo +{ + + /** @var int|string */ + private $foo; + + /** @var int|null */ + private $bar; // do not report "null" as not assigned + + /** @var int|null */ + private $barr = 1; // report "null" as not assigned + + /** @var int|null */ + private $barrr; // assigned in constructor - report "null" as not assigned + + private int|null $baz; // report "null" as not assigned + + public function __construct() + { + $this->barrr = 1; + } + + public function doFoo(): void + { + $this->foo = 1; + $this->bar = 1; + $this->barr = 1; + $this->barrr = 1; + $this->baz = 1; + } + +} + +class Bar +{ + + private ?int $a = null; + + private ?int $b = 1; + + private ?int $c = null; + + private ?int $d = 1; + + public function doFoo(): void + { + $this->a = 1; + $this->b = null; + } + +} + +class Baz +{ + + private ?int $a = null; + + public function doFoo(): self + { + $s = new self(); + $s->a = 1; + + return $s; + } + +} + +class Lorem +{ + + public function __construct( + private ?int $a = null + ) + { + + } + + public function doFoo(): void + { + $this->a = 1; + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/tooWideArrowFunctionReturnType.php b/tests/PHPStan/Rules/TooWideTypehints/data/tooWideArrowFunctionReturnType.php index 632430bdc9..11cfd16fdc 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/tooWideArrowFunctionReturnType.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/tooWideArrowFunctionReturnType.php @@ -1,4 +1,4 @@ -= 7.4 + + */ +class ConflictingTraitConstantsRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ConflictingTraitConstantsRule(self::getContainer()->getByType(InitializerExprTypeResolver::class), $this->createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/conflicting-trait-constants.php'], [ + [ + 'Protected constant ConflictingTraitConstants\Bar::PUBLIC_CONSTANT overriding public constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT should also be public.', + 23, + ], + [ + 'Private constant ConflictingTraitConstants\Bar2::PUBLIC_CONSTANT overriding public constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT should also be public.', + 32, + ], + [ + 'Public constant ConflictingTraitConstants\Bar3::PROTECTED_CONSTANT overriding protected constant ConflictingTraitConstants\Foo::PROTECTED_CONSTANT should also be protected.', + 41, + ], + [ + 'Private constant ConflictingTraitConstants\Bar4::PROTECTED_CONSTANT overriding protected constant ConflictingTraitConstants\Foo::PROTECTED_CONSTANT should also be protected.', + 50, + ], + [ + 'Protected constant ConflictingTraitConstants\Bar5::PRIVATE_CONSTANT overriding private constant ConflictingTraitConstants\Foo::PRIVATE_CONSTANT should also be private.', + 59, + ], + [ + 'Public constant ConflictingTraitConstants\Bar6::PRIVATE_CONSTANT overriding private constant ConflictingTraitConstants\Foo::PRIVATE_CONSTANT should also be private.', + 68, + ], + [ + 'Non-final constant ConflictingTraitConstants\Bar7::PUBLIC_FINAL_CONSTANT overriding final constant ConflictingTraitConstants\Foo::PUBLIC_FINAL_CONSTANT should also be final.', + 77, + ], + [ + 'Final constant ConflictingTraitConstants\Bar8::PUBLIC_CONSTANT overriding non-final constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT should also be non-final.', + 86, + ], + [ + 'Constant ConflictingTraitConstants\Bar9::PUBLIC_CONSTANT with value 2 overriding constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT with different value 1 should have the same value.', + 96, + ], + ]); + } + + public function testNativeTypes(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/conflicting-trait-constants-types.php'], [ + [ + 'Constant ConflictingTraitConstantsTypes\Baz::FOO_CONST (int) overriding constant ConflictingTraitConstantsTypes\Foo::FOO_CONST (int|string) should have the same native type int|string.', + 28, + ], + [ + 'Constant ConflictingTraitConstantsTypes\Baz::BAR_CONST (int) overriding constant ConflictingTraitConstantsTypes\Foo::BAR_CONST should not have a native type.', + 30, + ], + [ + 'Constant ConflictingTraitConstantsTypes\Lorem::FOO_CONST overriding constant ConflictingTraitConstantsTypes\Foo::FOO_CONST (int|string) should also have native type int|string.', + 39, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php b/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php new file mode 100644 index 0000000000..3b6f591527 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php @@ -0,0 +1,57 @@ + + */ +class ConstantsInTraitsRuleTest extends RuleTestCase +{ + + private int $phpVersionId; + + protected function getRule(): Rule + { + return new ConstantsInTraitsRule(new PhpVersion($this->phpVersionId)); + } + + public function dataRule(): array + { + return [ + [ + 80100, + [ + [ + 'Constant is declared inside a trait but is only supported on PHP 8.2 and later.', + 7, + ], + [ + 'Constant is declared inside a trait but is only supported on PHP 8.2 and later.', + 8, + ], + ], + ], + [ + 80200, + [], + ], + ]; + } + + /** + * @dataProvider dataRule + * + * @param list $errors + */ + public function testRule(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/constants-in-traits.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Traits/NotAnalysedTraitRuleTest.php b/tests/PHPStan/Rules/Traits/NotAnalysedTraitRuleTest.php new file mode 100644 index 0000000000..03544a59d6 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/NotAnalysedTraitRuleTest.php @@ -0,0 +1,38 @@ + + */ +class NotAnalysedTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NotAnalysedTraitRule(); + } + + protected function getCollectors(): array + { + return [ + new TraitDeclarationCollector(), + new TraitUseCollector(), + ]; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/not-analysed-trait.php'], [ + [ + 'Trait NotAnalysedTrait\Bar is used zero times and is not analysed.', + 10, + 'See: https://phpstan.org/blog/how-phpstan-analyses-traits', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php new file mode 100644 index 0000000000..b4e2455cb8 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -0,0 +1,104 @@ + + */ +class TraitAttributesRuleTest extends RuleTestCase +{ + + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new TraitAttributesRule( + new AttributesCheck( + $reflectionProvider, + new FunctionCallParametersCheck( + new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), + new NullsafeCheck(), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, + true, + true, + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + ), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/trait-attributes.php'], [ + [ + 'Attribute class TraitAttributes\AbstractAttribute is abstract.', + 8, + ], + [ + 'Attribute class TraitAttributes\MyTargettedAttribute does not have the class target.', + 20, + ], + ]); + } + + public function testBug12011(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12011.php'], [ + [ + 'Parameter #1 $name of attribute class Bug12011Trait\Table constructor expects string|null, int given.', + 8, + ], + ]); + } + + public function testBug12281(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-12281.php'], [ + [ + 'Attribute class AllowDynamicProperties cannot be used with trait.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Traits/data/bug-12011.php b/tests/PHPStan/Rules/Traits/data/bug-12011.php new file mode 100644 index 0000000000..32b09d38d3 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/bug-12011.php @@ -0,0 +1,26 @@ += 8.3 + +namespace Bug12011Trait; + +use Attribute; + + +#[Table(self::TABLE_NAME)] +trait MyTrait +{ + private const int TABLE_NAME = 1; +} + +class X { + use MyTrait; +} + +#[Attribute(Attribute::TARGET_CLASS)] +final class Table +{ + public function __construct( + public readonly string|null $name = null, + public readonly string|null $schema = null, + ) { + } +} diff --git a/tests/PHPStan/Rules/Traits/data/bug-12281.php b/tests/PHPStan/Rules/Traits/data/bug-12281.php new file mode 100644 index 0000000000..da7d088f1a --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/bug-12281.php @@ -0,0 +1,19 @@ += 8.2 + +namespace Bug12281Traits; + +#[\AllowDynamicProperties] +enum BlogDataEnum { /* … */ } // reported by ClassAttributesRule + +#[\AllowDynamicProperties] +interface BlogDataInterface { /* … */ } // reported by ClassAttributesRule + +#[\AllowDynamicProperties] +trait BlogDataTrait { /* … */ } + +class Uses +{ + + use BlogDataTrait; + +} diff --git a/tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php b/tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php new file mode 100644 index 0000000000..eaa130ffeb --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php @@ -0,0 +1,41 @@ += 8.2 + +namespace ConflictingTraitConstants; + +trait Foo +{ + + public const PUBLIC_CONSTANT = 1; + + protected const PROTECTED_CONSTANT = 1; + + private const PRIVATE_CONSTANT = 1; + + final public const PUBLIC_FINAL_CONSTANT = 1; + +} + +class Bar +{ + + use Foo; + + protected const PUBLIC_CONSTANT = 1; + +} + +class Bar2 +{ + + use Foo; + + private const PUBLIC_CONSTANT = 1; + +} + +class Bar3 +{ + + use Foo; + + public const PROTECTED_CONSTANT = 1; + +} + +class Bar4 +{ + + use Foo; + + private const PROTECTED_CONSTANT = 1; + +} + +class Bar5 +{ + + use Foo; + + protected const PRIVATE_CONSTANT = 1; + +} + +class Bar6 +{ + + use Foo; + + public const PRIVATE_CONSTANT = 1; + +} + +class Bar7 +{ + + use Foo; + + public const PUBLIC_FINAL_CONSTANT = 1; + +} + +class Bar8 +{ + + use Foo; + + final public const PUBLIC_CONSTANT = 1; + +} + + +class Bar9 +{ + + use Foo; + + public const PUBLIC_CONSTANT = 2; + +} + +class Bar10 +{ + use Foo; + + final public const PUBLIC_FINAL_CONSTANT = 1; +} diff --git a/tests/PHPStan/Rules/Traits/data/constants-in-traits.php b/tests/PHPStan/Rules/Traits/data/constants-in-traits.php new file mode 100644 index 0000000000..f13d2fd172 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/constants-in-traits.php @@ -0,0 +1,14 @@ += 8.2 + +namespace ConstantsInTraits; + +trait FooBar +{ + const FOO = 'foo'; + public const BAR = 'bar', QUX = 'qux'; +} + +class Consumer +{ + use FooBar; +} diff --git a/tests/PHPStan/Rules/Traits/data/not-analysed-trait.php b/tests/PHPStan/Rules/Traits/data/not-analysed-trait.php new file mode 100644 index 0000000000..f84bc5f44f --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/not-analysed-trait.php @@ -0,0 +1,20 @@ + + */ +class InvalidTypesInUnionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidTypesInUnionRule(); + } + + public function testRuleOnUnionWithVoid(): void + { + $this->analyse([__DIR__ . '/data/invalid-union-with-void.php'], [ + [ + 'Type void cannot be part of a union type declaration.', + 11, + ], + [ + 'Type void cannot be part of a nullable type declaration.', + 15, + ], + ]); + } + + /** + * @requires PHP 8.0 + */ + public function testRuleOnUnionWithMixed(): void + { + $this->analyse([__DIR__ . '/data/invalid-union-with-mixed.php'], [ + [ + 'Type mixed cannot be part of a nullable type declaration.', + 9, + ], + [ + 'Type mixed cannot be part of a union type declaration.', + 12, + ], + [ + 'Type mixed cannot be part of a union type declaration.', + 16, + ], + [ + 'Type mixed cannot be part of a union type declaration.', + 17, + ], + [ + 'Type mixed cannot be part of a union type declaration.', + 22, + ], + [ + 'Type mixed cannot be part of a nullable type declaration.', + 29, + ], + [ + 'Type mixed cannot be part of a nullable type declaration.', + 29, + ], + [ + 'Type mixed cannot be part of a nullable type declaration.', + 34, + ], + ]); + } + + /** + * @requires PHP 8.1 + */ + public function testRuleOnUnionWithNever(): void + { + $this->analyse([__DIR__ . '/data/invalid-union-with-never.php'], [ + [ + 'Type never cannot be part of a nullable type declaration.', + 7, + ], + [ + 'Type never cannot be part of a union type declaration.', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Types/data/invalid-union-with-mixed.php b/tests/PHPStan/Rules/Types/data/invalid-union-with-mixed.php new file mode 100644 index 0000000000..2db7a96193 --- /dev/null +++ b/tests/PHPStan/Rules/Types/data/invalid-union-with-mixed.php @@ -0,0 +1,34 @@ + $a; diff --git a/tests/PHPStan/Rules/Types/data/invalid-union-with-never.php b/tests/PHPStan/Rules/Types/data/invalid-union-with-never.php new file mode 100644 index 0000000000..d959067f89 --- /dev/null +++ b/tests/PHPStan/Rules/Types/data/invalid-union-with-never.php @@ -0,0 +1,24 @@ + */ private $nodeType; - /** @var (callable(TNodeType, Scope): array) */ + /** @var (callable(TNodeType, Scope): list) */ private $processNodeCallback; /** * @param class-string $nodeType - * @param (callable(TNodeType, Scope): array) $processNodeCallback + * @param (callable(TNodeType, Scope): list) $processNodeCallback */ public function __construct(string $nodeType, callable $processNodeCallback) { @@ -35,7 +35,7 @@ public function getNodeType(): string /** * @param TNodeType $node - * @return array + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 6349d31808..71ad7280c8 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -66,6 +66,10 @@ public function testDefinedVariables(): void 'Undefined variable: $parseStrParameter', 34, ], + [ + 'Undefined variable: $parseStrParameter', + 36, + ], [ 'Undefined variable: $foo', 39, @@ -98,6 +102,10 @@ public function testDefinedVariables(): void 'Undefined variable: $variableInEmpty', 145, ], + [ + 'Undefined variable: $negatedVariableInEmpty', + 152, + ], [ 'Undefined variable: $variableInEmpty', 155, @@ -207,27 +215,23 @@ public function testDefinedVariables(): void 360, ], [ - 'Undefined variable: $variableInWhileIsset', - 365, - ], - [ - 'Undefined variable: $unknownVariablePassedToReset', + 'Variable $unknownVariablePassedToReset might not be defined.', 368, ], [ - 'Undefined variable: $unknownVariablePassedToReset', + 'Variable $unknownVariablePassedToReset might not be defined.', 369, ], [ - 'Undefined variable: $variableInAssign', + 'Variable $variableInAssign might not be defined.', 384, ], [ - 'Undefined variable: $undefinedArrayIndex', + 'Variable $undefinedArrayIndex might not be defined.', 409, ], [ - 'Undefined variable: $anotherUndefinedArrayIndex', + 'Variable $anotherUndefinedArrayIndex might not be defined.', 409, ], [ @@ -569,7 +573,7 @@ public function dataForeachPolluteScopeWithAlwaysIterableForeach(): array /** * @dataProvider dataForeachPolluteScopeWithAlwaysIterableForeach * - * @param mixed[] $errors + * @param list $errors */ public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void { @@ -600,10 +604,6 @@ public function testBooleanOperatorsTruthyFalsey(): void public function testArrowFunctions(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; @@ -622,10 +622,6 @@ public function testArrowFunctions(): void public function testCoalesceAssign(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; @@ -754,10 +750,6 @@ public function testClosureUse(): void public function testNullsafeIsset(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; @@ -817,10 +809,6 @@ public function testBug3283(): void 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; @@ -862,4 +850,340 @@ public function testBug6112(): void $this->analyse([__DIR__ . '/data/bug-6112.php'], []); } + public function testBug3601(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-3601.php'], []); + } + + public function testBug1016(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-1016.php'], []); + } + + public function testBug1016b(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-1016b.php'], []); + } + + public function testBug8142(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-8142.php'], []); + } + + public function testBug5401(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-5401.php'], []); + } + + public function testBug8212(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-8212.php'], []); + } + + public function testBug4173(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-4173.php'], [ + [ + 'Variable $value might not be defined.', // could be fixed + 30, + ], + ]); + } + + public function testBug5805(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-5805.php'], []); + } + + public function testBug8467c(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = false; + $this->analyse([__DIR__ . '/data/bug-8467c.php'], [ + [ + 'Variable $v might not be defined.', + 16, + ], + [ + 'Variable $v might not be defined.', + 18, + ], + ]); + } + + public function testBug393(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-393.php'], []); + } + + public function testBug9474(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-9474.php'], []); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/defined-variables-enum.php'], []); + } + + public function testBug5326(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-5326.php'], []); + } + + public function testBug5266(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-5266.php'], []); + } + + public function testIsStringNarrowsCertainty(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/isstring-certainty.php'], [ + [ + 'Variable $a might not be defined.', + 11, + ], + [ + 'Undefined variable: $a', + 19, + ], + ]); + } + + public function testDiscussion10252(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/discussion-10252.php'], []); + } + + public function testBug10418(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-10418.php'], []); + } + + public function testPassByReferenceIntoNotNullable(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/pass-by-reference-into-not-nullable.php'], [ + [ + 'Undefined variable: $three', + 32, + ], + ]); + } + + public function testBug10228(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-10228.php'], []); + } + + public function testPropertyHooks(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/property-hooks.php'], [ + [ + 'Undefined variable: $val', + 16, + ], + [ + 'Undefined variable: $value', + 28, + ], + [ + 'Undefined variable: $val', + 43, + ], + [ + 'Undefined variable: $value', + 51, + ], + ]); + } + + public function testDynamicAccess(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/dynamic-access.php'], [ + [ + 'Undefined variable: $bar', + 15, + ], + [ + 'Undefined variable: $bar', + 18, + ], + [ + 'Undefined variable: $buz', + 18, + ], + [ + 'Variable $foo might not be defined.', + 36, + ], + [ + 'Variable $foo might not be defined.', + 37, + ], + [ + 'Variable $bar might not be defined.', + 38, + ], + [ + 'Variable $bar might not be defined.', + 40, + ], + [ + 'Variable $foo might not be defined.', + 41, + ], + [ + 'Variable $bar might not be defined.', + 42, + ], + [ + 'Undefined variable: $buz', + 44, + ], + [ + 'Undefined variable: $foo', + 45, + ], + [ + 'Undefined variable: $bar', + 46, + ], + [ + 'Undefined variable: $buz', + 49, + ], + [ + 'Variable $bar might not be defined.', + 49, + ], + [ + 'Variable $foo might not be defined.', + 49, + ], + [ + 'Variable $foo might not be defined.', + 50, + ], + [ + 'Variable $bar might not be defined.', + 51, + ], + ]); + } + + public function testBug8719(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-8719.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php index 4fcb08548b..178101b951 100644 --- a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.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 @@ -31,20 +32,25 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + 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.', + 'Offset \'nonexistent\' on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, 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.', + 'Offset 3 on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, 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.', + 'Offset 4 on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, true} in empty() always exists and is not falsy.', 25, ], [ @@ -81,4 +87,148 @@ public function testBug970(): void ]); } + public function testBug6974(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-6974.php'], [ + [ + 'Variable $a in empty() always exists and is always falsy.', + 12, + ], + ]); + } + + public function testBug6974TreatPhpDocTypesAsCertain(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6974.php'], [ + [ + 'Variable $a in empty() always exists and is always falsy.', + 12, + ], + [ + 'Variable $a in empty() always exists and is not falsy.', + 30, + ], + ]); + } + + public function testBug7109(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ + [ + 'Using nullsafe property access "?->aaa" in empty() is unnecessary. Use -> instead.', + 19, + ], + [ + 'Using nullsafe property access "?->aaa" in empty() is unnecessary. Use -> instead.', + 30, + ], + [ + 'Using nullsafe property access "?->aaa" in empty() is unnecessary. Use -> instead.', + 42, + ], + [ + 'Using nullsafe property access "?->notFalsy" in empty() is unnecessary. Use -> instead.', + 54, + ], + [ + 'Expression in empty() is not falsy.', + 59, + ], + [ + 'Using nullsafe property access "?->aaa" in empty() is unnecessary. Use -> instead.', + 68, + ], + [ + 'Using nullsafe property access "?->(Expression)" in empty() is unnecessary. Use -> instead.', + 75, + ], + ]); + } + + public function testBug7318(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../Properties/data/bug-7318.php'], []); + } + + public function testBug7424(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-7424.php'], []); + } + + public function testBug7724(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-7724.php'], []); + } + + public function testBug7199(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-7199.php'], []); + } + + public function testBug9126(): void + { + $this->treatPhpDocTypesAsCertain = false; + + $this->analyse([__DIR__ . '/data/bug-9126.php'], []); + } + + public function dataBug9403(): iterable + { + yield [true]; + yield [false]; + } + + /** + * @dataProvider dataBug9403 + */ + public function testBug9403(bool $treatPhpDocTypesAsCertain): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + + $this->analyse([__DIR__ . '/data/bug-9403.php'], []); + } + + public function testBug12658(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-12658.php'], []); + } + + public function testIssetAfterRememberedConstructor(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/isset-after-remembered-constructor.php'], [ + [ + 'Property IssetOrCoalesceOnNonNullableInitializedProperty\MoreEmptyCases::$false in empty() is always falsy and initialized.', + 93, + ], + [ + 'Property IssetOrCoalesceOnNonNullableInitializedProperty\MoreEmptyCases::$true in empty() is not falsy nor uninitialized.', + 95, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 1ee394fea7..fb3dbb66ee 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -32,6 +32,11 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; @@ -200,9 +205,6 @@ public function testRuleWithoutTreatPhpDocTypesAsCertain(): void 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'], [ /*[ @@ -271,11 +273,13 @@ public function testVariableCertaintyInIsset(): void 112, ], [ - 'Variable $variableInFirstCase in isset() always exists and is not nullable.', + // could be Variable $variableInFirstCase in isset() always exists and is not nullable. + 'Variable $variableInFirstCase in isset() is never defined.', 116, ], [ - 'Variable $variableInSecondCase in isset() always exists and is always null.', + // could be Variable $variableInSecondCase in isset() always exists and is not nullable. + 'Variable $variableInSecondCase in isset() is never defined.', 117, ], [ @@ -322,12 +326,175 @@ public function testIssetInGlobalScope(): void public function testNullsafe(): void { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/isset-nullsafe.php'], [ + [ + 'Using nullsafe property access "?->bla" in isset() is unnecessary. Use -> instead.', + 10, + ], + ]); + } + + public function testBug7109(): void + { + if (PHP_VERSION_ID < 80000) { $this->markTestSkipped('Test requires PHP 8.0.'); } $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/isset-nullsafe.php'], []); + + $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ + [ + 'Using nullsafe property access "?->aaa" in isset() is unnecessary. Use -> instead.', + 18, + ], + [ + 'Using nullsafe property access "?->aaa" in isset() is unnecessary. Use -> instead.', + 29, + ], + [ + 'Expression in isset() is not nullable.', + 41, + ], + [ + 'Using nullsafe property access "?->aaa" in isset() is unnecessary. Use -> instead.', + 67, + ], + [ + 'Expression in isset() is not nullable.', + 74, + ], + ]); + } + + public function testBug7318(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../Properties/data/bug-7318.php'], [ + [ + "Offset 'unique' on array{unique: bool} in isset() always exists and is not nullable.", + 27, + ], + ]); + } + + public function testBug6163(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-6163.php'], []); + } + + public function testBug6997(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-6997.php'], []); + } + + public function testBug7776(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7776.php'], []); + } + + public function testBug6008(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-6008.php'], []); + } + + public function testBug7292(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-7292.php'], []); + } + + public function testObjectShapes(): void + { + $this->treatPhpDocTypesAsCertain = true; + + // could be checked but current is not + $this->analyse([__DIR__ . '/data/isset-object-shapes.php'], []); + } + + public function testBug10151(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-10151.php'], []); + } + + public function testBug3985(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3985.php'], [ + [ + 'Variable $foo in isset() is never defined.', + 13, + ], + [ + 'Variable $foo in isset() is never defined.', + 21, + ], + ]); + } + + public function testBug10064(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-10064.php'], []); + } + + public function testVirtualProperty(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/isset-virtual-property.php'], [ + [ + 'Property IssetVirtualProperty\Example::$noon (DateTimeImmutable) in isset() is not nullable.', + 16, + ], + ]); + } + + public function testBug9328(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-9328.php'], []); + } + + public function testBug12771(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-12771.php'], []); + } + + public function testIssetAfterRememberedConstructor(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/isset-after-remembered-constructor.php'], [ + [ + 'Property IssetOrCoalesceOnNonNullableInitializedProperty\User::$string in isset() is not nullable nor uninitialized.', + 34, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 4bc02691d9..ba73fbe2fb 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -32,6 +32,11 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + public function testCoalesceRule(): void { $this->treatPhpDocTypesAsCertain = true; @@ -140,10 +145,6 @@ public function testCoalesceRule(): void public function testCoalesceAssignRule(): void { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/null-coalesce-assign.php'], [ [ @@ -207,10 +208,6 @@ public function testCoalesceAssignRule(): void 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/null-coalesce-nullsafe.php'], []); } @@ -236,10 +233,6 @@ public function testVariableCertaintyInNullCoalesce(): void 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'], [ [ @@ -268,4 +261,116 @@ public function testNullCoalesceInGlobalScope(): void ]); } + public function testBug5933(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5933.php'], []); + } + + public function testBug7109(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [ + [ + 'Using nullsafe property access "?->aaa" on left side of ?? is unnecessary. Use -> instead.', + 17, + ], + [ + 'Using nullsafe property access "?->aaa" on left side of ?? is unnecessary. Use -> instead.', + 28, + ], + [ + 'Expression on left side of ?? is not nullable.', + 40, + ], + [ + 'Using nullsafe property access "?->aaa" on left side of ?? is unnecessary. Use -> instead.', + 66, + ], + [ + 'Expression on left side of ?? is not nullable.', + 73, + ], + ]); + } + + public function testBug7190(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../Properties/data/bug-7190.php'], [ + [ + 'Offset int on array on left side of ?? always exists and is not nullable.', + 20, + ], + ]); + } + + public function testBug7318(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/../Properties/data/bug-7318.php'], [ + [ + "Offset 'unique' on array{unique: bool} on left side of ?? always exists and is not nullable.", + 24, + ], + ]); + } + + public function testBug7968(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-7968.php'], []); + } + + public function testBug8084(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-8084.php'], []); + } + + public function testBug10577(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-10577.php'], []); + } + + public function testBug10610(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-10610.php'], []); + } + + public function testBug12553(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12553.php'], []); + } + + public function testIssetAfterRememberedConstructor(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/isset-after-remembered-constructor.php'], [ + [ + 'Property IssetOrCoalesceOnNonNullableInitializedProperty\User::$string on left side of ?? is not nullable nor uninitialized.', + 46, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php new file mode 100644 index 0000000000..f8268f8fcd --- /dev/null +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -0,0 +1,67 @@ + + */ +class ParameterOutAssignedTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ParameterOutAssignedTypeRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, false, true), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/parameter-out-assigned-type.php'], [ + [ + 'Parameter &$p @param-out type of function ParameterOutAssignedType\foo() expects int, string given.', + 10, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doFoo() expects int, string given.', + 21, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBar() expects string, int given.', + 29, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz() expects list, array<0|int<2, max>, int> given.', + 38, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz2() expects list, non-empty-list<\'str\'|int> given.', + 47, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz3() expects list>, array, array, int>> given.', + 56, + ], + [ + 'Parameter &$p by-ref type of method ParameterOutAssignedType\Foo::doNoParamOut() expects string, int given.', + 61, + 'You can change the parameter out type with @param-out PHPDoc tag.', + ], + ]); + } + + public function testBug10699(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10699.php'], []); + } + + public function testBenevolentArrayKey(): void + { + $this->analyse([__DIR__ . '/data/benevolent-array-key.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php new file mode 100644 index 0000000000..5929aad03a --- /dev/null +++ b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php @@ -0,0 +1,61 @@ + + */ +class ParameterOutExecutionEndTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ParameterOutExecutionEndTypeRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, false, true), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/parameter-out-execution-end.php'], [ + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo2() expects string, string|null given.', + 21, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo2() expects string, string|null given.', + 23, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo3() expects string, string|null given.', + 34, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo4() expects string, string|null given.', + 47, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo6() expects int, string given.', + 69, + ], + [ + 'Parameter &$p @param-out type of function ParameterOutExecutionEnd\foo2() expects string, string|null given.', + 80, + ], + [ + 'Parameter &$p @param-out type of function ParameterOutExecutionEnd\foo2() expects string, string|null given.', + 82, + ], + ]); + } + + public function testBug11363(): void + { + $this->analyse([__DIR__ . '/data/bug-11363.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php deleted file mode 100644 index 0e18f94b78..0000000000 --- a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ -class ThrowTypeRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new ThrowTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); - } - - public function testRule(): void - { - $this->analyse( - [__DIR__ . '/data/throw-values.php'], - [ - [ - 'Invalid type int to throw.', - 29, - ], - [ - 'Invalid type ThrowValues\InvalidException to throw.', - 32, - ], - [ - 'Invalid type ThrowValues\InvalidInterfaceException to throw.', - 35, - ], - [ - 'Invalid type Exception|null to throw.', - 38, - ], - [ - 'Throwing object of an unknown class ThrowValues\NonexistentClass.', - 44, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - ], - ); - } - - 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 370fe14ea6..df34c15d50 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -2,8 +2,12 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,7 +17,10 @@ class UnsetRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnsetRule(); + return new UnsetRule( + self::getContainer()->getByType(PropertyReflectionFinder::class), + self::getContainer()->getByType(PhpVersion::class), + ); } public function testUnsetRule(): void @@ -57,4 +64,160 @@ public function testBug4289(): void $this->analyse([__DIR__ . '/data/bug-4289.php'], []); } + public function testBug5223(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5223.php'], [ + [ + 'Cannot unset offset \'page\' on array{categoryKeys: array, tagNames: array}.', + 20, + ], + [ + 'Cannot unset offset \'limit\' on array{categoryKeys: array, tagNames: array}.', + 23, + ], + ]); + } + + public function testBug3391(): void + { + $this->analyse([__DIR__ . '/data/bug-3391.php'], []); + } + + public function testBug7417(): void + { + $this->analyse([__DIR__ . '/data/bug-7417.php'], []); + } + + public function testBug8113(): void + { + $this->analyse([__DIR__ . '/data/bug-8113.php'], []); + } + + public function testBug4565(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4565.php'], []); + } + + public function testBug12421(): void + { + $errors = []; + if (PHP_VERSION_ID >= 80400) { + $errors[] = [ + 'Cannot unset property Bug12421\RegularProperty::$y because it might have hooks in a subclass.', + 6, + ]; + $errors[] = [ + 'Cannot unset property Bug12421\RegularProperty::$y because it might have hooks in a subclass.', + 9, + ]; + } + + $errors = array_merge($errors, [ + [ + 'Cannot unset readonly Bug12421\NativeReadonlyClass::$y property.', + 13, + ], + [ + 'Cannot unset readonly Bug12421\NativeReadonlyProperty::$y property.', + 17, + ], + [ + 'Cannot unset @readonly Bug12421\PhpdocReadonlyClass::$y property.', + 21, + ], + [ + 'Cannot unset @readonly Bug12421\PhpdocReadonlyProperty::$y property.', + 25, + ], + [ + 'Cannot unset @readonly Bug12421\PhpdocImmutableClass::$y property.', + 29, + ], + [ + 'Cannot unset readonly Bug12421\NativeReadonlyProperty::$y property.', + 36, + ], + ]); + + $this->analyse([__DIR__ . '/data/bug-12421.php'], $errors); + } + + public function testUnsetHookedProperty(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/unset-hooked-property.php'], [ + [ + 'Cannot unset hooked UnsetHookedProperty\User::$name property.', + 6, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', + 7, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\Foo::$ii property.', + 9, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\Foo::$iii property.', + 10, + ], + [ + 'Cannot unset property UnsetHookedProperty\NonFinalClass::$publicProperty because it might have hooks in a subclass.', + 13, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$finalClass because it might have hooks in a subclass.', + 83, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$nonFinalClass because it might have hooks in a subclass.', + 87, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\Foo::$iii property.', + 89, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$foo because it might have hooks in a subclass.', + 90, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$name property.', + 92, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', + 93, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$user because it might have hooks in a subclass.', + 94, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$name property.', + 96, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$name property.', + 97, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', + 98, + ], + [ + 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', + 99, + ], + [ + 'Cannot unset property UnsetHookedProperty\ContainerClass::$arrayOfUsers because it might have hooks in a subclass.', + 100, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php index e7c170f694..d26101b421 100644 --- a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php +++ b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php @@ -15,7 +15,7 @@ class VariableCloningRuleTest extends RuleTestCase protected function getRule(): Rule { - return new VariableCloningRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new VariableCloningRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true)); } public function testClone(): void @@ -38,8 +38,12 @@ public function testClone(): void 19, ], [ - 'Cloning object of an unknown class VariableCloning\Bar.', + 'Cannot clone non-object variable $baz of type VariableCloning\Bar|VariableCloning\Foo|null.', 23, + ], + [ + 'Cloning object of an unknown class VariableCloning\Bar.', + 35, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); diff --git a/tests/PHPStan/Rules/Variables/data/benevolent-array-key.php b/tests/PHPStan/Rules/Variables/data/benevolent-array-key.php new file mode 100644 index 0000000000..83be0c39f6 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/benevolent-array-key.php @@ -0,0 +1,53 @@ + $matches + * @param-out array> $matches + */ + public static function matchAllStrictGroups(array &$matches): int + { + $result = self::matchAll($matches); + + return $result; + } + + /** + * @param array $matches + * @param-out array> $matches + */ + public static function matchAll(array &$matches): int + { + $matches = [['foo']]; + + return 1; + } +} + +class HelloWorld2 +{ + /** + * @param array $matches + * @param-out array> $matches + */ + public static function matchAllStrictGroups(array &$matches): int + { + $result = self::matchAll($matches); + + return $result; + } + + /** + * @param array $matches + * @param-out array> $matches + */ + public static function matchAll(array &$matches): int + { + $matches = [['foo']]; + + return 1; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-10064.php b/tests/PHPStan/Rules/Variables/data/bug-10064.php new file mode 100644 index 0000000000..4f8808c669 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10064.php @@ -0,0 +1,23 @@ + 5 ? 42: null; // a possibly null var + $b = random_int(0, 10) > 6 ? 47: null; // a possibly null var + if (isset($a, $b)) { + return $check > $a && $check < $b; + } + if (isset($a)) { + return $check > $a; + } + if (isset($b)) { + return $check < $b; + } + + return false; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-10151.php b/tests/PHPStan/Rules/Variables/data/bug-10151.php new file mode 100644 index 0000000000..78d5c4219d --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10151.php @@ -0,0 +1,25 @@ + + */ + protected array $cache = []; + + public function getCachedItemId (string $keyName): void + { + $result = $this->cache[$keyName] ??= ($newIndex = count($this->cache) + 1); + + // WRONG ERROR: Variable $newIndex in isset() always exists and is not nullable. + if (isset($newIndex)) { + $this->recordNewCacheItem($keyName); + } + } + + protected function recordNewCacheItem (string $keyName): void { + // ... + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-1016.php b/tests/PHPStan/Rules/Variables/data/bug-1016.php new file mode 100644 index 0000000000..1282667cf4 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-1016.php @@ -0,0 +1,21 @@ += 8.1 + +namespace Bug10418; + +function (): void { + $text = '123'; + $result = match(1){ + preg_match('/(\d+)/', $text, $match) => 'matched number: ' . $match[1], + preg_match('/(\w+)/', $text, $match) => 'matched word: ' . json_encode($match), + default => 'no matches!' + }; +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-10577.php b/tests/PHPStan/Rules/Variables/data/bug-10577.php new file mode 100644 index 0000000000..36ecae0c59 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10577.php @@ -0,0 +1,41 @@ + 'Test1', + '20' => 'Test2', + ]; + + + public function validate(string $value): void + { + $value = trim($value); + + if ($value === '') { + throw new \RuntimeException(); + } + + assertType("non-empty-string", $value); + assertType("'Test1'|'Test2'", self::MAP[$value]); + + $value = self::MAP[$value] ?? $value; + + assertType("non-empty-string", $value); + assertType("'Test1'|'Test2'", self::MAP[$value]); + + // ... + } + + public function validateNumericString(string $value): void + { + if (!is_numeric($value)) return; + + assertType("numeric-string", $value); + assertType("'Test1'|'Test2'", self::MAP[$value]); + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-10610.php b/tests/PHPStan/Rules/Variables/data/bug-10610.php new file mode 100644 index 0000000000..d56a2a8b07 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10610.php @@ -0,0 +1,87 @@ + [ + '19' => '582', + '26' => '689', + '56' => '817', + '52' => '1050', + '67' => '2923', + '78' => '4057', + '75' => '4078', + '54' => '4078', + '76' => '4079', + '77' => '4080', + '9' => '4080', + '46' => '4091', + '22' => '4111', + '48' => '4112', + '70' => '4113', + '42' => '4117', + '43' => '4118', + '6' => '4126', + '36' => '4129', + '13' => '4309', + '14' => '4904', + '5' => '5222', + '71' => '5223', + '73' => '5242', + '74' => '5250', + '24' => '5252', + '58' => '5255', + '35' => '5261', + '1' => '5264', + '20' => '5268', + '21' => '5269', + '31' => '5270', + '51' => '5271', + '55' => '5271', + '39' => '5274', + '50' => '5277', + '49' => '5278', + '11' => '5279', + '41' => '5279', + '44' => '5280', + '59' => '5281', + '60' => '5281', + '23' => '5281', + '72' => '5283', + '32' => '5283', + '8' => '5285', + '40' => '5285', + '12' => '5298', + '37' => '5305', + '65' => '5310', + '64' => '5310', + '57' => '5352', + '33' => '5364', + '25' => '5375', + '34' => '5460', + '45' => '7581', + '3' => '7624', + '53' => '7672', + '999' => '7953', + '69' => '7953', + '2' => '8206', + '7' => '9697', + ], + 'bar' => [ + '30' => 'Test3', + ], + ]; + + public function validate(string $k, string $value): void + { + $res = self::MAP[$k][$value] ?? ''; + + assertType("'1050'|'2923'|'4057'|'4078'|'4079'|'4080'|'4091'|'4111'|'4112'|'4113'|'4117'|'4118'|'4126'|'4129'|'4309'|'4904'|'5222'|'5223'|'5242'|'5250'|'5252'|'5255'|'5261'|'5264'|'5268'|'5269'|'5270'|'5271'|'5274'|'5277'|'5278'|'5279'|'5280'|'5281'|'5283'|'5285'|'5298'|'5305'|'5310'|'5352'|'5364'|'5375'|'5460'|'582'|'689'|'7581'|'7624'|'7672'|'7953'|'817'|'8206'|'9697'|'Test3'", self::MAP[$k][$value]); + + // ... + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-11363.php b/tests/PHPStan/Rules/Variables/data/bug-11363.php new file mode 100644 index 0000000000..72bf9b9968 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-11363.php @@ -0,0 +1,17 @@ += 8.2 + +namespace Bug12421; + +function doFoo(RegularProperty $x) { + unset($x->y); + var_dump($x->y); + + unset($x->y); + var_dump($x->y); + + $x = new NativeReadonlyClass(); + unset($x->y); + var_dump($x->y); + + $x = new NativeReadonlyProperty(); + unset($x->y); + var_dump($x->y); + + $x = new PhpdocReadonlyClass(); + unset($x->y); + var_dump($x->y); + + $x = new PhpdocReadonlyProperty(); + unset($x->y); + var_dump($x->y); + + $x = new PhpdocImmutableClass(); + unset($x->y); + var_dump($x->y); + + $x = new \stdClass(); + unset($x->y); + + $x = new NativeReadonlyPropertySubClass(); + unset($x->y); + var_dump($x->y); +} + +readonly class NativeReadonlyClass +{ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +class NativeReadonlyProperty +{ + public readonly Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +/** @readonly */ +class PhpdocReadonlyClass +{ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +class PhpdocReadonlyProperty +{ + /** @readonly */ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +/** @immutable */ +class PhpdocImmutableClass +{ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +class RegularProperty +{ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +class NativeReadonlyPropertySubClass extends NativeReadonlyProperty +{ +} + +class Y +{ +} + diff --git a/tests/PHPStan/Rules/Variables/data/bug-12553.php b/tests/PHPStan/Rules/Variables/data/bug-12553.php new file mode 100644 index 0000000000..74d56dc0e8 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12553.php @@ -0,0 +1,22 @@ += 8.4 + +namespace Bug12553; + +interface TimestampsInterface +{ + public \DateTimeImmutable $createdAt { get; } +} + +trait Timestamps +{ + public private(set) \DateTimeImmutable $createdAt { + get { + return $this->createdAt ??= new \DateTimeImmutable(); + } + } +} + +class Example implements TimestampsInterface +{ + use Timestamps; +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-12658.php b/tests/PHPStan/Rules/Variables/data/bug-12658.php new file mode 100644 index 0000000000..8b8d4eec3e --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12658.php @@ -0,0 +1,14 @@ + $paragraph) { + if (!empty($ads)) { + $ad = array_shift($ads); + } + } +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-12771.php b/tests/PHPStan/Rules/Variables/data/bug-12771.php new file mode 100755 index 0000000000..30fb66f1a7 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12771.php @@ -0,0 +1,25 @@ += 3 + && ($_SESSION['prev_error_subm_time'] - time()) <= 3000 + ) { + $_SESSION['error_subm_count'] = 0; + $_SESSION['prev_errors'] = ''; + } else { + $_SESSION['prev_error_subm_time'] = time(); + $_SESSION['error_subm_count'] = isset($_SESSION['error_subm_count']) + ? $_SESSION['error_subm_count'] + 1 + : 0; + } + + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-3391.php b/tests/PHPStan/Rules/Variables/data/bug-3391.php new file mode 100644 index 0000000000..2d57843622 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-3391.php @@ -0,0 +1,32 @@ + 1]; + } + + public function test() + { + $data = $this->getArray(); + + $data['foo'] = 'a'; + $data['bar'] = 'b'; + assertType("non-empty-array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); + + unset($data['id']); + + assertType("non-empty-array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); + return $data; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-3601.php b/tests/PHPStan/Rules/Variables/data/bug-3601.php new file mode 100644 index 0000000000..4930ab2f49 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-3601.php @@ -0,0 +1,15 @@ + 'everything is fine']; +} + +if (isset($a, $c, $c[$a])) { + echo $c[$a]; +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-393.php b/tests/PHPStan/Rules/Variables/data/bug-393.php new file mode 100644 index 0000000000..492ce244e7 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-393.php @@ -0,0 +1,31 @@ +privateProperty = 123; + }, + new Foo(), + Foo::class + ))(); + + (\Closure::bind( + function () { + $this->privateProperty = 123; + }, + new Bar(), + Foo::class + ))(); +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-4173.php b/tests/PHPStan/Rules/Variables/data/bug-4173.php new file mode 100644 index 0000000000..9257376c96 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-4173.php @@ -0,0 +1,34 @@ + $b */ + $b = []; + array_push($a, ...$b); + $c = empty($a) ? 'empty' : 'non-empty'; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-6997.php b/tests/PHPStan/Rules/Variables/data/bug-6997.php new file mode 100644 index 0000000000..8d8b346d3b --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-6997.php @@ -0,0 +1,26 @@ +> */ + private $myMap = []; + + public function doSomething(MyMetadata $class): void + { + unset($this->myMap[$class->fqcn]['foo']); + + if (isset($this->myMap[$class->fqcn]) && ! $this->myMap[$class->fqcn]) { + unset($this->myMap[$class->fqcn]); + } + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-7417.php b/tests/PHPStan/Rules/Variables/data/bug-7417.php new file mode 100644 index 0000000000..fcf698a9ec --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-7417.php @@ -0,0 +1,28 @@ + ['test' => 0]]; +} + +function doFoo() { + $extensions = readThing(); + $extensions['theme']['test_basetheme'] = 0; +// This is the important part of the test. Themes are ordered alphabetically +// in core.extension so this will come before it's base theme. + $extensions['theme']['test_subtheme'] = 0; + $extensions['theme']['test_subsubtheme'] = 0; + assertType("non-empty-array&hasOffsetValue('theme', mixed)", $extensions); + unset($extensions['theme']['test_basetheme']); + unset($extensions['theme']['test_subsubtheme']); + unset($extensions['theme']['test_subtheme']); + assertType("non-empty-array&hasOffsetValue('theme', mixed)", $extensions); +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-7424.php b/tests/PHPStan/Rules/Variables/data/bug-7424.php new file mode 100644 index 0000000000..ac685e0b19 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-7424.php @@ -0,0 +1,35 @@ +getInitData(); + + array_push($data, ...$this->getExtra()); + + if (empty($data)) { + return; + } + + echo 'Proceeding to process data'; + } + + /** + * @return string[] + */ + protected function getInitData(): array + { + return []; + } + + /** + * @return string[] + */ + protected function getExtra(): array + { + return []; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-7724.php b/tests/PHPStan/Rules/Variables/data/bug-7724.php new file mode 100644 index 0000000000..1d756049f4 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-7724.php @@ -0,0 +1,22 @@ + $b['name'], + $a['age'] <=> $b['age'], + ]; + + $sort = array_filter($sort, function (int $value): bool { + return $value !== 0; + }); + + return array_shift($sort) ?? 0; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-8084.php b/tests/PHPStan/Rules/Variables/data/bug-8084.php new file mode 100644 index 0000000000..8c55e7ad53 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8084.php @@ -0,0 +1,19 @@ += 8.0 + +namespace Bug8084a; + +use Exception; +use function array_shift; +use function PHPStan\Testing\assertType; + +class Bug8084 +{ + /** + * @param string[] $params + */ + public function run(array $params): void + { + $a = array_shift($params) ?? throw new Exception(); + $b = array_shift($params) ?? "default_b"; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-8113.php b/tests/PHPStan/Rules/Variables/data/bug-8113.php new file mode 100644 index 0000000000..27ebe729ae --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8113.php @@ -0,0 +1,48 @@ + array('id' => 23, + 'User' => array( + 'first_name' => 'x', + ), + ), + 'SurveyInvitation' => array( + 'is_too_old_to_follow' => 'yes', + ), + 'User' => array( + 'first_name' => 'x', + ), + ); + + assertType('array>', $review); + + if ( + array_key_exists('review', $review['SurveyInvitation']) && + $review['SurveyInvitation']['review'] === null + ) { + assertType("non-empty-array>&hasOffsetValue('SurveyInvitation', non-empty-array&hasOffsetValue('review', null))", $review); + $review['Review'] = [ + 'id' => null, + 'text' => null, + 'answer' => null, + ]; + assertType("non-empty-array>&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', non-empty-array&hasOffsetValue('review', null))", $review); + unset($review['SurveyInvitation']['review']); + assertType("non-empty-array>&hasOffsetValue('Review', array)&hasOffsetValue('SurveyInvitation', array)", $review); + } + assertType('array>', $review); + if (array_key_exists('User', $review['Review'])) { + assertType("non-empty-array>&hasOffsetValue('Review', non-empty-array&hasOffset('User'))", $review); + $review['User'] = $review['Review']['User']; + assertType("non-empty-array&hasOffsetValue('Review', non-empty-array&hasOffset('User'))&hasOffsetValue('User', mixed)", $review); + unset($review['Review']['User']); + assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('User', array)", $review); + } + assertType("non-empty-array&hasOffsetValue('Review', array)", $review); +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-8142.php b/tests/PHPStan/Rules/Variables/data/bug-8142.php new file mode 100644 index 0000000000..ac7fa741a5 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8142.php @@ -0,0 +1,16 @@ += 8.0 + +namespace Bug8142; + +/** @param string &$out */ +function foo($foo, $bar = null, &$out = null): void { + $out = 'good'; +} + +function () { + foo(1, null, $good); + var_dump($good); + + foo(1, out: $bad); + var_dump($bad); +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-8212.php b/tests/PHPStan/Rules/Variables/data/bug-8212.php new file mode 100644 index 0000000000..384ce1b97e --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8212.php @@ -0,0 +1,14 @@ +getCase()) { + case self::CASE_1: + $foo = 'bar'; + break; + case self::CASE_2: + $foo = 'baz'; + break; + case self::CASE_3: + $foo = 'barbaz'; + break; + } + + return $foo; + } + + public function not_ok(): string + { + switch($this->getCase()) { + case self::CASE_1: + $foo = 'bar'; + break; + case self::CASE_2: + case self::CASE_3: + $foo = 'barbaz'; + break; + } + + return $foo; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-9126.php b/tests/PHPStan/Rules/Variables/data/bug-9126.php new file mode 100644 index 0000000000..7a5948d232 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9126.php @@ -0,0 +1,23 @@ +owner; + } +} + +function (): void { + $resume = new Resume(); + $owner = $resume->getOwner(); + if (!empty($owner)) { + echo "not empty"; + } +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-9328.php b/tests/PHPStan/Rules/Variables/data/bug-9328.php new file mode 100644 index 0000000000..92221f9040 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9328.php @@ -0,0 +1,37 @@ + 2 + ) { + // flush previously collected section: + if ($lines) { + $sections[] = [ + 'name' => $currentSection, + 'lines' => $lines, + ]; + } + $currentSection = substr($line, 1, -1); + $lines = []; + } + $lines[] = $line; + } + + if (isset($sections[1])) { + echo "We have multiple remaining sections!\n"; + } +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-9403.php b/tests/PHPStan/Rules/Variables/data/bug-9403.php new file mode 100644 index 0000000000..0889949c57 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9403.php @@ -0,0 +1,31 @@ +>', $result); + assertNativeType('list>', $result); + + if (!empty($result)) { + rsort($result); + } + return $result; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-9474.php b/tests/PHPStan/Rules/Variables/data/bug-9474.php new file mode 100644 index 0000000000..5ea6ec4104 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9474.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug9474; + +class GlazedTerracotta{ + public function getColor() : int{ return 1; } +} + +class HelloWorld +{ + public function sayHello(): void + { + var_dump((function(GlazedTerracotta $block) : int{ + $i = match($color = $block->getColor()){ + 1 => 1, + default => throw new \Exception("Unhandled dye colour " . $color) + }; + echo $color; + return $i; + })(new GlazedTerracotta)); + } +} diff --git a/tests/PHPStan/Rules/Variables/data/defined-variables-anonymous-function-use.php b/tests/PHPStan/Rules/Variables/data/defined-variables-anonymous-function-use.php index eb2599f89e..e02deb647a 100644 --- a/tests/PHPStan/Rules/Variables/data/defined-variables-anonymous-function-use.php +++ b/tests/PHPStan/Rules/Variables/data/defined-variables-anonymous-function-use.php @@ -14,7 +14,7 @@ function () use (&$errorHandler) { $onlyInIf = 1; } -for ($forI = 0; $forI < 10, $anotherVariableFromForCond = 1; $forI++, $forJ = $forI) { +for ($forI = 0; $anotherVariableFromForCond = 1, $forI < 10; $forI++, $forJ = $forI) { } diff --git a/tests/PHPStan/Rules/Variables/data/defined-variables-arrow-functions.php b/tests/PHPStan/Rules/Variables/data/defined-variables-arrow-functions.php index 10b0fdcb42..3196608c7a 100644 --- a/tests/PHPStan/Rules/Variables/data/defined-variables-arrow-functions.php +++ b/tests/PHPStan/Rules/Variables/data/defined-variables-arrow-functions.php @@ -1,4 +1,4 @@ -= 7.4 += 7.4 += 8.1 + +declare(strict_types=1); + +namespace DefinedVariablesEnum; + +enum Foo +{ + case A; + case B; +} + +class HelloWorld +{ + public function sayHello(Foo $f): void + { + switch ($f) { + case Foo::A: + $i = 5; + break; + case Foo::B: + $i = 6; + break; + } + + var_dump($i); + } +} diff --git a/tests/PHPStan/Rules/Variables/data/defined-variables.php b/tests/PHPStan/Rules/Variables/data/defined-variables.php index e0f62a0e4c..4b2ec40f04 100644 --- a/tests/PHPStan/Rules/Variables/data/defined-variables.php +++ b/tests/PHPStan/Rules/Variables/data/defined-variables.php @@ -243,7 +243,7 @@ function () { } - for ($forI = 0; $forI < 10, $forK = 5; $forI++, $forK++, $forJ = $forI) { + for ($forI = 0; $forK = 5, $forI < 10; $forI++, $forK++, $forJ = $forI) { echo $forI; } @@ -322,7 +322,7 @@ function () { include($fileB='includeB.php'); echo $fileB; - for ($forLoopVariableInit = 0; $forLoopVariableInit < 5; $forLoopVariableInit = $forLoopVariable, $anotherForLoopVariable = 1) { + for ($forLoopVariableInit = 0; $forLoopVariableInit < 5 && rand(0, 1); $forLoopVariableInit = $forLoopVariable, $anotherForLoopVariable = 1) { $forLoopVariable = 2; } echo $anotherForLoopVariable; @@ -357,7 +357,7 @@ function () { } - for (; $forVariableUsedAndThenDefined && $forVariableUsedAndThenDefined = 1;) { + for (; $forVariableUsedAndThenDefined && $forVariableUsedAndThenDefined = 1 && rand(0, 1);) { } diff --git a/tests/PHPStan/Rules/Variables/data/discussion-10252.php b/tests/PHPStan/Rules/Variables/data/discussion-10252.php new file mode 100644 index 0000000000..00f3d90429 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/discussion-10252.php @@ -0,0 +1,11 @@ +name}; + } + + public function testScope(): void + { + $name1 = 'foo'; + $rand = rand(); + if ($rand === 1) { + $foo = 1; + $name = $name1; + } elseif ($rand === 2) { + $name = 'bar'; + $bar = 'str'; + } else { + $name = 'buz'; + } + + if ($name === 'foo') { + echo $$name; // ok + echo $foo; // ok + echo $bar; + } elseif ($name === 'bar') { + echo $$name; // ok + echo $foo; + echo $bar; // ok + } else { + echo $$name; // ok + echo $foo; + echo $bar; + } + + echo $$name; // ok + echo $foo; + echo $bar; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/isset-after-remembered-constructor.php b/tests/PHPStan/Rules/Variables/data/isset-after-remembered-constructor.php new file mode 100644 index 0000000000..2c48e6249d --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isset-after-remembered-constructor.php @@ -0,0 +1,98 @@ += 8.2 + +namespace IssetOrCoalesceOnNonNullableInitializedProperty; + +class User +{ + private ?string $nullableString; + private string $maybeUninitializedString; + private string $string; + + private $untyped; + + public function __construct() + { + if (rand(0, 1)) { + $this->nullableString = 'hello'; + $this->string = 'world'; + $this->maybeUninitializedString = 'something'; + } else { + $this->nullableString = null; + $this->string = 'world 2'; + $this->untyped = 123; + } + } + + public function doFoo(): void + { + if (isset($this->maybeUninitializedString)) { + echo $this->maybeUninitializedString; + } + if (isset($this->nullableString)) { + echo $this->nullableString; + } + if (isset($this->string)) { + echo $this->string; + } + if (isset($this->untyped)) { + echo $this->untyped; + } + } + + public function doBar(): void + { + echo $this->maybeUninitializedString ?? 'default'; + echo $this->nullableString ?? 'default'; + echo $this->string ?? 'default'; + echo $this->untyped ?? 'default'; + } + + public function doFooBar(): void + { + if (empty($this->maybeUninitializedString)) { + echo $this->maybeUninitializedString; + } + if (empty($this->nullableString)) { + echo $this->nullableString; + } + if (empty($this->string)) { + echo $this->string; + } + if (empty($this->untyped)) { + echo $this->untyped; + } + } +} + +class MoreEmptyCases +{ + private false|string $union; + private false $false; + private true $true; + private bool $bool; + + public function __construct() + { + if (rand(0, 1)) { + $this->union = 'nope'; + $this->bool = true; + } elseif (rand(10, 20)) { + $this->union = false; + $this->bool = false; + } + $this->false = false; + $this->true = true; + } + + public function doFoo(): void + { + if (empty($this->union)) { + } + if (empty($this->bool)) { + } + if (empty($this->false)) { + } + if (empty($this->true)) { + } + } +} diff --git a/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php b/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php index f493c1fac6..1d93d3f647 100644 --- a/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php +++ b/tests/PHPStan/Rules/Variables/data/isset-native-property-types.php @@ -1,4 +1,4 @@ -= 7.4 +foo)) { + + } + + if (isset($o->bar)) { + + } + + if (isset($o->baz)) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/isset-virtual-property.php b/tests/PHPStan/Rules/Variables/data/isset-virtual-property.php new file mode 100644 index 0000000000..ea9488ba44 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isset-virtual-property.php @@ -0,0 +1,23 @@ += 8.4 + +namespace IssetVirtualProperty; + +class Example { + public \DateTimeImmutable $noon { + get => new \DateTimeImmutable('12:00'); + } + + public ?\DateTimeImmutable $nullableNoon { + get => new \DateTimeImmutable('12:00'); + } + + public function doFoo(): void + { + if (isset($this->noon)) { + + } + if (isset($this->nullableNoon)) { + + } + } +} diff --git a/tests/PHPStan/Rules/Variables/data/isstring-certainty.php b/tests/PHPStan/Rules/Variables/data/isstring-certainty.php new file mode 100644 index 0000000000..270e978e68 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isstring-certainty.php @@ -0,0 +1,22 @@ += 7.4 +doFoo($p); + } + + /** + * @param list $p + * @param-out list $p + */ + function doBaz(&$p): void + { + unset($p[1]); + } + + /** + * @param list $p + * @param-out list $p + */ + function doBaz2(&$p): void + { + $p[] = 'str'; + } + + /** + * @param list> $p + * @param-out list> $p + */ + function doBaz3(&$p): void + { + unset($p[1][2]); + } + + function doNoParamOut(string &$p): void + { + $p = 1; + } + + function doNoParamOut2(string &$p): void + { + $p = 'foo'; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php b/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php new file mode 100644 index 0000000000..6f55e987cc --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php @@ -0,0 +1,106 @@ += 8.0 + +namespace PassByReferenceIntoNotNullable; + +class Foo +{ + + public function doFooNoType(&$test) + { + + } + + public function doFooMixedType(mixed &$test) + { + + } + + public function doFooIntType(int &$test) + { + + } + + public function doFooNullableType(?int &$test) + { + + } + + public function test() + { + $this->doFooNoType($one); + $this->doFooMixedType($two); + $this->doFooIntType($three); + $this->doFooNullableType($four); + } + +} + +class FooPhpDocs +{ + + /** + * @param mixed $test + */ + public function doFooMixedType(&$test) + { + + } + + /** + * @param int $test + */ + public function doFooIntType(&$test) + { + + } + + /** + * @param int|null $test + */ + public function doFooNullableType(&$test) + { + + } + + public function test() + { + $this->doFooMixedType($two); + $this->doFooIntType($three); + $this->doFooNullableType($four); + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/property-hooks.php b/tests/PHPStan/Rules/Variables/data/property-hooks.php new file mode 100644 index 0000000000..1fc6f744b2 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/property-hooks.php @@ -0,0 +1,54 @@ += 8.4 + +namespace PropertyHooksVariables; + +class Foo +{ + + public int $i { + set { + $this->i = $value + 10; + } + } + + public int $iErr { + set { + $this->iErr = $val + 10; + } + } + + public int $j { + set (int $val) { + $this->j = $val + 10; + } + } + + public int $jErr { + set (int $val) { + $this->jErr = $value + 10; + } + } + +} + + +class FooShort +{ + + public int $i { + set => $value + 10; + } + + public int $iErr { + set => $val + 10; + } + + public int $j { + set (int $val) => $val + 10; + } + + public int $jErr { + set (int $val) => $value + 10; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php b/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php new file mode 100644 index 0000000000..d98eed672a --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php @@ -0,0 +1,150 @@ += 8.4 + +namespace UnsetHookedProperty; + +function doUnset(Foo $foo, User $user, NonFinalClass $nonFinalClass, FinalClass $finalClass): void { + unset($user->name); + unset($user->fullName); + + unset($foo->ii); + unset($foo->iii); + + unset($nonFinalClass->publicFinalProperty); + unset($nonFinalClass->publicProperty); + + unset($finalClass->publicFinalProperty); + unset($finalClass->publicProperty); +} + +class User +{ + public string $name { + set { + if (strlen($value) === 0) { + throw new \ValueError("Name must be non-empty"); + } + $this->name = $value; + } + } + + public string $fullName { + get { + return "Yennefer of Vengerberg"; + } + } + + public function __construct(string $name) { + $this->name = $name; + } +} + +abstract class Foo +{ + abstract protected int $ii { get; } + + abstract public int $iii { get; } +} + +class NonFinalClass { + private string $privateProperty; + public string $publicProperty; + final public string $publicFinalProperty; + + function doFoo() { + unset($this->privateProperty); + } +} + +final class FinalClass { + private string $privateProperty; + public string $publicProperty; + final public string $publicFinalProperty; + + function doFoo() { + unset($this->privateProperty); + } +} + +class ContainerClass { + public FinalClass $finalClass; + public FinalClass $nonFinalClass; + + public Foo $foo; + + public User $user; + + /** @var array */ + public array $arrayOfUsers; +} + +function dooNestedUnset(ContainerClass $containerClass) { + unset($containerClass->finalClass->publicFinalProperty); + unset($containerClass->finalClass->publicProperty); + unset($containerClass->finalClass); + + unset($containerClass->nonFinalClass->publicFinalProperty); + unset($containerClass->nonFinalClass->publicProperty); + unset($containerClass->nonFinalClass); + + unset($containerClass->foo->iii); + unset($containerClass->foo); + + unset($containerClass->user->name); + unset($containerClass->user->fullName); + unset($containerClass->user); + + unset($containerClass->arrayOfUsers[0]->name); + unset($containerClass->arrayOfUsers[0]->name); + unset($containerClass->arrayOfUsers['hans']->fullName); + unset($containerClass->arrayOfUsers['hans']->fullName); + unset($containerClass->arrayOfUsers); +} + +class Bug12695 +{ + /** @var int[] */ + public array $values = [1]; + public function test(): void + { + unset($this->values[0]); + } +} + +abstract class Bug12695_AbstractJsonView +{ + protected array $variables = []; + + public function render(): array + { + return $this->variables; + } +} + +class Bug12695_GetSeminarDateJsonView extends Bug12695_AbstractJsonView +{ + public function render(): array + { + unset($this->variables['settings']); + return parent::render(); + } +} + +class Bug12695_AddBookingsJsonView extends Bug12695_GetSeminarDateJsonView +{ + public function render(): array + { + unset($this->variables['seminarDate']); + return parent::render(); + } +} + +class UnsetReadonly +{ + /** @var int[][] */ + public readonly array $a; + + public function doFoo(): void + { + unset($this->a[5]); + } +} diff --git a/tests/PHPStan/Rules/Variables/data/variable-certainty-null-assign.php b/tests/PHPStan/Rules/Variables/data/variable-certainty-null-assign.php index 8500b14063..d0d2b7c927 100644 --- a/tests/PHPStan/Rules/Variables/data/variable-certainty-null-assign.php +++ b/tests/PHPStan/Rules/Variables/data/variable-certainty-null-assign.php @@ -1,4 +1,4 @@ -= 7.4 + + */ +class WarningEmittingRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new class implements Rule { + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + echo $undefined; // @phpstan-ignore variable.undefined + return []; + } + + }; + } + + public function testRule(): void + { + try { + $this->analyse([__DIR__ . '/data/empty-file.php'], []); + self::fail('Should throw an exception'); + + } catch (AssertionFailedError $e) { + self::assertStringContainsString('Undefined variable', $e->getMessage()); // exact message differs between PHPStan versions + } + } + +} diff --git a/tests/PHPStan/Rules/data/datetime-instantiation.php b/tests/PHPStan/Rules/data/datetime-instantiation.php index dada942a30..47bb5deb95 100644 --- a/tests/PHPStan/Rules/data/datetime-instantiation.php +++ b/tests/PHPStan/Rules/data/datetime-instantiation.php @@ -18,3 +18,6 @@ function foo(string $date, string $date2): void { } new \DateTime('2020-04-31'); + +new \dateTime('2020.11.17'); +new \dateTimeImmutablE('2020.11.17'); diff --git a/tests/PHPStan/Rules/data/dummy-collector.php b/tests/PHPStan/Rules/data/dummy-collector.php new file mode 100644 index 0000000000..e9b294b837 --- /dev/null +++ b/tests/PHPStan/Rules/data/dummy-collector.php @@ -0,0 +1,25 @@ +doFoo(); + $this->doBar(); + } + +} + +class Bar +{ + + public function doBar() + { + $this->doFoo(); + $this->doBar(); + } + +} diff --git a/tests/PHPStan/Rules/data/empty-file.php b/tests/PHPStan/Rules/data/empty-file.php new file mode 100644 index 0000000000..60ac8c38d2 --- /dev/null +++ b/tests/PHPStan/Rules/data/empty-file.php @@ -0,0 +1,3 @@ += 8.0 + +namespace ScopeFunctionCallStack; + +function (): void +{ + var_dump(print_r(sleep(throw new \Exception()))); + + var_dump(print_r(function () { + sleep(throw new \Exception()); + })); + + var_dump(print_r(fn () => sleep(throw new \Exception()))); +}; diff --git a/tests/PHPStan/Testing/NonexistentAnalysedClassRuleTest.php b/tests/PHPStan/Testing/NonexistentAnalysedClassRuleTest.php new file mode 100644 index 0000000000..706f0e1533 --- /dev/null +++ b/tests/PHPStan/Testing/NonexistentAnalysedClassRuleTest.php @@ -0,0 +1,62 @@ +> + */ +class NonexistentAnalysedClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new /** @implements Rule */class implements Rule { + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->name instanceof Node\Name && $node->name->toString() === 'error') { + return [ + RuleErrorBuilder::message('Error call') + ->identifier('test.errorCall') + ->nonIgnorable() + ->build(), + ]; + } + + return []; + } + + }; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/../../notAutoloaded/nonexistentClasses.php'], []); + } + + public function testRuleWithError(): void + { + try { + $this->analyse([__DIR__ . '/../../notAutoloaded/nonexistentClasses-error.php'], []); + $this->fail('Should have failed'); + } catch (ExpectationFailedException $e) { + if ($e->getComparisonFailure() === null) { + throw $e; + } + $this->assertStringContainsString('not found in ReflectionProvider', $e->getComparisonFailure()->getDiff()); + } + } + +} diff --git a/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php b/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php new file mode 100644 index 0000000000..c8220a0e9a --- /dev/null +++ b/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php @@ -0,0 +1,124 @@ +getByType(FileHelper::class); + + yield [ + __DIR__ . '/data/assert-certainty-missing-namespace.php', + sprintf( + 'Missing use statement for assertVariableCertainty() in %s on line 8.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-certainty-missing-namespace.php'), + ), + ]; + yield [ + __DIR__ . '/data/assert-native-type-missing-namespace.php', + sprintf( + 'Missing use statement for assertNativeType() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-native-type-missing-namespace.php'), + ), + ]; + yield [ + __DIR__ . '/data/assert-type-missing-namespace.php', + sprintf( + 'Missing use statement for assertType() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-type-missing-namespace.php'), + ), + ]; + yield [ + __DIR__ . '/data/assert-certainty-wrong-namespace.php', + sprintf( + 'Function PHPStan\Testing\assertVariableCertainty imported with wrong namespace SomeWrong\Namespace\assertVariableCertainty called in %s on line 9.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-certainty-wrong-namespace.php'), + ), + ]; + yield [ + __DIR__ . '/data/assert-native-type-wrong-namespace.php', + sprintf( + 'Function PHPStan\Testing\assertNativeType imported with wrong namespace SomeWrong\Namespace\assertNativeType called in %s on line 8.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-native-type-wrong-namespace.php'), + ), + ]; + yield [ + __DIR__ . '/data/assert-type-wrong-namespace.php', + sprintf( + 'Function PHPStan\Testing\assertType imported with wrong namespace SomeWrong\Namespace\assertType called in %s on line 8.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-type-wrong-namespace.php'), + ), + ]; + yield [ + __DIR__ . '/data/assert-certainty-case-insensitive.php', + sprintf( + 'Missing use statement for assertvariablecertainty() in %s on line 8.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-certainty-case-insensitive.php'), + ), + ]; + yield [ + __DIR__ . '/data/assert-native-type-case-insensitive.php', + sprintf( + 'Missing use statement for assertNATIVEType() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-native-type-case-insensitive.php'), + ), + ]; + yield [ + __DIR__ . '/data/assert-type-case-insensitive.php', + sprintf( + 'Missing use statement for assertTYPe() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-type-case-insensitive.php'), + ), + ]; + } + + /** + * @dataProvider dataFileAssertionFailedErrors + */ + public function testFileAssertionFailedErrors(string $filePath, string $errorMessage): void + { + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage($errorMessage); + + $this->gatherAssertTypes($filePath); + } + + public function testVariableOrOffsetDescription(): void + { + $filePath = __DIR__ . '/data/assert-certainty-variable-or-offset.php'; + + [$variableAssert, $offsetAssert] = array_values($this->gatherAssertTypes($filePath)); + + $this->assertSame('variable $context', $variableAssert[4]); + $this->assertSame("offset 'email'", $offsetAssert[4]); + } + + public function testNonexistentClassInAnalysedFile(): void + { + foreach ($this->gatherAssertTypes(__DIR__ . '/../../notAutoloaded/nonexistentClasses.php') as $data) { + $this->assertFileAsserts(...$data); + } + } + + public function testNonexistentClassInAnalysedFileWithError(): void + { + try { + foreach ($this->gatherAssertTypes(__DIR__ . '/../../notAutoloaded/nonexistentClasses-error.php') as $data) { + $this->assertFileAsserts(...$data); + } + + $this->fail('Should have failed'); + } catch (AssertionFailedError $e) { + $this->assertStringContainsString('not found in ReflectionProvider', $e->getMessage()); + } + } + +} diff --git a/tests/PHPStan/Testing/data/assert-certainty-case-insensitive.php b/tests/PHPStan/Testing/data/assert-certainty-case-insensitive.php new file mode 100644 index 0000000000..79ea770cc4 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-certainty-case-insensitive.php @@ -0,0 +1,9 @@ += 8.0 + +namespace MissingAssertCertaintyCaseSensitive; + +use PHPStan\TrinaryLogic; + +function doFoo(string $s) { + assertvariablecertainty(TrinaryLogic::createMaybe(), $s); +} diff --git a/tests/PHPStan/Testing/data/assert-certainty-missing-namespace.php b/tests/PHPStan/Testing/data/assert-certainty-missing-namespace.php new file mode 100644 index 0000000000..228bac1208 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-certainty-missing-namespace.php @@ -0,0 +1,9 @@ += 8.0 + +namespace MissingAssertCertaintyNamespace; + +use PHPStan\TrinaryLogic; + +function doFoo(string $s) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $s); +} diff --git a/tests/PHPStan/Testing/data/assert-certainty-variable-or-offset.php b/tests/PHPStan/Testing/data/assert-certainty-variable-or-offset.php new file mode 100644 index 0000000000..b06391db45 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-certainty-variable-or-offset.php @@ -0,0 +1,15 @@ += 8.0 + +namespace WrongAssertCertaintyNamespace; + +use PHPStan\TrinaryLogic; +use function SomeWrong\Namespace\assertVariableCertainty; + +function doFoo(string $s) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $s); +} diff --git a/tests/PHPStan/Testing/data/assert-native-type-case-insensitive.php b/tests/PHPStan/Testing/data/assert-native-type-case-insensitive.php new file mode 100644 index 0000000000..effd8b777a --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-native-type-case-insensitive.php @@ -0,0 +1,7 @@ += 8.0 + +namespace MissingAssertNativeCaseSensitive; + +function doFoo(string $s) { + assertNATIVEType('string', $s); +} diff --git a/tests/PHPStan/Testing/data/assert-native-type-missing-namespace.php b/tests/PHPStan/Testing/data/assert-native-type-missing-namespace.php new file mode 100644 index 0000000000..7df11d72fc --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-native-type-missing-namespace.php @@ -0,0 +1,7 @@ += 8.0 + +namespace MissingAssertNativeNamespace; + +function doFoo(string $s) { + assertNativeType('string', $s); +} diff --git a/tests/PHPStan/Testing/data/assert-native-type-wrong-namespace.php b/tests/PHPStan/Testing/data/assert-native-type-wrong-namespace.php new file mode 100644 index 0000000000..fb08c4829f --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-native-type-wrong-namespace.php @@ -0,0 +1,9 @@ += 8.0 + +namespace WrongAssertNativeNamespace; + +use function SomeWrong\Namespace\assertNativeType; + +function doFoo(string $s) { + assertNativeType('string', $s); +} diff --git a/tests/PHPStan/Testing/data/assert-type-case-insensitive.php b/tests/PHPStan/Testing/data/assert-type-case-insensitive.php new file mode 100644 index 0000000000..7738ae6f38 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-type-case-insensitive.php @@ -0,0 +1,8 @@ += 8.0 + +namespace MissingTypeCaseSensitive; + +function doFoo(string $s) { + assertTYPe('string', $s); +} + diff --git a/tests/PHPStan/Testing/data/assert-type-missing-namespace.php b/tests/PHPStan/Testing/data/assert-type-missing-namespace.php new file mode 100644 index 0000000000..d0f9018efa --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-type-missing-namespace.php @@ -0,0 +1,8 @@ += 8.0 + +namespace MissingAssertTypeNamespace; + +function doFoo(string $s) { + assertType('string', $s); +} + diff --git a/tests/PHPStan/Testing/data/assert-type-wrong-namespace.php b/tests/PHPStan/Testing/data/assert-type-wrong-namespace.php new file mode 100644 index 0000000000..67715fd548 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-type-wrong-namespace.php @@ -0,0 +1,10 @@ += 8.0 + +namespace WrongAssertTypeNamespace; + +use function SomeWrong\Namespace\assertType; + +function doFoo(string $s) { + assertType('string', $s); +} + diff --git a/tests/PHPStan/TrinaryLogicTest.php b/tests/PHPStan/TrinaryLogicTest.php index d979c37a84..c487e2d5c3 100644 --- a/tests/PHPStan/TrinaryLogicTest.php +++ b/tests/PHPStan/TrinaryLogicTest.php @@ -40,6 +40,18 @@ public function testAnd( $this->assertTrue($expectedResult->equals($value->and(...$operands))); } + /** + * @dataProvider dataAnd + */ + public function testLazyAnd( + TrinaryLogic $expectedResult, + TrinaryLogic $value, + TrinaryLogic ...$operands, + ): void + { + $this->assertTrue($expectedResult->equals($value->lazyAnd($operands, static fn (TrinaryLogic $result) => $result))); + } + public function dataOr(): array { return [ @@ -73,6 +85,18 @@ public function testOr( $this->assertTrue($expectedResult->equals($value->or(...$operands))); } + /** + * @dataProvider dataOr + */ + public function testLazyOr( + TrinaryLogic $expectedResult, + TrinaryLogic $value, + TrinaryLogic ...$operands, + ): void + { + $this->assertTrue($expectedResult->equals($value->lazyOr($operands, static fn (TrinaryLogic $result) => $result))); + } + public function dataNegate(): array { return [ diff --git a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php index a70ff82ceb..a941dd86ee 100644 --- a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php @@ -70,11 +70,6 @@ public function dataIsSuperTypeOf(): array new HasPropertyType('bar'), TrinaryLogic::createMaybe(), ], - [ - new HasMethodType('foo'), - new HasOffsetType(new MixedType()), - TrinaryLogic::createMaybe(), - ], [ new HasMethodType('foo'), new IterableType(new MixedType(), new MixedType()), diff --git a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php index 79743043b0..4cecca14c1 100644 --- a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php @@ -17,6 +17,7 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; +use const PHP_VERSION_ID; class HasPropertyTypeTest extends PHPStanTestCase { @@ -47,7 +48,7 @@ public function dataIsSuperTypeOf(): array [ new HasPropertyType('foo'), new ObjectType(Closure::class), - TrinaryLogic::createNo(), + PHP_VERSION_ID < 80200 ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(), ], [ new HasPropertyType('foo'), @@ -59,11 +60,6 @@ public function dataIsSuperTypeOf(): array new HasPropertyType('bar'), TrinaryLogic::createMaybe(), ], - [ - new HasPropertyType('foo'), - new HasOffsetType(new MixedType()), - TrinaryLogic::createMaybe(), - ], [ new HasPropertyType('foo'), new IterableType(new MixedType(), new MixedType()), diff --git a/tests/PHPStan/Type/ArrayTypeTest.php b/tests/PHPStan/Type/ArrayTypeTest.php index 0390892aef..e701997c6c 100644 --- a/tests/PHPStan/Type/ArrayTypeTest.php +++ b/tests/PHPStan/Type/ArrayTypeTest.php @@ -5,6 +5,7 @@ use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -65,6 +66,22 @@ public function dataIsSuperTypeOf(): array new ConstantArrayType([], []), TrinaryLogic::createYes(), ], + [ + new ArrayType(new IntegerType(), new StringType()), + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new OversizedArrayType()]), + TrinaryLogic::createYes(), + ], + [ + new ArrayType(new StringType(), new MixedType()), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + TrinaryLogic::createYes(), + ], ]; } @@ -139,7 +156,7 @@ public function testAccepts( TrinaryLogic $expectedResult, ): void { - $actualResult = $acceptingType->accepts($acceptedType, true); + $actualResult = $acceptingType->accepts($acceptedType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), @@ -173,7 +190,7 @@ public function testDescribe( public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, new MixedType(), diff --git a/tests/PHPStan/Type/BenevolentUnionTypeTest.php b/tests/PHPStan/Type/BenevolentUnionTypeTest.php new file mode 100644 index 0000000000..18a37f8be0 --- /dev/null +++ b/tests/PHPStan/Type/BenevolentUnionTypeTest.php @@ -0,0 +1,537 @@ +canAccessProperties(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> canAccessProperties()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataHasProperty(): Iterator + { + yield [ + new BenevolentUnionType([ + new IntersectionType([new ObjectWithoutClassType(), new HasPropertyType('foo')]), + new IntersectionType([new ObjectWithoutClassType(), new HasPropertyType('foo')]), + ]), + 'foo', + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([ + new IntersectionType([new ObjectWithoutClassType(), new HasPropertyType('foo')]), + new NullType(), + ]), + 'foo', + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + 'foo', + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataHasProperty */ + public function testHasProperty(BenevolentUnionType $type, string $propertyName, TrinaryLogic $expectedResult): void + { + $actualResult = $type->hasProperty($propertyName); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> hasProperty()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataCanCallMethods(): Iterator + { + yield [ + new BenevolentUnionType([new ObjectWithoutClassType(), new ObjectWithoutClassType()]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new ObjectWithoutClassType(), new NullType()]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataCanCallMethods */ + public function testCanCanCallMethods(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->canCallMethods(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> canCallMethods()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataHasMethod(): Iterator + { + yield [ + new BenevolentUnionType([ + new ObjectType(DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), + ]), + 'format', + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new ObjectType(DateTimeImmutable::class), new NullType()]), + 'format', + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + 'format', + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataHasMethod */ + public function testHasMethod(BenevolentUnionType $type, string $methodName, TrinaryLogic $expectedResult): void + { + $actualResult = $type->hasMethod($methodName); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> hasMethod()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataCanAccessConstants(): Iterator + { + yield [ + new BenevolentUnionType([new ObjectWithoutClassType(), new ObjectWithoutClassType()]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new ObjectWithoutClassType(), new NullType()]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataCanAccessConstants */ + public function testCanAccessConstants(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->canAccessConstants(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> canAccessConstants()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsIterable(): Iterator + { + yield [ + new BenevolentUnionType([ + new ArrayType(new MixedType(), new MixedType()), + new ArrayType(new MixedType(), new MixedType()), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([ + new ArrayType(new MixedType(), new MixedType()), + new NullType(), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsIterable */ + public function testIsIterable(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isIterable(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsIterableAtLeastOnce(): Iterator + { + yield [ + new BenevolentUnionType([ + new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new NonEmptyArrayType()]), + new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new NonEmptyArrayType()]), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([ + new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new NonEmptyArrayType()]), + new NullType(), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsIterableAtLeastOnce */ + public function testIsIterableAtLeastOnce(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isIterableAtLeastOnce(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isIterableAtLeastOnce()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsArray(): Iterator + { + yield [ + new BenevolentUnionType([new ArrayType(new MixedType(), new MixedType()), new ConstantArrayType([], [])]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new ArrayType(new MixedType(), new MixedType()), new NullType()]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsArray */ + public function testIsArray(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isArray(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isArray()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsString(): Iterator + { + yield [ + new BenevolentUnionType([ + new StringType(), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new IntegerType(), new StringType()]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new BenevolentUnionType([new IntegerType(), new IntegerType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsString */ + public function testIsString(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isString(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isString()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsNumericString(): Iterator + { + yield [ + new BenevolentUnionType([ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryNumericStringType()])]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new IntegerType(), new StringType()]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new BenevolentUnionType([new IntegerType(), new IntegerType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsNumericString */ + public function testIsNumericString(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isNumericString(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNumericString()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsNonFalsyString(): Iterator + { + yield [ + new BenevolentUnionType([ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()])]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new IntegerType(), new StringType()]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new BenevolentUnionType([new IntegerType(), new IntegerType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsNonFalsyString */ + public function testIsNonFalsyString(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isNonFalsyString(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNonFalsyString()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsLiteralString(): Iterator + { + yield [ + new BenevolentUnionType([ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()])]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new IntegerType(), new StringType()]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new BenevolentUnionType([new IntegerType(), new IntegerType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsLiteralString */ + public function testIsLiteralString(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isLiteralString(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isLiteralString()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsOffsetAccesible(): Iterator + { + yield [ + new BenevolentUnionType([ + new ArrayType(new MixedType(), new MixedType()), + new ArrayType(new MixedType(), new MixedType()), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StrictMixedType(), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new StrictMixedType(), new StrictMixedType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsOffsetAccesible */ + public function testIsOffsetAccessible(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isOffsetAccessible(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isOffsetAccessible()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataHasOffsetValueType(): Iterator + { + yield [ + new BenevolentUnionType([ + new ConstantArrayType([new ConstantStringType('foo')], [new MixedType()]), + new ConstantArrayType([new ConstantStringType('foo')], [new MixedType()]), + ]), + new ConstantStringType('foo'), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([ + new ConstantArrayType([new ConstantStringType('foo')], [new MixedType()]), + new NullType(), + ]), + new ConstantStringType('foo'), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + new ConstantStringType('foo'), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataHasOffsetValueType */ + public function testHasOffsetValue(BenevolentUnionType $type, Type $offsetType, TrinaryLogic $expectedResult): void + { + $actualResult = $type->hasOffsetValueType($offsetType); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> hasOffsetValueType()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsCallable(): Iterator + { + yield [ + new BenevolentUnionType([new CallableType(), new CallableType()]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new CallableType(), new NullType()]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsCallable */ + public function testIsCallable(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isCallable(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsCloneable(): Iterator + { + yield [ + new BenevolentUnionType([new ObjectWithoutClassType(), new ObjectWithoutClassType()]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new ObjectWithoutClassType(), new NullType()]), + TrinaryLogic::createYes(), + ]; + + yield [ + new BenevolentUnionType([new NullType(), new NullType()]), + TrinaryLogic::createNo(), + ]; + } + + /** @dataProvider dataIsCloneable */ + public function testIsCloneable(BenevolentUnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isCloneable(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isCloneable()', $type->describe(VerbosityLevel::precise())), + ); + } + +} diff --git a/tests/PHPStan/Type/BitwiseFlagHelperTest.php b/tests/PHPStan/Type/BitwiseFlagHelperTest.php new file mode 100644 index 0000000000..7703524f47 --- /dev/null +++ b/tests/PHPStan/Type/BitwiseFlagHelperTest.php @@ -0,0 +1,143 @@ +getByType(ScopeFactory::class); + $scope = $scopeFactory->create(ScopeContext::create('file.php')) + ->assignVariable('mixedVar', new MixedType(), new MixedType(), TrinaryLogic::createYes()) + ->assignVariable('stringVar', new StringType(), new StringType(), TrinaryLogic::createYes()) + ->assignVariable('integerVar', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()) + ->assignVariable('booleanVar', new BooleanType(), new BooleanType(), TrinaryLogic::createYes()) + ->assignVariable('floatVar', new FloatType(), new FloatType(), TrinaryLogic::createYes()) + ->assignVariable('unionIntFloatVar', new UnionType([new IntegerType(), new FloatType()]), new UnionType([new IntegerType(), new FloatType()]), TrinaryLogic::createYes()) + ->assignVariable('unionStringFloatVar', new UnionType([new StringType(), new FloatType()]), new UnionType([new StringType(), new FloatType()]), TrinaryLogic::createYes()); + + $analyser = new BitwiseFlagHelper($this->createReflectionProvider()); + $actual = $analyser->bitwiseOrContainsConstant($expr, $scope, $constName); + $this->assertTrue($expected->equals($actual), sprintf('Expected Trinary::%s but got Trinary::%s.', $expected->describe(), $actual->describe())); + } + +} diff --git a/tests/PHPStan/Type/BooleanTypeTest.php b/tests/PHPStan/Type/BooleanTypeTest.php index 375210eea2..f7552cba51 100644 --- a/tests/PHPStan/Type/BooleanTypeTest.php +++ b/tests/PHPStan/Type/BooleanTypeTest.php @@ -52,7 +52,7 @@ public function dataAccepts(): array */ public function testAccepts(BooleanType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), diff --git a/tests/PHPStan/Type/CallableTypeTest.php b/tests/PHPStan/Type/CallableTypeTest.php index 1f6e0dc379..a59308f40c 100644 --- a/tests/PHPStan/Type/CallableTypeTest.php +++ b/tests/PHPStan/Type/CallableTypeTest.php @@ -50,6 +50,14 @@ public function dataIsSuperTypeOf(): array new CallableType([new NativeParameterReflection('foo', false, new MixedType(), PassedByReference::createNo(), false, null)], new MixedType(), false), TrinaryLogic::createYes(), ], + [ + new CallableType([ + new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null), + new NativeParameterReflection('bar', false, new StringType(), PassedByReference::createNo(), false, null), + ], new MixedType(), false), + new CallableType([new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null)], new MixedType(), false), + TrinaryLogic::createMaybe(), + ], ]; } @@ -169,7 +177,7 @@ public function dataInferTemplateTypes(): array null, ); - $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, new MixedType(), @@ -338,6 +346,69 @@ public function dataAccepts(): array ]), TrinaryLogic::createYes(), ], + [ + new CallableType([ + new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null), + new NativeParameterReflection('bar', false, new StringType(), PassedByReference::createNo(), false, null), + ], new MixedType(), false), + new CallableType([new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null)], new MixedType(), false), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createNo(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], ]; } @@ -352,7 +423,7 @@ public function testAccepts( { $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), + $type->accepts($acceptedType, true)->result->describe(), 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 2d827f7271..f4d19a2760 100644 --- a/tests/PHPStan/Type/ClassStringTypeTest.php +++ b/tests/PHPStan/Type/ClassStringTypeTest.php @@ -108,7 +108,7 @@ public function dataAccepts(): iterable */ public function testAccepts(ClassStringType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), diff --git a/tests/PHPStan/Type/ClosureTypeFactoryTest.php b/tests/PHPStan/Type/ClosureTypeFactoryTest.php new file mode 100644 index 0000000000..7e3a4537b3 --- /dev/null +++ b/tests/PHPStan/Type/ClosureTypeFactoryTest.php @@ -0,0 +1,69 @@ + 5, 'int'], + ]; + } + + /** + * @param Closure(): mixed $closure + * @dataProvider dataFromClosureObjectReturnType + */ + public function testFromClosureObjectReturnType(Closure $closure, string $returnType): void + { + $closureType = $this->getClosureType($closure); + + $this->assertSame($returnType, $closureType->getReturnType()->describe(VerbosityLevel::precise())); + } + + public function dataFromClosureObjectParameter(): array + { + return [ + [static function (string $foo): void { + }, 0, 'string'], + [static function (string $foo = 'boo'): void { + }, 0, 'string'], + [static function (string $foo = 'foo', int $bar = 5): void { + }, 1, 'int'], + [static function (array $foo): void { + }, 0, 'array'], + [static function (array $foo = [1]): void { + }, 0, 'array'], + ]; + } + + /** + * @param Closure(): mixed $closure + * @dataProvider dataFromClosureObjectParameter + */ + public function testFromClosureObjectParameter(Closure $closure, int $index, string $type): void + { + $closureType = $this->getClosureType($closure); + + $this->assertArrayHasKey($index, $closureType->getParameters()); + $this->assertSame($type, $closureType->getParameters()[$index]->getType()->describe(VerbosityLevel::precise())); + } + + /** + * @param Closure(): mixed $closure + */ + private function getClosureType(Closure $closure): ClosureType + { + return self::getContainer()->getByType(ClosureTypeFactory::class)->fromClosureObject($closure); + } + +} diff --git a/tests/PHPStan/Type/ClosureTypeTest.php b/tests/PHPStan/Type/ClosureTypeTest.php index f8d029048e..ccaaa4ca78 100644 --- a/tests/PHPStan/Type/ClosureTypeTest.php +++ b/tests/PHPStan/Type/ClosureTypeTest.php @@ -23,6 +23,11 @@ public function dataIsSuperTypeOf(): array new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], + [ + new ClosureType([], new MixedType(), false, null, null, null, [], [], []), + new ClosureType([], new MixedType(), false), + TrinaryLogic::createMaybe(), + ], [ new ClosureType([], new UnionType([new IntegerType(), new StringType()]), false), new ClosureType([], new IntegerType(), false), diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php new file mode 100644 index 0000000000..2d1aaa237c --- /dev/null +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php @@ -0,0 +1,165 @@ +setOffsetValueType(null, new ConstantIntegerType(1)); + + $array1 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array1); + $this->assertSame('array{1}', $array1->describe(VerbosityLevel::precise())); + $this->assertSame([1], $array1->getNextAutoIndexes()); + + $builder->setOffsetValueType(null, new ConstantIntegerType(2), true); + $array2 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array2); + $this->assertSame('array{0: 1, 1?: 2}', $array2->describe(VerbosityLevel::precise())); + $this->assertSame([1, 2], $array2->getNextAutoIndexes()); + + $builder->setOffsetValueType(null, new ConstantIntegerType(3)); + $array3 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array3); + $this->assertSame('array{0: 1, 1: 2|3, 2?: 3}', $array3->describe(VerbosityLevel::precise())); + $this->assertSame([2, 3], $array3->getNextAutoIndexes()); + + $this->assertTrue($array3->isKeysSupersetOf($array2)); + $array2MergedWith3 = $array3->mergeWith($array2); + $this->assertSame('array{0: 1, 1?: 2|3, 2?: 3}', $array2MergedWith3->describe(VerbosityLevel::precise())); + $this->assertSame([1, 2, 3], $array2MergedWith3->getNextAutoIndexes()); + + $builder->setOffsetValueType(null, new ConstantIntegerType(4)); + $array4 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array4); + $this->assertSame('array{0: 1, 1: 2|3, 2: 3|4, 3?: 4}', $array4->describe(VerbosityLevel::precise())); + $this->assertSame([3, 4], $array4->getNextAutoIndexes()); + + $builder->setOffsetValueType(new ConstantIntegerType(3), new ConstantIntegerType(5), true); + $array5 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array5); + $this->assertSame('array{0: 1, 1: 2|3, 2: 3|4, 3?: 4|5}', $array5->describe(VerbosityLevel::precise())); + $this->assertSame([3, 4], $array5->getNextAutoIndexes()); + + $builder->setOffsetValueType(new ConstantIntegerType(3), new ConstantIntegerType(6)); + $array6 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array6); + $this->assertSame('array{1, 2|3, 3|4, 6}', $array6->describe(VerbosityLevel::precise())); + $this->assertSame([4], $array6->getNextAutoIndexes()); + } + + public function testNextAutoIndex(): void + { + $builder = ConstantArrayTypeBuilder::createFromConstantArray(new ConstantArrayType( + [new ConstantIntegerType(0)], + [new ConstantStringType('foo')], + [1], + )); + $builder->setOffsetValueType(new ConstantIntegerType(0), new ConstantStringType('bar')); + $array = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame('array{\'bar\'}', $array->describe(VerbosityLevel::precise())); + $this->assertSame([1], $array->getNextAutoIndexes()); + } + + public function testNextAutoIndexAnother(): void + { + $builder = ConstantArrayTypeBuilder::createFromConstantArray(new ConstantArrayType( + [new ConstantIntegerType(0)], + [new ConstantStringType('foo')], + [1], + )); + $builder->setOffsetValueType(new ConstantIntegerType(1), new ConstantStringType('bar')); + $array = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame('array{\'foo\', \'bar\'}', $array->describe(VerbosityLevel::precise())); + $this->assertSame([2], $array->getNextAutoIndexes()); + } + + public function testAppendingOptionalKeys(): void + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $builder->setOffsetValueType(null, new BooleanType(), true); + $this->assertSame('array{0?: bool}', $builder->getArray()->describe(VerbosityLevel::precise())); + + $builder->setOffsetValueType(null, new NullType(), true); + $this->assertSame('array{0?: bool|null, 1?: null}', $builder->getArray()->describe(VerbosityLevel::precise())); + + $builder->setOffsetValueType(null, new ConstantIntegerType(17)); + $this->assertSame('array{0: 17|bool|null, 1?: 17|null, 2?: 17}', $builder->getArray()->describe(VerbosityLevel::precise())); + } + + public function testDegradedArrayIsNotAlwaysOversized(): void + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->degradeToGeneralArray(); + for ($i = 0; $i < 300; $i++) { + $builder->setOffsetValueType(new StringType(), new StringType()); + } + + $array = $builder->getArray(); + $this->assertSame('non-empty-array', $array->describe(VerbosityLevel::precise())); + } + + public function testIsList(): void + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $builder->setOffsetValueType(null, new ConstantIntegerType(0)); + $this->assertTrue($builder->isList()); + + $builder->setOffsetValueType(new ConstantIntegerType(0), new NullType()); + $this->assertTrue($builder->isList()); + + $builder->setOffsetValueType(new ConstantIntegerType(1), new NullType(), true); + $this->assertTrue($builder->isList()); + + $builder->setOffsetValueType(new ConstantIntegerType(2), new NullType(), true); + $this->assertFalse($builder->isList()); + } + + public function testIsListWithUnion(): void + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $builder->setOffsetValueType(null, new ConstantIntegerType(0)); + $this->assertTrue($builder->isList()); + + $builder->setOffsetValueType(new ConstantIntegerType(0), new NullType()); + $this->assertTrue($builder->isList()); + + $builder->setOffsetValueType(new ConstantIntegerType(1), new NullType()); + $this->assertTrue($builder->isList()); + + $builder->setOffsetValueType(new ConstantIntegerType(2), new NullType()); + $this->assertTrue($builder->isList()); + + $oneOrZero = TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ); + + $builder->setOffsetValueType($oneOrZero, new NullType()); + $this->assertTrue($builder->isList()); + + $oneOrFour = TypeCombinator::union( + new ConstantIntegerType(1), + new ConstantIntegerType(4), + ); + + $builder->setOffsetValueType($oneOrFour, new NullType()); + $this->assertFalse($builder->isList()); + } + +} diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 0bad091f2d..b047b86a69 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -5,6 +5,8 @@ use Closure; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\ArrayType; use PHPStan\Type\CallableType; use PHPStan\Type\Generic\GenericClassStringType; @@ -12,12 +14,16 @@ use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function array_map; use function sprintf; @@ -184,7 +190,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new IntegerType(), - ], 0, [0, 1]), + ], [0], [0, 1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -220,7 +226,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new IntegerType(), - ], 0, [1]), + ], [0], [1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -236,7 +242,7 @@ public function dataAccepts(): iterable new ConstantStringType('limit'), ], [ new IntegerType(), - ], 0, [0]), + ], [0], [0]), new ConstantArrayType([ new ConstantStringType('limit'), ], [ @@ -250,7 +256,7 @@ public function dataAccepts(): iterable new ConstantStringType('limit'), ], [ new IntegerType(), - ], 0), + ], [0]), new ConstantArrayType([ new ConstantStringType('limit'), ], [ @@ -266,7 +272,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], [0], [0, 1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -284,7 +290,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], [0], [0, 1]), new ConstantArrayType([ new ConstantStringType('color'), ], [ @@ -300,7 +306,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], [0], [0, 1]), new ConstantArrayType([ new ConstantStringType('sound'), ], [ @@ -316,14 +322,14 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], [0], [0, 1]), new ConstantArrayType([ new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ new ConstantStringType('s'), new ConstantStringType('m'), - ], 0, [0, 1]), + ], [0], [0, 1]), TrinaryLogic::createYes(), ]; @@ -334,7 +340,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new IntegerType(), - ], 0, [0, 1]), + ], [0], [0, 1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -344,6 +350,63 @@ public function dataAccepts(): iterable ]), TrinaryLogic::createNo(), ]; + + yield [ + new ConstantArrayType([], []), + new NeverType(), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new NeverType(), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('test')), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new StringType()]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantStringType('test'), new StringType()), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('test')), + ]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new StringType()]), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantStringType('test'), new StringType()), + ]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), + new IntersectionType([ + new UnionType([new ArrayType(new MixedType(), new MixedType()), new IterableType(new MixedType(), new MixedType())]), + new HasOffsetType(new ConstantStringType('test')), + ]), + TrinaryLogic::createMaybe(), + ]; } /** @@ -351,7 +414,7 @@ public function dataAccepts(): iterable */ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), @@ -460,7 +523,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2), + ], [2]), new ConstantArrayType([], []), TrinaryLogic::createNo(), ]; @@ -472,7 +535,7 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0]), + ], [2], [0]), new ConstantArrayType([], []), TrinaryLogic::createNo(), ]; @@ -484,11 +547,75 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), new ConstantArrayType([], []), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), TrinaryLogic::createMaybe(), ]; + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + TrinaryLogic::createNo(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + TrinaryLogic::createNo(), + ]; + yield [ new ConstantArrayType([], []), new ConstantArrayType([ @@ -497,9 +624,73 @@ public function dataIsSuperTypeOf(): iterable ], [ new IntegerType(), new IntegerType(), - ], 2, [0, 1]), + ], [2], [0, 1]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + new ArrayType(new StringType(), new MixedType()), TrinaryLogic::createMaybe(), ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createNo(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + new ArrayType(new StringType(), new MixedType()), + TrinaryLogic::createNo(), + ]; } /** @@ -517,7 +708,7 @@ public function testIsSuperTypeOf(ConstantArrayType $type, Type $otherType, Trin public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, new MixedType(), @@ -718,4 +909,146 @@ public function dataIsCallable(): iterable ]; } + public function dataValuesArray(): iterable + { + yield 'empty' => [ + new ConstantArrayType([], []), + new ConstantArrayType([], []), + ]; + + yield 'non-optional' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [20], [], TrinaryLogic::createNo()), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [2], [], TrinaryLogic::createYes()), + ]; + + yield 'optional-1' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + new ConstantIntegerType(12), + new ConstantIntegerType(13), + new ConstantIntegerType(14), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + new ConstantStringType('d'), + new ConstantStringType('e'), + ], [15], [1, 3], TrinaryLogic::createNo()), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + new ConstantIntegerType(4), + ], [ + new ConstantStringType('a'), + new UnionType([new ConstantStringType('b'), new ConstantStringType('c')]), + new UnionType([new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e')]), + new UnionType([new ConstantStringType('d'), new ConstantStringType('e')]), + new ConstantStringType('e'), + ], [3, 4, 5], [3, 4], TrinaryLogic::createYes()), + ]; + + yield 'optional-2' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + new ConstantIntegerType(12), + new ConstantIntegerType(13), + new ConstantIntegerType(14), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + new ConstantStringType('d'), + new ConstantStringType('e'), + ], [15], [0, 2, 4], TrinaryLogic::createNo()), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + new ConstantIntegerType(4), + ], [ + new UnionType([new ConstantStringType('a'), new ConstantStringType('b')]), + new UnionType([new ConstantStringType('b'), new ConstantStringType('c'), new ConstantStringType('d')]), + new UnionType([new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e')]), + new UnionType([new ConstantStringType('d'), new ConstantStringType('e')]), + new ConstantStringType('e'), + ], [2, 3, 4, 5], [2, 3, 4], TrinaryLogic::createYes()), + ]; + + yield 'optional-at-end-and-list' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + new ConstantIntegerType(12), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + ], [11, 12, 13], [1, 2], TrinaryLogic::createYes()), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + ], [1, 2, 3], [1, 2], TrinaryLogic::createYes()), + ]; + + yield 'optional-at-end-but-not-list' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + new ConstantIntegerType(12), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + ], [11, 12, 13], [1, 2], TrinaryLogic::createNo()), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ], [ + new ConstantStringType('a'), + new UnionType([new ConstantStringType('b'), new ConstantStringType('c')]), + new ConstantStringType('c'), + ], [1, 2, 3], [1, 2], TrinaryLogic::createYes()), + ]; + } + + /** + * @dataProvider dataValuesArray + */ + public function testValuesArray(ConstantArrayType $type, ConstantArrayType $expectedType): void + { + $actualType = $type->getValuesArray(); + $message = sprintf( + 'Values array of %s is %s, but should be %s', + $type->describe(VerbosityLevel::precise()), + $actualType->describe(VerbosityLevel::precise()), + $expectedType->describe(VerbosityLevel::precise()), + ); + $this->assertTrue($expectedType->equals($actualType), $message); + $this->assertSame($expectedType->isList(), $actualType->isList()); + $this->assertSame($expectedType->getNextAutoIndexes(), $actualType->getNextAutoIndexes()); + } + } diff --git a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php index 00ec37a8b6..2122e3c829 100644 --- a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php @@ -23,6 +23,14 @@ public function dataDescribe(): array new ConstantFloatType(1.2000000992884E-10), '1.2000000992884E-10', ], + [ + new ConstantFloatType(-1.200000099288476E+10), + '-12000000992.88476', + ], + [ + new ConstantFloatType(-1.200000099288476E+20), + '-1.200000099288476E+20', + ], [ new ConstantFloatType(1.2 * 1.4), '1.68', diff --git a/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php b/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php index 1b39e5ff55..c33c33e5a6 100644 --- a/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php @@ -38,7 +38,7 @@ public function dataAccepts(): iterable */ public function testAccepts(ConstantIntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index f2be3f6973..2098a1f02b 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -151,12 +151,20 @@ public function testIsSuperTypeOf(ConstantStringType $type, Type $otherType, Tri public function testGeneralize(): void { - $this->assertSame('literal-string&non-empty-string', (new ConstantStringType('NonexistentClass'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-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('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string&uppercase-string', (new ConstantStringType('A'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-empty-string&numeric-string&uppercase-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string', (new ConstantStringType('1e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('1e91e9'))->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('literal-string&non-falsy-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())); } diff --git a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php new file mode 100644 index 0000000000..cd278837d6 --- /dev/null +++ b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php @@ -0,0 +1,91 @@ +&oversized-array', + ]; + + yield [ + '[1, 2, 3, ...[1, 2, 3]]', + 'non-empty-list&oversized-array', + ]; + + yield [ + '[1, 2, 3, ...[1, \'foo\' => 2, 3]]', + 'non-empty-array&oversized-array', + ]; + + yield [ + '[1, 2, 3, ...[1, \'FOO\' => 2, 3]]', + 'non-empty-array&oversized-array', + ]; + + yield [ + '[1, 2, 2 => 3]', + 'non-empty-list&oversized-array', + ]; + yield [ + '[1, 2, 3 => 3]', + 'non-empty-array&oversized-array', + ]; + yield [ + '[1, 1 => 2, 3]', + 'non-empty-list&oversized-array', + ]; + yield [ + '[1, 2 => 2, 3]', + 'non-empty-array&oversized-array', + ]; + yield [ + '[1, \'foo\' => 2, 3]', + 'non-empty-array&oversized-array', + ]; + yield [ + '[1, \'FOO\' => 2, 3]', + 'non-empty-array&oversized-array', + ]; + } + + /** + * @dataProvider dataBuild + */ + public function testBuild(string $sourceCode, string $expectedTypeDescription): void + { + $parser = self::getParser(); + $ast = $parser->parseString('assertInstanceOf(Expression::class, $expr); + + $array = $expr->expr; + $this->assertInstanceOf(Array_::class, $array); + + $builder = new OversizedArrayBuilder(); + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + $arrayType = $builder->build($array, static fn (Expr $expr): Type => $initializerExprTypeResolver->getType($expr, InitializerExprContext::createEmpty())); + $this->assertSame($expectedTypeDescription, $arrayType->describe(VerbosityLevel::precise())); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../../conf/bleedingEdge.neon', + ]; + } + +} diff --git a/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php b/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php index e700d67a3d..1bf9013c42 100644 --- a/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php +++ b/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php @@ -219,7 +219,7 @@ public function testAccepts( $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), + $type->accepts($acceptedType, true)->result->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 cc8a6632b6..c1af0c7740 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -3,7 +3,6 @@ namespace PHPStan\Type; use DependentPhpDocs\Foo; -use PHPStan\Broker\Broker; use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; @@ -18,7 +17,7 @@ public function testGetResolvedPhpDoc(): void /** @var FileTypeMapper $fileTypeMapper */ $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); - $resolvedA = $fileTypeMapper->getResolvedPhpDoc(__DIR__ . '/data/annotations.php', 'Foo', null, null, '/** + $resolvedA = $fileTypeMapper->getResolvedPhpDoc(__DIR__ . '/data/annotations.php', 'TestAnnotations\\Foo', null, null, '/** * @property int | float $numericBazBazProperty * @property X $singleLetterObjectName * @@ -34,8 +33,14 @@ public function testGetResolvedPhpDoc(): void $this->assertCount(0, $resolvedA->getParamTags()); $this->assertCount(2, $resolvedA->getPropertyTags()); $this->assertNull($resolvedA->getReturnTag()); - $this->assertSame('float|int', $resolvedA->getPropertyTags()['numericBazBazProperty']->getType()->describe(VerbosityLevel::precise())); - $this->assertSame('X', $resolvedA->getPropertyTags()['singleLetterObjectName']->getType()->describe(VerbosityLevel::precise())); + $this->assertNotNull($resolvedA->getPropertyTags()['numericBazBazProperty']->getReadableType()); + $this->assertNotNull($resolvedA->getPropertyTags()['numericBazBazProperty']->getWritableType()); + $this->assertSame('float|int', $resolvedA->getPropertyTags()['numericBazBazProperty']->getReadableType()->describe(VerbosityLevel::precise())); + $this->assertSame('float|int', $resolvedA->getPropertyTags()['numericBazBazProperty']->getWritableType()->describe(VerbosityLevel::precise())); + $this->assertNotNull($resolvedA->getPropertyTags()['singleLetterObjectName']->getReadableType()); + $this->assertNotNull($resolvedA->getPropertyTags()['singleLetterObjectName']->getWritableType()); + $this->assertSame('TestAnnotations\\X', $resolvedA->getPropertyTags()['singleLetterObjectName']->getReadableType()->describe(VerbosityLevel::precise())); + $this->assertSame('TestAnnotations\\X', $resolvedA->getPropertyTags()['singleLetterObjectName']->getWritableType()->describe(VerbosityLevel::precise())); $this->assertCount(6, $resolvedA->getMethodTags()); $this->assertArrayNotHasKey('complicatedParameters', $resolvedA->getMethodTags()); // ambiguous parameter types @@ -60,7 +65,7 @@ public function testGetResolvedPhpDoc(): void $this->assertCount(0, $returningNullableObject->getParameters()); $rotate = $resolvedA->getMethodTags()['rotate']; - $this->assertSame('Image', $rotate->getReturnType()->describe(VerbosityLevel::precise())); + $this->assertSame('TestAnnotations\\Image', $rotate->getReturnType()->describe(VerbosityLevel::precise())); $this->assertFalse($rotate->isStatic()); $this->assertCount(2, $rotate->getParameters()); $this->assertSame('float', $rotate->getParameters()['angle']->getType()->describe(VerbosityLevel::precise())); @@ -80,7 +85,7 @@ public function testGetResolvedPhpDoc(): void $this->assertTrue($paramMultipleTypesWithExtraSpaces->getParameters()['string']->passedByReference()->no()); $this->assertFalse($paramMultipleTypesWithExtraSpaces->getParameters()['string']->isOptional()); $this->assertFalse($paramMultipleTypesWithExtraSpaces->getParameters()['string']->isVariadic()); - $this->assertSame('stdClass|null', $paramMultipleTypesWithExtraSpaces->getParameters()['object']->getType()->describe(VerbosityLevel::precise())); + $this->assertSame('TestAnnotations\\stdClass|null', $paramMultipleTypesWithExtraSpaces->getParameters()['object']->getType()->describe(VerbosityLevel::precise())); $this->assertTrue($paramMultipleTypesWithExtraSpaces->getParameters()['object']->passedByReference()->no()); $this->assertFalse($paramMultipleTypesWithExtraSpaces->getParameters()['object']->isOptional()); $this->assertFalse($paramMultipleTypesWithExtraSpaces->getParameters()['object']->isVariadic()); @@ -155,7 +160,7 @@ public function testFileThrowsPhpDocs(): void public function testFileWithCyclicPhpDocs(): void { - self::getContainer()->getByType(Broker::class); + $this->createReflectionProvider(); /** @var FileTypeMapper $fileTypeMapper */ $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); diff --git a/tests/PHPStan/Type/FloatTypeTest.php b/tests/PHPStan/Type/FloatTypeTest.php index ef878bf6dd..04a9c270ca 100644 --- a/tests/PHPStan/Type/FloatTypeTest.php +++ b/tests/PHPStan/Type/FloatTypeTest.php @@ -66,7 +66,7 @@ public function dataAccepts(): array public function testAccepts(Type $otherType, TrinaryLogic $expectedResult): void { $type = new FloatType(); - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), diff --git a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php index bfd00bcbb1..ef6a0faf8d 100644 --- a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php @@ -9,7 +9,9 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ClassStringType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; @@ -73,7 +75,7 @@ public function dataIsSuperTypeOf(): array 8 => [ new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), new ConstantStringType(Exception::class), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], 9 => [ new GenericClassStringType(new ObjectType(stdClass::class)), @@ -128,7 +130,7 @@ public function dataIsSuperTypeOf(): array TemplateTypeVariance::createInvariant(), )), new ConstantStringType(Throwable::class), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], 15 => [ new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), @@ -138,13 +140,21 @@ public function dataIsSuperTypeOf(): array 16 => [ new GenericClassStringType(new StaticType($reflectionProvider->getClass(InvalidArgumentException::class))), new ConstantStringType(Exception::class), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], 17 => [ new GenericClassStringType(new StaticType($reflectionProvider->getClass(Throwable::class))), new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], + 18 => [ + new GenericClassStringType(new ObjectType(Type::class, new UnionType([ + new ObjectType(ConstantIntegerType::class), + new ObjectType(IntegerRangeType::class), + ]))), + new ConstantStringType(IntegerType::class), + TrinaryLogic::createMaybe(), + ], ]; } @@ -274,7 +284,7 @@ public function testAccepts( TrinaryLogic $expectedResult, ): void { - $actualResult = $acceptingType->accepts($acceptedType, true); + $actualResult = $acceptingType->accepts($acceptedType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index ac6c6e9ad0..3be9b7193d 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -17,6 +17,7 @@ use PHPStan\Type\Test\B; use PHPStan\Type\Test\C; use PHPStan\Type\Test\D; +use PHPStan\Type\Test\E; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -25,6 +26,7 @@ use Traversable; use function array_map; use function sprintf; +use const PHP_VERSION_ID; class GenericObjectTypeTest extends PHPStanTestCase { @@ -63,7 +65,7 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], 'implementation with @extends with different type args' => [ - new GenericObjectType(B\I::class, [new ObjectType('DateTimeInteface')]), + new GenericObjectType(B\I::class, [new ObjectType('DateTimeInterface')]), new GenericObjectType(B\IImpl::class, [new ObjectType('DateTime')]), TrinaryLogic::createNo(), ], @@ -97,6 +99,21 @@ public function dataIsSuperTypeOf(): array new GenericObjectType(C\Covariant::class, [new ObjectType('DateTimeInterface')]), TrinaryLogic::createMaybe(), ], + 'contravariant with equal types' => [ + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + TrinaryLogic::createYes(), + ], + 'contravariant with sub type' => [ + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTimeInterface')]), + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + TrinaryLogic::createMaybe(), + ], + 'contravariant with super type' => [ + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTimeInterface')]), + TrinaryLogic::createYes(), + ], [ new ObjectType(ReflectionClass::class), new GenericObjectType(ReflectionClass::class, [ @@ -118,7 +135,7 @@ public function dataIsSuperTypeOf(): array new GenericObjectType(ReflectionClass::class, [ new ObjectType(stdClass::class), ]), - TrinaryLogic::createYes(), + PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createYes(), ], [ new GenericObjectType(ReflectionClass::class, [ @@ -127,7 +144,7 @@ public function dataIsSuperTypeOf(): array new GenericObjectType(ReflectionClass::class, [ new ObjectWithoutClassType(), ]), - TrinaryLogic::createMaybe(), + PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(), ], [ new GenericObjectType(ReflectionClass::class, [ @@ -138,11 +155,115 @@ public function dataIsSuperTypeOf(): array ]), TrinaryLogic::createNo(), ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createInvariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createContravariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createNo(), + ], + ]; + } + + public function dataTypeProjections(): array + { + $invariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createInvariant()]); + $invariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createInvariant()]); + $invariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createInvariant()]); + + $covariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createCovariant()]); + $covariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createCovariant()]); + $covariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createCovariant()]); + + $contravariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createContravariant()]); + $contravariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createContravariant()]); + $contravariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createContravariant()]); + + $bivariant = new GenericObjectType(E\Foo::class, [new MixedType(true)], null, null, [TemplateTypeVariance::createBivariant()]); + + return [ + [$invariantB, $invariantA, TrinaryLogic::createNo()], + [$invariantB, $invariantB, TrinaryLogic::createYes()], + [$invariantB, $invariantC, TrinaryLogic::createNo()], + [$invariantB, $covariantA, TrinaryLogic::createNo()], + [$invariantB, $covariantB, TrinaryLogic::createNo()], + [$invariantB, $covariantC, TrinaryLogic::createNo()], + [$invariantB, $contravariantA, TrinaryLogic::createNo()], + [$invariantB, $contravariantB, TrinaryLogic::createNo()], + [$invariantB, $contravariantC, TrinaryLogic::createNo()], + [$invariantB, $bivariant, TrinaryLogic::createNo()], + + [$covariantB, $invariantA, TrinaryLogic::createMaybe()], + [$covariantB, $invariantB, TrinaryLogic::createYes()], + [$covariantB, $invariantC, TrinaryLogic::createYes()], + [$covariantB, $covariantA, TrinaryLogic::createMaybe()], + [$covariantB, $covariantB, TrinaryLogic::createYes()], + [$covariantB, $covariantC, TrinaryLogic::createYes()], + [$covariantB, $contravariantA, TrinaryLogic::createNo()], + [$covariantB, $contravariantB, TrinaryLogic::createNo()], + [$covariantB, $contravariantC, TrinaryLogic::createNo()], + [$covariantB, $bivariant, TrinaryLogic::createNo()], + + [$contravariantB, $invariantA, TrinaryLogic::createYes()], + [$contravariantB, $invariantB, TrinaryLogic::createYes()], + [$contravariantB, $invariantC, TrinaryLogic::createMaybe()], + [$contravariantB, $covariantA, TrinaryLogic::createNo()], + [$contravariantB, $covariantB, TrinaryLogic::createNo()], + [$contravariantB, $covariantC, TrinaryLogic::createNo()], + [$contravariantB, $contravariantA, TrinaryLogic::createYes()], + [$contravariantB, $contravariantB, TrinaryLogic::createYes()], + [$contravariantB, $contravariantC, TrinaryLogic::createMaybe()], + [$contravariantB, $bivariant, TrinaryLogic::createNo()], + + [$bivariant, $invariantA, TrinaryLogic::createYes()], + [$bivariant, $invariantB, TrinaryLogic::createYes()], + [$bivariant, $invariantC, TrinaryLogic::createYes()], + [$bivariant, $covariantA, TrinaryLogic::createYes()], + [$bivariant, $covariantB, TrinaryLogic::createYes()], + [$bivariant, $covariantC, TrinaryLogic::createYes()], + [$bivariant, $contravariantA, TrinaryLogic::createYes()], + [$bivariant, $contravariantB, TrinaryLogic::createYes()], + [$bivariant, $contravariantC, TrinaryLogic::createYes()], + [$bivariant, $bivariant, TrinaryLogic::createYes()], ]; } /** * @dataProvider dataIsSuperTypeOf + * @dataProvider dataTypeProjections */ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -188,12 +309,12 @@ public function dataAccepts(): array TrinaryLogic::createYes(), ], 'implementation with @extends with different type args' => [ - new GenericObjectType(B\I::class, [new ObjectType('DateTimeInteface')]), + new GenericObjectType(B\I::class, [new ObjectType('DateTimeInterface')]), new GenericObjectType(B\IImpl::class, [new ObjectType('DateTime')]), TrinaryLogic::createNo(), ], 'generic object accepts normal object of same type' => [ - new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInteface')]), + new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInterface')]), new ObjectType(Traversable::class), TrinaryLogic::createYes(), ], @@ -212,6 +333,7 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts + * @dataProvider dataTypeProjections */ public function testAccepts( Type $acceptingType, @@ -219,7 +341,7 @@ public function testAccepts( TrinaryLogic $expectedResult, ): void { - $actualResult = $acceptingType->accepts($acceptedType, true); + $actualResult = $acceptingType->accepts($acceptedType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), @@ -230,7 +352,7 @@ public function testAccepts( /** @return array}> */ public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name, ?Type $bound = null): Type => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, $bound ?? new MixedType(), @@ -343,7 +465,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ /** @return array}> */ public function dataGetReferencedTypeArguments(): array { - $templateType = static fn (string $name, ?Type $bound = null): TemplateType => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, $bound ?? new MixedType(), @@ -405,6 +527,76 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + 'param: In' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'param: In>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: In>>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'param: In>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'param: Out>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], 'return: Invariant' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Invariant::class, [ @@ -459,6 +651,192 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + 'return: In' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'return: In>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: In>>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'return: In>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'return: Out>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: Out>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: In>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: Invariant>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: Invariant>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: In>>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: Out>>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: Invariant' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: Invariant' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], 'return: Out>' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Out::class, [ @@ -473,6 +851,108 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + 'return: In>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: Invariant>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: Invariant>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: In>>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: Out>>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: Invariant' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: Invariant' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], ]; } diff --git a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php index a83addb405..2bcde9560a 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php @@ -25,6 +25,8 @@ public function testIssue2512(): void new TemplateTypeMap([ 'T' => $templateType, ]), + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), ); $this->assertEquals( @@ -40,6 +42,8 @@ public function testIssue2512(): void $templateType, ]), ]), + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), ); $this->assertEquals( diff --git a/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php b/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php index 71a57dda6d..c5bd9ac0a6 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php @@ -87,14 +87,15 @@ public function testIsValidVariance( TrinaryLogic $expectedInversed, ): void { + $templateType = TemplateTypeFactory::create(TemplateTypeScope::createWithFunction('foo'), 'T', null, $variance); $this->assertSame( $expected->describe(), - $variance->isValidVariance($a, $b)->describe(), + $variance->isValidVariance($templateType, $a, $b)->result->describe(), sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())), ); $this->assertSame( $expectedInversed->describe(), - $variance->isValidVariance($b, $a)->describe(), + $variance->isValidVariance($templateType, $b, $a)->result->describe(), sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $b->describe(VerbosityLevel::precise()), $a->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-c.php b/tests/PHPStan/Type/Generic/data/generic-classes-c.php index cd17cc9261..417797587b 100644 --- a/tests/PHPStan/Type/Generic/data/generic-classes-c.php +++ b/tests/PHPStan/Type/Generic/data/generic-classes-c.php @@ -9,3 +9,7 @@ interface Invariant { /** @template-covariant T */ interface Covariant { } + +/** @template-contravariant T */ +interface Contravariant { +} diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-d.php b/tests/PHPStan/Type/Generic/data/generic-classes-d.php index b25dbc6bac..69621f8e08 100644 --- a/tests/PHPStan/Type/Generic/data/generic-classes-d.php +++ b/tests/PHPStan/Type/Generic/data/generic-classes-d.php @@ -13,3 +13,9 @@ interface Out { /** @return T */ public function get(); } + +/** @template-contravariant T */ +interface In { + /** @return T */ + public function get(); +} diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-e.php b/tests/PHPStan/Type/Generic/data/generic-classes-e.php new file mode 100644 index 0000000000..ed60625429 --- /dev/null +++ b/tests/PHPStan/Type/Generic/data/generic-classes-e.php @@ -0,0 +1,10 @@ +accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index c771c085fd..d5259d53e5 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -4,18 +4,25 @@ use DoctrineIntersectionTypeIsSupertypeOf\Collection; use Iterator; +use ObjectTypeEnums\FooEnum; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; -use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use stdClass; use Test\ClassWithToString; use Traversable; +use function count; use function sprintf; +use const PHP_VERSION_ID; class IntersectionTypeTest extends PHPStanTestCase { @@ -42,7 +49,7 @@ public function dataAccepts(): Iterator yield [ $intersectionType, new IterableType(new MixedType(), new ObjectType('Item')), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ]; yield [ @@ -57,7 +64,7 @@ public function dataAccepts(): Iterator yield [ TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new CallableType()), new CallableType(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ]; } @@ -66,7 +73,7 @@ public function dataAccepts(): Iterator */ public function testAccepts(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), @@ -148,52 +155,6 @@ public function dataIsSuperTypeOf(): Iterator TrinaryLogic::createNo(), ]; - yield [ - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new StringType()), - ]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - new ConstantStringType('c'), - ], [ - new ConstantIntegerType(1), - new ConstantIntegerType(2), - new ConstantIntegerType(3), - ]), - TrinaryLogic::createMaybe(), - ]; - - yield [ - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new StringType()), - ]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - new ConstantStringType('c'), - new ConstantStringType('d'), - new ConstantStringType('e'), - new ConstantStringType('f'), - new ConstantStringType('g'), - new ConstantStringType('h'), - new ConstantStringType('i'), - ], [ - new ConstantIntegerType(1), - new ConstantIntegerType(2), - new ConstantIntegerType(3), - new ConstantIntegerType(1), - new ConstantIntegerType(2), - new ConstantIntegerType(3), - new ConstantIntegerType(1), - new ConstantIntegerType(2), - new ConstantIntegerType(3), - ]), - TrinaryLogic::createMaybe(), - ]; - yield [ new IntersectionType([ new ObjectType(Traversable::class), @@ -265,6 +226,12 @@ public function dataIsSuperTypeOf(): Iterator ]), TrinaryLogic::createYes(), ]; + + yield [ + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new OversizedArrayType()]), + new ArrayType(new IntegerType(), new StringType()), + TrinaryLogic::createMaybe(), + ]; } /** @@ -399,4 +366,369 @@ public function testToBooleanCrash(): void $this->assertSame('true', $type->toBoolean()->describe(VerbosityLevel::precise())); } + public function dataGetEnumCases(): iterable + { + if (PHP_VERSION_ID < 80100) { + return []; + } + + $reflectionProvider = $this->createReflectionProvider(); + $classReflection = $reflectionProvider->getClass(FooEnum::class); + + yield [ + new IntersectionType([ + new ThisType($classReflection), + new EnumCaseObjectType(FooEnum::class, 'FOO'), + ]), + [ + new EnumCaseObjectType(FooEnum::class, 'FOO'), + ], + ]; + } + + /** + * @dataProvider dataGetEnumCases + * @param list $expectedEnumCases + */ + public function testGetEnumCases( + IntersectionType $type, + array $expectedEnumCases, + ): void + { + $enumCases = $type->getEnumCases(); + $this->assertCount(count($expectedEnumCases), $enumCases); + foreach ($enumCases as $i => $enumCase) { + $expectedEnumCase = $expectedEnumCases[$i]; + $this->assertTrue($expectedEnumCase->equals($enumCase), sprintf('%s->equals(%s)', $expectedEnumCase->describe(VerbosityLevel::precise()), $enumCase->describe(VerbosityLevel::precise()))); + } + } + + public function dataDescribe(): iterable + { + yield [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + VerbosityLevel::typeOnly(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + VerbosityLevel::value(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + VerbosityLevel::precise(), + 'lowercase-string', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-list', + ]; + + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new IntegerType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new IntegerType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-list', + ]; + + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array', + ]; + + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new IntegerType()), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new IntegerType()), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new IntegerType()), + new AccessoryArrayListType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new IntegerType()), + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new AccessoryArrayListType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new AccessoryArrayListType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array', + ]; + + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + new OversizedArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array', + ]; + + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + new OversizedArrayType(), + ]), + VerbosityLevel::precise(), + 'non-empty-array&oversized-array', + ]; + + $constantArrayWithOptionalKeys = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new StringType(), + new StringType(), + new StringType(), + new StringType(), + ], [3], [2, 3], TrinaryLogic::createMaybe()); + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list{0: string, 1: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + VerbosityLevel::precise(), + 'list{0: string, 1: string, 2?: string, 3?: string}', + ]; + + $constantArrayWithAllOptionalKeys = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new StringType(), + new StringType(), + new StringType(), + new StringType(), + ], [3], [0, 1, 2, 3], TrinaryLogic::createMaybe()); + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'non-empty-list{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + VerbosityLevel::typeOnly(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + VerbosityLevel::value(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + VerbosityLevel::precise(), + 'uppercase-string', + ]; + } + + /** + * @dataProvider dataDescribe + */ + public function testDescribe(IntersectionType $type, VerbosityLevel $verbosityLevel, string $expected): void + { + static::assertSame($expected, $type->describe($verbosityLevel)); + } + } diff --git a/tests/PHPStan/Type/IterableTypeTest.php b/tests/PHPStan/Type/IterableTypeTest.php index 02a10d0dde..013c0fd15c 100644 --- a/tests/PHPStan/Type/IterableTypeTest.php +++ b/tests/PHPStan/Type/IterableTypeTest.php @@ -185,7 +185,7 @@ public function testIsSubTypeOfInversed(IterableType $type, Type $otherType, Tri public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, new MixedType(), @@ -329,7 +329,7 @@ public function dataAccepts(): array */ public function testAccepts(IterableType $iterableType, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $iterableType->accepts($otherType, true); + $actualResult = $iterableType->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php index e639b15708..3ffc8f9db7 100644 --- a/tests/PHPStan/Type/MixedTypeTest.php +++ b/tests/PHPStan/Type/MixedTypeTest.php @@ -2,9 +2,17 @@ namespace PHPStan\Type; +use ArrayAccess; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use function sprintf; class MixedTypeTest extends PHPStanTestCase @@ -164,4 +172,996 @@ public function testIsSuperTypeOf(MixedType $type, Type $otherType, TrinaryLogic ); } + public function dataSubstractedIsArray(): array + { + return [ + [ + new MixedType(), + new ArrayType(new IntegerType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new ConstantArrayType( + [new ConstantIntegerType(1)], + [new ConstantStringType('hello')], + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new ArrayType(new MixedType(), new MixedType())]), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new ArrayType(new StringType(), new MixedType())]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new IntegerType()]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new FloatType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(true), + new FloatType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsArray + */ + public function testSubstractedIsArray(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isArray(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isArray()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsConstantArray(): array + { + return [ + [ + new MixedType(), + new ArrayType(new IntegerType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new ConstantArrayType( + [new ConstantIntegerType(1)], + [new ConstantStringType('hello')], + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantArrayType([], []), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new ArrayType(new MixedType(), new MixedType())]), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new ArrayType(new StringType(), new MixedType())]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new IntegerType()]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new FloatType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(true), + new FloatType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsConstantArray + */ + public function testSubstractedIsConstantArray(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isConstantArray(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isConstantArray()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsString + */ + public function testSubstractedIsString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsNumericString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsNumericString + */ + public function testSubstractedIsNumericString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isNumericString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNumericString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsNonEmptyString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonFalsyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsNonEmptyString + */ + public function testSubstractedIsNonEmptyString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isNonEmptyString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNonEmptyString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsNonFalsyString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonFalsyStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsNonFalsyString + */ + public function testSubstractedIsNonFalsyString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isNonFalsyString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNonFalsyString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsLiteralString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryLiteralStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonFalsyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsClassString + */ + public function testSubstractedIsClassString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isClassString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isClassStringType()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsClassString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new ClassStringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** @dataProvider dataSubtractedIsVoid */ + public function testSubtractedIsVoid(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isVoid(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isVoid()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubtractedIsVoid(): array + { + return [ + [ + new MixedType(), + new VoidType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** @dataProvider dataSubtractedIsScalar */ + public function testSubtractedIsScalar(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isScalar(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isScalar()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubtractedIsScalar(): array + { + return [ + [ + new MixedType(), + new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType()]), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsLiteralString + */ + public function testSubstractedIsLiteralString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isLiteralString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isLiteralString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsIterable(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryLiteralStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IterableType(new MixedType(), new MixedType()), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new IterableType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsBoolean + */ + public function testSubstractedIsBoolean(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isBoolean(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isBoolean()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsBoolean(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(true), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(false), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new BooleanType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsFalse + */ + public function testSubstractedIsFalse(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isFalse(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isFalse()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsFalse(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(true), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(false), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new BooleanType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsNull + */ + public function testSubstractedIsNull(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isNull(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNull()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsNull(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(true), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(false), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new BooleanType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new NullType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsTrue + */ + public function testSubstractedIsTrue(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isTrue(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isTrue()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsTrue(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(true), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new ConstantBooleanType(false), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new BooleanType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsFloat + */ + public function testSubstractedIsFloat(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isFloat(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isFloat()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsFloat(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + IntegerRangeType::fromInterval(-5, 5), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new FloatType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsInteger + */ + public function testSubstractedIsInteger(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isInteger(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isInteger()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsInteger(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + IntegerRangeType::fromInterval(-5, 5), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsIterable + */ + public function testSubstractedIsIterable(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isIterable(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isIterable()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsOffsetAccessible(): array + { + return [ + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ObjectType(ArrayAccess::class), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + new ObjectType(ArrayAccess::class), + ]), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + new ObjectType(ArrayAccess::class), + new FloatType(), + ]), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsOffsetAccessible + */ + public function testSubstractedIsOffsetAccessible(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isOffsetAccessible(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isOffsetAccessible()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsOffsetLegal(): array + { + return [ + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntersectionType([ + new ObjectWithoutClassType(), + new ObjectType(ArrayAccess::class), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ObjectWithoutClassType(), + TrinaryLogic::createYes(), + ], + [ + new MixedType(), + new UnionType([ + new ObjectWithoutClassType(), + new StringType(), + ]), + TrinaryLogic::createYes(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsOffsetLegal + */ + public function testSubstractedIsOffsetLegal(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isOffsetAccessLegal(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isOffsetAccessLegal()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubtractedHasOffsetValueType(): array + { + return [ + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new StringType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ObjectType(ArrayAccess::class), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + new ObjectType(ArrayAccess::class), + ]), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + new ObjectType(ArrayAccess::class), + new FloatType(), + ]), + new StringType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** @dataProvider dataSubtractedHasOffsetValueType */ + public function testSubtractedHasOffsetValueType(MixedType $mixedType, Type $typeToSubtract, Type $offsetType, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->hasOffsetValueType($offsetType); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> hasOffsetValueType()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + } diff --git a/tests/PHPStan/Type/ObjectTypeTest.php b/tests/PHPStan/Type/ObjectTypeTest.php index 81298886b5..9a1bdf2fea 100644 --- a/tests/PHPStan/Type/ObjectTypeTest.php +++ b/tests/PHPStan/Type/ObjectTypeTest.php @@ -4,30 +4,45 @@ use ArrayAccess; use ArrayObject; +use Bug4008\BaseModel; +use Bug4008\ChildGenericGenericClass; +use Bug4008\GenericClass; +use Bug4008\Model; +use Bug8850\UserInSessionInRoleEndpointExtension; +use Bug9006\TestInterface; use Closure; use Countable; use DateInterval; use DateTime; use DateTimeImmutable; use DateTimeInterface; +use Exception; +use ExtendsThrowable\ExtendsThrowable; use Generator; use InvalidArgumentException; use Iterator; use LogicException; +use ObjectTypeEnums\FooEnum; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasPropertyType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait; use SimpleXMLElement; use stdClass; use Throwable; +use ThrowPoints\TryCatch\MyInvalidArgumentException; use Traversable; +use function count; use function sprintf; +use const PHP_VERSION_ID; class ObjectTypeTest extends PHPStanTestCase { @@ -55,6 +70,35 @@ public function testIsIterable(ObjectType $type, TrinaryLogic $expectedResult): ); } + /** + * @return iterable + */ + public function dataIsEnum(): iterable + { + if (PHP_VERSION_ID >= 80000) { + yield [new ObjectType('UnitEnum'), TrinaryLogic::createYes()]; + yield [new ObjectType('BackedEnum'), TrinaryLogic::createYes()]; + } + yield [new ObjectType('Unknown'), TrinaryLogic::createMaybe()]; + yield [new ObjectType('Countable'), TrinaryLogic::createMaybe()]; + yield [new ObjectType('Stringable'), TrinaryLogic::createNo()]; + yield [new ObjectType('Throwable'), TrinaryLogic::createNo()]; + yield [new ObjectType('DateTime'), TrinaryLogic::createNo()]; + } + + /** + * @dataProvider dataIsEnum + */ + public function testIsEnum(ObjectType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isEnum(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isEnum()', $type->describe(VerbosityLevel::precise())), + ); + } + public function dataIsCallable(): array { return [ @@ -260,7 +304,7 @@ public function dataIsSuperTypeOf(): array 32 => [ new ObjectType(Closure::class), new HasPropertyType('d'), - TrinaryLogic::createNo(), + PHP_VERSION_ID < 80200 ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(), ], 33 => [ new ObjectType(DateInterval::class), @@ -298,7 +342,7 @@ public function dataIsSuperTypeOf(): array 39 => [ new ObjectType(Throwable::class, new ObjectType(InvalidArgumentException::class)), new ObjectType('Exception'), - TrinaryLogic::createYes(), + TrinaryLogic::createMaybe(), ], 40 => [ new ObjectType(Throwable::class, new ObjectType('Exception')), @@ -313,7 +357,7 @@ public function dataIsSuperTypeOf(): array 42 => [ new ObjectType(Throwable::class, new ObjectType('Exception')), new ObjectType(Throwable::class), - TrinaryLogic::createYes(), + TrinaryLogic::createMaybe(), ], 43 => [ new ObjectType(Throwable::class), @@ -360,6 +404,84 @@ public function dataIsSuperTypeOf(): array ), TrinaryLogic::createMaybe(), ], + 49 => [ + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + new ObjectType(InvalidArgumentException::class), + TrinaryLogic::createNo(), + ], + 50 => [ + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + new ObjectType(MyInvalidArgumentException::class), + TrinaryLogic::createNo(), + ], + 51 => [ + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + new ObjectType(LogicException::class), + TrinaryLogic::createMaybe(), + ], + 52 => [ + new ObjectType(InvalidArgumentException::class, new ObjectType(MyInvalidArgumentException::class)), + new ObjectType(Exception::class), + TrinaryLogic::createMaybe(), + ], + 53 => [ + new ObjectType(InvalidArgumentException::class, new ObjectType(MyInvalidArgumentException::class)), + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + TrinaryLogic::createNo(), + ], + 54 => [ + new ObjectType(InvalidArgumentException::class), + new ObjectType(Exception::class, new ObjectType(InvalidArgumentException::class)), + TrinaryLogic::createNo(), + ], + 55 => [ + new ObjectType(stdClass::class, new ObjectType(Throwable::class)), + new ObjectType(Throwable::class), + TrinaryLogic::createNo(), + ], + 56 => [ + new ObjectType(Type::class, new UnionType([ + new ObjectType(ConstantIntegerType::class), + new ObjectType(IntegerRangeType::class), + ])), + new ObjectType(IntegerType::class), + TrinaryLogic::createMaybe(), + ], + 57 => [ + new ObjectType(Throwable::class), + new ObjectType(ExtendsThrowable::class), + TrinaryLogic::createYes(), + ], + 58 => [ + new ObjectType(Throwable::class, new ObjectType(InvalidArgumentException::class)), + new ObjectType(ExtendsThrowable::class), + TrinaryLogic::createMaybe(), + ], + 59 => [ + new ObjectType(DateTime::class), + new ObjectType(ConstantNumericComparisonTypeTrait::class), + TrinaryLogic::createNo(), + ], + 60 => [ + new ObjectType(ConstantNumericComparisonTypeTrait::class), + new ObjectType(DateTime::class), + TrinaryLogic::createNo(), + ], + 61 => [ + new ObjectType(UserInSessionInRoleEndpointExtension::class), + new ThisType($reflectionProvider->getClass(UserInSessionInRoleEndpointExtension::class)), + TrinaryLogic::createYes(), + ], + 62 => [ + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), + TrinaryLogic::createNo(), + ], + 63 => [ + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), + TrinaryLogic::createNo(), + ], ]; } @@ -391,7 +513,7 @@ public function dataAccepts(): array ], [ new ObjectType(Traversable::class), - new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInteface')]), + new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInterface')]), TrinaryLogic::createYes(), ], [ @@ -414,6 +536,16 @@ public function dataAccepts(): array ), TrinaryLogic::createNo(), ], + [ + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), + TrinaryLogic::createNo(), + ], + 63 => [ + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), + TrinaryLogic::createNo(), + ], ]; } @@ -428,7 +560,7 @@ public function testAccepts( { $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), + $type->accepts($acceptedType, true)->result->describe(), sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } @@ -513,4 +645,79 @@ public function testHasOffsetValueType( ); } + public function dataGetEnumCases(): iterable + { + yield [ + new ObjectType(stdClass::class), + [], + ]; + + yield [ + new ObjectType(FooEnum::class), + [ + new EnumCaseObjectType(FooEnum::class, 'FOO'), + new EnumCaseObjectType(FooEnum::class, 'BAR'), + new EnumCaseObjectType(FooEnum::class, 'BAZ'), + ], + ]; + + yield [ + new ObjectType(FooEnum::class, new EnumCaseObjectType(FooEnum::class, 'FOO')), + [ + new EnumCaseObjectType(FooEnum::class, 'BAR'), + new EnumCaseObjectType(FooEnum::class, 'BAZ'), + ], + ]; + + yield [ + new ObjectType(FooEnum::class, new UnionType([new EnumCaseObjectType(FooEnum::class, 'FOO'), new EnumCaseObjectType(FooEnum::class, 'BAR')])), + [ + new EnumCaseObjectType(FooEnum::class, 'BAZ'), + ], + ]; + } + + /** + * @dataProvider dataGetEnumCases + * @param list $expectedEnumCases + */ + public function testGetEnumCases( + ObjectType $type, + array $expectedEnumCases, + ): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $enumCases = $type->getEnumCases(); + $this->assertCount(count($expectedEnumCases), $enumCases); + foreach ($enumCases as $i => $enumCase) { + $expectedEnumCase = $expectedEnumCases[$i]; + $this->assertTrue($expectedEnumCase->equals($enumCase), sprintf('%s->equals(%s)', $expectedEnumCase->describe(VerbosityLevel::precise()), $enumCase->describe(VerbosityLevel::precise()))); + } + } + + public function testClassReflectionWithTemplateBound(): void + { + $type = new ObjectType(GenericClass::class); + $classReflection = $type->getClassReflection(); + $this->assertNotNull($classReflection); + $tModlel = $classReflection->getActiveTemplateTypeMap()->getType('TModlel'); + $this->assertNotNull($tModlel); + $this->assertSame(BaseModel::class, $tModlel->describe(VerbosityLevel::precise())); + } + + public function testClassReflectionParentWithTemplateBound(): void + { + $type = new ObjectType(ChildGenericGenericClass::class); + $classReflection = $type->getClassReflection(); + $this->assertNotNull($classReflection); + $ancestor = $classReflection->getAncestorWithClassName(GenericClass::class); + $this->assertNotNull($ancestor); + $tModlel = $ancestor->getActiveTemplateTypeMap()->getType('TModlel'); + $this->assertNotNull($tModlel); + $this->assertSame(Model::class, $tModlel->describe(VerbosityLevel::precise())); + } + } diff --git a/tests/PHPStan/Type/Regex/RegexExpressionHelperTest.php b/tests/PHPStan/Type/Regex/RegexExpressionHelperTest.php new file mode 100644 index 0000000000..330594145f --- /dev/null +++ b/tests/PHPStan/Type/Regex/RegexExpressionHelperTest.php @@ -0,0 +1,61 @@ +getByType(RegexExpressionHelper::class); + + $this->assertSame( + $expectedPatternWithoutDelimiter, + $regexExpressionHelper->removeDelimitersAndModifiers($inputPattern), + ); + } + +} diff --git a/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php b/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php new file mode 100644 index 0000000000..3917423ff3 --- /dev/null +++ b/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php @@ -0,0 +1,92 @@ +', + ]; + yield [ + new ArrayType(new MixedType(), new IntegerType()), + new ConstantArrayType( + [new ConstantIntegerType(0)], + [new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()])], + [1], + ), + 'array', + ]; + yield [ + new ArrayType(new MixedType(), new StringType()), + new ConstantArrayType( + [new ConstantIntegerType(0)], + [new IntegerType()], + [1], + ), + 'array', + ]; + + yield [ + new BenevolentUnionType([ + new StringType(), + new IntegerType(), + ]), + new UnionType([ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntegerType(), + new FloatType(), + ]), + '(int|string)', + ]; + + yield [ + new BenevolentUnionType([ + new StringType(), + new IntegerType(), + ]), + new UnionType([ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntegerType(), + ]), + '(int|non-empty-string)', + ]; + } + + /** + * @dataProvider dataChangeStringIntoNonEmptyString + */ + public function testChangeIntegerIntoString(Type $left, Type $right, string $expectedTypeDescription): void + { + $cb = static function (Type $left, Type $right, callable $traverse): Type { + if (!$left->isString()->yes()) { + return $traverse($left, $right); + } + if (!$right->isNonEmptyString()->yes()) { + return $traverse($left, $right); + } + return $right; + }; + $actualType = SimultaneousTypeTraverser::map($left, $right, $cb); + $this->assertSame($expectedTypeDescription, $actualType->describe(VerbosityLevel::precise())); + } + +} diff --git a/tests/PHPStan/Type/StaticTypeTest.php b/tests/PHPStan/Type/StaticTypeTest.php index 670d42aaf7..c681142840 100644 --- a/tests/PHPStan/Type/StaticTypeTest.php +++ b/tests/PHPStan/Type/StaticTypeTest.php @@ -10,8 +10,14 @@ use InvalidArgumentException; use Iterator; use LogicException; +use PHPStan\Generics\FunctionsAssertType\C; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; use StaticTypeTest\Base; use StaticTypeTest\Child; use StaticTypeTest\FinalChild; @@ -253,6 +259,35 @@ public function dataIsSuperTypeOf(): array new ObjectType(FinalChild::class), TrinaryLogic::createYes(), ], + [ + new ThisType( + $reflectionProvider->getClass(\ThisSubtractable\Foo::class), // phpcs:ignore + new UnionType([new ObjectType(\ThisSubtractable\Bar::class), new ObjectType(\ThisSubtractable\Baz::class)]), // phpcs:ignore + ), + new UnionType([ + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Bar::class), // phpcs:ignore + ]), + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Baz::class), // phpcs:ignore + ]), + ]), + TrinaryLogic::createNo(), + ], + [ + new GenericStaticType( + $reflectionProvider->getClass(\MethodSignatureGenericStaticType\Foo::class), // phpcs:ignore + [new StringType()], + null, + [], + ), + new GenericObjectType(\MethodSignatureGenericStaticType\FinalBar::class, [ // phpcs:ignore + new IntegerType(), + ]), + TrinaryLogic::createNo(), + ], ]; } @@ -306,4 +341,131 @@ public function testEquals(StaticType $type, StaticType $otherType, bool $expect $this->assertSame($expected, $otherType->equals($type)); } + public function dataAccepts(): iterable + { + $reflectionProvider = $this->createReflectionProvider(); + $c = $reflectionProvider->getClass(C::class); + + yield [ + new StaticType($c), + new StaticType($c), + TrinaryLogic::createYes(), + ]; + + yield [ + // static !== static + new StaticType($c), + new GenericStaticType($c, [new IntegerType()], null, []), + TrinaryLogic::createNo(), + ]; + + yield [ + // static !== static + new StaticType($c), + new GenericStaticType($c, [new IntegerType()], null, [ + TemplateTypeVariance::createCovariant(), + ]), + TrinaryLogic::createNo(), + ]; + + yield [ + // static === static + new StaticType($c), + new GenericStaticType($c, [ + TemplateTypeFactory::create(TemplateTypeScope::createWithClass($c->getName()), 'T', null, TemplateTypeVariance::createInvariant()), + ], null, []), + TrinaryLogic::createYes(), + ]; + + yield [ + // static !== static + new GenericStaticType($c, [ + TemplateTypeFactory::create(TemplateTypeScope::createWithClass($c->getName()), 'T', null, TemplateTypeVariance::createInvariant()), + ], null, []), + new StaticType($c), + TrinaryLogic::createNo(), // could be Yes + ]; + + yield [ + new GenericStaticType($c, [new IntegerType()], null, []), + new GenericStaticType($c, [new IntegerType()], null, []), + TrinaryLogic::createYes(), + ]; + + yield [ + new GenericStaticType($c, [new UnionType([ + new IntegerType(), + new StringType(), + ])], null, []), + new GenericStaticType($c, [new IntegerType()], null, []), + TrinaryLogic::createNo(), + ]; + + yield [ + new GenericStaticType($c, [new UnionType([ + new IntegerType(), + new StringType(), + ])], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new IntegerType()], null, []), + TrinaryLogic::createYes(), + ]; + + yield [ + new GenericStaticType($c, [new IntegerType()], null, []), + new GenericStaticType($c, [new UnionType([ + new IntegerType(), + new StringType(), + ])], null, []), + TrinaryLogic::createNo(), + ]; + + yield [ + new GenericStaticType($c, [new IntegerType()], null, [ + TemplateTypeVariance::createContravariant(), + ]), + new GenericStaticType($c, [new UnionType([ + new IntegerType(), + new StringType(), + ])], null, []), + TrinaryLogic::createYes(), + ]; + + yield [ + new GenericStaticType($c, [new IntegerType()], null, []), + new ObjectType($c->getName()), + TrinaryLogic::createNo(), + ]; + + yield [ + new GenericStaticType($c, [new IntegerType()], null, []), + new GenericObjectType($c->getName(), [new IntegerType()], null), + TrinaryLogic::createNo(), + ]; + + yield [ + new GenericStaticType($c, [new IntegerType()], null, []), + new ObjectWithoutClassType(), + TrinaryLogic::createNo(), + ]; + + yield [ + new GenericStaticType($c, [new IntegerType()], null, []), + new ObjectWithoutClassType(), + TrinaryLogic::createNo(), + ]; + } + + /** + * @dataProvider dataAccepts + */ + public function testAccepts(StaticType $type, Type $otherType, TrinaryLogic $expectedResult): void + { + $actualResult = $type->accepts($otherType, true); + $this->assertSame( + $expectedResult->describe(), + $actualResult->result->describe(), + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + } diff --git a/tests/PHPStan/Type/StringTypeTest.php b/tests/PHPStan/Type/StringTypeTest.php index d31be3e1fd..813349b91a 100644 --- a/tests/PHPStan/Type/StringTypeTest.php +++ b/tests/PHPStan/Type/StringTypeTest.php @@ -86,6 +86,11 @@ public function dataIsSuperTypeOf(): array new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createMaybe(), ], + [ + new StringAlwaysAcceptingObjectWithToStringType(), + new ObjectType(ClassWithToString::class), + TrinaryLogic::createYes(), + ], ]; } @@ -174,7 +179,7 @@ public function dataAccepts(): iterable */ public function testAccepts(StringType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), diff --git a/tests/PHPStan/Type/TemplateTypeTest.php b/tests/PHPStan/Type/TemplateTypeTest.php index 36281bdba1..6b184eef8a 100644 --- a/tests/PHPStan/Type/TemplateTypeTest.php +++ b/tests/PHPStan/Type/TemplateTypeTest.php @@ -25,7 +25,7 @@ class TemplateTypeTest extends PHPStanTestCase public function dataAccepts(): array { - $templateType = static fn (string $name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction($functionName ?? '_'), $name, $bound, @@ -108,7 +108,7 @@ public function testAccepts( { assert($type instanceof TemplateType); - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedAccept->describe(), $actualResult->describe(), @@ -117,7 +117,7 @@ public function testAccepts( $type = $type->toArgument(); - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedAcceptArg->describe(), $actualResult->describe(), @@ -127,7 +127,7 @@ public function testAccepts( public function dataIsSuperTypeOf(): array { - $templateType = static fn (string $name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction($functionName ?? '_'), $name, $bound, @@ -315,13 +315,12 @@ public function testIsSuperTypeOf( /** @return array}> */ public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name, ?Type $bound = null, ?string $functionName = null): Type => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound = null, ?string $functionName = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction($functionName ?? '_'), $name, $bound, TemplateTypeVariance::createInvariant(), ); - return [ 'simple' => [ new IntegerType(), diff --git a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php index 41db098905..5d34d6f911 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php @@ -10,7 +10,7 @@ final class TestDecimalOperatorTypeSpecifyingExtension implements OperatorTypeSp public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type $rightSide): bool { - return in_array($operatorSigil, ['-', '+', '*', '/'], true) + return in_array($operatorSigil, ['-', '+', '*', '/', '^', '**'], true) && $leftSide->isSuperTypeOf(new ObjectType(TestDecimal::class))->yes() && $rightSide->isSuperTypeOf(new ObjectType(TestDecimal::class))->yes(); } diff --git a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php index 3f64bd6221..55f44ec4fe 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php @@ -46,6 +46,18 @@ public function dataSigilAndSidesProvider(): iterable new ObjectType(TestDecimal::class), new ObjectType(TestDecimal::class), ]; + + yield '^' => [ + '^', + new ObjectType(TestDecimal::class), + new ObjectType(TestDecimal::class), + ]; + + yield '**' => [ + '**', + new ObjectType(TestDecimal::class), + new ObjectType(TestDecimal::class), + ]; } /** diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 297c5b32fa..a85b6a4709 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2,23 +2,35 @@ namespace PHPStan\Type; +use Bug9006\TestInterface; use CheckTypeFunctionCall\FinalClassWithMethodExists; use CheckTypeFunctionCall\FinalClassWithPropertyExists; use Closure; use DateTime; use DateTimeImmutable; use DateTimeInterface; +use DynamicProperties\FinalFoo; use Exception; use InvalidArgumentException; use Iterator; +use ObjectShapesAcceptance\ClassWithFooIntProperty; use PHPStan\Fixture\FinalClass; +use PHPStan\Generics\FunctionsAssertType\C; +use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -27,6 +39,7 @@ use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateObjectType; @@ -44,6 +57,7 @@ use Traversable; use function array_map; use function array_reverse; +use function get_class; use function implode; use function sprintf; use const PHP_VERSION_ID; @@ -711,8 +725,8 @@ public function dataUnion(): iterable new StringType(), ]), ], - ConstantArrayType::class, - 'array{foo: DateTimeImmutable|null, bar: int|string}', + UnionType::class, + 'array{foo: DateTimeImmutable, bar: int}|array{foo: null, bar: string}', ], [ [ @@ -729,8 +743,8 @@ public function dataUnion(): iterable new NullType(), ]), ], - ConstantArrayType::class, - 'array{foo: DateTimeImmutable|null, bar?: int}', + UnionType::class, + 'array{foo: DateTimeImmutable, bar: int}|array{foo: null}', ], [ [ @@ -751,8 +765,8 @@ public function dataUnion(): iterable new IntegerType(), ]), ], - ConstantArrayType::class, - 'array{foo: DateTimeImmutable|null, bar: int|string, baz?: int}', + UnionType::class, + 'array{foo: DateTimeImmutable, bar: int}|array{foo: null, bar: string, baz: int}', ], [ [ @@ -863,20 +877,6 @@ public function dataUnion(): iterable UnionType::class, "'bar'|'barr'|'baz'|'bazz'|'foo'|'fooo'|'lorem'|'loremm'|'loremmm'", ], - [ - [ - new IntersectionType([ - new ArrayType(new MixedType(), new StringType()), - new HasOffsetType(new StringType()), - ]), - new IntersectionType([ - new ArrayType(new MixedType(), new StringType()), - new HasOffsetType(new StringType()), - ]), - ], - IntersectionType::class, - 'array&hasOffset(string)', - ], [ [ new IntersectionType([ @@ -953,8 +953,8 @@ public function dataUnion(): iterable new HasOffsetType(new ConstantStringType('bar')), ]), ], - ArrayType::class, - 'array', + IntersectionType::class, + 'non-empty-array', ], [ [ @@ -969,7 +969,21 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - 'array&hasOffset(\'foo\')', + 'non-empty-array&hasOffsetValue(\'foo\', mixed)', + ], + [ + [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('foo')), + ]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantIntegerType(2), new ConstantStringType('foo')), + ]), + ], + IntersectionType::class, + 'non-empty-array', ], [ [ @@ -1069,7 +1083,7 @@ public function dataUnion(): iterable new ObjectWithoutClassType(new ObjectType('A')), ], MixedType::class, - 'mixed=implicit', + 'mixed~int=implicit', ], [ [ @@ -1125,7 +1139,7 @@ public function dataUnion(): iterable new ObjectType('InvalidArgumentException'), ], MixedType::class, - 'mixed=implicit', // should be MixedType~Exception+InvalidArgumentException + 'mixed~Exception~InvalidArgumentException=implicit', ], [ [ @@ -1519,7 +1533,7 @@ public function dataUnion(): iterable new ConstantStringType('test_function'), ], UnionType::class, - '\'test_function\'|(callable(): mixed&string)', + '\'test_function\'|callable-string', ], [ [ @@ -1527,7 +1541,7 @@ public function dataUnion(): iterable new IntegerType(), ], UnionType::class, - '(callable(): mixed&string)|int', + 'callable-string|int', ], [ [ @@ -1561,6 +1575,34 @@ public function dataUnion(): iterable UnionType::class, 'int<1, 3>|int<7, 9>', ], + [ + [ + IntegerRangeType::fromInterval(4, 9), + IntegerRangeType::fromInterval(16, 81), + IntegerRangeType::fromInterval(8, 27), + ], + IntegerRangeType::class, + 'int<4, 81>', + ], + [ + [ + IntegerRangeType::fromInterval(8, 27), + IntegerRangeType::fromInterval(4, 6), + new ConstantIntegerType(7), + IntegerRangeType::fromInterval(16, 81), + ], + IntegerRangeType::class, + 'int<4, 81>', + ], + [ + [ + new IntegerType(), + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), + ], + IntegerType::class, + 'int', + ], [ [ IntegerRangeType::fromInterval(1, 3), @@ -1784,7 +1826,7 @@ public function dataUnion(): iterable new ConstantIntegerType(0), ], [ new StringType(), - ], 1, [0]), + ], [1], [0]), ], UnionType::class, 'array{}|array{0?: string}', @@ -1847,22 +1889,6 @@ public function dataUnion(): iterable UnionType::class, 'array{a: int, b: int}|array{b: int, c: int}', ], - [ - [ - TypeCombinator::intersect(new StringType(), new HasOffsetType(new IntegerType())), - TypeCombinator::intersect(new StringType(), new HasOffsetType(new IntegerType())), - ], - IntersectionType::class, - 'string&hasOffset(int)', - ], - [ - [ - TypeCombinator::intersect(new ConstantStringType('abc'), new HasOffsetType(new IntegerType())), - TypeCombinator::intersect(new ConstantStringType('abc'), new HasOffsetType(new IntegerType())), - ], - IntersectionType::class, - '\'abc\'&hasOffset(int)', - ], [ [ StaticTypeFactory::falsey(), @@ -1877,7 +1903,7 @@ public function dataUnion(): iterable StaticTypeFactory::truthy(), ], MixedType::class, - 'mixed~0|0.0|\'\'|\'0\'|array{}|false|null=implicit', + 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)=implicit', ], [ [ @@ -1905,6 +1931,28 @@ public function dataUnion(): iterable StringType::class, 'string', ], + [ + [ + new ConstantStringType('0'), + new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]), + ], + IntersectionType::class, + 'non-empty-string', + ], + [ + [ + new ConstantStringType(''), + new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]), + ], + StringType::class, + 'string', + ], [ [ new StringType(), @@ -1929,6 +1977,102 @@ public function dataUnion(): iterable UnionType::class, 'string|false', ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + ], + UnionType::class, + 'literal-string|numeric-string', + ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|numeric-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-falsy-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-empty-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'literal-string|lowercase-string', + ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'numeric-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-falsy-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-empty-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'literal-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|uppercase-string', + ], [ [ TemplateTypeFactory::create( @@ -2076,1340 +2220,2513 @@ public function dataUnion(): iterable ]; yield [ [ - new MixedType(false, new IntegerRangeType(17, null)), - new IntegerRangeType(19, null), + new MixedType(false, IntegerRangeType::fromInterval(17, null)), + IntegerRangeType::fromInterval(19, null), ], MixedType::class, 'mixed~int<17, 18>=implicit', ]; - } - - /** - * @dataProvider dataUnion - * @param Type[] $types - * @param class-string $expectedTypeClass - */ - public function testUnion( - array $types, - string $expectedTypeClass, - string $expectedTypeDescription, - ): void - { - $actualType = TypeCombinator::union(...$types); - $actualTypeDescription = $actualType->describe(VerbosityLevel::precise()); - if ($actualType instanceof MixedType) { - if ($actualType->isExplicitMixed()) { - $actualTypeDescription .= '=explicit'; - } else { - $actualTypeDescription .= '=implicit'; - } - } - - $this->assertSame( - $expectedTypeDescription, - $actualTypeDescription, - sprintf('union(%s)', implode(', ', array_map( - static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), - $types, - ))), - ); - - $this->assertInstanceOf($expectedTypeClass, $actualType); - - $hasSubtraction = false; - foreach ($types as $type) { - if (!($type instanceof SubtractableType) || $type->getSubtractedType() === null) { - continue; - } - - $hasSubtraction = true; - } - - if ($hasSubtraction) { - return; - } - } - /** - * @dataProvider dataUnion - * @param Type[] $types - * @param class-string $expectedTypeClass - */ - public function testUnionInversed( - array $types, - string $expectedTypeClass, - string $expectedTypeDescription, - ): void - { - $types = array_reverse($types); - $actualType = TypeCombinator::union(...$types); - $actualTypeDescription = $actualType->describe(VerbosityLevel::precise()); - if ($actualType instanceof MixedType) { - if ($actualType->isExplicitMixed()) { - $actualTypeDescription .= '=explicit'; - } else { - $actualTypeDescription .= '=implicit'; - } - } - $this->assertSame( - $expectedTypeDescription, - $actualTypeDescription, - sprintf('union(%s)', implode(', ', array_map( - static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), - $types, - ))), - ); - $this->assertInstanceOf($expectedTypeClass, $actualType); - } - - public function dataIntersect(): iterable - { $reflectionProvider = $this->createReflectionProvider(); - - yield from [ + yield [ [ - [ - new IterableType(new MixedType(), new StringType()), - new ObjectType('ArrayObject'), - ], - IntersectionType::class, - 'ArrayObject&iterable', + new StaticType($reflectionProvider->getClass(stdClass::class)), + new ThisType($reflectionProvider->getClass(stdClass::class)), ], + StaticType::class, + 'static(stdClass)', + ]; + + yield [ [ - [ - new IterableType(new MixedType(), new StringType()), - new ArrayType(new MixedType(), new StringType()), - ], - ArrayType::class, - 'array', + new StaticType($reflectionProvider->getClass(stdClass::class)), + new ObjectType(stdClass::class), ], + ObjectType::class, + 'stdClass', + ]; + + yield [ [ - [ - new IterableType(new MixedType(true), new StringType()), - new ObjectType('Iterator'), - ], - IntersectionType::class, - 'iterable&Iterator', + new StaticType($reflectionProvider->getClass(stdClass::class)), + new EnumCaseObjectType(stdClass::class, 'foo'), ], - [ + UnionType::class, + 'static(stdClass)|stdClass::foo', + ]; + + yield [ + [ + new ThisType($reflectionProvider->getClass(stdClass::class)), + new EnumCaseObjectType(stdClass::class, 'foo'), + ], + UnionType::class, + '$this(stdClass)|stdClass::foo', + ]; + + yield [ + [ + new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + ])), + new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A')), + ], + ObjectType::class, + 'PHPStan\Fixture\ManyCasesTestEnum~PHPStan\Fixture\ManyCasesTestEnum::A', + ]; + + yield [ + [ + new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + ])), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'C'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'), + ]), + ], + ObjectType::class, + 'PHPStan\Fixture\ManyCasesTestEnum~(PHPStan\Fixture\ManyCasesTestEnum::A|PHPStan\Fixture\ManyCasesTestEnum::B)', + ]; + + yield [ + [ + new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + ])), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'), + ]), + ], + ObjectType::class, + 'PHPStan\Fixture\ManyCasesTestEnum~PHPStan\Fixture\ManyCasesTestEnum::B', + ]; + + yield [ + [ + new ThisType( + $reflectionProvider->getClass(\ThisSubtractable\Foo::class), // phpcs:ignore + new UnionType([new ObjectType(\ThisSubtractable\Bar::class), new ObjectType(\ThisSubtractable\Baz::class)]), // phpcs:ignore + ), + new UnionType([ + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Bar::class), // phpcs:ignore + ]), + new IntersectionType([ + new ThisType($reflectionProvider->getClass(\ThisSubtractable\Foo::class)), // phpcs:ignore + new ObjectType(\ThisSubtractable\Baz::class), // phpcs:ignore + ]), + ]), + ], + ThisType::class, + '$this(ThisSubtractable\Foo)', + ]; + + yield [ + [ + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), + ], + IntersectionType::class, + 'non-empty-string', + ]; + + yield [ + [ + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + new ConstantStringType('0'), + ], + IntersectionType::class, + 'non-empty-string', + ]; + + yield [ + [ + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), + new ConstantStringType('0'), + ], + IntersectionType::class, + 'non-empty-string', + ]; + + yield [ + [ + TypeCombinator::intersect(new StringType(), new AccessoryLiteralStringType(), new AccessoryNonFalsyStringType()), + new ConstantStringType('bar'), + new ConstantStringType('baz'), + new ConstantStringType('foo'), + ], + IntersectionType::class, + 'literal-string&non-falsy-string', + ]; + + yield [ + [ + TypeCombinator::intersect(new StringType(), new AccessoryLiteralStringType(), new AccessoryNonEmptyStringType()), + new ConstantStringType('bar'), + new ConstantStringType('baz'), + new ConstantStringType('foo'), + ], + IntersectionType::class, + 'literal-string&non-empty-string', + ]; + + yield [ + [ + new HasOffsetValueType(new ConstantStringType('a'), new ConstantIntegerType(1)), + new HasOffsetValueType(new ConstantStringType('a'), new IntegerType()), + ], + HasOffsetValueType::class, + 'hasOffsetValue(\'a\', int)', + ]; + + yield [ + [ + TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new HasOffsetValueType(new ConstantStringType('a'), new ConstantIntegerType(1))), + TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new HasOffsetValueType(new ConstantStringType('a'), new IntegerType())), + ], + IntersectionType::class, + 'non-empty-array&hasOffsetValue(\'a\', int)', + ]; + + yield [ + [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType( + new ConstantStringType('a'), + StaticTypeFactory::falsey(), + ), + ]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType( + new ConstantStringType('a'), + StaticTypeFactory::truthy(), + ), + ]), + ], + IntersectionType::class, + "non-empty-array&hasOffsetValue('a', mixed)", + ]; + + yield [ + [ + new IntersectionType([ + new ArrayType(new IntegerType(), new ArrayType(new MixedType(), new MixedType())), + new HasOffsetValueType( + new ConstantIntegerType(0), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantStringType('code'), new ConstantIntegerType(1)), + ]), + ), + ]), + new IntersectionType([ + new ArrayType(new IntegerType(), new ArrayType(new MixedType(), new MixedType())), + new HasOffsetValueType( + new ConstantIntegerType(0), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantStringType('code'), new MixedType(true, new ConstantIntegerType(1))), + ]), + ), + ]), + ], + IntersectionType::class, + "non-empty-array&hasOffsetValue(0, non-empty-array&hasOffsetValue('code', mixed))", + ]; + + yield [ + [ + new ConstantArrayType( + [new ConstantStringType('default'), new ConstantStringType('range')], + [new ObjectType(Foo::class), new ObjectType(Foo::class)], + [0], + [0, 1], + ), + new ConstantArrayType( + [new ConstantStringType('range')], + [new ObjectType(Foo::class)], + [0], + [0], + ), + ], + ConstantArrayType::class, + 'array{default?: RecursionCallable\Foo, range?: RecursionCallable\Foo}', + ]; + + yield [ + [ + new IntersectionType([ + new ConstantArrayType( + [new ConstantStringType('default'), new ConstantStringType('range')], + [new ObjectType(Foo::class), new ObjectType(Foo::class)], + [0], + [0, 1], + ), + new NonEmptyArrayType(), + ]), + new ConstantArrayType( + [new ConstantStringType('range')], + [new ObjectType(Foo::class)], + [0], + [0], + ), + ], + ConstantArrayType::class, + 'array{default?: RecursionCallable\Foo, range?: RecursionCallable\Foo}', + ]; + yield [ + [ + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new OversizedArrayType()]), + new ArrayType(new IntegerType(), new StringType()), + ], + IntersectionType::class, + 'array&oversized-array', + ]; + yield [ + [ + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), + ], + UnionType::class, + 'Bug9006\TestInterface|(Closure(): mixed)', + ]; + yield [ + [ + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), + ], + UnionType::class, + 'Bug9006\TestInterface|Closure', + ]; + yield [ + [ + new ObjectShapeType([], []), + new ObjectWithoutClassType(), + ], + ObjectWithoutClassType::class, + 'object', + ]; + yield [ + [ + new ObjectShapeType([], []), + new ObjectType(stdClass::class), + ], + UnionType::class, + 'object{}|stdClass', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new IntegerType()], []), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new ConstantIntegerType(1)], []), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new StringType()], []), + ], + UnionType::class, + 'object{foo: int}|object{foo: string}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['bar' => new StringType()], []), + ], + UnionType::class, + 'object{bar: string}|object{foo: int}', + ]; + + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(Traversable::class), + ], + UnionType::class, + 'object{foo: int}|Traversable', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShape\Foo::class), + ], + UnionType::class, + 'ObjectShape\Foo|object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(ClassWithFooIntProperty::class), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\FinalClass::class), + ], + UnionType::class, + 'ObjectShapesAcceptance\FinalClass|object{foo: int}', + ]; + yield [ + [ + new NeverType(), + new NonAcceptingNeverType(), + ], + NeverType::class, + '*NEVER*', + ]; + yield [ + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + ], + UnionType::class, + 'array{a?: true, b: true}|array{a?: true, c?: true}', + ]; + + yield [ + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new IntersectionType([ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + new NonEmptyArrayType(), + ]), + ], + UnionType::class, + 'array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', + ]; + + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'TWO'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\AnotherTestEnum::TWO|PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + new ObjectWithoutClassType(), + ], + ObjectWithoutClassType::class, + 'object', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ClosureType::createPure(), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + ClosureType::createPure(), + ], + UnionType::class, + '(Closure(): mixed)|(pure-Closure)', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', false), + ]), + ClosureType::createPure(), + ], + ClosureType::class, + 'Closure(): mixed', + ]; + yield [ + [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantStringType('thing'), new ConstantStringType('bla')), + ]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('thing')), + ]), + ], + IntersectionType::class, + 'non-empty-array&hasOffsetValue(\'thing\', mixed)', + ]; + + $c = $reflectionProvider->getClass(C::class); + + yield [ + [ + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ + [ + new GenericStaticType($c, [new StringType()], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + ], + UnionType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)|static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ + [ + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new UnionType([ + new IntegerType(), + new StringType(), + ])], null, [TemplateTypeVariance::createCovariant()]), + ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ + [ + new StaticType($c), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + ], + StaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ + [ + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new ObjectWithoutClassType(), + ], + ObjectWithoutClassType::class, + 'object', + ]; + + yield [ + [ + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new ObjectType($c->getName()), + ], + ObjectType::class, + $c->getName(), + ]; + + $nonFinalClass = $reflectionProvider->getClass(\NullCoalesceIsAlwaysFinal\Foo::class); + $finalClass = $nonFinalClass->asFinal(); + + yield [ + [ + new ObjectType($finalClass->getName(), null, $finalClass), + new ObjectType($nonFinalClass->getName(), null, $nonFinalClass), + ], + ObjectType::class, + $nonFinalClass->getDisplayName(), + ]; + } + + /** + * @dataProvider dataUnion + * @param Type[] $types + * @param class-string $expectedTypeClass + */ + public function testUnion( + array $types, + string $expectedTypeClass, + string $expectedTypeDescription, + ): void + { + $actualType = TypeCombinator::union(...$types); + $actualTypeDescription = $actualType->describe(VerbosityLevel::precise()); + if ($actualType instanceof MixedType) { + if ($actualType->isExplicitMixed()) { + $actualTypeDescription .= '=explicit'; + } else { + $actualTypeDescription .= '=implicit'; + } + } + if (get_class($actualType) === ObjectType::class) { + $actualClassReflection = $actualType->getClassReflection(); + if ( + $actualClassReflection !== null + && $actualClassReflection->hasFinalByKeywordOverride() + && $actualClassReflection->isFinal() + ) { + $actualTypeDescription .= '=final'; + } + } + + $this->assertSame( + $expectedTypeDescription, + $actualTypeDescription, + sprintf('union(%s)', implode(', ', array_map( + static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), + $types, + ))), + ); + + $this->assertInstanceOf($expectedTypeClass, $actualType); + + $hasSubtraction = false; + foreach ($types as $type) { + if (!($type instanceof SubtractableType) || $type->getSubtractedType() === null) { + continue; + } + + $hasSubtraction = true; + } + + if ($hasSubtraction) { + return; + } + } + + /** + * @dataProvider dataUnion + * @param Type[] $types + * @param class-string $expectedTypeClass + */ + public function testUnionInversed( + array $types, + string $expectedTypeClass, + string $expectedTypeDescription, + ): void + { + $types = array_reverse($types); + $actualType = TypeCombinator::union(...$types); + $actualTypeDescription = $actualType->describe(VerbosityLevel::precise()); + if ($actualType instanceof MixedType) { + if ($actualType->isExplicitMixed()) { + $actualTypeDescription .= '=explicit'; + } else { + $actualTypeDescription .= '=implicit'; + } + } + if (get_class($actualType) === ObjectType::class) { + $actualClassReflection = $actualType->getClassReflection(); + if ( + $actualClassReflection !== null + && $actualClassReflection->hasFinalByKeywordOverride() + && $actualClassReflection->isFinal() + ) { + $actualTypeDescription .= '=final'; + } + } + $this->assertSame( + $expectedTypeDescription, + $actualTypeDescription, + sprintf('union(%s)', implode(', ', array_map( + static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), + $types, + ))), + ); + $this->assertInstanceOf($expectedTypeClass, $actualType); + } + + public function dataIntersect(): iterable + { + $reflectionProvider = $this->createReflectionProvider(); + + yield from [ + [ + [ + new IterableType(new MixedType(), new StringType()), + new ObjectType('ArrayObject'), + ], + IntersectionType::class, + 'ArrayObject&iterable', + ], + [ + [ + new IterableType(new MixedType(), new StringType()), + new ArrayType(new MixedType(), new StringType()), + ], + ArrayType::class, + 'array', + ], + [ + [ + new IterableType(new MixedType(true), new StringType()), + new ObjectType('Iterator'), + ], + IntersectionType::class, + 'iterable&Iterator', + ], + [ + [ + new ObjectType('Iterator'), + new IterableType( + new MixedType(true), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('_'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + ), + ], + IntersectionType::class, + 'iterable&Iterator', + ], + [ + [ + new ObjectType('Foo'), + new StaticType($reflectionProvider->getClass('Foo')), + ], + StaticType::class, + 'static(Foo)', + ], + [ + [ + new VoidType(), + new MixedType(), + ], + VoidType::class, + 'void', + ], + [ + [ + new ObjectType('UnknownClass'), + new ObjectType('UnknownClass'), + ], + ObjectType::class, + 'UnknownClass', + ], + [ + [ + new UnionType([new ObjectType('UnknownClassA'), new ObjectType('UnknownClassB')]), + new UnionType([new ObjectType('UnknownClassA'), new ObjectType('UnknownClassB')]), + ], + UnionType::class, + 'UnknownClassA|UnknownClassB', + ], + [ + [ + new ConstantBooleanType(true), + new BooleanType(), + ], + ConstantBooleanType::class, + 'true', + ], + [ + [ + new ConstantBooleanType(false), + new BooleanType(), + ], + ConstantBooleanType::class, + 'false', + ], + [ + [ + StaticTypeFactory::truthy(), + new BooleanType(), + ], + ConstantBooleanType::class, + 'true', + ], + [ + [ + StaticTypeFactory::falsey(), + new BooleanType(), + ], + ConstantBooleanType::class, + 'false', + ], + [ + [ + StaticTypeFactory::falsey(), + StaticTypeFactory::truthy(), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new StringType(), + new NeverType(), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new ObjectType('Iterator'), + new ObjectType('Countable'), + new ObjectType('Traversable'), + ], + IntersectionType::class, + 'Countable&Iterator', + ], + [ + [ + new ObjectType('Iterator'), + new ObjectType('Traversable'), + new ObjectType('Countable'), + ], + IntersectionType::class, + 'Countable&Iterator', + ], + [ + [ + new ObjectType('Traversable'), + new ObjectType('Iterator'), + new ObjectType('Countable'), + ], + IntersectionType::class, + 'Countable&Iterator', + ], + [ + [ + new IterableType(new MixedType(), new MixedType()), + new IterableType(new MixedType(), new StringType()), + ], + IterableType::class, + 'iterable', + ], + [ + [ + new ArrayType(new MixedType(), new MixedType()), + new IterableType(new MixedType(), new StringType()), + ], + ArrayType::class, + 'array', + ], + [ + [ + new ArrayType(new IntegerType(), new MixedType()), + new IterableType(new MixedType(), new StringType()), + ], + ArrayType::class, + 'array', + ], + [ + [ + new ArrayType(new IntegerType(), new MixedType()), + new IterableType(new StringType(), new MixedType()), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new ArrayType(new MixedType(), new IntegerType()), + new IterableType(new MixedType(), new StringType()), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new ArrayType(new IntegerType(), new MixedType()), + new ArrayType(new MixedType(), new StringType()), + ], + ArrayType::class, + 'array', + ], + [ + [ + new IterableType(new IntegerType(), new MixedType()), + new IterableType(new MixedType(), new StringType()), + ], + IterableType::class, + 'iterable', + ], + [ + [ + new MixedType(), + new IterableType(new MixedType(), new MixedType()), + ], + IterableType::class, + 'iterable', + ], + [ + [ + new IntegerType(), + new BenevolentUnionType([new IntegerType(), new StringType()]), + ], + IntegerType::class, + 'int', + ], + [ + [ + new ConstantIntegerType(1), + new BenevolentUnionType([new IntegerType(), new StringType()]), + ], + ConstantIntegerType::class, + '1', + ], + [ + [ + new ConstantStringType('foo'), + new BenevolentUnionType([new IntegerType(), new StringType()]), + ], + ConstantStringType::class, + '\'foo\'', + ], + [ + [ + new StringType(), + new BenevolentUnionType([new IntegerType(), new StringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new UnionType([new StringType(), new IntegerType()]), + new BenevolentUnionType([new IntegerType(), new StringType()]), + ], + UnionType::class, + '(int|string)', + ], + [ + [ + new ObjectType(\Test\Foo::class), + new HasMethodType('__toString'), + ], + IntersectionType::class, + 'Test\Foo&hasMethod(__toString)', + ], + [ + [ + new ObjectType(ClassWithToString::class), + new HasMethodType('__toString'), + ], + ObjectType::class, + 'Test\ClassWithToString', + ], + [ + [ + new ObjectType(FinalClassWithMethodExists::class), + new HasMethodType('doBar'), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new ObjectWithoutClassType(), + new HasMethodType('__toString'), + ], + IntersectionType::class, + 'object&hasMethod(__toString)', + ], + [ + [ + new IntegerType(), + new HasMethodType('__toString'), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new IntersectionType([ + new ObjectWithoutClassType(), + new HasMethodType('__toString'), + ]), + new HasMethodType('__toString'), + ], + IntersectionType::class, + 'object&hasMethod(__toString)', + ], + [ [ - new ObjectType('Iterator'), - new IterableType( - new MixedType(true), - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('_'), - 'T', - null, - TemplateTypeVariance::createInvariant(), - ), - ), + new IntersectionType([ + new ObjectWithoutClassType(), + new HasMethodType('foo'), + ]), + new HasMethodType('bar'), ], IntersectionType::class, - 'iterable&Iterator', + 'object&hasMethod(bar)&hasMethod(foo)', ], [ [ - new ObjectType('Foo'), - new StaticType($reflectionProvider->getClass('Foo')), + new UnionType([ + new ObjectType(\Test\Foo::class), + new ObjectType(FirstInterface::class), + ]), + new HasMethodType('__toString'), ], - StaticType::class, - 'static(Foo)', + UnionType::class, + '(Test\FirstInterface&hasMethod(__toString))|(Test\Foo&hasMethod(__toString))', ], [ [ - new VoidType(), - new MixedType(), + new ObjectType(\Test\Foo::class), + new HasPropertyType('fooProperty'), ], - VoidType::class, - 'void', + IntersectionType::class, + 'Test\Foo&hasProperty(fooProperty)', ], [ [ - new ObjectType('UnknownClass'), - new ObjectType('UnknownClass'), + new ObjectType(FinalFoo::class), + new HasPropertyType('fooProperty'), + ], + PHP_VERSION_ID < 80200 ? IntersectionType::class : NeverType::class, + PHP_VERSION_ID < 80200 ? 'DynamicProperties\FinalFoo&hasProperty(fooProperty)' : '*NEVER*=implicit', + ], + [ + [ + new ObjectType(ClassWithNullableProperty::class), + new HasPropertyType('foo'), ], ObjectType::class, - 'UnknownClass', + 'Test\ClassWithNullableProperty', ], [ [ - new UnionType([new ObjectType('UnknownClassA'), new ObjectType('UnknownClassB')]), - new UnionType([new ObjectType('UnknownClassA'), new ObjectType('UnknownClassB')]), + new ObjectType(FinalClassWithPropertyExists::class), + new HasPropertyType('barProperty'), + ], + IntersectionType::class, + 'CheckTypeFunctionCall\FinalClassWithPropertyExists&hasProperty(barProperty)', + ], + [ + [ + new ObjectWithoutClassType(), + new HasPropertyType('fooProperty'), + ], + IntersectionType::class, + 'object&hasProperty(fooProperty)', + ], + [ + [ + new IntegerType(), + new HasPropertyType('fooProperty'), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType('fooProperty'), + ]), + new HasPropertyType('fooProperty'), + ], + IntersectionType::class, + 'object&hasProperty(fooProperty)', + ], + [ + [ + new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType('foo'), + ]), + new HasPropertyType('bar'), + ], + IntersectionType::class, + 'object&hasProperty(bar)&hasProperty(foo)', + ], + [ + [ + new UnionType([ + new ObjectType(\Test\Foo::class), + new ObjectType(FirstInterface::class), + ]), + new HasPropertyType('fooProperty'), ], UnionType::class, - 'UnknownClassA|UnknownClassB', + '(Test\FirstInterface&hasProperty(fooProperty))|(Test\Foo&hasProperty(fooProperty))', ], [ [ - new ConstantBooleanType(true), - new BooleanType(), + new UnionType([ + new ObjectType(FinalFoo::class), + new ObjectType(FirstInterface::class), + ]), + new HasPropertyType('fooProperty'), ], - ConstantBooleanType::class, - 'true', + PHP_VERSION_ID < 80200 ? UnionType::class : IntersectionType::class, + PHP_VERSION_ID < 80200 ? '(DynamicProperties\FinalFoo&hasProperty(fooProperty))|(Test\FirstInterface&hasProperty(fooProperty))' : 'Test\FirstInterface&hasProperty(fooProperty)', ], [ [ - new ConstantBooleanType(false), - new BooleanType(), + new ArrayType(new StringType(), new StringType()), + new HasOffsetType(new ConstantStringType('a')), ], - ConstantBooleanType::class, - 'false', + IntersectionType::class, + 'non-empty-array&hasOffset(\'a\')', ], [ [ - StaticTypeFactory::truthy(), - new BooleanType(), + new ArrayType(new StringType(), new StringType()), + new HasOffsetType(new ConstantStringType('a')), + new HasOffsetType(new ConstantStringType('a')), ], - ConstantBooleanType::class, - 'true', + IntersectionType::class, + 'non-empty-array&hasOffset(\'a\')', ], [ [ - StaticTypeFactory::falsey(), - new BooleanType(), + new ConstantArrayType( + [new ConstantStringType('a')], + [new ConstantStringType('foo')], + ), + new HasOffsetType(new ConstantStringType('a')), ], - ConstantBooleanType::class, - 'false', + ConstantArrayType::class, + 'array{a: \'foo\'}', ], [ [ - StaticTypeFactory::falsey(), - StaticTypeFactory::truthy(), + new ConstantArrayType( + [new ConstantStringType('a')], + [new ConstantStringType('foo')], + ), + new HasOffsetType(new ConstantStringType('b')), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new StringType(), - new NeverType(), + new ClosureType([], new MixedType(), false), + new HasOffsetType(new ConstantStringType('a')), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new ObjectType('Iterator'), - new ObjectType('Countable'), - new ObjectType('Traversable'), + TypeCombinator::union( + new ConstantArrayType( + [new ConstantStringType('a')], + [new ConstantStringType('foo')], + ), + new ConstantArrayType( + [new ConstantStringType('b')], + [new ConstantStringType('foo')], + ), + ), + new HasOffsetType(new ConstantStringType('b')), ], - IntersectionType::class, - 'Countable&Iterator', + ConstantArrayType::class, + 'array{b: \'foo\'}', ], [ [ - new ObjectType('Iterator'), - new ObjectType('Traversable'), - new ObjectType('Countable'), + TypeCombinator::union( + new ConstantArrayType( + [new ConstantStringType('a')], + [new ConstantStringType('foo')], + ), + new ClosureType([], new MixedType(), false), + ), + new HasOffsetType(new ConstantStringType('a')), + ], + ConstantArrayType::class, + 'array{a: \'foo\'}', + ], + [ + [ + new ClosureType([], new MixedType(), false), + new ObjectType(Closure::class), + ], + ClosureType::class, + 'Closure(): mixed', + ], + [ + [ + new ClosureType([], new MixedType(), false), + new CallableType(), + ], + ClosureType::class, + 'Closure(): mixed', + ], + [ + [ + new ClosureType([], new MixedType(), false), + new ObjectWithoutClassType(), + ], + ClosureType::class, + 'Closure(): mixed', + ], + [ + [ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), ], IntersectionType::class, - 'Countable&Iterator', + 'non-empty-array', ], [ [ - new ObjectType('Traversable'), - new ObjectType('Iterator'), - new ObjectType('Countable'), + new StringType(), + new NonEmptyArrayType(), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + new NonEmptyArrayType(), ], IntersectionType::class, - 'Countable&Iterator', + 'non-empty-array', ], [ [ - new IterableType(new MixedType(), new MixedType()), - new IterableType(new MixedType(), new StringType()), + TypeCombinator::union( + new ConstantArrayType([], []), + new ConstantArrayType([ + new ConstantIntegerType(0), + ], [ + new StringType(), + ]), + ), + new NonEmptyArrayType(), ], - IterableType::class, - 'iterable', + ConstantArrayType::class, + 'array{string}', ], [ [ - new ArrayType(new MixedType(), new MixedType()), - new IterableType(new MixedType(), new StringType()), + new ConstantArrayType([], []), + new NonEmptyArrayType(), ], - ArrayType::class, - 'array', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new ArrayType(new IntegerType(), new MixedType()), - new IterableType(new MixedType(), new StringType()), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('foo')), + ]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('bar')), + ]), ], - ArrayType::class, - 'array', + IntersectionType::class, + 'non-empty-array&hasOffset(\'bar\')&hasOffset(\'foo\')', ], [ [ - new ArrayType(new IntegerType(), new MixedType()), - new IterableType(new StringType(), new MixedType()), + new StringType(), + new IntegerType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new ArrayType(new MixedType(), new IntegerType()), - new IterableType(new MixedType(), new StringType()), + new MixedType(false, new StringType()), + new StringType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new ArrayType(new IntegerType(), new MixedType()), - new ArrayType(new MixedType(), new StringType()), + new MixedType(false, new StringType()), + new ConstantStringType('foo'), ], - ArrayType::class, - 'array', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new IterableType(new IntegerType(), new MixedType()), - new IterableType(new MixedType(), new StringType()), + new MixedType(false, new StringType()), + new ConstantIntegerType(1), ], - IterableType::class, - 'iterable', + ConstantIntegerType::class, + '1', ], [ [ - new MixedType(), - new IterableType(new MixedType(), new MixedType()), + new MixedType(false, new StringType()), + new MixedType(false, new IntegerType()), ], - IterableType::class, - 'iterable', + MixedType::class, + 'mixed~(int|string)=implicit', ], [ [ - new IntegerType(), - new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new ObjectType('DateTime'), ], - IntegerType::class, - 'int', + IntersectionType::class, + 'DateTime&T (function a(), parameter)', ], [ [ - new ConstantIntegerType(1), - new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + new ObjectType('DateTime'), ], - ConstantIntegerType::class, - '1', + TemplateObjectType::class, + 'T of DateTime (function a(), parameter)', ], [ [ - new ConstantStringType('foo'), - new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), ], - ConstantStringType::class, - '\'foo\'', + TemplateType::class, + 'T of DateTime (function a(), parameter)', ], [ [ - new StringType(), - new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'U', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), ], - StringType::class, - 'string', + IntersectionType::class, + 'T of DateTime (function a(), parameter)&U of DateTime (function a(), parameter)', ], [ [ - new UnionType([new StringType(), new IntegerType()]), - new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new MixedType(), ], - UnionType::class, - '(int|string)', + TemplateType::class, + 'T (function a(), parameter)=explicit', ], [ [ - new ObjectType(\Test\Foo::class), - new HasMethodType('__toString'), + new StringType(), + new ClassStringType(), ], - IntersectionType::class, - 'Test\Foo&hasMethod(__toString)', + ClassStringType::class, + 'class-string', ], [ [ - new ObjectType(ClassWithToString::class), - new HasMethodType('__toString'), + new ClassStringType(), + new ConstantStringType(stdClass::class), ], - ObjectType::class, - 'Test\ClassWithToString', + ConstantStringType::class, + '\'stdClass\'', ], [ [ - new ObjectType(FinalClassWithMethodExists::class), - new HasMethodType('doBar'), + new ClassStringType(), + new ConstantStringType('Nonexistent'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new ObjectWithoutClassType(), - new HasMethodType('__toString'), + new ClassStringType(), + new IntegerType(), ], - IntersectionType::class, - 'object&hasMethod(__toString)', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new IntegerType(), - new HasMethodType('__toString'), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), ], - NeverType::class, - '*NEVER*', + ConstantStringType::class, + '\'Exception\'', ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasMethodType('__toString'), - ]), - new HasMethodType('__toString'), + new GenericClassStringType(new ObjectType(Exception::class)), + new ClassStringType(), ], - IntersectionType::class, - 'object&hasMethod(__toString)', + GenericClassStringType::class, + 'class-string', ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasMethodType('foo'), - ]), - new HasMethodType('bar'), + new GenericClassStringType(new ObjectType(Exception::class)), + new StringType(), ], - IntersectionType::class, - 'object&hasMethod(bar)&hasMethod(foo)', + GenericClassStringType::class, + 'class-string', ], [ [ - new UnionType([ - new ObjectType(\Test\Foo::class), - new ObjectType(FirstInterface::class), - ]), - new HasMethodType('__toString'), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), ], - UnionType::class, - '(Test\FirstInterface&hasMethod(__toString))|(Test\Foo&hasMethod(__toString))', + GenericClassStringType::class, + 'class-string', ], [ [ - new ObjectType(\Test\Foo::class), - new HasPropertyType('fooProperty'), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Throwable::class)), ], - IntersectionType::class, - 'Test\Foo&hasProperty(fooProperty)', + GenericClassStringType::class, + 'class-string', ], [ [ - new ObjectType(ClassWithNullableProperty::class), - new HasPropertyType('foo'), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), ], - ObjectType::class, - 'Test\ClassWithNullableProperty', + GenericClassStringType::class, + 'class-string', ], [ [ - new ObjectType(FinalClassWithPropertyExists::class), - new HasPropertyType('barProperty'), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new ObjectWithoutClassType(), - new HasPropertyType('fooProperty'), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), ], - IntersectionType::class, - 'object&hasProperty(fooProperty)', + ConstantStringType::class, + '\'Exception\'', ], [ [ - new IntegerType(), - new HasPropertyType('fooProperty'), + new GenericClassStringType(new ObjectType(Throwable::class)), + new ConstantStringType(Exception::class), ], - NeverType::class, - '*NEVER*', + ConstantStringType::class, + '\'Exception\'', ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType('fooProperty'), - ]), - new HasPropertyType('fooProperty'), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new ConstantStringType(Exception::class), ], IntersectionType::class, - 'object&hasProperty(fooProperty)', + "'Exception'&class-string", ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType('foo'), - ]), - new HasPropertyType('bar'), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(stdClass::class), ], - IntersectionType::class, - 'object&hasProperty(bar)&hasProperty(foo)', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new UnionType([ - new ObjectType(\Test\Foo::class), - new ObjectType(FirstInterface::class), - ]), - new HasPropertyType('fooProperty'), + IntegerRangeType::fromInterval(1, 3), + IntegerRangeType::fromInterval(2, 5), ], - UnionType::class, - '(Test\FirstInterface&hasProperty(fooProperty))|(Test\Foo&hasProperty(fooProperty))', + IntegerRangeType::class, + 'int<2, 3>', ], [ [ - new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new ConstantStringType('a')), + IntegerRangeType::fromInterval(1, 3), + IntegerRangeType::fromInterval(3, 5), ], - IntersectionType::class, - 'array&hasOffset(\'a\')', + ConstantIntegerType::class, + '3', ], [ [ - new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new ConstantStringType('a')), - new HasOffsetType(new ConstantStringType('a')), + IntegerRangeType::fromInterval(1, 3), + IntegerRangeType::fromInterval(7, 9), ], - IntersectionType::class, - 'array&hasOffset(\'a\')', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new StringType()), - new HasOffsetType(new StringType()), + IntegerRangeType::fromInterval(1, 3), + new ConstantIntegerType(3), ], - IntersectionType::class, - 'array&hasOffset(string)', + ConstantIntegerType::class, + '3', ], [ [ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new StringType()), - new HasOffsetType(new StringType()), + IntegerRangeType::fromInterval(1, 3), + new ConstantIntegerType(4), ], - IntersectionType::class, - 'array&hasOffset(string)', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new ConstantArrayType( - [new ConstantStringType('a')], - [new ConstantStringType('foo')], - ), - new HasOffsetType(new ConstantStringType('a')), + IntegerRangeType::fromInterval(1, 3), + new IntegerType(), ], - ConstantArrayType::class, - 'array{a: \'foo\'}', + IntegerRangeType::class, + 'int<1, 3>', ], [ [ - new ConstantArrayType( - [new ConstantStringType('a')], - [new ConstantStringType('foo')], - ), - new HasOffsetType(new ConstantStringType('b')), + new ObjectType(Traversable::class), + new IterableType(new MixedType(), new MixedType()), ], - NeverType::class, - '*NEVER*', + ObjectType::class, + 'Traversable', ], [ [ - new ClosureType([], new MixedType(), false), - new HasOffsetType(new ConstantStringType('a')), + new ObjectType(Traversable::class), + new IterableType(new MixedType(), new MixedType()), ], - NeverType::class, - '*NEVER*', + ObjectType::class, + 'Traversable', ], [ [ - TypeCombinator::union( - new ConstantArrayType( - [new ConstantStringType('a')], - [new ConstantStringType('foo')], - ), - new ConstantArrayType( - [new ConstantStringType('b')], - [new ConstantStringType('foo')], - ), - ), - new HasOffsetType(new ConstantStringType('b')), + new ObjectType(Traversable::class), + new IterableType(new MixedType(), new MixedType(true)), ], - ConstantArrayType::class, - 'array{b: \'foo\'}', + IntersectionType::class, + 'iterable&Traversable', ], [ [ - TypeCombinator::union( - new ConstantArrayType( - [new ConstantStringType('a')], - [new ConstantStringType('foo')], - ), - new ClosureType([], new MixedType(), false), - ), - new HasOffsetType(new ConstantStringType('a')), + new ObjectType(Traversable::class), + new IterableType(new MixedType(true), new MixedType()), ], - ConstantArrayType::class, - 'array{a: \'foo\'}', + IntersectionType::class, + 'iterable&Traversable', ], [ [ - new ClosureType([], new MixedType(), false), - new ObjectType(Closure::class), + new ObjectType(Traversable::class), + new IterableType(new MixedType(true), new MixedType(true)), ], - ClosureType::class, - 'Closure(): mixed', + IntersectionType::class, + 'iterable&Traversable', ], [ [ - new ClosureType([], new MixedType(), false), - new CallableType(), + new MixedType(), + new MixedType(), ], - ClosureType::class, - 'Closure(): mixed', + MixedType::class, + 'mixed=implicit', ], [ [ - new ClosureType([], new MixedType(), false), - new ObjectWithoutClassType(), + new MixedType(true), + new MixedType(), ], - ClosureType::class, - 'Closure(): mixed', + MixedType::class, + 'mixed=explicit', ], [ [ - new UnionType([ - new ArrayType(new MixedType(), new StringType()), - new NullType(), - ]), - new HasOffsetType(new StringType()), + new MixedType(true), + new MixedType(true), ], - IntersectionType::class, - 'array&hasOffset(string)', + MixedType::class, + 'mixed=explicit', ], [ [ - new ArrayType(new MixedType(), new MixedType()), - new NonEmptyArrayType(), + new GenericObjectType(Variance\Covariant::class, [ + new ObjectType(DateTimeInterface::class), + ]), + new GenericObjectType(Variance\Covariant::class, [ + new ObjectType(DateTime::class), + ]), ], - IntersectionType::class, - 'non-empty-array', + GenericObjectType::class, + 'PHPStan\Type\Variance\Covariant', ], [ [ - new StringType(), - new NonEmptyArrayType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new ObjectWithoutClassType(), ], - NeverType::class, - '*NEVER*', + TemplateObjectWithoutClassType::class, + 'T of object (function a(), parameter)', ], [ [ - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new NonEmptyArrayType(), - ]), - new NonEmptyArrayType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new ObjectType(stdClass::class), ], IntersectionType::class, - 'non-empty-array', + 'stdClass&T of object (function a(), parameter)', ], [ [ - TypeCombinator::union( - new ConstantArrayType([], []), - new ConstantArrayType([ - new ConstantIntegerType(0), - ], [ - new StringType(), - ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), ), - new NonEmptyArrayType(), + new MixedType(), ], - ConstantArrayType::class, - 'array{string}', + TemplateObjectWithoutClassType::class, + 'T of object (function a(), parameter)', ], [ [ - new ConstantArrayType([], []), - new NonEmptyArrayType(), + new ConstantStringType('NonexistentClass'), + new ClassStringType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new ConstantStringType('foo')), - ]), - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new ConstantStringType('bar')), - ]), + new ConstantStringType(stdClass::class), + new ClassStringType(), + ], + ConstantStringType::class, + '\'stdClass\'', + ], + [ + [ + new ObjectType(DateTimeInterface::class), + new ObjectType(Iterator::class), ], IntersectionType::class, - 'array&hasOffset(\'bar\')&hasOffset(\'foo\')', + 'DateTimeInterface&Iterator', ], [ [ - new StringType(), - new IntegerType(), + new ObjectType(DateTimeInterface::class), + new GenericObjectType(Iterator::class, [new MixedType(), new MixedType()]), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'DateTimeInterface&Iterator', ], [ [ - new MixedType(false, new StringType()), - new StringType(), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0]), + new HasOffsetType(new ConstantStringType('a')), ], - NeverType::class, - '*NEVER*', + ConstantArrayType::class, + 'array{a: int, b: int}', ], [ [ - new MixedType(false, new StringType()), - new ConstantStringType('foo'), + new BenevolentUnionType([new IntegerType(), new StringType()]), + new MixedType(), ], - NeverType::class, - '*NEVER*', + BenevolentUnionType::class, + '(int|string)', ], [ [ - new MixedType(false, new StringType()), - new ConstantIntegerType(1), + new ConstantStringType('abc'), + new AccessoryNumericStringType(), ], - ConstantIntegerType::class, - '1', + NeverType::class, + '*NEVER*=implicit', ], [ [ - new MixedType(false, new StringType()), - new MixedType(false, new IntegerType()), + new ConstantStringType('123'), + new AccessoryNumericStringType(), ], - MixedType::class, - 'mixed~int|string=implicit', + ConstantStringType::class, + '\'123\'', ], [ [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - null, - TemplateTypeVariance::createInvariant(), - ), - new ObjectType('DateTime'), + new StringType(), + new AccessoryNumericStringType(), + ], + IntersectionType::class, + 'numeric-string', + ], + [ + [ + new IntegerType(), + new AccessoryNumericStringType(), ], - IntersectionType::class, - 'DateTime&T (function a(), parameter)', + NeverType::class, + '*NEVER*=implicit', ], [ [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant(), - ), - new ObjectType('DateTime'), + new IntersectionType([ + new ArrayType(new StringType(), new IntegerType()), + new NonEmptyArrayType(), + ]), + new NeverType(), ], - TemplateObjectType::class, - 'T of DateTime (function a(), parameter)', + NeverType::class, + '*NEVER*=implicit', ], [ [ TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant(), - ), - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), + TemplateTypeScope::createWithFunction('my_array_keys'), 'T', - new ObjectType('DateTime'), + new BenevolentUnionType([new IntegerType(), new StringType()]), TemplateTypeVariance::createInvariant(), ), + new UnionType([new IntegerType(), new StringType()]), ], - TemplateType::class, - 'T of DateTime (function a(), parameter)', + TemplateBenevolentUnionType::class, + 'T of (int|string) (function my_array_keys(), parameter)', ], [ [ TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), + TemplateTypeScope::createWithFunction('my_array_keys'), 'T', - new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant(), - ), - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'U', - new ObjectType('DateTime'), + new BenevolentUnionType([new IntegerType(), new StringType()]), TemplateTypeVariance::createInvariant(), ), + new BenevolentUnionType([new IntegerType(), new StringType()]), ], - IntersectionType::class, - 'T of DateTime (function a(), parameter)&U of DateTime (function a(), parameter)', + TemplateBenevolentUnionType::class, + 'T of (int|string) (function my_array_keys(), parameter)', ], [ [ TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), + TemplateTypeScope::createWithFunction('my_array_keys'), 'T', - null, + new UnionType([new IntegerType(), new StringType()]), TemplateTypeVariance::createInvariant(), ), - new MixedType(), + new UnionType([new IntegerType(), new StringType()]), ], - TemplateType::class, - 'T (function a(), parameter)=explicit', + UnionType::class, + 'T of int|string (function my_array_keys(), parameter)', ], [ [ - new StringType(), - new ClassStringType(), + new MixedType(), + new StrictMixedType(), ], - ClassStringType::class, - 'class-string', + StrictMixedType::class, + 'mixed', ], [ [ - new ClassStringType(), - new ConstantStringType(stdClass::class), + new NeverType(true), + new IntegerType(), ], - ConstantStringType::class, - '\'stdClass\'', + NeverType::class, + '*NEVER*=explicit', ], [ [ - new ClassStringType(), - new ConstantStringType('Nonexistent'), + new NeverType(), + new IntegerType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new ClassStringType(), - new IntegerType(), + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'lowercase-string', ], [ [ - new ConstantStringType(Exception::class), - new GenericClassStringType(new ObjectType(Exception::class)), + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - ConstantStringType::class, - '\'Exception\'', + IntersectionType::class, + 'lowercase-string&numeric-string', ], [ [ - new GenericClassStringType(new ObjectType(Exception::class)), - new ClassStringType(), + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - GenericClassStringType::class, - 'class-string', + IntersectionType::class, + 'lowercase-string&non-falsy-string', ], [ [ - new GenericClassStringType(new ObjectType(Exception::class)), - new StringType(), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - GenericClassStringType::class, - 'class-string', + IntersectionType::class, + 'lowercase-string&non-empty-string', ], [ [ - new GenericClassStringType(new ObjectType(Exception::class)), - new GenericClassStringType(new ObjectType(Exception::class)), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - GenericClassStringType::class, - 'class-string', + IntersectionType::class, + 'literal-string&lowercase-string', ], [ [ - new GenericClassStringType(new ObjectType(Exception::class)), - new GenericClassStringType(new ObjectType(Throwable::class)), + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - GenericClassStringType::class, - 'class-string', + IntersectionType::class, + 'uppercase-string', ], [ [ - new GenericClassStringType(new ObjectType(Exception::class)), - new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - GenericClassStringType::class, - 'class-string', + IntersectionType::class, + 'numeric-string&uppercase-string', ], [ [ - new GenericClassStringType(new ObjectType(Exception::class)), - new GenericClassStringType(new ObjectType(stdClass::class)), + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'non-falsy-string&uppercase-string', ], [ [ - new GenericClassStringType(new ObjectType(Exception::class)), - new ConstantStringType(Exception::class), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - ConstantStringType::class, - '\'Exception\'', + IntersectionType::class, + 'non-empty-string&uppercase-string', ], [ [ - new GenericClassStringType(new ObjectType(Throwable::class)), - new ConstantStringType(Exception::class), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - ConstantStringType::class, - '\'Exception\'', + IntersectionType::class, + 'literal-string&uppercase-string', ], [ [ - new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), - new ConstantStringType(Exception::class), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'lowercase-string&uppercase-string', + ], + ]; + + 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*=implicit', + ]; + 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*=implicit', + ]; + 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*=implicit', + ]; + yield [ + [ + new ObjectType(FinalClass::class), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + 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*=implicit', + ]; + yield [ + [ + new MixedType(false, IntegerRangeType::fromInterval(17, null)), + new MixedType(), + ], + MixedType::class, + 'mixed~int<17, max>=implicit', + ]; + yield [ + [ + new StaticType($reflectionProvider->getClass(stdClass::class)), + new ThisType($reflectionProvider->getClass(stdClass::class)), + ], + ThisType::class, + '$this(stdClass)', + ]; + + yield [ + [ + new StaticType($reflectionProvider->getClass(stdClass::class)), + new ObjectType(stdClass::class), + ], + StaticType::class, + 'static(stdClass)', + ]; + + yield [ + [ + new StaticType($reflectionProvider->getClass(stdClass::class)), + new EnumCaseObjectType(stdClass::class, 'foo'), + ], + IntersectionType::class, + 'static(stdClass)&stdClass::foo', + ]; + + yield [ + [ + new ThisType($reflectionProvider->getClass(stdClass::class)), + new EnumCaseObjectType(stdClass::class, 'foo'), ], + IntersectionType::class, + '$this(stdClass)&stdClass::foo', + ]; + + yield [ [ - [ - new GenericClassStringType(new ObjectType(Exception::class)), - new ConstantStringType(stdClass::class), - ], - NeverType::class, - '*NEVER*', + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new MixedType(false, new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A')), ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - IntegerRangeType::fromInterval(2, 5), - ], - IntegerRangeType::class, - 'int<2, 3>', + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + ]), + new MixedType(false, new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A')), ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\ManyCasesTestEnum::B', + ]; + + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - IntegerRangeType::fromInterval(3, 5), - ], - ConstantIntegerType::class, - '3', + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), ], + IntersectionType::class, + 'non-falsy-string', + ]; + + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - IntegerRangeType::fromInterval(7, 9), - ], - NeverType::class, - '*NEVER*', + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), + new ConstantStringType('0'), ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - new ConstantIntegerType(3), - ], - ConstantIntegerType::class, - '3', + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + new ConstantStringType('0'), ], + ConstantStringType::class, + "'0'", + ]; + + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - new ConstantIntegerType(4), - ], - NeverType::class, - '*NEVER*', + new HasOffsetValueType(new ConstantStringType('a'), new ConstantIntegerType(1)), + new HasOffsetValueType(new ConstantStringType('a'), new IntegerType()), ], + HasOffsetValueType::class, + 'hasOffsetValue(\'a\', 1)', + ]; + + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - new IntegerType(), - ], - IntegerRangeType::class, - 'int<1, 3>', + TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new HasOffsetValueType(new ConstantStringType('a'), new ConstantIntegerType(1))), + TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new HasOffsetValueType(new ConstantStringType('a'), new IntegerType())), ], + IntersectionType::class, + 'non-empty-array&hasOffsetValue(\'a\', 1)', + ]; + yield [ [ - [ - new ObjectType(Traversable::class), - new IterableType(new MixedType(), new MixedType()), - ], - ObjectType::class, - 'Traversable', + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new OversizedArrayType()]), + new ArrayType(new IntegerType(), new StringType()), ], + IntersectionType::class, + 'array&oversized-array', + ]; + yield [ [ - [ - new ObjectType(Traversable::class), - new IterableType(new MixedType(), new MixedType()), - ], - ObjectType::class, - 'Traversable', + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - new ObjectType(Traversable::class), - new IterableType(new MixedType(), new MixedType(true)), - ], - IntersectionType::class, - 'iterable&Traversable', + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - new ObjectType(Traversable::class), - new IterableType(new MixedType(true), new MixedType()), - ], - IntersectionType::class, - 'iterable&Traversable', + new ObjectShapeType([], []), + new ObjectWithoutClassType(), ], + ObjectShapeType::class, + 'object{}', + ]; + yield [ [ - [ - new ObjectType(Traversable::class), - new IterableType(new MixedType(true), new MixedType(true)), - ], - IntersectionType::class, - 'iterable&Traversable', + new ObjectShapeType([], []), + new ObjectType(stdClass::class), ], + IntersectionType::class, + 'object{}&stdClass', + ]; + yield [ [ - [ - new MixedType(), - new MixedType(), - ], - MixedType::class, - 'mixed=implicit', + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('foo'), ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ [ - [ - new MixedType(true), - new MixedType(), - ], - MixedType::class, - 'mixed=explicit', + new ObjectShapeType(['foo' => new IntegerType()], ['foo']), + new HasPropertyType('foo'), ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ [ - [ - new MixedType(true), - new MixedType(true), - ], - MixedType::class, - 'mixed=explicit', + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('bar'), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - new GenericObjectType(Variance\Covariant::class, [ - new ObjectType(DateTimeInterface::class), - ]), - new GenericObjectType(Variance\Covariant::class, [ - new ObjectType(DateTime::class), - ]), - ], - GenericObjectType::class, - 'PHPStan\Type\Variance\Covariant', + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new IntegerType()], []), ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant(), - ), - new ObjectWithoutClassType(), - ], - TemplateObjectWithoutClassType::class, - 'T of object (function a(), parameter)', + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new ConstantIntegerType(1)], []), ], + ObjectShapeType::class, + 'object{foo: 1}', + ]; + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant(), - ), - new ObjectType(stdClass::class), - ], - IntersectionType::class, - 'stdClass&T of object (function a(), parameter)', + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new StringType()], []), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant(), - ), - new MixedType(), - ], - TemplateObjectWithoutClassType::class, - 'T of object (function a(), parameter)', + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['bar' => new StringType()], []), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(Traversable::class), ], + IntersectionType::class, + 'object{foo: int}&Traversable', + ]; + yield [ [ - [ - new ConstantStringType('NonexistentClass'), - new ClassStringType(), - ], - NeverType::class, - '*NEVER*', + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\Foo::class), ], + IntersectionType::class, + 'ObjectShapesAcceptance\Foo&object{foo: int}', + ]; + yield [ [ - [ - new ConstantStringType(stdClass::class), - new ClassStringType(), - ], - ConstantStringType::class, - '\'stdClass\'', + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(ClassWithFooIntProperty::class), ], + ObjectType::class, + 'ObjectShapesAcceptance\ClassWithFooIntProperty', + ]; + yield [ [ - [ - new ObjectType(DateTimeInterface::class), - new ObjectType(Iterator::class), - ], - IntersectionType::class, - 'DateTimeInterface&Iterator', + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\FinalClass::class), ], + PHP_VERSION_ID < 80200 ? IntersectionType::class : NeverType::class, + PHP_VERSION_ID < 80200 ? 'ObjectShapesAcceptance\FinalClass&object{foo: int}' : '*NEVER*=implicit', + ]; + yield [ [ - [ - new ObjectType(DateTimeInterface::class), - new GenericObjectType(Iterator::class, [new MixedType(), new MixedType()]), - ], - IntersectionType::class, - 'DateTimeInterface&Iterator', + new NeverType(true), + new NonAcceptingNeverType(), ], + NonAcceptingNeverType::class, + 'never=explicit', + ]; + yield [ [ - [ + new UnionType([ + new ConstantArrayType([], []), new ConstantArrayType([ new ConstantStringType('a'), new ConstantStringType('b'), ], [ - new IntegerType(), - new IntegerType(), - ], 2, [0]), - new HasOffsetType(new ConstantStringType('a')), - ], - ConstantArrayType::class, - 'array{a: int, b: int}', - ], - [ - [ - new StringType(), - new HasOffsetType(new IntegerType()), - ], - IntersectionType::class, - 'string&hasOffset(int)', - ], - [ - [ - new BenevolentUnionType([new IntegerType(), new StringType()]), - new MixedType(), - ], - BenevolentUnionType::class, - '(int|string)', + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + ]), + new NonEmptyArrayType(), ], + UnionType::class, + 'array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', + ]; + yield [ [ - [ - new ConstantStringType('abc'), - new AccessoryNumericStringType(), - ], - NeverType::class, - '*NEVER*', + new ConstantArrayType([], []), + new NonEmptyArrayType(), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - new ConstantStringType('123'), - new AccessoryNumericStringType(), - ], - ConstantStringType::class, - '\'123\'', + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new NonEmptyArrayType(), ], + ConstantArrayType::class, + 'array{a?: true, b: true}', + ]; + yield [ [ - [ - new StringType(), - new AccessoryNumericStringType(), - ], - IntersectionType::class, - 'numeric-string', + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + new NonEmptyArrayType(), ], + IntersectionType::class, + 'non-empty-array{a?: true, c?: true}', + ]; + yield [ [ - [ - new IntegerType(), - new AccessoryNumericStringType(), - ], - NeverType::class, - '*NEVER*', + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(), ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ [ - [ - new IntersectionType([ - new ArrayType(new StringType(), new IntegerType()), - new NonEmptyArrayType(), - ]), - new NeverType(), - ], - NeverType::class, - '*NEVER*', + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ClosureType::createPure(), ], + ClosureType::class, + 'pure-Closure', + ]; + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('my_array_keys'), - 'T', - new BenevolentUnionType([new IntegerType(), new StringType()]), - TemplateTypeVariance::createInvariant(), - ), - new UnionType([new IntegerType(), new StringType()]), - ], - TemplateBenevolentUnionType::class, - 'T of (int|string) (function my_array_keys(), parameter)', + new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('my_array_keys'), - 'T', - new BenevolentUnionType([new IntegerType(), new StringType()]), - TemplateTypeVariance::createInvariant(), - ), - new BenevolentUnionType([new IntegerType(), new StringType()]), - ], - TemplateBenevolentUnionType::class, - 'T of (int|string) (function my_array_keys(), parameter)', + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + ClosureType::createPure(), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('my_array_keys'), - 'T', - new UnionType([new IntegerType(), new StringType()]), - TemplateTypeVariance::createInvariant(), - ), - new UnionType([new IntegerType(), new StringType()]), - ], - UnionType::class, - 'T of int|string (function my_array_keys(), parameter)', + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', false), + ]), + ClosureType::createPure(), ], + ClosureType::class, + 'pure-Closure', + ]; + + $xy = new ConstantArrayType([ + new ConstantIntegerType(0), + ], [ + new ConstantStringType('xy'), + ]); + $abxy = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new ConstantStringType('ab'), + new ConstantStringType('xy'), + ], [2], [1]); + + yield [ [ - [ - new MixedType(), - new StrictMixedType(), - ], - StrictMixedType::class, - 'mixed', + new UnionType([ + new ConstantArrayType([], []), + $xy, + $abxy, + ]), + new UnionType([ + $xy, + $abxy, + ]), ], + UnionType::class, + "array{'xy'}|array{0: 'ab', 1?: 'xy'}", ]; - if (PHP_VERSION_ID < 80100) { - return; - } - yield [ [ - new ObjectType('PHPStan\Fixture\TestEnum'), - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ConstantArrayType([], []), + new UnionType([ + $xy, + $abxy, + ]), ], - EnumCaseObjectType::class, - 'PHPStan\Fixture\TestEnum::ONE', + NeverType::class, + '*NEVER*=implicit', ]; + yield [ [ - new ObjectType('PHPStan\Fixture\TestEnum'), - new EnumCaseObjectType(stdClass::class, 'ONE'), + new ConstantArrayType([], []), + $abxy, ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ]; + yield [ [ - new ObjectType('PHPStan\Fixture\TestEnumInterface'), - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - EnumCaseObjectType::class, - 'PHPStan\Fixture\TestEnum::ONE', + NeverType::class, + '*NEVER*=implicit', ]; yield [ [ - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - EnumCaseObjectType::class, - 'PHPStan\Fixture\TestEnum::ONE', + ConstantStringType::class, + '\'foo\'', ]; + yield [ [ - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ]; yield [ [ - new ObjectType('PHPStan\Fixture\TestEnum'), - new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - ObjectType::class, - 'PHPStan\Fixture\TestEnum', + ConstantStringType::class, + '\'FOO\'', ]; + + $c = $reflectionProvider->getClass(C::class); + yield [ [ - new ObjectType('PHPStan\Fixture\TestEnumInterface'), - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), ], - EnumCaseObjectType::class, - 'PHPStan\Fixture\TestEnum::ONE', + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', ]; + yield [ [ - new ObjectType('PHPStan\Fixture\TestEnumInterface'), - new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + new GenericStaticType($c, [new StringType()], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ]; + yield [ [ - new ObjectType(FinalClass::class), - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new UnionType([ + new IntegerType(), + new StringType(), + ])], null, [TemplateTypeVariance::createCovariant()]), ], - NeverType::class, - '*NEVER*', + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', ]; + + yield [ + [ + new StaticType($c), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + yield [ [ + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), new ObjectWithoutClassType(), - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], - EnumCaseObjectType::class, - 'PHPStan\Fixture\TestEnum::ONE', + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', ]; + yield [ [ - new ObjectType('stdClass'), - new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new ObjectType($c->getName()), ], - NeverType::class, - '*NEVER*', + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', ]; + + $nonFinalClass = $reflectionProvider->getClass(\NullCoalesceIsAlwaysFinal\Foo::class); + $finalClass = $nonFinalClass->asFinal(); + yield [ [ - new MixedType(false, new IntegerRangeType(17, null)), - new MixedType(), + new ObjectType($finalClass->getName(), null, $finalClass), + new ObjectType($nonFinalClass->getName(), null, $nonFinalClass), ], - MixedType::class, - 'mixed~int<17, max>=implicit', + ObjectType::class, + $nonFinalClass->getDisplayName() . '=final', ]; } @@ -3433,6 +4750,25 @@ public function testIntersect( $actualTypeDescription .= '=implicit'; } } + if ($actualType instanceof NeverType) { + if ($actualType->isExplicit()) { + $actualTypeDescription .= '=explicit'; + } else { + $actualTypeDescription .= '=implicit'; + } + } + + if (get_class($actualType) === ObjectType::class && $actualType->isEnum()->no()) { + $actualClassReflection = $actualType->getClassReflection(); + if ( + $actualClassReflection !== null + && $actualClassReflection->hasFinalByKeywordOverride() + && $actualClassReflection->isFinal() + ) { + $actualTypeDescription .= '=final'; + } + } + $this->assertSame($expectedTypeDescription, $actualTypeDescription); $this->assertInstanceOf($expectedTypeClass, $actualType); } @@ -3457,6 +4793,24 @@ public function testIntersectInversed( $actualTypeDescription .= '=implicit'; } } + if ($actualType instanceof NeverType) { + if ($actualType->isExplicit()) { + $actualTypeDescription .= '=explicit'; + } else { + $actualTypeDescription .= '=implicit'; + } + } + + if (get_class($actualType) === ObjectType::class && $actualType->isEnum()->no()) { + $actualClassReflection = $actualType->getClassReflection(); + if ( + $actualClassReflection !== null + && $actualClassReflection->hasFinalByKeywordOverride() + && $actualClassReflection->isFinal() + ) { + $actualTypeDescription .= '=final'; + } + } $this->assertSame($expectedTypeDescription, $actualTypeDescription); $this->assertInstanceOf($expectedTypeClass, $actualType); } @@ -3468,7 +4822,7 @@ public function dataRemove(): array new ConstantBooleanType(true), new ConstantBooleanType(true), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new UnionType([ @@ -3524,13 +4878,13 @@ public function dataRemove(): array new ConstantBooleanType(true), new BooleanType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ConstantBooleanType(false), new BooleanType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new BooleanType(), @@ -3548,25 +4902,25 @@ public function dataRemove(): array new BooleanType(), new BooleanType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ StaticTypeFactory::falsey(), StaticTypeFactory::falsey(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ StaticTypeFactory::truthy(), StaticTypeFactory::truthy(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ StaticTypeFactory::truthy(), StaticTypeFactory::falsey(), MixedType::class, - 'mixed~0|0.0|\'\'|\'0\'|array{}|false|null', + 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)', ], [ StaticTypeFactory::falsey(), @@ -3681,7 +5035,7 @@ public function dataRemove(): array new BenevolentUnionType([new IntegerType(), new StringType()]), new UnionType([new IntegerType(), new StringType()]), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ArrayType(new MixedType(), new MixedType()), @@ -3709,7 +5063,7 @@ public function dataRemove(): array ]), new NonEmptyArrayType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ArrayType(new MixedType(), new MixedType()), @@ -3742,19 +5096,19 @@ public function dataRemove(): array new MixedType(false, new IntegerType()), new StringType(), MixedType::class, - 'mixed~int|string', + 'mixed~(int|string)', ], [ new MixedType(false), new MixedType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new MixedType(false, new StringType()), new MixedType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new MixedType(false), @@ -3778,13 +5132,13 @@ public function dataRemove(): array new ObjectType('Exception', new ObjectType('InvalidArgumentException')), new ObjectType('LengthException'), ObjectType::class, - 'Exception~InvalidArgumentException|LengthException', + 'Exception~(InvalidArgumentException|LengthException)', ], [ new ObjectType('Exception'), new ObjectType('Throwable'), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ObjectType('Exception', new ObjectType('InvalidArgumentException')), @@ -3826,25 +5180,25 @@ public function dataRemove(): array IntegerRangeType::fromInterval(0, 2), IntegerRangeType::fromInterval(-1, 3), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ IntegerRangeType::fromInterval(0, 2), IntegerRangeType::fromInterval(0, 3), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ IntegerRangeType::fromInterval(0, 2), IntegerRangeType::fromInterval(-1, 2), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ IntegerRangeType::fromInterval(0, 2), new IntegerType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ IntegerRangeType::fromInterval(null, 1), @@ -3874,10 +5228,10 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2), + ], [2]), new HasOffsetType(new ConstantIntegerType(1)), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ConstantArrayType([ @@ -3886,7 +5240,7 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2, [1]), + ], [2], [1]), new HasOffsetType(new ConstantIntegerType(1)), ConstantArrayType::class, 'array{string}', @@ -3898,10 +5252,10 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2, [1]), + ], [2], [1]), new HasOffsetType(new ConstantIntegerType(0)), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new MixedType(), @@ -3932,6 +5286,30 @@ public function dataRemove(): array TemplateMixedType::class, // should be TemplateConstantBooleanType 'T (class Foo, parameter)', // should be T of true ], + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('foo'), + NeverType::class, + '*NEVER*=implicit', + ], + [ + new ObjectShapeType(['foo' => new IntegerType()], ['foo']), + new HasPropertyType('foo'), + ObjectShapeType::class, + 'object{}', + ], + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('bar'), + ObjectShapeType::class, + 'object{foo: int}', + ], + [ + new ObjectShapeType(['foo' => new IntegerType()], ['foo']), + new HasPropertyType('bar'), + ObjectShapeType::class, + 'object{foo?: int}', + ], ]; } @@ -3947,7 +5325,15 @@ public function testRemove( ): void { $result = TypeCombinator::remove($fromType, $type); - $this->assertSame($expectedTypeDescription, $result->describe(VerbosityLevel::precise())); + $actualTypeDescription = $result->describe(VerbosityLevel::precise()); + if ($result instanceof NeverType) { + if ($result->isExplicit()) { + $actualTypeDescription .= '=explicit'; + } else { + $actualTypeDescription .= '=implicit'; + } + } + $this->assertSame($expectedTypeDescription, $actualTypeDescription); $this->assertInstanceOf($expectedTypeClass, $result); } @@ -3967,7 +5353,25 @@ 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, 1?: string, 2?: string, 3?: string, 4?: string, test?: string}', $resultType->describe(VerbosityLevel::precise())); + } + + /** + * @dataProvider dataContainsNull + */ + public function testContainsNull( + Type $type, + bool $expectedResult, + ): void + { + $this->assertSame($expectedResult, TypeCombinator::containsNull($type)); + } + + public function dataContainsNull(): iterable + { + yield [new NullType(), true]; + yield [new UnionType([new IntegerType(), new NullType()]), true]; + yield [new MixedType(), false]; } } diff --git a/tests/PHPStan/Type/TypeGetFiniteTypesTest.php b/tests/PHPStan/Type/TypeGetFiniteTypesTest.php new file mode 100644 index 0000000000..9770a62a72 --- /dev/null +++ b/tests/PHPStan/Type/TypeGetFiniteTypesTest.php @@ -0,0 +1,145 @@ + $expectedTypes + */ + public function testGetFiniteTypes( + Type $type, + array $expectedTypes, + ): void + { + $this->assertEquals($expectedTypes, $type->getFiniteTypes()); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + +} diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php new file mode 100644 index 0000000000..c630063120 --- /dev/null +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -0,0 +1,571 @@ +', + ]; + + yield [ + new ArrayType(new IntegerType(), new IntegerType()), + 'array', + ]; + + yield [ + new MixedType(), + 'mixed', + ]; + + yield [ + new ObjectType(stdClass::class), + 'stdClass', + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + new ConstantStringType('baz'), + new ConstantStringType('$ref'), + ], [ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + new ConstantIntegerType(4), + ], [0], [2]), + 'array{foo: 1, bar: 2, baz?: 3, \'$ref\': 4}', + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('1100-RB'), + ], [ + new ConstantIntegerType(1), + ], [0]), + "array{'1100-RB': 1}", + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('Karlovy Vary'), + ], [ + new ConstantIntegerType(1), + ], [0]), + "array{'Karlovy Vary': 1}", + ]; + + yield [ + new ObjectShapeType([ + '1100-RB' => new ConstantIntegerType(1), + ], []), + "object{'1100-RB': 1}", + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + new ConstantStringType('baz'), + ], [0], [2]), + 'array{1: \'foo\', 2: \'bar\', 3?: \'baz\'}', + ]; + + yield [ + new ConstantIntegerType(42), + '42', + ]; + + yield [ + new ConstantFloatType(2.5), + '2.5', + ]; + + yield [ + new ConstantBooleanType(true), + 'true', + ]; + + yield [ + new ConstantBooleanType(false), + 'false', + ]; + + yield [ + new ConstantStringType('foo'), + "'foo'", + ]; + + yield [ + new GenericClassStringType(new ObjectType('stdClass')), + 'class-string', + ]; + + yield [ + new GenericObjectType('stdClass', [ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ]), + 'stdClass<1, 2>', + ]; + + yield [ + new GenericObjectType('stdClass', [ + new StringType(), + new IntegerType(), + new MixedType(), + ], null, null, [ + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createContravariant(), + TemplateTypeVariance::createBivariant(), + ]), + 'stdClass', + ]; + + yield [ + new IterableType(new MixedType(), new MixedType()), + 'iterable', + ]; + + yield [ + new IterableType(new MixedType(), new IntegerType()), + 'iterable', + ]; + + yield [ + new IterableType(new IntegerType(), new IntegerType()), + 'iterable', + ]; + + yield [ + new UnionType([new StringType(), new IntegerType()]), + '(int | string)', + ]; + + yield [ + new UnionType([new IntegerType(), new StringType()]), + '(int | string)', + ]; + + yield [ + new ObjectShapeType([ + 'foo' => new ConstantIntegerType(1), + 'bar' => new StringType(), + 'baz' => new ConstantIntegerType(2), + ], ['baz']), + 'object{foo: 1, bar: string, baz?: 2}', + ]; + + yield [ + new ConditionalType( + new ObjectWithoutClassType(), + new ObjectType('stdClass'), + new IntegerType(), + new StringType(), + false, + ), + '(object is stdClass ? int : string)', + ]; + + yield [ + new ConditionalType( + new ObjectWithoutClassType(), + new ObjectType('stdClass'), + new IntegerType(), + new StringType(), + true, + ), + '(object is not stdClass ? int : string)', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + 'literal-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + 'lowercase-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + 'uppercase-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + 'non-empty-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + 'numeric-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType(), new AccessoryNonEmptyStringType()]), + '(literal-string & non-empty-string)', + ]; + + yield [ + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new NonEmptyArrayType()]), + 'non-empty-array', + ]; + + yield [ + new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()), new AccessoryArrayListType()]), + 'list', + ]; + + yield [ + new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()), new NonEmptyArrayType(), new AccessoryArrayListType()]), + 'non-empty-list', + ]; + + yield [ + new IntersectionType([new ClassStringType(), new AccessoryLiteralStringType()]), + '(class-string & literal-string)', + ]; + + yield [ + new IntersectionType([new GenericClassStringType(new ObjectType('Foo')), new AccessoryLiteralStringType()]), + '(class-string & literal-string)', + ]; + + yield [ + new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), new AccessoryArrayListType()]), + 'list', + ]; + + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ]), + 'non-empty-list', + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ]), + "array{'foo', 'bar'}", + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(2), + ], [ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ]), + "array{0: 'foo', 2: 'bar'}", + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [2], [1]), + "array{0: 'foo', 1?: 'bar'}", + ]; + + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new IntegerType()), + new AccessoryArrayListType(), + ]), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), + new AccessoryArrayListType(), + ]), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType(true)), + new AccessoryArrayListType(), + ]), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + 'non-empty-list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType(true)), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + 'non-empty-list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + 'non-empty-array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + ]), + 'non-empty-array', + ]; + $constantArrayWithOptionalKeys = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new StringType(), + new StringType(), + new StringType(), + new StringType(), + ], [3], [2, 3], TrinaryLogic::createMaybe()); + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + 'list{0: string, 1: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + 'list{0: string, 1: string, 2?: string, 3?: string}', + ]; + + $constantArrayWithAllOptionalKeys = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new StringType(), + new StringType(), + new StringType(), + new StringType(), + ], [3], [0, 1, 2, 3], TrinaryLogic::createMaybe()); + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new AccessoryArrayListType(), + ]), + 'list{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ]), + 'non-empty-list{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new NonEmptyArrayType(), + ]), + 'non-empty-array{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + } + + /** + * @dataProvider dataToPhpDocNode + */ + public function testToPhpDocNode(Type $type, string $expected): void + { + $phpDocNode = $type->toPhpDocNode(); + + $typeString = (string) $phpDocNode; + $this->assertSame($expected, $typeString); + + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $parsedType = $typeStringResolver->resolve($typeString); + $this->assertTrue($type->equals($parsedType), sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $parsedType->describe(VerbosityLevel::precise()))); + } + + public function dataToPhpDocNodeWithoutCheckingEquals(): iterable + { + yield [ + new ConstantStringType("foo\nbar\nbaz"), + '(literal-string & lowercase-string & non-falsy-string)', + ]; + + yield [ + new ConstantStringType("FOO\nBAR\nBAZ"), + '(literal-string & non-falsy-string & uppercase-string)', + ]; + + yield [ + new ConstantIntegerType(PHP_INT_MIN), + (string) PHP_INT_MIN, + ]; + + yield [ + new ConstantIntegerType(PHP_INT_MAX), + (string) PHP_INT_MAX, + ]; + + yield [ + new ConstantFloatType(9223372036854775807), + '9.223372036854776E+18', + ]; + + yield [ + new ConstantFloatType(-9223372036854775808), + '-9.223372036854776E+18', + ]; + + yield [ + new ConstantFloatType(2.35), + '2.35', + ]; + + yield [ + new ConstantFloatType(100), + '100.0', + ]; + + yield [ + new ConstantFloatType(8.202343767574732), + '8.202343767574732', + ]; + + yield [ + new ConstantFloatType(1e80), + '1.0E+80', + ]; + + yield [ + new ConstantFloatType(-5e-80), + '-5.0E-80', + ]; + + yield [ + new ConstantFloatType(0.0), + '0.0', + ]; + + yield [ + new ConstantFloatType(-0.0), + '-0.0', + ]; + } + + /** + * @dataProvider dataToPhpDocNodeWithoutCheckingEquals + */ + public function testToPhpDocNodeWithoutCheckingEquals(Type $type, string $expected): void + { + $phpDocNode = $type->toPhpDocNode(); + + $typeString = (string) $phpDocNode; + $this->assertSame($expected, $typeString); + + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $typeStringResolver->resolve($typeString); + } + + public function dataFromTypeStringToPhpDocNode(): iterable + { + foreach ($this->dataToPhpDocNode() as [, $typeString]) { + yield [$typeString]; + } + + yield ['callable']; + yield ['callable(Foo): Bar']; + yield ['callable(Foo=, Bar=): Bar']; + yield ['Closure(Foo=, Bar=): Bar']; + + yield ['callable(Foo $foo): Bar']; + yield ['callable(Foo $foo=, Bar $bar=): Bar']; + yield ['Closure(Foo $foo=, Bar $bar=): Bar']; + yield ['Closure(Foo $foo=, Bar $bar=): (Closure(Foo): Bar)']; + } + + /** + * @dataProvider dataFromTypeStringToPhpDocNode + */ + public function testFromTypeStringToPhpDocNode(string $typeString): void + { + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $type = $typeStringResolver->resolve($typeString); + $this->assertSame($typeString, (string) $type->toPhpDocNode()); + + $typeAgain = $typeStringResolver->resolve((string) $type->toPhpDocNode()); + $this->assertTrue($type->equals($typeAgain)); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + +} diff --git a/tests/PHPStan/Type/UnionTypeTest.php b/tests/PHPStan/Type/UnionTypeTest.php index 36636f4c05..46b5803374 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; @@ -20,18 +21,19 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use RecursionCallable\Foo; use stdClass; -use function array_map; use function array_merge; use function array_reverse; use function get_class; -use function implode; use function sprintf; +use const PHP_VERSION_ID; class UnionTypeTest extends PHPStanTestCase { @@ -109,7 +111,7 @@ public function dataSelfCompare(): Iterator yield [new CallableType([$mixedParam, $integerParam], $stringType, false)]; yield [new ClassStringType()]; yield [new ClosureType([$mixedParam, $integerParam], $stringType, false)]; - yield [new ConstantArrayType([$constantStringType, $constantIntegerType], [$mixedType, $stringType], 10, [1])]; + yield [new ConstantArrayType([$constantStringType, $constantIntegerType], [$mixedType, $stringType], [10], [1])]; yield [new ConstantBooleanType(true)]; yield [new ConstantFloatType(3.14)]; yield [$constantIntegerType]; @@ -645,6 +647,69 @@ public function testIsSubTypeOfInversed(UnionType $type, Type $otherType, Trinar ); } + public function dataIsScalar(): array + { + return [ + [ + TypeCombinator::union( + new BooleanType(), + new IntegerType(), + new FloatType(), + new StringType(), + ), + TrinaryLogic::createYes(), + ], + [ + new UnionType([ + new BooleanType(), + new ObjectType(DateTimeImmutable::class), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new IntegerType(), + new NullType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new FloatType(), + new MixedType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new ArrayType(new IntegerType(), new StringType()), + new StringType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new ArrayType(new IntegerType(), new StringType()), + new NullType(), + new ObjectType(DateTimeImmutable::class), + new ResourceType(), + ]), + TrinaryLogic::createNo(), + ], + ]; + } + + /** @dataProvider dataIsScalar */ + public function testIsScalar(UnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isScalar(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isScalar()', $type->describe(VerbosityLevel::precise())), + ); + } + public function dataDescribe(): array { return [ @@ -652,11 +717,13 @@ public function dataDescribe(): array new UnionType([new IntegerType(), new StringType()]), 'int|string', 'int|string', + 'int|string', ], [ new UnionType([new IntegerType(), new StringType(), new NullType()]), 'int|string|null', 'int|string|null', + 'int|string|null', ], [ new UnionType([ @@ -677,6 +744,7 @@ public function dataDescribe(): array new ConstantStringType('1'), ]), "1|2|2.2|10|'1'|'10'|'10aaa'|'11aaa'|'1aaa'|'2'|'2aaa'|'foo'|stdClass|true|null", + "1|2|2.2|10|'1'|'10'|'10aaa'|'11aaa'|'1aaa'|'2'|'2aaa'|'foo'|stdClass|true|null", 'float|int|stdClass|string|true|null', ], [ @@ -697,7 +765,8 @@ public function dataDescribe(): array ]), new ConstantStringType('aaa'), ), - '\'aaa\'|array{a: int|string, b: bool|float}', + '\'aaa\'|array{a: int, b: float}|array{a: string, b: bool}', + '\'aaa\'|array{a: int, b: float}|array{a: string, b: bool}', 'array|string', ], [ @@ -719,6 +788,7 @@ public function dataDescribe(): array new ConstantStringType('aaa'), ), '\'aaa\'|array{a: string, b: bool}|array{b: int, c: float}', + '\'aaa\'|array{a: string, b: bool}|array{b: int, c: float}', 'array|string', ], [ @@ -740,6 +810,7 @@ public function dataDescribe(): array new ConstantStringType('aaa'), ), '\'aaa\'|array{a: string, b: bool}|array{c: int, d: float}', + '\'aaa\'|array{a: string, b: bool}|array{c: int, d: float}', 'array|string', ], [ @@ -759,7 +830,8 @@ public function dataDescribe(): array new FloatType(), ]), ), - 'array{0: int|string, 1?: bool, 2?: float}', + 'array{int, bool, float}|array{string}', + 'array{int, bool, float}|array{string}', 'array', ], [ @@ -772,6 +844,7 @@ public function dataDescribe(): array ]), ), 'array{}|array{foooo: \'barrr\'}', + 'array{}|array{foooo: \'barrr\'}', 'array', ], [ @@ -783,6 +856,7 @@ public function dataDescribe(): array ]), ), 'int|numeric-string', + 'int|numeric-string', 'int|string', ], [ @@ -792,6 +866,74 @@ public function dataDescribe(): array ), 'int<0, 4>|int<6, 10>', 'int<0, 4>|int<6, 10>', + 'int<0, 4>|int<6, 10>', + ], + [ + TypeCombinator::union( + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('foo'), + 'TFoo', + new IntegerType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ), + 'TFoo of int (class foo, parameter)|null', + '(TFoo of int)|null', + '(TFoo of int)|null', + ], + [ + TypeCombinator::union( + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('foo'), + 'TFoo', + new IntegerType(), + TemplateTypeVariance::createInvariant(), + ), + new GenericClassStringType(new ObjectType('Abc')), + ), + 'class-string|TFoo of int (class foo, parameter)', + 'class-string|TFoo of int', + 'class-string|TFoo of int', + ], + [ + TypeCombinator::union( + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('foo'), + 'TFoo', + new MixedType(true), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ), + 'TFoo (class foo, parameter)|null', + 'TFoo|null', + 'TFoo|null', + ], + [ + TypeCombinator::union( + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('foo'), + 'TFoo', + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('foo'), + 'TBar', + new MixedType(true), + TemplateTypeVariance::createInvariant(), + ), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ), + 'TFoo of TBar (class foo, parameter) (class foo, parameter)|null', + '(TFoo of TBar)|null', + '(TFoo of TBar)|null', + ], + [ + new UnionType([new ObjectType('Foo'), new ObjectType('Foo')]), + 'Foo#1|Foo#2', + 'Foo', + 'Foo', ], ]; } @@ -801,17 +943,19 @@ public function dataDescribe(): array */ public function testDescribe( Type $type, + string $expectedPreciseDescription, string $expectedValueDescription, string $expectedTypeOnlyDescription, ): void { + $this->assertSame($expectedPreciseDescription, $type->describe(VerbosityLevel::precise())); $this->assertSame($expectedValueDescription, $type->describe(VerbosityLevel::value())); $this->assertSame($expectedTypeOnlyDescription, $type->describe(VerbosityLevel::typeOnly())); } - public function dataAccepts(): array + public function dataAccepts(): iterable { - return [ + yield from [ [ new UnionType([new CallableType(), new NullType()]), new ClosureType([], new StringType(), false), @@ -879,6 +1023,89 @@ public function dataAccepts(): array new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], + + ]; + + if (PHP_VERSION_ID >= 80100) { + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'C'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'), + ]), + new ObjectType( + 'PHPStan\Fixture\ManyCasesTestEnum', + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'E'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'F'), + ]), + ), + TrinaryLogic::createYes(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'C'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'E'), + ]), + new ObjectType( + 'PHPStan\Fixture\ManyCasesTestEnum', + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'F'), + ), + TrinaryLogic::createYes(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ]), + new ObjectType('PHPStan\Fixture\TestEnum'), + TrinaryLogic::createYes(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'TWO'), + ]), + new ObjectType('PHPStan\Fixture\TestEnum'), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new NullType(), + ]), + new ObjectType( + 'PHPStan\Fixture\TestEnum', + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ), + TrinaryLogic::createYes(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new NullType(), + ]), + new UnionType([ + new ObjectType( + 'PHPStan\Fixture\TestEnum', + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ), + new NullType(), + ]), + TrinaryLogic::createYes(), + ]; + } + + yield from [ 'accepts template-of-union with same members' => [ new UnionType([ new IntegerType(), @@ -1064,7 +1291,6 @@ public function dataAccepts(): array ]), TrinaryLogic::createYes(), ], - ]; } @@ -1079,7 +1305,7 @@ public function testAccepts( { $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), + $type->accepts($acceptedType, true)->result->describe(), sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } @@ -1151,8 +1377,8 @@ public function testSorting(): void $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())), + $type1->describe(VerbosityLevel::precise()), + $type2->describe(VerbosityLevel::precise()), 'UnionType sorting always produces the same order', ); @@ -1162,4 +1388,256 @@ public function testSorting(): void ); } + /** + * @dataProvider dataGetConstantArrays + * @param Type[] $types + * @param list $expectedDescriptions + */ + public function testGetConstantArrays( + array $types, + array $expectedDescriptions, + ): void + { + $unionType = TypeCombinator::union(...$types); + $constantArrays = $unionType->getConstantArrays(); + + $actualDescriptions = []; + foreach ($constantArrays as $constantArray) { + $actualDescriptions[] = $constantArray->describe(VerbosityLevel::precise()); + } + + $this->assertSame($expectedDescriptions, $actualDescriptions); + } + + public function dataGetConstantArrays(): iterable + { + yield from [ + [ + [ + TypeCombinator::intersect( + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + [2], + [0, 1], + ), + new NonEmptyArrayType(), + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ObjectType(Foo::class), new ObjectType(stdClass::class)], + [2], + ), + ], + [ + 'array{1?: int, 2?: string}', + 'array{RecursionCallable\Foo, stdClass}', + ], + ], + [ + [ + TypeCombinator::intersect( + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + [2], + [0, 1], + ), + ), + new IntegerType(), + ], + [], + ], + ]; + } + + /** + * @dataProvider dataGetConstantStrings + * @param list $expectedDescriptions + */ + public function testGetConstantStrings( + Type $unionType, + array $expectedDescriptions, + ): void + { + $constantStrings = $unionType->getConstantStrings(); + + $actualDescriptions = []; + foreach ($constantStrings as $constantString) { + $actualDescriptions[] = $constantString->describe(VerbosityLevel::precise()); + } + + $this->assertSame($expectedDescriptions, $actualDescriptions); + } + + public function dataGetConstantStrings(): iterable + { + yield from [ + [ + TypeCombinator::union( + new ConstantStringType('hello'), + new ConstantStringType('world'), + ), + [ + "'hello'", + "'world'", + ], + ], + [ + TypeCombinator::union( + new ConstantStringType(''), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + ), + [], + ], + [ + new UnionType([ + new IntersectionType( + [ + new ConstantStringType('foo'), + new AccessoryLiteralStringType(), + ], + ), + new IntersectionType( + [ + new ConstantStringType('bar'), + new AccessoryLiteralStringType(), + ], + ), + ]), + [ + "'foo'", + "'bar'", + ], + ], + [ + new BenevolentUnionType([ + new ConstantStringType('foo'), + new NullType(), + ]), + [ + "'foo'", + ], + ], + ]; + } + + /** + * @dataProvider dataGetObjectClassNames + * @param list $expectedObjectClassNames + */ + public function testGetObjectClassNames( + Type $unionType, + array $expectedObjectClassNames, + ): void + { + $this->assertSame($expectedObjectClassNames, $unionType->getObjectClassNames()); + } + + public function dataGetObjectClassNames(): iterable + { + yield from [ + [ + TypeCombinator::union( + new ObjectType(stdClass::class), + new ObjectType(DateTimeImmutable::class), + ), + [ + 'stdClass', + 'DateTimeImmutable', + ], + ], + [ + TypeCombinator::union( + new ObjectType(stdClass::class), + new NullType(), + ), + [], + ], + [ + TypeCombinator::union( + new StringType(), + new NullType(), + ), + [], + ], + ]; + } + + /** + * @dataProvider dataGetArrays + * @param list $expectedDescriptions + */ + public function testGetArrays( + Type $unionType, + array $expectedDescriptions, + ): void + { + $arrays = $unionType->getArrays(); + + $actualDescriptions = []; + foreach ($arrays as $arrayType) { + $actualDescriptions[] = $arrayType->describe(VerbosityLevel::precise()); + } + + $this->assertSame($expectedDescriptions, $actualDescriptions); + } + + public function dataGetArrays(): iterable + { + yield from [ + [ + TypeCombinator::union( + new ConstantStringType('hello'), + new ConstantStringType('world'), + ), + [], + ], + [ + TypeCombinator::union( + TypeCombinator::intersect( + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + [2], + [0, 1], + ), + new NonEmptyArrayType(), + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ObjectType(Foo::class), new ObjectType(stdClass::class)], + [2], + ), + ), + [ + 'array{1?: int, 2?: string}', + 'array{RecursionCallable\Foo, stdClass}', + ], + ], + [ + TypeCombinator::union( + new ArrayType(new IntegerType(), new StringType()), + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + [2], + [0, 1], + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ObjectType(Foo::class), new ObjectType(stdClass::class)], + [2], + ), + ), + [ + 'array', + ], + ], + ]; + } + } diff --git a/tests/PHPStan/Type/VerbosityLevelTest.php b/tests/PHPStan/Type/VerbosityLevelTest.php new file mode 100644 index 0000000000..0ff71d5025 --- /dev/null +++ b/tests/PHPStan/Type/VerbosityLevelTest.php @@ -0,0 +1,61 @@ +assertSame($expected->getLevelValue(), $level->getLevelValue()); + } + +} diff --git a/tests/PHPStan/Type/data/ExtendsThrowable.php b/tests/PHPStan/Type/data/ExtendsThrowable.php new file mode 100644 index 0000000000..035bf18cef --- /dev/null +++ b/tests/PHPStan/Type/data/ExtendsThrowable.php @@ -0,0 +1,10 @@ += 8.1 + +namespace ObjectTypeEnums; + +enum FooEnum +{ + + case FOO; + case BAR; + case BAZ; + +} diff --git a/tests/PHPStan/Type/data/annotations.php b/tests/PHPStan/Type/data/annotations.php index 0732fd31c4..bf157ab365 100644 --- a/tests/PHPStan/Type/data/annotations.php +++ b/tests/PHPStan/Type/data/annotations.php @@ -1,5 +1,7 @@ &1', escapeshellarg(__DIR__ . '/PHP-Parser')), $outputLines, $exitCode); - if ($exitCode === 0) { - return; - } - - $this->fail(implode("\n", $outputLines)); - } - - public function testResultCache(): void - { - $this->runPhpstan(0); - $this->assertResultCache(__DIR__ . '/resultCache_1.php'); - - $this->runPhpstan(0); - $this->assertResultCache(__DIR__ . '/resultCache_1.php'); - - $lexerPath = __DIR__ . '/PHP-Parser/lib/PhpParser/Lexer.php'; - $lexerCode = FileReader::read($lexerPath); - $originalLexerCode = $lexerCode; - - $lexerCode = str_replace('@param string $code', '', $lexerCode); - $lexerCode = str_replace('public function startLexing($code', 'public function startLexing(\\PhpParser\\Node\\Expr\\MethodCall $code', $lexerCode); - file_put_contents($lexerPath, $lexerCode); - - $errorHandlerPath = __DIR__ . '/PHP-Parser/lib/PhpParser/ErrorHandler.php'; - $errorHandlerContents = FileReader::read($errorHandlerPath); - $errorHandlerContents .= "\n\n"; - file_put_contents($errorHandlerPath, $errorHandlerContents); - - $bootstrapPath = __DIR__ . '/PHP-Parser/lib/bootstrap.php'; - $originalBootstrapContents = FileReader::read($bootstrapPath); - file_put_contents($bootstrapPath, "\n\n echo ['foo'];", FILE_APPEND); - - $this->runPhpstanWithErrors(); - $this->runPhpstanWithErrors(); - - file_put_contents($lexerPath, $originalLexerCode); - - unlink($bootstrapPath); - $this->runPhpstan(0); - $this->assertResultCache(__DIR__ . '/resultCache_3.php'); - - file_put_contents($bootstrapPath, $originalBootstrapContents); - $this->runPhpstan(0); - $this->assertResultCache(__DIR__ . '/resultCache_1.php'); - } - - private function runPhpstanWithErrors(): void - { - $result = $this->runPhpstan(1); - $this->assertSame(3, $result['totals']['file_errors']); - $this->assertSame(0, $result['totals']['errors']); - - $fileHelper = new FileHelper(__DIR__); - - $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->assertResultCache(__DIR__ . '/resultCache_2.php'); - } - - public function testResultCacheDeleteFile(): void - { - $this->runPhpstan(0); - $this->assertResultCache(__DIR__ . '/resultCache_1.php'); - - $serializerPath = __DIR__ . '/PHP-Parser/lib/PhpParser/Serializer.php'; - $serializerCode = FileReader::read($serializerPath); - $originalSerializerCode = $serializerCode; - unlink($serializerPath); - - $fileHelper = new FileHelper(__DIR__); - - $result = $this->runPhpstan(1); - $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']; - $this->assertStringContainsString('Ignored error pattern #^Argument of an invalid type PhpParser\\\\Node supplied for foreach, only iterables are supported\\.$# in path', $message); - $this->assertStringContainsString('was not matched in reported errors.', $message); - $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); - $this->assertResultCache(__DIR__ . '/resultCache_1.php'); - } - - /** - * @return mixed[] - */ - private function runPhpstan(int $expectedExitCode): array - { - exec(sprintf( - '%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'), - ), $outputLines, $exitCode); - $output = implode("\n", $outputLines); - - try { - $json = Json::decode($output, Json::FORCE_ARRAY); - } catch (JsonException $e) { - $this->fail(sprintf('%s: %s', $e->getMessage(), $output)); - } - - if ($exitCode !== $expectedExitCode) { - $this->fail($output); - } - - return $json; - } - - /** - * @param mixed[] $resultCache - * @return mixed[] - */ - private function transformResultCache(array $resultCache): array - { - $new = []; - foreach ($resultCache['dependencies'] as $file => $data) { - $files = array_map(fn (string $file): string => $this->relativizePath($file), $data['dependentFiles']); - sort($files); - $new[$this->relativizePath($file)] = $files; - } - - ksort($new); - - return $new; - } - - private function relativizePath(string $path): string - { - $path = str_replace('\\', '/', $path); - $helper = new SimpleRelativePathHelper(str_replace('\\', '/', __DIR__ . '/PHP-Parser')); - return $helper->getRelativePath($path); - } - - private function assertResultCache(string $expectedCachePath): void - { - $resultCachePath = __DIR__ . '/tmp/resultCache.php'; - $resultCache = $this->transformResultCache(require $resultCachePath); - $expectedResultCachePath = require $expectedCachePath; - $this->assertSame($expectedResultCachePath, $resultCache); - } - -} diff --git a/tests/e2e/baseline.neon b/tests/e2e/baseline.neon deleted file mode 100644 index a6bdfbe83c..0000000000 --- a/tests/e2e/baseline.neon +++ /dev/null @@ -1,192 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^PHPDoc tag @param references unknown parameter\\: \\$interfaces$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Builder/Class_.php - - - - message: "#^PHPDoc tag @param references unknown parameter\\: \\$interfaces$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Builder/Interface_.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$stmts\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Builder/Interface_.php - - - - message: "#^Result of && is always false\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/BuilderAbstract.php - - - - message: "#^Method PhpParser\\\\BuilderAbstract\\:\\:normalizeValue\\(\\) should return PhpParser\\\\Node\\\\Expr but returns PhpParser\\\\Node\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/BuilderAbstract.php - - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/BuilderAbstract.php - - - - message: "#^Access to an undefined property PhpParser\\\\BuilderAbstract\\:\\:\\$flags\\.$#" - count: 2 - path: PHP-Parser/lib/PhpParser/BuilderAbstract.php - - - - message: "#^PHPDoc tag @param has invalid value \\(string\\|Node\\\\Name Name to alias\\)\\: Unexpected token \"Name\", expected variable at offset 88$#" - count: 1 - path: PHP-Parser/lib/PhpParser/BuilderFactory.php - - - - message: "#^Expression \"@\\$undefinedVariable\" on a separate line does not do anything\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Lexer.php - - - - message: "#^Undefined variable\\: \\$undefinedVariable$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Lexer.php - - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Lexer.php - - - - message: "#^Empty array passed to foreach\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Lexer/Emulative.php - - - - message: "#^Method PhpParser\\\\Node\\\\Expr\\\\Closure\\:\\:getStmts\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Node/Expr/Closure.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 4 - path: PHP-Parser/lib/PhpParser/Node/Name.php - - - - message: "#^Method PhpParser\\\\Node\\\\Stmt\\\\ClassMethod\\:\\:getStmts\\(\\) should return array\\ but returns array\\\\|null\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Node/Stmt/ClassMethod.php - - - - message: "#^Method PhpParser\\\\Node\\\\Stmt\\\\Function_\\:\\:getStmts\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Node/Stmt/Function_.php - - - - message: "#^PHPDoc tag @param for parameter \\$attributes with type array\\|null is not subtype of native type array\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Node/Stmt/TryCatch.php - - - - message: "#^Method PhpParser\\\\NodeVisitor\\\\NameResolver\\:\\:beforeTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - - - message: "#^Method PhpParser\\\\NodeVisitor\\\\NameResolver\\:\\:enterNode\\(\\) should return int\\|PhpParser\\\\Node\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$name\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$namespacedName\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:beforeTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:enterNode\\(\\) should return int\\|PhpParser\\\\Node\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:leaveNode\\(\\) should return array\\\\|int\\|PhpParser\\\\Node\\|false\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:afterTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\\\Expr\\:\\:\\$class\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Parser/Php5.php - - - - message: "#^Access to an undefined property PhpParser\\\\Node\\\\Expr\\:\\:\\$name\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Parser/Php5.php - - - - message: "#^Variable \\$s might not be defined\\.$#" - count: 3 - path: PHP-Parser/lib/PhpParser/Parser/Php5.php - - - - message: "#^Variable \\$s might not be defined\\.$#" - count: 3 - path: PHP-Parser/lib/PhpParser/Parser/Php7.php - - - - message: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributes \\(array\\) does not accept string\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributeStack \\(array\\\\) does not accept array\\\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Comparison operation \"\\<\" between \\(array\\|float\\|int\\<0, max\\>\\) and int results in an error\\.$#" - count: 3 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Comparison operation \"\\>\\=\" between \\(array\\|float\\|int\\) and 0 results in an error\\.$#" - count: 3 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Variable \\$tokenValue might not be defined\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Variable \\$action might not be defined\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Strict comparison using \\=\\=\\= between null and PhpParser\\\\Node will always evaluate to false\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/PrettyPrinterAbstract.php - - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/PrettyPrinterAbstract.php - - - - message: "#^Argument of an invalid type PhpParser\\\\Node supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/Serializer/XML.php - diff --git a/tests/e2e/data/empty.neon b/tests/e2e/data/empty.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/data/soap.php b/tests/e2e/data/soap.php index e57afe1481..d75edf768e 100644 --- a/tests/e2e/data/soap.php +++ b/tests/e2e/data/soap.php @@ -14,9 +14,9 @@ class MySoapClient2 extends \SoapClient /** * @param string|null $wsdl - * @param mixed[]|null $options + * @param mixed[] $options */ - public function __construct($wsdl, array $options = null) + public function __construct($wsdl, array $options = []) { parent::__construct($wsdl, $options); } @@ -46,7 +46,7 @@ class MySoapHeader extends \SoapHeader public function __construct(string $username, string $password) { - parent::SoapHeader($username, $password); + parent::__construct($username, $password); } } diff --git a/tests/e2e/data/timecop.php b/tests/e2e/data/timecop.php index 3a0ee354a1..0c7ad015cb 100644 --- a/tests/e2e/data/timecop.php +++ b/tests/e2e/data/timecop.php @@ -20,4 +20,9 @@ public static function create(): self return new self(new DateTimeImmutable()); } + public function getBar(): DateTimeImmutable + { + return $this->bar; + } + } diff --git a/tests/e2e/phpstan.neon b/tests/e2e/phpstan.neon deleted file mode 100644 index 4bf004daf0..0000000000 --- a/tests/e2e/phpstan.neon +++ /dev/null @@ -1,5 +0,0 @@ -includes: - - baseline.neon - -parameters: - tmpDir: tmp diff --git a/tests/e2e/resultCache_1.php b/tests/e2e/resultCache_1.php deleted file mode 100644 index 8aabcb449b..0000000000 --- a/tests/e2e/resultCache_1.php +++ /dev/null @@ -1,2018 +0,0 @@ - - array ( - 0 => 'lib/bootstrap.php', - ), - 'lib/PhpParser/Builder.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Class_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Declaration.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Function_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Interface_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Method.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Property.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Trait_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Use_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderFactory.php' => - array ( - ), - 'lib/PhpParser/Comment.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment/Doc.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/NodeDumper.php', - 8 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 9 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Comment/Doc.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Error.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler.php', - 1 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 2 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 3 => 'lib/PhpParser/Lexer.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/String_.php', - 6 => 'lib/PhpParser/Node/Stmt/Class_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Multiple.php', - 9 => 'lib/PhpParser/Parser/Php5.php', - 10 => 'lib/PhpParser/Parser/Php7.php', - 11 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 1 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 2 => 'lib/PhpParser/Lexer.php', - 3 => 'lib/PhpParser/Lexer/Emulative.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser.php', - 6 => 'lib/PhpParser/Parser/Multiple.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler/Collecting.php' => - array ( - ), - 'lib/PhpParser/ErrorHandler/Throwing.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Multiple.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/Lexer.php' => - array ( - 0 => 'lib/PhpParser/Lexer/Emulative.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Lexer/Emulative.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Node.php' => - array ( - 0 => 'lib/PhpParser/Builder.php', - 1 => 'lib/PhpParser/Builder/Class_.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - 13 => 'lib/PhpParser/Node/Arg.php', - 14 => 'lib/PhpParser/Node/Const_.php', - 15 => 'lib/PhpParser/Node/Expr.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 17 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 18 => 'lib/PhpParser/Node/Expr/Array_.php', - 19 => 'lib/PhpParser/Node/Expr/Assign.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 32 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 33 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 61 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 62 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 63 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 64 => 'lib/PhpParser/Node/Expr/Cast.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 71 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 72 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 73 => 'lib/PhpParser/Node/Expr/Clone_.php', - 74 => 'lib/PhpParser/Node/Expr/Closure.php', - 75 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 76 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 77 => 'lib/PhpParser/Node/Expr/Empty_.php', - 78 => 'lib/PhpParser/Node/Expr/Error.php', - 79 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 80 => 'lib/PhpParser/Node/Expr/Eval_.php', - 81 => 'lib/PhpParser/Node/Expr/Exit_.php', - 82 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 83 => 'lib/PhpParser/Node/Expr/Include_.php', - 84 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 85 => 'lib/PhpParser/Node/Expr/Isset_.php', - 86 => 'lib/PhpParser/Node/Expr/List_.php', - 87 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 88 => 'lib/PhpParser/Node/Expr/New_.php', - 89 => 'lib/PhpParser/Node/Expr/PostDec.php', - 90 => 'lib/PhpParser/Node/Expr/PostInc.php', - 91 => 'lib/PhpParser/Node/Expr/PreDec.php', - 92 => 'lib/PhpParser/Node/Expr/PreInc.php', - 93 => 'lib/PhpParser/Node/Expr/Print_.php', - 94 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 95 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 96 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 97 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 98 => 'lib/PhpParser/Node/Expr/Ternary.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 100 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 101 => 'lib/PhpParser/Node/Expr/Variable.php', - 102 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 103 => 'lib/PhpParser/Node/Expr/Yield_.php', - 104 => 'lib/PhpParser/Node/FunctionLike.php', - 105 => 'lib/PhpParser/Node/Name.php', - 106 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 107 => 'lib/PhpParser/Node/Name/Relative.php', - 108 => 'lib/PhpParser/Node/NullableType.php', - 109 => 'lib/PhpParser/Node/Param.php', - 110 => 'lib/PhpParser/Node/Scalar.php', - 111 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 112 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 113 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 114 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 123 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 124 => 'lib/PhpParser/Node/Scalar/String_.php', - 125 => 'lib/PhpParser/Node/Stmt.php', - 126 => 'lib/PhpParser/Node/Stmt/Break_.php', - 127 => 'lib/PhpParser/Node/Stmt/Case_.php', - 128 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 131 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 132 => 'lib/PhpParser/Node/Stmt/Class_.php', - 133 => 'lib/PhpParser/Node/Stmt/Const_.php', - 134 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 135 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 136 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 137 => 'lib/PhpParser/Node/Stmt/Do_.php', - 138 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 139 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 140 => 'lib/PhpParser/Node/Stmt/Else_.php', - 141 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 142 => 'lib/PhpParser/Node/Stmt/For_.php', - 143 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 144 => 'lib/PhpParser/Node/Stmt/Function_.php', - 145 => 'lib/PhpParser/Node/Stmt/Global_.php', - 146 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 147 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 148 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 149 => 'lib/PhpParser/Node/Stmt/If_.php', - 150 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 151 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 152 => 'lib/PhpParser/Node/Stmt/Label.php', - 153 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 154 => 'lib/PhpParser/Node/Stmt/Nop.php', - 155 => 'lib/PhpParser/Node/Stmt/Property.php', - 156 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 157 => 'lib/PhpParser/Node/Stmt/Return_.php', - 158 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 159 => 'lib/PhpParser/Node/Stmt/Static_.php', - 160 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 161 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 165 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 166 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 167 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 168 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 169 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 170 => 'lib/PhpParser/Node/Stmt/Use_.php', - 171 => 'lib/PhpParser/Node/Stmt/While_.php', - 172 => 'lib/PhpParser/NodeAbstract.php', - 173 => 'lib/PhpParser/NodeDumper.php', - 174 => 'lib/PhpParser/NodeTraverser.php', - 175 => 'lib/PhpParser/NodeTraverserInterface.php', - 176 => 'lib/PhpParser/NodeVisitor.php', - 177 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 178 => 'lib/PhpParser/NodeVisitorAbstract.php', - 179 => 'lib/PhpParser/Parser.php', - 180 => 'lib/PhpParser/Parser/Multiple.php', - 181 => 'lib/PhpParser/Parser/Php5.php', - 182 => 'lib/PhpParser/Parser/Php7.php', - 183 => 'lib/PhpParser/ParserAbstract.php', - 184 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 185 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 186 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Node/Arg.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 1 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 2 => 'lib/PhpParser/Node/Expr/New_.php', - 3 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Const_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 1 => 'lib/PhpParser/Node/Stmt/Const_.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr.php' => - array ( - 0 => 'lib/PhpParser/Builder/Param.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Node/Arg.php', - 4 => 'lib/PhpParser/Node/Const_.php', - 5 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 6 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 7 => 'lib/PhpParser/Node/Expr/Array_.php', - 8 => 'lib/PhpParser/Node/Expr/Assign.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 12 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 13 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 14 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 15 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 22 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 27 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 28 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 29 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 30 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 31 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 32 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 51 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 52 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 53 => 'lib/PhpParser/Node/Expr/Cast.php', - 54 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 55 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 56 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 57 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 58 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 59 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 60 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 61 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 62 => 'lib/PhpParser/Node/Expr/Clone_.php', - 63 => 'lib/PhpParser/Node/Expr/Closure.php', - 64 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 65 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 66 => 'lib/PhpParser/Node/Expr/Empty_.php', - 67 => 'lib/PhpParser/Node/Expr/Error.php', - 68 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 69 => 'lib/PhpParser/Node/Expr/Eval_.php', - 70 => 'lib/PhpParser/Node/Expr/Exit_.php', - 71 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 72 => 'lib/PhpParser/Node/Expr/Include_.php', - 73 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 74 => 'lib/PhpParser/Node/Expr/Isset_.php', - 75 => 'lib/PhpParser/Node/Expr/List_.php', - 76 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 77 => 'lib/PhpParser/Node/Expr/New_.php', - 78 => 'lib/PhpParser/Node/Expr/PostDec.php', - 79 => 'lib/PhpParser/Node/Expr/PostInc.php', - 80 => 'lib/PhpParser/Node/Expr/PreDec.php', - 81 => 'lib/PhpParser/Node/Expr/PreInc.php', - 82 => 'lib/PhpParser/Node/Expr/Print_.php', - 83 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 84 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 85 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 86 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 87 => 'lib/PhpParser/Node/Expr/Ternary.php', - 88 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 89 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 90 => 'lib/PhpParser/Node/Expr/Variable.php', - 91 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 92 => 'lib/PhpParser/Node/Expr/Yield_.php', - 93 => 'lib/PhpParser/Node/Param.php', - 94 => 'lib/PhpParser/Node/Scalar.php', - 95 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 96 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 97 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 98 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 99 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 100 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 101 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 102 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 103 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 104 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 105 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 106 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 107 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 108 => 'lib/PhpParser/Node/Scalar/String_.php', - 109 => 'lib/PhpParser/Node/Stmt/Break_.php', - 110 => 'lib/PhpParser/Node/Stmt/Case_.php', - 111 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 112 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 113 => 'lib/PhpParser/Node/Stmt/Do_.php', - 114 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 115 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 116 => 'lib/PhpParser/Node/Stmt/For_.php', - 117 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 118 => 'lib/PhpParser/Node/Stmt/Global_.php', - 119 => 'lib/PhpParser/Node/Stmt/If_.php', - 120 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 121 => 'lib/PhpParser/Node/Stmt/Return_.php', - 122 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 123 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 124 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 125 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 126 => 'lib/PhpParser/Node/Stmt/While_.php', - 127 => 'lib/PhpParser/NodeDumper.php', - 128 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 129 => 'lib/PhpParser/Parser/Php5.php', - 130 => 'lib/PhpParser/Parser/Php7.php', - 131 => 'lib/PhpParser/ParserAbstract.php', - 132 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 133 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Expr/ArrayDimFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ArrayItem.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Expr/Array_.php', - 2 => 'lib/PhpParser/Node/Expr/List_.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Array_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Assign.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 4 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 5 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 6 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 7 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 8 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 12 => 'lib/PhpParser/Parser/Php5.php', - 13 => 'lib/PhpParser/Parser/Php7.php', - 14 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignRef.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 4 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 5 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 6 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 7 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 8 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 9 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 10 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 11 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 12 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 13 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 14 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 15 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 19 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 20 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 21 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 22 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 27 => 'lib/PhpParser/Parser/Php5.php', - 28 => 'lib/PhpParser/Parser/Php7.php', - 29 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BitwiseNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BooleanNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 1 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 2 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 3 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 4 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 5 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 6 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Array_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Bool_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Double.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Int_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Object_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/String_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClassConstFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Clone_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Closure.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClosureUse.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Closure.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ConstFetch.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Empty_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Error.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ErrorSuppress.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Eval_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Exit_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/FuncCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Include_.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Instanceof_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Isset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/List_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/MethodCall.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/New_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Print_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ShellExec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Ternary.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryMinus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryPlus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Variable.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/YieldFrom.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Yield_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Trait_.php', - 3 => 'lib/PhpParser/Node/Expr/Closure.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 6 => 'lib/PhpParser/Node/Stmt/Function_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/ParserAbstract.php', - 11 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 11 => 'lib/PhpParser/Node/Expr/Closure.php', - 12 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 13 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 14 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 15 => 'lib/PhpParser/Node/Expr/New_.php', - 16 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 17 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 18 => 'lib/PhpParser/Node/FunctionLike.php', - 19 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 20 => 'lib/PhpParser/Node/Name/Relative.php', - 21 => 'lib/PhpParser/Node/NullableType.php', - 22 => 'lib/PhpParser/Node/Param.php', - 23 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 24 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 25 => 'lib/PhpParser/Node/Stmt/Class_.php', - 26 => 'lib/PhpParser/Node/Stmt/Function_.php', - 27 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 28 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 29 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 30 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 31 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 32 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 33 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 34 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 35 => 'lib/PhpParser/Parser/Php5.php', - 36 => 'lib/PhpParser/Parser/Php7.php', - 37 => 'lib/PhpParser/ParserAbstract.php', - 38 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 39 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Name/FullyQualified.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name/Relative.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/NullableType.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Param.php', - 4 => 'lib/PhpParser/BuilderAbstract.php', - 5 => 'lib/PhpParser/Node/Expr/Closure.php', - 6 => 'lib/PhpParser/Node/FunctionLike.php', - 7 => 'lib/PhpParser/Node/Param.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Function_.php', - 10 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Param.php', - 2 => 'lib/PhpParser/Node/Expr/Closure.php', - 3 => 'lib/PhpParser/Node/FunctionLike.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 5 => 'lib/PhpParser/Node/Stmt/Function_.php', - 6 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/ParserAbstract.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 2 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 3 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 8 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 9 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 10 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 11 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 12 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 13 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 14 => 'lib/PhpParser/Node/Scalar/String_.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/DNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/Encapsed.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/LNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/ParserAbstract.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst.php' => - array ( - 0 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 1 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 2 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 3 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 4 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/File.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Line.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Method.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/String_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Interface_.php', - 3 => 'lib/PhpParser/Builder/Method.php', - 4 => 'lib/PhpParser/Builder/Namespace_.php', - 5 => 'lib/PhpParser/Builder/Property.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/Closure.php', - 11 => 'lib/PhpParser/Node/Expr/New_.php', - 12 => 'lib/PhpParser/Node/FunctionLike.php', - 13 => 'lib/PhpParser/Node/Stmt/Break_.php', - 14 => 'lib/PhpParser/Node/Stmt/Case_.php', - 15 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 16 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 17 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 18 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 19 => 'lib/PhpParser/Node/Stmt/Class_.php', - 20 => 'lib/PhpParser/Node/Stmt/Const_.php', - 21 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 22 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 23 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 24 => 'lib/PhpParser/Node/Stmt/Do_.php', - 25 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 26 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 27 => 'lib/PhpParser/Node/Stmt/Else_.php', - 28 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 29 => 'lib/PhpParser/Node/Stmt/For_.php', - 30 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 31 => 'lib/PhpParser/Node/Stmt/Function_.php', - 32 => 'lib/PhpParser/Node/Stmt/Global_.php', - 33 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 34 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 35 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 36 => 'lib/PhpParser/Node/Stmt/If_.php', - 37 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 38 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 39 => 'lib/PhpParser/Node/Stmt/Label.php', - 40 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 41 => 'lib/PhpParser/Node/Stmt/Nop.php', - 42 => 'lib/PhpParser/Node/Stmt/Property.php', - 43 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 44 => 'lib/PhpParser/Node/Stmt/Return_.php', - 45 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 46 => 'lib/PhpParser/Node/Stmt/Static_.php', - 47 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 48 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 49 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 50 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 51 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 52 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 53 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 54 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 55 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 56 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 57 => 'lib/PhpParser/Node/Stmt/Use_.php', - 58 => 'lib/PhpParser/Node/Stmt/While_.php', - 59 => 'lib/PhpParser/NodeDumper.php', - 60 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 61 => 'lib/PhpParser/Parser/Php5.php', - 62 => 'lib/PhpParser/Parser/Php7.php', - 63 => 'lib/PhpParser/ParserAbstract.php', - 64 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 65 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Break_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Case_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Catch_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassConst.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Interface_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Property.php', - 4 => 'lib/PhpParser/Builder/Trait_.php', - 5 => 'lib/PhpParser/BuilderAbstract.php', - 6 => 'lib/PhpParser/Node/Expr/New_.php', - 7 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Class_.php', - 10 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 11 => 'lib/PhpParser/Node/Stmt/Property.php', - 12 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 13 => 'lib/PhpParser/NodeDumper.php', - 14 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassMethod.php' => - array ( - 0 => 'lib/PhpParser/Builder/Method.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/ParserAbstract.php', - 7 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Class_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Property.php', - 3 => 'lib/PhpParser/BuilderAbstract.php', - 4 => 'lib/PhpParser/Node/Expr/New_.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 6 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 7 => 'lib/PhpParser/Node/Stmt/Property.php', - 8 => 'lib/PhpParser/NodeDumper.php', - 9 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 10 => 'lib/PhpParser/Parser/Php5.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/ParserAbstract.php', - 13 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Const_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Continue_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/DeclareDeclare.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Declare_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Do_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Echo_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ElseIf_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Else_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Finally_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/For_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Foreach_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Function_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Global_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Goto_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/GroupUse.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/HaltCompiler.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/If_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/InlineHTML.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Interface_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Interface_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Label.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Namespace_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 6 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Nop.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Property.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/PropertyProperty.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Node/Stmt/Property.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Return_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/StaticVar.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Static_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Static_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Switch_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Throw_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 1 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 2 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TryCatch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/UseUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 2 => 'lib/PhpParser/Node/Stmt/Use_.php', - 3 => 'lib/PhpParser/NodeDumper.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser/Php5.php', - 6 => 'lib/PhpParser/Parser/Php7.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Use_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - 2 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 3 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 4 => 'lib/PhpParser/NodeDumper.php', - 5 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 6 => 'lib/PhpParser/Parser/Php5.php', - 7 => 'lib/PhpParser/Parser/Php7.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/While_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/NodeAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Property.php', - 8 => 'lib/PhpParser/Builder/Trait_.php', - 9 => 'lib/PhpParser/Builder/Use_.php', - 10 => 'lib/PhpParser/BuilderAbstract.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - 12 => 'lib/PhpParser/Node/Arg.php', - 13 => 'lib/PhpParser/Node/Const_.php', - 14 => 'lib/PhpParser/Node/Expr.php', - 15 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 17 => 'lib/PhpParser/Node/Expr/Array_.php', - 18 => 'lib/PhpParser/Node/Expr/Assign.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 32 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 61 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 62 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 63 => 'lib/PhpParser/Node/Expr/Cast.php', - 64 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 71 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 72 => 'lib/PhpParser/Node/Expr/Clone_.php', - 73 => 'lib/PhpParser/Node/Expr/Closure.php', - 74 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 75 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 76 => 'lib/PhpParser/Node/Expr/Empty_.php', - 77 => 'lib/PhpParser/Node/Expr/Error.php', - 78 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 79 => 'lib/PhpParser/Node/Expr/Eval_.php', - 80 => 'lib/PhpParser/Node/Expr/Exit_.php', - 81 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 82 => 'lib/PhpParser/Node/Expr/Include_.php', - 83 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 84 => 'lib/PhpParser/Node/Expr/Isset_.php', - 85 => 'lib/PhpParser/Node/Expr/List_.php', - 86 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 87 => 'lib/PhpParser/Node/Expr/New_.php', - 88 => 'lib/PhpParser/Node/Expr/PostDec.php', - 89 => 'lib/PhpParser/Node/Expr/PostInc.php', - 90 => 'lib/PhpParser/Node/Expr/PreDec.php', - 91 => 'lib/PhpParser/Node/Expr/PreInc.php', - 92 => 'lib/PhpParser/Node/Expr/Print_.php', - 93 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 94 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 95 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 96 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 97 => 'lib/PhpParser/Node/Expr/Ternary.php', - 98 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 100 => 'lib/PhpParser/Node/Expr/Variable.php', - 101 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 102 => 'lib/PhpParser/Node/Expr/Yield_.php', - 103 => 'lib/PhpParser/Node/FunctionLike.php', - 104 => 'lib/PhpParser/Node/Name.php', - 105 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 106 => 'lib/PhpParser/Node/Name/Relative.php', - 107 => 'lib/PhpParser/Node/NullableType.php', - 108 => 'lib/PhpParser/Node/Param.php', - 109 => 'lib/PhpParser/Node/Scalar.php', - 110 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 111 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 112 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 113 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 114 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 123 => 'lib/PhpParser/Node/Scalar/String_.php', - 124 => 'lib/PhpParser/Node/Stmt.php', - 125 => 'lib/PhpParser/Node/Stmt/Break_.php', - 126 => 'lib/PhpParser/Node/Stmt/Case_.php', - 127 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 128 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 131 => 'lib/PhpParser/Node/Stmt/Class_.php', - 132 => 'lib/PhpParser/Node/Stmt/Const_.php', - 133 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 134 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 135 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 136 => 'lib/PhpParser/Node/Stmt/Do_.php', - 137 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 138 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 139 => 'lib/PhpParser/Node/Stmt/Else_.php', - 140 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 141 => 'lib/PhpParser/Node/Stmt/For_.php', - 142 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 143 => 'lib/PhpParser/Node/Stmt/Function_.php', - 144 => 'lib/PhpParser/Node/Stmt/Global_.php', - 145 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 146 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 147 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 148 => 'lib/PhpParser/Node/Stmt/If_.php', - 149 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 150 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 151 => 'lib/PhpParser/Node/Stmt/Label.php', - 152 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 153 => 'lib/PhpParser/Node/Stmt/Nop.php', - 154 => 'lib/PhpParser/Node/Stmt/Property.php', - 155 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 156 => 'lib/PhpParser/Node/Stmt/Return_.php', - 157 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 158 => 'lib/PhpParser/Node/Stmt/Static_.php', - 159 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 160 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 161 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 165 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 166 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 167 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 168 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 169 => 'lib/PhpParser/Node/Stmt/Use_.php', - 170 => 'lib/PhpParser/Node/Stmt/While_.php', - 171 => 'lib/PhpParser/NodeDumper.php', - 172 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 173 => 'lib/PhpParser/Parser/Php5.php', - 174 => 'lib/PhpParser/Parser/Php7.php', - 175 => 'lib/PhpParser/ParserAbstract.php', - 176 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 177 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/NodeDumper.php' => - array ( - ), - 'lib/PhpParser/NodeTraverser.php' => - array ( - ), - 'lib/PhpParser/NodeTraverserInterface.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - ), - 'lib/PhpParser/NodeVisitor.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - 1 => 'lib/PhpParser/NodeTraverserInterface.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/NodeVisitorAbstract.php', - ), - 'lib/PhpParser/NodeVisitor/NameResolver.php' => - array ( - ), - 'lib/PhpParser/NodeVisitorAbstract.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - ), - 'lib/PhpParser/Parser.php' => - array ( - 0 => 'lib/PhpParser/Parser/Multiple.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Multiple.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php5.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php7.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Tokens.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/Lexer/Emulative.php', - ), - 'lib/PhpParser/ParserAbstract.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/ParserFactory.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinter/Standard.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinterAbstract.php' => - array ( - 0 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Serializer.php' => - array ( - 0 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Serializer/XML.php' => - array ( - ), - 'lib/PhpParser/Unserializer.php' => - array ( - 0 => 'lib/PhpParser/Unserializer/XML.php', - ), - 'lib/PhpParser/Unserializer/XML.php' => - array ( - ), - 'lib/bootstrap.php' => - array ( - ), -); \ No newline at end of file diff --git a/tests/e2e/resultCache_2.php b/tests/e2e/resultCache_2.php deleted file mode 100644 index 5befe26310..0000000000 --- a/tests/e2e/resultCache_2.php +++ /dev/null @@ -1,2022 +0,0 @@ - - array ( - 0 => 'lib/bootstrap.php', - ), - 'lib/PhpParser/Builder.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Class_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Declaration.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Function_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Interface_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Method.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Property.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Trait_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Use_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderFactory.php' => - array ( - ), - 'lib/PhpParser/Comment.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment/Doc.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/NodeDumper.php', - 8 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 9 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Comment/Doc.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Error.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler.php', - 1 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 2 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 3 => 'lib/PhpParser/Lexer.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/String_.php', - 6 => 'lib/PhpParser/Node/Stmt/Class_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Multiple.php', - 9 => 'lib/PhpParser/Parser/Php5.php', - 10 => 'lib/PhpParser/Parser/Php7.php', - 11 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 1 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 2 => 'lib/PhpParser/Lexer.php', - 3 => 'lib/PhpParser/Lexer/Emulative.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser.php', - 6 => 'lib/PhpParser/Parser/Multiple.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler/Collecting.php' => - array ( - ), - 'lib/PhpParser/ErrorHandler/Throwing.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Multiple.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/Lexer.php' => - array ( - 0 => 'lib/PhpParser/Lexer/Emulative.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Lexer/Emulative.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Node.php' => - array ( - 0 => 'lib/PhpParser/Builder.php', - 1 => 'lib/PhpParser/Builder/Class_.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - 13 => 'lib/PhpParser/Lexer.php', - 14 => 'lib/PhpParser/Node/Arg.php', - 15 => 'lib/PhpParser/Node/Const_.php', - 16 => 'lib/PhpParser/Node/Expr.php', - 17 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 18 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 19 => 'lib/PhpParser/Node/Expr/Array_.php', - 20 => 'lib/PhpParser/Node/Expr/Assign.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 32 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 33 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 34 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 61 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 62 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 63 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 64 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 65 => 'lib/PhpParser/Node/Expr/Cast.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 71 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 72 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 73 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 74 => 'lib/PhpParser/Node/Expr/Clone_.php', - 75 => 'lib/PhpParser/Node/Expr/Closure.php', - 76 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 77 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 78 => 'lib/PhpParser/Node/Expr/Empty_.php', - 79 => 'lib/PhpParser/Node/Expr/Error.php', - 80 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 81 => 'lib/PhpParser/Node/Expr/Eval_.php', - 82 => 'lib/PhpParser/Node/Expr/Exit_.php', - 83 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 84 => 'lib/PhpParser/Node/Expr/Include_.php', - 85 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 86 => 'lib/PhpParser/Node/Expr/Isset_.php', - 87 => 'lib/PhpParser/Node/Expr/List_.php', - 88 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 89 => 'lib/PhpParser/Node/Expr/New_.php', - 90 => 'lib/PhpParser/Node/Expr/PostDec.php', - 91 => 'lib/PhpParser/Node/Expr/PostInc.php', - 92 => 'lib/PhpParser/Node/Expr/PreDec.php', - 93 => 'lib/PhpParser/Node/Expr/PreInc.php', - 94 => 'lib/PhpParser/Node/Expr/Print_.php', - 95 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 96 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 97 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 98 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 99 => 'lib/PhpParser/Node/Expr/Ternary.php', - 100 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 101 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 102 => 'lib/PhpParser/Node/Expr/Variable.php', - 103 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 104 => 'lib/PhpParser/Node/Expr/Yield_.php', - 105 => 'lib/PhpParser/Node/FunctionLike.php', - 106 => 'lib/PhpParser/Node/Name.php', - 107 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 108 => 'lib/PhpParser/Node/Name/Relative.php', - 109 => 'lib/PhpParser/Node/NullableType.php', - 110 => 'lib/PhpParser/Node/Param.php', - 111 => 'lib/PhpParser/Node/Scalar.php', - 112 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 113 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 114 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 115 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 123 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 124 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 125 => 'lib/PhpParser/Node/Scalar/String_.php', - 126 => 'lib/PhpParser/Node/Stmt.php', - 127 => 'lib/PhpParser/Node/Stmt/Break_.php', - 128 => 'lib/PhpParser/Node/Stmt/Case_.php', - 129 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 131 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 132 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 133 => 'lib/PhpParser/Node/Stmt/Class_.php', - 134 => 'lib/PhpParser/Node/Stmt/Const_.php', - 135 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 136 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 137 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 138 => 'lib/PhpParser/Node/Stmt/Do_.php', - 139 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 140 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 141 => 'lib/PhpParser/Node/Stmt/Else_.php', - 142 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 143 => 'lib/PhpParser/Node/Stmt/For_.php', - 144 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 145 => 'lib/PhpParser/Node/Stmt/Function_.php', - 146 => 'lib/PhpParser/Node/Stmt/Global_.php', - 147 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 148 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 149 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 150 => 'lib/PhpParser/Node/Stmt/If_.php', - 151 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 152 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 153 => 'lib/PhpParser/Node/Stmt/Label.php', - 154 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 155 => 'lib/PhpParser/Node/Stmt/Nop.php', - 156 => 'lib/PhpParser/Node/Stmt/Property.php', - 157 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 158 => 'lib/PhpParser/Node/Stmt/Return_.php', - 159 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 160 => 'lib/PhpParser/Node/Stmt/Static_.php', - 161 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 162 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 165 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 166 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 167 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 168 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 169 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 170 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 171 => 'lib/PhpParser/Node/Stmt/Use_.php', - 172 => 'lib/PhpParser/Node/Stmt/While_.php', - 173 => 'lib/PhpParser/NodeAbstract.php', - 174 => 'lib/PhpParser/NodeDumper.php', - 175 => 'lib/PhpParser/NodeTraverser.php', - 176 => 'lib/PhpParser/NodeTraverserInterface.php', - 177 => 'lib/PhpParser/NodeVisitor.php', - 178 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 179 => 'lib/PhpParser/NodeVisitorAbstract.php', - 180 => 'lib/PhpParser/Parser.php', - 181 => 'lib/PhpParser/Parser/Multiple.php', - 182 => 'lib/PhpParser/Parser/Php5.php', - 183 => 'lib/PhpParser/Parser/Php7.php', - 184 => 'lib/PhpParser/ParserAbstract.php', - 185 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 186 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 187 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Node/Arg.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 1 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 2 => 'lib/PhpParser/Node/Expr/New_.php', - 3 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Const_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 1 => 'lib/PhpParser/Node/Stmt/Const_.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr.php' => - array ( - 0 => 'lib/PhpParser/Builder/Param.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Lexer.php', - 4 => 'lib/PhpParser/Node/Arg.php', - 5 => 'lib/PhpParser/Node/Const_.php', - 6 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 7 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 8 => 'lib/PhpParser/Node/Expr/Array_.php', - 9 => 'lib/PhpParser/Node/Expr/Assign.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 12 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 13 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 14 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 15 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 16 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 17 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 18 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 23 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 27 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 28 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 29 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 30 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 31 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 32 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 52 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 53 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 54 => 'lib/PhpParser/Node/Expr/Cast.php', - 55 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 56 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 57 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 58 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 59 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 60 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 61 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 62 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 63 => 'lib/PhpParser/Node/Expr/Clone_.php', - 64 => 'lib/PhpParser/Node/Expr/Closure.php', - 65 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 66 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 67 => 'lib/PhpParser/Node/Expr/Empty_.php', - 68 => 'lib/PhpParser/Node/Expr/Error.php', - 69 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 70 => 'lib/PhpParser/Node/Expr/Eval_.php', - 71 => 'lib/PhpParser/Node/Expr/Exit_.php', - 72 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 73 => 'lib/PhpParser/Node/Expr/Include_.php', - 74 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 75 => 'lib/PhpParser/Node/Expr/Isset_.php', - 76 => 'lib/PhpParser/Node/Expr/List_.php', - 77 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 78 => 'lib/PhpParser/Node/Expr/New_.php', - 79 => 'lib/PhpParser/Node/Expr/PostDec.php', - 80 => 'lib/PhpParser/Node/Expr/PostInc.php', - 81 => 'lib/PhpParser/Node/Expr/PreDec.php', - 82 => 'lib/PhpParser/Node/Expr/PreInc.php', - 83 => 'lib/PhpParser/Node/Expr/Print_.php', - 84 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 85 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 86 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 87 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 88 => 'lib/PhpParser/Node/Expr/Ternary.php', - 89 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 90 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 91 => 'lib/PhpParser/Node/Expr/Variable.php', - 92 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 93 => 'lib/PhpParser/Node/Expr/Yield_.php', - 94 => 'lib/PhpParser/Node/Param.php', - 95 => 'lib/PhpParser/Node/Scalar.php', - 96 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 97 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 98 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 99 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 100 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 101 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 102 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 103 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 104 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 105 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 106 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 107 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 108 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 109 => 'lib/PhpParser/Node/Scalar/String_.php', - 110 => 'lib/PhpParser/Node/Stmt/Break_.php', - 111 => 'lib/PhpParser/Node/Stmt/Case_.php', - 112 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 113 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 114 => 'lib/PhpParser/Node/Stmt/Do_.php', - 115 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 116 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 117 => 'lib/PhpParser/Node/Stmt/For_.php', - 118 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 119 => 'lib/PhpParser/Node/Stmt/Global_.php', - 120 => 'lib/PhpParser/Node/Stmt/If_.php', - 121 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 122 => 'lib/PhpParser/Node/Stmt/Return_.php', - 123 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 124 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 125 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 126 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 127 => 'lib/PhpParser/Node/Stmt/While_.php', - 128 => 'lib/PhpParser/NodeDumper.php', - 129 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 130 => 'lib/PhpParser/Parser/Php5.php', - 131 => 'lib/PhpParser/Parser/Php7.php', - 132 => 'lib/PhpParser/ParserAbstract.php', - 133 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 134 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Expr/ArrayDimFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ArrayItem.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Expr/Array_.php', - 2 => 'lib/PhpParser/Node/Expr/List_.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Array_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Assign.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 4 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 5 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 6 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 7 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 8 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 12 => 'lib/PhpParser/Parser/Php5.php', - 13 => 'lib/PhpParser/Parser/Php7.php', - 14 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignRef.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 4 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 5 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 6 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 7 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 8 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 9 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 10 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 11 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 12 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 13 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 14 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 15 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 19 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 20 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 21 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 22 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 27 => 'lib/PhpParser/Parser/Php5.php', - 28 => 'lib/PhpParser/Parser/Php7.php', - 29 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BitwiseNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BooleanNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 1 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 2 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 3 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 4 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 5 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 6 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Array_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Bool_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Double.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Int_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Object_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/String_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClassConstFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Clone_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Closure.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClosureUse.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Closure.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ConstFetch.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Empty_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Error.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ErrorSuppress.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Eval_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Exit_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/FuncCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Include_.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Instanceof_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Isset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/List_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/MethodCall.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/New_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Print_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ShellExec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Ternary.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryMinus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryPlus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Variable.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/YieldFrom.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Yield_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Trait_.php', - 3 => 'lib/PhpParser/Node/Expr/Closure.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 6 => 'lib/PhpParser/Node/Stmt/Function_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/ParserAbstract.php', - 11 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 11 => 'lib/PhpParser/Node/Expr/Closure.php', - 12 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 13 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 14 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 15 => 'lib/PhpParser/Node/Expr/New_.php', - 16 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 17 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 18 => 'lib/PhpParser/Node/FunctionLike.php', - 19 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 20 => 'lib/PhpParser/Node/Name/Relative.php', - 21 => 'lib/PhpParser/Node/NullableType.php', - 22 => 'lib/PhpParser/Node/Param.php', - 23 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 24 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 25 => 'lib/PhpParser/Node/Stmt/Class_.php', - 26 => 'lib/PhpParser/Node/Stmt/Function_.php', - 27 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 28 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 29 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 30 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 31 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 32 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 33 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 34 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 35 => 'lib/PhpParser/Parser/Php5.php', - 36 => 'lib/PhpParser/Parser/Php7.php', - 37 => 'lib/PhpParser/ParserAbstract.php', - 38 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 39 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Name/FullyQualified.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name/Relative.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/NullableType.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Param.php', - 4 => 'lib/PhpParser/BuilderAbstract.php', - 5 => 'lib/PhpParser/Node/Expr/Closure.php', - 6 => 'lib/PhpParser/Node/FunctionLike.php', - 7 => 'lib/PhpParser/Node/Param.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Function_.php', - 10 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Param.php', - 2 => 'lib/PhpParser/Node/Expr/Closure.php', - 3 => 'lib/PhpParser/Node/FunctionLike.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 5 => 'lib/PhpParser/Node/Stmt/Function_.php', - 6 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/ParserAbstract.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 2 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 3 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 8 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 9 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 10 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 11 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 12 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 13 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 14 => 'lib/PhpParser/Node/Scalar/String_.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/DNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/Encapsed.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/LNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/ParserAbstract.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst.php' => - array ( - 0 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 1 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 2 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 3 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 4 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/File.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Line.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Method.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/String_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Interface_.php', - 3 => 'lib/PhpParser/Builder/Method.php', - 4 => 'lib/PhpParser/Builder/Namespace_.php', - 5 => 'lib/PhpParser/Builder/Property.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/Closure.php', - 11 => 'lib/PhpParser/Node/Expr/New_.php', - 12 => 'lib/PhpParser/Node/FunctionLike.php', - 13 => 'lib/PhpParser/Node/Stmt/Break_.php', - 14 => 'lib/PhpParser/Node/Stmt/Case_.php', - 15 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 16 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 17 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 18 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 19 => 'lib/PhpParser/Node/Stmt/Class_.php', - 20 => 'lib/PhpParser/Node/Stmt/Const_.php', - 21 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 22 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 23 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 24 => 'lib/PhpParser/Node/Stmt/Do_.php', - 25 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 26 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 27 => 'lib/PhpParser/Node/Stmt/Else_.php', - 28 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 29 => 'lib/PhpParser/Node/Stmt/For_.php', - 30 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 31 => 'lib/PhpParser/Node/Stmt/Function_.php', - 32 => 'lib/PhpParser/Node/Stmt/Global_.php', - 33 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 34 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 35 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 36 => 'lib/PhpParser/Node/Stmt/If_.php', - 37 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 38 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 39 => 'lib/PhpParser/Node/Stmt/Label.php', - 40 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 41 => 'lib/PhpParser/Node/Stmt/Nop.php', - 42 => 'lib/PhpParser/Node/Stmt/Property.php', - 43 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 44 => 'lib/PhpParser/Node/Stmt/Return_.php', - 45 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 46 => 'lib/PhpParser/Node/Stmt/Static_.php', - 47 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 48 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 49 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 50 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 51 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 52 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 53 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 54 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 55 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 56 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 57 => 'lib/PhpParser/Node/Stmt/Use_.php', - 58 => 'lib/PhpParser/Node/Stmt/While_.php', - 59 => 'lib/PhpParser/NodeDumper.php', - 60 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 61 => 'lib/PhpParser/Parser/Php5.php', - 62 => 'lib/PhpParser/Parser/Php7.php', - 63 => 'lib/PhpParser/ParserAbstract.php', - 64 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 65 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Break_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Case_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Catch_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassConst.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Interface_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Property.php', - 4 => 'lib/PhpParser/Builder/Trait_.php', - 5 => 'lib/PhpParser/BuilderAbstract.php', - 6 => 'lib/PhpParser/Node/Expr/New_.php', - 7 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Class_.php', - 10 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 11 => 'lib/PhpParser/Node/Stmt/Property.php', - 12 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 13 => 'lib/PhpParser/NodeDumper.php', - 14 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassMethod.php' => - array ( - 0 => 'lib/PhpParser/Builder/Method.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/ParserAbstract.php', - 7 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Class_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Property.php', - 3 => 'lib/PhpParser/BuilderAbstract.php', - 4 => 'lib/PhpParser/Node/Expr/New_.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 6 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 7 => 'lib/PhpParser/Node/Stmt/Property.php', - 8 => 'lib/PhpParser/NodeDumper.php', - 9 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 10 => 'lib/PhpParser/Parser/Php5.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/ParserAbstract.php', - 13 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Const_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Continue_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/DeclareDeclare.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Declare_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Do_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Echo_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ElseIf_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Else_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Finally_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/For_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Foreach_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Function_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Global_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Goto_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/GroupUse.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/HaltCompiler.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/If_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/InlineHTML.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Interface_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Interface_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Label.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Namespace_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 6 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Nop.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Property.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/PropertyProperty.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Node/Stmt/Property.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Return_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/StaticVar.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Static_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Static_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Switch_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Throw_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 1 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 2 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TryCatch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/UseUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 2 => 'lib/PhpParser/Node/Stmt/Use_.php', - 3 => 'lib/PhpParser/NodeDumper.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser/Php5.php', - 6 => 'lib/PhpParser/Parser/Php7.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Use_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - 2 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 3 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 4 => 'lib/PhpParser/NodeDumper.php', - 5 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 6 => 'lib/PhpParser/Parser/Php5.php', - 7 => 'lib/PhpParser/Parser/Php7.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/While_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/NodeAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Property.php', - 8 => 'lib/PhpParser/Builder/Trait_.php', - 9 => 'lib/PhpParser/Builder/Use_.php', - 10 => 'lib/PhpParser/BuilderAbstract.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - 12 => 'lib/PhpParser/Lexer.php', - 13 => 'lib/PhpParser/Node/Arg.php', - 14 => 'lib/PhpParser/Node/Const_.php', - 15 => 'lib/PhpParser/Node/Expr.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 17 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 18 => 'lib/PhpParser/Node/Expr/Array_.php', - 19 => 'lib/PhpParser/Node/Expr/Assign.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 32 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 33 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 61 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 62 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 63 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 64 => 'lib/PhpParser/Node/Expr/Cast.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 71 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 72 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 73 => 'lib/PhpParser/Node/Expr/Clone_.php', - 74 => 'lib/PhpParser/Node/Expr/Closure.php', - 75 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 76 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 77 => 'lib/PhpParser/Node/Expr/Empty_.php', - 78 => 'lib/PhpParser/Node/Expr/Error.php', - 79 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 80 => 'lib/PhpParser/Node/Expr/Eval_.php', - 81 => 'lib/PhpParser/Node/Expr/Exit_.php', - 82 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 83 => 'lib/PhpParser/Node/Expr/Include_.php', - 84 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 85 => 'lib/PhpParser/Node/Expr/Isset_.php', - 86 => 'lib/PhpParser/Node/Expr/List_.php', - 87 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 88 => 'lib/PhpParser/Node/Expr/New_.php', - 89 => 'lib/PhpParser/Node/Expr/PostDec.php', - 90 => 'lib/PhpParser/Node/Expr/PostInc.php', - 91 => 'lib/PhpParser/Node/Expr/PreDec.php', - 92 => 'lib/PhpParser/Node/Expr/PreInc.php', - 93 => 'lib/PhpParser/Node/Expr/Print_.php', - 94 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 95 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 96 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 97 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 98 => 'lib/PhpParser/Node/Expr/Ternary.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 100 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 101 => 'lib/PhpParser/Node/Expr/Variable.php', - 102 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 103 => 'lib/PhpParser/Node/Expr/Yield_.php', - 104 => 'lib/PhpParser/Node/FunctionLike.php', - 105 => 'lib/PhpParser/Node/Name.php', - 106 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 107 => 'lib/PhpParser/Node/Name/Relative.php', - 108 => 'lib/PhpParser/Node/NullableType.php', - 109 => 'lib/PhpParser/Node/Param.php', - 110 => 'lib/PhpParser/Node/Scalar.php', - 111 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 112 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 113 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 114 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 123 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 124 => 'lib/PhpParser/Node/Scalar/String_.php', - 125 => 'lib/PhpParser/Node/Stmt.php', - 126 => 'lib/PhpParser/Node/Stmt/Break_.php', - 127 => 'lib/PhpParser/Node/Stmt/Case_.php', - 128 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 131 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 132 => 'lib/PhpParser/Node/Stmt/Class_.php', - 133 => 'lib/PhpParser/Node/Stmt/Const_.php', - 134 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 135 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 136 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 137 => 'lib/PhpParser/Node/Stmt/Do_.php', - 138 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 139 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 140 => 'lib/PhpParser/Node/Stmt/Else_.php', - 141 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 142 => 'lib/PhpParser/Node/Stmt/For_.php', - 143 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 144 => 'lib/PhpParser/Node/Stmt/Function_.php', - 145 => 'lib/PhpParser/Node/Stmt/Global_.php', - 146 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 147 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 148 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 149 => 'lib/PhpParser/Node/Stmt/If_.php', - 150 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 151 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 152 => 'lib/PhpParser/Node/Stmt/Label.php', - 153 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 154 => 'lib/PhpParser/Node/Stmt/Nop.php', - 155 => 'lib/PhpParser/Node/Stmt/Property.php', - 156 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 157 => 'lib/PhpParser/Node/Stmt/Return_.php', - 158 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 159 => 'lib/PhpParser/Node/Stmt/Static_.php', - 160 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 161 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 165 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 166 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 167 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 168 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 169 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 170 => 'lib/PhpParser/Node/Stmt/Use_.php', - 171 => 'lib/PhpParser/Node/Stmt/While_.php', - 172 => 'lib/PhpParser/NodeDumper.php', - 173 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 174 => 'lib/PhpParser/Parser/Php5.php', - 175 => 'lib/PhpParser/Parser/Php7.php', - 176 => 'lib/PhpParser/ParserAbstract.php', - 177 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 178 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/NodeDumper.php' => - array ( - ), - 'lib/PhpParser/NodeTraverser.php' => - array ( - ), - 'lib/PhpParser/NodeTraverserInterface.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - ), - 'lib/PhpParser/NodeVisitor.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - 1 => 'lib/PhpParser/NodeTraverserInterface.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/NodeVisitorAbstract.php', - ), - 'lib/PhpParser/NodeVisitor/NameResolver.php' => - array ( - ), - 'lib/PhpParser/NodeVisitorAbstract.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - ), - 'lib/PhpParser/Parser.php' => - array ( - 0 => 'lib/PhpParser/Parser/Multiple.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Multiple.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php5.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php7.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Tokens.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/Lexer/Emulative.php', - ), - 'lib/PhpParser/ParserAbstract.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/ParserFactory.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinter/Standard.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinterAbstract.php' => - array ( - 0 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Serializer.php' => - array ( - 0 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Serializer/XML.php' => - array ( - ), - 'lib/PhpParser/Unserializer.php' => - array ( - 0 => 'lib/PhpParser/Unserializer/XML.php', - ), - 'lib/PhpParser/Unserializer/XML.php' => - array ( - ), - 'lib/bootstrap.php' => - array ( - ), -); \ No newline at end of file diff --git a/tests/e2e/resultCache_3.php b/tests/e2e/resultCache_3.php deleted file mode 100644 index ccd09a149b..0000000000 --- a/tests/e2e/resultCache_3.php +++ /dev/null @@ -1,2015 +0,0 @@ - - array ( - 0 => 'lib/bootstrap.php', - ), - 'lib/PhpParser/Builder.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Class_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Declaration.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Function_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Interface_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Method.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Property.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Trait_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/Builder/Use_.php' => - array ( - 0 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Declaration.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - ), - 'lib/PhpParser/BuilderFactory.php' => - array ( - ), - 'lib/PhpParser/Comment.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment/Doc.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/NodeDumper.php', - 8 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 9 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Comment/Doc.php' => - array ( - 0 => 'lib/PhpParser/Builder/Declaration.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Comment.php', - 4 => 'lib/PhpParser/Lexer.php', - 5 => 'lib/PhpParser/Node.php', - 6 => 'lib/PhpParser/NodeAbstract.php', - 7 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Error.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler.php', - 1 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 2 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 3 => 'lib/PhpParser/Lexer.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/String_.php', - 6 => 'lib/PhpParser/Node/Stmt/Class_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Multiple.php', - 9 => 'lib/PhpParser/Parser/Php5.php', - 10 => 'lib/PhpParser/Parser/Php7.php', - 11 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler.php' => - array ( - 0 => 'lib/PhpParser/ErrorHandler/Collecting.php', - 1 => 'lib/PhpParser/ErrorHandler/Throwing.php', - 2 => 'lib/PhpParser/Lexer.php', - 3 => 'lib/PhpParser/Lexer/Emulative.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser.php', - 6 => 'lib/PhpParser/Parser/Multiple.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/ErrorHandler/Collecting.php' => - array ( - ), - 'lib/PhpParser/ErrorHandler/Throwing.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Multiple.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - ), - 'lib/PhpParser/Lexer.php' => - array ( - 0 => 'lib/PhpParser/Lexer/Emulative.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Lexer/Emulative.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Node.php' => - array ( - 0 => 'lib/PhpParser/Builder.php', - 1 => 'lib/PhpParser/Builder/Class_.php', - 2 => 'lib/PhpParser/Builder/FunctionLike.php', - 3 => 'lib/PhpParser/Builder/Function_.php', - 4 => 'lib/PhpParser/Builder/Interface_.php', - 5 => 'lib/PhpParser/Builder/Method.php', - 6 => 'lib/PhpParser/Builder/Namespace_.php', - 7 => 'lib/PhpParser/Builder/Param.php', - 8 => 'lib/PhpParser/Builder/Property.php', - 9 => 'lib/PhpParser/Builder/Trait_.php', - 10 => 'lib/PhpParser/Builder/Use_.php', - 11 => 'lib/PhpParser/BuilderAbstract.php', - 12 => 'lib/PhpParser/BuilderFactory.php', - 13 => 'lib/PhpParser/Node/Arg.php', - 14 => 'lib/PhpParser/Node/Const_.php', - 15 => 'lib/PhpParser/Node/Expr.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 17 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 18 => 'lib/PhpParser/Node/Expr/Array_.php', - 19 => 'lib/PhpParser/Node/Expr/Assign.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 32 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 33 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 61 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 62 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 63 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 64 => 'lib/PhpParser/Node/Expr/Cast.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 71 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 72 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 73 => 'lib/PhpParser/Node/Expr/Clone_.php', - 74 => 'lib/PhpParser/Node/Expr/Closure.php', - 75 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 76 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 77 => 'lib/PhpParser/Node/Expr/Empty_.php', - 78 => 'lib/PhpParser/Node/Expr/Error.php', - 79 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 80 => 'lib/PhpParser/Node/Expr/Eval_.php', - 81 => 'lib/PhpParser/Node/Expr/Exit_.php', - 82 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 83 => 'lib/PhpParser/Node/Expr/Include_.php', - 84 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 85 => 'lib/PhpParser/Node/Expr/Isset_.php', - 86 => 'lib/PhpParser/Node/Expr/List_.php', - 87 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 88 => 'lib/PhpParser/Node/Expr/New_.php', - 89 => 'lib/PhpParser/Node/Expr/PostDec.php', - 90 => 'lib/PhpParser/Node/Expr/PostInc.php', - 91 => 'lib/PhpParser/Node/Expr/PreDec.php', - 92 => 'lib/PhpParser/Node/Expr/PreInc.php', - 93 => 'lib/PhpParser/Node/Expr/Print_.php', - 94 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 95 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 96 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 97 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 98 => 'lib/PhpParser/Node/Expr/Ternary.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 100 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 101 => 'lib/PhpParser/Node/Expr/Variable.php', - 102 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 103 => 'lib/PhpParser/Node/Expr/Yield_.php', - 104 => 'lib/PhpParser/Node/FunctionLike.php', - 105 => 'lib/PhpParser/Node/Name.php', - 106 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 107 => 'lib/PhpParser/Node/Name/Relative.php', - 108 => 'lib/PhpParser/Node/NullableType.php', - 109 => 'lib/PhpParser/Node/Param.php', - 110 => 'lib/PhpParser/Node/Scalar.php', - 111 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 112 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 113 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 114 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 123 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 124 => 'lib/PhpParser/Node/Scalar/String_.php', - 125 => 'lib/PhpParser/Node/Stmt.php', - 126 => 'lib/PhpParser/Node/Stmt/Break_.php', - 127 => 'lib/PhpParser/Node/Stmt/Case_.php', - 128 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 131 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 132 => 'lib/PhpParser/Node/Stmt/Class_.php', - 133 => 'lib/PhpParser/Node/Stmt/Const_.php', - 134 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 135 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 136 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 137 => 'lib/PhpParser/Node/Stmt/Do_.php', - 138 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 139 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 140 => 'lib/PhpParser/Node/Stmt/Else_.php', - 141 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 142 => 'lib/PhpParser/Node/Stmt/For_.php', - 143 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 144 => 'lib/PhpParser/Node/Stmt/Function_.php', - 145 => 'lib/PhpParser/Node/Stmt/Global_.php', - 146 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 147 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 148 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 149 => 'lib/PhpParser/Node/Stmt/If_.php', - 150 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 151 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 152 => 'lib/PhpParser/Node/Stmt/Label.php', - 153 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 154 => 'lib/PhpParser/Node/Stmt/Nop.php', - 155 => 'lib/PhpParser/Node/Stmt/Property.php', - 156 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 157 => 'lib/PhpParser/Node/Stmt/Return_.php', - 158 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 159 => 'lib/PhpParser/Node/Stmt/Static_.php', - 160 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 161 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 165 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 166 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 167 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 168 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 169 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 170 => 'lib/PhpParser/Node/Stmt/Use_.php', - 171 => 'lib/PhpParser/Node/Stmt/While_.php', - 172 => 'lib/PhpParser/NodeAbstract.php', - 173 => 'lib/PhpParser/NodeDumper.php', - 174 => 'lib/PhpParser/NodeTraverser.php', - 175 => 'lib/PhpParser/NodeTraverserInterface.php', - 176 => 'lib/PhpParser/NodeVisitor.php', - 177 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 178 => 'lib/PhpParser/NodeVisitorAbstract.php', - 179 => 'lib/PhpParser/Parser.php', - 180 => 'lib/PhpParser/Parser/Multiple.php', - 181 => 'lib/PhpParser/Parser/Php5.php', - 182 => 'lib/PhpParser/Parser/Php7.php', - 183 => 'lib/PhpParser/ParserAbstract.php', - 184 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 185 => 'lib/PhpParser/PrettyPrinterAbstract.php', - 186 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Node/Arg.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 1 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 2 => 'lib/PhpParser/Node/Expr/New_.php', - 3 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Const_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 1 => 'lib/PhpParser/Node/Stmt/Const_.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr.php' => - array ( - 0 => 'lib/PhpParser/Builder/Param.php', - 1 => 'lib/PhpParser/Builder/Property.php', - 2 => 'lib/PhpParser/BuilderAbstract.php', - 3 => 'lib/PhpParser/Node/Arg.php', - 4 => 'lib/PhpParser/Node/Const_.php', - 5 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 6 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 7 => 'lib/PhpParser/Node/Expr/Array_.php', - 8 => 'lib/PhpParser/Node/Expr/Assign.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 12 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 13 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 14 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 15 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 22 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 27 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 28 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 29 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 30 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 31 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 32 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 51 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 52 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 53 => 'lib/PhpParser/Node/Expr/Cast.php', - 54 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 55 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 56 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 57 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 58 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 59 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 60 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 61 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 62 => 'lib/PhpParser/Node/Expr/Clone_.php', - 63 => 'lib/PhpParser/Node/Expr/Closure.php', - 64 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 65 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 66 => 'lib/PhpParser/Node/Expr/Empty_.php', - 67 => 'lib/PhpParser/Node/Expr/Error.php', - 68 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 69 => 'lib/PhpParser/Node/Expr/Eval_.php', - 70 => 'lib/PhpParser/Node/Expr/Exit_.php', - 71 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 72 => 'lib/PhpParser/Node/Expr/Include_.php', - 73 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 74 => 'lib/PhpParser/Node/Expr/Isset_.php', - 75 => 'lib/PhpParser/Node/Expr/List_.php', - 76 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 77 => 'lib/PhpParser/Node/Expr/New_.php', - 78 => 'lib/PhpParser/Node/Expr/PostDec.php', - 79 => 'lib/PhpParser/Node/Expr/PostInc.php', - 80 => 'lib/PhpParser/Node/Expr/PreDec.php', - 81 => 'lib/PhpParser/Node/Expr/PreInc.php', - 82 => 'lib/PhpParser/Node/Expr/Print_.php', - 83 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 84 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 85 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 86 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 87 => 'lib/PhpParser/Node/Expr/Ternary.php', - 88 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 89 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 90 => 'lib/PhpParser/Node/Expr/Variable.php', - 91 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 92 => 'lib/PhpParser/Node/Expr/Yield_.php', - 93 => 'lib/PhpParser/Node/Param.php', - 94 => 'lib/PhpParser/Node/Scalar.php', - 95 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 96 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 97 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 98 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 99 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 100 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 101 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 102 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 103 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 104 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 105 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 106 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 107 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 108 => 'lib/PhpParser/Node/Scalar/String_.php', - 109 => 'lib/PhpParser/Node/Stmt/Break_.php', - 110 => 'lib/PhpParser/Node/Stmt/Case_.php', - 111 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 112 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 113 => 'lib/PhpParser/Node/Stmt/Do_.php', - 114 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 115 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 116 => 'lib/PhpParser/Node/Stmt/For_.php', - 117 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 118 => 'lib/PhpParser/Node/Stmt/Global_.php', - 119 => 'lib/PhpParser/Node/Stmt/If_.php', - 120 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 121 => 'lib/PhpParser/Node/Stmt/Return_.php', - 122 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 123 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 124 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 125 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 126 => 'lib/PhpParser/Node/Stmt/While_.php', - 127 => 'lib/PhpParser/NodeDumper.php', - 128 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 129 => 'lib/PhpParser/Parser/Php5.php', - 130 => 'lib/PhpParser/Parser/Php7.php', - 131 => 'lib/PhpParser/ParserAbstract.php', - 132 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 133 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Expr/ArrayDimFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ArrayItem.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Expr/Array_.php', - 2 => 'lib/PhpParser/Node/Expr/List_.php', - 3 => 'lib/PhpParser/Parser/Php5.php', - 4 => 'lib/PhpParser/Parser/Php7.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Array_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Assign.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 4 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 5 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 6 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 7 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 8 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 9 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 10 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 11 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 12 => 'lib/PhpParser/Parser/Php5.php', - 13 => 'lib/PhpParser/Parser/Php7.php', - 14 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/AssignRef.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 1 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 2 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 3 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 4 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 5 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 6 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 7 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 8 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 9 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 10 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 11 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 12 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 13 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 14 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 15 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 16 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 17 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 18 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 19 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 20 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 21 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 22 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 23 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 24 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 25 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 26 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 27 => 'lib/PhpParser/Parser/Php5.php', - 28 => 'lib/PhpParser/Parser/Php7.php', - 29 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Div.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BitwiseNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/BooleanNot.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 1 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 2 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 3 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 4 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 5 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 6 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Array_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Bool_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Double.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Int_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Object_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/String_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Cast/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClassConstFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Clone_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Closure.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ClosureUse.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/Closure.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ConstFetch.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Empty_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Error.php' => - array ( - 0 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ErrorSuppress.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Eval_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Exit_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/FuncCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Include_.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Instanceof_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Isset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/List_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/MethodCall.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/New_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PostInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreDec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PreInc.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Print_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/PropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/ShellExec.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticCall.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Ternary.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryMinus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/UnaryPlus.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Variable.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/YieldFrom.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Expr/Yield_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/FunctionLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Trait_.php', - 3 => 'lib/PhpParser/Node/Expr/Closure.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 6 => 'lib/PhpParser/Node/Stmt/Function_.php', - 7 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/ParserAbstract.php', - 11 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 11 => 'lib/PhpParser/Node/Expr/Closure.php', - 12 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 13 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 14 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 15 => 'lib/PhpParser/Node/Expr/New_.php', - 16 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 17 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 18 => 'lib/PhpParser/Node/FunctionLike.php', - 19 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 20 => 'lib/PhpParser/Node/Name/Relative.php', - 21 => 'lib/PhpParser/Node/NullableType.php', - 22 => 'lib/PhpParser/Node/Param.php', - 23 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 24 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 25 => 'lib/PhpParser/Node/Stmt/Class_.php', - 26 => 'lib/PhpParser/Node/Stmt/Function_.php', - 27 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 28 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 29 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 30 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 31 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 32 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 33 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 34 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 35 => 'lib/PhpParser/Parser/Php5.php', - 36 => 'lib/PhpParser/Parser/Php7.php', - 37 => 'lib/PhpParser/ParserAbstract.php', - 38 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 39 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Name/FullyQualified.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Name/Relative.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/NullableType.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Param.php', - 4 => 'lib/PhpParser/BuilderAbstract.php', - 5 => 'lib/PhpParser/Node/Expr/Closure.php', - 6 => 'lib/PhpParser/Node/FunctionLike.php', - 7 => 'lib/PhpParser/Node/Param.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Function_.php', - 10 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Param.php' => - array ( - 0 => 'lib/PhpParser/Builder/FunctionLike.php', - 1 => 'lib/PhpParser/Builder/Param.php', - 2 => 'lib/PhpParser/Node/Expr/Closure.php', - 3 => 'lib/PhpParser/Node/FunctionLike.php', - 4 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 5 => 'lib/PhpParser/Node/Stmt/Function_.php', - 6 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 7 => 'lib/PhpParser/Parser/Php5.php', - 8 => 'lib/PhpParser/Parser/Php7.php', - 9 => 'lib/PhpParser/ParserAbstract.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 2 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 3 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 4 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 8 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 9 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 10 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 11 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 12 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 13 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 14 => 'lib/PhpParser/Node/Scalar/String_.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/DNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/Encapsed.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/LNumber.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/ParserAbstract.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst.php' => - array ( - 0 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 1 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 2 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 3 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 4 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 5 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 6 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 7 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 8 => 'lib/PhpParser/Parser/Php5.php', - 9 => 'lib/PhpParser/Parser/Php7.php', - 10 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/File.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Line.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Method.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Scalar/String_.php' => - array ( - 0 => 'lib/PhpParser/BuilderAbstract.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Function_.php', - 2 => 'lib/PhpParser/Builder/Interface_.php', - 3 => 'lib/PhpParser/Builder/Method.php', - 4 => 'lib/PhpParser/Builder/Namespace_.php', - 5 => 'lib/PhpParser/Builder/Property.php', - 6 => 'lib/PhpParser/Builder/Trait_.php', - 7 => 'lib/PhpParser/Builder/Use_.php', - 8 => 'lib/PhpParser/BuilderAbstract.php', - 9 => 'lib/PhpParser/BuilderFactory.php', - 10 => 'lib/PhpParser/Node/Expr/Closure.php', - 11 => 'lib/PhpParser/Node/Expr/New_.php', - 12 => 'lib/PhpParser/Node/FunctionLike.php', - 13 => 'lib/PhpParser/Node/Stmt/Break_.php', - 14 => 'lib/PhpParser/Node/Stmt/Case_.php', - 15 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 16 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 17 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 18 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 19 => 'lib/PhpParser/Node/Stmt/Class_.php', - 20 => 'lib/PhpParser/Node/Stmt/Const_.php', - 21 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 22 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 23 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 24 => 'lib/PhpParser/Node/Stmt/Do_.php', - 25 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 26 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 27 => 'lib/PhpParser/Node/Stmt/Else_.php', - 28 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 29 => 'lib/PhpParser/Node/Stmt/For_.php', - 30 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 31 => 'lib/PhpParser/Node/Stmt/Function_.php', - 32 => 'lib/PhpParser/Node/Stmt/Global_.php', - 33 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 34 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 35 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 36 => 'lib/PhpParser/Node/Stmt/If_.php', - 37 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 38 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 39 => 'lib/PhpParser/Node/Stmt/Label.php', - 40 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 41 => 'lib/PhpParser/Node/Stmt/Nop.php', - 42 => 'lib/PhpParser/Node/Stmt/Property.php', - 43 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 44 => 'lib/PhpParser/Node/Stmt/Return_.php', - 45 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 46 => 'lib/PhpParser/Node/Stmt/Static_.php', - 47 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 48 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 49 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 50 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 51 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 52 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 53 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 54 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 55 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 56 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 57 => 'lib/PhpParser/Node/Stmt/Use_.php', - 58 => 'lib/PhpParser/Node/Stmt/While_.php', - 59 => 'lib/PhpParser/NodeDumper.php', - 60 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 61 => 'lib/PhpParser/Parser/Php5.php', - 62 => 'lib/PhpParser/Parser/Php7.php', - 63 => 'lib/PhpParser/ParserAbstract.php', - 64 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 65 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Break_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Case_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Catch_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassConst.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassLike.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Interface_.php', - 2 => 'lib/PhpParser/Builder/Method.php', - 3 => 'lib/PhpParser/Builder/Property.php', - 4 => 'lib/PhpParser/Builder/Trait_.php', - 5 => 'lib/PhpParser/BuilderAbstract.php', - 6 => 'lib/PhpParser/Node/Expr/New_.php', - 7 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 8 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 9 => 'lib/PhpParser/Node/Stmt/Class_.php', - 10 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 11 => 'lib/PhpParser/Node/Stmt/Property.php', - 12 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 13 => 'lib/PhpParser/NodeDumper.php', - 14 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 15 => 'lib/PhpParser/Parser/Php5.php', - 16 => 'lib/PhpParser/Parser/Php7.php', - 17 => 'lib/PhpParser/ParserAbstract.php', - 18 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ClassMethod.php' => - array ( - 0 => 'lib/PhpParser/Builder/Method.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/ParserAbstract.php', - 7 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Class_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/Method.php', - 2 => 'lib/PhpParser/Builder/Property.php', - 3 => 'lib/PhpParser/BuilderAbstract.php', - 4 => 'lib/PhpParser/Node/Expr/New_.php', - 5 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 6 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 7 => 'lib/PhpParser/Node/Stmt/Property.php', - 8 => 'lib/PhpParser/NodeDumper.php', - 9 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 10 => 'lib/PhpParser/Parser/Php5.php', - 11 => 'lib/PhpParser/Parser/Php7.php', - 12 => 'lib/PhpParser/ParserAbstract.php', - 13 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Const_.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Continue_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/DeclareDeclare.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Declare_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Do_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Echo_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/ElseIf_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Else_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/If_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Finally_.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/For_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Foreach_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Function_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Function_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Global_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Goto_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/GroupUse.php' => - array ( - 0 => 'lib/PhpParser/NodeDumper.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/HaltCompiler.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/If_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/InlineHTML.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Interface_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Interface_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Label.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Namespace_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Namespace_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 6 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Nop.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 4 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/Node/Stmt/Property.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Builder/Trait_.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/ParserAbstract.php', - 5 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/PropertyProperty.php' => - array ( - 0 => 'lib/PhpParser/Builder/Property.php', - 1 => 'lib/PhpParser/Node/Stmt/Property.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Return_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/StaticVar.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/Static_.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Static_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Switch_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Throw_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php' => - array ( - 0 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 1 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 2 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 3 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 4 => 'lib/PhpParser/Parser/Php5.php', - 5 => 'lib/PhpParser/Parser/Php7.php', - 6 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Trait_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Trait_.php', - 1 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 2 => 'lib/PhpParser/Parser/Php5.php', - 3 => 'lib/PhpParser/Parser/Php7.php', - 4 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/TryCatch.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserAbstract.php', - 3 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Unset_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/UseUse.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 2 => 'lib/PhpParser/Node/Stmt/Use_.php', - 3 => 'lib/PhpParser/NodeDumper.php', - 4 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 5 => 'lib/PhpParser/Parser/Php5.php', - 6 => 'lib/PhpParser/Parser/Php7.php', - 7 => 'lib/PhpParser/ParserAbstract.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/Use_.php' => - array ( - 0 => 'lib/PhpParser/Builder/Use_.php', - 1 => 'lib/PhpParser/BuilderFactory.php', - 2 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 3 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 4 => 'lib/PhpParser/NodeDumper.php', - 5 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 6 => 'lib/PhpParser/Parser/Php5.php', - 7 => 'lib/PhpParser/Parser/Php7.php', - 8 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Node/Stmt/While_.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/NodeAbstract.php' => - array ( - 0 => 'lib/PhpParser/Builder/Class_.php', - 1 => 'lib/PhpParser/Builder/FunctionLike.php', - 2 => 'lib/PhpParser/Builder/Function_.php', - 3 => 'lib/PhpParser/Builder/Interface_.php', - 4 => 'lib/PhpParser/Builder/Method.php', - 5 => 'lib/PhpParser/Builder/Namespace_.php', - 6 => 'lib/PhpParser/Builder/Param.php', - 7 => 'lib/PhpParser/Builder/Property.php', - 8 => 'lib/PhpParser/Builder/Trait_.php', - 9 => 'lib/PhpParser/Builder/Use_.php', - 10 => 'lib/PhpParser/BuilderAbstract.php', - 11 => 'lib/PhpParser/BuilderFactory.php', - 12 => 'lib/PhpParser/Node/Arg.php', - 13 => 'lib/PhpParser/Node/Const_.php', - 14 => 'lib/PhpParser/Node/Expr.php', - 15 => 'lib/PhpParser/Node/Expr/ArrayDimFetch.php', - 16 => 'lib/PhpParser/Node/Expr/ArrayItem.php', - 17 => 'lib/PhpParser/Node/Expr/Array_.php', - 18 => 'lib/PhpParser/Node/Expr/Assign.php', - 19 => 'lib/PhpParser/Node/Expr/AssignOp.php', - 20 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.php', - 21 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.php', - 22 => 'lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.php', - 23 => 'lib/PhpParser/Node/Expr/AssignOp/Concat.php', - 24 => 'lib/PhpParser/Node/Expr/AssignOp/Div.php', - 25 => 'lib/PhpParser/Node/Expr/AssignOp/Minus.php', - 26 => 'lib/PhpParser/Node/Expr/AssignOp/Mod.php', - 27 => 'lib/PhpParser/Node/Expr/AssignOp/Mul.php', - 28 => 'lib/PhpParser/Node/Expr/AssignOp/Plus.php', - 29 => 'lib/PhpParser/Node/Expr/AssignOp/Pow.php', - 30 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.php', - 31 => 'lib/PhpParser/Node/Expr/AssignOp/ShiftRight.php', - 32 => 'lib/PhpParser/Node/Expr/AssignRef.php', - 33 => 'lib/PhpParser/Node/Expr/BinaryOp.php', - 34 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.php', - 35 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.php', - 36 => 'lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.php', - 37 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.php', - 38 => 'lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.php', - 39 => 'lib/PhpParser/Node/Expr/BinaryOp/Coalesce.php', - 40 => 'lib/PhpParser/Node/Expr/BinaryOp/Concat.php', - 41 => 'lib/PhpParser/Node/Expr/BinaryOp/Div.php', - 42 => 'lib/PhpParser/Node/Expr/BinaryOp/Equal.php', - 43 => 'lib/PhpParser/Node/Expr/BinaryOp/Greater.php', - 44 => 'lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php', - 45 => 'lib/PhpParser/Node/Expr/BinaryOp/Identical.php', - 46 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.php', - 47 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.php', - 48 => 'lib/PhpParser/Node/Expr/BinaryOp/LogicalXor.php', - 49 => 'lib/PhpParser/Node/Expr/BinaryOp/Minus.php', - 50 => 'lib/PhpParser/Node/Expr/BinaryOp/Mod.php', - 51 => 'lib/PhpParser/Node/Expr/BinaryOp/Mul.php', - 52 => 'lib/PhpParser/Node/Expr/BinaryOp/NotEqual.php', - 53 => 'lib/PhpParser/Node/Expr/BinaryOp/NotIdentical.php', - 54 => 'lib/PhpParser/Node/Expr/BinaryOp/Plus.php', - 55 => 'lib/PhpParser/Node/Expr/BinaryOp/Pow.php', - 56 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftLeft.php', - 57 => 'lib/PhpParser/Node/Expr/BinaryOp/ShiftRight.php', - 58 => 'lib/PhpParser/Node/Expr/BinaryOp/Smaller.php', - 59 => 'lib/PhpParser/Node/Expr/BinaryOp/SmallerOrEqual.php', - 60 => 'lib/PhpParser/Node/Expr/BinaryOp/Spaceship.php', - 61 => 'lib/PhpParser/Node/Expr/BitwiseNot.php', - 62 => 'lib/PhpParser/Node/Expr/BooleanNot.php', - 63 => 'lib/PhpParser/Node/Expr/Cast.php', - 64 => 'lib/PhpParser/Node/Expr/Cast/Array_.php', - 65 => 'lib/PhpParser/Node/Expr/Cast/Bool_.php', - 66 => 'lib/PhpParser/Node/Expr/Cast/Double.php', - 67 => 'lib/PhpParser/Node/Expr/Cast/Int_.php', - 68 => 'lib/PhpParser/Node/Expr/Cast/Object_.php', - 69 => 'lib/PhpParser/Node/Expr/Cast/String_.php', - 70 => 'lib/PhpParser/Node/Expr/Cast/Unset_.php', - 71 => 'lib/PhpParser/Node/Expr/ClassConstFetch.php', - 72 => 'lib/PhpParser/Node/Expr/Clone_.php', - 73 => 'lib/PhpParser/Node/Expr/Closure.php', - 74 => 'lib/PhpParser/Node/Expr/ClosureUse.php', - 75 => 'lib/PhpParser/Node/Expr/ConstFetch.php', - 76 => 'lib/PhpParser/Node/Expr/Empty_.php', - 77 => 'lib/PhpParser/Node/Expr/Error.php', - 78 => 'lib/PhpParser/Node/Expr/ErrorSuppress.php', - 79 => 'lib/PhpParser/Node/Expr/Eval_.php', - 80 => 'lib/PhpParser/Node/Expr/Exit_.php', - 81 => 'lib/PhpParser/Node/Expr/FuncCall.php', - 82 => 'lib/PhpParser/Node/Expr/Include_.php', - 83 => 'lib/PhpParser/Node/Expr/Instanceof_.php', - 84 => 'lib/PhpParser/Node/Expr/Isset_.php', - 85 => 'lib/PhpParser/Node/Expr/List_.php', - 86 => 'lib/PhpParser/Node/Expr/MethodCall.php', - 87 => 'lib/PhpParser/Node/Expr/New_.php', - 88 => 'lib/PhpParser/Node/Expr/PostDec.php', - 89 => 'lib/PhpParser/Node/Expr/PostInc.php', - 90 => 'lib/PhpParser/Node/Expr/PreDec.php', - 91 => 'lib/PhpParser/Node/Expr/PreInc.php', - 92 => 'lib/PhpParser/Node/Expr/Print_.php', - 93 => 'lib/PhpParser/Node/Expr/PropertyFetch.php', - 94 => 'lib/PhpParser/Node/Expr/ShellExec.php', - 95 => 'lib/PhpParser/Node/Expr/StaticCall.php', - 96 => 'lib/PhpParser/Node/Expr/StaticPropertyFetch.php', - 97 => 'lib/PhpParser/Node/Expr/Ternary.php', - 98 => 'lib/PhpParser/Node/Expr/UnaryMinus.php', - 99 => 'lib/PhpParser/Node/Expr/UnaryPlus.php', - 100 => 'lib/PhpParser/Node/Expr/Variable.php', - 101 => 'lib/PhpParser/Node/Expr/YieldFrom.php', - 102 => 'lib/PhpParser/Node/Expr/Yield_.php', - 103 => 'lib/PhpParser/Node/FunctionLike.php', - 104 => 'lib/PhpParser/Node/Name.php', - 105 => 'lib/PhpParser/Node/Name/FullyQualified.php', - 106 => 'lib/PhpParser/Node/Name/Relative.php', - 107 => 'lib/PhpParser/Node/NullableType.php', - 108 => 'lib/PhpParser/Node/Param.php', - 109 => 'lib/PhpParser/Node/Scalar.php', - 110 => 'lib/PhpParser/Node/Scalar/DNumber.php', - 111 => 'lib/PhpParser/Node/Scalar/Encapsed.php', - 112 => 'lib/PhpParser/Node/Scalar/EncapsedStringPart.php', - 113 => 'lib/PhpParser/Node/Scalar/LNumber.php', - 114 => 'lib/PhpParser/Node/Scalar/MagicConst.php', - 115 => 'lib/PhpParser/Node/Scalar/MagicConst/Class_.php', - 116 => 'lib/PhpParser/Node/Scalar/MagicConst/Dir.php', - 117 => 'lib/PhpParser/Node/Scalar/MagicConst/File.php', - 118 => 'lib/PhpParser/Node/Scalar/MagicConst/Function_.php', - 119 => 'lib/PhpParser/Node/Scalar/MagicConst/Line.php', - 120 => 'lib/PhpParser/Node/Scalar/MagicConst/Method.php', - 121 => 'lib/PhpParser/Node/Scalar/MagicConst/Namespace_.php', - 122 => 'lib/PhpParser/Node/Scalar/MagicConst/Trait_.php', - 123 => 'lib/PhpParser/Node/Scalar/String_.php', - 124 => 'lib/PhpParser/Node/Stmt.php', - 125 => 'lib/PhpParser/Node/Stmt/Break_.php', - 126 => 'lib/PhpParser/Node/Stmt/Case_.php', - 127 => 'lib/PhpParser/Node/Stmt/Catch_.php', - 128 => 'lib/PhpParser/Node/Stmt/ClassConst.php', - 129 => 'lib/PhpParser/Node/Stmt/ClassLike.php', - 130 => 'lib/PhpParser/Node/Stmt/ClassMethod.php', - 131 => 'lib/PhpParser/Node/Stmt/Class_.php', - 132 => 'lib/PhpParser/Node/Stmt/Const_.php', - 133 => 'lib/PhpParser/Node/Stmt/Continue_.php', - 134 => 'lib/PhpParser/Node/Stmt/DeclareDeclare.php', - 135 => 'lib/PhpParser/Node/Stmt/Declare_.php', - 136 => 'lib/PhpParser/Node/Stmt/Do_.php', - 137 => 'lib/PhpParser/Node/Stmt/Echo_.php', - 138 => 'lib/PhpParser/Node/Stmt/ElseIf_.php', - 139 => 'lib/PhpParser/Node/Stmt/Else_.php', - 140 => 'lib/PhpParser/Node/Stmt/Finally_.php', - 141 => 'lib/PhpParser/Node/Stmt/For_.php', - 142 => 'lib/PhpParser/Node/Stmt/Foreach_.php', - 143 => 'lib/PhpParser/Node/Stmt/Function_.php', - 144 => 'lib/PhpParser/Node/Stmt/Global_.php', - 145 => 'lib/PhpParser/Node/Stmt/Goto_.php', - 146 => 'lib/PhpParser/Node/Stmt/GroupUse.php', - 147 => 'lib/PhpParser/Node/Stmt/HaltCompiler.php', - 148 => 'lib/PhpParser/Node/Stmt/If_.php', - 149 => 'lib/PhpParser/Node/Stmt/InlineHTML.php', - 150 => 'lib/PhpParser/Node/Stmt/Interface_.php', - 151 => 'lib/PhpParser/Node/Stmt/Label.php', - 152 => 'lib/PhpParser/Node/Stmt/Namespace_.php', - 153 => 'lib/PhpParser/Node/Stmt/Nop.php', - 154 => 'lib/PhpParser/Node/Stmt/Property.php', - 155 => 'lib/PhpParser/Node/Stmt/PropertyProperty.php', - 156 => 'lib/PhpParser/Node/Stmt/Return_.php', - 157 => 'lib/PhpParser/Node/Stmt/StaticVar.php', - 158 => 'lib/PhpParser/Node/Stmt/Static_.php', - 159 => 'lib/PhpParser/Node/Stmt/Switch_.php', - 160 => 'lib/PhpParser/Node/Stmt/Throw_.php', - 161 => 'lib/PhpParser/Node/Stmt/TraitUse.php', - 162 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation.php', - 163 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.php', - 164 => 'lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.php', - 165 => 'lib/PhpParser/Node/Stmt/Trait_.php', - 166 => 'lib/PhpParser/Node/Stmt/TryCatch.php', - 167 => 'lib/PhpParser/Node/Stmt/Unset_.php', - 168 => 'lib/PhpParser/Node/Stmt/UseUse.php', - 169 => 'lib/PhpParser/Node/Stmt/Use_.php', - 170 => 'lib/PhpParser/Node/Stmt/While_.php', - 171 => 'lib/PhpParser/NodeDumper.php', - 172 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 173 => 'lib/PhpParser/Parser/Php5.php', - 174 => 'lib/PhpParser/Parser/Php7.php', - 175 => 'lib/PhpParser/ParserAbstract.php', - 176 => 'lib/PhpParser/PrettyPrinter/Standard.php', - 177 => 'lib/PhpParser/PrettyPrinterAbstract.php', - ), - 'lib/PhpParser/NodeDumper.php' => - array ( - ), - 'lib/PhpParser/NodeTraverser.php' => - array ( - ), - 'lib/PhpParser/NodeTraverserInterface.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - ), - 'lib/PhpParser/NodeVisitor.php' => - array ( - 0 => 'lib/PhpParser/NodeTraverser.php', - 1 => 'lib/PhpParser/NodeTraverserInterface.php', - 2 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - 3 => 'lib/PhpParser/NodeVisitorAbstract.php', - ), - 'lib/PhpParser/NodeVisitor/NameResolver.php' => - array ( - ), - 'lib/PhpParser/NodeVisitorAbstract.php' => - array ( - 0 => 'lib/PhpParser/NodeVisitor/NameResolver.php', - ), - 'lib/PhpParser/Parser.php' => - array ( - 0 => 'lib/PhpParser/Parser/Multiple.php', - 1 => 'lib/PhpParser/Parser/Php5.php', - 2 => 'lib/PhpParser/Parser/Php7.php', - 3 => 'lib/PhpParser/ParserAbstract.php', - 4 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Multiple.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php5.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Php7.php' => - array ( - 0 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/Parser/Tokens.php' => - array ( - 0 => 'lib/PhpParser/Lexer.php', - 1 => 'lib/PhpParser/Lexer/Emulative.php', - ), - 'lib/PhpParser/ParserAbstract.php' => - array ( - 0 => 'lib/PhpParser/Parser/Php5.php', - 1 => 'lib/PhpParser/Parser/Php7.php', - 2 => 'lib/PhpParser/ParserFactory.php', - ), - 'lib/PhpParser/ParserFactory.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinter/Standard.php' => - array ( - ), - 'lib/PhpParser/PrettyPrinterAbstract.php' => - array ( - 0 => 'lib/PhpParser/PrettyPrinter/Standard.php', - ), - 'lib/PhpParser/Serializer.php' => - array ( - 0 => 'lib/PhpParser/Serializer/XML.php', - ), - 'lib/PhpParser/Serializer/XML.php' => - array ( - ), - 'lib/PhpParser/Unserializer.php' => - array ( - 0 => 'lib/PhpParser/Unserializer/XML.php', - ), - 'lib/PhpParser/Unserializer/XML.php' => - array ( - ), -); diff --git a/tests/generate-reflection-test.php b/tests/generate-reflection-test.php new file mode 100644 index 0000000000..eba37b5454 --- /dev/null +++ b/tests/generate-reflection-test.php @@ -0,0 +1,10 @@ +